diff --git a/src/webrtc/groupCall.ts b/src/webrtc/groupCall.ts index 5f192a08ec5..0b12f97f9e5 100644 --- a/src/webrtc/groupCall.ts +++ b/src/webrtc/groupCall.ts @@ -12,7 +12,6 @@ import { ISendEventResponse } from "../@types/requests"; import { MatrixEvent } from "../models/event"; import { EventType } from "../@types/event"; import { CallEventHandlerEvent } from "./callEventHandler"; -import { RoomStateEvent } from "../matrix"; import { GroupCallEventHandlerEvent } from "./groupCallEventHandler"; export enum GroupCallIntent { @@ -115,6 +114,7 @@ export interface IGroupCallRoomMemberCallState { export interface IGroupCallRoomMemberState { "m.calls": IGroupCallRoomMemberCallState[]; + "m.expires_ts": number; } export enum GroupCallState { @@ -133,6 +133,16 @@ interface ICallHandlers { onCallReplaced: (newCall: MatrixCall) => void; } +const CALL_MEMBER_STATE_TIMEOUT = 1000 * 60 * 60; // 1 hour + +const callMemberStateIsExpired = (event: MatrixEvent): boolean => { + const now = Date.now(); + const content = event?.getContent() ?? {}; + const expiresAt = typeof content["m.expires_ts"] === "number" ? content["m.expires_ts"] : -Infinity; + // The event is expired if the expiration date has passed, or if it's unreasonably far in the future + return expiresAt <= now || expiresAt > now + CALL_MEMBER_STATE_TIMEOUT * 5 / 4; +}; + function getCallUserId(call: MatrixCall): string | null { return call.getOpponentMember()?.userId || call.invitee || null; } @@ -188,6 +198,8 @@ export class GroupCall extends TypedEventEmitter = new Map(); private reEmitter: ReEmitter; private transmitTimer: ReturnType | null = null; + private memberStateExpirationTimers: Map> = new Map(); + private resendMemberStateTimer: ReturnType | null = null; constructor( private client: MatrixClient, @@ -203,10 +215,7 @@ export class GroupCall extends TypedEventEmitter { - const deviceId = this.client.getDeviceId(); + private getMemberStateEvents(): MatrixEvent[]; + private getMemberStateEvents(userId: string): MatrixEvent | null; + private getMemberStateEvents(userId?: string): MatrixEvent[] | MatrixEvent | null { + if (userId != null) { + const event = this.room.currentState.getStateEvents(EventType.GroupCallMemberPrefix, userId); + return callMemberStateIsExpired(event) ? null : event; + } else { + return this.room.currentState.getStateEvents(EventType.GroupCallMemberPrefix) + .filter(event => !callMemberStateIsExpired(event)); + } + } - return this.updateMemberCallState({ + private async sendMemberStateEvent(): Promise { + const send = () => this.updateMemberCallState({ "m.call_id": this.groupCallId, "m.devices": [ { - "device_id": deviceId, + "device_id": this.client.getDeviceId(), "session_id": this.client.getSessionId(), "feeds": this.getLocalFeeds().map((feed) => ({ purpose: feed.purpose, @@ -679,23 +690,34 @@ export class GroupCall extends TypedEventEmitter { + logger.log("Resending call member state"); + await send(); + }, CALL_MEMBER_STATE_TIMEOUT * 3 / 4); + + return res; } - private removeMemberStateEvent(): Promise { - return this.updateMemberCallState(undefined); + private async removeMemberStateEvent(): Promise { + clearInterval(this.resendMemberStateTimer); + this.resendMemberStateTimer = null; + return await this.updateMemberCallState(undefined); } private async updateMemberCallState(memberCallState?: IGroupCallRoomMemberCallState): Promise { const localUserId = this.client.getUserId(); - const currentStateEvent = this.room.currentState.getStateEvents(EventType.GroupCallMemberPrefix, localUserId); - const memberStateEvent = currentStateEvent?.getContent(); + const memberState = this.getMemberStateEvents(localUserId)?.getContent(); let calls: IGroupCallRoomMemberCallState[] = []; // Sanitize existing member state event - if (memberStateEvent && Array.isArray(memberStateEvent["m.calls"])) { - calls = memberStateEvent["m.calls"].filter((call) => !!call); + if (memberState && Array.isArray(memberState["m.calls"])) { + calls = memberState["m.calls"].filter((call) => !!call); } const existingCallIndex = calls.findIndex((call) => call && call["m.call_id"] === this.groupCallId); @@ -712,6 +734,7 @@ export class GroupCall extends TypedEventEmitter { // The member events may be received for another room, which we will ignore. - if (event.getRoomId() !== this.room.roomId) { - return; - } + if (event.getRoomId() !== this.room.roomId) return; const member = this.room.getMember(event.getStateKey()); + if (!member) return; - if (!member) { - return; - } - - let callsState = event.getContent()["m.calls"]; + const ignore = () => { + this.removeParticipant(member); + clearTimeout(this.memberStateExpirationTimers.get(member.userId)); + this.memberStateExpirationTimers.delete(member.userId); + }; - if (Array.isArray(callsState)) { - callsState = callsState.filter((call) => !!call); - } + const content = event.getContent(); + const callsState = !callMemberStateIsExpired(event) && Array.isArray(content["m.calls"]) + ? content["m.calls"].filter((call) => call) + : []; // Ignore expired device data - if (!Array.isArray(callsState) || callsState.length === 0) { - logger.warn(`Ignoring member state from ${member.userId} member not in any calls.`); - this.removeParticipant(member); + if (callsState.length === 0) { + logger.log(`Ignoring member state from ${member.userId} member not in any calls.`); + ignore(); return; } // Currently we only support a single call per room. So grab the first call. const callState = callsState[0]; - const callId = callState["m.call_id"]; if (!callId) { logger.warn(`Room member ${member.userId} does not have a valid m.call_id set. Ignoring.`); - this.removeParticipant(member); + ignore(); return; } if (callId !== this.groupCallId) { logger.warn(`Call id ${callId} does not match group call id ${this.groupCallId}, ignoring.`); - this.removeParticipant(member); + ignore(); return; } this.addParticipant(member); + clearTimeout(this.memberStateExpirationTimers.get(member.userId)); + this.memberStateExpirationTimers.set(member.userId, setTimeout(() => { + logger.warn(`Call member state for ${member.userId} has expired`); + this.removeParticipant(member); + }, content["m.expires_ts"] - Date.now())); + // Don't process your own member. const localUserId = this.client.getUserId(); @@ -847,7 +875,7 @@ export class GroupCall extends TypedEventEmitter { - const roomState = this.room.currentState; - const memberStateEvents = roomState.getStateEvents(EventType.GroupCallMemberPrefix); - - for (const event of memberStateEvents) { + for (const event of this.getMemberStateEvents()) { const memberId = event.getStateKey(); const existingCall = this.calls.find((call) => getCallUserId(call) === memberId); const retryCallCount = this.retryCallCounts.get(memberId) || 0;