@stream-io/video-client 1.20.2 → 1.22.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 (70) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/index.browser.es.js +521 -285
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +521 -285
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.d.ts +0 -1
  7. package/dist/index.es.js +521 -285
  8. package/dist/index.es.js.map +1 -1
  9. package/dist/src/Call.d.ts +3 -0
  10. package/dist/src/StreamSfuClient.d.ts +1 -1
  11. package/dist/src/devices/InputMediaDeviceManager.d.ts +3 -3
  12. package/dist/src/devices/InputMediaDeviceManagerState.d.ts +0 -20
  13. package/dist/src/devices/SpeakerState.d.ts +3 -21
  14. package/dist/src/devices/devices.d.ts +8 -8
  15. package/dist/src/events/call.d.ts +1 -1
  16. package/dist/src/gen/video/sfu/models/models.d.ts +50 -0
  17. package/dist/src/gen/video/sfu/signal_rpc/signal.d.ts +29 -3
  18. package/dist/src/rpc/createClient.d.ts +1 -1
  19. package/dist/src/rtc/BasePeerConnection.d.ts +13 -7
  20. package/dist/src/rtc/Publisher.d.ts +4 -4
  21. package/dist/src/stats/CallStateStatsReporter.d.ts +5 -3
  22. package/dist/src/stats/SfuStatsReporter.d.ts +8 -1
  23. package/dist/src/stats/rtc/StatsTracer.d.ts +54 -0
  24. package/dist/src/stats/rtc/Tracer.d.ts +1 -5
  25. package/dist/src/stats/rtc/index.d.ts +2 -0
  26. package/dist/src/stats/rtc/types.d.ts +19 -0
  27. package/dist/src/stats/types.d.ts +13 -0
  28. package/dist/src/stats/utils.d.ts +14 -0
  29. package/index.ts +0 -4
  30. package/package.json +10 -10
  31. package/src/Call.ts +15 -4
  32. package/src/StreamSfuClient.ts +30 -18
  33. package/src/devices/CameraManager.ts +27 -23
  34. package/src/devices/CameraManagerState.ts +3 -2
  35. package/src/devices/InputMediaDeviceManager.ts +8 -5
  36. package/src/devices/InputMediaDeviceManagerState.ts +10 -31
  37. package/src/devices/MicrophoneManager.ts +1 -1
  38. package/src/devices/MicrophoneManagerState.ts +3 -2
  39. package/src/devices/ScreenShareManager.ts +2 -2
  40. package/src/devices/ScreenShareState.ts +5 -4
  41. package/src/devices/SpeakerManager.ts +2 -1
  42. package/src/devices/SpeakerState.ts +11 -27
  43. package/src/devices/__tests__/CameraManager.test.ts +43 -27
  44. package/src/devices/__tests__/MicrophoneManager.test.ts +5 -3
  45. package/src/devices/__tests__/ScreenShareManager.test.ts +5 -1
  46. package/src/devices/__tests__/mocks.ts +2 -3
  47. package/src/devices/devices.ts +38 -16
  48. package/src/events/__tests__/call.test.ts +23 -0
  49. package/src/events/call.ts +12 -1
  50. package/src/gen/video/sfu/models/models.ts +84 -0
  51. package/src/gen/video/sfu/signal_rpc/signal.ts +50 -2
  52. package/src/helpers/DynascaleManager.ts +0 -9
  53. package/src/rpc/createClient.ts +1 -1
  54. package/src/rtc/BasePeerConnection.ts +43 -16
  55. package/src/rtc/Publisher.ts +18 -16
  56. package/src/rtc/Subscriber.ts +2 -0
  57. package/src/rtc/__tests__/Publisher.test.ts +19 -0
  58. package/src/rtc/__tests__/Subscriber.test.ts +15 -1
  59. package/src/stats/CallStateStatsReporter.ts +19 -31
  60. package/src/stats/SfuStatsReporter.ts +45 -16
  61. package/src/stats/rtc/StatsTracer.ts +289 -0
  62. package/src/stats/rtc/Tracer.ts +1 -6
  63. package/src/stats/rtc/__tests__/Tracer.test.ts +57 -0
  64. package/src/stats/rtc/index.ts +3 -0
  65. package/src/stats/rtc/pc.ts +6 -76
  66. package/src/stats/rtc/types.ts +22 -0
  67. package/src/stats/types.ts +15 -0
  68. package/src/stats/utils.ts +15 -0
  69. package/dist/src/stats/rtc/mediaDevices.d.ts +0 -2
  70. package/src/stats/rtc/mediaDevices.ts +0 -42
package/dist/index.cjs.js CHANGED
@@ -10,74 +10,6 @@ var uaParserJs = require('ua-parser-js');
10
10
  var sdpTransform = require('sdp-transform');
11
11
  var https = require('https');
12
12
 
