@stream-io/video-client 1.48.0 → 1.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/dist/index.browser.es.js +1497 -677
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +1497 -677
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +1497 -677
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +77 -4
- package/dist/src/StreamSfuClient.d.ts +8 -1
- package/dist/src/coordinator/connection/client.d.ts +1 -1
- package/dist/src/coordinator/connection/connection.d.ts +31 -25
- package/dist/src/coordinator/connection/types.d.ts +14 -0
- package/dist/src/coordinator/connection/utils.d.ts +1 -0
- package/dist/src/devices/DeviceManager.d.ts +3 -0
- package/dist/src/devices/DeviceManagerState.d.ts +13 -1
- package/dist/src/gen/video/sfu/event/events.d.ts +5 -1
- package/dist/src/gen/video/sfu/models/models.d.ts +34 -0
- package/dist/src/helpers/AudioBindingsWatchdog.d.ts +3 -3
- package/dist/src/helpers/BlockedAudioTracker.d.ts +30 -0
- package/dist/src/helpers/DynascaleManager.d.ts +8 -86
- package/dist/src/helpers/MediaPlaybackWatchdog.d.ts +32 -0
- package/dist/src/helpers/SlidingWindowRateLimiter.d.ts +28 -0
- package/dist/src/helpers/TrackSubscriptionManager.d.ts +114 -0
- package/dist/src/helpers/ViewportTracker.d.ts +11 -17
- package/dist/src/helpers/browsers.d.ts +13 -0
- package/dist/src/helpers/concurrency.d.ts +6 -4
- package/dist/src/rtc/BasePeerConnection.d.ts +11 -2
- package/dist/src/rtc/Publisher.d.ts +17 -0
- package/dist/src/rtc/Subscriber.d.ts +1 -0
- package/dist/src/rtc/helpers/degradationPreference.d.ts +2 -0
- package/dist/src/rtc/index.d.ts +1 -0
- package/dist/src/rtc/types.d.ts +33 -1
- package/dist/src/stats/rtc/types.d.ts +1 -1
- package/dist/src/store/rxUtils.d.ts +9 -0
- package/dist/src/types.d.ts +18 -0
- package/package.json +2 -2
- package/src/Call.ts +268 -40
- package/src/StreamSfuClient.ts +75 -12
- package/src/__tests__/Call.lifecycle.test.ts +67 -0
- package/src/__tests__/Call.publishing.test.ts +103 -0
- package/src/__tests__/StreamSfuClient.test.ts +275 -0
- package/src/coordinator/connection/__tests__/connection.test.ts +482 -0
- package/src/coordinator/connection/client.ts +1 -1
- package/src/coordinator/connection/connection.ts +149 -96
- package/src/coordinator/connection/types.ts +15 -0
- package/src/coordinator/connection/utils.ts +15 -0
- package/src/devices/DeviceManager.ts +92 -32
- package/src/devices/DeviceManagerState.ts +20 -1
- package/src/devices/__tests__/DeviceManager.test.ts +283 -0
- package/src/devices/__tests__/ScreenShareManager.test.ts +0 -2
- package/src/devices/__tests__/mocks.ts +2 -0
- package/src/devices/devices.ts +2 -1
- package/src/gen/video/sfu/event/events.ts +15 -0
- package/src/gen/video/sfu/models/models.ts +44 -0
- package/src/helpers/AudioBindingsWatchdog.ts +10 -7
- package/src/helpers/BlockedAudioTracker.ts +74 -0
- package/src/helpers/DynascaleManager.ts +46 -337
- package/src/helpers/MediaPlaybackWatchdog.ts +121 -0
- package/src/helpers/SlidingWindowRateLimiter.ts +49 -0
- package/src/helpers/TrackSubscriptionManager.ts +243 -0
- package/src/helpers/ViewportTracker.ts +74 -19
- package/src/helpers/__tests__/BlockedAudioTracker.test.ts +114 -0
- package/src/helpers/__tests__/DynascaleManager.test.ts +175 -122
- package/src/helpers/__tests__/MediaPlaybackWatchdog.test.ts +180 -0
- package/src/helpers/__tests__/SlidingWindowRateLimiter.test.ts +43 -0
- package/src/helpers/__tests__/TrackSubscriptionManager.test.ts +310 -0
- package/src/helpers/__tests__/ViewportTracker.test.ts +83 -0
- package/src/helpers/__tests__/browsers.test.ts +85 -1
- package/src/helpers/browsers.ts +24 -0
- package/src/helpers/concurrency.ts +9 -10
- package/src/rpc/retryable.ts +0 -1
- package/src/rtc/BasePeerConnection.ts +96 -6
- package/src/rtc/Publisher.ts +49 -2
- package/src/rtc/Subscriber.ts +42 -14
- package/src/rtc/__tests__/Call.reconnect.test.ts +761 -0
- package/src/rtc/__tests__/Publisher.test.ts +332 -10
- package/src/rtc/__tests__/Subscriber.test.ts +202 -1
- package/src/rtc/__tests__/mocks/webrtc.mocks.ts +13 -2
- package/src/rtc/helpers/__tests__/degradationPreference.test.ts +23 -0
- package/src/rtc/helpers/degradationPreference.ts +22 -0
- package/src/rtc/index.ts +1 -0
- package/src/rtc/types.ts +38 -1
- package/src/stats/rtc/types.ts +1 -0
- package/src/store/__tests__/rxUtils.test.ts +276 -0
- package/src/store/rxUtils.ts +19 -0
- package/src/types.ts +19 -0
|
@@ -2,12 +2,15 @@ import './mocks/webrtc.mocks';
|
|
|
2
2
|
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
4
|
import { anyString } from 'vitest-mock-extended';
|
|
5
|
+
import { fromPartial } from '@total-typescript/shoehorn';
|
|
5
6
|
import { NegotiationError } from '../NegotiationError';
|
|
6
7
|
import { Publisher } from '../Publisher';
|
|
8
|
+
import { ReconnectReason } from '../types';
|
|
7
9
|
import { CallState } from '../../store';
|
|
8
10
|
import { StreamSfuClient } from '../../StreamSfuClient';
|
|
9
11
|
import { DispatchableMessage, Dispatcher } from '../Dispatcher';
|
|
10
12
|
import {
|
|
13
|
+
DegradationPreference,
|
|
11
14
|
ErrorCode,
|
|
12
15
|
PeerType,
|
|
13
16
|
PublishOption,
|
|
@@ -81,12 +84,14 @@ describe('Publisher', () => {
|
|
|
81
84
|
fps: 30,
|
|
82
85
|
maxTemporalLayers: 3,
|
|
83
86
|
maxSpatialLayers: 3,
|
|
87
|
+
degradationPreference: DegradationPreference.UNSPECIFIED,
|
|
84
88
|
},
|
|
85
89
|
],
|
|
86
90
|
);
|
|
87
91
|
});
|
|
88
92
|
|
|
89
93
|
afterEach(() => {
|
|
94
|
+
vi.useRealTimers();
|
|
90
95
|
vi.clearAllMocks();
|
|
91
96
|
vi.resetModules();
|
|
92
97
|
publisher.dispose();
|
|
@@ -166,16 +171,19 @@ describe('Publisher', () => {
|
|
|
166
171
|
changePublishQuality: {
|
|
167
172
|
audioSenders: [],
|
|
168
173
|
videoSenders: [
|
|
169
|
-
{
|
|
174
|
+
fromPartial({
|
|
170
175
|
publishOptionId: 1,
|
|
171
176
|
trackType: TrackType.VIDEO,
|
|
172
177
|
layers: [],
|
|
173
|
-
|
|
174
|
-
|
|
178
|
+
degradationPreference: DegradationPreference.BALANCED,
|
|
179
|
+
}),
|
|
180
|
+
fromPartial({
|
|
175
181
|
publishOptionId: 2,
|
|
176
182
|
trackType: TrackType.SCREEN_SHARE,
|
|
177
183
|
layers: [],
|
|
178
|
-
|
|
184
|
+
degradationPreference:
|
|
185
|
+
DegradationPreference.MAINTAIN_RESOLUTION,
|
|
186
|
+
}),
|
|
179
187
|
],
|
|
180
188
|
},
|
|
181
189
|
},
|
|
@@ -250,7 +258,14 @@ describe('Publisher', () => {
|
|
|
250
258
|
expect(publisher['negotiate']).toHaveBeenCalled();
|
|
251
259
|
});
|
|
252
260
|
|
|
261
|
+
const simulatePriorIceConnected = () => {
|
|
262
|
+
// @ts-expect-error private api
|
|
263
|
+
publisher['pc'].iceConnectionState = 'connected';
|
|
264
|
+
publisher['onIceConnectionStateChange']();
|
|
265
|
+
};
|
|
266
|
+
|
|
253
267
|
it(`should perform ICE restart when connection state changes to 'failed'`, () => {
|
|
268
|
+
simulatePriorIceConnected();
|
|
254
269
|
vi.spyOn(publisher, 'restartIce').mockResolvedValue();
|
|
255
270
|
// @ts-expect-error private api
|
|
256
271
|
publisher['pc'].iceConnectionState = 'failed';
|
|
@@ -259,6 +274,7 @@ describe('Publisher', () => {
|
|
|
259
274
|
});
|
|
260
275
|
|
|
261
276
|
it(`should perform rejoin when ICE restart fails after connection state changes to 'failed'`, async () => {
|
|
277
|
+
simulatePriorIceConnected();
|
|
262
278
|
const { promise: lock, resolve: unlock } = promiseWithResolvers<void>();
|
|
263
279
|
publisher['onReconnectionNeeded'] = vi
|
|
264
280
|
.fn()
|
|
@@ -274,6 +290,7 @@ describe('Publisher', () => {
|
|
|
274
290
|
});
|
|
275
291
|
|
|
276
292
|
it(`should perform fast reconnect when ICE restart fails with SIGNAL_LOST error`, async () => {
|
|
293
|
+
simulatePriorIceConnected();
|
|
277
294
|
const { promise: lock, resolve: unlock } = promiseWithResolvers<void>();
|
|
278
295
|
publisher['onReconnectionNeeded'] = vi
|
|
279
296
|
.fn()
|
|
@@ -325,6 +342,7 @@ describe('Publisher', () => {
|
|
|
325
342
|
});
|
|
326
343
|
|
|
327
344
|
it(`should perform REJOIN reconnect when ICE restart fails with any other error code`, async () => {
|
|
345
|
+
simulatePriorIceConnected();
|
|
328
346
|
const { promise: lock, resolve: unlock } = promiseWithResolvers<void>();
|
|
329
347
|
publisher['onReconnectionNeeded'] = vi
|
|
330
348
|
.fn()
|
|
@@ -365,6 +383,7 @@ describe('Publisher', () => {
|
|
|
365
383
|
});
|
|
366
384
|
|
|
367
385
|
it(`should schedule ICE restart when connection state changes to 'disconnected'`, () => {
|
|
386
|
+
simulatePriorIceConnected();
|
|
368
387
|
vi.spyOn(publisher, 'restartIce').mockResolvedValue();
|
|
369
388
|
vi.useFakeTimers();
|
|
370
389
|
// @ts-expect-error private api
|
|
@@ -376,6 +395,7 @@ describe('Publisher', () => {
|
|
|
376
395
|
});
|
|
377
396
|
|
|
378
397
|
it(`should perform rejoin when scheduled ICE restart fails`, async () => {
|
|
398
|
+
simulatePriorIceConnected();
|
|
379
399
|
vi.spyOn(publisher, 'restartIce').mockRejectedValue('ICE restart failed');
|
|
380
400
|
const { promise: lock, resolve } = promiseWithResolvers<void>();
|
|
381
401
|
publisher['onReconnectionNeeded'] = vi
|
|
@@ -393,6 +413,202 @@ describe('Publisher', () => {
|
|
|
393
413
|
expect(publisher['onReconnectionNeeded']).toHaveBeenCalled();
|
|
394
414
|
});
|
|
395
415
|
|
|
416
|
+
it(`iceHasEverConnected is false before any connected state is observed`, () => {
|
|
417
|
+
expect(publisher['iceHasEverConnected']).toBe(false);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it(`iceHasEverConnected becomes true after 'connected' ICE state`, () => {
|
|
421
|
+
// @ts-expect-error private api
|
|
422
|
+
publisher['pc'].iceConnectionState = 'connected';
|
|
423
|
+
publisher['onIceConnectionStateChange']();
|
|
424
|
+
expect(publisher['iceHasEverConnected']).toBe(true);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it(`iceHasEverConnected also flips on 'completed' ICE state`, () => {
|
|
428
|
+
// @ts-expect-error private api
|
|
429
|
+
publisher['pc'].iceConnectionState = 'completed';
|
|
430
|
+
publisher['onIceConnectionStateChange']();
|
|
431
|
+
expect(publisher['iceHasEverConnected']).toBe(true);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it(`does NOT call restartIce when ICE never connected and state goes to 'failed' — emits REJOIN with 'ice_never_connected'`, () => {
|
|
435
|
+
vi.spyOn(publisher, 'restartIce').mockResolvedValue();
|
|
436
|
+
publisher['onReconnectionNeeded'] = vi.fn();
|
|
437
|
+
// @ts-expect-error private api
|
|
438
|
+
publisher['pc'].iceConnectionState = 'failed';
|
|
439
|
+
publisher['onIceConnectionStateChange']();
|
|
440
|
+
expect(publisher.restartIce).not.toHaveBeenCalled();
|
|
441
|
+
expect(publisher['onReconnectionNeeded']).toHaveBeenCalledWith(
|
|
442
|
+
WebsocketReconnectStrategy.REJOIN,
|
|
443
|
+
ReconnectReason.ICE_NEVER_CONNECTED,
|
|
444
|
+
PeerType.PUBLISHER_UNSPECIFIED,
|
|
445
|
+
);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it(`pre-connect 'disconnected' does not restart or escalate immediately`, () => {
|
|
449
|
+
// ICE has never reached `connected`. A `disconnected` transition at
|
|
450
|
+
// this point is just the browser's checking phase wobbling; the
|
|
451
|
+
// browser may yet move back to checking/connected. The SDK should
|
|
452
|
+
// wait it out — no synchronous restart, no synchronous REJOIN. Only
|
|
453
|
+
// a terminal `failed` before connect, or the pre-connect watchdog
|
|
454
|
+
// expiring, should escalate via `ICE_NEVER_CONNECTED`.
|
|
455
|
+
vi.spyOn(publisher, 'restartIce').mockResolvedValue();
|
|
456
|
+
publisher['onReconnectionNeeded'] = vi.fn();
|
|
457
|
+
// @ts-expect-error private api
|
|
458
|
+
publisher['pc'].iceConnectionState = 'disconnected';
|
|
459
|
+
publisher['onIceConnectionStateChange']();
|
|
460
|
+
expect(publisher.restartIce).not.toHaveBeenCalled();
|
|
461
|
+
expect(publisher['onReconnectionNeeded']).not.toHaveBeenCalled();
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it(`pre-connect 'disconnected' watchdog escalates to REJOIN if state stays stuck`, () => {
|
|
465
|
+
vi.spyOn(publisher, 'restartIce').mockResolvedValue();
|
|
466
|
+
publisher['onReconnectionNeeded'] = vi.fn();
|
|
467
|
+
vi.useFakeTimers();
|
|
468
|
+
const watchdogMs = publisher['iceRestartDelay'] * 2;
|
|
469
|
+
|
|
470
|
+
// @ts-expect-error private api
|
|
471
|
+
publisher['pc'].iceConnectionState = 'disconnected';
|
|
472
|
+
publisher['onIceConnectionStateChange']();
|
|
473
|
+
// before the watchdog fires, no escalation
|
|
474
|
+
vi.advanceTimersByTime(watchdogMs - 1);
|
|
475
|
+
expect(publisher['onReconnectionNeeded']).not.toHaveBeenCalled();
|
|
476
|
+
|
|
477
|
+
// watchdog fires; still stuck in disconnected → escalate
|
|
478
|
+
vi.advanceTimersByTime(2);
|
|
479
|
+
expect(publisher.restartIce).not.toHaveBeenCalled();
|
|
480
|
+
expect(publisher['onReconnectionNeeded']).toHaveBeenCalledWith(
|
|
481
|
+
WebsocketReconnectStrategy.REJOIN,
|
|
482
|
+
ReconnectReason.ICE_NEVER_CONNECTED,
|
|
483
|
+
PeerType.PUBLISHER_UNSPECIFIED,
|
|
484
|
+
);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it(`pre-connect 'disconnected' watchdog is canceled when ICE recovers to 'connected'`, () => {
|
|
488
|
+
publisher['onReconnectionNeeded'] = vi.fn();
|
|
489
|
+
vi.useFakeTimers();
|
|
490
|
+
const watchdogMs = publisher['iceRestartDelay'] * 2;
|
|
491
|
+
|
|
492
|
+
// @ts-expect-error private api
|
|
493
|
+
publisher['pc'].iceConnectionState = 'disconnected';
|
|
494
|
+
publisher['onIceConnectionStateChange']();
|
|
495
|
+
// recover before the watchdog window expires
|
|
496
|
+
// @ts-expect-error private api
|
|
497
|
+
publisher['pc'].iceConnectionState = 'connected';
|
|
498
|
+
publisher['onIceConnectionStateChange']();
|
|
499
|
+
|
|
500
|
+
// advance past the original watchdog window — must NOT fire now
|
|
501
|
+
vi.advanceTimersByTime(watchdogMs + 100);
|
|
502
|
+
|
|
503
|
+
expect(publisher['iceHasEverConnected']).toBe(true);
|
|
504
|
+
expect(publisher['onReconnectionNeeded']).not.toHaveBeenCalled();
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it(`pre-connect 'disconnected' that recovers to 'connected' continues normally`, () => {
|
|
508
|
+
publisher['onReconnectionNeeded'] = vi.fn();
|
|
509
|
+
// @ts-expect-error private api
|
|
510
|
+
publisher['pc'].iceConnectionState = 'disconnected';
|
|
511
|
+
publisher['onIceConnectionStateChange']();
|
|
512
|
+
// browser now reaches connected — the normal connected branch runs
|
|
513
|
+
// and `iceHasEverConnected` flips to true.
|
|
514
|
+
// @ts-expect-error private api
|
|
515
|
+
publisher['pc'].iceConnectionState = 'connected';
|
|
516
|
+
publisher['onIceConnectionStateChange']();
|
|
517
|
+
expect(publisher['iceHasEverConnected']).toBe(true);
|
|
518
|
+
expect(publisher['onReconnectionNeeded']).not.toHaveBeenCalled();
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it(`isStable() returns false when ICE is 'new'`, () => {
|
|
522
|
+
// @ts-expect-error private api
|
|
523
|
+
publisher['pc'].iceConnectionState = 'new';
|
|
524
|
+
// default connectionState in mock is 'connected'
|
|
525
|
+
expect(publisher.isStable()).toBe(false);
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it(`isStable() returns true when ICE is 'connected' and connectionState is 'connected'`, () => {
|
|
529
|
+
// @ts-expect-error private api
|
|
530
|
+
publisher['pc'].iceConnectionState = 'connected';
|
|
531
|
+
// @ts-expect-error private api
|
|
532
|
+
publisher['pc'].connectionState = 'connected';
|
|
533
|
+
expect(publisher.isStable()).toBe(true);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it(`isStable() returns true when ICE is 'completed' and connectionState is 'connected'`, () => {
|
|
537
|
+
// @ts-expect-error private api
|
|
538
|
+
publisher['pc'].iceConnectionState = 'completed';
|
|
539
|
+
// @ts-expect-error private api
|
|
540
|
+
publisher['pc'].connectionState = 'connected';
|
|
541
|
+
expect(publisher.isStable()).toBe(true);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it(`isStable() returns false when ICE is 'disconnected'`, () => {
|
|
545
|
+
// @ts-expect-error private api
|
|
546
|
+
publisher['pc'].iceConnectionState = 'disconnected';
|
|
547
|
+
// @ts-expect-error private api
|
|
548
|
+
publisher['pc'].connectionState = 'connected';
|
|
549
|
+
expect(publisher.isStable()).toBe(false);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it(`after connected→disconnected→connected cycle, subsequent 'failed' DOES trigger ICE restart (flag stays true)`, () => {
|
|
553
|
+
// @ts-expect-error private api
|
|
554
|
+
publisher['pc'].iceConnectionState = 'connected';
|
|
555
|
+
publisher['onIceConnectionStateChange']();
|
|
556
|
+
// @ts-expect-error private api
|
|
557
|
+
publisher['pc'].iceConnectionState = 'disconnected';
|
|
558
|
+
publisher['onIceConnectionStateChange']();
|
|
559
|
+
// @ts-expect-error private api
|
|
560
|
+
publisher['pc'].iceConnectionState = 'connected';
|
|
561
|
+
publisher['onIceConnectionStateChange']();
|
|
562
|
+
|
|
563
|
+
vi.spyOn(publisher, 'restartIce').mockResolvedValue();
|
|
564
|
+
publisher['onReconnectionNeeded'] = vi.fn();
|
|
565
|
+
// @ts-expect-error private api
|
|
566
|
+
publisher['pc'].iceConnectionState = 'failed';
|
|
567
|
+
publisher['onIceConnectionStateChange']();
|
|
568
|
+
expect(publisher.restartIce).toHaveBeenCalled();
|
|
569
|
+
// the reason here is the regular restart path, NOT 'ice_never_connected'
|
|
570
|
+
expect(publisher['onReconnectionNeeded']).not.toHaveBeenCalledWith(
|
|
571
|
+
WebsocketReconnectStrategy.REJOIN,
|
|
572
|
+
ReconnectReason.ICE_NEVER_CONNECTED,
|
|
573
|
+
PeerType.PUBLISHER_UNSPECIFIED,
|
|
574
|
+
);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it(`connection-state 'failed' (distinct from ICE state) still fires REJOIN even after ICE was connected`, () => {
|
|
578
|
+
// mark ICE as connected first
|
|
579
|
+
// @ts-expect-error private api
|
|
580
|
+
publisher['pc'].iceConnectionState = 'connected';
|
|
581
|
+
publisher['onIceConnectionStateChange']();
|
|
582
|
+
|
|
583
|
+
publisher['onReconnectionNeeded'] = vi.fn();
|
|
584
|
+
// @ts-expect-error private api
|
|
585
|
+
publisher['pc'].connectionState = 'failed';
|
|
586
|
+
publisher['onConnectionStateChange']();
|
|
587
|
+
expect(publisher['onReconnectionNeeded']).toHaveBeenCalledWith(
|
|
588
|
+
WebsocketReconnectStrategy.REJOIN,
|
|
589
|
+
ReconnectReason.CONNECTION_FAILED,
|
|
590
|
+
PeerType.PUBLISHER_UNSPECIFIED,
|
|
591
|
+
);
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
it(`'completed' state is treated as connectivity — subsequent 'failed' DOES trigger ICE restart`, () => {
|
|
595
|
+
// @ts-expect-error private api
|
|
596
|
+
publisher['pc'].iceConnectionState = 'completed';
|
|
597
|
+
publisher['onIceConnectionStateChange']();
|
|
598
|
+
|
|
599
|
+
vi.spyOn(publisher, 'restartIce').mockResolvedValue();
|
|
600
|
+
publisher['onReconnectionNeeded'] = vi.fn();
|
|
601
|
+
// @ts-expect-error private api
|
|
602
|
+
publisher['pc'].iceConnectionState = 'failed';
|
|
603
|
+
publisher['onIceConnectionStateChange']();
|
|
604
|
+
expect(publisher.restartIce).toHaveBeenCalled();
|
|
605
|
+
expect(publisher['onReconnectionNeeded']).not.toHaveBeenCalledWith(
|
|
606
|
+
WebsocketReconnectStrategy.REJOIN,
|
|
607
|
+
ReconnectReason.ICE_NEVER_CONNECTED,
|
|
608
|
+
PeerType.PUBLISHER_UNSPECIFIED,
|
|
609
|
+
);
|
|
610
|
+
});
|
|
611
|
+
|
|
396
612
|
it(`should schedule ICE restart but cancel it if connection recovers in the meantime`, () => {
|
|
397
613
|
vi.spyOn(publisher, 'restartIce').mockResolvedValue();
|
|
398
614
|
vi.useFakeTimers();
|
|
@@ -447,6 +663,7 @@ describe('Publisher', () => {
|
|
|
447
663
|
await publisher['changePublishQuality']({
|
|
448
664
|
publishOptionId: 1,
|
|
449
665
|
trackType: TrackType.VIDEO,
|
|
666
|
+
degradationPreference: DegradationPreference.UNSPECIFIED,
|
|
450
667
|
layers: [
|
|
451
668
|
{
|
|
452
669
|
name: 'q',
|
|
@@ -523,6 +740,7 @@ describe('Publisher', () => {
|
|
|
523
740
|
await publisher['changePublishQuality']({
|
|
524
741
|
publishOptionId: 1,
|
|
525
742
|
trackType: TrackType.VIDEO,
|
|
743
|
+
degradationPreference: DegradationPreference.UNSPECIFIED,
|
|
526
744
|
layers: [
|
|
527
745
|
{
|
|
528
746
|
name: 'q',
|
|
@@ -586,6 +804,7 @@ describe('Publisher', () => {
|
|
|
586
804
|
await publisher['changePublishQuality']({
|
|
587
805
|
publishOptionId: 1,
|
|
588
806
|
trackType: TrackType.VIDEO,
|
|
807
|
+
degradationPreference: DegradationPreference.UNSPECIFIED,
|
|
589
808
|
layers: [
|
|
590
809
|
{
|
|
591
810
|
name: 'q',
|
|
@@ -645,6 +864,7 @@ describe('Publisher', () => {
|
|
|
645
864
|
await publisher['changePublishQuality']({
|
|
646
865
|
publishOptionId: 1,
|
|
647
866
|
trackType: TrackType.VIDEO,
|
|
867
|
+
degradationPreference: DegradationPreference.UNSPECIFIED,
|
|
648
868
|
layers: [
|
|
649
869
|
{
|
|
650
870
|
name: 'q',
|
|
@@ -669,6 +889,93 @@ describe('Publisher', () => {
|
|
|
669
889
|
},
|
|
670
890
|
]);
|
|
671
891
|
});
|
|
892
|
+
|
|
893
|
+
it('applies degradationPreference from the SFU event', async () => {
|
|
894
|
+
const transceiver = new RTCRtpTransceiver();
|
|
895
|
+
const setParametersSpy = vi
|
|
896
|
+
.spyOn(transceiver.sender, 'setParameters')
|
|
897
|
+
.mockResolvedValue();
|
|
898
|
+
vi.spyOn(transceiver.sender, 'getParameters').mockReturnValue({
|
|
899
|
+
// @ts-expect-error incomplete data
|
|
900
|
+
codecs: [{ mimeType: 'video/VP8' }],
|
|
901
|
+
encodings: [{ rid: 'q', active: true }],
|
|
902
|
+
degradationPreference: 'maintain-framerate',
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
publisher['transceiverCache'].add({
|
|
906
|
+
// @ts-expect-error incomplete data
|
|
907
|
+
publishOption: { trackType: TrackType.VIDEO, id: 1 },
|
|
908
|
+
transceiver,
|
|
909
|
+
options: {},
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
await publisher['changePublishQuality']({
|
|
913
|
+
publishOptionId: 1,
|
|
914
|
+
trackType: TrackType.VIDEO,
|
|
915
|
+
degradationPreference: DegradationPreference.BALANCED,
|
|
916
|
+
layers: [
|
|
917
|
+
{
|
|
918
|
+
name: 'q',
|
|
919
|
+
active: true,
|
|
920
|
+
maxBitrate: 100,
|
|
921
|
+
scaleResolutionDownBy: 1,
|
|
922
|
+
maxFramerate: 30,
|
|
923
|
+
scalabilityMode: '',
|
|
924
|
+
},
|
|
925
|
+
],
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
expect(setParametersSpy).toHaveBeenCalled();
|
|
929
|
+
expect(setParametersSpy.mock.calls[0][0].degradationPreference).toBe(
|
|
930
|
+
'balanced',
|
|
931
|
+
);
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
it('does not call setParameters when nothing changes and degradationPreference is UNSPECIFIED', async () => {
|
|
935
|
+
const transceiver = new RTCRtpTransceiver();
|
|
936
|
+
const setParametersSpy = vi
|
|
937
|
+
.spyOn(transceiver.sender, 'setParameters')
|
|
938
|
+
.mockResolvedValue();
|
|
939
|
+
vi.spyOn(transceiver.sender, 'getParameters').mockReturnValue({
|
|
940
|
+
// @ts-expect-error incomplete data
|
|
941
|
+
codecs: [{ mimeType: 'video/VP8' }],
|
|
942
|
+
encodings: [
|
|
943
|
+
{
|
|
944
|
+
rid: 'q',
|
|
945
|
+
active: true,
|
|
946
|
+
maxBitrate: 100,
|
|
947
|
+
scaleResolutionDownBy: 1,
|
|
948
|
+
maxFramerate: 30,
|
|
949
|
+
},
|
|
950
|
+
],
|
|
951
|
+
degradationPreference: 'maintain-framerate',
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
publisher['transceiverCache'].add({
|
|
955
|
+
// @ts-expect-error incomplete data
|
|
956
|
+
publishOption: { trackType: TrackType.VIDEO, id: 1 },
|
|
957
|
+
transceiver,
|
|
958
|
+
options: {},
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
await publisher['changePublishQuality']({
|
|
962
|
+
publishOptionId: 1,
|
|
963
|
+
trackType: TrackType.VIDEO,
|
|
964
|
+
degradationPreference: DegradationPreference.UNSPECIFIED,
|
|
965
|
+
layers: [
|
|
966
|
+
{
|
|
967
|
+
name: 'q',
|
|
968
|
+
active: true,
|
|
969
|
+
maxBitrate: 100,
|
|
970
|
+
scaleResolutionDownBy: 1,
|
|
971
|
+
maxFramerate: 30,
|
|
972
|
+
scalabilityMode: '',
|
|
973
|
+
},
|
|
974
|
+
],
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
expect(setParametersSpy).not.toHaveBeenCalled();
|
|
978
|
+
});
|
|
672
979
|
});
|
|
673
980
|
|
|
674
981
|
describe('changePublishOptions', () => {
|
|
@@ -683,12 +990,27 @@ describe('Publisher', () => {
|
|
|
683
990
|
vi.spyOn(publisher, 'negotiate').mockResolvedValue();
|
|
684
991
|
|
|
685
992
|
publisher['publishOptions'] = [
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
993
|
+
{
|
|
994
|
+
trackType: TrackType.VIDEO,
|
|
995
|
+
id: 0,
|
|
996
|
+
// @ts-expect-error incomplete data
|
|
997
|
+
codec: { name: 'vp8' },
|
|
998
|
+
degradationPreference: DegradationPreference.UNSPECIFIED,
|
|
999
|
+
},
|
|
1000
|
+
{
|
|
1001
|
+
trackType: TrackType.VIDEO,
|
|
1002
|
+
id: 1,
|
|
1003
|
+
// @ts-expect-error incomplete data
|
|
1004
|
+
codec: { name: 'av1' },
|
|
1005
|
+
degradationPreference: DegradationPreference.UNSPECIFIED,
|
|
1006
|
+
},
|
|
1007
|
+
{
|
|
1008
|
+
trackType: TrackType.VIDEO,
|
|
1009
|
+
id: 2,
|
|
1010
|
+
// @ts-expect-error incomplete data
|
|
1011
|
+
codec: { name: 'vp9' },
|
|
1012
|
+
degradationPreference: DegradationPreference.UNSPECIFIED,
|
|
1013
|
+
},
|
|
692
1014
|
];
|
|
693
1015
|
|
|
694
1016
|
publisher['transceiverCache'].add({
|