@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.cjs.js CHANGED
@@ -1206,6 +1206,14 @@ var SdkType;
1206
1206
  * @generated from protobuf enum value: SDK_TYPE_PLAIN_JAVASCRIPT = 9;
1207
1207
  */
1208
1208
  SdkType[SdkType["PLAIN_JAVASCRIPT"] = 9] = "PLAIN_JAVASCRIPT";
1209
+ /**
1210
+ * @generated from protobuf enum value: SDK_TYPE_PYTHON = 10;
1211
+ */
1212
+ SdkType[SdkType["PYTHON"] = 10] = "PYTHON";
1213
+ /**
1214
+ * @generated from protobuf enum value: SDK_TYPE_VISION_AGENTS = 11;
1215
+ */
1216
+ SdkType[SdkType["VISION_AGENTS"] = 11] = "VISION_AGENTS";
1209
1217
  })(SdkType || (SdkType = {}));
1210
1218
  /**
1211
1219
  * @generated from protobuf enum stream.video.sfu.models.TrackUnpublishReason
@@ -1417,6 +1425,12 @@ var ClientCapability;
1417
1425
  * @generated from protobuf enum value: CLIENT_CAPABILITY_SUBSCRIBER_VIDEO_PAUSE = 1;
1418
1426
  */
1419
1427
  ClientCapability[ClientCapability["SUBSCRIBER_VIDEO_PAUSE"] = 1] = "SUBSCRIBER_VIDEO_PAUSE";
1428
+ /**
1429
+ * Instructs SFU that stats will be sent to the coordinator
1430
+ *
1431
+ * @generated from protobuf enum value: CLIENT_CAPABILITY_COORDINATOR_STATS = 2;
1432
+ */
1433
+ ClientCapability[ClientCapability["COORDINATOR_STATS"] = 2] = "COORDINATOR_STATS";
1420
1434
  })(ClientCapability || (ClientCapability = {}));
1421
1435
  /**
1422
1436
  * DegradationPreference represents the RTCDegradationPreference from WebRTC.
@@ -1882,6 +1896,12 @@ class ClientDetails$Type extends runtime.MessageType {
1882
1896
  { no: 2, name: 'os', kind: 'message', T: () => OS },
1883
1897
  { no: 3, name: 'browser', kind: 'message', T: () => Browser },
1884
1898
  { no: 4, name: 'device', kind: 'message', T: () => Device },
1899
+ {
1900
+ no: 5,
1901
+ name: 'webrtc_version',
1902
+ kind: 'scalar',
1903
+ T: 9 /*ScalarType.STRING*/,
1904
+ },
1885
1905
  ]);
1886
1906
  }
1887
1907
  }
