@stream-io/video-client 1.42.2 → 1.43.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
4
4
 
5
+ ## [1.43.0](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.42.3...@stream-io/video-client-1.43.0) (2026-02-20)
6
+
7
+ - **client:** trace updatePublishOptions overrides ([#2136](https://github.com/GetStream/stream-video-js/issues/2136)) ([bcc1e92](https://github.com/GetStream/stream-video-js/commit/bcc1e92ac89374324a57d1df85be38a2661a4c53))
8
+
9
+ ### Features
10
+
11
+ - **client:** add list recording APIs and deprecate query methods ([#2135](https://github.com/GetStream/stream-video-js/issues/2135)) ([5331cb5](https://github.com/GetStream/stream-video-js/commit/5331cb5205466dc052c729fb07d84209208af362))
12
+
13
+ ### Bug Fixes
14
+
15
+ - **client:** harden flat-line no-audio detection ([#2131](https://github.com/GetStream/stream-video-js/issues/2131)) ([9c2aa22](https://github.com/GetStream/stream-video-js/commit/9c2aa222b189c5e24510430dfddbf164555abf1c))
16
+ - **client:** prevent stale speaking-while-muted detector ([#2130](https://github.com/GetStream/stream-video-js/issues/2130)) ([e5c408d](https://github.com/GetStream/stream-video-js/commit/e5c408d73de1b8f20e775642b0b19eb0ffd979a8))
17
+
18
+ ## [1.42.3](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.42.2...@stream-io/video-client-1.42.3) (2026-02-16)
19
+
20
+ ### Bug Fixes
21
+
22
+ - guard from parallel accept/reject invocations ([#2127](https://github.com/GetStream/stream-video-js/issues/2127)) ([621218f](https://github.com/GetStream/stream-video-js/commit/621218f4ab6b4623370fd66f1b02b8cb7cb1baad))
23
+
5
24
  ## [1.42.2](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.42.1...@stream-io/video-client-1.42.2) (2026-02-13)
6
25
 
7
26
  ### Bug Fixes
@@ -6231,7 +6231,7 @@ const getSdkVersion = (sdk) => {
6231
6231
  return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
6232
6232
  };
6233
6233
 
6234
- const version = "1.42.2";
6234
+ const version = "1.43.0";
6235
6235
  const [major, minor, patch] = version.split('.');
6236
6236
  let sdkInfo = {
6237
6237
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -11480,12 +11480,21 @@ const createSoundDetector = (audioStream, onSoundDetectedStateChanged, options =
11480
11480
  };
11481
11481
 
11482
11482
  /**
11483
- * Analyzes frequency data to determine if audio is being captured.
11483
+ * Analyzes time-domain waveform data to determine if audio is being captured.
11484
+ * Uses the waveform RMS around the 128 midpoint for robust silence detection.
11484
11485
  */
11485
- const hasAudio = (analyser, threshold) => {
11486
- const data = new Uint8Array(analyser.frequencyBinCount);
11487
- analyser.getByteFrequencyData(data);
11488
- return data.some((value) => value > threshold);
11486
+ const hasAudio = (analyser) => {
11487
+ const data = new Uint8Array(analyser.fftSize);
11488
+ analyser.getByteTimeDomainData(data);
11489
+ let squareSum = 0;
11490
+ for (const sample of data) {
11491
+ const centered = sample - 128;
11492
+ // Ignore tiny quantization/jitter around midpoint (e.g. 127/128 samples).
11493
+ const signal = Math.abs(centered) <= 1 ? 0 : centered;
11494
+ squareSum += signal * signal;
11495
+ }
11496
+ const rms = Math.sqrt(squareSum / data.length);
11497
+ return rms > 0;
11489
11498
  };
11490
11499
  /** Helper for "no event" transitions */
11491
11500
  const noEmit = (nextState) => ({
@@ -11499,9 +11508,9 @@ const emit = (capturesAudio, nextState) => ({ shouldEmit: true, nextState, captu
11499
11508
  */
11500
11509
  const transitionState = (state, audioDetected, options) => {
11501
11510
  if (audioDetected) {
11502
- return state.kind === 'IDLE' || state.kind === 'EMITTING'
11503
- ? emit(true, state)
11504
- : noEmit(state);
11511
+ // Any observed audio means the microphone is capturing.
11512
+ // Emit recovery/success and let the caller stop the detector.
11513
+ return emit(true, { kind: 'IDLE' });
11505
11514
  }
11506
11515
  const { noAudioThresholdMs, emitIntervalMs } = options;
11507
11516
  const now = Date.now();
@@ -11542,16 +11551,17 @@ const createAudioAnalyzer = (audioStream, fftSize) => {
11542
11551
  * @returns a cleanup function which once invoked stops the no-audio detector.
11543
11552
  */
11544
11553
  const createNoAudioDetector = (audioStream, options) => {
11545
- const { detectionFrequencyInMs = 350, audioLevelThreshold = 0, fftSize = 256, onCaptureStatusChange, } = options;
11554
+ const { detectionFrequencyInMs = 350, fftSize = 512, onCaptureStatusChange, } = options;
11546
11555
  let state = { kind: 'IDLE' };
11547
11556
  const { audioContext, analyser } = createAudioAnalyzer(audioStream, fftSize);
11548
11557
  const detectionIntervalId = setInterval(() => {
11549
- const [audioTrack] = audioStream.getAudioTracks();
11550
- if (!audioTrack?.enabled || audioTrack.readyState === 'ended') {
11558
+ const [track] = audioStream.getAudioTracks();
11559
+ if (track && !track.enabled) {
11551
11560
  state = { kind: 'IDLE' };
11552
11561
  return;
11553
11562
  }
11554
- const audioDetected = hasAudio(analyser, audioLevelThreshold);
11563
+ // Missing or ended track is treated as no-audio to surface abrupt capture loss.
11564
+ const audioDetected = track?.readyState === 'live' && hasAudio(analyser);
11555
11565
  const transition = transitionState(state, audioDetected, options);
11556
11566
  state = transition.nextState;
11557
11567
  if (!transition.shouldEmit)
@@ -12024,6 +12034,9 @@ class MicrophoneManager extends AudioDeviceManager {
12024
12034
  }
12025
12035
  async startSpeakingWhileMutedDetection(deviceId) {
12026
12036
  await withoutConcurrency(this.soundDetectorConcurrencyTag, async () => {
12037
+ if (this.soundDetectorCleanup && this.soundDetectorDeviceId === deviceId)
12038
+ return;
12039
+ await this.teardownSpeakingWhileMutedDetection();
12027
12040
  if (isReactNative()) {
12028
12041
  this.rnSpeechDetector = new RNSpeechDetector();
12029
12042
  const unsubscribe = await this.rnSpeechDetector.start((event) => {
@@ -12044,16 +12057,23 @@ class MicrophoneManager extends AudioDeviceManager {
12044
12057
  this.state.setSpeakingWhileMuted(event.isSoundDetected);
12045
12058
  });
12046
12059
  }
12060
+ this.soundDetectorDeviceId = deviceId;
12047
12061
  });
12048
12062
  }
12049
12063
  async stopSpeakingWhileMutedDetection() {
12050
12064
  await withoutConcurrency(this.soundDetectorConcurrencyTag, async () => {
12051
- if (!this.soundDetectorCleanup)
12052
- return;
12053
- const soundDetectorCleanup = this.soundDetectorCleanup;
12054
- this.soundDetectorCleanup = undefined;
12055
- this.state.setSpeakingWhileMuted(false);
12056
- await soundDetectorCleanup();
12065
+ return this.teardownSpeakingWhileMutedDetection();
12066
+ });
12067
+ }
12068
+ async teardownSpeakingWhileMutedDetection() {
12069
+ const soundDetectorCleanup = this.soundDetectorCleanup;
12070
+ this.soundDetectorCleanup = undefined;
12071
+ this.soundDetectorDeviceId = undefined;
12072
+ this.state.setSpeakingWhileMuted(false);
12073
+ if (!soundDetectorCleanup)
12074
+ return;
12075
+ await soundDetectorCleanup().catch((err) => {
12076
+ this.logger.warn('Failed to stop speaking while muted detector', err);
12057
12077
  });
12058
12078
  }
12059
12079
  async hasPermission(permissionState) {
@@ -12446,6 +12466,7 @@ class Call {
12446
12466
  this.hasJoinedOnce = false;
12447
12467
  this.deviceSettingsAppliedOnce = false;
12448
12468
  this.initialized = false;
12469
+ this.acceptRejectConcurrencyTag = Symbol('acceptRejectTag');
12449
12470
  this.joinLeaveConcurrencyTag = Symbol('joinLeaveConcurrencyTag');
12450
12471
  /**
12451
12472
  * A list hooks/functions to invoke when the call is left.
@@ -12860,8 +12881,10 @@ class Call {
12860
12881
  * Unless you are implementing a custom "ringing" flow, you should not use this method.
12861
12882
  */
12862
12883
  this.accept = async () => {
12863
- this.tracer.trace('call.accept', '');
12864
- return this.streamClient.post(`${this.streamClientBasePath}/accept`);
12884
+ return withoutConcurrency(this.acceptRejectConcurrencyTag, () => {
12885
+ this.tracer.trace('call.accept', '');
12886
+ return this.streamClient.post(`${this.streamClientBasePath}/accept`);
12887
+ });
12865
12888
  };
12866
12889
  /**
12867
12890
  * Marks the incoming call as rejected.
@@ -12873,8 +12896,10 @@ class Call {
12873
12896
  * @param reason the reason for rejecting the call.
12874
12897
  */
12875
12898
  this.reject = async (reason = 'decline') => {
12876
- this.tracer.trace('call.reject', reason);
12877
- return this.streamClient.post(`${this.streamClientBasePath}/reject`, { reason: reason });
12899
+ return withoutConcurrency(this.acceptRejectConcurrencyTag, () => {
12900
+ this.tracer.trace('call.reject', reason);
12901
+ return this.streamClient.post(`${this.streamClientBasePath}/reject`, { reason });
12902
+ });
12878
12903
  };
12879
12904
  /**
12880
12905
  * Will start to watch for call related WebSocket events and initiate a call session with the server.
@@ -13701,6 +13726,7 @@ class Call {
13701
13726
  if (this.state.callingState === CallingState.JOINED) {
13702
13727
  this.logger.warn('Updating publish options after joining the call does not have an effect');
13703
13728
  }
13729
+ this.tracer.trace('updatePublishOptions', options);
13704
13730
  this.clientPublishOptions = { ...this.clientPublishOptions, ...options };
13705
13731
  };
13706
13732
  /**
@@ -14126,6 +14152,15 @@ class Call {
14126
14152
  type: this.type,
14127
14153
  });
14128
14154
  };
14155
+ /**
14156
+ * Query call participants with optional filters.
14157
+ *
14158
+ * @param data the request data.
14159
+ * @param params optional query parameters.
14160
+ */
14161
+ this.queryParticipants = async (data = {}, params = {}) => {
14162
+ return this.streamClient.post(`${this.streamClientBasePath}/participants`, data, params);
14163
+ };
14129
14164
  /**
14130
14165
  * Will update the call members.
14131
14166
  *
@@ -14180,20 +14215,59 @@ class Call {
14180
14215
  * Otherwise, all recordings for the current call will be returned.
14181
14216
  *
14182
14217
  * @param callSessionId the call session id to retrieve recordings for.
14218
+ * @deprecated use {@link listRecordings} instead.
14183
14219
  */
14184
14220
  this.queryRecordings = async (callSessionId) => {
14221
+ return this.listRecordings(callSessionId);
14222
+ };
14223
+ /**
14224
+ * Retrieves the list of recordings for the current call or call session.
14225
+ *
14226
+ * If `callSessionId` is provided, it will return the recordings for that call session.
14227
+ * Otherwise, all recordings for the current call will be returned.
14228
+ *
14229
+ * @param callSessionId the call session id to retrieve recordings for.
14230
+ */
14231
+ this.listRecordings = async (callSessionId) => {
14185
14232
  let endpoint = this.streamClientBasePath;
14186
14233
  if (callSessionId) {
14187
14234
  endpoint = `${endpoint}/${callSessionId}`;
14188
14235
  }
14189
14236
  return this.streamClient.get(`${endpoint}/recordings`);
14190
14237
  };
14238
+ /**
14239
+ * Deletes a recording for the given call session.
14240
+ *
14241
+ * @param callSessionId the call session id that the recording belongs to.
14242
+ * @param filename the recording filename.
14243
+ */
14244
+ this.deleteRecording = async (callSessionId, filename) => {
14245
+ return this.streamClient.delete(`${this.streamClientBasePath}/${encodeURIComponent(callSessionId)}/recordings/${encodeURIComponent(filename)}`);
14246
+ };
14247
+ /**
14248
+ * Deletes a transcription for the given call session.
14249
+ *
14250
+ * @param callSessionId the call session id that the transcription belongs to.
14251
+ * @param filename the transcription filename.
14252
+ */
14253
+ this.deleteTranscription = async (callSessionId, filename) => {
14254
+ return this.streamClient.delete(`${this.streamClientBasePath}/${encodeURIComponent(callSessionId)}/transcriptions/${encodeURIComponent(filename)}`);
14255
+ };
14191
14256
  /**
14192
14257
  * Retrieves the list of transcriptions for the current call.
14193
14258
  *
14194
14259
  * @returns the list of transcriptions.
14260
+ * @deprecated use {@link listTranscriptions} instead.
14195
14261
  */
14196
14262
  this.queryTranscriptions = async () => {
14263
+ return this.listTranscriptions();
14264
+ };
14265
+ /**
14266
+ * Retrieves the list of transcriptions for the current call.
14267
+ *
14268
+ * @returns the list of transcriptions.
14269
+ */
14270
+ this.listTranscriptions = async () => {
14197
14271
  return this.streamClient.get(`${this.streamClientBasePath}/transcriptions`);
14198
14272
  };
14199
14273
  /**
@@ -15596,7 +15670,7 @@ class StreamClient {
15596
15670
  this.getUserAgent = () => {
15597
15671
  if (!this.cachedUserAgent) {
15598
15672
  const { clientAppIdentifier = {} } = this.options;
15599
- const { sdkName = 'js', sdkVersion = "1.42.2", ...extras } = clientAppIdentifier;
15673
+ const { sdkName = 'js', sdkVersion = "1.43.0", ...extras } = clientAppIdentifier;
15600
15674
  this.cachedUserAgent = [
15601
15675
  `stream-video-${sdkName}-v${sdkVersion}`,
15602
15676
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),