@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.
@@ -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 StreamVideoEvent = (VideoEvent | NetworkChangedEvent | ConnectionChangedEvent | TransportChangedEvent | ConnectionRecoveredEvent | MicCaptureReportEvent) & {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-client",
3
- "version": "1.44.5",
3
+ "version": "1.45.0",
4
4
  "main": "dist/index.cjs.js",
5
5
  "module": "dist/index.es.js",
6
6
  "browser": "dist/index.browser.es.js",
@@ -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 { audioStream, screenShareAudioStream, sessionId, userId } = p;
95
- if (audioStream && !this.bindings.has(toBindingKey(sessionId))) {
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');