@stream-io/video-client 1.54.0 → 1.55.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.
Files changed (64) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/index.browser.es.js +9641 -8767
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +9638 -8764
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.es.js +9639 -8765
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/src/Call.d.ts +13 -1
  9. package/dist/src/StreamSfuClient.d.ts +11 -3
  10. package/dist/src/coordinator/connection/connection.d.ts +1 -1
  11. package/dist/src/gen/google/protobuf/struct.d.ts +3 -1
  12. package/dist/src/gen/google/protobuf/timestamp.d.ts +3 -1
  13. package/dist/src/gen/video/sfu/event/events.d.ts +22 -1
  14. package/dist/src/gen/video/sfu/models/models.d.ts +4 -0
  15. package/dist/src/gen/video/sfu/signal_rpc/signal.client.d.ts +23 -2
  16. package/dist/src/reporting/ClientEventReporter.d.ts +1 -0
  17. package/dist/src/rtc/BasePeerConnection.d.ts +2 -12
  18. package/dist/src/rtc/IceTrickleBuffer.d.ts +41 -3
  19. package/dist/src/rtc/Publisher.d.ts +5 -2
  20. package/dist/src/rtc/Subscriber.d.ts +8 -0
  21. package/dist/src/rtc/helpers/iceCandiates.d.ts +12 -0
  22. package/dist/src/rtc/types.d.ts +2 -0
  23. package/dist/src/stats/SfuStatsReporter.d.ts +32 -1
  24. package/dist/src/stats/rtc/StatsTracer.d.ts +38 -8
  25. package/dist/src/stats/rtc/Tracer.d.ts +9 -2
  26. package/dist/src/stats/rtc/types.d.ts +10 -4
  27. package/package.json +5 -3
  28. package/src/Call.ts +83 -35
  29. package/src/StreamSfuClient.ts +36 -21
  30. package/src/__tests__/StreamSfuClient.test.ts +159 -1
  31. package/src/__tests__/StreamVideoClient.api.test.ts +123 -97
  32. package/src/coordinator/connection/__tests__/connection.test.ts +22 -0
  33. package/src/coordinator/connection/connection.ts +8 -5
  34. package/src/gen/google/protobuf/struct.ts +7 -12
  35. package/src/gen/google/protobuf/timestamp.ts +6 -7
  36. package/src/gen/video/sfu/event/events.ts +22 -25
  37. package/src/gen/video/sfu/models/models.ts +10 -1
  38. package/src/gen/video/sfu/signal_rpc/signal.client.ts +24 -29
  39. package/src/helpers/MediaPlaybackWatchdog.ts +1 -0
  40. package/src/helpers/__tests__/browsers.test.ts +12 -12
  41. package/src/helpers/browsers.ts +5 -5
  42. package/src/reporting/ClientEventReporter.ts +17 -12
  43. package/src/reporting/__tests__/ClientEventReporter.test.ts +52 -0
  44. package/src/rtc/BasePeerConnection.ts +15 -34
  45. package/src/rtc/IceTrickleBuffer.ts +105 -12
  46. package/src/rtc/Publisher.ts +23 -19
  47. package/src/rtc/Subscriber.ts +97 -36
  48. package/src/rtc/__tests__/Call.reconnect.test.ts +45 -45
  49. package/src/rtc/__tests__/IceTrickleBuffer.test.ts +127 -0
  50. package/src/rtc/__tests__/Publisher.test.ts +2 -31
  51. package/src/rtc/__tests__/Subscriber.test.ts +271 -20
  52. package/src/rtc/helpers/__tests__/iceCandiates.test.ts +88 -0
  53. package/src/rtc/helpers/degradationPreference.ts +1 -0
  54. package/src/rtc/helpers/iceCandiates.ts +35 -0
  55. package/src/rtc/helpers/sdp.ts +3 -2
  56. package/src/rtc/helpers/tracks.ts +2 -0
  57. package/src/rtc/types.ts +2 -0
  58. package/src/stats/SfuStatsReporter.ts +149 -49
  59. package/src/stats/__tests__/SfuStatsReporter.test.ts +235 -0
  60. package/src/stats/rtc/StatsTracer.ts +90 -32
  61. package/src/stats/rtc/Tracer.ts +23 -2
  62. package/src/stats/rtc/__tests__/StatsTracer.test.ts +213 -6
  63. package/src/stats/rtc/__tests__/Tracer.test.ts +34 -0
  64. package/src/stats/rtc/types.ts +11 -4
