1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-08-09 10:22:46 +03:00

TS strict mode compliance in the call / groupcall code (#2805)

* TS strict mode compliance in the call / groupcall code

* Also the test

* Fix initOpponentCrypto

to not panic if it doesn't actually need to init crypto
This commit is contained in:
David Baker
2022-10-26 11:45:03 +01:00
committed by GitHub
parent 450ff00c3e
commit c374ba2367
9 changed files with 186 additions and 153 deletions

View File

@@ -99,7 +99,7 @@ describe("CallEventHandler", () => {
expect(callEventHandler.callEventBuffer.length).toBe(2); expect(callEventHandler.callEventBuffer.length).toBe(2);
expect(callEventHandler.nextSeqByCall.get("123")).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({ const event4 = new MatrixEvent({
type: EventType.CallCandidates, type: EventType.CallCandidates,
@@ -112,7 +112,7 @@ describe("CallEventHandler", () => {
expect(callEventHandler.callEventBuffer.length).toBe(2); expect(callEventHandler.callEventBuffer.length).toBe(2);
expect(callEventHandler.nextSeqByCall.get("123")).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({ const event5 = new MatrixEvent({
type: EventType.CallCandidates, type: EventType.CallCandidates,
@@ -125,7 +125,7 @@ describe("CallEventHandler", () => {
expect(callEventHandler.callEventBuffer.length).toBe(5); expect(callEventHandler.callEventBuffer.length).toBe(5);
expect(callEventHandler.nextSeqByCall.get("123")).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", () => { 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 () => { it("should ignore non-call events", async () => {
// @ts-ignore Mock handleCallEvent is private // @ts-ignore Mock handleCallEvent is private
jest.spyOn(client.callEventHandler, "handleCallEvent"); 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 room = new Room("!room:id", client, "@user:id");
const timelineData: IRoomTimelineData = { timeline: new EventTimeline(new EventTimelineSet(room, {})) }; const timelineData: IRoomTimelineData = { timeline: new EventTimeline(new EventTimelineSet(room, {})) };
@@ -186,10 +186,10 @@ describe("CallEventHandler", () => {
let room: Room; let room: Room;
beforeEach(() => { beforeEach(() => {
room = new Room("!room:id", client, client.getUserId()); room = new Room("!room:id", client, client.getUserId()!);
timelineData = { timeline: new EventTimeline(new EventTimelineSet(room, {})) }; 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(client, "getRoom").mockReturnValue(room);
jest.spyOn(room, "getMember").mockReturnValue({ user_id: client.getUserId() } as unknown as RoomMember); jest.spyOn(room, "getMember").mockReturnValue({ user_id: client.getUserId() } as unknown as RoomMember);
@@ -246,10 +246,10 @@ describe("CallEventHandler", () => {
await sync(); await sync();
expect(incomingCallListener).toHaveBeenCalled(); expect(incomingCallListener).toHaveBeenCalled();
expect(call.groupCallId).toBe(GROUP_CALL_ID); expect(call!.groupCallId).toBe(GROUP_CALL_ID);
// @ts-ignore Mock opponentDeviceId is private // @ts-ignore Mock opponentDeviceId is private
expect(call.opponentDeviceId).toBe(DEVICE_ID); expect(call.opponentDeviceId).toBe(DEVICE_ID);
expect(call.getOpponentSessionId()).toBe(SESSION_ID); expect(call!.getOpponentSessionId()).toBe(SESSION_ID);
// @ts-ignore Mock onIncomingCall is private // @ts-ignore Mock onIncomingCall is private
expect(groupCall.onIncomingCall).toHaveBeenCalledWith(call); expect(groupCall.onIncomingCall).toHaveBeenCalledWith(call);

View File

@@ -116,8 +116,8 @@ class MockCall {
setAudioVideoMuted: jest.fn<void, [boolean, boolean]>(), setAudioVideoMuted: jest.fn<void, [boolean, boolean]>(),
stream: new MockMediaStream("stream"), stream: new MockMediaStream("stream"),
}; };
public remoteUsermediaFeed: CallFeed; public remoteUsermediaFeed?: CallFeed;
public remoteScreensharingFeed: CallFeed; public remoteScreensharingFeed?: CallFeed;
public reject = jest.fn<void, []>(); public reject = jest.fn<void, []>();
public answerWithCallFeeds = jest.fn<void, [CallFeed[]]>(); public answerWithCallFeeds = jest.fn<void, [CallFeed[]]>();
@@ -128,7 +128,7 @@ class MockCall {
on = jest.fn(); on = jest.fn();
removeListener = jest.fn(); removeListener = jest.fn();
getOpponentMember() { getOpponentMember(): Partial<RoomMember> {
return { return {
userId: this.opponentUserId, userId: this.opponentUserId,
}; };
@@ -276,7 +276,7 @@ describe('Group Call', function() {
await groupCall.initLocalCallFeed(); 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 // arbitrary values, important part is that they're the same afterwards
await groupCall.setLocalVideoMuted(true); await groupCall.setLocalVideoMuted(true);
@@ -286,7 +286,7 @@ describe('Group Call', function() {
groupCall.updateLocalUsermediaStream(newStream); groupCall.updateLocalUsermediaStream(newStream);
expect(groupCall.localCallFeed.stream).toBe(newStream); expect(groupCall.localCallFeed?.stream).toBe(newStream);
expect(groupCall.isLocalVideoMuted()).toEqual(true); expect(groupCall.isLocalVideoMuted()).toEqual(true);
expect(groupCall.isMicrophoneMuted()).toEqual(false); 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 // we should still be muted at this point because the metadata update hasn't sent
expect(groupCall.isMicrophoneMuted()).toEqual(true); expect(groupCall.isMicrophoneMuted()).toEqual(true);
expect(mockCall.localUsermediaFeed.setAudioVideoMuted).not.toHaveBeenCalled(); expect(mockCall.localUsermediaFeed.setAudioVideoMuted).not.toHaveBeenCalled();
metadataUpdateResolve(); metadataUpdateResolve!();
await mutePromise; await mutePromise;
@@ -500,7 +500,7 @@ describe('Group Call', function() {
// we should be muted at this point, before the metadata update has been sent // we should be muted at this point, before the metadata update has been sent
expect(groupCall.isMicrophoneMuted()).toEqual(true); expect(groupCall.isMicrophoneMuted()).toEqual(true);
expect(mockCall.localUsermediaFeed.setAudioVideoMuted).toHaveBeenCalled(); expect(mockCall.localUsermediaFeed.setAudioVideoMuted).toHaveBeenCalled();
metadataUpdateResolve(); metadataUpdateResolve!();
await mutePromise; await mutePromise;
@@ -550,7 +550,7 @@ describe('Group Call', function() {
groupCall1.onMemberStateChanged(fakeEvent); groupCall1.onMemberStateChanged(fakeEvent);
groupCall2.onMemberStateChanged(fakeEvent); groupCall2.onMemberStateChanged(fakeEvent);
} }
return Promise.resolve(null); return Promise.resolve({ "event_id": "foo" });
}; };
client1.sendStateEvent.mockImplementation(fakeSendStateEvents); client1.sendStateEvent.mockImplementation(fakeSendStateEvents);
@@ -644,7 +644,7 @@ describe('Group Call', function() {
expect(client1.sendToDevice).toHaveBeenCalled(); expect(client1.sendToDevice).toHaveBeenCalled();
const oldCall = groupCall1.getCallByUserId(client2.userId); const oldCall = groupCall1.getCallByUserId(client2.userId);
oldCall.emit(CallEvent.Hangup, oldCall); oldCall!.emit(CallEvent.Hangup, oldCall!);
client1.sendToDevice.mockClear(); 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 // 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 // be made. We don't have that luxury now, so first have to wait for the call
// to even be created... // to even be created...
let newCall: MatrixCall; let newCall: MatrixCall | undefined;
while ( while (
(newCall = groupCall1.getCallByUserId(client2.userId)) === undefined || (newCall = groupCall1.getCallByUserId(client2.userId)) === undefined ||
newCall.peerConn === undefined || newCall.peerConn === undefined ||
newCall.callId == oldCall.callId newCall.callId == oldCall!.callId
) { ) {
await flushPromises(); await flushPromises();
} }
@@ -704,7 +704,7 @@ describe('Group Call', function() {
groupCall1.setMicrophoneMuted(false); groupCall1.setMicrophoneMuted(false);
groupCall1.setLocalVideoMuted(false); groupCall1.setLocalVideoMuted(false);
const call = groupCall1.getCallByUserId(client2.userId); const call = groupCall1.getCallByUserId(client2.userId)!;
call.isMicrophoneMuted = jest.fn().mockReturnValue(true); call.isMicrophoneMuted = jest.fn().mockReturnValue(true);
call.setMicrophoneMuted = jest.fn(); call.setMicrophoneMuted = jest.fn();
call.isLocalVideoMuted = jest.fn().mockReturnValue(true); call.isLocalVideoMuted = jest.fn().mockReturnValue(true);
@@ -743,13 +743,13 @@ describe('Group Call', function() {
it("should mute local audio when calling setMicrophoneMuted()", async () => { it("should mute local audio when calling setMicrophoneMuted()", async () => {
const groupCall = await createAndEnterGroupCall(mockClient, room); const groupCall = await createAndEnterGroupCall(mockClient, room);
groupCall.localCallFeed.setAudioVideoMuted = jest.fn(); groupCall.localCallFeed!.setAudioVideoMuted = jest.fn();
const setAVMutedArray = groupCall.calls.map(call => { const setAVMutedArray = groupCall.calls.map(call => {
call.localUsermediaFeed.setAudioVideoMuted = jest.fn(); call.localUsermediaFeed!.setAudioVideoMuted = jest.fn();
return call.localUsermediaFeed.setAudioVideoMuted; return call.localUsermediaFeed!.setAudioVideoMuted;
}); });
const tracksArray = groupCall.calls.reduce((acc, call) => { const tracksArray = groupCall.calls.reduce((acc: MediaStreamTrack[], call: MatrixCall) => {
acc.push(...call.localUsermediaStream.getAudioTracks()); acc.push(...call.localUsermediaStream!.getAudioTracks());
return acc; return acc;
}, []); }, []);
const sendMetadataUpdateArray = groupCall.calls.map(call => { const sendMetadataUpdateArray = groupCall.calls.map(call => {
@@ -759,8 +759,8 @@ describe('Group Call', function() {
await groupCall.setMicrophoneMuted(true); await groupCall.setMicrophoneMuted(true);
groupCall.localCallFeed.stream.getAudioTracks().forEach(track => expect(track.enabled).toBe(false)); groupCall.localCallFeed!.stream.getAudioTracks().forEach(track => expect(track.enabled).toBe(false));
expect(groupCall.localCallFeed.setAudioVideoMuted).toHaveBeenCalledWith(true, null); expect(groupCall.localCallFeed!.setAudioVideoMuted).toHaveBeenCalledWith(true, null);
setAVMutedArray.forEach(f => expect(f).toHaveBeenCalledWith(true, null)); setAVMutedArray.forEach(f => expect(f).toHaveBeenCalledWith(true, null));
tracksArray.forEach(track => expect(track.enabled).toBe(false)); tracksArray.forEach(track => expect(track.enabled).toBe(false));
sendMetadataUpdateArray.forEach(f => expect(f).toHaveBeenCalled()); sendMetadataUpdateArray.forEach(f => expect(f).toHaveBeenCalled());
@@ -771,14 +771,14 @@ describe('Group Call', function() {
it("should mute local video when calling setLocalVideoMuted()", async () => { it("should mute local video when calling setLocalVideoMuted()", async () => {
const groupCall = await createAndEnterGroupCall(mockClient, room); const groupCall = await createAndEnterGroupCall(mockClient, room);
groupCall.localCallFeed.setAudioVideoMuted = jest.fn(); groupCall.localCallFeed!.setAudioVideoMuted = jest.fn();
const setAVMutedArray = groupCall.calls.map(call => { const setAVMutedArray = groupCall.calls.map(call => {
call.localUsermediaFeed.setAudioVideoMuted = jest.fn(); call.localUsermediaFeed!.setAudioVideoMuted = jest.fn();
call.localUsermediaFeed.isVideoMuted = jest.fn().mockReturnValue(true); call.localUsermediaFeed!.isVideoMuted = jest.fn().mockReturnValue(true);
return call.localUsermediaFeed.setAudioVideoMuted; return call.localUsermediaFeed!.setAudioVideoMuted;
}); });
const tracksArray = groupCall.calls.reduce((acc, call) => { const tracksArray = groupCall.calls.reduce((acc: MediaStreamTrack[], call: MatrixCall) => {
acc.push(...call.localUsermediaStream.getVideoTracks()); acc.push(...call.localUsermediaStream!.getVideoTracks());
return acc; return acc;
}, []); }, []);
const sendMetadataUpdateArray = groupCall.calls.map(call => { const sendMetadataUpdateArray = groupCall.calls.map(call => {
@@ -788,8 +788,8 @@ describe('Group Call', function() {
await groupCall.setLocalVideoMuted(true); await groupCall.setLocalVideoMuted(true);
groupCall.localCallFeed.stream.getVideoTracks().forEach(track => expect(track.enabled).toBe(false)); groupCall.localCallFeed!.stream.getVideoTracks().forEach(track => expect(track.enabled).toBe(false));
expect(groupCall.localCallFeed.setAudioVideoMuted).toHaveBeenCalledWith(null, true); expect(groupCall.localCallFeed!.setAudioVideoMuted).toHaveBeenCalledWith(null, true);
setAVMutedArray.forEach(f => expect(f).toHaveBeenCalledWith(null, true)); setAVMutedArray.forEach(f => expect(f).toHaveBeenCalledWith(null, true));
tracksArray.forEach(track => expect(track.enabled).toBe(false)); tracksArray.forEach(track => expect(track.enabled).toBe(false));
sendMetadataUpdateArray.forEach(f => expect(f).toHaveBeenCalled()); sendMetadataUpdateArray.forEach(f => expect(f).toHaveBeenCalled());
@@ -827,9 +827,9 @@ describe('Group Call', function() {
])); ]));
call.onSDPStreamMetadataChangedReceived(metadataEvent); call.onSDPStreamMetadataChangedReceived(metadataEvent);
const feed = groupCall.getUserMediaFeedByUserId(call.invitee); const feed = groupCall.getUserMediaFeedByUserId(call.invitee!);
expect(feed.isAudioMuted()).toBe(true); expect(feed!.isAudioMuted()).toBe(true);
expect(feed.isVideoMuted()).toBe(false); expect(feed!.isVideoMuted()).toBe(false);
groupCall.terminate(); groupCall.terminate();
}); });
@@ -850,9 +850,9 @@ describe('Group Call', function() {
])); ]));
call.onSDPStreamMetadataChangedReceived(metadataEvent); call.onSDPStreamMetadataChangedReceived(metadataEvent);
const feed = groupCall.getUserMediaFeedByUserId(call.invitee); const feed = groupCall.getUserMediaFeedByUserId(call.invitee!);
expect(feed.isAudioMuted()).toBe(false); expect(feed!.isAudioMuted()).toBe(false);
expect(feed.isVideoMuted()).toBe(true); expect(feed!.isVideoMuted()).toBe(true);
groupCall.terminate(); groupCall.terminate();
}); });

View File

@@ -38,7 +38,7 @@ export const acquireContext = (): AudioContext => {
export const releaseContext = () => { export const releaseContext = () => {
refCount--; refCount--;
if (refCount === 0) { if (refCount === 0) {
audioContext.close(); audioContext?.close();
audioContext = null; audioContext = null;
} }
}; };

View File

@@ -331,7 +331,7 @@ function getTransceiverKey(purpose: SDPStreamMetadataPurpose, kind: TransceiverK
* @param {MatrixClient} opts.client The Matrix Client instance to send events to. * @param {MatrixClient} opts.client The Matrix Client instance to send events to.
*/ */
export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap> { export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap> {
public roomId: string; public roomId?: string;
public callId: string; public callId: string;
public invitee?: string; public invitee?: string;
public state = CallState.Fledgling; public state = CallState.Fledgling;
@@ -361,15 +361,15 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
private transceivers = new Map<TransceiverKey, RTCRtpTransceiver>(); private transceivers = new Map<TransceiverKey, RTCRtpTransceiver>();
private inviteOrAnswerSent = false; private inviteOrAnswerSent = false;
private waitForLocalAVStream: boolean; private waitForLocalAVStream = false;
private successor?: MatrixCall; private successor?: MatrixCall;
private opponentMember?: RoomMember; private opponentMember?: RoomMember;
private opponentVersion?: number | string; private opponentVersion?: number | string;
// The party ID of the other side: undefined if we haven't chosen a partner // 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. // yet, null if we have but they didn't send a party ID.
private opponentPartyId: string | null; private opponentPartyId: string | null | undefined;
private opponentCaps: CallCapabilities; private opponentCaps?: CallCapabilities;
private iceDisconnectedTimeout: ReturnType<typeof setTimeout>; private iceDisconnectedTimeout?: ReturnType<typeof setTimeout>;
private inviteTimeout?: ReturnType<typeof setTimeout>; private inviteTimeout?: ReturnType<typeof setTimeout>;
private readonly removeTrackListeners = new Map<MediaStream, () => void>(); private readonly removeTrackListeners = new Map<MediaStream, () => void>();
@@ -384,7 +384,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
// Perfect negotiation state: https://www.w3.org/TR/webrtc/#perfect-negotiation-example // Perfect negotiation state: https://www.w3.org/TR/webrtc/#perfect-negotiation-example
private makingOffer = false; private makingOffer = false;
private ignoreOffer: boolean; private ignoreOffer = false;
private responsePromiseChain?: Promise<void>; private responsePromiseChain?: Promise<void>;
@@ -399,17 +399,21 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
private callLengthInterval?: ReturnType<typeof setInterval>; private callLengthInterval?: ReturnType<typeof setInterval>;
private callLength = 0; private callLength = 0;
private opponentDeviceId: string; private opponentDeviceId?: string;
private opponentDeviceInfo: DeviceInfo; private opponentDeviceInfo?: DeviceInfo;
private opponentSessionId: string; private opponentSessionId?: string;
public groupCallId: string; public groupCallId?: string;
constructor(opts: CallOpts) { constructor(opts: CallOpts) {
super(); super();
this.roomId = opts.roomId; this.roomId = opts.roomId;
this.invitee = opts.invitee; this.invitee = opts.invitee;
this.client = opts.client; 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.ourPartyId = this.client.deviceId;
this.opponentDeviceId = opts.opponentDeviceId; this.opponentDeviceId = opts.opponentDeviceId;
this.opponentSessionId = opts.opponentSessionId; this.opponentSessionId = opts.opponentSessionId;
@@ -448,7 +452,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
* @param label A human readable label for this datachannel * @param label A human readable label for this datachannel
* @param options An object providing configuration options for the data channel. * @param options An object providing configuration options for the data channel.
*/ */
public createDataChannel(label: string, options: RTCDataChannelInit) { public createDataChannel(label: string, options: RTCDataChannelInit | undefined) {
const dataChannel = this.peerConn!.createDataChannel(label, options); const dataChannel = this.peerConn!.createDataChannel(label, options);
this.emit(CallEvent.DataChannel, dataChannel); this.emit(CallEvent.DataChannel, dataChannel);
return dataChannel; return dataChannel;
@@ -458,7 +462,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
return this.opponentMember; return this.opponentMember;
} }
public getOpponentSessionId(): string { public getOpponentSessionId(): string | undefined {
return this.opponentSessionId; return this.opponentSessionId;
} }
@@ -570,8 +574,13 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
this.opponentDeviceInfo = new DeviceInfo(this.opponentDeviceId); this.opponentDeviceInfo = new DeviceInfo(this.opponentDeviceId);
return; return;
} }
// if we've got to this point, we do want to init crypto, so throw if we can't
if (!this.client.crypto) throw new Error("Crypto is not initialised.");
const userId = this.invitee || this.getOpponentMember()?.userId;
if (!userId) throw new Error("Couldn't find opponent user ID to init crypto");
const userId = this.invitee || this.getOpponentMember().userId;
const deviceInfoMap = await this.client.crypto.deviceList.downloadKeys([userId], false); const deviceInfoMap = await this.client.crypto.deviceList.downloadKeys([userId], false);
this.opponentDeviceInfo = deviceInfoMap[userId][this.opponentDeviceId]; this.opponentDeviceInfo = deviceInfoMap[userId][this.opponentDeviceId];
if (this.opponentDeviceInfo === undefined) { if (this.opponentDeviceInfo === undefined) {
@@ -749,7 +758,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
// accumulate which makes the SDP very large very quickly: in fact it only takes // accumulate which makes the SDP very large very quickly: in fact it only takes
// about 6 video tracks to exceed the maximum size of an Olm-encrypted // about 6 video tracks to exceed the maximum size of an Olm-encrypted
// Matrix event. // Matrix event.
const transceiver = this.transceivers.get(tKey); const transceiver = this.transceivers.get(tKey)!;
// this is what would allow us to use addTransceiver(), but it's not available // this is what would allow us to use addTransceiver(), but it's not available
// on Firefox yet. We call it anyway if we have it. // on Firefox yet. We call it anyway if we have it.
@@ -764,10 +773,15 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
// doesn't yet implement RTCRTPSender.setStreams() // doesn't yet implement RTCRTPSender.setStreams()
// (https://bugzilla.mozilla.org/show_bug.cgi?id=1510802) so we'd have no way to group the // (https://bugzilla.mozilla.org/show_bug.cgi?id=1510802) so we'd have no way to group the
// two tracks together into a stream. // two tracks together into a stream.
const newSender = this.peerConn.addTrack(track, callFeed.stream); const newSender = this.peerConn!.addTrack(track, callFeed.stream);
// now go & fish for the new transceiver // now go & fish for the new transceiver
this.transceivers.set(tKey, this.peerConn.getTransceivers().find(t => 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<CallEvent, CallEventHandlerMap
// There is no way to actually remove a transceiver, so this just sets it to inactive // There is no way to actually remove a transceiver, so this just sets it to inactive
// (or recvonly) and replaces the source with nothing. // (or recvonly) and replaces the source with nothing.
if (this.transceivers.has(transceiverKey)) { if (this.transceivers.has(transceiverKey)) {
const transceiver = this.transceivers.get(transceiverKey); const transceiver = this.transceivers.get(transceiverKey)!;
if (transceiver.sender) this.peerConn.removeTrack(transceiver.sender); if (transceiver.sender) this.peerConn!.removeTrack(transceiver.sender);
} }
} }
@@ -850,7 +864,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
if (!this.peerConn) return; if (!this.peerConn) return;
const statsReport = await this.peerConn.getStats(); const statsReport = await this.peerConn.getStats();
const stats = []; const stats: any[] = [];
statsReport.forEach(item => { statsReport.forEach(item => {
stats.push(item); stats.push(item);
}); });
@@ -917,8 +931,8 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
this.hangupParty = CallParty.Remote; // effectively this.hangupParty = CallParty.Remote; // effectively
this.setState(CallState.Ended); this.setState(CallState.Ended);
this.stopAllMedia(); this.stopAllMedia();
if (this.peerConn.signalingState != 'closed') { if (this.peerConn!.signalingState != 'closed') {
this.peerConn.close(); this.peerConn!.close();
} }
this.emit(CallEvent.Hangup, this); this.emit(CallEvent.Hangup, this);
} }
@@ -947,7 +961,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
private shouldAnswerWithMediaType( private shouldAnswerWithMediaType(
wantedValue: boolean | undefined, wantedValue: boolean | undefined,
valueOfTheOtherSide: boolean | undefined, valueOfTheOtherSide: boolean,
type: "audio" | "video", type: "audio" | "video",
): boolean { ): boolean {
if (wantedValue && !valueOfTheOtherSide) { if (wantedValue && !valueOfTheOtherSide) {
@@ -1186,7 +1200,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
for (const transceiver of [audioTransceiver, videoTransceiver]) { for (const transceiver of [audioTransceiver, videoTransceiver]) {
// this is slightly mixing the track and transceiver API but is basically just shorthand // this is slightly mixing the track and transceiver API but is basically just shorthand
// for removing the sender. // for removing the sender.
if (transceiver && transceiver.sender) this.peerConn.removeTrack(transceiver.sender); if (transceiver && transceiver.sender) this.peerConn!.removeTrack(transceiver.sender);
} }
this.client.getMediaHandler().stopScreensharingStream(this.localScreensharingStream!); this.client.getMediaHandler().stopScreensharingStream(this.localScreensharingStream!);
@@ -1215,9 +1229,9 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
const sender = this.transceivers.get(getTransceiverKey( const sender = this.transceivers.get(getTransceiverKey(
SDPStreamMetadataPurpose.Usermedia, "video", SDPStreamMetadataPurpose.Usermedia, "video",
)).sender; ))?.sender;
sender?.replaceTrack(track); sender?.replaceTrack(track ?? null);
this.pushNewLocalFeed(stream, SDPStreamMetadataPurpose.Screenshare, false); this.pushNewLocalFeed(stream, SDPStreamMetadataPurpose.Screenshare, false);
@@ -1230,8 +1244,8 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
const track = this.localUsermediaStream?.getTracks().find((track) => track.kind === "video"); const track = this.localUsermediaStream?.getTracks().find((track) => track.kind === "video");
const sender = this.transceivers.get(getTransceiverKey( const sender = this.transceivers.get(getTransceiverKey(
SDPStreamMetadataPurpose.Usermedia, "video", SDPStreamMetadataPurpose.Usermedia, "video",
)).sender; ))?.sender;
sender?.replaceTrack(track); sender?.replaceTrack(track ?? null);
this.client.getMediaHandler().stopScreensharingStream(this.localScreensharingStream!); this.client.getMediaHandler().stopScreensharingStream(this.localScreensharingStream!);
this.deleteFeedByStream(this.localScreensharingStream!); this.deleteFeedByStream(this.localScreensharingStream!);
@@ -1298,8 +1312,13 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
`) to peer connection`, `) to peer connection`,
); );
const newSender = this.peerConn!.addTrack(track, this.localUsermediaStream); const newSender = this.peerConn!.addTrack(track, this.localUsermediaStream!);
this.transceivers.set(tKey, this.peerConn.getTransceivers().find(t => t.sender === newSender)); 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<CallEvent, CallEventHandlerMap
const micShouldBeMuted = this.isMicrophoneMuted() || this.remoteOnHold; const micShouldBeMuted = this.isMicrophoneMuted() || this.remoteOnHold;
const vidShouldBeMuted = this.isLocalVideoMuted() || this.remoteOnHold; const vidShouldBeMuted = this.isLocalVideoMuted() || this.remoteOnHold;
logger.log(`call ${this.callId} updateMuteStatus stream ${this.localUsermediaStream.id} micShouldBeMuted ${ logger.log(`call ${this.callId} updateMuteStatus stream ${this.localUsermediaStream!.id} micShouldBeMuted ${
micShouldBeMuted} vidShouldBeMuted ${vidShouldBeMuted}`); micShouldBeMuted} vidShouldBeMuted ${vidShouldBeMuted}`);
setTracksEnabled(this.localUsermediaStream!.getAudioTracks(), !micShouldBeMuted); setTracksEnabled(this.localUsermediaStream!.getAudioTracks(), !micShouldBeMuted);
setTracksEnabled(this.localUsermediaStream!.getVideoTracks(), !vidShouldBeMuted); setTracksEnabled(this.localUsermediaStream!.getVideoTracks(), !vidShouldBeMuted);
@@ -1463,7 +1482,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
} }
if (requestScreenshareFeed) { if (requestScreenshareFeed) {
this.peerConn.addTransceiver("video", { this.peerConn!.addTransceiver("video", {
direction: "recvonly", direction: "recvonly",
}); });
} }
@@ -1535,7 +1554,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
// bandwidth when transmitting silence // bandwidth when transmitting silence
private mungeSdp(description: RTCSessionDescriptionInit, mods: CodecParamsMod[]): void { private mungeSdp(description: RTCSessionDescriptionInit, mods: CodecParamsMod[]): void {
// The only way to enable DTX at this time is through SDP munging // The only way to enable DTX at this time is through SDP munging
const sdp = parseSdp(description.sdp); const sdp = parseSdp(description.sdp!);
sdp.media.forEach(media => { sdp.media.forEach(media => {
const payloadTypeToCodecMap = new Map<number, string>(); const payloadTypeToCodecMap = new Map<number, string>();
@@ -1570,7 +1589,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
} }
if (!found) { if (!found) {
media.fmtp.push({ media.fmtp.push({
payload: codecToPayloadTypeMap.get(mod.codec), payload: codecToPayloadTypeMap.get(mod.codec)!,
config: extraconfig.join(";"), config: extraconfig.join(";"),
}); });
} }
@@ -1580,13 +1599,13 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
} }
private async createOffer(): Promise<RTCSessionDescriptionInit> { private async createOffer(): Promise<RTCSessionDescriptionInit> {
const offer = await this.peerConn.createOffer(); const offer = await this.peerConn!.createOffer();
this.mungeSdp(offer, getCodecParamMods(this.isPtt)); this.mungeSdp(offer, getCodecParamMods(this.isPtt));
return offer; return offer;
} }
private async createAnswer(): Promise<RTCSessionDescriptionInit> { private async createAnswer(): Promise<RTCSessionDescriptionInit> {
const answer = await this.peerConn.createAnswer(); const answer = await this.peerConn!.createAnswer();
this.mungeSdp(answer, getCodecParamMods(this.isPtt)); this.mungeSdp(answer, getCodecParamMods(this.isPtt));
return answer; return answer;
} }
@@ -1665,7 +1684,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
}; };
private onIceGatheringStateChange = (event: Event): void => { private onIceGatheringStateChange = (event: Event): void => {
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') { if (this.peerConn?.iceGatheringState === 'complete') {
this.queueCandidate(null); this.queueCandidate(null);
} }
@@ -1688,10 +1707,12 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
if (this.opponentPartyId === undefined) { if (this.opponentPartyId === undefined) {
// we haven't picked an opponent yet so save the candidates // we haven't picked an opponent yet so save the candidates
logger.info(`Call ${this.callId} Buffering ${candidates.length} candidates until we pick an opponent`); if (fromPartyId) {
const bufferedCandidates = this.remoteCandidateBuffer.get(fromPartyId) || []; logger.info(`Call ${this.callId} Buffering ${candidates.length} candidates until we pick an opponent`);
bufferedCandidates.push(...candidates); const bufferedCandidates = this.remoteCandidateBuffer.get(fromPartyId) || [];
this.remoteCandidateBuffer.set(fromPartyId, bufferedCandidates); bufferedCandidates.push(...candidates);
this.remoteCandidateBuffer.set(fromPartyId, bufferedCandidates);
}
return; return;
} }
@@ -1905,7 +1926,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
try { try {
await this.gotLocalOffer(); await this.gotLocalOffer();
} catch (e) { } catch (e) {
this.getLocalOfferFailed(e); this.getLocalOfferFailed(e as Error);
return; return;
} finally { } finally {
this.makingOffer = false; this.makingOffer = false;
@@ -1932,7 +1953,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
} }
try { try {
await this.peerConn.setLocalDescription(offer); await this.peerConn!.setLocalDescription(offer);
} catch (err) { } catch (err) {
logger.debug(`Call ${this.callId} Error setting local description!`, err); logger.debug(`Call ${this.callId} Error setting local description!`, err);
this.terminate(CallParty.Local, CallErrorCode.SetLocalDescription, true); this.terminate(CallParty.Local, CallErrorCode.SetLocalDescription, true);
@@ -2057,7 +2078,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
// ideally we'd consider the call to be connected when we get media but // ideally we'd consider the call to be connected when we get media but
// chrome doesn't implement any of the 'onstarted' events yet // chrome doesn't implement any of the 'onstarted' events yet
if (["connected", "completed"].includes(this.peerConn?.iceConnectionState)) { if (["connected", "completed"].includes(this.peerConn?.iceConnectionState ?? '')) {
clearTimeout(this.iceDisconnectedTimeout); clearTimeout(this.iceDisconnectedTimeout);
this.setState(CallState.Connected); this.setState(CallState.Connected);
@@ -2069,7 +2090,9 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
} }
} else if (this.peerConn?.iceConnectionState == 'failed') { } else if (this.peerConn?.iceConnectionState == 'failed') {
// Firefox for Android does not yet have support for restartIce() // Firefox for Android does not yet have support for restartIce()
if (this.peerConn?.restartIce) { // (the types say it's always defined though, so we have to cast
// to prevent typescript from warning).
if (this.peerConn?.restartIce as (() => void) | null) {
this.candidatesEnded = false; this.candidatesEnded = false;
this.peerConn!.restartIce(); this.peerConn!.restartIce();
} else { } else {
@@ -2085,7 +2108,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
// the peer, since we don't want to block the line if they're not saying anything. // the peer, since we don't want to block the line if they're not saying anything.
// Experimenting in Chrome, this happens after 5 or 6 seconds, which is probably // Experimenting in Chrome, this happens after 5 or 6 seconds, which is probably
// fast enough. // fast enough.
if (this.isPtt && ["failed", "disconnected"].includes(this.peerConn.iceConnectionState)) { if (this.isPtt && ["failed", "disconnected"].includes(this.peerConn!.iceConnectionState)) {
for (const feed of this.getRemoteFeeds()) { for (const feed of this.getRemoteFeeds()) {
feed.setAudioVideoMuted(true, true); feed.setAudioVideoMuted(true, true);
} }
@@ -2243,16 +2266,16 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
this.emit(CallEvent.SendVoipEvent, { this.emit(CallEvent.SendVoipEvent, {
type: "toDevice", type: "toDevice",
eventType, eventType,
userId: this.invitee || this.getOpponentMember().userId, userId: this.invitee || this.getOpponentMember()?.userId,
opponentDeviceId: this.opponentDeviceId, opponentDeviceId: this.opponentDeviceId,
content, content,
}); });
const userId = this.invitee || this.getOpponentMember().userId; const userId = this.invitee || this.getOpponentMember()!.userId;
if (this.client.getUseE2eForGroupCall()) { if (this.client.getUseE2eForGroupCall()) {
await this.client.encryptAndSendToDevices([{ await this.client.encryptAndSendToDevices([{
userId, userId,
deviceInfo: this.opponentDeviceInfo, deviceInfo: this.opponentDeviceInfo!,
}], { }], {
type: eventType, type: eventType,
content, content,
@@ -2273,7 +2296,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
userId: this.invitee || this.getOpponentMember()?.userId, userId: this.invitee || this.getOpponentMember()?.userId,
}); });
await this.client.sendEvent(this.roomId, eventType, realContent); await this.client.sendEvent(this.roomId!, eventType, realContent);
} }
} }
@@ -2322,7 +2345,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
// Call this method before sending an invite or answer message // Call this method before sending an invite or answer message
private discardDuplicateCandidates(): number { private discardDuplicateCandidates(): number {
let discardCount = 0; let discardCount = 0;
const newQueue = []; const newQueue: RTCIceCandidate[] = [];
for (let i = 0; i < this.candidateSendQueue.length; i++) { for (let i = 0; i < this.candidateSendQueue.length; i++) {
const candidate = this.candidateSendQueue[i]; const candidate = this.candidateSendQueue[i];
@@ -2647,7 +2670,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
this.opponentPartyId = msg.party_id || null; this.opponentPartyId = msg.party_id || null;
} }
this.opponentCaps = msg.capabilities || {} as CallCapabilities; this.opponentCaps = msg.capabilities || {} as CallCapabilities;
this.opponentMember = this.client.getRoom(this.roomId).getMember(ev.getSender()); this.opponentMember = this.client.getRoom(this.roomId)!.getMember(ev.getSender()) ?? undefined;
} }
private async addBufferedIceCandidates(): Promise<void> { private async addBufferedIceCandidates(): Promise<void> {

View File

@@ -16,7 +16,7 @@ limitations under the License.
import { MatrixEvent } from '../models/event'; import { MatrixEvent } from '../models/event';
import { logger } from '../logger'; 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 { EventType } from '../@types/event';
import { ClientEvent, MatrixClient } from '../client'; import { ClientEvent, MatrixClient } from '../client';
import { MCallAnswer, MCallHangupReject } from "./callEventTypes"; import { MCallAnswer, MCallHangupReject } from "./callEventTypes";
@@ -152,7 +152,7 @@ export class CallEventHandler {
this.toDeviceEventBuffers.set(content.call_id, []); 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); const index = buffer.findIndex((e) => e.getContent().seq > content.seq);
if (index === -1) { if (index === -1) {
@@ -172,7 +172,7 @@ export class CallEventHandler {
while (nextEvent && nextEvent.getContent().seq === this.nextSeqByCall.get(callId)) { while (nextEvent && nextEvent.getContent().seq === this.nextSeqByCall.get(callId)) {
this.callEventBuffer.push(nextEvent); this.callEventBuffer.push(nextEvent);
this.nextSeqByCall.set(callId, nextEvent.getContent().seq + 1); 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 opponentDeviceId: string | undefined;
let groupCall: GroupCall; let groupCall: GroupCall | undefined;
if (groupCallId) { if (groupCallId) {
groupCall = this.client.groupCallEventHandler.getGroupCallById(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 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"); logger.info("Current turn creds expire in " + timeUntilTurnCresExpire + " ms");
call = createNewMatrixCall( call = createNewMatrixCall(
this.client, this.client,
@@ -267,10 +267,12 @@ export class CallEventHandler {
try { try {
await call.initWithInvite(event); await call.initWithInvite(event);
} catch (e) { } catch (e) {
if (e.code === GroupCallErrorCode.UnknownDevice) { if (e instanceof CallError) {
groupCall?.emit(GroupCallEvent.Error, e); if (e.code === GroupCallErrorCode.UnknownDevice) {
} else { groupCall?.emit(GroupCallEvent.Error, e);
logger.error(e); } else {
logger.error(e);
}
} }
} }
this.calls.set(call.callId, call); this.calls.set(call.callId, call);
@@ -292,7 +294,7 @@ export class CallEventHandler {
if ( if (
call.roomId === thisCall.roomId && call.roomId === thisCall.roomId &&
thisCall.direction === CallDirection.Outbound && thisCall.direction === CallDirection.Outbound &&
call.getOpponentMember().userId === thisCall.invitee && call.getOpponentMember()?.userId === thisCall.invitee &&
isCalling isCalling
) { ) {
existingCall = thisCall; existingCall = thisCall;

View File

@@ -27,7 +27,7 @@ const SPEAKING_SAMPLE_COUNT = 8; // samples
export interface ICallFeedOpts { export interface ICallFeedOpts {
client: MatrixClient; client: MatrixClient;
roomId: string; roomId?: string;
userId: string; userId: string;
stream: MediaStream; stream: MediaStream;
purpose: SDPStreamMetadataPurpose; purpose: SDPStreamMetadataPurpose;
@@ -67,7 +67,7 @@ export class CallFeed extends TypedEventEmitter<CallFeedEvent, EventHandlerMap>
public speakingVolumeSamples: number[]; public speakingVolumeSamples: number[];
private client: MatrixClient; private client: MatrixClient;
private roomId: string; private roomId?: string;
private audioMuted: boolean; private audioMuted: boolean;
private videoMuted: boolean; private videoMuted: boolean;
private localVolume = 1; private localVolume = 1;
@@ -295,8 +295,8 @@ export class CallFeed extends TypedEventEmitter<CallFeedEvent, EventHandlerMap>
clearTimeout(this.volumeLooperTimeout); clearTimeout(this.volumeLooperTimeout);
this.stream?.removeEventListener("addtrack", this.onAddTrack); this.stream?.removeEventListener("addtrack", this.onAddTrack);
if (this.audioContext) { if (this.audioContext) {
this.audioContext = null; this.audioContext = undefined;
this.analyser = null; this.analyser = undefined;
releaseContext(); releaseContext();
} }
this._disposed = true; this._disposed = true;

View File

@@ -9,6 +9,7 @@ import { CallErrorCode,
MatrixCall, MatrixCall,
setTracksEnabled, setTracksEnabled,
createNewMatrixCall, createNewMatrixCall,
CallError,
} from "./call"; } from "./call";
import { RoomMember } from "../models/room-member"; import { RoomMember } from "../models/room-member";
import { Room } from "../models/room"; import { Room } from "../models/room";
@@ -56,7 +57,7 @@ export type GroupCallEventHandlerMap = {
[GroupCallEvent.UserMediaFeedsChanged]: (feeds: CallFeed[]) => void; [GroupCallEvent.UserMediaFeedsChanged]: (feeds: CallFeed[]) => void;
[GroupCallEvent.ScreenshareFeedsChanged]: (feeds: CallFeed[]) => void; [GroupCallEvent.ScreenshareFeedsChanged]: (feeds: CallFeed[]) => void;
[GroupCallEvent.LocalScreenshareStateChanged]: ( [GroupCallEvent.LocalScreenshareStateChanged]: (
isScreensharing: boolean, feed: CallFeed, sourceId: string, isScreensharing: boolean, feed?: CallFeed, sourceId?: string,
) => void; ) => void;
[GroupCallEvent.LocalMuteStateChanged]: (audioMuted: boolean, videoMuted: boolean) => void; [GroupCallEvent.LocalMuteStateChanged]: (audioMuted: boolean, videoMuted: boolean) => void;
[GroupCallEvent.ParticipantsChanged]: (participants: RoomMember[]) => void; [GroupCallEvent.ParticipantsChanged]: (participants: RoomMember[]) => void;
@@ -136,7 +137,7 @@ export enum GroupCallState {
interface ICallHandlers { interface ICallHandlers {
onCallFeedsChanged: (feeds: CallFeed[]) => void; onCallFeedsChanged: (feeds: CallFeed[]) => void;
onCallStateChanged: (state: CallState, oldState: CallState) => void; onCallStateChanged: (state: CallState, oldState: CallState | undefined) => void;
onCallHangup: (call: MatrixCall) => void; onCallHangup: (call: MatrixCall) => void;
onCallReplaced: (newCall: MatrixCall) => void; onCallReplaced: (newCall: MatrixCall) => void;
} }
@@ -232,7 +233,7 @@ export class GroupCall extends TypedEventEmitter<
} }
public getLocalFeeds(): CallFeed[] { public getLocalFeeds(): CallFeed[] {
const feeds = []; const feeds: CallFeed[] = [];
if (this.localCallFeed) feeds.push(this.localCallFeed); if (this.localCallFeed) feeds.push(this.localCallFeed);
if (this.localScreenshareFeed) feeds.push(this.localScreenshareFeed); if (this.localScreenshareFeed) feeds.push(this.localScreenshareFeed);
@@ -311,11 +312,11 @@ export class GroupCall extends TypedEventEmitter<
await this.initLocalCallFeed(); await this.initLocalCallFeed();
} }
this.addParticipant(this.room.getMember(this.client.getUserId())); this.addParticipant(this.room.getMember(this.client.getUserId()!)!);
await this.sendMemberStateEvent(); await this.sendMemberStateEvent();
this.activeSpeaker = null; this.activeSpeaker = undefined;
this.setState(GroupCallState.Entered); this.setState(GroupCallState.Entered);
@@ -343,7 +344,7 @@ export class GroupCall extends TypedEventEmitter<
private dispose() { private dispose() {
if (this.localCallFeed) { if (this.localCallFeed) {
this.removeUserMediaFeed(this.localCallFeed); this.removeUserMediaFeed(this.localCallFeed);
this.localCallFeed = null; this.localCallFeed = undefined;
} }
if (this.localScreenshareFeed) { if (this.localScreenshareFeed) {
@@ -359,7 +360,7 @@ export class GroupCall extends TypedEventEmitter<
return; return;
} }
this.removeParticipant(this.room.getMember(this.client.getUserId())); this.removeParticipant(this.room.getMember(this.client.getUserId()!)!);
this.removeMemberStateEvent(); this.removeMemberStateEvent();
@@ -367,7 +368,7 @@ export class GroupCall extends TypedEventEmitter<
this.removeCall(this.calls[this.calls.length - 1], CallErrorCode.UserHangup); this.removeCall(this.calls[this.calls.length - 1], CallErrorCode.UserHangup);
} }
this.activeSpeaker = null; this.activeSpeaker = undefined;
clearTimeout(this.activeSpeakerLoopTimeout); clearTimeout(this.activeSpeakerLoopTimeout);
this.retryCallCounts.clear(); this.retryCallCounts.clear();
@@ -470,7 +471,7 @@ export class GroupCall extends TypedEventEmitter<
this.setMicrophoneMuted(true); this.setMicrophoneMuted(true);
}, this.pttMaxTransmitTime); }, this.pttMaxTransmitTime);
} else if (muted && !this.isMicrophoneMuted()) { } else if (muted && !this.isMicrophoneMuted()) {
clearTimeout(this.transmitTimer); if (this.transmitTimer !== null) clearTimeout(this.transmitTimer);
this.transmitTimer = null; this.transmitTimer = null;
} }
} }
@@ -502,7 +503,7 @@ export class GroupCall extends TypedEventEmitter<
} }
for (const call of this.calls) { 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()); this.emit(GroupCallEvent.LocalMuteStateChanged, muted, this.isLocalVideoMuted());
@@ -576,7 +577,7 @@ export class GroupCall extends TypedEventEmitter<
this.localScreenshareFeed = new CallFeed({ this.localScreenshareFeed = new CallFeed({
client: this.client, client: this.client,
roomId: this.room.roomId, roomId: this.room.roomId,
userId: this.client.getUserId(), userId: this.client.getUserId()!,
stream, stream,
purpose: SDPStreamMetadataPurpose.Screenshare, purpose: SDPStreamMetadataPurpose.Screenshare,
audioMuted: false, audioMuted: false,
@@ -593,7 +594,7 @@ export class GroupCall extends TypedEventEmitter<
// TODO: handle errors // TODO: handle errors
await Promise.all(this.calls.map(call => call.pushLocalFeed( await Promise.all(this.calls.map(call => call.pushLocalFeed(
this.localScreenshareFeed.clone(), this.localScreenshareFeed!.clone(),
))); )));
await this.sendMemberStateEvent(); await this.sendMemberStateEvent();
@@ -603,7 +604,10 @@ export class GroupCall extends TypedEventEmitter<
if (opts.throwOnFail) throw error; if (opts.throwOnFail) throw error;
logger.error("Enabling screensharing error", error); logger.error("Enabling screensharing error", error);
this.emit(GroupCallEvent.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; return false;
} }
@@ -611,8 +615,8 @@ export class GroupCall extends TypedEventEmitter<
await Promise.all(this.calls.map(call => { await Promise.all(this.calls.map(call => {
if (call.localScreensharingFeed) call.removeLocalFeed(call.localScreensharingFeed); if (call.localScreensharingFeed) call.removeLocalFeed(call.localScreensharingFeed);
})); }));
this.client.getMediaHandler().stopScreensharingStream(this.localScreenshareFeed.stream); this.client.getMediaHandler().stopScreensharingStream(this.localScreenshareFeed!.stream);
this.removeScreenshareFeed(this.localScreenshareFeed); this.removeScreenshareFeed(this.localScreenshareFeed!);
this.localScreenshareFeed = undefined; this.localScreenshareFeed = undefined;
this.localDesktopCapturerSourceId = undefined; this.localDesktopCapturerSourceId = undefined;
await this.sendMemberStateEvent(); await this.sendMemberStateEvent();
@@ -652,8 +656,8 @@ export class GroupCall extends TypedEventEmitter<
return; return;
} }
const opponentMemberId = newCall.getOpponentMember().userId; const opponentMemberId = newCall.getOpponentMember()?.userId;
const existingCall = this.getCallByUserId(opponentMemberId); const existingCall = opponentMemberId ? this.getCallByUserId(opponentMemberId) : null;
if (existingCall && existingCall.callId === newCall.callId) { if (existingCall && existingCall.callId === newCall.callId) {
return; return;
@@ -709,7 +713,7 @@ export class GroupCall extends TypedEventEmitter<
const res = await send(); const res = await send();
// Clear the old interval first, so that it isn't forgot // 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 // Resend the state event every so often so it doesn't become stale
this.resendMemberStateTimer = setInterval(async () => { this.resendMemberStateTimer = setInterval(async () => {
logger.log("Resending call member state"); logger.log("Resending call member state");
@@ -720,13 +724,13 @@ export class GroupCall extends TypedEventEmitter<
} }
private async removeMemberStateEvent(): Promise<ISendEventResponse> { private async removeMemberStateEvent(): Promise<ISendEventResponse> {
clearInterval(this.resendMemberStateTimer); if (this.resendMemberStateTimer !== null) clearInterval(this.resendMemberStateTimer);
this.resendMemberStateTimer = null; this.resendMemberStateTimer = null;
return await this.updateMemberCallState(undefined); return await this.updateMemberCallState(undefined);
} }
private async updateMemberCallState(memberCallState?: IGroupCallRoomMemberCallState): Promise<ISendEventResponse> { private async updateMemberCallState(memberCallState?: IGroupCallRoomMemberCallState): Promise<ISendEventResponse> {
const localUserId = this.client.getUserId(); const localUserId = this.client.getUserId()!;
const memberState = this.getMemberStateEvents(localUserId)?.getContent<IGroupCallRoomMemberState>(); const memberState = this.getMemberStateEvents(localUserId)?.getContent<IGroupCallRoomMemberState>();
@@ -766,7 +770,7 @@ export class GroupCall extends TypedEventEmitter<
// The member events may be received for another room, which we will ignore. // The member events may be received for another room, which we will ignore.
if (event.getRoomId() !== this.room.roomId) return; if (event.getRoomId() !== this.room.roomId) return;
const member = this.room.getMember(event.getStateKey()); const member = this.room.getMember(event.getStateKey()!);
if (!member) { if (!member) {
logger.warn(`Couldn't find room member for ${event.getStateKey()}: ignoring member state event!`); logger.warn(`Couldn't find room member for ${event.getStateKey()}: ignoring member state event!`);
return; return;
@@ -816,7 +820,7 @@ export class GroupCall extends TypedEventEmitter<
}, content["m.expires_ts"] - Date.now())); }, content["m.expires_ts"] - Date.now()));
// Don't process your own member. // Don't process your own member.
const localUserId = this.client.getUserId(); const localUserId = this.client.getUserId()!;
if (member.userId === localUserId) { if (member.userId === localUserId) {
return; return;
@@ -860,6 +864,11 @@ export class GroupCall extends TypedEventEmitter<
}, },
); );
if (!newCall) {
logger.error("Failed to create call!");
return;
}
if (existingCall) { if (existingCall) {
logger.debug(`Replacing call ${existingCall.callId} to ${member.userId} with ${newCall.callId}`); logger.debug(`Replacing call ${existingCall.callId} to ${member.userId} with ${newCall.callId}`);
this.replaceCall(existingCall, newCall, CallErrorCode.NewSession); this.replaceCall(existingCall, newCall, CallErrorCode.NewSession);
@@ -884,7 +893,7 @@ export class GroupCall extends TypedEventEmitter<
); );
} catch (e) { } catch (e) {
logger.warn(`Failed to place call to ${member.userId}!`, 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); this.emit(GroupCallEvent.Error, e);
} else { } else {
this.emit( 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); const memberStateEvent = this.getMemberStateEvents(userId);
if (!memberStateEvent) { if (!memberStateEvent) {
@@ -931,7 +940,7 @@ export class GroupCall extends TypedEventEmitter<
private onRetryCallLoop = () => { private onRetryCallLoop = () => {
for (const event of this.getMemberStateEvents()) { for (const event of this.getMemberStateEvents()) {
const memberId = event.getStateKey(); const memberId = event.getStateKey()!;
const existingCall = this.calls.find((call) => getCallUserId(call) === memberId); const existingCall = this.calls.find((call) => getCallUserId(call) === memberId);
const retryCallCount = this.retryCallCounts.get(memberId) || 0; const retryCallCount = this.retryCallCounts.get(memberId) || 0;
@@ -948,7 +957,7 @@ export class GroupCall extends TypedEventEmitter<
* Call Event Handlers * Call Event Handlers
*/ */
public getCallByUserId(userId: string): MatrixCall { public getCallByUserId(userId: string): MatrixCall | undefined {
return this.calls.find((call) => getCallUserId(call) === userId); return this.calls.find((call) => getCallUserId(call) === userId);
} }
@@ -996,7 +1005,7 @@ export class GroupCall extends TypedEventEmitter<
const onCallFeedsChanged = () => this.onCallFeedsChanged(call); const onCallFeedsChanged = () => this.onCallFeedsChanged(call);
const onCallStateChanged = 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 onCallHangup = this.onCallHangup;
const onCallReplaced = (newCall: MatrixCall) => this.replaceCall(call, newCall); const onCallReplaced = (newCall: MatrixCall) => this.replaceCall(call, newCall);
@@ -1029,7 +1038,7 @@ export class GroupCall extends TypedEventEmitter<
onCallStateChanged, onCallStateChanged,
onCallHangup, onCallHangup,
onCallReplaced, onCallReplaced,
} = this.callHandlers.get(opponentMemberId); } = this.callHandlers.get(opponentMemberId)!;
call.removeListener(CallEvent.FeedsChanged, onCallFeedsChanged); call.removeListener(CallEvent.FeedsChanged, onCallFeedsChanged);
call.removeListener(CallEvent.State, onCallStateChanged); call.removeListener(CallEvent.State, onCallStateChanged);
@@ -1095,8 +1104,8 @@ export class GroupCall extends TypedEventEmitter<
} }
}; };
private onCallStateChanged = (call: MatrixCall, state: CallState, _oldState: CallState) => { private onCallStateChanged = (call: MatrixCall, state: CallState, _oldState: CallState | undefined) => {
const audioMuted = this.localCallFeed.isAudioMuted(); const audioMuted = this.localCallFeed!.isAudioMuted();
if ( if (
call.localUsermediaStream && call.localUsermediaStream &&
@@ -1105,7 +1114,7 @@ export class GroupCall extends TypedEventEmitter<
call.setMicrophoneMuted(audioMuted); call.setMicrophoneMuted(audioMuted);
} }
const videoMuted = this.localCallFeed.isVideoMuted(); const videoMuted = this.localCallFeed!.isVideoMuted();
if ( if (
call.localUsermediaStream && call.localUsermediaStream &&
@@ -1115,7 +1124,7 @@ export class GroupCall extends TypedEventEmitter<
} }
if (state === CallState.Connected) { 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 = () => { private onActiveSpeakerLoop = () => {
let topAvg: number; let topAvg: number | undefined = undefined;
let nextActiveSpeaker: string; let nextActiveSpeaker: string | undefined = undefined;
for (const callFeed of this.userMediaFeeds) { for (const callFeed of this.userMediaFeeds) {
if (callFeed.userId === this.client.getUserId() && this.userMediaFeeds.length > 1) { 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.activeSpeaker = nextActiveSpeaker;
this.emit(GroupCallEvent.ActiveSpeakerChanged, this.activeSpeaker); this.emit(GroupCallEvent.ActiveSpeakerChanged, this.activeSpeaker);
} }

View File

@@ -91,7 +91,7 @@ export class GroupCallEventHandler {
} }
private getRoomDeferred(roomId: string): RoomDeferred { private getRoomDeferred(roomId: string): RoomDeferred {
let deferred: RoomDeferred = this.roomDeferreds.get(roomId); let deferred = this.roomDeferreds.get(roomId);
if (deferred === undefined) { if (deferred === undefined) {
let resolveFunc: () => void; let resolveFunc: () => void;
deferred = { deferred = {
@@ -99,7 +99,7 @@ export class GroupCallEventHandler {
resolveFunc = resolve; resolveFunc = resolve;
}), }),
}; };
deferred.resolve = resolveFunc; deferred.resolve = resolveFunc!;
this.roomDeferreds.set(roomId, deferred); this.roomDeferreds.set(roomId, deferred);
} }
@@ -110,7 +110,7 @@ export class GroupCallEventHandler {
return this.getRoomDeferred(roomId).prom; 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); 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); 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 { private createGroupCallFromRoomStateEvent(event: MatrixEvent): GroupCall | undefined {

View File

@@ -228,8 +228,8 @@ export class MediaHandler extends TypedEventEmitter<
this.localUserMediaStream = stream; this.localUserMediaStream = stream;
} }
} else { } else {
stream = this.localUserMediaStream.clone(); stream = this.localUserMediaStream!.clone();
logger.log(`mediaHandler clone userMediaStream ${this.localUserMediaStream.id} new stream ${ logger.log(`mediaHandler clone userMediaStream ${this.localUserMediaStream?.id} new stream ${
stream.id} shouldRequestAudio ${shouldRequestAudio} shouldRequestVideo ${shouldRequestVideo}`); stream.id} shouldRequestAudio ${shouldRequestAudio} shouldRequestVideo ${shouldRequestVideo}`);
if (!shouldRequestAudio) { if (!shouldRequestAudio) {
@@ -282,12 +282,11 @@ export class MediaHandler extends TypedEventEmitter<
* @param reusable is allowed to be reused by the MediaHandler * @param reusable is allowed to be reused by the MediaHandler
* @returns {MediaStream} based on passed parameters * @returns {MediaStream} based on passed parameters
*/ */
public async getScreensharingStream(opts: IScreensharingOpts = {}, reusable = true): Promise<MediaStream | null> { public async getScreensharingStream(opts: IScreensharingOpts = {}, reusable = true): Promise<MediaStream> {
let stream: MediaStream; let stream: MediaStream;
if (this.screensharingStreams.length === 0) { if (this.screensharingStreams.length === 0) {
const screenshareConstraints = this.getScreenshareContraints(opts); const screenshareConstraints = this.getScreenshareContraints(opts);
if (!screenshareConstraints) return null;
if (opts.desktopCapturerSourceId) { if (opts.desktopCapturerSourceId) {
// We are using Electron // We are using Electron
@@ -385,7 +384,7 @@ export class MediaHandler extends TypedEventEmitter<
if (desktopCapturerSourceId) { if (desktopCapturerSourceId) {
logger.debug("Using desktop capturer source", desktopCapturerSourceId); logger.debug("Using desktop capturer source", desktopCapturerSourceId);
return { return {
audio, audio: audio ?? false,
video: { video: {
mandatory: { mandatory: {
chromeMediaSource: "desktop", chromeMediaSource: "desktop",
@@ -396,7 +395,7 @@ export class MediaHandler extends TypedEventEmitter<
} else { } else {
logger.debug("Not using desktop capturer source"); logger.debug("Not using desktop capturer source");
return { return {
audio, audio: audio ?? false,
video: true, video: true,
}; };
} }