@stream-io/video-client 1.26.0 → 1.27.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/index.browser.es.js +241 -47
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +241 -46
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.es.js +241 -47
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/src/Call.d.ts +14 -2
  9. package/dist/src/StreamSfuClient.d.ts +7 -3
  10. package/dist/src/events/internal.d.ts +7 -1
  11. package/dist/src/gen/video/sfu/event/events.d.ts +57 -1
  12. package/dist/src/gen/video/sfu/models/models.d.ts +21 -0
  13. package/dist/src/helpers/array.d.ts +7 -0
  14. package/dist/src/helpers/participantUtils.d.ts +8 -1
  15. package/dist/src/rtc/BasePeerConnection.d.ts +2 -2
  16. package/dist/src/rtc/Dispatcher.d.ts +1 -1
  17. package/dist/src/rtc/signal.d.ts +1 -1
  18. package/dist/src/store/CallState.d.ts +2 -1
  19. package/dist/src/types.d.ts +10 -1
  20. package/package.json +1 -1
  21. package/src/Call.ts +48 -5
  22. package/src/StreamSfuClient.ts +33 -14
  23. package/src/coordinator/connection/connection.ts +0 -1
  24. package/src/events/__tests__/internal.test.ts +78 -0
  25. package/src/events/__tests__/participant.test.ts +66 -0
  26. package/src/events/callEventHandlers.ts +2 -0
  27. package/src/events/internal.ts +28 -1
  28. package/src/events/participant.ts +4 -1
  29. package/src/gen/video/sfu/event/events.ts +104 -0
  30. package/src/gen/video/sfu/models/models.ts +21 -0
  31. package/src/helpers/DynascaleManager.ts +6 -0
  32. package/src/helpers/__tests__/participantUtils.test.ts +167 -0
  33. package/src/helpers/array.ts +16 -0
  34. package/src/helpers/participantUtils.ts +23 -1
  35. package/src/rtc/BasePeerConnection.ts +6 -5
  36. package/src/rtc/Dispatcher.ts +3 -2
  37. package/src/rtc/__tests__/Publisher.test.ts +3 -2
  38. package/src/rtc/__tests__/Subscriber.test.ts +3 -2
  39. package/src/rtc/signal.ts +3 -3
  40. package/src/store/CallState.ts +7 -4
  41. package/src/types.ts +11 -0
@@ -2,7 +2,7 @@ import { Publisher, Subscriber } from './rtc';
2
2
  import { CallState } from './store';
3
3
  import type { AcceptCallResponse, BlockUserResponse, CallRingEvent, CallSettingsResponse, CollectUserFeedbackRequest, CollectUserFeedbackResponse, DeleteCallRequest, DeleteCallResponse, EndCallResponse, GetCallReportResponse, GetCallResponse, GetOrCreateCallRequest, GetOrCreateCallResponse, GoLiveRequest, GoLiveResponse, JoinCallResponse, ListRecordingsResponse, ListTranscriptionsResponse, MuteUsersResponse, PinRequest, PinResponse, QueryCallMembersRequest, QueryCallMembersResponse, RejectCallResponse, RequestPermissionRequest, RequestPermissionResponse, SendCallEventResponse, SendReactionRequest, SendReactionResponse, StartClosedCaptionsRequest, StartClosedCaptionsResponse, StartFrameRecordingRequest, StartFrameRecordingResponse, StartHLSBroadcastingResponse, StartRecordingRequest, StartRecordingResponse, StartRTMPBroadcastsRequest, StartRTMPBroadcastsResponse, StartTranscriptionRequest, StartTranscriptionResponse, StopAllRTMPBroadcastsResponse, StopClosedCaptionsRequest, StopClosedCaptionsResponse, StopFrameRecordingResponse, StopHLSBroadcastingResponse, StopLiveRequest, StopLiveResponse, StopRecordingResponse, StopRTMPBroadcastsResponse, StopTranscriptionResponse, UnblockUserResponse, UnpinRequest, UnpinResponse, UpdateCallMembersRequest, UpdateCallMembersResponse, UpdateCallRequest, UpdateCallResponse, UpdateUserPermissionsRequest, UpdateUserPermissionsResponse } from './gen/coordinator';
4
4
  import { AudioTrackType, CallConstructor, CallLeaveOptions, ClientPublishOptions, ClosedCaptionsSettings, JoinCallData, TrackMuteType, VideoTrackType } from './types';
