From 448a5c9a778d239a0f6c8b42c6865333e98e75bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 18 Aug 2022 10:42:03 +0200 Subject: [PATCH] Add screensharing tests (#2598) --- spec/test-utils/webrtc.ts | 34 ++++++++++++++ spec/unit/webrtc/groupCall.spec.ts | 73 ++++++++++++++++++++++++++++++ src/webrtc/groupCall.ts | 4 +- 3 files changed, 110 insertions(+), 1 deletion(-) diff --git a/spec/test-utils/webrtc.ts b/spec/test-utils/webrtc.ts index 28ee7e8a8..9ed142fcc 100644 --- a/spec/test-utils/webrtc.ts +++ b/spec/test-utils/webrtc.ts @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { IScreensharingOpts } from "../../src/webrtc/mediaHandler"; + export const DUMMY_SDP = ( "v=0\r\n" + "o=- 5022425983810148698 2 IN IP4 127.0.0.1\r\n" + @@ -75,6 +77,7 @@ export class MockRTCPeerConnection { private negotiationNeededListener: () => void; private needsNegotiation = false; localDescription: RTCSessionDescription; + signalingState: RTCSignalingState = "stable"; public static triggerAllNegotiations() { for (const inst of this.instances) { @@ -137,6 +140,26 @@ export class MockMediaStreamTrack { constructor(public readonly id: string, public readonly kind: "audio" | "video", public enabled = true) { } stop() { } + + listeners: [string, (...args: any[]) => any][] = []; + public isStopped = false; + + // XXX: Using EventTarget in jest doesn't seem to work, so we write our own + // implementation + dispatchEvent(eventType: string) { + this.listeners.forEach(([t, c]) => { + if (t !== eventType) return; + c(); + }); + } + 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; + }); + } } // XXX: Using EventTarget in jest doesn't seem to work, so we write our own @@ -200,6 +223,17 @@ export class MockMediaHandler { stopUserMediaStream(stream: MockMediaStream) { stream.isStopped = true; } + getScreensharingStream(opts?: IScreensharingOpts) { + const tracks = [new MockMediaStreamTrack("video_track", "video")]; + if (opts?.audio) tracks.push(new MockMediaStreamTrack("audio_track", "audio")); + + const stream = new MockMediaStream("mock_screen_stream_from_media_handler", tracks); + this.screensharingStreams.push(stream); + return stream; + } + stopScreensharingStream(stream: MockMediaStream) { + stream.isStopped = true; + } hasAudioDevice() { return true; } hasVideoDevice() { return true; } stopAllStreams() {} diff --git a/spec/unit/webrtc/groupCall.spec.ts b/spec/unit/webrtc/groupCall.spec.ts index 161062720..0423930ae 100644 --- a/spec/unit/webrtc/groupCall.spec.ts +++ b/spec/unit/webrtc/groupCall.spec.ts @@ -730,4 +730,77 @@ describe('Group Call', function() { expect(groupCall.calls).toEqual([newMockCall]); }); }); + + describe("screensharing", () => { + let mockClient: MatrixClient; + let room: Room; + let groupCall: GroupCall; + + beforeEach(async () => { + const typedMockClient = new MockCallMatrixClient( + FAKE_USER_ID_1, FAKE_DEVICE_ID_1, FAKE_SESSION_ID_1, + ); + mockClient = typedMockClient as unknown as MatrixClient; + + room = new Room(FAKE_ROOM_ID, mockClient, FAKE_USER_ID_1); + room.getMember = jest.fn().mockImplementation((userId) => ({ userId })); + room.currentState.getStateEvents = jest.fn().mockImplementation((type: EventType, userId: string) => { + return type === EventType.GroupCallMemberPrefix + ? FAKE_STATE_EVENTS.find(e => e.getStateKey() === userId) || FAKE_STATE_EVENTS + : { getContent: () => ([]) }; + }); + + groupCall = await createAndEnterGroupCall(mockClient, room); + }); + + it("sending screensharing stream", async () => { + const onNegotiationNeededArray = groupCall.calls.map(call => { + // @ts-ignore Mock + call.gotLocalOffer = jest.fn(); + // @ts-ignore Mock + return call.gotLocalOffer; + }); + + await groupCall.setScreensharingEnabled(true); + MockRTCPeerConnection.triggerAllNegotiations(); + + expect(groupCall.screenshareFeeds).toHaveLength(1); + groupCall.calls.forEach(c => { + expect(c.getLocalFeeds().find(f => f.purpose === SDPStreamMetadataPurpose.Screenshare)).toBeDefined(); + }); + onNegotiationNeededArray.forEach(f => expect(f).toHaveBeenCalled()); + + groupCall.terminate(); + }); + + it("receiving screensharing stream", async () => { + // It takes a bit of time for the calls to get created + await sleep(10); + + const call = groupCall.calls[0]; + call.getOpponentMember = () => ({ userId: call.invitee }) as RoomMember; + call.onNegotiateReceived({ + getContent: () => ({ + [SDPStreamMetadataKey]: { + "screensharing_stream": { + purpose: SDPStreamMetadataPurpose.Screenshare, + }, + }, + description: { + type: "offer", + sdp: "...", + }, + }), + } as MatrixEvent); + // @ts-ignore Mock + call.pushRemoteFeed(new MockMediaStream("screensharing_stream", [ + new MockMediaStreamTrack("video_track", "video"), + ])); + + expect(groupCall.screenshareFeeds).toHaveLength(1); + expect(groupCall.getScreenshareFeedByUserId(call.invitee)).toBeDefined(); + + groupCall.terminate(); + }); + }); }); diff --git a/src/webrtc/groupCall.ts b/src/webrtc/groupCall.ts index 879bc49be..6672dbd53 100644 --- a/src/webrtc/groupCall.ts +++ b/src/webrtc/groupCall.ts @@ -182,7 +182,7 @@ export class GroupCall extends TypedEventEmitter< private reEmitter: ReEmitter; private transmitTimer: ReturnType | null = null; private memberStateExpirationTimers: Map> = new Map(); - private resendMemberStateTimer: ReturnType | null = null; + private resendMemberStateTimer: ReturnType | null = null; constructor( private client: MatrixClient, @@ -698,6 +698,8 @@ export class GroupCall extends TypedEventEmitter< const res = await send(); + // Clear the old interval first, so that it isn't forgot + clearInterval(this.resendMemberStateTimer); // Resend the state event every so often so it doesn't become stale this.resendMemberStateTimer = setInterval(async () => { logger.log("Resending call member state");