UNPKG

matrix-js-sdk

Version:
1,058 lines (1,037 loc) 61.7 kB
import _asyncToGenerator from "@babel/runtime/helpers/asyncToGenerator"; import _defineProperty from "@babel/runtime/helpers/defineProperty"; function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } import { TypedEventEmitter } from "../models/typed-event-emitter.js"; import { CallFeed, SPEAKING_THRESHOLD } from "./callFeed.js"; import { CallErrorCode, CallEvent, CallState, genCallID, setTracksEnabled, createNewMatrixCall, CallError } from "./call.js"; import { RoomStateEvent } from "../models/room-state.js"; import { logger } from "../logger.js"; import { ReEmitter } from "../ReEmitter.js"; import { SDPStreamMetadataPurpose } from "./callEventTypes.js"; import { EventType } from "../@types/event.js"; import { CallEventHandlerEvent } from "./callEventHandler.js"; import { GroupCallEventHandlerEvent } from "./groupCallEventHandler.js"; import { mapsEqual } from "../utils.js"; import { GroupCallStats } from "./stats/groupCallStats.js"; import { StatsReport } from "./stats/statsReport.js"; import { SummaryStatsReportGatherer } from "./stats/summaryStatsReportGatherer.js"; import { CallFeedStatsReporter } from "./stats/callFeedStatsReporter.js"; import { KnownMembership } from "../@types/membership.js"; export var GroupCallIntent = /*#__PURE__*/function (GroupCallIntent) { GroupCallIntent["Ring"] = "m.ring"; GroupCallIntent["Prompt"] = "m.prompt"; GroupCallIntent["Room"] = "m.room"; return GroupCallIntent; }({}); export var GroupCallType = /*#__PURE__*/function (GroupCallType) { GroupCallType["Video"] = "m.video"; GroupCallType["Voice"] = "m.voice"; return GroupCallType; }({}); export var GroupCallTerminationReason = /*#__PURE__*/function (GroupCallTerminationReason) { GroupCallTerminationReason["CallEnded"] = "call_ended"; return 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. */ export var GroupCallEvent = /*#__PURE__*/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"; return GroupCallEvent; }({}); export var GroupCallStatsReportEvent = /*#__PURE__*/function (GroupCallStatsReportEvent) { GroupCallStatsReportEvent["ConnectionStats"] = "GroupCall.connection_stats"; GroupCallStatsReportEvent["ByteSentStats"] = "GroupCall.byte_sent_stats"; GroupCallStatsReportEvent["SummaryStats"] = "GroupCall.summary_stats"; GroupCallStatsReportEvent["CallFeedStats"] = "GroupCall.call_feed_stats"; return GroupCallStatsReportEvent; }({}); /** * The final report-events that get consumed by client. */ export var GroupCallErrorCode = /*#__PURE__*/function (GroupCallErrorCode) { GroupCallErrorCode["NoUserMedia"] = "no_user_media"; GroupCallErrorCode["UnknownDevice"] = "unknown_device"; GroupCallErrorCode["PlaceCallFailed"] = "place_call_failed"; return GroupCallErrorCode; }({}); export 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); _defineProperty(this, "code", void 0); } else { super(msg); _defineProperty(this, "code", void 0); } this.code = code; } } export class GroupCallUnknownDeviceError extends GroupCallError { constructor(userId) { super(GroupCallErrorCode.UnknownDevice, "No device found for " + userId); this.userId = userId; } } export class OtherUserSpeakingError extends Error { constructor() { super("Cannot unmute: another user is speaking"); } } export var GroupCallState = /*#__PURE__*/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"; return GroupCallState; }({}); var 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; } export class GroupCall extends TypedEventEmitter { constructor(client, room, type, isPtt, intent, groupCallId, dataChannelsEnabled, dataChannelOptions, isCallWithoutVideoAndAudio) { var _room$currentState$ge, _room$currentState$ge2; var useLivekit = arguments.length > 9 && arguments[9] !== undefined ? arguments[9] : false; var livekitServiceURL = arguments.length > 10 ? arguments[10] : undefined; super(); this.client = client; this.room = room; this.type = type; this.isPtt = isPtt; this.intent = intent; this.dataChannelsEnabled = dataChannelsEnabled; this.dataChannelOptions = dataChannelOptions; this.useLivekit = useLivekit; // Config _defineProperty(this, "activeSpeakerInterval", 1000); _defineProperty(this, "retryCallInterval", 5000); _defineProperty(this, "participantTimeout", 1000 * 15); _defineProperty(this, "pttMaxTransmitTime", 1000 * 20); _defineProperty(this, "activeSpeaker", void 0); _defineProperty(this, "localCallFeed", void 0); _defineProperty(this, "localScreenshareFeed", void 0); _defineProperty(this, "localDesktopCapturerSourceId", void 0); _defineProperty(this, "userMediaFeeds", []); _defineProperty(this, "screenshareFeeds", []); _defineProperty(this, "groupCallId", void 0); _defineProperty(this, "allowCallWithoutVideoAndAudio", void 0); _defineProperty(this, "calls", new Map()); // user_id -> device_id -> MatrixCall _defineProperty(this, "callHandlers", new Map()); // user_id -> device_id -> ICallHandlers _defineProperty(this, "activeSpeakerLoopInterval", void 0); _defineProperty(this, "retryCallLoopInterval", void 0); _defineProperty(this, "retryCallCounts", new Map()); // user_id -> device_id -> count _defineProperty(this, "reEmitter", void 0); _defineProperty(this, "transmitTimer", null); _defineProperty(this, "participantsExpirationTimer", null); _defineProperty(this, "resendMemberStateTimer", null); _defineProperty(this, "initWithAudioMuted", false); _defineProperty(this, "initWithVideoMuted", false); _defineProperty(this, "initCallFeedPromise", void 0); _defineProperty(this, "_livekitServiceURL", void 0); _defineProperty(this, "stats", void 0); /** * Configure default webrtc stats collection interval in ms * Disable collecting webrtc stats by setting interval to 0 */ _defineProperty(this, "statsCollectIntervalTime", 0); _defineProperty(this, "onConnectionStats", report => { // Final emit of the summary event, to be consumed by the client this.emit(GroupCallStatsReportEvent.ConnectionStats, { report }); }); _defineProperty(this, "onByteSentStats", report => { // Final emit of the summary event, to be consumed by the client this.emit(GroupCallStatsReportEvent.ByteSentStats, { report }); }); _defineProperty(this, "onSummaryStats", report => { SummaryStatsReportGatherer.extendSummaryReport(report, this.participants); // Final emit of the summary event, to be consumed by the client this.emit(GroupCallStatsReportEvent.SummaryStats, { report }); }); _defineProperty(this, "onCallFeedReport", report => { if (this.localCallFeed) { report = CallFeedStatsReporter.expandCallFeedReport(report, [this.localCallFeed], "from-local-feed"); } var callFeeds = []; this.forEachCall(call => { if (call.callId === report.callId) { call.getFeeds().forEach(f => callFeeds.push(f)); } }); report = CallFeedStatsReporter.expandCallFeedReport(report, callFeeds, "from-call-feed"); this.emit(GroupCallStatsReportEvent.CallFeedStats, { report }); }); _defineProperty(this, "_state", GroupCallState.LocalCallFeedUninitialized); _defineProperty(this, "_participants", new Map()); _defineProperty(this, "_creationTs", null); _defineProperty(this, "_enteredViaAnotherSession", false); /* * 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. */ _defineProperty(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 !== CallState.Ringing) { logger.warn("GroupCall ".concat(this.groupCallId, " onIncomingCall() incoming call no longer in ringing state - ignoring")); return; } if (!newCall.groupCallId || newCall.groupCallId !== this.groupCallId) { logger.log("GroupCall ".concat(this.groupCallId, " onIncomingCall() ignored because it doesn't match the current group call")); newCall.reject(); return; } var opponentUserId = (_newCall$getOpponentM = newCall.getOpponentMember()) === null || _newCall$getOpponentM === void 0 ? void 0 : _newCall$getOpponentM.userId; if (opponentUserId === undefined) { logger.warn("GroupCall ".concat(this.groupCallId, " onIncomingCall() incoming call with no member - ignoring")); return; } if (this.useLivekit) { logger.info("Received incoming call whilst in signaling-only mode! Ignoring."); return; } var deviceMap = (_this$calls$get = this.calls.get(opponentUserId)) !== null && _this$calls$get !== void 0 ? _this$calls$get : new Map(); var prevCall = deviceMap.get(newCall.getOpponentDeviceId()); if ((prevCall === null || prevCall === void 0 ? void 0 : prevCall.callId) === newCall.callId) return; logger.log("GroupCall ".concat(this.groupCallId, " onIncomingCall() incoming call (userId=").concat(opponentUserId, ", callId=").concat(newCall.callId, ")")); if (prevCall) prevCall.hangup(CallErrorCode.Replaced, false); // We must do this before we start initialising / answering the call as we // need to know it is the active call for this user+deviceId and to not ignore // events from it. deviceMap.set(newCall.getOpponentDeviceId(), newCall); this.calls.set(opponentUserId, deviceMap); this.initCall(newCall); var 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 (var feed of feeds) { setTracksEnabled(feed.stream.getAudioTracks(), false); setTracksEnabled(feed.stream.getVideoTracks(), false); } } newCall.answerWithCallFeeds(feeds); this.emit(GroupCallEvent.CallsChanged, this.calls); }); _defineProperty(this, "onRetryCallLoop", () => { var needsRetry = false; for (var [{ userId: _userId }, participantMap] of this.participants) { var callMap = this.calls.get(_userId); var retriesMap = this.retryCallCounts.get(_userId); for (var [deviceId, participant] of participantMap) { var _retriesMap$get, _retriesMap; var call = callMap === null || callMap === void 0 ? void 0 : callMap.get(deviceId); var 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(); }); _defineProperty(this, "onCallFeedsChanged", call => { var opponentMemberId = getCallUserId(call); var opponentDeviceId = call.getOpponentDeviceId(); if (!opponentMemberId) { throw new Error("Cannot change call feeds without user id"); } var currentUserMediaFeed = this.getUserMediaFeed(opponentMemberId, opponentDeviceId); var remoteUsermediaFeed = call.remoteUsermediaFeed; var remoteFeedChanged = remoteUsermediaFeed !== currentUserMediaFeed; var deviceMap = this.calls.get(opponentMemberId); var currentCallForUserDevice = deviceMap === null || deviceMap === void 0 ? void 0 : deviceMap.get(opponentDeviceId); if ((currentCallForUserDevice === null || currentCallForUserDevice === void 0 ? void 0 : currentCallForUserDevice.callId) !== call.callId) { // the call in question is not the current call for this user/deviceId // so ignore feed events from it otherwise we'll remove our real feeds return; } if (remoteFeedChanged) { if (!currentUserMediaFeed && remoteUsermediaFeed) { this.addUserMediaFeed(remoteUsermediaFeed); } else if (currentUserMediaFeed && remoteUsermediaFeed) { this.replaceUserMediaFeed(currentUserMediaFeed, remoteUsermediaFeed); } else if (currentUserMediaFeed && !remoteUsermediaFeed) { this.removeUserMediaFeed(currentUserMediaFeed); } } var currentScreenshareFeed = this.getScreenshareFeed(opponentMemberId, opponentDeviceId); var remoteScreensharingFeed = call.remoteScreensharingFeed; var 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); } } }); _defineProperty(this, "onCallStateChanged", (call, state, _oldState) => { var _call$getOpponentMemb2; if (state === CallState.Ended) return; var audioMuted = this.localCallFeed.isAudioMuted(); if (call.localUsermediaStream && call.isMicrophoneMuted() !== audioMuted) { call.setMicrophoneMuted(audioMuted); } var videoMuted = this.localCallFeed.isVideoMuted(); if (call.localUsermediaStream && call.isLocalVideoMuted() !== videoMuted) { call.setLocalVideoMuted(videoMuted); } var opponentUserId = (_call$getOpponentMemb2 = call.getOpponentMember()) === null || _call$getOpponentMemb2 === void 0 ? void 0 : _call$getOpponentMemb2.userId; if (state === CallState.Connected && opponentUserId) { var retriesMap = this.retryCallCounts.get(opponentUserId); retriesMap === null || retriesMap === void 0 || retriesMap.delete(call.getOpponentDeviceId()); if ((retriesMap === null || retriesMap === void 0 ? void 0 : retriesMap.size) === 0) this.retryCallCounts.delete(opponentUserId); } }); _defineProperty(this, "onCallHangup", call => { var _call$getOpponentMemb3, _call$getOpponentMemb4; if (call.hangupReason === CallErrorCode.Replaced) return; var 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; var 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); } }); _defineProperty(this, "onCallReplaced", (prevCall, newCall) => { var opponentUserId = prevCall.getOpponentMember().userId; var deviceMap = this.calls.get(opponentUserId); if (deviceMap === undefined) { deviceMap = new Map(); this.calls.set(opponentUserId, deviceMap); } prevCall.hangup(CallErrorCode.Replaced, false); this.initCall(newCall); deviceMap.set(prevCall.getOpponentDeviceId(), newCall); this.emit(GroupCallEvent.CallsChanged, this.calls); }); _defineProperty(this, "onActiveSpeakerLoop", () => { var topAvg = undefined; var nextActiveSpeaker = undefined; for (var callFeed of this.userMediaFeeds) { if (callFeed.isLocal() && this.userMediaFeeds.length > 1) continue; var total = callFeed.speakingVolumeSamples.reduce((acc, volume) => acc + Math.max(volume, SPEAKING_THRESHOLD)); var avg = total / callFeed.speakingVolumeSamples.length; if (!topAvg || avg > topAvg) { topAvg = avg; nextActiveSpeaker = callFeed; } } if (nextActiveSpeaker && this.activeSpeaker !== nextActiveSpeaker && topAvg && topAvg > SPEAKING_THRESHOLD) { this.activeSpeaker = nextActiveSpeaker; this.emit(GroupCallEvent.ActiveSpeakerChanged, this.activeSpeaker); } }); _defineProperty(this, "onRoomState", () => this.updateParticipants()); _defineProperty(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 => { var expected = this.callExpected(call); for (var feed of call.getLocalFeeds()) { setTracksEnabled(feed.stream.getAudioTracks(), !feed.isAudioMuted() && expected); setTracksEnabled(feed.stream.getVideoTracks(), !feed.isVideoMuted() && expected); } }); if (this.state === GroupCallState.Entered && !this.useLivekit) this.placeOutgoingCalls(); // Update the participants stored in the stats object }); _defineProperty(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.error("GroupCall ".concat(this.groupCallId, " onStateChanged() failed to update member state devices\""), e)); } }); _defineProperty(this, "onLocalFeedsChanged", () => { if (this.state === GroupCallState.Entered) { this.updateMemberState().catch(e => logger.error("GroupCall ".concat(this.groupCallId, " onLocalFeedsChanged() failed to update member state feeds"), e)); } }); this.reEmitter = new ReEmitter(this); this.groupCallId = groupCallId !== null && groupCallId !== void 0 ? groupCallId : genCallID(); this._livekitServiceURL = livekitServiceURL; this.creationTs = (_room$currentState$ge = (_room$currentState$ge2 = room.currentState.getStateEvents(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(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; } create() { var _this = this; return _asyncToGenerator(function* () { _this.creationTs = Date.now(); _this.client.groupCallEventHandler.groupCalls.set(_this.room.roomId, _this); _this.client.emit(GroupCallEventHandlerEvent.Outgoing, _this); yield _this.sendCallStateEvent(); return _this; })(); } sendCallStateEvent() { var _this2 = this; return _asyncToGenerator(function* () { var groupCallState = { "m.intent": _this2.intent, "m.type": _this2.type, "io.element.ptt": _this2.isPtt, // TODO: Specify data-channels better "dataChannelsEnabled": _this2.dataChannelsEnabled, "dataChannelOptions": _this2.dataChannelsEnabled ? _this2.dataChannelOptions : undefined }; if (_this2.livekitServiceURL) { groupCallState["io.element.livekit_service_url"] = _this2.livekitServiceURL; } yield _this2.client.sendStateEvent(_this2.room.roomId, EventType.GroupCallPrefix, groupCallState, _this2.groupCallId); })(); } get livekitServiceURL() { return this._livekitServiceURL; } updateLivekitServiceURL(newURL) { this._livekitServiceURL = newURL; return this.sendCallStateEvent(); } /** * The group call's state. */ get state() { return this._state; } set state(value) { var 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) { var prevValue = this._participants; var participantStateEqual = (x, y) => x.sessionId === y.sessionId && x.screensharing === y.screensharing; var deviceMapsEqual = (x, y) => mapsEqual(x, y, participantStateEqual); // Only update if the map actually changed if (!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 (var deviceMap of this.calls.values()) { for (var call of deviceMap.values()) f(call); } } getLocalFeeds() { var 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; var userId = getCallUserId(call); var member = userId === null ? null : this.room.getMember(userId); var 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; } initLocalCallFeed() { var _this3 = this; return _asyncToGenerator(function* () { if (_this3.useLivekit) { logger.info("Livekit group call: not starting local call feed."); return; } if (_this3.state !== GroupCallState.LocalCallFeedUninitialized) { throw new Error("Cannot initialize local call feed in the \"".concat(_this3.state, "\" state.")); } _this3.state = GroupCallState.InitializingLocalCallFeed; // wraps the real method to serialise calls, because we don't want to try starting // multiple call feeds at once if (_this3.initCallFeedPromise) return _this3.initCallFeedPromise; try { _this3.initCallFeedPromise = _this3.initLocalCallFeedInternal(); yield _this3.initCallFeedPromise; } finally { _this3.initCallFeedPromise = undefined; } })(); } initLocalCallFeedInternal() { var _this4 = this; return _asyncToGenerator(function* () { logger.log("GroupCall ".concat(_this4.groupCallId, " initLocalCallFeedInternal() running")); var stream; try { stream = yield _this4.client.getMediaHandler().getUserMediaStream(true, _this4.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 (_this4.allowCallWithoutVideoAndAudio) { stream = new MediaStream(); } else { _this4.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 (_this4._state !== GroupCallState.InitializingLocalCallFeed) { _this4.client.getMediaHandler().stopUserMediaStream(stream); throw new Error("Group call disposed while gathering media stream"); } var callFeed = new CallFeed({ client: _this4.client, roomId: _this4.room.roomId, userId: _this4.client.getUserId(), deviceId: _this4.client.getDeviceId(), stream, purpose: SDPStreamMetadataPurpose.Usermedia, audioMuted: _this4.initWithAudioMuted || stream.getAudioTracks().length === 0 || _this4.isPtt, videoMuted: _this4.initWithVideoMuted || stream.getVideoTracks().length === 0 }); setTracksEnabled(stream.getAudioTracks(), !callFeed.isAudioMuted()); setTracksEnabled(stream.getVideoTracks(), !callFeed.isVideoMuted()); _this4.localCallFeed = callFeed; _this4.addUserMediaFeed(callFeed); _this4.state = GroupCallState.LocalCallFeedInitialized; })(); } updateLocalUsermediaStream(stream) { var _this5 = this; return _asyncToGenerator(function* () { if (_this5.localCallFeed) { var oldStream = _this5.localCallFeed.stream; _this5.localCallFeed.setNewStream(stream); var micShouldBeMuted = _this5.localCallFeed.isAudioMuted(); var vidShouldBeMuted = _this5.localCallFeed.isVideoMuted(); logger.log("GroupCall ".concat(_this5.groupCallId, " updateLocalUsermediaStream() (oldStreamId=").concat(oldStream.id, ", newStreamId=").concat(stream.id, ", micShouldBeMuted=").concat(micShouldBeMuted, ", vidShouldBeMuted=").concat(vidShouldBeMuted, ")")); setTracksEnabled(stream.getAudioTracks(), !micShouldBeMuted); setTracksEnabled(stream.getVideoTracks(), !vidShouldBeMuted); _this5.client.getMediaHandler().stopUserMediaStream(oldStream); } })(); } enter() { var _this6 = this; return _asyncToGenerator(function* () { if (_this6.state === GroupCallState.LocalCallFeedUninitialized) { yield _this6.initLocalCallFeed(); } else if (_this6.state !== GroupCallState.LocalCallFeedInitialized) { throw new Error("Cannot enter call in the \"".concat(_this6.state, "\" state")); } logger.log("GroupCall ".concat(_this6.groupCallId, " enter() running")); _this6.state = GroupCallState.Entered; _this6.client.on(CallEventHandlerEvent.Incoming, _this6.onIncomingCall); for (var call of _this6.client.callEventHandler.calls.values()) { _this6.onIncomingCall(call); } if (!_this6.useLivekit) { _this6.retryCallLoopInterval = setInterval(_this6.onRetryCallLoop, _this6.retryCallInterval); _this6.activeSpeaker = undefined; _this6.onActiveSpeakerLoop(); _this6.activeSpeakerLoopInterval = setInterval(_this6.onActiveSpeakerLoop, _this6.activeSpeakerInterval); } })(); } dispose() { var _this$stats; 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(CallErrorCode.UserHangup, false)); this.activeSpeaker = undefined; clearInterval(this.activeSpeakerLoopInterval); this.retryCallCounts.clear(); clearInterval(this.retryCallLoopInterval); this.client.removeListener(CallEventHandlerEvent.Incoming, this.onIncomingCall); (_this$stats = this.stats) === null || _this$stats === void 0 || _this$stats.stop(); } leave() { this.dispose(); this.state = GroupCallState.LocalCallFeedUninitialized; } terminate() { var _arguments = arguments, _this7 = this; return _asyncToGenerator(function* () { var emitStateEvent = _arguments.length > 0 && _arguments[0] !== undefined ? _arguments[0] : true; _this7.dispose(); _this7.room.off(RoomStateEvent.Update, _this7.onRoomState); _this7.client.groupCallEventHandler.groupCalls.delete(_this7.room.roomId); _this7.client.emit(GroupCallEventHandlerEvent.Ended, _this7); _this7.state = GroupCallState.Ended; if (emitStateEvent) { var existingStateEvent = _this7.room.currentState.getStateEvents(EventType.GroupCallPrefix, _this7.groupCallId); yield _this7.client.sendStateEvent(_this7.room.roomId, EventType.GroupCallPrefix, _objectSpread(_objectSpread({}, existingStateEvent.getContent()), {}, { "m.terminated": GroupCallTerminationReason.CallEnded }), _this7.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 */ setMicrophoneMuted(muted) { var _this8 = this; return _asyncToGenerator(function* () { // 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 && !(yield _this8.client.getMediaHandler().hasAudioDevice())) { return false; } var sendUpdatesBefore = !muted && _this8.isPtt; // set a timer for the maximum transmit time on PTT calls if (_this8.isPtt) { // Set or clear the max transmit timer if (!muted && _this8.isMicrophoneMuted()) { _this8.transmitTimer = setTimeout(() => { _this8.setMicrophoneMuted(true); }, _this8.pttMaxTransmitTime); } else if (muted && !_this8.isMicrophoneMuted()) { if (_this8.transmitTimer !== null) clearTimeout(_this8.transmitTimer); _this8.transmitTimer = null; } } _this8.forEachCall(call => { var _call$localUsermediaF; return (_call$localUsermediaF = call.localUsermediaFeed) === null || _call$localUsermediaF === void 0 ? void 0 : _call$localUsermediaF.setAudioVideoMuted(muted, null); }); var sendUpdates = /*#__PURE__*/function () { var _ref = _asyncToGenerator(function* () { var updates = []; _this8.forEachCall(call => updates.push(call.sendMetadataUpdate())); yield Promise.all(updates).catch(e => logger.info("GroupCall ".concat(_this8.groupCallId, " setMicrophoneMuted() failed to send some metadata updates"), e)); }); return function sendUpdates() { return _ref.apply(this, arguments); }; }(); if (sendUpdatesBefore) yield sendUpdates(); if (_this8.localCallFeed) { logger.log("GroupCall ".concat(_this8.groupCallId, " setMicrophoneMuted() (streamId=").concat(_this8.localCallFeed.stream.id, ", muted=").concat(muted, ")")); var hasPermission = yield _this8.checkAudioPermissionIfNecessary(muted); if (!hasPermission) { return false; } _this8.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. setTracksEnabled(_this8.localCallFeed.stream.getAudioTracks(), !muted); } else { logger.log("GroupCall ".concat(_this8.groupCallId, " setMicrophoneMuted() no stream muted (muted=").concat(muted, ")")); _this8.initWithAudioMuted = muted; } _this8.forEachCall(call => setTracksEnabled(call.localUsermediaFeed.stream.getAudioTracks(), !muted && _this8.callExpected(call))); _this8.emit(GroupCallEvent.LocalMuteStateChanged, muted, _this8.isLocalVideoMuted()); if (!sendUpdatesBefore) yield sendUpdates(); return true; })(); } /** * If we allow entering a call without a camera and without video, it can happen that the access rights to the * devices have not yet been queried. If a stream does not yet have an audio track, we assume that the rights have * not yet been checked. * * `this.client.getMediaHandler().getUserMediaStream` clones the current stream, so it only wanted to be called when * not Audio Track exists. * As such, this is a compromise, because, the access rights should always be queried before the call. */ checkAudioPermissionIfNecessary(muted) { var _this9 = this; return _asyncToGenerator(function* () { // We needed this here to avoid an error in case user join a call without a device. try { if (!muted && _this9.localCallFeed && !_this9.localCallFeed.hasAudioTrack) { var stream = yield _this9.client.getMediaHandler().getUserMediaStream(true, !_this9.localCallFeed.isVideoMuted()); if ((stream === null || stream === void 0 ? void 0 : stream.getTracks().length) === 0) { // if case permission denied to get a stream stop this here /* istanbul ignore next */ logger.log("GroupCall ".concat(_this9.groupCallId, " setMicrophoneMuted() no device to receive local stream, muted=").concat(muted)); return false; } } } catch (_unused) { /* istanbul ignore next */ logger.log("GroupCall ".concat(_this9.groupCallId, " setMicrophoneMuted() no device or permission to receive local stream, muted=").concat(muted)); return false; } 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 */ setLocalVideoMuted(muted) { var _this0 = this; return _asyncToGenerator(function* () { // 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 && !(yield _this0.client.getMediaHandler().hasVideoDevice())) { return false; } if (_this0.localCallFeed) { /* istanbul ignore next */ logger.log("GroupCall ".concat(_this0.groupCallId, " setLocalVideoMuted() (stream=").concat(_this0.localCallFeed.stream.id, ", muted=").concat(muted, ")")); try { var stream = yield _this0.client.getMediaHandler().getUserMediaStream(true, !muted); yield _this0.updateLocalUsermediaStream(stream); _this0.localCallFeed.setAudioVideoMuted(null, muted); setTracksEnabled(_this0.localCallFeed.stream.getVideoTracks(), !muted); } catch (_unused2) { // No permission to video device /* istanbul ignore next */ logger.log("GroupCall ".concat(_this0.groupCallId, " setLocalVideoMuted() no device or permission to receive local stream, muted=").concat(muted)); return false; } } else { logger.log("GroupCall ".concat(_this0.groupCallId, " setLocalVideoMuted() no stream muted (muted=").concat(muted, ")")); _this0.initWithVideoMuted = muted; } var updates = []; _this0.forEachCall(call => updates.push(call.setLocalVideoMuted(muted))); yield Promise.all(updates); // We setTracksEnabled again, independently from the call doing it // internally, since we might not be expecting the call _this0.forEachCall(call => setTracksEnabled(call.localUsermediaFeed.stream.getVideoTracks(), !muted && _this0.callExpected(call))); _this0.emit(GroupCallEvent.LocalMuteStateChanged, _this0.isMicrophoneMuted(), muted); return true; })(); } setScreensharingEnabled(enabled) { var _arguments2 = arguments, _this1 = this; return _asyncToGenerator(function* () { var opts = _arguments2.length > 1 && _arguments2[1] !== undefined ? _arguments2[1] : {}; if (enabled === _this1.isScreensharing()) { return enabled; } if (enabled) { try { logger.log("GroupCall ".concat(_this1.groupCallId, " setScreensharingEnabled() is asking for screensharing permissions")); var stream = yield _this1.client.getMediaHandler().getScreensharingStream(opts); var _loop = function* _loop(track) { var onTrackEnded = () => { _this1.setScreensharingEnabled(false); track.removeEventListener("ended", onTrackEnded); }; track.addEventListener("ended", onTrackEnded); }; for (var track of stream.getTracks()) { yield* _loop(track); } logger.log("GroupCall ".concat(_this1.groupCallId, " setScreensharingEnabled() granted screensharing permissions. Setting screensharing enabled on all calls")); _this1.localDesktopCapturerSourceId = opts.desktopCapturerSourceId; _this1.localScreenshareFeed = new CallFeed({ client: _this1.client, roomId: _this1.room.roomId, userId: _this1.client.getUserId(), deviceId: _this1.client.getDeviceId(), stream, purpose: SDPStreamMetadataPurpose.Screenshare, audioMuted: false, videoMuted: false }); _this1.addScreenshareFeed(_this1.localScreenshareFeed); _this1.emit(GroupCallEvent.LocalScreenshareStateChanged, true, _this1.localScreenshareFeed, _this1.localDesktopCapturerSourceId); // TODO: handle errors _this1.forEachCall(call => call.pushLocalFeed(_this1.localScreenshareFeed.clone())); return true; } catch (error) { if (opts.throwOnFail) throw error; logger.error("GroupCall ".concat(_this1.groupCallId, " setScreensharingEnabled() enabling screensharing error"), error); _this1.emit(GroupCallEvent.Error, new GroupCallError(GroupCallErrorCode.NoUserMedia, "Failed to get screen-sharing stream: ", error)); return false; } } else { _this1.forEachCall(call => { if (call.localScreensharingFeed) call.removeLocalFeed(call.localScreensharingFeed); }); _this1.client.getMediaHandler().stopScreensharingStream(_this1.localScreenshareFeed.stream); _this1.removeScreenshareFeed(_this1.localScreenshareFeed); _this1.localScreenshareFeed = undefined; _this1.localDesktopCapturerSourceId = undefined; _this1.emit(GroupCallEvent.LocalScreenshareStateChanged, false, undefined, undefined); return false; } })(); } isScreensharing() { return !!this.localScreenshareFeed; } /** * 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) { var localUserId = this.client.getUserId(); var 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() { var _this10 = this; var callsChanged = false; var _loop2 = function _loop2(_userId2) { var _this10$calls$get; var callMap = (_this10$calls$get = _this10.calls.get(_userId2)) !== null && _this10$calls$get !== void 0 ? _this10$calls$get : new Map(); var _loop3 = function _loop3(deviceId) { var prevCall = callMap.get(deviceId); if ((prevCall === null || prevCall === void 0 ? void 0 : prevCall.getOpponentSessionId()) !== participant.sessionId && _this10.wantsOutgoingCall(_userId2, deviceId)) { callsChanged = true; if (prevCall !== undefined) { logger.debug("GroupCall ".concat(_this10.groupCallId, " placeOutgoingCalls() replacing call (userId=").concat(_userId2, ", deviceId=").concat(deviceId, ", callId=").concat(prevCall.callId, ")")); prevCall.hangup(CallErrorCode.NewSession, false); } var newCall = createNewMatrixCall(_this10.client, _this10.room.roomId, { invitee: _userId2, opponentDeviceId: deviceId, opponentSessionId: participant.sessionId, groupCallId: _this10.groupCallId }); if (newCall === null) { logger.error("GroupCall ".concat(_this10.groupCallId, " placeOutgoingCalls() failed to create call (userId=").concat(_userId2, ", device=").concat(deviceId, ")")); callMap.delete(deviceId); } else { _this10.initCall(newCall); callMap.set(deviceId, newCall); logger.debug("GroupCall ".concat(_this10.groupCallId, " placeOutgoingCalls() placing call (userId=").concat(_userId2, ", deviceId=").concat(deviceId, ", sessionId=").concat(participant.sessionId, ")")); newCall.placeCallWithCallFeeds(_this10.getLocalFeeds().map(feed => feed.clone()), participant.screensharing).then(() => { if (_this10.dataChannelsEnabled) { newCall.createDataChannel("datachannel", _this10.dataChannelOptions); } }).catch(e => { logger.warn("GroupCall ".concat(_this10.groupCallId, " placeOutgoingCalls() failed to place call (userId=").concat(_userId2, ")"), e); if (e instanceof CallError && e.code === GroupCallErrorCode.UnknownDevice) { _this10.emit(GroupCallEvent.Error, e); } else { _this10.emit(GroupCallEvent.Error, new GroupCallError(GroupCallErrorCode.PlaceCallFailed, "Failed to place call to ".concat(_userId2))); } newCall.hangup(CallErrorCode.SignallingFailed, false); if (callMap.get(deviceId) === newCall) callMap.delete(deviceId); }); } } }; for (var [deviceId, participant] of participantMap) { _loop3(deviceId); } if (callMap.size > 0) { _this10.calls.set(_userId2, callMap); } else { _this10.calls.delete(_userId2); } }; for (var [{ userId: _userId2 }, participantMap] of this.participants) { _loop2(_userId2); } if (callsChanged) this.emit(GroupCallEvent.CallsChanged, this.calls); } /* * Room Member State */ getMemberStateEvents(userId) { return userId === undefined ? this.room.currentState.getStateEvents(EventType.GroupCallMemberPrefix) : this.room.currentState.getStateEvents(EventType.GroupCallMemberPrefix, userId); } initCall(call) { var opponentMemberId = getCallUserId(call); if (!opponentMemberId) { throw new Error("Cannot init call without user id"); } var onCallFeedsChanged = () => this.onCallFeedsChanged(call); var onCallStateChanged = (state, oldState) => this.onCallStateChanged(call, state, oldState); var onCallHangup = this.onCallHangup; var onCallReplaced = newCall => this.onCallReplaced(call, newCall); var 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(CallEvent.FeedsChanged, onCallFeedsChanged); call.on(CallEvent.State, onCallStateChanged); call.on(CallEvent.Hangup, onCallHangup); call.on(CallEvent.Replaced, onCallReplaced); call.isPtt = this.isPtt; this.reEmitter.reEmit(call, Object.values(CallEvent)); call.initStats(this.getGroupCallStats()); onCallFeedsChanged(); } disposeCall(call, hangupReason) { var opponentMemberId = getCallUserId(call); var opponentDeviceId = call.getOpponentDeviceId(); if (!opponentMemberId) { throw new Error("Cannot dispose call without user id"); } var deviceMap = this.callHandlers.get(opponentMemberId); var { onCallFeedsChanged, onCallStateChanged, onCallHangup, onCallReplaced } = deviceMap.get(opponentDeviceId); call.removeListener(CallEvent.FeedsChanged, onCallFeedsChanged); call.removeListener(CallEvent.State, onCallStateChanged); call.removeListener(CallEvent.Hangup, onCallHangup); call.removeListener(CallEvent.Replaced, onCallReplaced); deviceMap.delete(opponentMemberId); if (deviceMap.size === 0) this.callHandlers.delete(opponentMemberId); if (call.hangupReason === CallErrorCode.Replaced) { return; } var usermediaFeed = this.getUserMediaFeed(opponentMemberId, opponentDeviceId); if (usermediaFeed) { this.removeUserMediaFeed(usermediaFeed); } var screenshareFeed = this.getScreenshareFeed(opponentMemberId, opponentDeviceId); if (screenshareFeed) { this.removeScreenshareFeed(screen