@stream-io/video-client 1.20.2 → 1.21.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 (50) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/index.browser.es.js +389 -162
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +389 -162
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.es.js +389 -162
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/src/Call.d.ts +1 -0
  9. package/dist/src/StreamSfuClient.d.ts +1 -1
  10. package/dist/src/devices/InputMediaDeviceManagerState.d.ts +0 -20
  11. package/dist/src/devices/SpeakerState.d.ts +0 -20
  12. package/dist/src/gen/video/sfu/models/models.d.ts +50 -0
  13. package/dist/src/gen/video/sfu/signal_rpc/signal.d.ts +29 -3
  14. package/dist/src/rpc/createClient.d.ts +1 -1
  15. package/dist/src/rtc/BasePeerConnection.d.ts +12 -7
  16. package/dist/src/rtc/Publisher.d.ts +4 -4
  17. package/dist/src/stats/CallStateStatsReporter.d.ts +5 -3
  18. package/dist/src/stats/SfuStatsReporter.d.ts +5 -1
  19. package/dist/src/stats/rtc/StatsTracer.d.ts +54 -0
  20. package/dist/src/stats/rtc/Tracer.d.ts +1 -5
  21. package/dist/src/stats/rtc/index.d.ts +2 -0
  22. package/dist/src/stats/rtc/types.d.ts +19 -0
  23. package/dist/src/stats/types.d.ts +13 -0
  24. package/package.json +1 -1
  25. package/src/Call.ts +10 -2
  26. package/src/StreamSfuClient.ts +30 -18
  27. package/src/devices/CameraManagerState.ts +3 -2
  28. package/src/devices/InputMediaDeviceManagerState.ts +10 -31
  29. package/src/devices/MicrophoneManagerState.ts +3 -2
  30. package/src/devices/ScreenShareState.ts +5 -4
  31. package/src/devices/SpeakerState.ts +7 -26
  32. package/src/gen/video/sfu/models/models.ts +84 -0
  33. package/src/gen/video/sfu/signal_rpc/signal.ts +50 -2
  34. package/src/helpers/DynascaleManager.ts +0 -9
  35. package/src/rpc/createClient.ts +1 -1
  36. package/src/rtc/BasePeerConnection.ts +37 -15
  37. package/src/rtc/Publisher.ts +17 -15
  38. package/src/rtc/Subscriber.ts +2 -0
  39. package/src/rtc/__tests__/Publisher.test.ts +19 -0
  40. package/src/rtc/__tests__/Subscriber.test.ts +15 -1
  41. package/src/stats/CallStateStatsReporter.ts +19 -31
  42. package/src/stats/SfuStatsReporter.ts +37 -12
  43. package/src/stats/rtc/StatsTracer.ts +289 -0
  44. package/src/stats/rtc/Tracer.ts +1 -6
  45. package/src/stats/rtc/__tests__/Tracer.test.ts +57 -0
  46. package/src/stats/rtc/index.ts +3 -0
  47. package/src/stats/rtc/mediaDevices.ts +4 -3
  48. package/src/stats/rtc/pc.ts +6 -76
  49. package/src/stats/rtc/types.ts +22 -0
  50. package/src/stats/types.ts +15 -0
