forked from mystiq/hydrogen-web
fix multiple device support
This commit is contained in:
parent
c54ffd4fc3
commit
d7360e7741
7 changed files with 103 additions and 63 deletions
|
@ -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?
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 ?? [];
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue