UNPKG

@epicgames-ps/lib-pixelstreamingfrontend-ue5.4

Version:
625 lines (479 loc) 30.2 kB
import { mockRTCRtpReceiver, unmockRTCRtpReceiver } from '../__test__/mockRTCRtpReceiver'; import { Config, NumericParameters, } from '../Config/Config'; import { PixelStreaming } from './PixelStreaming'; import { SettingsChangedEvent, StreamerListMessageEvent, WebRtcConnectedEvent, WebRtcSdpEvent } from '../Util/EventEmitter'; import { mockWebSocket, MockWebSocketSpyFunctions, MockWebSocketTriggerFunctions, unmockWebSocket } from '../__test__/mockWebSocket'; import { MessageRecvTypes } from '../WebSockets/MessageReceive'; import { mockRTCPeerConnection, MockRTCPeerConnectionSpyFunctions, MockRTCPeerConnectionTriggerFunctions, unmockRTCPeerConnection } from '../__test__/mockRTCPeerConnection'; import { mockHTMLMediaElement, mockMediaStream, unmockMediaStream } from '../__test__/mockMediaStream'; import { InitialSettings } from '../DataChannel/InitialSettings'; const flushPromises = () => new Promise(jest.requireActual("timers").setImmediate); describe('PixelStreaming', () => { let webSocketSpyFunctions: MockWebSocketSpyFunctions; let webSocketTriggerFunctions: MockWebSocketTriggerFunctions; let rtcPeerConnectionSpyFunctions: MockRTCPeerConnectionSpyFunctions; let rtcPeerConnectionTriggerFunctions: MockRTCPeerConnectionTriggerFunctions; const mockSignallingUrl = 'ws://localhost:24680/'; const streamerId = "MOCK_PIXEL_STREAMING"; const streamerIdList = [streamerId]; const sdp = "v=0\r\no=- 974006863270230083 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0 1 2\r\na=extmap-allow-mixed\r\na=msid-semantic: WMS pixelstreaming_audio_stream_id pixelstreaming_video_stream_id\r\nm=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:+JE1\r\na=ice-pwd:R2dKmHqM47E++7TRKKkHMyHj\r\na=ice-options:trickle\r\na=fingerprint:sha-256 20:EE:85:F0:DA:F4:90:F3:0D:13:2E:A9:1E:36:8C:81:E1:BD:38:78:20:AA:38:F3:FC:65:3F:8E:06:1D:A7:53\r\na=setup:actpass\r\na=mid:0\r\na=extmap:1 urn:ietf:params:rtp-hdrext:toffset\r\na=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=extmap:3 urn:3gpp:video-orientation\r\na=extmap:4 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\na=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay\r\na=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type\r\na=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing\r\na=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space\r\na=extmap:9 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\na=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\na=sendonly\r\na=msid:pixelstreaming_video_stream_id pixelstreaming_video_track_label\r\na=rtcp-mux\r\na=rtcp-rsize\r\na=rtpmap:96 H264/90000\r\na=rtcp-fb:96 goog-remb\r\na=rtcp-fb:96 transport-cc\r\na=rtcp-fb:96 ccm fir\r\na=rtcp-fb:96 nack\r\na=rtcp-fb:96 nack pli\r\na=fmtp:96 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\na=rtpmap:97 rtx/90000\r\na=fmtp:97 apt=96\r\na=rtpmap:98 H264/90000\r\na=rtcp-fb:98 goog-remb\r\na=rtcp-fb:98 transport-cc\r\na=rtcp-fb:98 ccm fir\r\na=rtcp-fb:98 nack\r\na=rtcp-fb:98 nack pli\r\na=fmtp:98 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f\r\na=rtpmap:99 rtx/90000\r\na=fmtp:99 apt=98\r\na=rtpmap:100 red/90000\r\na=rtpmap:101 rtx/90000\r\na=fmtp:101 apt=100\r\na=rtpmap:102 ulpfec/90000\r\na=ssrc-group:FID 3702690738 1574960745\r\na=ssrc:3702690738 cname:I/iLZxsY4mZ0aoNG\r\na=ssrc:3702690738 msid:pixelstreaming_video_stream_id pixelstreaming_video_track_label\r\na=ssrc:3702690738 mslabel:pixelstreaming_video_stream_id\r\na=ssrc:3702690738 label:pixelstreaming_video_track_label\r\na=ssrc:1574960745 cname:I/iLZxsY4mZ0aoNG\r\na=ssrc:1574960745 msid:pixelstreaming_video_stream_id pixelstreaming_video_track_label\r\na=ssrc:1574960745 mslabel:pixelstreaming_video_stream_id\r\na=ssrc:1574960745 label:pixelstreaming_video_track_label\r\nm=audio 9 UDP/TLS/RTP/SAVPF 111 63 110\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:+JE1\r\na=ice-pwd:R2dKmHqM47E++7TRKKkHMyHj\r\na=ice-options:trickle\r\na=fingerprint:sha-256 20:EE:85:F0:DA:F4:90:F3:0D:13:2E:A9:1E:36:8C:81:E1:BD:38:78:20:AA:38:F3:FC:65:3F:8E:06:1D:A7:53\r\na=setup:actpass\r\na=mid:1\r\na=extmap:14 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=extmap:4 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\na=extmap:9 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\na=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\na=sendrecv\r\na=msid:pixelstreaming_audio_stream_id pixelstreaming_audio_track_label\r\na=rtcp-mux\r\na=rtpmap:111 opus/48000/2\r\na=rtcp-fb:111 transport-cc\r\na=fmtp:111 maxaveragebitrate=510000;maxplaybackrate=48000;minptime=3;sprop-stereo=1;stereo=1;usedtx=0;useinbandfec=1\r\na=rtpmap:63 red/48000/2\r\na=fmtp:63 111/111\r\na=rtpmap:110 telephone-event/48000\r\na=maxptime:120\r\na=ptime:20\r\na=ssrc:2587776314 cname:I/iLZxsY4mZ0aoNG\r\na=ssrc:2587776314 msid:pixelstreaming_audio_stream_id pixelstreaming_audio_track_label\r\na=ssrc:2587776314 mslabel:pixelstreaming_audio_stream_id\r\na=ssrc:2587776314 label:pixelstreaming_audio_track_label\r\nm=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\nc=IN IP4 0.0.0.0\r\na=ice-ufrag:+JE1\r\na=ice-pwd:R2dKmHqM47E++7TRKKkHMyHj\r\na=ice-options:trickle\r\na=fingerprint:sha-256 20:EE:85:F0:DA:F4:90:F3:0D:13:2E:A9:1E:36:8C:81:E1:BD:38:78:20:AA:38:F3:FC:65:3F:8E:06:1D:A7:53\r\na=setup:actpass\r\na=mid:2\r\na=sctp-port:5000\r\na=max-message-size:262144\r\n"; const iceCandidate: RTCIceCandidateInit = { sdpMid: "0", sdpMLineIndex: null, usernameFragment: null, candidate:"candidate:2199032595 1 udp 2122260223 192.168.1.89 64674 typ host generation 0 ufrag +JE1 network-id 1" }; const triggerWebSocketOpen = () => webSocketTriggerFunctions.triggerOnOpen?.(); const triggerConfigMessage = () => webSocketTriggerFunctions.triggerOnMessage?.({ type: MessageRecvTypes.CONFIG, peerConnectionOptions: {} }); const triggerStreamerListMessage = (streamerIdList: string[]) => webSocketTriggerFunctions.triggerOnMessage?.({ type: MessageRecvTypes.STREAMER_LIST, ids: streamerIdList }); const triggerSdpOfferMessage = () => webSocketTriggerFunctions.triggerOnMessage?.({ type: MessageRecvTypes.OFFER, sdp }); const triggerIceCandidateMessage = () => webSocketTriggerFunctions.triggerOnMessage?.({ type: MessageRecvTypes.ICE_CANDIDATE, candidate: iceCandidate }); const triggerIceConnectionState = (state: RTCIceConnectionState) => rtcPeerConnectionTriggerFunctions.triggerIceConnectionStateChange?.( state ); const triggerAddTrack = () => { const stream = new MediaStream(); const track = new MediaStreamTrack(); rtcPeerConnectionTriggerFunctions.triggerOnTrack?.({ track, streams: [stream] } as RTCTrackEventInit); return { stream, track }; }; const triggerOpenDataChannel = () => { const channel = new RTCDataChannel(); rtcPeerConnectionTriggerFunctions.triggerOnDataChannel?.({ channel }); channel.onopen?.(new Event('open')); return { channel }; }; const establishMockedPixelStreamingConnection = ( streamerIds = streamerIdList, iceConnectionState: RTCIceConnectionState = 'connected' ) => { triggerWebSocketOpen(); triggerConfigMessage(); triggerStreamerListMessage(streamerIds); triggerSdpOfferMessage(); triggerIceCandidateMessage(); triggerIceConnectionState(iceConnectionState); const { stream, track } = triggerAddTrack(); const { channel } = triggerOpenDataChannel(); return { channel, stream, track }; }; beforeEach(() => { mockRTCRtpReceiver(); mockMediaStream(); [webSocketSpyFunctions, webSocketTriggerFunctions] = mockWebSocket(); [rtcPeerConnectionSpyFunctions, rtcPeerConnectionTriggerFunctions] = mockRTCPeerConnection(); mockHTMLMediaElement({ ableToPlay: true }); jest.useFakeTimers(); }); afterEach(() => { unmockRTCRtpReceiver(); unmockMediaStream(); unmockWebSocket(); unmockRTCPeerConnection(); jest.resetAllMocks(); }); it('should emit settingsChanged events when the configuration is updated', () => { const config = new Config(); const pixelStreaming = new PixelStreaming(config); const settingsChangedSpy = jest.fn(); pixelStreaming.addEventListener("settingsChanged", settingsChangedSpy); expect(settingsChangedSpy).not.toHaveBeenCalled(); config.setNumericSetting(NumericParameters.WebRTCMaxBitrate, 123); expect(settingsChangedSpy).toHaveBeenCalledWith(new SettingsChangedEvent({ id: NumericParameters.WebRTCMaxBitrate, target: config.getNumericSettings().find((setting) => setting.id === NumericParameters.WebRTCMaxBitrate)!, type: 'number', value: 123, })); }); it('should connect to signalling server when connect is called', () => { const config = new Config({ initialSettings: {ss: mockSignallingUrl}}); const pixelStreaming = new PixelStreaming(config); expect(webSocketSpyFunctions.constructorSpy).not.toHaveBeenCalled(); pixelStreaming.connect(); expect(webSocketSpyFunctions.constructorSpy).toHaveBeenCalledWith(mockSignallingUrl); }); it('should autoconnect to signalling server if autoconnect setting is enabled', () => { const config = new Config({ initialSettings: {ss: mockSignallingUrl, AutoConnect: true}}); expect(webSocketSpyFunctions.constructorSpy).not.toHaveBeenCalled(); const pixelStreaming = new PixelStreaming(config); expect(webSocketSpyFunctions.constructorSpy).toHaveBeenCalledWith(mockSignallingUrl); }); it('should disconnect from signalling server if disconnect is called', () => { const config = new Config({ initialSettings: {ss: mockSignallingUrl, AutoConnect: true}}); const disconnectedSpy = jest.fn(); expect(webSocketSpyFunctions.constructorSpy).not.toHaveBeenCalled(); const pixelStreaming = new PixelStreaming(config); pixelStreaming.addEventListener("webRtcDisconnected", disconnectedSpy); expect(webSocketSpyFunctions.constructorSpy).toHaveBeenCalledWith(mockSignallingUrl); expect(webSocketSpyFunctions.closeSpy).not.toHaveBeenCalled(); pixelStreaming.disconnect(); expect(webSocketSpyFunctions.closeSpy).toHaveBeenCalled(); expect(disconnectedSpy).toHaveBeenCalled(); }); it('should connect immediately to signalling server if reconnect is called and connection is not up', () => { const config = new Config({ initialSettings: {ss: mockSignallingUrl}}); const pixelStreaming = new PixelStreaming(config); expect(webSocketSpyFunctions.constructorSpy).not.toHaveBeenCalled(); pixelStreaming.reconnect(); expect(webSocketSpyFunctions.closeSpy).not.toHaveBeenCalled(); expect(webSocketSpyFunctions.constructorSpy).toHaveBeenCalledWith(mockSignallingUrl); }); it('should disconnect and reconnect to signalling server if reconnect is called and connection is up', () => { // We explicitly set the max reconnect attempts to 0 to stop the auto-reconnect flow as that is tested separate const config = new Config({ initialSettings: {ss: mockSignallingUrl, AutoConnect: true, MaxReconnectAttempts: 0}}); const autoconnectedSpy = jest.fn(); const pixelStreaming = new PixelStreaming(config); pixelStreaming.addEventListener("webRtcAutoConnect", autoconnectedSpy); expect(webSocketSpyFunctions.constructorSpy).toHaveBeenCalledWith(mockSignallingUrl); expect(webSocketSpyFunctions.constructorSpy).toHaveBeenCalledTimes(1); expect(webSocketSpyFunctions.closeSpy).not.toHaveBeenCalled(); pixelStreaming.reconnect(); expect(webSocketSpyFunctions.closeSpy).toHaveBeenCalled(); // delayed reconnect after 3 seconds jest.advanceTimersByTime(3000); expect(webSocketSpyFunctions.constructorSpy).toHaveBeenCalledWith(mockSignallingUrl); expect(webSocketSpyFunctions.constructorSpy).toHaveBeenCalledTimes(2); expect(autoconnectedSpy).toHaveBeenCalled(); }); it('should automatically reconnect and request streamer list N times on websocket close', () => { const config = new Config({ initialSettings: {ss: mockSignallingUrl, AutoConnect: true, MaxReconnectAttempts: 3}}); const autoconnectedSpy = jest.fn(); const pixelStreaming = new PixelStreaming(config); pixelStreaming.addEventListener("webRtcAutoConnect", autoconnectedSpy); expect(webSocketSpyFunctions.constructorSpy).toHaveBeenCalledWith(mockSignallingUrl); expect(webSocketSpyFunctions.constructorSpy).toHaveBeenCalledTimes(1); expect(webSocketSpyFunctions.closeSpy).not.toHaveBeenCalled(); pixelStreaming.webSocketController.close(); expect(webSocketSpyFunctions.closeSpy).toHaveBeenCalled(); // wait 2 seconds jest.advanceTimersByTime(2000); // we should have attempted a reconnection expect(webSocketSpyFunctions.constructorSpy).toHaveBeenCalledWith(mockSignallingUrl); expect(webSocketSpyFunctions.constructorSpy).toHaveBeenCalledTimes(2); // Reconnect triggers the first list streamer message triggerWebSocketOpen(); expect(webSocketSpyFunctions.sendSpy).toHaveBeenCalledWith( expect.stringMatching(/"type":"listStreamers"/) ); // We don't have a signalling server to respond with data so lets just fake a response with no streamers triggerStreamerListMessage([]); // Wait 2 seconds. This delay waits for the WebRtcPlayerController to realise the previously received list doesn't contain // the streamer is was preiously subscribed to, so it'll request the list again jest.advanceTimersByTime(2000); // Same as above but repeated for the second call expect(webSocketSpyFunctions.sendSpy).toHaveBeenCalledWith( expect.stringMatching(/"type":"listStreamers"/) ); triggerStreamerListMessage([]); jest.advanceTimersByTime(2000); // Expect the third call expect(webSocketSpyFunctions.sendSpy).toHaveBeenCalledWith( expect.stringMatching(/"type":"listStreamers"/) ); triggerStreamerListMessage([]); jest.advanceTimersByTime(2000); // We should expect only 3 calls based on our config expect(webSocketSpyFunctions.sendSpy).toHaveBeenCalledTimes(3); }); it('should request streamer list when connected to the signalling server', () => { const config = new Config({ initialSettings: {ss: mockSignallingUrl, AutoConnect: true}}); const pixelStreaming = new PixelStreaming(config); triggerWebSocketOpen(); expect(webSocketSpyFunctions.sendSpy).toHaveBeenCalledWith( expect.stringMatching(/"type":"listStreamers"/) ); }); it('should autoselect a streamer if receiving only one streamer in streamerList message', () => { const config = new Config({ initialSettings: {ss: mockSignallingUrl, AutoConnect: true}}); const streamerListSpy = jest.fn(); const pixelStreaming = new PixelStreaming(config); pixelStreaming.addEventListener("streamerListMessage", streamerListSpy); triggerWebSocketOpen(); triggerConfigMessage(); triggerStreamerListMessage(streamerIdList); expect(streamerListSpy).toHaveBeenCalledWith(new StreamerListMessageEvent({ messageStreamerList: expect.objectContaining({ type: MessageRecvTypes.STREAMER_LIST, ids: streamerIdList }), autoSelectedStreamerId: streamerId, wantedStreamerId: null })); expect(webSocketSpyFunctions.sendSpy).toHaveBeenCalledWith( expect.stringMatching(/"type":"subscribe".*MOCK_PIXEL_STREAMING/) ); }); it('should not autoselect a streamer if receiving multiple streamers in streamerList message', () => { const config = new Config({ initialSettings: {ss: mockSignallingUrl, AutoConnect: true}}); const streamerId2 = "MOCK_2_PIXEL_STREAMING"; const extendedStreamerIdList = [streamerId, streamerId2]; const streamerListSpy = jest.fn(); const pixelStreaming = new PixelStreaming(config); pixelStreaming.addEventListener("streamerListMessage", streamerListSpy); triggerWebSocketOpen(); triggerConfigMessage(); triggerStreamerListMessage(extendedStreamerIdList); expect(streamerListSpy).toHaveBeenCalledWith(new StreamerListMessageEvent({ messageStreamerList: expect.objectContaining({ type: MessageRecvTypes.STREAMER_LIST, ids: extendedStreamerIdList }), autoSelectedStreamerId: null, wantedStreamerId: null })); expect(webSocketSpyFunctions.sendSpy).not.toHaveBeenCalledWith( expect.stringMatching(/"type":"subscribe"/) ); }); it('should set remoteDescription and emit webRtcSdp event when an offer is received', () => { const config = new Config({ initialSettings: {ss: mockSignallingUrl, AutoConnect: true}}); const eventSpy = jest.fn(); const pixelStreaming = new PixelStreaming(config); pixelStreaming.addEventListener("webRtcSdp", eventSpy); triggerWebSocketOpen(); triggerConfigMessage(); triggerStreamerListMessage(streamerIdList); expect(eventSpy).not.toHaveBeenCalled(); triggerSdpOfferMessage(); expect(rtcPeerConnectionSpyFunctions.setRemoteDescriptionSpy).toHaveBeenCalledWith(expect.objectContaining({ sdp })); expect(eventSpy).toHaveBeenCalledWith(new WebRtcSdpEvent()); }); it('should add an ICE candidate when receiving a iceCandidate message', () => { const config = new Config({ initialSettings: {ss: mockSignallingUrl, AutoConnect: true}}); const pixelStreaming = new PixelStreaming(config); triggerWebSocketOpen(); triggerConfigMessage(); triggerStreamerListMessage(streamerIdList); triggerSdpOfferMessage(); triggerIceCandidateMessage(); expect(rtcPeerConnectionSpyFunctions.addIceCandidateSpy).toHaveBeenCalledWith(iceCandidate) }); it('should emit webRtcConnected event when ICE connection state is connected', () => { const config = new Config({ initialSettings: {ss: mockSignallingUrl, AutoConnect: true}}); const connectedSpy = jest.fn(); const pixelStreaming = new PixelStreaming(config); pixelStreaming.addEventListener("webRtcConnected", connectedSpy); triggerWebSocketOpen(); expect(rtcPeerConnectionSpyFunctions.constructorSpy).not.toHaveBeenCalled(); triggerConfigMessage(); expect(rtcPeerConnectionSpyFunctions.constructorSpy).toHaveBeenCalled(); triggerIceCandidateMessage(); triggerIceConnectionState('connected') expect(connectedSpy).toHaveBeenCalledWith(new WebRtcConnectedEvent()); }); it('should call RTCPeerConnection close and emit webRtcDisconnected when disconnect is called', () => { const config = new Config({ initialSettings: {ss: mockSignallingUrl}}); const disconnectedSpy = jest.fn(); const dataChannelSpy = jest.fn(); const pixelStreaming = new PixelStreaming(config); pixelStreaming.addEventListener("webRtcDisconnected", disconnectedSpy); pixelStreaming.addEventListener("dataChannelClose", dataChannelSpy); pixelStreaming.connect(); establishMockedPixelStreamingConnection(); pixelStreaming.disconnect(); expect(rtcPeerConnectionSpyFunctions.closeSpy).toHaveBeenCalled(); expect(disconnectedSpy).toHaveBeenCalled(); expect(dataChannelSpy).toHaveBeenCalled(); }); it('should emit statistics when connected', async () => { const config = new Config({ initialSettings: {ss: mockSignallingUrl, AutoConnect: true}}); const statsSpy = jest.fn(); const pixelStreaming = new PixelStreaming(config); pixelStreaming.addEventListener("statsReceived", statsSpy); establishMockedPixelStreamingConnection(); expect(statsSpy).not.toHaveBeenCalled(); // New stats sent at 1s intervals jest.advanceTimersByTime(1000); await flushPromises(); expect(statsSpy).toHaveBeenCalledTimes(1); expect(statsSpy).toHaveBeenCalledWith( expect.objectContaining({ data: { aggregatedStats: expect.objectContaining({ candidatePairs: [ expect.objectContaining({ bytesReceived: 123 }) ], localCandidates: [ expect.objectContaining({ address: 'mock-address' }) ] }) } }) ); jest.advanceTimersByTime(1000); await flushPromises(); expect(statsSpy).toHaveBeenCalledTimes(2); }); it('should emit dataChannelOpen when data channel is opened', () => { const config = new Config({ initialSettings: {ss: mockSignallingUrl}}); const dataChannelSpy = jest.fn(); const pixelStreaming = new PixelStreaming(config); pixelStreaming.addEventListener("dataChannelOpen", dataChannelSpy); pixelStreaming.connect(); establishMockedPixelStreamingConnection(); expect(dataChannelSpy).toHaveBeenCalled(); }); it('should emit playStream when video play is called', () => { const config = new Config({ initialSettings: {ss: mockSignallingUrl}}); const streamSpy = jest.fn(); const pixelStreaming = new PixelStreaming(config); pixelStreaming.addEventListener("playStream", streamSpy); pixelStreaming.connect(); establishMockedPixelStreamingConnection(); pixelStreaming.play(); expect(streamSpy).toHaveBeenCalled(); }); it('should emit playStreamRejected if video play is rejected', async () => { mockHTMLMediaElement({ ableToPlay: false }); const config = new Config({ initialSettings: {ss: mockSignallingUrl}}); const streamRejectedSpy = jest.fn(); const pixelStreaming = new PixelStreaming(config); pixelStreaming.addEventListener("playStreamRejected", streamRejectedSpy); pixelStreaming.connect(); establishMockedPixelStreamingConnection(); pixelStreaming.play(); await flushPromises(); expect(streamRejectedSpy).toHaveBeenCalled(); }); it('should send data through the data channel when emitCommand is called', () => { mockHTMLMediaElement({ ableToPlay: true, readyState: 2 }); const config = new Config({ initialSettings: {ss: mockSignallingUrl}}); const pixelStreaming = new PixelStreaming(config); pixelStreaming.connect(); establishMockedPixelStreamingConnection(); pixelStreaming.play(); expect(rtcPeerConnectionSpyFunctions.sendDataSpy).not.toHaveBeenCalled(); const commandSent = pixelStreaming.emitCommand({ 'Resolution.Width': 123, 'Resolution.Height': 456 }); expect(commandSent).toEqual(true); expect(rtcPeerConnectionSpyFunctions.sendDataSpy).toHaveBeenCalled(); }); it('should prevent sending console commands unless permitted by streamer', () => { mockHTMLMediaElement({ ableToPlay: true, readyState: 2 }); const config = new Config({ initialSettings: {ss: mockSignallingUrl}}); const pixelStreaming = new PixelStreaming(config); pixelStreaming.connect(); establishMockedPixelStreamingConnection(); pixelStreaming.play(); expect(rtcPeerConnectionSpyFunctions.sendDataSpy).not.toHaveBeenCalled(); const commandSent = pixelStreaming.emitConsoleCommand("console command"); expect(commandSent).toEqual(false); expect(rtcPeerConnectionSpyFunctions.sendDataSpy).not.toHaveBeenCalled(); }); it('should allow sending console commands if permitted by streamer', () => { mockHTMLMediaElement({ ableToPlay: true, readyState: 2 }); const config = new Config({ initialSettings: {ss: mockSignallingUrl}}); const initialSettingsSpy = jest.fn(); const pixelStreaming = new PixelStreaming(config); pixelStreaming.addEventListener("initialSettings", initialSettingsSpy); pixelStreaming.connect(); establishMockedPixelStreamingConnection(); pixelStreaming.play(); expect(rtcPeerConnectionSpyFunctions.sendDataSpy).not.toHaveBeenCalled(); expect(initialSettingsSpy).not.toHaveBeenCalled(); const initialSettings = new InitialSettings(); initialSettings.PixelStreamingSettings.AllowPixelStreamingCommands = true; pixelStreaming._onInitialSettings(initialSettings); expect(initialSettingsSpy).toHaveBeenCalled(); const commandSent = pixelStreaming.emitConsoleCommand("console command"); expect(commandSent).toEqual(true); expect(rtcPeerConnectionSpyFunctions.sendDataSpy).toHaveBeenCalled(); }); it('should send data through the data channel when emitUIInteraction is called', () => { mockHTMLMediaElement({ ableToPlay: true, readyState: 2 }); const config = new Config({ initialSettings: {ss: mockSignallingUrl}}); const pixelStreaming = new PixelStreaming(config); pixelStreaming.connect(); establishMockedPixelStreamingConnection(); pixelStreaming.play(); expect(rtcPeerConnectionSpyFunctions.sendDataSpy).not.toHaveBeenCalled(); const commandSent = pixelStreaming.emitUIInteraction({ custom: "descriptor" }); expect(commandSent).toEqual(true); expect(rtcPeerConnectionSpyFunctions.sendDataSpy).toHaveBeenCalled(); }); it('should call user-provided callback if receiving a data channel Response message from the streamer', () => { mockHTMLMediaElement({ ableToPlay: true, readyState: 2 }); const config = new Config({ initialSettings: {ss: mockSignallingUrl}}); const responseListenerSpy = jest.fn(); const pixelStreaming = new PixelStreaming(config); pixelStreaming.addResponseEventListener('responseListener', responseListenerSpy); pixelStreaming.connect(); const { channel } = establishMockedPixelStreamingConnection(); pixelStreaming.play(); expect(responseListenerSpy).not.toHaveBeenCalled(); const testMessageContents = JSON.stringify({ test: "mock-data" }); const data = new DataView(new ArrayBuffer(1 + 2 * testMessageContents.length)); data.setUint8(0, 1); // type 1 == Response let byteIdx = 1; for (let i = 0; i < testMessageContents.length; i++) { data.setUint16(byteIdx, testMessageContents.charCodeAt(i), true); byteIdx += 2; } channel.dispatchEvent(new MessageEvent('message', { data: data.buffer })); expect(responseListenerSpy).toHaveBeenCalledWith(testMessageContents); }); it('should emit StreamConnectEvent when streamer connects', () => { const config = new Config({ initialSettings: {ss: mockSignallingUrl}}); const streamConnectSpy = jest.fn(); const pixelStreaming = new PixelStreaming(config); pixelStreaming.addEventListener("streamConnect", streamConnectSpy); pixelStreaming.connect(); establishMockedPixelStreamingConnection(); expect(streamConnectSpy).toHaveBeenCalled(); }); it('should emit StreamDisconnectEvent when streamer disconnects', () => { const config = new Config({ initialSettings: {ss: mockSignallingUrl}}); const streamDisconnectSpy = jest.fn(); const pixelStreaming = new PixelStreaming(config); pixelStreaming.addEventListener("streamDisconnect", streamDisconnectSpy); pixelStreaming.connect(); establishMockedPixelStreamingConnection(); expect(streamDisconnectSpy).not.toHaveBeenCalled(); pixelStreaming.disconnect(); expect(streamDisconnectSpy).toHaveBeenCalled(); }); it('should emit StreamReconnectEvent when streamer reconnects', () => { const config = new Config({ initialSettings: {ss: mockSignallingUrl}}); const streamReconnectSpy = jest.fn(); const pixelStreaming = new PixelStreaming(config); pixelStreaming.addEventListener("streamReconnect", streamReconnectSpy); pixelStreaming.connect(); establishMockedPixelStreamingConnection(); expect(streamReconnectSpy).not.toHaveBeenCalled(); pixelStreaming.reconnect(); expect(streamReconnectSpy).toHaveBeenCalled(); pixelStreaming.disconnect(); expect(streamReconnectSpy).toHaveBeenCalledTimes(1); }); });