@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.
- package/CHANGELOG.md +16 -0
- package/dist/index.browser.es.js +241 -47
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +241 -46
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +241 -47
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +14 -2
- package/dist/src/StreamSfuClient.d.ts +7 -3
- package/dist/src/events/internal.d.ts +7 -1
- package/dist/src/gen/video/sfu/event/events.d.ts +57 -1
- package/dist/src/gen/video/sfu/models/models.d.ts +21 -0
- package/dist/src/helpers/array.d.ts +7 -0
- package/dist/src/helpers/participantUtils.d.ts +8 -1
- package/dist/src/rtc/BasePeerConnection.d.ts +2 -2
- package/dist/src/rtc/Dispatcher.d.ts +1 -1
- package/dist/src/rtc/signal.d.ts +1 -1
- package/dist/src/store/CallState.d.ts +2 -1
- package/dist/src/types.d.ts +10 -1
- package/package.json +1 -1
- package/src/Call.ts +48 -5
- package/src/StreamSfuClient.ts +33 -14
- package/src/coordinator/connection/connection.ts +0 -1
- package/src/events/__tests__/internal.test.ts +78 -0
- package/src/events/__tests__/participant.test.ts +66 -0
- package/src/events/callEventHandlers.ts +2 -0
- package/src/events/internal.ts +28 -1
- package/src/events/participant.ts +4 -1
- package/src/gen/video/sfu/event/events.ts +104 -0
- package/src/gen/video/sfu/models/models.ts +21 -0
- package/src/helpers/DynascaleManager.ts +6 -0
- package/src/helpers/__tests__/participantUtils.test.ts +167 -0
- package/src/helpers/array.ts +16 -0
- package/src/helpers/participantUtils.ts +23 -1
- package/src/rtc/BasePeerConnection.ts +6 -5
- package/src/rtc/Dispatcher.ts +3 -2
- package/src/rtc/__tests__/Publisher.test.ts +3 -2
- package/src/rtc/__tests__/Subscriber.test.ts +3 -2
- package/src/rtc/signal.ts +3 -3
- package/src/store/CallState.ts +7 -4
- package/src/types.ts +11 -0
package/dist/src/Call.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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>,
|
|
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
|
}
|
package/dist/src/rtc/signal.d.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
package/dist/src/types.d.ts
CHANGED
|
@@ -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
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/StreamSfuClient.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
208
|
-
this.logger = getLogger(['SfuClient',
|
|
221
|
+
this.tag = tag;
|
|
222
|
+
this.logger = getLogger(['SfuClient', tag]);
|
|
209
223
|
this.tracer = enableTracing
|
|
210
|
-
? new Tracer(`${
|
|
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
|
-
|
|
255
|
-
endpoint: `${this.credentials.server.ws_endpoint}
|
|
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.
|
|
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
|
});
|