diff options
Diffstat (limited to 'includes/external/matrix/node_modules/matrix-js-sdk/lib/webrtc/callEventHandler.js')
-rw-r--r-- | includes/external/matrix/node_modules/matrix-js-sdk/lib/webrtc/callEventHandler.js | 347 |
1 files changed, 347 insertions, 0 deletions
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/lib/webrtc/callEventHandler.js b/includes/external/matrix/node_modules/matrix-js-sdk/lib/webrtc/callEventHandler.js new file mode 100644 index 0000000..5e3c242 --- /dev/null +++ b/includes/external/matrix/node_modules/matrix-js-sdk/lib/webrtc/callEventHandler.js @@ -0,0 +1,347 @@ +"use strict"; + +var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.CallEventHandlerEvent = exports.CallEventHandler = void 0; +var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); +var _logger = require("../logger"); +var _call = require("./call"); +var _event = require("../@types/event"); +var _client = require("../client"); +var _groupCall3 = require("./groupCall"); +var _room = require("../models/room"); +/* +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. +*/ + +// 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; +let CallEventHandlerEvent; +exports.CallEventHandlerEvent = CallEventHandlerEvent; +(function (CallEventHandlerEvent) { + CallEventHandlerEvent["Incoming"] = "Call.incoming"; +})(CallEventHandlerEvent || (exports.CallEventHandlerEvent = CallEventHandlerEvent = {})); +class CallEventHandler { + // XXX: Most of these are only public because of the tests + + constructor(client) { + (0, _defineProperty2.default)(this, "calls", void 0); + (0, _defineProperty2.default)(this, "callEventBuffer", void 0); + (0, _defineProperty2.default)(this, "nextSeqByCall", new Map()); + (0, _defineProperty2.default)(this, "toDeviceEventBuffers", new Map()); + (0, _defineProperty2.default)(this, "client", void 0); + (0, _defineProperty2.default)(this, "candidateEventsByCall", void 0); + (0, _defineProperty2.default)(this, "eventBufferPromiseChain", void 0); + (0, _defineProperty2.default)(this, "onSync", () => { + // 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); + } + }); + (0, _defineProperty2.default)(this, "onRoomTimeline", event => { + this.callEventBuffer.push(event); + }); + (0, _defineProperty2.default)(this, "onToDeviceEvent", event => { + 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(); + } + } + }); + this.client = client; + this.calls = new Map(); + // 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(); + } + start() { + this.client.on(_client.ClientEvent.Sync, this.onSync); + this.client.on(_room.RoomEvent.Timeline, this.onRoomTimeline); + this.client.on(_client.ClientEvent.ToDeviceEvent, this.onToDeviceEvent); + } + stop() { + this.client.removeListener(_client.ClientEvent.Sync, this.onSync); + this.client.removeListener(_room.RoomEvent.Timeline, this.onRoomTimeline); + this.client.removeListener(_client.ClientEvent.ToDeviceEvent, this.onToDeviceEvent); + } + async evaluateEventBuffer(eventBuffer) { + 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(); + + // 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 === _event.EventType.CallAnswer || eventType === _event.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 === _event.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.logger.error("CallEventHandler evaluateEventBuffer() caught exception handling call event", e); + } + } + } + async handleCallEvent(event) { + var _getGroupCallById, _getGroupCallById$roo; + this.client.emit(_client.ClientEvent.ReceivedVoipEvent, event); + const content = event.getContent(); + const callRoomId = event.getRoomId() || ((_getGroupCallById = this.client.groupCallEventHandler.getGroupCallById(content.conf_id)) === null || _getGroupCallById === void 0 ? void 0 : (_getGroupCallById$roo = _getGroupCallById.room) === null || _getGroupCallById$roo === void 0 ? void 0 : _getGroupCallById$roo.roomId); + const groupCallId = content.conf_id; + const type = event.getType(); + const senderId = event.getSender(); + let call = content.call_id ? this.calls.get(content.call_id) : undefined; + let opponentDeviceId; + let groupCall; + if (groupCallId) { + groupCall = this.client.groupCallEventHandler.getGroupCallById(groupCallId); + if (!groupCall) { + _logger.logger.warn(`CallEventHandler handleCallEvent() could not find a group call - ignoring event (groupCallId=${groupCallId}, type=${type})`); + return; + } + opponentDeviceId = content.device_id; + if (!opponentDeviceId) { + _logger.logger.warn(`CallEventHandler handleCallEvent() could not find a device id - ignoring event (senderId=${senderId})`); + groupCall.emit(_groupCall3.GroupCallEvent.Error, new _groupCall3.GroupCallUnknownDeviceError(senderId)); + return; + } + if (content.dest_session_id !== this.client.getSessionId()) { + _logger.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 === _event.EventType.CallInvite) { + var _this$client$getTurnS, _createNewMatrixCall, _groupCall; + // 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 === _call.CallState.Ended) return; + if (call) { + _logger.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$getTurnS = this.client.getTurnServersExpiry()) !== null && _this$client$getTurnS !== void 0 ? _this$client$getTurnS : 0) - Date.now(); + _logger.logger.info("CallEventHandler handleCallEvent() current turn creds expire in " + timeUntilTurnCresExpire + " ms"); + call = (_createNewMatrixCall = (0, _call.createNewMatrixCall)(this.client, callRoomId, { + forceTURN: this.client.forceTURN, + opponentDeviceId, + groupCallId, + opponentSessionId: content.sender_session_id + })) !== null && _createNewMatrixCall !== void 0 ? _createNewMatrixCall : undefined; + if (!call) { + _logger.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 = groupCall) === null || _groupCall === void 0 ? void 0 : _groupCall.getGroupCallStats(); + if (stats) { + call.initStats(stats); + } + try { + await call.initWithInvite(event); + } catch (e) { + if (e instanceof _call.CallError) { + if (e.code === _groupCall3.GroupCallErrorCode.UnknownDevice) { + var _groupCall2; + (_groupCall2 = groupCall) === null || _groupCall2 === void 0 ? void 0 : _groupCall2.emit(_groupCall3.GroupCallEvent.Error, e); + } else { + _logger.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; + for (const thisCall of this.calls.values()) { + var _call$getOpponentMemb; + const isCalling = [_call.CallState.WaitLocalMedia, _call.CallState.CreateOffer, _call.CallState.InviteSent].includes(thisCall.state); + if (call.roomId === thisCall.roomId && thisCall.direction === _call.CallDirection.Outbound && ((_call$getOpponentMemb = call.getOpponentMember()) === null || _call$getOpponentMemb === void 0 ? void 0 : _call$getOpponentMemb.userId) === thisCall.invitee && isCalling) { + existingCall = thisCall; + break; + } + } + if (existingCall) { + if (existingCall.callId > call.callId) { + _logger.logger.log(`CallEventHandler handleCallEvent() detected glare - answering incoming call and canceling outgoing call (incomingId=${call.callId}, outgoingId=${existingCall.callId})`); + existingCall.replacedBy(call); + } else { + _logger.logger.log(`CallEventHandler handleCallEvent() detected glare - hanging up incoming call (incomingId=${call.callId}, outgoingId=${existingCall.callId})`); + call.hangup(_call.CallErrorCode.Replaced, true); + } + } else { + this.client.emit(CallEventHandlerEvent.Incoming, call); + } + return; + } else if (type === _event.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 ([_event.EventType.CallHangup, _event.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) { + var _createNewMatrixCall2; + // 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 = (_createNewMatrixCall2 = (0, _call.createNewMatrixCall)(this.client, callRoomId, { + opponentDeviceId, + opponentSessionId: content.sender_session_id + })) !== null && _createNewMatrixCall2 !== void 0 ? _createNewMatrixCall2 : undefined; + if (call) { + call.callId = content.call_id; + call.initWithHangup(event); + this.calls.set(content.call_id, call); + } + } else { + if (call.state !== _call.CallState.Ended) { + if (type === _event.EventType.CallHangup) { + call.onHangupReceived(content); + } else { + call.onRejectReceived(content); + } + + // @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 === _call.CallState.Ended) this.calls.delete(content.call_id); + } + } + return; + } + + // The following events need a call and a peer connection + if (!call || !call.hasPeerConnection) { + _logger.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 _event.EventType.CallAnswer: + if (weSentTheEvent) { + if (call.state === _call.CallState.Ringing) { + call.onAnsweredElsewhere(content); + } + } else { + call.onAnswerReceived(event); + } + break; + case _event.EventType.CallSelectAnswer: + call.onSelectAnswerReceived(event); + break; + case _event.EventType.CallNegotiate: + call.onNegotiateReceived(event); + break; + case _event.EventType.CallAssertedIdentity: + case _event.EventType.CallAssertedIdentityPrefix: + call.onAssertedIdentityReceived(event); + break; + case _event.EventType.CallSDPStreamMetadataChanged: + case _event.EventType.CallSDPStreamMetadataChangedPrefix: + call.onSDPStreamMetadataChangedReceived(event); + break; + } + } +} +exports.CallEventHandler = CallEventHandler; +//# sourceMappingURL=callEventHandler.js.map
\ No newline at end of file |