Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the call object to Call events #3229

Merged
merged 7 commits into from
Mar 29, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 49 additions & 2 deletions spec/unit/webrtc/call.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,33 @@ describe("Call", function () {
expect(transceivers.get("m.usermedia:video")!.sender.track!.id).toBe("usermedia_video_track");
});

it("should handle error on call upgrade", async () => {
const onError = jest.fn();
call.on(CallEvent.Error, onError);

await startVoiceCall(client, call);

await call.onAnswerReceived(
makeMockEvent("@test:foo", {
version: 1,
call_id: call.callId,
party_id: "party_id",
answer: {
sdp: DUMMY_SDP,
},
[SDPStreamMetadataKey]: {},
}),
);

const mockGetUserMediaStream = jest.fn().mockRejectedValue(new Error("Test error"));
client.client.getMediaHandler().getUserMediaStream = mockGetUserMediaStream;

// then unmute which should cause an upgrade
await call.setLocalVideoMuted(false);

expect(onError).toHaveBeenCalled();
});

it("should unmute video after upgrading to video call", async () => {
// Regression test for https://github.com/vector-im/element-call/issues/925
await startVoiceCall(client, call);
Expand Down Expand Up @@ -737,7 +764,7 @@ describe("Call", function () {

const dataChannel = call.createDataChannel("data_channel_label", { id: 123 });

expect(dataChannelCallback).toHaveBeenCalledWith(dataChannel);
expect(dataChannelCallback).toHaveBeenCalledWith(dataChannel, call);
expect(dataChannel.label).toBe("data_channel_label");
expect(dataChannel.id).toBe(123);
});
Expand Down Expand Up @@ -1604,7 +1631,7 @@ describe("Call", function () {
hasAdvancedBy += advanceBy;

expect(lengthChangedListener).toHaveBeenCalledTimes(hasAdvancedBy);
expect(lengthChangedListener).toHaveBeenCalledWith(hasAdvancedBy);
expect(lengthChangedListener).toHaveBeenCalledWith(hasAdvancedBy, call);
}
});

Expand Down Expand Up @@ -1634,4 +1661,24 @@ describe("Call", function () {
expect(call.hangup).not.toHaveBeenCalled();
});
});

