@stream-io/video-client 1.15.6 → 1.16.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 +14 -0
- package/dist/index.browser.es.js +249 -137
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +253 -141
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.es.js +249 -137
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +18 -2
- package/dist/src/coordinator/connection/types.d.ts +4 -4
- package/dist/src/gen/coordinator/index.d.ts +1876 -2591
- package/dist/src/helpers/RNSpeechDetector.d.ts +3 -4
- package/dist/src/store/CallState.d.ts +2 -3
- package/index.ts +0 -1
- package/package.json +1 -1
- package/src/Call.ts +53 -2
- package/src/coordinator/connection/types.ts +4 -4
- package/src/devices/MicrophoneManager.ts +4 -12
- package/src/devices/__tests__/MicrophoneManagerRN.test.ts +6 -4
- package/src/gen/coordinator/index.ts +2762 -3476
- package/src/helpers/RNSpeechDetector.ts +104 -40
- package/src/store/CallState.ts +18 -26
- package/src/store/__tests__/CallState.test.ts +13 -1
- package/dist/src/gen/shims.d.ts +0 -41
- package/src/gen/shims.ts +0 -44
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
import { BaseStats } from '../stats';
|
|
2
2
|
import { SoundStateChangeHandler } from './sound-detector';
|
|
3
3
|
import { flatten } from '../stats/utils';
|
|
4
|
-
|
|
5
|
-
const AUDIO_LEVEL_THRESHOLD = 0.2;
|
|
4
|
+
import { getLogger } from '../logger';
|
|
6
5
|
|
|
7
6
|
export class RNSpeechDetector {
|
|
8
7
|
private pc1 = new RTCPeerConnection({});
|
|
9
8
|
private pc2 = new RTCPeerConnection({});
|
|
10
|
-
private intervalId: NodeJS.Timeout | undefined;
|
|
11
9
|
private audioStream: MediaStream | undefined;
|
|
12
10
|
|
|
13
11
|
/**
|
|
14
12
|
* Starts the speech detection.
|
|
15
13
|
*/
|
|
16
|
-
public async start() {
|
|
14
|
+
public async start(onSoundDetectedStateChanged: SoundStateChangeHandler) {
|
|
17
15
|
try {
|
|
18
16
|
this.cleanupAudioStream();
|
|
19
17
|
const audioStream = await navigator.mediaDevices.getUserMedia({
|
|
@@ -31,6 +29,14 @@ export class RNSpeechDetector {
|
|
|
31
29
|
e.candidate as RTCIceCandidateInit | undefined,
|
|
32
30
|
);
|
|
33
31
|
});
|
|
32
|
+
this.pc2.addEventListener('track', (e) => {
|
|
33
|
+
e.streams[0].getTracks().forEach((track) => {
|
|
34
|
+
// In RN, the remote track is automatically added to the audio output device
|
|
35
|
+
// so we need to mute it to avoid hearing the audio back
|
|
36
|
+
// @ts-ignore _setVolume is a private method in react-native-webrtc
|
|
37
|
+
track._setVolume(0);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
34
40
|
|
|
35
41
|
audioStream
|
|
36
42
|
.getTracks()
|
|
@@ -41,64 +47,122 @@ export class RNSpeechDetector {
|
|
|
41
47
|
const answer = await this.pc2.createAnswer();
|
|
42
48
|
await this.pc1.setRemoteDescription(answer);
|
|
43
49
|
await this.pc2.setLocalDescription(answer);
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
audioTracks.forEach((track) => (track.enabled = false));
|
|
47
|
-
} catch (error) {
|
|
48
|
-
console.error(
|
|
49
|
-
'Error connecting and negotiating between PeerConnections:',
|
|
50
|
-
error,
|
|
50
|
+
const unsub = this.onSpeakingDetectedStateChange(
|
|
51
|
+
onSoundDetectedStateChanged,
|
|
51
52
|
);
|
|
53
|
+
return () => {
|
|
54
|
+
unsub();
|
|
55
|
+
this.stop();
|
|
56
|
+
};
|
|
57
|
+
} catch (error) {
|
|
58
|
+
const logger = getLogger(['RNSpeechDetector']);
|
|
59
|
+
logger('error', 'error handling permissions: ', error);
|
|
60
|
+
return () => {};
|
|
52
61
|
}
|
|
53
62
|
}
|
|
54
63
|
|
|
55
64
|
/**
|
|
56
65
|
* Stops the speech detection and releases all allocated resources.
|
|
57
66
|
*/
|
|
58
|
-
|
|
67
|
+
private stop() {
|
|
59
68
|
this.pc1.close();
|
|
60
69
|
this.pc2.close();
|
|
61
70
|
this.cleanupAudioStream();
|
|
62
|
-
if (this.intervalId) {
|
|
63
|
-
clearInterval(this.intervalId);
|
|
64
|
-
}
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
/**
|
|
68
74
|
* Public method that detects the audio levels and returns the status.
|
|
69
75
|
*/
|
|
70
|
-
|
|
76
|
+
private onSpeakingDetectedStateChange(
|
|
71
77
|
onSoundDetectedStateChanged: SoundStateChangeHandler,
|
|
72
78
|
) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
79
|
+
const initialBaselineNoiseLevel = 0.13;
|
|
80
|
+
let baselineNoiseLevel = initialBaselineNoiseLevel;
|
|
81
|
+
let speechDetected = false;
|
|
82
|
+
let intervalId: NodeJS.Timeout | undefined;
|
|
83
|
+
let speechTimer: NodeJS.Timeout | undefined;
|
|
84
|
+
let silenceTimer: NodeJS.Timeout | undefined;
|
|
85
|
+
let audioLevelHistory = []; // Store recent audio levels for smoother detection
|
|
86
|
+
const historyLength = 10;
|
|
87
|
+
const silenceThreshold = 1.1;
|
|
88
|
+
const resetThreshold = 0.9;
|
|
89
|
+
const speechTimeout = 500; // Speech is set to true after 500ms of audio detection
|
|
90
|
+
const silenceTimeout = 5000; // Reset baseline after 5 seconds of silence
|
|
91
|
+
|
|
92
|
+
const checkAudioLevel = async () => {
|
|
93
|
+
try {
|
|
94
|
+
const stats = (await this.pc1.getStats()) as RTCStatsReport;
|
|
95
|
+
const report = flatten(stats);
|
|
96
|
+
// Audio levels are present inside stats of type `media-source` and of kind `audio`
|
|
97
|
+
const audioMediaSourceStats = report.find(
|
|
98
|
+
(stat) =>
|
|
99
|
+
stat.type === 'media-source' &&
|
|
100
|
+
(stat as RTCRtpStreamStats).kind === 'audio',
|
|
101
|
+
) as BaseStats;
|
|
102
|
+
if (audioMediaSourceStats) {
|
|
103
|
+
const { audioLevel } = audioMediaSourceStats;
|
|
104
|
+
if (audioLevel) {
|
|
105
|
+
// Update audio level history (with max historyLength sized array)
|
|
106
|
+
audioLevelHistory.push(audioLevel);
|
|
107
|
+
if (audioLevelHistory.length > historyLength) {
|
|
108
|
+
audioLevelHistory.shift();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Calculate average audio level
|
|
112
|
+
const avgAudioLevel =
|
|
113
|
+
audioLevelHistory.reduce((a, b) => a + b, 0) /
|
|
114
|
+
audioLevelHistory.length;
|
|
115
|
+
|
|
116
|
+
// Update baseline (if necessary) based on silence detection
|
|
117
|
+
if (avgAudioLevel < baselineNoiseLevel * silenceThreshold) {
|
|
118
|
+
if (!silenceTimer) {
|
|
119
|
+
silenceTimer = setTimeout(() => {
|
|
120
|
+
baselineNoiseLevel = Math.min(
|
|
121
|
+
avgAudioLevel * resetThreshold,
|
|
122
|
+
initialBaselineNoiseLevel,
|
|
123
|
+
);
|
|
124
|
+
}, silenceTimeout);
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
clearTimeout(silenceTimer);
|
|
128
|
+
silenceTimer = undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Speech detection with hysteresis
|
|
132
|
+
if (avgAudioLevel > baselineNoiseLevel * 1.5) {
|
|
133
|
+
if (!speechDetected) {
|
|
134
|
+
speechDetected = true;
|
|
135
|
+
onSoundDetectedStateChanged({
|
|
136
|
+
isSoundDetected: true,
|
|
137
|
+
audioLevel,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
clearTimeout(speechTimer);
|
|
142
|
+
|
|
143
|
+
speechTimer = setTimeout(() => {
|
|
144
|
+
speechDetected = false;
|
|
145
|
+
onSoundDetectedStateChanged({
|
|
146
|
+
isSoundDetected: false,
|
|
147
|
+
audioLevel: 0,
|
|
148
|
+
});
|
|
149
|
+
}, speechTimeout);
|
|
150
|
+
}
|
|
95
151
|
}
|
|
96
152
|
}
|
|
153
|
+
} catch (error) {
|
|
154
|
+
const logger = getLogger(['RNSpeechDetector']);
|
|
155
|
+
logger('error', 'error checking audio level from stats', error);
|
|
97
156
|
}
|
|
98
|
-
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// Call checkAudioLevel periodically (every 100ms)
|
|
160
|
+
intervalId = setInterval(checkAudioLevel, 100);
|
|
99
161
|
|
|
100
162
|
return () => {
|
|
101
|
-
clearInterval(
|
|
163
|
+
clearInterval(intervalId);
|
|
164
|
+
clearTimeout(speechTimer);
|
|
165
|
+
clearTimeout(silenceTimer);
|
|
102
166
|
};
|
|
103
167
|
}
|
|
104
168
|
|
package/src/store/CallState.ts
CHANGED
|
@@ -21,7 +21,6 @@ import { CallStatsReport } from '../stats';
|
|
|
21
21
|
import {
|
|
22
22
|
BlockedUserEvent,
|
|
23
23
|
CallClosedCaption,
|
|
24
|
-
CallHLSBroadcastingStartedEvent,
|
|
25
24
|
CallIngressResponse,
|
|
26
25
|
CallMemberAddedEvent,
|
|
27
26
|
CallMemberRemovedEvent,
|
|
@@ -42,7 +41,7 @@ import {
|
|
|
42
41
|
UnblockedUserEvent,
|
|
43
42
|
UpdatedCallPermissionsEvent,
|
|
44
43
|
UserResponse,
|
|
45
|
-
|
|
44
|
+
VideoEvent,
|
|
46
45
|
} from '../gen/coordinator';
|
|
47
46
|
import { Timestamp } from '../gen/google/protobuf/timestamp';
|
|
48
47
|
import { ReconnectDetails } from '../gen/video/sfu/event/events';
|
|
@@ -60,7 +59,7 @@ import { hasScreenShare } from '../helpers/participantUtils';
|
|
|
60
59
|
*/
|
|
61
60
|
const defaultEgress: EgressResponse = {
|
|
62
61
|
broadcasting: false,
|
|
63
|
-
hls: { playlist_url: '' },
|
|
62
|
+
hls: { playlist_url: '', status: '' },
|
|
64
63
|
rtmps: [],
|
|
65
64
|
};
|
|
66
65
|
|
|
@@ -314,8 +313,8 @@ export class CallState {
|
|
|
314
313
|
private closedCaptionsTasks = new Map<string, NodeJS.Timeout>();
|
|
315
314
|
|
|
316
315
|
private readonly eventHandlers: {
|
|
317
|
-
[EventType in
|
|
318
|
-
| ((event: Extract<
|
|
316
|
+
[EventType in VideoEvent['type']]:
|
|
317
|
+
| ((event: Extract<VideoEvent, { type: EventType }>) => void)
|
|
319
318
|
| undefined;
|
|
320
319
|
};
|
|
321
320
|
|
|
@@ -414,21 +413,16 @@ export class CallState {
|
|
|
414
413
|
|
|
415
414
|
this.eventHandlers = {
|
|
416
415
|
// these events are not updating the call state:
|
|
417
|
-
'call.deleted': undefined,
|
|
418
416
|
'call.permission_request': undefined,
|
|
419
417
|
'call.recording_ready': undefined,
|
|
418
|
+
'call.rtmp_broadcast_failed': undefined,
|
|
419
|
+
'call.rtmp_broadcast_started': undefined,
|
|
420
|
+
'call.rtmp_broadcast_stopped': undefined,
|
|
420
421
|
'call.transcription_ready': undefined,
|
|
421
422
|
'call.user_muted': undefined,
|
|
422
423
|
'connection.error': undefined,
|
|
423
424
|
'connection.ok': undefined,
|
|
424
425
|
'health.check': undefined,
|
|
425
|
-
'user.banned': undefined,
|
|
426
|
-
'user.deactivated': undefined,
|
|
427
|
-
'user.deleted': undefined,
|
|
428
|
-
'user.muted': undefined,
|
|
429
|
-
'user.presence.changed': undefined,
|
|
430
|
-
'user.reactivated': undefined,
|
|
431
|
-
'user.unbanned': undefined,
|
|
432
426
|
'user.updated': undefined,
|
|
433
427
|
custom: undefined,
|
|
434
428
|
|
|
@@ -446,12 +440,15 @@ export class CallState {
|
|
|
446
440
|
this.setCurrentValue(this.captioningSubject, false);
|
|
447
441
|
},
|
|
448
442
|
'call.created': (e) => this.updateFromCallResponse(e.call),
|
|
443
|
+
'call.deleted': (e) => this.updateFromCallResponse(e.call),
|
|
449
444
|
'call.ended': (e) => {
|
|
450
445
|
this.updateFromCallResponse(e.call);
|
|
451
446
|
this.setCurrentValue(this.endedBySubject, e.user);
|
|
452
447
|
},
|
|
453
448
|
'call.hls_broadcasting_failed': this.updateFromHLSBroadcastingFailed,
|
|
454
|
-
'call.hls_broadcasting_started':
|
|
449
|
+
'call.hls_broadcasting_started': (e) => {
|
|
450
|
+
this.updateFromCallResponse(e.call);
|
|
451
|
+
},
|
|
455
452
|
'call.hls_broadcasting_stopped': this.updateFromHLSBroadcastStopped,
|
|
456
453
|
'call.live_started': (e) => this.updateFromCallResponse(e.call),
|
|
457
454
|
'call.member_added': this.updateFromMemberAdded,
|
|
@@ -1012,7 +1009,7 @@ export class CallState {
|
|
|
1012
1009
|
*
|
|
1013
1010
|
* @param event the video event that our backend sent us.
|
|
1014
1011
|
*/
|
|
1015
|
-
updateFromEvent = (event:
|
|
1012
|
+
updateFromEvent = (event: VideoEvent) => {
|
|
1016
1013
|
const update = this.eventHandlers[event.type];
|
|
1017
1014
|
if (update) {
|
|
1018
1015
|
update(event as any);
|
|
@@ -1205,6 +1202,10 @@ export class CallState {
|
|
|
1205
1202
|
this.setCurrentValue(this.egressSubject, (egress = defaultEgress) => ({
|
|
1206
1203
|
...egress,
|
|
1207
1204
|
broadcasting: false,
|
|
1205
|
+
hls: {
|
|
1206
|
+
...egress.hls!,
|
|
1207
|
+
status: '',
|
|
1208
|
+
},
|
|
1208
1209
|
}));
|
|
1209
1210
|
};
|
|
1210
1211
|
|
|
@@ -1212,18 +1213,9 @@ export class CallState {
|
|
|
1212
1213
|
this.setCurrentValue(this.egressSubject, (egress = defaultEgress) => ({
|
|
1213
1214
|
...egress,
|
|
1214
1215
|
broadcasting: false,
|
|
1215
|
-
}));
|
|
1216
|
-
};
|
|
1217
|
-
|
|
1218
|
-
private updateFromHLSBroadcastStarted = (
|
|
1219
|
-
event: CallHLSBroadcastingStartedEvent,
|
|
1220
|
-
) => {
|
|
1221
|
-
this.setCurrentValue(this.egressSubject, (egress = defaultEgress) => ({
|
|
1222
|
-
...egress,
|
|
1223
|
-
broadcasting: true,
|
|
1224
1216
|
hls: {
|
|
1225
|
-
...egress.hls
|
|
1226
|
-
|
|
1217
|
+
...egress.hls!,
|
|
1218
|
+
status: '',
|
|
1227
1219
|
},
|
|
1228
1220
|
}));
|
|
1229
1221
|
};
|
|
@@ -669,18 +669,30 @@ describe('CallState', () => {
|
|
|
669
669
|
broadcasting: false,
|
|
670
670
|
hls: {
|
|
671
671
|
playlist_url: '',
|
|
672
|
+
status: 'starting',
|
|
672
673
|
},
|
|
673
674
|
},
|
|
674
675
|
});
|
|
675
676
|
// @ts-ignore
|
|
676
677
|
state.updateFromEvent({
|
|
677
678
|
type: 'call.hls_broadcasting_started',
|
|
678
|
-
|
|
679
|
+
// @ts-expect-error incomplete data
|
|
680
|
+
call: {
|
|
681
|
+
egress: {
|
|
682
|
+
broadcasting: true,
|
|
683
|
+
hls: {
|
|
684
|
+
playlist_url: 'https://example.com/playlist.m3u8',
|
|
685
|
+
status: 'running',
|
|
686
|
+
},
|
|
687
|
+
rtmps: [],
|
|
688
|
+
},
|
|
689
|
+
},
|
|
679
690
|
});
|
|
680
691
|
expect(state.egress?.broadcasting).toBe(true);
|
|
681
692
|
expect(state.egress?.hls?.playlist_url).toBe(
|
|
682
693
|
'https://example.com/playlist.m3u8',
|
|
683
694
|
);
|
|
695
|
+
expect(state.egress?.hls?.status).toBe('running');
|
|
684
696
|
});
|
|
685
697
|
|
|
686
698
|
it('handles call.hls_broadcasting_stopped events', () => {
|
package/dist/src/gen/shims.d.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import type { TargetResolution } from './coordinator';
|
|
2
|
-
export type TargetResolutionRequest = TargetResolution;
|
|
3
|
-
export type TargetResolutionResponse = Required<TargetResolution>;
|
|
4
|
-
/**
|
|
5
|
-
*
|
|
6
|
-
* @export
|
|
7
|
-
* @interface Bound
|
|
8
|
-
*/
|
|
9
|
-
export interface Bound {
|
|
10
|
-
/**
|
|
11
|
-
*
|
|
12
|
-
* @type {boolean}
|
|
13
|
-
* @memberof Bound
|
|
14
|
-
*/
|
|
15
|
-
inclusive: boolean;
|
|
16
|
-
/**
|
|
17
|
-
*
|
|
18
|
-
* @type {number}
|
|
19
|
-
* @memberof Bound
|
|
20
|
-
*/
|
|
21
|
-
value: number;
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
*
|
|
25
|
-
* @export
|
|
26
|
-
* @interface DailyAggregateCallStatsResponse
|
|
27
|
-
*/
|
|
28
|
-
export interface DailyAggregateCallStatsResponse<T = any> {
|
|
29
|
-
/**
|
|
30
|
-
*
|
|
31
|
-
* @type {string}
|
|
32
|
-
* @memberof DailyAggregateCallStatsResponse<T>
|
|
33
|
-
*/
|
|
34
|
-
date: string;
|
|
35
|
-
/**
|
|
36
|
-
*
|
|
37
|
-
* @type {T}
|
|
38
|
-
* @memberof DailyAggregateCallStatsResponse<T>
|
|
39
|
-
*/
|
|
40
|
-
report: T;
|
|
41
|
-
}
|
package/src/gen/shims.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import type { TargetResolution } from './coordinator';
|
|
2
|
-
|
|
3
|
-
export type TargetResolutionRequest = TargetResolution;
|
|
4
|
-
export type TargetResolutionResponse = Required<TargetResolution>;
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
*
|
|
8
|
-
* @export
|
|
9
|
-
* @interface Bound
|
|
10
|
-
*/
|
|
11
|
-
export interface Bound {
|
|
12
|
-
/**
|
|
13
|
-
*
|
|
14
|
-
* @type {boolean}
|
|
15
|
-
* @memberof Bound
|
|
16
|
-
*/
|
|
17
|
-
inclusive: boolean;
|
|
18
|
-
/**
|
|
19
|
-
*
|
|
20
|
-
* @type {number}
|
|
21
|
-
* @memberof Bound
|
|
22
|
-
*/
|
|
23
|
-
value: number;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
*
|
|
28
|
-
* @export
|
|
29
|
-
* @interface DailyAggregateCallStatsResponse
|
|
30
|
-
*/
|
|
31
|
-
export interface DailyAggregateCallStatsResponse<T = any> {
|
|
32
|
-
/**
|
|
33
|
-
*
|
|
34
|
-
* @type {string}
|
|
35
|
-
* @memberof DailyAggregateCallStatsResponse<T>
|
|
36
|
-
*/
|
|
37
|
-
date: string;
|
|
38
|
-
/**
|
|
39
|
-
*
|
|
40
|
-
* @type {T}
|
|
41
|
-
* @memberof DailyAggregateCallStatsResponse<T>
|
|
42
|
-
*/
|
|
43
|
-
report: T;
|
|
44
|
-
}
|