fix multiple device support

This commit is contained in:
Bruno Windels 2022-03-30 15:18:46 +02:00
parent c54ffd4fc3
commit d7360e7741
7 changed files with 103 additions and 63 deletions

View file

@ -59,7 +59,11 @@ export class DeviceMessageHandler {
})); }));
// TODO: pass this in the prep and run it in afterSync or afterSyncComplete (as callHandler can send events as well)? // TODO: pass this in the prep and run it in afterSync or afterSyncComplete (as callHandler can send events as well)?
for (const dr of callMessages) { for (const dr of callMessages) {
this._callHandler.handleDeviceMessage(dr.event, dr.device.userId, dr.device.deviceId, log); if (dr.device) {
this._callHandler.handleDeviceMessage(dr.event, dr.device.userId, dr.device.deviceId, log);
} else {
console.error("could not deliver message because don't have device for sender key", dr.event);
}
} }
// TODO: somehow include rooms that received a call to_device message in the sync state? // TODO: somehow include rooms that received a call to_device message in the sync state?
// or have updates flow through event emitter? // or have updates flow through event emitter?

View file

@ -131,7 +131,7 @@ export class CallHandler {
const callId = call["m.call_id"]; const callId = call["m.call_id"];
const groupCall = this._calls.get(callId); const groupCall = this._calls.get(callId);
// TODO: also check the member when receiving the m.call event // TODO: also check the member when receiving the m.call event
groupCall?.updateMember(userId, call, log); groupCall?.updateMembership(userId, call, log);
}; };
const newCallIdsMemberOf = new Set<string>(calls.map(call => call["m.call_id"])); const newCallIdsMemberOf = new Set<string>(calls.map(call => call["m.call_id"]));
let previousCallIdsMemberOf = this.memberToCallIds.get(userId); let previousCallIdsMemberOf = this.memberToCallIds.get(userId);
@ -140,7 +140,7 @@ export class CallHandler {
for (const previousCallId of previousCallIdsMemberOf) { for (const previousCallId of previousCallIdsMemberOf) {
if (!newCallIdsMemberOf.has(previousCallId)) { if (!newCallIdsMemberOf.has(previousCallId)) {
const groupCall = this._calls.get(previousCallId); const groupCall = this._calls.get(previousCallId);
groupCall?.removeMember(userId, log); groupCall?.removeMembership(userId, log);
} }
} }
} }

View file