@@ -2154,6 +2174,171 @@ class PerformanceStats$Type extends runtime.MessageType {
2154
2174
  * @generated MessageType for protobuf message stream.video.sfu.models.PerformanceStats
2155
2175
  */
2156
2176
  const PerformanceStats = new PerformanceStats$Type();
2177
+ // @generated message type with reflection information, may provide speed optimized methods
2178
+ class RtpBase$Type extends runtime.MessageType {
2179
+ constructor() {
2180
+ super('stream.video.sfu.models.RtpBase', [
2181
+ { no: 1, name: 'ssrc', kind: 'scalar', T: 13 /*ScalarType.UINT32*/ },
2182
+ { no: 2, name: 'kind', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
2183
+ {
2184
+ no: 3,
2185
+ name: 'timestamp_ms',
2186
+ kind: 'scalar',
2187
+ T: 1 /*ScalarType.DOUBLE*/,
2188
+ },
2189
+ ]);
2190
+ }
2191
+ }
2192
+ /**
2193
+ * @generated MessageType for protobuf message stream.video.sfu.models.RtpBase
2194
+ */
2195
+ const RtpBase = new RtpBase$Type();
2196
+ // @generated message type with reflection information, may provide speed optimized methods
2197
+ class InboundRtp$Type extends runtime.MessageType {
2198
+ constructor() {
2199
+ super('stream.video.sfu.models.InboundRtp', [
2200
+ { no: 1, name: 'base', kind: 'message', T: () => RtpBase },
2201
+ {
2202
+ no: 2,
2203
+ name: 'jitter_seconds',
2204
+ kind: 'scalar',
2205
+ T: 1 /*ScalarType.DOUBLE*/,
2206
+ },
2207
+ {
2208
+ no: 3,
2209
+ name: 'packets_received',
2210
+ kind: 'scalar',
2211
+ T: 4 /*ScalarType.UINT64*/,
2212
+ },
2213
+ {
2214
+ no: 4,
2215
+ name: 'packets_lost',
2216
+ kind: 'scalar',
2217
+ T: 4 /*ScalarType.UINT64*/,
2218
+ },
2219
+ {
2220
+ no: 5,
2221
+ name: 'packet_loss_percent',
2222
+ kind: 'scalar',
2223
+ T: 1 /*ScalarType.DOUBLE*/,
2224
+ },
2225
+ {
2226
+ no: 10,
2227
+ name: 'concealment_events',
2228
+ kind: 'scalar',
2229
+ T: 13 /*ScalarType.UINT32*/,
2230
+ },
2231
+ {
2232
+ no: 11,
2233
+ name: 'concealment_percent',
2234
+ kind: 'scalar',
2235
+ T: 1 /*ScalarType.DOUBLE*/,
2236
+ },
2237
+ { no: 20, name: 'fps', kind: 'scalar', T: 1 /*ScalarType.DOUBLE*/ },
2238
+ {
2239
+ no: 21,
2240
+ name: 'freeze_duration_seconds',
2241
+ kind: 'scalar',
2242
+ T: 1 /*ScalarType.DOUBLE*/,
2243
+ },
2244
+ {
2245
+ no: 22,
2246
+ name: 'avg_decode_time_seconds',
2247
+ kind: 'scalar',
2248
+ T: 1 /*ScalarType.DOUBLE*/,
2249
+ },
2250
+ {
2251
+ no: 23,
2252
+ name: 'min_dimension_px',
2253
+ kind: 'scalar',
2254
+ T: 13 /*ScalarType.UINT32*/,
2255
+ },
2256
+ ]);
2257
+ }
2258
+ }
2259
+ /**
2260
+ * @generated MessageType for protobuf message stream.video.sfu.models.InboundRtp
2261
+ */
2262
+ const InboundRtp = new InboundRtp$Type();
2263
+ // @generated message type with reflection information, may provide speed optimized methods
2264
+ class OutboundRtp$Type extends runtime.MessageType {
2265
+ constructor() {
2266
+ super('stream.video.sfu.models.OutboundRtp', [
2267
+ { no: 1, name: 'base', kind: 'message', T: () => RtpBase },
2268
+ { no: 10, name: 'fps', kind: 'scalar', T: 1 /*ScalarType.DOUBLE*/ },
2269
+ {
2270
+ no: 11,
2271
+ name: 'avg_encode_time_seconds',
2272
+ kind: 'scalar',
2273
+ T: 1 /*ScalarType.DOUBLE*/,
2274
+ },
2275
+ {
2276
+ no: 12,
2277
+ name: 'bitrate_bps',
2278
+ kind: 'scalar',
2279
+ T: 1 /*ScalarType.DOUBLE*/,
2280
+ },
2281
+ {
2282
+ no: 13,
2283
+ name: 'min_dimension_px',
2284
+ kind: 'scalar',
2285
+ T: 13 /*ScalarType.UINT32*/,
2286
+ },
2287
+ ]);
2288
+ }
2289
+ }
2290
+ /**
2291
+ * @generated MessageType for protobuf message stream.video.sfu.models.OutboundRtp
2292
+ */
2293
+ const OutboundRtp = new OutboundRtp$Type();
2294
+ // @generated message type with reflection information, may provide speed optimized methods
2295
+ class RemoteInboundRtp$Type extends runtime.MessageType {
2296
+ constructor() {
2297
+ super('stream.video.sfu.models.RemoteInboundRtp', [
2298
+ { no: 1, name: 'base', kind: 'message', T: () => RtpBase },
2299
+ {
2300
+ no: 2,
2301
+ name: 'jitter_seconds',
2302
+ kind: 'scalar',
2303
+ T: 1 /*ScalarType.DOUBLE*/,
2304
+ },
2305
+ {
2306
+ no: 3,
2307
+ name: 'round_trip_time_s',
2308
+ kind: 'scalar',
2309
+ T: 1 /*ScalarType.DOUBLE*/,
2310
+ },
2311
+ ]);
2312
+ }
2313
+ }
2314
+ /**
2315
+ * @generated MessageType for protobuf message stream.video.sfu.models.RemoteInboundRtp
2316
+ */
2317
+ const RemoteInboundRtp = new RemoteInboundRtp$Type();
2318
+ // @generated message type with reflection information, may provide speed optimized methods
2319
+ class RemoteOutboundRtp$Type extends runtime.MessageType {
2320
+ constructor() {
2321
+ super('stream.video.sfu.models.RemoteOutboundRtp', [
2322
+ { no: 1, name: 'base', kind: 'message', T: () => RtpBase },
2323
+ {
2324
+ no: 2,
2325
+ name: 'jitter_seconds',
2326
+ kind: 'scalar',
2327
+ T: 1 /*ScalarType.DOUBLE*/,
2328
+ },
2329
+ {
2330
+ no: 3,
2331
+ name: 'round_trip_time_s',
2332
+ kind: 'scalar',
2333
+ T: 1 /*ScalarType.DOUBLE*/,
2334
+ },
2335
+ ]);
2336
+ }
2337
+ }
2338
+ /**
2339
+ * @generated MessageType for protobuf message stream.video.sfu.models.RemoteOutboundRtp
2340
+ */
2341
+ const RemoteOutboundRtp = new RemoteOutboundRtp$Type();
2157
2342
 
2158
2343
  var models = /*#__PURE__*/Object.freeze({
2159
2344
  __proto__: null,
@@ -2178,8 +2363,10 @@ var models = /*#__PURE__*/Object.freeze({
2178
2363
  get ErrorCode () { return ErrorCode; },
2179
2364
  get GoAwayReason () { return GoAwayReason; },
2180
2365
  ICETrickle: ICETrickle$1,
2366
+ InboundRtp: InboundRtp,
2181
2367
  InputDevices: InputDevices,
2182
2368
  OS: OS,
2369
+ OutboundRtp: OutboundRtp,
2183
2370
  Participant: Participant,
2184
2371
  ParticipantCount: ParticipantCount,
2185
2372
  get ParticipantSource () { return ParticipantSource; },
@@ -2188,6 +2375,9 @@ var models = /*#__PURE__*/Object.freeze({
2188
2375
  Pin: Pin,
2189
2376
  PublishOption: PublishOption,
2190
2377
  RTMPIngress: RTMPIngress,
2378
+ RemoteInboundRtp: RemoteInboundRtp,
2379
+ RemoteOutboundRtp: RemoteOutboundRtp,
2380
+ RtpBase: RtpBase,
2191
2381
  Sdk: Sdk,
2192
2382
  get SdkType () { return SdkType; },
2193
2383
  StreamQuality: StreamQuality,
@@ -2305,6 +2495,62 @@ class Telemetry$Type extends runtime.MessageType {
2305
2495
  */
2306
2496
  const Telemetry = new Telemetry$Type();
2307
2497
  // @generated message type with reflection information, may provide speed optimized methods
2498
+ class SendMetricsRequest$Type extends runtime.MessageType {
2499
+ constructor() {
2500
+ super('stream.video.sfu.signal.SendMetricsRequest', [
2501
+ { no: 1, name: 'session_id', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
2502
+ {
2503
+ no: 2,
2504
+ name: 'unified_session_id',
2505
+ kind: 'scalar',
2506
+ T: 9 /*ScalarType.STRING*/,
2507
+ },
2508
+ {
2509
+ no: 3,
2510
+ name: 'inbounds',
2511
+ kind: 'message',
2512
+ repeat: 2 /*RepeatType.UNPACKED*/,
2513
+ T: () => InboundRtp,
2514
+ },
2515
+ {
2516
+ no: 4,
2517
+ name: 'outbounds',
2518
+ kind: 'message',
2519
+ repeat: 2 /*RepeatType.UNPACKED*/,
2520
+ T: () => OutboundRtp,
2521
+ },
2522
+ {
2523
+ no: 5,
2524
+ name: 'remote_inbounds',
2525
+ kind: 'message',
2526
+ repeat: 2 /*RepeatType.UNPACKED*/,
2527
+ T: () => RemoteInboundRtp,
2528
+ },
2529
+ {
2530
+ no: 6,
2531
+ name: 'remote_outbounds',
2532
+ kind: 'message',
2533
+ repeat: 2 /*RepeatType.UNPACKED*/,
2534
+ T: () => RemoteOutboundRtp,
2535
+ },
2536
+ ]);
2537
+ }
2538
+ }
2539
+ /**
2540
+ * @generated MessageType for protobuf message stream.video.sfu.signal.SendMetricsRequest
2541
+ */
2542
+ const SendMetricsRequest = new SendMetricsRequest$Type();
2543
+ // @generated message type with reflection information, may provide speed optimized methods
2544
+ class SendMetricsResponse$Type extends runtime.MessageType {
2545
+ constructor() {
2546
+ super('stream.video.sfu.signal.SendMetricsResponse', []);
2547
+ }
2548
+ }
2549
+ /**
2550
+ * @generated MessageType for protobuf message stream.video.sfu.signal.SendMetricsResponse
2551
+ */
2552
+ const SendMetricsResponse = new SendMetricsResponse$Type();
2553
+ // @generated message type with reflection information, may provide speed optimized methods
2308
2554
  class SendStatsRequest$Type extends runtime.MessageType {
2309
2555
  constructor() {
2310
2556
  super('stream.video.sfu.signal.SendStatsRequest', [
@@ -2578,6 +2824,12 @@ class SendAnswerRequest$Type extends runtime.MessageType {
2578
2824
  },
2579
2825
  { no: 2, name: 'sdp', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
2580
2826
  { no: 3, name: 'session_id', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
2827
+ {
2828
+ no: 4,
2829
+ name: 'negotiation_id',
2830
+ kind: 'scalar',
2831
+ T: 13 /*ScalarType.UINT32*/,
2832
+ },
2581
2833
  ]);
2582
2834
  }
2583
2835
  }
@@ -2685,6 +2937,12 @@ const SignalServer = new runtimeRpc.ServiceType('stream.video.sfu.signal.SignalS
2685
2937
  I: SendStatsRequest,
2686
2938
  O: SendStatsResponse,
2687
2939
  },
2940
+ {
2941
+ name: 'SendMetrics',
2942
+ options: {},
2943
+ I: SendMetricsRequest,
2944
+ O: SendMetricsResponse,
2945
+ },
2688
2946
  {
2689
2947
  name: 'StartNoiseCancellation',
2690
2948
  options: {},
@@ -3365,6 +3623,12 @@ class SubscriberOffer$Type extends runtime.MessageType {
3365
3623
  super('stream.video.sfu.event.SubscriberOffer', [
3366
3624
  { no: 1, name: 'ice_restart', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ },
3367
3625
  { no: 2, name: 'sdp', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
3626
+ {
3627
+ no: 3,
3628
+ name: 'negotiation_id',
3629
+ kind: 'scalar',
3630
+ T: 13 /*ScalarType.UINT32*/,
3631
+ },
3368
3632
  ]);
3369
3633
  }
3370
3634
  }
@@ -3823,18 +4087,25 @@ class SignalServerClient {
3823
4087
  const method = this.methods[6], opt = this._transport.mergeOptions(options);
3824
4088
  return runtimeRpc.stackIntercept('unary', this._transport, method, opt, input);
3825
4089
  }
4090
+ /**
4091
+ * @generated from protobuf rpc: SendMetrics(stream.video.sfu.signal.SendMetricsRequest) returns (stream.video.sfu.signal.SendMetricsResponse);
4092
+ */
4093
+ sendMetrics(input, options) {
4094
+ const method = this.methods[7], opt = this._transport.mergeOptions(options);
4095
+ return runtimeRpc.stackIntercept('unary', this._transport, method, opt, input);
4096
+ }
3826
4097
  /**
3827
4098
  * @generated from protobuf rpc: StartNoiseCancellation(stream.video.sfu.signal.StartNoiseCancellationRequest) returns (stream.video.sfu.signal.StartNoiseCancellationResponse);
3828
4099
  */
3829
4100
  startNoiseCancellation(input, options) {
3830
- const method = this.methods[7], opt = this._transport.mergeOptions(options);
4101
+ const method = this.methods[8], opt = this._transport.mergeOptions(options);
3831
4102
  return runtimeRpc.stackIntercept('unary', this._transport, method, opt, input);
3832
4103
  }
3833
4104
  /**
3834
4105
  * @generated from protobuf rpc: StopNoiseCancellation(stream.video.sfu.signal.StopNoiseCancellationRequest) returns (stream.video.sfu.signal.StopNoiseCancellationResponse);
3835
4106
  */
3836
4107
  stopNoiseCancellation(input, options) {
3837
- const method = this.methods[8], opt = this._transport.mergeOptions(options);
4108
+ const method = this.methods[9], opt = this._transport.mergeOptions(options);
3838
4109
  return runtimeRpc.stackIntercept('unary', this._transport, method, opt, input);
3839
4110
  }
3840
4111
  }
@@ -5014,6 +5285,16 @@ const hasScreenShareAudio = (p) => p.publishedTracks.includes(TrackType.SCREEN_S
5014
5285
  * @param p the participant.
5015
5286
  */
5016
5287
  const isPinned = (p) => !!p.pin && (p.pin.isLocalPin || p.pin.pinnedAt > 0);
5288
+ /**
5289
+ * Check if a participant has a track that is currently interrupted: the
5290
+ * participant intends to publish it (it is in `publishedTracks`) but no
5291
+ * media is flowing right now (it is in `interruptedTracks`).
5292
+ *
5293
+ * @param p the participant to check.
5294
+ * @param trackType the track type to check.
5295
+ */
5296
+ const hasInterruptedTrack = (p, trackType) => !!p.interruptedTracks?.includes(trackType) &&
5297
+ p.publishedTracks.includes(trackType);
5017
5298
  /**
5018
5299
  * Check if a participant has a paused track of the specified type.
5019
5300
  *
@@ -6379,7 +6660,7 @@ const getSdkVersion = (sdk) => {
6379
6660
  return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
6380
6661
  };
6381
6662
 
6382
- const version = "1.50.0";
6663
+ const version = "1.52.0";
6383
6664
  const [major, minor, patch] = version.split('.');
6384
6665
  let sdkInfo = {
6385
6666
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -6477,6 +6758,7 @@ const getClientDetails = async () => {
6477
6758
  sdk: sdkInfo,
6478
6759
  os: osInfo,
6479
6760
  device: deviceInfo,
6761
+ webrtcVersion: webRtcInfo?.version || '',
6480
6762
  };
6481
6763
  }
6482
6764
  // @ts-expect-error - userAgentData is not yet in the TS types
@@ -6504,11 +6786,12 @@ const getClientDetails = async () => {
6504
6786
  // Eliminates the generic "Chromium" name and "Not)A_Brand" name from the list.
6505
6787
  // https://wicg.github.io/ua-client-hints/#create-arbitrary-brands-section
6506
6788
  const uaBrowser = userAgentData?.fullVersionList?.find((v) => !v.brand.includes('Chromium') && !v.brand.match(/[()\-./:;=?_]/g));
6789
+ const browserVersion = uaBrowser?.version || browser.version || '';
6507
6790
  return {
6508
6791
  sdk: sdkInfo,
6509
6792
  browser: {
6510
6793
  name: uaBrowser?.brand || browser.name || navigator.userAgent,
6511
- version: uaBrowser?.version || browser.version || '',
6794
+ version: browserVersion,
6512
6795
  },
6513
6796
  os: {
6514
6797
  name: userAgentData?.platform || os.name || '',
@@ -6521,6 +6804,7 @@ const getClientDetails = async () => {
6521
6804
  .join(' '),
6522
6805
  version: '',
6523
6806
  },
6807
+ webrtcVersion: browserVersion,
6524
6808
  };
6525
6809
  };
6526
6810
 
@@ -7184,7 +7468,7 @@ class StatsTracer {
7184
7468
  /**
7185
7469
  * Creates a new StatsTracer instance.
7186
7470
  */
7187
- constructor(pc, peerType, trackIdToTrackType) {
7471
+ constructor(pc, peerType, trackIdToTrackType, statsTimestampDriftThresholdMs = 0) {
7188
7472
  this.previousStats = {};
7189
7473
  this.frameTimeHistory = [];
7190
7474
  this.fpsHistory = [];
@@ -7198,7 +7482,7 @@ class StatsTracer {
7198
7482
  */
7199
7483
  this.get = async () => {
7200
7484
  const stats = await this.pc.getStats();
7201
- const currentStats = toObject(stats);
7485
+ const currentStats = toObjectWithCorrectedTimestamp(stats, Date.now(), this.driftThresholdMs);
7202
7486
  const performanceStats = this.withOverrides(this.peerType === PeerType.SUBSCRIBER
7203
7487
  ? this.getDecodeStats(currentStats)
7204
7488
  : this.getEncodeStats(currentStats));
@@ -7317,17 +7601,28 @@ class StatsTracer {
7317
7601
  this.pc = pc;
7318
7602
  this.peerType = peerType;
7319
7603
  this.trackIdToTrackType = trackIdToTrackType;
7604
+ this.driftThresholdMs = statsTimestampDriftThresholdMs;
7320
7605
  }
7321
7606
  }
7322
7607
  /**
7323
- * Convert the stat report to an object.
7608
+ * Convert the stat report to an object, correcting clock drift along the way.
7609
+ * Entries whose `timestamp` differs from `wallNow` by more than `thresholdMs`
7610
+ * are replaced with a clone whose `timestamp` is set to `wallNow`. The platform
7611
+ * clock backing `DOMHighResTimeStamp` can desynchronise from `Date.now()` after
7612
+ * system sleep or clock-jump events (notably on Electron/Chromium), which
7613
+ * corrupts the delta-compressed stats payload. A non-positive `thresholdMs`
7614
+ * disables correction.
7324
7615
  *
7325
7616
  * @param report the stat report to convert.
7617
+ * @param wallNow current wall-clock time used as the drift reference.
7618
+ * @param thresholdMs maximum tolerated drift in milliseconds.
7326
7619
  */
7327
- const toObject = (report) => {
7620
+ const toObjectWithCorrectedTimestamp = (report, wallNow, thresholdMs) => {
7328
7621
  const obj = {};
7622
+ const correct = thresholdMs > 0;
7329
7623
  report.forEach((v, k) => {
7330
- obj[k] = v;
7624
+ const drift = Math.abs(v.timestamp - wallNow);
7625
+ obj[k] = correct && drift > thresholdMs ? { ...v, timestamp: wallNow } : v;
7331
7626
  });
7332
7627
  return obj;
7333
7628
  };
@@ -7459,7 +7754,7 @@ class BasePeerConnection {
7459
7754
  /**
7460
7755
  * Constructs a new `BasePeerConnection` instance.
7461
7756
  */
7462
- constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, onIceConnected, tag, enableTracing, clientPublishOptions, iceRestartDelay = 2500, }) {
7757
+ constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, onIceConnected, tag, enableTracing, clientPublishOptions, iceRestartDelay = 2500, statsTimestampDriftThresholdMs = 0, }) {
7463
7758
  this.iceHasEverConnected = false;
7464
7759
  this.isIceRestarting = false;
7465
7760
  this.isDisposed = false;
@@ -7498,7 +7793,7 @@ class BasePeerConnection {
7498
7793
  this.on = (event, fn) => {
7499
7794
  const getTag = () => this.tag;
7500
7795
  this.subscriptions.push(this.dispatcher.on(event, getTag, (e) => {
7501
- const lockKey = `pc.${this.lock}.${event}`;
7796
+ const lockKey = this.eventLockKey(event);
7502
7797
  withoutConcurrency(lockKey, async () => fn(e)).catch((err) => {
7503
7798
  if (this.isDisposed)
7504
7799
  return;
@@ -7506,6 +7801,13 @@ class BasePeerConnection {
7506
7801
  });
7507
7802
  }));
7508
7803
  };
7804
+ /**
7805
+ * Returns the per-event `withoutConcurrency` tag used to serialize the
7806
+ * dispatcher handler for `event` on this peer connection.
7807
+ */
7808
+ this.eventLockKey = (event) => {
7809
+ return `pc.${this.lock}.${event}`;
7810
+ };
7509
7811
  /**
7510
7812
  * Appends the trickled ICE candidates to the `RTCPeerConnection`.
7511
7813
  */
@@ -7746,7 +8048,7 @@ class BasePeerConnection {
7746
8048
  this.onIceConnected = onIceConnected;
7747
8049
  this.logger = videoLoggerSystem.getLogger(peerType === PeerType.SUBSCRIBER ? 'Subscriber' : 'Publisher', { tags: [tag] });
7748
8050
  this.pc = this.createPeerConnection(connectionConfig);
7749
- this.stats = new StatsTracer(this.pc, peerType, this.trackIdToTrackType);
8051
+ this.stats = new StatsTracer(this.pc, peerType, this.trackIdToTrackType, statsTimestampDriftThresholdMs);
7750
8052
  if (enableTracing) {
7751
8053
  this.tracer = new Tracer(`${tag}-${peerType === PeerType.SUBSCRIBER ? 'sub' : 'pub'}`);
7752
8054
  this.tracer.trace('create', {
@@ -7759,7 +8061,7 @@ class BasePeerConnection {
7759
8061
  /**
7760
8062
  * Disposes the `RTCPeerConnection` instance.
7761
8063
  */
7762
- dispose() {
8064
+ async dispose() {
7763
8065
  clearTimeout(this.iceRestartTimeout);
7764
8066
  this.iceRestartTimeout = undefined;
7765
8067
  clearTimeout(this.preConnectStuckTimeout);
@@ -7781,6 +8083,7 @@ class BasePeerConnection {
7781
8083
  pc.removeEventListener('signalingstatechange', this.onSignalingChange);
7782
8084
  pc.removeEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
7783
8085
  pc.removeEventListener('icegatheringstatechange', this.onIceGatherChange);
8086
+ pc.removeEventListener('connectionstatechange', this.onConnectionStateChange);
7784
8087
  this.unsubscribeIceTrickle?.();
7785
8088
  this.subscriptions.forEach((unsubscribe) => unsubscribe());
7786
8089
  this.subscriptions = [];
@@ -7808,8 +8111,14 @@ class TransceiverCache {
7808
8111
  * Gets the transceiver for the given publish option.
7809
8112
  */
7810
8113
  this.get = (publishOption) => {
7811
- return this.cache.find((bundle) => bundle.publishOption.id === publishOption.id &&
7812
- bundle.publishOption.trackType === publishOption.trackType);
8114
+ return this.getBy(publishOption.id, publishOption.trackType);
8115
+ };
8116
+ /**
8117
+ * Gets the transceiver for the given publish option id and track type.
8118
+ */
8119
+ this.getBy = (publishOptionId, trackType) => {
8120
+ return this.cache.find((bundle) => bundle.publishOption.id === publishOptionId &&
8121
+ bundle.publishOption.trackType === trackType);
7813
8122
  };
7814
8123
  /**
7815
8124
  * Updates the cached bundle with the given patch.
@@ -8094,6 +8403,21 @@ const toRTCDegradationPreference = (preference) => {
8094
8403
  ensureExhausted(preference, 'Unknown degradation preference');
8095
8404
  }
8096
8405
  };
8406
+ const fromRTCDegradationPreference = (preference) => {
8407
+ switch (preference) {
8408
+ case 'balanced':
8409
+ return DegradationPreference.BALANCED;
8410
+ case 'maintain-framerate':
8411
+ return DegradationPreference.MAINTAIN_FRAMERATE;
8412
+ case 'maintain-resolution':
8413
+ return DegradationPreference.MAINTAIN_RESOLUTION;
8414
+ // @ts-expect-error not in the typedefs yet
8415
+ case 'maintain-framerate-and-resolution':
8416
+ return DegradationPreference.MAINTAIN_FRAMERATE_AND_RESOLUTION;
8417
+ default:
8418
+ return DegradationPreference.UNSPECIFIED;
8419
+ }
8420
+ };
8097
8421
 
8098
8422
  /**
8099
8423
  * The `Publisher` is responsible for publishing/unpublishing media streams to/from the SFU
@@ -8128,13 +8452,13 @@ class Publisher extends BasePeerConnection {
8128
8452
  // create a clone of the track as otherwise the same trackId will
8129
8453
  // appear in the SDP in multiple transceivers
8130
8454
  const trackToPublish = this.cloneTrack(track);
8131
- const { transceiver } = this.transceiverCache.get(publishOption) || {};
8132
- if (!transceiver) {
8455
+ const bundle = this.transceiverCache.get(publishOption);
8456
+ if (!bundle) {
8133
8457
  await this.addTransceiver(trackToPublish, publishOption, options);
8134
8458
  }
8135
8459
  else {
8136
- const previousTrack = transceiver.sender.track;
8137
- await this.updateTransceiver(transceiver, trackToPublish, trackType, options);
8460
+ const previousTrack = bundle.transceiver.sender.track;
8461
+ await this.updateTransceiver(bundle, trackToPublish, options);
8138
8462
  if (!isReactNative()) {
8139
8463
  this.stopTrack(previousTrack);
8140
8464
  }
@@ -8169,13 +8493,20 @@ class Publisher extends BasePeerConnection {
8169
8493
  /**
8170
8494
  * Updates the transceiver with the given track and track type.
8171
8495
  */
8172
- this.updateTransceiver = async (transceiver, track, trackType, options = {}) => {
8496
+ this.updateTransceiver = async (bundle, track, options = {}) => {
8497
+ const { transceiver, publishOption } = bundle;
8498
+ const trackType = publishOption.trackType;
8173
8499
  const sender = transceiver.sender;
8174
8500
  if (sender.track)
8175
8501
  this.trackIdToTrackType.delete(sender.track.id);
8176
8502
  await sender.replaceTrack(track);
8177
- if (track)
8503
+ if (track) {
8178
8504
  this.trackIdToTrackType.set(track.id, trackType);
8505
+ if (isFirefox() && bundle.videoSender) {
8506
+ // restore the encoding config from the cache, if any
8507
+ await this.changePublishQuality(bundle.videoSender, bundle);
8508
+ }
8509
+ }
8179
8510
  if (isAudioTrackType(trackType)) {
8180
8511
  await this.updateAudioPublishOptions(trackType, options);
8181
8512
  }
@@ -8235,7 +8566,7 @@ class Publisher extends BasePeerConnection {
8235
8566
  continue;
8236
8567
  // it is safe to stop the track here, it is a clone
8237
8568
  this.stopTrack(transceiver.sender.track);
8238
- await this.updateTransceiver(transceiver, null, publishOption.trackType);
8569
+ await this.updateTransceiver(item, null);
8239
8570
  }
8240
8571
  };
8241
8572
  /**
@@ -8292,33 +8623,38 @@ class Publisher extends BasePeerConnection {
8292
8623
  /**
8293
8624
  * Stops the cloned track that is being published to the SFU.
8294
8625
  */
8295
- this.stopTracks = (...trackTypes) => {
8296
- for (const item of this.transceiverCache.items()) {
8297
- const { publishOption, transceiver } = item;
8298
- if (!trackTypes.includes(publishOption.trackType))
8299
- continue;
8300
- this.stopTrack(transceiver.sender.track);
8301
- }
8626
+ this.stopTracks = async (...trackTypes) => {
8627
+ return withoutConcurrency(this.eventLockKey('changePublishQuality'), async () => {
8628
+ for (const item of this.transceiverCache.items()) {
8629
+ const { publishOption, transceiver } = item;
8630
+ if (!trackTypes.includes(publishOption.trackType))
8631
+ continue;
8632
+ const track = transceiver.sender.track;
8633
+ await this.silenceSenderOnFirefox(item);
8634
+ this.stopTrack(track);
8635
+ }
8636
+ });
8302
8637
  };
8303
8638
  /**
8304
8639
  * Stops all the cloned tracks that are being published to the SFU.
8305
8640
  */
8306
- this.stopAllTracks = () => {
8307
- for (const { transceiver } of this.transceiverCache.items()) {
8308
- this.stopTrack(transceiver.sender.track);
8309
- }
8310
- for (const track of this.clonedTracks) {
8311
- this.stopTrack(track);
8312
- }
8641
+ this.stopAllTracks = async () => {
8642
+ return withoutConcurrency(this.eventLockKey('changePublishQuality'), async () => {
8643
+ for (const item of this.transceiverCache.items()) {
8644
+ const track = item.transceiver.sender.track;
8645
+ await this.silenceSenderOnFirefox(item);
8646
+ this.stopTrack(track);
8647
+ }
8648
+ for (const track of this.clonedTracks) {
8649
+ this.stopTrack(track);
8650
+ }
8651
+ });
8313
8652
  };
8314
- this.changePublishQuality = async (videoSender) => {
8315
- const { trackType, layers, publishOptionId } = videoSender;
8316
- const enabledLayers = layers.filter((l) => l.active);
8653
+ this.changePublishQuality = async (videoSender, bundle) => {
8654
+ const enabledLayers = videoSender.layers.filter((l) => l.active);
8317
8655
  const tag = 'Update publish quality:';
8318
8656
  this.logger.info(`${tag} requested layers by SFU:`, enabledLayers);
8319
- const transceiverId = this.transceiverCache.find((t) => t.publishOption.id === publishOptionId &&
8320
- t.publishOption.trackType === trackType);
8321
- const sender = transceiverId?.transceiver.sender;
8657
+ const sender = bundle?.transceiver.sender;
8322
8658
  if (!sender) {
8323
8659
  return this.logger.warn(`${tag} no video sender found.`);
8324
8660
  }
@@ -8326,7 +8662,7 @@ class Publisher extends BasePeerConnection {
8326
8662
  if (params.encodings.length === 0) {
8327
8663
  return this.logger.warn(`${tag} there are no encodings set.`);
8328
8664
  }
8329
- const codecInUse = transceiverId?.publishOption.codec?.name;
8665
+ const codecInUse = bundle?.publishOption.codec?.name;
8330
8666
  const usesSvcCodec = codecInUse && isSvcCodec(codecInUse);
8331
8667
  let changed = false;
8332
8668
  for (const encoder of params.encodings) {
@@ -8526,6 +8862,72 @@ class Publisher extends BasePeerConnection {
8526
8862
  track.stop();
8527
8863
  this.clonedTracks.delete(track);
8528
8864
  };
8865
+ /**
8866
+ * Silences a Firefox sender on the wire during unpublish.
8867
+ *
8868
+ * Firefox keeps emitting RTP after track.stop(), but the right lever
8869
+ * differs by track type:
8870
+ * - audio: `replaceTrack(null)` is the only reliable silencer;
8871
+ * `setParameters({encodings:[...active:false]})` does NOT stop
8872
+ * the Opus encoder.
8873
+ * - video: `setParameters({encodings:[...active:false]})` pauses
8874
+ * the encoder; `replaceTrack(null)` does NOT reliably stop the
8875
+ * video encoder. The prior active=true configuration is captured
8876
+ * onto `bundle.videoSender` so `updateTransceiver` can restore
8877
+ * it on the next publish.
8878
+ *
8879
+ * No-op on non-Firefox browsers and during teardown.
8880
+ */
8881
+ this.silenceSenderOnFirefox = async (bundle) => {
8882
+ if (this.isDisposed || !isFirefox())
8883
+ return;
8884
+ const { transceiver, publishOption } = bundle;
8885
+ if (isAudioTrackType(publishOption.trackType)) {
8886
+ await transceiver.sender.replaceTrack(null).catch((err) => {
8887
+ this.logger.warn('Failed to clear audio sender track', err);
8888
+ });
8889
+ return;
8890
+ }
8891
+ await this.disableAllEncodings(bundle);
8892
+ };
8893
+ this.disableAllEncodings = async (bundle) => {
8894
+ const { transceiver, publishOption } = bundle;
8895
+ const sender = transceiver.sender;
8896
+ const params = sender.getParameters();
8897
+ if (!params.encodings || params.encodings.length === 0)
8898
+ return;
8899
+ if (!bundle.videoSender) {
8900
+ this.transceiverCache.update(publishOption, {
8901
+ videoSender: {
8902
+ trackType: publishOption.trackType,
8903
+ publishOptionId: publishOption.id,
8904
+ codec: publishOption.codec,
8905
+ degradationPreference: fromRTCDegradationPreference(params.degradationPreference),
8906
+ layers: params.encodings.map((e) => ({
8907
+ name: e.rid ?? 'q',
8908
+ active: e.active ?? true,
8909
+ maxBitrate: e.maxBitrate ?? 0,
8910
+ scaleResolutionDownBy: e.scaleResolutionDownBy ?? 0,
8911
+ maxFramerate: e.maxFramerate ?? 0,
8912
+ // @ts-expect-error scalabilityMode is not in the typedefs yet
8913
+ scalabilityMode: e.scalabilityMode ?? '',
8914
+ })),
8915
+ },
8916
+ });
8917
+ }
8918
+ let changed = false;
8919
+ for (const encoding of params.encodings) {
8920
+ if (encoding.active !== false) {
8921
+ encoding.active = false;
8922
+ changed = true;
8923
+ }
8924
+ }
8925
+ if (!changed)
8926
+ return;
8927
+ await sender.setParameters(params).catch((err) => {
8928
+ this.logger.error('Failed to disable video sender encodings:', err);
8929
+ });
8930
+ };
8529
8931
  this.publishOptions = publishOptions;
8530
8932
  this.on('iceRestart', (iceRestart) => {
8531
8933
  if (iceRestart.peerType !== PeerType.PUBLISHER_UNSPECIFIED)
@@ -8534,7 +8936,16 @@ class Publisher extends BasePeerConnection {
8534
8936
  });
8535
8937
  this.on('changePublishQuality', async (event) => {
8536
8938
  for (const videoSender of event.videoSenders) {
8537
- await this.changePublishQuality(videoSender);
8939
+ // if not publishing, update the encodingConfigCache and don't modify the state.
8940
+ // we'll apply this config on the next publish/unmute.
8941
+ const { trackType, publishOptionId } = videoSender;
8942
+ const bundle = this.transceiverCache.getBy(publishOptionId, trackType);
8943
+ if (bundle) {
8944
+ this.transceiverCache.update(bundle.publishOption, { videoSender });
8945
+ }
8946
+ if (isFirefox() && !this.isPublishing(trackType))
8947
+ continue;
8948
+ await this.changePublishQuality(videoSender, bundle);
8538
8949
  }
8539
8950
  });
8540
8951
  this.on('changePublishOptions', (event) => {
@@ -8545,9 +8956,14 @@ class Publisher extends BasePeerConnection {
8545
8956
  /**
8546
8957
  * Disposes this Publisher instance.
8547
8958
  */
8548
- dispose() {
8549
- super.dispose();
8550
- this.stopAllTracks();
8959
+ async dispose() {
8960
+ await super.dispose();
8961
+ try {
8962
+ await this.stopAllTracks();
8963
+ }
8964
+ catch (err) {
8965
+ this.logger.warn('Failed to stop tracks during dispose', err);
8966
+ }
8551
8967
  this.clonedTracks.clear();
8552
8968
  }
8553
8969
  }
@@ -8718,6 +9134,7 @@ class Subscriber extends BasePeerConnection {
8718
9134
  await this.sfuClient.sendAnswer({
8719
9135
  peerType: PeerType.SUBSCRIBER,
8720
9136
  sdp: answer.sdp || '',
9137
+ negotiationId: subscriberOffer.negotiationId,
8721
9138
  });
8722
9139
  this.isIceRestarting = false;
8723
9140
  };
@@ -11331,8 +11748,8 @@ const normalize = (options) => {
11331
11748
  : false,
11332
11749
  };
11333
11750
  };
11334
- const createSyntheticDevice = (deviceId, kind) => {
11335
- return { deviceId, kind, label: '', groupId: '' };
11751
+ const createSyntheticDevice = (deviceId, kind, label = '') => {
11752
+ return { deviceId, kind, label, groupId: '' };
11336
11753
  };
11337
11754
  const readPreferences = (storageKey) => {
11338
11755
  try {
@@ -11386,6 +11803,8 @@ class DeviceManager {
11386
11803
  this.areSubscriptionsSetUp = false;
11387
11804
  this.isTrackStoppedDueToTrackEnd = false;
11388
11805
  this.filters = [];
11806
+ this.virtualDevicesSubject = new rxjs.BehaviorSubject([]);
11807
+ this.virtualDeviceConcurrencyTag = Symbol('virtualDeviceConcurrencyTag');
11389
11808
  this.statusChangeConcurrencyTag = Symbol('statusChangeConcurrencyTag');
11390
11809
  this.filterRegistrationConcurrencyTag = Symbol('filterRegistrationConcurrencyTag');
11391
11810
  /**
@@ -11398,6 +11817,7 @@ class DeviceManager {
11398
11817
  this.subscriptions.forEach((s) => s());
11399
11818
  this.subscriptions = [];
11400
11819
  this.areSubscriptionsSetUp = false;
11820
+ this.virtualDevicesSubject.next([]);
11401
11821
  };
11402
11822
  this.runCurrentStreamCleanups = () => {
11403
11823
  this.currentStreamCleanups.forEach((c) => c());
@@ -11456,7 +11876,93 @@ class DeviceManager {
11456
11876
  * @returns an Observable that will be updated if a device is connected or disconnected
11457
11877
  */
11458
11878
  listDevices() {
11459
- return this.getDevices();
11879
+ return rxjs.combineLatest([this.getDevices(), this.virtualDevicesSubject]).pipe(rxjs.map(([real, virtual]) => [
11880
+ ...real,
11881
+ ...virtual.map((d) => createSyntheticDevice(d.deviceId, d.kind, d.label)),
11882
+ ]));
11883
+ }
11884
+ /**
11885
+ * Registers a virtual camera or microphone backed by a caller-supplied
11886
+ * stream factory. The device appears in `listDevices()` and can be selected
11887
+ * via `select()` like any real device.
11888
+ *
11889
+ * Web only. React Native is not supported.
11890
+ *
11891
+ * Only supported for camera and microphone managers; calling on any other
11892
+ * manager throws.
11893
+ */
11894
+ registerVirtualDevice(virtualDevice) {
11895
+ if (isReactNative()) {
11896
+ throw new Error('Virtual devices are not supported on React Native.');
11897
+ }
11898
+ if (this.trackType !== TrackType.AUDIO &&
11899
+ this.trackType !== TrackType.VIDEO) {
11900
+ throw new Error('Virtual devices are only supported for camera and microphone.');
11901
+ }
11902
+ const deviceId = `stream-virtual:${generateUUIDv4()}`;
11903
+ const entry = {
11904
+ deviceId,
11905
+ kind: this.mediaDeviceKind,
11906
+ ...virtualDevice,
11907
+ };
11908
+ setCurrentValue(this.virtualDevicesSubject, (current) => [
11909
+ ...current,
11910
+ entry,
11911
+ ]);
11912
+ return {
11913
+ deviceId: entry.deviceId,
11914
+ unregister: async () => {
11915
+ await withoutConcurrency(this.virtualDeviceConcurrencyTag, async () => {
11916
+ setCurrentValue(this.virtualDevicesSubject, (current) => current.filter((d) => d !== entry));
11917
+ if (this.activeVirtualSession?.deviceId === deviceId) {
11918
+ await this.stopActiveVirtualSession();
11919
+ }
11920
+ });
11921
+ if (this.state.selectedDevice === deviceId) {
11922
+ await this.statusChangeSettled();
11923
+ await this.disable({ forceStop: true });
11924
+ await this.select(undefined);
11925
+ }
11926
+ },
11927
+ };
11928
+ }
11929
+ sanitizeVirtualStream(stream) {
11930
+ stream.getTracks().forEach((track) => {
11931
+ const originalGetSettings = track.getSettings.bind(track);
11932
+ track.getSettings = () => {
11933
+ const settings = originalGetSettings();
11934
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
11935
+ const { deviceId, ...rest } = settings;
11936
+ return rest;
11937
+ };
11938
+ });
11939
+ return stream;
11940
+ }
11941
+ findVirtualDevice(deviceId) {
11942
+ if (!deviceId)
11943
+ return undefined;
11944
+ return getCurrentValue(this.virtualDevicesSubject).find((d) => d.deviceId === deviceId);
11945
+ }
11946
+ async stopActiveVirtualSession() {
11947
+ const session = this.activeVirtualSession;
11948
+ this.activeVirtualSession = undefined;
11949
+ await session?.stop?.();
11950
+ }
11951
+ async getSelectedStream(constraints) {
11952
+ const deviceId = this.state.selectedDevice;
11953
+ if (!deviceId?.startsWith('stream-virtual')) {
11954
+ return this.getStream(constraints);
11955
+ }
11956
+ return withoutConcurrency(this.virtualDeviceConcurrencyTag, async () => {
11957
+ const virtualDevice = this.findVirtualDevice(deviceId);
11958
+ if (!virtualDevice) {
11959
+ throw new Error(`Virtual device is not registered: ${deviceId}`);
11960
+ }
11961
+ await this.stopActiveVirtualSession();
11962
+ const { stream, stop } = await virtualDevice.getUserMedia(constraints);
11963
+ this.activeVirtualSession = { deviceId, stop };
11964
+ return this.sanitizeVirtualStream(stream);
11965
+ });
11460
11966
  }
11461
11967
  /**
11462
11968
  * Returns `true` when this device is in enabled state.
@@ -11616,6 +12122,9 @@ class DeviceManager {
11616
12122
  }
11617
12123
  });
11618
12124
  }
12125
+ getResolvedConstraints(constraints) {
12126
+ return constraints;
12127
+ }
11619
12128
  publishStream(stream, options) {
11620
12129
  return this.call.publish(stream, this.trackType, options);
11621
12130
  }
@@ -11636,6 +12145,7 @@ class DeviceManager {
11636
12145
  this.muteLocalStream(stopTracks);
11637
12146
  const allEnded = this.getTracks().every((t) => t.readyState === 'ended');
11638
12147
  if (allEnded) {
12148
+ await this.stopActiveVirtualSession();
11639
12149
  // @ts-expect-error release() is present in react-native-webrtc
11640
12150
  if (typeof mediaStream.release === 'function') {
11641
12151
  // @ts-expect-error called to dispose the stream in RN
@@ -11691,12 +12201,12 @@ class DeviceManager {
11691
12201
  // before chainWith below registers new ones for the new chain.
11692
12202
  this.runCurrentStreamCleanups();
11693
12203
  const defaultConstraints = this.state.defaultConstraints;
11694
- const constraints = {
12204
+ const constraints = this.getResolvedConstraints({
11695
12205
  ...defaultConstraints,
11696
12206
  deviceId: this.state.selectedDevice
11697
12207
  ? { exact: this.state.selectedDevice }
11698
12208
  : undefined,
11699
- };
12209
+ });
11700
12210
  /**
11701
12211
  * Chains two media streams together.
11702
12212
  *
@@ -11753,7 +12263,7 @@ class DeviceManager {
11753
12263
  };
11754
12264
  // the rootStream represents the stream coming from the actual device
11755
12265
  // e.g. camera or microphone stream
11756
- rootStreamPromise = this.getStream(constraints);
12266
+ rootStreamPromise = this.getSelectedStream(constraints);
11757
12267
  // we publish the last MediaStream of the chain
11758
12268
  stream = await this.filters.reduce((parent, entry) => parent
11759
12269
  .then((inputStream) => {
@@ -12070,7 +12580,10 @@ class DeviceManagerState {
12070
12580
  setCurrentValue(this.mediaStreamSubject, stream);
12071
12581
  setCurrentValue(this.rootMediaStreamSubject, rootStream);
12072
12582
  if (rootStream) {
12073
- this.setDevice(this.getDeviceIdFromStream(rootStream));
12583
+ const derived = this.getDeviceIdFromStream(rootStream);
12584
+ if (derived) {
12585
+ this.setDevice(derived);
12586
+ }
12074
12587
  }
12075
12588
  }
12076
12589
  /**
@@ -12283,7 +12796,7 @@ class CameraManager extends DeviceManager {
12283
12796
  getDevices() {
12284
12797
  return getVideoDevices(this.call.tracer);
12285
12798
  }
12286
- getStream(constraints) {
12799
+ getResolvedConstraints(constraints) {
12287
12800
  constraints.width = this.targetResolution.width;
12288
12801
  constraints.height = this.targetResolution.height;
12289
12802
  // We can't set both device id and facing mode
@@ -12294,6 +12807,9 @@ class CameraManager extends DeviceManager {
12294
12807
  constraints.facingMode =
12295
12808
  this.state.direction === 'front' ? 'user' : 'environment';
12296
12809
  }
12810
+ return constraints;
12811
+ }
12812
+ getStream(constraints) {
12297
12813
  return getVideoStream(constraints, this.call.tracer);
12298
12814
  }
12299
12815
  }
@@ -13621,9 +14137,10 @@ class Call {
13621
14137
  this.sfuStatsReporter?.flush();
13622
14138
  this.sfuStatsReporter?.stop();
13623
14139
  this.sfuStatsReporter = undefined;
13624
- this.subscriber?.dispose();
14140
+ this.lastStatsOptions = undefined;
14141
+ await this.subscriber?.dispose();
13625
14142
  this.subscriber = undefined;
13626
- this.publisher?.dispose();
14143
+ await this.publisher?.dispose();
13627
14144
  this.publisher = undefined;
13628
14145
  await this.sfuClient?.leaveAndClose(leaveReason);
13629
14146
  this.sfuClient = undefined;
@@ -13899,15 +14416,17 @@ class Call {
13899
14416
  const performingMigration = this.reconnectStrategy === WebsocketReconnectStrategy.MIGRATE;
13900
14417
  const performingRejoin = this.reconnectStrategy === WebsocketReconnectStrategy.REJOIN;
13901
14418
  const performingFastReconnect = this.reconnectStrategy === WebsocketReconnectStrategy.FAST;
13902
- let statsOptions = this.sfuStatsReporter?.options;
14419
+ let statsOptions = this.lastStatsOptions;
13903
14420
  if (!this.credentials ||
13904
14421
  !statsOptions ||
13905
14422
  performingRejoin ||
13906
- performingMigration) {
14423
+ performingMigration ||
14424
+ data?.migrating_from) {
13907
14425
  try {
13908
14426
  const joinResponse = await this.doJoinRequest(data);
13909
14427
  this.credentials = joinResponse.credentials;
13910
14428
  statsOptions = joinResponse.stats_options;
14429
+ this.lastStatsOptions = statsOptions;
13911
14430
  }
13912
14431
  catch (error) {
13913
14432
  // prevent triggering reconnect flow if the state is OFFLINE
@@ -14014,7 +14533,7 @@ class Call {
14014
14533
  }
14015
14534
  else {
14016
14535
  const connectionConfig = toRtcConfiguration(this.credentials.ice_servers);
14017
- this.initPublisherAndSubscriber({
14536
+ await this.initPublisherAndSubscriber({
14018
14537
  sfuClient,
14019
14538
  connectionConfig,
14020
14539
  clientDetails,
@@ -14159,11 +14678,11 @@ class Call {
14159
14678
  * Initializes the Publisher and Subscriber Peer Connections.
14160
14679
  * @internal
14161
14680
  */
14162
- this.initPublisherAndSubscriber = (opts) => {
14681
+ this.initPublisherAndSubscriber = async (opts) => {
14163
14682
  const { sfuClient, connectionConfig, clientDetails, statsOptions, publishOptions, closePreviousInstances, unifiedSessionId, } = opts;
14164
- const { enable_rtc_stats: enableTracing } = statsOptions;
14683
+ const { enable_rtc_stats: enableTracing, reporting_interval_ms: reportingIntervalMs, } = statsOptions;
14165
14684
  if (closePreviousInstances && this.subscriber) {
14166
- this.subscriber.dispose();
14685
+ await this.subscriber.dispose();
14167
14686
  }
14168
14687
  const basePeerConnectionOptions = {
14169
14688
  sfuClient,
@@ -14172,6 +14691,7 @@ class Call {
14172
14691
  connectionConfig,
14173
14692
  tag: sfuClient.tag,
14174
14693
  enableTracing,
14694
+ statsTimestampDriftThresholdMs: reportingIntervalMs / 2,
14175
14695
  clientPublishOptions: this.clientPublishOptions,
14176
14696
  onReconnectionNeeded: (kind, reason, peerType) => {
14177
14697
  this.reconnect(kind, reason).catch((err) => {
@@ -14192,7 +14712,7 @@ class Call {
14192
14712
  const isAnonymous = this.streamClient.user?.type === 'anonymous';
14193
14713
  if (!isAnonymous) {
14194
14714
  if (closePreviousInstances && this.publisher) {
14195
- this.publisher.dispose();
14715
+ await this.publisher.dispose();
14196
14716
  }
14197
14717
  this.publisher = new Publisher(basePeerConnectionOptions, publishOptions);
14198
14718
  }
@@ -14295,10 +14815,17 @@ class Call {
14295
14815
  * `ICE_NEVER_CONNECTED` increments the unsupported-network counter).
14296
14816
  */
14297
14817
  this.reconnect = async (strategy, reason) => {
14298
- if (this.state.callingState === exports.CallingState.RECONNECTING ||
14818
+ if (this.state.callingState === exports.CallingState.JOINING ||
14819
+ this.state.callingState === exports.CallingState.RECONNECTING ||
14299
14820
  this.state.callingState === exports.CallingState.MIGRATING ||
14300
14821
  this.state.callingState === exports.CallingState.RECONNECTING_FAILED)
14301
14822
  return;
14823
+ // Drop redundant reconnect calls. If a reconnect is already queued or
14824
+ // running for this Call, that entry will resolve whatever broke;
14825
+ // queueing more entries just replays the full REJOIN cycle (one extra
14826
+ // `POST /join` per entry) once the call is already healthy again.
14827
+ if (hasPending(this.reconnectConcurrencyTag))
14828
+ return;
14302
14829
  return withoutConcurrency(this.reconnectConcurrencyTag, async () => {
14303
14830
  const reconnectStartTime = Date.now();
14304
14831
  this.reconnectStrategy = strategy;
@@ -14503,8 +15030,8 @@ class Call {
14503
15030
  this.state.setCallingState(exports.CallingState.JOINED);
14504
15031
  }
14505
15032
  finally {
14506
- currentSubscriber?.dispose();
14507
- currentPublisher?.dispose();
15033
+ await currentSubscriber?.dispose();
15034
+ await currentPublisher?.dispose();
14508
15035
  // and close the previous SFU client, without specifying close code
14509
15036
  currentSfuClient.close(StreamSfuClient.NORMAL_CLOSURE, 'Migrating away');
14510
15037
  }
@@ -14693,7 +15220,7 @@ class Call {
14693
15220
  this.stopPublish = async (...trackTypes) => {
14694
15221
  if (!this.sfuClient || !this.publisher)
14695
15222
  return;
14696
- this.publisher.stopTracks(...trackTypes);
15223
+ await this.publisher.stopTracks(...trackTypes);
14697
15224
  await this.updateLocalStreamState(undefined, ...trackTypes);
14698
15225
  };
14699
15226
  /**
@@ -16806,7 +17333,7 @@ class StreamClient {
16806
17333
  this.getUserAgent = () => {
16807
17334
  if (!this.cachedUserAgent) {
16808
17335
  const { clientAppIdentifier = {} } = this.options;
16809
- const { sdkName = 'js', sdkVersion = "1.50.0", ...extras } = clientAppIdentifier;
17336
+ const { sdkName = 'js', sdkVersion = "1.52.0", ...extras } = clientAppIdentifier;
16810
17337
  this.cachedUserAgent = [
16811
17338
  `stream-video-${sdkName}-v${sdkVersion}`,
16812
17339
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
@@ -17532,6 +18059,7 @@ exports.getVideoDevices = getVideoDevices;
17532
18059
  exports.getVideoStream = getVideoStream;
17533
18060
  exports.getWebRTCInfo = getWebRTCInfo;
17534
18061
  exports.hasAudio = hasAudio$1;
18062
+ exports.hasInterruptedTrack = hasInterruptedTrack;
17535
18063
  exports.hasPausedTrack = hasPausedTrack;
17536
18064
  exports.hasScreenShare = hasScreenShare;
17537
18065
  exports.hasScreenShareAudio = hasScreenShareAudio;