diff options
author | RaindropsSys <contact@minteck.org> | 2023-04-24 14:03:36 +0200 |
---|---|---|
committer | RaindropsSys <contact@minteck.org> | 2023-04-24 14:03:36 +0200 |
commit | 633c92eae865e957121e08de634aeee11a8b3992 (patch) | |
tree | 09d881bee1dae0b6eee49db1dfaf0f500240606c /includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/callEventHandler.ts | |
parent | c4657e4509733699c0f26a3c900bab47e915d5a0 (diff) | |
download | pluralconnect-633c92eae865e957121e08de634aeee11a8b3992.tar.gz pluralconnect-633c92eae865e957121e08de634aeee11a8b3992.tar.bz2 pluralconnect-633c92eae865e957121e08de634aeee11a8b3992.zip |
Updated 18 files, added 1692 files and deleted includes/system/compare.inc (automated)
Diffstat (limited to 'includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/callEventHandler.ts')
-rw-r--r-- | includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/callEventHandler.ts | 425 |
1 files changed, 425 insertions, 0 deletions
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/callEventHandler.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/callEventHandler.ts new file mode 100644 index 0000000..4ee183a --- /dev/null +++ b/includes/external/matrix/node_modules/matrix-js-sdk/src/webrtc/callEventHandler.ts @@ -0,0 +1,425 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { MatrixEvent } from "../models/event"; +import { logger } from "../logger"; +import { CallDirection, CallError, CallErrorCode, CallState, createNewMatrixCall, MatrixCall } from "./call"; +import { EventType } from "../@types/event"; +import { ClientEvent, MatrixClient } from "../client"; +import { MCallAnswer, MCallHangupReject } from "./callEventTypes"; +import { GroupCall, GroupCallErrorCode, GroupCallEvent, GroupCallUnknownDeviceError } from "./groupCall"; +import { RoomEvent } from "../models/room"; + +// Don't ring unless we'd be ringing for at least 3 seconds: the user needs some +// time to press the 'accept' button +const RING_GRACE_PERIOD = 3000; + +export enum CallEventHandlerEvent { + Incoming = "Call.incoming", +} + +export type CallEventHandlerEventHandlerMap = { + /** + * Fires whenever an incoming call arrives. + * @param call - The incoming call. + * @example + * ``` + * matrixClient.on("Call.incoming", function(call){ + * call.answer(); // auto-answer + * }); + * ``` + */ + [CallEventHandlerEvent.Incoming]: (call: MatrixCall) => void; +}; + +export class CallEventHandler { + // XXX: Most of these are only public because of the tests + public calls: Map<string, MatrixCall>; + public callEventBuffer: MatrixEvent[]; + public nextSeqByCall: Map<string, number> = new Map(); + public toDeviceEventBuffers: Map<string, Array<MatrixEvent>> = new Map(); + + private client: MatrixClient; + private candidateEventsByCall: Map<string, Array<MatrixEvent>>; + private eventBufferPromiseChain?: Promise<void>; + + public constructor(client: MatrixClient) { + this.client = client; + this.calls = new Map<string, MatrixCall>(); + // The sync code always emits one event at a time, so it will patiently + // wait for us to finish processing a call invite before delivering the + // next event, even if that next event is a hangup. We therefore accumulate + // all our call events and then process them on the 'sync' event, ie. + // each time a sync has completed. This way, we can avoid emitting incoming + // call events if we get both the invite and answer/hangup in the same sync. + // This happens quite often, eg. replaying sync from storage, catchup sync + // after loading and after we've been offline for a bit. + this.callEventBuffer = []; + this.candidateEventsByCall = new Map<string, Array<MatrixEvent>>(); + } + + public start(): void { + this.client.on(ClientEvent.Sync, this.onSync); + this.client.on(RoomEvent.Timeline, this.onRoomTimeline); + this.client.on(ClientEvent.ToDeviceEvent, this.onToDeviceEvent); + } + + public stop(): void { + this.client.removeListener(ClientEvent.Sync, this.onSync); + this.client.removeListener(RoomEvent.Timeline, this.onRoomTimeline); + this.client.removeListener(ClientEvent.ToDeviceEvent, this.onToDeviceEvent); + } + + private onSync = (): void => { + // Process the current event buffer and start queuing into a new one. + const currentEventBuffer = this.callEventBuffer; + this.callEventBuffer = []; + + // Ensure correct ordering by only processing this queue after the previous one has finished processing + if (this.eventBufferPromiseChain) { + this.eventBufferPromiseChain = this.eventBufferPromiseChain.then(() => + this.evaluateEventBuffer(currentEventBuffer), + ); + } else { + this.eventBufferPromiseChain = this.evaluateEventBuffer(currentEventBuffer); + } + }; + + private async evaluateEventBuffer(eventBuffer: MatrixEvent[]): Promise<void> { + await Promise.all(eventBuffer.map((event) => this.client.decryptEventIfNeeded(event))); + + const callEvents = eventBuffer.filter((event) => { + const eventType = event.getType(); + return eventType.startsWith("m.call.") || eventType.startsWith("org.matrix.call."); + }); + + const ignoreCallIds = new Set<string>(); + + // inspect the buffer and mark all calls which have been answered + // or hung up before passing them to the call event handler. + for (const event of callEvents) { + const eventType = event.getType(); + + if (eventType === EventType.CallAnswer || eventType === EventType.CallHangup) { + ignoreCallIds.add(event.getContent().call_id); + } + } + + // Process call events in the order that they were received + for (const event of callEvents) { + const eventType = event.getType(); + const callId = event.getContent().call_id; + + if (eventType === EventType.CallInvite && ignoreCallIds.has(callId)) { + // This call has previously been answered or hung up: ignore it + continue; + } + + try { + await this.handleCallEvent(event); + } catch (e) { + logger.error("CallEventHandler evaluateEventBuffer() caught exception handling call event", e); + } + } + } + + private onRoomTimeline = (event: MatrixEvent): void => { + this.callEventBuffer.push(event); + }; + + private onToDeviceEvent = (event: MatrixEvent): void => { + const content = event.getContent(); + + if (!content.call_id) { + this.callEventBuffer.push(event); + return; + } + + if (!this.nextSeqByCall.has(content.call_id)) { + this.nextSeqByCall.set(content.call_id, 0); + } + + if (content.seq === undefined) { + this.callEventBuffer.push(event); + return; + } + + const nextSeq = this.nextSeqByCall.get(content.call_id) || 0; + + if (content.seq !== nextSeq) { + if (!this.toDeviceEventBuffers.has(content.call_id)) { + this.toDeviceEventBuffers.set(content.call_id, []); + } + + const buffer = this.toDeviceEventBuffers.get(content.call_id)!; + const index = buffer.findIndex((e) => e.getContent().seq > content.seq); + + if (index === -1) { + buffer.push(event); + } else { + buffer.splice(index, 0, event); + } + } else { + const callId = content.call_id; + this.callEventBuffer.push(event); + this.nextSeqByCall.set(callId, content.seq + 1); + + const buffer = this.toDeviceEventBuffers.get(callId); + + let nextEvent = buffer && buffer.shift(); + + while (nextEvent && nextEvent.getContent().seq === this.nextSeqByCall.get(callId)) { + this.callEventBuffer.push(nextEvent); + this.nextSeqByCall.set(callId, nextEvent.getContent().seq + 1); + nextEvent = buffer!.shift(); + } + } + }; + + private async handleCallEvent(event: MatrixEvent): Promise<void> { + this.client.emit(ClientEvent.ReceivedVoipEvent, event); + + const content = event.getContent(); + const callRoomId = + event.getRoomId() || this.client.groupCallEventHandler!.getGroupCallById(content.conf_id)?.room?.roomId; + const groupCallId = content.conf_id; + const type = event.getType() as EventType; + const senderId = event.getSender()!; + let call = content.call_id ? this.calls.get(content.call_id) : undefined; + + let opponentDeviceId: string | undefined; + + let groupCall: GroupCall | undefined; + if (groupCallId) { + groupCall = this.client.groupCallEventHandler!.getGroupCallById(groupCallId); + + if (!groupCall) { + logger.warn( + `CallEventHandler handleCallEvent() could not find a group call - ignoring event (groupCallId=${groupCallId}, type=${type})`, + ); + return; + } + + opponentDeviceId = content.device_id; + + if (!opponentDeviceId) { + logger.warn( + `CallEventHandler handleCallEvent() could not find a device id - ignoring event (senderId=${senderId})`, + ); + groupCall.emit(GroupCallEvent.Error, new GroupCallUnknownDeviceError(senderId)); + return; + } + + if (content.dest_session_id !== this.client.getSessionId()) { + logger.warn( + "CallEventHandler handleCallEvent() call event does not match current session id - ignoring", + ); + return; + } + } + + const weSentTheEvent = + senderId === this.client.credentials.userId && + (opponentDeviceId === undefined || opponentDeviceId === this.client.getDeviceId()!); + + if (!callRoomId) return; + + if (type === EventType.CallInvite) { + // ignore invites you send + if (weSentTheEvent) return; + // expired call + if (event.getLocalAge() > content.lifetime - RING_GRACE_PERIOD) return; + // stale/old invite event + if (call && call.state === CallState.Ended) return; + + if (call) { + logger.warn( + `CallEventHandler handleCallEvent() already has a call but got an invite - clobbering (callId=${content.call_id})`, + ); + } + + if (content.invitee && content.invitee !== this.client.getUserId()) { + return; // This invite was meant for another user in the room + } + + const timeUntilTurnCresExpire = (this.client.getTurnServersExpiry() ?? 0) - Date.now(); + logger.info( + "CallEventHandler handleCallEvent() current turn creds expire in " + timeUntilTurnCresExpire + " ms", + ); + call = + createNewMatrixCall(this.client, callRoomId, { + forceTURN: this.client.forceTURN, + opponentDeviceId, + groupCallId, + opponentSessionId: content.sender_session_id, + }) ?? undefined; + if (!call) { + logger.log( + `CallEventHandler handleCallEvent() this client does not support WebRTC (callId=${content.call_id})`, + ); + // don't hang up the call: there could be other clients + // connected that do support WebRTC and declining the + // the call on their behalf would be really annoying. + return; + } + + call.callId = content.call_id; + const stats = groupCall?.getGroupCallStats(); + if (stats) { + call.initStats(stats); + } + + try { + await call.initWithInvite(event); + } catch (e) { + if (e instanceof CallError) { + if (e.code === GroupCallErrorCode.UnknownDevice) { + groupCall?.emit(GroupCallEvent.Error, e); + } else { + logger.error(e); + } + } + } + this.calls.set(call.callId, call); + + // if we stashed candidate events for that call ID, play them back now + if (this.candidateEventsByCall.get(call.callId)) { + for (const ev of this.candidateEventsByCall.get(call.callId)!) { + call.onRemoteIceCandidatesReceived(ev); + } + } + + // Were we trying to call that user (room)? + let existingCall: MatrixCall | undefined; + for (const thisCall of this.calls.values()) { + const isCalling = [CallState.WaitLocalMedia, CallState.CreateOffer, CallState.InviteSent].includes( + thisCall.state, + ); + + if ( + call.roomId === thisCall.roomId && + thisCall.direction === CallDirection.Outbound && + call.getOpponentMember()?.userId === thisCall.invitee && + isCalling + ) { + existingCall = thisCall; + break; + } + } + + if (existingCall) { + if (existingCall.callId > call.callId) { + logger.log( + `CallEventHandler handleCallEvent() detected glare - answering incoming call and canceling outgoing call (incomingId=${call.callId}, outgoingId=${existingCall.callId})`, + ); + existingCall.replacedBy(call); + } else { + logger.log( + `CallEventHandler handleCallEvent() detected glare - hanging up incoming call (incomingId=${call.callId}, outgoingId=${existingCall.callId})`, + ); + call.hangup(CallErrorCode.Replaced, true); + } + } else { + this.client.emit(CallEventHandlerEvent.Incoming, call); + } + return; + } else if (type === EventType.CallCandidates) { + if (weSentTheEvent) return; + + if (!call) { + // store the candidates; we may get a call eventually. + if (!this.candidateEventsByCall.has(content.call_id)) { + this.candidateEventsByCall.set(content.call_id, []); + } + this.candidateEventsByCall.get(content.call_id)!.push(event); + } else { + call.onRemoteIceCandidatesReceived(event); + } + return; + } else if ([EventType.CallHangup, EventType.CallReject].includes(type)) { + // Note that we also observe our own hangups here so we can see + // if we've already rejected a call that would otherwise be valid + if (!call) { + // if not live, store the fact that the call has ended because + // we're probably getting events backwards so + // the hangup will come before the invite + call = + createNewMatrixCall(this.client, callRoomId, { + opponentDeviceId, + opponentSessionId: content.sender_session_id, + }) ?? undefined; + if (call) { + call.callId = content.call_id; + call.initWithHangup(event); + this.calls.set(content.call_id, call); + } + } else { + if (call.state !== CallState.Ended) { + if (type === EventType.CallHangup) { + call.onHangupReceived(content as MCallHangupReject); + } else { + call.onRejectReceived(content as MCallHangupReject); + } + + // @ts-expect-error typescript thinks the state can't be 'ended' because we're + // inside the if block where it wasn't, but it could have changed because + // on[Hangup|Reject]Received are side-effecty. + if (call.state === CallState.Ended) this.calls.delete(content.call_id); + } + } + return; + } + + // The following events need a call and a peer connection + if (!call || !call.hasPeerConnection) { + logger.info( + `CallEventHandler handleCallEvent() discarding possible call event as we don't have a call (type=${type})`, + ); + return; + } + // Ignore remote echo + if (event.getContent().party_id === call.ourPartyId) return; + + switch (type) { + case EventType.CallAnswer: + if (weSentTheEvent) { + if (call.state === CallState.Ringing) { + call.onAnsweredElsewhere(content as MCallAnswer); + } + } else { + call.onAnswerReceived(event); + } + break; + case EventType.CallSelectAnswer: + call.onSelectAnswerReceived(event); + break; + + case EventType.CallNegotiate: + call.onNegotiateReceived(event); + break; + + case EventType.CallAssertedIdentity: + case EventType.CallAssertedIdentityPrefix: + call.onAssertedIdentityReceived(event); + break; + + case EventType.CallSDPStreamMetadataChanged: + case EventType.CallSDPStreamMetadataChangedPrefix: + call.onSDPStreamMetadataChangedReceived(event); + break; + } + } +} |