@stream-io/video-client 1.20.1 → 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 (59) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/index.browser.es.js +416 -174
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +416 -174
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.es.js +416 -174
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/src/Call.d.ts +2 -1
  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/coordinator/index.d.ts +7 -0
  13. package/dist/src/gen/video/sfu/models/models.d.ts +50 -0
  14. package/dist/src/gen/video/sfu/signal_rpc/signal.d.ts +29 -3
  15. package/dist/src/rpc/createClient.d.ts +1 -1
  16. package/dist/src/rtc/BasePeerConnection.d.ts +12 -7
  17. package/dist/src/rtc/Publisher.d.ts +4 -4
  18. package/dist/src/stats/CallStateStatsReporter.d.ts +5 -3
  19. package/dist/src/stats/SfuStatsReporter.d.ts +5 -1
  20. package/dist/src/stats/rtc/StatsTracer.d.ts +54 -0
  21. package/dist/src/stats/rtc/Tracer.d.ts +1 -5
  22. package/dist/src/stats/rtc/index.d.ts +2 -0
  23. package/dist/src/stats/rtc/types.d.ts +19 -0
  24. package/dist/src/stats/types.d.ts +13 -0
  25. package/dist/src/types.d.ts +7 -2
  26. package/package.json +1 -1
  27. package/src/Call.ts +22 -12
  28. package/src/StreamSfuClient.ts +30 -18
  29. package/src/__tests__/Call.autodrop.test.ts +101 -0
  30. package/src/devices/CameraManagerState.ts +3 -2
  31. package/src/devices/InputMediaDeviceManagerState.ts +10 -31
  32. package/src/devices/MicrophoneManagerState.ts +3 -2
  33. package/src/devices/ScreenShareState.ts +5 -4
  34. package/src/devices/SpeakerState.ts +7 -26
  35. package/src/events/__tests__/call.test.ts +5 -1
  36. package/src/events/call.ts +8 -4
  37. package/src/events/internal.ts +1 -1
  38. package/src/gen/coordinator/index.ts +7 -0
  39. package/src/gen/video/sfu/models/models.ts +84 -0
  40. package/src/gen/video/sfu/signal_rpc/signal.ts +50 -2
  41. package/src/helpers/DynascaleManager.ts +0 -9
  42. package/src/rpc/createClient.ts +1 -1
  43. package/src/rtc/BasePeerConnection.ts +37 -15
  44. package/src/rtc/Publisher.ts +17 -15
  45. package/src/rtc/Subscriber.ts +2 -0
  46. package/src/rtc/__tests__/Publisher.test.ts +19 -0
  47. package/src/rtc/__tests__/Subscriber.test.ts +15 -1
  48. package/src/stats/CallStateStatsReporter.ts +19 -31
  49. package/src/stats/SfuStatsReporter.ts +37 -12
  50. package/src/stats/rtc/StatsTracer.ts +289 -0
  51. package/src/stats/rtc/Tracer.ts +1 -6
  52. package/src/stats/rtc/__tests__/Tracer.test.ts +57 -0
  53. package/src/stats/rtc/index.ts +3 -0
  54. package/src/stats/rtc/mediaDevices.ts +4 -3
  55. package/src/stats/rtc/pc.ts +6 -76
  56. package/src/stats/rtc/types.ts +22 -0
  57. package/src/stats/types.ts +15 -0
  58. package/src/store/stateStore.ts +1 -1
  59. package/src/types.ts +8 -2
