forked from EpicGames/PixelStreamingInfrastructure
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathPixelStreaming.test.ts
624 lines (479 loc) · 29.5 KB
/
PixelStreaming.test.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
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);
});
});