@stream-io/video-client 1.15.5 → 1.15.7
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 +14 -0
- package/dist/index.browser.es.js +94 -42
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +94 -42
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +94 -42
- package/dist/index.es.js.map +1 -1
- package/dist/src/helpers/RNSpeechDetector.d.ts +3 -4
- package/package.json +1 -1
- package/src/devices/MicrophoneManager.ts +4 -12
- package/src/devices/__tests__/MicrophoneManagerRN.test.ts +6 -4
- package/src/helpers/RNSpeechDetector.ts +104 -40
- package/src/rtc/__tests__/videoLayers.test.ts +17 -0
- package/src/rtc/videoLayers.ts +2 -1
package/dist/index.cjs.js
CHANGED
|
@@ -5500,7 +5500,7 @@ const computeVideoLayers = (videoTrack, publishOption) => {
|
|
|
5500
5500
|
rid,
|
|
5501
5501
|
width: Math.round(width / downscaleFactor),
|
|
5502
5502
|
height: Math.round(height / downscaleFactor),
|
|
5503
|
-
maxBitrate: maxBitrate / bitrateFactor || defaultBitratePerRid[rid],
|
|
5503
|
+
maxBitrate: Math.round(maxBitrate / bitrateFactor) || defaultBitratePerRid[rid],
|
|
5504
5504
|
maxFramerate: fps,
|
|
5505
5505
|
};
|
|
5506
5506
|
if (svcCodec) {
|
|
@@ -7358,7 +7358,7 @@ const aggregate = (stats) => {
|
|
|
7358
7358
|
return report;
|
|
7359
7359
|
};
|
|
7360
7360
|
|
|
7361
|
-
const version = "1.15.
|
|
7361
|
+
const version = "1.15.7";
|
|
7362
7362
|
const [major, minor, patch] = version.split('.');
|
|
7363
7363
|
let sdkInfo = {
|
|
7364
7364
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -9339,7 +9339,7 @@ class MicrophoneManagerState extends InputMediaDeviceManagerState {
|
|
|
9339
9339
|
}
|
|
9340
9340
|
|
|
9341
9341
|
const DETECTION_FREQUENCY_IN_MS = 500;
|
|
9342
|
-
const AUDIO_LEVEL_THRESHOLD
|
|
9342
|
+
const AUDIO_LEVEL_THRESHOLD = 150;
|
|
9343
9343
|
const FFT_SIZE = 128;
|
|
9344
9344
|
/**
|
|
9345
9345
|
* Creates a new sound detector.
|
|
@@ -9350,7 +9350,7 @@ const FFT_SIZE = 128;
|
|
|
9350
9350
|
* @returns a clean-up function which once invoked stops the sound detector.
|
|
9351
9351
|
*/
|
|
9352
9352
|
const createSoundDetector = (audioStream, onSoundDetectedStateChanged, options = {}) => {
|
|
9353
|
-
const { detectionFrequencyInMs = DETECTION_FREQUENCY_IN_MS, audioLevelThreshold = AUDIO_LEVEL_THRESHOLD
|
|
9353
|
+
const { detectionFrequencyInMs = DETECTION_FREQUENCY_IN_MS, audioLevelThreshold = AUDIO_LEVEL_THRESHOLD, fftSize = FFT_SIZE, destroyStreamOnStop = true, } = options;
|
|
9354
9354
|
const audioContext = new AudioContext();
|
|
9355
9355
|
const analyser = audioContext.createAnalyser();
|
|
9356
9356
|
analyser.fftSize = fftSize;
|
|
@@ -9391,7 +9391,6 @@ const createSoundDetector = (audioStream, onSoundDetectedStateChanged, options =
|
|
|
9391
9391
|
};
|
|
9392
9392
|
};
|
|
9393
9393
|
|
|
9394
|
-
const AUDIO_LEVEL_THRESHOLD = 0.2;
|
|
9395
9394
|
class RNSpeechDetector {
|
|
9396
9395
|
constructor() {
|
|
9397
9396
|
this.pc1 = new RTCPeerConnection({});
|
|
@@ -9400,7 +9399,7 @@ class RNSpeechDetector {
|
|
|
9400
9399
|
/**
|
|
9401
9400
|
* Starts the speech detection.
|
|
9402
9401
|
*/
|
|
9403
|
-
async start() {
|
|
9402
|
+
async start(onSoundDetectedStateChanged) {
|
|
9404
9403
|
try {
|
|
9405
9404
|
this.cleanupAudioStream();
|
|
9406
9405
|
const audioStream = await navigator.mediaDevices.getUserMedia({
|
|
@@ -9413,6 +9412,14 @@ class RNSpeechDetector {
|
|
|
9413
9412
|
this.pc2.addEventListener('icecandidate', async (e) => {
|
|
9414
9413
|
await this.pc1.addIceCandidate(e.candidate);
|
|
9415
9414
|
});
|
|
9415
|
+
this.pc2.addEventListener('track', (e) => {
|
|
9416
|
+
e.streams[0].getTracks().forEach((track) => {
|
|
9417
|
+
// In RN, the remote track is automatically added to the audio output device
|
|
9418
|
+
// so we need to mute it to avoid hearing the audio back
|
|
9419
|
+
// @ts-ignore _setVolume is a private method in react-native-webrtc
|
|
9420
|
+
track._setVolume(0);
|
|
9421
|
+
});
|
|
9422
|
+
});
|
|
9416
9423
|
audioStream
|
|
9417
9424
|
.getTracks()
|
|
9418
9425
|
.forEach((track) => this.pc1.addTrack(track, audioStream));
|
|
@@ -9422,12 +9429,16 @@ class RNSpeechDetector {
|
|
|
9422
9429
|
const answer = await this.pc2.createAnswer();
|
|
9423
9430
|
await this.pc1.setRemoteDescription(answer);
|
|
9424
9431
|
await this.pc2.setLocalDescription(answer);
|
|
9425
|
-
const
|
|
9426
|
-
|
|
9427
|
-
|
|
9432
|
+
const unsub = this.onSpeakingDetectedStateChange(onSoundDetectedStateChanged);
|
|
9433
|
+
return () => {
|
|
9434
|
+
unsub();
|
|
9435
|
+
this.stop();
|
|
9436
|
+
};
|
|
9428
9437
|
}
|
|
9429
9438
|
catch (error) {
|
|
9430
|
-
|
|
9439
|
+
const logger = getLogger(['RNSpeechDetector']);
|
|
9440
|
+
logger('error', 'error handling permissions: ', error);
|
|
9441
|
+
return () => { };
|
|
9431
9442
|
}
|
|
9432
9443
|
}
|
|
9433
9444
|
/**
|
|
@@ -9437,40 +9448,85 @@ class RNSpeechDetector {
|
|
|
9437
9448
|
this.pc1.close();
|
|
9438
9449
|
this.pc2.close();
|
|
9439
9450
|
this.cleanupAudioStream();
|
|
9440
|
-
if (this.intervalId) {
|
|
9441
|
-
clearInterval(this.intervalId);
|
|
9442
|
-
}
|
|
9443
9451
|
}
|
|
9444
9452
|
/**
|
|
9445
9453
|
* Public method that detects the audio levels and returns the status.
|
|
9446
9454
|
*/
|
|
9447
9455
|
onSpeakingDetectedStateChange(onSoundDetectedStateChanged) {
|
|
9448
|
-
|
|
9449
|
-
|
|
9450
|
-
|
|
9451
|
-
|
|
9452
|
-
|
|
9453
|
-
|
|
9454
|
-
|
|
9455
|
-
|
|
9456
|
-
|
|
9457
|
-
|
|
9458
|
-
|
|
9459
|
-
|
|
9460
|
-
|
|
9461
|
-
|
|
9462
|
-
|
|
9463
|
-
|
|
9464
|
-
|
|
9465
|
-
|
|
9466
|
-
|
|
9467
|
-
|
|
9456
|
+
const initialBaselineNoiseLevel = 0.13;
|
|
9457
|
+
let baselineNoiseLevel = initialBaselineNoiseLevel;
|
|
9458
|
+
let speechDetected = false;
|
|
9459
|
+
let intervalId;
|
|
9460
|
+
let speechTimer;
|
|
9461
|
+
let silenceTimer;
|
|
9462
|
+
let audioLevelHistory = []; // Store recent audio levels for smoother detection
|
|
9463
|
+
const historyLength = 10;
|
|
9464
|
+
const silenceThreshold = 1.1;
|
|
9465
|
+
const resetThreshold = 0.9;
|
|
9466
|
+
const speechTimeout = 500; // Speech is set to true after 500ms of audio detection
|
|
9467
|
+
const silenceTimeout = 5000; // Reset baseline after 5 seconds of silence
|
|
9468
|
+
const checkAudioLevel = async () => {
|
|
9469
|
+
try {
|
|
9470
|
+
const stats = (await this.pc1.getStats());
|
|
9471
|
+
const report = flatten(stats);
|
|
9472
|
+
// Audio levels are present inside stats of type `media-source` and of kind `audio`
|
|
9473
|
+
const audioMediaSourceStats = report.find((stat) => stat.type === 'media-source' &&
|
|
9474
|
+
stat.kind === 'audio');
|
|
9475
|
+
if (audioMediaSourceStats) {
|
|
9476
|
+
const { audioLevel } = audioMediaSourceStats;
|
|
9477
|
+
if (audioLevel) {
|
|
9478
|
+
// Update audio level history (with max historyLength sized array)
|
|
9479
|
+
audioLevelHistory.push(audioLevel);
|
|
9480
|
+
if (audioLevelHistory.length > historyLength) {
|
|
9481
|
+
audioLevelHistory.shift();
|
|
9482
|
+
}
|
|
9483
|
+
// Calculate average audio level
|
|
9484
|
+
const avgAudioLevel = audioLevelHistory.reduce((a, b) => a + b, 0) /
|
|
9485
|
+
audioLevelHistory.length;
|
|
9486
|
+
// Update baseline (if necessary) based on silence detection
|
|
9487
|
+
if (avgAudioLevel < baselineNoiseLevel * silenceThreshold) {
|
|
9488
|
+
if (!silenceTimer) {
|
|
9489
|
+
silenceTimer = setTimeout(() => {
|
|
9490
|
+
baselineNoiseLevel = Math.min(avgAudioLevel * resetThreshold, initialBaselineNoiseLevel);
|
|
9491
|
+
}, silenceTimeout);
|
|
9492
|
+
}
|
|
9493
|
+
}
|
|
9494
|
+
else {
|
|
9495
|
+
clearTimeout(silenceTimer);
|
|
9496
|
+
silenceTimer = undefined;
|
|
9497
|
+
}
|
|
9498
|
+
// Speech detection with hysteresis
|
|
9499
|
+
if (avgAudioLevel > baselineNoiseLevel * 1.5) {
|
|
9500
|
+
if (!speechDetected) {
|
|
9501
|
+
speechDetected = true;
|
|
9502
|
+
onSoundDetectedStateChanged({
|
|
9503
|
+
isSoundDetected: true,
|
|
9504
|
+
audioLevel,
|
|
9505
|
+
});
|
|
9506
|
+
}
|
|
9507
|
+
clearTimeout(speechTimer);
|
|
9508
|
+
speechTimer = setTimeout(() => {
|
|
9509
|
+
speechDetected = false;
|
|
9510
|
+
onSoundDetectedStateChanged({
|
|
9511
|
+
isSoundDetected: false,
|
|
9512
|
+
audioLevel: 0,
|
|
9513
|
+
});
|
|
9514
|
+
}, speechTimeout);
|
|
9515
|
+
}
|
|
9468
9516
|
}
|
|
9469
9517
|
}
|
|
9470
9518
|
}
|
|
9471
|
-
|
|
9519
|
+
catch (error) {
|
|
9520
|
+
const logger = getLogger(['RNSpeechDetector']);
|
|
9521
|
+
logger('error', 'error checking audio level from stats', error);
|
|
9522
|
+
}
|
|
9523
|
+
};
|
|
9524
|
+
// Call checkAudioLevel periodically (every 100ms)
|
|
9525
|
+
intervalId = setInterval(checkAudioLevel, 100);
|
|
9472
9526
|
return () => {
|
|
9473
|
-
clearInterval(
|
|
9527
|
+
clearInterval(intervalId);
|
|
9528
|
+
clearTimeout(speechTimer);
|
|
9529
|
+
clearTimeout(silenceTimer);
|
|
9474
9530
|
};
|
|
9475
9531
|
}
|
|
9476
9532
|
cleanupAudioStream() {
|
|
@@ -9488,9 +9544,7 @@ class RNSpeechDetector {
|
|
|
9488
9544
|
}
|
|
9489
9545
|
|
|
9490
9546
|
class MicrophoneManager extends InputMediaDeviceManager {
|
|
9491
|
-
constructor(call, disableMode =
|
|
9492
|
-
? 'disable-tracks'
|
|
9493
|
-
: 'stop-tracks') {
|
|
9547
|
+
constructor(call, disableMode = 'stop-tracks') {
|
|
9494
9548
|
super(call, new MicrophoneManagerState(disableMode), TrackType.AUDIO);
|
|
9495
9549
|
this.speakingWhileMutedNotificationEnabled = true;
|
|
9496
9550
|
this.soundDetectorConcurrencyTag = Symbol('soundDetectorConcurrencyTag');
|
|
@@ -9671,13 +9725,11 @@ class MicrophoneManager extends InputMediaDeviceManager {
|
|
|
9671
9725
|
await this.stopSpeakingWhileMutedDetection();
|
|
9672
9726
|
if (isReactNative()) {
|
|
9673
9727
|
this.rnSpeechDetector = new RNSpeechDetector();
|
|
9674
|
-
await this.rnSpeechDetector.start()
|
|
9675
|
-
const unsubscribe = this.rnSpeechDetector?.onSpeakingDetectedStateChange((event) => {
|
|
9728
|
+
const unsubscribe = await this.rnSpeechDetector.start((event) => {
|
|
9676
9729
|
this.state.setSpeakingWhileMuted(event.isSoundDetected);
|
|
9677
9730
|
});
|
|
9678
9731
|
this.soundDetectorCleanup = () => {
|
|
9679
9732
|
unsubscribe();
|
|
9680
|
-
this.rnSpeechDetector?.stop();
|
|
9681
9733
|
this.rnSpeechDetector = undefined;
|
|
9682
9734
|
};
|
|
9683
9735
|
}
|
|
@@ -12863,7 +12915,7 @@ class StreamClient {
|
|
|
12863
12915
|
return await this.wsConnection.connect(this.defaultWSTimeout);
|
|
12864
12916
|
};
|
|
12865
12917
|
this.getUserAgent = () => {
|
|
12866
|
-
const version = "1.15.
|
|
12918
|
+
const version = "1.15.7";
|
|
12867
12919
|
return (this.userAgent ||
|
|
12868
12920
|
`stream-video-javascript-client-${this.node ? 'node' : 'browser'}-${version}`);
|
|
12869
12921
|
};
|