@stream-io/video-client 1.44.5 → 1.45.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 +10 -0
- package/dist/index.browser.es.js +23 -5
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +23 -5
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +23 -5
- package/dist/index.es.js.map +1 -1
- package/dist/src/coordinator/connection/types.d.ts +22 -1
- package/dist/src/devices/DeviceManager.d.ts +1 -0
- package/package.json +1 -1
- package/src/coordinator/connection/types.ts +23 -0
- package/src/devices/DeviceManager.ts +20 -1
- package/src/devices/__tests__/DeviceManager.test.ts +8 -0
- package/src/devices/__tests__/mocks.ts +4 -0
- package/src/helpers/AudioBindingsWatchdog.ts +14 -2
- package/src/helpers/__tests__/AudioBindingsWatchdog.test.ts +27 -1
|
@@ -3,6 +3,7 @@ import { ConnectedEvent, UserRequest, VideoEvent } from '../../gen/coordinator';
|
|
|
3
3
|
import { AllSfuEvents } from '../../rtc';
|
|
4
4
|
import type { ConfigureLoggersOptions, LogLevel } from '@stream-io/logger';
|
|
5
5
|
import type { DevicePersistenceOptions } from '../../devices/devicePersistence';
|
|
6
|
+
import { InputDeviceStatus } from '../../devices';
|
|
6
7
|
export type UR = Record<string, unknown>;
|
|
7
8
|
export type User = (Omit<UserRequest, 'role'> & {
|
|
8
9
|
type?: 'authenticated';
|
|
@@ -79,7 +80,27 @@ export type MicCaptureReportEvent = {
|
|
|
79
80
|
*/
|
|
80
81
|
label?: string;
|
|
81
82
|
};
|
|
82
|
-
export type
|
|
83
|
+
export type DeviceDisconnectedEvent = {
|
|
84
|
+
type: 'device.disconnected';
|
|
85
|
+
call_cid: string;
|
|
86
|
+
/**
|
|
87
|
+
* The device status at the time it was disconnected.
|
|
88
|
+
*/
|
|
89
|
+
status: InputDeviceStatus;
|
|
90
|
+
/**
|
|
91
|
+
* The disconnected device ID.
|
|
92
|
+
*/
|
|
93
|
+
deviceId: string;
|
|
94
|
+
/**
|
|
95
|
+
* The human-readable label of the disconnected device.
|
|
96
|
+
*/
|
|
97
|
+
label?: string;
|
|
98
|
+
/**
|
|
99
|
+
* The disconnected device kind.
|
|
100
|
+
*/
|
|
101
|
+
kind: MediaDeviceKind;
|
|
102
|
+
};
|
|
103
|
+
export type StreamVideoEvent = (VideoEvent | NetworkChangedEvent | ConnectionChangedEvent | TransportChangedEvent | ConnectionRecoveredEvent | MicCaptureReportEvent | DeviceDisconnectedEvent) & {
|
|
83
104
|
received_at?: string | Date;
|
|
84
105
|
};
|
|
85
106
|
export type StreamCallEvent = Extract<StreamVideoEvent, {
|
|
@@ -104,6 +104,7 @@ export declare abstract class DeviceManager<S extends DeviceManagerState<C>, C =
|
|
|
104
104
|
private get mediaDeviceKind();
|
|
105
105
|
private handleDisconnectedOrReplacedDevices;
|
|
106
106
|
protected findDevice(devices: MediaDeviceInfo[], deviceId: string): MediaDeviceInfo | undefined;
|
|
107
|
+
private dispatchDeviceDisconnectedEvent;
|
|
107
108
|
private persistPreference;
|
|
108
109
|
protected applyPersistedPreferences(enabledInCallType: boolean): Promise<boolean>;
|
|
109
110
|
private applyMutedState;
|
package/package.json
CHANGED
|
@@ -3,6 +3,7 @@ import { ConnectedEvent, UserRequest, VideoEvent } from '../../gen/coordinator';
|
|
|
3
3
|
import { AllSfuEvents } from '../../rtc';
|
|
4
4
|
import type { ConfigureLoggersOptions, LogLevel } from '@stream-io/logger';
|
|
5
5
|
import type { DevicePersistenceOptions } from '../../devices/devicePersistence';
|
|
6
|
+
import { InputDeviceStatus } from '../../devices';
|
|
6
7
|
|
|
7
8
|
export type UR = Record<string, unknown>;
|
|
8
9
|
|
|
@@ -126,6 +127,27 @@ export type MicCaptureReportEvent = {
|
|
|
126
127
|
label?: string;
|
|
127
128
|
};
|
|
128
129
|
|
|
130
|
+
export type DeviceDisconnectedEvent = {
|
|
131
|
+
type: 'device.disconnected';
|
|
132
|
+
call_cid: string;
|
|
133
|
+
/**
|
|
134
|
+
* The device status at the time it was disconnected.
|
|
135
|
+
*/
|
|
136
|
+
status: InputDeviceStatus;
|
|
137
|
+
/**
|
|
138
|
+
* The disconnected device ID.
|
|
139
|
+
*/
|
|
140
|
+
deviceId: string;
|
|
141
|
+
/**
|
|
142
|
+
* The human-readable label of the disconnected device.
|
|
143
|
+
*/
|
|
144
|
+
label?: string;
|
|
145
|
+
/**
|
|
146
|
+
* The disconnected device kind.
|
|
147
|
+
*/
|
|
148
|
+
kind: MediaDeviceKind;
|
|
149
|
+
};
|
|
150
|
+
|
|
129
151
|
export type StreamVideoEvent = (
|
|
130
152
|
| VideoEvent
|
|
131
153
|
| NetworkChangedEvent
|
|
@@ -133,6 +155,7 @@ export type StreamVideoEvent = (
|
|
|
133
155
|
| TransportChangedEvent
|
|
134
156
|
| ConnectionRecoveredEvent
|
|
135
157
|
| MicCaptureReportEvent
|
|
158
|
+
| DeviceDisconnectedEvent
|
|
136
159
|
) & { received_at?: string | Date };
|
|
137
160
|
|
|
138
161
|
// TODO: we should use WSCallEvent here but that needs fixing
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { combineLatest, firstValueFrom, Observable, pairwise } from 'rxjs';
|
|
2
2
|
import { Call } from '../Call';
|
|
3
|
+
import type { DeviceDisconnectedEvent } from '../coordinator/connection/types';
|
|
3
4
|
import { TrackPublishOptions } from '../rtc';
|
|
4
5
|
import { CallingState } from '../store';
|
|
5
6
|
import { createSubscription, getCurrentValue } from '../store/rxUtils';
|
|
@@ -13,6 +14,7 @@ import { ScopedLogger, videoLoggerSystem } from '../logger';
|
|
|
13
14
|
import { TrackType } from '../gen/video/sfu/models/models';
|
|
14
15
|
import { deviceIds$ } from './devices';
|
|
15
16
|
import {
|
|
17
|
+
hasPending,
|
|
16
18
|
settled,
|
|
17
19
|
withCancellation,
|
|
18
20
|
withoutConcurrency,
|
|
@@ -543,6 +545,7 @@ export abstract class DeviceManager<
|
|
|
543
545
|
}
|
|
544
546
|
|
|
545
547
|
if (isDeviceDisconnected) {
|
|
548
|
+
this.dispatchDeviceDisconnectedEvent(prevDevice!);
|
|
546
549
|
await this.disable();
|
|
547
550
|
await this.select(undefined);
|
|
548
551
|
}
|
|
@@ -553,7 +556,7 @@ export abstract class DeviceManager<
|
|
|
553
556
|
) {
|
|
554
557
|
await this.enable();
|
|
555
558
|
this.isTrackStoppedDueToTrackEnd = false;
|
|
556
|
-
} else {
|
|
559
|
+
} else if (!hasPending(this.statusChangeConcurrencyTag)) {
|
|
557
560
|
await this.applySettingsToStream();
|
|
558
561
|
}
|
|
559
562
|
}
|
|
@@ -573,6 +576,22 @@ export abstract class DeviceManager<
|
|
|
573
576
|
return devices.find((d) => d.deviceId === deviceId && d.kind === kind);
|
|
574
577
|
}
|
|
575
578
|
|
|
579
|
+
private dispatchDeviceDisconnectedEvent(device: MediaDeviceInfo) {
|
|
580
|
+
const event: DeviceDisconnectedEvent = {
|
|
581
|
+
type: 'device.disconnected',
|
|
582
|
+
call_cid: this.call.cid,
|
|
583
|
+
status: this.isTrackStoppedDueToTrackEnd
|
|
584
|
+
? this.state.prevStatus
|
|
585
|
+
: this.state.status,
|
|
586
|
+
deviceId: device.deviceId,
|
|
587
|
+
label: device.label,
|
|
588
|
+
kind: device.kind,
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
this.call.tracer.trace('device.disconnected', event);
|
|
592
|
+
this.call.streamClient.dispatchEvent(event);
|
|
593
|
+
}
|
|
594
|
+
|
|
576
595
|
private persistPreference(
|
|
577
596
|
selectedDevice: string | undefined,
|
|
578
597
|
status: InputDeviceStatus,
|
|
@@ -380,6 +380,14 @@ describe('Device Manager', () => {
|
|
|
380
380
|
|
|
381
381
|
expect(manager.state.selectedDevice).toBe(undefined);
|
|
382
382
|
expect(manager.state.status).toBe('disabled');
|
|
383
|
+
expect(manager['call'].streamClient.dispatchEvent).toHaveBeenCalledWith({
|
|
384
|
+
type: 'device.disconnected',
|
|
385
|
+
call_cid: manager['call'].cid,
|
|
386
|
+
status: 'enabled',
|
|
387
|
+
deviceId: device.deviceId,
|
|
388
|
+
label: device.label,
|
|
389
|
+
kind: device.kind,
|
|
390
|
+
});
|
|
383
391
|
|
|
384
392
|
vi.useRealTimers();
|
|
385
393
|
});
|
|
@@ -95,9 +95,13 @@ export const mockCall = (): Partial<Call> => {
|
|
|
95
95
|
}),
|
|
96
96
|
);
|
|
97
97
|
return {
|
|
98
|
+
cid: 'default:test-call',
|
|
98
99
|
state: callState,
|
|
99
100
|
publish: vi.fn(),
|
|
100
101
|
stopPublish: vi.fn(),
|
|
102
|
+
streamClient: fromPartial({
|
|
103
|
+
dispatchEvent: vi.fn(),
|
|
104
|
+
}),
|
|
101
105
|
notifyNoiseCancellationStarting: vi.fn().mockResolvedValue(undefined),
|
|
102
106
|
notifyNoiseCancellationStopped: vi.fn().mockResolvedValue(undefined),
|
|
103
107
|
tracer: new Tracer('tests'),
|
|
@@ -3,6 +3,7 @@ import { CallingState, CallState } from '../store';
|
|
|
3
3
|
import { createSubscription } from '../store/rxUtils';
|
|
4
4
|
import { videoLoggerSystem } from '../logger';
|
|
5
5
|
import { Tracer } from '../stats';
|
|
6
|
+
import { TrackType } from '../gen/video/sfu/models/models';
|
|
6
7
|
|
|
7
8
|
const toBindingKey = (
|
|
8
9
|
sessionId: string,
|
|
@@ -91,12 +92,23 @@ export class AudioBindingsWatchdog {
|
|
|
91
92
|
const danglingUserIds: string[] = [];
|
|
92
93
|
for (const p of this.state.participants) {
|
|
93
94
|
if (p.isLocalParticipant) continue;
|
|
94
|
-
const {
|
|
95
|
-
|
|
95
|
+
const {
|
|
96
|
+
audioStream,
|
|
97
|
+
screenShareAudioStream,
|
|
98
|
+
sessionId,
|
|
99
|
+
userId,
|
|
100
|
+
publishedTracks,
|
|
101
|
+
} = p;
|
|
102
|
+
if (
|
|
103
|
+
audioStream &&
|
|
104
|
+
publishedTracks.includes(TrackType.AUDIO) &&
|
|
105
|
+
!this.bindings.has(toBindingKey(sessionId))
|
|
106
|
+
) {
|
|
96
107
|
danglingUserIds.push(userId);
|
|
97
108
|
}
|
|
98
109
|
if (
|
|
99
110
|
screenShareAudioStream &&
|
|
111
|
+
publishedTracks.includes(TrackType.SCREEN_SHARE_AUDIO) &&
|
|
100
112
|
!this.bindings.has(toBindingKey(sessionId, 'screenShareAudioTrack'))
|
|
101
113
|
) {
|
|
102
114
|
danglingUserIds.push(userId);
|
|
@@ -11,6 +11,7 @@ import { StreamClient } from '../../coordinator/connection/client';
|
|
|
11
11
|
import { CallingState, StreamVideoWriteableStateStore } from '../../store';
|
|
12
12
|
import { noopComparator } from '../../sorting';
|
|
13
13
|
import { fromPartial } from '@total-typescript/shoehorn';
|
|
14
|
+
import { TrackType } from '../../gen/video/sfu/models/models';
|
|
14
15
|
|
|
15
16
|
describe('AudioBindingsWatchdog', () => {
|
|
16
17
|
let watchdog: AudioBindingsWatchdog;
|
|
@@ -44,12 +45,17 @@ describe('AudioBindingsWatchdog', () => {
|
|
|
44
45
|
screenShareAudioStream?: MediaStream;
|
|
45
46
|
},
|
|
46
47
|
) => {
|
|
48
|
+
const publishedTracks = [];
|
|
49
|
+
if (streams?.audioStream) publishedTracks.push(TrackType.AUDIO);
|
|
50
|
+
if (streams?.screenShareAudioStream) {
|
|
51
|
+
publishedTracks.push(TrackType.SCREEN_SHARE_AUDIO);
|
|
52
|
+
}
|
|
47
53
|
call.state.updateOrAddParticipant(
|
|
48
54
|
sessionId,
|
|
49
55
|
fromPartial({
|
|
50
56
|
userId,
|
|
51
57
|
sessionId,
|
|
52
|
-
publishedTracks
|
|
58
|
+
publishedTracks,
|
|
53
59
|
...streams,
|
|
54
60
|
}),
|
|
55
61
|
);
|
|
@@ -233,6 +239,26 @@ describe('AudioBindingsWatchdog', () => {
|
|
|
233
239
|
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('user-1'));
|
|
234
240
|
});
|
|
235
241
|
|
|
242
|
+
it('should not warn when audioStream exists but audio is not published', () => {
|
|
243
|
+
// @ts-expect-error private property
|
|
244
|
+
const warnSpy = vi.spyOn(watchdog.logger, 'warn');
|
|
245
|
+
|
|
246
|
+
call.state.updateOrAddParticipant(
|
|
247
|
+
'session-1',
|
|
248
|
+
fromPartial({
|
|
249
|
+
userId: 'user-1',
|
|
250
|
+
sessionId: 'session-1',
|
|
251
|
+
publishedTracks: [],
|
|
252
|
+
audioStream: new MediaStream(),
|
|
253
|
+
}),
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
call.state.setCallingState(CallingState.JOINED);
|
|
257
|
+
vi.advanceTimersByTime(3000);
|
|
258
|
+
|
|
259
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
260
|
+
});
|
|
261
|
+
|
|
236
262
|
it('should not warn when screenShareAudio element is bound', () => {
|
|
237
263
|
// @ts-expect-error private property
|
|
238
264
|
const warnSpy = vi.spyOn(watchdog.logger, 'warn');
|