@stream-io/video-client 1.52.1-beta.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 +801 -123
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +801 -122
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +801 -123
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +6 -14
- 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/gen/google/protobuf/struct.d.ts +1 -3
- package/dist/src/gen/google/protobuf/timestamp.d.ts +1 -3
- package/dist/src/gen/video/sfu/event/events.d.ts +1 -22
- package/dist/src/gen/video/sfu/models/models.d.ts +0 -4
- package/dist/src/gen/video/sfu/signal_rpc/signal.client.d.ts +2 -23
- 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/Publisher.d.ts +1 -4
- package/dist/src/rtc/Subscriber.d.ts +0 -7
- 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 +185 -106
- 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/gen/google/protobuf/struct.ts +12 -7
- package/src/gen/google/protobuf/timestamp.ts +7 -6
- package/src/gen/video/sfu/event/events.ts +25 -23
- package/src/gen/video/sfu/models/models.ts +1 -11
- package/src/gen/video/sfu/signal_rpc/signal.client.ts +29 -25
- package/src/gen/video/sfu/signal_rpc/signal.ts +0 -1
- 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/client-details.ts +1 -1
- 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/Publisher.ts +0 -4
- package/src/rtc/Subscriber.ts +2 -28
- 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
|
@@ -508,7 +508,6 @@ class ErrorFromResponse extends Error {
|
|
|
508
508
|
}
|
|
509
509
|
}
|
|
510
510
|
|
|
511
|
-
/* eslint-disable */
|
|
512
511
|
// @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
|
|
513
512
|
// @generated from protobuf file "google/protobuf/struct.proto" (package "google.protobuf", syntax proto3)
|
|
514
513
|
// tslint:disable
|
|
@@ -774,7 +773,6 @@ class ListValue$Type extends MessageType {
|
|
|
774
773
|
*/
|
|
775
774
|
const ListValue = new ListValue$Type();
|
|
776
775
|
|
|
777
|
-
/* eslint-disable */
|
|
778
776
|
// @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
|
|
779
777
|
// @generated from protobuf file "google/protobuf/timestamp.proto" (package "google.protobuf", syntax proto3)
|
|
780
778
|
// tslint:disable
|
|
@@ -1841,12 +1839,6 @@ class TrackInfo$Type extends MessageType {
|
|
|
1841
1839
|
kind: 'scalar',
|
|
1842
1840
|
T: 5 /*ScalarType.INT32*/,
|
|
1843
1841
|
},
|
|
1844
|
-
{
|
|
1845
|
-
no: 13,
|
|
1846
|
-
name: 'self_sub_audio_video',
|
|
1847
|
-
kind: 'scalar',
|
|
1848
|
-
T: 8 /*ScalarType.BOOL*/,
|
|
1849
|
-
},
|
|
1850
1842
|
]);
|
|
1851
1843
|
}
|
|
1852
1844
|
}
|
|
@@ -6649,7 +6641,7 @@ const getSdkVersion = (sdk) => {
|
|
|
6649
6641
|
return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
|
|
6650
6642
|
};
|
|
6651
6643
|
|
|
6652
|
-
const version = "1.
|
|
6644
|
+
const version = "1.53.0";
|
|
6653
6645
|
const [major, minor, patch] = version.split('.');
|
|
6654
6646
|
let sdkInfo = {
|
|
6655
6647
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -6793,7 +6785,7 @@ const getClientDetails = async () => {
|
|
|
6793
6785
|
.join(' '),
|
|
6794
6786
|
version: '',
|
|
6795
6787
|
},
|
|
6796
|
-
webrtcVersion:
|
|
6788
|
+
webrtcVersion: browserVersion,
|
|
6797
6789
|
};
|
|
6798
6790
|
};
|
|
6799
6791
|
|
|
@@ -7743,7 +7735,7 @@ class BasePeerConnection {
|
|
|
7743
7735
|
/**
|
|
7744
7736
|
* Constructs a new `BasePeerConnection` instance.
|
|
7745
7737
|
*/
|
|
7746
|
-
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, }) {
|
|
7747
7739
|
this.iceHasEverConnected = false;
|
|
7748
7740
|
this.isIceRestarting = false;
|
|
7749
7741
|
this.isDisposed = false;
|
|
@@ -7897,6 +7889,10 @@ class BasePeerConnection {
|
|
|
7897
7889
|
this.onConnectionStateChange = async () => {
|
|
7898
7890
|
const state = this.pc.connectionState;
|
|
7899
7891
|
this.logger.debug(`Connection state changed`, state);
|
|
7892
|
+
this.fireOnPeerConnectionStateChange({
|
|
7893
|
+
stateType: 'peerConnection',
|
|
7894
|
+
state,
|
|
7895
|
+
});
|
|
7900
7896
|
if (this.tracer && (state === 'connected' || state === 'failed')) {
|
|
7901
7897
|
try {
|
|
7902
7898
|
const stats = await this.stats.get();
|
|
@@ -7919,8 +7915,20 @@ class BasePeerConnection {
|
|
|
7919
7915
|
this.onIceConnectionStateChange = () => {
|
|
7920
7916
|
const state = this.pc.iceConnectionState;
|
|
7921
7917
|
this.logger.debug(`ICE connection state changed`, state);
|
|
7918
|
+
this.fireOnPeerConnectionStateChange({ stateType: 'ice', state });
|
|
7922
7919
|
this.handleConnectionStateUpdate(state);
|
|
7923
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
|
+
};
|
|
7924
7932
|
this.handleConnectionStateUpdate = (state) => {
|
|
7925
7933
|
const { callingState } = this.state;
|
|
7926
7934
|
if (callingState === CallingState.OFFLINE)
|
|
@@ -8035,6 +8043,8 @@ class BasePeerConnection {
|
|
|
8035
8043
|
this.tag = tag;
|
|
8036
8044
|
this.onReconnectionNeeded = onReconnectionNeeded;
|
|
8037
8045
|
this.onIceConnected = onIceConnected;
|
|
8046
|
+
this.onPeerConnectionStateChange = onPeerConnectionStateChange;
|
|
8047
|
+
this.onRemoteTrackUnmute = onRemoteTrackUnmute;
|
|
8038
8048
|
this.logger = videoLoggerSystem.getLogger(peerType === PeerType.SUBSCRIBER ? 'Subscriber' : 'Publisher', { tags: [tag] });
|
|
8039
8049
|
this.pc = this.createPeerConnection(connectionConfig);
|
|
8040
8050
|
this.stats = new StatsTracer(this.pc, peerType, this.trackIdToTrackType, statsTimestampDriftThresholdMs);
|
|
@@ -8057,6 +8067,8 @@ class BasePeerConnection {
|
|
|
8057
8067
|
this.preConnectStuckTimeout = undefined;
|
|
8058
8068
|
this.onReconnectionNeeded = undefined;
|
|
8059
8069
|
this.onIceConnected = undefined;
|
|
8070
|
+
this.onPeerConnectionStateChange = undefined;
|
|
8071
|
+
this.onRemoteTrackUnmute = undefined;
|
|
8060
8072
|
this.isDisposed = true;
|
|
8061
8073
|
this.detachEventHandlers();
|
|
8062
8074
|
this.pc.close();
|
|
@@ -8417,7 +8429,7 @@ class Publisher extends BasePeerConnection {
|
|
|
8417
8429
|
/**
|
|
8418
8430
|
* Constructs a new `Publisher` instance.
|
|
8419
8431
|
*/
|
|
8420
|
-
constructor(baseOptions, publishOptions
|
|
8432
|
+
constructor(baseOptions, publishOptions) {
|
|
8421
8433
|
super(PeerType.PUBLISHER_UNSPECIFIED, baseOptions);
|
|
8422
8434
|
this.transceiverCache = new TransceiverCache();
|
|
8423
8435
|
this.clonedTracks = new Set();
|
|
@@ -8838,7 +8850,6 @@ class Publisher extends BasePeerConnection {
|
|
|
8838
8850
|
muted: !isTrackLive,
|
|
8839
8851
|
codec: publishOption.codec,
|
|
8840
8852
|
publishOptionId: publishOption.id,
|
|
8841
|
-
selfSubAudioVideo: this.selfSubEnabled,
|
|
8842
8853
|
};
|
|
8843
8854
|
};
|
|
8844
8855
|
this.cloneTrack = (track) => {
|
|
@@ -8919,7 +8930,6 @@ class Publisher extends BasePeerConnection {
|
|
|
8919
8930
|
});
|
|
8920
8931
|
};
|
|
8921
8932
|
this.publishOptions = publishOptions;
|
|
8922
|
-
this.selfSubEnabled = opts.selfSubEnabled ?? false;
|
|
8923
8933
|
this.on('iceRestart', (iceRestart) => {
|
|
8924
8934
|
if (iceRestart.peerType !== PeerType.PUBLISHER_UNSPECIFIED)
|
|
8925
8935
|
return;
|
|
@@ -9001,13 +9011,6 @@ class Subscriber extends BasePeerConnection {
|
|
|
9001
9011
|
*/
|
|
9002
9012
|
constructor(opts) {
|
|
9003
9013
|
super(PeerType.SUBSCRIBER, opts);
|
|
9004
|
-
/**
|
|
9005
|
-
* Remote streams received from the SFU. For a self-sub case
|
|
9006
|
-
* we need to be able to distinguish between the local capture stream.
|
|
9007
|
-
* The map will never contain local streams so we can safely use it to
|
|
9008
|
-
* check if the stream is remote and dispose it when needed.
|
|
9009
|
-
*/
|
|
9010
|
-
this.trackedStreams = new WeakSet();
|
|
9011
9014
|
/**
|
|
9012
9015
|
* Restarts the ICE connection and renegotiates with the SFU.
|
|
9013
9016
|
*/
|
|
@@ -9042,7 +9045,6 @@ class Subscriber extends BasePeerConnection {
|
|
|
9042
9045
|
// example: `e3f6aaf8-b03d-4911-be36-83f47d37a76a:TRACK_TYPE_VIDEO`
|
|
9043
9046
|
const [trackId, rawTrackType] = primaryStream.id.split(':');
|
|
9044
9047
|
const participantToUpdate = this.state.participants.find((p) => p.trackLookupPrefix === trackId);
|
|
9045
|
-
const isSelfSub = !!participantToUpdate?.isLocalParticipant;
|
|
9046
9048
|
this.logger.debug(`[onTrack]: Got remote ${rawTrackType} track for userId: ${participantToUpdate?.userId}`, track.id, track);
|
|
9047
9049
|
const trackType = toTrackType(rawTrackType);
|
|
9048
9050
|
if (!trackType) {
|
|
@@ -9056,6 +9058,7 @@ class Subscriber extends BasePeerConnection {
|
|
|
9056
9058
|
track.addEventListener('unmute', () => {
|
|
9057
9059
|
this.logger.info(`[onTrack]: Track unmuted: ${trackDebugInfo}`);
|
|
9058
9060
|
this.setRemoteTrackInterrupted(trackId, trackType, false);
|
|
9061
|
+
this.onRemoteTrackUnmute?.(trackType, track.id);
|
|
9059
9062
|
});
|
|
9060
9063
|
track.addEventListener('ended', () => {
|
|
9061
9064
|
this.logger.info(`[onTrack]: Track ended: ${trackDebugInfo}`);
|
|
@@ -9066,9 +9069,6 @@ class Subscriber extends BasePeerConnection {
|
|
|
9066
9069
|
this.setRemoteTrackInterrupted(trackId, trackType, true);
|
|
9067
9070
|
}
|
|
9068
9071
|
this.trackIdToTrackType.set(track.id, trackType);
|
|
9069
|
-
if (isSelfSub) {
|
|
9070
|
-
this.trackedStreams.add(primaryStream);
|
|
9071
|
-
}
|
|
9072
9072
|
if (!participantToUpdate) {
|
|
9073
9073
|
this.logger.warn(`[onTrack]: Received track for unknown participant: ${trackId}`, e);
|
|
9074
9074
|
this.state.registerOrphanedTrack({
|
|
@@ -9084,12 +9084,6 @@ class Subscriber extends BasePeerConnection {
|
|
|
9084
9084
|
this.logger.error(`Unknown track type: ${rawTrackType}`);
|
|
9085
9085
|
return;
|
|
9086
9086
|
}
|
|
9087
|
-
// Self-sub loopback audio routes to the speaker by default, which
|
|
9088
|
-
// would echo the local user's voice. Default-mute here; consumers
|
|
9089
|
-
// (the loopback recording hook) re-enable explicitly when needed.
|
|
9090
|
-
if (isSelfSub && e.track.kind === 'audio') {
|
|
9091
|
-
e.track.enabled = false;
|
|
9092
|
-
}
|
|
9093
9087
|
// get the previous stream to dispose it later
|
|
9094
9088
|
// usually this happens during migration, when the stream is replaced
|
|
9095
9089
|
// with a new one but the old one is still in the state
|
|
@@ -9098,12 +9092,8 @@ class Subscriber extends BasePeerConnection {
|
|
|
9098
9092
|
this.state.updateParticipant(participantToUpdate.sessionId, {
|
|
9099
9093
|
[streamKindProp]: primaryStream,
|
|
9100
9094
|
});
|
|
9095
|
+
// now, dispose the previous stream if it exists
|
|
9101
9096
|
if (previousStream) {
|
|
9102
|
-
if (isSelfSub && !this.trackedStreams.has(previousStream)) {
|
|
9103
|
-
// this is the local capture stream, we don't want to dispose it
|
|
9104
|
-
this.logger.debug(`[onTrack]: Skipping cleanup of previous ${e.track.kind} stream for userId: ${participantToUpdate.userId} because it is not tracked`);
|
|
9105
|
-
return;
|
|
9106
|
-
}
|
|
9107
9097
|
this.logger.info(`[onTrack]: Cleaning up previous remote ${track.kind} tracks for userId: ${participantToUpdate.userId}`);
|
|
9108
9098
|
previousStream.getTracks().forEach((t) => {
|
|
9109
9099
|
t.stop();
|
|
@@ -9308,6 +9298,15 @@ class SfuJoinError extends Error {
|
|
|
9308
9298
|
}
|
|
9309
9299
|
}
|
|
9310
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
|
+
|
|
9311
9310
|
/**
|
|
9312
9311
|
* Creates a fresh `joinResponseTask` with a no-op rejection handler attached
|
|
9313
9312
|
* to the underlying promise. The handler marks the rejection path as handled
|
|
@@ -9413,7 +9412,7 @@ class StreamSfuClient {
|
|
|
9413
9412
|
timeoutId = setTimeout(() => {
|
|
9414
9413
|
const message = `SFU WS connection failed to open after ${this.joinResponseTimeout}ms`;
|
|
9415
9414
|
this.tracer?.trace('signal.timeout', message);
|
|
9416
|
-
reject(new
|
|
9415
|
+
reject(new SfuTimeoutError(message));
|
|
9417
9416
|
}, this.joinResponseTimeout);
|
|
9418
9417
|
}),
|
|
9419
9418
|
]));
|
|
@@ -9583,7 +9582,7 @@ class StreamSfuClient {
|
|
|
9583
9582
|
cleanupJoinSubscriptions();
|
|
9584
9583
|
const message = `Waiting for "joinResponse" has timed out after ${this.joinResponseTimeout}ms`;
|
|
9585
9584
|
this.tracer?.trace('joinRequestTimeout', message);
|
|
9586
|
-
current.reject(new
|
|
9585
|
+
current.reject(new SfuTimeoutError(message));
|
|
9587
9586
|
}, this.joinResponseTimeout);
|
|
9588
9587
|
const joinRequest = SfuRequest.create({
|
|
9589
9588
|
requestPayload: {
|
|
@@ -9800,6 +9799,10 @@ const watchCallEnded = (call) => {
|
|
|
9800
9799
|
const { callingState } = call.state;
|
|
9801
9800
|
if (callingState !== CallingState.IDLE &&
|
|
9802
9801
|
callingState !== CallingState.LEFT) {
|
|
9802
|
+
call.clientEventReporter.abort(call.cid, {
|
|
9803
|
+
code: 'BACKEND_LEAVE',
|
|
9804
|
+
reason: 'call.ended event received',
|
|
9805
|
+
});
|
|
9803
9806
|
call
|
|
9804
9807
|
.leave({ message: 'call.ended event received', reject: false })
|
|
9805
9808
|
.catch((err) => {
|
|
@@ -9829,6 +9832,10 @@ const watchSfuCallEnded = (call) => {
|
|
|
9829
9832
|
call.state.setEndedAt(new Date());
|
|
9830
9833
|
const reason = CallEndedReason[e.reason];
|
|
9831
9834
|
globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
|
|
9835
|
+
call.clientEventReporter.abort(call.cid, {
|
|
9836
|
+
code: 'BACKEND_LEAVE',
|
|
9837
|
+
reason: `callEnded received: ${reason}`,
|
|
9838
|
+
});
|
|
9832
9839
|
await call.leave({ message: `callEnded received: ${reason}` });
|
|
9833
9840
|
}
|
|
9834
9841
|
catch (err) {
|
|
@@ -10981,6 +10988,40 @@ class DynascaleManager {
|
|
|
10981
10988
|
}
|
|
10982
10989
|
}
|
|
10983
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
|
+
|
|
10984
11025
|
const DEFAULT_THRESHOLD = 0.35;
|
|
10985
11026
|
const DEFAULT_VIEWPORT_VISIBILITY_STATE = {
|
|
10986
11027
|
videoTrack: VisibilityState.UNKNOWN,
|
|
@@ -13824,7 +13865,7 @@ class Call {
|
|
|
13824
13865
|
* Use the [`StreamVideoClient.call`](./StreamVideoClient.md/#call)
|
|
13825
13866
|
* method to construct a `Call` instance.
|
|
13826
13867
|
*/
|
|
13827
|
-
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, }) {
|
|
13828
13869
|
/**
|
|
13829
13870
|
* The state of this call.
|
|
13830
13871
|
*/
|
|
@@ -13861,7 +13902,6 @@ class Call {
|
|
|
13861
13902
|
// maintain the order of publishing tracks to restore them after a reconnection
|
|
13862
13903
|
// it shouldn't contain duplicates
|
|
13863
13904
|
this.trackPublishOrder = [];
|
|
13864
|
-
this.selfSubEnabled = false;
|
|
13865
13905
|
this.hasJoinedOnce = false;
|
|
13866
13906
|
this.deviceSettingsAppliedOnce = false;
|
|
13867
13907
|
this.initialized = false;
|
|
@@ -14152,9 +14192,14 @@ class Call {
|
|
|
14152
14192
|
this.sfuStatsReporter = undefined;
|
|
14153
14193
|
this.lastStatsOptions = undefined;
|
|
14154
14194
|
await this.subscriber?.dispose();
|
|
14195
|
+
this.clientEventReporter.abort(this.cid, {
|
|
14196
|
+
code: 'CLIENT_ABORTED',
|
|
14197
|
+
reason: leaveReason,
|
|
14198
|
+
});
|
|
14155
14199
|
this.subscriber = undefined;
|
|
14156
14200
|
await this.publisher?.dispose();
|
|
14157
14201
|
this.publisher = undefined;
|
|
14202
|
+
this.clientEventReporter.unregisterCall(this.cid);
|
|
14158
14203
|
await this.sfuClient?.leaveAndClose(leaveReason);
|
|
14159
14204
|
this.sfuClient = undefined;
|
|
14160
14205
|
this.trackSubscriptionManager.setSfuClient(undefined);
|
|
@@ -14206,30 +14251,6 @@ class Call {
|
|
|
14206
14251
|
await Promise.all(stopOnLeavePromises);
|
|
14207
14252
|
});
|
|
14208
14253
|
};
|
|
14209
|
-
/**
|
|
14210
|
-
* The largest video publish dimension across the current publish options.
|
|
14211
|
-
*
|
|
14212
|
-
* @internal
|
|
14213
|
-
*/
|
|
14214
|
-
this.getMaxVideoPublishDimension = () => {
|
|
14215
|
-
if (!this.currentPublishOptions)
|
|
14216
|
-
return undefined;
|
|
14217
|
-
let maxDimension;
|
|
14218
|
-
let maxArea = 0;
|
|
14219
|
-
for (const opt of this.currentPublishOptions) {
|
|
14220
|
-
if (opt.trackType !== TrackType.VIDEO)
|
|
14221
|
-
continue;
|
|
14222
|
-
const dim = opt.videoDimension;
|
|
14223
|
-
if (!dim || !dim.width || !dim.height)
|
|
14224
|
-
continue;
|
|
14225
|
-
const area = dim.width * dim.height;
|
|
14226
|
-
if (area > maxArea) {
|
|
14227
|
-
maxDimension = dim;
|
|
14228
|
-
maxArea = area;
|
|
14229
|
-
}
|
|
14230
|
-
}
|
|
14231
|
-
return maxDimension;
|
|
14232
|
-
};
|
|
14233
14254
|
/**
|
|
14234
14255
|
* Update from the call response from the "call.ring" event
|
|
14235
14256
|
* @internal
|
|
@@ -14376,7 +14397,7 @@ class Call {
|
|
|
14376
14397
|
*
|
|
14377
14398
|
* @returns a promise which resolves once the call join-flow has finished.
|
|
14378
14399
|
*/
|
|
14379
|
-
this.join = async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout,
|
|
14400
|
+
this.join = async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, ...data } = {}) => {
|
|
14380
14401
|
const callingState = this.state.callingState;
|
|
14381
14402
|
if ([CallingState.JOINED, CallingState.JOINING].includes(callingState)) {
|
|
14382
14403
|
throw new Error(`Illegal State: call.join() shall be called only once`);
|
|
@@ -14384,15 +14405,19 @@ class Call {
|
|
|
14384
14405
|
if (data?.ring) {
|
|
14385
14406
|
this.ringingSubject.next(true);
|
|
14386
14407
|
}
|
|
14387
|
-
// we need this to be set before the callingx.joinCall() is
|
|
14388
|
-
// called to avoid registering the test call in the CallKit/Telecom
|
|
14389
|
-
this.selfSubEnabled = selfSubEnabled;
|
|
14390
14408
|
const callingX = globalThis.streamRNVideoSDK?.callingX;
|
|
14391
14409
|
if (callingX) {
|
|
14392
14410
|
// for Android/iOS, we need to start the call in the callingx library as soon as possible
|
|
14393
14411
|
await callingX.joinCall(this, this.clientStore.calls);
|
|
14394
14412
|
}
|
|
14395
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
|
+
});
|
|
14396
14421
|
this.joinResponseTimeout = joinResponseTimeout;
|
|
14397
14422
|
this.rpcRequestTimeout = rpcRequestTimeout;
|
|
14398
14423
|
// we will count the number of join failures per SFU.
|
|
@@ -14402,39 +14427,42 @@ class Call {
|
|
|
14402
14427
|
const joinData = data;
|
|
14403
14428
|
maxJoinRetries = Math.max(maxJoinRetries, 1);
|
|
14404
14429
|
try {
|
|
14405
|
-
|
|
14406
|
-
|
|
14407
|
-
|
|
14408
|
-
|
|
14409
|
-
|
|
14410
|
-
|
|
14411
|
-
|
|
14412
|
-
|
|
14413
|
-
catch (err) {
|
|
14414
|
-
this.logger.warn(`Failed to join call (${attempt})`, this.cid);
|
|
14415
|
-
if ((err instanceof ErrorFromResponse && err.unrecoverable) ||
|
|
14416
|
-
(err instanceof SfuJoinError && err.unrecoverable)) {
|
|
14417
|
-
// if the error is unrecoverable, we should not retry as that signals
|
|
14418
|
-
// that connectivity is good, but the coordinator doesn't allow the user
|
|
14419
|
-
// to join the call due to some reason (e.g., ended call, expired token...)
|
|
14420
|
-
throw err;
|
|
14421
|
-
}
|
|
14422
|
-
// immediately switch to a different SFU in case of recoverable join error
|
|
14423
|
-
const switchSfu = err instanceof SfuJoinError &&
|
|
14424
|
-
SfuJoinError.isJoinErrorCode(err.errorEvent);
|
|
14425
|
-
const sfuId = this.credentials?.server.edge_name || '';
|
|
14426
|
-
const failures = (sfuJoinFailures.get(sfuId) || 0) + 1;
|
|
14427
|
-
sfuJoinFailures.set(sfuId, failures);
|
|
14428
|
-
if (switchSfu || failures >= 2) {
|
|
14429
|
-
joinData.migrating_from = sfuId;
|
|
14430
|
-
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;
|
|
14431
14438
|
}
|
|
14432
|
-
|
|
14433
|
-
|
|
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
|
+
}
|
|
14434
14462
|
}
|
|
14463
|
+
await sleep(retryInterval(attempt));
|
|
14435
14464
|
}
|
|
14436
|
-
|
|
14437
|
-
}
|
|
14465
|
+
});
|
|
14438
14466
|
}
|
|
14439
14467
|
catch (error) {
|
|
14440
14468
|
callingX?.endCall(this, 'error');
|
|
@@ -14463,7 +14491,7 @@ class Call {
|
|
|
14463
14491
|
performingMigration ||
|
|
14464
14492
|
data?.migrating_from) {
|
|
14465
14493
|
try {
|
|
14466
|
-
const joinResponse = await this.doJoinRequest(data);
|
|
14494
|
+
const joinResponse = await this.clientEventReporter.track(this.cid, 'CoordinatorJoin', () => this.doJoinRequest(data));
|
|
14467
14495
|
this.credentials = joinResponse.credentials;
|
|
14468
14496
|
statsOptions = joinResponse.stats_options;
|
|
14469
14497
|
this.lastStatsOptions = statsOptions;
|
|
@@ -14521,9 +14549,11 @@ class Call {
|
|
|
14521
14549
|
const preferredSubscribeOptions = !isReconnecting
|
|
14522
14550
|
? this.getPreferredSubscribeOptions()
|
|
14523
14551
|
: [];
|
|
14552
|
+
const unifiedSessionId = this.unifiedSessionId;
|
|
14553
|
+
const capabilities = Array.from(this.clientCapabilities);
|
|
14524
14554
|
try {
|
|
14525
|
-
const { callState, fastReconnectDeadlineSeconds, publishOptions } = await sfuClient.join({
|
|
14526
|
-
unifiedSessionId
|
|
14555
|
+
const { callState, fastReconnectDeadlineSeconds, publishOptions } = await this.clientEventReporter.track(this.cid, 'WSJoin', () => sfuClient.join({
|
|
14556
|
+
unifiedSessionId,
|
|
14527
14557
|
subscriberSdp,
|
|
14528
14558
|
publisherSdp,
|
|
14529
14559
|
clientDetails,
|
|
@@ -14531,9 +14561,9 @@ class Call {
|
|
|
14531
14561
|
reconnectDetails,
|
|
14532
14562
|
preferredPublishOptions,
|
|
14533
14563
|
preferredSubscribeOptions,
|
|
14534
|
-
capabilities
|
|
14564
|
+
capabilities,
|
|
14535
14565
|
source: ParticipantSource.WEBRTC_UNSPECIFIED,
|
|
14536
|
-
});
|
|
14566
|
+
}));
|
|
14537
14567
|
this.currentPublishOptions = publishOptions;
|
|
14538
14568
|
this.fastReconnectDeadlineSeconds = fastReconnectDeadlineSeconds;
|
|
14539
14569
|
if (callState) {
|
|
@@ -14745,6 +14775,16 @@ class Call {
|
|
|
14745
14775
|
// "ICE never connected" failure budget can be cleared.
|
|
14746
14776
|
this.iceFailuresWithoutConnect = 0;
|
|
14747
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
|
+
},
|
|
14748
14788
|
};
|
|
14749
14789
|
this.subscriber = new Subscriber(basePeerConnectionOptions);
|
|
14750
14790
|
// anonymous users can't publish anything hence, there is no need
|
|
@@ -14754,9 +14794,7 @@ class Call {
|
|
|
14754
14794
|
if (closePreviousInstances && this.publisher) {
|
|
14755
14795
|
await this.publisher.dispose();
|
|
14756
14796
|
}
|
|
14757
|
-
this.publisher = new Publisher(basePeerConnectionOptions, publishOptions
|
|
14758
|
-
selfSubEnabled: this.selfSubEnabled,
|
|
14759
|
-
});
|
|
14797
|
+
this.publisher = new Publisher(basePeerConnectionOptions, publishOptions);
|
|
14760
14798
|
}
|
|
14761
14799
|
this.statsReporter?.stop();
|
|
14762
14800
|
if (this.statsReportingIntervalInMs > 0) {
|
|
@@ -15023,7 +15061,10 @@ class Call {
|
|
|
15023
15061
|
const reconnectStartTime = Date.now();
|
|
15024
15062
|
this.reconnectStrategy = WebsocketReconnectStrategy.REJOIN;
|
|
15025
15063
|
this.state.setCallingState(CallingState.RECONNECTING);
|
|
15026
|
-
|
|
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));
|
|
15027
15068
|
await this.restorePublishedTracks();
|
|
15028
15069
|
this.restoreSubscribedTracks();
|
|
15029
15070
|
this.sfuStatsReporter?.sendReconnectionTime(WebsocketReconnectStrategy.REJOIN, (Date.now() - reconnectStartTime) / 1000);
|
|
@@ -15047,11 +15088,11 @@ class Call {
|
|
|
15047
15088
|
const migrationTask = makeSafePromise(currentSfuClient.enterMigration());
|
|
15048
15089
|
try {
|
|
15049
15090
|
const currentSfu = currentSfuClient.edgeName;
|
|
15050
|
-
await this.doJoin({
|
|
15091
|
+
await this.clientEventReporter.withJoinLifecycle(this.cid, 'migration', () => this.doJoin({
|
|
15051
15092
|
...this.joinCallData,
|
|
15052
15093
|
migrating_from: currentSfu,
|
|
15053
15094
|
migrating_from_list: [currentSfu],
|
|
15054
|
-
});
|
|
15095
|
+
}));
|
|
15055
15096
|
}
|
|
15056
15097
|
finally {
|
|
15057
15098
|
// cleanup the migration_from field after the migration is complete or failed
|
|
@@ -15087,11 +15128,22 @@ class Call {
|
|
|
15087
15128
|
this.registerReconnectHandlers = () => {
|
|
15088
15129
|
// handles the legacy "goAway" event
|
|
15089
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
|
+
});
|
|
15090
15135
|
this.reconnect(WebsocketReconnectStrategy.MIGRATE, ReconnectReason.GO_AWAY).catch((err) => this.logger.warn('[Reconnect] Error reconnecting', err));
|
|
15091
15136
|
});
|
|
15092
15137
|
// handles the "error" event, through which the SFU can request a reconnect
|
|
15093
15138
|
const unregisterOnError = this.on('error', (e) => {
|
|
15094
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
|
+
}
|
|
15095
15147
|
// SFU_FULL is a join error, and when emitted, although it specifies a
|
|
15096
15148
|
// `migrate` strategy, we should actually perform a REJOIN to a new SFU.
|
|
15097
15149
|
// This is now handled separately in the `call.join()` method.
|
|
@@ -15786,7 +15838,9 @@ class Call {
|
|
|
15786
15838
|
this.leave({
|
|
15787
15839
|
reject: true,
|
|
15788
15840
|
reason: 'timeout',
|
|
15789
|
-
message: `ringing timeout - ${this.isCreatedByMe
|
|
15841
|
+
message: `ringing timeout - ${this.isCreatedByMe
|
|
15842
|
+
? 'no one accepted'
|
|
15843
|
+
: `user didn't interact with incoming call screen`}`,
|
|
15790
15844
|
}).catch((err) => {
|
|
15791
15845
|
this.logger.error('Failed to drop call', err);
|
|
15792
15846
|
});
|
|
@@ -15992,15 +16046,36 @@ class Call {
|
|
|
15992
16046
|
* @param trackType the kind of video.
|
|
15993
16047
|
*/
|
|
15994
16048
|
this.bindVideoElement = (videoElement, sessionId, trackType) => {
|
|
15995
|
-
const
|
|
15996
|
-
|
|
16049
|
+
const unbindDynascale = this.dynascaleManager?.bindVideoElement(videoElement, sessionId, trackType);
|
|
16050
|
+
const stopFirstFrameDetector = this.bindFirstVideoFrameDetector(videoElement, sessionId, trackType);
|
|
16051
|
+
if (!unbindDynascale && !stopFirstFrameDetector)
|
|
15997
16052
|
return;
|
|
16053
|
+
const unbind = () => {
|
|
16054
|
+
stopFirstFrameDetector?.();
|
|
16055
|
+
unbindDynascale?.();
|
|
16056
|
+
};
|
|
15998
16057
|
this.leaveCallHooks.add(unbind);
|
|
15999
16058
|
return () => {
|
|
16000
16059
|
this.leaveCallHooks.delete(unbind);
|
|
16001
16060
|
unbind();
|
|
16002
16061
|
};
|
|
16003
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
|
+
};
|
|
16004
16079
|
/**
|
|
16005
16080
|
* Binds a DOM <audio> element to the given session id.
|
|
16006
16081
|
*
|
|
@@ -16150,6 +16225,7 @@ class Call {
|
|
|
16150
16225
|
this.ringingSubject = new BehaviorSubject(ringing);
|
|
16151
16226
|
this.watching = watching;
|
|
16152
16227
|
this.streamClient = streamClient;
|
|
16228
|
+
this.clientEventReporter = clientEventReporter;
|
|
16153
16229
|
this.clientStore = clientStore;
|
|
16154
16230
|
this.streamClientBasePath = `/call/${this.type}/${this.id}`;
|
|
16155
16231
|
this.logger = videoLoggerSystem.getLogger('Call');
|
|
@@ -16186,12 +16262,6 @@ class Call {
|
|
|
16186
16262
|
get currentUserId() {
|
|
16187
16263
|
return this.clientStore.connectedUser?.id;
|
|
16188
16264
|
}
|
|
16189
|
-
/**
|
|
16190
|
-
* A flag indicating whether self-subscription is enabled for the call.
|
|
16191
|
-
*/
|
|
16192
|
-
get isSelfSubEnabled() {
|
|
16193
|
-
return this.selfSubEnabled;
|
|
16194
|
-
}
|
|
16195
16265
|
/**
|
|
16196
16266
|
* A flag indicating whether the call was created by the current user.
|
|
16197
16267
|
*/
|
|
@@ -17378,10 +17448,12 @@ class StreamClient {
|
|
|
17378
17448
|
this.logger.info('StreamClient.connect: this.wsConnection.connect()');
|
|
17379
17449
|
return await this.wsConnection.connect(this.defaultWSTimeout);
|
|
17380
17450
|
};
|
|
17451
|
+
this.getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
|
|
17452
|
+
"1.53.0";
|
|
17381
17453
|
this.getUserAgent = () => {
|
|
17382
17454
|
if (!this.cachedUserAgent) {
|
|
17383
17455
|
const { clientAppIdentifier = {} } = this.options;
|
|
17384
|
-
const { sdkName = 'js', sdkVersion = "1.
|
|
17456
|
+
const { sdkName = 'js', sdkVersion = "1.53.0", ...extras } = clientAppIdentifier;
|
|
17385
17457
|
this.cachedUserAgent = [
|
|
17386
17458
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
17387
17459
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|
|
@@ -17558,6 +17630,602 @@ const createTokenOrProvider = (options) => {
|
|
|
17558
17630
|
return token || tokenProvider;
|
|
17559
17631
|
};
|
|
17560
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
|
+
|
|
17561
18229
|
/**
|
|
17562
18230
|
* A `StreamVideoClient` instance lets you communicate with our API, and authenticate users.
|
|
17563
18231
|
*/
|
|
@@ -17621,6 +18289,7 @@ class StreamVideoClient {
|
|
|
17621
18289
|
}
|
|
17622
18290
|
call = new Call({
|
|
17623
18291
|
streamClient: this.streamClient,
|
|
18292
|
+
clientEventReporter: this.clientEventReporter,
|
|
17624
18293
|
type: e.call.type,
|
|
17625
18294
|
id: e.call.id,
|
|
17626
18295
|
members: e.members,
|
|
@@ -17690,6 +18359,8 @@ class StreamVideoClient {
|
|
|
17690
18359
|
user.id = '!anon';
|
|
17691
18360
|
return this.connectAnonymousUser(user, tokenOrProvider);
|
|
17692
18361
|
}
|
|
18362
|
+
const reporter = this.clientEventReporter;
|
|
18363
|
+
reporter.startCoordinatorConnection(user.id);
|
|
17693
18364
|
const connectUserResponse = await withoutConcurrency(this.connectionConcurrencyTag, async () => {
|
|
17694
18365
|
const client = this.streamClient;
|
|
17695
18366
|
const { onConnectUserError, persistUserOnConnectionFailure } = client.options;
|
|
@@ -17699,14 +18370,15 @@ class StreamVideoClient {
|
|
|
17699
18370
|
for (let attempt = 0; attempt < maxConnectUserRetries; attempt++) {
|
|
17700
18371
|
try {
|
|
17701
18372
|
this.logger.trace(`Connecting user (${attempt})`, user);
|
|
17702
|
-
return user.type === 'guest'
|
|
17703
|
-
?
|
|
17704
|
-
:
|
|
18373
|
+
return await reporter.trackCoordinatorWs(() => user.type === 'guest'
|
|
18374
|
+
? client.connectGuestUser(user)
|
|
18375
|
+
: client.connectUser(user, tokenOrProvider));
|
|
17705
18376
|
}
|
|
17706
18377
|
catch (err) {
|
|
17707
18378
|
this.logger.warn(`Failed to connect a user (${attempt})`, err);
|
|
17708
18379
|
errorQueue.push(err);
|
|
17709
18380
|
if (attempt === maxConnectUserRetries - 1) {
|
|
18381
|
+
reporter.closeCoordinatorWs();
|
|
17710
18382
|
onConnectUserError?.(err, errorQueue);
|
|
17711
18383
|
throw err;
|
|
17712
18384
|
}
|
|
@@ -17784,6 +18456,7 @@ class StreamVideoClient {
|
|
|
17784
18456
|
return (call ??
|
|
17785
18457
|
new Call({
|
|
17786
18458
|
streamClient: this.streamClient,
|
|
18459
|
+
clientEventReporter: this.clientEventReporter,
|
|
17787
18460
|
id: id,
|
|
17788
18461
|
type: type,
|
|
17789
18462
|
clientStore: this.writeableStateStore,
|
|
@@ -17808,6 +18481,7 @@ class StreamVideoClient {
|
|
|
17808
18481
|
for (const c of response.calls) {
|
|
17809
18482
|
const call = new Call({
|
|
17810
18483
|
streamClient: this.streamClient,
|
|
18484
|
+
clientEventReporter: this.clientEventReporter,
|
|
17811
18485
|
id: c.call.id,
|
|
17812
18486
|
type: c.call.type,
|
|
17813
18487
|
members: c.members,
|
|
@@ -17915,6 +18589,7 @@ class StreamVideoClient {
|
|
|
17915
18589
|
const [callType, callId] = call_cid.split(':');
|
|
17916
18590
|
call = new Call({
|
|
17917
18591
|
streamClient: this.streamClient,
|
|
18592
|
+
clientEventReporter: this.clientEventReporter,
|
|
17918
18593
|
type: callType,
|
|
17919
18594
|
id: callId,
|
|
17920
18595
|
clientStore: this.writeableStateStore,
|
|
@@ -17955,6 +18630,9 @@ class StreamVideoClient {
|
|
|
17955
18630
|
this.logger = videoLoggerSystem.getLogger('client');
|
|
17956
18631
|
this.rejectCallWhenBusy = clientOptions?.rejectCallWhenBusy ?? false;
|
|
17957
18632
|
this.streamClient = createCoordinatorClient(apiKey, clientOptions);
|
|
18633
|
+
this.clientEventReporter = new ClientEventReporter({
|
|
18634
|
+
streamClient: this.streamClient,
|
|
18635
|
+
});
|
|
17958
18636
|
this.writeableStateStore = new StreamVideoWriteableStateStore();
|
|
17959
18637
|
this.readOnlyStateStore = new StreamVideoReadOnlyStateStore(this.writeableStateStore);
|
|
17960
18638
|
if (typeof apiKeyOrArgs !== 'string' && apiKeyOrArgs.user) {
|
|
@@ -18017,5 +18695,5 @@ const humanize = (n) => {
|
|
|
18017
18695
|
return String(n);
|
|
18018
18696
|
};
|
|
18019
18697
|
|
|
18020
|
-
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 };
|
|
18021
18699
|
//# sourceMappingURL=index.es.js.map
|