@ -412,7 +412,8 @@ export class PeerCall implements IDisposable {
log.log(`Invite has expired. Hanging up.`); log.log(`Invite has expired. Hanging up.`);
this.hangupParty = CallParty.Remote; // effectively this.hangupParty = CallParty.Remote; // effectively
this.setState(CallState.Ended, log); this.setState(CallState.Ended, log);
this.stopAllMedia(); //this.localMedia?.dispose();
//this.localMedia = undefined;
if (this.peerConnection.signalingState != 'closed') { if (this.peerConnection.signalingState != 'closed') {
this.peerConnection.close(); this.peerConnection.close();
} }
@ -772,21 +773,14 @@ export class PeerCall implements IDisposable {
this.hangupParty = hangupParty; this.hangupParty = hangupParty;
// this.hangupReason = hangupReason; // this.hangupReason = hangupReason;
this.setState(CallState.Ended, log); this.setState(CallState.Ended, log);
this.stopAllMedia(); //this.localMedia?.dispose();
//this.localMedia = undefined;
if (this.peerConnection && this.peerConnection.signalingState !== 'closed') { if (this.peerConnection && this.peerConnection.signalingState !== 'closed') {
this.peerConnection.close(); this.peerConnection.close();
} }
} }
private stopAllMedia(): void {
if (this.localMedia) {
for (const track of this.localMedia.tracks) {
track.stop();
}
}
}
private async delay(timeoutMs: number): Promise<void> { private async delay(timeoutMs: number): Promise<void> {
// Allow a short time for initial candidates to be gathered // Allow a short time for initial candidates to be gathered
const timeout = this.disposables.track(this.options.createTimeout(timeoutMs)); const timeout = this.disposables.track(this.options.createTimeout(timeoutMs));

View file

@ -95,10 +95,18 @@ export class GroupCall extends EventEmitter<{change: never}> {
return this.callContent?.["m.terminated"] === true; return this.callContent?.["m.terminated"] === true;
} }
get isRinging(): boolean {
return this._state === GroupCallState.Created && this.intent === "m.ring" && !this.isMember(this.options.ownUserId);
}
get name(): string { get name(): string {
return this.callContent?.["m.name"]; return this.callContent?.["m.name"];
} }
get intent(): string {
return this.callContent?.["m.intent"];
}
join(localMedia: LocalMedia): Promise<void> { join(localMedia: LocalMedia): Promise<void> {
return this.logItem.wrap("join", async log => { return this.logItem.wrap("join", async log => {
if (this._state !== GroupCallState.Created) { if (this._state !== GroupCallState.Created) {
@ -134,6 +142,8 @@ export class GroupCall extends EventEmitter<{change: never}> {
if (this._members.size === 0) { if (this._members.size === 0) {
await this.terminate(); await this.terminate();
} }
} else {
log.set("already_left", true);
} }
}); });
} }
@ -184,7 +194,7 @@ export class GroupCall extends EventEmitter<{change: never}> {
} }
/** @internal */ /** @internal */
updateMember(userId: string, callMembership: CallMembership, syncLog: ILogItem) { updateMembership(userId: string, callMembership: CallMembership, syncLog: ILogItem) {
this.logItem.wrap({l: "updateMember", id: userId}, log => { this.logItem.wrap({l: "updateMember", id: userId}, log => {
syncLog.refDetached(log); syncLog.refDetached(log);
const devices = callMembership["m.devices"]; const devices = callMembership["m.devices"];
@ -192,45 +202,62 @@ export class GroupCall extends EventEmitter<{change: never}> {
for (const device of devices) { for (const device of devices) {
const deviceId = device.device_id; const deviceId = device.device_id;
const memberKey = getMemberKey(userId, deviceId); const memberKey = getMemberKey(userId, deviceId);
if (userId === this.options.ownUserId && deviceId === this.options.ownDeviceId) { log.wrap({l: "update device member", id: memberKey}, log => {
if (this._state === GroupCallState.Joining) { if (userId === this.options.ownUserId && deviceId === this.options.ownDeviceId) {
this._state = GroupCallState.Joined; if (this._state === GroupCallState.Joining) {
this.emitChange(); log.set("update_own", true);
this._state = GroupCallState.Joined;
this.emitChange();
}
} else {
let member = this._members.get(memberKey);
if (member) {
log.set("update", true);
member!.updateCallInfo(device);
} else {
const logItem = this.logItem.child({l: "member", id: memberKey});
log.set("add", true);
log.refDetached(logItem);
member = new Member(
RoomMember.fromUserId(this.roomId, userId, "join"),
device, this._memberOptions, logItem
);
this._members.add(memberKey, member);
if (this._state === GroupCallState.Joining || this._state === GroupCallState.Joined) {
member.connect(this._localMedia!.clone());
}
}
} }
return; });
}
let member = this._members.get(memberKey);
if (member) {
member.updateCallInfo(device);
} else {
const logItem = this.logItem.child("member");
member = new Member(
RoomMember.fromUserId(this.roomId, userId, "join"),
device, this._memberOptions, logItem
);
this._members.add(memberKey, member);
if (this._state === GroupCallState.Joining || this._state === GroupCallState.Joined) {
member.connect(this._localMedia!.clone());
}
}
} }
const newDeviceIds = new Set<string>(devices.map(call => call.device_id)); const newDeviceIds = new Set<string>(devices.map(call => call.device_id));
// remove user as member of any calls not present anymore // remove user as member of any calls not present anymore
for (const previousDeviceId of previousDeviceIds) { for (const previousDeviceId of previousDeviceIds) {
if (!newDeviceIds.has(previousDeviceId)) { if (!newDeviceIds.has(previousDeviceId)) {
this.removeMemberDevice(userId, previousDeviceId, syncLog); log.wrap({l: "remove device member", id: getMemberKey(userId, previousDeviceId)}, log => {
this.removeMemberDevice(userId, previousDeviceId, log);
});
} }
} }
if (userId === this.options.ownUserId && !newDeviceIds.has(this.options.ownDeviceId)) {
this.removeOwnDevice(log);
}
}); });
} }
/** @internal */ /** @internal */
removeMember(userId: string, syncLog: ILogItem) { removeMembership(userId: string, syncLog: ILogItem) {
const deviceIds = this.getDeviceIdsForUserId(userId); const deviceIds = this.getDeviceIdsForUserId(userId);
for (const deviceId of deviceIds) { this.logItem.wrap("removeMember", log => {
this.removeMemberDevice(userId, deviceId, syncLog); syncLog.refDetached(log);
} for (const deviceId of deviceIds) {
this.removeMemberDevice(userId, deviceId, log);
}
if (userId === this.options.ownUserId) {
this.removeOwnDevice(log);
}
});
} }
private getDeviceIdsForUserId(userId: string): string[] { private getDeviceIdsForUserId(userId: string): string[] {
@ -239,26 +266,32 @@ export class GroupCall extends EventEmitter<{change: never}> {
.map(key => getDeviceFromMemberKey(key)); .map(key => getDeviceFromMemberKey(key));
} }
private isMember(userId: string): boolean {
return Array.from(this._members.keys()).some(key => memberKeyIsForUser(key, userId));
}
private removeOwnDevice(log: ILogItem) {
if (this._state === GroupCallState.Joined) {
log.set("leave_own", true);
this._localMedia?.dispose();
this._localMedia = undefined;
for (const [,member] of this._members) {
member.disconnect();
}
this._state = GroupCallState.Created;
this.emitChange();
}
}
/** @internal */ /** @internal */
private removeMemberDevice(userId: string, deviceId: string, syncLog: ILogItem) { private removeMemberDevice(userId: string, deviceId: string, log: ILogItem) {
const memberKey = getMemberKey(userId, deviceId); const memberKey = getMemberKey(userId, deviceId);
this.logItem.wrap({l: "removeMemberDevice", id: memberKey}, log => { log.wrap({l: "removeMemberDevice", id: memberKey}, log => {
syncLog.refDetached(log); const member = this._members.get(memberKey);
if (userId === this.options.ownUserId && deviceId === this.options.ownDeviceId) { if (member) {
if (this._state === GroupCallState.Joined) { log.set("leave", true);
this._localMedia?.dispose(); this._members.remove(memberKey);
this._localMedia = undefined; member.disconnect();
for (const [,member] of this._members) {
member.disconnect();
}
this._state = GroupCallState.Created;
}
} else {
const member = this._members.get(memberKey);
if (member) {
this._members.remove(memberKey);
member.disconnect();
}
} }
this.emitChange(); this.emitChange();
}); });
@ -315,9 +348,15 @@ export class GroupCall extends EventEmitter<{change: never}> {
const stateEvent = await txn.roomState.get(this.roomId, CALL_MEMBER_TYPE, this.options.ownUserId); const stateEvent = await txn.roomState.get(this.roomId, CALL_MEMBER_TYPE, this.options.ownUserId);
if (stateEvent) { if (stateEvent) {
const content = stateEvent.event.content; const content = stateEvent.event.content;
const callsInfo = content["m.calls"]; const callInfo = content["m.calls"]?.find(c => c["m.call_id"] === this.id);
content["m.calls"] = callsInfo?.filter(c => c["m.call_id"] !== this.id); if (callInfo) {
return content; const devicesInfo = callInfo["m.devices"];
const deviceIndex = devicesInfo.findIndex(d => d["device_id"] === this.options.ownDeviceId);
if (deviceIndex !== -1) {
devicesInfo.splice(deviceIndex, 1);
return content;
}
}
} }
} }

View file

@ -47,9 +47,7 @@ export class Member {
private callDeviceMembership: CallDeviceMembership, private callDeviceMembership: CallDeviceMembership,
private readonly options: Options, private readonly options: Options,
private readonly logItem: ILogItem, private readonly logItem: ILogItem,
) { ) {}
logItem.set("id", member.userId);
}
get remoteTracks(): Track[] { get remoteTracks(): Track[] {
return this.peerCall?.remoteTracks ?? []; return this.peerCall?.remoteTracks ?? [];

View file

@ -37,6 +37,7 @@ export interface Track {
get muted(): boolean; get muted(): boolean;
setMuted(muted: boolean): void; setMuted(muted: boolean): void;
stop(): void; stop(): void;
clone(): Track;
} }
export interface AudioTrack extends Track { export interface AudioTrack extends Track {

View file

@ -111,6 +111,10 @@ export class TrackWrapper implements Track {
stop() { stop() {
this.track.stop(); this.track.stop();
} }
clone() {
return this.track.clone();
}
} }
export class AudioTrackWrapper extends TrackWrapper { export class AudioTrackWrapper extends TrackWrapper {