5
- import { TrackType, VideoDimension } from './gen/video/sfu/models/models';
5
+ import { ClientCapability, TrackType, VideoDimension } from './gen/video/sfu/models/models';
6
6
  import { Tracer } from './stats';
7
7
  import { DynascaleManager } from './helpers/DynascaleManager';
8
8
  import { PermissionsContext } from './permissions';
@@ -106,6 +106,10 @@ export declare class Call {
106
106
  private readonly leaveCallHooks;
107
107
  private readonly streamClientBasePath;
108
108
  private streamClientEventHandlers;
109
+ /**
110
+ * A list of capabilities that the client supports and are enabled.
111
+ */
112
+ private clientCapabilities;
109
113
  /**
110
114
  * Constructs a new `Call` instance.
111
115
  *
@@ -229,7 +233,7 @@ export declare class Call {
229
233
  *
230
234
  * @returns a promise which resolves once the call join-flow has finished.
231
235
  */
232
- doJoin: (data?: JoinCallData) => Promise<void>;
236
+ private doJoin;
233
237
  /**
234
238
  * Prepares Reconnect Details object.
235
239
  * @internal
@@ -759,4 +763,12 @@ export declare class Call {
759
763
  * @param timeoutSeconds Timeout in seconds, or 0 to keep reconnecting indefinetely
760
764
  */
761
765
  setDisconnectionTimeout: (timeoutSeconds: number) => void;
766
+ /**
767
+ * Enables the provided client capabilities.
768
+ */
769
+ enableClientCapabilities: (...capabilities: ClientCapability[]) => void;
770
+ /**
771
+ * Disables the provided client capabilities.
772
+ */
773
+ disableClientCapabilities: (...capabilities: ClientCapability[]) => void;
762
774
  }
@@ -14,6 +14,10 @@ export type StreamSfuClientConstructor = {
14
14
  * The credentials to use for the connection.
15
15
  */
16
16
  credentials: Credentials;
17
+ /**
18
+ * The `cid` (call ID) to use for the connection.
19
+ */
20
+ cid: string;
17
21
  /**
18
22
  * `sessionId` to use for the connection.
19
23
  */
@@ -21,7 +25,7 @@ export type StreamSfuClientConstructor = {
21
25
  /**
22
26
  * A log tag to use for logging. Useful for debugging multiple instances.
23
27
  */
24
- logTag: string;
28
+ tag: string;
25
29
  /**
26
30
  * The timeout in milliseconds for waiting for the `joinResponse`.
27
31
  * Defaults to 5000ms.
@@ -90,7 +94,7 @@ export declare class StreamSfuClient {
90
94
  private readonly unsubscribeNetworkChanged;
91
95
  private readonly onSignalClose;
92
96
  private readonly logger;
93
- private readonly logTag;
97
+ readonly tag: string;
94
98
  private readonly credentials;
95
99
  private readonly dispatcher;
96
100
  private readonly joinResponseTimeout;
@@ -133,7 +137,7 @@ export declare class StreamSfuClient {
133
137
  /**
134
138
  * Constructs a new SFU client.
135
139
  */
136
- constructor({ dispatcher, credentials, sessionId, logTag, joinResponseTimeout, onSignalClose, streamClient, enableTracing, }: StreamSfuClientConstructor);
140
+ constructor({ dispatcher, credentials, sessionId, cid, tag, joinResponseTimeout, onSignalClose, streamClient, enableTracing, }: StreamSfuClientConstructor);
137
141
  private createWebSocket;
138
142
  private cleanUpWebSocket;
139
143
  get isHealthy(): boolean;
@@ -1,7 +1,7 @@
1
1
  import { Dispatcher } from '../rtc';
2
2
  import { Call } from '../Call';
3
3
  import { CallState } from '../store';
4
- import type { PinsChanged } from '../gen/video/sfu/event/events';
4
+ import type { InboundStateNotification, PinsChanged } from '../gen/video/sfu/event/events';
5
5
  export declare const watchConnectionQualityChanged: (dispatcher: Dispatcher, state: CallState) => () => void;
6
6
  /**
7
7
  * Updates the approximate number of participants in the call by peeking at the
@@ -18,3 +18,9 @@ export declare const watchSfuErrorReports: (dispatcher: Dispatcher) => () => voi
18
18
  * in the call.
19
19
  */
20
20
  export declare const watchPinsUpdated: (state: CallState) => (e: PinsChanged) => void;
21
+ /**
22
+ * Watches for inbound state notifications and updates the paused tracks
23
+ *
24
+ * @param state the call state to update.
25
+ */
26
+ export declare const watchInboundStateNotification: (state: CallState) => (e: InboundStateNotification) => void;
@@ -1,5 +1,5 @@
1
1
  import { MessageType } from '@protobuf-ts/runtime';
2
- import { CallEndedReason, CallGrants, CallState, ClientDetails, Codec, ConnectionQuality, Error as Error$, GoAwayReason, ICETrickle as ICETrickle$, Participant, ParticipantCount, PeerType, Pin, PublishOption, SubscribeOption, TrackInfo, TrackType, TrackUnpublishReason, WebsocketReconnectStrategy } from '../models/models';
2
+ import { CallEndedReason, CallGrants, CallState, ClientCapability, ClientDetails, Codec, ConnectionQuality, Error as Error$, GoAwayReason, ICETrickle as ICETrickle$, Participant, ParticipantCount, PeerType, Pin, PublishOption, SubscribeOption, TrackInfo, TrackType, TrackUnpublishReason, WebsocketReconnectStrategy } from '../models/models';
3
3
  import { TrackSubscriptionDetails } from '../signal_rpc/signal';
4
4
  /**
5
5
  * SFUEvent is a message that is sent from the SFU to the client.
@@ -208,6 +208,14 @@ export interface SfuEvent {
208
208
  * @generated from protobuf field: stream.video.sfu.event.ChangePublishOptions change_publish_options = 27;
209
209
  */
210
210
  changePublishOptions: ChangePublishOptions;
211
+ } | {
212
+ oneofKind: 'inboundStateNotification';
213
+ /**
214
+ * InboundStateNotification
215
+ *
216
+ * @generated from protobuf field: stream.video.sfu.event.InboundStateNotification inbound_state_notification = 28;
217
+ */
218
+ inboundStateNotification: InboundStateNotification;
211
219
  } | {
212
220
  oneofKind: undefined;
213
221
  };
@@ -460,6 +468,10 @@ export interface JoinRequest {
460
468
  * @generated from protobuf field: repeated stream.video.sfu.models.SubscribeOption preferred_subscribe_options = 10;
461
469
  */
462
470
  preferredSubscribeOptions: SubscribeOption[];
471
+ /**
472
+ * @generated from protobuf field: repeated stream.video.sfu.models.ClientCapability capabilities = 11;
473
+ */
474
+ capabilities: ClientCapability[];
463
475
  }
464
476
  /**
465
477
  * @generated from protobuf message stream.video.sfu.event.ReconnectDetails
@@ -827,6 +839,36 @@ export interface CallEnded {
827
839
  */
828
840
  reason: CallEndedReason;
829
841
  }
