diff --git a/spec/unit/webrtc/callEventHandler.spec.ts b/spec/unit/webrtc/callEventHandler.spec.ts index e9bbeea28..d6012a7a2 100644 --- a/spec/unit/webrtc/callEventHandler.spec.ts +++ b/spec/unit/webrtc/callEventHandler.spec.ts @@ -99,7 +99,7 @@ describe("CallEventHandler", () => { expect(callEventHandler.callEventBuffer.length).toBe(2); expect(callEventHandler.nextSeqByCall.get("123")).toBe(2); - expect(callEventHandler.toDeviceEventBuffers.get("123").length).toBe(1); + expect(callEventHandler.toDeviceEventBuffers.get("123")?.length).toBe(1); const event4 = new MatrixEvent({ type: EventType.CallCandidates, @@ -112,7 +112,7 @@ describe("CallEventHandler", () => { expect(callEventHandler.callEventBuffer.length).toBe(2); expect(callEventHandler.nextSeqByCall.get("123")).toBe(2); - expect(callEventHandler.toDeviceEventBuffers.get("123").length).toBe(2); + expect(callEventHandler.toDeviceEventBuffers.get("123")?.length).toBe(2); const event5 = new MatrixEvent({ type: EventType.CallCandidates, @@ -125,7 +125,7 @@ describe("CallEventHandler", () => { expect(callEventHandler.callEventBuffer.length).toBe(5); expect(callEventHandler.nextSeqByCall.get("123")).toBe(5); - expect(callEventHandler.toDeviceEventBuffers.get("123").length).toBe(0); + expect(callEventHandler.toDeviceEventBuffers.get("123")?.length).toBe(0); }); it("should ignore a call if invite & hangup come within a single sync", () => { @@ -161,7 +161,7 @@ describe("CallEventHandler", () => { it("should ignore non-call events", async () => { // @ts-ignore Mock handleCallEvent is private jest.spyOn(client.callEventHandler, "handleCallEvent"); - jest.spyOn(client, "checkTurnServers").mockReturnValue(undefined); + jest.spyOn(client, "checkTurnServers").mockReturnValue(Promise.resolve(true)); const room = new Room("!room:id", client, "@user:id"); const timelineData: IRoomTimelineData = { timeline: new EventTimeline(new EventTimelineSet(room, {})) }; @@ -186,10 +186,10 @@ describe("CallEventHandler", () => { let room: Room; beforeEach(() => { - room = new Room("!room:id", client, client.getUserId()); + room = new Room("!room:id", client, client.getUserId()!); timelineData = { timeline: new EventTimeline(new EventTimelineSet(room, {})) }; - jest.spyOn(client, "checkTurnServers").mockReturnValue(undefined); + jest.spyOn(client, "checkTurnServers").mockReturnValue(Promise.resolve(true)); jest.spyOn(client, "getRoom").mockReturnValue(room); jest.spyOn(room, "getMember").mockReturnValue({ user_id: client.getUserId() } as unknown as RoomMember); @@ -246,10 +246,10 @@ describe("CallEventHandler", () => { await sync(); expect(incomingCallListener).toHaveBeenCalled(); - expect(call.groupCallId).toBe(GROUP_CALL_ID); + expect(call!.groupCallId).toBe(GROUP_CALL_ID); // @ts-ignore Mock opponentDeviceId is private expect(call.opponentDeviceId).toBe(DEVICE_ID); - expect(call.getOpponentSessionId()).toBe(SESSION_ID); + expect(call!.getOpponentSessionId()).toBe(SESSION_ID); // @ts-ignore Mock onIncomingCall is private expect(groupCall.onIncomingCall).toHaveBeenCalledWith(call); diff --git a/spec/unit/webrtc/groupCall.spec.ts b/spec/unit/webrtc/groupCall.spec.ts index ae95e4c86..75b75c2e4 100644 --- a/spec/unit/webrtc/groupCall.spec.ts +++ b/spec/unit/webrtc/groupCall.spec.ts @@ -116,8 +116,8 @@ class MockCall { setAudioVideoMuted: jest.fn(), stream: new MockMediaStream("stream"), }; - public remoteUsermediaFeed: CallFeed; - public remoteScreensharingFeed: CallFeed; + public remoteUsermediaFeed?: CallFeed; + public remoteScreensharingFeed?: CallFeed; public reject = jest.fn(); public answerWithCallFeeds = jest.fn(); @@ -128,7 +128,7 @@ class MockCall { on = jest.fn(); removeListener = jest.fn(); - getOpponentMember() { + getOpponentMember(): Partial { return { userId: this.opponentUserId, }; @@ -276,7 +276,7 @@ describe('Group Call', function() { await groupCall.initLocalCallFeed(); - const oldStream = groupCall.localCallFeed.stream as unknown as MockMediaStream; + const oldStream = groupCall.localCallFeed?.stream as unknown as MockMediaStream; // arbitrary values, important part is that they're the same afterwards await groupCall.setLocalVideoMuted(true); @@ -286,7 +286,7 @@ describe('Group Call', function() { groupCall.updateLocalUsermediaStream(newStream); - expect(groupCall.localCallFeed.stream).toBe(newStream); + expect(groupCall.localCallFeed?.stream).toBe(newStream); expect(groupCall.isLocalVideoMuted()).toEqual(true); expect(groupCall.isMicrophoneMuted()).toEqual(false); @@ -474,7 +474,7 @@ describe('Group Call', function() { // we should still be muted at this point because the metadata update hasn't sent expect(groupCall.isMicrophoneMuted()).toEqual(true); expect(mockCall.localUsermediaFeed.setAudioVideoMuted).not.toHaveBeenCalled(); - metadataUpdateResolve(); + metadataUpdateResolve!(); await mutePromise; @@ -500,7 +500,7 @@ describe('Group Call', function() { // we should be muted at this point, before the metadata update has been sent expect(groupCall.isMicrophoneMuted()).toEqual(true); expect(mockCall.localUsermediaFeed.setAudioVideoMuted).toHaveBeenCalled(); - metadataUpdateResolve(); + metadataUpdateResolve!(); await mutePromise; @@ -550,7 +550,7 @@ describe('Group Call', function() { groupCall1.onMemberStateChanged(fakeEvent); groupCall2.onMemberStateChanged(fakeEvent); } - return Promise.resolve(null); + return Promise.resolve({ "event_id": "foo" }); }; client1.sendStateEvent.mockImplementation(fakeSendStateEvents); @@ -644,7 +644,7 @@ describe('Group Call', function() { expect(client1.sendToDevice).toHaveBeenCalled(); const oldCall = groupCall1.getCallByUserId(client2.userId); - oldCall.emit(CallEvent.Hangup, oldCall); + oldCall!.emit(CallEvent.Hangup, oldCall!); client1.sendToDevice.mockClear(); @@ -660,11 +660,11 @@ describe('Group Call', function() { // when we placed the call, we could await on enter which waited for the call to // be made. We don't have that luxury now, so first have to wait for the call // to even be created... - let newCall: MatrixCall; + let newCall: MatrixCall | undefined; while ( (newCall = groupCall1.getCallByUserId(client2.userId)) === undefined || newCall.peerConn === undefined || - newCall.callId == oldCall.callId + newCall.callId == oldCall!.callId ) { await flushPromises(); } @@ -704,7 +704,7 @@ describe('Group Call', function() { groupCall1.setMicrophoneMuted(false); groupCall1.setLocalVideoMuted(false); - const call = groupCall1.getCallByUserId(client2.userId); + const call = groupCall1.getCallByUserId(client2.userId)!; call.isMicrophoneMuted = jest.fn().mockReturnValue(true); call.setMicrophoneMuted = jest.fn(); call.isLocalVideoMuted = jest.fn().mockReturnValue(true); @@ -743,13 +743,13 @@ describe('Group Call', function() { it("should mute local audio when calling setMicrophoneMuted()", async () => { const groupCall = await createAndEnterGroupCall(mockClient, room); - groupCall.localCallFeed.setAudioVideoMuted = jest.fn(); + groupCall.localCallFeed!.setAudioVideoMuted = jest.fn(); const setAVMutedArray = groupCall.calls.map(call => { - call.localUsermediaFeed.setAudioVideoMuted = jest.fn(); - return call.localUsermediaFeed.setAudioVideoMuted; + call.localUsermediaFeed!.setAudioVideoMuted = jest.fn(); + return call.localUsermediaFeed!.setAudioVideoMuted; }); - const tracksArray = groupCall.calls.reduce((acc, call) => { - acc.push(...call.localUsermediaStream.getAudioTracks()); + const tracksArray = groupCall.calls.reduce((acc: MediaStreamTrack[], call: MatrixCall) => { + acc.push(...call.localUsermediaStream!.getAudioTracks()); return acc; }, []); const sendMetadataUpdateArray = groupCall.calls.map(call => { @@ -759,8 +759,8 @@ describe('Group Call', function() { await groupCall.setMicrophoneMuted(true); - groupCall.localCallFeed.stream.getAudioTracks().forEach(track => expect(track.enabled).toBe(false)); - expect(groupCall.localCallFeed.setAudioVideoMuted).toHaveBeenCalledWith(true, null); + groupCall.localCallFeed!.stream.getAudioTracks().forEach(track => expect(track.enabled).toBe(false)); + expect(groupCall.localCallFeed!.setAudioVideoMuted).toHaveBeenCalledWith(true, null); setAVMutedArray.forEach(f => expect(f).toHaveBeenCalledWith(true, null)); tracksArray.forEach(track => expect(track.enabled).toBe(false)); sendMetadataUpdateArray.forEach(f => expect(f).toHaveBeenCalled()); @@ -771,14 +771,14 @@ describe('Group Call', function() { it("should mute local video when calling setLocalVideoMuted()", async () => { const groupCall = await createAndEnterGroupCall(mockClient, room); - groupCall.localCallFeed.setAudioVideoMuted = jest.fn(); + groupCall.localCallFeed!.setAudioVideoMuted = jest.fn(); const setAVMutedArray = groupCall.calls.map(call => { - call.localUsermediaFeed.setAudioVideoMuted = jest.fn(); - call.localUsermediaFeed.isVideoMuted = jest.fn().mockReturnValue(true); - return call.localUsermediaFeed.setAudioVideoMuted; + call.localUsermediaFeed!.setAudioVideoMuted = jest.fn(); + call.localUsermediaFeed!.isVideoMuted = jest.fn().mockReturnValue(true); + return call.localUsermediaFeed!.setAudioVideoMuted; }); - const tracksArray = groupCall.calls.reduce((acc, call) => { - acc.push(...call.localUsermediaStream.getVideoTracks()); + const tracksArray = groupCall.calls.reduce((acc: MediaStreamTrack[], call: MatrixCall) => { + acc.push(...call.localUsermediaStream!.getVideoTracks()); return acc; }, []); const sendMetadataUpdateArray = groupCall.calls.map(call => { @@ -788,8 +788,8 @@ describe('Group Call', function() { await groupCall.setLocalVideoMuted(true); - groupCall.localCallFeed.stream.getVideoTracks().forEach(track => expect(track.enabled).toBe(false)); - expect(groupCall.localCallFeed.setAudioVideoMuted).toHaveBeenCalledWith(null, true); + groupCall.localCallFeed!.stream.getVideoTracks().forEach(track => expect(track.enabled).toBe(false)); + expect(groupCall.localCallFeed!.setAudioVideoMuted).toHaveBeenCalledWith(null, true); setAVMutedArray.forEach(f => expect(f).toHaveBeenCalledWith(null, true)); tracksArray.forEach(track => expect(track.enabled).toBe(false)); sendMetadataUpdateArray.forEach(f => expect(f).toHaveBeenCalled()); @@ -827,9 +827,9 @@ describe('Group Call', function() { ])); call.onSDPStreamMetadataChangedReceived(metadataEvent); - const feed = groupCall.getUserMediaFeedByUserId(call.invitee); - expect(feed.isAudioMuted()).toBe(true); - expect(feed.isVideoMuted()).toBe(false); + const feed = groupCall.getUserMediaFeedByUserId(call.invitee!); + expect(feed!.isAudioMuted()).toBe(true); + expect(feed!.isVideoMuted()).toBe(false); groupCall.terminate(); }); @@ -850,9 +850,9 @@ describe('Group Call', function() { ])); call.onSDPStreamMetadataChangedReceived(metadataEvent); - const feed = groupCall.getUserMediaFeedByUserId(call.invitee); - expect(feed.isAudioMuted()).toBe(false); - expect(feed.isVideoMuted()).toBe(true); + const feed = groupCall.getUserMediaFeedByUserId(call.invitee!); + expect(feed!.isAudioMuted()).toBe(false); + expect(feed!.isVideoMuted()).toBe(true); groupCall.terminate(); }); diff --git a/src/webrtc/audioContext.ts b/src/webrtc/audioContext.ts index 10f0dd949..8a9ceb15d 100644 --- a/src/webrtc/audioContext.ts +++ b/src/webrtc/audioContext.ts @@ -38,7 +38,7 @@ export const acquireContext = (): AudioContext => { export const releaseContext = () => { refCount--; if (refCount === 0) { - audioContext.close(); + audioContext?.close(); audioContext = null; } }; diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index 961ddcd56..d3d09ba6f 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -331,7 +331,7 @@ function getTransceiverKey(purpose: SDPStreamMetadataPurpose, kind: TransceiverK * @param {MatrixClient} opts.client The Matrix Client instance to send events to. */ export class MatrixCall extends TypedEventEmitter { - public roomId: string; + public roomId?: string; public callId: string; public invitee?: string; public state = CallState.Fledgling; @@ -361,15 +361,15 @@ export class MatrixCall extends TypedEventEmitter(); private inviteOrAnswerSent = false; - private waitForLocalAVStream: boolean; + private waitForLocalAVStream = false; private successor?: MatrixCall; private opponentMember?: RoomMember; private opponentVersion?: number | string; // The party ID of the other side: undefined if we haven't chosen a partner // yet, null if we have but they didn't send a party ID. - private opponentPartyId: string | null; - private opponentCaps: CallCapabilities; - private iceDisconnectedTimeout: ReturnType; + private opponentPartyId: string | null | undefined; + private opponentCaps?: CallCapabilities; + private iceDisconnectedTimeout?: ReturnType; private inviteTimeout?: ReturnType; private readonly removeTrackListeners = new Map void>(); @@ -384,7 +384,7 @@ export class MatrixCall extends TypedEventEmitter; @@ -399,17 +399,21 @@ export class MatrixCall extends TypedEventEmitter; private callLength = 0; - private opponentDeviceId: string; - private opponentDeviceInfo: DeviceInfo; - private opponentSessionId: string; - public groupCallId: string; + private opponentDeviceId?: string; + private opponentDeviceInfo?: DeviceInfo; + private opponentSessionId?: string; + public groupCallId?: string; constructor(opts: CallOpts) { super(); + this.roomId = opts.roomId; this.invitee = opts.invitee; this.client = opts.client; - this.forceTURN = opts.forceTURN; + + if (!this.client.deviceId) throw new Error("Client must have a device ID to start calls"); + + this.forceTURN = opts.forceTURN ?? false; this.ourPartyId = this.client.deviceId; this.opponentDeviceId = opts.opponentDeviceId; this.opponentSessionId = opts.opponentSessionId; @@ -448,7 +452,7 @@ export class MatrixCall extends TypedEventEmitter t.sender === newSender)); + const newTransciever = this.peerConn!.getTransceivers().find(t => t.sender === newSender); + if (newTransciever) { + this.transceivers.set(tKey, newTransciever); + } else { + logger.warn("Didn't find a matching transceiver after adding track!"); + } } } } @@ -797,8 +811,8 @@ export class MatrixCall extends TypedEventEmitter { stats.push(item); }); @@ -917,8 +931,8 @@ export class MatrixCall extends TypedEventEmitter track.kind === "video"); const sender = this.transceivers.get(getTransceiverKey( SDPStreamMetadataPurpose.Usermedia, "video", - )).sender; - sender?.replaceTrack(track); + ))?.sender; + sender?.replaceTrack(track ?? null); this.client.getMediaHandler().stopScreensharingStream(this.localScreensharingStream!); this.deleteFeedByStream(this.localScreensharingStream!); @@ -1298,8 +1312,13 @@ export class MatrixCall extends TypedEventEmitter t.sender === newSender)); + const newSender = this.peerConn!.addTrack(track, this.localUsermediaStream!); + const newTransciever = this.peerConn!.getTransceivers().find(t => t.sender === newSender); + if (newTransciever) { + this.transceivers.set(tKey, newTransciever); + } else { + logger.warn("Couldn't find matching transceiver for newly added track!"); + } } } } @@ -1436,7 +1455,7 @@ export class MatrixCall extends TypedEventEmitter { const payloadTypeToCodecMap = new Map(); @@ -1570,7 +1589,7 @@ export class MatrixCall extends TypedEventEmitter { - const offer = await this.peerConn.createOffer(); + const offer = await this.peerConn!.createOffer(); this.mungeSdp(offer, getCodecParamMods(this.isPtt)); return offer; } private async createAnswer(): Promise { - const answer = await this.peerConn.createAnswer(); + const answer = await this.peerConn!.createAnswer(); this.mungeSdp(answer, getCodecParamMods(this.isPtt)); return answer; } @@ -1665,7 +1684,7 @@ export class MatrixCall extends TypedEventEmitter { - logger.debug(`Call ${this.callId} ice gathering state changed to ${this.peerConn.iceGatheringState}`); + logger.debug(`Call ${this.callId} ice gathering state changed to ${this.peerConn!.iceGatheringState}`); if (this.peerConn?.iceGatheringState === 'complete') { this.queueCandidate(null); } @@ -1688,10 +1707,12 @@ export class MatrixCall extends TypedEventEmitter void) | null) { this.candidatesEnded = false; this.peerConn!.restartIce(); } else { @@ -2085,7 +2108,7 @@ export class MatrixCall extends TypedEventEmitter { diff --git a/src/webrtc/callEventHandler.ts b/src/webrtc/callEventHandler.ts index 6a3d5339d..7c2be58ff 100644 --- a/src/webrtc/callEventHandler.ts +++ b/src/webrtc/callEventHandler.ts @@ -16,7 +16,7 @@ limitations under the License. import { MatrixEvent } from '../models/event'; import { logger } from '../logger'; -import { CallDirection, CallErrorCode, CallState, createNewMatrixCall, MatrixCall } from './call'; +import { CallDirection, CallError, CallErrorCode, CallState, createNewMatrixCall, MatrixCall } from './call'; import { EventType } from '../@types/event'; import { ClientEvent, MatrixClient } from '../client'; import { MCallAnswer, MCallHangupReject } from "./callEventTypes"; @@ -152,7 +152,7 @@ export class CallEventHandler { this.toDeviceEventBuffers.set(content.call_id, []); } - const buffer = this.toDeviceEventBuffers.get(content.call_id); + const buffer = this.toDeviceEventBuffers.get(content.call_id)!; const index = buffer.findIndex((e) => e.getContent().seq > content.seq); if (index === -1) { @@ -172,7 +172,7 @@ export class CallEventHandler { while (nextEvent && nextEvent.getContent().seq === this.nextSeqByCall.get(callId)) { this.callEventBuffer.push(nextEvent); this.nextSeqByCall.set(callId, nextEvent.getContent().seq + 1); - nextEvent = buffer.shift(); + nextEvent = buffer!.shift(); } } }; @@ -194,7 +194,7 @@ export class CallEventHandler { let opponentDeviceId: string | undefined; - let groupCall: GroupCall; + let groupCall: GroupCall | undefined; if (groupCallId) { groupCall = this.client.groupCallEventHandler.getGroupCallById(groupCallId); @@ -241,7 +241,7 @@ export class CallEventHandler { return; // This invite was meant for another user in the room } - const timeUntilTurnCresExpire = this.client.getTurnServersExpiry() - Date.now(); + const timeUntilTurnCresExpire = (this.client.getTurnServersExpiry() ?? 0) - Date.now(); logger.info("Current turn creds expire in " + timeUntilTurnCresExpire + " ms"); call = createNewMatrixCall( this.client, @@ -267,10 +267,12 @@ export class CallEventHandler { try { await call.initWithInvite(event); } catch (e) { - if (e.code === GroupCallErrorCode.UnknownDevice) { - groupCall?.emit(GroupCallEvent.Error, e); - } else { - logger.error(e); + if (e instanceof CallError) { + if (e.code === GroupCallErrorCode.UnknownDevice) { + groupCall?.emit(GroupCallEvent.Error, e); + } else { + logger.error(e); + } } } this.calls.set(call.callId, call); @@ -292,7 +294,7 @@ export class CallEventHandler { if ( call.roomId === thisCall.roomId && thisCall.direction === CallDirection.Outbound && - call.getOpponentMember().userId === thisCall.invitee && + call.getOpponentMember()?.userId === thisCall.invitee && isCalling ) { existingCall = thisCall; diff --git a/src/webrtc/callFeed.ts b/src/webrtc/callFeed.ts index 170910296..51318c50f 100644 --- a/src/webrtc/callFeed.ts +++ b/src/webrtc/callFeed.ts @@ -27,7 +27,7 @@ const SPEAKING_SAMPLE_COUNT = 8; // samples export interface ICallFeedOpts { client: MatrixClient; - roomId: string; + roomId?: string; userId: string; stream: MediaStream; purpose: SDPStreamMetadataPurpose; @@ -67,7 +67,7 @@ export class CallFeed extends TypedEventEmitter public speakingVolumeSamples: number[]; private client: MatrixClient; - private roomId: string; + private roomId?: string; private audioMuted: boolean; private videoMuted: boolean; private localVolume = 1; @@ -295,8 +295,8 @@ export class CallFeed extends TypedEventEmitter clearTimeout(this.volumeLooperTimeout); this.stream?.removeEventListener("addtrack", this.onAddTrack); if (this.audioContext) { - this.audioContext = null; - this.analyser = null; + this.audioContext = undefined; + this.analyser = undefined; releaseContext(); } this._disposed = true; diff --git a/src/webrtc/groupCall.ts b/src/webrtc/groupCall.ts index eb122a395..150b71ea3 100644 --- a/src/webrtc/groupCall.ts +++ b/src/webrtc/groupCall.ts @@ -9,6 +9,7 @@ import { CallErrorCode, MatrixCall, setTracksEnabled, createNewMatrixCall, + CallError, } from "./call"; import { RoomMember } from "../models/room-member"; import { Room } from "../models/room"; @@ -56,7 +57,7 @@ export type GroupCallEventHandlerMap = { [GroupCallEvent.UserMediaFeedsChanged]: (feeds: CallFeed[]) => void; [GroupCallEvent.ScreenshareFeedsChanged]: (feeds: CallFeed[]) => void; [GroupCallEvent.LocalScreenshareStateChanged]: ( - isScreensharing: boolean, feed: CallFeed, sourceId: string, + isScreensharing: boolean, feed?: CallFeed, sourceId?: string, ) => void; [GroupCallEvent.LocalMuteStateChanged]: (audioMuted: boolean, videoMuted: boolean) => void; [GroupCallEvent.ParticipantsChanged]: (participants: RoomMember[]) => void; @@ -136,7 +137,7 @@ export enum GroupCallState { interface ICallHandlers { onCallFeedsChanged: (feeds: CallFeed[]) => void; - onCallStateChanged: (state: CallState, oldState: CallState) => void; + onCallStateChanged: (state: CallState, oldState: CallState | undefined) => void; onCallHangup: (call: MatrixCall) => void; onCallReplaced: (newCall: MatrixCall) => void; } @@ -232,7 +233,7 @@ export class GroupCall extends TypedEventEmitter< } public getLocalFeeds(): CallFeed[] { - const feeds = []; + const feeds: CallFeed[] = []; if (this.localCallFeed) feeds.push(this.localCallFeed); if (this.localScreenshareFeed) feeds.push(this.localScreenshareFeed); @@ -311,11 +312,11 @@ export class GroupCall extends TypedEventEmitter< await this.initLocalCallFeed(); } - this.addParticipant(this.room.getMember(this.client.getUserId())); + this.addParticipant(this.room.getMember(this.client.getUserId()!)!); await this.sendMemberStateEvent(); - this.activeSpeaker = null; + this.activeSpeaker = undefined; this.setState(GroupCallState.Entered); @@ -343,7 +344,7 @@ export class GroupCall extends TypedEventEmitter< private dispose() { if (this.localCallFeed) { this.removeUserMediaFeed(this.localCallFeed); - this.localCallFeed = null; + this.localCallFeed = undefined; } if (this.localScreenshareFeed) { @@ -359,7 +360,7 @@ export class GroupCall extends TypedEventEmitter< return; } - this.removeParticipant(this.room.getMember(this.client.getUserId())); + this.removeParticipant(this.room.getMember(this.client.getUserId()!)!); this.removeMemberStateEvent(); @@ -367,7 +368,7 @@ export class GroupCall extends TypedEventEmitter< this.removeCall(this.calls[this.calls.length - 1], CallErrorCode.UserHangup); } - this.activeSpeaker = null; + this.activeSpeaker = undefined; clearTimeout(this.activeSpeakerLoopTimeout); this.retryCallCounts.clear(); @@ -470,7 +471,7 @@ export class GroupCall extends TypedEventEmitter< this.setMicrophoneMuted(true); }, this.pttMaxTransmitTime); } else if (muted && !this.isMicrophoneMuted()) { - clearTimeout(this.transmitTimer); + if (this.transmitTimer !== null) clearTimeout(this.transmitTimer); this.transmitTimer = null; } } @@ -502,7 +503,7 @@ export class GroupCall extends TypedEventEmitter< } for (const call of this.calls) { - setTracksEnabled(call.localUsermediaFeed.stream.getAudioTracks(), !muted); + setTracksEnabled(call.localUsermediaFeed!.stream.getAudioTracks(), !muted); } this.emit(GroupCallEvent.LocalMuteStateChanged, muted, this.isLocalVideoMuted()); @@ -576,7 +577,7 @@ export class GroupCall extends TypedEventEmitter< this.localScreenshareFeed = new CallFeed({ client: this.client, roomId: this.room.roomId, - userId: this.client.getUserId(), + userId: this.client.getUserId()!, stream, purpose: SDPStreamMetadataPurpose.Screenshare, audioMuted: false, @@ -593,7 +594,7 @@ export class GroupCall extends TypedEventEmitter< // TODO: handle errors await Promise.all(this.calls.map(call => call.pushLocalFeed( - this.localScreenshareFeed.clone(), + this.localScreenshareFeed!.clone(), ))); await this.sendMemberStateEvent(); @@ -603,7 +604,10 @@ export class GroupCall extends TypedEventEmitter< if (opts.throwOnFail) throw error; logger.error("Enabling screensharing error", error); this.emit(GroupCallEvent.Error, - new GroupCallError(GroupCallErrorCode.NoUserMedia, "Failed to get screen-sharing stream: ", error), + new GroupCallError( + GroupCallErrorCode.NoUserMedia, + "Failed to get screen-sharing stream: ", error as Error, + ), ); return false; } @@ -611,8 +615,8 @@ export class GroupCall extends TypedEventEmitter< await Promise.all(this.calls.map(call => { if (call.localScreensharingFeed) call.removeLocalFeed(call.localScreensharingFeed); })); - this.client.getMediaHandler().stopScreensharingStream(this.localScreenshareFeed.stream); - this.removeScreenshareFeed(this.localScreenshareFeed); + this.client.getMediaHandler().stopScreensharingStream(this.localScreenshareFeed!.stream); + this.removeScreenshareFeed(this.localScreenshareFeed!); this.localScreenshareFeed = undefined; this.localDesktopCapturerSourceId = undefined; await this.sendMemberStateEvent(); @@ -652,8 +656,8 @@ export class GroupCall extends TypedEventEmitter< return; } - const opponentMemberId = newCall.getOpponentMember().userId; - const existingCall = this.getCallByUserId(opponentMemberId); + const opponentMemberId = newCall.getOpponentMember()?.userId; + const existingCall = opponentMemberId ? this.getCallByUserId(opponentMemberId) : null; if (existingCall && existingCall.callId === newCall.callId) { return; @@ -709,7 +713,7 @@ export class GroupCall extends TypedEventEmitter< const res = await send(); // Clear the old interval first, so that it isn't forgot - clearInterval(this.resendMemberStateTimer); + if (this.resendMemberStateTimer !== null) clearInterval(this.resendMemberStateTimer); // Resend the state event every so often so it doesn't become stale this.resendMemberStateTimer = setInterval(async () => { logger.log("Resending call member state"); @@ -720,13 +724,13 @@ export class GroupCall extends TypedEventEmitter< } private async removeMemberStateEvent(): Promise { - clearInterval(this.resendMemberStateTimer); + if (this.resendMemberStateTimer !== null) clearInterval(this.resendMemberStateTimer); this.resendMemberStateTimer = null; return await this.updateMemberCallState(undefined); } private async updateMemberCallState(memberCallState?: IGroupCallRoomMemberCallState): Promise { - const localUserId = this.client.getUserId(); + const localUserId = this.client.getUserId()!; const memberState = this.getMemberStateEvents(localUserId)?.getContent(); @@ -766,7 +770,7 @@ export class GroupCall extends TypedEventEmitter< // The member events may be received for another room, which we will ignore. if (event.getRoomId() !== this.room.roomId) return; - const member = this.room.getMember(event.getStateKey()); + const member = this.room.getMember(event.getStateKey()!); if (!member) { logger.warn(`Couldn't find room member for ${event.getStateKey()}: ignoring member state event!`); return; @@ -816,7 +820,7 @@ export class GroupCall extends TypedEventEmitter< }, content["m.expires_ts"] - Date.now())); // Don't process your own member. - const localUserId = this.client.getUserId(); + const localUserId = this.client.getUserId()!; if (member.userId === localUserId) { return; @@ -860,6 +864,11 @@ export class GroupCall extends TypedEventEmitter< }, ); + if (!newCall) { + logger.error("Failed to create call!"); + return; + } + if (existingCall) { logger.debug(`Replacing call ${existingCall.callId} to ${member.userId} with ${newCall.callId}`); this.replaceCall(existingCall, newCall, CallErrorCode.NewSession); @@ -884,7 +893,7 @@ export class GroupCall extends TypedEventEmitter< ); } catch (e) { logger.warn(`Failed to place call to ${member.userId}!`, e); - if (e.code === GroupCallErrorCode.UnknownDevice) { + if (e instanceof CallError && e.code === GroupCallErrorCode.UnknownDevice) { this.emit(GroupCallEvent.Error, e); } else { this.emit( @@ -904,7 +913,7 @@ export class GroupCall extends TypedEventEmitter< } }; - public getDeviceForMember(userId: string): IGroupCallRoomMemberDevice { + public getDeviceForMember(userId: string): IGroupCallRoomMemberDevice | undefined { const memberStateEvent = this.getMemberStateEvents(userId); if (!memberStateEvent) { @@ -931,7 +940,7 @@ export class GroupCall extends TypedEventEmitter< private onRetryCallLoop = () => { for (const event of this.getMemberStateEvents()) { - const memberId = event.getStateKey(); + const memberId = event.getStateKey()!; const existingCall = this.calls.find((call) => getCallUserId(call) === memberId); const retryCallCount = this.retryCallCounts.get(memberId) || 0; @@ -948,7 +957,7 @@ export class GroupCall extends TypedEventEmitter< * Call Event Handlers */ - public getCallByUserId(userId: string): MatrixCall { + public getCallByUserId(userId: string): MatrixCall | undefined { return this.calls.find((call) => getCallUserId(call) === userId); } @@ -996,7 +1005,7 @@ export class GroupCall extends TypedEventEmitter< const onCallFeedsChanged = () => this.onCallFeedsChanged(call); const onCallStateChanged = - (state: CallState, oldState: CallState) => this.onCallStateChanged(call, state, oldState); + (state: CallState, oldState: CallState | undefined) => this.onCallStateChanged(call, state, oldState); const onCallHangup = this.onCallHangup; const onCallReplaced = (newCall: MatrixCall) => this.replaceCall(call, newCall); @@ -1029,7 +1038,7 @@ export class GroupCall extends TypedEventEmitter< onCallStateChanged, onCallHangup, onCallReplaced, - } = this.callHandlers.get(opponentMemberId); + } = this.callHandlers.get(opponentMemberId)!; call.removeListener(CallEvent.FeedsChanged, onCallFeedsChanged); call.removeListener(CallEvent.State, onCallStateChanged); @@ -1095,8 +1104,8 @@ export class GroupCall extends TypedEventEmitter< } }; - private onCallStateChanged = (call: MatrixCall, state: CallState, _oldState: CallState) => { - const audioMuted = this.localCallFeed.isAudioMuted(); + private onCallStateChanged = (call: MatrixCall, state: CallState, _oldState: CallState | undefined) => { + const audioMuted = this.localCallFeed!.isAudioMuted(); if ( call.localUsermediaStream && @@ -1105,7 +1114,7 @@ export class GroupCall extends TypedEventEmitter< call.setMicrophoneMuted(audioMuted); } - const videoMuted = this.localCallFeed.isVideoMuted(); + const videoMuted = this.localCallFeed!.isVideoMuted(); if ( call.localUsermediaStream && @@ -1115,7 +1124,7 @@ export class GroupCall extends TypedEventEmitter< } if (state === CallState.Connected) { - this.retryCallCounts.delete(getCallUserId(call)); + this.retryCallCounts.delete(getCallUserId(call)!); } }; @@ -1177,8 +1186,8 @@ export class GroupCall extends TypedEventEmitter< } private onActiveSpeakerLoop = () => { - let topAvg: number; - let nextActiveSpeaker: string; + let topAvg: number | undefined = undefined; + let nextActiveSpeaker: string | undefined = undefined; for (const callFeed of this.userMediaFeeds) { if (callFeed.userId === this.client.getUserId() && this.userMediaFeeds.length > 1) { @@ -1200,7 +1209,7 @@ export class GroupCall extends TypedEventEmitter< } } - if (nextActiveSpeaker && this.activeSpeaker !== nextActiveSpeaker && topAvg > SPEAKING_THRESHOLD) { + if (nextActiveSpeaker && this.activeSpeaker !== nextActiveSpeaker && topAvg && topAvg > SPEAKING_THRESHOLD) { this.activeSpeaker = nextActiveSpeaker; this.emit(GroupCallEvent.ActiveSpeakerChanged, this.activeSpeaker); } diff --git a/src/webrtc/groupCallEventHandler.ts b/src/webrtc/groupCallEventHandler.ts index e38d7f2b4..86df72289 100644 --- a/src/webrtc/groupCallEventHandler.ts +++ b/src/webrtc/groupCallEventHandler.ts @@ -91,7 +91,7 @@ export class GroupCallEventHandler { } private getRoomDeferred(roomId: string): RoomDeferred { - let deferred: RoomDeferred = this.roomDeferreds.get(roomId); + let deferred = this.roomDeferreds.get(roomId); if (deferred === undefined) { let resolveFunc: () => void; deferred = { @@ -99,7 +99,7 @@ export class GroupCallEventHandler { resolveFunc = resolve; }), }; - deferred.resolve = resolveFunc; + deferred.resolve = resolveFunc!; this.roomDeferreds.set(roomId, deferred); } @@ -110,7 +110,7 @@ export class GroupCallEventHandler { return this.getRoomDeferred(roomId).prom; } - public getGroupCallById(groupCallId: string): GroupCall { + public getGroupCallById(groupCallId: string): GroupCall | undefined { return [...this.groupCalls.values()].find((groupCall) => groupCall.groupCallId === groupCallId); } @@ -135,7 +135,7 @@ export class GroupCallEventHandler { } logger.info("Group call event handler processed room", room.roomId); - this.getRoomDeferred(room.roomId).resolve(); + this.getRoomDeferred(room.roomId).resolve!(); } private createGroupCallFromRoomStateEvent(event: MatrixEvent): GroupCall | undefined { diff --git a/src/webrtc/mediaHandler.ts b/src/webrtc/mediaHandler.ts index fa337098f..bff633018 100644 --- a/src/webrtc/mediaHandler.ts +++ b/src/webrtc/mediaHandler.ts @@ -228,8 +228,8 @@ export class MediaHandler extends TypedEventEmitter< this.localUserMediaStream = stream; } } else { - stream = this.localUserMediaStream.clone(); - logger.log(`mediaHandler clone userMediaStream ${this.localUserMediaStream.id} new stream ${ + stream = this.localUserMediaStream!.clone(); + logger.log(`mediaHandler clone userMediaStream ${this.localUserMediaStream?.id} new stream ${ stream.id} shouldRequestAudio ${shouldRequestAudio} shouldRequestVideo ${shouldRequestVideo}`); if (!shouldRequestAudio) { @@ -282,12 +282,11 @@ export class MediaHandler extends TypedEventEmitter< * @param reusable is allowed to be reused by the MediaHandler * @returns {MediaStream} based on passed parameters */ - public async getScreensharingStream(opts: IScreensharingOpts = {}, reusable = true): Promise { + public async getScreensharingStream(opts: IScreensharingOpts = {}, reusable = true): Promise { let stream: MediaStream; if (this.screensharingStreams.length === 0) { const screenshareConstraints = this.getScreenshareContraints(opts); - if (!screenshareConstraints) return null; if (opts.desktopCapturerSourceId) { // We are using Electron @@ -385,7 +384,7 @@ export class MediaHandler extends TypedEventEmitter< if (desktopCapturerSourceId) { logger.debug("Using desktop capturer source", desktopCapturerSourceId); return { - audio, + audio: audio ?? false, video: { mandatory: { chromeMediaSource: "desktop", @@ -396,7 +395,7 @@ export class MediaHandler extends TypedEventEmitter< } else { logger.debug("Not using desktop capturer source"); return { - audio, + audio: audio ?? false, video: true, }; }