package/dist/index.cjs.js CHANGED
@@ -62,16 +62,17 @@ if (typeof navigator !== 'undefined' &&
62
62
  const original = target[method];
63
63
  if (!original)
64
64
  continue;
65
+ let mark = 0;
65
66
  target[method] = async function tracedMethod(constraints) {
66
- const tag = `navigator.mediaDevices.${method}`;
67
+ const tag = `navigator.mediaDevices.${method}.${mark++}`;
67
68
  trace(tag, constraints);
68
69
  try {
69
70
  const stream = await original.call(target, constraints);
70
- trace(`${tag}OnSuccess`, dumpStream(stream));
71
+ trace(`${tag}.OnSuccess`, dumpStream(stream));
71
72
  return stream;
72
73
  }
73
74
  catch (err) {
74
- trace(`${tag}OnFailure`, err.name);
75
+ trace(`${tag}.OnFailure`, err.name);
75
76
  throw err;
76
77
  }
77
78
  };
@@ -119,6 +120,7 @@ const FrameRecordingSettingsRequestQualityEnum = {
119
120
  _720P: '720p',
120
121
  _1080P: '1080p',
121
122
  _1440P: '1440p',
123
+ _2160P: '2160p',
122
124
  };
123
125
  /**
124
126
  * @export
@@ -191,11 +193,13 @@ const RTMPBroadcastRequestQualityEnum = {
191
193
  _720P: '720p',
192
194
  _1080P: '1080p',
193
195
  _1440P: '1440p',
196
+ _2160P: '2160p',
194
197
  PORTRAIT_360X640: 'portrait-360x640',
195
198
  PORTRAIT_480X854: 'portrait-480x854',
196
199
  PORTRAIT_720X1280: 'portrait-720x1280',
197
200
  PORTRAIT_1080X1920: 'portrait-1080x1920',
198
201
  PORTRAIT_1440X2560: 'portrait-1440x2560',
202
+ PORTRAIT_2160X3840: 'portrait-2160x3840',
199
203
  };
200
204
  /**
201
205
  * @export
@@ -206,11 +210,13 @@ const RTMPSettingsRequestQualityEnum = {
206
210
  _720P: '720p',
207
211
  _1080P: '1080p',
208
212
  _1440P: '1440p',
213
+ _2160P: '2160p',
209
214
  PORTRAIT_360X640: 'portrait-360x640',
210
215
  PORTRAIT_480X854: 'portrait-480x854',
211
216
  PORTRAIT_720X1280: 'portrait-720x1280',
212
217
  PORTRAIT_1080X1920: 'portrait-1080x1920',
213
218
  PORTRAIT_1440X2560: 'portrait-1440x2560',
219
+ PORTRAIT_2160X3840: 'portrait-2160x3840',
214
220
  };
215
221
  /**
216
222
  * @export
@@ -229,11 +235,13 @@ const RecordSettingsRequestQualityEnum = {
229
235
  _720P: '720p',
230
236
  _1080P: '1080p',
231
237
  _1440P: '1440p',
238
+ _2160P: '2160p',
232
239
  PORTRAIT_360X640: 'portrait-360x640',
233
240
  PORTRAIT_480X854: 'portrait-480x854',
234
241
  PORTRAIT_720X1280: 'portrait-720x1280',
235
242
  PORTRAIT_1080X1920: 'portrait-1080x1920',
236
243
  PORTRAIT_1440X2560: 'portrait-1440x2560',
244
+ PORTRAIT_2160X3840: 'portrait-2160x3840',
237
245
  };
238
246
  /**
239
247
  * @export
@@ -1789,6 +1797,47 @@ class AppleState$Type extends runtime.MessageType {
1789
1797
  * @generated MessageType for protobuf message stream.video.sfu.models.AppleState
1790
1798
  */
1791
1799
  const AppleState = new AppleState$Type();
1800
+ // @generated message type with reflection information, may provide speed optimized methods
1801
+ class PerformanceStats$Type extends runtime.MessageType {
1802
+ constructor() {
1803
+ super('stream.video.sfu.models.PerformanceStats', [
1804
+ {
1805
+ no: 1,
1806
+ name: 'track_type',
1807
+ kind: 'enum',
1808
+ T: () => [
1809
+ 'stream.video.sfu.models.TrackType',
1810
+ TrackType,
1811
+ 'TRACK_TYPE_',
1812
+ ],
1813
+ },
1814
+ { no: 2, name: 'codec', kind: 'message', T: () => Codec },
1815
+ {
1816
+ no: 3,
1817
+ name: 'avg_frame_time_ms',
1818
+ kind: 'scalar',
1819
+ T: 2 /*ScalarType.FLOAT*/,
1820
+ },
1821
+ { no: 4, name: 'avg_fps', kind: 'scalar', T: 2 /*ScalarType.FLOAT*/ },
1822
+ {
1823
+ no: 5,
1824
+ name: 'video_dimension',
1825
+ kind: 'message',
1826
+ T: () => VideoDimension,
1827
+ },
1828
+ {
1829
+ no: 6,
1830
+ name: 'target_bitrate',
1831
+ kind: 'scalar',
1832
+ T: 5 /*ScalarType.INT32*/,
1833
+ },
1834
+ ]);
1835
+ }
1836
+ }
1837
+ /**
1838
+ * @generated MessageType for protobuf message stream.video.sfu.models.PerformanceStats
1839
+ */
1840
+ const PerformanceStats = new PerformanceStats$Type();
1792
1841
 
1793
1842
  var models = /*#__PURE__*/Object.freeze({
1794
1843
  __proto__: null,
@@ -1814,6 +1863,7 @@ var models = /*#__PURE__*/Object.freeze({
1814
1863
  Participant: Participant,
1815
1864
  ParticipantCount: ParticipantCount,
1816
1865
  get PeerType () { return PeerType; },
1866
+ PerformanceStats: PerformanceStats,
1817
1867
  Pin: Pin,
1818
1868
  PublishOption: PublishOption,
1819
1869
  RTMPIngress: RTMPIngress,
@@ -1993,6 +2043,27 @@ class SendStatsRequest$Type extends runtime.MessageType {
1993
2043
  kind: 'scalar',
1994
2044
  T: 9 /*ScalarType.STRING*/,
1995
2045
  },
2046
+ { no: 15, name: 'rtc_stats', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
2047
+ {
2048
+ no: 16,
2049
+ name: 'encode_stats',
2050
+ kind: 'message',
2051
+ repeat: 1 /*RepeatType.PACKED*/,
2052
+ T: () => PerformanceStats,
2053
+ },
2054
+ {
2055
+ no: 17,
2056
+ name: 'decode_stats',
2057
+ kind: 'message',
2058
+ repeat: 1 /*RepeatType.PACKED*/,
2059
+ T: () => PerformanceStats,
2060
+ },
2061
+ {
2062
+ no: 18,
2063
+ name: 'unified_session_id',
2064
+ kind: 'scalar',
2065
+ T: 9 /*ScalarType.STRING*/,
2066
+ },
1996
2067
  ]);
1997
2068
  }
1998
2069
  }
@@ -4060,7 +4131,7 @@ class StreamVideoWriteableStateStore {
4060
4131
  continue;
4061
4132
  logger('info', `User disconnected, leaving call: ${call.cid}`);
4062
4133
  await call
4063
- .leave({ reason: 'client.disconnectUser() called' })
4134
+ .leave({ message: 'client.disconnectUser() called' })
4064
4135
  .catch((err) => {
4065
4136
  logger('error', `Error leaving call: ${call.cid}`, err);
4066
4137
  });
@@ -5464,30 +5535,17 @@ const createStatsReporter = ({ subscriber, publisher, state, datacenter, polling
5464
5535
  }
5465
5536
  }
5466
5537
  }
5467
- const [subscriberStats, publisherStats] = await Promise.all([
5468
- subscriber
5469
- .getStats()
5470
- .then((report) => transform(report, {
5471
- kind: 'subscriber',
5472
- trackKind: 'video',
5473
- publisher,
5474
- }))
5475
- .then(aggregate),
5476
- publisher
5477
- ? publisher
5478
- .getStats()
5479
- .then((report) => transform(report, {
5480
- kind: 'publisher',
5481
- trackKind: 'video',
5482
- publisher,
5483
- }))
5484
- .then(aggregate)
5485
- : getEmptyStats(),
5486
- ]);
5487
5538
  const [subscriberRawStats, publisherRawStats] = await Promise.all([
5488
5539
  getRawStatsForTrack('subscriber'),
5489
5540
  publisher ? getRawStatsForTrack('publisher') : undefined,
5490
5541
  ]);
5542
+ const process = (report, kind) => aggregate(transform(report, { kind, trackKind: 'video', publisher }));
5543
+ const subscriberStats = subscriberRawStats
5544
+ ? process(subscriberRawStats, 'subscriber')
5545
+ : getEmptyStats();
5546
+ const publisherStats = publisherRawStats
5547
+ ? process(publisherRawStats, 'publisher')
5548
+ : getEmptyStats();
5491
5549
  state.setCallStatsReport({
5492
5550
  datacenter,
5493
5551
  publisherStats,
@@ -5645,7 +5703,7 @@ const aggregate = (stats) => {
5645
5703
  return report;
5646
5704
  };
5647
5705
 
5648
- const version = "1.20.1";
5706
+ const version = "1.21.0";
5649
5707
  const [major, minor, patch] = version.split('.');
5650
5708
  let sdkInfo = {
5651
5709
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -5782,7 +5840,7 @@ const getClientDetails = async () => {
5782
5840
  };
5783
5841
 
5784
5842
  class SfuStatsReporter {
5785
- constructor(sfuClient, { options, clientDetails, subscriber, publisher, microphone, camera, state, }) {
5843
+ constructor(sfuClient, { options, clientDetails, subscriber, publisher, microphone, camera, state, unifiedSessionId, }) {
5786
5844
  this.logger = getLogger(['SfuStatsReporter']);
5787
5845
  this.inputDevices = new Map();
5788
5846
  this.observeDevice = (device, kind) => {
@@ -5839,31 +5897,40 @@ class SfuStatsReporter {
5839
5897
  };
5840
5898
  this.run = async (telemetry) => {
5841
5899
  const [subscriberStats, publisherStats] = await Promise.all([
5842
- this.subscriber.getStats().then(flatten).then(JSON.stringify),
5843
- this.publisher?.getStats().then(flatten).then(JSON.stringify) ?? '[]',
5900
+ this.subscriber.stats.get(),
5901
+ this.publisher?.stats.get(),
5844
5902
  ]);
5845
- const subscriberTrace = this.subscriber.getTrace();
5846
- const publisherTrace = this.publisher?.getTrace();
5903
+ this.subscriber.tracer?.trace('getstats', subscriberStats.delta);
5904
+ if (publisherStats) {
5905
+ this.publisher?.tracer?.trace('getstats', publisherStats.delta);
5906
+ }
5907
+ const subscriberTrace = this.subscriber.tracer?.take();
5908
+ const publisherTrace = this.publisher?.tracer?.take();
5847
5909
  const mediaTrace = tracer.take();
5848
5910
  const sfuTrace = this.sfuClient.getTrace();
5849
- const publisherTraces = [
5911
+ const traces = [
5850
5912
  ...mediaTrace.snapshot,
5851
5913
  ...(sfuTrace?.snapshot ?? []),
5852
5914
  ...(publisherTrace?.snapshot ?? []),
5915
+ ...(subscriberTrace?.snapshot ?? []),
5853
5916
  ];
5854
5917
  try {
5855
5918
  await this.sfuClient.sendStats({
5856
5919
  sdk: this.sdkName,
5857
5920
  sdkVersion: this.sdkVersion,
5858
5921
  webrtcVersion: this.webRTCVersion,
5859
- subscriberStats,
5860
- subscriberRtcStats: subscriberTrace
5861
- ? JSON.stringify(subscriberTrace.snapshot)
5862
- : '',
5863
- publisherStats,
5864
- publisherRtcStats: publisherTraces.length > 0 ? JSON.stringify(publisherTraces) : '',
5922
+ subscriberStats: JSON.stringify(flatten(subscriberStats.stats)),
5923
+ publisherStats: publisherStats
5924
+ ? JSON.stringify(flatten(publisherStats.stats))
5925
+ : '[]',
5926
+ subscriberRtcStats: '',
5927
+ publisherRtcStats: '',
5928
+ rtcStats: JSON.stringify(traces),
5929
+ encodeStats: publisherStats?.performanceStats ?? [],
5930
+ decodeStats: subscriberStats.performanceStats,
5865
5931
  audioDevices: this.inputDevices.get('mic'),
5866
5932
  videoDevices: this.inputDevices.get('camera'),
5933
+ unifiedSessionId: this.unifiedSessionId,
5867
5934
  deviceState: getDeviceState(),
5868
5935
  telemetry,
5869
5936
  });
@@ -5896,6 +5963,16 @@ class SfuStatsReporter {
5896
5963
  this.inputDevices.clear();
5897
5964
  clearInterval(this.intervalId);
5898
5965
  this.intervalId = undefined;
5966
+ clearTimeout(this.timeoutId);
5967
+ this.timeoutId = undefined;
5968
+ };
5969
+ this.scheduleOne = (timeout) => {
5970
+ clearTimeout(this.timeoutId);
5971
+ this.timeoutId = setTimeout(() => {
5972
+ this.run().catch((err) => {
5973
+ this.logger('warn', 'Failed to report stats', err);
5974
+ });
5975
+ }, timeout);
5899
5976
  };
5900
5977
  this.sfuClient = sfuClient;
5901
5978
  this.options = options;
@@ -5904,6 +5981,7 @@ class SfuStatsReporter {
5904
5981
  this.microphone = microphone;
5905
5982
  this.camera = camera;
5906
5983
  this.state = state;
5984
+ this.unifiedSessionId = unifiedSessionId;
5907
5985
  const { sdk, browser } = clientDetails;
5908
5986
  this.sdkName = getSdkName(sdk);
5909
5987
  this.sdkVersion = getSdkVersion(sdk);
@@ -5926,47 +6004,25 @@ const traceRTCPeerConnection = (pc, trace) => {
5926
6004
  trace('ontrack', `${e.track.kind}:${e.track.id} ${streams}`);
5927
6005
  });
5928
6006
  pc.addEventListener('signalingstatechange', () => {
5929
- trace('onsignalingstatechange', pc.signalingState);
6007
+ trace('signalingstatechange', pc.signalingState);
5930
6008
  });
5931
6009
  pc.addEventListener('iceconnectionstatechange', () => {
5932
- trace('oniceconnectionstatechange', pc.iceConnectionState);
6010
+ trace('iceconnectionstatechange', pc.iceConnectionState);
5933
6011
  });
5934
6012
  pc.addEventListener('icegatheringstatechange', () => {
5935
- trace('onicegatheringstatechange', pc.iceGatheringState);
6013
+ trace('icegatheringstatechange', pc.iceGatheringState);
5936
6014
  });
5937
6015
  pc.addEventListener('connectionstatechange', () => {
5938
- trace('onconnectionstatechange', pc.connectionState);
6016
+ trace('connectionstatechange', pc.connectionState);
5939
6017
  });
5940
6018
  pc.addEventListener('negotiationneeded', () => {
5941
- trace('onnegotiationneeded', undefined);
6019
+ trace('negotiationneeded', undefined);
5942
6020
  });
5943
6021
  pc.addEventListener('datachannel', ({ channel }) => {
5944
- trace('ondatachannel', [channel.id, channel.label]);
5945
- });
5946
- let prev = {};
5947
- const getStats = () => {
5948
- pc.getStats(null)
5949
- .then((stats) => {
5950
- const now = toObject(stats);
5951
- trace('getstats', deltaCompression(prev, now));
5952
- prev = now;
5953
- })
5954
- .catch((err) => {
5955
- trace('getstatsOnFailure', err.toString());
5956
- });
5957
- };
5958
- const interval = setInterval(() => {
5959
- getStats();
5960
- }, 8000);
5961
- pc.addEventListener('connectionstatechange', () => {
5962
- const state = pc.connectionState;
5963
- if (state === 'connected' || state === 'failed') {
5964
- getStats();
5965
- }
6022
+ trace('datachannel', [channel.id, channel.label]);
5966
6023
  });
5967
6024
  const origClose = pc.close;
5968
6025
  pc.close = function tracedClose() {
5969
- clearInterval(interval);
5970
6026
  trace('close', undefined);
5971
6027
  return origClose.call(this);
5972
6028
  };
@@ -5996,9 +6052,162 @@ const traceRTCPeerConnection = (pc, trace) => {
5996
6052
  };
5997
6053
  }
5998
6054
  };
5999
- const toObject = (s) => {
6055
+
6056
+ /**
6057
+ * StatsTracer is a class that collects and processes WebRTC stats.
6058
+ * It is used to track the performance of the WebRTC connection
6059
+ * and to provide information about the media streams.
6060
+ * It is used by both the Publisher and Subscriber classes.
6061
+ *
6062
+ * @internal
6063
+ */
6064
+ class StatsTracer {
6065
+ /**
6066
+ * Creates a new StatsTracer instance.
6067
+ */
6068
+ constructor(pc, peerType, trackIdToTrackType) {
6069
+ this.previousStats = {};
6070
+ this.frameTimeHistory = [];
6071
+ this.fpsHistory = [];
6072
+ /**
6073
+ * Get the stats from the RTCPeerConnection.
6074
+ * When called, it will return the stats for the current connection.
6075
+ * It will also return the delta between the current stats and the previous stats.
6076
+ * This is used to track the performance of the connection.
6077
+ *
6078
+ * @internal
6079
+ */
6080
+ this.get = async () => {
6081
+ const stats = await this.pc.getStats();
6082
+ const currentStats = toObject(stats);
6083
+ const performanceStats = this.withOverrides(this.peerType === PeerType.SUBSCRIBER
6084
+ ? this.getDecodeStats(currentStats)
6085
+ : this.getEncodeStats(currentStats));
6086
+ const delta = deltaCompression(this.previousStats, currentStats);
6087
+ // store the current data for the next iteration
6088
+ this.previousStats = currentStats;
6089
+ this.frameTimeHistory = this.frameTimeHistory.slice(-2);
6090
+ this.fpsHistory = this.fpsHistory.slice(-2);
6091
+ return { performanceStats, delta, stats };
6092
+ };
6093
+ /**
6094
+ * Collects encode stats from the RTCPeerConnection.
6095
+ */
6096
+ this.getEncodeStats = (currentStats) => {
6097
+ const encodeStats = [];
6098
+ for (const rtp of Object.values(currentStats)) {
6099
+ if (rtp.type !== 'outbound-rtp')
6100
+ continue;
6101
+ const { codecId, framesSent = 0, kind, id, totalEncodeTime = 0, framesPerSecond = 0, frameHeight = 0, frameWidth = 0, targetBitrate = 0, mediaSourceId, } = rtp;
6102
+ if (kind === 'audio' || !this.previousStats[id])
6103
+ continue;
6104
+ const prevRtp = this.previousStats[id];
6105
+ const deltaTotalEncodeTime = totalEncodeTime - (prevRtp.totalEncodeTime || 0);
6106
+ const deltaFramesSent = framesSent - (prevRtp.framesSent || 0);
6107
+ const framesEncodeTime = deltaFramesSent > 0
6108
+ ? (deltaTotalEncodeTime / deltaFramesSent) * 1000
6109
+ : 0;
6110
+ this.frameTimeHistory.push(framesEncodeTime);
6111
+ this.fpsHistory.push(framesPerSecond);
6112
+ let trackType = TrackType.VIDEO;
6113
+ if (mediaSourceId && currentStats[mediaSourceId]) {
6114
+ const mediaSource = currentStats[mediaSourceId];
6115
+ trackType =
6116
+ this.trackIdToTrackType.get(mediaSource.trackIdentifier) || trackType;
6117
+ }
6118
+ encodeStats.push({
6119
+ trackType,
6120
+ codec: getCodecFromStats(currentStats, codecId),
6121
+ avgFrameTimeMs: average(this.frameTimeHistory),
6122
+ avgFps: average(this.fpsHistory),
6123
+ targetBitrate: Math.round(targetBitrate),
6124
+ videoDimension: { width: frameWidth, height: frameHeight },
6125
+ });
6126
+ }
6127
+ return encodeStats;
6128
+ };
6129
+ /**
6130
+ * Collects decode stats from the RTCPeerConnection.
6131
+ */
6132
+ this.getDecodeStats = (currentStats) => {
6133
+ let rtp = undefined;
6134
+ let max = 0;
6135
+ for (const item of Object.values(currentStats)) {
6136
+ if (item.type !== 'inbound-rtp')
6137
+ continue;
6138
+ const rtpItem = item;
6139
+ const { kind, frameWidth = 0, frameHeight = 0 } = rtpItem;
6140
+ const area = frameWidth * frameHeight;
6141
+ if (kind === 'video' && area > max) {
6142
+ rtp = rtpItem;
6143
+ max = area;
6144
+ }
6145
+ }
6146
+ if (!rtp || !this.previousStats[rtp.id])
6147
+ return [];
6148
+ const prevRtp = this.previousStats[rtp.id];
6149
+ const { framesDecoded = 0, framesPerSecond = 0, totalDecodeTime = 0, trackIdentifier, } = rtp;
6150
+ const deltaTotalDecodeTime = totalDecodeTime - (prevRtp.totalDecodeTime || 0);
6151
+ const deltaFramesDecoded = framesDecoded - (prevRtp.framesDecoded || 0);
6152
+ const framesDecodeTime = deltaFramesDecoded > 0
6153
+ ? (deltaTotalDecodeTime / deltaFramesDecoded) * 1000
6154
+ : 0;
6155
+ this.frameTimeHistory.push(framesDecodeTime);
6156
+ this.fpsHistory.push(framesPerSecond);
6157
+ const trackType = this.trackIdToTrackType.get(trackIdentifier) || TrackType.VIDEO;
6158
+ return [
6159
+ PerformanceStats.create({
6160
+ trackType,
6161
+ codec: getCodecFromStats(currentStats, rtp.codecId),
6162
+ avgFrameTimeMs: average(this.frameTimeHistory),
6163
+ avgFps: average(this.fpsHistory),
6164
+ videoDimension: { width: rtp.frameWidth, height: rtp.frameHeight },
6165
+ }),
6166
+ ];
6167
+ };
6168
+ /**
6169
+ * Applies cost overrides to the performance stats.
6170
+ * This is used to override the default encode/decode times with custom values.
6171
+ * This is useful for testing and debugging purposes, and it shouldn't be used in production.
6172
+ */
6173
+ this.withOverrides = (performanceStats) => {
6174
+ if (this.costOverrides) {
6175
+ for (const s of performanceStats) {
6176
+ const override = this.costOverrides.get(s.trackType);
6177
+ if (override !== undefined) {
6178
+ // override the average encode/decode time with the provided cost.
6179
+ // format: [override].[original-frame-time]
6180
+ s.avgFrameTimeMs = override + (s.avgFrameTimeMs || 0) / 1000;
6181
+ }
6182
+ }
6183
+ }
6184
+ return performanceStats;
6185
+ };
6186
+ /**
6187
+ * Set the encode/decode cost for a specific track type.
6188
+ * This is used to override the default encode/decode times with custom values.
6189
+ * This is useful for testing and debugging purposes, and it shouldn't be used in production.
6190
+ *
6191
+ * @internal
6192
+ */
6193
+ this.setCost = (cost, trackType = TrackType.VIDEO) => {
6194
+ if (!this.costOverrides)
6195
+ this.costOverrides = new Map();
6196
+ this.costOverrides.set(trackType, cost);
6197
+ };
6198
+ this.pc = pc;
6199
+ this.peerType = peerType;
6200
+ this.trackIdToTrackType = trackIdToTrackType;
6201
+ }
6202
+ }
6203
+ /**
6204
+ * Convert the stat report to an object.
6205
+ *
6206
+ * @param report the stat report to convert.
6207
+ */
6208
+ const toObject = (report) => {
6000
6209
  const obj = {};
6001
- s.forEach((v, k) => {
6210
+ report.forEach((v, k) => {
6002
6211
  obj[k] = v;
6003
6212
  });
6004
6213
  return obj;
@@ -6021,12 +6230,13 @@ const deltaCompression = (oldStats, newStats) => {
6021
6230
  }
6022
6231
  }
6023
6232
  let timestamp = -Infinity;
6024
- for (const report of Object.values(newStats)) {
6233
+ const values = Object.values(newStats);
6234
+ for (const report of values) {
6025
6235
  if (report.timestamp > timestamp) {
6026
6236
  timestamp = report.timestamp;
6027
6237
  }
6028
6238
  }
6029
- for (const report of Object.values(newStats)) {
6239
+ for (const report of values) {
6030
6240
  if (report.timestamp === timestamp) {
6031
6241
  report.timestamp = 0;
6032
6242
  }
@@ -6034,6 +6244,27 @@ const deltaCompression = (oldStats, newStats) => {
6034
6244
  newStats.timestamp = timestamp;
6035
6245
  return newStats;
6036
6246
  };
6247
+ /**
6248
+ * Calculates an average value.
6249
+ */
6250
+ const average = (arr) => arr.reduce((a, b) => a + b, 0) / arr.length;
6251
+ /**
6252
+ * Create a Codec object from the codec stats.
6253
+ *
6254
+ * @param stats the stats report.
6255
+ * @param codecId the codec ID to look for.
6256
+ */
6257
+ const getCodecFromStats = (stats, codecId) => {
6258
+ if (!codecId || !stats[codecId])
6259
+ return;
6260
+ const codecStats = stats[codecId];
6261
+ return Codec.create({
6262
+ name: codecStats.mimeType.split('/').pop(), // video/av1 -> av1
6263
+ clockRate: codecStats.clockRate,
6264
+ payloadType: codecStats.payloadType,
6265
+ fmtp: codecStats.sdpFmtpLine,
6266
+ });
6267
+ };
6037
6268
 
6038
6269
  /**
6039
6270
  * A base class for the `Publisher` and `Subscriber` classes.
@@ -6043,9 +6274,10 @@ class BasePeerConnection {
6043
6274
  /**
6044
6275
  * Constructs a new `BasePeerConnection` instance.
6045
6276
  */
6046
- constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onUnrecoverableError, logTag, clientDetails, enableTracing, }) {
6277
+ constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onUnrecoverableError, logTag, enableTracing, }) {
6047
6278
  this.isIceRestarting = false;
6048
6279
  this.isDisposed = false;
6280
+ this.trackIdToTrackType = new Map();
6049
6281
  this.subscriptions = [];
6050
6282
  /**
6051
6283
  * Handles events synchronously.
@@ -6093,10 +6325,10 @@ class BasePeerConnection {
6093
6325
  return this.pc.getStats(selector);
6094
6326
  };
6095
6327
  /**
6096
- * Returns the current tracing buffer.
6328
+ * Maps the given track ID to the corresponding track type.
6097
6329
  */
6098
- this.getTrace = () => {
6099
- return this.tracer?.take();
6330
+ this.getTrackType = (trackId) => {
6331
+ return this.trackIdToTrackType.get(trackId);
6100
6332
  };
6101
6333
  /**
6102
6334
  * Handles the ICECandidate event and
@@ -6130,6 +6362,24 @@ class BasePeerConnection {
6130
6362
  }
6131
6363
  return JSON.stringify(candidate.toJSON());
6132
6364
  };
6365
+ /**
6366
+ * Handles the ConnectionStateChange event.
6367
+ */
6368
+ this.onConnectionStateChange = async () => {
6369
+ const state = this.pc.connectionState;
6370
+ this.logger('debug', `Connection state changed`, state);
6371
+ if (!this.tracer)
6372
+ return;
6373
+ if (state === 'connected' || state === 'failed') {
6374
+ try {
6375
+ const stats = await this.stats.get();
6376
+ this.tracer.trace('getstats', stats.delta);
6377
+ }
6378
+ catch (err) {
6379
+ this.tracer.trace('getstatsOnFailure', err.toString());
6380
+ }
6381
+ }
6382
+ };
6133
6383
  /**
6134
6384
  * Handles the ICE connection state change event.
6135
6385
  */
@@ -6186,18 +6436,19 @@ class BasePeerConnection {
6186
6436
  logTag,
6187
6437
  ]);
6188
6438
  this.pc = new RTCPeerConnection(connectionConfig);
6189
- if (enableTracing) {
6190
- const tag = `${logTag}-${peerType === PeerType.SUBSCRIBER ? 'sub' : 'pub'}`;
6191
- this.tracer = new Tracer(tag);
6192
- this.tracer.trace('clientDetails', clientDetails);
6193
- this.tracer.trace('create', connectionConfig);
6194
- traceRTCPeerConnection(this.pc, this.tracer.trace);
6195
- }
6196
6439
  this.pc.addEventListener('icecandidate', this.onIceCandidate);
6197
6440
  this.pc.addEventListener('icecandidateerror', this.onIceCandidateError);
6198
6441
  this.pc.addEventListener('iceconnectionstatechange', this.onIceConnectionStateChange);
6199
6442
  this.pc.addEventListener('icegatheringstatechange', this.onIceGatherChange);
6200
6443
  this.pc.addEventListener('signalingstatechange', this.onSignalingChange);
6444
+ this.pc.addEventListener('connectionstatechange', this.onConnectionStateChange);
6445
+ this.stats = new StatsTracer(this.pc, peerType, this.trackIdToTrackType);
6446
+ if (enableTracing) {
6447
+ const tag = `${logTag}-${peerType === PeerType.SUBSCRIBER ? 'sub' : 'pub'}-${sfuClient.edgeName}`;
6448
+ this.tracer = new Tracer(tag);
6449
+ this.tracer.trace('create', connectionConfig);
6450
+ traceRTCPeerConnection(this.pc, this.tracer.trace);
6451
+ }
6201
6452
  }
6202
6453
  /**
6203
6454
  * Disposes the `RTCPeerConnection` instance.
@@ -6563,7 +6814,7 @@ class Publisher extends BasePeerConnection {
6563
6814
  }
6564
6815
  else {
6565
6816
  const previousTrack = transceiver.sender.track;
6566
- await transceiver.sender.replaceTrack(trackToPublish);
6817
+ await this.updateTransceiver(transceiver, trackToPublish, trackType);
6567
6818
  if (!isReactNative()) {
6568
6819
  this.stopTrack(previousTrack);
6569
6820
  }
@@ -6585,8 +6836,20 @@ class Publisher extends BasePeerConnection {
6585
6836
  const trackType = publishOption.trackType;
6586
6837
  this.logger('debug', `Added ${TrackType[trackType]} transceiver`);
6587
6838
  this.transceiverCache.add(publishOption, transceiver);
6839
+ this.trackIdToTrackType.set(track.id, trackType);
6588
6840
  await this.negotiate();
6589
6841
  };
6842
+ /**
6843
+ * Updates the transceiver with the given track and track type.
6844
+ */
6845
+ this.updateTransceiver = async (transceiver, track, trackType) => {
6846
+ const sender = transceiver.sender;
6847
+ if (sender.track)
6848
+ this.trackIdToTrackType.delete(sender.track.id);
6849
+ await sender.replaceTrack(track);
6850
+ if (track)
6851
+ this.trackIdToTrackType.set(track.id, trackType);
6852
+ };
6590
6853
  /**
6591
6854
  * Synchronizes the current Publisher state with the provided publish options.
6592
6855
  */
@@ -6616,7 +6879,7 @@ class Publisher extends BasePeerConnection {
6616
6879
  continue;
6617
6880
  // it is safe to stop the track here, it is a clone
6618
6881
  this.stopTrack(transceiver.sender.track);
6619
- await transceiver.sender.replaceTrack(null);
6882
+ await this.updateTransceiver(transceiver, null, publishOption.trackType);
6620
6883
  }
6621
6884
  };
6622
6885
  /**
@@ -6636,18 +6899,6 @@ class Publisher extends BasePeerConnection {
6636
6899
  }
6637
6900
  return false;
6638
6901
  };
6639
- /**
6640
- * Maps the given track ID to the corresponding track type.
6641
- */
6642
- this.getTrackType = (trackId) => {
6643
- for (const transceiverId of this.transceiverCache.items()) {
6644
- const { publishOption, transceiver } = transceiverId;
6645
- if (transceiver.sender.track?.id === trackId) {
6646
- return publishOption.trackType;
6647
- }
6648
- }
6649
- return undefined;
6650
- };
6651
6902
  /**
6652
6903
  * Stops the cloned track that is being published to the SFU.
6653
6904
  */
@@ -6941,6 +7192,7 @@ class Subscriber extends BasePeerConnection {
6941
7192
  if (!trackType) {
6942
7193
  return this.logger('error', `Unknown track type: ${rawTrackType}`);
6943
7194
  }
7195
+ this.trackIdToTrackType.set(e.track.id, trackType);
6944
7196
  if (!participantToUpdate) {
6945
7197
  this.logger('warn', `[onTrack]: Received track for unknown participant: ${trackId}`, e);
6946
7198
  this.state.registerOrphanedTrack({
@@ -7284,12 +7536,22 @@ class StreamSfuClient {
7284
7536
  */
7285
7537
  this.abortController = new AbortController();
7286
7538
  this.createWebSocket = () => {
7539
+ const eventsToTrace = {
7540
+ callEnded: true,
7541
+ changePublishQuality: true,
7542
+ error: true,
7543
+ goAway: true,
7544
+ };
7287
7545
  this.signalWs = createWebSocketSignalChannel({
7288
7546
  logTag: this.logTag,
7289
7547
  endpoint: `${this.credentials.server.ws_endpoint}?tag=${this.logTag}`,
7290
7548
  onMessage: (message) => {
7291
7549
  this.lastMessageTimestamp = new Date();
7292
7550
  this.scheduleConnectionCheck();
7551
+ const eventKind = message.eventPayload.oneofKind;
7552
+ if (eventsToTrace[eventKind]) {
7553
+ this.tracer?.trace(eventKind, message);
7554
+ }
7293
7555
  this.dispatcher.dispatch(message, this.logTag);
7294
7556
  },
7295
7557
  });
@@ -7382,7 +7644,8 @@ class StreamSfuClient {
7382
7644
  };
7383
7645
  this.sendStats = async (stats) => {
7384
7646
  await this.joinTask;
7385
- return retryable(() => this.rpc.sendStats({ ...stats, sessionId: this.sessionId }), this.abortController.signal);
7647
+ // NOTE: we don't retry sending stats
7648
+ return this.rpc.sendStats({ ...stats, sessionId: this.sessionId });
7386
7649
  };
7387
7650
  this.startNoiseCancellation = async () => {
7388
7651
  await this.joinTask;
@@ -7434,7 +7697,7 @@ class StreamSfuClient {
7434
7697
  unsubscribe();
7435
7698
  current.reject(new Error('Waiting for "joinResponse" has timed out'));
7436
7699
  }, this.joinResponseTimeout);
7437
- await this.send(SfuRequest.create({
7700
+ const joinRequest = SfuRequest.create({
7438
7701
  requestPayload: {
7439
7702
  oneofKind: 'joinRequest',
7440
7703
  joinRequest: JoinRequest.create({
@@ -7443,7 +7706,9 @@ class StreamSfuClient {
7443
7706
  token: this.credentials.token,
7444
7707
  }),
7445
7708
  },
7446
- }));
7709
+ });
7710
+ this.tracer?.trace('joinRequest', joinRequest);
7711
+ await this.send(joinRequest);
7447
7712
  return current.promise;
7448
7713
  };
7449
7714
  this.ping = async () => {
@@ -7504,7 +7769,9 @@ class StreamSfuClient {
7504
7769
  this.joinResponseTimeout = joinResponseTimeout;
7505
7770
  this.logTag = logTag;
7506
7771
  this.logger = getLogger(['SfuClient', logTag]);
7507
- this.tracer = enableTracing ? new Tracer(logTag) : undefined;
7772
+ this.tracer = enableTracing
7773
+ ? new Tracer(`${logTag}-${this.edgeName}`)
7774
+ : undefined;
7508
7775
  this.rpc = createSignalClient({
7509
7776
  baseUrl: server.url,
7510
7777
  interceptors: [
@@ -7603,13 +7870,17 @@ const watchCallRejected = (call) => {
7603
7870
  .every((m) => rejectedBy[m.user_id]);
7604
7871
  if (everyoneElseRejected) {
7605
7872
  call.logger('info', 'everyone rejected, leaving the call');
7606
- await call.leave({ reason: 'ring: everyone rejected' });
7873
+ await call.leave({
7874
+ reject: true,
7875
+ reason: 'cancel',
7876
+ message: 'ring: everyone rejected',
7877
+ });
7607
7878
  }
7608
7879
  }
7609
7880
  else {
7610
7881
  if (rejectedBy[eventCall.created_by.id]) {
7611
7882
  call.logger('info', 'call creator rejected, leaving call');
7612
- await call.leave({ reason: 'ring: creator rejected' });
7883
+ await call.leave({ message: 'ring: creator rejected' });
7613
7884
  }
7614
7885
  }
7615
7886
  };
@@ -7623,7 +7894,7 @@ const watchCallEnded = (call) => {
7623
7894
  if (callingState !== exports.CallingState.IDLE &&
7624
7895
  callingState !== exports.CallingState.LEFT) {
7625
7896
  call
7626
- .leave({ reason: 'call.ended event received', reject: false })
7897
+ .leave({ message: 'call.ended event received', reject: false })
7627
7898
  .catch((err) => {
7628
7899
  call.logger('error', 'Failed to leave call after call.ended ', err);
7629
7900
  });
@@ -7643,7 +7914,7 @@ const watchSfuCallEnded = (call) => {
7643
7914
  // update the call state to reflect the call has ended.
7644
7915
  call.state.setEndedAt(new Date());
7645
7916
  const reason = CallEndedReason[e.reason];
7646
- await call.leave({ reason: `callEnded received: ${reason}` });
7917
+ await call.leave({ message: `callEnded received: ${reason}` });
7647
7918
  }
7648
7919
  catch (err) {
7649
7920
  call.logger('error', 'Failed to leave call after being ended by the SFU', err);
@@ -7710,7 +7981,7 @@ const watchLiveEnded = (dispatcher, call) => {
7710
7981
  return;
7711
7982
  call.state.setBackstage(true);
7712
7983
  if (!call.permissionsContext.hasPermission(OwnCapability.JOIN_BACKSTAGE)) {
7713
- call.leave({ reason: 'live ended' }).catch((err) => {
7984
+ call.leave({ message: 'live ended' }).catch((err) => {
7714
7985
  call.logger('error', 'Failed to leave call after live ended', err);
7715
7986
  });
7716
7987
  }
@@ -8400,7 +8671,6 @@ class DynascaleManager {
8400
8671
  const { selectedDevice } = this.speaker.state;
8401
8672
  if (selectedDevice && 'setSinkId' in audioElement) {
8402
8673
  audioElement.setSinkId(selectedDevice);
8403
- tracer.trace('navigator.mediaDevices.setSinkId', selectedDevice);
8404
8674
  }
8405
8675
  }
8406
8676
  });
@@ -8410,7 +8680,6 @@ class DynascaleManager {
8410
8680
  : this.speaker.state.selectedDevice$.subscribe((deviceId) => {
8411
8681
  if (deviceId) {
8412
8682
  audioElement.setSinkId(deviceId);
8413
- tracer.trace('navigator.mediaDevices.setSinkId', deviceId);
8414
8683
  }
8415
8684
  });
8416
8685
  const volumeSubscription = rxjs.combineLatest([
@@ -9482,25 +9751,6 @@ class InputMediaDeviceManagerState {
9482
9751
  * The default constraints for the device.
9483
9752
  */
9484
9753
  this.defaultConstraints$ = this.defaultConstraintsSubject.asObservable();
9485
- /**
9486
- * Gets the current value of an observable, or undefined if the observable has
9487
- * not emitted a value yet.
9488
- *
9489
- * @param observable$ the observable to get the value from.
9490
- */
9491
- this.getCurrentValue = getCurrentValue;
9492
- /**
9493
- * Updates the value of the provided Subject.
9494
- * An `update` can either be a new value or a function which takes
9495
- * the current value and returns a new value.
9496
- *
9497
- * @internal
9498
- *
9499
- * @param subject the subject to update.
9500
- * @param update the update to apply to the subject.
9501
- * @return the updated value.
9502
- */
9503
- this.setCurrentValue = setCurrentValue;
9504
9754
  this.hasBrowserPermission$ = permission
9505
9755
  ? permission.asObservable().pipe(rxjs.shareReplay(1))
9506
9756
  : rxjs.of(true);
@@ -9515,39 +9765,39 @@ class InputMediaDeviceManagerState {
9515
9765
  * The device status
9516
9766
  */
9517
9767
  get status() {
9518
- return this.getCurrentValue(this.status$);
9768
+ return getCurrentValue(this.status$);
9519
9769
  }
9520
9770
  /**
9521
9771
  * The requested device status. Useful for optimistic UIs
9522
9772
  */
9523
9773
  get optimisticStatus() {
9524
- return this.getCurrentValue(this.optimisticStatus$);
9774
+ return getCurrentValue(this.optimisticStatus$);
9525
9775
  }
9526
9776
  /**
9527
9777
  * The currently selected device
9528
9778
  */
9529
9779
  get selectedDevice() {
9530
- return this.getCurrentValue(this.selectedDevice$);
9780
+ return getCurrentValue(this.selectedDevice$);
9531
9781
  }
9532
9782
  /**
9533
9783
  * The current media stream, or `undefined` if the device is currently disabled.
9534
9784
  */
9535
9785
  get mediaStream() {
9536
- return this.getCurrentValue(this.mediaStream$);
9786
+ return getCurrentValue(this.mediaStream$);
9537
9787
  }
9538
9788
  /**
9539
9789
  * @internal
9540
9790
  * @param status
9541
9791
  */
9542
9792
  setStatus(status) {
9543
- this.setCurrentValue(this.statusSubject, status);
9793
+ setCurrentValue(this.statusSubject, status);
9544
9794
  }
9545
9795
  /**
9546
9796
  * @internal
9547
9797
  * @param pendingStatus
9548
9798
  */
9549
9799
  setPendingStatus(pendingStatus) {
9550
- this.setCurrentValue(this.optimisticStatusSubject, pendingStatus);
9800
+ setCurrentValue(this.optimisticStatusSubject, pendingStatus);
9551
9801
  }
9552
9802
  /**
9553
9803
  * Updates the `mediaStream` state variable.
@@ -9558,7 +9808,7 @@ class InputMediaDeviceManagerState {
9558
9808
  * as this is the stream that holds the actual deviceId information.
9559
9809
  */
9560
9810
  setMediaStream(stream, rootStream) {
9561
- this.setCurrentValue(this.mediaStreamSubject, stream);
9811
+ setCurrentValue(this.mediaStreamSubject, stream);
9562
9812
  if (rootStream) {
9563
9813
  this.setDevice(this.getDeviceIdFromStream(rootStream));
9564
9814
  }
@@ -9568,13 +9818,13 @@ class InputMediaDeviceManagerState {
9568
9818
  * @param deviceId the device id to set.
9569
9819
  */
9570
9820
  setDevice(deviceId) {
9571
- this.setCurrentValue(this.selectedDeviceSubject, deviceId);
9821
+ setCurrentValue(this.selectedDeviceSubject, deviceId);
9572
9822
  }
9573
9823
  /**
9574
9824
  * Gets the default constraints for the device.
9575
9825
  */
9576
9826
  get defaultConstraints() {
9577
- return this.getCurrentValue(this.defaultConstraints$);
9827
+ return getCurrentValue(this.defaultConstraints$);
9578
9828
  }
9579
9829
  /**
9580
9830
  * Sets the default constraints for the device.
@@ -9583,7 +9833,7 @@ class InputMediaDeviceManagerState {
9583
9833
  * @param constraints the constraints to set.
9584
9834
  */
9585
9835
  setDefaultConstraints(constraints) {
9586
- this.setCurrentValue(this.defaultConstraintsSubject, constraints);
9836
+ setCurrentValue(this.defaultConstraintsSubject, constraints);
9587
9837
  }
9588
9838
  }
9589
9839
 
@@ -9601,13 +9851,13 @@ class CameraManagerState extends InputMediaDeviceManagerState {
9601
9851
  * back - means the camera facing the environment
9602
9852
  */
9603
9853
  get direction() {
9604
- return this.getCurrentValue(this.direction$);
9854
+ return getCurrentValue(this.direction$);
9605
9855
  }
9606
9856
  /**
9607
9857
  * @internal
9608
9858
  */
9609
9859
  setDirection(direction) {
9610
- this.setCurrentValue(this.directionSubject, direction);
9860
+ setCurrentValue(this.directionSubject, direction);
9611
9861
  }
9612
9862
  /**
9613
9863
  * @internal
@@ -9780,13 +10030,13 @@ class MicrophoneManagerState extends InputMediaDeviceManagerState {
9780
10030
  * This feature is not available in the React Native SDK.
9781
10031
  */
9782
10032
  get speakingWhileMuted() {
9783
- return this.getCurrentValue(this.speakingWhileMuted$);
10033
+ return getCurrentValue(this.speakingWhileMuted$);
9784
10034
  }
9785
10035
  /**
9786
10036
  * @internal
9787
10037
  */
9788
10038
  setSpeakingWhileMuted(isSpeaking) {
9789
- this.setCurrentValue(this.speakingWhileMutedSubject, isSpeaking);
10039
+ setCurrentValue(this.speakingWhileMutedSubject, isSpeaking);
9790
10040
  }
9791
10041
  getDeviceIdFromStream(stream) {
9792
10042
  const [track] = stream.getAudioTracks();
@@ -10238,19 +10488,19 @@ class ScreenShareState extends InputMediaDeviceManagerState {
10238
10488
  * The current screen share audio status.
10239
10489
  */
10240
10490
  get audioEnabled() {
10241
- return this.getCurrentValue(this.audioEnabled$);
10491
+ return getCurrentValue(this.audioEnabled$);
10242
10492
  }
10243
10493
  /**
10244
10494
  * Set the current screen share audio status.
10245
10495
  */
10246
10496
  setAudioEnabled(isEnabled) {
10247
- this.setCurrentValue(this.audioEnabledSubject, isEnabled);
10497
+ setCurrentValue(this.audioEnabledSubject, isEnabled);
10248
10498
  }
10249
10499
  /**
10250
10500
  * The current screen share settings.
10251
10501
  */
10252
10502
  get settings() {
10253
- return this.getCurrentValue(this.settings$);
10503
+ return getCurrentValue(this.settings$);
10254
10504
  }
10255
10505
  /**
10256
10506
  * Set the current screen share settings.
@@ -10258,7 +10508,7 @@ class ScreenShareState extends InputMediaDeviceManagerState {
10258
10508
  * @param settings the screen share settings to set.
10259
10509
  */
10260
10510
  setSettings(settings) {
10261
- this.setCurrentValue(this.settingsSubject, settings);
10511
+ setCurrentValue(this.settingsSubject, settings);
10262
10512
  }
10263
10513
  }
10264
10514
 
@@ -10337,25 +10587,6 @@ class SpeakerState {
10337
10587
  * [Tells if the browser supports audio output change on 'audio' elements](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/setSinkId).
10338
10588
  */
10339
10589
  this.isDeviceSelectionSupported = checkIfAudioOutputChangeSupported();
10340
- /**
10341
- * Gets the current value of an observable, or undefined if the observable has
10342
- * not emitted a value yet.
10343
- *
10344
- * @param observable$ the observable to get the value from.
10345
- */
10346
- this.getCurrentValue = getCurrentValue;
10347
- /**
10348
- * Updates the value of the provided Subject.
10349
- * An `update` can either be a new value or a function which takes
10350
- * the current value and returns a new value.
10351
- *
10352
- * @internal
10353
- *
10354
- * @param subject the subject to update.
10355
- * @param update the update to apply to the subject.
10356
- * @return the updated value.
10357
- */
10358
- this.setCurrentValue = setCurrentValue;
10359
10590
  this.selectedDevice$ = this.selectedDeviceSubject
10360
10591
  .asObservable()
10361
10592
  .pipe(rxjs.distinctUntilChanged());
@@ -10369,7 +10600,7 @@ class SpeakerState {
10369
10600
  * Note: this feature is not supported in React Native
10370
10601
  */
10371
10602
  get selectedDevice() {
10372
- return this.getCurrentValue(this.selectedDevice$);
10603
+ return getCurrentValue(this.selectedDevice$);
10373
10604
  }
10374
10605
  /**
10375
10606
  * The currently selected volume
@@ -10377,21 +10608,22 @@ class SpeakerState {
10377
10608
  * Note: this feature is not supported in React Native
10378
10609
  */
10379
10610
  get volume() {
10380
- return this.getCurrentValue(this.volume$);
10611
+ return getCurrentValue(this.volume$);
10381
10612
  }
10382
10613
  /**
10383
10614
  * @internal
10384
10615
  * @param deviceId
10385
10616
  */
10386
10617
  setDevice(deviceId) {
10387
- this.setCurrentValue(this.selectedDeviceSubject, deviceId);
10618
+ setCurrentValue(this.selectedDeviceSubject, deviceId);
10619
+ tracer.trace('navigator.mediaDevices.setSinkId', deviceId);
10388
10620
  }
10389
10621
  /**
10390
10622
  * @internal
10391
10623
  * @param volume
10392
10624
  */
10393
10625
  setVolume(volume) {
10394
- this.setCurrentValue(this.volumeSubject, volume);
10626
+ setCurrentValue(this.volumeSubject, volume);
10395
10627
  }
10396
10628
  }
10397
10629
 
@@ -10572,7 +10804,7 @@ class Call {
10572
10804
  const currentUserId = this.currentUserId;
10573
10805
  if (currentUserId && blockedUserIds.includes(currentUserId)) {
10574
10806
  this.logger('info', 'Leaving call because of being blocked');
10575
- await this.leave({ reason: 'user blocked' }).catch((err) => {
10807
+ await this.leave({ message: 'user blocked' }).catch((err) => {
10576
10808
  this.logger('error', 'Error leaving call after being blocked', err);
10577
10809
  });
10578
10810
  }
@@ -10725,7 +10957,7 @@ class Call {
10725
10957
  /**
10726
10958
  * Leave the call and stop the media streams that were published by the call.
10727
10959
  */
10728
- this.leave = async ({ reject, reason = 'user is leaving the call', } = {}) => {
10960
+ this.leave = async ({ reject, reason, message } = {}) => {
10729
10961
  await withoutConcurrency(this.joinLeaveConcurrencyTag, async () => {
10730
10962
  const callingState = this.state.callingState;
10731
10963
  if (callingState === exports.CallingState.LEFT) {
@@ -10743,7 +10975,7 @@ class Call {
10743
10975
  }
10744
10976
  if (callingState === exports.CallingState.RINGING && reject !== false) {
10745
10977
  if (reject) {
10746
- await this.reject('decline');
10978
+ await this.reject(reason ?? 'decline');
10747
10979
  }
10748
10980
  else {
10749
10981
  // if reject was undefined, we still have to cancel the call automatically
@@ -10762,7 +10994,7 @@ class Call {
10762
10994
  this.subscriber = undefined;
10763
10995
  this.publisher?.dispose();
10764
10996
  this.publisher = undefined;
10765
- await this.sfuClient?.leaveAndClose(reason);
10997
+ await this.sfuClient?.leaveAndClose(message ?? reason ?? 'user is leaving the call');
10766
10998
  this.sfuClient = undefined;
10767
10999
  this.dynascaleManager.setSfuClient(undefined);
10768
11000
  this.state.setCallingState(exports.CallingState.LEFT);
@@ -10772,6 +11004,7 @@ class Call {
10772
11004
  this.leaveCallHooks.forEach((hook) => hook());
10773
11005
  this.initialized = false;
10774
11006
  this.hasJoinedOnce = false;
11007
+ this.unifiedSessionId = undefined;
10775
11008
  this.ringingSubject.next(false);
10776
11009
  this.cancelAutoDrop();
10777
11010
  this.clientStore.unregisterCall(this);
@@ -11199,7 +11432,6 @@ class Call {
11199
11432
  state: this.state,
11200
11433
  connectionConfig,
11201
11434
  logTag: String(this.sfuClientTag),
11202
- clientDetails,
11203
11435
  enableTracing,
11204
11436
  onUnrecoverableError: (reason) => {
11205
11437
  this.reconnect(WebsocketReconnectStrategy.REJOIN, reason).catch((err) => {
@@ -11221,7 +11453,6 @@ class Call {
11221
11453
  connectionConfig,
11222
11454
  publishOptions,
11223
11455
  logTag: String(this.sfuClientTag),
11224
- clientDetails,
11225
11456
  enableTracing,
11226
11457
  onUnrecoverableError: (reason) => {
11227
11458
  this.reconnect(WebsocketReconnectStrategy.REJOIN, reason).catch((err) => {
@@ -11240,6 +11471,7 @@ class Call {
11240
11471
  });
11241
11472
  this.sfuStatsReporter?.stop();
11242
11473
  if (statsOptions?.reporting_interval_ms > 0) {
11474
+ this.unifiedSessionId ?? (this.unifiedSessionId = sfuClient.sessionId);
11243
11475
  this.sfuStatsReporter = new SfuStatsReporter(sfuClient, {
11244
11476
  clientDetails,
11245
11477
  options: statsOptions,
@@ -11248,6 +11480,7 @@ class Call {
11248
11480
  microphone: this.microphone,
11249
11481
  camera: this.camera,
11250
11482
  state: this.state,
11483
+ unifiedSessionId: this.unifiedSessionId,
11251
11484
  });
11252
11485
  this.sfuStatsReporter.start();
11253
11486
  }
@@ -11469,7 +11702,7 @@ class Call {
11469
11702
  if (strategy === WebsocketReconnectStrategy.UNSPECIFIED)
11470
11703
  return;
11471
11704
  if (strategy === WebsocketReconnectStrategy.DISCONNECT) {
11472
- this.leave({ reason: 'SFU instructed to disconnect' }).catch((err) => {
11705
+ this.leave({ message: 'SFU instructed to disconnect' }).catch((err) => {
11473
11706
  this.logger('warn', `Can't leave call after disconnect request`, err);
11474
11707
  });
11475
11708
  }
@@ -11617,6 +11850,11 @@ class Call {
11617
11850
  trackTypes.push(TrackType.SCREEN_SHARE_AUDIO);
11618
11851
  }
11619
11852
  }
11853
+ if (track.kind === 'video') {
11854
+ // schedules calibration report - the SFU will use the performance stats
11855
+ // to adjust the quality thresholds as early as possible
11856
+ this.sfuStatsReporter?.scheduleOne(3000);
11857
+ }
11620
11858
  await this.updateLocalStreamState(mediaStream, ...trackTypes);
11621
11859
  };
11622
11860
  /**
@@ -12099,7 +12337,11 @@ class Call {
12099
12337
  // e.g. it was already accepted and joined
12100
12338
  if (this.state.callingState !== exports.CallingState.RINGING)
12101
12339
  return;
12102
- this.leave({ reject: true, reason: 'timeout' }).catch((err) => {
12340
+ this.leave({
12341
+ reject: true,
12342
+ reason: 'timeout',
12343
+ message: `ringing timeout - ${this.isCreatedByMe ? 'no one accepted' : `user didn't interact with incoming call screen`}`,
12344
+ }).catch((err) => {
12103
12345
  this.logger('error', 'Failed to drop call', err);
12104
12346
  });
12105
12347
  }, timeoutInMs);
@@ -13472,7 +13714,7 @@ class StreamClient {
13472
13714
  this.getUserAgent = () => {
13473
13715
  if (!this.cachedUserAgent) {
13474
13716
  const { clientAppIdentifier = {} } = this.options;
13475
- const { sdkName = 'js', sdkVersion = "1.20.1", ...extras } = clientAppIdentifier;
13717
+ const { sdkName = 'js', sdkVersion = "1.21.0", ...extras } = clientAppIdentifier;
13476
13718
  this.cachedUserAgent = [
13477
13719
  `stream-video-${sdkName}-v${sdkVersion}`,
13478
13720
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),