@stream-io/video-client 1.21.0 → 1.22.1

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 (45) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/index.browser.es.js +171 -143
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +171 -143
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.d.ts +0 -1
  7. package/dist/index.es.js +171 -143
  8. package/dist/index.es.js.map +1 -1
  9. package/dist/src/Call.d.ts +2 -0
  10. package/dist/src/devices/InputMediaDeviceManager.d.ts +3 -3
  11. package/dist/src/devices/SpeakerState.d.ts +3 -1
  12. package/dist/src/devices/devices.d.ts +8 -8
  13. package/dist/src/events/call.d.ts +1 -1
  14. package/dist/src/gen/video/sfu/models/models.d.ts +8 -0
  15. package/dist/src/rtc/BasePeerConnection.d.ts +1 -0
  16. package/dist/src/stats/SfuStatsReporter.d.ts +5 -1
  17. package/dist/src/stats/utils.d.ts +14 -0
  18. package/index.ts +0 -4
  19. package/package.json +10 -10
  20. package/src/Call.ts +15 -8
  21. package/src/devices/CameraManager.ts +27 -23
  22. package/src/devices/InputMediaDeviceManager.ts +8 -5
  23. package/src/devices/MicrophoneManager.ts +1 -1
  24. package/src/devices/ScreenShareManager.ts +2 -2
  25. package/src/devices/SpeakerManager.ts +2 -1
  26. package/src/devices/SpeakerState.ts +6 -3
  27. package/src/devices/__tests__/CameraManager.test.ts +43 -27
  28. package/src/devices/__tests__/MicrophoneManager.test.ts +5 -3
  29. package/src/devices/__tests__/ScreenShareManager.test.ts +5 -1
  30. package/src/devices/__tests__/mocks.ts +2 -3
  31. package/src/devices/devices.ts +38 -16
  32. package/src/events/__tests__/call.test.ts +23 -0
  33. package/src/events/call.ts +12 -1
  34. package/src/gen/video/sfu/models/models.ts +14 -0
  35. package/src/rtc/BasePeerConnection.ts +11 -5
  36. package/src/rtc/Publisher.ts +5 -1
  37. package/src/rtc/__tests__/Publisher.test.ts +2 -2
  38. package/src/rtc/__tests__/Subscriber.test.ts +2 -2
  39. package/src/rtc/__tests__/mocks/webrtc.mocks.ts +1 -1
  40. package/src/rtc/__tests__/videoLayers.test.ts +44 -0
  41. package/src/rtc/videoLayers.ts +6 -3
  42. package/src/stats/SfuStatsReporter.ts +15 -5
  43. package/src/stats/utils.ts +15 -0
  44. package/dist/src/stats/rtc/mediaDevices.d.ts +0 -2
  45. package/src/stats/rtc/mediaDevices.ts +0 -43
package/dist/index.es.js CHANGED
@@ -9,75 +9,6 @@ import { UAParser } from 'ua-parser-js';
9
9
  import { parse } from 'sdp-transform';
10
10
  import https from 'https';
11
11
 
12
- class Tracer {
13
- constructor(id) {
14
- this.buffer = [];
15
- this.enabled = true;
16
- this.setEnabled = (enabled) => {
17
- if (this.enabled === enabled)
18
- return;
19
- this.enabled = enabled;
20
- this.buffer = [];
21
- };
22
- this.trace = (tag, data) => {
23
- if (!this.enabled)
24
- return;
25
- this.buffer.push([tag, this.id, data, Date.now()]);
26
- };
27
- this.take = () => {
28
- const snapshot = this.buffer;
29
- this.buffer = [];
30
- return {
31
- snapshot,
32
- rollback: () => {
33
- this.buffer.unshift(...snapshot);
34
- },
35
- };
36
- };
37
- this.dispose = () => {
38
- this.buffer = [];
39
- };
40
- this.id = id;
41
- }
42
- }
43
-
44
- const tracer = new Tracer(null);
45
- if (typeof navigator !== 'undefined' &&
46
- typeof navigator.mediaDevices !== 'undefined') {
47
- const dumpStream = (stream) => ({
48
- id: stream.id,
49
- tracks: stream.getTracks().map((track) => ({
50
- id: track.id,
51
- kind: track.kind,
52
- label: track.label,
53
- enabled: track.enabled,
54
- muted: track.muted,
55
- readyState: track.readyState,
56
- })),
57
- });
58
- const trace = tracer.trace;
59
- const target = navigator.mediaDevices;
60
- for (const method of ['getUserMedia', 'getDisplayMedia']) {
61
- const original = target[method];
62
- if (!original)
63
- continue;
64
- let mark = 0;
65
- target[method] = async function tracedMethod(constraints) {
66
- const tag = `navigator.mediaDevices.${method}.${mark++}`;
67
- trace(tag, constraints);
68
- try {
69
- const stream = await original.call(target, constraints);
70
- trace(`${tag}.OnSuccess`, dumpStream(stream));
71
- return stream;
72
- }
73
- catch (err) {
74
- trace(`${tag}.OnFailure`, err.name);
75
- throw err;
76
- }
77
- };
78
- }
79
- }
80
-
81
12
  /* tslint:disable */
