@stream-io/video-client 1.44.3 → 1.44.5
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 +134 -45
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +134 -45
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +134 -45
- package/dist/index.es.js.map +1 -1
- package/dist/src/devices/BrowserPermission.d.ts +2 -0
- package/dist/src/devices/CameraManagerState.d.ts +2 -1
- package/dist/src/devices/MicrophoneManagerState.d.ts +2 -1
- package/dist/src/devices/devices.d.ts +2 -2
- package/dist/src/helpers/AudioBindingsWatchdog.d.ts +37 -0
- package/dist/src/helpers/DynascaleManager.d.ts +3 -1
- package/package.json +2 -2
- package/src/devices/BrowserPermission.ts +5 -0
- package/src/devices/CameraManager.ts +6 -1
- package/src/devices/CameraManagerState.ts +3 -2
- package/src/devices/MicrophoneManager.ts +1 -2
- package/src/devices/MicrophoneManagerState.ts +3 -2
- package/src/devices/devices.ts +26 -34
- package/src/helpers/AudioBindingsWatchdog.ts +118 -0
- package/src/helpers/DynascaleManager.ts +22 -24
- package/src/helpers/__tests__/AudioBindingsWatchdog.test.ts +325 -0
- package/src/helpers/__tests__/DynascaleManager.test.ts +64 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment happy-dom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import '../../rtc/__tests__/mocks/webrtc.mocks';
|
|
6
|
+
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
8
|
+
import { AudioBindingsWatchdog } from '../AudioBindingsWatchdog';
|
|
9
|
+
import { Call } from '../../Call';
|
|
10
|
+
import { StreamClient } from '../../coordinator/connection/client';
|
|
11
|
+
import { CallingState, StreamVideoWriteableStateStore } from '../../store';
|
|
12
|
+
import { noopComparator } from '../../sorting';
|
|
13
|
+
import { fromPartial } from '@total-typescript/shoehorn';
|
|
14
|
+
|
|
15
|
+
describe('AudioBindingsWatchdog', () => {
|
|
16
|
+
let watchdog: AudioBindingsWatchdog;
|
|
17
|
+
let call: Call;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
vi.useFakeTimers();
|
|
21
|
+
call = new Call({
|
|
22
|
+
id: 'id',
|
|
23
|
+
type: 'default',
|
|
24
|
+
streamClient: new StreamClient('api-key', {
|
|
25
|
+
devicePersistence: { enabled: false },
|
|
26
|
+
}),
|
|
27
|
+
clientStore: new StreamVideoWriteableStateStore(),
|
|
28
|
+
});
|
|
29
|
+
call.setSortParticipantsBy(noopComparator());
|
|
30
|
+
watchdog = new AudioBindingsWatchdog(call.state, call.tracer);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
watchdog.dispose();
|
|
35
|
+
call.leave();
|
|
36
|
+
vi.useRealTimers();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const addRemoteParticipant = (
|
|
40
|
+
sessionId: string,
|
|
41
|
+
userId: string,
|
|
42
|
+
streams?: {
|
|
43
|
+
audioStream?: MediaStream;
|
|
44
|
+
screenShareAudioStream?: MediaStream;
|
|
45
|
+
},
|
|
46
|
+
) => {
|
|
47
|
+
call.state.updateOrAddParticipant(
|
|
48
|
+
sessionId,
|
|
49
|
+
fromPartial({
|
|
50
|
+
userId,
|
|
51
|
+
sessionId,
|
|
52
|
+
publishedTracks: [],
|
|
53
|
+
...streams,
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
it('should warn about dangling audio streams when active', () => {
|
|
59
|
+
// @ts-expect-error private property
|
|
60
|
+
const warnSpy = vi.spyOn(watchdog.logger, 'warn');
|
|
61
|
+
|
|
62
|
+
addRemoteParticipant('session-1', 'user-1', {
|
|
63
|
+
audioStream: new MediaStream(),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
call.state.setCallingState(CallingState.JOINED);
|
|
67
|
+
vi.advanceTimersByTime(3000);
|
|
68
|
+
|
|
69
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
70
|
+
expect.stringContaining('Dangling audio bindings detected'),
|
|
71
|
+
);
|
|
72
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('user-1'));
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should not warn when all audio elements are bound', () => {
|
|
76
|
+
// @ts-expect-error private property
|
|
77
|
+
const warnSpy = vi.spyOn(watchdog.logger, 'warn');
|
|
78
|
+
|
|
79
|
+
addRemoteParticipant('session-1', 'user-1', {
|
|
80
|
+
audioStream: new MediaStream(),
|
|
81
|
+
});
|
|
82
|
+
watchdog.register(
|
|
83
|
+
document.createElement('audio'),
|
|
84
|
+
'session-1',
|
|
85
|
+
'audioTrack',
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
call.state.setCallingState(CallingState.JOINED);
|
|
89
|
+
vi.advanceTimersByTime(3000);
|
|
90
|
+
|
|
91
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should skip local participant', () => {
|
|
95
|
+
// @ts-expect-error private property
|
|
96
|
+
const warnSpy = vi.spyOn(watchdog.logger, 'warn');
|
|
97
|
+
|
|
98
|
+
// @ts-expect-error incomplete data
|
|
99
|
+
call.state.updateOrAddParticipant('local-session', {
|
|
100
|
+
userId: 'local-user',
|
|
101
|
+
sessionId: 'local-session',
|
|
102
|
+
isLocalParticipant: true,
|
|
103
|
+
publishedTracks: [],
|
|
104
|
+
audioStream: new MediaStream(),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
call.state.setCallingState(CallingState.JOINED);
|
|
108
|
+
vi.advanceTimersByTime(3000);
|
|
109
|
+
|
|
110
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should start on JOINED and stop on non-JOINED state', () => {
|
|
114
|
+
// @ts-expect-error private property
|
|
115
|
+
const warnSpy = vi.spyOn(watchdog.logger, 'warn');
|
|
116
|
+
|
|
117
|
+
addRemoteParticipant('session-1', 'user-1', {
|
|
118
|
+
audioStream: new MediaStream(),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
call.state.setCallingState(CallingState.JOINED);
|
|
122
|
+
vi.advanceTimersByTime(3000);
|
|
123
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
124
|
+
|
|
125
|
+
warnSpy.mockClear();
|
|
126
|
+
|
|
127
|
+
call.state.setCallingState(CallingState.IDLE);
|
|
128
|
+
vi.advanceTimersByTime(6000);
|
|
129
|
+
|
|
130
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should be disableable via setEnabled', () => {
|
|
134
|
+
// @ts-expect-error private property
|
|
135
|
+
const warnSpy = vi.spyOn(watchdog.logger, 'warn');
|
|
136
|
+
|
|
137
|
+
addRemoteParticipant('session-1', 'user-1', {
|
|
138
|
+
audioStream: new MediaStream(),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
watchdog.setEnabled(false);
|
|
142
|
+
|
|
143
|
+
call.state.setCallingState(CallingState.JOINED);
|
|
144
|
+
vi.advanceTimersByTime(6000);
|
|
145
|
+
|
|
146
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should re-enable after disabling', () => {
|
|
150
|
+
// @ts-expect-error private property
|
|
151
|
+
const warnSpy = vi.spyOn(watchdog.logger, 'warn');
|
|
152
|
+
|
|
153
|
+
addRemoteParticipant('session-1', 'user-1', {
|
|
154
|
+
audioStream: new MediaStream(),
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
watchdog.setEnabled(false);
|
|
158
|
+
watchdog.setEnabled(true);
|
|
159
|
+
|
|
160
|
+
vi.advanceTimersByTime(3000);
|
|
161
|
+
|
|
162
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
163
|
+
expect.stringContaining('Dangling audio bindings detected'),
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should warn when binding a different element to the same key', () => {
|
|
168
|
+
// @ts-expect-error private property
|
|
169
|
+
const warnSpy = vi.spyOn(watchdog.logger, 'warn');
|
|
170
|
+
|
|
171
|
+
const audioElement1 = document.createElement('audio');
|
|
172
|
+
const audioElement2 = document.createElement('audio');
|
|
173
|
+
|
|
174
|
+
watchdog.register(audioElement1, 'session-1', 'audioTrack');
|
|
175
|
+
watchdog.register(audioElement2, 'session-1', 'audioTrack');
|
|
176
|
+
|
|
177
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
178
|
+
expect.stringContaining('Audio element already bound'),
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should not warn when re-binding the same element', () => {
|
|
183
|
+
// @ts-expect-error private property
|
|
184
|
+
const warnSpy = vi.spyOn(watchdog.logger, 'warn');
|
|
185
|
+
|
|
186
|
+
const audioElement = document.createElement('audio');
|
|
187
|
+
|
|
188
|
+
watchdog.register(audioElement, 'session-1', 'audioTrack');
|
|
189
|
+
watchdog.register(audioElement, 'session-1', 'audioTrack');
|
|
190
|
+
|
|
191
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('unregisterBinding should remove the binding', () => {
|
|
195
|
+
// @ts-expect-error private property
|
|
196
|
+
const warnSpy = vi.spyOn(watchdog.logger, 'warn');
|
|
197
|
+
|
|
198
|
+
addRemoteParticipant('session-1', 'user-1', {
|
|
199
|
+
audioStream: new MediaStream(),
|
|
200
|
+
});
|
|
201
|
+
watchdog.register(
|
|
202
|
+
document.createElement('audio'),
|
|
203
|
+
'session-1',
|
|
204
|
+
'audioTrack',
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
call.state.setCallingState(CallingState.JOINED);
|
|
208
|
+
vi.advanceTimersByTime(3000);
|
|
209
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
210
|
+
|
|
211
|
+
watchdog.unregister('session-1', 'audioTrack');
|
|
212
|
+
vi.advanceTimersByTime(3000);
|
|
213
|
+
|
|
214
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
215
|
+
expect.stringContaining('Dangling audio bindings detected'),
|
|
216
|
+
);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should warn about dangling screenShareAudioStream', () => {
|
|
220
|
+
// @ts-expect-error private property
|
|
221
|
+
const warnSpy = vi.spyOn(watchdog.logger, 'warn');
|
|
222
|
+
|
|
223
|
+
addRemoteParticipant('session-1', 'user-1', {
|
|
224
|
+
screenShareAudioStream: new MediaStream(),
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
call.state.setCallingState(CallingState.JOINED);
|
|
228
|
+
vi.advanceTimersByTime(3000);
|
|
229
|
+
|
|
230
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
231
|
+
expect.stringContaining('Dangling audio bindings detected'),
|
|
232
|
+
);
|
|
233
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('user-1'));
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should not warn when screenShareAudio element is bound', () => {
|
|
237
|
+
// @ts-expect-error private property
|
|
238
|
+
const warnSpy = vi.spyOn(watchdog.logger, 'warn');
|
|
239
|
+
|
|
240
|
+
addRemoteParticipant('session-1', 'user-1', {
|
|
241
|
+
screenShareAudioStream: new MediaStream(),
|
|
242
|
+
});
|
|
243
|
+
watchdog.register(
|
|
244
|
+
document.createElement('audio'),
|
|
245
|
+
'session-1',
|
|
246
|
+
'screenShareAudioTrack',
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
call.state.setCallingState(CallingState.JOINED);
|
|
250
|
+
vi.advanceTimersByTime(3000);
|
|
251
|
+
|
|
252
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('should warn only about the unbound track type', () => {
|
|
256
|
+
// @ts-expect-error private property
|
|
257
|
+
const warnSpy = vi.spyOn(watchdog.logger, 'warn');
|
|
258
|
+
|
|
259
|
+
addRemoteParticipant('session-1', 'user-1', {
|
|
260
|
+
audioStream: new MediaStream(),
|
|
261
|
+
screenShareAudioStream: new MediaStream(),
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// bind only the regular audio track
|
|
265
|
+
watchdog.register(
|
|
266
|
+
document.createElement('audio'),
|
|
267
|
+
'session-1',
|
|
268
|
+
'audioTrack',
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
call.state.setCallingState(CallingState.JOINED);
|
|
272
|
+
vi.advanceTimersByTime(3000);
|
|
273
|
+
|
|
274
|
+
// should still warn because screenShareAudio is unbound
|
|
275
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
276
|
+
expect.stringContaining('Dangling audio bindings detected'),
|
|
277
|
+
);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should not warn when both audio and screenShareAudio are bound', () => {
|
|
281
|
+
// @ts-expect-error private property
|
|
282
|
+
const warnSpy = vi.spyOn(watchdog.logger, 'warn');
|
|
283
|
+
|
|
284
|
+
addRemoteParticipant('session-1', 'user-1', {
|
|
285
|
+
audioStream: new MediaStream(),
|
|
286
|
+
screenShareAudioStream: new MediaStream(),
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
watchdog.register(
|
|
290
|
+
document.createElement('audio'),
|
|
291
|
+
'session-1',
|
|
292
|
+
'audioTrack',
|
|
293
|
+
);
|
|
294
|
+
watchdog.register(
|
|
295
|
+
document.createElement('audio'),
|
|
296
|
+
'session-1',
|
|
297
|
+
'screenShareAudioTrack',
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
call.state.setCallingState(CallingState.JOINED);
|
|
301
|
+
vi.advanceTimersByTime(3000);
|
|
302
|
+
|
|
303
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('dispose should stop the watchdog', () => {
|
|
307
|
+
// @ts-expect-error private property
|
|
308
|
+
const warnSpy = vi.spyOn(watchdog.logger, 'warn');
|
|
309
|
+
|
|
310
|
+
addRemoteParticipant('session-1', 'user-1', {
|
|
311
|
+
audioStream: new MediaStream(),
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
call.state.setCallingState(CallingState.JOINED);
|
|
315
|
+
vi.advanceTimersByTime(3000);
|
|
316
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
317
|
+
|
|
318
|
+
warnSpy.mockClear();
|
|
319
|
+
|
|
320
|
+
watchdog.dispose();
|
|
321
|
+
vi.advanceTimersByTime(6000);
|
|
322
|
+
|
|
323
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
324
|
+
});
|
|
325
|
+
});
|
|
@@ -618,6 +618,70 @@ describe('DynascaleManager', () => {
|
|
|
618
618
|
});
|
|
619
619
|
});
|
|
620
620
|
|
|
621
|
+
it('audio: should register and unregister watchdog binding', () => {
|
|
622
|
+
const watchdog = dynascaleManager.audioBindingsWatchdog!;
|
|
623
|
+
const registerSpy = vi.spyOn(watchdog, 'register');
|
|
624
|
+
const unregisterSpy = vi.spyOn(watchdog, 'unregister');
|
|
625
|
+
|
|
626
|
+
// @ts-expect-error incomplete data
|
|
627
|
+
call.state.updateOrAddParticipant('session-id', {
|
|
628
|
+
userId: 'user-id',
|
|
629
|
+
sessionId: 'session-id',
|
|
630
|
+
publishedTracks: [],
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
const cleanup = dynascaleManager.bindAudioElement(
|
|
634
|
+
document.createElement('audio'),
|
|
635
|
+
'session-id',
|
|
636
|
+
'audioTrack',
|
|
637
|
+
);
|
|
638
|
+
|
|
639
|
+
expect(registerSpy).toHaveBeenCalledWith(
|
|
640
|
+
expect.any(HTMLAudioElement),
|
|
641
|
+
'session-id',
|
|
642
|
+
'audioTrack',
|
|
643
|
+
);
|
|
644
|
+
|
|
645
|
+
cleanup?.();
|
|
646
|
+
|
|
647
|
+
expect(unregisterSpy).toHaveBeenCalledWith('session-id', 'audioTrack');
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
it('audio: should warn when binding an already-bound session', () => {
|
|
651
|
+
const watchdog = dynascaleManager.audioBindingsWatchdog!;
|
|
652
|
+
// @ts-expect-error private property
|
|
653
|
+
const warnSpy = vi.spyOn(watchdog.logger, 'warn');
|
|
654
|
+
|
|
655
|
+
// @ts-expect-error incomplete data
|
|
656
|
+
call.state.updateOrAddParticipant('session-id', {
|
|
657
|
+
userId: 'user-id',
|
|
658
|
+
sessionId: 'session-id',
|
|
659
|
+
publishedTracks: [],
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
const audioElement1 = document.createElement('audio');
|
|
663
|
+
const audioElement2 = document.createElement('audio');
|
|
664
|
+
|
|
665
|
+
const cleanup1 = dynascaleManager.bindAudioElement(
|
|
666
|
+
audioElement1,
|
|
667
|
+
'session-id',
|
|
668
|
+
'audioTrack',
|
|
669
|
+
);
|
|
670
|
+
|
|
671
|
+
const cleanup2 = dynascaleManager.bindAudioElement(
|
|
672
|
+
audioElement2,
|
|
673
|
+
'session-id',
|
|
674
|
+
'audioTrack',
|
|
675
|
+
);
|
|
676
|
+
|
|
677
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
678
|
+
expect.stringContaining('Audio element already bound'),
|
|
679
|
+
);
|
|
680
|
+
|
|
681
|
+
cleanup1?.();
|
|
682
|
+
cleanup2?.();
|
|
683
|
+
});
|
|
684
|
+
|
|
621
685
|
it('video: should unsubscribe when element dimensions are zero', () => {
|
|
622
686
|
// @ts-expect-error incomplete data
|
|
623
687
|
call.state.updateOrAddParticipant('session-id', {
|