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