@@ -141,26 +141,6 @@ describe('Subscriber', () => {
141
141
  );
142
142
  });
143
143
 
144
- it(`isStable() returns true only when ICE is connected/completed and connectionState is connected`, () => {
145
- // @ts-expect-error - private field
146
- subscriber['pc'].iceConnectionState = 'connected';
147
- // @ts-expect-error - private field
148
- subscriber['pc'].connectionState = 'connected';
149
- expect(subscriber.isStable()).toBe(true);
150
-
151
- // @ts-expect-error - private field
152
- subscriber['pc'].iceConnectionState = 'completed';
153
- expect(subscriber.isStable()).toBe(true);
154
-
155
- // @ts-expect-error - private field
156
- subscriber['pc'].iceConnectionState = 'disconnected';
157
- expect(subscriber.isStable()).toBe(false);
158
-
159
- // @ts-expect-error - private field
160
- subscriber['pc'].iceConnectionState = 'new';
161
- expect(subscriber.isStable()).toBe(false);
162
- });
163
-
164
144
  it(`iceHasEverConnected tracks lifetime connectivity`, () => {
165
145
  expect(subscriber['iceHasEverConnected']).toBe(false);
166
146
  simulatePriorIceConnected();
@@ -194,6 +174,277 @@ describe('Subscriber', () => {
194
174
  });
195
175
  });
196
176
 
