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/lib/webrtc/groupCall.js | |
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/lib/webrtc/groupCall.js')
-rw-r--r-- | includes/external/matrix/node_modules/matrix-js-sdk/lib/webrtc/groupCall.js | 1184 |
1 files changed, 1184 insertions, 0 deletions
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/lib/webrtc/groupCall.js b/includes/external/matrix/node_modules/matrix-js-sdk/lib/webrtc/groupCall.js new file mode 100644 index 0000000..d9c044b --- /dev/null +++ b/includes/external/matrix/node_modules/matrix-js-sdk/lib/webrtc/groupCall.js @@ -0,0 +1,1184 @@ +"use strict"; + +var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.OtherUserSpeakingError = exports.GroupCallUnknownDeviceError = exports.GroupCallType = exports.GroupCallTerminationReason = exports.GroupCallStatsReportEvent = exports.GroupCallState = exports.GroupCallIntent = exports.GroupCallEvent = exports.GroupCallErrorCode = exports.GroupCallError = exports.GroupCall = void 0; +var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); +var _typedEventEmitter = require("../models/typed-event-emitter"); +var _callFeed = require("./callFeed"); +var _call = require("./call"); +var _roomState = require("../models/room-state"); +var _logger = require("../logger"); +var _ReEmitter = require("../ReEmitter"); +var _callEventTypes = require("./callEventTypes"); +var _event = require("../@types/event"); +var _callEventHandler = require("./callEventHandler"); +var _groupCallEventHandler = require("./groupCallEventHandler"); +var _utils = require("../utils"); +var _groupCallStats = require("./stats/groupCallStats"); +var _statsReport = require("./stats/statsReport"); +function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } +function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } +let GroupCallIntent; +exports.GroupCallIntent = GroupCallIntent; +(function (GroupCallIntent) { + GroupCallIntent["Ring"] = "m.ring"; + GroupCallIntent["Prompt"] = "m.prompt"; + GroupCallIntent["Room"] = "m.room"; +})(GroupCallIntent || (exports.GroupCallIntent = GroupCallIntent = {})); +let GroupCallType; +exports.GroupCallType = GroupCallType; +(function (GroupCallType) { + GroupCallType["Video"] = "m.video"; + GroupCallType["Voice"] = "m.voice"; +})(GroupCallType || (exports.GroupCallType = GroupCallType = {})); +let GroupCallTerminationReason; +exports.GroupCallTerminationReason = GroupCallTerminationReason; +(function (GroupCallTerminationReason) { + GroupCallTerminationReason["CallEnded"] = "call_ended"; +})(GroupCallTerminationReason || (exports.GroupCallTerminationReason = GroupCallTerminationReason = {})); +/** + * Because event names are just strings, they do need + * to be unique over all event types of event emitter. + * Some objects could emit more then one set of events. + */ +let GroupCallEvent; +exports.GroupCallEvent = GroupCallEvent; +(function (GroupCallEvent) { + GroupCallEvent["GroupCallStateChanged"] = "group_call_state_changed"; + GroupCallEvent["ActiveSpeakerChanged"] = "active_speaker_changed"; + GroupCallEvent["CallsChanged"] = "calls_changed"; + GroupCallEvent["UserMediaFeedsChanged"] = "user_media_feeds_changed"; + GroupCallEvent["ScreenshareFeedsChanged"] = "screenshare_feeds_changed"; + GroupCallEvent["LocalScreenshareStateChanged"] = "local_screenshare_state_changed"; + GroupCallEvent["LocalMuteStateChanged"] = "local_mute_state_changed"; + GroupCallEvent["ParticipantsChanged"] = "participants_changed"; + GroupCallEvent["Error"] = "group_call_error"; +})(GroupCallEvent || (exports.GroupCallEvent = GroupCallEvent = {})); +let GroupCallStatsReportEvent; +exports.GroupCallStatsReportEvent = GroupCallStatsReportEvent; +(function (GroupCallStatsReportEvent) { + GroupCallStatsReportEvent["ConnectionStats"] = "GroupCall.connection_stats"; + GroupCallStatsReportEvent["ByteSentStats"] = "GroupCall.byte_sent_stats"; +})(GroupCallStatsReportEvent || (exports.GroupCallStatsReportEvent = GroupCallStatsReportEvent = {})); +let GroupCallErrorCode; +exports.GroupCallErrorCode = GroupCallErrorCode; +(function (GroupCallErrorCode) { + GroupCallErrorCode["NoUserMedia"] = "no_user_media"; + GroupCallErrorCode["UnknownDevice"] = "unknown_device"; + GroupCallErrorCode["PlaceCallFailed"] = "place_call_failed"; +})(GroupCallErrorCode || (exports.GroupCallErrorCode = GroupCallErrorCode = {})); +class GroupCallError extends Error { + constructor(code, msg, err) { + // Still don't think there's any way to have proper nested errors + if (err) { + super(msg + ": " + err); + (0, _defineProperty2.default)(this, "code", void 0); + } else { + super(msg); + (0, _defineProperty2.default)(this, "code", void 0); + } + this.code = code; + } +} +exports.GroupCallError = GroupCallError; +class GroupCallUnknownDeviceError extends GroupCallError { + constructor(userId) { + super(GroupCallErrorCode.UnknownDevice, "No device found for " + userId); + this.userId = userId; + } +} +exports.GroupCallUnknownDeviceError = GroupCallUnknownDeviceError; +class OtherUserSpeakingError extends Error { + constructor() { + super("Cannot unmute: another user is speaking"); + } +} +exports.OtherUserSpeakingError = OtherUserSpeakingError; +let GroupCallState; +exports.GroupCallState = GroupCallState; +(function (GroupCallState) { + GroupCallState["LocalCallFeedUninitialized"] = "local_call_feed_uninitialized"; + GroupCallState["InitializingLocalCallFeed"] = "initializing_local_call_feed"; + GroupCallState["LocalCallFeedInitialized"] = "local_call_feed_initialized"; + GroupCallState["Entered"] = "entered"; + GroupCallState["Ended"] = "ended"; +})(GroupCallState || (exports.GroupCallState = GroupCallState = {})); +const DEVICE_TIMEOUT = 1000 * 60 * 60; // 1 hour + +function getCallUserId(call) { + var _call$getOpponentMemb; + return ((_call$getOpponentMemb = call.getOpponentMember()) === null || _call$getOpponentMemb === void 0 ? void 0 : _call$getOpponentMemb.userId) || call.invitee || null; +} +class GroupCall extends _typedEventEmitter.TypedEventEmitter { + // Config + + // user_id -> device_id -> MatrixCall + // user_id -> device_id -> ICallHandlers + + // user_id -> device_id -> count + + constructor(client, room, type, isPtt, intent, groupCallId, dataChannelsEnabled, dataChannelOptions, isCallWithoutVideoAndAudio) { + var _room$currentState$ge, _room$currentState$ge2; + super(); + this.client = client; + this.room = room; + this.type = type; + this.isPtt = isPtt; + this.intent = intent; + this.dataChannelsEnabled = dataChannelsEnabled; + this.dataChannelOptions = dataChannelOptions; + (0, _defineProperty2.default)(this, "activeSpeakerInterval", 1000); + (0, _defineProperty2.default)(this, "retryCallInterval", 5000); + (0, _defineProperty2.default)(this, "participantTimeout", 1000 * 15); + (0, _defineProperty2.default)(this, "pttMaxTransmitTime", 1000 * 20); + (0, _defineProperty2.default)(this, "activeSpeaker", void 0); + (0, _defineProperty2.default)(this, "localCallFeed", void 0); + (0, _defineProperty2.default)(this, "localScreenshareFeed", void 0); + (0, _defineProperty2.default)(this, "localDesktopCapturerSourceId", void 0); + (0, _defineProperty2.default)(this, "userMediaFeeds", []); + (0, _defineProperty2.default)(this, "screenshareFeeds", []); + (0, _defineProperty2.default)(this, "groupCallId", void 0); + (0, _defineProperty2.default)(this, "allowCallWithoutVideoAndAudio", void 0); + (0, _defineProperty2.default)(this, "calls", new Map()); + (0, _defineProperty2.default)(this, "callHandlers", new Map()); + (0, _defineProperty2.default)(this, "activeSpeakerLoopInterval", void 0); + (0, _defineProperty2.default)(this, "retryCallLoopInterval", void 0); + (0, _defineProperty2.default)(this, "retryCallCounts", new Map()); + (0, _defineProperty2.default)(this, "reEmitter", void 0); + (0, _defineProperty2.default)(this, "transmitTimer", null); + (0, _defineProperty2.default)(this, "participantsExpirationTimer", null); + (0, _defineProperty2.default)(this, "resendMemberStateTimer", null); + (0, _defineProperty2.default)(this, "initWithAudioMuted", false); + (0, _defineProperty2.default)(this, "initWithVideoMuted", false); + (0, _defineProperty2.default)(this, "initCallFeedPromise", void 0); + (0, _defineProperty2.default)(this, "stats", void 0); + (0, _defineProperty2.default)(this, "onConnectionStats", report => { + // @TODO: Implement data argumentation + this.emit(GroupCallStatsReportEvent.ConnectionStats, { + report + }); + }); + (0, _defineProperty2.default)(this, "onByteSentStats", report => { + // @TODO: Implement data argumentation + this.emit(GroupCallStatsReportEvent.ByteSentStats, { + report + }); + }); + (0, _defineProperty2.default)(this, "_state", GroupCallState.LocalCallFeedUninitialized); + (0, _defineProperty2.default)(this, "_participants", new Map()); + (0, _defineProperty2.default)(this, "_creationTs", null); + (0, _defineProperty2.default)(this, "_enteredViaAnotherSession", false); + (0, _defineProperty2.default)(this, "onIncomingCall", newCall => { + var _newCall$getOpponentM, _this$calls$get; + // The incoming calls may be for another room, which we will ignore. + if (newCall.roomId !== this.room.roomId) { + return; + } + if (newCall.state !== _call.CallState.Ringing) { + _logger.logger.warn(`GroupCall ${this.groupCallId} onIncomingCall() incoming call no longer in ringing state - ignoring`); + return; + } + if (!newCall.groupCallId || newCall.groupCallId !== this.groupCallId) { + _logger.logger.log(`GroupCall ${this.groupCallId} onIncomingCall() ignored because it doesn't match the current group call`); + newCall.reject(); + return; + } + const opponentUserId = (_newCall$getOpponentM = newCall.getOpponentMember()) === null || _newCall$getOpponentM === void 0 ? void 0 : _newCall$getOpponentM.userId; + if (opponentUserId === undefined) { + _logger.logger.warn(`GroupCall ${this.groupCallId} onIncomingCall() incoming call with no member - ignoring`); + return; + } + const deviceMap = (_this$calls$get = this.calls.get(opponentUserId)) !== null && _this$calls$get !== void 0 ? _this$calls$get : new Map(); + const prevCall = deviceMap.get(newCall.getOpponentDeviceId()); + if ((prevCall === null || prevCall === void 0 ? void 0 : prevCall.callId) === newCall.callId) return; + _logger.logger.log(`GroupCall ${this.groupCallId} onIncomingCall() incoming call (userId=${opponentUserId}, callId=${newCall.callId})`); + if (prevCall) prevCall.hangup(_call.CallErrorCode.Replaced, false); + this.initCall(newCall); + const feeds = this.getLocalFeeds().map(feed => feed.clone()); + if (!this.callExpected(newCall)) { + // Disable our tracks for users not explicitly participating in the + // call but trying to receive the feeds + for (const feed of feeds) { + (0, _call.setTracksEnabled)(feed.stream.getAudioTracks(), false); + (0, _call.setTracksEnabled)(feed.stream.getVideoTracks(), false); + } + } + newCall.answerWithCallFeeds(feeds); + deviceMap.set(newCall.getOpponentDeviceId(), newCall); + this.calls.set(opponentUserId, deviceMap); + this.emit(GroupCallEvent.CallsChanged, this.calls); + }); + (0, _defineProperty2.default)(this, "onRetryCallLoop", () => { + let needsRetry = false; + for (const [{ + userId + }, participantMap] of this.participants) { + const callMap = this.calls.get(userId); + let retriesMap = this.retryCallCounts.get(userId); + for (const [deviceId, participant] of participantMap) { + var _retriesMap$get, _retriesMap; + const call = callMap === null || callMap === void 0 ? void 0 : callMap.get(deviceId); + const retries = (_retriesMap$get = (_retriesMap = retriesMap) === null || _retriesMap === void 0 ? void 0 : _retriesMap.get(deviceId)) !== null && _retriesMap$get !== void 0 ? _retriesMap$get : 0; + if ((call === null || call === void 0 ? void 0 : call.getOpponentSessionId()) !== participant.sessionId && this.wantsOutgoingCall(userId, deviceId) && retries < 3) { + if (retriesMap === undefined) { + retriesMap = new Map(); + this.retryCallCounts.set(userId, retriesMap); + } + retriesMap.set(deviceId, retries + 1); + needsRetry = true; + } + } + } + if (needsRetry) this.placeOutgoingCalls(); + }); + (0, _defineProperty2.default)(this, "onCallFeedsChanged", call => { + const opponentMemberId = getCallUserId(call); + const opponentDeviceId = call.getOpponentDeviceId(); + if (!opponentMemberId) { + throw new Error("Cannot change call feeds without user id"); + } + const currentUserMediaFeed = this.getUserMediaFeed(opponentMemberId, opponentDeviceId); + const remoteUsermediaFeed = call.remoteUsermediaFeed; + const remoteFeedChanged = remoteUsermediaFeed !== currentUserMediaFeed; + if (remoteFeedChanged) { + if (!currentUserMediaFeed && remoteUsermediaFeed) { + this.addUserMediaFeed(remoteUsermediaFeed); + } else if (currentUserMediaFeed && remoteUsermediaFeed) { + this.replaceUserMediaFeed(currentUserMediaFeed, remoteUsermediaFeed); + } else if (currentUserMediaFeed && !remoteUsermediaFeed) { + this.removeUserMediaFeed(currentUserMediaFeed); + } + } + const currentScreenshareFeed = this.getScreenshareFeed(opponentMemberId, opponentDeviceId); + const remoteScreensharingFeed = call.remoteScreensharingFeed; + const remoteScreenshareFeedChanged = remoteScreensharingFeed !== currentScreenshareFeed; + if (remoteScreenshareFeedChanged) { + if (!currentScreenshareFeed && remoteScreensharingFeed) { + this.addScreenshareFeed(remoteScreensharingFeed); + } else if (currentScreenshareFeed && remoteScreensharingFeed) { + this.replaceScreenshareFeed(currentScreenshareFeed, remoteScreensharingFeed); + } else if (currentScreenshareFeed && !remoteScreensharingFeed) { + this.removeScreenshareFeed(currentScreenshareFeed); + } + } + }); + (0, _defineProperty2.default)(this, "onCallStateChanged", (call, state, _oldState) => { + var _call$getOpponentMemb2; + if (state === _call.CallState.Ended) return; + const audioMuted = this.localCallFeed.isAudioMuted(); + if (call.localUsermediaStream && call.isMicrophoneMuted() !== audioMuted) { + call.setMicrophoneMuted(audioMuted); + } + const videoMuted = this.localCallFeed.isVideoMuted(); + if (call.localUsermediaStream && call.isLocalVideoMuted() !== videoMuted) { + call.setLocalVideoMuted(videoMuted); + } + const opponentUserId = (_call$getOpponentMemb2 = call.getOpponentMember()) === null || _call$getOpponentMemb2 === void 0 ? void 0 : _call$getOpponentMemb2.userId; + if (state === _call.CallState.Connected && opponentUserId) { + const retriesMap = this.retryCallCounts.get(opponentUserId); + retriesMap === null || retriesMap === void 0 ? void 0 : retriesMap.delete(call.getOpponentDeviceId()); + if ((retriesMap === null || retriesMap === void 0 ? void 0 : retriesMap.size) === 0) this.retryCallCounts.delete(opponentUserId); + } + }); + (0, _defineProperty2.default)(this, "onCallHangup", call => { + var _call$getOpponentMemb3, _call$getOpponentMemb4; + if (call.hangupReason === _call.CallErrorCode.Replaced) return; + const opponentUserId = (_call$getOpponentMemb3 = (_call$getOpponentMemb4 = call.getOpponentMember()) === null || _call$getOpponentMemb4 === void 0 ? void 0 : _call$getOpponentMemb4.userId) !== null && _call$getOpponentMemb3 !== void 0 ? _call$getOpponentMemb3 : this.room.getMember(call.invitee).userId; + const deviceMap = this.calls.get(opponentUserId); + + // Sanity check that this call is in fact in the map + if ((deviceMap === null || deviceMap === void 0 ? void 0 : deviceMap.get(call.getOpponentDeviceId())) === call) { + this.disposeCall(call, call.hangupReason); + deviceMap.delete(call.getOpponentDeviceId()); + if (deviceMap.size === 0) this.calls.delete(opponentUserId); + this.emit(GroupCallEvent.CallsChanged, this.calls); + } + }); + (0, _defineProperty2.default)(this, "onCallReplaced", (prevCall, newCall) => { + const opponentUserId = prevCall.getOpponentMember().userId; + let deviceMap = this.calls.get(opponentUserId); + if (deviceMap === undefined) { + deviceMap = new Map(); + this.calls.set(opponentUserId, deviceMap); + } + prevCall.hangup(_call.CallErrorCode.Replaced, false); + this.initCall(newCall); + deviceMap.set(prevCall.getOpponentDeviceId(), newCall); + this.emit(GroupCallEvent.CallsChanged, this.calls); + }); + (0, _defineProperty2.default)(this, "onActiveSpeakerLoop", () => { + let topAvg = undefined; + let nextActiveSpeaker = undefined; + for (const callFeed of this.userMediaFeeds) { + if (callFeed.isLocal() && this.userMediaFeeds.length > 1) continue; + const total = callFeed.speakingVolumeSamples.reduce((acc, volume) => acc + Math.max(volume, _callFeed.SPEAKING_THRESHOLD)); + const avg = total / callFeed.speakingVolumeSamples.length; + if (!topAvg || avg > topAvg) { + topAvg = avg; + nextActiveSpeaker = callFeed; + } + } + if (nextActiveSpeaker && this.activeSpeaker !== nextActiveSpeaker && topAvg && topAvg > _callFeed.SPEAKING_THRESHOLD) { + this.activeSpeaker = nextActiveSpeaker; + this.emit(GroupCallEvent.ActiveSpeakerChanged, this.activeSpeaker); + } + }); + (0, _defineProperty2.default)(this, "onRoomState", () => this.updateParticipants()); + (0, _defineProperty2.default)(this, "onParticipantsChanged", () => { + // Re-run setTracksEnabled on all calls, so that participants that just + // left get denied access to our media, and participants that just + // joined get granted access + this.forEachCall(call => { + const expected = this.callExpected(call); + for (const feed of call.getLocalFeeds()) { + (0, _call.setTracksEnabled)(feed.stream.getAudioTracks(), !feed.isAudioMuted() && expected); + (0, _call.setTracksEnabled)(feed.stream.getVideoTracks(), !feed.isVideoMuted() && expected); + } + }); + if (this.state === GroupCallState.Entered) this.placeOutgoingCalls(); + }); + (0, _defineProperty2.default)(this, "onStateChanged", (newState, oldState) => { + if (newState === GroupCallState.Entered || oldState === GroupCallState.Entered || newState === GroupCallState.Ended) { + // We either entered, left, or ended the call + this.updateParticipants(); + this.updateMemberState().catch(e => _logger.logger.error(`GroupCall ${this.groupCallId} onStateChanged() failed to update member state devices"`, e)); + } + }); + (0, _defineProperty2.default)(this, "onLocalFeedsChanged", () => { + if (this.state === GroupCallState.Entered) { + this.updateMemberState().catch(e => _logger.logger.error(`GroupCall ${this.groupCallId} onLocalFeedsChanged() failed to update member state feeds`, e)); + } + }); + this.reEmitter = new _ReEmitter.ReEmitter(this); + this.groupCallId = groupCallId !== null && groupCallId !== void 0 ? groupCallId : (0, _call.genCallID)(); + this.creationTs = (_room$currentState$ge = (_room$currentState$ge2 = room.currentState.getStateEvents(_event.EventType.GroupCallPrefix, this.groupCallId)) === null || _room$currentState$ge2 === void 0 ? void 0 : _room$currentState$ge2.getTs()) !== null && _room$currentState$ge !== void 0 ? _room$currentState$ge : null; + this.updateParticipants(); + room.on(_roomState.RoomStateEvent.Update, this.onRoomState); + this.on(GroupCallEvent.ParticipantsChanged, this.onParticipantsChanged); + this.on(GroupCallEvent.GroupCallStateChanged, this.onStateChanged); + this.on(GroupCallEvent.LocalScreenshareStateChanged, this.onLocalFeedsChanged); + this.allowCallWithoutVideoAndAudio = !!isCallWithoutVideoAndAudio; + const userID = this.client.getUserId() || "unknown"; + this.stats = new _groupCallStats.GroupCallStats(this.groupCallId, userID); + this.stats.reports.on(_statsReport.StatsReport.CONNECTION_STATS, this.onConnectionStats); + this.stats.reports.on(_statsReport.StatsReport.BYTE_SENT_STATS, this.onByteSentStats); + } + async create() { + this.creationTs = Date.now(); + this.client.groupCallEventHandler.groupCalls.set(this.room.roomId, this); + this.client.emit(_groupCallEventHandler.GroupCallEventHandlerEvent.Outgoing, this); + const groupCallState = { + "m.intent": this.intent, + "m.type": this.type, + "io.element.ptt": this.isPtt, + // TODO: Specify data-channels better + "dataChannelsEnabled": this.dataChannelsEnabled, + "dataChannelOptions": this.dataChannelsEnabled ? this.dataChannelOptions : undefined + }; + await this.client.sendStateEvent(this.room.roomId, _event.EventType.GroupCallPrefix, groupCallState, this.groupCallId); + return this; + } + /** + * The group call's state. + */ + get state() { + return this._state; + } + set state(value) { + const prevValue = this._state; + if (value !== prevValue) { + this._state = value; + this.emit(GroupCallEvent.GroupCallStateChanged, value, prevValue); + } + } + /** + * The current participants in the call, as a map from members to device IDs + * to participant info. + */ + get participants() { + return this._participants; + } + set participants(value) { + const prevValue = this._participants; + const participantStateEqual = (x, y) => x.sessionId === y.sessionId && x.screensharing === y.screensharing; + const deviceMapsEqual = (x, y) => (0, _utils.mapsEqual)(x, y, participantStateEqual); + + // Only update if the map actually changed + if (!(0, _utils.mapsEqual)(value, prevValue, deviceMapsEqual)) { + this._participants = value; + this.emit(GroupCallEvent.ParticipantsChanged, value); + } + } + /** + * The timestamp at which the call was created, or null if it has not yet + * been created. + */ + get creationTs() { + return this._creationTs; + } + set creationTs(value) { + this._creationTs = value; + } + /** + * Whether the local device has entered this call via another session, such + * as a widget. + */ + get enteredViaAnotherSession() { + return this._enteredViaAnotherSession; + } + set enteredViaAnotherSession(value) { + this._enteredViaAnotherSession = value; + this.updateParticipants(); + } + + /** + * Executes the given callback on all calls in this group call. + * @param f - The callback. + */ + forEachCall(f) { + for (const deviceMap of this.calls.values()) { + for (const call of deviceMap.values()) f(call); + } + } + getLocalFeeds() { + const feeds = []; + if (this.localCallFeed) feeds.push(this.localCallFeed); + if (this.localScreenshareFeed) feeds.push(this.localScreenshareFeed); + return feeds; + } + hasLocalParticipant() { + var _this$participants$ge, _this$participants$ge2; + return (_this$participants$ge = (_this$participants$ge2 = this.participants.get(this.room.getMember(this.client.getUserId()))) === null || _this$participants$ge2 === void 0 ? void 0 : _this$participants$ge2.has(this.client.getDeviceId())) !== null && _this$participants$ge !== void 0 ? _this$participants$ge : false; + } + + /** + * Determines whether the given call is one that we were expecting to exist + * given our knowledge of who is participating in the group call. + */ + callExpected(call) { + var _this$participants$ge3; + const userId = getCallUserId(call); + const member = userId === null ? null : this.room.getMember(userId); + const deviceId = call.getOpponentDeviceId(); + return member !== null && deviceId !== undefined && ((_this$participants$ge3 = this.participants.get(member)) === null || _this$participants$ge3 === void 0 ? void 0 : _this$participants$ge3.get(deviceId)) !== undefined; + } + async initLocalCallFeed() { + if (this.state !== GroupCallState.LocalCallFeedUninitialized) { + throw new Error(`Cannot initialize local call feed in the "${this.state}" state.`); + } + this.state = GroupCallState.InitializingLocalCallFeed; + + // wraps the real method to serialise calls, because we don't want to try starting + // multiple call feeds at once + if (this.initCallFeedPromise) return this.initCallFeedPromise; + try { + this.initCallFeedPromise = this.initLocalCallFeedInternal(); + await this.initCallFeedPromise; + } finally { + this.initCallFeedPromise = undefined; + } + } + async initLocalCallFeedInternal() { + _logger.logger.log(`GroupCall ${this.groupCallId} initLocalCallFeedInternal() running`); + let stream; + try { + stream = await this.client.getMediaHandler().getUserMediaStream(true, this.type === GroupCallType.Video); + } catch (error) { + // If is allowed to join a call without a media stream, then we + // don't throw an error here. But we need an empty Local Feed to establish + // a connection later. + if (this.allowCallWithoutVideoAndAudio) { + stream = new MediaStream(); + } else { + this.state = GroupCallState.LocalCallFeedUninitialized; + throw error; + } + } + + // The call could've been disposed while we were waiting, and could + // also have been started back up again (hello, React 18) so if we're + // still in this 'initializing' state, carry on, otherwise bail. + if (this._state !== GroupCallState.InitializingLocalCallFeed) { + this.client.getMediaHandler().stopUserMediaStream(stream); + throw new Error("Group call disposed while gathering media stream"); + } + const callFeed = new _callFeed.CallFeed({ + client: this.client, + roomId: this.room.roomId, + userId: this.client.getUserId(), + deviceId: this.client.getDeviceId(), + stream, + purpose: _callEventTypes.SDPStreamMetadataPurpose.Usermedia, + audioMuted: this.initWithAudioMuted || stream.getAudioTracks().length === 0 || this.isPtt, + videoMuted: this.initWithVideoMuted || stream.getVideoTracks().length === 0 + }); + (0, _call.setTracksEnabled)(stream.getAudioTracks(), !callFeed.isAudioMuted()); + (0, _call.setTracksEnabled)(stream.getVideoTracks(), !callFeed.isVideoMuted()); + this.localCallFeed = callFeed; + this.addUserMediaFeed(callFeed); + this.state = GroupCallState.LocalCallFeedInitialized; + } + async updateLocalUsermediaStream(stream) { + if (this.localCallFeed) { + const oldStream = this.localCallFeed.stream; + this.localCallFeed.setNewStream(stream); + const micShouldBeMuted = this.localCallFeed.isAudioMuted(); + const vidShouldBeMuted = this.localCallFeed.isVideoMuted(); + _logger.logger.log(`GroupCall ${this.groupCallId} updateLocalUsermediaStream() (oldStreamId=${oldStream.id}, newStreamId=${stream.id}, micShouldBeMuted=${micShouldBeMuted}, vidShouldBeMuted=${vidShouldBeMuted})`); + (0, _call.setTracksEnabled)(stream.getAudioTracks(), !micShouldBeMuted); + (0, _call.setTracksEnabled)(stream.getVideoTracks(), !vidShouldBeMuted); + this.client.getMediaHandler().stopUserMediaStream(oldStream); + } + } + async enter() { + if (this.state === GroupCallState.LocalCallFeedUninitialized) { + await this.initLocalCallFeed(); + } else if (this.state !== GroupCallState.LocalCallFeedInitialized) { + throw new Error(`Cannot enter call in the "${this.state}" state`); + } + _logger.logger.log(`GroupCall ${this.groupCallId} enter() running`); + this.state = GroupCallState.Entered; + this.client.on(_callEventHandler.CallEventHandlerEvent.Incoming, this.onIncomingCall); + for (const call of this.client.callEventHandler.calls.values()) { + this.onIncomingCall(call); + } + this.retryCallLoopInterval = setInterval(this.onRetryCallLoop, this.retryCallInterval); + this.activeSpeaker = undefined; + this.onActiveSpeakerLoop(); + this.activeSpeakerLoopInterval = setInterval(this.onActiveSpeakerLoop, this.activeSpeakerInterval); + } + dispose() { + if (this.localCallFeed) { + this.removeUserMediaFeed(this.localCallFeed); + this.localCallFeed = undefined; + } + if (this.localScreenshareFeed) { + this.client.getMediaHandler().stopScreensharingStream(this.localScreenshareFeed.stream); + this.removeScreenshareFeed(this.localScreenshareFeed); + this.localScreenshareFeed = undefined; + this.localDesktopCapturerSourceId = undefined; + } + this.client.getMediaHandler().stopAllStreams(); + if (this.transmitTimer !== null) { + clearTimeout(this.transmitTimer); + this.transmitTimer = null; + } + if (this.retryCallLoopInterval !== undefined) { + clearInterval(this.retryCallLoopInterval); + this.retryCallLoopInterval = undefined; + } + if (this.participantsExpirationTimer !== null) { + clearTimeout(this.participantsExpirationTimer); + this.participantsExpirationTimer = null; + } + if (this.state !== GroupCallState.Entered) { + return; + } + this.forEachCall(call => call.hangup(_call.CallErrorCode.UserHangup, false)); + this.activeSpeaker = undefined; + clearInterval(this.activeSpeakerLoopInterval); + this.retryCallCounts.clear(); + clearInterval(this.retryCallLoopInterval); + this.client.removeListener(_callEventHandler.CallEventHandlerEvent.Incoming, this.onIncomingCall); + this.stats.stop(); + } + leave() { + this.dispose(); + this.state = GroupCallState.LocalCallFeedUninitialized; + } + async terminate(emitStateEvent = true) { + this.dispose(); + this.room.off(_roomState.RoomStateEvent.Update, this.onRoomState); + this.client.groupCallEventHandler.groupCalls.delete(this.room.roomId); + this.client.emit(_groupCallEventHandler.GroupCallEventHandlerEvent.Ended, this); + this.state = GroupCallState.Ended; + if (emitStateEvent) { + const existingStateEvent = this.room.currentState.getStateEvents(_event.EventType.GroupCallPrefix, this.groupCallId); + await this.client.sendStateEvent(this.room.roomId, _event.EventType.GroupCallPrefix, _objectSpread(_objectSpread({}, existingStateEvent.getContent()), {}, { + "m.terminated": GroupCallTerminationReason.CallEnded + }), this.groupCallId); + } + } + + /* + * Local Usermedia + */ + + isLocalVideoMuted() { + if (this.localCallFeed) { + return this.localCallFeed.isVideoMuted(); + } + return true; + } + isMicrophoneMuted() { + if (this.localCallFeed) { + return this.localCallFeed.isAudioMuted(); + } + return true; + } + + /** + * Sets the mute state of the local participants's microphone. + * @param muted - Whether to mute the microphone + * @returns Whether muting/unmuting was successful + */ + async setMicrophoneMuted(muted) { + // hasAudioDevice can block indefinitely if the window has lost focus, + // and it doesn't make much sense to keep a device from being muted, so + // we always allow muted = true changes to go through + if (!muted && !(await this.client.getMediaHandler().hasAudioDevice())) { + return false; + } + const sendUpdatesBefore = !muted && this.isPtt; + + // set a timer for the maximum transmit time on PTT calls + if (this.isPtt) { + // Set or clear the max transmit timer + if (!muted && this.isMicrophoneMuted()) { + this.transmitTimer = setTimeout(() => { + this.setMicrophoneMuted(true); + }, this.pttMaxTransmitTime); + } else if (muted && !this.isMicrophoneMuted()) { + if (this.transmitTimer !== null) clearTimeout(this.transmitTimer); + this.transmitTimer = null; + } + } + this.forEachCall(call => { + var _call$localUsermediaF; + return (_call$localUsermediaF = call.localUsermediaFeed) === null || _call$localUsermediaF === void 0 ? void 0 : _call$localUsermediaF.setAudioVideoMuted(muted, null); + }); + const sendUpdates = async () => { + const updates = []; + this.forEachCall(call => updates.push(call.sendMetadataUpdate())); + await Promise.all(updates).catch(e => _logger.logger.info(`GroupCall ${this.groupCallId} setMicrophoneMuted() failed to send some metadata updates`, e)); + }; + if (sendUpdatesBefore) await sendUpdates(); + if (this.localCallFeed) { + _logger.logger.log(`GroupCall ${this.groupCallId} setMicrophoneMuted() (streamId=${this.localCallFeed.stream.id}, muted=${muted})`); + + // We needed this here to avoid an error in case user join a call without a device. + // I can not use .then .catch functions because linter :-( + try { + if (!muted) { + const stream = await this.client.getMediaHandler().getUserMediaStream(true, !this.localCallFeed.isVideoMuted()); + if (stream === null) { + // if case permission denied to get a stream stop this here + /* istanbul ignore next */ + _logger.logger.log(`GroupCall ${this.groupCallId} setMicrophoneMuted() no device to receive local stream, muted=${muted}`); + return false; + } + } + } catch (e) { + /* istanbul ignore next */ + _logger.logger.log(`GroupCall ${this.groupCallId} setMicrophoneMuted() no device or permission to receive local stream, muted=${muted}`); + return false; + } + this.localCallFeed.setAudioVideoMuted(muted, null); + // I don't believe its actually necessary to enable these tracks: they + // are the one on the GroupCall's own CallFeed and are cloned before being + // given to any of the actual calls, so these tracks don't actually go + // anywhere. Let's do it anyway to avoid confusion. + (0, _call.setTracksEnabled)(this.localCallFeed.stream.getAudioTracks(), !muted); + } else { + _logger.logger.log(`GroupCall ${this.groupCallId} setMicrophoneMuted() no stream muted (muted=${muted})`); + this.initWithAudioMuted = muted; + } + this.forEachCall(call => (0, _call.setTracksEnabled)(call.localUsermediaFeed.stream.getAudioTracks(), !muted && this.callExpected(call))); + this.emit(GroupCallEvent.LocalMuteStateChanged, muted, this.isLocalVideoMuted()); + if (!sendUpdatesBefore) await sendUpdates(); + return true; + } + + /** + * Sets the mute state of the local participants's video. + * @param muted - Whether to mute the video + * @returns Whether muting/unmuting was successful + */ + async setLocalVideoMuted(muted) { + // hasAudioDevice can block indefinitely if the window has lost focus, + // and it doesn't make much sense to keep a device from being muted, so + // we always allow muted = true changes to go through + if (!muted && !(await this.client.getMediaHandler().hasVideoDevice())) { + return false; + } + if (this.localCallFeed) { + /* istanbul ignore next */ + _logger.logger.log(`GroupCall ${this.groupCallId} setLocalVideoMuted() (stream=${this.localCallFeed.stream.id}, muted=${muted})`); + try { + const stream = await this.client.getMediaHandler().getUserMediaStream(true, !muted); + await this.updateLocalUsermediaStream(stream); + this.localCallFeed.setAudioVideoMuted(null, muted); + (0, _call.setTracksEnabled)(this.localCallFeed.stream.getVideoTracks(), !muted); + } catch (_) { + // No permission to video device + /* istanbul ignore next */ + _logger.logger.log(`GroupCall ${this.groupCallId} setLocalVideoMuted() no device or permission to receive local stream, muted=${muted}`); + return false; + } + } else { + _logger.logger.log(`GroupCall ${this.groupCallId} setLocalVideoMuted() no stream muted (muted=${muted})`); + this.initWithVideoMuted = muted; + } + const updates = []; + this.forEachCall(call => updates.push(call.setLocalVideoMuted(muted))); + await Promise.all(updates); + + // We setTracksEnabled again, independently from the call doing it + // internally, since we might not be expecting the call + this.forEachCall(call => (0, _call.setTracksEnabled)(call.localUsermediaFeed.stream.getVideoTracks(), !muted && this.callExpected(call))); + this.emit(GroupCallEvent.LocalMuteStateChanged, this.isMicrophoneMuted(), muted); + return true; + } + async setScreensharingEnabled(enabled, opts = {}) { + if (enabled === this.isScreensharing()) { + return enabled; + } + if (enabled) { + try { + _logger.logger.log(`GroupCall ${this.groupCallId} setScreensharingEnabled() is asking for screensharing permissions`); + const stream = await this.client.getMediaHandler().getScreensharingStream(opts); + for (const track of stream.getTracks()) { + const onTrackEnded = () => { + this.setScreensharingEnabled(false); + track.removeEventListener("ended", onTrackEnded); + }; + track.addEventListener("ended", onTrackEnded); + } + _logger.logger.log(`GroupCall ${this.groupCallId} setScreensharingEnabled() granted screensharing permissions. Setting screensharing enabled on all calls`); + this.localDesktopCapturerSourceId = opts.desktopCapturerSourceId; + this.localScreenshareFeed = new _callFeed.CallFeed({ + client: this.client, + roomId: this.room.roomId, + userId: this.client.getUserId(), + deviceId: this.client.getDeviceId(), + stream, + purpose: _callEventTypes.SDPStreamMetadataPurpose.Screenshare, + audioMuted: false, + videoMuted: false + }); + this.addScreenshareFeed(this.localScreenshareFeed); + this.emit(GroupCallEvent.LocalScreenshareStateChanged, true, this.localScreenshareFeed, this.localDesktopCapturerSourceId); + + // TODO: handle errors + this.forEachCall(call => call.pushLocalFeed(this.localScreenshareFeed.clone())); + return true; + } catch (error) { + if (opts.throwOnFail) throw error; + _logger.logger.error(`GroupCall ${this.groupCallId} setScreensharingEnabled() enabling screensharing error`, error); + this.emit(GroupCallEvent.Error, new GroupCallError(GroupCallErrorCode.NoUserMedia, "Failed to get screen-sharing stream: ", error)); + return false; + } + } else { + this.forEachCall(call => { + if (call.localScreensharingFeed) call.removeLocalFeed(call.localScreensharingFeed); + }); + this.client.getMediaHandler().stopScreensharingStream(this.localScreenshareFeed.stream); + this.removeScreenshareFeed(this.localScreenshareFeed); + this.localScreenshareFeed = undefined; + this.localDesktopCapturerSourceId = undefined; + this.emit(GroupCallEvent.LocalScreenshareStateChanged, false, undefined, undefined); + return false; + } + } + isScreensharing() { + return !!this.localScreenshareFeed; + } + + /* + * Call Setup + * + * There are two different paths for calls to be created: + * 1. Incoming calls triggered by the Call.incoming event. + * 2. Outgoing calls to the initial members of a room or new members + * as they are observed by the RoomState.members event. + */ + + /** + * Determines whether a given participant expects us to call them (versus + * them calling us). + * @param userId - The participant's user ID. + * @param deviceId - The participant's device ID. + * @returns Whether we need to place an outgoing call to the participant. + */ + wantsOutgoingCall(userId, deviceId) { + const localUserId = this.client.getUserId(); + const localDeviceId = this.client.getDeviceId(); + return ( + // If a user's ID is less than our own, they'll call us + userId >= localUserId && ( + // If this is another one of our devices, compare device IDs to tell whether it'll call us + userId !== localUserId || deviceId > localDeviceId) + ); + } + + /** + * Places calls to all participants that we're responsible for calling. + */ + placeOutgoingCalls() { + let callsChanged = false; + for (const [{ + userId + }, participantMap] of this.participants) { + var _this$calls$get2; + const callMap = (_this$calls$get2 = this.calls.get(userId)) !== null && _this$calls$get2 !== void 0 ? _this$calls$get2 : new Map(); + for (const [deviceId, participant] of participantMap) { + const prevCall = callMap.get(deviceId); + if ((prevCall === null || prevCall === void 0 ? void 0 : prevCall.getOpponentSessionId()) !== participant.sessionId && this.wantsOutgoingCall(userId, deviceId)) { + callsChanged = true; + if (prevCall !== undefined) { + _logger.logger.debug(`GroupCall ${this.groupCallId} placeOutgoingCalls() replacing call (userId=${userId}, deviceId=${deviceId}, callId=${prevCall.callId})`); + prevCall.hangup(_call.CallErrorCode.NewSession, false); + } + const newCall = (0, _call.createNewMatrixCall)(this.client, this.room.roomId, { + invitee: userId, + opponentDeviceId: deviceId, + opponentSessionId: participant.sessionId, + groupCallId: this.groupCallId + }); + if (newCall === null) { + _logger.logger.error(`GroupCall ${this.groupCallId} placeOutgoingCalls() failed to create call (userId=${userId}, device=${deviceId})`); + callMap.delete(deviceId); + } else { + this.initCall(newCall); + callMap.set(deviceId, newCall); + _logger.logger.debug(`GroupCall ${this.groupCallId} placeOutgoingCalls() placing call (userId=${userId}, deviceId=${deviceId}, sessionId=${participant.sessionId})`); + newCall.placeCallWithCallFeeds(this.getLocalFeeds().map(feed => feed.clone()), participant.screensharing).then(() => { + if (this.dataChannelsEnabled) { + newCall.createDataChannel("datachannel", this.dataChannelOptions); + } + }).catch(e => { + _logger.logger.warn(`GroupCall ${this.groupCallId} placeOutgoingCalls() failed to place call (userId=${userId})`, e); + if (e instanceof _call.CallError && e.code === GroupCallErrorCode.UnknownDevice) { + this.emit(GroupCallEvent.Error, e); + } else { + this.emit(GroupCallEvent.Error, new GroupCallError(GroupCallErrorCode.PlaceCallFailed, `Failed to place call to ${userId}`)); + } + newCall.hangup(_call.CallErrorCode.SignallingFailed, false); + if (callMap.get(deviceId) === newCall) callMap.delete(deviceId); + }); + } + } + } + if (callMap.size > 0) { + this.calls.set(userId, callMap); + } else { + this.calls.delete(userId); + } + } + if (callsChanged) this.emit(GroupCallEvent.CallsChanged, this.calls); + } + + /* + * Room Member State + */ + + getMemberStateEvents(userId) { + return userId === undefined ? this.room.currentState.getStateEvents(_event.EventType.GroupCallMemberPrefix) : this.room.currentState.getStateEvents(_event.EventType.GroupCallMemberPrefix, userId); + } + initCall(call) { + const opponentMemberId = getCallUserId(call); + if (!opponentMemberId) { + throw new Error("Cannot init call without user id"); + } + const onCallFeedsChanged = () => this.onCallFeedsChanged(call); + const onCallStateChanged = (state, oldState) => this.onCallStateChanged(call, state, oldState); + const onCallHangup = this.onCallHangup; + const onCallReplaced = newCall => this.onCallReplaced(call, newCall); + let deviceMap = this.callHandlers.get(opponentMemberId); + if (deviceMap === undefined) { + deviceMap = new Map(); + this.callHandlers.set(opponentMemberId, deviceMap); + } + deviceMap.set(call.getOpponentDeviceId(), { + onCallFeedsChanged, + onCallStateChanged, + onCallHangup, + onCallReplaced + }); + call.on(_call.CallEvent.FeedsChanged, onCallFeedsChanged); + call.on(_call.CallEvent.State, onCallStateChanged); + call.on(_call.CallEvent.Hangup, onCallHangup); + call.on(_call.CallEvent.Replaced, onCallReplaced); + call.isPtt = this.isPtt; + this.reEmitter.reEmit(call, Object.values(_call.CallEvent)); + call.initStats(this.stats); + onCallFeedsChanged(); + } + disposeCall(call, hangupReason) { + const opponentMemberId = getCallUserId(call); + const opponentDeviceId = call.getOpponentDeviceId(); + if (!opponentMemberId) { + throw new Error("Cannot dispose call without user id"); + } + const deviceMap = this.callHandlers.get(opponentMemberId); + const { + onCallFeedsChanged, + onCallStateChanged, + onCallHangup, + onCallReplaced + } = deviceMap.get(opponentDeviceId); + call.removeListener(_call.CallEvent.FeedsChanged, onCallFeedsChanged); + call.removeListener(_call.CallEvent.State, onCallStateChanged); + call.removeListener(_call.CallEvent.Hangup, onCallHangup); + call.removeListener(_call.CallEvent.Replaced, onCallReplaced); + deviceMap.delete(opponentMemberId); + if (deviceMap.size === 0) this.callHandlers.delete(opponentMemberId); + if (call.hangupReason === _call.CallErrorCode.Replaced) { + return; + } + const usermediaFeed = this.getUserMediaFeed(opponentMemberId, opponentDeviceId); + if (usermediaFeed) { + this.removeUserMediaFeed(usermediaFeed); + } + const screenshareFeed = this.getScreenshareFeed(opponentMemberId, opponentDeviceId); + if (screenshareFeed) { + this.removeScreenshareFeed(screenshareFeed); + } + } + /* + * UserMedia CallFeed Event Handlers + */ + + getUserMediaFeed(userId, deviceId) { + return this.userMediaFeeds.find(f => f.userId === userId && f.deviceId === deviceId); + } + addUserMediaFeed(callFeed) { + this.userMediaFeeds.push(callFeed); + callFeed.measureVolumeActivity(true); + this.emit(GroupCallEvent.UserMediaFeedsChanged, this.userMediaFeeds); + } + replaceUserMediaFeed(existingFeed, replacementFeed) { + const feedIndex = this.userMediaFeeds.findIndex(f => f.userId === existingFeed.userId && f.deviceId === existingFeed.deviceId); + if (feedIndex === -1) { + throw new Error("Couldn't find user media feed to replace"); + } + this.userMediaFeeds.splice(feedIndex, 1, replacementFeed); + existingFeed.dispose(); + replacementFeed.measureVolumeActivity(true); + this.emit(GroupCallEvent.UserMediaFeedsChanged, this.userMediaFeeds); + } + removeUserMediaFeed(callFeed) { + const feedIndex = this.userMediaFeeds.findIndex(f => f.userId === callFeed.userId && f.deviceId === callFeed.deviceId); + if (feedIndex === -1) { + throw new Error("Couldn't find user media feed to remove"); + } + this.userMediaFeeds.splice(feedIndex, 1); + callFeed.dispose(); + this.emit(GroupCallEvent.UserMediaFeedsChanged, this.userMediaFeeds); + if (this.activeSpeaker === callFeed) { + this.activeSpeaker = this.userMediaFeeds[0]; + this.emit(GroupCallEvent.ActiveSpeakerChanged, this.activeSpeaker); + } + } + /* + * Screenshare Call Feed Event Handlers + */ + + getScreenshareFeed(userId, deviceId) { + return this.screenshareFeeds.find(f => f.userId === userId && f.deviceId === deviceId); + } + addScreenshareFeed(callFeed) { + this.screenshareFeeds.push(callFeed); + this.emit(GroupCallEvent.ScreenshareFeedsChanged, this.screenshareFeeds); + } + replaceScreenshareFeed(existingFeed, replacementFeed) { + const feedIndex = this.screenshareFeeds.findIndex(f => f.userId === existingFeed.userId && f.deviceId === existingFeed.deviceId); + if (feedIndex === -1) { + throw new Error("Couldn't find screenshare feed to replace"); + } + this.screenshareFeeds.splice(feedIndex, 1, replacementFeed); + existingFeed.dispose(); + this.emit(GroupCallEvent.ScreenshareFeedsChanged, this.screenshareFeeds); + } + removeScreenshareFeed(callFeed) { + const feedIndex = this.screenshareFeeds.findIndex(f => f.userId === callFeed.userId && f.deviceId === callFeed.deviceId); + if (feedIndex === -1) { + throw new Error("Couldn't find screenshare feed to remove"); + } + this.screenshareFeeds.splice(feedIndex, 1); + callFeed.dispose(); + this.emit(GroupCallEvent.ScreenshareFeedsChanged, this.screenshareFeeds); + } + + /** + * Recalculates and updates the participant map to match the room state. + */ + updateParticipants() { + const localMember = this.room.getMember(this.client.getUserId()); + if (!localMember) { + // The client hasn't fetched enough of the room state to get our own member + // event. This probably shouldn't happen, but sanity check & exit for now. + _logger.logger.warn(`GroupCall ${this.groupCallId} updateParticipants() tried to update participants before local room member is available`); + return; + } + if (this.participantsExpirationTimer !== null) { + clearTimeout(this.participantsExpirationTimer); + this.participantsExpirationTimer = null; + } + if (this.state === GroupCallState.Ended) { + this.participants = new Map(); + return; + } + const participants = new Map(); + const now = Date.now(); + const entered = this.state === GroupCallState.Entered || this.enteredViaAnotherSession; + let nextExpiration = Infinity; + for (const e of this.getMemberStateEvents()) { + const member = this.room.getMember(e.getStateKey()); + const content = e.getContent(); + const calls = Array.isArray(content["m.calls"]) ? content["m.calls"] : []; + const call = calls.find(call => call["m.call_id"] === this.groupCallId); + const devices = Array.isArray(call === null || call === void 0 ? void 0 : call["m.devices"]) ? call["m.devices"] : []; + + // Filter out invalid and expired devices + let validDevices = devices.filter(d => typeof d.device_id === "string" && typeof d.session_id === "string" && typeof d.expires_ts === "number" && d.expires_ts > now && Array.isArray(d.feeds)); + + // Apply local echo for the unentered case + if (!entered && (member === null || member === void 0 ? void 0 : member.userId) === this.client.getUserId()) { + validDevices = validDevices.filter(d => d.device_id !== this.client.getDeviceId()); + } + + // Must have a connected device and be joined to the room + if (validDevices.length > 0 && (member === null || member === void 0 ? void 0 : member.membership) === "join") { + const deviceMap = new Map(); + participants.set(member, deviceMap); + for (const d of validDevices) { + deviceMap.set(d.device_id, { + sessionId: d.session_id, + screensharing: d.feeds.some(f => f.purpose === _callEventTypes.SDPStreamMetadataPurpose.Screenshare) + }); + if (d.expires_ts < nextExpiration) nextExpiration = d.expires_ts; + } + } + } + + // Apply local echo for the entered case + if (entered) { + let deviceMap = participants.get(localMember); + if (deviceMap === undefined) { + deviceMap = new Map(); + participants.set(localMember, deviceMap); + } + if (!deviceMap.has(this.client.getDeviceId())) { + deviceMap.set(this.client.getDeviceId(), { + sessionId: this.client.getSessionId(), + screensharing: this.getLocalFeeds().some(f => f.purpose === _callEventTypes.SDPStreamMetadataPurpose.Screenshare) + }); + } + } + this.participants = participants; + if (nextExpiration < Infinity) { + this.participantsExpirationTimer = setTimeout(() => this.updateParticipants(), nextExpiration - now); + } + } + + /** + * Updates the local user's member state with the devices returned by the given function. + * @param fn - A function from the current devices to the new devices. If it + * returns null, the update will be skipped. + * @param keepAlive - Whether the request should outlive the window. + */ + async updateDevices(fn, keepAlive = false) { + var _event$getContent; + const now = Date.now(); + const localUserId = this.client.getUserId(); + const event = this.getMemberStateEvents(localUserId); + const content = (_event$getContent = event === null || event === void 0 ? void 0 : event.getContent()) !== null && _event$getContent !== void 0 ? _event$getContent : {}; + const calls = Array.isArray(content["m.calls"]) ? content["m.calls"] : []; + let call = null; + const otherCalls = []; + for (const c of calls) { + if (c["m.call_id"] === this.groupCallId) { + call = c; + } else { + otherCalls.push(c); + } + } + if (call === null) call = {}; + const devices = Array.isArray(call["m.devices"]) ? call["m.devices"] : []; + + // Filter out invalid and expired devices + const validDevices = devices.filter(d => typeof d.device_id === "string" && typeof d.session_id === "string" && typeof d.expires_ts === "number" && d.expires_ts > now && Array.isArray(d.feeds)); + const newDevices = fn(validDevices); + if (newDevices === null) return; + const newCalls = [...otherCalls]; + if (newDevices.length > 0) { + newCalls.push(_objectSpread(_objectSpread({}, call), {}, { + "m.call_id": this.groupCallId, + "m.devices": newDevices + })); + } + const newContent = { + "m.calls": newCalls + }; + await this.client.sendStateEvent(this.room.roomId, _event.EventType.GroupCallMemberPrefix, newContent, localUserId, { + keepAlive + }); + } + async addDeviceToMemberState() { + await this.updateDevices(devices => [...devices.filter(d => d.device_id !== this.client.getDeviceId()), { + device_id: this.client.getDeviceId(), + session_id: this.client.getSessionId(), + expires_ts: Date.now() + DEVICE_TIMEOUT, + feeds: this.getLocalFeeds().map(feed => ({ + purpose: feed.purpose + })) + // TODO: Add data channels + }]); + } + + async updateMemberState() { + // Clear the old update interval before proceeding + if (this.resendMemberStateTimer !== null) { + clearInterval(this.resendMemberStateTimer); + this.resendMemberStateTimer = null; + } + if (this.state === GroupCallState.Entered) { + // Add the local device + await this.addDeviceToMemberState(); + + // Resend the state event every so often so it doesn't become stale + this.resendMemberStateTimer = setInterval(async () => { + _logger.logger.log(`GroupCall ${this.groupCallId} updateMemberState() resending call member state"`); + try { + await this.addDeviceToMemberState(); + } catch (e) { + _logger.logger.error(`GroupCall ${this.groupCallId} updateMemberState() failed to resend call member state`, e); + } + }, DEVICE_TIMEOUT * 3 / 4); + } else { + // Remove the local device + await this.updateDevices(devices => devices.filter(d => d.device_id !== this.client.getDeviceId()), true); + } + } + + /** + * Cleans up our member state by filtering out logged out devices, inactive + * devices, and our own device (if we know we haven't entered). + */ + async cleanMemberState() { + const { + devices: myDevices + } = await this.client.getDevices(); + const deviceMap = new Map(myDevices.map(d => [d.device_id, d])); + + // updateDevices takes care of filtering out inactive devices for us + await this.updateDevices(devices => { + const newDevices = devices.filter(d => { + const device = deviceMap.get(d.device_id); + return (device === null || device === void 0 ? void 0 : device.last_seen_ts) !== undefined && !(d.device_id === this.client.getDeviceId() && this.state !== GroupCallState.Entered && !this.enteredViaAnotherSession); + }); + + // Skip the update if the devices are unchanged + return newDevices.length === devices.length ? null : newDevices; + }); + } + getGroupCallStats() { + return this.stats; + } +} +exports.GroupCall = GroupCall; +//# sourceMappingURL=groupCall.js.map
\ No newline at end of file |