You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-07-31 15:24:23 +03:00
Improve VoIP integrations testing (#2495)
This commit is contained in:
@ -236,4 +236,8 @@ export class TestClient {
|
||||
public isFallbackICEServerAllowed(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
public getUserId(): string {
|
||||
return this.userId;
|
||||
}
|
||||
}
|
||||
|
146
spec/test-utils/webrtc.ts
Normal file
146
spec/test-utils/webrtc.ts
Normal file
@ -0,0 +1,146 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export const DUMMY_SDP = (
|
||||
"v=0\r\n" +
|
||||
"o=- 5022425983810148698 2 IN IP4 127.0.0.1\r\n" +
|
||||
"s=-\r\nt=0 0\r\na=group:BUNDLE 0\r\n" +
|
||||
"a=msid-semantic: WMS h3wAi7s8QpiQMH14WG3BnDbmlOqo9I5ezGZA\r\n" +
|
||||
"m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126\r\n" +
|
||||
"c=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:hLDR\r\n" +
|
||||
"a=ice-pwd:bMGD9aOldHWiI+6nAq/IIlRw\r\n" +
|
||||
"a=ice-options:trickle\r\n" +
|
||||
"a=fingerprint:sha-256 E4:94:84:F9:4A:98:8A:56:F5:5F:FD:AF:72:B9:32:89:49:5C:4B:9A:" +
|
||||
"4A:15:8E:41:8A:F3:69:E4:39:52:DC:D6\r\n" +
|
||||
"a=setup:active\r\n" +
|
||||
"a=mid:0\r\n" +
|
||||
"a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n" +
|
||||
"a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n" +
|
||||
"a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n" +
|
||||
"a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\r\n" +
|
||||
"a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\n" +
|
||||
"a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\n" +
|
||||
"a=sendrecv\r\n" +
|
||||
"a=msid:h3wAi7s8QpiQMH14WG3BnDbmlOqo9I5ezGZA 4357098f-3795-4131-bff4-9ba9c0348c49\r\n" +
|
||||
"a=rtcp-mux\r\n" +
|
||||
"a=rtpmap:111 opus/48000/2\r\n" +
|
||||
"a=rtcp-fb:111 transport-cc\r\n" +
|
||||
"a=fmtp:111 minptime=10;useinbandfec=1\r\n" +
|
||||
"a=rtpmap:103 ISAC/16000\r\n" +
|
||||
"a=rtpmap:104 ISAC/32000\r\n" +
|
||||
"a=rtpmap:9 G722/8000\r\n" +
|
||||
"a=rtpmap:0 PCMU/8000\r\n" +
|
||||
"a=rtpmap:8 PCMA/8000\r\n" +
|
||||
"a=rtpmap:106 CN/32000\r\n" +
|
||||
"a=rtpmap:105 CN/16000\r\n" +
|
||||
"a=rtpmap:13 CN/8000\r\n" +
|
||||
"a=rtpmap:110 telephone-event/48000\r\n" +
|
||||
"a=rtpmap:112 telephone-event/32000\r\n" +
|
||||
"a=rtpmap:113 telephone-event/16000\r\n" +
|
||||
"a=rtpmap:126 telephone-event/8000\r\n" +
|
||||
"a=ssrc:3619738545 cname:2RWtmqhXLdoF4sOi\r\n"
|
||||
);
|
||||
|
||||
export class MockRTCPeerConnection {
|
||||
localDescription: RTCSessionDescription;
|
||||
|
||||
constructor() {
|
||||
this.localDescription = {
|
||||
sdp: DUMMY_SDP,
|
||||
type: 'offer',
|
||||
toJSON: function() { },
|
||||
};
|
||||
}
|
||||
|
||||
addEventListener() { }
|
||||
createDataChannel(label: string, opts: RTCDataChannelInit) { return { label, ...opts }; }
|
||||
createOffer() {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
setRemoteDescription() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
setLocalDescription() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
close() { }
|
||||
getStats() { return []; }
|
||||
addTrack(track: MockMediaStreamTrack) { return new MockRTCRtpSender(track); }
|
||||
}
|
||||
|
||||
export class MockRTCRtpSender {
|
||||
constructor(public track: MockMediaStreamTrack) { }
|
||||
|
||||
replaceTrack(track: MockMediaStreamTrack) { this.track = track; }
|
||||
}
|
||||
|
||||
export class MockMediaStreamTrack {
|
||||
constructor(public readonly id: string, public readonly kind: "audio" | "video", public enabled = true) { }
|
||||
|
||||
stop() { }
|
||||
}
|
||||
|
||||
// XXX: Using EventTarget in jest doesn't seem to work, so we write our own
|
||||
// implementation
|
||||
export class MockMediaStream {
|
||||
constructor(
|
||||
public id: string,
|
||||
private tracks: MockMediaStreamTrack[] = [],
|
||||
) {}
|
||||
|
||||
listeners: [string, (...args: any[]) => any][] = [];
|
||||
|
||||
dispatchEvent(eventType: string) {
|
||||
this.listeners.forEach(([t, c]) => {
|
||||
if (t !== eventType) return;
|
||||
c();
|
||||
});
|
||||
}
|
||||
getTracks() { return this.tracks; }
|
||||
getAudioTracks() { return this.tracks.filter((track) => track.kind === "audio"); }
|
||||
getVideoTracks() { return this.tracks.filter((track) => track.kind === "video"); }
|
||||
addEventListener(eventType: string, callback: (...args: any[]) => any) {
|
||||
this.listeners.push([eventType, callback]);
|
||||
}
|
||||
removeEventListener(eventType: string, callback: (...args: any[]) => any) {
|
||||
this.listeners.filter(([t, c]) => {
|
||||
return t !== eventType || c !== callback;
|
||||
});
|
||||
}
|
||||
addTrack(track: MockMediaStreamTrack) {
|
||||
this.tracks.push(track);
|
||||
this.dispatchEvent("addtrack");
|
||||
}
|
||||
removeTrack(track: MockMediaStreamTrack) { this.tracks.splice(this.tracks.indexOf(track), 1); }
|
||||
}
|
||||
|
||||
export class MockMediaDeviceInfo {
|
||||
constructor(
|
||||
public kind: "audio" | "video",
|
||||
) { }
|
||||
}
|
||||
|
||||
export class MockMediaHandler {
|
||||
getUserMediaStream(audio: boolean, video: boolean) {
|
||||
const tracks = [];
|
||||
if (audio) tracks.push(new MockMediaStreamTrack("audio_track", "audio"));
|
||||
if (video) tracks.push(new MockMediaStreamTrack("video_track", "video"));
|
||||
|
||||
return new MockMediaStream("mock_stream_from_media_handler", tracks);
|
||||
}
|
||||
stopUserMediaStream() { }
|
||||
hasAudioDevice() { return true; }
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2020 - 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -15,119 +15,25 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { TestClient } from '../../TestClient';
|
||||
import { MatrixCall, CallErrorCode, CallEvent, supportsMatrixCall } from '../../../src/webrtc/call';
|
||||
import { MatrixCall, CallErrorCode, CallEvent, supportsMatrixCall, CallType } from '../../../src/webrtc/call';
|
||||
import { SDPStreamMetadataKey, SDPStreamMetadataPurpose } from '../../../src/webrtc/callEventTypes';
|
||||
import { RoomMember } from "../../../src";
|
||||
import {
|
||||
DUMMY_SDP,
|
||||
MockMediaHandler,
|
||||
MockMediaStream,
|
||||
MockMediaStreamTrack,
|
||||
MockMediaDeviceInfo,
|
||||
MockRTCPeerConnection,
|
||||
} from "../../test-utils/webrtc";
|
||||
import { CallFeed } from "../../../src/webrtc/callFeed";
|
||||
|
||||
const DUMMY_SDP = (
|
||||
"v=0\r\n" +
|
||||
"o=- 5022425983810148698 2 IN IP4 127.0.0.1\r\n" +
|
||||
"s=-\r\nt=0 0\r\na=group:BUNDLE 0\r\n" +
|
||||
"a=msid-semantic: WMS h3wAi7s8QpiQMH14WG3BnDbmlOqo9I5ezGZA\r\n" +
|
||||
"m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126\r\n" +
|
||||
"c=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:hLDR\r\n" +
|
||||
"a=ice-pwd:bMGD9aOldHWiI+6nAq/IIlRw\r\n" +
|
||||
"a=ice-options:trickle\r\n" +
|
||||
"a=fingerprint:sha-256 E4:94:84:F9:4A:98:8A:56:F5:5F:FD:AF:72:B9:32:89:49:5C:4B:9A:" +
|
||||
"4A:15:8E:41:8A:F3:69:E4:39:52:DC:D6\r\n" +
|
||||
"a=setup:active\r\n" +
|
||||
"a=mid:0\r\n" +
|
||||
"a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n" +
|
||||
"a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n" +
|
||||
"a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n" +
|
||||
"a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\r\n" +
|
||||
"a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\n" +
|
||||
"a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\n" +
|
||||
"a=sendrecv\r\n" +
|
||||
"a=msid:h3wAi7s8QpiQMH14WG3BnDbmlOqo9I5ezGZA 4357098f-3795-4131-bff4-9ba9c0348c49\r\n" +
|
||||
"a=rtcp-mux\r\n" +
|
||||
"a=rtpmap:111 opus/48000/2\r\n" +
|
||||
"a=rtcp-fb:111 transport-cc\r\n" +
|
||||
"a=fmtp:111 minptime=10;useinbandfec=1\r\n" +
|
||||
"a=rtpmap:103 ISAC/16000\r\n" +
|
||||
"a=rtpmap:104 ISAC/32000\r\n" +
|
||||
"a=rtpmap:9 G722/8000\r\n" +
|
||||
"a=rtpmap:0 PCMU/8000\r\n" +
|
||||
"a=rtpmap:8 PCMA/8000\r\n" +
|
||||
"a=rtpmap:106 CN/32000\r\n" +
|
||||
"a=rtpmap:105 CN/16000\r\n" +
|
||||
"a=rtpmap:13 CN/8000\r\n" +
|
||||
"a=rtpmap:110 telephone-event/48000\r\n" +
|
||||
"a=rtpmap:112 telephone-event/32000\r\n" +
|
||||
"a=rtpmap:113 telephone-event/16000\r\n" +
|
||||
"a=rtpmap:126 telephone-event/8000\r\n" +
|
||||
"a=ssrc:3619738545 cname:2RWtmqhXLdoF4sOi\r\n"
|
||||
);
|
||||
const startVoiceCall = async (client: TestClient, call: MatrixCall): Promise<void> => {
|
||||
const callPromise = call.placeVoiceCall();
|
||||
await client.httpBackend.flush("");
|
||||
await callPromise;
|
||||
|
||||
class MockRTCPeerConnection {
|
||||
localDescription: RTCSessionDescription;
|
||||
|
||||
constructor() {
|
||||
this.localDescription = {
|
||||
sdp: DUMMY_SDP,
|
||||
type: 'offer',
|
||||
toJSON: function() {},
|
||||
call.getOpponentMember = jest.fn().mockReturnValue({ userId: "@bob:bar.uk" });
|
||||
};
|
||||
}
|
||||
|
||||
addEventListener() {}
|
||||
createOffer() {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
setRemoteDescription() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
setLocalDescription() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
close() {}
|
||||
getStats() { return []; }
|
||||
addTrack(track: MockMediaStreamTrack) {return new MockRTCRtpSender(track);}
|
||||
}
|
||||
|
||||
class MockRTCRtpSender {
|
||||
constructor(public track: MockMediaStreamTrack) {}
|
||||
|
||||
replaceTrack(track: MockMediaStreamTrack) {this.track = track;}
|
||||
}
|
||||
|
||||
class MockMediaStreamTrack {
|
||||
constructor(public readonly id: string, public readonly kind: "audio" | "video", public enabled = true) {}
|
||||
|
||||
stop() {}
|
||||
}
|
||||
|
||||
class MockMediaStream {
|
||||
constructor(
|
||||
public id: string,
|
||||
private tracks: MockMediaStreamTrack[] = [],
|
||||
) {}
|
||||
|
||||
getTracks() { return this.tracks; }
|
||||
getAudioTracks() { return this.tracks.filter((track) => track.kind === "audio"); }
|
||||
getVideoTracks() { return this.tracks.filter((track) => track.kind === "video"); }
|
||||
addEventListener() {}
|
||||
removeEventListener() { }
|
||||
addTrack(track: MockMediaStreamTrack) {this.tracks.push(track);}
|
||||
removeTrack(track: MockMediaStreamTrack) {this.tracks.splice(this.tracks.indexOf(track), 1);}
|
||||
}
|
||||
|
||||
class MockMediaDeviceInfo {
|
||||
constructor(
|
||||
public kind: "audio" | "video",
|
||||
) {}
|
||||
}
|
||||
|
||||
class MockMediaHandler {
|
||||
getUserMediaStream(audio: boolean, video: boolean) {
|
||||
const tracks = [];
|
||||
if (audio) tracks.push(new MockMediaStreamTrack("audio_track", "audio"));
|
||||
if (video) tracks.push(new MockMediaStreamTrack("video_track", "video"));
|
||||
|
||||
return new MockMediaStream("mock_stream_from_media_handler", tracks);
|
||||
}
|
||||
stopUserMediaStream() {}
|
||||
}
|
||||
|
||||
describe('Call', function() {
|
||||
let client;
|
||||
@ -185,9 +91,8 @@ describe('Call', function() {
|
||||
});
|
||||
|
||||
it('should ignore candidate events from non-matching party ID', async function() {
|
||||
const callPromise = call.placeVoiceCall();
|
||||
await client.httpBackend.flush();
|
||||
await callPromise;
|
||||
await startVoiceCall(client, call);
|
||||
|
||||
await call.onAnswerReceived({
|
||||
getContent: () => {
|
||||
return {
|
||||
@ -241,9 +146,7 @@ describe('Call', function() {
|
||||
});
|
||||
|
||||
it('should add candidates received before answer if party ID is correct', async function() {
|
||||
const callPromise = call.placeVoiceCall();
|
||||
await client.httpBackend.flush();
|
||||
await callPromise;
|
||||
await startVoiceCall(client, call);
|
||||
call.peerConn.addIceCandidate = jest.fn();
|
||||
|
||||
call.onRemoteIceCandidatesReceived({
|
||||
@ -301,9 +204,7 @@ describe('Call', function() {
|
||||
});
|
||||
|
||||
it('should map asserted identity messages to remoteAssertedIdentity', async function() {
|
||||
const callPromise = call.placeVoiceCall();
|
||||
await client.httpBackend.flush();
|
||||
await callPromise;
|
||||
await startVoiceCall(client, call);
|
||||
await call.onAnswerReceived({
|
||||
getContent: () => {
|
||||
return {
|
||||
@ -345,13 +246,7 @@ describe('Call', function() {
|
||||
});
|
||||
|
||||
it("should map SDPStreamMetadata to feeds", async () => {
|
||||
const callPromise = call.placeVoiceCall();
|
||||
await client.httpBackend.flush();
|
||||
await callPromise;
|
||||
|
||||
call.getOpponentMember = () => {
|
||||
return { userId: "@bob:bar.uk" };
|
||||
};
|
||||
await startVoiceCall(client, call);
|
||||
|
||||
await call.onAnswerReceived({
|
||||
getContent: () => {
|
||||
@ -389,13 +284,7 @@ describe('Call', function() {
|
||||
});
|
||||
|
||||
it("should fallback to replaceTrack() if the other side doesn't support SPDStreamMetadata", async () => {
|
||||
const callPromise = call.placeVoiceCall();
|
||||
await client.httpBackend.flush();
|
||||
await callPromise;
|
||||
|
||||
call.getOpponentMember = () => {
|
||||
return { userId: "@bob:bar.uk" } as RoomMember;
|
||||
};
|
||||
await startVoiceCall(client, call);
|
||||
|
||||
await call.onAnswerReceived({
|
||||
getContent: () => {
|
||||
@ -438,9 +327,7 @@ describe('Call', function() {
|
||||
),
|
||||
);
|
||||
|
||||
const callPromise = call.placeVideoCall();
|
||||
await client.httpBackend.flush();
|
||||
await callPromise;
|
||||
await startVoiceCall(client, call);
|
||||
|
||||
await call.onAnswerReceived({
|
||||
getContent: () => {
|
||||
@ -476,9 +363,7 @@ describe('Call', function() {
|
||||
});
|
||||
|
||||
it("should handle upgrade to video call", async () => {
|
||||
const callPromise = call.placeVoiceCall();
|
||||
await client.httpBackend.flush();
|
||||
await callPromise;
|
||||
await startVoiceCall(client, call);
|
||||
|
||||
await call.onAnswerReceived({
|
||||
getContent: () => {
|
||||
@ -506,6 +391,338 @@ describe('Call', function() {
|
||||
}).track.id).toBe("video_track");
|
||||
});
|
||||
|
||||
describe("should handle stream replacement", () => {
|
||||
it("with both purpose and id", async () => {
|
||||
await startVoiceCall(client, call);
|
||||
|
||||
call.updateRemoteSDPStreamMetadata({
|
||||
"remote_stream1": {
|
||||
purpose: SDPStreamMetadataPurpose.Usermedia,
|
||||
},
|
||||
});
|
||||
call.pushRemoteFeed(new MockMediaStream("remote_stream1", []));
|
||||
const feed = call.getFeeds().find((feed) => feed.stream.id === "remote_stream1");
|
||||
|
||||
call.updateRemoteSDPStreamMetadata({
|
||||
"remote_stream2": {
|
||||
purpose: SDPStreamMetadataPurpose.Usermedia,
|
||||
},
|
||||
});
|
||||
call.pushRemoteFeed(new MockMediaStream("remote_stream2", []));
|
||||
|
||||
expect(feed?.stream?.id).toBe("remote_stream2");
|
||||
});
|
||||
|
||||
it("with just purpose", async () => {
|
||||
await startVoiceCall(client, call);
|
||||
|
||||
call.updateRemoteSDPStreamMetadata({
|
||||
"remote_stream1": {
|
||||
purpose: SDPStreamMetadataPurpose.Usermedia,
|
||||
},
|
||||
});
|
||||
call.pushRemoteFeed(new MockMediaStream("remote_stream1", []));
|
||||
const feed = call.getFeeds().find((feed) => feed.stream.id === "remote_stream1");
|
||||
|
||||
call.updateRemoteSDPStreamMetadata({
|
||||
"remote_stream2": {
|
||||
purpose: SDPStreamMetadataPurpose.Usermedia,
|
||||
},
|
||||
});
|
||||
call.pushRemoteFeed(new MockMediaStream("remote_stream2", []));
|
||||
|
||||
expect(feed?.stream?.id).toBe("remote_stream2");
|
||||
});
|
||||
|
||||
it("should not replace purpose is different", async () => {
|
||||
await startVoiceCall(client, call);
|
||||
|
||||
call.updateRemoteSDPStreamMetadata({
|
||||
"remote_stream1": {
|
||||
purpose: SDPStreamMetadataPurpose.Usermedia,
|
||||
},
|
||||
});
|
||||
call.pushRemoteFeed(new MockMediaStream("remote_stream1", []));
|
||||
const feed = call.getFeeds().find((feed) => feed.stream.id === "remote_stream1");
|
||||
|
||||
call.updateRemoteSDPStreamMetadata({
|
||||
"remote_stream2": {
|
||||
purpose: SDPStreamMetadataPurpose.Screenshare,
|
||||
},
|
||||
});
|
||||
call.pushRemoteFeed(new MockMediaStream("remote_stream2", []));
|
||||
|
||||
expect(feed?.stream?.id).toBe("remote_stream1");
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle SDPStreamMetadata changes", async () => {
|
||||
await startVoiceCall(client, call);
|
||||
|
||||
call.updateRemoteSDPStreamMetadata({
|
||||
"remote_stream": {
|
||||
purpose: SDPStreamMetadataPurpose.Usermedia,
|
||||
audio_muted: false,
|
||||
video_muted: false,
|
||||
},
|
||||
});
|
||||
call.pushRemoteFeed(new MockMediaStream("remote_stream", []));
|
||||
const feed = call.getFeeds().find((feed) => feed.stream.id === "remote_stream");
|
||||
|
||||
call.onSDPStreamMetadataChangedReceived({
|
||||
getContent: () => ({
|
||||
[SDPStreamMetadataKey]: {
|
||||
"remote_stream": {
|
||||
purpose: SDPStreamMetadataPurpose.Screenshare,
|
||||
audio_muted: true,
|
||||
video_muted: true,
|
||||
id: "feed_id2",
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
expect(feed?.purpose).toBe(SDPStreamMetadataPurpose.Screenshare);
|
||||
expect(feed?.audioMuted).toBe(true);
|
||||
expect(feed?.videoMuted).toBe(true);
|
||||
});
|
||||
|
||||
it("should choose opponent member", async () => {
|
||||
const callPromise = call.placeVoiceCall();
|
||||
await client.httpBackend.flush();
|
||||
await callPromise;
|
||||
|
||||
const opponentMember = {
|
||||
roomId: call.roomId,
|
||||
userId: "opponentUserId",
|
||||
};
|
||||
const opponentCaps = {
|
||||
"m.call.transferee": true,
|
||||
"m.call.dtmf": false,
|
||||
};
|
||||
call.chooseOpponent({
|
||||
getContent: () => ({
|
||||
version: 1,
|
||||
party_id: "party_id",
|
||||
capabilities: opponentCaps,
|
||||
}),
|
||||
sender: opponentMember,
|
||||
});
|
||||
|
||||
expect(call.getOpponentMember()).toBe(opponentMember);
|
||||
expect(call.opponentPartyId).toBe("party_id");
|
||||
expect(call.opponentCaps).toBe(opponentCaps);
|
||||
expect(call.opponentCanBeTransferred()).toBe(true);
|
||||
expect(call.opponentSupportsDTMF()).toBe(false);
|
||||
});
|
||||
|
||||
describe("should deduce the call type correctly", () => {
|
||||
it("if no video", async () => {
|
||||
call.getOpponentMember = jest.fn().mockReturnValue({ userId: "@bob:bar.uk" });
|
||||
|
||||
call.pushRemoteFeed(new MockMediaStream("remote_stream1", []));
|
||||
expect(call.type).toBe(CallType.Voice);
|
||||
});
|
||||
|
||||
it("if remote video", async () => {
|
||||
call.getOpponentMember = jest.fn().mockReturnValue({ userId: "@bob:bar.uk" });
|
||||
|
||||
call.pushRemoteFeed(new MockMediaStream("remote_stream1", [new MockMediaStreamTrack("track_id", "video")]));
|
||||
expect(call.type).toBe(CallType.Video);
|
||||
});
|
||||
|
||||
it("if local video", async () => {
|
||||
call.getOpponentMember = jest.fn().mockReturnValue({ userId: "@bob:bar.uk" });
|
||||
|
||||
call.pushNewLocalFeed(
|
||||
new MockMediaStream("remote_stream1", [new MockMediaStreamTrack("track_id", "video")]),
|
||||
SDPStreamMetadataPurpose.Usermedia,
|
||||
false,
|
||||
);
|
||||
expect(call.type).toBe(CallType.Video);
|
||||
});
|
||||
});
|
||||
|
||||
it("should correctly generate local SDPStreamMetadata", async () => {
|
||||
const callPromise = call.placeCallWithCallFeeds([new CallFeed({
|
||||
client,
|
||||
// @ts-ignore Mock
|
||||
stream: new MockMediaStream("local_stream1", [new MockMediaStreamTrack("track_id", "audio")]),
|
||||
roomId: call.roomId,
|
||||
userId: client.getUserId(),
|
||||
purpose: SDPStreamMetadataPurpose.Usermedia,
|
||||
audioMuted: false,
|
||||
videoMuted: false,
|
||||
})]);
|
||||
await client.httpBackend.flush();
|
||||
await callPromise;
|
||||
call.getOpponentMember = jest.fn().mockReturnValue({ userId: "@bob:bar.uk" });
|
||||
|
||||
call.pushNewLocalFeed(
|
||||
new MockMediaStream("local_stream2", [new MockMediaStreamTrack("track_id", "video")]),
|
||||
SDPStreamMetadataPurpose.Screenshare, "feed_id2",
|
||||
);
|
||||
await call.setMicrophoneMuted(true);
|
||||
|
||||
expect(call.getLocalSDPStreamMetadata()).toStrictEqual({
|
||||
"local_stream1": {
|
||||
"purpose": SDPStreamMetadataPurpose.Usermedia,
|
||||
"audio_muted": true,
|
||||
"video_muted": true,
|
||||
},
|
||||
"local_stream2": {
|
||||
"purpose": SDPStreamMetadataPurpose.Screenshare,
|
||||
"audio_muted": true,
|
||||
"video_muted": false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("feed and stream getters return correctly", async () => {
|
||||
const localUsermediaStream = new MockMediaStream("local_usermedia_stream_id", []);
|
||||
const localScreensharingStream = new MockMediaStream("local_screensharing_stream_id", []);
|
||||
const remoteUsermediaStream = new MockMediaStream("remote_usermedia_stream_id", []);
|
||||
const remoteScreensharingStream = new MockMediaStream("remote_screensharing_stream_id", []);
|
||||
|
||||
const callPromise = call.placeCallWithCallFeeds([
|
||||
new CallFeed({
|
||||
client,
|
||||
userId: client.getUserId(),
|
||||
// @ts-ignore Mock
|
||||
stream: localUsermediaStream,
|
||||
purpose: SDPStreamMetadataPurpose.Usermedia,
|
||||
id: "local_usermedia_feed_id",
|
||||
audioMuted: false,
|
||||
videoMuted: false,
|
||||
}),
|
||||
new CallFeed({
|
||||
client,
|
||||
userId: client.getUserId(),
|
||||
// @ts-ignore Mock
|
||||
stream: localScreensharingStream,
|
||||
purpose: SDPStreamMetadataPurpose.Screenshare,
|
||||
id: "local_screensharing_feed_id",
|
||||
audioMuted: false,
|
||||
videoMuted: false,
|
||||
}),
|
||||
]);
|
||||
await client.httpBackend.flush();
|
||||
await callPromise;
|
||||
call.getOpponentMember = jest.fn().mockReturnValue({ userId: "@bob:bar.uk" });
|
||||
|
||||
call.updateRemoteSDPStreamMetadata({
|
||||
"remote_usermedia_stream_id": {
|
||||
purpose: SDPStreamMetadataPurpose.Usermedia,
|
||||
id: "remote_usermedia_feed_id",
|
||||
audio_muted: false,
|
||||
video_muted: false,
|
||||
},
|
||||
"remote_screensharing_stream_id": {
|
||||
purpose: SDPStreamMetadataPurpose.Screenshare,
|
||||
id: "remote_screensharing_feed_id",
|
||||
audio_muted: false,
|
||||
video_muted: false,
|
||||
},
|
||||
});
|
||||
call.pushRemoteFeed(remoteUsermediaStream);
|
||||
call.pushRemoteFeed(remoteScreensharingStream);
|
||||
|
||||
expect(call.localUsermediaFeed.stream).toBe(localUsermediaStream);
|
||||
expect(call.localUsermediaStream).toBe(localUsermediaStream);
|
||||
expect(call.localScreensharingFeed.stream).toBe(localScreensharingStream);
|
||||
expect(call.localScreensharingStream).toBe(localScreensharingStream);
|
||||
expect(call.remoteUsermediaFeed.stream).toBe(remoteUsermediaStream);
|
||||
expect(call.remoteUsermediaStream).toBe(remoteUsermediaStream);
|
||||
expect(call.remoteScreensharingFeed.stream).toBe(remoteScreensharingStream);
|
||||
expect(call.remoteScreensharingStream).toBe(remoteScreensharingStream);
|
||||
expect(call.hasRemoteUserMediaAudioTrack).toBe(false);
|
||||
});
|
||||
|
||||
it("should end call after receiving a select event with a different party id", async () => {
|
||||
const callPromise = call.initWithInvite({
|
||||
getContent: () => ({
|
||||
version: 1,
|
||||
call_id: "call_id",
|
||||
party_id: "remote_party_id",
|
||||
offer: {
|
||||
sdp: DUMMY_SDP,
|
||||
},
|
||||
}),
|
||||
getLocalAge: () => null,
|
||||
});
|
||||
call.feeds.push(new CallFeed({
|
||||
client,
|
||||
userId: "remote_user_id",
|
||||
// @ts-ignore Mock
|
||||
stream: new MockMediaStream("remote_stream_id", [new MockMediaStreamTrack("remote_tack_id")]),
|
||||
id: "remote_feed_id",
|
||||
purpose: SDPStreamMetadataPurpose.Usermedia,
|
||||
}));
|
||||
await client.httpBackend.flush();
|
||||
await callPromise;
|
||||
|
||||
const callHangupCallback = jest.fn();
|
||||
call.on(CallEvent.Hangup, callHangupCallback);
|
||||
|
||||
await call.onSelectAnswerReceived({
|
||||
getContent: () => ({
|
||||
version: 1,
|
||||
call_id: call.callId,
|
||||
party_id: 'party_id',
|
||||
selected_party_id: "different_party_id",
|
||||
}),
|
||||
});
|
||||
|
||||
expect(callHangupCallback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("turn servers", () => {
|
||||
it("should fallback if allowed", async () => {
|
||||
client.client.isFallbackICEServerAllowed = () => true;
|
||||
const localCall = new MatrixCall({
|
||||
client: client.client,
|
||||
roomId: '!room_id',
|
||||
});
|
||||
|
||||
expect((localCall as any).turnServers).toStrictEqual([{ urls: ["stun:turn.matrix.org"] }]);
|
||||
});
|
||||
|
||||
it("should not fallback if not allowed", async () => {
|
||||
client.client.isFallbackICEServerAllowed = () => false;
|
||||
const localCall = new MatrixCall({
|
||||
client: client.client,
|
||||
roomId: '!room_id',
|
||||
});
|
||||
|
||||
expect((localCall as any).turnServers).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("should not fallback if we supplied turn servers", async () => {
|
||||
client.client.isFallbackICEServerAllowed = () => true;
|
||||
const turnServers = [{ urls: ["turn.server.org"] }];
|
||||
const localCall = new MatrixCall({
|
||||
client: client.client,
|
||||
roomId: '!room_id',
|
||||
turnServers,
|
||||
});
|
||||
|
||||
expect((localCall as any).turnServers).toStrictEqual(turnServers);
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle creating a data channel", async () => {
|
||||
await startVoiceCall(client, call);
|
||||
|
||||
const dataChannelCallback = jest.fn();
|
||||
call.on(CallEvent.DataChannel, dataChannelCallback);
|
||||
|
||||
const dataChannel = call.createDataChannel("data_channel_label", { id: 123 });
|
||||
|
||||
expect(dataChannelCallback).toHaveBeenCalledWith(dataChannel);
|
||||
expect(dataChannel.label).toBe("data_channel_label");
|
||||
expect(dataChannel.id).toBe(123);
|
||||
});
|
||||
|
||||
describe("supportsMatrixCall", () => {
|
||||
it("should return true when the environment is right", () => {
|
||||
expect(supportsMatrixCall()).toBe(true);
|
||||
|
61
spec/unit/webrtc/callFeed.spec.ts
Normal file
61
spec/unit/webrtc/callFeed.spec.ts
Normal file
@ -0,0 +1,61 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { SDPStreamMetadataPurpose } from "../../../src/webrtc/callEventTypes";
|
||||
import { CallFeed, CallFeedEvent } from "../../../src/webrtc/callFeed";
|
||||
import { MockMediaStream, MockMediaStreamTrack } from "../../test-utils/webrtc";
|
||||
import { TestClient } from "../../TestClient";
|
||||
|
||||
describe("CallFeed", () => {
|
||||
const roomId = "room_id";
|
||||
|
||||
let client;
|
||||
|
||||
beforeEach(() => {
|
||||
client = new TestClient("@alice:foo", "somedevice", "token", undefined, {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
client.stop();
|
||||
});
|
||||
|
||||
it("should handle stream replacement", () => {
|
||||
const feedNewStreamCallback = jest.fn();
|
||||
const feed = new CallFeed({
|
||||
client,
|
||||
roomId,
|
||||
userId: "user1",
|
||||
// @ts-ignore Mock
|
||||
stream: new MockMediaStream("stream1"),
|
||||
id: "id",
|
||||
purpose: SDPStreamMetadataPurpose.Usermedia,
|
||||
audioMuted: false,
|
||||
videoMuted: false,
|
||||
});
|
||||
feed.on(CallFeedEvent.NewStream, feedNewStreamCallback);
|
||||
|
||||
const replacementStream = new MockMediaStream("stream2");
|
||||
// @ts-ignore Mock
|
||||
feed.setNewStream(replacementStream);
|
||||
expect(feedNewStreamCallback).toHaveBeenCalledWith(replacementStream);
|
||||
expect(feed.stream).toBe(replacementStream);
|
||||
|
||||
feedNewStreamCallback.mockReset();
|
||||
|
||||
replacementStream.addTrack(new MockMediaStreamTrack("track_id", "audio"));
|
||||
expect(feedNewStreamCallback).toHaveBeenCalledWith(replacementStream);
|
||||
});
|
||||
});
|
@ -1518,7 +1518,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
|
||||
if (selectedPartyId !== this.ourPartyId) {
|
||||
logger.info(`Got select_answer for party ID ${selectedPartyId}: we are party ID ${this.ourPartyId}.`);
|
||||
// The other party has picked somebody else's answer
|
||||
this.terminate(CallParty.Remote, CallErrorCode.AnsweredElsewhere, true);
|
||||
await this.terminate(CallParty.Remote, CallErrorCode.AnsweredElsewhere, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user