@stream-io/video-client 1.50.0 → 1.52.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 (61) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/index.browser.es.js +597 -70
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +597 -69
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.es.js +597 -70
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/src/Call.d.ts +1 -0
  9. package/dist/src/devices/CameraManager.d.ts +1 -0
  10. package/dist/src/devices/DeviceManager.d.ts +20 -0
  11. package/dist/src/devices/VirtualDevice.d.ts +59 -0
  12. package/dist/src/devices/devicePersistence.d.ts +1 -1
  13. package/dist/src/devices/index.d.ts +1 -0
  14. package/dist/src/gen/video/sfu/event/events.d.ts +4 -0
  15. package/dist/src/gen/video/sfu/models/models.d.ts +204 -2
  16. package/dist/src/gen/video/sfu/signal_rpc/signal.client.d.ts +9 -1
  17. package/dist/src/gen/video/sfu/signal_rpc/signal.d.ts +67 -1
  18. package/dist/src/helpers/participantUtils.d.ts +10 -0
  19. package/dist/src/rtc/BasePeerConnection.d.ts +8 -3
  20. package/dist/src/rtc/Publisher.d.ts +21 -3
  21. package/dist/src/rtc/TransceiverCache.d.ts +5 -1
  22. package/dist/src/rtc/helpers/degradationPreference.d.ts +1 -0
  23. package/dist/src/rtc/types.d.ts +3 -0
  24. package/dist/src/stats/rtc/StatsTracer.d.ts +2 -1
  25. package/dist/src/stats/utils.d.ts +1 -0
  26. package/package.json +14 -14
  27. package/src/Call.ts +27 -12
  28. package/src/devices/CameraManager.ts +9 -2
  29. package/src/devices/DeviceManager.ts +148 -8
  30. package/src/devices/DeviceManagerState.ts +4 -1
  31. package/src/devices/VirtualDevice.ts +69 -0
  32. package/src/devices/__tests__/CameraManager.test.ts +22 -1
  33. package/src/devices/__tests__/DeviceManager.test.ts +124 -2
  34. package/src/devices/__tests__/MicrophoneManager.test.ts +3 -1
  35. package/src/devices/__tests__/MicrophoneManagerRN.test.ts +3 -1
  36. package/src/devices/__tests__/ScreenShareManager.test.ts +3 -1
  37. package/src/devices/__tests__/web-audio.mocks.ts +3 -1
  38. package/src/devices/devicePersistence.ts +2 -1
  39. package/src/devices/index.ts +1 -0
  40. package/src/gen/video/sfu/event/events.ts +10 -0
  41. package/src/gen/video/sfu/models/models.ts +338 -0
  42. package/src/gen/video/sfu/signal_rpc/signal.client.ts +28 -2
  43. package/src/gen/video/sfu/signal_rpc/signal.ts +121 -15
  44. package/src/helpers/__tests__/DynascaleManager.test.ts +8 -7
  45. package/src/helpers/__tests__/browsers.test.ts +4 -4
  46. package/src/helpers/__tests__/participantUtils.test.ts +47 -0
  47. package/src/helpers/client-details.ts +4 -1
  48. package/src/helpers/participantUtils.ts +15 -0
  49. package/src/rtc/BasePeerConnection.ts +22 -4
  50. package/src/rtc/Publisher.ts +140 -41
  51. package/src/rtc/Subscriber.ts +1 -0
  52. package/src/rtc/TransceiverCache.ts +10 -3
  53. package/src/rtc/__tests__/Call.reconnect.test.ts +121 -2
  54. package/src/rtc/__tests__/Publisher.test.ts +659 -112
  55. package/src/rtc/__tests__/Subscriber.test.ts +7 -3
  56. package/src/rtc/__tests__/mocks/webrtc.mocks.ts +16 -15
  57. package/src/rtc/helpers/__tests__/degradationPreference.test.ts +33 -1
  58. package/src/rtc/helpers/degradationPreference.ts +18 -0
  59. package/src/rtc/types.ts +3 -0
  60. package/src/stats/rtc/StatsTracer.ts +25 -4
  61. package/src/stats/rtc/__tests__/StatsTracer.test.ts +155 -0
package/dist/index.es.js CHANGED
@@ -1187,6 +1187,14 @@ var SdkType;
1187
1187
  * @generated from protobuf enum value: SDK_TYPE_PLAIN_JAVASCRIPT = 9;
1188
1188
  */
1189
1189
  SdkType[SdkType["PLAIN_JAVASCRIPT"] = 9] = "PLAIN_JAVASCRIPT";
1190
+ /**
1191
+ * @generated from protobuf enum value: SDK_TYPE_PYTHON = 10;
1192
+ */
1193
+ SdkType[SdkType["PYTHON"] = 10] = "PYTHON";
1194
+ /**
1195
+ * @generated from protobuf enum value: SDK_TYPE_VISION_AGENTS = 11;
1196
+ */
1197
+ SdkType[SdkType["VISION_AGENTS"] = 11] = "VISION_AGENTS";
1190
1198
  })(SdkType || (SdkType = {}));
1191
1199
  /**
1192
1200
  * @generated from protobuf enum stream.video.sfu.models.TrackUnpublishReason
@@ -1398,6 +1406,12 @@ var ClientCapability;
1398
1406
  * @generated from protobuf enum value: CLIENT_CAPABILITY_SUBSCRIBER_VIDEO_PAUSE = 1;
1399
1407
  */
1400
1408
  ClientCapability[ClientCapability["SUBSCRIBER_VIDEO_PAUSE"] = 1] = "SUBSCRIBER_VIDEO_PAUSE";
1409
+ /**
1410
+ * Instructs SFU that stats will be sent to the coordinator
1411
+ *
1412
+ * @generated from protobuf enum value: CLIENT_CAPABILITY_COORDINATOR_STATS = 2;
1413
+ */
1414
+ ClientCapability[ClientCapability["COORDINATOR_STATS"] = 2] = "COORDINATOR_STATS";
1401
1415
  })(ClientCapability || (ClientCapability = {}));
1402
1416
  /**
1403
1417
  * DegradationPreference represents the RTCDegradationPreference from WebRTC.
@@ -1863,6 +1877,12 @@ class ClientDetails$Type extends MessageType {
1863
1877
  { no: 2, name: 'os', kind: 'message', T: () => OS },
1864
1878
  { no: 3, name: 'browser', kind: 'message', T: () => Browser },
1865
1879
  { no: 4, name: 'device', kind: 'message', T: () => Device },
1880
+ {
1881
+ no: 5,
1882
+ name: 'webrtc_version',
1883
+ kind: 'scalar',
1884
+ T: 9 /*ScalarType.STRING*/,
1885
+ },
1866
1886
  ]);
1867
1887
  }
1868
1888
  }
