@stream-io/video-client 1.46.1 → 1.48.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,22 @@
2
2
 
3
3
  This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
4
4
 
5
+ ## [1.48.0](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.47.0...@stream-io/video-client-1.48.0) (2026-04-28)
6
+
7
+ ### Features
8
+
9
+ - **rn:** remove peer connection usage in speech detection ([#2200](https://github.com/GetStream/stream-video-js/issues/2200)) ([1c73d10](https://github.com/GetStream/stream-video-js/commit/1c73d10cc25761c08a8f9350e44137afaee33acf))
10
+
11
+ ## [1.47.0](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.46.1...@stream-io/video-client-1.47.0) (2026-04-15)
12
+
13
+ ### Features
14
+
15
+ - **client:** JoinCall with hints for high scale livestream ([#2199](https://github.com/GetStream/stream-video-js/issues/2199)) ([704681a](https://github.com/GetStream/stream-video-js/commit/704681ad9ce7a0013325b6db91644e1907d0db0b))
16
+
17
+ ### Bug Fixes
18
+
19
+ - **client:** align device preference persistence with permission and track end events ([#2196](https://github.com/GetStream/stream-video-js/issues/2196)) ([b4ed7c2](https://github.com/GetStream/stream-video-js/commit/b4ed7c2c6bc6fb6777a411b69747ccc36aa82f44))
20
+
5
21
  ## [1.46.1](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.46.0...@stream-io/video-client-1.46.1) (2026-04-09)
6
22
 
7
23
  - remove listeners and stop even on permission error - rn speech detector ([f4fdd9e](https://github.com/GetStream/stream-video-js/commit/f4fdd9e1a008b52011ef18562152aad60a1f7936))
@@ -6284,7 +6284,7 @@ const getSdkVersion = (sdk) => {
6284
6284
  return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
6285
6285
  };
6286
6286
 
6287
- const version = "1.46.1";
6287
+ const version = "1.48.0";
6288
6288
  const [major, minor, patch] = version.split('.');
6289
6289
  let sdkInfo = {
6290
6290
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -10865,8 +10865,14 @@ class DeviceManager {
10865
10865
  this.handleDisconnectedOrReplacedDevices();
10866
10866
  }
10867
10867
  if (this.devicePersistence.enabled) {
10868
- this.subscriptions.push(createSubscription(combineLatest([this.state.selectedDevice$, this.state.status$]), ([selectedDevice, status]) => {
10869
- if (!status)
10868
+ this.subscriptions.push(createSubscription(combineLatest([
10869
+ this.state.selectedDevice$,
10870
+ this.state.status$,
10871
+ this.state.browserPermissionState$,
10872
+ ]), ([selectedDevice, status, browserPermissionState]) => {
10873
+ if (!status ||
10874
+ (this.isTrackStoppedDueToTrackEnd && status === 'disabled') ||
10875
+ browserPermissionState !== 'granted')
10870
10876
  return;
10871
10877
  this.persistPreference(selectedDevice, status);
10872
10878
  }));
@@ -11631,7 +11637,10 @@ class CameraManager extends DeviceManager {
11631
11637
  const shouldApplyDefaults = this.state.status === undefined &&
11632
11638
  this.state.optimisticStatus === undefined;
11633
11639
  let persistedPreferencesApplied = false;
11634
- if (shouldApplyDefaults && this.devicePersistence.enabled) {
11640
+ const permissionState = await firstValueFrom(this.state.browserPermissionState$);
11641
+ if (shouldApplyDefaults &&
11642
+ this.devicePersistence.enabled &&
11643
+ permissionState === 'granted') {
11635
11644
  persistedPreferencesApplied =
11636
11645
  await this.applyPersistedPreferences(enabledInCallType);
11637
11646
  }
@@ -11923,192 +11932,6 @@ const createNoAudioDetector = (audioStream, options) => {
11923
11932
  return stop;
11924
11933
  };
11925
11934
 
11926
- class RNSpeechDetector {
11927
- constructor(externalAudioStream) {
11928
- this.pc1 = new RTCPeerConnection({});
11929
- this.pc2 = new RTCPeerConnection({});
11930
- this.isStopped = false;
11931
- this.externalAudioStream = externalAudioStream;
11932
- }
11933
- /**
11934
- * Starts the speech detection.
11935
- */
11936
- async start(onSoundDetectedStateChanged) {
11937
- let detachListeners;
11938
- let unsubscribe;
11939
- try {
11940
- this.isStopped = false;
11941
- const audioStream = this.externalAudioStream != null
11942
- ? this.externalAudioStream
11943
- : await navigator.mediaDevices.getUserMedia({ audio: true });
11944
- this.audioStream = audioStream;
11945
- const onPc1IceCandidate = (e) => {
11946
- this.forwardIceCandidate(this.pc2, e.candidate);
11947
- };
11948
- const onPc2IceCandidate = (e) => {
11949
- this.forwardIceCandidate(this.pc1, e.candidate);
11950
- };
11951
- const onTrackPc2 = (e) => {
11952
- e.streams[0].getTracks().forEach((track) => {
11953
- // In RN, the remote track is automatically added to the audio output device
11954
- // so we need to mute it to avoid hearing the audio back
11955
- // @ts-expect-error _setVolume is a private method in react-native-webrtc
11956
- track._setVolume(0);
11957
- });
11958
- };
11959
- this.pc1.addEventListener('icecandidate', onPc1IceCandidate);
11960
- this.pc2.addEventListener('icecandidate', onPc2IceCandidate);
11961
- this.pc2.addEventListener('track', onTrackPc2);
11962
- detachListeners = () => {
11963
- this.pc1.removeEventListener('icecandidate', onPc1IceCandidate);
11964
- this.pc2.removeEventListener('icecandidate', onPc2IceCandidate);
11965
- this.pc2.removeEventListener('track', onTrackPc2);
11966
- };
11967
- audioStream
11968
- .getTracks()
11969
- .forEach((track) => this.pc1.addTrack(track, audioStream));
11970
- const offer = await this.pc1.createOffer({});
11971
- await this.pc2.setRemoteDescription(offer);
11972
- await this.pc1.setLocalDescription(offer);
11973
- const answer = await this.pc2.createAnswer();
11974
- await this.pc1.setRemoteDescription(answer);
11975
- await this.pc2.setLocalDescription(answer);
11976
- unsubscribe = this.onSpeakingDetectedStateChange(onSoundDetectedStateChanged);
11977
- return () => {
11978
- detachListeners?.();
11979
- unsubscribe?.();
11980
- this.stop();
11981
- };
11982
- }
11983
- catch (error) {
11984
- detachListeners?.();
11985
- unsubscribe?.();
11986
- this.stop();
11987
- const logger = videoLoggerSystem.getLogger('RNSpeechDetector');
11988
- logger.error('error handling permissions: ', error);
11989
- return () => { };
11990
- }
11991
- }
11992
- /**
11993
- * Stops the speech detection and releases all allocated resources.
11994
- */
11995
- stop() {
11996
- if (this.isStopped)
11997
- return;
11998
- this.isStopped = true;
11999
- this.pc1.close();
12000
- this.pc2.close();
12001
- if (this.externalAudioStream != null) {
12002
- this.externalAudioStream = undefined;
12003
- }
12004
- else {
12005
- this.cleanupAudioStream();
12006
- }
12007
- }
12008
- /**
12009
- * Public method that detects the audio levels and returns the status.
12010
- */
12011
- onSpeakingDetectedStateChange(onSoundDetectedStateChanged) {
12012
- const initialBaselineNoiseLevel = 0.13;
12013
- let baselineNoiseLevel = initialBaselineNoiseLevel;
12014
- let speechDetected = false;
12015
- let speechTimer;
12016
- let silenceTimer;
12017
- const audioLevelHistory = []; // Store recent audio levels for smoother detection
12018
- const historyLength = 10;
12019
- const silenceThreshold = 1.1;
12020
- const resetThreshold = 0.9;
12021
- const speechTimeout = 500; // Speech is set to true after 500ms of audio detection
12022
- const silenceTimeout = 5000; // Reset baseline after 5 seconds of silence
12023
- const checkAudioLevel = async () => {
12024
- try {
12025
- const stats = await this.pc1.getStats();
12026
- const report = flatten(stats);
12027
- // Audio levels are present inside stats of type `media-source` and of kind `audio`
12028
- const audioMediaSourceStats = report.find((stat) => stat.type === 'media-source' &&
12029
- stat.kind === 'audio');
12030
- if (audioMediaSourceStats) {
12031
- const { audioLevel } = audioMediaSourceStats;
12032
- if (audioLevel) {
12033
- // Update audio level history (with max historyLength sized array)
12034
- audioLevelHistory.push(audioLevel);
12035
- if (audioLevelHistory.length > historyLength) {
12036
- audioLevelHistory.shift();
12037
- }
12038
- // Calculate average audio level
12039
- const avgAudioLevel = audioLevelHistory.reduce((a, b) => a + b, 0) /
12040
- audioLevelHistory.length;
12041
- // Update baseline (if necessary) based on silence detection
12042
- if (avgAudioLevel < baselineNoiseLevel * silenceThreshold) {
12043
- if (!silenceTimer) {
12044
- silenceTimer = setTimeout(() => {
12045
- baselineNoiseLevel = Math.min(avgAudioLevel * resetThreshold, initialBaselineNoiseLevel);
12046
- }, silenceTimeout);
12047
- }
12048
- }
12049
- else {
12050
- clearTimeout(silenceTimer);
12051
- silenceTimer = undefined;
12052
- }
12053
- // Speech detection with hysteresis
12054
- if (avgAudioLevel > baselineNoiseLevel * 1.5) {
12055
- if (!speechDetected) {
12056
- speechDetected = true;
12057
- onSoundDetectedStateChanged({
12058
- isSoundDetected: true,
12059
- audioLevel,
12060
- });
12061
- }
12062
- clearTimeout(speechTimer);
12063
- speechTimer = setTimeout(() => {
12064
- speechDetected = false;
12065
- onSoundDetectedStateChanged({
12066
- isSoundDetected: false,
12067
- audioLevel: 0,
12068
- });
12069
- }, speechTimeout);
12070
- }
12071
- }
12072
- }
12073
- }
12074
- catch (error) {
12075
- const logger = videoLoggerSystem.getLogger('RNSpeechDetector');
12076
- logger.error('error checking audio level from stats', error);
12077
- }
12078
- };
12079
- const intervalId = setInterval(checkAudioLevel, 250);
12080
- return () => {
12081
- clearInterval(intervalId);
12082
- clearTimeout(speechTimer);
12083
- clearTimeout(silenceTimer);
12084
- };
12085
- }
12086
- cleanupAudioStream() {
12087
- if (!this.audioStream) {
12088
- return;
12089
- }
12090
- this.audioStream.getTracks().forEach((track) => track.stop());
12091
- if (
12092
- // @ts-expect-error release() is present in react-native-webrtc
12093
- typeof this.audioStream.release === 'function') {
12094
- // @ts-expect-error called to dispose the stream in RN
12095
- this.audioStream.release();
12096
- }
12097
- }
12098
- forwardIceCandidate(destination, candidate) {
12099
- if (this.isStopped ||
12100
- !candidate ||
12101
- destination.signalingState === 'closed') {
12102
- return;
12103
- }
12104
- destination.addIceCandidate(candidate).catch(() => {
12105
- // silently ignore the error
12106
- const logger = videoLoggerSystem.getLogger('RNSpeechDetector');
12107
- logger.info('cannot add ice candidate - ignoring');
12108
- });
12109
- }
12110
- }
12111
-
12112
11935
  class MicrophoneManager extends AudioDeviceManager {
12113
11936
  constructor(call, devicePersistence, disableMode = 'stop-tracks') {
12114
11937
  super(call, new MicrophoneManagerState(disableMode, call.tracer), TrackType.AUDIO, devicePersistence);
@@ -12373,7 +12196,10 @@ class MicrophoneManager extends AudioDeviceManager {
12373
12196
  const shouldApplyDefaults = this.state.status === undefined &&
12374
12197
  this.state.optimisticStatus === undefined;
12375
12198
  let persistedPreferencesApplied = false;
12376
- if (shouldApplyDefaults && this.devicePersistence.enabled) {
12199
+ const permissionState = await firstValueFrom(this.state.browserPermissionState$);
12200
+ if (shouldApplyDefaults &&
12201
+ this.devicePersistence.enabled &&
12202
+ permissionState === 'granted') {
12377
12203
  persistedPreferencesApplied = await this.applyPersistedPreferences(true);
12378
12204
  }
12379
12205
  const canPublish = this.call.permissionsContext.canPublish(this.trackType);
@@ -12418,13 +12244,16 @@ class MicrophoneManager extends AudioDeviceManager {
12418
12244
  return;
12419
12245
  await this.teardownSpeakingWhileMutedDetection();
12420
12246
  if (isReactNative()) {
12421
- this.rnSpeechDetector = new RNSpeechDetector();
12422
- const unsubscribe = await this.rnSpeechDetector.start((event) => {
12247
+ const speechActivity = globalThis.streamRNVideoSDK?.nativeEvents?.speechActivity;
12248
+ if (!speechActivity) {
12249
+ this.logger.warn('Native speech activity not available, make sure the "@stream-io/react-native-webrtc" peer dependency version is satisfied');
12250
+ return;
12251
+ }
12252
+ const unsubscribe = speechActivity.subscribe((event) => {
12423
12253
  this.state.setSpeakingWhileMuted(event.isSoundDetected);
12424
12254
  });
12425
12255
  this.soundDetectorCleanup = async () => {
12426
12256
  unsubscribe();
12427
- this.rnSpeechDetector = undefined;
12428
12257
  };
12429
12258
  }
12430
12259
  else {
@@ -12757,7 +12586,12 @@ class SpeakerManager {
12757
12586
  }));
12758
12587
  }
12759
12588
  if (!isReactNative() && this.devicePersistence.enabled) {
12760
- this.subscriptions.push(createSubscription(this.state.selectedDevice$, (selectedDevice) => {
12589
+ this.subscriptions.push(createSubscription(combineLatest([
12590
+ this.state.selectedDevice$,
12591
+ getAudioBrowserPermission(this.call.tracer).asStateObservable(),
12592
+ ]), ([selectedDevice, browserPermissionState]) => {
12593
+ if (!selectedDevice || browserPermissionState !== 'granted')
12594
+ return;
12761
12595
  this.persistSpeakerDevicePreference(selectedDevice);
12762
12596
  }));
12763
12597
  }
@@ -16134,7 +15968,7 @@ class StreamClient {
16134
15968
  this.getUserAgent = () => {
16135
15969
  if (!this.cachedUserAgent) {
16136
15970
  const { clientAppIdentifier = {} } = this.options;
16137
- const { sdkName = 'js', sdkVersion = "1.46.1", ...extras } = clientAppIdentifier;
15971
+ const { sdkName = 'js', sdkVersion = "1.48.0", ...extras } = clientAppIdentifier;
16138
15972
  this.cachedUserAgent = [
16139
15973
  `stream-video-${sdkName}-v${sdkVersion}`,
16140
15974
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
@@ -16770,5 +16604,5 @@ const humanize = (n) => {
16770
16604
  return String(n);
16771
16605
  };
16772
16606
 
16773
- export { AudioSettingsRequestDefaultDeviceEnum, AudioSettingsResponseDefaultDeviceEnum, browsers as Browsers, Call, CallRecordingFailedEventRecordingTypeEnum, CallRecordingReadyEventRecordingTypeEnum, CallRecordingStartedEventRecordingTypeEnum, CallRecordingStoppedEventRecordingTypeEnum, CallState, CallType, CallTypes, CallingState, CameraManager, CameraManagerState, CreateDeviceRequestPushProviderEnum, DebounceType, DeviceManager, DeviceManagerState, DynascaleManager, ErrorFromResponse, FrameRecordingSettingsRequestModeEnum, FrameRecordingSettingsRequestQualityEnum, FrameRecordingSettingsResponseModeEnum, IndividualRecordingSettingsRequestModeEnum, IndividualRecordingSettingsResponseModeEnum, IngressAudioEncodingOptionsRequestChannelsEnum, IngressSourceRequestFpsEnum, IngressVideoLayerRequestCodecEnum, LayoutSettingsRequestNameEnum, MicrophoneManager, MicrophoneManagerState, NoiseCancellationSettingsModeEnum, OwnCapability, RNSpeechDetector, RTMPBroadcastRequestQualityEnum, RTMPSettingsRequestQualityEnum, RawRecordingSettingsRequestModeEnum, RawRecordingSettingsResponseModeEnum, RecordSettingsRequestModeEnum, RecordSettingsRequestQualityEnum, rxUtils as RxUtils, ScreenShareManager, ScreenShareState, events as SfuEvents, SfuJoinError, models as SfuModels, SpeakerManager, SpeakerState, StartClosedCaptionsRequestLanguageEnum, StartTranscriptionRequestLanguageEnum, StreamSfuClient, StreamVideoClient, StreamVideoReadOnlyStateStore, StreamVideoWriteableStateStore, TranscriptionSettingsRequestClosedCaptionModeEnum, TranscriptionSettingsRequestLanguageEnum, TranscriptionSettingsRequestModeEnum, TranscriptionSettingsResponseClosedCaptionModeEnum, TranscriptionSettingsResponseLanguageEnum, TranscriptionSettingsResponseModeEnum, VideoSettingsRequestCameraFacingEnum, VideoSettingsResponseCameraFacingEnum, ViewportTracker, VisibilityState, checkIfAudioOutputChangeSupported, combineComparators, conditional, createSoundDetector, defaultSortPreset, descending, deviceIds$, disposeOfMediaStream, dominantSpeaker, getAudioBrowserPermission, getAudioDevices, getAudioOutputDevices, getAudioStream, getClientDetails, getDeviceState, getScreenShareStream, getSdkInfo, getVideoBrowserPermission, getVideoDevices, getVideoStream, getWebRTCInfo, hasAudio$1 as hasAudio, hasPausedTrack, hasScreenShare, hasScreenShareAudio, hasVideo, humanize, isPinned, livestreamOrAudioRoomSortPreset, logToConsole, name, noopComparator, paginatedLayoutSortPreset, pinned, publishingAudio, publishingVideo, reactionType, resolveDeviceId, role, screenSharing, setDeviceInfo, setOSInfo, setPowerState, setSdkInfo, setThermalState, setWebRTCInfo, speakerLayoutSortPreset, speaking, videoLoggerSystem, withParticipantSource };
16607
+ export { AudioSettingsRequestDefaultDeviceEnum, AudioSettingsResponseDefaultDeviceEnum, browsers as Browsers, Call, CallRecordingFailedEventRecordingTypeEnum, CallRecordingReadyEventRecordingTypeEnum, CallRecordingStartedEventRecordingTypeEnum, CallRecordingStoppedEventRecordingTypeEnum, CallState, CallType, CallTypes, CallingState, CameraManager, CameraManagerState, CreateDeviceRequestPushProviderEnum, DebounceType, DeviceManager, DeviceManagerState, DynascaleManager, ErrorFromResponse, FrameRecordingSettingsRequestModeEnum, FrameRecordingSettingsRequestQualityEnum, FrameRecordingSettingsResponseModeEnum, IndividualRecordingSettingsRequestModeEnum, IndividualRecordingSettingsResponseModeEnum, IngressAudioEncodingOptionsRequestChannelsEnum, IngressSourceRequestFpsEnum, IngressVideoLayerRequestCodecEnum, LayoutSettingsRequestNameEnum, MicrophoneManager, MicrophoneManagerState, NoiseCancellationSettingsModeEnum, OwnCapability, RTMPBroadcastRequestQualityEnum, RTMPSettingsRequestQualityEnum, RawRecordingSettingsRequestModeEnum, RawRecordingSettingsResponseModeEnum, RecordSettingsRequestModeEnum, RecordSettingsRequestQualityEnum, rxUtils as RxUtils, ScreenShareManager, ScreenShareState, events as SfuEvents, SfuJoinError, models as SfuModels, SpeakerManager, SpeakerState, StartClosedCaptionsRequestLanguageEnum, StartTranscriptionRequestLanguageEnum, StreamSfuClient, StreamVideoClient, StreamVideoReadOnlyStateStore, StreamVideoWriteableStateStore, TranscriptionSettingsRequestClosedCaptionModeEnum, TranscriptionSettingsRequestLanguageEnum, TranscriptionSettingsRequestModeEnum, TranscriptionSettingsResponseClosedCaptionModeEnum, TranscriptionSettingsResponseLanguageEnum, TranscriptionSettingsResponseModeEnum, VideoSettingsRequestCameraFacingEnum, VideoSettingsResponseCameraFacingEnum, ViewportTracker, VisibilityState, checkIfAudioOutputChangeSupported, combineComparators, conditional, createSoundDetector, defaultSortPreset, descending, deviceIds$, disposeOfMediaStream, dominantSpeaker, getAudioBrowserPermission, getAudioDevices, getAudioOutputDevices, getAudioStream, getClientDetails, getDeviceState, getScreenShareStream, getSdkInfo, getVideoBrowserPermission, getVideoDevices, getVideoStream, getWebRTCInfo, hasAudio$1 as hasAudio, hasPausedTrack, hasScreenShare, hasScreenShareAudio, hasVideo, humanize, isPinned, livestreamOrAudioRoomSortPreset, logToConsole, name, noopComparator, paginatedLayoutSortPreset, pinned, publishingAudio, publishingVideo, reactionType, resolveDeviceId, role, screenSharing, setDeviceInfo, setOSInfo, setPowerState, setSdkInfo, setThermalState, setWebRTCInfo, speakerLayoutSortPreset, speaking, videoLoggerSystem, withParticipantSource };
16774
16608
  //# sourceMappingURL=index.browser.es.js.map