@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.es.js
CHANGED
|
@@ -6641,7 +6641,7 @@ const getSdkVersion = (sdk) => {
|
|
|
6641
6641
|
return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
|
|
6642
6642
|
};
|
|
6643
6643
|
|
|
6644
|
-
const version = "1.
|
|
6644
|
+
const version = "1.53.0";
|
|
6645
6645
|
const [major, minor, patch] = version.split('.');
|
|
6646
6646
|
let sdkInfo = {
|
|
6647
6647
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -7735,7 +7735,7 @@ class BasePeerConnection {
|
|
|
7735
7735
|
/**
|
|
7736
7736
|
* Constructs a new `BasePeerConnection` instance.
|
|
7737
7737
|
*/
|
|
7738
|
-
constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, onIceConnected, tag, enableTracing, clientPublishOptions, iceRestartDelay = 2500, statsTimestampDriftThresholdMs = 0, }) {
|
|
7738
|
+
constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, onIceConnected, onPeerConnectionStateChange, onRemoteTrackUnmute, tag, enableTracing, clientPublishOptions, iceRestartDelay = 2500, statsTimestampDriftThresholdMs = 0, }) {
|
|
7739
7739
|
this.iceHasEverConnected = false;
|
|
7740
7740
|
this.isIceRestarting = false;
|
|
7741
7741
|
this.isDisposed = false;
|
|
@@ -7889,6 +7889,10 @@ class BasePeerConnection {
|
|
|
7889
7889
|
this.onConnectionStateChange = async () => {
|
|
7890
7890
|
const state = this.pc.connectionState;
|
|
7891
7891
|
this.logger.debug(`Connection state changed`, state);
|
|
7892
|
+
this.fireOnPeerConnectionStateChange({
|
|
7893
|
+
stateType: 'peerConnection',
|
|
7894
|
+
state,
|
|
7895
|
+
});
|
|
7892
7896
|
if (this.tracer && (state === 'connected' || state === 'failed')) {
|
|
7893
7897
|
try {
|
|
7894
7898
|
const stats = await this.stats.get();
|
|
@@ -7911,8 +7915,20 @@ class BasePeerConnection {
|
|
|
7911
7915
|
this.onIceConnectionStateChange = () => {
|
|
7912
7916
|
const state = this.pc.iceConnectionState;
|
|
7913
7917
|
this.logger.debug(`ICE connection state changed`, state);
|
|
7918
|
+
this.fireOnPeerConnectionStateChange({ stateType: 'ice', state });
|
|
7914
7919
|
this.handleConnectionStateUpdate(state);
|
|
7915
7920
|
};
|
|
7921
|
+
this.fireOnPeerConnectionStateChange = (event) => {
|
|
7922
|
+
try {
|
|
7923
|
+
this.onPeerConnectionStateChange?.({
|
|
7924
|
+
peerType: this.peerType,
|
|
7925
|
+
...event,
|
|
7926
|
+
});
|
|
7927
|
+
}
|
|
7928
|
+
catch (err) {
|
|
7929
|
+
this.logger.warn('onPeerConnectionStateChange listener threw', err);
|
|
7930
|
+
}
|
|
7931
|
+
};
|
|
7916
7932
|
this.handleConnectionStateUpdate = (state) => {
|
|
7917
7933
|
const { callingState } = this.state;
|
|
7918
7934
|
if (callingState === CallingState.OFFLINE)
|
|
@@ -8027,6 +8043,8 @@ class BasePeerConnection {
|
|
|
8027
8043
|
this.tag = tag;
|
|
8028
8044
|
this.onReconnectionNeeded = onReconnectionNeeded;
|
|
8029
8045
|
this.onIceConnected = onIceConnected;
|
|
8046
|
+
this.onPeerConnectionStateChange = onPeerConnectionStateChange;
|
|
8047
|
+
this.onRemoteTrackUnmute = onRemoteTrackUnmute;
|
|
8030
8048
|
this.logger = videoLoggerSystem.getLogger(peerType === PeerType.SUBSCRIBER ? 'Subscriber' : 'Publisher', { tags: [tag] });
|
|
8031
8049
|
this.pc = this.createPeerConnection(connectionConfig);
|
|
8032
8050
|
this.stats = new StatsTracer(this.pc, peerType, this.trackIdToTrackType, statsTimestampDriftThresholdMs);
|
|
@@ -8049,6 +8067,8 @@ class BasePeerConnection {
|
|
|
8049
8067
|
this.preConnectStuckTimeout = undefined;
|
|
8050
8068
|
this.onReconnectionNeeded = undefined;
|
|
8051
8069
|
this.onIceConnected = undefined;
|
|
8070
|
+
this.onPeerConnectionStateChange = undefined;
|
|
8071
|
+
this.onRemoteTrackUnmute = undefined;
|
|
8052
8072
|
this.isDisposed = true;
|
|
8053
8073
|
this.detachEventHandlers();
|
|
8054
8074
|
this.pc.close();
|
|
@@ -9038,6 +9058,7 @@ class Subscriber extends BasePeerConnection {
|
|
|
9038
9058
|
track.addEventListener('unmute', () => {
|
|
9039
9059
|
this.logger.info(`[onTrack]: Track unmuted: ${trackDebugInfo}`);
|
|
9040
9060
|
this.setRemoteTrackInterrupted(trackId, trackType, false);
|
|
9061
|
+
this.onRemoteTrackUnmute?.(trackType, track.id);
|
|
9041
9062
|
});
|
|
9042
9063
|
track.addEventListener('ended', () => {
|
|
9043
9064
|
this.logger.info(`[onTrack]: Track ended: ${trackDebugInfo}`);
|
|
@@ -9277,6 +9298,15 @@ class SfuJoinError extends Error {
|
|
|
9277
9298
|
}
|
|
9278
9299
|
}
|
|
9279
9300
|
|
|
9301
|
+
/**
|
|
9302
|
+
* An error thrown when a client-side SFU deadline (e.g., waiting for the
|
|
9303
|
+
* signaling WS to open or for the `joinResponse` to arrive) fires before
|
|
9304
|
+
* the awaited operation resolves. Allows consumers (e.g., the client event
|
|
9305
|
+
* reporter) to classify timeouts without relying on message wording.
|
|
9306
|
+
*/
|
|
9307
|
+
class SfuTimeoutError extends Error {
|
|
9308
|
+
}
|
|
9309
|
+
|
|
9280
9310
|
/**
|
|
9281
9311
|
* Creates a fresh `joinResponseTask` with a no-op rejection handler attached
|
|
9282
9312
|
* to the underlying promise. The handler marks the rejection path as handled
|
|
@@ -9382,7 +9412,7 @@ class StreamSfuClient {
|
|
|
9382
9412
|
timeoutId = setTimeout(() => {
|
|
9383
9413
|
const message = `SFU WS connection failed to open after ${this.joinResponseTimeout}ms`;
|
|
9384
9414
|
this.tracer?.trace('signal.timeout', message);
|
|
9385
|
-
reject(new
|
|
9415
|
+
reject(new SfuTimeoutError(message));
|
|
9386
9416
|
}, this.joinResponseTimeout);
|
|
9387
9417
|
}),
|
|
9388
9418
|
]));
|
|
@@ -9552,7 +9582,7 @@ class StreamSfuClient {
|
|
|
9552
9582
|
cleanupJoinSubscriptions();
|
|
9553
9583
|
const message = `Waiting for "joinResponse" has timed out after ${this.joinResponseTimeout}ms`;
|
|
9554
9584
|
this.tracer?.trace('joinRequestTimeout', message);
|
|
9555
|
-
current.reject(new
|
|
9585
|
+
current.reject(new SfuTimeoutError(message));
|
|
9556
9586
|
}, this.joinResponseTimeout);
|
|
9557
9587
|
const joinRequest = SfuRequest.create({
|
|
9558
9588
|
requestPayload: {
|
|
@@ -9769,6 +9799,10 @@ const watchCallEnded = (call) => {
|
|
|
9769
9799
|
const { callingState } = call.state;
|
|
9770
9800
|
if (callingState !== CallingState.IDLE &&
|
|
9771
9801
|
callingState !== CallingState.LEFT) {
|
|
9802
|
+
call.clientEventReporter.abort(call.cid, {
|
|
9803
|
+
code: 'BACKEND_LEAVE',
|
|
9804
|
+
reason: 'call.ended event received',
|
|
9805
|
+
});
|
|
9772
9806
|
call
|
|
9773
9807
|
.leave({ message: 'call.ended event received', reject: false })
|
|
9774
9808
|
.catch((err) => {
|
|
@@ -9798,6 +9832,10 @@ const watchSfuCallEnded = (call) => {
|
|
|
9798
9832
|
call.state.setEndedAt(new Date());
|
|
9799
9833
|
const reason = CallEndedReason[e.reason];
|
|
9800
9834
|
globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
|
|
9835
|
+
call.clientEventReporter.abort(call.cid, {
|
|
9836
|
+
code: 'BACKEND_LEAVE',
|
|
9837
|
+
reason: `callEnded received: ${reason}`,
|
|
9838
|
+
});
|
|
9801
9839
|
await call.leave({ message: `callEnded received: ${reason}` });
|
|
9802
9840
|
}
|
|
9803
9841
|
catch (err) {
|
|
@@ -10950,6 +10988,40 @@ class DynascaleManager {
|
|
|
10950
10988
|
}
|
|
10951
10989
|
}
|
|
10952
10990
|
|
|
10991
|
+
/**
|
|
10992
|
+
* Invokes `onFirstFrame` once when the video element renders a frame.
|
|
10993
|
+
*
|
|
10994
|
+
* Uses `requestVideoFrameCallback` when available, falling back to `loadeddata`
|
|
10995
|
+
* for browsers that don't support it.
|
|
10996
|
+
*/
|
|
10997
|
+
const createFirstVideoFrameDetector = (videoElement, onFirstFrame) => {
|
|
10998
|
+
let done = false;
|
|
10999
|
+
const notify = () => {
|
|
11000
|
+
if (done)
|
|
11001
|
+
return;
|
|
11002
|
+
done = true;
|
|
11003
|
+
onFirstFrame();
|
|
11004
|
+
};
|
|
11005
|
+
if (typeof videoElement.requestVideoFrameCallback === 'function') {
|
|
11006
|
+
const handle = videoElement.requestVideoFrameCallback(notify);
|
|
11007
|
+
return () => {
|
|
11008
|
+
done = true;
|
|
11009
|
+
videoElement.cancelVideoFrameCallback(handle);
|
|
11010
|
+
};
|
|
11011
|
+
}
|
|
11012
|
+
if (videoElement.readyState >= videoElement.HAVE_CURRENT_DATA) {
|
|
11013
|
+
queueMicrotask(notify);
|
|
11014
|
+
return () => {
|
|
11015
|
+
done = true;
|
|
11016
|
+
};
|
|
11017
|
+
}
|
|
11018
|
+
videoElement.addEventListener('loadeddata', notify, { once: true });
|
|
11019
|
+
return () => {
|
|
11020
|
+
done = true;
|
|
11021
|
+
videoElement.removeEventListener('loadeddata', notify);
|
|
11022
|
+
};
|
|
11023
|
+
};
|
|
11024
|
+
|
|
10953
11025
|
const DEFAULT_THRESHOLD = 0.35;
|
|
10954
11026
|
const DEFAULT_VIEWPORT_VISIBILITY_STATE = {
|
|
10955
11027
|
videoTrack: VisibilityState.UNKNOWN,
|
|
@@ -13793,7 +13865,7 @@ class Call {
|
|
|
13793
13865
|
* Use the [`StreamVideoClient.call`](./StreamVideoClient.md/#call)
|
|
13794
13866
|
* method to construct a `Call` instance.
|
|
13795
13867
|
*/
|
|
13796
|
-
constructor({ type, id, streamClient, members, ownCapabilities, sortParticipantsBy, clientStore, ringing = false, watching = false, }) {
|
|
13868
|
+
constructor({ type, id, streamClient, clientEventReporter, members, ownCapabilities, sortParticipantsBy, clientStore, ringing = false, watching = false, }) {
|
|
13797
13869
|
/**
|
|
13798
13870
|
* The state of this call.
|
|
13799
13871
|
*/
|
|
@@ -14120,9 +14192,14 @@ class Call {
|
|
|
14120
14192
|
this.sfuStatsReporter = undefined;
|
|
14121
14193
|
this.lastStatsOptions = undefined;
|
|
14122
14194
|
await this.subscriber?.dispose();
|
|
14195
|
+
this.clientEventReporter.abort(this.cid, {
|
|
14196
|
+
code: 'CLIENT_ABORTED',
|
|
14197
|
+
reason: leaveReason,
|
|
14198
|
+
});
|
|
14123
14199
|
this.subscriber = undefined;
|
|
14124
14200
|
await this.publisher?.dispose();
|
|
14125
14201
|
this.publisher = undefined;
|
|
14202
|
+
this.clientEventReporter.unregisterCall(this.cid);
|
|
14126
14203
|
await this.sfuClient?.leaveAndClose(leaveReason);
|
|
14127
14204
|
this.sfuClient = undefined;
|
|
14128
14205
|
this.trackSubscriptionManager.setSfuClient(undefined);
|
|
@@ -14334,6 +14411,13 @@ class Call {
|
|
|
14334
14411
|
await callingX.joinCall(this, this.clientStore.calls);
|
|
14335
14412
|
}
|
|
14336
14413
|
await this.setup();
|
|
14414
|
+
this.clientEventReporter.registerCall(this.cid, {
|
|
14415
|
+
callType: this.type,
|
|
14416
|
+
callId: this.id,
|
|
14417
|
+
getCallSessionId: () => this.state.session?.id ?? '',
|
|
14418
|
+
getSfuId: () => this.credentials?.server.edge_name ?? '',
|
|
14419
|
+
getUserSessionId: () => this.sfuClient?.sessionId ?? '',
|
|
14420
|
+
});
|
|
14337
14421
|
this.joinResponseTimeout = joinResponseTimeout;
|
|
14338
14422
|
this.rpcRequestTimeout = rpcRequestTimeout;
|
|
14339
14423
|
// we will count the number of join failures per SFU.
|
|
@@ -14343,39 +14427,42 @@ class Call {
|
|
|
14343
14427
|
const joinData = data;
|
|
14344
14428
|
maxJoinRetries = Math.max(maxJoinRetries, 1);
|
|
14345
14429
|
try {
|
|
14346
|
-
|
|
14347
|
-
|
|
14348
|
-
|
|
14349
|
-
|
|
14350
|
-
|
|
14351
|
-
|
|
14352
|
-
|
|
14353
|
-
|
|
14354
|
-
catch (err) {
|
|
14355
|
-
this.logger.warn(`Failed to join call (${attempt})`, this.cid);
|
|
14356
|
-
if ((err instanceof ErrorFromResponse && err.unrecoverable) ||
|
|
14357
|
-
(err instanceof SfuJoinError && err.unrecoverable)) {
|
|
14358
|
-
// if the error is unrecoverable, we should not retry as that signals
|
|
14359
|
-
// that connectivity is good, but the coordinator doesn't allow the user
|
|
14360
|
-
// to join the call due to some reason (e.g., ended call, expired token...)
|
|
14361
|
-
throw err;
|
|
14362
|
-
}
|
|
14363
|
-
// immediately switch to a different SFU in case of recoverable join error
|
|
14364
|
-
const switchSfu = err instanceof SfuJoinError &&
|
|
14365
|
-
SfuJoinError.isJoinErrorCode(err.errorEvent);
|
|
14366
|
-
const sfuId = this.credentials?.server.edge_name || '';
|
|
14367
|
-
const failures = (sfuJoinFailures.get(sfuId) || 0) + 1;
|
|
14368
|
-
sfuJoinFailures.set(sfuId, failures);
|
|
14369
|
-
if (switchSfu || failures >= 2) {
|
|
14370
|
-
joinData.migrating_from = sfuId;
|
|
14371
|
-
joinData.migrating_from_list = Array.from(sfuJoinFailures.keys());
|
|
14430
|
+
await this.clientEventReporter.withJoinLifecycle(this.cid, 'first-attempt', async () => {
|
|
14431
|
+
for (let attempt = 0; attempt < maxJoinRetries; attempt++) {
|
|
14432
|
+
try {
|
|
14433
|
+
this.logger.trace(`Joining call (${attempt})`, this.cid);
|
|
14434
|
+
await this.doJoin(data);
|
|
14435
|
+
delete joinData.migrating_from;
|
|
14436
|
+
delete joinData.migrating_from_list;
|
|
14437
|
+
return;
|
|
14372
14438
|
}
|
|
14373
|
-
|
|
14374
|
-
|
|
14439
|
+
catch (err) {
|
|
14440
|
+
this.logger.warn(`Failed to join call (${attempt})`, this.cid);
|
|
14441
|
+
if ((err instanceof ErrorFromResponse && err.unrecoverable) ||
|
|
14442
|
+
(err instanceof SfuJoinError && err.unrecoverable)) {
|
|
14443
|
+
throw err;
|
|
14444
|
+
}
|
|
14445
|
+
const switchSfu = err instanceof SfuJoinError &&
|
|
14446
|
+
SfuJoinError.isJoinErrorCode(err.errorEvent);
|
|
14447
|
+
const sfuId = this.credentials?.server.edge_name;
|
|
14448
|
+
if (sfuId) {
|
|
14449
|
+
const failures = (sfuJoinFailures.get(sfuId) || 0) + 1;
|
|
14450
|
+
sfuJoinFailures.set(sfuId, failures);
|
|
14451
|
+
if (switchSfu || failures >= 2) {
|
|
14452
|
+
joinData.migrating_from = sfuId;
|
|
14453
|
+
joinData.migrating_from_list = Array.from(sfuJoinFailures.keys());
|
|
14454
|
+
if (attempt < maxJoinRetries - 1) {
|
|
14455
|
+
this.clientEventReporter.startCorrelation(this.cid, 'first-attempt');
|
|
14456
|
+
}
|
|
14457
|
+
}
|
|
14458
|
+
}
|
|
14459
|
+
if (attempt === maxJoinRetries - 1) {
|
|
14460
|
+
throw err;
|
|
14461
|
+
}
|
|
14375
14462
|
}
|
|
14463
|
+
await sleep(retryInterval(attempt));
|
|
14376
14464
|
}
|
|
14377
|
-
|
|
14378
|
-
}
|
|
14465
|
+
});
|
|
14379
14466
|
}
|
|
14380
14467
|
catch (error) {
|
|
14381
14468
|
callingX?.endCall(this, 'error');
|
|
@@ -14404,7 +14491,7 @@ class Call {
|
|
|
14404
14491
|
performingMigration ||
|
|
14405
14492
|
data?.migrating_from) {
|
|
14406
14493
|
try {
|
|
14407
|
-
const joinResponse = await this.doJoinRequest(data);
|
|
14494
|
+
const joinResponse = await this.clientEventReporter.track(this.cid, 'CoordinatorJoin', () => this.doJoinRequest(data));
|
|
14408
14495
|
this.credentials = joinResponse.credentials;
|
|
14409
14496
|
statsOptions = joinResponse.stats_options;
|
|
14410
14497
|
this.lastStatsOptions = statsOptions;
|
|
@@ -14462,9 +14549,11 @@ class Call {
|
|
|
14462
14549
|
const preferredSubscribeOptions = !isReconnecting
|
|
14463
14550
|
? this.getPreferredSubscribeOptions()
|
|
14464
14551
|
: [];
|
|
14552
|
+
const unifiedSessionId = this.unifiedSessionId;
|
|
14553
|
+
const capabilities = Array.from(this.clientCapabilities);
|
|
14465
14554
|
try {
|
|
14466
|
-
const { callState, fastReconnectDeadlineSeconds, publishOptions } = await sfuClient.join({
|
|
14467
|
-
unifiedSessionId
|
|
14555
|
+
const { callState, fastReconnectDeadlineSeconds, publishOptions } = await this.clientEventReporter.track(this.cid, 'WSJoin', () => sfuClient.join({
|
|
14556
|
+
unifiedSessionId,
|
|
14468
14557
|
subscriberSdp,
|
|
14469
14558
|
publisherSdp,
|
|
14470
14559
|
clientDetails,
|
|
@@ -14472,9 +14561,9 @@ class Call {
|
|
|
14472
14561
|
reconnectDetails,
|
|
14473
14562
|
preferredPublishOptions,
|
|
14474
14563
|
preferredSubscribeOptions,
|
|
14475
|
-
capabilities
|
|
14564
|
+
capabilities,
|
|
14476
14565
|
source: ParticipantSource.WEBRTC_UNSPECIFIED,
|
|
14477
|
-
});
|
|
14566
|
+
}));
|
|
14478
14567
|
this.currentPublishOptions = publishOptions;
|
|
14479
14568
|
this.fastReconnectDeadlineSeconds = fastReconnectDeadlineSeconds;
|
|
14480
14569
|
if (callState) {
|
|
@@ -14686,6 +14775,16 @@ class Call {
|
|
|
14686
14775
|
// "ICE never connected" failure budget can be cleared.
|
|
14687
14776
|
this.iceFailuresWithoutConnect = 0;
|
|
14688
14777
|
},
|
|
14778
|
+
onPeerConnectionStateChange: (event) => {
|
|
14779
|
+
this.clientEventReporter.onPeerConnectionStateChange(this.cid, event);
|
|
14780
|
+
},
|
|
14781
|
+
onRemoteTrackUnmute: (trackType, trackId) => {
|
|
14782
|
+
const reportable = trackType === TrackType.AUDIO ||
|
|
14783
|
+
(isReactNative() && trackType === TrackType.VIDEO);
|
|
14784
|
+
if (!reportable)
|
|
14785
|
+
return;
|
|
14786
|
+
this.clientEventReporter.reportFirstFrame(this.cid, trackType, trackId);
|
|
14787
|
+
},
|
|
14689
14788
|
};
|
|
14690
14789
|
this.subscriber = new Subscriber(basePeerConnectionOptions);
|
|
14691
14790
|
// anonymous users can't publish anything hence, there is no need
|
|
@@ -14962,7 +15061,10 @@ class Call {
|
|
|
14962
15061
|
const reconnectStartTime = Date.now();
|
|
14963
15062
|
this.reconnectStrategy = WebsocketReconnectStrategy.REJOIN;
|
|
14964
15063
|
this.state.setCallingState(CallingState.RECONNECTING);
|
|
14965
|
-
|
|
15064
|
+
const joinReason = this.reconnectReason === ReconnectReason.NETWORK_BACK_ONLINE
|
|
15065
|
+
? 'network-available'
|
|
15066
|
+
: 'full-rejoin';
|
|
15067
|
+
await this.clientEventReporter.withJoinLifecycle(this.cid, joinReason, () => this.doJoin(this.joinCallData));
|
|
14966
15068
|
await this.restorePublishedTracks();
|
|
14967
15069
|
this.restoreSubscribedTracks();
|
|
14968
15070
|
this.sfuStatsReporter?.sendReconnectionTime(WebsocketReconnectStrategy.REJOIN, (Date.now() - reconnectStartTime) / 1000);
|
|
@@ -14986,11 +15088,11 @@ class Call {
|
|
|
14986
15088
|
const migrationTask = makeSafePromise(currentSfuClient.enterMigration());
|
|
14987
15089
|
try {
|
|
14988
15090
|
const currentSfu = currentSfuClient.edgeName;
|
|
14989
|
-
await this.doJoin({
|
|
15091
|
+
await this.clientEventReporter.withJoinLifecycle(this.cid, 'migration', () => this.doJoin({
|
|
14990
15092
|
...this.joinCallData,
|
|
14991
15093
|
migrating_from: currentSfu,
|
|
14992
15094
|
migrating_from_list: [currentSfu],
|
|
14993
|
-
});
|
|
15095
|
+
}));
|
|
14994
15096
|
}
|
|
14995
15097
|
finally {
|
|
14996
15098
|
// cleanup the migration_from field after the migration is complete or failed
|
|
@@ -15026,11 +15128,22 @@ class Call {
|
|
|
15026
15128
|
this.registerReconnectHandlers = () => {
|
|
15027
15129
|
// handles the legacy "goAway" event
|
|
15028
15130
|
const unregisterGoAway = this.on('goAway', () => {
|
|
15131
|
+
this.clientEventReporter.captureWsError(this.cid, {
|
|
15132
|
+
code: 'SFU_GO_AWAY',
|
|
15133
|
+
reason: 'SFU goAway received during WS join',
|
|
15134
|
+
});
|
|
15029
15135
|
this.reconnect(WebsocketReconnectStrategy.MIGRATE, ReconnectReason.GO_AWAY).catch((err) => this.logger.warn('[Reconnect] Error reconnecting', err));
|
|
15030
15136
|
});
|
|
15031
15137
|
// handles the "error" event, through which the SFU can request a reconnect
|
|
15032
15138
|
const unregisterOnError = this.on('error', (e) => {
|
|
15033
15139
|
const { reconnectStrategy: strategy, error } = e;
|
|
15140
|
+
if (!SfuJoinError.isJoinErrorCode(e)) {
|
|
15141
|
+
const code = error?.code ? ErrorCode[error.code] : undefined;
|
|
15142
|
+
this.clientEventReporter.captureWsError(this.cid, {
|
|
15143
|
+
code: code ?? 'SFU_ERROR',
|
|
15144
|
+
reason: error?.message || 'SFU error during WS join',
|
|
15145
|
+
});
|
|
15146
|
+
}
|
|
15034
15147
|
// SFU_FULL is a join error, and when emitted, although it specifies a
|
|
15035
15148
|
// `migrate` strategy, we should actually perform a REJOIN to a new SFU.
|
|
15036
15149
|
// This is now handled separately in the `call.join()` method.
|
|
@@ -15725,7 +15838,9 @@ class Call {
|
|
|
15725
15838
|
this.leave({
|
|
15726
15839
|
reject: true,
|
|
15727
15840
|
reason: 'timeout',
|
|
15728
|
-
message: `ringing timeout - ${this.isCreatedByMe
|
|
15841
|
+
message: `ringing timeout - ${this.isCreatedByMe
|
|
15842
|
+
? 'no one accepted'
|
|
15843
|
+
: `user didn't interact with incoming call screen`}`,
|
|
15729
15844
|
}).catch((err) => {
|
|
15730
15845
|
this.logger.error('Failed to drop call', err);
|
|
15731
15846
|
});
|
|
@@ -15931,15 +16046,36 @@ class Call {
|
|
|
15931
16046
|
* @param trackType the kind of video.
|
|
15932
16047
|
*/
|
|
15933
16048
|
this.bindVideoElement = (videoElement, sessionId, trackType) => {
|
|
15934
|
-
const
|
|
15935
|
-
|
|
16049
|
+
const unbindDynascale = this.dynascaleManager?.bindVideoElement(videoElement, sessionId, trackType);
|
|
16050
|
+
const stopFirstFrameDetector = this.bindFirstVideoFrameDetector(videoElement, sessionId, trackType);
|
|
16051
|
+
if (!unbindDynascale && !stopFirstFrameDetector)
|
|
15936
16052
|
return;
|
|
16053
|
+
const unbind = () => {
|
|
16054
|
+
stopFirstFrameDetector?.();
|
|
16055
|
+
unbindDynascale?.();
|
|
16056
|
+
};
|
|
15937
16057
|
this.leaveCallHooks.add(unbind);
|
|
15938
16058
|
return () => {
|
|
15939
16059
|
this.leaveCallHooks.delete(unbind);
|
|
15940
16060
|
unbind();
|
|
15941
16061
|
};
|
|
15942
16062
|
};
|
|
16063
|
+
this.bindFirstVideoFrameDetector = (videoElement, sessionId, trackType) => {
|
|
16064
|
+
if (trackType !== 'videoTrack')
|
|
16065
|
+
return;
|
|
16066
|
+
return createFirstVideoFrameDetector(videoElement, () => {
|
|
16067
|
+
this.reportFirstRenderedVideoFrame(sessionId);
|
|
16068
|
+
});
|
|
16069
|
+
};
|
|
16070
|
+
this.reportFirstRenderedVideoFrame = (sessionId) => {
|
|
16071
|
+
const participant = this.state.findParticipantBySessionId(sessionId);
|
|
16072
|
+
if (participant?.isLocalParticipant)
|
|
16073
|
+
return;
|
|
16074
|
+
const trackId = participant?.videoStream?.getVideoTracks()[0]?.id;
|
|
16075
|
+
if (!trackId)
|
|
16076
|
+
return;
|
|
16077
|
+
this.clientEventReporter.reportFirstFrame(this.cid, TrackType.VIDEO, trackId);
|
|
16078
|
+
};
|
|
15943
16079
|
/**
|
|
15944
16080
|
* Binds a DOM <audio> element to the given session id.
|
|
15945
16081
|
*
|
|
@@ -16089,6 +16225,7 @@ class Call {
|
|
|
16089
16225
|
this.ringingSubject = new BehaviorSubject(ringing);
|
|
16090
16226
|
this.watching = watching;
|
|
16091
16227
|
this.streamClient = streamClient;
|
|
16228
|
+
this.clientEventReporter = clientEventReporter;
|
|
16092
16229
|
this.clientStore = clientStore;
|
|
16093
16230
|
this.streamClientBasePath = `/call/${this.type}/${this.id}`;
|
|
16094
16231
|
this.logger = videoLoggerSystem.getLogger('Call');
|
|
@@ -17311,10 +17448,12 @@ class StreamClient {
|
|
|
17311
17448
|
this.logger.info('StreamClient.connect: this.wsConnection.connect()');
|
|
17312
17449
|
return await this.wsConnection.connect(this.defaultWSTimeout);
|
|
17313
17450
|
};
|
|
17451
|
+
this.getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
|
|
17452
|
+
"1.53.0";
|
|
17314
17453
|
this.getUserAgent = () => {
|
|
17315
17454
|
if (!this.cachedUserAgent) {
|
|
17316
17455
|
const { clientAppIdentifier = {} } = this.options;
|
|
17317
|
-
const { sdkName = 'js', sdkVersion = "1.
|
|
17456
|
+
const { sdkName = 'js', sdkVersion = "1.53.0", ...extras } = clientAppIdentifier;
|
|
17318
17457
|
this.cachedUserAgent = [
|
|
17319
17458
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
17320
17459
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|
|
@@ -17491,6 +17630,602 @@ const createTokenOrProvider = (options) => {
|
|
|
17491
17630
|
return token || tokenProvider;
|
|
17492
17631
|
};
|
|
17493
17632
|
|
|
17633
|
+
const pcKey = (cid, role) => `${cid}:${role}`;
|
|
17634
|
+
class ClientEventReporter {
|
|
17635
|
+
constructor(options) {
|
|
17636
|
+
this.logger = videoLoggerSystem.getLogger('ClientEventReporter');
|
|
17637
|
+
this.callContexts = new Map();
|
|
17638
|
+
this.joinAttemptIds = new Map();
|
|
17639
|
+
this.joinReasons = new Map();
|
|
17640
|
+
this.coordinatorPairs = new Map();
|
|
17641
|
+
this.wsPairs = new Map();
|
|
17642
|
+
this.peerConnectionPairs = new Map();
|
|
17643
|
+
this.pcEverConnected = new Map();
|
|
17644
|
+
this.firstFrameReported = new Set();
|
|
17645
|
+
/**
|
|
17646
|
+
* Starts a new coordinator connection correlation scope.
|
|
17647
|
+
*
|
|
17648
|
+
* @param userId the id of the user being connected. Captured here because
|
|
17649
|
+
* the `CoordinatorWS` stage emits before the connection flow assigns
|
|
17650
|
+
* the user to the client, so it can't be read from the client yet.
|
|
17651
|
+
*/
|
|
17652
|
+
this.startCoordinatorConnection = (userId) => {
|
|
17653
|
+
this.coordinatorConnectId = generateUUIDv4();
|
|
17654
|
+
this.coordinatorConnectUserId = userId;
|
|
17655
|
+
return this.coordinatorConnectId;
|
|
17656
|
+
};
|
|
17657
|
+
this.trackCoordinatorWs = async (op) => {
|
|
17658
|
+
this.beginCoordinatorWs();
|
|
17659
|
+
try {
|
|
17660
|
+
const result = await op();
|
|
17661
|
+
this.succeedCoordinatorWs();
|
|
17662
|
+
return result;
|
|
17663
|
+
}
|
|
17664
|
+
catch (err) {
|
|
17665
|
+
applyError(this.coordinatorWsPair, mapCoordinatorWsError(err));
|
|
17666
|
+
throw err;
|
|
17667
|
+
}
|
|
17668
|
+
};
|
|
17669
|
+
this.beginCoordinatorWs = () => {
|
|
17670
|
+
if (!this.coordinatorWsPair) {
|
|
17671
|
+
this.coordinatorWsPair = {
|
|
17672
|
+
sid: generateUUIDv4(),
|
|
17673
|
+
attempts: 0,
|
|
17674
|
+
startedAt: Date.now(),
|
|
17675
|
+
userIdSnapshot: this.coordinatorConnectUserId,
|
|
17676
|
+
};
|
|
17677
|
+
this.send({
|
|
17678
|
+
...this.buildCoordinatorWsCommon(this.coordinatorWsPair),
|
|
17679
|
+
event_type: 'initiated',
|
|
17680
|
+
});
|
|
17681
|
+
}
|
|
17682
|
+
this.coordinatorWsPair.attempts++;
|
|
17683
|
+
};
|
|
17684
|
+
this.succeedCoordinatorWs = () => {
|
|
17685
|
+
const pair = this.coordinatorWsPair;
|
|
17686
|
+
if (!pair)
|
|
17687
|
+
return;
|
|
17688
|
+
this.send({
|
|
17689
|
+
...this.buildCoordinatorWsCommon(pair),
|
|
17690
|
+
event_type: 'completed',
|
|
17691
|
+
outcome: 'success',
|
|
17692
|
+
retry_count_attempt: pair.attempts - 1,
|
|
17693
|
+
elapsed_time: Date.now() - pair.startedAt,
|
|
17694
|
+
});
|
|
17695
|
+
this.coordinatorWsPair = undefined;
|
|
17696
|
+
};
|
|
17697
|
+
this.closeCoordinatorWs = () => {
|
|
17698
|
+
const pair = this.coordinatorWsPair;
|
|
17699
|
+
if (!pair || !pair.lastError) {
|
|
17700
|
+
this.coordinatorWsPair = undefined;
|
|
17701
|
+
return;
|
|
17702
|
+
}
|
|
17703
|
+
const { reason, code } = pair.lastError;
|
|
17704
|
+
this.send({
|
|
17705
|
+
...this.buildCoordinatorWsCommon(pair),
|
|
17706
|
+
event_type: 'completed',
|
|
17707
|
+
outcome: 'failure',
|
|
17708
|
+
retry_count_attempt: pair.attempts - 1,
|
|
17709
|
+
elapsed_time: Date.now() - pair.startedAt,
|
|
17710
|
+
retry_failure_reason: reason,
|
|
17711
|
+
retry_failure_code: code,
|
|
17712
|
+
});
|
|
17713
|
+
this.coordinatorWsPair = undefined;
|
|
17714
|
+
};
|
|
17715
|
+
this.buildCoordinatorWsCommon = (pair) => ({
|
|
17716
|
+
user_id: pair.userIdSnapshot ?? this.streamClient.userID,
|
|
17717
|
+
stage: 'CoordinatorWS',
|
|
17718
|
+
stage_id: pair.sid,
|
|
17719
|
+
...(this.coordinatorConnectId && {
|
|
17720
|
+
coordinator_connect_id: this.coordinatorConnectId,
|
|
17721
|
+
}),
|
|
17722
|
+
timestamp: new Date().toISOString(),
|
|
17723
|
+
user_agent: this.streamClient.getUserAgent(),
|
|
17724
|
+
sdk_version: this.streamClient.getSdkVersion(),
|
|
17725
|
+
});
|
|
17726
|
+
this.emitMediaPermission = (cid) => {
|
|
17727
|
+
if (isReactNative() || !this.callContexts.has(cid))
|
|
17728
|
+
return;
|
|
17729
|
+
const pair = {
|
|
17730
|
+
sid: generateUUIDv4(),
|
|
17731
|
+
attempts: 0,
|
|
17732
|
+
startedAt: Date.now(),
|
|
17733
|
+
joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
|
|
17734
|
+
};
|
|
17735
|
+
this.send({
|
|
17736
|
+
...this.buildCommon(cid, 'MediaDevicePermission', pair),
|
|
17737
|
+
...this.sessionIdField(cid),
|
|
17738
|
+
microphone_permission_status: readPermissionStatus(getAudioBrowserPermission()),
|
|
17739
|
+
camera_permission_status: readPermissionStatus(getVideoBrowserPermission()),
|
|
17740
|
+
event_type: 'initiated',
|
|
17741
|
+
});
|
|
17742
|
+
};
|
|
17743
|
+
this.registerCall = (cid, ctx) => {
|
|
17744
|
+
this.callContexts.set(cid, ctx);
|
|
17745
|
+
};
|
|
17746
|
+
this.unregisterCall = (cid) => {
|
|
17747
|
+
this.callContexts.delete(cid);
|
|
17748
|
+
this.joinAttemptIds.delete(cid);
|
|
17749
|
+
this.joinReasons.delete(cid);
|
|
17750
|
+
this.coordinatorPairs.delete(cid);
|
|
17751
|
+
this.wsPairs.delete(cid);
|
|
17752
|
+
this.firstFrameReported.delete(`${cid}:FirstVideoFrame`);
|
|
17753
|
+
this.firstFrameReported.delete(`${cid}:FirstAudioFrame`);
|
|
17754
|
+
for (const role of ['publish', 'subscribe']) {
|
|
17755
|
+
const key = pcKey(cid, role);
|
|
17756
|
+
this.peerConnectionPairs.delete(key);
|
|
17757
|
+
this.pcEverConnected.delete(key);
|
|
17758
|
+
}
|
|
17759
|
+
};
|
|
17760
|
+
this.startCorrelation = (cid, joinReason) => {
|
|
17761
|
+
try {
|
|
17762
|
+
this.closeCallPairs(cid);
|
|
17763
|
+
this.joinAttemptIds.set(cid, generateUUIDv4());
|
|
17764
|
+
this.joinReasons.set(cid, joinReason);
|
|
17765
|
+
this.firstFrameReported.delete(`${cid}:FirstVideoFrame`);
|
|
17766
|
+
this.firstFrameReported.delete(`${cid}:FirstAudioFrame`);
|
|
17767
|
+
this.emitJoinInitiated(cid);
|
|
17768
|
+
this.emitMediaPermission(cid);
|
|
17769
|
+
}
|
|
17770
|
+
catch (err) {
|
|
17771
|
+
this.logger.warn('Failed to start join correlation', err);
|
|
17772
|
+
}
|
|
17773
|
+
};
|
|
17774
|
+
this.withJoinLifecycle = async (cid, joinReason, op) => {
|
|
17775
|
+
this.startCorrelation(cid, joinReason);
|
|
17776
|
+
try {
|
|
17777
|
+
return await op();
|
|
17778
|
+
}
|
|
17779
|
+
catch (err) {
|
|
17780
|
+
this.closeCallPairs(cid);
|
|
17781
|
+
throw err;
|
|
17782
|
+
}
|
|
17783
|
+
};
|
|
17784
|
+
this.track = async (cid, stage, op) => {
|
|
17785
|
+
this.beginAttempt(cid, stage);
|
|
17786
|
+
try {
|
|
17787
|
+
const result = await op();
|
|
17788
|
+
this.succeedAttempt(cid, stage);
|
|
17789
|
+
return result;
|
|
17790
|
+
}
|
|
17791
|
+
catch (err) {
|
|
17792
|
+
this.applyStageError(cid, stage, err);
|
|
17793
|
+
throw err;
|
|
17794
|
+
}
|
|
17795
|
+
};
|
|
17796
|
+
this.reportFirstFrame = (cid, trackType, trackId) => {
|
|
17797
|
+
const stage = trackType === TrackType.VIDEO
|
|
17798
|
+
? 'FirstVideoFrame'
|
|
17799
|
+
: trackType === TrackType.AUDIO
|
|
17800
|
+
? 'FirstAudioFrame'
|
|
17801
|
+
: undefined;
|
|
17802
|
+
if (!stage)
|
|
17803
|
+
return;
|
|
17804
|
+
const key = `${cid}:${stage}`;
|
|
17805
|
+
if (this.firstFrameReported.has(key))
|
|
17806
|
+
return;
|
|
17807
|
+
this.firstFrameReported.add(key);
|
|
17808
|
+
const pair = {
|
|
17809
|
+
sid: generateUUIDv4(),
|
|
17810
|
+
attempts: 0,
|
|
17811
|
+
startedAt: Date.now(),
|
|
17812
|
+
joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
|
|
17813
|
+
};
|
|
17814
|
+
const resolvedSfuId = this.getSfuId(cid);
|
|
17815
|
+
this.send({
|
|
17816
|
+
...this.buildCommon(cid, stage, pair),
|
|
17817
|
+
...this.sessionIdField(cid),
|
|
17818
|
+
...(resolvedSfuId && { sfu_id: resolvedSfuId }),
|
|
17819
|
+
track_id: trackId,
|
|
17820
|
+
event_type: 'initiated',
|
|
17821
|
+
});
|
|
17822
|
+
};
|
|
17823
|
+
this.captureWsError = (cid, opts) => {
|
|
17824
|
+
const pair = this.wsPairs.get(cid);
|
|
17825
|
+
if (!pair)
|
|
17826
|
+
return;
|
|
17827
|
+
applyError(pair, { reason: opts.reason, code: opts.code });
|
|
17828
|
+
};
|
|
17829
|
+
this.close = (cid) => {
|
|
17830
|
+
this.closeCallPairs(cid);
|
|
17831
|
+
};
|
|
17832
|
+
this.abort = (cid, opts) => {
|
|
17833
|
+
try {
|
|
17834
|
+
const { code, reason } = opts;
|
|
17835
|
+
const stageError = { code, reason };
|
|
17836
|
+
applyErrorIfAbsent(this.coordinatorPairs.get(cid), stageError);
|
|
17837
|
+
applyErrorIfAbsent(this.wsPairs.get(cid), stageError);
|
|
17838
|
+
this.failCoordinator(cid);
|
|
17839
|
+
this.failWs(cid);
|
|
17840
|
+
this.emitPeerConnectionFailure(cid, 'publish', code, reason, 'NOT_CONNECTED');
|
|
17841
|
+
this.emitPeerConnectionFailure(cid, 'subscribe', code, reason, 'NOT_CONNECTED');
|
|
17842
|
+
}
|
|
17843
|
+
catch (err) {
|
|
17844
|
+
this.logger.warn('Failed to report abort', err);
|
|
17845
|
+
}
|
|
17846
|
+
};
|
|
17847
|
+
this.closeCallPairs = (cid) => {
|
|
17848
|
+
if (this.coordinatorPairs.get(cid))
|
|
17849
|
+
this.failCoordinator(cid);
|
|
17850
|
+
if (this.wsPairs.get(cid))
|
|
17851
|
+
this.failWs(cid);
|
|
17852
|
+
for (const role of ['publish', 'subscribe']) {
|
|
17853
|
+
this.emitPeerConnectionFailure(cid, role, 'CLIENT_ABORTED', 'superseded by a new join attempt', 'NOT_CONNECTED');
|
|
17854
|
+
}
|
|
17855
|
+
};
|
|
17856
|
+
this.emitJoinInitiated = (cid) => {
|
|
17857
|
+
const joinAttemptId = this.joinAttemptIds.get(cid);
|
|
17858
|
+
if (!joinAttemptId)
|
|
17859
|
+
return;
|
|
17860
|
+
const coordinatorConnectId = this.coordinatorConnectId;
|
|
17861
|
+
this.send({
|
|
17862
|
+
user_id: this.streamClient.userID,
|
|
17863
|
+
stage: 'JoinInitiated',
|
|
17864
|
+
join_attempt_id: joinAttemptId,
|
|
17865
|
+
...(coordinatorConnectId && {
|
|
17866
|
+
coordinator_connect_id: coordinatorConnectId,
|
|
17867
|
+
}),
|
|
17868
|
+
timestamp: new Date().toISOString(),
|
|
17869
|
+
user_agent: this.streamClient.getUserAgent(),
|
|
17870
|
+
sdk_version: this.streamClient.getSdkVersion(),
|
|
17871
|
+
event_type: 'initiated',
|
|
17872
|
+
});
|
|
17873
|
+
};
|
|
17874
|
+
this.beginAttempt = (cid, stage) => {
|
|
17875
|
+
if (stage === 'CoordinatorJoin')
|
|
17876
|
+
this.beginCoordinatorAttempt(cid);
|
|
17877
|
+
else
|
|
17878
|
+
this.beginWsAttempt(cid);
|
|
17879
|
+
};
|
|
17880
|
+
this.succeedAttempt = (cid, stage) => {
|
|
17881
|
+
if (stage === 'CoordinatorJoin')
|
|
17882
|
+
this.succeedCoordinator(cid);
|
|
17883
|
+
else
|
|
17884
|
+
this.succeedWs(cid);
|
|
17885
|
+
};
|
|
17886
|
+
this.applyStageError = (cid, stage, err) => {
|
|
17887
|
+
const pair = stage === 'CoordinatorJoin'
|
|
17888
|
+
? this.coordinatorPairs.get(cid)
|
|
17889
|
+
: this.wsPairs.get(cid);
|
|
17890
|
+
applyErrorIfAbsent(pair, stage === 'CoordinatorJoin'
|
|
17891
|
+
? mapCoordinatorHttpError(err)
|
|
17892
|
+
: mapWsJoinError(err));
|
|
17893
|
+
};
|
|
17894
|
+
this.beginCoordinatorAttempt = (cid) => {
|
|
17895
|
+
let pair = this.coordinatorPairs.get(cid);
|
|
17896
|
+
if (!pair) {
|
|
17897
|
+
pair = {
|
|
17898
|
+
sid: generateUUIDv4(),
|
|
17899
|
+
attempts: 0,
|
|
17900
|
+
startedAt: Date.now(),
|
|
17901
|
+
joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
|
|
17902
|
+
joinReasonSnapshot: this.joinReasons.get(cid),
|
|
17903
|
+
};
|
|
17904
|
+
this.coordinatorPairs.set(cid, pair);
|
|
17905
|
+
this.send({
|
|
17906
|
+
...this.buildCommon(cid, 'CoordinatorJoin', pair),
|
|
17907
|
+
...(pair.joinReasonSnapshot && {
|
|
17908
|
+
join_reason: pair.joinReasonSnapshot,
|
|
17909
|
+
}),
|
|
17910
|
+
event_type: 'initiated',
|
|
17911
|
+
});
|
|
17912
|
+
}
|
|
17913
|
+
pair.lastError = undefined;
|
|
17914
|
+
pair.attempts++;
|
|
17915
|
+
};
|
|
17916
|
+
this.succeedCoordinator = (cid) => {
|
|
17917
|
+
const pair = this.coordinatorPairs.get(cid);
|
|
17918
|
+
if (!pair)
|
|
17919
|
+
return;
|
|
17920
|
+
this.send({
|
|
17921
|
+
...this.buildCommon(cid, 'CoordinatorJoin', pair),
|
|
17922
|
+
...this.sessionIdField(cid),
|
|
17923
|
+
...(pair.joinReasonSnapshot && { join_reason: pair.joinReasonSnapshot }),
|
|
17924
|
+
event_type: 'completed',
|
|
17925
|
+
outcome: 'success',
|
|
17926
|
+
retry_count_attempt: pair.attempts - 1,
|
|
17927
|
+
elapsed_time: Date.now() - pair.startedAt,
|
|
17928
|
+
});
|
|
17929
|
+
this.coordinatorPairs.delete(cid);
|
|
17930
|
+
};
|
|
17931
|
+
this.failCoordinator = (cid) => {
|
|
17932
|
+
const pair = this.coordinatorPairs.get(cid);
|
|
17933
|
+
if (!pair || !pair.lastError) {
|
|
17934
|
+
this.coordinatorPairs.delete(cid);
|
|
17935
|
+
return;
|
|
17936
|
+
}
|
|
17937
|
+
const { reason, code } = pair.lastError;
|
|
17938
|
+
this.send({
|
|
17939
|
+
...this.buildCommon(cid, 'CoordinatorJoin', pair),
|
|
17940
|
+
...this.sessionIdField(cid),
|
|
17941
|
+
...(pair.joinReasonSnapshot && { join_reason: pair.joinReasonSnapshot }),
|
|
17942
|
+
event_type: 'completed',
|
|
17943
|
+
outcome: 'failure',
|
|
17944
|
+
retry_count_attempt: pair.attempts - 1,
|
|
17945
|
+
elapsed_time: Date.now() - pair.startedAt,
|
|
17946
|
+
retry_failure_reason: reason,
|
|
17947
|
+
retry_failure_code: code,
|
|
17948
|
+
});
|
|
17949
|
+
this.coordinatorPairs.delete(cid);
|
|
17950
|
+
};
|
|
17951
|
+
this.beginWsAttempt = (cid) => {
|
|
17952
|
+
let pair = this.wsPairs.get(cid);
|
|
17953
|
+
if (!pair) {
|
|
17954
|
+
pair = {
|
|
17955
|
+
sid: generateUUIDv4(),
|
|
17956
|
+
attempts: 0,
|
|
17957
|
+
startedAt: Date.now(),
|
|
17958
|
+
joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
|
|
17959
|
+
};
|
|
17960
|
+
this.wsPairs.set(cid, pair);
|
|
17961
|
+
const sfuId = this.getSfuId(cid);
|
|
17962
|
+
this.send({
|
|
17963
|
+
...this.buildCommon(cid, 'WSJoin', pair),
|
|
17964
|
+
...this.sessionIdField(cid),
|
|
17965
|
+
...(sfuId && { sfu_id: sfuId }),
|
|
17966
|
+
event_type: 'initiated',
|
|
17967
|
+
});
|
|
17968
|
+
}
|
|
17969
|
+
pair.lastError = undefined;
|
|
17970
|
+
pair.attempts++;
|
|
17971
|
+
};
|
|
17972
|
+
this.succeedWs = (cid) => {
|
|
17973
|
+
const pair = this.wsPairs.get(cid);
|
|
17974
|
+
if (!pair)
|
|
17975
|
+
return;
|
|
17976
|
+
const sfuId = this.getSfuId(cid);
|
|
17977
|
+
this.send({
|
|
17978
|
+
...this.buildCommon(cid, 'WSJoin', pair),
|
|
17979
|
+
...this.sessionIdField(cid),
|
|
17980
|
+
...(sfuId && { sfu_id: sfuId }),
|
|
17981
|
+
event_type: 'completed',
|
|
17982
|
+
outcome: 'success',
|
|
17983
|
+
retry_count_attempt: pair.attempts - 1,
|
|
17984
|
+
elapsed_time: Date.now() - pair.startedAt,
|
|
17985
|
+
});
|
|
17986
|
+
this.wsPairs.delete(cid);
|
|
17987
|
+
};
|
|
17988
|
+
this.failWs = (cid) => {
|
|
17989
|
+
const pair = this.wsPairs.get(cid);
|
|
17990
|
+
if (!pair || !pair.lastError) {
|
|
17991
|
+
this.wsPairs.delete(cid);
|
|
17992
|
+
return;
|
|
17993
|
+
}
|
|
17994
|
+
const { reason, code } = pair.lastError;
|
|
17995
|
+
const sfuId = this.getSfuId(cid);
|
|
17996
|
+
this.send({
|
|
17997
|
+
...this.buildCommon(cid, 'WSJoin', pair),
|
|
17998
|
+
...this.sessionIdField(cid),
|
|
17999
|
+
event_type: 'completed',
|
|
18000
|
+
outcome: 'failure',
|
|
18001
|
+
retry_count_attempt: pair.attempts - 1,
|
|
18002
|
+
elapsed_time: Date.now() - pair.startedAt,
|
|
18003
|
+
...(sfuId && { sfu_id: sfuId }),
|
|
18004
|
+
retry_failure_reason: reason,
|
|
18005
|
+
retry_failure_code: code,
|
|
18006
|
+
});
|
|
18007
|
+
this.wsPairs.delete(cid);
|
|
18008
|
+
};
|
|
18009
|
+
this.onPeerConnectionStateChange = (cid, event) => {
|
|
18010
|
+
const role = event.peerType === PeerType.SUBSCRIBER ? 'subscribe' : 'publish';
|
|
18011
|
+
if (event.stateType === 'ice' && event.state === 'failed') {
|
|
18012
|
+
this.emitPeerConnectionFailure(cid, role, 'ICE_CONNECTIVITY_FAILED', 'ICE connectivity checks failed', 'FAILED');
|
|
18013
|
+
return;
|
|
18014
|
+
}
|
|
18015
|
+
if (event.stateType === 'peerConnection' && event.state === 'failed') {
|
|
18016
|
+
this.emitPeerConnectionFailure(cid, role, 'DTLS_CONNECTIVITY_FAILED', 'DTLS connectivity checks failed', 'CONNECTED');
|
|
18017
|
+
return;
|
|
18018
|
+
}
|
|
18019
|
+
if (event.stateType !== 'peerConnection')
|
|
18020
|
+
return;
|
|
18021
|
+
switch (event.state) {
|
|
18022
|
+
case 'connecting':
|
|
18023
|
+
if (this.peerConnectionPairs.has(pcKey(cid, role)))
|
|
18024
|
+
return;
|
|
18025
|
+
this.openPeerConnectionPair(cid, role);
|
|
18026
|
+
break;
|
|
18027
|
+
case 'connected':
|
|
18028
|
+
this.emitPeerConnectionSuccess(cid, role);
|
|
18029
|
+
this.pcEverConnected.set(pcKey(cid, role), true);
|
|
18030
|
+
break;
|
|
18031
|
+
}
|
|
18032
|
+
};
|
|
18033
|
+
this.openPeerConnectionPair = (cid, role) => {
|
|
18034
|
+
const key = pcKey(cid, role);
|
|
18035
|
+
const pair = {
|
|
18036
|
+
sid: generateUUIDv4(),
|
|
18037
|
+
attempts: 0,
|
|
18038
|
+
startedAt: Date.now(),
|
|
18039
|
+
joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
|
|
18040
|
+
sfuId: this.getSfuId(cid),
|
|
18041
|
+
userSessionId: this.getUserSessionId(cid),
|
|
18042
|
+
wasPreviouslyConnected: this.pcEverConnected.get(key) === true,
|
|
18043
|
+
};
|
|
18044
|
+
this.peerConnectionPairs.set(key, pair);
|
|
18045
|
+
this.send({
|
|
18046
|
+
...this.buildCommon(cid, 'PeerConnectionConnect', pair),
|
|
18047
|
+
...this.sessionIdField(cid),
|
|
18048
|
+
peer_connection: role,
|
|
18049
|
+
was_previously_connected: pair.wasPreviouslyConnected,
|
|
18050
|
+
...(pair.sfuId && { sfu_id: pair.sfuId }),
|
|
18051
|
+
...(pair.userSessionId && {
|
|
18052
|
+
user_session_id: pair.userSessionId,
|
|
18053
|
+
}),
|
|
18054
|
+
event_type: 'initiated',
|
|
18055
|
+
});
|
|
18056
|
+
};
|
|
18057
|
+
this.emitPeerConnectionSuccess = (cid, role) => {
|
|
18058
|
+
const key = pcKey(cid, role);
|
|
18059
|
+
const pair = this.peerConnectionPairs.get(key);
|
|
18060
|
+
if (!pair)
|
|
18061
|
+
return;
|
|
18062
|
+
this.send({
|
|
18063
|
+
...this.buildCommon(cid, 'PeerConnectionConnect', pair),
|
|
18064
|
+
...this.sessionIdField(cid),
|
|
18065
|
+
peer_connection: role,
|
|
18066
|
+
was_previously_connected: pair.wasPreviouslyConnected,
|
|
18067
|
+
...(pair.sfuId && { sfu_id: pair.sfuId }),
|
|
18068
|
+
...(pair.userSessionId && {
|
|
18069
|
+
user_session_id: pair.userSessionId,
|
|
18070
|
+
}),
|
|
18071
|
+
event_type: 'completed',
|
|
18072
|
+
outcome: 'success',
|
|
18073
|
+
retry_count_attempt: 0,
|
|
18074
|
+
elapsed_time: Date.now() - pair.startedAt,
|
|
18075
|
+
});
|
|
18076
|
+
this.peerConnectionPairs.delete(key);
|
|
18077
|
+
};
|
|
18078
|
+
this.emitPeerConnectionFailure = (cid, role, code, reason, iceState) => {
|
|
18079
|
+
const key = pcKey(cid, role);
|
|
18080
|
+
const pair = this.peerConnectionPairs.get(key);
|
|
18081
|
+
if (!pair)
|
|
18082
|
+
return;
|
|
18083
|
+
this.send({
|
|
18084
|
+
...this.buildCommon(cid, 'PeerConnectionConnect', pair),
|
|
18085
|
+
...this.sessionIdField(cid),
|
|
18086
|
+
peer_connection: role,
|
|
18087
|
+
was_previously_connected: pair.wasPreviouslyConnected,
|
|
18088
|
+
...(pair.userSessionId && {
|
|
18089
|
+
user_session_id: pair.userSessionId,
|
|
18090
|
+
}),
|
|
18091
|
+
...(pair.sfuId && { sfu_id: pair.sfuId }),
|
|
18092
|
+
event_type: 'completed',
|
|
18093
|
+
outcome: 'failure',
|
|
18094
|
+
retry_count_attempt: 0,
|
|
18095
|
+
elapsed_time: Date.now() - pair.startedAt,
|
|
18096
|
+
ice_state: iceState,
|
|
18097
|
+
retry_failure_reason: reason,
|
|
18098
|
+
retry_failure_code: code,
|
|
18099
|
+
});
|
|
18100
|
+
this.peerConnectionPairs.delete(key);
|
|
18101
|
+
};
|
|
18102
|
+
this.getSfuId = (cid) => this.callContexts.get(cid)?.getSfuId() ?? '';
|
|
18103
|
+
this.getUserSessionId = (cid) => this.callContexts.get(cid)?.getUserSessionId() ?? '';
|
|
18104
|
+
this.sessionIdField = (cid) => {
|
|
18105
|
+
const callSessionId = this.callContexts.get(cid)?.getCallSessionId() ?? '';
|
|
18106
|
+
return callSessionId ? { call_session_id: callSessionId } : {};
|
|
18107
|
+
};
|
|
18108
|
+
this.buildCommon = (cid, stage, pair) => {
|
|
18109
|
+
const ctx = this.callContexts.get(cid);
|
|
18110
|
+
const coordinatorConnectId = this.coordinatorConnectId;
|
|
18111
|
+
return {
|
|
18112
|
+
user_id: this.streamClient.userID,
|
|
18113
|
+
type: ctx?.callType ?? '',
|
|
18114
|
+
id: ctx?.callId ?? '',
|
|
18115
|
+
call_cid: cid,
|
|
18116
|
+
stage,
|
|
18117
|
+
stage_id: pair.sid,
|
|
18118
|
+
...(pair.joinAttemptIdSnapshot && {
|
|
18119
|
+
join_attempt_id: pair.joinAttemptIdSnapshot,
|
|
18120
|
+
}),
|
|
18121
|
+
...(coordinatorConnectId && {
|
|
18122
|
+
coordinator_connect_id: coordinatorConnectId,
|
|
18123
|
+
}),
|
|
18124
|
+
timestamp: new Date().toISOString(),
|
|
18125
|
+
user_agent: this.streamClient.getUserAgent(),
|
|
18126
|
+
sdk_version: this.streamClient.getSdkVersion(),
|
|
18127
|
+
};
|
|
18128
|
+
};
|
|
18129
|
+
this.send = (body) => {
|
|
18130
|
+
void this.sendWithRetry(body);
|
|
18131
|
+
};
|
|
18132
|
+
this.sendWithRetry = async (body) => {
|
|
18133
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
18134
|
+
try {
|
|
18135
|
+
await this.streamClient.doAxiosRequest('post', '/call_client_event', { events: [body] }, { publicEndpoint: true });
|
|
18136
|
+
return true;
|
|
18137
|
+
}
|
|
18138
|
+
catch (err) {
|
|
18139
|
+
const status = err?.response
|
|
18140
|
+
?.status;
|
|
18141
|
+
if (typeof status === 'number' && status >= 400 && status < 500) {
|
|
18142
|
+
this.logger.debug(`Client event rejected (${status}), not retrying`, body.stage, body.event_type);
|
|
18143
|
+
return false;
|
|
18144
|
+
}
|
|
18145
|
+
if (attempt === 4) {
|
|
18146
|
+
this.logger.debug('Client event delivery failed after retries', body.stage, body.event_type, err);
|
|
18147
|
+
return false;
|
|
18148
|
+
}
|
|
18149
|
+
await sleep(retryInterval(attempt));
|
|
18150
|
+
}
|
|
18151
|
+
}
|
|
18152
|
+
return false;
|
|
18153
|
+
};
|
|
18154
|
+
this.streamClient = options.streamClient;
|
|
18155
|
+
}
|
|
18156
|
+
}
|
|
18157
|
+
const readPermissionStatus = (permission) => {
|
|
18158
|
+
const state = getCurrentValue(permission.asStateObservable());
|
|
18159
|
+
switch (state) {
|
|
18160
|
+
case 'granted':
|
|
18161
|
+
return 'GRANTED';
|
|
18162
|
+
case 'denied':
|
|
18163
|
+
return 'FAILED';
|
|
18164
|
+
case 'prompting':
|
|
18165
|
+
return 'INITIATED';
|
|
18166
|
+
case 'prompt':
|
|
18167
|
+
default:
|
|
18168
|
+
return 'NOT_INITIATED';
|
|
18169
|
+
}
|
|
18170
|
+
};
|
|
18171
|
+
const errorMessage = (err) => err instanceof Error ? err.message : String(err);
|
|
18172
|
+
const applyError = (pair, next) => {
|
|
18173
|
+
if (!pair)
|
|
18174
|
+
return;
|
|
18175
|
+
pair.lastError = next;
|
|
18176
|
+
};
|
|
18177
|
+
const applyErrorIfAbsent = (pair, next) => {
|
|
18178
|
+
if (!pair || pair.lastError)
|
|
18179
|
+
return;
|
|
18180
|
+
pair.lastError = next;
|
|
18181
|
+
};
|
|
18182
|
+
const mapCoordinatorHttpError = (err) => {
|
|
18183
|
+
if (err instanceof ErrorFromResponse) {
|
|
18184
|
+
return {
|
|
18185
|
+
reason: err.message,
|
|
18186
|
+
code: err.code != null ? String(err.code) : 'SERVER_ERROR',
|
|
18187
|
+
};
|
|
18188
|
+
}
|
|
18189
|
+
return { reason: errorMessage(err), code: 'SERVER_ERROR' };
|
|
18190
|
+
};
|
|
18191
|
+
const mapCoordinatorWsError = (err) => {
|
|
18192
|
+
if (err instanceof ErrorFromResponse) {
|
|
18193
|
+
return {
|
|
18194
|
+
reason: err.message,
|
|
18195
|
+
code: err.code != null ? String(err.code) : 'SERVER_ERROR',
|
|
18196
|
+
};
|
|
18197
|
+
}
|
|
18198
|
+
if (err instanceof Error) {
|
|
18199
|
+
try {
|
|
18200
|
+
const parsed = JSON.parse(err.message);
|
|
18201
|
+
if (typeof parsed.isWSFailure === 'boolean') {
|
|
18202
|
+
return {
|
|
18203
|
+
reason: parsed.message || err.message,
|
|
18204
|
+
code: !parsed.isWSFailure && parsed.code
|
|
18205
|
+
? String(parsed.code)
|
|
18206
|
+
: 'SERVER_ERROR',
|
|
18207
|
+
};
|
|
18208
|
+
}
|
|
18209
|
+
}
|
|
18210
|
+
catch { }
|
|
18211
|
+
}
|
|
18212
|
+
return { reason: errorMessage(err), code: 'SERVER_ERROR' };
|
|
18213
|
+
};
|
|
18214
|
+
const mapWsJoinError = (err) => {
|
|
18215
|
+
if (err instanceof SfuJoinError) {
|
|
18216
|
+
const sfuError = err.errorEvent.error;
|
|
18217
|
+
return {
|
|
18218
|
+
reason: sfuError?.message || err.message,
|
|
18219
|
+
code: (sfuError && ErrorCode[sfuError.code]) || 'SFU_ERROR',
|
|
18220
|
+
};
|
|
18221
|
+
}
|
|
18222
|
+
const reason = errorMessage(err);
|
|
18223
|
+
if (err instanceof SfuTimeoutError) {
|
|
18224
|
+
return { reason, code: 'REQUEST_TIMEOUT' };
|
|
18225
|
+
}
|
|
18226
|
+
return { reason, code: 'SFU_ERROR' };
|
|
18227
|
+
};
|
|
18228
|
+
|
|
17494
18229
|
/**
|
|
17495
18230
|
* A `StreamVideoClient` instance lets you communicate with our API, and authenticate users.
|
|
17496
18231
|
*/
|
|
@@ -17554,6 +18289,7 @@ class StreamVideoClient {
|
|
|
17554
18289
|
}
|
|
17555
18290
|
call = new Call({
|
|
17556
18291
|
streamClient: this.streamClient,
|
|
18292
|
+
clientEventReporter: this.clientEventReporter,
|
|
17557
18293
|
type: e.call.type,
|
|
17558
18294
|
id: e.call.id,
|
|
17559
18295
|
members: e.members,
|
|
@@ -17623,6 +18359,8 @@ class StreamVideoClient {
|
|
|
17623
18359
|
user.id = '!anon';
|
|
17624
18360
|
return this.connectAnonymousUser(user, tokenOrProvider);
|
|
17625
18361
|
}
|
|
18362
|
+
const reporter = this.clientEventReporter;
|
|
18363
|
+
reporter.startCoordinatorConnection(user.id);
|
|
17626
18364
|
const connectUserResponse = await withoutConcurrency(this.connectionConcurrencyTag, async () => {
|
|
17627
18365
|
const client = this.streamClient;
|
|
17628
18366
|
const { onConnectUserError, persistUserOnConnectionFailure } = client.options;
|
|
@@ -17632,14 +18370,15 @@ class StreamVideoClient {
|
|
|
17632
18370
|
for (let attempt = 0; attempt < maxConnectUserRetries; attempt++) {
|
|
17633
18371
|
try {
|
|
17634
18372
|
this.logger.trace(`Connecting user (${attempt})`, user);
|
|
17635
|
-
return user.type === 'guest'
|
|
17636
|
-
?
|
|
17637
|
-
:
|
|
18373
|
+
return await reporter.trackCoordinatorWs(() => user.type === 'guest'
|
|
18374
|
+
? client.connectGuestUser(user)
|
|
18375
|
+
: client.connectUser(user, tokenOrProvider));
|
|
17638
18376
|
}
|
|
17639
18377
|
catch (err) {
|
|
17640
18378
|
this.logger.warn(`Failed to connect a user (${attempt})`, err);
|
|
17641
18379
|
errorQueue.push(err);
|
|
17642
18380
|
if (attempt === maxConnectUserRetries - 1) {
|
|
18381
|
+
reporter.closeCoordinatorWs();
|
|
17643
18382
|
onConnectUserError?.(err, errorQueue);
|
|
17644
18383
|
throw err;
|
|
17645
18384
|
}
|
|
@@ -17717,6 +18456,7 @@ class StreamVideoClient {
|
|
|
17717
18456
|
return (call ??
|
|
17718
18457
|
new Call({
|
|
17719
18458
|
streamClient: this.streamClient,
|
|
18459
|
+
clientEventReporter: this.clientEventReporter,
|
|
17720
18460
|
id: id,
|
|
17721
18461
|
type: type,
|
|
17722
18462
|
clientStore: this.writeableStateStore,
|
|
@@ -17741,6 +18481,7 @@ class StreamVideoClient {
|
|
|
17741
18481
|
for (const c of response.calls) {
|
|
17742
18482
|
const call = new Call({
|
|
17743
18483
|
streamClient: this.streamClient,
|
|
18484
|
+
clientEventReporter: this.clientEventReporter,
|
|
17744
18485
|
id: c.call.id,
|
|
17745
18486
|
type: c.call.type,
|
|
17746
18487
|
members: c.members,
|
|
@@ -17848,6 +18589,7 @@ class StreamVideoClient {
|
|
|
17848
18589
|
const [callType, callId] = call_cid.split(':');
|
|
17849
18590
|
call = new Call({
|
|
17850
18591
|
streamClient: this.streamClient,
|
|
18592
|
+
clientEventReporter: this.clientEventReporter,
|
|
17851
18593
|
type: callType,
|
|
17852
18594
|
id: callId,
|
|
17853
18595
|
clientStore: this.writeableStateStore,
|
|
@@ -17888,6 +18630,9 @@ class StreamVideoClient {
|
|
|
17888
18630
|
this.logger = videoLoggerSystem.getLogger('client');
|
|
17889
18631
|
this.rejectCallWhenBusy = clientOptions?.rejectCallWhenBusy ?? false;
|
|
17890
18632
|
this.streamClient = createCoordinatorClient(apiKey, clientOptions);
|
|
18633
|
+
this.clientEventReporter = new ClientEventReporter({
|
|
18634
|
+
streamClient: this.streamClient,
|
|
18635
|
+
});
|
|
17891
18636
|
this.writeableStateStore = new StreamVideoWriteableStateStore();
|
|
17892
18637
|
this.readOnlyStateStore = new StreamVideoReadOnlyStateStore(this.writeableStateStore);
|
|
17893
18638
|
if (typeof apiKeyOrArgs !== 'string' && apiKeyOrArgs.user) {
|
|
@@ -17950,5 +18695,5 @@ const humanize = (n) => {
|
|
|
17950
18695
|
return String(n);
|
|
17951
18696
|
};
|
|
17952
18697
|
|
|
17953
|
-
export { AudioSettingsRequestDefaultDeviceEnum, AudioSettingsResponseDefaultDeviceEnum, browsers as Browsers, Call, CallRecordingFailedEventRecordingTypeEnum, CallRecordingReadyEventRecordingTypeEnum, CallRecordingStartedEventRecordingTypeEnum, CallRecordingStoppedEventRecordingTypeEnum, CallState, CallType, CallTypes, CallingState, CameraManager, CameraManagerState, CreateDeviceRequestPushProviderEnum, DebounceType, DeviceManager, DeviceManagerState, DynascaleManager, ErrorFromResponse, FrameRecordingSettingsRequestModeEnum, FrameRecordingSettingsRequestQualityEnum, FrameRecordingSettingsResponseModeEnum, IndividualRecordingSettingsRequestModeEnum, IndividualRecordingSettingsResponseModeEnum, IngressAudioEncodingOptionsRequestChannelsEnum, IngressSourceRequestFpsEnum, IngressVideoLayerRequestCodecEnum, LayoutSettingsRequestNameEnum, MicrophoneManager, MicrophoneManagerState, NoiseCancellationSettingsModeEnum, OwnCapability, RTMPBroadcastRequestQualityEnum, RTMPSettingsRequestQualityEnum, RawRecordingSettingsRequestModeEnum, RawRecordingSettingsResponseModeEnum, RecordSettingsRequestModeEnum, RecordSettingsRequestQualityEnum, rxUtils as RxUtils, ScreenShareManager, ScreenShareState, events as SfuEvents, SfuJoinError, models as SfuModels, SpeakerManager, SpeakerState, StartClosedCaptionsRequestLanguageEnum, StartTranscriptionRequestLanguageEnum, StreamSfuClient, StreamVideoClient, StreamVideoReadOnlyStateStore, StreamVideoWriteableStateStore, TranscriptionSettingsRequestClosedCaptionModeEnum, TranscriptionSettingsRequestLanguageEnum, TranscriptionSettingsRequestModeEnum, TranscriptionSettingsResponseClosedCaptionModeEnum, TranscriptionSettingsResponseLanguageEnum, TranscriptionSettingsResponseModeEnum, VideoSettingsRequestCameraFacingEnum, VideoSettingsResponseCameraFacingEnum, ViewportTracker, VisibilityState, checkIfAudioOutputChangeSupported, combineComparators, conditional, createSoundDetector, defaultSortPreset, descending, deviceIds$, disposeOfMediaStream, dominantSpeaker, getAudioBrowserPermission, getAudioDevices, getAudioOutputDevices, getAudioStream, getClientDetails, getDeviceState, getScreenShareStream, getSdkInfo, getVideoBrowserPermission, getVideoDevices, getVideoStream, getWebRTCInfo, hasAudio$1 as hasAudio, hasInterruptedTrack, hasPausedTrack, hasScreenShare, hasScreenShareAudio, hasVideo, humanize, isPinned, livestreamOrAudioRoomSortPreset, logToConsole, name, noopComparator, paginatedLayoutSortPreset, pinned, publishingAudio, publishingVideo, reactionType, resolveDeviceId, role, screenSharing, setDeviceInfo, setOSInfo, setPowerState, setSdkInfo, setThermalState, setWebRTCInfo, speakerLayoutSortPreset, speaking, videoLoggerSystem, withParticipantSource };
|
|
18698
|
+
export { AudioSettingsRequestDefaultDeviceEnum, AudioSettingsResponseDefaultDeviceEnum, browsers as Browsers, Call, CallRecordingFailedEventRecordingTypeEnum, CallRecordingReadyEventRecordingTypeEnum, CallRecordingStartedEventRecordingTypeEnum, CallRecordingStoppedEventRecordingTypeEnum, CallState, CallType, CallTypes, CallingState, CameraManager, CameraManagerState, CreateDeviceRequestPushProviderEnum, DebounceType, DeviceManager, DeviceManagerState, DynascaleManager, ErrorFromResponse, FrameRecordingSettingsRequestModeEnum, FrameRecordingSettingsRequestQualityEnum, FrameRecordingSettingsResponseModeEnum, IndividualRecordingSettingsRequestModeEnum, IndividualRecordingSettingsResponseModeEnum, IngressAudioEncodingOptionsRequestChannelsEnum, IngressSourceRequestFpsEnum, IngressVideoLayerRequestCodecEnum, LayoutSettingsRequestNameEnum, MicrophoneManager, MicrophoneManagerState, NoiseCancellationSettingsModeEnum, OwnCapability, RTMPBroadcastRequestQualityEnum, RTMPSettingsRequestQualityEnum, RawRecordingSettingsRequestModeEnum, RawRecordingSettingsResponseModeEnum, RecordSettingsRequestModeEnum, RecordSettingsRequestQualityEnum, rxUtils as RxUtils, ScreenShareManager, ScreenShareState, events as SfuEvents, SfuJoinError, models as SfuModels, SfuTimeoutError, SpeakerManager, SpeakerState, StartClosedCaptionsRequestLanguageEnum, StartTranscriptionRequestLanguageEnum, StreamSfuClient, StreamVideoClient, StreamVideoReadOnlyStateStore, StreamVideoWriteableStateStore, TranscriptionSettingsRequestClosedCaptionModeEnum, TranscriptionSettingsRequestLanguageEnum, TranscriptionSettingsRequestModeEnum, TranscriptionSettingsResponseClosedCaptionModeEnum, TranscriptionSettingsResponseLanguageEnum, TranscriptionSettingsResponseModeEnum, VideoSettingsRequestCameraFacingEnum, VideoSettingsResponseCameraFacingEnum, ViewportTracker, VisibilityState, checkIfAudioOutputChangeSupported, combineComparators, conditional, createSoundDetector, defaultSortPreset, descending, deviceIds$, disposeOfMediaStream, dominantSpeaker, getAudioBrowserPermission, getAudioDevices, getAudioOutputDevices, getAudioStream, getClientDetails, getDeviceState, getScreenShareStream, getSdkInfo, getVideoBrowserPermission, getVideoDevices, getVideoStream, getWebRTCInfo, hasAudio$1 as hasAudio, hasInterruptedTrack, hasPausedTrack, hasScreenShare, hasScreenShareAudio, hasVideo, humanize, isPinned, livestreamOrAudioRoomSortPreset, logToConsole, name, noopComparator, paginatedLayoutSortPreset, pinned, publishingAudio, publishingVideo, reactionType, resolveDeviceId, role, screenSharing, setDeviceInfo, setOSInfo, setPowerState, setSdkInfo, setThermalState, setWebRTCInfo, speakerLayoutSortPreset, speaking, videoLoggerSystem, withParticipantSource };
|
|
17954
18699
|
//# sourceMappingURL=index.es.js.map
|