@@ -2135,6 +2155,171 @@ class PerformanceStats$Type extends MessageType {
2135
2155
  * @generated MessageType for protobuf message stream.video.sfu.models.PerformanceStats
2136
2156
  */
2137
2157
  const PerformanceStats = new PerformanceStats$Type();
2158
+ // @generated message type with reflection information, may provide speed optimized methods
2159
+ class RtpBase$Type extends MessageType {
2160
+ constructor() {
2161
+ super('stream.video.sfu.models.RtpBase', [
2162
+ { no: 1, name: 'ssrc', kind: 'scalar', T: 13 /*ScalarType.UINT32*/ },
2163
+ { no: 2, name: 'kind', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
2164
+ {
2165
+ no: 3,
2166
+ name: 'timestamp_ms',
2167
+ kind: 'scalar',
2168
+ T: 1 /*ScalarType.DOUBLE*/,
2169
+ },
2170
+ ]);
2171
+ }
2172
+ }
2173
+ /**
2174
+ * @generated MessageType for protobuf message stream.video.sfu.models.RtpBase
2175
+ */
2176
+ const RtpBase = new RtpBase$Type();
2177
+ // @generated message type with reflection information, may provide speed optimized methods
2178
+ class InboundRtp$Type extends MessageType {
2179
+ constructor() {
2180
+ super('stream.video.sfu.models.InboundRtp', [
2181
+ { no: 1, name: 'base', kind: 'message', T: () => RtpBase },
2182
+ {
2183
+ no: 2,
2184
+ name: 'jitter_seconds',
2185
+ kind: 'scalar',
2186
+ T: 1 /*ScalarType.DOUBLE*/,
2187
+ },
2188
+ {
2189
+ no: 3,
2190
+ name: 'packets_received',
2191
+ kind: 'scalar',
2192
+ T: 4 /*ScalarType.UINT64*/,
2193
+ },
2194
+ {
2195
+ no: 4,
2196
+ name: 'packets_lost',
2197
+ kind: 'scalar',
2198
+ T: 4 /*ScalarType.UINT64*/,
2199
+ },
2200
+ {
2201
+ no: 5,
2202
+ name: 'packet_loss_percent',
2203
+ kind: 'scalar',
2204
+ T: 1 /*ScalarType.DOUBLE*/,
2205
+ },
2206
+ {
2207
+ no: 10,
2208
+ name: 'concealment_events',
2209
+ kind: 'scalar',
2210
+ T: 13 /*ScalarType.UINT32*/,
2211
+ },
2212
+ {
2213
+ no: 11,
2214
+ name: 'concealment_percent',
2215
+ kind: 'scalar',
2216
+ T: 1 /*ScalarType.DOUBLE*/,
2217
+ },
2218
+ { no: 20, name: 'fps', kind: 'scalar', T: 1 /*ScalarType.DOUBLE*/ },
2219
+ {
2220
+ no: 21,
2221
+ name: 'freeze_duration_seconds',
2222
+ kind: 'scalar',
2223
+ T: 1 /*ScalarType.DOUBLE*/,
2224
+ },
2225
+ {
2226
+ no: 22,
2227
+ name: 'avg_decode_time_seconds',
2228
+ kind: 'scalar',
2229
+ T: 1 /*ScalarType.DOUBLE*/,
2230
+ },
2231
+ {
2232
+ no: 23,
2233
+ name: 'min_dimension_px',
2234
+ kind: 'scalar',
2235
+ T: 13 /*ScalarType.UINT32*/,
2236
+ },
2237
+ ]);
2238
+ }
2239
+ }
2240
+ /**
2241
+ * @generated MessageType for protobuf message stream.video.sfu.models.InboundRtp
2242
+ */
2243
+ const InboundRtp = new InboundRtp$Type();
2244
+ // @generated message type with reflection information, may provide speed optimized methods
2245
+ class OutboundRtp$Type extends MessageType {
2246
+ constructor() {
2247
+ super('stream.video.sfu.models.OutboundRtp', [
2248
+ { no: 1, name: 'base', kind: 'message', T: () => RtpBase },
2249
+ { no: 10, name: 'fps', kind: 'scalar', T: 1 /*ScalarType.DOUBLE*/ },
2250
+ {
2251
+ no: 11,
2252
+ name: 'avg_encode_time_seconds',
2253
+ kind: 'scalar',
2254
+ T: 1 /*ScalarType.DOUBLE*/,
2255
+ },
2256
+ {
2257
+ no: 12,
2258
+ name: 'bitrate_bps',
2259
+ kind: 'scalar',
2260
+ T: 1 /*ScalarType.DOUBLE*/,
2261
+ },
2262
+ {
2263
+ no: 13,
2264
+ name: 'min_dimension_px',
2265
+ kind: 'scalar',
2266
+ T: 13 /*ScalarType.UINT32*/,
2267
+ },
2268
+ ]);
2269
+ }
2270
+ }
2271
+ /**
2272
+ * @generated MessageType for protobuf message stream.video.sfu.models.OutboundRtp
2273
+ */
2274
+ const OutboundRtp = new OutboundRtp$Type();
2275
+ // @generated message type with reflection information, may provide speed optimized methods
2276
+ class RemoteInboundRtp$Type extends MessageType {
2277
+ constructor() {
2278
+ super('stream.video.sfu.models.RemoteInboundRtp', [
2279
+ { no: 1, name: 'base', kind: 'message', T: () => RtpBase },
2280
+ {
2281
+ no: 2,
2282
+ name: 'jitter_seconds',
2283
+ kind: 'scalar',
2284
+ T: 1 /*ScalarType.DOUBLE*/,
2285
+ },
2286
+ {
2287
+ no: 3,
2288
+ name: 'round_trip_time_s',
2289
+ kind: 'scalar',
2290
+ T: 1 /*ScalarType.DOUBLE*/,
2291
+ },
2292
+ ]);
2293
+ }
2294
+ }
2295
+ /**
2296
+ * @generated MessageType for protobuf message stream.video.sfu.models.RemoteInboundRtp
2297
+ */
2298
+ const RemoteInboundRtp = new RemoteInboundRtp$Type();
2299
+ // @generated message type with reflection information, may provide speed optimized methods
2300
+ class RemoteOutboundRtp$Type extends MessageType {
2301
+ constructor() {
2302
+ super('stream.video.sfu.models.RemoteOutboundRtp', [
2303
+ { no: 1, name: 'base', kind: 'message', T: () => RtpBase },
2304
+ {
2305
+ no: 2,
2306
+ name: 'jitter_seconds',
2307
+ kind: 'scalar',
2308
+ T: 1 /*ScalarType.DOUBLE*/,
2309
+ },
2310
+ {
2311
+ no: 3,
2312
+ name: 'round_trip_time_s',
2313
+ kind: 'scalar',
2314
+ T: 1 /*ScalarType.DOUBLE*/,
2315
+ },
2316
+ ]);
2317
+ }
2318
+ }
2319
+ /**
2320
+ * @generated MessageType for protobuf message stream.video.sfu.models.RemoteOutboundRtp
2321
+ */
2322
+ const RemoteOutboundRtp = new RemoteOutboundRtp$Type();
2138
2323
 
2139
2324
  var models = /*#__PURE__*/Object.freeze({
2140
2325
  __proto__: null,
@@ -2159,8 +2344,10 @@ var models = /*#__PURE__*/Object.freeze({
2159
2344
  get ErrorCode () { return ErrorCode; },
2160
2345
  get GoAwayReason () { return GoAwayReason; },
2161
2346
  ICETrickle: ICETrickle$1,
2347
+ InboundRtp: InboundRtp,
2162
2348
  InputDevices: InputDevices,
2163
2349
  OS: OS,
2350
+ OutboundRtp: OutboundRtp,
2164
2351
  Participant: Participant,
2165
2352
  ParticipantCount: ParticipantCount,
2166
2353
  get ParticipantSource () { return ParticipantSource; },
@@ -2169,6 +2356,9 @@ var models = /*#__PURE__*/Object.freeze({
2169
2356
  Pin: Pin,
2170
2357
  PublishOption: PublishOption,
2171
2358
  RTMPIngress: RTMPIngress,
2359
+ RemoteInboundRtp: RemoteInboundRtp,
2360
+ RemoteOutboundRtp: RemoteOutboundRtp,
2361
+ RtpBase: RtpBase,
2172
2362
  Sdk: Sdk,
2173
2363
  get SdkType () { return SdkType; },
2174
2364
  StreamQuality: StreamQuality,
@@ -2286,6 +2476,62 @@ class Telemetry$Type extends MessageType {
2286
2476
  */
2287
2477
  const Telemetry = new Telemetry$Type();
2288
2478
  // @generated message type with reflection information, may provide speed optimized methods
2479
+ class SendMetricsRequest$Type extends MessageType {
2480
+ constructor() {
2481
+ super('stream.video.sfu.signal.SendMetricsRequest', [
2482
+ { no: 1, name: 'session_id', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
2483
+ {
2484
+ no: 2,
2485
+ name: 'unified_session_id',
2486
+ kind: 'scalar',
2487
+ T: 9 /*ScalarType.STRING*/,
2488
+ },
2489
+ {
2490
+ no: 3,
2491
+ name: 'inbounds',
2492
+ kind: 'message',
2493
+ repeat: 2 /*RepeatType.UNPACKED*/,
2494
+ T: () => InboundRtp,
2495
+ },
2496
+ {
2497
+ no: 4,
2498
+ name: 'outbounds',
2499
+ kind: 'message',
2500
+ repeat: 2 /*RepeatType.UNPACKED*/,
2501
+ T: () => OutboundRtp,
2502
+ },
2503
+ {
2504
+ no: 5,
2505
+ name: 'remote_inbounds',
2506
+ kind: 'message',
2507
+ repeat: 2 /*RepeatType.UNPACKED*/,
2508
+ T: () => RemoteInboundRtp,
2509
+ },
2510
+ {
2511
+ no: 6,
2512
+ name: 'remote_outbounds',
2513
+ kind: 'message',
2514
+ repeat: 2 /*RepeatType.UNPACKED*/,
2515
+ T: () => RemoteOutboundRtp,
2516
+ },
2517
+ ]);
2518
+ }
2519
+ }
2520
+ /**
2521
+ * @generated MessageType for protobuf message stream.video.sfu.signal.SendMetricsRequest
2522
+ */
2523
+ const SendMetricsRequest = new SendMetricsRequest$Type();
2524
+ // @generated message type with reflection information, may provide speed optimized methods
2525
+ class SendMetricsResponse$Type extends MessageType {
2526
+ constructor() {
2527
+ super('stream.video.sfu.signal.SendMetricsResponse', []);
2528
+ }
2529
+ }
2530
+ /**
2531
+ * @generated MessageType for protobuf message stream.video.sfu.signal.SendMetricsResponse
2532
+ */
2533
+ const SendMetricsResponse = new SendMetricsResponse$Type();
2534
+ // @generated message type with reflection information, may provide speed optimized methods
2289
2535
  class SendStatsRequest$Type extends MessageType {
2290
2536
  constructor() {
2291
2537
  super('stream.video.sfu.signal.SendStatsRequest', [
@@ -2559,6 +2805,12 @@ class SendAnswerRequest$Type extends MessageType {
2559
2805
  },
2560
2806
  { no: 2, name: 'sdp', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
2561
2807
  { no: 3, name: 'session_id', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
2808
+ {
2809
+ no: 4,
2810
+ name: 'negotiation_id',
2811
+ kind: 'scalar',
2812
+ T: 13 /*ScalarType.UINT32*/,
2813
+ },
2562
2814
  ]);
2563
2815
  }
2564
2816
  }
@@ -2666,6 +2918,12 @@ const SignalServer = new ServiceType('stream.video.sfu.signal.SignalServer', [
2666
2918
  I: SendStatsRequest,
2667
2919
  O: SendStatsResponse,
2668
2920
  },
2921
+ {
2922
+ name: 'SendMetrics',
2923
+ options: {},
2924
+ I: SendMetricsRequest,
2925
+ O: SendMetricsResponse,
2926
+ },
2669
2927
  {
2670
2928
  name: 'StartNoiseCancellation',
2671
2929
  options: {},
@@ -3346,6 +3604,12 @@ class SubscriberOffer$Type extends MessageType {
3346
3604
  super('stream.video.sfu.event.SubscriberOffer', [
3347
3605
  { no: 1, name: 'ice_restart', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ },
3348
3606
  { no: 2, name: 'sdp', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
3607
+ {
3608
+ no: 3,
3609
+ name: 'negotiation_id',
3610
+ kind: 'scalar',
3611
+ T: 13 /*ScalarType.UINT32*/,
3612
+ },
3349
3613
  ]);
3350
3614
  }
3351
3615
  }
@@ -3804,18 +4068,25 @@ class SignalServerClient {
3804
4068
  const method = this.methods[6], opt = this._transport.mergeOptions(options);
3805
4069
  return stackIntercept('unary', this._transport, method, opt, input);
3806
4070
  }
4071
+ /**
4072
+ * @generated from protobuf rpc: SendMetrics(stream.video.sfu.signal.SendMetricsRequest) returns (stream.video.sfu.signal.SendMetricsResponse);
4073
+ */
4074
+ sendMetrics(input, options) {
4075
+ const method = this.methods[7], opt = this._transport.mergeOptions(options);
4076
+ return stackIntercept('unary', this._transport, method, opt, input);
4077
+ }
3807
4078
  /**
3808
4079
  * @generated from protobuf rpc: StartNoiseCancellation(stream.video.sfu.signal.StartNoiseCancellationRequest) returns (stream.video.sfu.signal.StartNoiseCancellationResponse);
3809
4080
  */
3810
4081
  startNoiseCancellation(input, options) {
3811
- const method = this.methods[7], opt = this._transport.mergeOptions(options);
4082
+ const method = this.methods[8], opt = this._transport.mergeOptions(options);
3812
4083
  return stackIntercept('unary', this._transport, method, opt, input);
3813
4084
  }
3814
4085
  /**
3815
4086
  * @generated from protobuf rpc: StopNoiseCancellation(stream.video.sfu.signal.StopNoiseCancellationRequest) returns (stream.video.sfu.signal.StopNoiseCancellationResponse);
3816
4087
  */
3817
4088
  stopNoiseCancellation(input, options) {
3818
- const method = this.methods[8], opt = this._transport.mergeOptions(options);
4089
+ const method = this.methods[9], opt = this._transport.mergeOptions(options);
3819
4090
  return stackIntercept('unary', this._transport, method, opt, input);
3820
4091
  }
3821
4092
  }
@@ -4995,6 +5266,16 @@ const hasScreenShareAudio = (p) => p.publishedTracks.includes(TrackType.SCREEN_S
4995
5266
  * @param p the participant.
4996
5267
  */
4997
5268
  const isPinned = (p) => !!p.pin && (p.pin.isLocalPin || p.pin.pinnedAt > 0);
5269
+ /**
5270
+ * Check if a participant has a track that is currently interrupted: the
5271
+ * participant intends to publish it (it is in `publishedTracks`) but no
5272
+ * media is flowing right now (it is in `interruptedTracks`).
5273
+ *
5274
+ * @param p the participant to check.
5275
+ * @param trackType the track type to check.
5276
+ */
5277
+ const hasInterruptedTrack = (p, trackType) => !!p.interruptedTracks?.includes(trackType) &&
5278
+ p.publishedTracks.includes(trackType);
4998
5279
  /**
4999
5280
  * Check if a participant has a paused track of the specified type.
5000
5281
  *
@@ -6360,7 +6641,7 @@ const getSdkVersion = (sdk) => {
6360
6641
  return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
6361
6642
  };
6362
6643
 
6363
- const version = "1.50.0";
6644
+ const version = "1.52.0";
6364
6645
  const [major, minor, patch] = version.split('.');
6365
6646
  let sdkInfo = {
6366
6647
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -6458,6 +6739,7 @@ const getClientDetails = async () => {
6458
6739
  sdk: sdkInfo,
6459
6740
  os: osInfo,
6460
6741
  device: deviceInfo,
6742
+ webrtcVersion: webRtcInfo?.version || '',
6461
6743
  };
6462
6744
  }
6463
6745
  // @ts-expect-error - userAgentData is not yet in the TS types
@@ -6485,11 +6767,12 @@ const getClientDetails = async () => {
6485
6767
  // Eliminates the generic "Chromium" name and "Not)A_Brand" name from the list.
6486
6768
  // https://wicg.github.io/ua-client-hints/#create-arbitrary-brands-section
6487
6769
  const uaBrowser = userAgentData?.fullVersionList?.find((v) => !v.brand.includes('Chromium') && !v.brand.match(/[()\-./:;=?_]/g));
6770
+ const browserVersion = uaBrowser?.version || browser.version || '';
6488
6771
  return {
6489
6772
  sdk: sdkInfo,
6490
6773
  browser: {
6491
6774
  name: uaBrowser?.brand || browser.name || navigator.userAgent,
6492
- version: uaBrowser?.version || browser.version || '',
6775
+ version: browserVersion,
6493
6776
  },
6494
6777
  os: {
6495
6778
  name: userAgentData?.platform || os.name || '',
@@ -6502,6 +6785,7 @@ const getClientDetails = async () => {
6502
6785
  .join(' '),
6503
6786
  version: '',
6504
6787
  },
6788
+ webrtcVersion: browserVersion,
6505
6789
  };
6506
6790
  };
6507
6791
 
@@ -7165,7 +7449,7 @@ class StatsTracer {
7165
7449
  /**
7166
7450
  * Creates a new StatsTracer instance.
7167
7451
  */
7168
- constructor(pc, peerType, trackIdToTrackType) {
7452
+ constructor(pc, peerType, trackIdToTrackType, statsTimestampDriftThresholdMs = 0) {
7169
7453
  this.previousStats = {};
7170
7454
  this.frameTimeHistory = [];
7171
7455
  this.fpsHistory = [];
@@ -7179,7 +7463,7 @@ class StatsTracer {
7179
7463
  */
7180
7464
  this.get = async () => {
7181
7465
  const stats = await this.pc.getStats();
7182
- const currentStats = toObject(stats);
7466
+ const currentStats = toObjectWithCorrectedTimestamp(stats, Date.now(), this.driftThresholdMs);
7183
7467
  const performanceStats = this.withOverrides(this.peerType === PeerType.SUBSCRIBER
7184
7468
  ? this.getDecodeStats(currentStats)
7185
7469
  : this.getEncodeStats(currentStats));
@@ -7298,17 +7582,28 @@ class StatsTracer {
7298
7582
  this.pc = pc;
7299
7583
  this.peerType = peerType;
7300
7584
  this.trackIdToTrackType = trackIdToTrackType;
7585
+ this.driftThresholdMs = statsTimestampDriftThresholdMs;
7301
7586
  }
7302
7587
  }
7303
7588
  /**
7304
- * Convert the stat report to an object.
7589
+ * Convert the stat report to an object, correcting clock drift along the way.
7590
+ * Entries whose `timestamp` differs from `wallNow` by more than `thresholdMs`
7591
+ * are replaced with a clone whose `timestamp` is set to `wallNow`. The platform
7592
+ * clock backing `DOMHighResTimeStamp` can desynchronise from `Date.now()` after
7593
+ * system sleep or clock-jump events (notably on Electron/Chromium), which
7594
+ * corrupts the delta-compressed stats payload. A non-positive `thresholdMs`
7595
+ * disables correction.
7305
7596
  *
7306
7597
  * @param report the stat report to convert.
7598
+ * @param wallNow current wall-clock time used as the drift reference.
7599
+ * @param thresholdMs maximum tolerated drift in milliseconds.
7307
7600
  */
7308
- const toObject = (report) => {
7601
+ const toObjectWithCorrectedTimestamp = (report, wallNow, thresholdMs) => {
7309
7602
  const obj = {};
7603
+ const correct = thresholdMs > 0;
7310
7604
  report.forEach((v, k) => {
7311
- obj[k] = v;
7605
+ const drift = Math.abs(v.timestamp - wallNow);
7606
+ obj[k] = correct && drift > thresholdMs ? { ...v, timestamp: wallNow } : v;
7312
7607
  });
7313
7608
  return obj;
7314
7609
  };
@@ -7440,7 +7735,7 @@ class BasePeerConnection {
7440
7735
  /**
7441
7736
  * Constructs a new `BasePeerConnection` instance.
7442
7737
  */
7443
- constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, onIceConnected, tag, enableTracing, clientPublishOptions, iceRestartDelay = 2500, }) {
7738
+ constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, onIceConnected, tag, enableTracing, clientPublishOptions, iceRestartDelay = 2500, statsTimestampDriftThresholdMs = 0, }) {
7444
7739
  this.iceHasEverConnected = false;
7445
7740
  this.isIceRestarting = false;
7446
7741
  this.isDisposed = false;
@@ -7479,7 +7774,7 @@ class BasePeerConnection {
7479
7774
  this.on = (event, fn) => {
7480
7775
  const getTag = () => this.tag;
7481
7776
  this.subscriptions.push(this.dispatcher.on(event, getTag, (e) => {
7482
- const lockKey = `pc.${this.lock}.${event}`;
7777
+ const lockKey = this.eventLockKey(event);
7483
7778
  withoutConcurrency(lockKey, async () => fn(e)).catch((err) => {
7484
7779
  if (this.isDisposed)
7485
7780
  return;
@@ -7487,6 +7782,13 @@ class BasePeerConnection {
7487
7782
  });
7488
7783
  }));
7489
7784
  };
7785
+ /**
7786
+ * Returns the per-event `withoutConcurrency` tag used to serialize the
7787
+ * dispatcher handler for `event` on this peer connection.
7788
+ */
7789
+ this.eventLockKey = (event) => {
7790
+ return `pc.${this.lock}.${event}`;
7791
+ };
7490
7792
  /**
7491
7793
  * Appends the trickled ICE candidates to the `RTCPeerConnection`.
7492
7794
  */
@@ -7727,7 +8029,7 @@ class BasePeerConnection {
7727
8029
  this.onIceConnected = onIceConnected;
7728
8030
  this.logger = videoLoggerSystem.getLogger(peerType === PeerType.SUBSCRIBER ? 'Subscriber' : 'Publisher', { tags: [tag] });
7729
8031
  this.pc = this.createPeerConnection(connectionConfig);
7730
- this.stats = new StatsTracer(this.pc, peerType, this.trackIdToTrackType);
8032
+ this.stats = new StatsTracer(this.pc, peerType, this.trackIdToTrackType, statsTimestampDriftThresholdMs);
7731
8033
  if (enableTracing) {
7732
8034
  this.tracer = new Tracer(`${tag}-${peerType === PeerType.SUBSCRIBER ? 'sub' : 'pub'}`);
7733
8035
  this.tracer.trace('create', {
@@ -7740,7 +8042,7 @@ class BasePeerConnection {
7740
8042
  /**
7741
8043
  * Disposes the `RTCPeerConnection` instance.
7742
8044
  */
7743
- dispose() {
8045
+ async dispose() {
7744
8046
  clearTimeout(this.iceRestartTimeout);
7745
8047
  this.iceRestartTimeout = undefined;
7746
8048
  clearTimeout(this.preConnectStuckTimeout);
@@ -7762,6 +8064,7 @@ class BasePeerConnection {
7762
8064
  pc.removeEventListener('signalingstatechange', this.onSignalingChange);
7763
8065
  pc.removeEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
7764
8066
  pc.removeEventListener('icegatheringstatechange', this.onIceGatherChange);
8067
+ pc.removeEventListener('connectionstatechange', this.onConnectionStateChange);
7765
8068
  this.unsubscribeIceTrickle?.();
7766
8069
  this.subscriptions.forEach((unsubscribe) => unsubscribe());
7767
8070
  this.subscriptions = [];
@@ -7789,8 +8092,14 @@ class TransceiverCache {
7789
8092
  * Gets the transceiver for the given publish option.
7790
8093
  */
7791
8094
  this.get = (publishOption) => {
7792
- return this.cache.find((bundle) => bundle.publishOption.id === publishOption.id &&
7793
- bundle.publishOption.trackType === publishOption.trackType);
8095
+ return this.getBy(publishOption.id, publishOption.trackType);
8096
+ };
8097
+ /**
8098
+ * Gets the transceiver for the given publish option id and track type.
8099
+ */
8100
+ this.getBy = (publishOptionId, trackType) => {
8101
+ return this.cache.find((bundle) => bundle.publishOption.id === publishOptionId &&
8102
+ bundle.publishOption.trackType === trackType);
7794
8103
  };
7795
8104
  /**
7796
8105
  * Updates the cached bundle with the given patch.
@@ -8075,6 +8384,21 @@ const toRTCDegradationPreference = (preference) => {
8075
8384
  ensureExhausted(preference, 'Unknown degradation preference');
8076
8385
  }
8077
8386
  };
8387
+ const fromRTCDegradationPreference = (preference) => {
8388
+ switch (preference) {
8389
+ case 'balanced':
8390
+ return DegradationPreference.BALANCED;
8391
+ case 'maintain-framerate':
8392
+ return DegradationPreference.MAINTAIN_FRAMERATE;
8393
+ case 'maintain-resolution':
8394
+ return DegradationPreference.MAINTAIN_RESOLUTION;
8395
+ // @ts-expect-error not in the typedefs yet
8396
+ case 'maintain-framerate-and-resolution':
8397
+ return DegradationPreference.MAINTAIN_FRAMERATE_AND_RESOLUTION;
8398
+ default:
8399
+ return DegradationPreference.UNSPECIFIED;
8400
+ }
8401
+ };
8078
8402
 
8079
8403
  /**
8080
8404
  * The `Publisher` is responsible for publishing/unpublishing media streams to/from the SFU
@@ -8109,13 +8433,13 @@ class Publisher extends BasePeerConnection {
8109
8433
  // create a clone of the track as otherwise the same trackId will
8110
8434
  // appear in the SDP in multiple transceivers
8111
8435
  const trackToPublish = this.cloneTrack(track);
8112
- const { transceiver } = this.transceiverCache.get(publishOption) || {};
8113
- if (!transceiver) {
8436
+ const bundle = this.transceiverCache.get(publishOption);
8437
+ if (!bundle) {
8114
8438
  await this.addTransceiver(trackToPublish, publishOption, options);
8115
8439
  }
8116
8440
  else {
8117
- const previousTrack = transceiver.sender.track;
8118
- await this.updateTransceiver(transceiver, trackToPublish, trackType, options);
8441
+ const previousTrack = bundle.transceiver.sender.track;
8442
+ await this.updateTransceiver(bundle, trackToPublish, options);
8119
8443
  if (!isReactNative()) {
8120
8444
  this.stopTrack(previousTrack);
8121
8445
  }
@@ -8150,13 +8474,20 @@ class Publisher extends BasePeerConnection {
8150
8474
  /**
8151
8475
  * Updates the transceiver with the given track and track type.
8152
8476
  */
8153
- this.updateTransceiver = async (transceiver, track, trackType, options = {}) => {
8477
+ this.updateTransceiver = async (bundle, track, options = {}) => {
8478
+ const { transceiver, publishOption } = bundle;
8479
+ const trackType = publishOption.trackType;
8154
8480
  const sender = transceiver.sender;
8155
8481
  if (sender.track)
8156
8482
  this.trackIdToTrackType.delete(sender.track.id);
8157
8483
  await sender.replaceTrack(track);
8158
- if (track)
8484
+ if (track) {
8159
8485
  this.trackIdToTrackType.set(track.id, trackType);
8486
+ if (isFirefox() && bundle.videoSender) {
8487
+ // restore the encoding config from the cache, if any
8488
+ await this.changePublishQuality(bundle.videoSender, bundle);
8489
+ }
8490
+ }
8160
8491
  if (isAudioTrackType(trackType)) {
8161
8492
  await this.updateAudioPublishOptions(trackType, options);
8162
8493
  }
@@ -8216,7 +8547,7 @@ class Publisher extends BasePeerConnection {
8216
8547
  continue;
8217
8548
  // it is safe to stop the track here, it is a clone
8218
8549
  this.stopTrack(transceiver.sender.track);
8219
- await this.updateTransceiver(transceiver, null, publishOption.trackType);
8550
+ await this.updateTransceiver(item, null);
8220
8551
  }
8221
8552
  };
8222
8553
  /**
@@ -8273,33 +8604,38 @@ class Publisher extends BasePeerConnection {
8273
8604
  /**
8274
8605
  * Stops the cloned track that is being published to the SFU.
8275
8606
  */
8276
- this.stopTracks = (...trackTypes) => {
8277
- for (const item of this.transceiverCache.items()) {
8278
- const { publishOption, transceiver } = item;
8279
- if (!trackTypes.includes(publishOption.trackType))
8280
- continue;
8281
- this.stopTrack(transceiver.sender.track);
8282
- }
8607
+ this.stopTracks = async (...trackTypes) => {
8608
+ return withoutConcurrency(this.eventLockKey('changePublishQuality'), async () => {
8609
+ for (const item of this.transceiverCache.items()) {
8610
+ const { publishOption, transceiver } = item;
8611
+ if (!trackTypes.includes(publishOption.trackType))
8612
+ continue;
8613
+ const track = transceiver.sender.track;
8614
+ await this.silenceSenderOnFirefox(item);
8615
+ this.stopTrack(track);
8616
+ }
8617
+ });
8283
8618
  };
8284
8619
  /**
8285
8620
  * Stops all the cloned tracks that are being published to the SFU.
8286
8621
  */
8287
- this.stopAllTracks = () => {
8288
- for (const { transceiver } of this.transceiverCache.items()) {
8289
- this.stopTrack(transceiver.sender.track);
8290
- }
8291
- for (const track of this.clonedTracks) {
8292
- this.stopTrack(track);
8293
- }
8622
+ this.stopAllTracks = async () => {
8623
+ return withoutConcurrency(this.eventLockKey('changePublishQuality'), async () => {
8624
+ for (const item of this.transceiverCache.items()) {
8625
+ const track = item.transceiver.sender.track;
8626
+ await this.silenceSenderOnFirefox(item);
8627
+ this.stopTrack(track);
8628
+ }
8629
+ for (const track of this.clonedTracks) {
8630
+ this.stopTrack(track);
8631
+ }
8632
+ });
8294
8633
  };
8295
- this.changePublishQuality = async (videoSender) => {
8296
- const { trackType, layers, publishOptionId } = videoSender;
8297
- const enabledLayers = layers.filter((l) => l.active);
8634
+ this.changePublishQuality = async (videoSender, bundle) => {
8635
+ const enabledLayers = videoSender.layers.filter((l) => l.active);
8298
8636
  const tag = 'Update publish quality:';
8299
8637
  this.logger.info(`${tag} requested layers by SFU:`, enabledLayers);
8300
- const transceiverId = this.transceiverCache.find((t) => t.publishOption.id === publishOptionId &&
8301
- t.publishOption.trackType === trackType);
8302
- const sender = transceiverId?.transceiver.sender;
8638
+ const sender = bundle?.transceiver.sender;
8303
8639
  if (!sender) {
8304
8640
  return this.logger.warn(`${tag} no video sender found.`);
8305
8641
  }
@@ -8307,7 +8643,7 @@ class Publisher extends BasePeerConnection {
8307
8643
  if (params.encodings.length === 0) {
8308
8644
  return this.logger.warn(`${tag} there are no encodings set.`);
8309
8645
  }
8310
- const codecInUse = transceiverId?.publishOption.codec?.name;
8646
+ const codecInUse = bundle?.publishOption.codec?.name;
8311
8647
  const usesSvcCodec = codecInUse && isSvcCodec(codecInUse);
8312
8648
  let changed = false;
8313
8649
  for (const encoder of params.encodings) {
@@ -8507,6 +8843,72 @@ class Publisher extends BasePeerConnection {
8507
8843
  track.stop();
8508
8844
  this.clonedTracks.delete(track);
8509
8845
  };
8846
+ /**
8847
+ * Silences a Firefox sender on the wire during unpublish.
8848
+ *
8849
+ * Firefox keeps emitting RTP after track.stop(), but the right lever
8850
+ * differs by track type:
8851
+ * - audio: `replaceTrack(null)` is the only reliable silencer;
8852
+ * `setParameters({encodings:[...active:false]})` does NOT stop
8853
+ * the Opus encoder.
8854
+ * - video: `setParameters({encodings:[...active:false]})` pauses
8855
+ * the encoder; `replaceTrack(null)` does NOT reliably stop the
8856
+ * video encoder. The prior active=true configuration is captured
8857
+ * onto `bundle.videoSender` so `updateTransceiver` can restore
8858
+ * it on the next publish.
8859
+ *
8860
+ * No-op on non-Firefox browsers and during teardown.
8861
+ */
8862
+ this.silenceSenderOnFirefox = async (bundle) => {
8863
+ if (this.isDisposed || !isFirefox())
8864
+ return;
8865
+ const { transceiver, publishOption } = bundle;
8866
+ if (isAudioTrackType(publishOption.trackType)) {
8867
+ await transceiver.sender.replaceTrack(null).catch((err) => {
8868
+ this.logger.warn('Failed to clear audio sender track', err);
8869
+ });
8870
+ return;
8871
+ }
8872
+ await this.disableAllEncodings(bundle);
8873
+ };
8874
+ this.disableAllEncodings = async (bundle) => {
8875
+ const { transceiver, publishOption } = bundle;
8876
+ const sender = transceiver.sender;
8877
+ const params = sender.getParameters();
8878
+ if (!params.encodings || params.encodings.length === 0)
8879
+ return;
8880
+ if (!bundle.videoSender) {
8881
+ this.transceiverCache.update(publishOption, {
8882
+ videoSender: {
8883
+ trackType: publishOption.trackType,
8884
+ publishOptionId: publishOption.id,
8885
+ codec: publishOption.codec,
8886
+ degradationPreference: fromRTCDegradationPreference(params.degradationPreference),
8887
+ layers: params.encodings.map((e) => ({
8888
+ name: e.rid ?? 'q',
8889
+ active: e.active ?? true,
8890
+ maxBitrate: e.maxBitrate ?? 0,
8891
+ scaleResolutionDownBy: e.scaleResolutionDownBy ?? 0,
8892
+ maxFramerate: e.maxFramerate ?? 0,
8893
+ // @ts-expect-error scalabilityMode is not in the typedefs yet
8894
+ scalabilityMode: e.scalabilityMode ?? '',
8895
+ })),
8896
+ },
8897
+ });
8898
+ }
8899
+ let changed = false;
8900
+ for (const encoding of params.encodings) {
8901
+ if (encoding.active !== false) {
8902
+ encoding.active = false;
8903
+ changed = true;
8904
+ }
8905
+ }
8906
+ if (!changed)
8907
+ return;
8908
+ await sender.setParameters(params).catch((err) => {
8909
+ this.logger.error('Failed to disable video sender encodings:', err);
8910
+ });
8911
+ };
8510
8912
  this.publishOptions = publishOptions;
8511
8913
  this.on('iceRestart', (iceRestart) => {
8512
8914
  if (iceRestart.peerType !== PeerType.PUBLISHER_UNSPECIFIED)
@@ -8515,7 +8917,16 @@ class Publisher extends BasePeerConnection {
8515
8917
  });
8516
8918
  this.on('changePublishQuality', async (event) => {
8517
8919
  for (const videoSender of event.videoSenders) {
8518
- await this.changePublishQuality(videoSender);
8920
+ // if not publishing, update the encodingConfigCache and don't modify the state.
8921
+ // we'll apply this config on the next publish/unmute.
8922
+ const { trackType, publishOptionId } = videoSender;
8923
+ const bundle = this.transceiverCache.getBy(publishOptionId, trackType);
8924
+ if (bundle) {
8925
+ this.transceiverCache.update(bundle.publishOption, { videoSender });
8926
+ }
8927
+ if (isFirefox() && !this.isPublishing(trackType))
8928
+ continue;
8929
+ await this.changePublishQuality(videoSender, bundle);
8519
8930
  }
8520
8931
  });
8521
8932
  this.on('changePublishOptions', (event) => {
@@ -8526,9 +8937,14 @@ class Publisher extends BasePeerConnection {
8526
8937
  /**
8527
8938
  * Disposes this Publisher instance.
8528
8939
  */
8529
- dispose() {
8530
- super.dispose();
8531
- this.stopAllTracks();
8940
+ async dispose() {
8941
+ await super.dispose();
8942
+ try {
8943
+ await this.stopAllTracks();
8944
+ }
8945
+ catch (err) {
8946
+ this.logger.warn('Failed to stop tracks during dispose', err);
8947
+ }
8532
8948
  this.clonedTracks.clear();
8533
8949
  }
8534
8950
  }
@@ -8699,6 +9115,7 @@ class Subscriber extends BasePeerConnection {
8699
9115
  await this.sfuClient.sendAnswer({
8700
9116
  peerType: PeerType.SUBSCRIBER,
8701
9117
  sdp: answer.sdp || '',
9118
+ negotiationId: subscriberOffer.negotiationId,
8702
9119
  });
8703
9120
  this.isIceRestarting = false;
8704
9121
  };
@@ -11312,8 +11729,8 @@ const normalize = (options) => {
11312
11729
  : false,
11313
11730
  };
11314
11731
  };
11315
- const createSyntheticDevice = (deviceId, kind) => {
11316
- return { deviceId, kind, label: '', groupId: '' };
11732
+ const createSyntheticDevice = (deviceId, kind, label = '') => {
11733
+ return { deviceId, kind, label, groupId: '' };
11317
11734
  };
11318
11735
  const readPreferences = (storageKey) => {
11319
11736
  try {
@@ -11367,6 +11784,8 @@ class DeviceManager {
11367
11784
  this.areSubscriptionsSetUp = false;
11368
11785
  this.isTrackStoppedDueToTrackEnd = false;
11369
11786
  this.filters = [];
11787
+ this.virtualDevicesSubject = new BehaviorSubject([]);
11788
+ this.virtualDeviceConcurrencyTag = Symbol('virtualDeviceConcurrencyTag');
11370
11789
  this.statusChangeConcurrencyTag = Symbol('statusChangeConcurrencyTag');
11371
11790
  this.filterRegistrationConcurrencyTag = Symbol('filterRegistrationConcurrencyTag');
11372
11791
  /**
@@ -11379,6 +11798,7 @@ class DeviceManager {
11379
11798
  this.subscriptions.forEach((s) => s());
11380
11799
  this.subscriptions = [];
11381
11800
  this.areSubscriptionsSetUp = false;
11801
+ this.virtualDevicesSubject.next([]);
11382
11802
  };
11383
11803
  this.runCurrentStreamCleanups = () => {
11384
11804
  this.currentStreamCleanups.forEach((c) => c());
@@ -11437,7 +11857,93 @@ class DeviceManager {
11437
11857
  * @returns an Observable that will be updated if a device is connected or disconnected
11438
11858
  */
11439
11859
  listDevices() {
11440
- return this.getDevices();
11860
+ return combineLatest([this.getDevices(), this.virtualDevicesSubject]).pipe(map(([real, virtual]) => [
11861
+ ...real,
11862
+ ...virtual.map((d) => createSyntheticDevice(d.deviceId, d.kind, d.label)),
11863
+ ]));
11864
+ }
11865
+ /**
11866
+ * Registers a virtual camera or microphone backed by a caller-supplied
11867
+ * stream factory. The device appears in `listDevices()` and can be selected
11868
+ * via `select()` like any real device.
11869
+ *
11870
+ * Web only. React Native is not supported.
11871
+ *
11872
+ * Only supported for camera and microphone managers; calling on any other
11873
+ * manager throws.
11874
+ */
11875
+ registerVirtualDevice(virtualDevice) {
11876
+ if (isReactNative()) {
11877
+ throw new Error('Virtual devices are not supported on React Native.');
11878
+ }
11879
+ if (this.trackType !== TrackType.AUDIO &&
11880
+ this.trackType !== TrackType.VIDEO) {
11881
+ throw new Error('Virtual devices are only supported for camera and microphone.');
11882
+ }
11883
+ const deviceId = `stream-virtual:${generateUUIDv4()}`;
11884
+ const entry = {
11885
+ deviceId,
11886
+ kind: this.mediaDeviceKind,
11887
+ ...virtualDevice,
11888
+ };
11889
+ setCurrentValue(this.virtualDevicesSubject, (current) => [
11890
+ ...current,
11891
+ entry,
11892
+ ]);
11893
+ return {
11894
+ deviceId: entry.deviceId,
11895
+ unregister: async () => {
11896
+ await withoutConcurrency(this.virtualDeviceConcurrencyTag, async () => {
11897
+ setCurrentValue(this.virtualDevicesSubject, (current) => current.filter((d) => d !== entry));
11898
+ if (this.activeVirtualSession?.deviceId === deviceId) {
11899
+ await this.stopActiveVirtualSession();
11900
+ }
11901
+ });
11902
+ if (this.state.selectedDevice === deviceId) {
11903
+ await this.statusChangeSettled();
11904
+ await this.disable({ forceStop: true });
11905
+ await this.select(undefined);
11906
+ }
11907
+ },
11908
+ };
11909
+ }
11910
+ sanitizeVirtualStream(stream) {
11911
+ stream.getTracks().forEach((track) => {
11912
+ const originalGetSettings = track.getSettings.bind(track);
11913
+ track.getSettings = () => {
11914
+ const settings = originalGetSettings();
11915
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
11916
+ const { deviceId, ...rest } = settings;
11917
+ return rest;
11918
+ };
11919
+ });
11920
+ return stream;
11921
+ }
11922
+ findVirtualDevice(deviceId) {
11923
+ if (!deviceId)
11924
+ return undefined;
11925
+ return getCurrentValue(this.virtualDevicesSubject).find((d) => d.deviceId === deviceId);
11926
+ }
11927
+ async stopActiveVirtualSession() {
11928
+ const session = this.activeVirtualSession;
11929
+ this.activeVirtualSession = undefined;
11930
+ await session?.stop?.();
11931
+ }
11932
+ async getSelectedStream(constraints) {
11933
+ const deviceId = this.state.selectedDevice;
11934
+ if (!deviceId?.startsWith('stream-virtual')) {
11935
+ return this.getStream(constraints);
11936
+ }
11937
+ return withoutConcurrency(this.virtualDeviceConcurrencyTag, async () => {
11938
+ const virtualDevice = this.findVirtualDevice(deviceId);
11939
+ if (!virtualDevice) {
11940
+ throw new Error(`Virtual device is not registered: ${deviceId}`);
11941
+ }
11942
+ await this.stopActiveVirtualSession();
11943
+ const { stream, stop } = await virtualDevice.getUserMedia(constraints);
11944
+ this.activeVirtualSession = { deviceId, stop };
11945
+ return this.sanitizeVirtualStream(stream);
11946
+ });
11441
11947
  }
11442
11948
  /**
11443
11949
  * Returns `true` when this device is in enabled state.
@@ -11597,6 +12103,9 @@ class DeviceManager {
11597
12103
  }
11598
12104
  });
11599
12105
  }
12106
+ getResolvedConstraints(constraints) {
12107
+ return constraints;
12108
+ }
11600
12109
  publishStream(stream, options) {
11601
12110
  return this.call.publish(stream, this.trackType, options);
11602
12111
  }
@@ -11617,6 +12126,7 @@ class DeviceManager {
11617
12126
  this.muteLocalStream(stopTracks);
11618
12127
  const allEnded = this.getTracks().every((t) => t.readyState === 'ended');
11619
12128
  if (allEnded) {
12129
+ await this.stopActiveVirtualSession();
11620
12130
  // @ts-expect-error release() is present in react-native-webrtc
11621
12131
  if (typeof mediaStream.release === 'function') {
11622
12132
  // @ts-expect-error called to dispose the stream in RN
@@ -11672,12 +12182,12 @@ class DeviceManager {
11672
12182
  // before chainWith below registers new ones for the new chain.
11673
12183
  this.runCurrentStreamCleanups();
11674
12184
  const defaultConstraints = this.state.defaultConstraints;
11675
- const constraints = {
12185
+ const constraints = this.getResolvedConstraints({
11676
12186
  ...defaultConstraints,
11677
12187
  deviceId: this.state.selectedDevice
11678
12188
  ? { exact: this.state.selectedDevice }
11679
12189
  : undefined,
11680
- };
12190
+ });
11681
12191
  /**
11682
12192
  * Chains two media streams together.
11683
12193
  *
@@ -11734,7 +12244,7 @@ class DeviceManager {
11734
12244
  };
11735
12245
  // the rootStream represents the stream coming from the actual device
11736
12246
  // e.g. camera or microphone stream
11737
- rootStreamPromise = this.getStream(constraints);
12247
+ rootStreamPromise = this.getSelectedStream(constraints);
11738
12248
  // we publish the last MediaStream of the chain
11739
12249
  stream = await this.filters.reduce((parent, entry) => parent
11740
12250
  .then((inputStream) => {
@@ -12051,7 +12561,10 @@ class DeviceManagerState {
12051
12561
  setCurrentValue(this.mediaStreamSubject, stream);
12052
12562
  setCurrentValue(this.rootMediaStreamSubject, rootStream);
12053
12563
  if (rootStream) {
12054
- this.setDevice(this.getDeviceIdFromStream(rootStream));
12564
+ const derived = this.getDeviceIdFromStream(rootStream);
12565
+ if (derived) {
12566
+ this.setDevice(derived);
12567
+ }
12055
12568
  }
12056
12569
  }
12057
12570
  /**
@@ -12264,7 +12777,7 @@ class CameraManager extends DeviceManager {
12264
12777
  getDevices() {
12265
12778
  return getVideoDevices(this.call.tracer);
12266
12779
  }
12267
- getStream(constraints) {
12780
+ getResolvedConstraints(constraints) {
12268
12781
  constraints.width = this.targetResolution.width;
12269
12782
  constraints.height = this.targetResolution.height;
12270
12783
  // We can't set both device id and facing mode
@@ -12275,6 +12788,9 @@ class CameraManager extends DeviceManager {
12275
12788
  constraints.facingMode =
12276
12789
  this.state.direction === 'front' ? 'user' : 'environment';
12277
12790
  }
12791
+ return constraints;
12792
+ }
12793
+ getStream(constraints) {
12278
12794
  return getVideoStream(constraints, this.call.tracer);
12279
12795
  }
12280
12796
  }
@@ -13602,9 +14118,10 @@ class Call {
13602
14118
  this.sfuStatsReporter?.flush();
13603
14119
  this.sfuStatsReporter?.stop();
13604
14120
  this.sfuStatsReporter = undefined;
13605
- this.subscriber?.dispose();
14121
+ this.lastStatsOptions = undefined;
14122
+ await this.subscriber?.dispose();
13606
14123
  this.subscriber = undefined;
13607
- this.publisher?.dispose();
14124
+ await this.publisher?.dispose();
13608
14125
  this.publisher = undefined;
13609
14126
  await this.sfuClient?.leaveAndClose(leaveReason);
13610
14127
  this.sfuClient = undefined;
@@ -13880,15 +14397,17 @@ class Call {
13880
14397
  const performingMigration = this.reconnectStrategy === WebsocketReconnectStrategy.MIGRATE;
13881
14398
  const performingRejoin = this.reconnectStrategy === WebsocketReconnectStrategy.REJOIN;
13882
14399
  const performingFastReconnect = this.reconnectStrategy === WebsocketReconnectStrategy.FAST;
13883
- let statsOptions = this.sfuStatsReporter?.options;
14400
+ let statsOptions = this.lastStatsOptions;
13884
14401
  if (!this.credentials ||
13885
14402
  !statsOptions ||
13886
14403
  performingRejoin ||
13887
- performingMigration) {
14404
+ performingMigration ||
14405
+ data?.migrating_from) {
13888
14406
  try {
13889
14407
  const joinResponse = await this.doJoinRequest(data);
13890
14408
  this.credentials = joinResponse.credentials;
13891
14409
  statsOptions = joinResponse.stats_options;
14410
+ this.lastStatsOptions = statsOptions;
13892
14411
  }
13893
14412
  catch (error) {
13894
14413
  // prevent triggering reconnect flow if the state is OFFLINE
@@ -13995,7 +14514,7 @@ class Call {
13995
14514
  }
13996
14515
  else {
13997
14516
  const connectionConfig = toRtcConfiguration(this.credentials.ice_servers);
13998
- this.initPublisherAndSubscriber({
14517
+ await this.initPublisherAndSubscriber({
13999
14518
  sfuClient,
14000
14519
  connectionConfig,
14001
14520
  clientDetails,
@@ -14140,11 +14659,11 @@ class Call {
14140
14659
  * Initializes the Publisher and Subscriber Peer Connections.
14141
14660
  * @internal
14142
14661
  */
14143
- this.initPublisherAndSubscriber = (opts) => {
14662
+ this.initPublisherAndSubscriber = async (opts) => {
14144
14663
  const { sfuClient, connectionConfig, clientDetails, statsOptions, publishOptions, closePreviousInstances, unifiedSessionId, } = opts;
14145
- const { enable_rtc_stats: enableTracing } = statsOptions;
14664
+ const { enable_rtc_stats: enableTracing, reporting_interval_ms: reportingIntervalMs, } = statsOptions;
14146
14665
  if (closePreviousInstances && this.subscriber) {
14147
- this.subscriber.dispose();
14666
+ await this.subscriber.dispose();
14148
14667
  }
14149
14668
  const basePeerConnectionOptions = {
14150
14669
  sfuClient,
@@ -14153,6 +14672,7 @@ class Call {
14153
14672
  connectionConfig,
14154
14673
  tag: sfuClient.tag,
14155
14674
  enableTracing,
14675
+ statsTimestampDriftThresholdMs: reportingIntervalMs / 2,
14156
14676
  clientPublishOptions: this.clientPublishOptions,
14157
14677
  onReconnectionNeeded: (kind, reason, peerType) => {
14158
14678
  this.reconnect(kind, reason).catch((err) => {
@@ -14173,7 +14693,7 @@ class Call {
14173
14693
  const isAnonymous = this.streamClient.user?.type === 'anonymous';
14174
14694
  if (!isAnonymous) {
14175
14695
  if (closePreviousInstances && this.publisher) {
14176
- this.publisher.dispose();
14696
+ await this.publisher.dispose();
14177
14697
  }
14178
14698
  this.publisher = new Publisher(basePeerConnectionOptions, publishOptions);
14179
14699
  }
@@ -14276,10 +14796,17 @@ class Call {
14276
14796
  * `ICE_NEVER_CONNECTED` increments the unsupported-network counter).
14277
14797
  */
14278
14798
  this.reconnect = async (strategy, reason) => {
14279
- if (this.state.callingState === CallingState.RECONNECTING ||
14799
+ if (this.state.callingState === CallingState.JOINING ||
14800
+ this.state.callingState === CallingState.RECONNECTING ||
14280
14801
  this.state.callingState === CallingState.MIGRATING ||
14281
14802
  this.state.callingState === CallingState.RECONNECTING_FAILED)
14282
14803
  return;
14804
+ // Drop redundant reconnect calls. If a reconnect is already queued or
14805
+ // running for this Call, that entry will resolve whatever broke;
14806
+ // queueing more entries just replays the full REJOIN cycle (one extra
14807
+ // `POST /join` per entry) once the call is already healthy again.
14808
+ if (hasPending(this.reconnectConcurrencyTag))
14809
+ return;
14283
14810
  return withoutConcurrency(this.reconnectConcurrencyTag, async () => {
14284
14811
  const reconnectStartTime = Date.now();
14285
14812
  this.reconnectStrategy = strategy;
@@ -14484,8 +15011,8 @@ class Call {
14484
15011
  this.state.setCallingState(CallingState.JOINED);
14485
15012
  }
14486
15013
  finally {
14487
- currentSubscriber?.dispose();
14488
- currentPublisher?.dispose();
15014
+ await currentSubscriber?.dispose();
15015
+ await currentPublisher?.dispose();
14489
15016
  // and close the previous SFU client, without specifying close code
14490
15017
  currentSfuClient.close(StreamSfuClient.NORMAL_CLOSURE, 'Migrating away');
14491
15018
  }
@@ -14674,7 +15201,7 @@ class Call {
14674
15201
  this.stopPublish = async (...trackTypes) => {
14675
15202
  if (!this.sfuClient || !this.publisher)
14676
15203
  return;
14677
- this.publisher.stopTracks(...trackTypes);
15204
+ await this.publisher.stopTracks(...trackTypes);
14678
15205
  await this.updateLocalStreamState(undefined, ...trackTypes);
14679
15206
  };
14680
15207
  /**
@@ -16787,7 +17314,7 @@ class StreamClient {
16787
17314
  this.getUserAgent = () => {
16788
17315
  if (!this.cachedUserAgent) {
16789
17316
  const { clientAppIdentifier = {} } = this.options;
16790
- const { sdkName = 'js', sdkVersion = "1.50.0", ...extras } = clientAppIdentifier;
17317
+ const { sdkName = 'js', sdkVersion = "1.52.0", ...extras } = clientAppIdentifier;
16791
17318
  this.cachedUserAgent = [
16792
17319
  `stream-video-${sdkName}-v${sdkVersion}`,
16793
17320
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
@@ -17423,5 +17950,5 @@ const humanize = (n) => {
17423
17950
  return String(n);
17424
17951
  };
17425
17952
 
17426
- export { AudioSettingsRequestDefaultDeviceEnum, AudioSettingsResponseDefaultDeviceEnum, browsers as Browsers, Call, CallRecordingFailedEventRecordingTypeEnum, CallRecordingReadyEventRecordingTypeEnum, CallRecordingStartedEventRecordingTypeEnum, CallRecordingStoppedEventRecordingTypeEnum, CallState, CallType, CallTypes, CallingState, CameraManager, CameraManagerState, CreateDeviceRequestPushProviderEnum, DebounceType, DeviceManager, DeviceManagerState, DynascaleManager, ErrorFromResponse, FrameRecordingSettingsRequestModeEnum, FrameRecordingSettingsRequestQualityEnum, FrameRecordingSettingsResponseModeEnum, IndividualRecordingSettingsRequestModeEnum, IndividualRecordingSettingsResponseModeEnum, IngressAudioEncodingOptionsRequestChannelsEnum, IngressSourceRequestFpsEnum, IngressVideoLayerRequestCodecEnum, LayoutSettingsRequestNameEnum, MicrophoneManager, MicrophoneManagerState, NoiseCancellationSettingsModeEnum, OwnCapability, RTMPBroadcastRequestQualityEnum, RTMPSettingsRequestQualityEnum, RawRecordingSettingsRequestModeEnum, RawRecordingSettingsResponseModeEnum, RecordSettingsRequestModeEnum, RecordSettingsRequestQualityEnum, rxUtils as RxUtils, ScreenShareManager, ScreenShareState, events as SfuEvents, SfuJoinError, models as SfuModels, SpeakerManager, SpeakerState, StartClosedCaptionsRequestLanguageEnum, StartTranscriptionRequestLanguageEnum, StreamSfuClient, StreamVideoClient, StreamVideoReadOnlyStateStore, StreamVideoWriteableStateStore, TranscriptionSettingsRequestClosedCaptionModeEnum, TranscriptionSettingsRequestLanguageEnum, TranscriptionSettingsRequestModeEnum, TranscriptionSettingsResponseClosedCaptionModeEnum, TranscriptionSettingsResponseLanguageEnum, TranscriptionSettingsResponseModeEnum, VideoSettingsRequestCameraFacingEnum, VideoSettingsResponseCameraFacingEnum, ViewportTracker, VisibilityState, checkIfAudioOutputChangeSupported, combineComparators, conditional, createSoundDetector, defaultSortPreset, descending, deviceIds$, disposeOfMediaStream, dominantSpeaker, getAudioBrowserPermission, getAudioDevices, getAudioOutputDevices, getAudioStream, getClientDetails, getDeviceState, getScreenShareStream, getSdkInfo, getVideoBrowserPermission, getVideoDevices, getVideoStream, getWebRTCInfo, hasAudio$1 as hasAudio, hasPausedTrack, hasScreenShare, hasScreenShareAudio, hasVideo, humanize, isPinned, livestreamOrAudioRoomSortPreset, logToConsole, name, noopComparator, paginatedLayoutSortPreset, pinned, publishingAudio, publishingVideo, reactionType, resolveDeviceId, role, screenSharing, setDeviceInfo, setOSInfo, setPowerState, setSdkInfo, setThermalState, setWebRTCInfo, speakerLayoutSortPreset, speaking, videoLoggerSystem, withParticipantSource };
17953
+ export { AudioSettingsRequestDefaultDeviceEnum, AudioSettingsResponseDefaultDeviceEnum, browsers as Browsers, Call, CallRecordingFailedEventRecordingTypeEnum, CallRecordingReadyEventRecordingTypeEnum, CallRecordingStartedEventRecordingTypeEnum, CallRecordingStoppedEventRecordingTypeEnum, CallState, CallType, CallTypes, CallingState, CameraManager, CameraManagerState, CreateDeviceRequestPushProviderEnum, DebounceType, DeviceManager, DeviceManagerState, DynascaleManager, ErrorFromResponse, FrameRecordingSettingsRequestModeEnum, FrameRecordingSettingsRequestQualityEnum, FrameRecordingSettingsResponseModeEnum, IndividualRecordingSettingsRequestModeEnum, IndividualRecordingSettingsResponseModeEnum, IngressAudioEncodingOptionsRequestChannelsEnum, IngressSourceRequestFpsEnum, IngressVideoLayerRequestCodecEnum, LayoutSettingsRequestNameEnum, MicrophoneManager, MicrophoneManagerState, NoiseCancellationSettingsModeEnum, OwnCapability, RTMPBroadcastRequestQualityEnum, RTMPSettingsRequestQualityEnum, RawRecordingSettingsRequestModeEnum, RawRecordingSettingsResponseModeEnum, RecordSettingsRequestModeEnum, RecordSettingsRequestQualityEnum, rxUtils as RxUtils, ScreenShareManager, ScreenShareState, events as SfuEvents, SfuJoinError, models as SfuModels, SpeakerManager, SpeakerState, StartClosedCaptionsRequestLanguageEnum, StartTranscriptionRequestLanguageEnum, StreamSfuClient, StreamVideoClient, StreamVideoReadOnlyStateStore, StreamVideoWriteableStateStore, TranscriptionSettingsRequestClosedCaptionModeEnum, TranscriptionSettingsRequestLanguageEnum, TranscriptionSettingsRequestModeEnum, TranscriptionSettingsResponseClosedCaptionModeEnum, TranscriptionSettingsResponseLanguageEnum, TranscriptionSettingsResponseModeEnum, VideoSettingsRequestCameraFacingEnum, VideoSettingsResponseCameraFacingEnum, ViewportTracker, VisibilityState, checkIfAudioOutputChangeSupported, combineComparators, conditional, createSoundDetector, defaultSortPreset, descending, deviceIds$, disposeOfMediaStream, dominantSpeaker, getAudioBrowserPermission, getAudioDevices, getAudioOutputDevices, getAudioStream, getClientDetails, getDeviceState, getScreenShareStream, getSdkInfo, getVideoBrowserPermission, getVideoDevices, getVideoStream, getWebRTCInfo, hasAudio$1 as hasAudio, hasInterruptedTrack, hasPausedTrack, hasScreenShare, hasScreenShareAudio, hasVideo, humanize, isPinned, livestreamOrAudioRoomSortPreset, logToConsole, name, noopComparator, paginatedLayoutSortPreset, pinned, publishingAudio, publishingVideo, reactionType, resolveDeviceId, role, screenSharing, setDeviceInfo, setOSInfo, setPowerState, setSdkInfo, setThermalState, setWebRTCInfo, speakerLayoutSortPreset, speaking, videoLoggerSystem, withParticipantSource };
17427
17954
  //# sourceMappingURL=index.es.js.map