@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 +16 -0
- package/dist/index.browser.es.js +31 -197
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +30 -197
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.es.js +31 -197
- package/dist/index.es.js.map +1 -1
- package/dist/src/devices/MicrophoneManager.d.ts +0 -1
- package/dist/src/gen/coordinator/index.d.ts +6 -0
- package/dist/src/types.d.ts +11 -0
- package/index.ts +0 -1
- package/package.json +1 -1
- package/src/devices/CameraManager.ts +9 -2
- package/src/devices/DeviceManager.ts +13 -3
- package/src/devices/MicrophoneManager.ts +17 -6
- package/src/devices/SpeakerManager.ts +16 -4
- package/src/devices/__tests__/CameraManager.test.ts +32 -0
- package/src/devices/__tests__/DeviceManager.test.ts +71 -0
- package/src/devices/__tests__/MicrophoneManager.test.ts +23 -0
- package/src/devices/__tests__/MicrophoneManagerRN.test.ts +28 -29
- package/src/devices/__tests__/SpeakerManager.test.ts +28 -0
- package/src/gen/coordinator/index.ts +6 -0
- package/src/types.ts +9 -0
- package/dist/src/helpers/RNSpeechDetector.d.ts +0 -23
- package/src/helpers/RNSpeechDetector.ts +0 -224
- package/src/helpers/__tests__/RNSpeechDetector.test.ts +0 -52
package/dist/index.cjs.js
CHANGED
|
@@ -6304,7 +6304,7 @@ const getSdkVersion = (sdk) => {
|
|
|
6304
6304
|
return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
|
|
6305
6305
|
};
|
|
6306
6306
|
|
|
6307
|
-
const version = "1.
|
|
6307
|
+
const version = "1.48.0";
|
|
6308
6308
|
const [major, minor, patch] = version.split('.');
|
|
6309
6309
|
let sdkInfo = {
|
|
6310
6310
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -10885,8 +10885,14 @@ class DeviceManager {
|
|
|
10885
10885
|
this.handleDisconnectedOrReplacedDevices();
|
|
10886
10886
|
}
|
|
10887
10887
|
if (this.devicePersistence.enabled) {
|
|
10888
|
-
this.subscriptions.push(createSubscription(rxjs.combineLatest([
|
|
10889
|
-
|
|
10888
|
+
this.subscriptions.push(createSubscription(rxjs.combineLatest([
|
|
10889
|
+
this.state.selectedDevice$,
|
|
10890
|
+
this.state.status$,
|
|
10891
|
+
this.state.browserPermissionState$,
|
|
10892
|
+
]), ([selectedDevice, status, browserPermissionState]) => {
|
|
10893
|
+
if (!status ||
|
|
10894
|
+
(this.isTrackStoppedDueToTrackEnd && status === 'disabled') ||
|
|
10895
|
+
browserPermissionState !== 'granted')
|
|
10890
10896
|
return;
|
|
10891
10897
|
this.persistPreference(selectedDevice, status);
|
|
10892
10898
|
}));
|
|
@@ -11651,7 +11657,10 @@ class CameraManager extends DeviceManager {
|
|
|
11651
11657
|
const shouldApplyDefaults = this.state.status === undefined &&
|
|
11652
11658
|
this.state.optimisticStatus === undefined;
|
|
11653
11659
|
let persistedPreferencesApplied = false;
|
|
11654
|
-
|
|
11660
|
+
const permissionState = await rxjs.firstValueFrom(this.state.browserPermissionState$);
|
|
11661
|
+
if (shouldApplyDefaults &&
|
|
11662
|
+
this.devicePersistence.enabled &&
|
|
11663
|
+
permissionState === 'granted') {
|
|
11655
11664
|
persistedPreferencesApplied =
|
|
11656
11665
|
await this.applyPersistedPreferences(enabledInCallType);
|
|
11657
11666
|
}
|
|
@@ -11943,192 +11952,6 @@ const createNoAudioDetector = (audioStream, options) => {
|
|
|
11943
11952
|
return stop;
|
|
11944
11953
|
};
|
|
11945
11954
|
|
|
11946
|
-
class RNSpeechDetector {
|
|
11947
|
-
constructor(externalAudioStream) {
|
|
11948
|
-
this.pc1 = new RTCPeerConnection({});
|
|
11949
|
-
this.pc2 = new RTCPeerConnection({});
|
|
11950
|
-
this.isStopped = false;
|
|
11951
|
-
this.externalAudioStream = externalAudioStream;
|
|
11952
|
-
}
|
|
11953
|
-
/**
|
|
11954
|
-
* Starts the speech detection.
|
|
11955
|
-
*/
|
|
11956
|
-
async start(onSoundDetectedStateChanged) {
|
|
11957
|
-
let detachListeners;
|
|
11958
|
-
let unsubscribe;
|
|
11959
|
-
try {
|
|
11960
|
-
this.isStopped = false;
|
|
11961
|
-
const audioStream = this.externalAudioStream != null
|
|
11962
|
-
? this.externalAudioStream
|
|
11963
|
-
: await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
11964
|
-
this.audioStream = audioStream;
|
|
11965
|
-
const onPc1IceCandidate = (e) => {
|
|
11966
|
-
this.forwardIceCandidate(this.pc2, e.candidate);
|
|
11967
|
-
};
|
|
11968
|
-
const onPc2IceCandidate = (e) => {
|
|
11969
|
-
this.forwardIceCandidate(this.pc1, e.candidate);
|
|
11970
|
-
};
|
|
11971
|
-
const onTrackPc2 = (e) => {
|
|
11972
|
-
e.streams[0].getTracks().forEach((track) => {
|
|
11973
|
-
// In RN, the remote track is automatically added to the audio output device
|
|
11974
|
-
// so we need to mute it to avoid hearing the audio back
|
|
11975
|
-
// @ts-expect-error _setVolume is a private method in react-native-webrtc
|
|
11976
|
-
track._setVolume(0);
|
|
11977
|
-
});
|
|
11978
|
-
};
|
|
11979
|
-
this.pc1.addEventListener('icecandidate', onPc1IceCandidate);
|
|
11980
|
-
this.pc2.addEventListener('icecandidate', onPc2IceCandidate);
|
|
11981
|
-
this.pc2.addEventListener('track', onTrackPc2);
|
|
11982
|
-
detachListeners = () => {
|
|
11983
|
-
this.pc1.removeEventListener('icecandidate', onPc1IceCandidate);
|
|
11984
|
-
this.pc2.removeEventListener('icecandidate', onPc2IceCandidate);
|
|
11985
|
-
this.pc2.removeEventListener('track', onTrackPc2);
|
|
11986
|
-
};
|
|
11987
|
-
audioStream
|
|
11988
|
-
.getTracks()
|
|
11989
|
-
.forEach((track) => this.pc1.addTrack(track, audioStream));
|
|
11990
|
-
const offer = await this.pc1.createOffer({});
|
|
11991
|
-
await this.pc2.setRemoteDescription(offer);
|
|
11992
|
-
await this.pc1.setLocalDescription(offer);
|
|
11993
|
-
const answer = await this.pc2.createAnswer();
|
|
11994
|
-
await this.pc1.setRemoteDescription(answer);
|
|
11995
|
-
await this.pc2.setLocalDescription(answer);
|
|
11996
|
-
unsubscribe = this.onSpeakingDetectedStateChange(onSoundDetectedStateChanged);
|
|
11997
|
-
return () => {
|
|
11998
|
-
detachListeners?.();
|
|
11999
|
-
unsubscribe?.();
|
|
12000
|
-
this.stop();
|
|
12001
|
-
};
|
|
12002
|
-
}
|
|
12003
|
-
catch (error) {
|
|
12004
|
-
detachListeners?.();
|
|
12005
|
-
unsubscribe?.();
|
|
12006
|
-
this.stop();
|
|
12007
|
-
const logger = videoLoggerSystem.getLogger('RNSpeechDetector');
|
|
12008
|
-
logger.error('error handling permissions: ', error);
|
|
12009
|
-
return () => { };
|
|
12010
|
-
}
|
|
12011
|
-
}
|
|
12012
|
-
/**
|
|
12013
|
-
* Stops the speech detection and releases all allocated resources.
|
|
12014
|
-
*/
|
|
12015
|
-
stop() {
|
|
12016
|
-
if (this.isStopped)
|
|
12017
|
-
return;
|
|
12018
|
-
this.isStopped = true;
|
|
12019
|
-
this.pc1.close();
|
|
12020
|
-
this.pc2.close();
|
|
12021
|
-
if (this.externalAudioStream != null) {
|
|
12022
|
-
this.externalAudioStream = undefined;
|
|
12023
|
-
}
|
|
12024
|
-
else {
|
|
12025
|
-
this.cleanupAudioStream();
|
|
12026
|
-
}
|
|
12027
|
-
}
|
|
12028
|
-
/**
|
|
12029
|
-
* Public method that detects the audio levels and returns the status.
|
|
12030
|
-
*/
|
|
12031
|
-
onSpeakingDetectedStateChange(onSoundDetectedStateChanged) {
|
|
12032
|
-
const initialBaselineNoiseLevel = 0.13;
|
|
12033
|
-
let baselineNoiseLevel = initialBaselineNoiseLevel;
|
|
12034
|
-
let speechDetected = false;
|
|
12035
|
-
let speechTimer;
|
|
12036
|
-
let silenceTimer;
|
|
12037
|
-
const audioLevelHistory = []; // Store recent audio levels for smoother detection
|
|
12038
|
-
const historyLength = 10;
|
|
12039
|
-
const silenceThreshold = 1.1;
|
|
12040
|
-
const resetThreshold = 0.9;
|
|
12041
|
-
const speechTimeout = 500; // Speech is set to true after 500ms of audio detection
|
|
12042
|
-
const silenceTimeout = 5000; // Reset baseline after 5 seconds of silence
|
|
12043
|
-
const checkAudioLevel = async () => {
|
|
12044
|
-
try {
|
|
12045
|
-
const stats = await this.pc1.getStats();
|
|
12046
|
-
const report = flatten(stats);
|
|
12047
|
-
// Audio levels are present inside stats of type `media-source` and of kind `audio`
|
|
12048
|
-
const audioMediaSourceStats = report.find((stat) => stat.type === 'media-source' &&
|
|
12049
|
-
stat.kind === 'audio');
|
|
12050
|
-
if (audioMediaSourceStats) {
|
|
12051
|
-
const { audioLevel } = audioMediaSourceStats;
|
|
12052
|
-
if (audioLevel) {
|
|
12053
|
-
// Update audio level history (with max historyLength sized array)
|
|
12054
|
-
audioLevelHistory.push(audioLevel);
|
|
12055
|
-
if (audioLevelHistory.length > historyLength) {
|
|
12056
|
-
audioLevelHistory.shift();
|
|
12057
|
-
}
|
|
12058
|
-
// Calculate average audio level
|
|
12059
|
-
const avgAudioLevel = audioLevelHistory.reduce((a, b) => a + b, 0) /
|
|
12060
|
-
audioLevelHistory.length;
|
|
12061
|
-
// Update baseline (if necessary) based on silence detection
|
|
12062
|
-
if (avgAudioLevel < baselineNoiseLevel * silenceThreshold) {
|
|
12063
|
-
if (!silenceTimer) {
|
|
12064
|
-
silenceTimer = setTimeout(() => {
|
|
12065
|
-
baselineNoiseLevel = Math.min(avgAudioLevel * resetThreshold, initialBaselineNoiseLevel);
|
|
12066
|
-
}, silenceTimeout);
|
|
12067
|
-
}
|
|
12068
|
-
}
|
|
12069
|
-
else {
|
|
12070
|
-
clearTimeout(silenceTimer);
|
|
12071
|
-
silenceTimer = undefined;
|
|
12072
|
-
}
|
|
12073
|
-
// Speech detection with hysteresis
|
|
12074
|
-
if (avgAudioLevel > baselineNoiseLevel * 1.5) {
|
|
12075
|
-
if (!speechDetected) {
|
|
12076
|
-
speechDetected = true;
|
|
12077
|
-
onSoundDetectedStateChanged({
|
|
12078
|
-
isSoundDetected: true,
|
|
12079
|
-
audioLevel,
|
|
12080
|
-
});
|
|
12081
|
-
}
|
|
12082
|
-
clearTimeout(speechTimer);
|
|
12083
|
-
speechTimer = setTimeout(() => {
|
|
12084
|
-
speechDetected = false;
|
|
12085
|
-
onSoundDetectedStateChanged({
|
|
12086
|
-
isSoundDetected: false,
|
|
12087
|
-
audioLevel: 0,
|
|
12088
|
-
});
|
|
12089
|
-
}, speechTimeout);
|
|
12090
|
-
}
|
|
12091
|
-
}
|
|
12092
|
-
}
|
|
12093
|
-
}
|
|
12094
|
-
catch (error) {
|
|
12095
|
-
const logger = videoLoggerSystem.getLogger('RNSpeechDetector');
|
|
12096
|
-
logger.error('error checking audio level from stats', error);
|
|
12097
|
-
}
|
|
12098
|
-
};
|
|
12099
|
-
const intervalId = setInterval(checkAudioLevel, 250);
|
|
12100
|
-
return () => {
|
|
12101
|
-
clearInterval(intervalId);
|
|
12102
|
-
clearTimeout(speechTimer);
|
|
12103
|
-
clearTimeout(silenceTimer);
|
|
12104
|
-
};
|
|
12105
|
-
}
|
|
12106
|
-
cleanupAudioStream() {
|
|
12107
|
-
if (!this.audioStream) {
|
|
12108
|
-
return;
|
|
12109
|
-
}
|
|
12110
|
-
this.audioStream.getTracks().forEach((track) => track.stop());
|
|
12111
|
-
if (
|
|
12112
|
-
// @ts-expect-error release() is present in react-native-webrtc
|
|
12113
|
-
typeof this.audioStream.release === 'function') {
|
|
12114
|
-
// @ts-expect-error called to dispose the stream in RN
|
|
12115
|
-
this.audioStream.release();
|
|
12116
|
-
}
|
|
12117
|
-
}
|
|
12118
|
-
forwardIceCandidate(destination, candidate) {
|
|
12119
|
-
if (this.isStopped ||
|
|
12120
|
-
!candidate ||
|
|
12121
|
-
destination.signalingState === 'closed') {
|
|
12122
|
-
return;
|
|
12123
|
-
}
|
|
12124
|
-
destination.addIceCandidate(candidate).catch(() => {
|
|
12125
|
-
// silently ignore the error
|
|
12126
|
-
const logger = videoLoggerSystem.getLogger('RNSpeechDetector');
|
|
12127
|
-
logger.info('cannot add ice candidate - ignoring');
|
|
12128
|
-
});
|
|
12129
|
-
}
|
|
12130
|
-
}
|
|
12131
|
-
|
|
12132
11955
|
class MicrophoneManager extends AudioDeviceManager {
|
|
12133
11956
|
constructor(call, devicePersistence, disableMode = 'stop-tracks') {
|
|
12134
11957
|
super(call, new MicrophoneManagerState(disableMode, call.tracer), TrackType.AUDIO, devicePersistence);
|
|
@@ -12393,7 +12216,10 @@ class MicrophoneManager extends AudioDeviceManager {
|
|
|
12393
12216
|
const shouldApplyDefaults = this.state.status === undefined &&
|
|
12394
12217
|
this.state.optimisticStatus === undefined;
|
|
12395
12218
|
let persistedPreferencesApplied = false;
|
|
12396
|
-
|
|
12219
|
+
const permissionState = await rxjs.firstValueFrom(this.state.browserPermissionState$);
|
|
12220
|
+
if (shouldApplyDefaults &&
|
|
12221
|
+
this.devicePersistence.enabled &&
|
|
12222
|
+
permissionState === 'granted') {
|
|
12397
12223
|
persistedPreferencesApplied = await this.applyPersistedPreferences(true);
|
|
12398
12224
|
}
|
|
12399
12225
|
const canPublish = this.call.permissionsContext.canPublish(this.trackType);
|
|
@@ -12438,13 +12264,16 @@ class MicrophoneManager extends AudioDeviceManager {
|
|
|
12438
12264
|
return;
|
|
12439
12265
|
await this.teardownSpeakingWhileMutedDetection();
|
|
12440
12266
|
if (isReactNative()) {
|
|
12441
|
-
|
|
12442
|
-
|
|
12267
|
+
const speechActivity = globalThis.streamRNVideoSDK?.nativeEvents?.speechActivity;
|
|
12268
|
+
if (!speechActivity) {
|
|
12269
|
+
this.logger.warn('Native speech activity not available, make sure the "@stream-io/react-native-webrtc" peer dependency version is satisfied');
|
|
12270
|
+
return;
|
|
12271
|
+
}
|
|
12272
|
+
const unsubscribe = speechActivity.subscribe((event) => {
|
|
12443
12273
|
this.state.setSpeakingWhileMuted(event.isSoundDetected);
|
|
12444
12274
|
});
|
|
12445
12275
|
this.soundDetectorCleanup = async () => {
|
|
12446
12276
|
unsubscribe();
|
|
12447
|
-
this.rnSpeechDetector = undefined;
|
|
12448
12277
|
};
|
|
12449
12278
|
}
|
|
12450
12279
|
else {
|
|
@@ -12777,7 +12606,12 @@ class SpeakerManager {
|
|
|
12777
12606
|
}));
|
|
12778
12607
|
}
|
|
12779
12608
|
if (!isReactNative() && this.devicePersistence.enabled) {
|
|
12780
|
-
this.subscriptions.push(createSubscription(
|
|
12609
|
+
this.subscriptions.push(createSubscription(rxjs.combineLatest([
|
|
12610
|
+
this.state.selectedDevice$,
|
|
12611
|
+
getAudioBrowserPermission(this.call.tracer).asStateObservable(),
|
|
12612
|
+
]), ([selectedDevice, browserPermissionState]) => {
|
|
12613
|
+
if (!selectedDevice || browserPermissionState !== 'granted')
|
|
12614
|
+
return;
|
|
12781
12615
|
this.persistSpeakerDevicePreference(selectedDevice);
|
|
12782
12616
|
}));
|
|
12783
12617
|
}
|
|
@@ -16152,7 +15986,7 @@ class StreamClient {
|
|
|
16152
15986
|
this.getUserAgent = () => {
|
|
16153
15987
|
if (!this.cachedUserAgent) {
|
|
16154
15988
|
const { clientAppIdentifier = {} } = this.options;
|
|
16155
|
-
const { sdkName = 'js', sdkVersion = "1.
|
|
15989
|
+
const { sdkName = 'js', sdkVersion = "1.48.0", ...extras } = clientAppIdentifier;
|
|
16156
15990
|
this.cachedUserAgent = [
|
|
16157
15991
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
16158
15992
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|
|
@@ -16827,7 +16661,6 @@ exports.MicrophoneManager = MicrophoneManager;
|
|
|
16827
16661
|
exports.MicrophoneManagerState = MicrophoneManagerState;
|
|
16828
16662
|
exports.NoiseCancellationSettingsModeEnum = NoiseCancellationSettingsModeEnum;
|
|
16829
16663
|
exports.OwnCapability = OwnCapability;
|
|
16830
|
-
exports.RNSpeechDetector = RNSpeechDetector;
|
|
16831
16664
|
exports.RTMPBroadcastRequestQualityEnum = RTMPBroadcastRequestQualityEnum;
|
|
16832
16665
|
exports.RTMPSettingsRequestQualityEnum = RTMPSettingsRequestQualityEnum;
|
|
16833
16666
|
exports.RawRecordingSettingsRequestModeEnum = RawRecordingSettingsRequestModeEnum;
|