842
+ /**
843
+ * @generated from protobuf message stream.video.sfu.event.InboundStateNotification
844
+ */
845
+ export interface InboundStateNotification {
846
+ /**
847
+ * @generated from protobuf field: repeated stream.video.sfu.event.InboundVideoState inbound_video_states = 1;
848
+ */
849
+ inboundVideoStates: InboundVideoState[];
850
+ }
851
+ /**
852
+ * @generated from protobuf message stream.video.sfu.event.InboundVideoState
853
+ */
854
+ export interface InboundVideoState {
855
+ /**
856
+ * @generated from protobuf field: string user_id = 1;
857
+ */
858
+ userId: string;
859
+ /**
860
+ * @generated from protobuf field: string session_id = 2;
861
+ */
862
+ sessionId: string;
863
+ /**
864
+ * @generated from protobuf field: stream.video.sfu.models.TrackType track_type = 3;
865
+ */
866
+ trackType: TrackType;
867
+ /**
868
+ * @generated from protobuf field: bool paused = 4;
869
+ */
870
+ paused: boolean;
871
+ }
830
872
  declare class SfuEvent$Type extends MessageType<SfuEvent> {
831
873
  constructor();
832
874
  }
@@ -1072,4 +1114,18 @@ declare class CallEnded$Type extends MessageType<CallEnded> {
1072
1114
  * @generated MessageType for protobuf message stream.video.sfu.event.CallEnded
1073
1115
  */
1074
1116
  export declare const CallEnded: CallEnded$Type;
1117
+ declare class InboundStateNotification$Type extends MessageType<InboundStateNotification> {
1118
+ constructor();
1119
+ }
1120
+ /**
1121
+ * @generated MessageType for protobuf message stream.video.sfu.event.InboundStateNotification
1122
+ */
1123
+ export declare const InboundStateNotification: InboundStateNotification$Type;
1124
+ declare class InboundVideoState$Type extends MessageType<InboundVideoState> {
1125
+ constructor();
1126
+ }
1127
+ /**
1128
+ * @generated MessageType for protobuf message stream.video.sfu.event.InboundVideoState
1129
+ */
1130
+ export declare const InboundVideoState: InboundVideoState$Type;
1075
1131
  export {};
@@ -815,6 +815,10 @@ export declare enum ErrorCode {
815
815
  * @generated from protobuf enum value: ERROR_CODE_CALL_NOT_FOUND = 300;
816
816
  */
817
817
  CALL_NOT_FOUND = 300,
818
+ /**
819
+ * @generated from protobuf enum value: ERROR_CODE_CALL_PARTICIPANT_LIMIT_REACHED = 301;
820
+ */
821
+ CALL_PARTICIPANT_LIMIT_REACHED = 301,
818
822
  /**
819
823
  * @generated from protobuf enum value: ERROR_CODE_REQUEST_VALIDATION_FAILED = 400;
820
824
  */
@@ -1076,6 +1080,23 @@ export declare enum AppleThermalState {
1076
1080
  */
1077
1081
  CRITICAL = 4
1078
1082
  }
1083
+ /**
1084
+ * ClientCapability defines a feature that client supports
1085
+ *
1086
+ * @generated from protobuf enum stream.video.sfu.models.ClientCapability
1087
+ */
1088
+ export declare enum ClientCapability {
1089
+ /**
1090
+ * @generated from protobuf enum value: CLIENT_CAPABILITY_UNSPECIFIED = 0;
1091
+ */
1092
+ UNSPECIFIED = 0,
1093
+ /**
1094
+ * Enables SFU pausing inbound video
1095
+ *
1096
+ * @generated from protobuf enum value: CLIENT_CAPABILITY_SUBSCRIBER_VIDEO_PAUSE = 1;
1097
+ */
1098
+ SUBSCRIBER_VIDEO_PAUSE = 1
1099
+ }
1079
1100
  declare class CallState$Type extends MessageType<CallState> {
1080
1101
  constructor();
1081
1102
  }
@@ -5,3 +5,10 @@
5
5
  * @param values the values to add.
6
6
  */
7
7
  export declare const pushToIfMissing: <T>(arr: T[], ...values: T[]) => T[];
8
+ /**
9
+ * Removes values from an array if they are present.
10
+ *
11
+ * @param arr the array to remove from.
12
+ * @param values the values to remove.
13
+ */
14
+ export declare const removeFromIfPresent: <T>(arr: T[], ...values: T[]) => T[];
@@ -1,4 +1,4 @@
1
- import { StreamVideoParticipant } from '../types';
1
+ import { StreamVideoParticipant, VideoTrackType } from '../types';
2
2
  /**
3
3
  * Check if a participant has a video.
4
4
  *
@@ -29,3 +29,10 @@ export declare const hasScreenShareAudio: (p: StreamVideoParticipant) => boolean
29
29
  * @param p the participant.
30
30
  */
31
31
  export declare const isPinned: (p: StreamVideoParticipant) => boolean;
32
+ /**
33
+ * Check if a participant has a paused track of the specified type.
34
+ *
35
+ * @param p the participant to check.
36
+ * @param videoTrackType the type of video track to check for ('videoTrack' or 'screenShareTrack').
37
+ */
38
+ export declare const hasPausedTrack: (p: StreamVideoParticipant, videoTrackType: VideoTrackType) => boolean;
@@ -11,7 +11,7 @@ export type BasePeerConnectionOpts = {
11
11
  connectionConfig?: RTCConfiguration;
12
12
  dispatcher: Dispatcher;
13
13
  onReconnectionNeeded?: OnReconnectionNeeded;
14
- logTag: string;
14
+ tag: string;
15
15
  enableTracing: boolean;
16
16
  iceRestartDelay?: number;
17
17
  };
@@ -40,7 +40,7 @@ export declare abstract class BasePeerConnection {
40
40
  /**
41
41
  * Constructs a new `BasePeerConnection` instance.
42
42
  */
43
- protected constructor(peerType: PeerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, logTag, enableTracing, iceRestartDelay, }: BasePeerConnectionOpts);
43
+ protected constructor(peerType: PeerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, tag, enableTracing, iceRestartDelay, }: BasePeerConnectionOpts);
44
44
  private createPeerConnection;
45
45
  /**
46
46
  * Disposes the `RTCPeerConnection` instance.
@@ -19,7 +19,7 @@ export declare const isSfuEvent: (eventName: SfuEventKinds | EventTypes) => even
19
19
  export declare class Dispatcher {
20
20
  private readonly logger;
21
21
  private subscribers;
22
- dispatch: <K extends SfuEventKinds>(message: DispatchableMessage<K>, logTag?: string) => void;
22
+ dispatch: <K extends SfuEventKinds>(message: DispatchableMessage<K>, tag?: string) => void;
23
23
  on: <E extends keyof AllSfuEvents>(eventName: E, fn: CallEventListener<E>) => () => void;
24
24
  off: <E extends keyof AllSfuEvents>(eventName: E, fn: CallEventListener<E>) => void;
25
25
  }
@@ -2,5 +2,5 @@ import { DispatchableMessage, SfuEventKinds } from './Dispatcher';
2
2
  export declare const createWebSocketSignalChannel: (opts: {
3
3
  endpoint: string;
4
4
  onMessage: <K extends SfuEventKinds>(message: DispatchableMessage<K>) => void;
5
- logTag: string;
5
+ tag: string;
6
6
  }) => WebSocket;
@@ -480,8 +480,9 @@ export declare class CallState {
480
480
  *
481
481
  * @param sessionId the session ID of the participant to update.
482
482
  * @param participant the participant to update or add.
483
+ * @param patch an optional patch to apply to the participant.
483
484
  */
484
- updateOrAddParticipant: (sessionId: string, participant: StreamVideoParticipant) => StreamVideoParticipant[];
485
+ updateOrAddParticipant: (sessionId: string, participant: StreamVideoParticipant, patch?: StreamVideoParticipantPatch | ((p: StreamVideoParticipant) => StreamVideoParticipantPatch)) => StreamVideoParticipant[];
485
486
  /**
486
487
  * Updates all participants in the current call whose session ID is in the given `sessionIds`.
487
488
  * If no patches are provided, this operation is no-op.
@@ -1,4 +1,4 @@
1
- import type { Participant, VideoDimension } from './gen/video/sfu/models/models';
1
+ import type { Participant, TrackType, VideoDimension } from './gen/video/sfu/models/models';
2
2
  import type { JoinCallRequest, MemberResponse, OwnCapability, ReactionResponse } from './gen/coordinator';
3
3
  import type { StreamClient } from './coordinator/connection/client';
4
4
  import type { Comparator } from './sorting';
@@ -48,6 +48,15 @@ export interface StreamVideoParticipant extends Participant {
48
48
  * Set it to `undefined` to unsubscribe from this participant's screen share.
49
49
  */
50
50
  screenShareDimension?: VideoDimension;
51
+ /**
52
+ * A list of tracks that are currently paused by our servers.
53
+ * Typically, a server-side pause happens when the local participant doesn't
54
+ * have enough bandwidth to receive all tracks. In this case, the server
55
+ * will pause some tracks to optimize the bandwidth usage.
56
+ * Once the bandwidth is restored, the server will resume the paused tracks.
57
+ * This is useful to avoid any unwanted video and audio artifacts.
58
+ */
59
+ pausedTracks?: TrackType[];
51
60
  /**
52
61
  * True if the participant is the local participant.
53
62
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-client",
3
- "version": "1.26.0",
3
+ "version": "1.27.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
@@ -107,6 +107,7 @@ import {
107
107
  import { BehaviorSubject, Subject, takeWhile } from 'rxjs';
108
108
  import { ReconnectDetails } from './gen/video/sfu/event/events';
109
109
  import {
110
+ ClientCapability,
110
111
  ClientDetails,
111
112
  Codec,
112
113
  PublishOption,
@@ -271,6 +272,13 @@ export class Call {
271
272
  private readonly streamClientBasePath: string;
272
273
  private streamClientEventHandlers = new Map<Function, () => void>();
273
274
 
275
+ /**
276
+ * A list of capabilities that the client supports and are enabled.
277
+ */
278
+ private clientCapabilities = new Set<ClientCapability>([
279
+ ClientCapability.SUBSCRIBER_VIDEO_PAUSE,
280
+ ]);
281
+
274
282
  /**
275
283
  * Constructs a new `Call` instance.
276
284
  *
@@ -854,13 +862,28 @@ export class Call {
854
862
 
855
863
  this.state.setCallingState(CallingState.JOINING);
856
864
 
865
+ // we will count the number of join failures per SFU.
866
+ // once the number of failures reaches 2, we will piggyback on the `migrating_from`
867
+ // field to force the coordinator to provide us another SFU
868
+ const sfuJoinFailures = new Map<string, number>();
869
+ const joinData: JoinCallData = data;
857
870
  maxJoinRetries = Math.max(maxJoinRetries, 1);
858
871
  for (let attempt = 0; attempt < maxJoinRetries; attempt++) {
859
872
  try {
860
873
  this.logger('trace', `Joining call (${attempt})`, this.cid);
861
- return await this.doJoin(data);
874
+ await this.doJoin(data);
875
+ delete joinData.migrating_from;
876
+ break;
862
877
  } catch (err) {
863
878
  this.logger('warn', `Failed to join call (${attempt})`, this.cid);
879
+
880
+ const sfuId = this.credentials?.server.edge_name || '';
881
+ const failures = (sfuJoinFailures.get(sfuId) || 0) + 1;
882
+ sfuJoinFailures.set(sfuId, failures);
883
+ if (failures >= 2) {
884
+ joinData.migrating_from = sfuId;
885
+ }
886
+
864
887
  if (attempt === maxJoinRetries - 1) {
865
888
  // restore the previous call state if the join-flow fails
866
889
  this.state.setCallingState(callingState);
@@ -878,7 +901,7 @@ export class Call {
878
901
  *
879
902
  * @returns a promise which resolves once the call join-flow has finished.
880
903
  */
881
- doJoin = async (data?: JoinCallData): Promise<void> => {
904
+ private doJoin = async (data?: JoinCallData): Promise<void> => {
882
905
  const connectStartTime = Date.now();
883
906
  const callingState = this.state.callingState;
884
907
 
@@ -924,7 +947,8 @@ export class Call {
924
947
  const sfuClient =
925
948
  performingRejoin || performingMigration || !isWsHealthy
926
949
  ? new StreamSfuClient({
927
- logTag: String(++this.sfuClientTag),
950
+ tag: String(this.sfuClientTag++),
951
+ cid: this.cid,
928
952
  dispatcher: this.dispatcher,
929
953
  credentials: this.credentials,
930
954
  streamClient: this.streamClient,
@@ -971,6 +995,7 @@ export class Call {
971
995
  reconnectDetails,
972
996
  preferredPublishOptions,
973
997
  preferredSubscribeOptions,
998
+ capabilities: Array.from(this.clientCapabilities),
974
999
  });
975
1000
 
976
1001
  this.currentPublishOptions = publishOptions;
@@ -1194,7 +1219,7 @@ export class Call {
1194
1219
  dispatcher: this.dispatcher,
1195
1220
  state: this.state,
1196
1221
  connectionConfig,
1197
- logTag: String(this.sfuClientTag),
1222
+ tag: sfuClient.tag,
1198
1223
  enableTracing,
1199
1224
  onReconnectionNeeded: (kind, reason) => {
1200
1225
  this.reconnect(kind, reason).catch((err) => {
@@ -1217,7 +1242,7 @@ export class Call {
1217
1242
  state: this.state,
1218
1243
  connectionConfig,
1219
1244
  publishOptions,
1220
- logTag: String(this.sfuClientTag),
1245
+ tag: sfuClient.tag,
1221
1246
  enableTracing,
1222
1247
  onReconnectionNeeded: (kind, reason) => {
1223
1248
  this.reconnect(kind, reason).catch((err) => {
@@ -2730,4 +2755,22 @@ export class Call {
2730
2755
  setDisconnectionTimeout = (timeoutSeconds: number) => {
2731
2756
  this.disconnectionTimeoutSeconds = timeoutSeconds;
2732
2757
  };
2758
+
2759
+ /**
2760
+ * Enables the provided client capabilities.
2761
+ */
2762
+ enableClientCapabilities = (...capabilities: ClientCapability[]) => {
2763
+ for (const capability of capabilities) {
2764
+ this.clientCapabilities.add(capability);
2765
+ }
2766
+ };
2767
+
2768
+ /**
2769
+ * Disables the provided client capabilities.
2770
+ */
2771
+ disableClientCapabilities = (...capabilities: ClientCapability[]) => {
2772
+ for (const capability of capabilities) {
2773
+ this.clientCapabilities.delete(capability);
2774
+ }
2775
+ };
2733
2776
  }
@@ -51,6 +51,11 @@ export type StreamSfuClientConstructor = {
51
51
  */
52
52
  credentials: Credentials;
53
53
 
54
+ /**
55
+ * The `cid` (call ID) to use for the connection.
56
+ */
57
+ cid: string;
58
+
54
59
  /**
55
60
  * `sessionId` to use for the connection.
56
61
  */
@@ -59,7 +64,7 @@ export type StreamSfuClientConstructor = {
59
64
  /**
60
65
  * A log tag to use for logging. Useful for debugging multiple instances.
61
66
  */
62
- logTag: string;
67
+ tag: string;
63
68
 
64
69
  /**
65
70
  * The timeout in milliseconds for waiting for the `joinResponse`.
@@ -83,6 +88,14 @@ export type StreamSfuClientConstructor = {
83
88
  enableTracing: boolean;
84
89
  };
85
90
 
91
+ type SfuWebSocketParams = {
92
+ attempt: string; // the reconnect attempt, start with 0
93
+ user_id: string;
94
+ api_key: string;
95
+ user_session_id: string;
96
+ cid: string;
97
+ };
98
+
86
99
  /**
87
100
  * The client used for exchanging information with the SFU.
88
101
  */
@@ -140,7 +153,7 @@ export class StreamSfuClient {
140
153
  private readonly unsubscribeNetworkChanged: () => void;
141
154
  private readonly onSignalClose: ((reason: string) => void) | undefined;
142
155
  private readonly logger: Logger;
143
- private readonly logTag: string;
156
+ readonly tag: string;
144
157
  private readonly credentials: Credentials;
145
158
  private readonly dispatcher: Dispatcher;
146
159
  private readonly joinResponseTimeout: number;
@@ -191,7 +204,8 @@ export class StreamSfuClient {
191
204
  dispatcher,
192
205
  credentials,
193
206
  sessionId,
194
- logTag,
207
+ cid,
208
+ tag,
195
209
  joinResponseTimeout = 5000,
196
210
  onSignalClose,
197
211
  streamClient,
@@ -204,10 +218,10 @@ export class StreamSfuClient {
204
218
  const { server, token } = credentials;
205
219
  this.edgeName = server.edge_name;
206
220
  this.joinResponseTimeout = joinResponseTimeout;
207
- this.logTag = logTag;
208
- this.logger = getLogger(['SfuClient', logTag]);
221
+ this.tag = tag;
222
+ this.logger = getLogger(['SfuClient', tag]);
209
223
  this.tracer = enableTracing
210
- ? new Tracer(`${logTag}-${this.edgeName}`)
224
+ ? new Tracer(`${tag}-${this.edgeName}`)
211
225
  : undefined;
212
226
  this.rpc = createSignalClient({
213
227
  baseUrl: server.url,
@@ -238,10 +252,16 @@ export class StreamSfuClient {
238
252
  }
239
253
  });
240
254
 
241
- this.createWebSocket();
255
+ this.createWebSocket({
256
+ attempt: tag,
257
+ user_id: streamClient.user?.id || '',
258
+ api_key: streamClient.key,
259
+ user_session_id: this.sessionId,
260
+ cid,
261
+ });
242
262
  }
243
263
 
244
- private createWebSocket = () => {
264
+ private createWebSocket = (params: SfuWebSocketParams) => {
245
265
  const eventsToTrace: Partial<Record<SfuEventKinds, boolean>> = {
246
266
  callEnded: true,
247
267
  changePublishQuality: true,
@@ -249,10 +269,11 @@ export class StreamSfuClient {
249
269
  connectionQualityChanged: true,
250
270
  error: true,
251
271
  goAway: true,
272
+ inboundStateNotification: true,
252
273
  };
253
274
  this.signalWs = createWebSocketSignalChannel({
254
- logTag: this.logTag,
255
- endpoint: `${this.credentials.server.ws_endpoint}?tag=${this.logTag}`,
275
+ tag: this.tag,
276
+ endpoint: `${this.credentials.server.ws_endpoint}?${new URLSearchParams(params).toString()}`,
256
277
  onMessage: (message) => {
257
278
  this.lastMessageTimestamp = new Date();
258
279
  this.scheduleConnectionCheck();
@@ -260,7 +281,7 @@ export class StreamSfuClient {
260
281
  if (eventsToTrace[eventKind]) {
261
282
  this.tracer?.trace(eventKind, message);
262
283
  }
263
- this.dispatcher.dispatch(message, this.logTag);
284
+ this.dispatcher.dispatch(message, this.tag);
264
285
  },
265
286
  });
266
287
 
@@ -443,9 +464,7 @@ export class StreamSfuClient {
443
464
  this.migrateAwayTimeout = setTimeout(() => {
444
465
  unsubscribe();
445
466
  task.reject(
446
- new Error(
447
- `Migration (${this.logTag}) failed to complete in ${timeout}ms`,
448
- ),
467
+ new Error(`Migration (${this.tag}) failed to complete in ${timeout}ms`),
449
468
  );
450
469
  }, timeout);
451
470
 
@@ -471,7 +471,6 @@ export class StableWSConnection {
471
471
  onmessage = (wsID: number, event: MessageEvent) => {
472
472
  if (this.wsID !== wsID) return;
473
473
 
474
- this._log('onmessage() - onmessage callback', { event, wsID });
475
474
  const data =
476
475
  typeof event.data === 'string'
477
476
  ? (JSON.parse(event.data) as StreamVideoEvent)
@@ -2,8 +2,10 @@ import { describe, expect, it, vi } from 'vitest';
2
2
  import { Call } from '../../Call';
3
3
  import { Dispatcher } from '../../rtc';
4
4
  import { CallState } from '../../store';
5
+ import { noopComparator } from '../../sorting';
5
6
  import {
6
7
  watchConnectionQualityChanged,
8
+ watchInboundStateNotification,
7
9
  watchLiveEnded,
8
10
  watchParticipantCountChanged,
9
11
  watchPinsUpdated,
@@ -11,6 +13,7 @@ import {
11
13
  import {
12
14
  ConnectionQuality,
13
15
  ErrorCode,
16
+ TrackType,
14
17
  } from '../../gen/video/sfu/models/models';
15
18
 
16
19
  describe('internal events', () => {
@@ -131,4 +134,79 @@ describe('internal events', () => {
131
134
  { userId: 'u2', sessionId: 'session-2', pin: undefined },
132
135
  ]);
133
136
  });
137
+
138
+ it('handles InboundStateNotification', () => {
139
+ const state = new CallState();
140
+ state.setSortParticipantsBy(noopComparator());
141
+ state.setParticipants([
142
+ // @ts-expect-error incomplete data
143
+ { sessionId: 'session-1' },
144
+ // @ts-expect-error incomplete data
145
+ { sessionId: 'session-2' },
146
+ ]);
147
+
148
+ const update = watchInboundStateNotification(state);
149
+ update({
150
+ inboundVideoStates: [
151
+ {
152
+ userId: '1',
153
+ sessionId: 'session-1',
154
+ trackType: TrackType.VIDEO,
155
+ paused: true,
156
+ },
157
+ {
158
+ userId: '2',
159
+ sessionId: 'session-2',
160
+ trackType: TrackType.VIDEO,
161
+ paused: false,
162
+ },
163
+ ],
164
+ });
165
+ expect(
166
+ state.findParticipantBySessionId('session-1')?.pausedTracks,
167
+ ).toContain(TrackType.VIDEO);
168
+ expect(
169
+ state.findParticipantBySessionId('session-2')?.pausedTracks,
170
+ ).not.toContain(TrackType.VIDEO);
171
+
172
+ update({
173
+ inboundVideoStates: [
174
+ {
175
+ userId: '2',
176
+ sessionId: 'session-2',
177
+ trackType: TrackType.VIDEO,
178
+ paused: true,
179
+ },
180
+ ],
181
+ });
182
+ expect(
183
+ state.findParticipantBySessionId('session-1')?.pausedTracks,
184
+ ).toContain(TrackType.VIDEO);
185
+ expect(
186
+ state.findParticipantBySessionId('session-2')?.pausedTracks,
187
+ ).toContain(TrackType.VIDEO);
188
+
189
+ update({
190
+ inboundVideoStates: [
191
+ {
192
+ userId: '1',
193
+ sessionId: 'session-1',
194
+ trackType: TrackType.VIDEO,
195
+ paused: false,
196
+ },
197
+ {
198
+ userId: '2',
199
+ sessionId: 'session-2',
200
+ trackType: TrackType.VIDEO,
201
+ paused: false,
202
+ },
203
+ ],
204
+ });
205
+ expect(
206
+ state.findParticipantBySessionId('session-1')?.pausedTracks,
207
+ ).not.toContain(TrackType.VIDEO);
208
+ expect(
209
+ state.findParticipantBySessionId('session-2')?.pausedTracks,
210
+ ).not.toContain(TrackType.VIDEO);
211
+ });
134
212
  });