package/dist/index.es.js CHANGED
@@ -61,16 +61,17 @@ if (typeof navigator !== 'undefined' &&
61
61
  const original = target[method];
62
62
  if (!original)
63
63
  continue;
64
+ let mark = 0;
64
65
  target[method] = async function tracedMethod(constraints) {
65
- const tag = `navigator.mediaDevices.${method}`;
66
+ const tag = `navigator.mediaDevices.${method}.${mark++}`;
66
67
  trace(tag, constraints);
67
68
  try {
68
69
  const stream = await original.call(target, constraints);
69
- trace(`${tag}OnSuccess`, dumpStream(stream));
70
+ trace(`${tag}.OnSuccess`, dumpStream(stream));
70
71
  return stream;
71
72
  }
72
73
  catch (err) {
73
- trace(`${tag}OnFailure`, err.name);
74
+ trace(`${tag}.OnFailure`, err.name);
74
75
  throw err;
75
76
  }
76
77
  };
@@ -1795,6 +1796,47 @@ class AppleState$Type extends MessageType {
1795
1796
  * @generated MessageType for protobuf message stream.video.sfu.models.AppleState
1796
1797
  */
1797
1798
  const AppleState = new AppleState$Type();
1799
+ // @generated message type with reflection information, may provide speed optimized methods
1800
+ class PerformanceStats$Type extends MessageType {
1801
+ constructor() {
1802
+ super('stream.video.sfu.models.PerformanceStats', [
1803
+ {
1804
+ no: 1,
1805
+ name: 'track_type',
1806
+ kind: 'enum',
1807
+ T: () => [
1808
+ 'stream.video.sfu.models.TrackType',
1809
+ TrackType,
1810
+ 'TRACK_TYPE_',
1811
+ ],
1812
+ },
1813
+ { no: 2, name: 'codec', kind: 'message', T: () => Codec },
1814
+ {
1815
+ no: 3,
1816
+ name: 'avg_frame_time_ms',
1817
+ kind: 'scalar',
1818
+ T: 2 /*ScalarType.FLOAT*/,
1819
+ },
1820
+ { no: 4, name: 'avg_fps', kind: 'scalar', T: 2 /*ScalarType.FLOAT*/ },
1821
+ {
1822
+ no: 5,
1823
+ name: 'video_dimension',
1824
+ kind: 'message',
1825
+ T: () => VideoDimension,
1826
+ },
1827
+ {
1828
+ no: 6,
1829
+ name: 'target_bitrate',
1830
+ kind: 'scalar',
1831
+ T: 5 /*ScalarType.INT32*/,
1832
+ },
1833
+ ]);
1834
+ }
1835
+ }
1836
+ /**
1837
+ * @generated MessageType for protobuf message stream.video.sfu.models.PerformanceStats
1838
+ */
1839
+ const PerformanceStats = new PerformanceStats$Type();
1798
1840
 
1799
1841
  var models = /*#__PURE__*/Object.freeze({
1800
1842
  __proto__: null,
@@ -1820,6 +1862,7 @@ var models = /*#__PURE__*/Object.freeze({
1820
1862
  Participant: Participant,
1821
1863
  ParticipantCount: ParticipantCount,
1822
1864
  get PeerType () { return PeerType; },
1865
+ PerformanceStats: PerformanceStats,
1823
1866
  Pin: Pin,
1824
1867
  PublishOption: PublishOption,
1825
1868
  RTMPIngress: RTMPIngress,
@@ -1999,6 +2042,27 @@ class SendStatsRequest$Type extends MessageType {
1999
2042
  kind: 'scalar',
2000
2043
  T: 9 /*ScalarType.STRING*/,
2001
2044
  },
2045
+ { no: 15, name: 'rtc_stats', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
2046
+ {
2047
+ no: 16,
2048
+ name: 'encode_stats',
2049
+ kind: 'message',
2050
+ repeat: 1 /*RepeatType.PACKED*/,
2051
+ T: () => PerformanceStats,
2052
+ },
2053
+ {
2054
+ no: 17,
2055
+ name: 'decode_stats',
2056
+ kind: 'message',
2057
+ repeat: 1 /*RepeatType.PACKED*/,
2058
+ T: () => PerformanceStats,
2059
+ },
2060
+ {
2061
+ no: 18,
2062
+ name: 'unified_session_id',
2063
+ kind: 'scalar',
2064
+ T: 9 /*ScalarType.STRING*/,
2065
+ },
2002
2066
  ]);
2003
2067
  }
2004
2068
  }
@@ -5470,30 +5534,17 @@ const createStatsReporter = ({ subscriber, publisher, state, datacenter, polling
5470
5534
  }
5471
5535
  }
5472
5536
  }
5473
- const [subscriberStats, publisherStats] = await Promise.all([
5474
- subscriber
5475
- .getStats()
5476
- .then((report) => transform(report, {
5477
- kind: 'subscriber',
5478
- trackKind: 'video',
5479
- publisher,
5480
- }))
5481
- .then(aggregate),
5482
- publisher
5483
- ? publisher
5484
- .getStats()
5485
- .then((report) => transform(report, {
5486
- kind: 'publisher',
5487
- trackKind: 'video',
5488
- publisher,
5489
- }))
5490
- .then(aggregate)
5491
- : getEmptyStats(),
5492
- ]);
5493
5537
  const [subscriberRawStats, publisherRawStats] = await Promise.all([
5494
5538
  getRawStatsForTrack('subscriber'),
5495
5539
  publisher ? getRawStatsForTrack('publisher') : undefined,
5496
5540
  ]);
5541
+ const process = (report, kind) => aggregate(transform(report, { kind, trackKind: 'video', publisher }));
5542
+ const subscriberStats = subscriberRawStats
5543
+ ? process(subscriberRawStats, 'subscriber')
5544
+ : getEmptyStats();
5545
+ const publisherStats = publisherRawStats
5546
+ ? process(publisherRawStats, 'publisher')
5547
+ : getEmptyStats();
5497
5548
  state.setCallStatsReport({
5498
5549
  datacenter,
5499
5550
  publisherStats,
@@ -5651,7 +5702,7 @@ const aggregate = (stats) => {
5651
5702
  return report;
5652
5703
  };
5653
5704
 
5654
- const version = "1.20.2";
5705
+ const version = "1.21.0";
5655
5706
  const [major, minor, patch] = version.split('.');
5656
5707
  let sdkInfo = {
5657
5708
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -5788,7 +5839,7 @@ const getClientDetails = async () => {
5788
5839
  };
5789
5840
 
5790
5841
  class SfuStatsReporter {
5791
- constructor(sfuClient, { options, clientDetails, subscriber, publisher, microphone, camera, state, }) {
5842
+ constructor(sfuClient, { options, clientDetails, subscriber, publisher, microphone, camera, state, unifiedSessionId, }) {
5792
5843
  this.logger = getLogger(['SfuStatsReporter']);
5793
5844
  this.inputDevices = new Map();
5794
5845
  this.observeDevice = (device, kind) => {
@@ -5845,31 +5896,40 @@ class SfuStatsReporter {
5845
5896
  };
5846
5897
  this.run = async (telemetry) => {
5847
5898
  const [subscriberStats, publisherStats] = await Promise.all([
5848
- this.subscriber.getStats().then(flatten).then(JSON.stringify),
5849
- this.publisher?.getStats().then(flatten).then(JSON.stringify) ?? '[]',
5899
+ this.subscriber.stats.get(),
5900
+ this.publisher?.stats.get(),
5850
5901
  ]);
5851
- const subscriberTrace = this.subscriber.getTrace();
5852
- const publisherTrace = this.publisher?.getTrace();
5902
+ this.subscriber.tracer?.trace('getstats', subscriberStats.delta);
5903
+ if (publisherStats) {
5904
+ this.publisher?.tracer?.trace('getstats', publisherStats.delta);
5905
+ }
5906
+ const subscriberTrace = this.subscriber.tracer?.take();
5907
+ const publisherTrace = this.publisher?.tracer?.take();
5853
5908
  const mediaTrace = tracer.take();
5854
5909
  const sfuTrace = this.sfuClient.getTrace();
5855
- const publisherTraces = [
5910
+ const traces = [
5856
5911
  ...mediaTrace.snapshot,
5857
5912
  ...(sfuTrace?.snapshot ?? []),
5858
5913
  ...(publisherTrace?.snapshot ?? []),
5914
+ ...(subscriberTrace?.snapshot ?? []),
5859
5915
  ];
5860
5916
  try {
5861
5917
  await this.sfuClient.sendStats({
5862
5918
  sdk: this.sdkName,
5863
5919
  sdkVersion: this.sdkVersion,
5864
5920
  webrtcVersion: this.webRTCVersion,
5865
- subscriberStats,
5866
- subscriberRtcStats: subscriberTrace
5867
- ? JSON.stringify(subscriberTrace.snapshot)
5868
- : '',
5869
- publisherStats,
5870
- publisherRtcStats: publisherTraces.length > 0 ? JSON.stringify(publisherTraces) : '',
5921
+ subscriberStats: JSON.stringify(flatten(subscriberStats.stats)),
5922
+ publisherStats: publisherStats
5923
+ ? JSON.stringify(flatten(publisherStats.stats))
5924
+ : '[]',
5925
+ subscriberRtcStats: '',
5926
+ publisherRtcStats: '',
5927
+ rtcStats: JSON.stringify(traces),
5928
+ encodeStats: publisherStats?.performanceStats ?? [],
5929
+ decodeStats: subscriberStats.performanceStats,
5871
5930
  audioDevices: this.inputDevices.get('mic'),
5872
5931
  videoDevices: this.inputDevices.get('camera'),
5932
+ unifiedSessionId: this.unifiedSessionId,
5873
5933
  deviceState: getDeviceState(),
5874
5934
  telemetry,
5875
5935
  });
@@ -5902,6 +5962,16 @@ class SfuStatsReporter {
5902
5962
  this.inputDevices.clear();
5903
5963
  clearInterval(this.intervalId);
5904
5964
  this.intervalId = undefined;
5965
+ clearTimeout(this.timeoutId);
5966
+ this.timeoutId = undefined;
5967
+ };
5968
+ this.scheduleOne = (timeout) => {
5969
+ clearTimeout(this.timeoutId);
5970
+ this.timeoutId = setTimeout(() => {
5971
+ this.run().catch((err) => {
5972
+ this.logger('warn', 'Failed to report stats', err);
5973
+ });
5974
+ }, timeout);
5905
5975
  };
5906
5976
  this.sfuClient = sfuClient;
5907
5977
  this.options = options;
@@ -5910,6 +5980,7 @@ class SfuStatsReporter {
5910
5980
  this.microphone = microphone;
5911
5981
  this.camera = camera;
5912
5982
  this.state = state;
5983
+ this.unifiedSessionId = unifiedSessionId;
5913
5984
  const { sdk, browser } = clientDetails;
5914
5985
  this.sdkName = getSdkName(sdk);
5915
5986
  this.sdkVersion = getSdkVersion(sdk);
@@ -5932,47 +6003,25 @@ const traceRTCPeerConnection = (pc, trace) => {
5932
6003
  trace('ontrack', `${e.track.kind}:${e.track.id} ${streams}`);
5933
6004
  });
5934
6005
  pc.addEventListener('signalingstatechange', () => {
5935
- trace('onsignalingstatechange', pc.signalingState);
6006
+ trace('signalingstatechange', pc.signalingState);
5936
6007
  });
5937
6008
  pc.addEventListener('iceconnectionstatechange', () => {
5938
- trace('oniceconnectionstatechange', pc.iceConnectionState);
6009
+ trace('iceconnectionstatechange', pc.iceConnectionState);
5939
6010
  });
5940
6011
  pc.addEventListener('icegatheringstatechange', () => {
5941
- trace('onicegatheringstatechange', pc.iceGatheringState);
6012
+ trace('icegatheringstatechange', pc.iceGatheringState);
5942
6013
  });
5943
6014
  pc.addEventListener('connectionstatechange', () => {
5944
- trace('onconnectionstatechange', pc.connectionState);
6015
+ trace('connectionstatechange', pc.connectionState);
5945
6016
  });
5946
6017
  pc.addEventListener('negotiationneeded', () => {
5947
- trace('onnegotiationneeded', undefined);
6018
+ trace('negotiationneeded', undefined);
5948
6019
  });
5949
6020
  pc.addEventListener('datachannel', ({ channel }) => {
5950
- trace('ondatachannel', [channel.id, channel.label]);
5951
- });
5952
- let prev = {};
5953
- const getStats = () => {
5954
- pc.getStats(null)
5955
- .then((stats) => {
5956
- const now = toObject(stats);
5957
- trace('getstats', deltaCompression(prev, now));
5958
- prev = now;
5959
- })
5960
- .catch((err) => {
5961
- trace('getstatsOnFailure', err.toString());
5962
- });
5963
- };
5964
- const interval = setInterval(() => {
5965
- getStats();
5966
- }, 8000);
5967
- pc.addEventListener('connectionstatechange', () => {
5968
- const state = pc.connectionState;
5969
- if (state === 'connected' || state === 'failed') {
5970
- getStats();
5971
- }
6021
+ trace('datachannel', [channel.id, channel.label]);
5972
6022
  });
5973
6023
  const origClose = pc.close;
5974
6024
  pc.close = function tracedClose() {
5975
- clearInterval(interval);
5976
6025
  trace('close', undefined);
5977
6026
  return origClose.call(this);
5978
6027
  };
@@ -6002,9 +6051,162 @@ const traceRTCPeerConnection = (pc, trace) => {
6002
6051
  };
6003
6052
  }
6004
6053
  };
6005
- const toObject = (s) => {
6054
+
6055
+ /**
6056
+ * StatsTracer is a class that collects and processes WebRTC stats.
6057
+ * It is used to track the performance of the WebRTC connection
6058
+ * and to provide information about the media streams.
6059
+ * It is used by both the Publisher and Subscriber classes.
6060
+ *
6061
+ * @internal
6062
+ */
6063
+ class StatsTracer {
6064
+ /**
6065
+ * Creates a new StatsTracer instance.
6066
+ */
6067
+ constructor(pc, peerType, trackIdToTrackType) {
6068
+ this.previousStats = {};
6069
+ this.frameTimeHistory = [];
6070
+ this.fpsHistory = [];
6071
+ /**
6072
+ * Get the stats from the RTCPeerConnection.
6073
+ * When called, it will return the stats for the current connection.
6074
+ * It will also return the delta between the current stats and the previous stats.
6075
+ * This is used to track the performance of the connection.
6076
+ *
6077
+ * @internal
6078
+ */
6079
+ this.get = async () => {
6080
+ const stats = await this.pc.getStats();
6081
+ const currentStats = toObject(stats);
6082
+ const performanceStats = this.withOverrides(this.peerType === PeerType.SUBSCRIBER
6083
+ ? this.getDecodeStats(currentStats)
6084
+ : this.getEncodeStats(currentStats));
6085
+ const delta = deltaCompression(this.previousStats, currentStats);
6086
+ // store the current data for the next iteration
6087
+ this.previousStats = currentStats;
6088
+ this.frameTimeHistory = this.frameTimeHistory.slice(-2);
6089
+ this.fpsHistory = this.fpsHistory.slice(-2);
6090
+ return { performanceStats, delta, stats };
6091
+ };
6092
+ /**
6093
+ * Collects encode stats from the RTCPeerConnection.
6094
+ */
6095
+ this.getEncodeStats = (currentStats) => {
6096
+ const encodeStats = [];
6097
+ for (const rtp of Object.values(currentStats)) {
6098
+ if (rtp.type !== 'outbound-rtp')
6099
+ continue;
6100
+ const { codecId, framesSent = 0, kind, id, totalEncodeTime = 0, framesPerSecond = 0, frameHeight = 0, frameWidth = 0, targetBitrate = 0, mediaSourceId, } = rtp;
6101
+ if (kind === 'audio' || !this.previousStats[id])
6102
+ continue;
6103
+ const prevRtp = this.previousStats[id];
6104
+ const deltaTotalEncodeTime = totalEncodeTime - (prevRtp.totalEncodeTime || 0);
6105
+ const deltaFramesSent = framesSent - (prevRtp.framesSent || 0);
6106
+ const framesEncodeTime = deltaFramesSent > 0
6107
+ ? (deltaTotalEncodeTime / deltaFramesSent) * 1000
6108
+ : 0;
6109
+ this.frameTimeHistory.push(framesEncodeTime);
6110
+ this.fpsHistory.push(framesPerSecond);
6111
+ let trackType = TrackType.VIDEO;
6112
+ if (mediaSourceId && currentStats[mediaSourceId]) {
6113
+ const mediaSource = currentStats[mediaSourceId];
6114
+ trackType =
6115
+ this.trackIdToTrackType.get(mediaSource.trackIdentifier) || trackType;
6116
+ }
6117
+ encodeStats.push({
6118
+ trackType,
6119
+ codec: getCodecFromStats(currentStats, codecId),
6120
+ avgFrameTimeMs: average(this.frameTimeHistory),
6121
+ avgFps: average(this.fpsHistory),
6122
+ targetBitrate: Math.round(targetBitrate),
6123
+ videoDimension: { width: frameWidth, height: frameHeight },
6124
+ });
6125
+ }
6126
+ return encodeStats;
6127
+ };
6128
+ /**
6129
+ * Collects decode stats from the RTCPeerConnection.
6130
+ */
6131
+ this.getDecodeStats = (currentStats) => {
6132
+ let rtp = undefined;
6133
+ let max = 0;
6134
+ for (const item of Object.values(currentStats)) {
6135
+ if (item.type !== 'inbound-rtp')
6136
+ continue;
6137
+ const rtpItem = item;
6138
+ const { kind, frameWidth = 0, frameHeight = 0 } = rtpItem;
6139
+ const area = frameWidth * frameHeight;
6140
+ if (kind === 'video' && area > max) {
6141
+ rtp = rtpItem;
6142
+ max = area;
6143
+ }
6144
+ }
6145
+ if (!rtp || !this.previousStats[rtp.id])
6146
+ return [];
6147
+ const prevRtp = this.previousStats[rtp.id];
6148
+ const { framesDecoded = 0, framesPerSecond = 0, totalDecodeTime = 0, trackIdentifier, } = rtp;
6149
+ const deltaTotalDecodeTime = totalDecodeTime - (prevRtp.totalDecodeTime || 0);
6150
+ const deltaFramesDecoded = framesDecoded - (prevRtp.framesDecoded || 0);
6151
+ const framesDecodeTime = deltaFramesDecoded > 0
6152
+ ? (deltaTotalDecodeTime / deltaFramesDecoded) * 1000
6153
+ : 0;
6154
+ this.frameTimeHistory.push(framesDecodeTime);
6155
+ this.fpsHistory.push(framesPerSecond);
6156
+ const trackType = this.trackIdToTrackType.get(trackIdentifier) || TrackType.VIDEO;
6157
+ return [
6158
+ PerformanceStats.create({
6159
+ trackType,
6160
+ codec: getCodecFromStats(currentStats, rtp.codecId),
6161
+ avgFrameTimeMs: average(this.frameTimeHistory),
6162
+ avgFps: average(this.fpsHistory),
6163
+ videoDimension: { width: rtp.frameWidth, height: rtp.frameHeight },
6164
+ }),
6165
+ ];
6166
+ };
6167
+ /**
6168
+ * Applies cost overrides to the performance stats.
6169
+ * This is used to override the default encode/decode times with custom values.
6170
+ * This is useful for testing and debugging purposes, and it shouldn't be used in production.
6171
+ */
6172
+ this.withOverrides = (performanceStats) => {
6173
+ if (this.costOverrides) {
6174
+ for (const s of performanceStats) {
6175
+ const override = this.costOverrides.get(s.trackType);
6176
+ if (override !== undefined) {
6177
+ // override the average encode/decode time with the provided cost.
6178
+ // format: [override].[original-frame-time]
6179
+ s.avgFrameTimeMs = override + (s.avgFrameTimeMs || 0) / 1000;
6180
+ }
6181
+ }
6182
+ }
6183
+ return performanceStats;
6184
+ };
6185
+ /**
6186
+ * Set the encode/decode cost for a specific track type.
6187
+ * This is used to override the default encode/decode times with custom values.
6188
+ * This is useful for testing and debugging purposes, and it shouldn't be used in production.
6189
+ *
6190
+ * @internal
6191
+ */
6192
+ this.setCost = (cost, trackType = TrackType.VIDEO) => {
6193
+ if (!this.costOverrides)
6194
+ this.costOverrides = new Map();
6195
+ this.costOverrides.set(trackType, cost);
6196
+ };
6197
+ this.pc = pc;
6198
+ this.peerType = peerType;
6199
+ this.trackIdToTrackType = trackIdToTrackType;
6200
+ }
6201
+ }
6202
+ /**
6203
+ * Convert the stat report to an object.
6204
+ *
6205
+ * @param report the stat report to convert.
6206
+ */
6207
+ const toObject = (report) => {
6006
6208
  const obj = {};
6007
- s.forEach((v, k) => {
6209
+ report.forEach((v, k) => {
6008
6210
  obj[k] = v;
6009
6211
  });
6010
6212
  return obj;
@@ -6027,12 +6229,13 @@ const deltaCompression = (oldStats, newStats) => {
6027
6229
  }
6028
6230
  }
6029
6231
  let timestamp = -Infinity;
6030
- for (const report of Object.values(newStats)) {
6232
+ const values = Object.values(newStats);
6233
+ for (const report of values) {
6031
6234
  if (report.timestamp > timestamp) {
6032
6235
  timestamp = report.timestamp;
6033
6236
  }
6034
6237
  }
6035
- for (const report of Object.values(newStats)) {
6238
+ for (const report of values) {
6036
6239
  if (report.timestamp === timestamp) {
6037
6240
  report.timestamp = 0;
6038
6241
  }
@@ -6040,6 +6243,27 @@ const deltaCompression = (oldStats, newStats) => {
6040
6243
  newStats.timestamp = timestamp;
6041
6244
  return newStats;
6042
6245
  };
6246
+ /**
6247
+ * Calculates an average value.
6248
+ */
6249
+ const average = (arr) => arr.reduce((a, b) => a + b, 0) / arr.length;
6250
+ /**
6251
+ * Create a Codec object from the codec stats.
6252
+ *
6253
+ * @param stats the stats report.
6254
+ * @param codecId the codec ID to look for.
6255
+ */
6256
+ const getCodecFromStats = (stats, codecId) => {
6257
+ if (!codecId || !stats[codecId])
6258
+ return;
6259
+ const codecStats = stats[codecId];
6260
+ return Codec.create({
6261
+ name: codecStats.mimeType.split('/').pop(), // video/av1 -> av1
6262
+ clockRate: codecStats.clockRate,
6263
+ payloadType: codecStats.payloadType,
6264
+ fmtp: codecStats.sdpFmtpLine,
6265
+ });
6266
+ };
6043
6267
 
6044
6268
  /**
6045
6269
  * A base class for the `Publisher` and `Subscriber` classes.
@@ -6049,9 +6273,10 @@ class BasePeerConnection {
6049
6273
  /**
6050
6274
  * Constructs a new `BasePeerConnection` instance.
6051
6275
  */
6052
- constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onUnrecoverableError, logTag, clientDetails, enableTracing, }) {
6276
+ constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onUnrecoverableError, logTag, enableTracing, }) {
6053
6277
  this.isIceRestarting = false;
6054
6278
  this.isDisposed = false;
6279
+ this.trackIdToTrackType = new Map();
6055
6280
  this.subscriptions = [];
6056
6281
  /**
6057
6282
  * Handles events synchronously.
@@ -6099,10 +6324,10 @@ class BasePeerConnection {
6099
6324
  return this.pc.getStats(selector);
6100
6325
  };
6101
6326
  /**
6102
- * Returns the current tracing buffer.
6327
+ * Maps the given track ID to the corresponding track type.
6103
6328
  */
6104
- this.getTrace = () => {
6105
- return this.tracer?.take();
6329
+ this.getTrackType = (trackId) => {
6330
+ return this.trackIdToTrackType.get(trackId);
6106
6331
  };
6107
6332
  /**
6108
6333
  * Handles the ICECandidate event and
@@ -6136,6 +6361,24 @@ class BasePeerConnection {
6136
6361
  }
6137
6362
  return JSON.stringify(candidate.toJSON());
6138
6363
  };
6364
+ /**
6365
+ * Handles the ConnectionStateChange event.
6366
+ */
6367
+ this.onConnectionStateChange = async () => {
6368
+ const state = this.pc.connectionState;
6369
+ this.logger('debug', `Connection state changed`, state);
6370
+ if (!this.tracer)
6371
+ return;
6372
+ if (state === 'connected' || state === 'failed') {
6373
+ try {
6374
+ const stats = await this.stats.get();
6375
+ this.tracer.trace('getstats', stats.delta);
6376
+ }
6377
+ catch (err) {
6378
+ this.tracer.trace('getstatsOnFailure', err.toString());
6379
+ }
6380
+ }
6381
+ };
6139
6382
  /**
6140
6383
  * Handles the ICE connection state change event.
6141
6384
  */
@@ -6192,18 +6435,19 @@ class BasePeerConnection {
6192
6435
  logTag,
6193
6436
  ]);
6194
6437
  this.pc = new RTCPeerConnection(connectionConfig);
6195
- if (enableTracing) {
6196
- const tag = `${logTag}-${peerType === PeerType.SUBSCRIBER ? 'sub' : 'pub'}`;
6197
- this.tracer = new Tracer(tag);
6198
- this.tracer.trace('clientDetails', clientDetails);
6199
- this.tracer.trace('create', connectionConfig);
6200
- traceRTCPeerConnection(this.pc, this.tracer.trace);
6201
- }
6202
6438
  this.pc.addEventListener('icecandidate', this.onIceCandidate);
6203
6439
  this.pc.addEventListener('icecandidateerror', this.onIceCandidateError);
6204
6440
  this.pc.addEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
6205
6441
  this.pc.addEventListener('icegatheringstatechange', this.onIceGatherChange);
6206
6442
  this.pc.addEventListener('signalingstatechange', this.onSignalingChange);
6443
+ this.pc.addEventListener('connectionstatechange', this.onConnectionStateChange);
6444
+ this.stats = new StatsTracer(this.pc, peerType, this.trackIdToTrackType);
6445
+ if (enableTracing) {
6446
+ const tag = `${logTag}-${peerType === PeerType.SUBSCRIBER ? 'sub' : 'pub'}-${sfuClient.edgeName}`;
6447
+ this.tracer = new Tracer(tag);
6448
+ this.tracer.trace('create', connectionConfig);
6449
+ traceRTCPeerConnection(this.pc, this.tracer.trace);
6450
+ }
6207
6451
  }
6208
6452
  /**
6209
6453
  * Disposes the `RTCPeerConnection` instance.
@@ -6569,7 +6813,7 @@ class Publisher extends BasePeerConnection {
6569
6813
  }
6570
6814
  else {
6571
6815
  const previousTrack = transceiver.sender.track;
6572
- await transceiver.sender.replaceTrack(trackToPublish);
6816
+ await this.updateTransceiver(transceiver, trackToPublish, trackType);
6573
6817
  if (!isReactNative()) {
6574
6818
  this.stopTrack(previousTrack);
6575
6819
  }
@@ -6591,8 +6835,20 @@ class Publisher extends BasePeerConnection {
6591
6835
  const trackType = publishOption.trackType;
6592
6836
  this.logger('debug', `Added ${TrackType[trackType]} transceiver`);
6593
6837
  this.transceiverCache.add(publishOption, transceiver);
6838
+ this.trackIdToTrackType.set(track.id, trackType);
6594
6839
  await this.negotiate();
6595
6840
  };
6841
+ /**
6842
+ * Updates the transceiver with the given track and track type.
6843
+ */
6844
+ this.updateTransceiver = async (transceiver, track, trackType) => {
6845
+ const sender = transceiver.sender;
6846
+ if (sender.track)
6847
+ this.trackIdToTrackType.delete(sender.track.id);
6848
+ await sender.replaceTrack(track);
6849
+ if (track)
6850
+ this.trackIdToTrackType.set(track.id, trackType);
6851
+ };
6596
6852
  /**
6597
6853
  * Synchronizes the current Publisher state with the provided publish options.
6598
6854
  */
@@ -6622,7 +6878,7 @@ class Publisher extends BasePeerConnection {
6622
6878
  continue;
6623
6879
  // it is safe to stop the track here, it is a clone
6624
6880
  this.stopTrack(transceiver.sender.track);
6625
- await transceiver.sender.replaceTrack(null);
6881
+ await this.updateTransceiver(transceiver, null, publishOption.trackType);
6626
6882
  }
6627
6883
  };
6628
6884
  /**
@@ -6642,18 +6898,6 @@ class Publisher extends BasePeerConnection {
6642
6898
  }
6643
6899
  return false;
6644
6900
  };
6645
- /**
6646
- * Maps the given track ID to the corresponding track type.
6647
- */
6648
- this.getTrackType = (trackId) => {
6649
- for (const transceiverId of this.transceiverCache.items()) {
6650
- const { publishOption, transceiver } = transceiverId;
6651
- if (transceiver.sender.track?.id === trackId) {
6652
- return publishOption.trackType;
6653
- }
6654
- }
6655
- return undefined;
6656
- };
6657
6901
  /**
6658
6902
  * Stops the cloned track that is being published to the SFU.
6659
6903
  */
@@ -6947,6 +7191,7 @@ class Subscriber extends BasePeerConnection {
6947
7191
  if (!trackType) {
6948
7192
  return this.logger('error', `Unknown track type: ${rawTrackType}`);
6949
7193
  }
7194
+ this.trackIdToTrackType.set(e.track.id, trackType);
6950
7195
  if (!participantToUpdate) {
6951
7196
  this.logger('warn', `[onTrack]: Received track for unknown participant: ${trackId}`, e);
6952
7197
  this.state.registerOrphanedTrack({
@@ -7290,12 +7535,22 @@ class StreamSfuClient {
7290
7535
  */
7291
7536
  this.abortController = new AbortController();
7292
7537
  this.createWebSocket = () => {
7538
+ const eventsToTrace = {
7539
+ callEnded: true,
7540
+ changePublishQuality: true,
7541
+ error: true,
7542
+ goAway: true,
7543
+ };
7293
7544
  this.signalWs = createWebSocketSignalChannel({
7294
7545
  logTag: this.logTag,
7295
7546
  endpoint: `${this.credentials.server.ws_endpoint}?tag=${this.logTag}`,
7296
7547
  onMessage: (message) => {
7297
7548
  this.lastMessageTimestamp = new Date();
7298
7549
  this.scheduleConnectionCheck();
7550
+ const eventKind = message.eventPayload.oneofKind;
7551
+ if (eventsToTrace[eventKind]) {
7552
+ this.tracer?.trace(eventKind, message);
7553
+ }
7299
7554
  this.dispatcher.dispatch(message, this.logTag);
7300
7555
  },
7301
7556
  });
@@ -7388,7 +7643,8 @@ class StreamSfuClient {
7388
7643
  };
7389
7644
  this.sendStats = async (stats) => {
7390
7645
  await this.joinTask;
7391
- return retryable(() => this.rpc.sendStats({ ...stats, sessionId: this.sessionId }), this.abortController.signal);
7646
+ // NOTE: we don't retry sending stats
7647
+ return this.rpc.sendStats({ ...stats, sessionId: this.sessionId });
7392
7648
  };
7393
7649
  this.startNoiseCancellation = async () => {
7394
7650
  await this.joinTask;
@@ -7440,7 +7696,7 @@ class StreamSfuClient {
7440
7696
  unsubscribe();
7441
7697
  current.reject(new Error('Waiting for "joinResponse" has timed out'));
7442
7698
  }, this.joinResponseTimeout);
7443
- await this.send(SfuRequest.create({
7699
+ const joinRequest = SfuRequest.create({
7444
7700
  requestPayload: {
7445
7701
  oneofKind: 'joinRequest',
7446
7702
  joinRequest: JoinRequest.create({
@@ -7449,7 +7705,9 @@ class StreamSfuClient {
7449
7705
  token: this.credentials.token,
7450
7706
  }),
7451
7707
  },
7452
- }));
7708
+ });
7709
+ this.tracer?.trace('joinRequest', joinRequest);
7710
+ await this.send(joinRequest);
7453
7711
  return current.promise;
7454
7712
  };
7455
7713
  this.ping = async () => {
@@ -7510,7 +7768,9 @@ class StreamSfuClient {
7510
7768
  this.joinResponseTimeout = joinResponseTimeout;
7511
7769
  this.logTag = logTag;
7512
7770
  this.logger = getLogger(['SfuClient', logTag]);
7513
- this.tracer = enableTracing ? new Tracer(logTag) : undefined;
7771
+ this.tracer = enableTracing
7772
+ ? new Tracer(`${logTag}-${this.edgeName}`)
7773
+ : undefined;
7514
7774
  this.rpc = createSignalClient({
7515
7775
  baseUrl: server.url,
7516
7776
  interceptors: [
@@ -8410,7 +8670,6 @@ class DynascaleManager {
8410
8670
  const { selectedDevice } = this.speaker.state;
8411
8671
  if (selectedDevice && 'setSinkId' in audioElement) {
8412
8672
  audioElement.setSinkId(selectedDevice);
8413
- tracer.trace('navigator.mediaDevices.setSinkId', selectedDevice);
8414
8673
  }
8415
8674
  }
8416
8675
  });
@@ -8420,7 +8679,6 @@ class DynascaleManager {
8420
8679
  : this.speaker.state.selectedDevice$.subscribe((deviceId) => {
8421
8680
  if (deviceId) {
8422
8681
  audioElement.setSinkId(deviceId);
8423
- tracer.trace('navigator.mediaDevices.setSinkId', deviceId);
8424
8682
  }
8425
8683
  });
8426
8684
  const volumeSubscription = combineLatest([
@@ -9492,25 +9750,6 @@ class InputMediaDeviceManagerState {
9492
9750
  * The default constraints for the device.
9493
9751
  */
9494
9752
  this.defaultConstraints$ = this.defaultConstraintsSubject.asObservable();
9495
- /**
9496
- * Gets the current value of an observable, or undefined if the observable has
9497
- * not emitted a value yet.
9498
- *
9499
- * @param observable$ the observable to get the value from.
9500
- */
9501
- this.getCurrentValue = getCurrentValue;
9502
- /**
9503
- * Updates the value of the provided Subject.
9504
- * An `update` can either be a new value or a function which takes
9505
- * the current value and returns a new value.
9506
- *
9507
- * @internal
9508
- *
9509
- * @param subject the subject to update.
9510
- * @param update the update to apply to the subject.
9511
- * @return the updated value.
9512
- */
9513
- this.setCurrentValue = setCurrentValue;
9514
9753
  this.hasBrowserPermission$ = permission
9515
9754
  ? permission.asObservable().pipe(shareReplay(1))
9516
9755
  : of(true);
@@ -9525,39 +9764,39 @@ class InputMediaDeviceManagerState {
9525
9764
  * The device status
9526
9765
  */
9527
9766
  get status() {
9528
- return this.getCurrentValue(this.status$);
9767
+ return getCurrentValue(this.status$);
9529
9768
  }
9530
9769
  /**
9531
9770
  * The requested device status. Useful for optimistic UIs
9532
9771
  */
9533
9772
  get optimisticStatus() {
9534
- return this.getCurrentValue(this.optimisticStatus$);
9773
+ return getCurrentValue(this.optimisticStatus$);
9535
9774
  }
9536
9775
  /**
9537
9776
  * The currently selected device
9538
9777
  */
9539
9778
  get selectedDevice() {
9540
- return this.getCurrentValue(this.selectedDevice$);
9779
+ return getCurrentValue(this.selectedDevice$);
9541
9780
  }
9542
9781
  /**
9543
9782
  * The current media stream, or `undefined` if the device is currently disabled.
9544
9783
  */
9545
9784
  get mediaStream() {
9546
- return this.getCurrentValue(this.mediaStream$);
9785
+ return getCurrentValue(this.mediaStream$);
9547
9786
  }
9548
9787
  /**
9549
9788
  * @internal
9550
9789
  * @param status
9551
9790
  */
9552
9791
  setStatus(status) {
9553
- this.setCurrentValue(this.statusSubject, status);
9792
+ setCurrentValue(this.statusSubject, status);
9554
9793
  }
9555
9794
  /**
9556
9795
  * @internal
9557
9796
  * @param pendingStatus
9558
9797
  */
9559
9798
  setPendingStatus(pendingStatus) {
9560
- this.setCurrentValue(this.optimisticStatusSubject, pendingStatus);
9799
+ setCurrentValue(this.optimisticStatusSubject, pendingStatus);
9561
9800
  }
9562
9801
  /**
9563
9802
  * Updates the `mediaStream` state variable.
@@ -9568,7 +9807,7 @@ class InputMediaDeviceManagerState {
9568
9807
  * as this is the stream that holds the actual deviceId information.
9569
9808
  */
9570
9809
  setMediaStream(stream, rootStream) {
9571
- this.setCurrentValue(this.mediaStreamSubject, stream);
9810
+ setCurrentValue(this.mediaStreamSubject, stream);
9572
9811
  if (rootStream) {
9573
9812
  this.setDevice(this.getDeviceIdFromStream(rootStream));
9574
9813
  }
@@ -9578,13 +9817,13 @@ class InputMediaDeviceManagerState {
9578
9817
  * @param deviceId the device id to set.
9579
9818
  */
9580
9819
  setDevice(deviceId) {
9581
- this.setCurrentValue(this.selectedDeviceSubject, deviceId);
9820
+ setCurrentValue(this.selectedDeviceSubject, deviceId);
9582
9821
  }
9583
9822
  /**
9584
9823
  * Gets the default constraints for the device.
9585
9824
  */
9586
9825
  get defaultConstraints() {
9587
- return this.getCurrentValue(this.defaultConstraints$);
9826
+ return getCurrentValue(this.defaultConstraints$);
9588
9827
  }
9589
9828
  /**
9590
9829
  * Sets the default constraints for the device.
@@ -9593,7 +9832,7 @@ class InputMediaDeviceManagerState {
9593
9832
  * @param constraints the constraints to set.
9594
9833
  */
9595
9834
  setDefaultConstraints(constraints) {
9596
- this.setCurrentValue(this.defaultConstraintsSubject, constraints);
9835
+ setCurrentValue(this.defaultConstraintsSubject, constraints);
9597
9836
  }
9598
9837
  }
9599
9838
 
@@ -9611,13 +9850,13 @@ class CameraManagerState extends InputMediaDeviceManagerState {
9611
9850
  * back - means the camera facing the environment
9612
9851
  */
9613
9852
  get direction() {
9614
- return this.getCurrentValue(this.direction$);
9853
+ return getCurrentValue(this.direction$);
9615
9854
  }
9616
9855
  /**
9617
9856
  * @internal
9618
9857
  */
9619
9858
  setDirection(direction) {
9620
- this.setCurrentValue(this.directionSubject, direction);
9859
+ setCurrentValue(this.directionSubject, direction);
9621
9860
  }
9622
9861
  /**
9623
9862
  * @internal
@@ -9790,13 +10029,13 @@ class MicrophoneManagerState extends InputMediaDeviceManagerState {
9790
10029
  * This feature is not available in the React Native SDK.
9791
10030
  */
9792
10031
  get speakingWhileMuted() {
9793
- return this.getCurrentValue(this.speakingWhileMuted$);
10032
+ return getCurrentValue(this.speakingWhileMuted$);
9794
10033
  }
9795
10034
  /**
9796
10035
  * @internal
9797
10036
  */
9798
10037
  setSpeakingWhileMuted(isSpeaking) {
9799
- this.setCurrentValue(this.speakingWhileMutedSubject, isSpeaking);
10038
+ setCurrentValue(this.speakingWhileMutedSubject, isSpeaking);
9800
10039
  }
9801
10040
  getDeviceIdFromStream(stream) {
9802
10041
  const [track] = stream.getAudioTracks();
@@ -10248,19 +10487,19 @@ class ScreenShareState extends InputMediaDeviceManagerState {
10248
10487
  * The current screen share audio status.
10249
10488
  */
10250
10489
  get audioEnabled() {
10251
- return this.getCurrentValue(this.audioEnabled$);
10490
+ return getCurrentValue(this.audioEnabled$);
10252
10491
  }
10253
10492
  /**
10254
10493
  * Set the current screen share audio status.
10255
10494
  */
10256
10495
  setAudioEnabled(isEnabled) {
10257
- this.setCurrentValue(this.audioEnabledSubject, isEnabled);
10496
+ setCurrentValue(this.audioEnabledSubject, isEnabled);
10258
10497
  }
10259
10498
  /**
10260
10499
  * The current screen share settings.
10261
10500
  */
10262
10501
  get settings() {
10263
- return this.getCurrentValue(this.settings$);
10502
+ return getCurrentValue(this.settings$);
10264
10503
  }
10265
10504
  /**
10266
10505
  * Set the current screen share settings.
@@ -10268,7 +10507,7 @@ class ScreenShareState extends InputMediaDeviceManagerState {
10268
10507
  * @param settings the screen share settings to set.
10269
10508
  */
10270
10509
  setSettings(settings) {
10271
- this.setCurrentValue(this.settingsSubject, settings);
10510
+ setCurrentValue(this.settingsSubject, settings);
10272
10511
  }
10273
10512
  }
10274
10513
 
@@ -10347,25 +10586,6 @@ class SpeakerState {
10347
10586
  * [Tells if the browser supports audio output change on 'audio' elements](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/setSinkId).
10348
10587
  */
10349
10588
  this.isDeviceSelectionSupported = checkIfAudioOutputChangeSupported();
10350
- /**
10351
- * Gets the current value of an observable, or undefined if the observable has
10352
- * not emitted a value yet.
10353
- *
10354
- * @param observable$ the observable to get the value from.
10355
- */
10356
- this.getCurrentValue = getCurrentValue;
10357
- /**
10358
- * Updates the value of the provided Subject.
10359
- * An `update` can either be a new value or a function which takes
10360
- * the current value and returns a new value.
10361
- *
10362
- * @internal
10363
- *
10364
- * @param subject the subject to update.
10365
- * @param update the update to apply to the subject.
10366
- * @return the updated value.
10367
- */
10368
- this.setCurrentValue = setCurrentValue;
10369
10589
  this.selectedDevice$ = this.selectedDeviceSubject
10370
10590
  .asObservable()
10371
10591
  .pipe(distinctUntilChanged());
@@ -10379,7 +10599,7 @@ class SpeakerState {
10379
10599
  * Note: this feature is not supported in React Native
10380
10600
  */
10381
10601
  get selectedDevice() {
10382
- return this.getCurrentValue(this.selectedDevice$);
10602
+ return getCurrentValue(this.selectedDevice$);
10383
10603
  }
10384
10604
  /**
10385
10605
  * The currently selected volume
@@ -10387,21 +10607,22 @@ class SpeakerState {
10387
10607
  * Note: this feature is not supported in React Native
10388
10608
  */
10389
10609
  get volume() {
10390
- return this.getCurrentValue(this.volume$);
10610
+ return getCurrentValue(this.volume$);
10391
10611
  }
10392
10612
  /**
10393
10613
  * @internal
10394
10614
  * @param deviceId
10395
10615
  */
10396
10616
  setDevice(deviceId) {
10397
- this.setCurrentValue(this.selectedDeviceSubject, deviceId);
10617
+ setCurrentValue(this.selectedDeviceSubject, deviceId);
10618
+ tracer.trace('navigator.mediaDevices.setSinkId', deviceId);
10398
10619
  }
10399
10620
  /**
10400
10621
  * @internal
10401
10622
  * @param volume
10402
10623
  */
10403
10624
  setVolume(volume) {
10404
- this.setCurrentValue(this.volumeSubject, volume);
10625
+ setCurrentValue(this.volumeSubject, volume);
10405
10626
  }
10406
10627
  }
10407
10628
 
@@ -10782,6 +11003,7 @@ class Call {
10782
11003
  this.leaveCallHooks.forEach((hook) => hook());
10783
11004
  this.initialized = false;
10784
11005
  this.hasJoinedOnce = false;
11006
+ this.unifiedSessionId = undefined;
10785
11007
  this.ringingSubject.next(false);
10786
11008
  this.cancelAutoDrop();
10787
11009
  this.clientStore.unregisterCall(this);
@@ -11209,7 +11431,6 @@ class Call {
11209
11431
  state: this.state,
11210
11432
  connectionConfig,
11211
11433
  logTag: String(this.sfuClientTag),
11212
- clientDetails,
11213
11434
  enableTracing,
11214
11435
  onUnrecoverableError: (reason) => {
11215
11436
  this.reconnect(WebsocketReconnectStrategy.REJOIN, reason).catch((err) => {
@@ -11231,7 +11452,6 @@ class Call {
11231
11452
  connectionConfig,
11232
11453
  publishOptions,
11233
11454
  logTag: String(this.sfuClientTag),
11234
- clientDetails,
11235
11455
  enableTracing,
11236
11456
  onUnrecoverableError: (reason) => {
11237
11457
  this.reconnect(WebsocketReconnectStrategy.REJOIN, reason).catch((err) => {
@@ -11250,6 +11470,7 @@ class Call {
11250
11470
  });
11251
11471
  this.sfuStatsReporter?.stop();
11252
11472
  if (statsOptions?.reporting_interval_ms > 0) {
11473
+ this.unifiedSessionId ?? (this.unifiedSessionId = sfuClient.sessionId);
11253
11474
  this.sfuStatsReporter = new SfuStatsReporter(sfuClient, {
11254
11475
  clientDetails,
11255
11476
  options: statsOptions,
@@ -11258,6 +11479,7 @@ class Call {
11258
11479
  microphone: this.microphone,
11259
11480
  camera: this.camera,
11260
11481
  state: this.state,
11482
+ unifiedSessionId: this.unifiedSessionId,
11261
11483
  });
11262
11484
  this.sfuStatsReporter.start();
11263
11485
  }
@@ -11627,6 +11849,11 @@ class Call {
11627
11849
  trackTypes.push(TrackType.SCREEN_SHARE_AUDIO);
11628
11850
  }
11629
11851
  }
11852
+ if (track.kind === 'video') {
11853
+ // schedules calibration report - the SFU will use the performance stats
11854
+ // to adjust the quality thresholds as early as possible
11855
+ this.sfuStatsReporter?.scheduleOne(3000);
11856
+ }
11630
11857
  await this.updateLocalStreamState(mediaStream, ...trackTypes);
11631
11858
  };
11632
11859
  /**
@@ -13486,7 +13713,7 @@ class StreamClient {
13486
13713
  this.getUserAgent = () => {
13487
13714
  if (!this.cachedUserAgent) {
13488
13715
  const { clientAppIdentifier = {} } = this.options;
13489
- const { sdkName = 'js', sdkVersion = "1.20.2", ...extras } = clientAppIdentifier;
13716
+ const { sdkName = 'js', sdkVersion = "1.21.0", ...extras } = clientAppIdentifier;
13490
13717
  this.cachedUserAgent = [
13491
13718
  `stream-video-${sdkName}-v${sdkVersion}`,
13492
13719
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),