13
- class Tracer {
14
- constructor(id) {
15
- this.buffer = [];
16
- this.enabled = true;
17
- this.setEnabled = (enabled) => {
18
- if (this.enabled === enabled)
19
- return;
20
- this.enabled = enabled;
21
- this.buffer = [];
22
- };
23
- this.trace = (tag, data) => {
24
- if (!this.enabled)
25
- return;
26
- this.buffer.push([tag, this.id, data, Date.now()]);
27
- };
28
- this.take = () => {
29
- const snapshot = this.buffer;
30
- this.buffer = [];
31
- return {
32
- snapshot,
33
- rollback: () => {
34
- this.buffer.unshift(...snapshot);
35
- },
36
- };
37
- };
38
- this.dispose = () => {
39
- this.buffer = [];
40
- };
41
- this.id = id;
42
- }
43
- }
44
-
45
- const tracer = new Tracer(null);
46
- if (typeof navigator !== 'undefined' &&
47
- typeof navigator.mediaDevices !== 'undefined') {
48
- const dumpStream = (stream) => ({
49
- id: stream.id,
50
- tracks: stream.getTracks().map((track) => ({
51
- id: track.id,
52
- kind: track.kind,
53
- label: track.label,
54
- enabled: track.enabled,
55
- muted: track.muted,
56
- readyState: track.readyState,
57
- })),
58
- });
59
- const trace = tracer.trace;
60
- const target = navigator.mediaDevices;
61
- for (const method of ['getUserMedia', 'getDisplayMedia']) {
62
- const original = target[method];
63
- if (!original)
64
- continue;
65
- target[method] = async function tracedMethod(constraints) {
66
- const tag = `navigator.mediaDevices.${method}`;
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
13
  /* tslint:disable */
82
14
  /**
83
15
  * @export
@@ -1796,6 +1728,47 @@ class AppleState$Type extends runtime.MessageType {
1796
1728
  * @generated MessageType for protobuf message stream.video.sfu.models.AppleState
1797
1729
  */
1798
1730
  const AppleState = new AppleState$Type();
1731
+ // @generated message type with reflection information, may provide speed optimized methods
1732
+ class PerformanceStats$Type extends runtime.MessageType {
1733
+ constructor() {
1734
+ super('stream.video.sfu.models.PerformanceStats', [
1735
+ {
1736
+ no: 1,
1737
+ name: 'track_type',
1738
+ kind: 'enum',
1739
+ T: () => [
1740
+ 'stream.video.sfu.models.TrackType',
1741
+ TrackType,
1742
+ 'TRACK_TYPE_',
1743
+ ],
1744
+ },
1745
+ { no: 2, name: 'codec', kind: 'message', T: () => Codec },
1746
+ {
1747
+ no: 3,
1748
+ name: 'avg_frame_time_ms',
1749
+ kind: 'scalar',
1750
+ T: 2 /*ScalarType.FLOAT*/,
1751
+ },
1752
+ { no: 4, name: 'avg_fps', kind: 'scalar', T: 2 /*ScalarType.FLOAT*/ },
1753
+ {
1754
+ no: 5,
1755
+ name: 'video_dimension',
1756
+ kind: 'message',
1757
+ T: () => VideoDimension,
1758
+ },
1759
+ {
1760
+ no: 6,
1761
+ name: 'target_bitrate',
1762
+ kind: 'scalar',
1763
+ T: 5 /*ScalarType.INT32*/,
1764
+ },
1765
+ ]);
1766
+ }
1767
+ }
1768
+ /**
1769
+ * @generated MessageType for protobuf message stream.video.sfu.models.PerformanceStats
1770
+ */
1771
+ const PerformanceStats = new PerformanceStats$Type();
1799
1772
 
1800
1773
  var models = /*#__PURE__*/Object.freeze({
1801
1774
  __proto__: null,
@@ -1821,6 +1794,7 @@ var models = /*#__PURE__*/Object.freeze({
1821
1794
  Participant: Participant,
1822
1795
  ParticipantCount: ParticipantCount,
1823
1796
  get PeerType () { return PeerType; },
1797
+ PerformanceStats: PerformanceStats,
1824
1798
  Pin: Pin,
1825
1799
  PublishOption: PublishOption,
1826
1800
  RTMPIngress: RTMPIngress,
@@ -2000,6 +1974,27 @@ class SendStatsRequest$Type extends runtime.MessageType {
2000
1974
  kind: 'scalar',
2001
1975
  T: 9 /*ScalarType.STRING*/,
2002
1976
  },
1977
+ { no: 15, name: 'rtc_stats', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
1978
+ {
1979
+ no: 16,
1980
+ name: 'encode_stats',
1981
+ kind: 'message',
1982
+ repeat: 1 /*RepeatType.PACKED*/,
1983
+ T: () => PerformanceStats,
1984
+ },
1985
+ {
1986
+ no: 17,
1987
+ name: 'decode_stats',
1988
+ kind: 'message',
1989
+ repeat: 1 /*RepeatType.PACKED*/,
1990
+ T: () => PerformanceStats,
1991
+ },
1992
+ {
1993
+ no: 18,
1994
+ name: 'unified_session_id',
1995
+ kind: 'scalar',
1996
+ T: 9 /*ScalarType.STRING*/,
1997
+ },
2003
1998
  ]);
2004
1999
  }
2005
2000
  }
@@ -5351,6 +5346,20 @@ const flatten = (report) => {
5351
5346
  });
5352
5347
  return stats;
5353
5348
  };
5349
+ /**
5350
+ * Dump the provided MediaStream into a JSON object.
5351
+ */
5352
+ const dumpStream = (stream) => ({
5353
+ id: stream.id,
5354
+ tracks: stream.getTracks().map((track) => ({
5355
+ id: track.id,
5356
+ kind: track.kind,
5357
+ label: track.label,
5358
+ enabled: track.enabled,
5359
+ muted: track.muted,
5360
+ readyState: track.readyState,
5361
+ })),
5362
+ });
5354
5363
  const getSdkSignature = (clientDetails) => {
5355
5364
  const { sdk, ...platform } = clientDetails;
5356
5365
  const sdkName = getSdkName(sdk);
@@ -5471,30 +5480,17 @@ const createStatsReporter = ({ subscriber, publisher, state, datacenter, polling
5471
5480
  }
5472
5481
  }
5473
5482
  }
5474
- const [subscriberStats, publisherStats] = await Promise.all([
5475
- subscriber
5476
- .getStats()
5477
- .then((report) => transform(report, {
5478
- kind: 'subscriber',
5479
- trackKind: 'video',
5480
- publisher,
5481
- }))
5482
- .then(aggregate),
5483
- publisher
5484
- ? publisher
5485
- .getStats()
5486
- .then((report) => transform(report, {
5487
- kind: 'publisher',
5488
- trackKind: 'video',
5489
- publisher,
5490
- }))
5491
- .then(aggregate)
5492
- : getEmptyStats(),
5493
- ]);
5494
5483
  const [subscriberRawStats, publisherRawStats] = await Promise.all([
5495
5484
  getRawStatsForTrack('subscriber'),
5496
5485
  publisher ? getRawStatsForTrack('publisher') : undefined,
5497
5486
  ]);
5487
+ const process = (report, kind) => aggregate(transform(report, { kind, trackKind: 'video', publisher }));
5488
+ const subscriberStats = subscriberRawStats
5489
+ ? process(subscriberRawStats, 'subscriber')
5490
+ : getEmptyStats();
5491
+ const publisherStats = publisherRawStats
5492
+ ? process(publisherRawStats, 'publisher')
5493
+ : getEmptyStats();
5498
5494
  state.setCallStatsReport({
5499
5495
  datacenter,
5500
5496
  publisherStats,
@@ -5652,7 +5648,7 @@ const aggregate = (stats) => {
5652
5648
  return report;
5653
5649
  };
5654
5650
 
5655
- const version = "1.20.2";
5651
+ const version = "1.22.0";
5656
5652
  const [major, minor, patch] = version.split('.');
5657
5653
  let sdkInfo = {
5658
5654
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -5789,7 +5785,7 @@ const getClientDetails = async () => {
5789
5785
  };
5790
5786
 
5791
5787
  class SfuStatsReporter {
5792
- constructor(sfuClient, { options, clientDetails, subscriber, publisher, microphone, camera, state, }) {
5788
+ constructor(sfuClient, { options, clientDetails, subscriber, publisher, microphone, camera, state, tracer, unifiedSessionId, }) {
5793
5789
  this.logger = getLogger(['SfuStatsReporter']);
5794
5790
  this.inputDevices = new Map();
5795
5791
  this.observeDevice = (device, kind) => {
@@ -5846,31 +5842,40 @@ class SfuStatsReporter {
5846
5842
  };
5847
5843
  this.run = async (telemetry) => {
5848
5844
  const [subscriberStats, publisherStats] = await Promise.all([
5849
- this.subscriber.getStats().then(flatten).then(JSON.stringify),
5850
- this.publisher?.getStats().then(flatten).then(JSON.stringify) ?? '[]',
5845
+ this.subscriber.stats.get(),
5846
+ this.publisher?.stats.get(),
5851
5847
  ]);
5852
- const subscriberTrace = this.subscriber.getTrace();
5853
- const publisherTrace = this.publisher?.getTrace();
5854
- const mediaTrace = tracer.take();
5848
+ this.subscriber.tracer?.trace('getstats', subscriberStats.delta);
5849
+ if (publisherStats) {
5850
+ this.publisher?.tracer?.trace('getstats', publisherStats.delta);
5851
+ }
5852
+ const subscriberTrace = this.subscriber.tracer?.take();
5853
+ const publisherTrace = this.publisher?.tracer?.take();
5854
+ const tracer = this.tracer.take();
5855
5855
  const sfuTrace = this.sfuClient.getTrace();
5856
- const publisherTraces = [
5857
- ...mediaTrace.snapshot,
5856
+ const traces = [
5857
+ ...tracer.snapshot,
5858
5858
  ...(sfuTrace?.snapshot ?? []),
5859
5859
  ...(publisherTrace?.snapshot ?? []),
5860
+ ...(subscriberTrace?.snapshot ?? []),
5860
5861
  ];
5861
5862
  try {
5862
5863
  await this.sfuClient.sendStats({
5863
5864
  sdk: this.sdkName,
5864
5865
  sdkVersion: this.sdkVersion,
5865
5866
  webrtcVersion: this.webRTCVersion,
5866
- subscriberStats,
5867
- subscriberRtcStats: subscriberTrace
5868
- ? JSON.stringify(subscriberTrace.snapshot)
5869
- : '',
5870
- publisherStats,
5871
- publisherRtcStats: publisherTraces.length > 0 ? JSON.stringify(publisherTraces) : '',
5867
+ subscriberStats: JSON.stringify(flatten(subscriberStats.stats)),
5868
+ publisherStats: publisherStats
5869
+ ? JSON.stringify(flatten(publisherStats.stats))
5870
+ : '[]',
5871
+ subscriberRtcStats: '',
5872
+ publisherRtcStats: '',
5873
+ rtcStats: JSON.stringify(traces),
5874
+ encodeStats: publisherStats?.performanceStats ?? [],
5875
+ decodeStats: subscriberStats.performanceStats,
5872
5876
  audioDevices: this.inputDevices.get('mic'),
5873
5877
  videoDevices: this.inputDevices.get('camera'),
5878
+ unifiedSessionId: this.unifiedSessionId,
5874
5879
  deviceState: getDeviceState(),
5875
5880
  telemetry,
5876
5881
  });
@@ -5878,7 +5883,7 @@ class SfuStatsReporter {
5878
5883
  catch (err) {
5879
5884
  publisherTrace?.rollback();
5880
5885
  subscriberTrace?.rollback();
5881
- mediaTrace.rollback();
5886
+ tracer.rollback();
5882
5887
  sfuTrace?.rollback();
5883
5888
  throw err;
5884
5889
  }
@@ -5903,6 +5908,16 @@ class SfuStatsReporter {
5903
5908
  this.inputDevices.clear();
5904
5909
  clearInterval(this.intervalId);
5905
5910
  this.intervalId = undefined;
5911
+ clearTimeout(this.timeoutId);
5912
+ this.timeoutId = undefined;
5913
+ };
5914
+ this.scheduleOne = (timeout) => {
5915
+ clearTimeout(this.timeoutId);
5916
+ this.timeoutId = setTimeout(() => {
5917
+ this.run().catch((err) => {
5918
+ this.logger('warn', 'Failed to report stats', err);
5919
+ });
5920
+ }, timeout);
5906
5921
  };
5907
5922
  this.sfuClient = sfuClient;
5908
5923
  this.options = options;
@@ -5911,6 +5926,8 @@ class SfuStatsReporter {
5911
5926
  this.microphone = microphone;
5912
5927
  this.camera = camera;
5913
5928
  this.state = state;
5929
+ this.tracer = tracer;
5930
+ this.unifiedSessionId = unifiedSessionId;
5914
5931
  const { sdk, browser } = clientDetails;
5915
5932
  this.sdkName = getSdkName(sdk);
5916
5933
  this.sdkVersion = getSdkVersion(sdk);
@@ -5933,47 +5950,25 @@ const traceRTCPeerConnection = (pc, trace) => {
5933
5950
  trace('ontrack', `${e.track.kind}:${e.track.id} ${streams}`);
5934
5951
  });
5935
5952
  pc.addEventListener('signalingstatechange', () => {
5936
- trace('onsignalingstatechange', pc.signalingState);
5953
+ trace('signalingstatechange', pc.signalingState);
5937
5954
  });
5938
5955
  pc.addEventListener('iceconnectionstatechange', () => {
5939
- trace('oniceconnectionstatechange', pc.iceConnectionState);
5956
+ trace('iceconnectionstatechange', pc.iceConnectionState);
5940
5957
  });
5941
5958
  pc.addEventListener('icegatheringstatechange', () => {
5942
- trace('onicegatheringstatechange', pc.iceGatheringState);
5959
+ trace('icegatheringstatechange', pc.iceGatheringState);
5943
5960
  });
5944
5961
  pc.addEventListener('connectionstatechange', () => {
5945
- trace('onconnectionstatechange', pc.connectionState);
5962
+ trace('connectionstatechange', pc.connectionState);
5946
5963
  });
5947
5964
  pc.addEventListener('negotiationneeded', () => {
5948
- trace('onnegotiationneeded', undefined);
5965
+ trace('negotiationneeded', undefined);
5949
5966
  });
5950
5967
  pc.addEventListener('datachannel', ({ channel }) => {
5951
- trace('ondatachannel', [channel.id, channel.label]);
5952
- });
5953
- let prev = {};
5954
- const getStats = () => {
5955
- pc.getStats(null)
5956
- .then((stats) => {
5957
- const now = toObject(stats);
5958
- trace('getstats', deltaCompression(prev, now));
5959
- prev = now;
5960
- })
5961
- .catch((err) => {
5962
- trace('getstatsOnFailure', err.toString());
5963
- });
5964
- };
5965
- const interval = setInterval(() => {
5966
- getStats();
5967
- }, 8000);
5968
- pc.addEventListener('connectionstatechange', () => {
5969
- const state = pc.connectionState;
5970
- if (state === 'connected' || state === 'failed') {
5971
- getStats();
5972
- }
5968
+ trace('datachannel', [channel.id, channel.label]);
5973
5969
  });
5974
5970
  const origClose = pc.close;
5975
5971
  pc.close = function tracedClose() {
5976
- clearInterval(interval);
5977
5972
  trace('close', undefined);
5978
5973
  return origClose.call(this);
5979
5974
  };
@@ -6003,9 +5998,162 @@ const traceRTCPeerConnection = (pc, trace) => {
6003
5998
  };
6004
5999
  }
6005
6000
  };
6006
- const toObject = (s) => {
6001
+
6002
+ /**
6003
+ * StatsTracer is a class that collects and processes WebRTC stats.
6004
+ * It is used to track the performance of the WebRTC connection
6005
+ * and to provide information about the media streams.
6006
+ * It is used by both the Publisher and Subscriber classes.
6007
+ *
6008
+ * @internal
6009
+ */
6010
+ class StatsTracer {
6011
+ /**
6012
+ * Creates a new StatsTracer instance.
6013
+ */
6014
+ constructor(pc, peerType, trackIdToTrackType) {
6015
+ this.previousStats = {};
6016
+ this.frameTimeHistory = [];
6017
+ this.fpsHistory = [];
6018
+ /**
6019
+ * Get the stats from the RTCPeerConnection.
6020
+ * When called, it will return the stats for the current connection.
6021
+ * It will also return the delta between the current stats and the previous stats.
6022
+ * This is used to track the performance of the connection.
6023
+ *
6024
+ * @internal
6025
+ */
6026
+ this.get = async () => {
6027
+ const stats = await this.pc.getStats();
6028
+ const currentStats = toObject(stats);
6029
+ const performanceStats = this.withOverrides(this.peerType === PeerType.SUBSCRIBER
6030
+ ? this.getDecodeStats(currentStats)
6031
+ : this.getEncodeStats(currentStats));
6032
+ const delta = deltaCompression(this.previousStats, currentStats);
6033
+ // store the current data for the next iteration
6034
+ this.previousStats = currentStats;
6035
+ this.frameTimeHistory = this.frameTimeHistory.slice(-2);
6036
+ this.fpsHistory = this.fpsHistory.slice(-2);
6037
+ return { performanceStats, delta, stats };
6038
+ };
6039
+ /**
6040
+ * Collects encode stats from the RTCPeerConnection.
6041
+ */
6042
+ this.getEncodeStats = (currentStats) => {
6043
+ const encodeStats = [];
6044
+ for (const rtp of Object.values(currentStats)) {
6045
+ if (rtp.type !== 'outbound-rtp')
6046
+ continue;
6047
+ const { codecId, framesSent = 0, kind, id, totalEncodeTime = 0, framesPerSecond = 0, frameHeight = 0, frameWidth = 0, targetBitrate = 0, mediaSourceId, } = rtp;
6048
+ if (kind === 'audio' || !this.previousStats[id])
6049
+ continue;
6050
+ const prevRtp = this.previousStats[id];
6051
+ const deltaTotalEncodeTime = totalEncodeTime - (prevRtp.totalEncodeTime || 0);
6052
+ const deltaFramesSent = framesSent - (prevRtp.framesSent || 0);
6053
+ const framesEncodeTime = deltaFramesSent > 0
6054
+ ? (deltaTotalEncodeTime / deltaFramesSent) * 1000
6055
+ : 0;
6056
+ this.frameTimeHistory.push(framesEncodeTime);
6057
+ this.fpsHistory.push(framesPerSecond);
6058
+ let trackType = TrackType.VIDEO;
6059
+ if (mediaSourceId && currentStats[mediaSourceId]) {
6060
+ const mediaSource = currentStats[mediaSourceId];
6061
+ trackType =
6062
+ this.trackIdToTrackType.get(mediaSource.trackIdentifier) || trackType;
6063
+ }
6064
+ encodeStats.push({
6065
+ trackType,
6066
+ codec: getCodecFromStats(currentStats, codecId),
6067
+ avgFrameTimeMs: average(this.frameTimeHistory),
6068
+ avgFps: average(this.fpsHistory),
6069
+ targetBitrate: Math.round(targetBitrate),
6070
+ videoDimension: { width: frameWidth, height: frameHeight },
6071
+ });
6072
+ }
6073
+ return encodeStats;
6074
+ };
6075
+ /**
6076
+ * Collects decode stats from the RTCPeerConnection.
6077
+ */
6078
+ this.getDecodeStats = (currentStats) => {
6079
+ let rtp = undefined;
6080
+ let max = 0;
6081
+ for (const item of Object.values(currentStats)) {
6082
+ if (item.type !== 'inbound-rtp')
6083
+ continue;
6084
+ const rtpItem = item;
6085
+ const { kind, frameWidth = 0, frameHeight = 0 } = rtpItem;
6086
+ const area = frameWidth * frameHeight;
6087
+ if (kind === 'video' && area > max) {
6088
+ rtp = rtpItem;
6089
+ max = area;
6090
+ }
6091
+ }
6092
+ if (!rtp || !this.previousStats[rtp.id])
6093
+ return [];
6094
+ const prevRtp = this.previousStats[rtp.id];
6095
+ const { framesDecoded = 0, framesPerSecond = 0, totalDecodeTime = 0, trackIdentifier, } = rtp;
6096
+ const deltaTotalDecodeTime = totalDecodeTime - (prevRtp.totalDecodeTime || 0);
6097
+ const deltaFramesDecoded = framesDecoded - (prevRtp.framesDecoded || 0);
6098
+ const framesDecodeTime = deltaFramesDecoded > 0
6099
+ ? (deltaTotalDecodeTime / deltaFramesDecoded) * 1000
6100
+ : 0;
6101
+ this.frameTimeHistory.push(framesDecodeTime);
6102
+ this.fpsHistory.push(framesPerSecond);
6103
+ const trackType = this.trackIdToTrackType.get(trackIdentifier) || TrackType.VIDEO;
6104
+ return [
6105
+ PerformanceStats.create({
6106
+ trackType,
6107
+ codec: getCodecFromStats(currentStats, rtp.codecId),
6108
+ avgFrameTimeMs: average(this.frameTimeHistory),
6109
+ avgFps: average(this.fpsHistory),
6110
+ videoDimension: { width: rtp.frameWidth, height: rtp.frameHeight },
6111
+ }),
6112
+ ];
6113
+ };
6114
+ /**
6115
+ * Applies cost overrides to the performance stats.
6116
+ * This is used to override the default encode/decode times with custom values.
6117
+ * This is useful for testing and debugging purposes, and it shouldn't be used in production.
6118
+ */
6119
+ this.withOverrides = (performanceStats) => {
6120
+ if (this.costOverrides) {
6121
+ for (const s of performanceStats) {
6122
+ const override = this.costOverrides.get(s.trackType);
6123
+ if (override !== undefined) {
6124
+ // override the average encode/decode time with the provided cost.
6125
+ // format: [override].[original-frame-time]
6126
+ s.avgFrameTimeMs = override + (s.avgFrameTimeMs || 0) / 1000;
6127
+ }
6128
+ }
6129
+ }
6130
+ return performanceStats;
6131
+ };
6132
+ /**
6133
+ * Set the encode/decode cost for a specific track type.
6134
+ * This is used to override the default encode/decode times with custom values.
6135
+ * This is useful for testing and debugging purposes, and it shouldn't be used in production.
6136
+ *
6137
+ * @internal
6138
+ */
6139
+ this.setCost = (cost, trackType = TrackType.VIDEO) => {
6140
+ if (!this.costOverrides)
6141
+ this.costOverrides = new Map();
6142
+ this.costOverrides.set(trackType, cost);
6143
+ };
6144
+ this.pc = pc;
6145
+ this.peerType = peerType;
6146
+ this.trackIdToTrackType = trackIdToTrackType;
6147
+ }
6148
+ }
6149
+ /**
6150
+ * Convert the stat report to an object.
6151
+ *
6152
+ * @param report the stat report to convert.
6153
+ */
6154
+ const toObject = (report) => {
6007
6155
  const obj = {};
6008
- s.forEach((v, k) => {
6156
+ report.forEach((v, k) => {
6009
6157
  obj[k] = v;
6010
6158
  });
6011
6159
  return obj;
@@ -6028,12 +6176,13 @@ const deltaCompression = (oldStats, newStats) => {
6028
6176
  }
6029
6177
  }
6030
6178
  let timestamp = -Infinity;
6031
- for (const report of Object.values(newStats)) {
6179
+ const values = Object.values(newStats);
6180
+ for (const report of values) {
6032
6181
  if (report.timestamp > timestamp) {
6033
6182
  timestamp = report.timestamp;
6034
6183
  }
6035
6184
  }
6036
- for (const report of Object.values(newStats)) {
6185
+ for (const report of values) {
6037
6186
  if (report.timestamp === timestamp) {
6038
6187
  report.timestamp = 0;
6039
6188
  }
@@ -6041,6 +6190,59 @@ const deltaCompression = (oldStats, newStats) => {
6041
6190
  newStats.timestamp = timestamp;
6042
6191
  return newStats;
6043
6192
  };
6193
+ /**
6194
+ * Calculates an average value.
6195
+ */
6196
+ const average = (arr) => arr.reduce((a, b) => a + b, 0) / arr.length;
6197
+ /**
6198
+ * Create a Codec object from the codec stats.
6199
+ *
6200
+ * @param stats the stats report.
6201
+ * @param codecId the codec ID to look for.
6202
+ */
6203
+ const getCodecFromStats = (stats, codecId) => {
6204
+ if (!codecId || !stats[codecId])
6205
+ return;
6206
+ const codecStats = stats[codecId];
6207
+ return Codec.create({
6208
+ name: codecStats.mimeType.split('/').pop(), // video/av1 -> av1
6209
+ clockRate: codecStats.clockRate,
6210
+ payloadType: codecStats.payloadType,
6211
+ fmtp: codecStats.sdpFmtpLine,
6212
+ });
6213
+ };
6214
+
6215
+ class Tracer {
6216
+ constructor(id) {
6217
+ this.buffer = [];
6218
+ this.enabled = true;
6219
+ this.setEnabled = (enabled) => {
6220
+ if (this.enabled === enabled)
6221
+ return;
6222
+ this.enabled = enabled;
6223
+ this.buffer = [];
6224
+ };
6225
+ this.trace = (tag, data) => {
6226
+ if (!this.enabled)
6227
+ return;
6228
+ this.buffer.push([tag, this.id, data, Date.now()]);
6229
+ };
6230
+ this.take = () => {
6231
+ const snapshot = this.buffer;
6232
+ this.buffer = [];
6233
+ return {
6234
+ snapshot,
6235
+ rollback: () => {
6236
+ this.buffer.unshift(...snapshot);
6237
+ },
6238
+ };
6239
+ };
6240
+ this.dispose = () => {
6241
+ this.buffer = [];
6242
+ };
6243
+ this.id = id;
6244
+ }
6245
+ }
6044
6246
 
6045
6247
  /**
6046
6248
  * A base class for the `Publisher` and `Subscriber` classes.
@@ -6050,17 +6252,20 @@ class BasePeerConnection {
6050
6252
  /**
6051
6253
  * Constructs a new `BasePeerConnection` instance.
6052
6254
  */
6053
- constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onUnrecoverableError, logTag, clientDetails, enableTracing, }) {
6255
+ constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onUnrecoverableError, logTag, enableTracing, }) {
6054
6256
  this.isIceRestarting = false;
6055
6257
  this.isDisposed = false;
6258
+ this.trackIdToTrackType = new Map();
6056
6259
  this.subscriptions = [];
6260
+ this.lock = Math.random().toString(36).slice(2);
6057
6261
  /**
6058
6262
  * Handles events synchronously.
6059
6263
  * Consecutive events are queued and executed one after the other.
6060
6264
  */
6061
6265
  this.on = (event, fn) => {
6062
6266
  this.subscriptions.push(this.dispatcher.on(event, (e) => {
6063
- withoutConcurrency(`pc.${event}`, async () => fn(e)).catch((err) => {
6267
+ const lockKey = `pc.${this.lock}.${event}`;
6268
+ withoutConcurrency(lockKey, async () => fn(e)).catch((err) => {
6064
6269
  if (this.isDisposed)
6065
6270
  return;
6066
6271
  this.logger('warn', `Error handling ${event}`, err);
@@ -6100,10 +6305,10 @@ class BasePeerConnection {
6100
6305
  return this.pc.getStats(selector);
6101
6306
  };
6102
6307
  /**
6103
- * Returns the current tracing buffer.
6308
+ * Maps the given track ID to the corresponding track type.
6104
6309
  */
6105
- this.getTrace = () => {
6106
- return this.tracer?.take();
6310
+ this.getTrackType = (trackId) => {
6311
+ return this.trackIdToTrackType.get(trackId);
6107
6312
  };
6108
6313
  /**
6109
6314
  * Handles the ICECandidate event and
@@ -6137,6 +6342,24 @@ class BasePeerConnection {
6137
6342
  }
6138
6343
  return JSON.stringify(candidate.toJSON());
6139
6344
  };
6345
+ /**
6346
+ * Handles the ConnectionStateChange event.
6347
+ */
6348
+ this.onConnectionStateChange = async () => {
6349
+ const state = this.pc.connectionState;
6350
+ this.logger('debug', `Connection state changed`, state);
6351
+ if (!this.tracer)
6352
+ return;
6353
+ if (state === 'connected' || state === 'failed') {
6354
+ try {
6355
+ const stats = await this.stats.get();
6356
+ this.tracer.trace('getstats', stats.delta);
6357
+ }
6358
+ catch (err) {
6359
+ this.tracer.trace('getstatsOnFailure', err.toString());
6360
+ }
6361
+ }
6362
+ };
6140
6363
  /**
6141
6364
  * Handles the ICE connection state change event.
6142
6365
  */
@@ -6193,18 +6416,22 @@ class BasePeerConnection {
6193
6416
  logTag,
6194
6417
  ]);
6195
6418
  this.pc = new RTCPeerConnection(connectionConfig);
6196
- if (enableTracing) {
6197
- const tag = `${logTag}-${peerType === PeerType.SUBSCRIBER ? 'sub' : 'pub'}`;
6198
- this.tracer = new Tracer(tag);
6199
- this.tracer.trace('clientDetails', clientDetails);
6200
- this.tracer.trace('create', connectionConfig);
6201
- traceRTCPeerConnection(this.pc, this.tracer.trace);
6202
- }
6203
6419
  this.pc.addEventListener('icecandidate', this.onIceCandidate);
6204
6420
  this.pc.addEventListener('icecandidateerror', this.onIceCandidateError);
6205
6421
  this.pc.addEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
6206
6422
  this.pc.addEventListener('icegatheringstatechange', this.onIceGatherChange);
6207
6423
  this.pc.addEventListener('signalingstatechange', this.onSignalingChange);
6424
+ this.pc.addEventListener('connectionstatechange', this.onConnectionStateChange);
6425
+ this.stats = new StatsTracer(this.pc, peerType, this.trackIdToTrackType);
6426
+ if (enableTracing) {
6427
+ const tag = `${logTag}-${peerType === PeerType.SUBSCRIBER ? 'sub' : 'pub'}`;
6428
+ this.tracer = new Tracer(tag);
6429
+ this.tracer.trace('create', {
6430
+ url: sfuClient.edgeName,
6431
+ ...connectionConfig,
6432
+ });
6433
+ traceRTCPeerConnection(this.pc, this.tracer.trace);
6434
+ }
6208
6435
  }
6209
6436
  /**
6210
6437
  * Disposes the `RTCPeerConnection` instance.
@@ -6570,7 +6797,7 @@ class Publisher extends BasePeerConnection {
6570
6797
  }
6571
6798
  else {
6572
6799
  const previousTrack = transceiver.sender.track;
6573
- await transceiver.sender.replaceTrack(trackToPublish);
6800
+ await this.updateTransceiver(transceiver, trackToPublish, trackType);
6574
6801
  if (!isReactNative()) {
6575
6802
  this.stopTrack(previousTrack);
6576
6803
  }
@@ -6592,8 +6819,20 @@ class Publisher extends BasePeerConnection {
6592
6819
  const trackType = publishOption.trackType;
6593
6820
  this.logger('debug', `Added ${TrackType[trackType]} transceiver`);
6594
6821
  this.transceiverCache.add(publishOption, transceiver);
6822
+ this.trackIdToTrackType.set(track.id, trackType);
6595
6823
  await this.negotiate();
6596
6824
  };
6825
+ /**
6826
+ * Updates the transceiver with the given track and track type.
6827
+ */
6828
+ this.updateTransceiver = async (transceiver, track, trackType) => {
6829
+ const sender = transceiver.sender;
6830
+ if (sender.track)
6831
+ this.trackIdToTrackType.delete(sender.track.id);
6832
+ await sender.replaceTrack(track);
6833
+ if (track)
6834
+ this.trackIdToTrackType.set(track.id, trackType);
6835
+ };
6597
6836
  /**
6598
6837
  * Synchronizes the current Publisher state with the provided publish options.
6599
6838
  */
@@ -6623,7 +6862,7 @@ class Publisher extends BasePeerConnection {
6623
6862
  continue;
6624
6863
  // it is safe to stop the track here, it is a clone
6625
6864
  this.stopTrack(transceiver.sender.track);
6626
- await transceiver.sender.replaceTrack(null);
6865
+ await this.updateTransceiver(transceiver, null, publishOption.trackType);
6627
6866
  }
6628
6867
  };
6629
6868
  /**
@@ -6643,18 +6882,6 @@ class Publisher extends BasePeerConnection {
6643
6882
  }
6644
6883
  return false;
6645
6884
  };
6646
- /**
6647
- * Maps the given track ID to the corresponding track type.
6648
- */
6649
- this.getTrackType = (trackId) => {
6650
- for (const transceiverId of this.transceiverCache.items()) {
6651
- const { publishOption, transceiver } = transceiverId;
6652
- if (transceiver.sender.track?.id === trackId) {
6653
- return publishOption.trackType;
6654
- }
6655
- }
6656
- return undefined;
6657
- };
6658
6885
  /**
6659
6886
  * Stops the cloned track that is being published to the SFU.
6660
6887
  */
@@ -6757,7 +6984,7 @@ class Publisher extends BasePeerConnection {
6757
6984
  * @param options the optional offer options to use.
6758
6985
  */
6759
6986
  this.negotiate = async (options) => {
6760
- return withoutConcurrency('publisher.negotiate', async () => {
6987
+ return withoutConcurrency(`publisher.negotiate.${this.lock}`, async () => {
6761
6988
  const offer = await this.pc.createOffer(options);
6762
6989
  const tracks = this.getAnnouncedTracks(offer.sdp);
6763
6990
  if (!tracks.length)
@@ -6948,6 +7175,7 @@ class Subscriber extends BasePeerConnection {
6948
7175
  if (!trackType) {
6949
7176
  return this.logger('error', `Unknown track type: ${rawTrackType}`);
6950
7177
  }
7178
+ this.trackIdToTrackType.set(e.track.id, trackType);
6951
7179
  if (!participantToUpdate) {
6952
7180
  this.logger('warn', `[onTrack]: Received track for unknown participant: ${trackId}`, e);
6953
7181
  this.state.registerOrphanedTrack({
@@ -7291,12 +7519,22 @@ class StreamSfuClient {
7291
7519
  */
7292
7520
  this.abortController = new AbortController();
7293
7521
  this.createWebSocket = () => {
7522
+ const eventsToTrace = {
7523
+ callEnded: true,
7524
+ changePublishQuality: true,
7525
+ error: true,
7526
+ goAway: true,
7527
+ };
7294
7528
  this.signalWs = createWebSocketSignalChannel({
7295
7529
  logTag: this.logTag,
7296
7530
  endpoint: `${this.credentials.server.ws_endpoint}?tag=${this.logTag}`,
7297
7531
  onMessage: (message) => {
7298
7532
  this.lastMessageTimestamp = new Date();
7299
7533
  this.scheduleConnectionCheck();
7534
+ const eventKind = message.eventPayload.oneofKind;
7535
+ if (eventsToTrace[eventKind]) {
7536
+ this.tracer?.trace(eventKind, message);
7537
+ }
7300
7538
  this.dispatcher.dispatch(message, this.logTag);
7301
7539
  },
7302
7540
  });
@@ -7389,7 +7627,8 @@ class StreamSfuClient {
7389
7627
  };
7390
7628
  this.sendStats = async (stats) => {
7391
7629
  await this.joinTask;
7392
- return retryable(() => this.rpc.sendStats({ ...stats, sessionId: this.sessionId }), this.abortController.signal);
7630
+ // NOTE: we don't retry sending stats
7631
+ return this.rpc.sendStats({ ...stats, sessionId: this.sessionId });
7393
7632
  };
7394
7633
  this.startNoiseCancellation = async () => {
7395
7634
  await this.joinTask;
@@ -7441,7 +7680,7 @@ class StreamSfuClient {
7441
7680
  unsubscribe();
7442
7681
  current.reject(new Error('Waiting for "joinResponse" has timed out'));
7443
7682
  }, this.joinResponseTimeout);
7444
- await this.send(SfuRequest.create({
7683
+ const joinRequest = SfuRequest.create({
7445
7684
  requestPayload: {
7446
7685
  oneofKind: 'joinRequest',
7447
7686
  joinRequest: JoinRequest.create({
@@ -7450,7 +7689,9 @@ class StreamSfuClient {
7450
7689
  token: this.credentials.token,
7451
7690
  }),
7452
7691
  },
7453
- }));
7692
+ });
7693
+ this.tracer?.trace('joinRequest', joinRequest);
7694
+ await this.send(joinRequest);
7454
7695
  return current.promise;
7455
7696
  };
7456
7697
  this.ping = async () => {
@@ -7511,7 +7752,9 @@ class StreamSfuClient {
7511
7752
  this.joinResponseTimeout = joinResponseTimeout;
7512
7753
  this.logTag = logTag;
7513
7754
  this.logger = getLogger(['SfuClient', logTag]);
7514
- this.tracer = enableTracing ? new Tracer(logTag) : undefined;
7755
+ this.tracer = enableTracing
7756
+ ? new Tracer(`${logTag}-${this.edgeName}`)
7757
+ : undefined;
7515
7758
  this.rpc = createSignalClient({
7516
7759
  baseUrl: server.url,
7517
7760
  interceptors: [
@@ -7649,6 +7892,13 @@ const watchSfuCallEnded = (call) => {
7649
7892
  if (call.state.callingState === exports.CallingState.LEFT)
7650
7893
  return;
7651
7894
  try {
7895
+ if (e.reason === CallEndedReason.LIVE_ENDED) {
7896
+ call.state.setBackstage(true);
7897
+ // don't leave the call if the user has permission to join backstage
7898
+ const { hasPermission } = call.permissionsContext;
7899
+ if (hasPermission(OwnCapability.JOIN_BACKSTAGE))
7900
+ return;
7901
+ }
7652
7902
  // `call.ended` event arrived after the call is already left
7653
7903
  // and all event handlers are detached. We need to manually
7654
7904
  // update the call state to reflect the call has ended.
@@ -8411,7 +8661,6 @@ class DynascaleManager {
8411
8661
  const { selectedDevice } = this.speaker.state;
8412
8662
  if (selectedDevice && 'setSinkId' in audioElement) {
8413
8663
  audioElement.setSinkId(selectedDevice);
8414
- tracer.trace('navigator.mediaDevices.setSinkId', selectedDevice);
8415
8664
  }
8416
8665
  }
8417
8666
  });
@@ -8421,7 +8670,6 @@ class DynascaleManager {
8421
8670
  : this.speaker.state.selectedDevice$.subscribe((deviceId) => {
8422
8671
  if (deviceId) {
8423
8672
  audioElement.setSinkId(deviceId);
8424
- tracer.trace('navigator.mediaDevices.setSinkId', deviceId);
8425
8673
  }
8426
8674
  });
8427
8675
  const volumeSubscription = rxjs.combineLatest([
@@ -8863,15 +9111,25 @@ const getVideoDevices = lazy(() => {
8863
9111
  const getAudioOutputDevices = lazy(() => {
8864
9112
  return rxjs.merge(getDeviceChangeObserver(), getAudioBrowserPermission().asObservable()).pipe(rxjs.startWith(undefined), rxjs.concatMap(() => getDevices(getAudioBrowserPermission(), 'audiooutput')), rxjs.shareReplay(1));
8865
9113
  });
8866
- const getStream = async (constraints) => {
8867
- const stream = await navigator.mediaDevices.getUserMedia(constraints);
8868
- if (isFirefox()) {
8869
- // When enumerating devices, Firefox will hide device labels unless there's been
8870
- // an active user media stream on the page. So we force device list updates after
8871
- // every successful getUserMedia call.
8872
- navigator.mediaDevices.dispatchEvent(new Event('devicechange'));
9114
+ let getUserMediaExecId = 0;
9115
+ const getStream = async (constraints, tracer) => {
9116
+ const tag = `navigator.mediaDevices.getUserMedia.${getUserMediaExecId++}.`;
9117
+ try {
9118
+ tracer?.trace(tag, constraints);
9119
+ const stream = await navigator.mediaDevices.getUserMedia(constraints);
9120
+ tracer?.trace(`${tag}OnSuccess`, dumpStream(stream));
9121
+ if (isFirefox()) {
9122
+ // When enumerating devices, Firefox will hide device labels unless there's been
9123
+ // an active user media stream on the page. So we force device list updates after
9124
+ // every successful getUserMedia call.
9125
+ navigator.mediaDevices.dispatchEvent(new Event('devicechange'));
9126
+ }
9127
+ return stream;
9128
+ }
9129
+ catch (error) {
9130
+ tracer?.trace(`${tag}OnFailure`, error.name);
9131
+ throw error;
8873
9132
  }
8874
- return stream;
8875
9133
  };
8876
9134
  function isNotFoundOrOverconstrainedError(error) {
8877
9135
  if (!error || typeof error !== 'object') {
@@ -8895,11 +9153,11 @@ function isNotFoundOrOverconstrainedError(error) {
8895
9153
  * Returns an audio media stream that fulfills the given constraints.
8896
9154
  * If no constraints are provided, it uses the browser's default ones.
8897
9155
  *
8898
- * @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.
8899
9156
  * @param trackConstraints the constraints to use when requesting the stream.
8900
- * @returns the new `MediaStream` fulfilling the given constraints.
9157
+ * @param tracer the tracer to use for tracing the stream creation.
9158
+ * @returns a new `MediaStream` fulfilling the given constraints.
8901
9159
  */
8902
- const getAudioStream = async (trackConstraints) => {
9160
+ const getAudioStream = async (trackConstraints, tracer) => {
8903
9161
  const constraints = {
8904
9162
  audio: {
8905
9163
  ...audioDeviceConstraints.audio,
@@ -8911,7 +9169,7 @@ const getAudioStream = async (trackConstraints) => {
8911
9169
  throwOnNotAllowed: true,
8912
9170
  forcePrompt: true,
8913
9171
  });
8914
- return await getStream(constraints);
9172
+ return await getStream(constraints, tracer);
8915
9173
  }
8916
9174
  catch (error) {
8917
9175
  if (isNotFoundOrOverconstrainedError(error) && trackConstraints?.deviceId) {
@@ -8931,11 +9189,11 @@ const getAudioStream = async (trackConstraints) => {
8931
9189
  * Returns a video media stream that fulfills the given constraints.
8932
9190
  * If no constraints are provided, it uses the browser's default ones.
8933
9191
  *
8934
- * @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.
8935
9192
  * @param trackConstraints the constraints to use when requesting the stream.
9193
+ * @param tracer the tracer to use for tracing the stream creation.
8936
9194
  * @returns a new `MediaStream` fulfilling the given constraints.
8937
9195
  */
8938
- const getVideoStream = async (trackConstraints) => {
9196
+ const getVideoStream = async (trackConstraints, tracer) => {
8939
9197
  const constraints = {
8940
9198
  video: {
8941
9199
  ...videoDeviceConstraints.video,
@@ -8947,7 +9205,7 @@ const getVideoStream = async (trackConstraints) => {
8947
9205
  throwOnNotAllowed: true,
8948
9206
  forcePrompt: true,
8949
9207
  });
8950
- return await getStream(constraints);
9208
+ return await getStream(constraints, tracer);
8951
9209
  }
8952
9210
  catch (error) {
8953
9211
  if (isNotFoundOrOverconstrainedError(error) && trackConstraints?.deviceId) {
@@ -8963,19 +9221,21 @@ const getVideoStream = async (trackConstraints) => {
8963
9221
  throw error;
8964
9222
  }
8965
9223
  };
9224
+ let getDisplayMediaExecId = 0;
8966
9225
  /**
8967
9226
  * Prompts the user for a permission to share a screen.
8968
9227
  * If the user grants the permission, a screen sharing stream is returned. Throws otherwise.
8969
9228
  *
8970
9229
  * The callers of this API are responsible to handle the possible errors.
8971
9230
  *
8972
- * @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.
8973
- *
8974
9231
  * @param options any additional options to pass to the [`getDisplayMedia`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia) API.
9232
+ * @param tracer the tracer to use for tracing the stream creation.
8975
9233
  */
8976
- const getScreenShareStream = async (options) => {
9234
+ const getScreenShareStream = async (options, tracer) => {
9235
+ const tag = `navigator.mediaDevices.getDisplayMedia.${getDisplayMediaExecId++}.`;
8977
9236
  try {
8978
- return await navigator.mediaDevices.getDisplayMedia({
9237
+ tracer?.trace(tag, options);
9238
+ const stream = await navigator.mediaDevices.getDisplayMedia({
8979
9239
  video: true,
8980
9240
  audio: {
8981
9241
  channelCount: {
@@ -8989,8 +9249,11 @@ const getScreenShareStream = async (options) => {
8989
9249
  systemAudio: 'include',
8990
9250
  ...options,
8991
9251
  });
9252
+ tracer?.trace(`${tag}OnSuccess`, dumpStream(stream));
9253
+ return stream;
8992
9254
  }
8993
9255
  catch (e) {
9256
+ tracer?.trace(`${tag}OnFailure`, e.name);
8994
9257
  getLogger(['devices'])('error', 'Failed to get screen share stream', e);
8995
9258
  throw e;
8996
9259
  }
@@ -9028,9 +9291,6 @@ const isMobile = () => /Mobi/i.test(navigator.userAgent);
9028
9291
 
9029
9292
  class InputMediaDeviceManager {
9030
9293
  constructor(call, state, trackType) {
9031
- this.call = call;
9032
- this.state = state;
9033
- this.trackType = trackType;
9034
9294
  /**
9035
9295
  * if true, stops the media stream when call is left
9036
9296
  */
@@ -9048,6 +9308,9 @@ class InputMediaDeviceManager {
9048
9308
  this.dispose = () => {
9049
9309
  this.subscriptions.forEach((s) => s());
9050
9310
  };
9311
+ this.call = call;
9312
+ this.state = state;
9313
+ this.trackType = trackType;
9051
9314
  this.logger = getLogger([`${TrackType[trackType].toLowerCase()} manager`]);
9052
9315
  if (deviceIds$ &&
9053
9316
  !isReactNative() &&
@@ -9493,25 +9756,6 @@ class InputMediaDeviceManagerState {
9493
9756
  * The default constraints for the device.
9494
9757
  */
9495
9758
  this.defaultConstraints$ = this.defaultConstraintsSubject.asObservable();
9496
- /**
9497
- * Gets the current value of an observable, or undefined if the observable has
9498
- * not emitted a value yet.
9499
- *
9500
- * @param observable$ the observable to get the value from.
9501
- */
9502
- this.getCurrentValue = getCurrentValue;
9503
- /**
9504
- * Updates the value of the provided Subject.
9505
- * An `update` can either be a new value or a function which takes
9506
- * the current value and returns a new value.
9507
- *
9508
- * @internal
9509
- *
9510
- * @param subject the subject to update.
9511
- * @param update the update to apply to the subject.
9512
- * @return the updated value.
9513
- */
9514
- this.setCurrentValue = setCurrentValue;
9515
9759
  this.hasBrowserPermission$ = permission
9516
9760
  ? permission.asObservable().pipe(rxjs.shareReplay(1))
9517
9761
  : rxjs.of(true);
@@ -9526,39 +9770,39 @@ class InputMediaDeviceManagerState {
9526
9770
  * The device status
9527
9771
  */
9528
9772
  get status() {
9529
- return this.getCurrentValue(this.status$);
9773
+ return getCurrentValue(this.status$);
9530
9774
  }
9531
9775
  /**
9532
9776
  * The requested device status. Useful for optimistic UIs
9533
9777
  */
9534
9778
  get optimisticStatus() {
9535
- return this.getCurrentValue(this.optimisticStatus$);
9779
+ return getCurrentValue(this.optimisticStatus$);
9536
9780
  }
9537
9781
  /**
9538
9782
  * The currently selected device
9539
9783
  */
9540
9784
  get selectedDevice() {
9541
- return this.getCurrentValue(this.selectedDevice$);
9785
+ return getCurrentValue(this.selectedDevice$);
9542
9786
  }
9543
9787
  /**
9544
9788
  * The current media stream, or `undefined` if the device is currently disabled.
9545
9789
  */
9546
9790
  get mediaStream() {
9547
- return this.getCurrentValue(this.mediaStream$);
9791
+ return getCurrentValue(this.mediaStream$);
9548
9792
  }
9549
9793
  /**
9550
9794
  * @internal
9551
9795
  * @param status
9552
9796
  */
9553
9797
  setStatus(status) {
9554
- this.setCurrentValue(this.statusSubject, status);
9798
+ setCurrentValue(this.statusSubject, status);
9555
9799
  }
9556
9800
  /**
9557
9801
  * @internal
9558
9802
  * @param pendingStatus
9559
9803
  */
9560
9804
  setPendingStatus(pendingStatus) {
9561
- this.setCurrentValue(this.optimisticStatusSubject, pendingStatus);
9805
+ setCurrentValue(this.optimisticStatusSubject, pendingStatus);
9562
9806
  }
9563
9807
  /**
9564
9808
  * Updates the `mediaStream` state variable.
@@ -9569,7 +9813,7 @@ class InputMediaDeviceManagerState {
9569
9813
  * as this is the stream that holds the actual deviceId information.
9570
9814
  */
9571
9815
  setMediaStream(stream, rootStream) {
9572
- this.setCurrentValue(this.mediaStreamSubject, stream);
9816
+ setCurrentValue(this.mediaStreamSubject, stream);
9573
9817
  if (rootStream) {
9574
9818
  this.setDevice(this.getDeviceIdFromStream(rootStream));
9575
9819
  }
@@ -9579,13 +9823,13 @@ class InputMediaDeviceManagerState {
9579
9823
  * @param deviceId the device id to set.
9580
9824
  */
9581
9825
  setDevice(deviceId) {
9582
- this.setCurrentValue(this.selectedDeviceSubject, deviceId);
9826
+ setCurrentValue(this.selectedDeviceSubject, deviceId);
9583
9827
  }
9584
9828
  /**
9585
9829
  * Gets the default constraints for the device.
9586
9830
  */
9587
9831
  get defaultConstraints() {
9588
- return this.getCurrentValue(this.defaultConstraints$);
9832
+ return getCurrentValue(this.defaultConstraints$);
9589
9833
  }
9590
9834
  /**
9591
9835
  * Sets the default constraints for the device.
@@ -9594,7 +9838,7 @@ class InputMediaDeviceManagerState {
9594
9838
  * @param constraints the constraints to set.
9595
9839
  */
9596
9840
  setDefaultConstraints(constraints) {
9597
- this.setCurrentValue(this.defaultConstraintsSubject, constraints);
9841
+ setCurrentValue(this.defaultConstraintsSubject, constraints);
9598
9842
  }
9599
9843
  }
9600
9844
 
@@ -9612,13 +9856,13 @@ class CameraManagerState extends InputMediaDeviceManagerState {
9612
9856
  * back - means the camera facing the environment
9613
9857
  */
9614
9858
  get direction() {
9615
- return this.getCurrentValue(this.direction$);
9859
+ return getCurrentValue(this.direction$);
9616
9860
  }
9617
9861
  /**
9618
9862
  * @internal
9619
9863
  */
9620
9864
  setDirection(direction) {
9621
- this.setCurrentValue(this.directionSubject, direction);
9865
+ setCurrentValue(this.directionSubject, direction);
9622
9866
  }
9623
9867
  /**
9624
9868
  * @internal
@@ -9663,32 +9907,32 @@ class CameraManager extends InputMediaDeviceManager {
9663
9907
  * @param direction the direction of the camera to select.
9664
9908
  */
9665
9909
  async selectDirection(direction) {
9666
- if (this.isDirectionSupportedByDevice()) {
9667
- if (isReactNative()) {
9668
- const videoTrack = this.getTracks()[0];
9669
- if (!videoTrack) {
9670
- this.logger('warn', 'No video track found to do direction selection');
9671
- return;
9672
- }
9673
- await videoTrack.applyConstraints({
9674
- facingMode: direction === 'front' ? 'user' : 'environment',
9675
- });
9676
- this.state.setDirection(direction);
9677
- this.state.setDevice(undefined);
9678
- }
9679
- else {
9680
- // web mobile
9681
- this.state.setDirection(direction);
9682
- // Providing both device id and direction doesn't work, so we deselect the device
9683
- this.state.setDevice(undefined);
9684
- this.getTracks().forEach((track) => {
9685
- track.stop();
9686
- });
9910
+ if (!this.isDirectionSupportedByDevice()) {
9911
+ this.logger('warn', 'Setting direction is not supported on this device');
9912
+ return;
9913
+ }
9914
+ // providing both device id and direction doesn't work, so we deselect the device
9915
+ this.state.setDirection(direction);
9916
+ this.state.setDevice(undefined);
9917
+ if (isReactNative()) {
9918
+ const videoTrack = this.getTracks()[0];
9919
+ await videoTrack?.applyConstraints({
9920
+ facingMode: direction === 'front' ? 'user' : 'environment',
9921
+ });
9922
+ return;
9923
+ }
9924
+ this.getTracks().forEach((track) => track.stop());
9925
+ try {
9926
+ await this.unmuteStream();
9927
+ }
9928
+ catch (error) {
9929
+ if (error instanceof Error && error.name === 'NotReadableError') {
9930
+ // the camera is already in use, and the device can't use it unless it's released.
9931
+ // in that case, we need to stop the stream and start it again.
9932
+ await this.muteStream();
9687
9933
  await this.unmuteStream();
9688
9934
  }
9689
- }
9690
- else {
9691
- this.logger('warn', 'Camera direction ignored for desktop devices');
9935
+ throw error;
9692
9936
  }
9693
9937
  }
9694
9938
  /**
@@ -9773,7 +10017,7 @@ class CameraManager extends InputMediaDeviceManager {
9773
10017
  constraints.facingMode =
9774
10018
  this.state.direction === 'front' ? 'user' : 'environment';
9775
10019
  }
9776
- return getVideoStream(constraints);
10020
+ return getVideoStream(constraints, this.call.tracer);
9777
10021
  }
9778
10022
  }
9779
10023
 
@@ -9791,13 +10035,13 @@ class MicrophoneManagerState extends InputMediaDeviceManagerState {
9791
10035
  * This feature is not available in the React Native SDK.
9792
10036
  */
9793
10037
  get speakingWhileMuted() {
9794
- return this.getCurrentValue(this.speakingWhileMuted$);
10038
+ return getCurrentValue(this.speakingWhileMuted$);
9795
10039
  }
9796
10040
  /**
9797
10041
  * @internal
9798
10042
  */
9799
10043
  setSpeakingWhileMuted(isSpeaking) {
9800
- this.setCurrentValue(this.speakingWhileMutedSubject, isSpeaking);
10044
+ setCurrentValue(this.speakingWhileMutedSubject, isSpeaking);
9801
10045
  }
9802
10046
  getDeviceIdFromStream(stream) {
9803
10047
  const [track] = stream.getAudioTracks();
@@ -10184,7 +10428,7 @@ class MicrophoneManager extends InputMediaDeviceManager {
10184
10428
  return getAudioDevices();
10185
10429
  }
10186
10430
  getStream(constraints) {
10187
- return getAudioStream(constraints);
10431
+ return getAudioStream(constraints, this.call.tracer);
10188
10432
  }
10189
10433
  async startSpeakingWhileMutedDetection(deviceId) {
10190
10434
  await withoutConcurrency(this.soundDetectorConcurrencyTag, async () => {
@@ -10249,19 +10493,19 @@ class ScreenShareState extends InputMediaDeviceManagerState {
10249
10493
  * The current screen share audio status.
10250
10494
  */
10251
10495
  get audioEnabled() {
10252
- return this.getCurrentValue(this.audioEnabled$);
10496
+ return getCurrentValue(this.audioEnabled$);
10253
10497
  }
10254
10498
  /**
10255
10499
  * Set the current screen share audio status.
10256
10500
  */
10257
10501
  setAudioEnabled(isEnabled) {
10258
- this.setCurrentValue(this.audioEnabledSubject, isEnabled);
10502
+ setCurrentValue(this.audioEnabledSubject, isEnabled);
10259
10503
  }
10260
10504
  /**
10261
10505
  * The current screen share settings.
10262
10506
  */
10263
10507
  get settings() {
10264
- return this.getCurrentValue(this.settings$);
10508
+ return getCurrentValue(this.settings$);
10265
10509
  }
10266
10510
  /**
10267
10511
  * Set the current screen share settings.
@@ -10269,7 +10513,7 @@ class ScreenShareState extends InputMediaDeviceManagerState {
10269
10513
  * @param settings the screen share settings to set.
10270
10514
  */
10271
10515
  setSettings(settings) {
10272
- this.setCurrentValue(this.settingsSubject, settings);
10516
+ setCurrentValue(this.settingsSubject, settings);
10273
10517
  }
10274
10518
  }
10275
10519
 
@@ -10327,7 +10571,7 @@ class ScreenShareManager extends InputMediaDeviceManager {
10327
10571
  if (!this.state.audioEnabled) {
10328
10572
  constraints.audio = false;
10329
10573
  }
10330
- return getScreenShareStream(constraints);
10574
+ return getScreenShareStream(constraints, this.call.tracer);
10331
10575
  }
10332
10576
  async stopPublishStream() {
10333
10577
  return this.call.stopPublish(TrackType.SCREEN_SHARE, TrackType.SCREEN_SHARE_AUDIO);
@@ -10336,37 +10580,19 @@ class ScreenShareManager extends InputMediaDeviceManager {
10336
10580
  * Overrides the default `select` method to throw an error.
10337
10581
  */
10338
10582
  async select() {
10339
- throw new Error('This method is not supported in for Screen Share');
10583
+ throw new Error('Not supported');
10340
10584
  }
10341
10585
  }
10342
10586
 
10343
10587
  class SpeakerState {
10344
- constructor() {
10588
+ constructor(tracer) {
10345
10589
  this.selectedDeviceSubject = new rxjs.BehaviorSubject('');
10346
10590
  this.volumeSubject = new rxjs.BehaviorSubject(1);
10347
10591
  /**
10348
10592
  * [Tells if the browser supports audio output change on 'audio' elements](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/setSinkId).
10349
10593
  */
10350
10594
  this.isDeviceSelectionSupported = checkIfAudioOutputChangeSupported();
10351
- /**
10352
- * Gets the current value of an observable, or undefined if the observable has
10353
- * not emitted a value yet.
10354
- *
10355
- * @param observable$ the observable to get the value from.
10356
- */
10357
- this.getCurrentValue = getCurrentValue;
10358
- /**
10359
- * Updates the value of the provided Subject.
10360
- * An `update` can either be a new value or a function which takes
10361
- * the current value and returns a new value.
10362
- *
10363
- * @internal
10364
- *
10365
- * @param subject the subject to update.
10366
- * @param update the update to apply to the subject.
10367
- * @return the updated value.
10368
- */
10369
- this.setCurrentValue = setCurrentValue;
10595
+ this.tracer = tracer;
10370
10596
  this.selectedDevice$ = this.selectedDeviceSubject
10371
10597
  .asObservable()
10372
10598
  .pipe(rxjs.distinctUntilChanged());
@@ -10380,7 +10606,7 @@ class SpeakerState {
10380
10606
  * Note: this feature is not supported in React Native
10381
10607
  */
10382
10608
  get selectedDevice() {
10383
- return this.getCurrentValue(this.selectedDevice$);
10609
+ return getCurrentValue(this.selectedDevice$);
10384
10610
  }
10385
10611
  /**
10386
10612
  * The currently selected volume
@@ -10388,27 +10614,27 @@ class SpeakerState {
10388
10614
  * Note: this feature is not supported in React Native
10389
10615
  */
10390
10616
  get volume() {
10391
- return this.getCurrentValue(this.volume$);
10617
+ return getCurrentValue(this.volume$);
10392
10618
  }
10393
10619
  /**
10394
10620
  * @internal
10395
10621
  * @param deviceId
10396
10622
  */
10397
10623
  setDevice(deviceId) {
10398
- this.setCurrentValue(this.selectedDeviceSubject, deviceId);
10624
+ setCurrentValue(this.selectedDeviceSubject, deviceId);
10625
+ this.tracer.trace('navigator.mediaDevices.setSinkId', deviceId);
10399
10626
  }
10400
10627
  /**
10401
10628
  * @internal
10402
10629
  * @param volume
10403
10630
  */
10404
10631
  setVolume(volume) {
10405
- this.setCurrentValue(this.volumeSubject, volume);
10632
+ setCurrentValue(this.volumeSubject, volume);
10406
10633
  }
10407
10634
  }
10408
10635
 
10409
10636
  class SpeakerManager {
10410
10637
  constructor(call) {
10411
- this.state = new SpeakerState();
10412
10638
  this.subscriptions = [];
10413
10639
  /**
10414
10640
  * Disposes the manager.
@@ -10419,6 +10645,7 @@ class SpeakerManager {
10419
10645
  this.subscriptions.forEach((s) => s.unsubscribe());
10420
10646
  };
10421
10647
  this.call = call;
10648
+ this.state = new SpeakerState(call.tracer);
10422
10649
  if (deviceIds$ && !isReactNative()) {
10423
10650
  this.subscriptions.push(rxjs.combineLatest([deviceIds$, this.state.selectedDevice$]).subscribe(([devices, deviceId]) => {
10424
10651
  if (!deviceId) {
@@ -10512,6 +10739,7 @@ class Call {
10512
10739
  * The permissions context of this call.
10513
10740
  */
10514
10741
  this.permissionsContext = new PermissionsContext();
10742
+ this.tracer = new Tracer(null);
10515
10743
  /**
10516
10744
  * The event dispatcher instance dedicated to this Call instance.
10517
10745
  * @private
@@ -10783,6 +11011,7 @@ class Call {
10783
11011
  this.leaveCallHooks.forEach((hook) => hook());
10784
11012
  this.initialized = false;
10785
11013
  this.hasJoinedOnce = false;
11014
+ this.unifiedSessionId = undefined;
10786
11015
  this.ringingSubject.next(false);
10787
11016
  this.cancelAutoDrop();
10788
11017
  this.clientStore.unregisterCall(this);
@@ -11210,7 +11439,6 @@ class Call {
11210
11439
  state: this.state,
11211
11440
  connectionConfig,
11212
11441
  logTag: String(this.sfuClientTag),
11213
- clientDetails,
11214
11442
  enableTracing,
11215
11443
  onUnrecoverableError: (reason) => {
11216
11444
  this.reconnect(WebsocketReconnectStrategy.REJOIN, reason).catch((err) => {
@@ -11232,7 +11460,6 @@ class Call {
11232
11460
  connectionConfig,
11233
11461
  publishOptions,
11234
11462
  logTag: String(this.sfuClientTag),
11235
- clientDetails,
11236
11463
  enableTracing,
11237
11464
  onUnrecoverableError: (reason) => {
11238
11465
  this.reconnect(WebsocketReconnectStrategy.REJOIN, reason).catch((err) => {
@@ -11241,7 +11468,6 @@ class Call {
11241
11468
  },
11242
11469
  });
11243
11470
  }
11244
- tracer.setEnabled(enableTracing);
11245
11471
  this.statsReporter?.stop();
11246
11472
  this.statsReporter = createStatsReporter({
11247
11473
  subscriber: this.subscriber,
@@ -11249,8 +11475,10 @@ class Call {
11249
11475
  state: this.state,
11250
11476
  datacenter: sfuClient.edgeName,
11251
11477
  });
11478
+ this.tracer.setEnabled(enableTracing);
11252
11479
  this.sfuStatsReporter?.stop();
11253
11480
  if (statsOptions?.reporting_interval_ms > 0) {
11481
+ this.unifiedSessionId ?? (this.unifiedSessionId = sfuClient.sessionId);
11254
11482
  this.sfuStatsReporter = new SfuStatsReporter(sfuClient, {
11255
11483
  clientDetails,
11256
11484
  options: statsOptions,
@@ -11259,6 +11487,8 @@ class Call {
11259
11487
  microphone: this.microphone,
11260
11488
  camera: this.camera,
11261
11489
  state: this.state,
11490
+ tracer: this.tracer,
11491
+ unifiedSessionId: this.unifiedSessionId,
11262
11492
  });
11263
11493
  this.sfuStatsReporter.start();
11264
11494
  }
@@ -11491,6 +11721,7 @@ class Call {
11491
11721
  }
11492
11722
  });
11493
11723
  const unregisterNetworkChanged = this.streamClient.on('network.changed', (e) => {
11724
+ this.tracer.trace('network.changed', e);
11494
11725
  if (!e.online) {
11495
11726
  this.logger('debug', '[Reconnect] Going offline');
11496
11727
  if (!this.hasJoinedOnce)
@@ -11628,6 +11859,11 @@ class Call {
11628
11859
  trackTypes.push(TrackType.SCREEN_SHARE_AUDIO);
11629
11860
  }
11630
11861
  }
11862
+ if (track.kind === 'video') {
11863
+ // schedules calibration report - the SFU will use the performance stats
11864
+ // to adjust the quality thresholds as early as possible
11865
+ this.sfuStatsReporter?.scheduleOne(3000);
11866
+ }
11631
11867
  await this.updateLocalStreamState(mediaStream, ...trackTypes);
11632
11868
  };
11633
11869
  /**
@@ -13487,7 +13723,7 @@ class StreamClient {
13487
13723
  this.getUserAgent = () => {
13488
13724
  if (!this.cachedUserAgent) {
13489
13725
  const { clientAppIdentifier = {} } = this.options;
13490
- const { sdkName = 'js', sdkVersion = "1.20.2", ...extras } = clientAppIdentifier;
13726
+ const { sdkName = 'js', sdkVersion = "1.22.0", ...extras } = clientAppIdentifier;
13491
13727
  this.cachedUserAgent = [
13492
13728
  `stream-video-${sdkName}-v${sdkVersion}`,
13493
13729
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),