@stream-io/video-client 1.47.0 → 1.49.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 +20 -0
- package/dist/index.browser.es.js +383 -238
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +382 -238
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.es.js +383 -238
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +35 -1
- package/dist/src/StreamSfuClient.d.ts +8 -1
- package/dist/src/devices/DeviceManagerState.d.ts +13 -0
- package/dist/src/devices/MicrophoneManager.d.ts +0 -1
- package/dist/src/helpers/SlidingWindowRateLimiter.d.ts +28 -0
- package/dist/src/rtc/BasePeerConnection.d.ts +11 -2
- package/dist/src/rtc/index.d.ts +1 -0
- package/dist/src/rtc/types.d.ts +33 -1
- package/dist/src/types.d.ts +11 -0
- package/index.ts +0 -1
- package/package.json +1 -1
- package/src/Call.ts +179 -18
- package/src/StreamSfuClient.ts +75 -12
- package/src/__tests__/Call.publishing.test.ts +103 -0
- package/src/__tests__/StreamSfuClient.test.ts +275 -0
- package/src/devices/DeviceManagerState.ts +20 -0
- package/src/devices/MicrophoneManager.ts +9 -5
- package/src/devices/__tests__/MicrophoneManagerRN.test.ts +28 -29
- package/src/devices/__tests__/ScreenShareManager.test.ts +0 -2
- package/src/devices/devices.ts +2 -1
- package/src/helpers/SlidingWindowRateLimiter.ts +49 -0
- package/src/helpers/__tests__/SlidingWindowRateLimiter.test.ts +43 -0
- package/src/rpc/retryable.ts +0 -1
- package/src/rtc/BasePeerConnection.ts +96 -6
- package/src/rtc/Publisher.ts +2 -1
- package/src/rtc/__tests__/Call.reconnect.test.ts +761 -0
- package/src/rtc/__tests__/Publisher.test.ts +210 -0
- package/src/rtc/__tests__/Subscriber.test.ts +56 -0
- package/src/rtc/index.ts +1 -0
- package/src/rtc/types.ts +38 -1
- package/src/types.ts +9 -0
- package/dist/src/helpers/RNSpeechDetector.d.ts +0 -23
- package/src/helpers/RNSpeechDetector.ts +0 -224
- package/src/helpers/__tests__/RNSpeechDetector.test.ts +0 -52
|
@@ -4,6 +4,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
4
4
|
import { anyString } from 'vitest-mock-extended';
|
|
5
5
|
import { NegotiationError } from '../NegotiationError';
|
|
6
6
|
import { Publisher } from '../Publisher';
|
|
7
|
+
import { ReconnectReason } from '../types';
|
|
7
8
|
import { CallState } from '../../store';
|
|
8
9
|
import { StreamSfuClient } from '../../StreamSfuClient';
|
|
9
10
|
import { DispatchableMessage, Dispatcher } from '../Dispatcher';
|
|
@@ -87,6 +88,7 @@ describe('Publisher', () => {
|
|
|
87
88
|
});
|
|
88
89
|
|
|
89
90
|
afterEach(() => {
|
|
91
|
+
vi.useRealTimers();
|
|
90
92
|
vi.clearAllMocks();
|
|
91
93
|
vi.resetModules();
|
|
92
94
|
publisher.dispose();
|
|
@@ -250,7 +252,14 @@ describe('Publisher', () => {
|
|
|
250
252
|
expect(publisher['negotiate']).toHaveBeenCalled();
|
|
251
253
|
});
|
|
252
254
|
|
|
255
|
+
const simulatePriorIceConnected = () => {
|
|
256
|
+
// @ts-expect-error private api
|
|
257
|
+
publisher['pc'].iceConnectionState = 'connected';
|
|
258
|
+
publisher['onIceConnectionStateChange']();
|
|
259
|
+
};
|
|
260
|
+
|
|
253
261
|
it(`should perform ICE restart when connection state changes to 'failed'`, () => {
|
|
262
|
+
simulatePriorIceConnected();
|
|
254
263
|
vi.spyOn(publisher, 'restartIce').mockResolvedValue();
|
|
255
264
|
// @ts-expect-error private api
|
|
256
265
|
publisher['pc'].iceConnectionState = 'failed';
|
|
@@ -259,6 +268,7 @@ describe('Publisher', () => {
|
|
|
259
268
|
});
|
|
260
269
|
|
|
261
270
|
it(`should perform rejoin when ICE restart fails after connection state changes to 'failed'`, async () => {
|
|
271
|
+
simulatePriorIceConnected();
|
|
262
272
|
const { promise: lock, resolve: unlock } = promiseWithResolvers<void>();
|
|
263
273
|
publisher['onReconnectionNeeded'] = vi
|
|
264
274
|
.fn()
|
|
@@ -274,6 +284,7 @@ describe('Publisher', () => {
|
|
|
274
284
|
});
|
|
275
285
|
|
|
276
286
|
it(`should perform fast reconnect when ICE restart fails with SIGNAL_LOST error`, async () => {
|
|
287
|
+
simulatePriorIceConnected();
|
|
277
288
|
const { promise: lock, resolve: unlock } = promiseWithResolvers<void>();
|
|
278
289
|
publisher['onReconnectionNeeded'] = vi
|
|
279
290
|
.fn()
|
|
@@ -325,6 +336,7 @@ describe('Publisher', () => {
|
|
|
325
336
|
});
|
|
326
337
|
|
|
327
338
|
it(`should perform REJOIN reconnect when ICE restart fails with any other error code`, async () => {
|
|
339
|
+
simulatePriorIceConnected();
|
|
328
340
|
const { promise: lock, resolve: unlock } = promiseWithResolvers<void>();
|
|
329
341
|
publisher['onReconnectionNeeded'] = vi
|
|
330
342
|
.fn()
|
|
@@ -365,6 +377,7 @@ describe('Publisher', () => {
|
|
|
365
377
|
});
|
|
366
378
|
|
|
367
379
|
it(`should schedule ICE restart when connection state changes to 'disconnected'`, () => {
|
|
380
|
+
simulatePriorIceConnected();
|
|
368
381
|
vi.spyOn(publisher, 'restartIce').mockResolvedValue();
|
|
369
382
|
vi.useFakeTimers();
|
|
370
383
|
// @ts-expect-error private api
|
|
@@ -376,6 +389,7 @@ describe('Publisher', () => {
|
|
|
376
389
|
});
|
|
377
390
|
|
|
378
391
|
it(`should perform rejoin when scheduled ICE restart fails`, async () => {
|
|
392
|
+
simulatePriorIceConnected();
|
|
379
393
|
vi.spyOn(publisher, 'restartIce').mockRejectedValue('ICE restart failed');
|
|
380
394
|
const { promise: lock, resolve } = promiseWithResolvers<void>();
|
|
381
395
|
publisher['onReconnectionNeeded'] = vi
|
|
@@ -393,6 +407,202 @@ describe('Publisher', () => {
|
|
|
393
407
|
expect(publisher['onReconnectionNeeded']).toHaveBeenCalled();
|
|
394
408
|
});
|
|
395
409
|
|
|
410
|
+
it(`iceHasEverConnected is false before any connected state is observed`, () => {
|
|
411
|
+
expect(publisher['iceHasEverConnected']).toBe(false);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it(`iceHasEverConnected becomes true after 'connected' ICE state`, () => {
|
|
415
|
+
// @ts-expect-error private api
|
|
416
|
+
publisher['pc'].iceConnectionState = 'connected';
|
|
417
|
+
publisher['onIceConnectionStateChange']();
|
|
418
|
+
expect(publisher['iceHasEverConnected']).toBe(true);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it(`iceHasEverConnected also flips on 'completed' ICE state`, () => {
|
|
422
|
+
// @ts-expect-error private api
|
|
423
|
+
publisher['pc'].iceConnectionState = 'completed';
|
|
424
|
+
publisher['onIceConnectionStateChange']();
|
|
425
|
+
expect(publisher['iceHasEverConnected']).toBe(true);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it(`does NOT call restartIce when ICE never connected and state goes to 'failed' — emits REJOIN with 'ice_never_connected'`, () => {
|
|
429
|
+
vi.spyOn(publisher, 'restartIce').mockResolvedValue();
|
|
430
|
+
publisher['onReconnectionNeeded'] = vi.fn();
|
|
431
|
+
// @ts-expect-error private api
|
|
432
|
+
publisher['pc'].iceConnectionState = 'failed';
|
|
433
|
+
publisher['onIceConnectionStateChange']();
|
|
434
|
+
expect(publisher.restartIce).not.toHaveBeenCalled();
|
|
435
|
+
expect(publisher['onReconnectionNeeded']).toHaveBeenCalledWith(
|
|
436
|
+
WebsocketReconnectStrategy.REJOIN,
|
|
437
|
+
ReconnectReason.ICE_NEVER_CONNECTED,
|
|
438
|
+
PeerType.PUBLISHER_UNSPECIFIED,
|
|
439
|
+
);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it(`pre-connect 'disconnected' does not restart or escalate immediately`, () => {
|
|
443
|
+
// ICE has never reached `connected`. A `disconnected` transition at
|
|
444
|
+
// this point is just the browser's checking phase wobbling; the
|
|
445
|
+
// browser may yet move back to checking/connected. The SDK should
|
|
446
|
+
// wait it out — no synchronous restart, no synchronous REJOIN. Only
|
|
447
|
+
// a terminal `failed` before connect, or the pre-connect watchdog
|
|
448
|
+
// expiring, should escalate via `ICE_NEVER_CONNECTED`.
|
|
449
|
+
vi.spyOn(publisher, 'restartIce').mockResolvedValue();
|
|
450
|
+
publisher['onReconnectionNeeded'] = vi.fn();
|
|
451
|
+
// @ts-expect-error private api
|
|
452
|
+
publisher['pc'].iceConnectionState = 'disconnected';
|
|
453
|
+
publisher['onIceConnectionStateChange']();
|
|
454
|
+
expect(publisher.restartIce).not.toHaveBeenCalled();
|
|
455
|
+
expect(publisher['onReconnectionNeeded']).not.toHaveBeenCalled();
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it(`pre-connect 'disconnected' watchdog escalates to REJOIN if state stays stuck`, () => {
|
|
459
|
+
vi.spyOn(publisher, 'restartIce').mockResolvedValue();
|
|
460
|
+
publisher['onReconnectionNeeded'] = vi.fn();
|
|
461
|
+
vi.useFakeTimers();
|
|
462
|
+
const watchdogMs = publisher['iceRestartDelay'] * 2;
|
|
463
|
+
|
|
464
|
+
// @ts-expect-error private api
|
|
465
|
+
publisher['pc'].iceConnectionState = 'disconnected';
|
|
466
|
+
publisher['onIceConnectionStateChange']();
|
|
467
|
+
// before the watchdog fires, no escalation
|
|
468
|
+
vi.advanceTimersByTime(watchdogMs - 1);
|
|
469
|
+
expect(publisher['onReconnectionNeeded']).not.toHaveBeenCalled();
|
|
470
|
+
|
|
471
|
+
// watchdog fires; still stuck in disconnected → escalate
|
|
472
|
+
vi.advanceTimersByTime(2);
|
|
473
|
+
expect(publisher.restartIce).not.toHaveBeenCalled();
|
|
474
|
+
expect(publisher['onReconnectionNeeded']).toHaveBeenCalledWith(
|
|
475
|
+
WebsocketReconnectStrategy.REJOIN,
|
|
476
|
+
ReconnectReason.ICE_NEVER_CONNECTED,
|
|
477
|
+
PeerType.PUBLISHER_UNSPECIFIED,
|
|
478
|
+
);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it(`pre-connect 'disconnected' watchdog is canceled when ICE recovers to 'connected'`, () => {
|
|
482
|
+
publisher['onReconnectionNeeded'] = vi.fn();
|
|
483
|
+
vi.useFakeTimers();
|
|
484
|
+
const watchdogMs = publisher['iceRestartDelay'] * 2;
|
|
485
|
+
|
|
486
|
+
// @ts-expect-error private api
|
|
487
|
+
publisher['pc'].iceConnectionState = 'disconnected';
|
|
488
|
+
publisher['onIceConnectionStateChange']();
|
|
489
|
+
// recover before the watchdog window expires
|
|
490
|
+
// @ts-expect-error private api
|
|
491
|
+
publisher['pc'].iceConnectionState = 'connected';
|
|
492
|
+
publisher['onIceConnectionStateChange']();
|
|
493
|
+
|
|
494
|
+
// advance past the original watchdog window — must NOT fire now
|
|
495
|
+
vi.advanceTimersByTime(watchdogMs + 100);
|
|
496
|
+
|
|
497
|
+
expect(publisher['iceHasEverConnected']).toBe(true);
|
|
498
|
+
expect(publisher['onReconnectionNeeded']).not.toHaveBeenCalled();
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it(`pre-connect 'disconnected' that recovers to 'connected' continues normally`, () => {
|
|
502
|
+
publisher['onReconnectionNeeded'] = vi.fn();
|
|
503
|
+
// @ts-expect-error private api
|
|
504
|
+
publisher['pc'].iceConnectionState = 'disconnected';
|
|
505
|
+
publisher['onIceConnectionStateChange']();
|
|
506
|
+
// browser now reaches connected — the normal connected branch runs
|
|
507
|
+
// and `iceHasEverConnected` flips to true.
|
|
508
|
+
// @ts-expect-error private api
|
|
509
|
+
publisher['pc'].iceConnectionState = 'connected';
|
|
510
|
+
publisher['onIceConnectionStateChange']();
|
|
511
|
+
expect(publisher['iceHasEverConnected']).toBe(true);
|
|
512
|
+
expect(publisher['onReconnectionNeeded']).not.toHaveBeenCalled();
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it(`isStable() returns false when ICE is 'new'`, () => {
|
|
516
|
+
// @ts-expect-error private api
|
|
517
|
+
publisher['pc'].iceConnectionState = 'new';
|
|
518
|
+
// default connectionState in mock is 'connected'
|
|
519
|
+
expect(publisher.isStable()).toBe(false);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it(`isStable() returns true when ICE is 'connected' and connectionState is 'connected'`, () => {
|
|
523
|
+
// @ts-expect-error private api
|
|
524
|
+
publisher['pc'].iceConnectionState = 'connected';
|
|
525
|
+
// @ts-expect-error private api
|
|
526
|
+
publisher['pc'].connectionState = 'connected';
|
|
527
|
+
expect(publisher.isStable()).toBe(true);
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
it(`isStable() returns true when ICE is 'completed' and connectionState is 'connected'`, () => {
|
|
531
|
+
// @ts-expect-error private api
|
|
532
|
+
publisher['pc'].iceConnectionState = 'completed';
|
|
533
|
+
// @ts-expect-error private api
|
|
534
|
+
publisher['pc'].connectionState = 'connected';
|
|
535
|
+
expect(publisher.isStable()).toBe(true);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it(`isStable() returns false when ICE is 'disconnected'`, () => {
|
|
539
|
+
// @ts-expect-error private api
|
|
540
|
+
publisher['pc'].iceConnectionState = 'disconnected';
|
|
541
|
+
// @ts-expect-error private api
|
|
542
|
+
publisher['pc'].connectionState = 'connected';
|
|
543
|
+
expect(publisher.isStable()).toBe(false);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it(`after connected→disconnected→connected cycle, subsequent 'failed' DOES trigger ICE restart (flag stays true)`, () => {
|
|
547
|
+
// @ts-expect-error private api
|
|
548
|
+
publisher['pc'].iceConnectionState = 'connected';
|
|
549
|
+
publisher['onIceConnectionStateChange']();
|
|
550
|
+
// @ts-expect-error private api
|
|
551
|
+
publisher['pc'].iceConnectionState = 'disconnected';
|
|
552
|
+
publisher['onIceConnectionStateChange']();
|
|
553
|
+
// @ts-expect-error private api
|
|
554
|
+
publisher['pc'].iceConnectionState = 'connected';
|
|
555
|
+
publisher['onIceConnectionStateChange']();
|
|
556
|
+
|
|
557
|
+
vi.spyOn(publisher, 'restartIce').mockResolvedValue();
|
|
558
|
+
publisher['onReconnectionNeeded'] = vi.fn();
|
|
559
|
+
// @ts-expect-error private api
|
|
560
|
+
publisher['pc'].iceConnectionState = 'failed';
|
|
561
|
+
publisher['onIceConnectionStateChange']();
|
|
562
|
+
expect(publisher.restartIce).toHaveBeenCalled();
|
|
563
|
+
// the reason here is the regular restart path, NOT 'ice_never_connected'
|
|
564
|
+
expect(publisher['onReconnectionNeeded']).not.toHaveBeenCalledWith(
|
|
565
|
+
WebsocketReconnectStrategy.REJOIN,
|
|
566
|
+
ReconnectReason.ICE_NEVER_CONNECTED,
|
|
567
|
+
PeerType.PUBLISHER_UNSPECIFIED,
|
|
568
|
+
);
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
it(`connection-state 'failed' (distinct from ICE state) still fires REJOIN even after ICE was connected`, () => {
|
|
572
|
+
// mark ICE as connected first
|
|
573
|
+
// @ts-expect-error private api
|
|
574
|
+
publisher['pc'].iceConnectionState = 'connected';
|
|
575
|
+
publisher['onIceConnectionStateChange']();
|
|
576
|
+
|
|
577
|
+
publisher['onReconnectionNeeded'] = vi.fn();
|
|
578
|
+
// @ts-expect-error private api
|
|
579
|
+
publisher['pc'].connectionState = 'failed';
|
|
580
|
+
publisher['onConnectionStateChange']();
|
|
581
|
+
expect(publisher['onReconnectionNeeded']).toHaveBeenCalledWith(
|
|
582
|
+
WebsocketReconnectStrategy.REJOIN,
|
|
583
|
+
ReconnectReason.CONNECTION_FAILED,
|
|
584
|
+
PeerType.PUBLISHER_UNSPECIFIED,
|
|
585
|
+
);
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it(`'completed' state is treated as connectivity — subsequent 'failed' DOES trigger ICE restart`, () => {
|
|
589
|
+
// @ts-expect-error private api
|
|
590
|
+
publisher['pc'].iceConnectionState = 'completed';
|
|
591
|
+
publisher['onIceConnectionStateChange']();
|
|
592
|
+
|
|
593
|
+
vi.spyOn(publisher, 'restartIce').mockResolvedValue();
|
|
594
|
+
publisher['onReconnectionNeeded'] = vi.fn();
|
|
595
|
+
// @ts-expect-error private api
|
|
596
|
+
publisher['pc'].iceConnectionState = 'failed';
|
|
597
|
+
publisher['onIceConnectionStateChange']();
|
|
598
|
+
expect(publisher.restartIce).toHaveBeenCalled();
|
|
599
|
+
expect(publisher['onReconnectionNeeded']).not.toHaveBeenCalledWith(
|
|
600
|
+
WebsocketReconnectStrategy.REJOIN,
|
|
601
|
+
ReconnectReason.ICE_NEVER_CONNECTED,
|
|
602
|
+
PeerType.PUBLISHER_UNSPECIFIED,
|
|
603
|
+
);
|
|
604
|
+
});
|
|
605
|
+
|
|
396
606
|
it(`should schedule ICE restart but cancel it if connection recovers in the meantime`, () => {
|
|
397
607
|
vi.spyOn(publisher, 'restartIce').mockResolvedValue();
|
|
398
608
|
vi.useFakeTimers();
|
|
@@ -11,8 +11,10 @@ import {
|
|
|
11
11
|
ErrorCode,
|
|
12
12
|
PeerType,
|
|
13
13
|
TrackType,
|
|
14
|
+
WebsocketReconnectStrategy,
|
|
14
15
|
} from '../../gen/video/sfu/models/models';
|
|
15
16
|
import { NegotiationError } from '../NegotiationError';
|
|
17
|
+
import { ReconnectReason } from '../types';
|
|
16
18
|
import { IceTrickleBuffer } from '../IceTrickleBuffer';
|
|
17
19
|
import { StreamClient } from '../../coordinator/connection/client';
|
|
18
20
|
|
|
@@ -62,6 +64,7 @@ describe('Subscriber', () => {
|
|
|
62
64
|
});
|
|
63
65
|
|
|
64
66
|
afterEach(() => {
|
|
67
|
+
vi.useRealTimers();
|
|
65
68
|
vi.clearAllMocks();
|
|
66
69
|
vi.resetModules();
|
|
67
70
|
subscriber.dispose();
|
|
@@ -97,7 +100,14 @@ describe('Subscriber', () => {
|
|
|
97
100
|
});
|
|
98
101
|
});
|
|
99
102
|
|
|
103
|
+
const simulatePriorIceConnected = () => {
|
|
104
|
+
// @ts-expect-error - private field
|
|
105
|
+
subscriber['pc'].iceConnectionState = 'connected';
|
|
106
|
+
subscriber['onIceConnectionStateChange']();
|
|
107
|
+
};
|
|
108
|
+
|
|
100
109
|
it(`should perform ICE restart when connection state changes to 'failed'`, () => {
|
|
110
|
+
simulatePriorIceConnected();
|
|
101
111
|
vi.spyOn(subscriber, 'restartIce').mockResolvedValue();
|
|
102
112
|
// @ts-expect-error - private field
|
|
103
113
|
subscriber['pc'].iceConnectionState = 'failed';
|
|
@@ -106,6 +116,7 @@ describe('Subscriber', () => {
|
|
|
106
116
|
});
|
|
107
117
|
|
|
108
118
|
it(`should perform ICE restart when connection state changes to 'disconnected'`, () => {
|
|
119
|
+
simulatePriorIceConnected();
|
|
109
120
|
vi.spyOn(subscriber, 'restartIce').mockResolvedValue();
|
|
110
121
|
vi.useFakeTimers();
|
|
111
122
|
// @ts-expect-error - private field
|
|
@@ -115,6 +126,51 @@ describe('Subscriber', () => {
|
|
|
115
126
|
expect(subscriber.restartIce).toHaveBeenCalled();
|
|
116
127
|
});
|
|
117
128
|
|
|
129
|
+
it(`does NOT perform ICE restart when ICE never connected and state goes to 'failed' — emits REJOIN with 'ice_never_connected'`, () => {
|
|
130
|
+
vi.spyOn(subscriber, 'restartIce').mockResolvedValue();
|
|
131
|
+
subscriber['onReconnectionNeeded'] = vi.fn();
|
|
132
|
+
// @ts-expect-error - private field
|
|
133
|
+
subscriber['pc'].iceConnectionState = 'failed';
|
|
134
|
+
subscriber['onIceConnectionStateChange']();
|
|
135
|
+
expect(subscriber.restartIce).not.toHaveBeenCalled();
|
|
136
|
+
expect(subscriber['onReconnectionNeeded']).toHaveBeenCalledWith(
|
|
137
|
+
WebsocketReconnectStrategy.REJOIN,
|
|
138
|
+
ReconnectReason.ICE_NEVER_CONNECTED,
|
|
139
|
+
PeerType.SUBSCRIBER,
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it(`isStable() returns true only when ICE is connected/completed and connectionState is connected`, () => {
|
|
144
|
+
// @ts-expect-error - private field
|
|
145
|
+
subscriber['pc'].iceConnectionState = 'connected';
|
|
146
|
+
// @ts-expect-error - private field
|
|
147
|
+
subscriber['pc'].connectionState = 'connected';
|
|
148
|
+
expect(subscriber.isStable()).toBe(true);
|
|
149
|
+
|
|
150
|
+
// @ts-expect-error - private field
|
|
151
|
+
subscriber['pc'].iceConnectionState = 'completed';
|
|
152
|
+
expect(subscriber.isStable()).toBe(true);
|
|
153
|
+
|
|
154
|
+
// @ts-expect-error - private field
|
|
155
|
+
subscriber['pc'].iceConnectionState = 'disconnected';
|
|
156
|
+
expect(subscriber.isStable()).toBe(false);
|
|
157
|
+
|
|
158
|
+
// @ts-expect-error - private field
|
|
159
|
+
subscriber['pc'].iceConnectionState = 'new';
|
|
160
|
+
expect(subscriber.isStable()).toBe(false);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it(`iceHasEverConnected tracks lifetime connectivity`, () => {
|
|
164
|
+
expect(subscriber['iceHasEverConnected']).toBe(false);
|
|
165
|
+
simulatePriorIceConnected();
|
|
166
|
+
expect(subscriber['iceHasEverConnected']).toBe(true);
|
|
167
|
+
// going disconnected does not reset the flag
|
|
168
|
+
// @ts-expect-error - private field
|
|
169
|
+
subscriber['pc'].iceConnectionState = 'disconnected';
|
|
170
|
+
subscriber['onIceConnectionStateChange']();
|
|
171
|
+
expect(subscriber['iceHasEverConnected']).toBe(true);
|
|
172
|
+
});
|
|
173
|
+
|
|
118
174
|
it(`should throw NegotiationError when SFU returns an error`, async () => {
|
|
119
175
|
sfuClient.iceRestart = vi.fn().mockResolvedValue({
|
|
120
176
|
response: {
|
package/src/rtc/index.ts
CHANGED
package/src/rtc/types.ts
CHANGED
|
@@ -10,18 +10,55 @@ import { Dispatcher } from './Dispatcher';
|
|
|
10
10
|
import type { OptimalVideoLayer } from './layers';
|
|
11
11
|
import type { ClientPublishOptions } from '../types';
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Canonical reasons the SDK uses to trigger a reconnection. Free-form strings
|
|
15
|
+
* are still accepted at the callback boundary (e.g. when forwarding an SFU
|
|
16
|
+
* error message), but only the members below influence reconnect-loop
|
|
17
|
+
* behavior. In particular, `Call.reconnect` programmatically inspects
|
|
18
|
+
* `ICE_NEVER_CONNECTED` to drive the unsupported-network detector — pass a
|
|
19
|
+
* canonical member when you want the SDK to react to the reason; pass a
|
|
20
|
+
* free-form string when the value is purely diagnostic.
|
|
21
|
+
*/
|
|
22
|
+
export const ReconnectReason = {
|
|
23
|
+
/** ICE never reached `connected`/`completed`, escalate to REJOIN. */
|
|
24
|
+
ICE_NEVER_CONNECTED: 'ice_never_connected',
|
|
25
|
+
/** RTCPeerConnection.connectionState became `failed`. */
|
|
26
|
+
CONNECTION_FAILED: 'connection_failed',
|
|
27
|
+
/** `restartIce()` rejected. */
|
|
28
|
+
RESTART_ICE_FAILED: 'restart_ice_failed',
|
|
29
|
+
/** SFU `goAway` event, migrate to a new SFU. */
|
|
30
|
+
GO_AWAY: 'go_away',
|
|
31
|
+
/** Network came back online after going offline. */
|
|
32
|
+
NETWORK_BACK_ONLINE: 'network_back_online',
|
|
33
|
+
/** SFU error event with no descriptive message. */
|
|
34
|
+
SFU_ERROR: 'sfu_error',
|
|
35
|
+
} as const;
|
|
36
|
+
|
|
37
|
+
export type ReconnectReason =
|
|
38
|
+
| (typeof ReconnectReason)[keyof typeof ReconnectReason]
|
|
39
|
+
| (string & {});
|
|
40
|
+
|
|
13
41
|
export type OnReconnectionNeeded = (
|
|
14
42
|
kind: WebsocketReconnectStrategy,
|
|
15
|
-
reason:
|
|
43
|
+
reason: ReconnectReason,
|
|
16
44
|
peerType: PeerType,
|
|
17
45
|
) => void;
|
|
18
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Fires the first time a peer connection's ICE transport reaches
|
|
49
|
+
* `connected` or `completed` during its lifetime. Used by `Call` to reset
|
|
50
|
+
* the "ICE never connected" failure counter only when WebRTC has actually
|
|
51
|
+
* recovered, not merely when the SFU join handshake succeeded.
|
|
52
|
+
*/
|
|
53
|
+
export type OnIceConnected = (peerType: PeerType) => void;
|
|
54
|
+
|
|
19
55
|
export type BasePeerConnectionOpts = {
|
|
20
56
|
sfuClient: StreamSfuClient;
|
|
21
57
|
state: CallState;
|
|
22
58
|
connectionConfig?: RTCConfiguration;
|
|
23
59
|
dispatcher: Dispatcher;
|
|
24
60
|
onReconnectionNeeded?: OnReconnectionNeeded;
|
|
61
|
+
onIceConnected?: OnIceConnected;
|
|
25
62
|
tag: string;
|
|
26
63
|
enableTracing: boolean;
|
|
27
64
|
iceRestartDelay?: number;
|
package/src/types.ts
CHANGED
|
@@ -462,6 +462,15 @@ export type StreamRNVideoSDKGlobals = {
|
|
|
462
462
|
*/
|
|
463
463
|
check(permission: 'microphone' | 'camera'): Promise<boolean>;
|
|
464
464
|
};
|
|
465
|
+
nativeEvents: {
|
|
466
|
+
speechActivity: {
|
|
467
|
+
/**
|
|
468
|
+
* Subscribes to native speech activity events.
|
|
469
|
+
* Returns an unsubscribe function.
|
|
470
|
+
*/
|
|
471
|
+
subscribe(cb: (state: { isSoundDetected: boolean }) => void): () => void;
|
|
472
|
+
};
|
|
473
|
+
};
|
|
465
474
|
};
|
|
466
475
|
|
|
467
476
|
declare global {
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { SoundStateChangeHandler } from './sound-detector';
|
|
2
|
-
export declare class RNSpeechDetector {
|
|
3
|
-
private readonly pc1;
|
|
4
|
-
private readonly pc2;
|
|
5
|
-
private audioStream;
|
|
6
|
-
private externalAudioStream;
|
|
7
|
-
private isStopped;
|
|
8
|
-
constructor(externalAudioStream?: MediaStream);
|
|
9
|
-
/**
|
|
10
|
-
* Starts the speech detection.
|
|
11
|
-
*/
|
|
12
|
-
start(onSoundDetectedStateChanged: SoundStateChangeHandler): Promise<() => void>;
|
|
13
|
-
/**
|
|
14
|
-
* Stops the speech detection and releases all allocated resources.
|
|
15
|
-
*/
|
|
16
|
-
private stop;
|
|
17
|
-
/**
|
|
18
|
-
* Public method that detects the audio levels and returns the status.
|
|
19
|
-
*/
|
|
20
|
-
private onSpeakingDetectedStateChange;
|
|
21
|
-
private cleanupAudioStream;
|
|
22
|
-
private forwardIceCandidate;
|
|
23
|
-
}
|