177
+ describe('Subscriber negotiation', () => {
178
+ const subscriberOffer: SubscriberOffer = {
179
+ sdp: 'subscriber-offer-sdp',
180
+ iceRestart: false,
181
+ negotiationId: 10,
182
+ };
183
+
184
+ beforeEach(() => {
185
+ sfuClient.sendAnswer = vi.fn().mockResolvedValue({ response: {} });
186
+ });
187
+
188
+ it('resets isIceRestarting once a negotiation completes', async () => {
189
+ subscriber['isIceRestarting'] = true;
190
+
191
+ await subscriber['negotiate'](subscriberOffer);
192
+
193
+ expect(sfuClient.sendAnswer).toHaveBeenCalledWith({
194
+ peerType: PeerType.SUBSCRIBER,
195
+ sdp: '',
196
+ negotiationId: 10,
197
+ });
198
+ expect(subscriber['isIceRestarting']).toBe(false);
199
+ });
200
+
201
+ it('resets isIceRestarting even when the negotiation fails', async () => {
202
+ subscriber['isIceRestarting'] = true;
203
+ sfuClient.sendAnswer = vi
204
+ .fn()
205
+ .mockRejectedValue(new Error('send answer failed'));
206
+
207
+ await expect(subscriber['negotiate'](subscriberOffer)).rejects.toThrow(
208
+ 'send answer failed',
209
+ );
210
+ expect(subscriber['isIceRestarting']).toBe(false);
211
+ });
212
+
213
+ it('rolls back the remote description when a negotiation fails mid-offer', async () => {
214
+ const setRemoteDescription = vi.fn().mockResolvedValue({});
215
+ subscriber['pc'].setRemoteDescription = setRemoteDescription;
216
+ // @ts-expect-error - readonly field
217
+ subscriber['pc'].signalingState = 'have-remote-offer';
218
+ sfuClient.sendAnswer = vi
219
+ .fn()
220
+ .mockRejectedValue(new Error('send answer failed'));
221
+
222
+ await expect(subscriber['negotiate'](subscriberOffer)).rejects.toThrow(
223
+ 'send answer failed',
224
+ );
225
+ expect(setRemoteDescription).toHaveBeenCalledWith({ type: 'rollback' });
226
+ });
227
+
228
+ it('does not roll back when the peer connection never applied the offer', async () => {
229
+ // signalingState stays 'stable' because setRemoteDescription rejected
230
+ subscriber['pc'].setRemoteDescription = vi
231
+ .fn()
232
+ .mockRejectedValue(new Error('set remote description failed'));
233
+
234
+ await expect(subscriber['negotiate'](subscriberOffer)).rejects.toThrow(
235
+ 'set remote description failed',
236
+ );
237
+ expect(subscriber['pc'].setRemoteDescription).not.toHaveBeenCalledWith({
238
+ type: 'rollback',
239
+ });
240
+ });
241
+
242
+ it('propagates the original error even when the rollback itself fails', async () => {
243
+ subscriber['pc'].setRemoteDescription = vi
244
+ .fn()
245
+ .mockResolvedValueOnce({}) // applying the offer succeeds
246
+ .mockRejectedValueOnce(new Error('rollback failed')); // rollback fails
247
+ // @ts-expect-error - readonly field
248
+ subscriber['pc'].signalingState = 'have-remote-offer';
249
+ sfuClient.sendAnswer = vi
250
+ .fn()
251
+ .mockRejectedValue(new Error('send answer failed'));
252
+
253
+ await expect(subscriber['negotiate'](subscriberOffer)).rejects.toThrow(
254
+ 'send answer failed',
255
+ );
256
+ });
257
+
258
+ it('restores the previous generation in the buffer when a negotiation rolls back', async () => {
259
+ const sdp = (ufrag: string) =>
260
+ `v=0\r\na=ice-ufrag:${ufrag}\r\na=ice-pwd:pwd\r\n`;
261
+ const updateActiveGeneration = vi.spyOn(
262
+ sfuClient.iceTrickleBuffer,
263
+ 'updateActiveGeneration',
264
+ );
265
+ // previously-committed generation u0; the new (failing) offer is u1
266
+ // @ts-expect-error - overriding readonly mock field
267
+ subscriber['pc'].currentRemoteDescription = {
268
+ type: 'offer',
269
+ sdp: sdp('u0'),
270
+ };
271
+ // @ts-expect-error - overriding readonly mock field
272
+ subscriber['pc'].remoteDescription = { type: 'offer', sdp: sdp('u1') };
273
+ // @ts-expect-error - readonly field
274
+ subscriber['pc'].signalingState = 'have-remote-offer';
275
+ sfuClient.sendAnswer = vi
276
+ .fn()
277
+ .mockRejectedValue(new Error('send answer failed'));
278
+
279
+ await expect(subscriber['negotiate'](subscriberOffer)).rejects.toThrow(
280
+ 'send answer failed',
281
+ );
282
+
283
+ // advanced to the new generation, then restored to the rolled-back one
284
+ expect(updateActiveGeneration).toHaveBeenCalledWith(
285
+ PeerType.SUBSCRIBER,
286
+ sdp('u1'),
287
+ );
288
+ expect(updateActiveGeneration).toHaveBeenLastCalledWith(
289
+ PeerType.SUBSCRIBER,
290
+ sdp('u0'),
291
+ );
292
+ });
293
+ });
294
+
295
+ describe('negotiation failure recovery', () => {
296
+ const offer = SubscriberOffer.create({
297
+ sdp: 'offer-sdp',
298
+ iceRestart: false,
299
+ });
300
+ const dispatchOffer = () =>
301
+ dispatcher.dispatch(
302
+ SfuEvent.create({
303
+ eventPayload: {
304
+ oneofKind: 'subscriberOffer',
305
+ subscriberOffer: offer,
306
+ },
307
+ }) as DispatchableMessage<'subscriberOffer'>,
308
+ 'test',
309
+ );
310
+ const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
311
+
312
+ it('retries via ICE restart on a single negotiation failure', async () => {
313
+ // @ts-expect-error - private method
314
+ subscriber.negotiate = vi.fn().mockRejectedValue(new Error('boom'));
315
+ // @ts-expect-error - protected method
316
+ subscriber.tryRestartIce = vi.fn();
317
+ subscriber['onReconnectionNeeded'] = vi.fn();
318
+
319
+ dispatchOffer();
320
+ await flush();
321
+
322
+ // @ts-expect-error - protected method
323
+ expect(subscriber.tryRestartIce).toHaveBeenCalledTimes(1);
324
+ expect(subscriber['onReconnectionNeeded']).not.toHaveBeenCalled();
325
+ });
326
+
327
+ it('escalates to REJOIN after repeated failures instead of looping ICE restarts', async () => {
328
+ // @ts-expect-error - private method
329
+ subscriber.negotiate = vi.fn().mockRejectedValue(new Error('boom'));
330
+ // @ts-expect-error - protected method
331
+ subscriber.tryRestartIce = vi.fn();
332
+ subscriber['onReconnectionNeeded'] = vi.fn();
333
+
334
+ // three consecutive failures (the configured ceiling)
335
+ for (let i = 0; i < 3; i++) {
336
+ dispatchOffer();
337
+ await flush();
338
+ }
339
+
340
+ // the first two fall through to an ICE restart; the third gives up and rejoins
341
+ // @ts-expect-error - protected method
342
+ expect(subscriber.tryRestartIce).toHaveBeenCalledTimes(2);
343
+ expect(subscriber['onReconnectionNeeded']).toHaveBeenCalledWith(
344
+ WebsocketReconnectStrategy.REJOIN,
345
+ ReconnectReason.SUBSCRIBER_NEGOTIATION_FAILED,
346
+ PeerType.SUBSCRIBER,
347
+ );
348
+ });
349
+
350
+ it('resets the failure counter after a successful negotiation', async () => {
351
+ // @ts-expect-error - private method
352
+ subscriber.negotiate = vi
353
+ .fn()
354
+ .mockRejectedValueOnce(new Error('boom'))
355
+ .mockRejectedValueOnce(new Error('boom'))
356
+ .mockResolvedValueOnce(undefined)
357
+ .mockRejectedValueOnce(new Error('boom'))
358
+ .mockRejectedValueOnce(new Error('boom'));
359
+ // @ts-expect-error - protected method
360
+ subscriber.tryRestartIce = vi.fn();
361
+ subscriber['onReconnectionNeeded'] = vi.fn();
362
+
363
+ for (let i = 0; i < 5; i++) {
364
+ dispatchOffer();
365
+ await flush();
366
+ }
367
+
368
+ // four failures total, but never three in a row, so no REJOIN
369
+ expect(subscriber['onReconnectionNeeded']).not.toHaveBeenCalled();
370
+ // @ts-expect-error - protected method
371
+ expect(subscriber.tryRestartIce).toHaveBeenCalledTimes(4);
372
+ });
373
+ });
374
+
375
+ describe('ICE candidate trickling', () => {
376
+ const trickle = (ufrag: string, candidate: string) => ({
377
+ peerType: PeerType.SUBSCRIBER,
378
+ iceCandidate: JSON.stringify({ usernameFragment: ufrag, candidate }),
379
+ });
380
+
381
+ const sdp = (ufrag: string) =>
382
+ `v=0\r\na=ice-ufrag:${ufrag}\r\na=ice-pwd:pwd\r\n`;
383
+
384
+ const setRemoteUfrag = (ufrag: string) => {
385
+ // @ts-expect-error - overriding readonly remoteDescription on the mock
386
+ subscriber['pc'].remoteDescription = { type: 'offer', sdp: sdp(ufrag) };
387
+ };
388
+
389
+ const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
390
+
391
+ it('declares the active generation from the remote description and adds emitted candidates', async () => {
392
+ const addIceCandidate = vi
393
+ .spyOn(subscriber['pc'], 'addIceCandidate')
394
+ .mockResolvedValue();
395
+ const updateActiveGeneration = vi.spyOn(
396
+ sfuClient.iceTrickleBuffer,
397
+ 'updateActiveGeneration',
398
+ );
399
+ setRemoteUfrag('u1');
400
+ sfuClient.iceTrickleBuffer.push(trickle('u1', 'c1'));
401
+
402
+ subscriber['addTrickledIceCandidates']();
403
+ await flush();
404
+
405
+ expect(updateActiveGeneration).toHaveBeenCalledWith(
406
+ PeerType.SUBSCRIBER,
407
+ sdp('u1'),
408
+ );
409
+ expect(addIceCandidate).toHaveBeenCalledWith({
410
+ usernameFragment: 'u1',
411
+ candidate: 'c1',
412
+ });
413
+ });
414
+
415
+ it('does not re-add a superseded-generation candidate after an ICE restart', async () => {
416
+ const addIceCandidate = vi
417
+ .spyOn(subscriber['pc'], 'addIceCandidate')
418
+ .mockResolvedValue();
419
+
420
+ setRemoteUfrag('u0');
421
+ sfuClient.iceTrickleBuffer.push(trickle('u0', 'c0'));
422
+ subscriber['addTrickledIceCandidates']();
423
+ await flush();
424
+ expect(addIceCandidate).toHaveBeenCalledWith({
425
+ usernameFragment: 'u0',
426
+ candidate: 'c0',
427
+ });
428
+
429
+ addIceCandidate.mockClear();
430
+
431
+ // ICE restart -> generation u1; the old u0 candidate must not be re-added
432
+ setRemoteUfrag('u1');
433
+ sfuClient.iceTrickleBuffer.push(trickle('u1', 'c1'));
434
+ subscriber['addTrickledIceCandidates']();
435
+ await flush();
436
+
437
+ expect(addIceCandidate).toHaveBeenCalledWith({
438
+ usernameFragment: 'u1',
439
+ candidate: 'c1',
440
+ });
441
+ expect(addIceCandidate).not.toHaveBeenCalledWith({
442
+ usernameFragment: 'u0',
443
+ candidate: 'c0',
444
+ });
445
+ });
446
+ });
447
+
197
448
  describe('OnTrack', () => {
198
449
  it('should add unknown tracks to the to the call state', () => {
199
450
  const mediaStream = new MediaStream();
@@ -0,0 +1,88 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { getCandidateUfrag, parseIceUfrag, toJSON } from '../iceCandiates';
3
+
4
+ describe('iceCandiates helpers', () => {
5
+ describe('toJSON', () => {
6
+ it('parses the ufrag from the candidate string when usernameFragment is missing (react-native-webrtc)', () => {
7
+ const candidate = {
8
+ candidate:
9
+ 'candidate:1 1 udp 100 1.2.3.4 5000 typ host ufrag ABC network-id 1',
10
+ sdpMid: '0',
11
+ } as RTCIceCandidate;
12
+
13
+ const result = JSON.parse(toJSON(candidate));
14
+ expect(result.usernameFragment).toBe('ABC');
15
+ expect(result.sdpMid).toBe('0');
16
+ });
17
+
18
+ it('leaves usernameFragment undefined when it is missing and the candidate has no ufrag token', () => {
19
+ const candidate = {
20
+ candidate: 'candidate:1 1 udp 100 1.2.3.4 5000 typ host generation 0',
21
+ } as unknown as RTCIceCandidate;
22
+
23
+ const result = JSON.parse(toJSON(candidate));
24
+ expect(result.usernameFragment).toBeUndefined();
25
+ });
26
+ });
27
+
28
+ describe('parseIceUfrag', () => {
29
+ it('extracts the ice-ufrag from an SDP', () => {
30
+ const sdp = 'v=0\r\na=ice-ufrag:F7gIaBcD\r\na=ice-pwd:somepwd\r\n';
31
+ expect(parseIceUfrag(sdp)).toBe('F7gIaBcD');
32
+ });
33
+
34
+ it('returns undefined when the SDP has no ice-ufrag', () => {
35
+ expect(parseIceUfrag('v=0\r\na=ice-pwd:somepwd\r\n')).toBeUndefined();
36
+ });
37
+
38
+ it('returns undefined for an undefined SDP', () => {
39
+ expect(parseIceUfrag(undefined)).toBeUndefined();
40
+ });
41
+ });
42
+
43
+ describe('getCandidateUfrag', () => {
44
+ it('uses usernameFragment when present', () => {
45
+ expect(
46
+ getCandidateUfrag({
47
+ candidate: 'candidate:1 1 udp 100 1.2.3.4 5000 typ host',
48
+ usernameFragment: 'DEF',
49
+ }),
50
+ ).toBe('DEF');
51
+ });
52
+
53
+ it('prefers usernameFragment over the candidate-string ufrag token', () => {
54
+ expect(
55
+ getCandidateUfrag({
56
+ candidate: 'candidate:1 1 udp 100 1.2.3.4 5000 typ host ufrag ABC',
57
+ usernameFragment: 'DEF',
58
+ }),
59
+ ).toBe('DEF');
60
+ });
61
+
62
+ it('falls back to the ufrag token in the candidate string when usernameFragment is absent', () => {
63
+ expect(
64
+ getCandidateUfrag({
65
+ candidate:
66
+ 'candidate:1 1 udp 100 1.2.3.4 5000 typ host ufrag ABC network-id 1',
67
+ }),
68
+ ).toBe('ABC');
69
+ });
70
+
71
+ it('reads the ufrag from a realistic SFU trickled candidate string', () => {
72
+ // The SFU embeds the generation as a `ufrag` token in the candidate
73
+ // string, so the consumer can classify it without a usernameFragment.
74
+ const candidate =
75
+ 'candidate:842163049 1 udp 1677729535 203.0.113.1 56789 typ srflx raddr 0.0.0.0 rport 0 generation 0 ufrag F7gIaBcD network-id 1 network-cost 10';
76
+ expect(getCandidateUfrag({ candidate })).toBe('F7gIaBcD');
77
+ });
78
+
79
+ it('returns undefined when neither a usernameFragment nor a ufrag token is present', () => {
80
+ expect(
81
+ getCandidateUfrag({
82
+ candidate: 'candidate:1 1 udp 100 1.2.3.4 5000 typ host',
83
+ }),
84
+ ).toBeUndefined();
85
+ expect(getCandidateUfrag({})).toBeUndefined();
86
+ });
87
+ });
88
+ });
@@ -18,6 +18,7 @@ export const toRTCDegradationPreference = (
18
18
  return undefined;
19
19
  default:
20
20
  ensureExhausted(preference, 'Unknown degradation preference');
21
+ return undefined;
21
22
  }
22
23
  };
23
24
 
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Converts the ICE candidate to a JSON string.
3
+ */
4
+ export const toJSON = (candidate: RTCIceCandidate) => {
5
+ if (!candidate.usernameFragment) {
6
+ // react-native-webrtc doesn't include usernameFragment in the candidate
7
+ const usernameFragment = parseUfragFromCandidate(candidate.candidate);
8
+ return JSON.stringify({ ...candidate, usernameFragment });
9
+ }
10
+ return JSON.stringify(candidate.toJSON());
11
+ };
12
+
13
+ /**
14
+ * Extracts the ICE ufrag from an SDP, or `undefined` when absent.
15
+ */
16
+ export const parseIceUfrag = (sdp: string | undefined): string | undefined => {
17
+ return sdp?.match(/^a=ice-ufrag:(\S+)/m)?.[1];
18
+ };
19
+
20
+ /**
21
+ * Extracts the ICE ufrag (generation) a trickled candidate was gathered under.
22
+ */
23
+ export const getCandidateUfrag = (ice: RTCIceCandidateInit) => {
24
+ return ice.usernameFragment ?? parseUfragFromCandidate(ice.candidate);
25
+ };
26
+
27
+ /**
28
+ * Parses the `ufrag` token from a raw ICE candidate string
29
+ * (e.g. `candidate:... ufrag <value> ...`). Returns `undefined` when absent.
30
+ */
31
+ const parseUfragFromCandidate = (candidate: string | undefined) => {
32
+ const segments = candidate?.split(' ') ?? [];
33
+ const index = segments.indexOf('ufrag');
34
+ return index !== -1 ? segments[index + 1] : undefined;
35
+ };
@@ -158,11 +158,12 @@ export const removeCodecsExcept = (
158
158
  // If a specific fmtp profile is requested, only keep payloads whose fmtp config matches it
159
159
  if (fmtpProfileToKeep) {
160
160
  const filtered = new Set<number>();
161
- const required = new Set(fmtpProfileToKeep.split(';'));
161
+ const required = fmtpProfileToKeep.split(';');
162
162
  for (const fmtp of media.fmtp) {
163
+ const actual = new Set(fmtp.config.split(';'));
163
164
  if (
164
165
  payloadsToKeep.has(fmtp.payload) &&
165
- required.difference(new Set(fmtp.config.split(';'))).size === 0
166
+ required.every((part) => actual.has(part))
166
167
  ) {
167
168
  filtered.add(fmtp.payload);
168
169
  }
@@ -23,6 +23,7 @@ export const trackTypeToParticipantStreamKey = (
23
23
  throw new Error('Track type is unspecified');
24
24
  default:
25
25
  ensureExhausted(trackType, 'Unknown track type');
26
+ return undefined;
26
27
  }
27
28
  };
28
29
 
@@ -40,6 +41,7 @@ export const muteTypeToTrackType = (
40
41
  return TrackType.SCREEN_SHARE_AUDIO;
41
42
  default:
42
43
  ensureExhausted(muteType, 'Unknown mute type');
44
+ return undefined;
43
45
  }
44
46
  };
45
47
 
package/src/rtc/types.ts CHANGED
@@ -28,6 +28,8 @@ export const ReconnectReason = {
28
28
  CONNECTION_FAILED: 'connection_failed',
29
29
  /** `restartIce()` rejected. */
30
30
  RESTART_ICE_FAILED: 'restart_ice_failed',
31
+ /** Subscriber renegotiation kept failing, escalate to REJOIN. */
32
+ SUBSCRIBER_NEGOTIATION_FAILED: 'subscriber_negotiation_failed',
31
33
  /** SFU `goAway` event, migrate to a new SFU. */
32
34
  GO_AWAY: 'go_away',
33
35
  /** Network came back online after going offline. */