@stream-io/video-client 1.44.4 → 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 +6 -0
- package/dist/index.browser.es.js +109 -16
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +109 -16
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +109 -16
- package/dist/index.es.js.map +1 -1
- package/dist/src/helpers/AudioBindingsWatchdog.d.ts +37 -0
- package/dist/src/helpers/DynascaleManager.d.ts +3 -1
- package/package.json +1 -1
- package/src/devices/devices.ts +1 -3
- 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,37 @@
|
|
|
1
|
+
import type { AudioTrackType } from '../types';
|
|
2
|
+
import { CallState } from '../store';
|
|
3
|
+
import { Tracer } from '../stats';
|
|
4
|
+
/**
|
|
5
|
+
* Tracks audio element bindings and periodically warns about
|
|
6
|
+
* remote participants whose audio streams have no bound element.
|
|
7
|
+
*/
|
|
8
|
+
export declare class AudioBindingsWatchdog {
|
|
9
|
+
private state;
|
|
10
|
+
private tracer;
|
|
11
|
+
private bindings;
|
|
12
|
+
private enabled;
|
|
13
|
+
private watchdogInterval?;
|
|
14
|
+
private readonly unsubscribeCallingState;
|
|
15
|
+
private logger;
|
|
16
|
+
constructor(state: CallState, tracer: Tracer);
|
|
17
|
+
/**
|
|
18
|
+
* Registers an audio element binding for the given session and track type.
|
|
19
|
+
* Warns if a different element is already bound to the same key.
|
|
20
|
+
*/
|
|
21
|
+
register: (audioElement: HTMLAudioElement, sessionId: string, trackType: AudioTrackType) => void;
|
|
22
|
+
/**
|
|
23
|
+
* Removes the audio element binding for the given session and track type.
|
|
24
|
+
*/
|
|
25
|
+
unregister: (sessionId: string, trackType: AudioTrackType) => void;
|
|
26
|
+
/**
|
|
27
|
+
* Enables or disables the watchdog.
|
|
28
|
+
* When disabled, the periodic check stops but bindings are still tracked.
|
|
29
|
+
*/
|
|
30
|
+
setEnabled: (enabled: boolean) => void;
|
|
31
|
+
/**
|
|
32
|
+
* Stops the watchdog and unsubscribes from callingState changes.
|
|
33
|
+
*/
|
|
34
|
+
dispose: () => void;
|
|
35
|
+
private start;
|
|
36
|
+
private stop;
|
|
37
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { AudioTrackType, DebounceType, VideoTrackType } from '../types';
|
|
2
2
|
import { VideoDimension } from '../gen/video/sfu/models/models';
|
|
3
3
|
import { ViewportTracker } from './ViewportTracker';
|
|
4
|
+
import { AudioBindingsWatchdog } from './AudioBindingsWatchdog';
|
|
4
5
|
import type { TrackSubscriptionDetails } from '../gen/video/sfu/signal_rpc/signal';
|
|
5
|
-
import
|
|
6
|
+
import { CallState } from '../store';
|
|
6
7
|
import type { StreamSfuClient } from '../StreamSfuClient';
|
|
7
8
|
import { SpeakerManager } from '../devices';
|
|
8
9
|
import { Tracer } from '../stats';
|
|
@@ -40,6 +41,7 @@ export declare class DynascaleManager {
|
|
|
40
41
|
private audioContext;
|
|
41
42
|
private sfuClient;
|
|
42
43
|
private pendingSubscriptionsUpdate;
|
|
44
|
+
readonly audioBindingsWatchdog: AudioBindingsWatchdog | undefined;
|
|
43
45
|
private videoTrackSubscriptionOverridesSubject;
|
|
44
46
|
videoTrackSubscriptionOverrides$: import("rxjs").Observable<VideoTrackSubscriptionOverrides>;
|
|
45
47
|
incomingVideoSettings$: import("rxjs").Observable<{
|
package/package.json
CHANGED
package/src/devices/devices.ts
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
} from 'rxjs';
|
|
12
12
|
import { BrowserPermission } from './BrowserPermission';
|
|
13
13
|
import { lazy } from '../helpers/lazy';
|
|
14
|
-
import { isFirefox
|
|
14
|
+
import { isFirefox } from '../helpers/browsers';
|
|
15
15
|
import { dumpStream, Tracer } from '../stats';
|
|
16
16
|
import { getCurrentValue } from '../store/rxUtils';
|
|
17
17
|
import { videoLoggerSystem } from '../logger';
|
|
@@ -61,8 +61,6 @@ const getDevices = (
|
|
|
61
61
|
*/
|
|
62
62
|
export const checkIfAudioOutputChangeSupported = () => {
|
|
63
63
|
if (typeof document === 'undefined') return false;
|
|
64
|
-
// Safari uses WebAudio API for playing audio, so we check the AudioContext prototype
|
|
65
|
-
if (isSafari()) return 'setSinkId' in AudioContext.prototype;
|
|
66
64
|
const element = document.createElement('audio');
|
|
67
65
|
return 'setSinkId' in element;
|
|
68
66
|
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { AudioTrackType } from '../types';
|
|
2
|
+
import { CallingState, CallState } from '../store';
|
|
3
|
+
import { createSubscription } from '../store/rxUtils';
|
|
4
|
+
import { videoLoggerSystem } from '../logger';
|
|
5
|
+
import { Tracer } from '../stats';
|
|
6
|
+
|
|
7
|
+
const toBindingKey = (
|
|
8
|
+
sessionId: string,
|
|
9
|
+
trackType: AudioTrackType = 'audioTrack',
|
|
10
|
+
) => `${sessionId}/${trackType}`;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Tracks audio element bindings and periodically warns about
|
|
14
|
+
* remote participants whose audio streams have no bound element.
|
|
15
|
+
*/
|
|
16
|
+
export class AudioBindingsWatchdog {
|
|
17
|
+
private bindings = new Map<string, HTMLAudioElement>();
|
|
18
|
+
private enabled = true;
|
|
19
|
+
private watchdogInterval?: NodeJS.Timeout;
|
|
20
|
+
private readonly unsubscribeCallingState: () => void;
|
|
21
|
+
private logger = videoLoggerSystem.getLogger('AudioBindingsWatchdog');
|
|
22
|
+
|
|
23
|
+
constructor(
|
|
24
|
+
private state: CallState,
|
|
25
|
+
private tracer: Tracer,
|
|
26
|
+
) {
|
|
27
|
+
this.unsubscribeCallingState = createSubscription(
|
|
28
|
+
state.callingState$,
|
|
29
|
+
(callingState) => {
|
|
30
|
+
if (!this.enabled) return;
|
|
31
|
+
if (callingState !== CallingState.JOINED) {
|
|
32
|
+
this.stop();
|
|
33
|
+
} else {
|
|
34
|
+
this.start();
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Registers an audio element binding for the given session and track type.
|
|
42
|
+
* Warns if a different element is already bound to the same key.
|
|
43
|
+
*/
|
|
44
|
+
register = (
|
|
45
|
+
audioElement: HTMLAudioElement,
|
|
46
|
+
sessionId: string,
|
|
47
|
+
trackType: AudioTrackType,
|
|
48
|
+
) => {
|
|
49
|
+
const key = toBindingKey(sessionId, trackType);
|
|
50
|
+
const existing = this.bindings.get(key);
|
|
51
|
+
if (existing && existing !== audioElement) {
|
|
52
|
+
this.logger.warn(
|
|
53
|
+
`Audio element already bound to ${sessionId} and ${trackType}`,
|
|
54
|
+
);
|
|
55
|
+
this.tracer.trace('audioBinding.alreadyBoundWarning', trackType);
|
|
56
|
+
}
|
|
57
|
+
this.bindings.set(key, audioElement);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Removes the audio element binding for the given session and track type.
|
|
62
|
+
*/
|
|
63
|
+
unregister = (sessionId: string, trackType: AudioTrackType) => {
|
|
64
|
+
this.bindings.delete(toBindingKey(sessionId, trackType));
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Enables or disables the watchdog.
|
|
69
|
+
* When disabled, the periodic check stops but bindings are still tracked.
|
|
70
|
+
*/
|
|
71
|
+
setEnabled = (enabled: boolean) => {
|
|
72
|
+
this.enabled = enabled;
|
|
73
|
+
if (enabled) {
|
|
74
|
+
this.start();
|
|
75
|
+
} else {
|
|
76
|
+
this.stop();
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Stops the watchdog and unsubscribes from callingState changes.
|
|
82
|
+
*/
|
|
83
|
+
dispose = () => {
|
|
84
|
+
this.stop();
|
|
85
|
+
this.unsubscribeCallingState();
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
private start = () => {
|
|
89
|
+
clearInterval(this.watchdogInterval);
|
|
90
|
+
this.watchdogInterval = setInterval(() => {
|
|
91
|
+
const danglingUserIds: string[] = [];
|
|
92
|
+
for (const p of this.state.participants) {
|
|
93
|
+
if (p.isLocalParticipant) continue;
|
|
94
|
+
const { audioStream, screenShareAudioStream, sessionId, userId } = p;
|
|
95
|
+
if (audioStream && !this.bindings.has(toBindingKey(sessionId))) {
|
|
96
|
+
danglingUserIds.push(userId);
|
|
97
|
+
}
|
|
98
|
+
if (
|
|
99
|
+
screenShareAudioStream &&
|
|
100
|
+
!this.bindings.has(toBindingKey(sessionId, 'screenShareAudioTrack'))
|
|
101
|
+
) {
|
|
102
|
+
danglingUserIds.push(userId);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (danglingUserIds.length > 0) {
|
|
106
|
+
const key = 'audioBinding.danglingWarning';
|
|
107
|
+
this.tracer.traceOnce(key, key, danglingUserIds);
|
|
108
|
+
this.logger.warn(
|
|
109
|
+
`Dangling audio bindings detected. Did you forget to bind the audio element? user_ids: ${danglingUserIds}.`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}, 3000);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
private stop = () => {
|
|
116
|
+
clearInterval(this.watchdogInterval);
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -15,14 +15,16 @@ import {
|
|
|
15
15
|
takeWhile,
|
|
16
16
|
} from 'rxjs';
|
|
17
17
|
import { ViewportTracker } from './ViewportTracker';
|
|
18
|
+
import { AudioBindingsWatchdog } from './AudioBindingsWatchdog';
|
|
18
19
|
import { isFirefox, isSafari } from './browsers';
|
|
20
|
+
import { isReactNative } from './platforms';
|
|
19
21
|
import {
|
|
20
22
|
hasScreenShare,
|
|
21
23
|
hasScreenShareAudio,
|
|
22
24
|
hasVideo,
|
|
23
25
|
} from './participantUtils';
|
|
24
26
|
import type { TrackSubscriptionDetails } from '../gen/video/sfu/signal_rpc/signal';
|
|
25
|
-
import
|
|
27
|
+
import { CallState } from '../store';
|
|
26
28
|
import type { StreamSfuClient } from '../StreamSfuClient';
|
|
27
29
|
import { SpeakerManager } from '../devices';
|
|
28
30
|
import { getCurrentValue, setCurrentValue } from '../store/rxUtils';
|
|
@@ -71,10 +73,11 @@ export class DynascaleManager {
|
|
|
71
73
|
private callState: CallState;
|
|
72
74
|
private speaker: SpeakerManager;
|
|
73
75
|
private tracer: Tracer;
|
|
74
|
-
private useWebAudio =
|
|
76
|
+
private useWebAudio = false;
|
|
75
77
|
private audioContext: AudioContext | undefined;
|
|
76
78
|
private sfuClient: StreamSfuClient | undefined;
|
|
77
79
|
private pendingSubscriptionsUpdate: NodeJS.Timeout | null = null;
|
|
80
|
+
readonly audioBindingsWatchdog: AudioBindingsWatchdog | undefined;
|
|
78
81
|
|
|
79
82
|
private videoTrackSubscriptionOverridesSubject =
|
|
80
83
|
new BehaviorSubject<VideoTrackSubscriptionOverrides>({});
|
|
@@ -120,6 +123,9 @@ export class DynascaleManager {
|
|
|
120
123
|
this.callState = callState;
|
|
121
124
|
this.speaker = speaker;
|
|
122
125
|
this.tracer = tracer;
|
|
126
|
+
if (!isReactNative()) {
|
|
127
|
+
this.audioBindingsWatchdog = new AudioBindingsWatchdog(callState, tracer);
|
|
128
|
+
}
|
|
123
129
|
}
|
|
124
130
|
|
|
125
131
|
/**
|
|
@@ -129,7 +135,8 @@ export class DynascaleManager {
|
|
|
129
135
|
if (this.pendingSubscriptionsUpdate) {
|
|
130
136
|
clearTimeout(this.pendingSubscriptionsUpdate);
|
|
131
137
|
}
|
|
132
|
-
|
|
138
|
+
this.audioBindingsWatchdog?.dispose();
|
|
139
|
+
const context = this.audioContext;
|
|
133
140
|
if (context && context.state !== 'closed') {
|
|
134
141
|
document.removeEventListener('click', this.resumeAudioContext);
|
|
135
142
|
await context.close();
|
|
@@ -447,6 +454,7 @@ export class DynascaleManager {
|
|
|
447
454
|
});
|
|
448
455
|
resizeObserver?.observe(videoElement);
|
|
449
456
|
|
|
457
|
+
const isVideoTrack = trackType === 'videoTrack';
|
|
450
458
|
// element renders and gets bound - track subscription gets
|
|
451
459
|
// triggered first other ones get skipped on initial subscriptions
|
|
452
460
|
const publishedTracksSubscription = boundParticipant.isLocalParticipant
|
|
@@ -454,9 +462,7 @@ export class DynascaleManager {
|
|
|
454
462
|
: participant$
|
|
455
463
|
.pipe(
|
|
456
464
|
distinctUntilKeyChanged('publishedTracks'),
|
|
457
|
-
map((p) =>
|
|
458
|
-
trackType === 'videoTrack' ? hasVideo(p) : hasScreenShare(p),
|
|
459
|
-
),
|
|
465
|
+
map((p) => (isVideoTrack ? hasVideo(p) : hasScreenShare(p))),
|
|
460
466
|
distinctUntilChanged(),
|
|
461
467
|
)
|
|
462
468
|
.subscribe((isPublishing) => {
|
|
@@ -480,15 +486,11 @@ export class DynascaleManager {
|
|
|
480
486
|
// https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide
|
|
481
487
|
videoElement.muted = true;
|
|
482
488
|
|
|
489
|
+
const trackKey = isVideoTrack ? 'videoStream' : 'screenShareStream';
|
|
483
490
|
const streamSubscription = participant$
|
|
484
|
-
.pipe(
|
|
485
|
-
distinctUntilKeyChanged(
|
|
486
|
-
trackType === 'videoTrack' ? 'videoStream' : 'screenShareStream',
|
|
487
|
-
),
|
|
488
|
-
)
|
|
491
|
+
.pipe(distinctUntilKeyChanged(trackKey))
|
|
489
492
|
.subscribe((p) => {
|
|
490
|
-
const source =
|
|
491
|
-
trackType === 'videoTrack' ? p.videoStream : p.screenShareStream;
|
|
493
|
+
const source = isVideoTrack ? p.videoStream : p.screenShareStream;
|
|
492
494
|
if (videoElement.srcObject === source) return;
|
|
493
495
|
videoElement.srcObject = source ?? null;
|
|
494
496
|
if (isSafari() || isFirefox()) {
|
|
@@ -532,6 +534,8 @@ export class DynascaleManager {
|
|
|
532
534
|
const participant = this.callState.findParticipantBySessionId(sessionId);
|
|
533
535
|
if (!participant || participant.isLocalParticipant) return;
|
|
534
536
|
|
|
537
|
+
this.audioBindingsWatchdog?.register(audioElement, sessionId, trackType);
|
|
538
|
+
|
|
535
539
|
const participant$ = this.callState.participants$.pipe(
|
|
536
540
|
map((ps) => ps.find((p) => p.sessionId === sessionId)),
|
|
537
541
|
takeWhile((p) => !!p),
|
|
@@ -561,19 +565,12 @@ export class DynascaleManager {
|
|
|
561
565
|
let sourceNode: MediaStreamAudioSourceNode | undefined = undefined;
|
|
562
566
|
let gainNode: GainNode | undefined = undefined;
|
|
563
567
|
|
|
568
|
+
const isAudioTrack = trackType === 'audioTrack';
|
|
569
|
+
const trackKey = isAudioTrack ? 'audioStream' : 'screenShareAudioStream';
|
|
564
570
|
const updateMediaStreamSubscription = participant$
|
|
565
|
-
.pipe(
|
|
566
|
-
distinctUntilKeyChanged(
|
|
567
|
-
trackType === 'screenShareAudioTrack'
|
|
568
|
-
? 'screenShareAudioStream'
|
|
569
|
-
: 'audioStream',
|
|
570
|
-
),
|
|
571
|
-
)
|
|
571
|
+
.pipe(distinctUntilKeyChanged(trackKey))
|
|
572
572
|
.subscribe((p) => {
|
|
573
|
-
const source =
|
|
574
|
-
trackType === 'screenShareAudioTrack'
|
|
575
|
-
? p.screenShareAudioStream
|
|
576
|
-
: p.audioStream;
|
|
573
|
+
const source = isAudioTrack ? p.audioStream : p.screenShareAudioStream;
|
|
577
574
|
if (audioElement.srcObject === source) return;
|
|
578
575
|
|
|
579
576
|
setTimeout(() => {
|
|
@@ -630,6 +627,7 @@ export class DynascaleManager {
|
|
|
630
627
|
audioElement.autoplay = true;
|
|
631
628
|
|
|
632
629
|
return () => {
|
|
630
|
+
this.audioBindingsWatchdog?.unregister(sessionId, trackType);
|
|
633
631
|
sinkIdSubscription?.unsubscribe();
|
|
634
632
|
volumeSubscription.unsubscribe();
|
|
635
633
|
updateMediaStreamSubscription.unsubscribe();
|
|
@@ -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
|
+
});
|