@stream-io/video-client 1.52.0 → 1.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/dist/index.browser.es.js +796 -51
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +796 -50
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +796 -51
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +5 -1
- package/dist/src/StreamVideoClient.d.ts +2 -0
- package/dist/src/coordinator/connection/client.d.ts +1 -0
- package/dist/src/errors/SfuTimeoutError.d.ts +8 -0
- package/dist/src/errors/index.d.ts +1 -0
- package/dist/src/helpers/firstVideoFrame.d.ts +7 -0
- package/dist/src/reporting/ClientEventReporter.d.ts +84 -0
- package/dist/src/reporting/index.d.ts +1 -0
- package/dist/src/rtc/BasePeerConnection.d.ts +5 -2
- package/dist/src/rtc/types.d.ts +24 -1
- package/dist/src/types.d.ts +5 -0
- package/package.json +1 -1
- package/src/Call.ts +184 -60
- package/src/StreamSfuClient.ts +3 -3
- package/src/StreamVideoClient.ts +18 -3
- package/src/__tests__/Call.autodrop.test.ts +4 -1
- package/src/__tests__/Call.lifecycle.test.ts +4 -1
- package/src/__tests__/Call.publishing.test.ts +4 -1
- package/src/__tests__/Call.test.ts +23 -0
- package/src/coordinator/connection/client.ts +5 -0
- package/src/devices/__tests__/CameraManager.test.ts +10 -1
- package/src/devices/__tests__/DeviceManager.test.ts +10 -1
- package/src/devices/__tests__/DeviceManagerFilters.test.ts +4 -1
- package/src/devices/__tests__/MicrophoneManager.test.ts +10 -1
- package/src/devices/__tests__/MicrophoneManagerRN.test.ts +4 -1
- package/src/devices/__tests__/ScreenShareManager.test.ts +4 -1
- package/src/devices/__tests__/SpeakerManager.test.ts +13 -4
- package/src/errors/SfuTimeoutError.ts +7 -0
- package/src/errors/index.ts +1 -0
- package/src/events/__tests__/call.test.ts +2 -0
- package/src/events/__tests__/mutes.test.ts +4 -1
- package/src/events/call.ts +8 -0
- package/src/helpers/__tests__/AudioBindingsWatchdog.test.ts +6 -3
- package/src/helpers/__tests__/DynascaleManager.test.ts +6 -3
- package/src/helpers/__tests__/ViewportTracker.test.ts +6 -3
- package/src/helpers/__tests__/firstVideoFrame.test.ts +91 -0
- package/src/helpers/firstVideoFrame.ts +38 -0
- package/src/reporting/ClientEventReporter.ts +859 -0
- package/src/reporting/__tests__/ClientEventReporter.test.ts +620 -0
- package/src/reporting/index.ts +1 -0
- package/src/rtc/BasePeerConnection.ts +30 -0
- package/src/rtc/Subscriber.ts +1 -0
- package/src/rtc/__tests__/Call.reconnect.test.ts +3 -0
- package/src/rtc/types.ts +34 -0
- package/src/types.ts +6 -0
package/dist/index.cjs.js
CHANGED
|
@@ -6660,7 +6660,7 @@ const getSdkVersion = (sdk) => {
|
|
|
6660
6660
|
return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
|
|
6661
6661
|
};
|
|
6662
6662
|
|
|
6663
|
-
const version = "1.
|
|
6663
|
+
const version = "1.53.0";
|
|
6664
6664
|
const [major, minor, patch] = version.split('.');
|
|
6665
6665
|
let sdkInfo = {
|
|
6666
6666
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -7754,7 +7754,7 @@ class BasePeerConnection {
|
|
|
7754
7754
|
/**
|
|
7755
7755
|
* Constructs a new `BasePeerConnection` instance.
|
|
7756
7756
|
*/
|
|
7757
|
-
constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, onIceConnected, tag, enableTracing, clientPublishOptions, iceRestartDelay = 2500, statsTimestampDriftThresholdMs = 0, }) {
|
|
7757
|
+
constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, onIceConnected, onPeerConnectionStateChange, onRemoteTrackUnmute, tag, enableTracing, clientPublishOptions, iceRestartDelay = 2500, statsTimestampDriftThresholdMs = 0, }) {
|
|
7758
7758
|
this.iceHasEverConnected = false;
|
|
7759
7759
|
this.isIceRestarting = false;
|
|
7760
7760
|
this.isDisposed = false;
|
|
@@ -7908,6 +7908,10 @@ class BasePeerConnection {
|
|
|
7908
7908
|
this.onConnectionStateChange = async () => {
|
|
7909
7909
|
const state = this.pc.connectionState;
|
|
7910
7910
|
this.logger.debug(`Connection state changed`, state);
|
|
7911
|
+
this.fireOnPeerConnectionStateChange({
|
|
7912
|
+
stateType: 'peerConnection',
|
|
7913
|
+
state,
|
|
7914
|
+
});
|
|
7911
7915
|
if (this.tracer && (state === 'connected' || state === 'failed')) {
|
|
7912
7916
|
try {
|
|
7913
7917
|
const stats = await this.stats.get();
|
|
@@ -7930,8 +7934,20 @@ class BasePeerConnection {
|
|
|
7930
7934
|
this.onIceConnectionStateChange = () => {
|
|
7931
7935
|
const state = this.pc.iceConnectionState;
|
|
7932
7936
|
this.logger.debug(`ICE connection state changed`, state);
|
|
7937
|
+
this.fireOnPeerConnectionStateChange({ stateType: 'ice', state });
|
|
7933
7938
|
this.handleConnectionStateUpdate(state);
|
|
7934
7939
|
};
|
|
7940
|
+
this.fireOnPeerConnectionStateChange = (event) => {
|
|
7941
|
+
try {
|
|
7942
|
+
this.onPeerConnectionStateChange?.({
|
|
7943
|
+
peerType: this.peerType,
|
|
7944
|
+
...event,
|
|
7945
|
+
});
|
|
7946
|
+
}
|
|
7947
|
+
catch (err) {
|
|
7948
|
+
this.logger.warn('onPeerConnectionStateChange listener threw', err);
|
|
7949
|
+
}
|
|
7950
|
+
};
|
|
7935
7951
|
this.handleConnectionStateUpdate = (state) => {
|
|
7936
7952
|
const { callingState } = this.state;
|
|
7937
7953
|
if (callingState === exports.CallingState.OFFLINE)
|
|
@@ -8046,6 +8062,8 @@ class BasePeerConnection {
|
|
|
8046
8062
|
this.tag = tag;
|
|
8047
8063
|
this.onReconnectionNeeded = onReconnectionNeeded;
|
|
8048
8064
|
this.onIceConnected = onIceConnected;
|
|
8065
|
+
this.onPeerConnectionStateChange = onPeerConnectionStateChange;
|
|
8066
|
+
this.onRemoteTrackUnmute = onRemoteTrackUnmute;
|
|
8049
8067
|
this.logger = videoLoggerSystem.getLogger(peerType === PeerType.SUBSCRIBER ? 'Subscriber' : 'Publisher', { tags: [tag] });
|
|
8050
8068
|
this.pc = this.createPeerConnection(connectionConfig);
|
|
8051
8069
|
this.stats = new StatsTracer(this.pc, peerType, this.trackIdToTrackType, statsTimestampDriftThresholdMs);
|
|
@@ -8068,6 +8086,8 @@ class BasePeerConnection {
|
|
|
8068
8086
|
this.preConnectStuckTimeout = undefined;
|
|
8069
8087
|
this.onReconnectionNeeded = undefined;
|
|
8070
8088
|
this.onIceConnected = undefined;
|
|
8089
|
+
this.onPeerConnectionStateChange = undefined;
|
|
8090
|
+
this.onRemoteTrackUnmute = undefined;
|
|
8071
8091
|
this.isDisposed = true;
|
|
8072
8092
|
this.detachEventHandlers();
|
|
8073
8093
|
this.pc.close();
|
|
@@ -9057,6 +9077,7 @@ class Subscriber extends BasePeerConnection {
|
|
|
9057
9077
|
track.addEventListener('unmute', () => {
|
|
9058
9078
|
this.logger.info(`[onTrack]: Track unmuted: ${trackDebugInfo}`);
|
|
9059
9079
|
this.setRemoteTrackInterrupted(trackId, trackType, false);
|
|
9080
|
+
this.onRemoteTrackUnmute?.(trackType, track.id);
|
|
9060
9081
|
});
|
|
9061
9082
|
track.addEventListener('ended', () => {
|
|
9062
9083
|
this.logger.info(`[onTrack]: Track ended: ${trackDebugInfo}`);
|
|
@@ -9296,6 +9317,15 @@ class SfuJoinError extends Error {
|
|
|
9296
9317
|
}
|
|
9297
9318
|
}
|
|
9298
9319
|
|
|
9320
|
+
/**
|
|
9321
|
+
* An error thrown when a client-side SFU deadline (e.g., waiting for the
|
|
9322
|
+
* signaling WS to open or for the `joinResponse` to arrive) fires before
|
|
9323
|
+
* the awaited operation resolves. Allows consumers (e.g., the client event
|
|
9324
|
+
* reporter) to classify timeouts without relying on message wording.
|
|
9325
|
+
*/
|
|
9326
|
+
class SfuTimeoutError extends Error {
|
|
9327
|
+
}
|
|
9328
|
+
|
|
9299
9329
|
/**
|
|
9300
9330
|
* Creates a fresh `joinResponseTask` with a no-op rejection handler attached
|
|
9301
9331
|
* to the underlying promise. The handler marks the rejection path as handled
|
|
@@ -9401,7 +9431,7 @@ class StreamSfuClient {
|
|
|
9401
9431
|
timeoutId = setTimeout(() => {
|
|
9402
9432
|
const message = `SFU WS connection failed to open after ${this.joinResponseTimeout}ms`;
|
|
9403
9433
|
this.tracer?.trace('signal.timeout', message);
|
|
9404
|
-
reject(new
|
|
9434
|
+
reject(new SfuTimeoutError(message));
|
|
9405
9435
|
}, this.joinResponseTimeout);
|
|
9406
9436
|
}),
|
|
9407
9437
|
]));
|
|
@@ -9571,7 +9601,7 @@ class StreamSfuClient {
|
|
|
9571
9601
|
cleanupJoinSubscriptions();
|
|
9572
9602
|
const message = `Waiting for "joinResponse" has timed out after ${this.joinResponseTimeout}ms`;
|
|
9573
9603
|
this.tracer?.trace('joinRequestTimeout', message);
|
|
9574
|
-
current.reject(new
|
|
9604
|
+
current.reject(new SfuTimeoutError(message));
|
|
9575
9605
|
}, this.joinResponseTimeout);
|
|
9576
9606
|
const joinRequest = SfuRequest.create({
|
|
9577
9607
|
requestPayload: {
|
|
@@ -9788,6 +9818,10 @@ const watchCallEnded = (call) => {
|
|
|
9788
9818
|
const { callingState } = call.state;
|
|
9789
9819
|
if (callingState !== exports.CallingState.IDLE &&
|
|
9790
9820
|
callingState !== exports.CallingState.LEFT) {
|
|
9821
|
+
call.clientEventReporter.abort(call.cid, {
|
|
9822
|
+
code: 'BACKEND_LEAVE',
|
|
9823
|
+
reason: 'call.ended event received',
|
|
9824
|
+
});
|
|
9791
9825
|
call
|
|
9792
9826
|
.leave({ message: 'call.ended event received', reject: false })
|
|
9793
9827
|
.catch((err) => {
|
|
@@ -9817,6 +9851,10 @@ const watchSfuCallEnded = (call) => {
|
|
|
9817
9851
|
call.state.setEndedAt(new Date());
|
|
9818
9852
|
const reason = CallEndedReason[e.reason];
|
|
9819
9853
|
globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
|
|
9854
|
+
call.clientEventReporter.abort(call.cid, {
|
|
9855
|
+
code: 'BACKEND_LEAVE',
|
|
9856
|
+
reason: `callEnded received: ${reason}`,
|
|
9857
|
+
});
|
|
9820
9858
|
await call.leave({ message: `callEnded received: ${reason}` });
|
|
9821
9859
|
}
|
|
9822
9860
|
catch (err) {
|
|
@@ -10969,6 +11007,40 @@ class DynascaleManager {
|
|
|
10969
11007
|
}
|
|
10970
11008
|
}
|
|
10971
11009
|
|
|
11010
|
+
/**
|
|
11011
|
+
* Invokes `onFirstFrame` once when the video element renders a frame.
|
|
11012
|
+
*
|
|
11013
|
+
* Uses `requestVideoFrameCallback` when available, falling back to `loadeddata`
|
|
11014
|
+
* for browsers that don't support it.
|
|
11015
|
+
*/
|
|
11016
|
+
const createFirstVideoFrameDetector = (videoElement, onFirstFrame) => {
|
|
11017
|
+
let done = false;
|
|
11018
|
+
const notify = () => {
|
|
11019
|
+
if (done)
|
|
11020
|
+
return;
|
|
11021
|
+
done = true;
|
|
11022
|
+
onFirstFrame();
|
|
11023
|
+
};
|
|
11024
|
+
if (typeof videoElement.requestVideoFrameCallback === 'function') {
|
|
11025
|
+
const handle = videoElement.requestVideoFrameCallback(notify);
|
|
11026
|
+
return () => {
|
|
11027
|
+
done = true;
|
|
11028
|
+
videoElement.cancelVideoFrameCallback(handle);
|
|
11029
|
+
};
|
|
11030
|
+
}
|
|
11031
|
+
if (videoElement.readyState >= videoElement.HAVE_CURRENT_DATA) {
|
|
11032
|
+
queueMicrotask(notify);
|
|
11033
|
+
return () => {
|
|
11034
|
+
done = true;
|
|
11035
|
+
};
|
|
11036
|
+
}
|
|
11037
|
+
videoElement.addEventListener('loadeddata', notify, { once: true });
|
|
11038
|
+
return () => {
|
|
11039
|
+
done = true;
|
|
11040
|
+
videoElement.removeEventListener('loadeddata', notify);
|
|
11041
|
+
};
|
|
11042
|
+
};
|
|
11043
|
+
|
|
10972
11044
|
const DEFAULT_THRESHOLD = 0.35;
|
|
10973
11045
|
const DEFAULT_VIEWPORT_VISIBILITY_STATE = {
|
|
10974
11046
|
videoTrack: exports.VisibilityState.UNKNOWN,
|
|
@@ -13812,7 +13884,7 @@ class Call {
|
|
|
13812
13884
|
* Use the [`StreamVideoClient.call`](./StreamVideoClient.md/#call)
|
|
13813
13885
|
* method to construct a `Call` instance.
|
|
13814
13886
|
*/
|
|
13815
|
-
constructor({ type, id, streamClient, members, ownCapabilities, sortParticipantsBy, clientStore, ringing = false, watching = false, }) {
|
|
13887
|
+
constructor({ type, id, streamClient, clientEventReporter, members, ownCapabilities, sortParticipantsBy, clientStore, ringing = false, watching = false, }) {
|
|
13816
13888
|
/**
|
|
13817
13889
|
* The state of this call.
|
|
13818
13890
|
*/
|
|
@@ -14139,9 +14211,14 @@ class Call {
|
|
|
14139
14211
|
this.sfuStatsReporter = undefined;
|
|
14140
14212
|
this.lastStatsOptions = undefined;
|
|
14141
14213
|
await this.subscriber?.dispose();
|
|
14214
|
+
this.clientEventReporter.abort(this.cid, {
|
|
14215
|
+
code: 'CLIENT_ABORTED',
|
|
14216
|
+
reason: leaveReason,
|
|
14217
|
+
});
|
|
14142
14218
|
this.subscriber = undefined;
|
|
14143
14219
|
await this.publisher?.dispose();
|
|
14144
14220
|
this.publisher = undefined;
|
|
14221
|
+
this.clientEventReporter.unregisterCall(this.cid);
|
|
14145
14222
|
await this.sfuClient?.leaveAndClose(leaveReason);
|
|
14146
14223
|
this.sfuClient = undefined;
|
|
14147
14224
|
this.trackSubscriptionManager.setSfuClient(undefined);
|
|
@@ -14353,6 +14430,13 @@ class Call {
|
|
|
14353
14430
|
await callingX.joinCall(this, this.clientStore.calls);
|
|
14354
14431
|
}
|
|
14355
14432
|
await this.setup();
|
|
14433
|
+
this.clientEventReporter.registerCall(this.cid, {
|
|
14434
|
+
callType: this.type,
|
|
14435
|
+
callId: this.id,
|
|
14436
|
+
getCallSessionId: () => this.state.session?.id ?? '',
|
|
14437
|
+
getSfuId: () => this.credentials?.server.edge_name ?? '',
|
|
14438
|
+
getUserSessionId: () => this.sfuClient?.sessionId ?? '',
|
|
14439
|
+
});
|
|
14356
14440
|
this.joinResponseTimeout = joinResponseTimeout;
|
|
14357
14441
|
this.rpcRequestTimeout = rpcRequestTimeout;
|
|
14358
14442
|
// we will count the number of join failures per SFU.
|
|
@@ -14362,39 +14446,42 @@ class Call {
|
|
|
14362
14446
|
const joinData = data;
|
|
14363
14447
|
maxJoinRetries = Math.max(maxJoinRetries, 1);
|
|
14364
14448
|
try {
|
|
14365
|
-
|
|
14366
|
-
|
|
14367
|
-
|
|
14368
|
-
|
|
14369
|
-
|
|
14370
|
-
|
|
14371
|
-
|
|
14372
|
-
|
|
14373
|
-
catch (err) {
|
|
14374
|
-
this.logger.warn(`Failed to join call (${attempt})`, this.cid);
|
|
14375
|
-
if ((err instanceof ErrorFromResponse && err.unrecoverable) ||
|
|
14376
|
-
(err instanceof SfuJoinError && err.unrecoverable)) {
|
|
14377
|
-
// if the error is unrecoverable, we should not retry as that signals
|
|
14378
|
-
// that connectivity is good, but the coordinator doesn't allow the user
|
|
14379
|
-
// to join the call due to some reason (e.g., ended call, expired token...)
|
|
14380
|
-
throw err;
|
|
14381
|
-
}
|
|
14382
|
-
// immediately switch to a different SFU in case of recoverable join error
|
|
14383
|
-
const switchSfu = err instanceof SfuJoinError &&
|
|
14384
|
-
SfuJoinError.isJoinErrorCode(err.errorEvent);
|
|
14385
|
-
const sfuId = this.credentials?.server.edge_name || '';
|
|
14386
|
-
const failures = (sfuJoinFailures.get(sfuId) || 0) + 1;
|
|
14387
|
-
sfuJoinFailures.set(sfuId, failures);
|
|
14388
|
-
if (switchSfu || failures >= 2) {
|
|
14389
|
-
joinData.migrating_from = sfuId;
|
|
14390
|
-
joinData.migrating_from_list = Array.from(sfuJoinFailures.keys());
|
|
14449
|
+
await this.clientEventReporter.withJoinLifecycle(this.cid, 'first-attempt', async () => {
|
|
14450
|
+
for (let attempt = 0; attempt < maxJoinRetries; attempt++) {
|
|
14451
|
+
try {
|
|
14452
|
+
this.logger.trace(`Joining call (${attempt})`, this.cid);
|
|
14453
|
+
await this.doJoin(data);
|
|
14454
|
+
delete joinData.migrating_from;
|
|
14455
|
+
delete joinData.migrating_from_list;
|
|
14456
|
+
return;
|
|
14391
14457
|
}
|
|
14392
|
-
|
|
14393
|
-
|
|
14458
|
+
catch (err) {
|
|
14459
|
+
this.logger.warn(`Failed to join call (${attempt})`, this.cid);
|
|
14460
|
+
if ((err instanceof ErrorFromResponse && err.unrecoverable) ||
|
|
14461
|
+
(err instanceof SfuJoinError && err.unrecoverable)) {
|
|
14462
|
+
throw err;
|
|
14463
|
+
}
|
|
14464
|
+
const switchSfu = err instanceof SfuJoinError &&
|
|
14465
|
+
SfuJoinError.isJoinErrorCode(err.errorEvent);
|
|
14466
|
+
const sfuId = this.credentials?.server.edge_name;
|
|
14467
|
+
if (sfuId) {
|
|
14468
|
+
const failures = (sfuJoinFailures.get(sfuId) || 0) + 1;
|
|
14469
|
+
sfuJoinFailures.set(sfuId, failures);
|
|
14470
|
+
if (switchSfu || failures >= 2) {
|
|
14471
|
+
joinData.migrating_from = sfuId;
|
|
14472
|
+
joinData.migrating_from_list = Array.from(sfuJoinFailures.keys());
|
|
14473
|
+
if (attempt < maxJoinRetries - 1) {
|
|
14474
|
+
this.clientEventReporter.startCorrelation(this.cid, 'first-attempt');
|
|
14475
|
+
}
|
|
14476
|
+
}
|
|
14477
|
+
}
|
|
14478
|
+
if (attempt === maxJoinRetries - 1) {
|
|
14479
|
+
throw err;
|
|
14480
|
+
}
|
|
14394
14481
|
}
|
|
14482
|
+
await sleep(retryInterval(attempt));
|
|
14395
14483
|
}
|
|
14396
|
-
|
|
14397
|
-
}
|
|
14484
|
+
});
|
|
14398
14485
|
}
|
|
14399
14486
|
catch (error) {
|
|
14400
14487
|
callingX?.endCall(this, 'error');
|
|
@@ -14423,7 +14510,7 @@ class Call {
|
|
|
14423
14510
|
performingMigration ||
|
|
14424
14511
|
data?.migrating_from) {
|
|
14425
14512
|
try {
|
|
14426
|
-
const joinResponse = await this.doJoinRequest(data);
|
|
14513
|
+
const joinResponse = await this.clientEventReporter.track(this.cid, 'CoordinatorJoin', () => this.doJoinRequest(data));
|
|
14427
14514
|
this.credentials = joinResponse.credentials;
|
|
14428
14515
|
statsOptions = joinResponse.stats_options;
|
|
14429
14516
|
this.lastStatsOptions = statsOptions;
|
|
@@ -14481,9 +14568,11 @@ class Call {
|
|
|
14481
14568
|
const preferredSubscribeOptions = !isReconnecting
|
|
14482
14569
|
? this.getPreferredSubscribeOptions()
|
|
14483
14570
|
: [];
|
|
14571
|
+
const unifiedSessionId = this.unifiedSessionId;
|
|
14572
|
+
const capabilities = Array.from(this.clientCapabilities);
|
|
14484
14573
|
try {
|
|
14485
|
-
const { callState, fastReconnectDeadlineSeconds, publishOptions } = await sfuClient.join({
|
|
14486
|
-
unifiedSessionId
|
|
14574
|
+
const { callState, fastReconnectDeadlineSeconds, publishOptions } = await this.clientEventReporter.track(this.cid, 'WSJoin', () => sfuClient.join({
|
|
14575
|
+
unifiedSessionId,
|
|
14487
14576
|
subscriberSdp,
|
|
14488
14577
|
publisherSdp,
|
|
14489
14578
|
clientDetails,
|
|
@@ -14491,9 +14580,9 @@ class Call {
|
|
|
14491
14580
|
reconnectDetails,
|
|
14492
14581
|
preferredPublishOptions,
|
|
14493
14582
|
preferredSubscribeOptions,
|
|
14494
|
-
capabilities
|
|
14583
|
+
capabilities,
|
|
14495
14584
|
source: ParticipantSource.WEBRTC_UNSPECIFIED,
|
|
14496
|
-
});
|
|
14585
|
+
}));
|
|
14497
14586
|
this.currentPublishOptions = publishOptions;
|
|
14498
14587
|
this.fastReconnectDeadlineSeconds = fastReconnectDeadlineSeconds;
|
|
14499
14588
|
if (callState) {
|
|
@@ -14705,6 +14794,16 @@ class Call {
|
|
|
14705
14794
|
// "ICE never connected" failure budget can be cleared.
|
|
14706
14795
|
this.iceFailuresWithoutConnect = 0;
|
|
14707
14796
|
},
|
|
14797
|
+
onPeerConnectionStateChange: (event) => {
|
|
14798
|
+
this.clientEventReporter.onPeerConnectionStateChange(this.cid, event);
|
|
14799
|
+
},
|
|
14800
|
+
onRemoteTrackUnmute: (trackType, trackId) => {
|
|
14801
|
+
const reportable = trackType === TrackType.AUDIO ||
|
|
14802
|
+
(isReactNative() && trackType === TrackType.VIDEO);
|
|
14803
|
+
if (!reportable)
|
|
14804
|
+
return;
|
|
14805
|
+
this.clientEventReporter.reportFirstFrame(this.cid, trackType, trackId);
|
|
14806
|
+
},
|
|
14708
14807
|
};
|
|
14709
14808
|
this.subscriber = new Subscriber(basePeerConnectionOptions);
|
|
14710
14809
|
// anonymous users can't publish anything hence, there is no need
|
|
@@ -14981,7 +15080,10 @@ class Call {
|
|
|
14981
15080
|
const reconnectStartTime = Date.now();
|
|
14982
15081
|
this.reconnectStrategy = WebsocketReconnectStrategy.REJOIN;
|
|
14983
15082
|
this.state.setCallingState(exports.CallingState.RECONNECTING);
|
|
14984
|
-
|
|
15083
|
+
const joinReason = this.reconnectReason === ReconnectReason.NETWORK_BACK_ONLINE
|
|
15084
|
+
? 'network-available'
|
|
15085
|
+
: 'full-rejoin';
|
|
15086
|
+
await this.clientEventReporter.withJoinLifecycle(this.cid, joinReason, () => this.doJoin(this.joinCallData));
|
|
14985
15087
|
await this.restorePublishedTracks();
|
|
14986
15088
|
this.restoreSubscribedTracks();
|
|
14987
15089
|
this.sfuStatsReporter?.sendReconnectionTime(WebsocketReconnectStrategy.REJOIN, (Date.now() - reconnectStartTime) / 1000);
|
|
@@ -15005,11 +15107,11 @@ class Call {
|
|
|
15005
15107
|
const migrationTask = makeSafePromise(currentSfuClient.enterMigration());
|
|
15006
15108
|
try {
|
|
15007
15109
|
const currentSfu = currentSfuClient.edgeName;
|
|
15008
|
-
await this.doJoin({
|
|
15110
|
+
await this.clientEventReporter.withJoinLifecycle(this.cid, 'migration', () => this.doJoin({
|
|
15009
15111
|
...this.joinCallData,
|
|
15010
15112
|
migrating_from: currentSfu,
|
|
15011
15113
|
migrating_from_list: [currentSfu],
|
|
15012
|
-
});
|
|
15114
|
+
}));
|
|
15013
15115
|
}
|
|
15014
15116
|
finally {
|
|
15015
15117
|
// cleanup the migration_from field after the migration is complete or failed
|
|
@@ -15045,11 +15147,22 @@ class Call {
|
|
|
15045
15147
|
this.registerReconnectHandlers = () => {
|
|
15046
15148
|
// handles the legacy "goAway" event
|
|
15047
15149
|
const unregisterGoAway = this.on('goAway', () => {
|
|
15150
|
+
this.clientEventReporter.captureWsError(this.cid, {
|
|
15151
|
+
code: 'SFU_GO_AWAY',
|
|
15152
|
+
reason: 'SFU goAway received during WS join',
|
|
15153
|
+
});
|
|
15048
15154
|
this.reconnect(WebsocketReconnectStrategy.MIGRATE, ReconnectReason.GO_AWAY).catch((err) => this.logger.warn('[Reconnect] Error reconnecting', err));
|
|
15049
15155
|
});
|
|
15050
15156
|
// handles the "error" event, through which the SFU can request a reconnect
|
|
15051
15157
|
const unregisterOnError = this.on('error', (e) => {
|
|
15052
15158
|
const { reconnectStrategy: strategy, error } = e;
|
|
15159
|
+
if (!SfuJoinError.isJoinErrorCode(e)) {
|
|
15160
|
+
const code = error?.code ? ErrorCode[error.code] : undefined;
|
|
15161
|
+
this.clientEventReporter.captureWsError(this.cid, {
|
|
15162
|
+
code: code ?? 'SFU_ERROR',
|
|
15163
|
+
reason: error?.message || 'SFU error during WS join',
|
|
15164
|
+
});
|
|
15165
|
+
}
|
|
15053
15166
|
// SFU_FULL is a join error, and when emitted, although it specifies a
|
|
15054
15167
|
// `migrate` strategy, we should actually perform a REJOIN to a new SFU.
|
|
15055
15168
|
// This is now handled separately in the `call.join()` method.
|
|
@@ -15744,7 +15857,9 @@ class Call {
|
|
|
15744
15857
|
this.leave({
|
|
15745
15858
|
reject: true,
|
|
15746
15859
|
reason: 'timeout',
|
|
15747
|
-
message: `ringing timeout - ${this.isCreatedByMe
|
|
15860
|
+
message: `ringing timeout - ${this.isCreatedByMe
|
|
15861
|
+
? 'no one accepted'
|
|
15862
|
+
: `user didn't interact with incoming call screen`}`,
|
|
15748
15863
|
}).catch((err) => {
|
|
15749
15864
|
this.logger.error('Failed to drop call', err);
|
|
15750
15865
|
});
|
|
@@ -15950,15 +16065,36 @@ class Call {
|
|
|
15950
16065
|
* @param trackType the kind of video.
|
|
15951
16066
|
*/
|
|
15952
16067
|
this.bindVideoElement = (videoElement, sessionId, trackType) => {
|
|
15953
|
-
const
|
|
15954
|
-
|
|
16068
|
+
const unbindDynascale = this.dynascaleManager?.bindVideoElement(videoElement, sessionId, trackType);
|
|
16069
|
+
const stopFirstFrameDetector = this.bindFirstVideoFrameDetector(videoElement, sessionId, trackType);
|
|
16070
|
+
if (!unbindDynascale && !stopFirstFrameDetector)
|
|
15955
16071
|
return;
|
|
16072
|
+
const unbind = () => {
|
|
16073
|
+
stopFirstFrameDetector?.();
|
|
16074
|
+
unbindDynascale?.();
|
|
16075
|
+
};
|
|
15956
16076
|
this.leaveCallHooks.add(unbind);
|
|
15957
16077
|
return () => {
|
|
15958
16078
|
this.leaveCallHooks.delete(unbind);
|
|
15959
16079
|
unbind();
|
|
15960
16080
|
};
|
|
15961
16081
|
};
|
|
16082
|
+
this.bindFirstVideoFrameDetector = (videoElement, sessionId, trackType) => {
|
|
16083
|
+
if (trackType !== 'videoTrack')
|
|
16084
|
+
return;
|
|
16085
|
+
return createFirstVideoFrameDetector(videoElement, () => {
|
|
16086
|
+
this.reportFirstRenderedVideoFrame(sessionId);
|
|
16087
|
+
});
|
|
16088
|
+
};
|
|
16089
|
+
this.reportFirstRenderedVideoFrame = (sessionId) => {
|
|
16090
|
+
const participant = this.state.findParticipantBySessionId(sessionId);
|
|
16091
|
+
if (participant?.isLocalParticipant)
|
|
16092
|
+
return;
|
|
16093
|
+
const trackId = participant?.videoStream?.getVideoTracks()[0]?.id;
|
|
16094
|
+
if (!trackId)
|
|
16095
|
+
return;
|
|
16096
|
+
this.clientEventReporter.reportFirstFrame(this.cid, TrackType.VIDEO, trackId);
|
|
16097
|
+
};
|
|
15962
16098
|
/**
|
|
15963
16099
|
* Binds a DOM <audio> element to the given session id.
|
|
15964
16100
|
*
|
|
@@ -16108,6 +16244,7 @@ class Call {
|
|
|
16108
16244
|
this.ringingSubject = new rxjs.BehaviorSubject(ringing);
|
|
16109
16245
|
this.watching = watching;
|
|
16110
16246
|
this.streamClient = streamClient;
|
|
16247
|
+
this.clientEventReporter = clientEventReporter;
|
|
16111
16248
|
this.clientStore = clientStore;
|
|
16112
16249
|
this.streamClientBasePath = `/call/${this.type}/${this.id}`;
|
|
16113
16250
|
this.logger = videoLoggerSystem.getLogger('Call');
|
|
@@ -17330,10 +17467,12 @@ class StreamClient {
|
|
|
17330
17467
|
this.logger.info('StreamClient.connect: this.wsConnection.connect()');
|
|
17331
17468
|
return await this.wsConnection.connect(this.defaultWSTimeout);
|
|
17332
17469
|
};
|
|
17470
|
+
this.getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
|
|
17471
|
+
"1.53.0";
|
|
17333
17472
|
this.getUserAgent = () => {
|
|
17334
17473
|
if (!this.cachedUserAgent) {
|
|
17335
17474
|
const { clientAppIdentifier = {} } = this.options;
|
|
17336
|
-
const { sdkName = 'js', sdkVersion = "1.
|
|
17475
|
+
const { sdkName = 'js', sdkVersion = "1.53.0", ...extras } = clientAppIdentifier;
|
|
17337
17476
|
this.cachedUserAgent = [
|
|
17338
17477
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
17339
17478
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|
|
@@ -17510,6 +17649,602 @@ const createTokenOrProvider = (options) => {
|
|
|
17510
17649
|
return token || tokenProvider;
|
|
17511
17650
|
};
|
|
17512
17651
|
|
|
17652
|
+
const pcKey = (cid, role) => `${cid}:${role}`;
|
|
17653
|
+
class ClientEventReporter {
|
|
17654
|
+
constructor(options) {
|
|
17655
|
+
this.logger = videoLoggerSystem.getLogger('ClientEventReporter');
|
|
17656
|
+
this.callContexts = new Map();
|
|
17657
|
+
this.joinAttemptIds = new Map();
|
|
17658
|
+
this.joinReasons = new Map();
|
|
17659
|
+
this.coordinatorPairs = new Map();
|
|
17660
|
+
this.wsPairs = new Map();
|
|
17661
|
+
this.peerConnectionPairs = new Map();
|
|
17662
|
+
this.pcEverConnected = new Map();
|
|
17663
|
+
this.firstFrameReported = new Set();
|
|
17664
|
+
/**
|
|
17665
|
+
* Starts a new coordinator connection correlation scope.
|
|
17666
|
+
*
|
|
17667
|
+
* @param userId the id of the user being connected. Captured here because
|
|
17668
|
+
* the `CoordinatorWS` stage emits before the connection flow assigns
|
|
17669
|
+
* the user to the client, so it can't be read from the client yet.
|
|
17670
|
+
*/
|
|
17671
|
+
this.startCoordinatorConnection = (userId) => {
|
|
17672
|
+
this.coordinatorConnectId = generateUUIDv4();
|
|
17673
|
+
this.coordinatorConnectUserId = userId;
|
|
17674
|
+
return this.coordinatorConnectId;
|
|
17675
|
+
};
|
|
17676
|
+
this.trackCoordinatorWs = async (op) => {
|
|
17677
|
+
this.beginCoordinatorWs();
|
|
17678
|
+
try {
|
|
17679
|
+
const result = await op();
|
|
17680
|
+
this.succeedCoordinatorWs();
|
|
17681
|
+
return result;
|
|
17682
|
+
}
|
|
17683
|
+
catch (err) {
|
|
17684
|
+
applyError(this.coordinatorWsPair, mapCoordinatorWsError(err));
|
|
17685
|
+
throw err;
|
|
17686
|
+
}
|
|
17687
|
+
};
|
|
17688
|
+
this.beginCoordinatorWs = () => {
|
|
17689
|
+
if (!this.coordinatorWsPair) {
|
|
17690
|
+
this.coordinatorWsPair = {
|
|
17691
|
+
sid: generateUUIDv4(),
|
|
17692
|
+
attempts: 0,
|
|
17693
|
+
startedAt: Date.now(),
|
|
17694
|
+
userIdSnapshot: this.coordinatorConnectUserId,
|
|
17695
|
+
};
|
|
17696
|
+
this.send({
|
|
17697
|
+
...this.buildCoordinatorWsCommon(this.coordinatorWsPair),
|
|
17698
|
+
event_type: 'initiated',
|
|
17699
|
+
});
|
|
17700
|
+
}
|
|
17701
|
+
this.coordinatorWsPair.attempts++;
|
|
17702
|
+
};
|
|
17703
|
+
this.succeedCoordinatorWs = () => {
|
|
17704
|
+
const pair = this.coordinatorWsPair;
|
|
17705
|
+
if (!pair)
|
|
17706
|
+
return;
|
|
17707
|
+
this.send({
|
|
17708
|
+
...this.buildCoordinatorWsCommon(pair),
|
|
17709
|
+
event_type: 'completed',
|
|
17710
|
+
outcome: 'success',
|
|
17711
|
+
retry_count_attempt: pair.attempts - 1,
|
|
17712
|
+
elapsed_time: Date.now() - pair.startedAt,
|
|
17713
|
+
});
|
|
17714
|
+
this.coordinatorWsPair = undefined;
|
|
17715
|
+
};
|
|
17716
|
+
this.closeCoordinatorWs = () => {
|
|
17717
|
+
const pair = this.coordinatorWsPair;
|
|
17718
|
+
if (!pair || !pair.lastError) {
|
|
17719
|
+
this.coordinatorWsPair = undefined;
|
|
17720
|
+
return;
|
|
17721
|
+
}
|
|
17722
|
+
const { reason, code } = pair.lastError;
|
|
17723
|
+
this.send({
|
|
17724
|
+
...this.buildCoordinatorWsCommon(pair),
|
|
17725
|
+
event_type: 'completed',
|
|
17726
|
+
outcome: 'failure',
|
|
17727
|
+
retry_count_attempt: pair.attempts - 1,
|
|
17728
|
+
elapsed_time: Date.now() - pair.startedAt,
|
|
17729
|
+
retry_failure_reason: reason,
|
|
17730
|
+
retry_failure_code: code,
|
|
17731
|
+
});
|
|
17732
|
+
this.coordinatorWsPair = undefined;
|
|
17733
|
+
};
|
|
17734
|
+
this.buildCoordinatorWsCommon = (pair) => ({
|
|
17735
|
+
user_id: pair.userIdSnapshot ?? this.streamClient.userID,
|
|
17736
|
+
stage: 'CoordinatorWS',
|
|
17737
|
+
stage_id: pair.sid,
|
|
17738
|
+
...(this.coordinatorConnectId && {
|
|
17739
|
+
coordinator_connect_id: this.coordinatorConnectId,
|
|
17740
|
+
}),
|
|
17741
|
+
timestamp: new Date().toISOString(),
|
|
17742
|
+
user_agent: this.streamClient.getUserAgent(),
|
|
17743
|
+
sdk_version: this.streamClient.getSdkVersion(),
|
|
17744
|
+
});
|
|
17745
|
+
this.emitMediaPermission = (cid) => {
|
|
17746
|
+
if (isReactNative() || !this.callContexts.has(cid))
|
|
17747
|
+
return;
|
|
17748
|
+
const pair = {
|
|
17749
|
+
sid: generateUUIDv4(),
|
|
17750
|
+
attempts: 0,
|
|
17751
|
+
startedAt: Date.now(),
|
|
17752
|
+
joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
|
|
17753
|
+
};
|
|
17754
|
+
this.send({
|
|
17755
|
+
...this.buildCommon(cid, 'MediaDevicePermission', pair),
|
|
17756
|
+
...this.sessionIdField(cid),
|
|
17757
|
+
microphone_permission_status: readPermissionStatus(getAudioBrowserPermission()),
|
|
17758
|
+
camera_permission_status: readPermissionStatus(getVideoBrowserPermission()),
|
|
17759
|
+
event_type: 'initiated',
|
|
17760
|
+
});
|
|
17761
|
+
};
|
|
17762
|
+
this.registerCall = (cid, ctx) => {
|
|
17763
|
+
this.callContexts.set(cid, ctx);
|
|
17764
|
+
};
|
|
17765
|
+
this.unregisterCall = (cid) => {
|
|
17766
|
+
this.callContexts.delete(cid);
|
|
17767
|
+
this.joinAttemptIds.delete(cid);
|
|
17768
|
+
this.joinReasons.delete(cid);
|
|
17769
|
+
this.coordinatorPairs.delete(cid);
|
|
17770
|
+
this.wsPairs.delete(cid);
|
|
17771
|
+
this.firstFrameReported.delete(`${cid}:FirstVideoFrame`);
|
|
17772
|
+
this.firstFrameReported.delete(`${cid}:FirstAudioFrame`);
|
|
17773
|
+
for (const role of ['publish', 'subscribe']) {
|
|
17774
|
+
const key = pcKey(cid, role);
|
|
17775
|
+
this.peerConnectionPairs.delete(key);
|
|
17776
|
+
this.pcEverConnected.delete(key);
|
|
17777
|
+
}
|
|
17778
|
+
};
|
|
17779
|
+
this.startCorrelation = (cid, joinReason) => {
|
|
17780
|
+
try {
|
|
17781
|
+
this.closeCallPairs(cid);
|
|
17782
|
+
this.joinAttemptIds.set(cid, generateUUIDv4());
|
|
17783
|
+
this.joinReasons.set(cid, joinReason);
|
|
17784
|
+
this.firstFrameReported.delete(`${cid}:FirstVideoFrame`);
|
|
17785
|
+
this.firstFrameReported.delete(`${cid}:FirstAudioFrame`);
|
|
17786
|
+
this.emitJoinInitiated(cid);
|
|
17787
|
+
this.emitMediaPermission(cid);
|
|
17788
|
+
}
|
|
17789
|
+
catch (err) {
|
|
17790
|
+
this.logger.warn('Failed to start join correlation', err);
|
|
17791
|
+
}
|
|
17792
|
+
};
|
|
17793
|
+
this.withJoinLifecycle = async (cid, joinReason, op) => {
|
|
17794
|
+
this.startCorrelation(cid, joinReason);
|
|
17795
|
+
try {
|
|
17796
|
+
return await op();
|
|
17797
|
+
}
|
|
17798
|
+
catch (err) {
|
|
17799
|
+
this.closeCallPairs(cid);
|
|
17800
|
+
throw err;
|
|
17801
|
+
}
|
|
17802
|
+
};
|
|
17803
|
+
this.track = async (cid, stage, op) => {
|
|
17804
|
+
this.beginAttempt(cid, stage);
|
|
17805
|
+
try {
|
|
17806
|
+
const result = await op();
|
|
17807
|
+
this.succeedAttempt(cid, stage);
|
|
17808
|
+
return result;
|
|
17809
|
+
}
|
|
17810
|
+
catch (err) {
|
|
17811
|
+
this.applyStageError(cid, stage, err);
|
|
17812
|
+
throw err;
|
|
17813
|
+
}
|
|
17814
|
+
};
|
|
17815
|
+
this.reportFirstFrame = (cid, trackType, trackId) => {
|
|
17816
|
+
const stage = trackType === TrackType.VIDEO
|
|
17817
|
+
? 'FirstVideoFrame'
|
|
17818
|
+
: trackType === TrackType.AUDIO
|
|
17819
|
+
? 'FirstAudioFrame'
|
|
17820
|
+
: undefined;
|
|
17821
|
+
if (!stage)
|
|
17822
|
+
return;
|
|
17823
|
+
const key = `${cid}:${stage}`;
|
|
17824
|
+
if (this.firstFrameReported.has(key))
|
|
17825
|
+
return;
|
|
17826
|
+
this.firstFrameReported.add(key);
|
|
17827
|
+
const pair = {
|
|
17828
|
+
sid: generateUUIDv4(),
|
|
17829
|
+
attempts: 0,
|
|
17830
|
+
startedAt: Date.now(),
|
|
17831
|
+
joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
|
|
17832
|
+
};
|
|
17833
|
+
const resolvedSfuId = this.getSfuId(cid);
|
|
17834
|
+
this.send({
|
|
17835
|
+
...this.buildCommon(cid, stage, pair),
|
|
17836
|
+
...this.sessionIdField(cid),
|
|
17837
|
+
...(resolvedSfuId && { sfu_id: resolvedSfuId }),
|
|
17838
|
+
track_id: trackId,
|
|
17839
|
+
event_type: 'initiated',
|
|
17840
|
+
});
|
|
17841
|
+
};
|
|
17842
|
+
this.captureWsError = (cid, opts) => {
|
|
17843
|
+
const pair = this.wsPairs.get(cid);
|
|
17844
|
+
if (!pair)
|
|
17845
|
+
return;
|
|
17846
|
+
applyError(pair, { reason: opts.reason, code: opts.code });
|
|
17847
|
+
};
|
|
17848
|
+
this.close = (cid) => {
|
|
17849
|
+
this.closeCallPairs(cid);
|
|
17850
|
+
};
|
|
17851
|
+
this.abort = (cid, opts) => {
|
|
17852
|
+
try {
|
|
17853
|
+
const { code, reason } = opts;
|
|
17854
|
+
const stageError = { code, reason };
|
|
17855
|
+
applyErrorIfAbsent(this.coordinatorPairs.get(cid), stageError);
|
|
17856
|
+
applyErrorIfAbsent(this.wsPairs.get(cid), stageError);
|
|
17857
|
+
this.failCoordinator(cid);
|
|
17858
|
+
this.failWs(cid);
|
|
17859
|
+
this.emitPeerConnectionFailure(cid, 'publish', code, reason, 'NOT_CONNECTED');
|
|
17860
|
+
this.emitPeerConnectionFailure(cid, 'subscribe', code, reason, 'NOT_CONNECTED');
|
|
17861
|
+
}
|
|
17862
|
+
catch (err) {
|
|
17863
|
+
this.logger.warn('Failed to report abort', err);
|
|
17864
|
+
}
|
|
17865
|
+
};
|
|
17866
|
+
this.closeCallPairs = (cid) => {
|
|
17867
|
+
if (this.coordinatorPairs.get(cid))
|
|
17868
|
+
this.failCoordinator(cid);
|
|
17869
|
+
if (this.wsPairs.get(cid))
|
|
17870
|
+
this.failWs(cid);
|
|
17871
|
+
for (const role of ['publish', 'subscribe']) {
|
|
17872
|
+
this.emitPeerConnectionFailure(cid, role, 'CLIENT_ABORTED', 'superseded by a new join attempt', 'NOT_CONNECTED');
|
|
17873
|
+
}
|
|
17874
|
+
};
|
|
17875
|
+
this.emitJoinInitiated = (cid) => {
|
|
17876
|
+
const joinAttemptId = this.joinAttemptIds.get(cid);
|
|
17877
|
+
if (!joinAttemptId)
|
|
17878
|
+
return;
|
|
17879
|
+
const coordinatorConnectId = this.coordinatorConnectId;
|
|
17880
|
+
this.send({
|
|
17881
|
+
user_id: this.streamClient.userID,
|
|
17882
|
+
stage: 'JoinInitiated',
|
|
17883
|
+
join_attempt_id: joinAttemptId,
|
|
17884
|
+
...(coordinatorConnectId && {
|
|
17885
|
+
coordinator_connect_id: coordinatorConnectId,
|
|
17886
|
+
}),
|
|
17887
|
+
timestamp: new Date().toISOString(),
|
|
17888
|
+
user_agent: this.streamClient.getUserAgent(),
|
|
17889
|
+
sdk_version: this.streamClient.getSdkVersion(),
|
|
17890
|
+
event_type: 'initiated',
|
|
17891
|
+
});
|
|
17892
|
+
};
|
|
17893
|
+
this.beginAttempt = (cid, stage) => {
|
|
17894
|
+
if (stage === 'CoordinatorJoin')
|
|
17895
|
+
this.beginCoordinatorAttempt(cid);
|
|
17896
|
+
else
|
|
17897
|
+
this.beginWsAttempt(cid);
|
|
17898
|
+
};
|
|
17899
|
+
this.succeedAttempt = (cid, stage) => {
|
|
17900
|
+
if (stage === 'CoordinatorJoin')
|
|
17901
|
+
this.succeedCoordinator(cid);
|
|
17902
|
+
else
|
|
17903
|
+
this.succeedWs(cid);
|
|
17904
|
+
};
|
|
17905
|
+
this.applyStageError = (cid, stage, err) => {
|
|
17906
|
+
const pair = stage === 'CoordinatorJoin'
|
|
17907
|
+
? this.coordinatorPairs.get(cid)
|
|
17908
|
+
: this.wsPairs.get(cid);
|
|
17909
|
+
applyErrorIfAbsent(pair, stage === 'CoordinatorJoin'
|
|
17910
|
+
? mapCoordinatorHttpError(err)
|
|
17911
|
+
: mapWsJoinError(err));
|
|
17912
|
+
};
|
|
17913
|
+
this.beginCoordinatorAttempt = (cid) => {
|
|
17914
|
+
let pair = this.coordinatorPairs.get(cid);
|
|
17915
|
+
if (!pair) {
|
|
17916
|
+
pair = {
|
|
17917
|
+
sid: generateUUIDv4(),
|
|
17918
|
+
attempts: 0,
|
|
17919
|
+
startedAt: Date.now(),
|
|
17920
|
+
joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
|
|
17921
|
+
joinReasonSnapshot: this.joinReasons.get(cid),
|
|
17922
|
+
};
|
|
17923
|
+
this.coordinatorPairs.set(cid, pair);
|
|
17924
|
+
this.send({
|
|
17925
|
+
...this.buildCommon(cid, 'CoordinatorJoin', pair),
|
|
17926
|
+
...(pair.joinReasonSnapshot && {
|
|
17927
|
+
join_reason: pair.joinReasonSnapshot,
|
|
17928
|
+
}),
|
|
17929
|
+
event_type: 'initiated',
|
|
17930
|
+
});
|
|
17931
|
+
}
|
|
17932
|
+
pair.lastError = undefined;
|
|
17933
|
+
pair.attempts++;
|
|
17934
|
+
};
|
|
17935
|
+
this.succeedCoordinator = (cid) => {
|
|
17936
|
+
const pair = this.coordinatorPairs.get(cid);
|
|
17937
|
+
if (!pair)
|
|
17938
|
+
return;
|
|
17939
|
+
this.send({
|
|
17940
|
+
...this.buildCommon(cid, 'CoordinatorJoin', pair),
|
|
17941
|
+
...this.sessionIdField(cid),
|
|
17942
|
+
...(pair.joinReasonSnapshot && { join_reason: pair.joinReasonSnapshot }),
|
|
17943
|
+
event_type: 'completed',
|
|
17944
|
+
outcome: 'success',
|
|
17945
|
+
retry_count_attempt: pair.attempts - 1,
|
|
17946
|
+
elapsed_time: Date.now() - pair.startedAt,
|
|
17947
|
+
});
|
|
17948
|
+
this.coordinatorPairs.delete(cid);
|
|
17949
|
+
};
|
|
17950
|
+
this.failCoordinator = (cid) => {
|
|
17951
|
+
const pair = this.coordinatorPairs.get(cid);
|
|
17952
|
+
if (!pair || !pair.lastError) {
|
|
17953
|
+
this.coordinatorPairs.delete(cid);
|
|
17954
|
+
return;
|
|
17955
|
+
}
|
|
17956
|
+
const { reason, code } = pair.lastError;
|
|
17957
|
+
this.send({
|
|
17958
|
+
...this.buildCommon(cid, 'CoordinatorJoin', pair),
|
|
17959
|
+
...this.sessionIdField(cid),
|
|
17960
|
+
...(pair.joinReasonSnapshot && { join_reason: pair.joinReasonSnapshot }),
|
|
17961
|
+
event_type: 'completed',
|
|
17962
|
+
outcome: 'failure',
|
|
17963
|
+
retry_count_attempt: pair.attempts - 1,
|
|
17964
|
+
elapsed_time: Date.now() - pair.startedAt,
|
|
17965
|
+
retry_failure_reason: reason,
|
|
17966
|
+
retry_failure_code: code,
|
|
17967
|
+
});
|
|
17968
|
+
this.coordinatorPairs.delete(cid);
|
|
17969
|
+
};
|
|
17970
|
+
this.beginWsAttempt = (cid) => {
|
|
17971
|
+
let pair = this.wsPairs.get(cid);
|
|
17972
|
+
if (!pair) {
|
|
17973
|
+
pair = {
|
|
17974
|
+
sid: generateUUIDv4(),
|
|
17975
|
+
attempts: 0,
|
|
17976
|
+
startedAt: Date.now(),
|
|
17977
|
+
joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
|
|
17978
|
+
};
|
|
17979
|
+
this.wsPairs.set(cid, pair);
|
|
17980
|
+
const sfuId = this.getSfuId(cid);
|
|
17981
|
+
this.send({
|
|
17982
|
+
...this.buildCommon(cid, 'WSJoin', pair),
|
|
17983
|
+
...this.sessionIdField(cid),
|
|
17984
|
+
...(sfuId && { sfu_id: sfuId }),
|
|
17985
|
+
event_type: 'initiated',
|
|
17986
|
+
});
|
|
17987
|
+
}
|
|
17988
|
+
pair.lastError = undefined;
|
|
17989
|
+
pair.attempts++;
|
|
17990
|
+
};
|
|
17991
|
+
this.succeedWs = (cid) => {
|
|
17992
|
+
const pair = this.wsPairs.get(cid);
|
|
17993
|
+
if (!pair)
|
|
17994
|
+
return;
|
|
17995
|
+
const sfuId = this.getSfuId(cid);
|
|
17996
|
+
this.send({
|
|
17997
|
+
...this.buildCommon(cid, 'WSJoin', pair),
|
|
17998
|
+
...this.sessionIdField(cid),
|
|
17999
|
+
...(sfuId && { sfu_id: sfuId }),
|
|
18000
|
+
event_type: 'completed',
|
|
18001
|
+
outcome: 'success',
|
|
18002
|
+
retry_count_attempt: pair.attempts - 1,
|
|
18003
|
+
elapsed_time: Date.now() - pair.startedAt,
|
|
18004
|
+
});
|
|
18005
|
+
this.wsPairs.delete(cid);
|
|
18006
|
+
};
|
|
18007
|
+
this.failWs = (cid) => {
|
|
18008
|
+
const pair = this.wsPairs.get(cid);
|
|
18009
|
+
if (!pair || !pair.lastError) {
|
|
18010
|
+
this.wsPairs.delete(cid);
|
|
18011
|
+
return;
|
|
18012
|
+
}
|
|
18013
|
+
const { reason, code } = pair.lastError;
|
|
18014
|
+
const sfuId = this.getSfuId(cid);
|
|
18015
|
+
this.send({
|
|
18016
|
+
...this.buildCommon(cid, 'WSJoin', pair),
|
|
18017
|
+
...this.sessionIdField(cid),
|
|
18018
|
+
event_type: 'completed',
|
|
18019
|
+
outcome: 'failure',
|
|
18020
|
+
retry_count_attempt: pair.attempts - 1,
|
|
18021
|
+
elapsed_time: Date.now() - pair.startedAt,
|
|
18022
|
+
...(sfuId && { sfu_id: sfuId }),
|
|
18023
|
+
retry_failure_reason: reason,
|
|
18024
|
+
retry_failure_code: code,
|
|
18025
|
+
});
|
|
18026
|
+
this.wsPairs.delete(cid);
|
|
18027
|
+
};
|
|
18028
|
+
this.onPeerConnectionStateChange = (cid, event) => {
|
|
18029
|
+
const role = event.peerType === PeerType.SUBSCRIBER ? 'subscribe' : 'publish';
|
|
18030
|
+
if (event.stateType === 'ice' && event.state === 'failed') {
|
|
18031
|
+
this.emitPeerConnectionFailure(cid, role, 'ICE_CONNECTIVITY_FAILED', 'ICE connectivity checks failed', 'FAILED');
|
|
18032
|
+
return;
|
|
18033
|
+
}
|
|
18034
|
+
if (event.stateType === 'peerConnection' && event.state === 'failed') {
|
|
18035
|
+
this.emitPeerConnectionFailure(cid, role, 'DTLS_CONNECTIVITY_FAILED', 'DTLS connectivity checks failed', 'CONNECTED');
|
|
18036
|
+
return;
|
|
18037
|
+
}
|
|
18038
|
+
if (event.stateType !== 'peerConnection')
|
|
18039
|
+
return;
|
|
18040
|
+
switch (event.state) {
|
|
18041
|
+
case 'connecting':
|
|
18042
|
+
if (this.peerConnectionPairs.has(pcKey(cid, role)))
|
|
18043
|
+
return;
|
|
18044
|
+
this.openPeerConnectionPair(cid, role);
|
|
18045
|
+
break;
|
|
18046
|
+
case 'connected':
|
|
18047
|
+
this.emitPeerConnectionSuccess(cid, role);
|
|
18048
|
+
this.pcEverConnected.set(pcKey(cid, role), true);
|
|
18049
|
+
break;
|
|
18050
|
+
}
|
|
18051
|
+
};
|
|
18052
|
+
this.openPeerConnectionPair = (cid, role) => {
|
|
18053
|
+
const key = pcKey(cid, role);
|
|
18054
|
+
const pair = {
|
|
18055
|
+
sid: generateUUIDv4(),
|
|
18056
|
+
attempts: 0,
|
|
18057
|
+
startedAt: Date.now(),
|
|
18058
|
+
joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
|
|
18059
|
+
sfuId: this.getSfuId(cid),
|
|
18060
|
+
userSessionId: this.getUserSessionId(cid),
|
|
18061
|
+
wasPreviouslyConnected: this.pcEverConnected.get(key) === true,
|
|
18062
|
+
};
|
|
18063
|
+
this.peerConnectionPairs.set(key, pair);
|
|
18064
|
+
this.send({
|
|
18065
|
+
...this.buildCommon(cid, 'PeerConnectionConnect', pair),
|
|
18066
|
+
...this.sessionIdField(cid),
|
|
18067
|
+
peer_connection: role,
|
|
18068
|
+
was_previously_connected: pair.wasPreviouslyConnected,
|
|
18069
|
+
...(pair.sfuId && { sfu_id: pair.sfuId }),
|
|
18070
|
+
...(pair.userSessionId && {
|
|
18071
|
+
user_session_id: pair.userSessionId,
|
|
18072
|
+
}),
|
|
18073
|
+
event_type: 'initiated',
|
|
18074
|
+
});
|
|
18075
|
+
};
|
|
18076
|
+
this.emitPeerConnectionSuccess = (cid, role) => {
|
|
18077
|
+
const key = pcKey(cid, role);
|
|
18078
|
+
const pair = this.peerConnectionPairs.get(key);
|
|
18079
|
+
if (!pair)
|
|
18080
|
+
return;
|
|
18081
|
+
this.send({
|
|
18082
|
+
...this.buildCommon(cid, 'PeerConnectionConnect', pair),
|
|
18083
|
+
...this.sessionIdField(cid),
|
|
18084
|
+
peer_connection: role,
|
|
18085
|
+
was_previously_connected: pair.wasPreviouslyConnected,
|
|
18086
|
+
...(pair.sfuId && { sfu_id: pair.sfuId }),
|
|
18087
|
+
...(pair.userSessionId && {
|
|
18088
|
+
user_session_id: pair.userSessionId,
|
|
18089
|
+
}),
|
|
18090
|
+
event_type: 'completed',
|
|
18091
|
+
outcome: 'success',
|
|
18092
|
+
retry_count_attempt: 0,
|
|
18093
|
+
elapsed_time: Date.now() - pair.startedAt,
|
|
18094
|
+
});
|
|
18095
|
+
this.peerConnectionPairs.delete(key);
|
|
18096
|
+
};
|
|
18097
|
+
this.emitPeerConnectionFailure = (cid, role, code, reason, iceState) => {
|
|
18098
|
+
const key = pcKey(cid, role);
|
|
18099
|
+
const pair = this.peerConnectionPairs.get(key);
|
|
18100
|
+
if (!pair)
|
|
18101
|
+
return;
|
|
18102
|
+
this.send({
|
|
18103
|
+
...this.buildCommon(cid, 'PeerConnectionConnect', pair),
|
|
18104
|
+
...this.sessionIdField(cid),
|
|
18105
|
+
peer_connection: role,
|
|
18106
|
+
was_previously_connected: pair.wasPreviouslyConnected,
|
|
18107
|
+
...(pair.userSessionId && {
|
|
18108
|
+
user_session_id: pair.userSessionId,
|
|
18109
|
+
}),
|
|
18110
|
+
...(pair.sfuId && { sfu_id: pair.sfuId }),
|
|
18111
|
+
event_type: 'completed',
|
|
18112
|
+
outcome: 'failure',
|
|
18113
|
+
retry_count_attempt: 0,
|
|
18114
|
+
elapsed_time: Date.now() - pair.startedAt,
|
|
18115
|
+
ice_state: iceState,
|
|
18116
|
+
retry_failure_reason: reason,
|
|
18117
|
+
retry_failure_code: code,
|
|
18118
|
+
});
|
|
18119
|
+
this.peerConnectionPairs.delete(key);
|
|
18120
|
+
};
|
|
18121
|
+
this.getSfuId = (cid) => this.callContexts.get(cid)?.getSfuId() ?? '';
|
|
18122
|
+
this.getUserSessionId = (cid) => this.callContexts.get(cid)?.getUserSessionId() ?? '';
|
|
18123
|
+
this.sessionIdField = (cid) => {
|
|
18124
|
+
const callSessionId = this.callContexts.get(cid)?.getCallSessionId() ?? '';
|
|
18125
|
+
return callSessionId ? { call_session_id: callSessionId } : {};
|
|
18126
|
+
};
|
|
18127
|
+
this.buildCommon = (cid, stage, pair) => {
|
|
18128
|
+
const ctx = this.callContexts.get(cid);
|
|
18129
|
+
const coordinatorConnectId = this.coordinatorConnectId;
|
|
18130
|
+
return {
|
|
18131
|
+
user_id: this.streamClient.userID,
|
|
18132
|
+
type: ctx?.callType ?? '',
|
|
18133
|
+
id: ctx?.callId ?? '',
|
|
18134
|
+
call_cid: cid,
|
|
18135
|
+
stage,
|
|
18136
|
+
stage_id: pair.sid,
|
|
18137
|
+
...(pair.joinAttemptIdSnapshot && {
|
|
18138
|
+
join_attempt_id: pair.joinAttemptIdSnapshot,
|
|
18139
|
+
}),
|
|
18140
|
+
...(coordinatorConnectId && {
|
|
18141
|
+
coordinator_connect_id: coordinatorConnectId,
|
|
18142
|
+
}),
|
|
18143
|
+
timestamp: new Date().toISOString(),
|
|
18144
|
+
user_agent: this.streamClient.getUserAgent(),
|
|
18145
|
+
sdk_version: this.streamClient.getSdkVersion(),
|
|
18146
|
+
};
|
|
18147
|
+
};
|
|
18148
|
+
this.send = (body) => {
|
|
18149
|
+
void this.sendWithRetry(body);
|
|
18150
|
+
};
|
|
18151
|
+
this.sendWithRetry = async (body) => {
|
|
18152
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
18153
|
+
try {
|
|
18154
|
+
await this.streamClient.doAxiosRequest('post', '/call_client_event', { events: [body] }, { publicEndpoint: true });
|
|
18155
|
+
return true;
|
|
18156
|
+
}
|
|
18157
|
+
catch (err) {
|
|
18158
|
+
const status = err?.response
|
|
18159
|
+
?.status;
|
|
18160
|
+
if (typeof status === 'number' && status >= 400 && status < 500) {
|
|
18161
|
+
this.logger.debug(`Client event rejected (${status}), not retrying`, body.stage, body.event_type);
|
|
18162
|
+
return false;
|
|
18163
|
+
}
|
|
18164
|
+
if (attempt === 4) {
|
|
18165
|
+
this.logger.debug('Client event delivery failed after retries', body.stage, body.event_type, err);
|
|
18166
|
+
return false;
|
|
18167
|
+
}
|
|
18168
|
+
await sleep(retryInterval(attempt));
|
|
18169
|
+
}
|
|
18170
|
+
}
|
|
18171
|
+
return false;
|
|
18172
|
+
};
|
|
18173
|
+
this.streamClient = options.streamClient;
|
|
18174
|
+
}
|
|
18175
|
+
}
|
|
18176
|
+
const readPermissionStatus = (permission) => {
|
|
18177
|
+
const state = getCurrentValue(permission.asStateObservable());
|
|
18178
|
+
switch (state) {
|
|
18179
|
+
case 'granted':
|
|
18180
|
+
return 'GRANTED';
|
|
18181
|
+
case 'denied':
|
|
18182
|
+
return 'FAILED';
|
|
18183
|
+
case 'prompting':
|
|
18184
|
+
return 'INITIATED';
|
|
18185
|
+
case 'prompt':
|
|
18186
|
+
default:
|
|
18187
|
+
return 'NOT_INITIATED';
|
|
18188
|
+
}
|
|
18189
|
+
};
|
|
18190
|
+
const errorMessage = (err) => err instanceof Error ? err.message : String(err);
|
|
18191
|
+
const applyError = (pair, next) => {
|
|
18192
|
+
if (!pair)
|
|
18193
|
+
return;
|
|
18194
|
+
pair.lastError = next;
|
|
18195
|
+
};
|
|
18196
|
+
const applyErrorIfAbsent = (pair, next) => {
|
|
18197
|
+
if (!pair || pair.lastError)
|
|
18198
|
+
return;
|
|
18199
|
+
pair.lastError = next;
|
|
18200
|
+
};
|
|
18201
|
+
const mapCoordinatorHttpError = (err) => {
|
|
18202
|
+
if (err instanceof ErrorFromResponse) {
|
|
18203
|
+
return {
|
|
18204
|
+
reason: err.message,
|
|
18205
|
+
code: err.code != null ? String(err.code) : 'SERVER_ERROR',
|
|
18206
|
+
};
|
|
18207
|
+
}
|
|
18208
|
+
return { reason: errorMessage(err), code: 'SERVER_ERROR' };
|
|
18209
|
+
};
|
|
18210
|
+
const mapCoordinatorWsError = (err) => {
|
|
18211
|
+
if (err instanceof ErrorFromResponse) {
|
|
18212
|
+
return {
|
|
18213
|
+
reason: err.message,
|
|
18214
|
+
code: err.code != null ? String(err.code) : 'SERVER_ERROR',
|
|
18215
|
+
};
|
|
18216
|
+
}
|
|
18217
|
+
if (err instanceof Error) {
|
|
18218
|
+
try {
|
|
18219
|
+
const parsed = JSON.parse(err.message);
|
|
18220
|
+
if (typeof parsed.isWSFailure === 'boolean') {
|
|
18221
|
+
return {
|
|
18222
|
+
reason: parsed.message || err.message,
|
|
18223
|
+
code: !parsed.isWSFailure && parsed.code
|
|
18224
|
+
? String(parsed.code)
|
|
18225
|
+
: 'SERVER_ERROR',
|
|
18226
|
+
};
|
|
18227
|
+
}
|
|
18228
|
+
}
|
|
18229
|
+
catch { }
|
|
18230
|
+
}
|
|
18231
|
+
return { reason: errorMessage(err), code: 'SERVER_ERROR' };
|
|
18232
|
+
};
|
|
18233
|
+
const mapWsJoinError = (err) => {
|
|
18234
|
+
if (err instanceof SfuJoinError) {
|
|
18235
|
+
const sfuError = err.errorEvent.error;
|
|
18236
|
+
return {
|
|
18237
|
+
reason: sfuError?.message || err.message,
|
|
18238
|
+
code: (sfuError && ErrorCode[sfuError.code]) || 'SFU_ERROR',
|
|
18239
|
+
};
|
|
18240
|
+
}
|
|
18241
|
+
const reason = errorMessage(err);
|
|
18242
|
+
if (err instanceof SfuTimeoutError) {
|
|
18243
|
+
return { reason, code: 'REQUEST_TIMEOUT' };
|
|
18244
|
+
}
|
|
18245
|
+
return { reason, code: 'SFU_ERROR' };
|
|
18246
|
+
};
|
|
18247
|
+
|
|
17513
18248
|
/**
|
|
17514
18249
|
* A `StreamVideoClient` instance lets you communicate with our API, and authenticate users.
|
|
17515
18250
|
*/
|
|
@@ -17573,6 +18308,7 @@ class StreamVideoClient {
|
|
|
17573
18308
|
}
|
|
17574
18309
|
call = new Call({
|
|
17575
18310
|
streamClient: this.streamClient,
|
|
18311
|
+
clientEventReporter: this.clientEventReporter,
|
|
17576
18312
|
type: e.call.type,
|
|
17577
18313
|
id: e.call.id,
|
|
17578
18314
|
members: e.members,
|
|
@@ -17642,6 +18378,8 @@ class StreamVideoClient {
|
|
|
17642
18378
|
user.id = '!anon';
|
|
17643
18379
|
return this.connectAnonymousUser(user, tokenOrProvider);
|
|
17644
18380
|
}
|
|
18381
|
+
const reporter = this.clientEventReporter;
|
|
18382
|
+
reporter.startCoordinatorConnection(user.id);
|
|
17645
18383
|
const connectUserResponse = await withoutConcurrency(this.connectionConcurrencyTag, async () => {
|
|
17646
18384
|
const client = this.streamClient;
|
|
17647
18385
|
const { onConnectUserError, persistUserOnConnectionFailure } = client.options;
|
|
@@ -17651,14 +18389,15 @@ class StreamVideoClient {
|
|
|
17651
18389
|
for (let attempt = 0; attempt < maxConnectUserRetries; attempt++) {
|
|
17652
18390
|
try {
|
|
17653
18391
|
this.logger.trace(`Connecting user (${attempt})`, user);
|
|
17654
|
-
return user.type === 'guest'
|
|
17655
|
-
?
|
|
17656
|
-
:
|
|
18392
|
+
return await reporter.trackCoordinatorWs(() => user.type === 'guest'
|
|
18393
|
+
? client.connectGuestUser(user)
|
|
18394
|
+
: client.connectUser(user, tokenOrProvider));
|
|
17657
18395
|
}
|
|
17658
18396
|
catch (err) {
|
|
17659
18397
|
this.logger.warn(`Failed to connect a user (${attempt})`, err);
|
|
17660
18398
|
errorQueue.push(err);
|
|
17661
18399
|
if (attempt === maxConnectUserRetries - 1) {
|
|
18400
|
+
reporter.closeCoordinatorWs();
|
|
17662
18401
|
onConnectUserError?.(err, errorQueue);
|
|
17663
18402
|
throw err;
|
|
17664
18403
|
}
|
|
@@ -17736,6 +18475,7 @@ class StreamVideoClient {
|
|
|
17736
18475
|
return (call ??
|
|
17737
18476
|
new Call({
|
|
17738
18477
|
streamClient: this.streamClient,
|
|
18478
|
+
clientEventReporter: this.clientEventReporter,
|
|
17739
18479
|
id: id,
|
|
17740
18480
|
type: type,
|
|
17741
18481
|
clientStore: this.writeableStateStore,
|
|
@@ -17760,6 +18500,7 @@ class StreamVideoClient {
|
|
|
17760
18500
|
for (const c of response.calls) {
|
|
17761
18501
|
const call = new Call({
|
|
17762
18502
|
streamClient: this.streamClient,
|
|
18503
|
+
clientEventReporter: this.clientEventReporter,
|
|
17763
18504
|
id: c.call.id,
|
|
17764
18505
|
type: c.call.type,
|
|
17765
18506
|
members: c.members,
|
|
@@ -17867,6 +18608,7 @@ class StreamVideoClient {
|
|
|
17867
18608
|
const [callType, callId] = call_cid.split(':');
|
|
17868
18609
|
call = new Call({
|
|
17869
18610
|
streamClient: this.streamClient,
|
|
18611
|
+
clientEventReporter: this.clientEventReporter,
|
|
17870
18612
|
type: callType,
|
|
17871
18613
|
id: callId,
|
|
17872
18614
|
clientStore: this.writeableStateStore,
|
|
@@ -17907,6 +18649,9 @@ class StreamVideoClient {
|
|
|
17907
18649
|
this.logger = videoLoggerSystem.getLogger('client');
|
|
17908
18650
|
this.rejectCallWhenBusy = clientOptions?.rejectCallWhenBusy ?? false;
|
|
17909
18651
|
this.streamClient = createCoordinatorClient(apiKey, clientOptions);
|
|
18652
|
+
this.clientEventReporter = new ClientEventReporter({
|
|
18653
|
+
streamClient: this.streamClient,
|
|
18654
|
+
});
|
|
17910
18655
|
this.writeableStateStore = new StreamVideoWriteableStateStore();
|
|
17911
18656
|
this.readOnlyStateStore = new StreamVideoReadOnlyStateStore(this.writeableStateStore);
|
|
17912
18657
|
if (typeof apiKeyOrArgs !== 'string' && apiKeyOrArgs.user) {
|
|
@@ -18020,6 +18765,7 @@ exports.ScreenShareState = ScreenShareState;
|
|
|
18020
18765
|
exports.SfuEvents = events;
|
|
18021
18766
|
exports.SfuJoinError = SfuJoinError;
|
|
18022
18767
|
exports.SfuModels = models;
|
|
18768
|
+
exports.SfuTimeoutError = SfuTimeoutError;
|
|
18023
18769
|
exports.SpeakerManager = SpeakerManager;
|
|
18024
18770
|
exports.SpeakerState = SpeakerState;
|
|
18025
18771
|
exports.StartClosedCaptionsRequestLanguageEnum = StartClosedCaptionsRequestLanguageEnum;
|