82
13
  /**
83
14
  * @export
@@ -1431,6 +1362,12 @@ class PublishOption$Type extends MessageType {
1431
1362
  T: () => VideoDimension,
1432
1363
  },
1433
1364
  { no: 8, name: 'id', kind: 'scalar', T: 5 /*ScalarType.INT32*/ },
1365
+ {
1366
+ no: 9,
1367
+ name: 'use_single_layer',
1368
+ kind: 'scalar',
1369
+ T: 8 /*ScalarType.BOOL*/,
1370
+ },
1434
1371
  ]);
1435
1372
  }
1436
1373
  }
@@ -5414,6 +5351,20 @@ const flatten = (report) => {
5414
5351
  });
5415
5352
  return stats;
5416
5353
  };
5354
+ /**
5355
+ * Dump the provided MediaStream into a JSON object.
5356
+ */
5357
+ const dumpStream = (stream) => ({
5358
+ id: stream.id,
5359
+ tracks: stream.getTracks().map((track) => ({
5360
+ id: track.id,
5361
+ kind: track.kind,
5362
+ label: track.label,
5363
+ enabled: track.enabled,
5364
+ muted: track.muted,
5365
+ readyState: track.readyState,
5366
+ })),
5367
+ });
5417
5368
  const getSdkSignature = (clientDetails) => {
5418
5369
  const { sdk, ...platform } = clientDetails;
5419
5370
  const sdkName = getSdkName(sdk);
@@ -5702,7 +5653,7 @@ const aggregate = (stats) => {
5702
5653
  return report;
5703
5654
  };
5704
5655
 
5705
- const version = "1.21.0";
5656
+ const version = "1.22.1";
5706
5657
  const [major, minor, patch] = version.split('.');
5707
5658
  let sdkInfo = {
5708
5659
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -5839,7 +5790,7 @@ const getClientDetails = async () => {
5839
5790
  };
5840
5791
 
5841
5792
  class SfuStatsReporter {
5842
- constructor(sfuClient, { options, clientDetails, subscriber, publisher, microphone, camera, state, unifiedSessionId, }) {
5793
+ constructor(sfuClient, { options, clientDetails, subscriber, publisher, microphone, camera, state, tracer, unifiedSessionId, }) {
5843
5794
  this.logger = getLogger(['SfuStatsReporter']);
5844
5795
  this.inputDevices = new Map();
5845
5796
  this.observeDevice = (device, kind) => {
@@ -5905,10 +5856,10 @@ class SfuStatsReporter {
5905
5856
  }
5906
5857
  const subscriberTrace = this.subscriber.tracer?.take();
5907
5858
  const publisherTrace = this.publisher?.tracer?.take();
5908
- const mediaTrace = tracer.take();
5859
+ const tracer = this.tracer.take();
5909
5860
  const sfuTrace = this.sfuClient.getTrace();
5910
5861
  const traces = [
5911
- ...mediaTrace.snapshot,
5862
+ ...tracer.snapshot,
5912
5863
  ...(sfuTrace?.snapshot ?? []),
5913
5864
  ...(publisherTrace?.snapshot ?? []),
5914
5865
  ...(subscriberTrace?.snapshot ?? []),
@@ -5937,7 +5888,7 @@ class SfuStatsReporter {
5937
5888
  catch (err) {
5938
5889
  publisherTrace?.rollback();
5939
5890
  subscriberTrace?.rollback();
5940
- mediaTrace.rollback();
5891
+ tracer.rollback();
5941
5892
  sfuTrace?.rollback();
5942
5893
  throw err;
5943
5894
  }
@@ -5965,6 +5916,11 @@ class SfuStatsReporter {
5965
5916
  clearTimeout(this.timeoutId);
5966
5917
  this.timeoutId = undefined;
5967
5918
  };
5919
+ this.flush = () => {
5920
+ this.run().catch((err) => {
5921
+ this.logger('warn', 'Failed to flush report stats', err);
5922
+ });
5923
+ };
5968
5924
  this.scheduleOne = (timeout) => {
5969
5925
  clearTimeout(this.timeoutId);
5970
5926
  this.timeoutId = setTimeout(() => {
@@ -5980,6 +5936,7 @@ class SfuStatsReporter {
5980
5936
  this.microphone = microphone;
5981
5937
  this.camera = camera;
5982
5938
  this.state = state;
5939
+ this.tracer = tracer;
5983
5940
  this.unifiedSessionId = unifiedSessionId;
5984
5941
  const { sdk, browser } = clientDetails;
5985
5942
  this.sdkName = getSdkName(sdk);
@@ -6265,6 +6222,38 @@ const getCodecFromStats = (stats, codecId) => {
6265
6222
  });
6266
6223
  };
6267
6224
 
6225
+ class Tracer {
6226
+ constructor(id) {
6227
+ this.buffer = [];
6228
+ this.enabled = true;
6229
+ this.setEnabled = (enabled) => {
6230
+ if (this.enabled === enabled)
6231
+ return;
6232
+ this.enabled = enabled;
6233
+ this.buffer = [];
6234
+ };
6235
+ this.trace = (tag, data) => {
6236
+ if (!this.enabled)
6237
+ return;
6238
+ this.buffer.push([tag, this.id, data, Date.now()]);
6239
+ };
6240
+ this.take = () => {
6241
+ const snapshot = this.buffer;
6242
+ this.buffer = [];
6243
+ return {
6244
+ snapshot,
6245
+ rollback: () => {
6246
+ this.buffer.unshift(...snapshot);
6247
+ },
6248
+ };
6249
+ };
6250
+ this.dispose = () => {
6251
+ this.buffer = [];
6252
+ };
6253
+ this.id = id;
6254
+ }
6255
+ }
6256
+
6268
6257
  /**
6269
6258
  * A base class for the `Publisher` and `Subscriber` classes.
6270
6259
  * @internal
@@ -6278,13 +6267,15 @@ class BasePeerConnection {
6278
6267
  this.isDisposed = false;
6279
6268
  this.trackIdToTrackType = new Map();
6280
6269
  this.subscriptions = [];
6270
+ this.lock = Math.random().toString(36).slice(2);
6281
6271
  /**
6282
6272
  * Handles events synchronously.
6283
6273
  * Consecutive events are queued and executed one after the other.
6284
6274
  */
6285
6275
  this.on = (event, fn) => {
6286
6276
  this.subscriptions.push(this.dispatcher.on(event, (e) => {
6287
- withoutConcurrency(`pc.${event}`, async () => fn(e)).catch((err) => {
6277
+ const lockKey = `pc.${this.lock}.${event}`;
6278
+ withoutConcurrency(lockKey, async () => fn(e)).catch((err) => {
6288
6279
  if (this.isDisposed)
6289
6280
  return;
6290
6281
  this.logger('warn', `Error handling ${event}`, err);
@@ -6392,11 +6383,12 @@ class BasePeerConnection {
6392
6383
  // do nothing when ICE is restarting
6393
6384
  if (this.isIceRestarting)
6394
6385
  return;
6395
- if (state === 'failed' || state === 'disconnected') {
6386
+ if (state === 'failed') {
6387
+ this.onUnrecoverableError?.('ICE connection failed');
6388
+ }
6389
+ else if (state === 'disconnected') {
6396
6390
  this.logger('debug', `Attempting to restart ICE`);
6397
6391
  this.restartIce().catch((e) => {
6398
- if (this.isDisposed)
6399
- return;
6400
6392
  const reason = `ICE restart failed`;
6401
6393
  this.logger('error', reason, e);
6402
6394
  this.onUnrecoverableError?.(`${reason}: ${e}`);
@@ -6443,9 +6435,12 @@ class BasePeerConnection {
6443
6435
  this.pc.addEventListener('connectionstatechange', this.onConnectionStateChange);
6444
6436
  this.stats = new StatsTracer(this.pc, peerType, this.trackIdToTrackType);
6445
6437
  if (enableTracing) {
6446
- const tag = `${logTag}-${peerType === PeerType.SUBSCRIBER ? 'sub' : 'pub'}-${sfuClient.edgeName}`;
6438
+ const tag = `${logTag}-${peerType === PeerType.SUBSCRIBER ? 'sub' : 'pub'}`;
6447
6439
  this.tracer = new Tracer(tag);
6448
- this.tracer.trace('create', connectionConfig);
6440
+ this.tracer.trace('create', {
6441
+ url: sfuClient.edgeName,
6442
+ ...connectionConfig,
6443
+ });
6449
6444
  traceRTCPeerConnection(this.pc, this.tracer.trace);
6450
6445
  }
6451
6446
  }
@@ -6661,7 +6656,7 @@ const computeVideoLayers = (videoTrack, publishOption) => {
6661
6656
  const optimalVideoLayers = [];
6662
6657
  const settings = videoTrack.getSettings();
6663
6658
  const { width = 0, height = 0 } = settings;
6664
- const { bitrate, codec, fps, maxSpatialLayers = 3, maxTemporalLayers = 3, videoDimension = { width: 1280, height: 720 }, } = publishOption;
6659
+ const { bitrate, codec, fps, maxSpatialLayers = 3, maxTemporalLayers = 3, videoDimension = { width: 1280, height: 720 }, useSingleLayer, } = publishOption;
6665
6660
  const maxBitrate = getComputedMaxBitrate(videoDimension, width, height, bitrate);
6666
6661
  let downscaleFactor = 1;
6667
6662
  let bitrateFactor = 1;
@@ -6678,7 +6673,7 @@ const computeVideoLayers = (videoTrack, publishOption) => {
6678
6673
  if (svcCodec) {
6679
6674
  // for SVC codecs, we need to set the scalability mode, and the
6680
6675
  // codec will handle the rest (layers, temporal layers, etc.)
6681
- layer.scalabilityMode = toScalabilityMode(maxSpatialLayers, maxTemporalLayers);
6676
+ layer.scalabilityMode = toScalabilityMode(useSingleLayer ? 1 : maxSpatialLayers, maxTemporalLayers);
6682
6677
  }
6683
6678
  else {
6684
6679
  // for non-SVC codecs, we need to downscale proportionally (simulcast)
@@ -6693,7 +6688,7 @@ const computeVideoLayers = (videoTrack, publishOption) => {
6693
6688
  }
6694
6689
  // for simplicity, we start with all layers enabled, then this function
6695
6690
  // will clear/reassign the layers that are not needed
6696
- return withSimulcastConstraints(settings, optimalVideoLayers);
6691
+ return withSimulcastConstraints(settings, optimalVideoLayers, useSingleLayer);
6697
6692
  };
6698
6693
  /**
6699
6694
  * Computes the maximum bitrate for a given resolution.
@@ -6727,7 +6722,7 @@ const getComputedMaxBitrate = (targetResolution, currentWidth, currentHeight, bi
6727
6722
  *
6728
6723
  * https://chromium.googlesource.com/external/webrtc/+/refs/heads/main/media/engine/simulcast.cc#90
6729
6724
  */
6730
- const withSimulcastConstraints = (settings, optimalVideoLayers) => {
6725
+ const withSimulcastConstraints = (settings, optimalVideoLayers, useSingleLayer) => {
6731
6726
  let layers;
6732
6727
  const size = Math.max(settings.width || 0, settings.height || 0);
6733
6728
  if (size <= 320) {
@@ -6743,9 +6738,10 @@ const withSimulcastConstraints = (settings, optimalVideoLayers) => {
6743
6738
  layers = optimalVideoLayers;
6744
6739
  }
6745
6740
  const ridMapping = ['q', 'h', 'f'];
6746
- return layers.map((layer, index) => ({
6741
+ return layers.map((layer, index, arr) => ({
6747
6742
  ...layer,
6748
6743
  rid: ridMapping[index], // reassign rid
6744
+ active: useSingleLayer && index < arr.length - 1 ? false : layer.active,
6749
6745
  }));
6750
6746
  };
6751
6747
 
@@ -6832,6 +6828,9 @@ class Publisher extends BasePeerConnection {
6832
6828
  direction: 'sendonly',
6833
6829
  sendEncodings,
6834
6830
  });
6831
+ const params = transceiver.sender.getParameters();
6832
+ params.degradationPreference = 'maintain-framerate';
6833
+ await transceiver.sender.setParameters(params);
6835
6834
  const trackType = publishOption.trackType;
6836
6835
  this.logger('debug', `Added ${TrackType[trackType]} transceiver`);
6837
6836
  this.transceiverCache.add(publishOption, transceiver);
@@ -7000,7 +6999,7 @@ class Publisher extends BasePeerConnection {
7000
6999
  * @param options the optional offer options to use.
7001
7000
  */
7002
7001
  this.negotiate = async (options) => {
7003
- return withoutConcurrency('publisher.negotiate', async () => {
7002
+ return withoutConcurrency(`publisher.negotiate.${this.lock}`, async () => {
7004
7003
  const offer = await this.pc.createOffer(options);
7005
7004
  const tracks = this.getAnnouncedTracks(offer.sdp);
7006
7005
  if (!tracks.length)
@@ -7908,6 +7907,13 @@ const watchSfuCallEnded = (call) => {
7908
7907
  if (call.state.callingState === CallingState.LEFT)
7909
7908
  return;
7910
7909
  try {
7910
+ if (e.reason === CallEndedReason.LIVE_ENDED) {
7911
+ call.state.setBackstage(true);
7912
+ // don't leave the call if the user has permission to join backstage
7913
+ const { hasPermission } = call.permissionsContext;
7914
+ if (hasPermission(OwnCapability.JOIN_BACKSTAGE))
7915
+ return;
7916
+ }
7911
7917
  // `call.ended` event arrived after the call is already left
7912
7918
  // and all event handlers are detached. We need to manually
7913
7919
  // update the call state to reflect the call has ended.
@@ -9120,15 +9126,25 @@ const getVideoDevices = lazy(() => {
9120
9126
  const getAudioOutputDevices = lazy(() => {
9121
9127
  return merge(getDeviceChangeObserver(), getAudioBrowserPermission().asObservable()).pipe(startWith(undefined), concatMap(() => getDevices(getAudioBrowserPermission(), 'audiooutput')), shareReplay(1));
9122
9128
  });
9123
- const getStream = async (constraints) => {
9124
- const stream = await navigator.mediaDevices.getUserMedia(constraints);
9125
- if (isFirefox()) {
9126
- // When enumerating devices, Firefox will hide device labels unless there's been
9127
- // an active user media stream on the page. So we force device list updates after
9128
- // every successful getUserMedia call.
9129
- navigator.mediaDevices.dispatchEvent(new Event('devicechange'));
9129
+ let getUserMediaExecId = 0;
9130
+ const getStream = async (constraints, tracer) => {
9131
+ const tag = `navigator.mediaDevices.getUserMedia.${getUserMediaExecId++}.`;
9132
+ try {
9133
+ tracer?.trace(tag, constraints);
9134
+ const stream = await navigator.mediaDevices.getUserMedia(constraints);
9135
+ tracer?.trace(`${tag}OnSuccess`, dumpStream(stream));
9136
+ if (isFirefox()) {
9137
+ // When enumerating devices, Firefox will hide device labels unless there's been
9138
+ // an active user media stream on the page. So we force device list updates after
9139
+ // every successful getUserMedia call.
9140
+ navigator.mediaDevices.dispatchEvent(new Event('devicechange'));
9141
+ }
9142
+ return stream;
9143
+ }
9144
+ catch (error) {
9145
+ tracer?.trace(`${tag}OnFailure`, error.name);
9146
+ throw error;
9130
9147
  }
9131
- return stream;
9132
9148
  };
9133
9149
  function isNotFoundOrOverconstrainedError(error) {
9134
9150
  if (!error || typeof error !== 'object') {
@@ -9152,11 +9168,11 @@ function isNotFoundOrOverconstrainedError(error) {
9152
9168
  * Returns an audio media stream that fulfills the given constraints.
9153
9169
  * If no constraints are provided, it uses the browser's default ones.
9154
9170
  *
9155
- * @angular It's recommended to use the [`DeviceManagerService`](./DeviceManagerService.md) for a higher level API, use this low-level method only if the `DeviceManagerService` doesn't suit your requirements.
9156
9171
  * @param trackConstraints the constraints to use when requesting the stream.
9157
- * @returns the new `MediaStream` fulfilling the given constraints.
9172
+ * @param tracer the tracer to use for tracing the stream creation.
9173
+ * @returns a new `MediaStream` fulfilling the given constraints.
9158
9174
  */
9159
- const getAudioStream = async (trackConstraints) => {
9175
+ const getAudioStream = async (trackConstraints, tracer) => {
9160
9176
  const constraints = {
9161
9177
  audio: {
9162
9178
  ...audioDeviceConstraints.audio,
@@ -9168,7 +9184,7 @@ const getAudioStream = async (trackConstraints) => {
9168
9184
  throwOnNotAllowed: true,
9169
9185
  forcePrompt: true,
9170
9186
  });
9171
- return await getStream(constraints);
9187
+ return await getStream(constraints, tracer);
9172
9188
  }
9173
9189
  catch (error) {
9174
9190
  if (isNotFoundOrOverconstrainedError(error) && trackConstraints?.deviceId) {
@@ -9188,11 +9204,11 @@ const getAudioStream = async (trackConstraints) => {
9188
9204
  * Returns a video media stream that fulfills the given constraints.
9189
9205
  * If no constraints are provided, it uses the browser's default ones.
9190
9206
  *
9191
- * @angular It's recommended to use the [`DeviceManagerService`](./DeviceManagerService.md) for a higher level API, use this low-level method only if the `DeviceManagerService` doesn't suit your requirements.
9192
9207
  * @param trackConstraints the constraints to use when requesting the stream.
9208
+ * @param tracer the tracer to use for tracing the stream creation.
9193
9209
  * @returns a new `MediaStream` fulfilling the given constraints.
9194
9210
  */
9195
- const getVideoStream = async (trackConstraints) => {
9211
+ const getVideoStream = async (trackConstraints, tracer) => {
9196
9212
  const constraints = {
9197
9213
  video: {
9198
9214
  ...videoDeviceConstraints.video,
@@ -9204,7 +9220,7 @@ const getVideoStream = async (trackConstraints) => {
9204
9220
  throwOnNotAllowed: true,
9205
9221
  forcePrompt: true,
9206
9222
  });
9207
- return await getStream(constraints);
9223
+ return await getStream(constraints, tracer);
9208
9224
  }
9209
9225
  catch (error) {
9210
9226
  if (isNotFoundOrOverconstrainedError(error) && trackConstraints?.deviceId) {
@@ -9220,19 +9236,21 @@ const getVideoStream = async (trackConstraints) => {
9220
9236
  throw error;
9221
9237
  }
9222
9238
  };
9239
+ let getDisplayMediaExecId = 0;
9223
9240
  /**
9224
9241
  * Prompts the user for a permission to share a screen.
9225
9242
  * If the user grants the permission, a screen sharing stream is returned. Throws otherwise.
9226
9243
  *
9227
9244
  * The callers of this API are responsible to handle the possible errors.
9228
9245
  *
9229
- * @angular It's recommended to use the [`DeviceManagerService`](./DeviceManagerService.md) for a higher level API, use this low-level method only if the `DeviceManagerService` doesn't suit your requirements.
9230
- *
9231
9246
  * @param options any additional options to pass to the [`getDisplayMedia`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia) API.
9247
+ * @param tracer the tracer to use for tracing the stream creation.
9232
9248
  */
9233
- const getScreenShareStream = async (options) => {
9249
+ const getScreenShareStream = async (options, tracer) => {
9250
+ const tag = `navigator.mediaDevices.getDisplayMedia.${getDisplayMediaExecId++}.`;
9234
9251
  try {
9235
- return await navigator.mediaDevices.getDisplayMedia({
9252
+ tracer?.trace(tag, options);
9253
+ const stream = await navigator.mediaDevices.getDisplayMedia({
9236
9254
  video: true,
9237
9255
  audio: {
9238
9256
  channelCount: {
@@ -9246,8 +9264,11 @@ const getScreenShareStream = async (options) => {
9246
9264
  systemAudio: 'include',
9247
9265
  ...options,
9248
9266
  });
9267
+ tracer?.trace(`${tag}OnSuccess`, dumpStream(stream));
9268
+ return stream;
9249
9269
  }
9250
9270
  catch (e) {
9271
+ tracer?.trace(`${tag}OnFailure`, e.name);
9251
9272
  getLogger(['devices'])('error', 'Failed to get screen share stream', e);
9252
9273
  throw e;
9253
9274
  }
@@ -9285,9 +9306,6 @@ const isMobile = () => /Mobi/i.test(navigator.userAgent);
9285
9306
 
9286
9307
  class InputMediaDeviceManager {
9287
9308
  constructor(call, state, trackType) {
9288
- this.call = call;
9289
- this.state = state;
9290
- this.trackType = trackType;
9291
9309
  /**
9292
9310
  * if true, stops the media stream when call is left
9293
9311
  */
@@ -9305,6 +9323,9 @@ class InputMediaDeviceManager {
9305
9323
  this.dispose = () => {
9306
9324
  this.subscriptions.forEach((s) => s());
9307
9325
  };
9326
+ this.call = call;
9327
+ this.state = state;
9328
+ this.trackType = trackType;
9308
9329
  this.logger = getLogger([`${TrackType[trackType].toLowerCase()} manager`]);
9309
9330
  if (deviceIds$ &&
9310
9331
  !isReactNative() &&
@@ -9901,32 +9922,32 @@ class CameraManager extends InputMediaDeviceManager {
9901
9922
  * @param direction the direction of the camera to select.
9902
9923
  */
9903
9924
  async selectDirection(direction) {
9904
- if (this.isDirectionSupportedByDevice()) {
9905
- if (isReactNative()) {
9906
- const videoTrack = this.getTracks()[0];
9907
- if (!videoTrack) {
9908
- this.logger('warn', 'No video track found to do direction selection');
9909
- return;
9910
- }
9911
- await videoTrack.applyConstraints({
9912
- facingMode: direction === 'front' ? 'user' : 'environment',
9913
- });
9914
- this.state.setDirection(direction);
9915
- this.state.setDevice(undefined);
9916
- }
9917
- else {
9918
- // web mobile
9919
- this.state.setDirection(direction);
9920
- // Providing both device id and direction doesn't work, so we deselect the device
9921
- this.state.setDevice(undefined);
9922
- this.getTracks().forEach((track) => {
9923
- track.stop();
9924
- });
9925
+ if (!this.isDirectionSupportedByDevice()) {
9926
+ this.logger('warn', 'Setting direction is not supported on this device');
9927
+ return;
9928
+ }
9929
+ // providing both device id and direction doesn't work, so we deselect the device
9930
+ this.state.setDirection(direction);
9931
+ this.state.setDevice(undefined);
9932
+ if (isReactNative()) {
9933
+ const videoTrack = this.getTracks()[0];
9934
+ await videoTrack?.applyConstraints({
9935
+ facingMode: direction === 'front' ? 'user' : 'environment',
9936
+ });
9937
+ return;
9938
+ }
9939
+ this.getTracks().forEach((track) => track.stop());
9940
+ try {
9941
+ await this.unmuteStream();
9942
+ }
9943
+ catch (error) {
9944
+ if (error instanceof Error && error.name === 'NotReadableError') {
9945
+ // the camera is already in use, and the device can't use it unless it's released.
9946
+ // in that case, we need to stop the stream and start it again.
9947
+ await this.muteStream();
9925
9948
  await this.unmuteStream();
9926
9949
  }
9927
- }
9928
- else {
9929
- this.logger('warn', 'Camera direction ignored for desktop devices');
9950
+ throw error;
9930
9951
  }
9931
9952
  }
9932
9953
  /**
@@ -10011,7 +10032,7 @@ class CameraManager extends InputMediaDeviceManager {
10011
10032
  constraints.facingMode =
10012
10033
  this.state.direction === 'front' ? 'user' : 'environment';
10013
10034
  }
10014
- return getVideoStream(constraints);
10035
+ return getVideoStream(constraints, this.call.tracer);
10015
10036
  }
10016
10037
  }
10017
10038
 
@@ -10422,7 +10443,7 @@ class MicrophoneManager extends InputMediaDeviceManager {
10422
10443
  return getAudioDevices();
10423
10444
  }
10424
10445
  getStream(constraints) {
10425
- return getAudioStream(constraints);
10446
+ return getAudioStream(constraints, this.call.tracer);
10426
10447
  }
10427
10448
  async startSpeakingWhileMutedDetection(deviceId) {
10428
10449
  await withoutConcurrency(this.soundDetectorConcurrencyTag, async () => {
@@ -10565,7 +10586,7 @@ class ScreenShareManager extends InputMediaDeviceManager {
10565
10586
  if (!this.state.audioEnabled) {
10566
10587
  constraints.audio = false;
10567
10588
  }
10568
- return getScreenShareStream(constraints);
10589
+ return getScreenShareStream(constraints, this.call.tracer);
10569
10590
  }
10570
10591
  async stopPublishStream() {
10571
10592
  return this.call.stopPublish(TrackType.SCREEN_SHARE, TrackType.SCREEN_SHARE_AUDIO);
@@ -10574,18 +10595,19 @@ class ScreenShareManager extends InputMediaDeviceManager {
10574
10595
  * Overrides the default `select` method to throw an error.
10575
10596
  */
10576
10597
  async select() {
10577
- throw new Error('This method is not supported in for Screen Share');
10598
+ throw new Error('Not supported');
10578
10599
  }
10579
10600
  }
10580
10601
 
10581
10602
  class SpeakerState {
10582
- constructor() {
10603
+ constructor(tracer) {
10583
10604
  this.selectedDeviceSubject = new BehaviorSubject('');
10584
10605
  this.volumeSubject = new BehaviorSubject(1);
10585
10606
  /**
10586
10607
  * [Tells if the browser supports audio output change on 'audio' elements](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/setSinkId).
10587
10608
  */
10588
10609
  this.isDeviceSelectionSupported = checkIfAudioOutputChangeSupported();
10610
+ this.tracer = tracer;
10589
10611
  this.selectedDevice$ = this.selectedDeviceSubject
10590
10612
  .asObservable()
10591
10613
  .pipe(distinctUntilChanged());
@@ -10615,7 +10637,7 @@ class SpeakerState {
10615
10637
  */
10616
10638
  setDevice(deviceId) {
10617
10639
  setCurrentValue(this.selectedDeviceSubject, deviceId);
10618
- tracer.trace('navigator.mediaDevices.setSinkId', deviceId);
10640
+ this.tracer.trace('navigator.mediaDevices.setSinkId', deviceId);
10619
10641
  }
10620
10642
  /**
10621
10643
  * @internal
@@ -10628,7 +10650,6 @@ class SpeakerState {
10628
10650
 
10629
10651
  class SpeakerManager {
10630
10652
  constructor(call) {
10631
- this.state = new SpeakerState();
10632
10653
  this.subscriptions = [];
10633
10654
  /**
10634
10655
  * Disposes the manager.
@@ -10639,6 +10660,7 @@ class SpeakerManager {
10639
10660
  this.subscriptions.forEach((s) => s.unsubscribe());
10640
10661
  };
10641
10662
  this.call = call;
10663
+ this.state = new SpeakerState(call.tracer);
10642
10664
  if (deviceIds$ && !isReactNative()) {
10643
10665
  this.subscriptions.push(combineLatest([deviceIds$, this.state.selectedDevice$]).subscribe(([devices, deviceId]) => {
10644
10666
  if (!deviceId) {
@@ -10732,6 +10754,7 @@ class Call {
10732
10754
  * The permissions context of this call.
10733
10755
  */
10734
10756
  this.permissionsContext = new PermissionsContext();
10757
+ this.tracer = new Tracer(null);
10735
10758
  /**
10736
10759
  * The event dispatcher instance dedicated to this Call instance.
10737
10760
  * @private
@@ -10987,6 +11010,7 @@ class Call {
10987
11010
  }
10988
11011
  this.statsReporter?.stop();
10989
11012
  this.statsReporter = undefined;
11013
+ this.sfuStatsReporter?.flush();
10990
11014
  this.sfuStatsReporter?.stop();
10991
11015
  this.sfuStatsReporter = undefined;
10992
11016
  this.subscriber?.dispose();
@@ -11300,7 +11324,7 @@ class Call {
11300
11324
  if (!performingRejoin && !performingFastReconnect && !performingMigration) {
11301
11325
  this.sfuStatsReporter?.sendConnectionTime((Date.now() - connectStartTime) / 1000);
11302
11326
  }
11303
- if (performingRejoin) {
11327
+ if (performingRejoin && isWsHealthy) {
11304
11328
  const strategy = WebsocketReconnectStrategy[this.reconnectStrategy];
11305
11329
  await previousSfuClient?.leaveAndClose(`Closing previous WS after reconnect with strategy: ${strategy}`);
11306
11330
  }
@@ -11460,7 +11484,6 @@ class Call {
11460
11484
  },
11461
11485
  });
11462
11486
  }
11463
- tracer.setEnabled(enableTracing);
11464
11487
  this.statsReporter?.stop();
11465
11488
  this.statsReporter = createStatsReporter({
11466
11489
  subscriber: this.subscriber,
@@ -11468,6 +11491,7 @@ class Call {
11468
11491
  state: this.state,
11469
11492
  datacenter: sfuClient.edgeName,
11470
11493
  });
11494
+ this.tracer.setEnabled(enableTracing);
11471
11495
  this.sfuStatsReporter?.stop();
11472
11496
  if (statsOptions?.reporting_interval_ms > 0) {
11473
11497
  this.unifiedSessionId ?? (this.unifiedSessionId = sfuClient.sessionId);
@@ -11479,6 +11503,7 @@ class Call {
11479
11503
  microphone: this.microphone,
11480
11504
  camera: this.camera,
11481
11505
  state: this.state,
11506
+ tracer: this.tracer,
11482
11507
  unifiedSessionId: this.unifiedSessionId,
11483
11508
  });
11484
11509
  this.sfuStatsReporter.start();
@@ -11551,10 +11576,10 @@ class Call {
11551
11576
  */
11552
11577
  this.reconnect = async (strategy, reason) => {
11553
11578
  if (this.state.callingState === CallingState.RECONNECTING ||
11579
+ this.state.callingState === CallingState.MIGRATING ||
11554
11580
  this.state.callingState === CallingState.RECONNECTING_FAILED)
11555
11581
  return;
11556
11582
  return withoutConcurrency(this.reconnectConcurrencyTag, async () => {
11557
- this.logger('info', `[Reconnect] Reconnecting with strategy ${WebsocketReconnectStrategy[strategy]}`);
11558
11583
  const reconnectStartTime = Date.now();
11559
11584
  this.reconnectStrategy = strategy;
11560
11585
  this.reconnectReason = reason;
@@ -11574,6 +11599,7 @@ class Call {
11574
11599
  try {
11575
11600
  // wait until the network is available
11576
11601
  await this.networkAvailableTask?.promise;
11602
+ this.logger('info', `[Reconnect] Reconnecting with strategy ${WebsocketReconnectStrategy[this.reconnectStrategy]}`);
11577
11603
  switch (this.reconnectStrategy) {
11578
11604
  case WebsocketReconnectStrategy.UNSPECIFIED:
11579
11605
  case WebsocketReconnectStrategy.DISCONNECT:
@@ -11613,6 +11639,7 @@ class Call {
11613
11639
  } while (this.state.callingState !== CallingState.JOINED &&
11614
11640
  this.state.callingState !== CallingState.RECONNECTING_FAILED &&
11615
11641
  this.state.callingState !== CallingState.LEFT);
11642
+ this.logger('info', '[Reconnect] Reconnection flow finished');
11616
11643
  });
11617
11644
  };
11618
11645
  /**
@@ -11712,6 +11739,7 @@ class Call {
11712
11739
  }
11713
11740
  });
11714
11741
  const unregisterNetworkChanged = this.streamClient.on('network.changed', (e) => {
11742
+ this.tracer.trace('network.changed', e);
11715
11743
  if (!e.online) {
11716
11744
  this.logger('debug', '[Reconnect] Going offline');
11717
11745
  if (!this.hasJoinedOnce)
@@ -13713,7 +13741,7 @@ class StreamClient {
13713
13741
  this.getUserAgent = () => {
13714
13742
  if (!this.cachedUserAgent) {
13715
13743
  const { clientAppIdentifier = {} } = this.options;
13716
- const { sdkName = 'js', sdkVersion = "1.21.0", ...extras } = clientAppIdentifier;
13744
+ const { sdkName = 'js', sdkVersion = "1.22.1", ...extras } = clientAppIdentifier;
13717
13745
  this.cachedUserAgent = [
13718
13746
  `stream-video-${sdkName}-v${sdkVersion}`,
13719
13747
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),