@stream-io/video-client 1.44.6-beta.0 → 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.
@@ -162,7 +162,7 @@ export declare class Call {
162
162
  /**
163
163
  * A flag indicating whether the call was created by the current user.
164
164
  */
165
- get isCreatedByMe(): boolean | "" | undefined;
165
+ get isCreatedByMe(): boolean;
166
166
  /**
167
167
  * Update from the call response from the "call.ring" event
168
168
  * @internal
@@ -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;
@@ -5,7 +5,6 @@ import type { RejectReason, StreamClientOptions, TokenProvider, User } from './c
5
5
  import type { Comparator } from './sorting';
6
6
  import type { StreamVideoWriteableStateStore } from './store';
7
7
  import { AxiosError } from 'axios';
8
- import type { Call } from './Call';
9
8
  export type StreamReaction = Pick<ReactionResponse, 'type' | 'emoji_code' | 'custom'>;
10
9
  export declare enum VisibilityState {
11
10
  UNKNOWN = "UNKNOWN",
@@ -317,53 +316,22 @@ export type StartCallRecordingFnType = {
317
316
  (request: StartRecordingRequest): Promise<StartRecordingResponse>;
318
317
  (request: StartRecordingRequest, type: CallRecordingType): Promise<StartRecordingResponse>;
319
318
  };
320
- type StreamRNVideoSDKCallManagerRingingParams = {
321
- isRingingTypeCall: boolean;
322
- };
323
- type StreamRNVideoSDKCallManagerSetupParams = StreamRNVideoSDKCallManagerRingingParams & {
324
- defaultDevice: AudioSettingsRequestDefaultDeviceEnum;
325
- };
326
- type StreamRNVideoSDKEndCallReason =
327
- /** Call ended by the local user (e.g., hanging up). */
328
- 'local'
329
- /** Call ended by the remote party, or outgoing call was not answered. */
330
- | 'remote'
331
- /** Call was rejected/declined by the user. */
332
- | 'rejected'
333
- /** Remote party was busy. */
334
- | 'busy'
335
- /** Call was answered on another device. */
336
- | 'answeredElsewhere'
337
- /** No response to an incoming call. */
338
- | 'missed'
339
- /** Call failed due to an error (e.g., network issue). */
340
- | 'error'
341
- /** Call was canceled before the remote party could answer. */
342
- | 'canceled'
343
- /** Call restricted (e.g., airplane mode, dialing restrictions). */
344
- | 'restricted'
345
- /** Unknown or unspecified disconnect reason. */
346
- | 'unknown';
347
- type StreamRNVideoSDKCallingX = {
348
- joinCall: (call: Call, activeCalls: Call[]) => Promise<void>;
349
- endCall: (call: Call, reason?: StreamRNVideoSDKEndCallReason) => Promise<void>;
350
- registerOutgoingCall: (call: Call) => Promise<void>;
351
- };
352
319
  export type StreamRNVideoSDKGlobals = {
353
- callingX: StreamRNVideoSDKCallingX;
354
320
  callManager: {
355
321
  /**
356
322
  * Sets up the in call manager.
357
323
  */
358
- setup({ defaultDevice, isRingingTypeCall, }: StreamRNVideoSDKCallManagerSetupParams): void;
324
+ setup({ defaultDevice, }: {
325
+ defaultDevice: AudioSettingsRequestDefaultDeviceEnum;
326
+ }): void;
359
327
  /**
360
328
  * Starts the in call manager.
361
329
  */
362
- start({ isRingingTypeCall, }: StreamRNVideoSDKCallManagerRingingParams): void;
330
+ start(): void;
363
331
  /**
364
332
  * Stops the in call manager.
365
333
  */
366
- stop({ isRingingTypeCall }: StreamRNVideoSDKCallManagerRingingParams): void;
334
+ stop(): void;
367
335
  };
368
336
  permissions: {
369
337
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-client",
3
- "version": "1.44.6-beta.0",
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",
package/src/Call.ts CHANGED
@@ -421,7 +421,6 @@ export class Call {
421
421
  const currentUserId = this.currentUserId;
422
422
  if (currentUserId && blockedUserIds.includes(currentUserId)) {
423
423
  this.logger.info('Leaving call because of being blocked');
424
- globalThis.streamRNVideoSDK?.callingX?.endCall(this, 'restricted');
425
424
  await this.leave({ message: 'user blocked' }).catch((err) => {
426
425
  this.logger.error('Error leaving call after being blocked', err);
427
426
  });
@@ -466,10 +465,6 @@ export class Call {
466
465
  (isAcceptedElsewhere || isRejectedByMe) &&
467
466
  !hasPending(this.joinLeaveConcurrencyTag)
468
467
  ) {
469
- globalThis.streamRNVideoSDK?.callingX?.endCall(
470
- this,
471
- isAcceptedElsewhere ? 'answeredElsewhere' : 'rejected',
472
- );
473
468
  this.leave().catch(() => {
474
469
  this.logger.error(
475
470
  'Could not leave a call that was accepted or rejected elsewhere',
@@ -485,10 +480,6 @@ export class Call {
485
480
  const receiver_id = this.clientStore.connectedUser?.id;
486
481
  const ended_at = callSession?.ended_at;
487
482
  const created_by_id = this.state.createdBy?.id;
488
-
489
- if (this.currentUserId && created_by_id === this.currentUserId) {
490
- globalThis.streamRNVideoSDK?.callingX?.registerOutgoingCall(this);
491
- }
492
483
  const rejected_by = callSession?.rejected_by;
493
484
  const accepted_by = callSession?.accepted_by;
494
485
  let leaveCallIdle = false;
@@ -645,30 +636,16 @@ export class Call {
645
636
 
646
637
  if (callingState === CallingState.RINGING && reject !== false) {
647
638
  if (reject) {
648
- const reasonToEndCallReason = {
649
- timeout: 'missed',
650
- cancel: 'canceled',
651
- busy: 'busy',
652
- decline: 'rejected',
653
- } as const;
654
- const rejectReason = reason ?? 'decline';
655
- const endCallReason =
656
- reasonToEndCallReason[
657
- rejectReason as keyof typeof reasonToEndCallReason
658
- ] ?? 'rejected';
659
- globalThis.streamRNVideoSDK?.callingX?.endCall(this, endCallReason);
660
- await this.reject(rejectReason);
639
+ await this.reject(reason ?? 'decline');
661
640
  } else {
662
641
  // if reject was undefined, we still have to cancel the call automatically
663
642
  // when I am the creator and everyone else left the call
664
643
  const hasOtherParticipants = this.state.remoteParticipants.length > 0;
665
644
  if (this.isCreatedByMe && !hasOtherParticipants) {
666
- globalThis.streamRNVideoSDK?.callingX?.endCall(this, 'canceled');
667
645
  await this.reject('cancel');
668
646
  }
669
647
  }
670
648
  }
671
- globalThis.streamRNVideoSDK?.callingX?.endCall(this);
672
649
 
673
650
  this.statsReporter?.stop();
674
651
  this.statsReporter = undefined;
@@ -703,9 +680,7 @@ export class Call {
703
680
  this.cancelAutoDrop();
704
681
  this.clientStore.unregisterCall(this);
705
682
 
706
- globalThis.streamRNVideoSDK?.callManager.stop({
707
- isRingingTypeCall: this.ringing,
708
- });
683
+ globalThis.streamRNVideoSDK?.callManager.stop();
709
684
 
710
685
  this.camera.dispose();
711
686
  this.microphone.dispose();
@@ -745,9 +720,7 @@ export class Call {
745
720
  * A flag indicating whether the call was created by the current user.
746
721
  */
747
722
  get isCreatedByMe() {
748
- return (
749
- this.currentUserId && this.state.createdBy?.id === this.currentUserId
750
- );
723
+ return this.state.createdBy?.id === this.currentUserId;
751
724
  }
752
725
 
753
726
  /**
@@ -793,7 +766,6 @@ export class Call {
793
766
  video?: boolean;
794
767
  }): Promise<GetCallResponse> => {
795
768
  await this.setup();
796
-
797
769
  const response = await this.streamClient.get<GetCallResponse>(
798
770
  this.streamClientBasePath,
799
771
  params,
@@ -833,7 +805,6 @@ export class Call {
833
805
  */
834
806
  getOrCreate = async (data?: GetOrCreateCallRequest) => {
835
807
  await this.setup();
836
-
837
808
  const response = await this.streamClient.post<
838
809
  GetOrCreateCallResponse,
839
810
  GetOrCreateCallRequest
@@ -959,73 +930,60 @@ export class Call {
959
930
  joinResponseTimeout?: number;
960
931
  rpcRequestTimeout?: number;
961
932
  } = {}): Promise<void> => {
933
+ await this.setup();
962
934
  const callingState = this.state.callingState;
963
935
 
964
936
  if ([CallingState.JOINED, CallingState.JOINING].includes(callingState)) {
965
937
  throw new Error(`Illegal State: call.join() shall be called only once`);
966
938
  }
967
939
 
968
- if (data?.ring) {
969
- this.ringingSubject.next(true);
970
- }
971
- const callingX = globalThis.streamRNVideoSDK?.callingX;
972
- if (callingX) {
973
- // for Android/iOS, we need to start the call in the callingx library as soon as possible
974
- await callingX.joinCall(this, this.clientStore.calls);
975
- }
976
-
977
- await this.setup();
978
-
979
940
  this.joinResponseTimeout = joinResponseTimeout;
980
941
  this.rpcRequestTimeout = rpcRequestTimeout;
942
+
981
943
  // we will count the number of join failures per SFU.
982
944
  // once the number of failures reaches 2, we will piggyback on the `migrating_from`
983
945
  // field to force the coordinator to provide us another SFU
984
946
  const sfuJoinFailures = new Map<string, number>();
985
947
  const joinData: JoinCallData = data;
986
948
  maxJoinRetries = Math.max(maxJoinRetries, 1);
987
- try {
988
- for (let attempt = 0; attempt < maxJoinRetries; attempt++) {
989
- try {
990
- this.logger.trace(`Joining call (${attempt})`, this.cid);
991
- await this.doJoin(data);
992
- delete joinData.migrating_from;
993
- delete joinData.migrating_from_list;
994
- break;
995
- } catch (err) {
996
- this.logger.warn(`Failed to join call (${attempt})`, this.cid);
997
- if (
998
- (err instanceof ErrorFromResponse && err.unrecoverable) ||
999
- (err instanceof SfuJoinError && err.unrecoverable)
1000
- ) {
1001
- // if the error is unrecoverable, we should not retry as that signals
1002
- // that connectivity is good, but the coordinator doesn't allow the user
1003
- // to join the call due to some reason (e.g., ended call, expired token...)
1004
- throw err;
1005
- }
949
+ for (let attempt = 0; attempt < maxJoinRetries; attempt++) {
950
+ try {
951
+ this.logger.trace(`Joining call (${attempt})`, this.cid);
952
+ await this.doJoin(data);
953
+ delete joinData.migrating_from;
954
+ delete joinData.migrating_from_list;
955
+ break;
956
+ } catch (err) {
957
+ this.logger.warn(`Failed to join call (${attempt})`, this.cid);
958
+ if (
959
+ (err instanceof ErrorFromResponse && err.unrecoverable) ||
960
+ (err instanceof SfuJoinError && err.unrecoverable)
961
+ ) {
962
+ // if the error is unrecoverable, we should not retry as that signals
963
+ // that connectivity is good, but the coordinator doesn't allow the user
964
+ // to join the call due to some reason (e.g., ended call, expired token...)
965
+ throw err;
966
+ }
1006
967
 
1007
- // immediately switch to a different SFU in case of recoverable join error
1008
- const switchSfu =
1009
- err instanceof SfuJoinError &&
1010
- SfuJoinError.isJoinErrorCode(err.errorEvent);
1011
-
1012
- const sfuId = this.credentials?.server.edge_name || '';
1013
- const failures = (sfuJoinFailures.get(sfuId) || 0) + 1;
1014
- sfuJoinFailures.set(sfuId, failures);
1015
- if (switchSfu || failures >= 2) {
1016
- joinData.migrating_from = sfuId;
1017
- joinData.migrating_from_list = Array.from(sfuJoinFailures.keys());
1018
- }
968
+ // immediately switch to a different SFU in case of recoverable join error
969
+ const switchSfu =
970
+ err instanceof SfuJoinError &&
971
+ SfuJoinError.isJoinErrorCode(err.errorEvent);
972
+
973
+ const sfuId = this.credentials?.server.edge_name || '';
974
+ const failures = (sfuJoinFailures.get(sfuId) || 0) + 1;
975
+ sfuJoinFailures.set(sfuId, failures);
976
+ if (switchSfu || failures >= 2) {
977
+ joinData.migrating_from = sfuId;
978
+ joinData.migrating_from_list = Array.from(sfuJoinFailures.keys());
979
+ }
1019
980
 
1020
- if (attempt === maxJoinRetries - 1) {
1021
- throw err;
1022
- }
981
+ if (attempt === maxJoinRetries - 1) {
982
+ throw err;
1023
983
  }
1024
- await sleep(retryInterval(attempt));
1025
984
  }
1026
- } catch (error) {
1027
- callingX?.endCall(this, 'error');
1028
- throw error;
985
+
986
+ await sleep(retryInterval(attempt));
1029
987
  }
1030
988
  };
1031
989
 
@@ -1208,9 +1166,7 @@ export class Call {
1208
1166
  // re-apply them on later reconnections or server-side data fetches
1209
1167
  if (!this.deviceSettingsAppliedOnce && this.state.settings) {
1210
1168
  await this.applyDeviceConfig(this.state.settings, true, false);
1211
- globalThis.streamRNVideoSDK?.callManager.start({
1212
- isRingingTypeCall: this.ringing,
1213
- });
1169
+ globalThis.streamRNVideoSDK?.callManager.start();
1214
1170
  this.deviceSettingsAppliedOnce = true;
1215
1171
  }
1216
1172
 
@@ -1755,7 +1711,6 @@ export class Call {
1755
1711
  if (SfuJoinError.isJoinErrorCode(e)) return;
1756
1712
  if (strategy === WebsocketReconnectStrategy.UNSPECIFIED) return;
1757
1713
  if (strategy === WebsocketReconnectStrategy.DISCONNECT) {
1758
- globalThis.streamRNVideoSDK?.callingX?.endCall(this, 'error');
1759
1714
  this.leave({ message: 'SFU instructed to disconnect' }).catch((err) => {
1760
1715
  this.logger.warn(`Can't leave call after disconnect request`, err);
1761
1716
  });
@@ -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,
@@ -85,7 +85,6 @@ export class SpeakerManager {
85
85
  this.defaultDevice = defaultDevice;
86
86
  globalThis.streamRNVideoSDK?.callManager.setup({
87
87
  defaultDevice,
88
- isRingingTypeCall: this.call.ringing,
89
88
  });
90
89
  }
91
90
  }
@@ -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'),
@@ -69,7 +69,6 @@ export const watchCallRejected = (call: Call) => {
69
69
  } else {
70
70
  if (rejectedBy[eventCall.created_by.id]) {
71
71
  call.logger.info('call creator rejected, leaving call');
72
- globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
73
72
  await call.leave({ message: 'ring: creator rejected' });
74
73
  }
75
74
  }
@@ -81,7 +80,6 @@ export const watchCallRejected = (call: Call) => {
81
80
  */
82
81
  export const watchCallEnded = (call: Call) => {
83
82
  return function onCallEnded() {
84
- globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
85
83
  const { callingState } = call.state;
86
84
  if (
87
85
  callingState !== CallingState.IDLE &&
@@ -115,7 +113,6 @@ export const watchSfuCallEnded = (call: Call) => {
115
113
  // update the call state to reflect the call has ended.
116
114
  call.state.setEndedAt(new Date());
117
115
  const reason = CallEndedReason[e.reason];
118
- globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
119
116
  await call.leave({ message: `callEnded received: ${reason}` });
120
117
  } catch (err) {
121
118
  call.logger.error(
@@ -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');
@@ -42,7 +42,7 @@ export class StreamVideoWriteableStateStore {
42
42
  * The currently connected user.
43
43
  */
44
44
  get connectedUser(): OwnUserResponse | undefined {
45
- return this.connectedUserSubject.getValue();
45
+ return RxUtils.getCurrentValue(this.connectedUserSubject);
46
46
  }
47
47
 
48
48
  /**