describe("Call replace", () => {
it("Fires event when call replaced", async () => {
const onReplace = jest.fn();
call.on(CallEvent.Replaced, onReplace);

await call.placeVoiceCall();

const call2 = new MatrixCall({
client: client.client,
roomId: FAKE_ROOM_ID,
});
call2.on(CallEvent.Error, errorListener);
await fakeIncomingCall(client, call2);

call.replacedBy(call2);

expect(onReplace).toHaveBeenCalled();
});
});
});
2 changes: 1 addition & 1 deletion spec/unit/webrtc/callFeed.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ describe("CallFeed", () => {
[CallState.Connected, true],
[CallState.Connecting, false],
])("should react to call state, when !isLocal()", (state: CallState, expected: Boolean) => {
call.emit(CallEvent.State, state);
call.emit(CallEvent.State, state, CallState.InviteSent, call.typed());

expect(feed.connected).toBe(expected);
});
Expand Down
6 changes: 3 additions & 3 deletions spec/unit/webrtc/groupCall.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -792,7 +792,7 @@ describe("Group Call", function () {
call.isLocalVideoMuted = jest.fn().mockReturnValue(true);
call.setLocalVideoMuted = jest.fn();

call.emit(CallEvent.State, CallState.Connected);
call.emit(CallEvent.State, CallState.Connected, CallState.InviteSent, call);

expect(call.setMicrophoneMuted).toHaveBeenCalledWith(false);
expect(call.setLocalVideoMuted).toHaveBeenCalledWith(false);
Expand Down Expand Up @@ -1080,7 +1080,7 @@ describe("Group Call", function () {
});

it("handles regular case", () => {
oldMockCall.emit(CallEvent.Replaced, newMockCall.typed());
oldMockCall.emit(CallEvent.Replaced, newMockCall.typed(), oldMockCall.typed());

expect(oldMockCall.hangup).toHaveBeenCalled();
expect(callChangedListener).toHaveBeenCalledWith(newCallsMap);
Expand All @@ -1091,7 +1091,7 @@ describe("Group Call", function () {
it("handles case where call is missing from the calls map", () => {
// @ts-ignore
groupCall.calls = new Map();
oldMockCall.emit(CallEvent.Replaced, newMockCall.typed());
oldMockCall.emit(CallEvent.Replaced, newMockCall.typed(), oldMockCall.typed());

expect(oldMockCall.hangup).toHaveBeenCalled();
expect(callChangedListener).toHaveBeenCalledWith(newCallsMap);
Expand Down
110 changes: 69 additions & 41 deletions src/webrtc/call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,20 +296,34 @@ export interface VoipEvent {
content: Record<string, unknown>;
}

/**
* These now all have the call object as an argument. Why? Well, to know which call a given event is
* about you have three options:
* 1. Use a closure as the callback that remembers what call it's listening to. This can be
* a pain because you need to pass the listener function again when you remove the listener,
* which might be somewhere else.
* 2. Use not-very-well-known fact that EventEmitter sets 'this' to the emitter object in the
* callback. This doesn't really play well with modern Typescript and eslint and doesn't work
* with our pattern of re-emitting events.
* 3. Pass the object in question as an argument to the callback.
*
* Now that we have group calls which have to deal with multiple call objects, this will
* become more important, and I think methods 1 and 2 are just going to cause issues.
*/
export type CallEventHandlerMap = {
[CallEvent.DataChannel]: (channel: RTCDataChannel) => void;
[CallEvent.FeedsChanged]: (feeds: CallFeed[]) => void;
[CallEvent.Replaced]: (newCall: MatrixCall) => void;
[CallEvent.Error]: (error: CallError) => void;
[CallEvent.RemoteHoldUnhold]: (onHold: boolean) => void;
[CallEvent.LocalHoldUnhold]: (onHold: boolean) => void;
[CallEvent.LengthChanged]: (length: number) => void;
[CallEvent.State]: (state: CallState, oldState?: CallState) => void;
[CallEvent.DataChannel]: (channel: RTCDataChannel, call: MatrixCall) => void;
[CallEvent.FeedsChanged]: (feeds: CallFeed[], call: MatrixCall) => void;
[CallEvent.Replaced]: (newCall: MatrixCall, oldCall: MatrixCall) => void;
[CallEvent.Error]: (error: CallError, call: MatrixCall) => void;
[CallEvent.RemoteHoldUnhold]: (onHold: boolean, call: MatrixCall) => void;
[CallEvent.LocalHoldUnhold]: (onHold: boolean, call: MatrixCall) => void;
[CallEvent.LengthChanged]: (length: number, call: MatrixCall) => void;
[CallEvent.State]: (state: CallState, oldState: CallState, call: MatrixCall) => void;
[CallEvent.Hangup]: (call: MatrixCall) => void;
[CallEvent.AssertedIdentityChanged]: () => void;
[CallEvent.AssertedIdentityChanged]: (call: MatrixCall) => void;
/* @deprecated */
[CallEvent.HoldUnhold]: (onHold: boolean) => void;
[CallEvent.SendVoipEvent]: (event: VoipEvent) => void;
[CallEvent.SendVoipEvent]: (event: VoipEvent, call: MatrixCall) => void;
};

// The key of the transceiver map (purpose + media type, separated by ':')
Expand Down Expand Up @@ -459,7 +473,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
*/
public createDataChannel(label: string, options: RTCDataChannelInit | undefined): RTCDataChannel {
const dataChannel = this.peerConn!.createDataChannel(label, options);
this.emit(CallEvent.DataChannel, dataChannel);
this.emit(CallEvent.DataChannel, dataChannel, this);
return dataChannel;
}

Expand Down Expand Up @@ -494,7 +508,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
private set state(state: CallState) {
const oldState = this._state;
this._state = state;
this.emit(CallEvent.State, state, oldState);
this.emit(CallEvent.State, state, oldState, this);
}

public get type(): CallType {
Expand Down Expand Up @@ -684,7 +698,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
}),
);

this.emit(CallEvent.FeedsChanged, this.feeds);
this.emit(CallEvent.FeedsChanged, this.feeds, this);

logger.info(
`Call ${this.callId} pushRemoteFeed() pushed stream (streamId=${stream.id}, active=${stream.active}, purpose=${purpose})`,
Expand Down Expand Up @@ -732,7 +746,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
}),
);

this.emit(CallEvent.FeedsChanged, this.feeds);
this.emit(CallEvent.FeedsChanged, this.feeds, this);

logger.info(
`Call ${this.callId} pushRemoteFeedWithoutMetadata() pushed stream (streamId=${stream.id}, active=${stream.active})`,
Expand Down Expand Up @@ -832,7 +846,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
`Call ${this.callId} pushLocalFeed() pushed stream (id=${callFeed.stream.id}, active=${callFeed.stream.active}, purpose=${callFeed.purpose})`,
);

this.emit(CallEvent.FeedsChanged, this.feeds);
this.emit(CallEvent.FeedsChanged, this.feeds, this);
}

/**
Expand Down Expand Up @@ -869,7 +883,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
}

this.feeds = [];
this.emit(CallEvent.FeedsChanged, this.feeds);
this.emit(CallEvent.FeedsChanged, this.feeds, this);
}

private deleteFeedByStream(stream: MediaStream): void {
Expand All @@ -886,7 +900,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
private deleteFeed(feed: CallFeed): void {
feed.dispose();
this.feeds.splice(this.feeds.indexOf(feed), 1);
this.emit(CallEvent.FeedsChanged, this.feeds);
this.emit(CallEvent.FeedsChanged, this.feeds, this);
}

// The typescript definitions have this type as 'any' :(
Expand Down Expand Up @@ -1117,7 +1131,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
}
}
this.successor = newCall;
this.emit(CallEvent.Replaced, newCall);
this.emit(CallEvent.Replaced, newCall, this);
this.hangup(CallErrorCode.Replaced, true);
}

Expand Down Expand Up @@ -1188,6 +1202,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
this.emit(
CallEvent.Error,
new CallError(CallErrorCode.NoUserMedia, "Failed to get camera access: ", <Error>error),
this,
);
}
}
Expand Down Expand Up @@ -1513,7 +1528,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
this.updateMuteStatus();
this.sendMetadataUpdate();

this.emit(CallEvent.RemoteHoldUnhold, this.remoteOnHold);
this.emit(CallEvent.RemoteHoldUnhold, this.remoteOnHold, this);
}

/**
Expand Down Expand Up @@ -1638,7 +1653,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
code = CallErrorCode.UnknownDevices;
message = "Unknown devices present in the room";
}
this.emit(CallEvent.Error, new CallError(code, message, <Error>error));
this.emit(CallEvent.Error, new CallError(code, message, <Error>error), this);
throw error;
}

Expand Down Expand Up @@ -1987,7 +2002,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap

const newLocalOnHold = this.isLocalOnHold();
if (prevLocalOnHold !== newLocalOnHold) {
this.emit(CallEvent.LocalHoldUnhold, newLocalOnHold);
this.emit(CallEvent.LocalHoldUnhold, newLocalOnHold, this);
// also this one for backwards compat
this.emit(CallEvent.HoldUnhold, newLocalOnHold);
}
Expand Down Expand Up @@ -2018,7 +2033,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
id: content.asserted_identity.id,
displayName: content.asserted_identity.display_name,
};
this.emit(CallEvent.AssertedIdentityChanged);
this.emit(CallEvent.AssertedIdentityChanged, this);
}

public callHasEnded(): boolean {
Expand Down Expand Up @@ -2134,7 +2149,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
message = "Unknown devices present in the room";
}

this.emit(CallEvent.Error, new CallError(code, message, <Error>error));
this.emit(CallEvent.Error, new CallError(code, message, <Error>error), this);
this.terminate(CallParty.Local, code, false);

// no need to carry on & send the candidate queue, but we also
Expand All @@ -2158,7 +2173,11 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
private getLocalOfferFailed = (err: Error): void => {
logger.error(`Call ${this.callId} getLocalOfferFailed() running`, err);

this.emit(CallEvent.Error, new CallError(CallErrorCode.LocalOfferFailed, "Failed to get local offer!", err));
this.emit(
CallEvent.Error,
new CallError(CallErrorCode.LocalOfferFailed, "Failed to get local offer!", err),
this,
);
this.terminate(CallParty.Local, CallErrorCode.LocalOfferFailed, false);
};

Expand All @@ -2177,6 +2196,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
"Couldn't start capturing media! Is your microphone set up and " + "does this app have permission?",
err,
),
this,
);
this.terminate(CallParty.Local, CallErrorCode.NoUserMedia, false);
};
Expand All @@ -2200,7 +2220,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
this.callStartTime = Date.now();

this.callLengthInterval = setInterval(() => {
this.emit(CallEvent.LengthChanged, Math.round((Date.now() - this.callStartTime!) / 1000));
this.emit(CallEvent.LengthChanged, Math.round((Date.now() - this.callStartTime!) / 1000), this);
}, CALL_LENGTH_INTERVAL);
}
} else if (this.peerConn?.iceConnectionState == "failed") {
Expand Down Expand Up @@ -2267,7 +2287,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
};

private onDataChannel = (ev: RTCDataChannelEvent): void => {
this.emit(CallEvent.DataChannel, ev.channel);
this.emit(CallEvent.DataChannel, ev.channel, this);
};

/**
Expand Down Expand Up @@ -2380,13 +2400,17 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
[ToDeviceMessageId]: uuidv4(),
};

this.emit(CallEvent.SendVoipEvent, {
type: "toDevice",
eventType,
userId: this.invitee || this.getOpponentMember()?.userId,
opponentDeviceId: this.opponentDeviceId,
content,
});
this.emit(
CallEvent.SendVoipEvent,
{
type: "toDevice",
eventType,
userId: this.invitee || this.getOpponentMember()?.userId,
opponentDeviceId: this.opponentDeviceId,
content,
},
this,
);

const userId = this.invitee || this.getOpponentMember()!.userId;
if (this.client.getUseE2eForGroupCall()) {
Expand Down Expand Up @@ -2415,13 +2439,17 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
});
}
} else {
this.emit(CallEvent.SendVoipEvent, {
type: "sendEvent",
eventType,
roomId: this.roomId,
content: realContent,
userId: this.invitee || this.getOpponentMember()?.userId,
});
this.emit(
CallEvent.SendVoipEvent,
{
type: "sendEvent",
eventType,
roomId: this.roomId,
content: realContent,
userId: this.invitee || this.getOpponentMember()?.userId,
},
this,
);

await this.client.sendEvent(this.roomId!, eventType, realContent);
}
Expand Down Expand Up @@ -2670,7 +2698,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
const code = CallErrorCode.SignallingFailed;
const message = "Signalling failed";

this.emit(CallEvent.Error, new CallError(code, message, <Error>error));
this.emit(CallEvent.Error, new CallError(code, message, <Error>error), this);
this.hangup(code, false);

return;
Expand Down
Loading