@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.cjs.js
CHANGED
|
@@ -527,7 +527,6 @@ class ErrorFromResponse extends Error {
|
|
|
527
527
|
}
|
|
528
528
|
}
|
|
529
529
|
|
|
530
|
-
/* eslint-disable */
|
|
531
530
|
// @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
|
|
532
531
|
// @generated from protobuf file "google/protobuf/struct.proto" (package "google.protobuf", syntax proto3)
|
|
533
532
|
// tslint:disable
|
|
@@ -793,7 +792,6 @@ class ListValue$Type extends runtime.MessageType {
|
|
|
793
792
|
*/
|
|
794
793
|
const ListValue = new ListValue$Type();
|
|
795
794
|
|
|
796
|
-
/* eslint-disable */
|
|
797
795
|
// @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
|
|
798
796
|
// @generated from protobuf file "google/protobuf/timestamp.proto" (package "google.protobuf", syntax proto3)
|
|
799
797
|
// tslint:disable
|
|
@@ -1860,12 +1858,6 @@ class TrackInfo$Type extends runtime.MessageType {
|
|
|
1860
1858
|
kind: 'scalar',
|
|
1861
1859
|
T: 5 /*ScalarType.INT32*/,
|
|
1862
1860
|
},
|
|
1863
|
-
{
|
|
1864
|
-
no: 13,
|
|
1865
|
-
name: 'self_sub_audio_video',
|
|
1866
|
-
kind: 'scalar',
|
|
1867
|
-
T: 8 /*ScalarType.BOOL*/,
|
|
1868
|
-
},
|
|
1869
1861
|
]);
|
|
1870
1862
|
}
|
|
1871
1863
|
}
|
|
@@ -6668,7 +6660,7 @@ const getSdkVersion = (sdk) => {
|
|
|
6668
6660
|
return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
|
|
6669
6661
|
};
|
|
6670
6662
|
|
|
6671
|
-
const version = "1.
|
|
6663
|
+
const version = "1.53.0";
|
|
6672
6664
|
const [major, minor, patch] = version.split('.');
|
|
6673
6665
|
let sdkInfo = {
|
|
6674
6666
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -6812,7 +6804,7 @@ const getClientDetails = async () => {
|
|
|
6812
6804
|
.join(' '),
|
|
6813
6805
|
version: '',
|
|
6814
6806
|
},
|
|
6815
|
-
webrtcVersion:
|
|
6807
|
+
webrtcVersion: browserVersion,
|
|
6816
6808
|
};
|
|
6817
6809
|
};
|
|
6818
6810
|
|
|
@@ -7762,7 +7754,7 @@ class BasePeerConnection {
|
|
|
7762
7754
|
/**
|
|
7763
7755
|
* Constructs a new `BasePeerConnection` instance.
|
|
7764
7756
|
*/
|
|
7765
|
-
constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, onIceConnected, tag, enableTracing, clientPublishOptions, iceRestartDelay = 2500, statsTimestampDriftThresholdMs = 0, }) {
|
|
7757
|
+
constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, onIceConnected, onPeerConnectionStateChange, onRemoteTrackUnmute, tag, enableTracing, clientPublishOptions, iceRestartDelay = 2500, statsTimestampDriftThresholdMs = 0, }) {
|
|
7766
7758
|
this.iceHasEverConnected = false;
|
|
7767
7759
|
this.isIceRestarting = false;
|
|
7768
7760
|
this.isDisposed = false;
|
|
@@ -7916,6 +7908,10 @@ class BasePeerConnection {
|
|
|
7916
7908
|
this.onConnectionStateChange = async () => {
|
|
7917
7909
|
const state = this.pc.connectionState;
|
|
7918
7910
|
this.logger.debug(`Connection state changed`, state);
|
|
7911
|
+
this.fireOnPeerConnectionStateChange({
|
|
7912
|
+
stateType: 'peerConnection',
|
|
7913
|
+
state,
|
|
7914
|
+
});
|
|
7919
7915
|
if (this.tracer && (state === 'connected' || state === 'failed')) {
|
|
7920
7916
|
try {
|
|
7921
7917
|
const stats = await this.stats.get();
|
|
@@ -7938,8 +7934,20 @@ class BasePeerConnection {
|
|
|
7938
7934
|
this.onIceConnectionStateChange = () => {
|
|
7939
7935
|
const state = this.pc.iceConnectionState;
|
|
7940
7936
|
this.logger.debug(`ICE connection state changed`, state);
|
|
7937
|
+
this.fireOnPeerConnectionStateChange({ stateType: 'ice', state });
|
|
7941
7938
|
this.handleConnectionStateUpdate(state);
|
|
7942
7939
|
};
|
|
7940
|
+
this.fireOnPeerConnectionStateChange = (event) => {
|
|
7941
|
+
try {
|
|
7942
|
+
this.onPeerConnectionStateChange?.({
|
|
7943
|
+
peerType: this.peerType,
|
|
7944
|
+
...event,
|
|
7945
|
+
});
|
|
7946
|
+
}
|
|
7947
|
+
catch (err) {
|
|
7948
|
+
this.logger.warn('onPeerConnectionStateChange listener threw', err);
|
|
7949
|
+
}
|
|
7950
|
+
};
|
|
7943
7951
|
this.handleConnectionStateUpdate = (state) => {
|
|
7944
7952
|
const { callingState } = this.state;
|
|
7945
7953
|
if (callingState === exports.CallingState.OFFLINE)
|
|
@@ -8054,6 +8062,8 @@ class BasePeerConnection {
|
|
|
8054
8062
|
this.tag = tag;
|
|
8055
8063
|
this.onReconnectionNeeded = onReconnectionNeeded;
|
|
8056
8064
|
this.onIceConnected = onIceConnected;
|
|
8065
|
+
this.onPeerConnectionStateChange = onPeerConnectionStateChange;
|
|
8066
|
+
this.onRemoteTrackUnmute = onRemoteTrackUnmute;
|
|
8057
8067
|
this.logger = videoLoggerSystem.getLogger(peerType === PeerType.SUBSCRIBER ? 'Subscriber' : 'Publisher', { tags: [tag] });
|
|
8058
8068
|
this.pc = this.createPeerConnection(connectionConfig);
|
|
8059
8069
|
this.stats = new StatsTracer(this.pc, peerType, this.trackIdToTrackType, statsTimestampDriftThresholdMs);
|
|
@@ -8076,6 +8086,8 @@ class BasePeerConnection {
|
|
|
8076
8086
|
this.preConnectStuckTimeout = undefined;
|
|
8077
8087
|
this.onReconnectionNeeded = undefined;
|
|
8078
8088
|
this.onIceConnected = undefined;
|
|
8089
|
+
this.onPeerConnectionStateChange = undefined;
|
|
8090
|
+
this.onRemoteTrackUnmute = undefined;
|
|
8079
8091
|
this.isDisposed = true;
|
|
8080
8092
|
this.detachEventHandlers();
|
|
8081
8093
|
this.pc.close();
|
|
@@ -8436,7 +8448,7 @@ class Publisher extends BasePeerConnection {
|
|
|
8436
8448
|
/**
|
|
8437
8449
|
* Constructs a new `Publisher` instance.
|
|
8438
8450
|
*/
|
|
8439
|
-
constructor(baseOptions, publishOptions
|
|
8451
|
+
constructor(baseOptions, publishOptions) {
|
|
8440
8452
|
super(PeerType.PUBLISHER_UNSPECIFIED, baseOptions);
|
|
8441
8453
|
this.transceiverCache = new TransceiverCache();
|
|
8442
8454
|
this.clonedTracks = new Set();
|
|
@@ -8857,7 +8869,6 @@ class Publisher extends BasePeerConnection {
|
|
|
8857
8869
|
muted: !isTrackLive,
|
|
8858
8870
|
codec: publishOption.codec,
|
|
8859
8871
|
publishOptionId: publishOption.id,
|
|
8860
|
-
selfSubAudioVideo: this.selfSubEnabled,
|
|
8861
8872
|
};
|
|
8862
8873
|
};
|
|
8863
8874
|
this.cloneTrack = (track) => {
|
|
@@ -8938,7 +8949,6 @@ class Publisher extends BasePeerConnection {
|
|
|
8938
8949
|
});
|
|
8939
8950
|
};
|
|
8940
8951
|
this.publishOptions = publishOptions;
|
|
8941
|
-
this.selfSubEnabled = opts.selfSubEnabled ?? false;
|
|
8942
8952
|
this.on('iceRestart', (iceRestart) => {
|
|
8943
8953
|
if (iceRestart.peerType !== PeerType.PUBLISHER_UNSPECIFIED)
|
|
8944
8954
|
return;
|
|
@@ -9020,13 +9030,6 @@ class Subscriber extends BasePeerConnection {
|
|
|
9020
9030
|
*/
|
|
9021
9031
|
constructor(opts) {
|
|
9022
9032
|
super(PeerType.SUBSCRIBER, opts);
|
|
9023
|
-
/**
|
|
9024
|
-
* Remote streams received from the SFU. For a self-sub case
|
|
9025
|
-
* we need to be able to distinguish between the local capture stream.
|
|
9026
|
-
* The map will never contain local streams so we can safely use it to
|
|
9027
|
-
* check if the stream is remote and dispose it when needed.
|
|
9028
|
-
*/
|
|
9029
|
-
this.trackedStreams = new WeakSet();
|
|
9030
9033
|
/**
|
|
9031
9034
|
* Restarts the ICE connection and renegotiates with the SFU.
|
|
9032
9035
|
*/
|
|
@@ -9061,7 +9064,6 @@ class Subscriber extends BasePeerConnection {
|
|
|
9061
9064
|
// example: `e3f6aaf8-b03d-4911-be36-83f47d37a76a:TRACK_TYPE_VIDEO`
|
|
9062
9065
|
const [trackId, rawTrackType] = primaryStream.id.split(':');
|
|
9063
9066
|
const participantToUpdate = this.state.participants.find((p) => p.trackLookupPrefix === trackId);
|
|
9064
|
-
const isSelfSub = !!participantToUpdate?.isLocalParticipant;
|
|
9065
9067
|
this.logger.debug(`[onTrack]: Got remote ${rawTrackType} track for userId: ${participantToUpdate?.userId}`, track.id, track);
|
|
9066
9068
|
const trackType = toTrackType(rawTrackType);
|
|
9067
9069
|
if (!trackType) {
|
|
@@ -9075,6 +9077,7 @@ class Subscriber extends BasePeerConnection {
|
|
|
9075
9077
|
track.addEventListener('unmute', () => {
|
|
9076
9078
|
this.logger.info(`[onTrack]: Track unmuted: ${trackDebugInfo}`);
|
|
9077
9079
|
this.setRemoteTrackInterrupted(trackId, trackType, false);
|
|
9080
|
+
this.onRemoteTrackUnmute?.(trackType, track.id);
|
|
9078
9081
|
});
|
|
9079
9082
|
track.addEventListener('ended', () => {
|
|
9080
9083
|
this.logger.info(`[onTrack]: Track ended: ${trackDebugInfo}`);
|
|
@@ -9085,9 +9088,6 @@ class Subscriber extends BasePeerConnection {
|
|
|
9085
9088
|
this.setRemoteTrackInterrupted(trackId, trackType, true);
|
|
9086
9089
|
}
|
|
9087
9090
|
this.trackIdToTrackType.set(track.id, trackType);
|
|
9088
|
-
if (isSelfSub) {
|
|
9089
|
-
this.trackedStreams.add(primaryStream);
|
|
9090
|
-
}
|
|
9091
9091
|
if (!participantToUpdate) {
|
|
9092
9092
|
this.logger.warn(`[onTrack]: Received track for unknown participant: ${trackId}`, e);
|
|
9093
9093
|
this.state.registerOrphanedTrack({
|
|
@@ -9103,12 +9103,6 @@ class Subscriber extends BasePeerConnection {
|
|
|
9103
9103
|
this.logger.error(`Unknown track type: ${rawTrackType}`);
|
|
9104
9104
|
return;
|
|
9105
9105
|
}
|
|
9106
|
-
// Self-sub loopback audio routes to the speaker by default, which
|
|
9107
|
-
// would echo the local user's voice. Default-mute here; consumers
|
|
9108
|
-
// (the loopback recording hook) re-enable explicitly when needed.
|
|
9109
|
-
if (isSelfSub && e.track.kind === 'audio') {
|
|
9110
|
-
e.track.enabled = false;
|
|
9111
|
-
}
|
|
9112
9106
|
// get the previous stream to dispose it later
|
|
9113
9107
|
// usually this happens during migration, when the stream is replaced
|
|
9114
9108
|
// with a new one but the old one is still in the state
|
|
@@ -9117,12 +9111,8 @@ class Subscriber extends BasePeerConnection {
|
|
|
9117
9111
|
this.state.updateParticipant(participantToUpdate.sessionId, {
|
|
9118
9112
|
[streamKindProp]: primaryStream,
|
|
9119
9113
|
});
|
|
9114
|
+
// now, dispose the previous stream if it exists
|
|
9120
9115
|
if (previousStream) {
|
|
9121
|
-
if (isSelfSub && !this.trackedStreams.has(previousStream)) {
|
|
9122
|
-
// this is the local capture stream, we don't want to dispose it
|
|
9123
|
-
this.logger.debug(`[onTrack]: Skipping cleanup of previous ${e.track.kind} stream for userId: ${participantToUpdate.userId} because it is not tracked`);
|
|
9124
|
-
return;
|
|
9125
|
-
}
|
|
9126
9116
|
this.logger.info(`[onTrack]: Cleaning up previous remote ${track.kind} tracks for userId: ${participantToUpdate.userId}`);
|
|
9127
9117
|
previousStream.getTracks().forEach((t) => {
|
|
9128
9118
|
t.stop();
|
|
@@ -9327,6 +9317,15 @@ class SfuJoinError extends Error {
|
|
|
9327
9317
|
}
|
|
9328
9318
|
}
|
|
9329
9319
|
|
|
9320
|
+
/**
|
|
9321
|
+
* An error thrown when a client-side SFU deadline (e.g., waiting for the
|
|
9322
|
+
* signaling WS to open or for the `joinResponse` to arrive) fires before
|
|
9323
|
+
* the awaited operation resolves. Allows consumers (e.g., the client event
|
|
9324
|
+
* reporter) to classify timeouts without relying on message wording.
|
|
9325
|
+
*/
|
|
9326
|
+
class SfuTimeoutError extends Error {
|
|
9327
|
+
}
|
|
9328
|
+
|
|
9330
9329
|
/**
|
|
9331
9330
|
* Creates a fresh `joinResponseTask` with a no-op rejection handler attached
|
|
9332
9331
|
* to the underlying promise. The handler marks the rejection path as handled
|
|
@@ -9432,7 +9431,7 @@ class StreamSfuClient {
|
|
|
9432
9431
|
timeoutId = setTimeout(() => {
|
|
9433
9432
|
const message = `SFU WS connection failed to open after ${this.joinResponseTimeout}ms`;
|
|
9434
9433
|
this.tracer?.trace('signal.timeout', message);
|
|
9435
|
-
reject(new
|
|
9434
|
+
reject(new SfuTimeoutError(message));
|
|
9436
9435
|
}, this.joinResponseTimeout);
|
|
9437
9436
|
}),
|
|
9438
9437
|
]));
|
|
@@ -9602,7 +9601,7 @@ class StreamSfuClient {
|
|
|
9602
9601
|
cleanupJoinSubscriptions();
|
|
9603
9602
|
const message = `Waiting for "joinResponse" has timed out after ${this.joinResponseTimeout}ms`;
|
|
9604
9603
|
this.tracer?.trace('joinRequestTimeout', message);
|
|
9605
|
-
current.reject(new
|
|
9604
|
+
current.reject(new SfuTimeoutError(message));
|
|
9606
9605
|
}, this.joinResponseTimeout);
|
|
9607
9606
|
const joinRequest = SfuRequest.create({
|
|
9608
9607
|
requestPayload: {
|
|
@@ -9819,6 +9818,10 @@ const watchCallEnded = (call) => {
|
|
|
9819
9818
|
const { callingState } = call.state;
|
|
9820
9819
|
if (callingState !== exports.CallingState.IDLE &&
|
|
9821
9820
|
callingState !== exports.CallingState.LEFT) {
|
|
9821
|
+
call.clientEventReporter.abort(call.cid, {
|
|
9822
|
+
code: 'BACKEND_LEAVE',
|
|
9823
|
+
reason: 'call.ended event received',
|
|
9824
|
+
});
|
|
9822
9825
|
call
|
|
9823
9826
|
.leave({ message: 'call.ended event received', reject: false })
|
|
9824
9827
|
.catch((err) => {
|
|
@@ -9848,6 +9851,10 @@ const watchSfuCallEnded = (call) => {
|
|
|
9848
9851
|
call.state.setEndedAt(new Date());
|
|
9849
9852
|
const reason = CallEndedReason[e.reason];
|
|
9850
9853
|
globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
|
|
9854
|
+
call.clientEventReporter.abort(call.cid, {
|
|
9855
|
+
code: 'BACKEND_LEAVE',
|
|
9856
|
+
reason: `callEnded received: ${reason}`,
|
|
9857
|
+
});
|
|
9851
9858
|
await call.leave({ message: `callEnded received: ${reason}` });
|
|
9852
9859
|
}
|
|
9853
9860
|
catch (err) {
|
|
@@ -11000,6 +11007,40 @@ class DynascaleManager {
|
|
|
11000
11007
|
}
|
|
11001
11008
|
}
|
|
11002
11009
|
|
|
11010
|
+
/**
|
|
11011
|
+
* Invokes `onFirstFrame` once when the video element renders a frame.
|
|
11012
|
+
*
|
|
11013
|
+
* Uses `requestVideoFrameCallback` when available, falling back to `loadeddata`
|
|
11014
|
+
* for browsers that don't support it.
|
|
11015
|
+
*/
|
|
11016
|
+
const createFirstVideoFrameDetector = (videoElement, onFirstFrame) => {
|
|
11017
|
+
let done = false;
|
|
11018
|
+
const notify = () => {
|
|
11019
|
+
if (done)
|
|
11020
|
+
return;
|
|
11021
|
+
done = true;
|
|
11022
|
+
onFirstFrame();
|
|
11023
|
+
};
|
|
11024
|
+
if (typeof videoElement.requestVideoFrameCallback === 'function') {
|
|
11025
|
+
const handle = videoElement.requestVideoFrameCallback(notify);
|
|
11026
|
+
return () => {
|
|
11027
|
+
done = true;
|
|
11028
|
+
videoElement.cancelVideoFrameCallback(handle);
|
|
11029
|
+
};
|
|
11030
|
+
}
|
|
11031
|
+
if (videoElement.readyState >= videoElement.HAVE_CURRENT_DATA) {
|
|
11032
|
+
queueMicrotask(notify);
|
|
11033
|
+
return () => {
|
|
11034
|
+
done = true;
|
|
11035
|
+
};
|
|
11036
|
+
}
|
|
11037
|
+
videoElement.addEventListener('loadeddata', notify, { once: true });
|
|
11038
|
+
return () => {
|
|
11039
|
+
done = true;
|
|
11040
|
+
videoElement.removeEventListener('loadeddata', notify);
|
|
11041
|
+
};
|
|
11042
|
+
};
|
|
11043
|
+
|
|
11003
11044
|
const DEFAULT_THRESHOLD = 0.35;
|
|
11004
11045
|
const DEFAULT_VIEWPORT_VISIBILITY_STATE = {
|
|
11005
11046
|
videoTrack: exports.VisibilityState.UNKNOWN,
|
|
@@ -13843,7 +13884,7 @@ class Call {
|
|
|
13843
13884
|
* Use the [`StreamVideoClient.call`](./StreamVideoClient.md/#call)
|
|
13844
13885
|
* method to construct a `Call` instance.
|
|
13845
13886
|
*/
|
|
13846
|
-
constructor({ type, id, streamClient, members, ownCapabilities, sortParticipantsBy, clientStore, ringing = false, watching = false, }) {
|
|
13887
|
+
constructor({ type, id, streamClient, clientEventReporter, members, ownCapabilities, sortParticipantsBy, clientStore, ringing = false, watching = false, }) {
|
|
13847
13888
|
/**
|
|
13848
13889
|
* The state of this call.
|
|
13849
13890
|
*/
|
|
@@ -13880,7 +13921,6 @@ class Call {
|
|
|
13880
13921
|
// maintain the order of publishing tracks to restore them after a reconnection
|
|
13881
13922
|
// it shouldn't contain duplicates
|
|
13882
13923
|
this.trackPublishOrder = [];
|
|
13883
|
-
this.selfSubEnabled = false;
|
|
13884
13924
|
this.hasJoinedOnce = false;
|
|
13885
13925
|
this.deviceSettingsAppliedOnce = false;
|
|
13886
13926
|
this.initialized = false;
|
|
@@ -14171,9 +14211,14 @@ class Call {
|
|
|
14171
14211
|
this.sfuStatsReporter = undefined;
|
|
14172
14212
|
this.lastStatsOptions = undefined;
|
|
14173
14213
|
await this.subscriber?.dispose();
|
|
14214
|
+
this.clientEventReporter.abort(this.cid, {
|
|
14215
|
+
code: 'CLIENT_ABORTED',
|
|
14216
|
+
reason: leaveReason,
|
|
14217
|
+
});
|
|
14174
14218
|
this.subscriber = undefined;
|
|
14175
14219
|
await this.publisher?.dispose();
|
|
14176
14220
|
this.publisher = undefined;
|
|
14221
|
+
this.clientEventReporter.unregisterCall(this.cid);
|
|
14177
14222
|
await this.sfuClient?.leaveAndClose(leaveReason);
|
|
14178
14223
|
this.sfuClient = undefined;
|
|
14179
14224
|
this.trackSubscriptionManager.setSfuClient(undefined);
|
|
@@ -14225,30 +14270,6 @@ class Call {
|
|
|
14225
14270
|
await Promise.all(stopOnLeavePromises);
|
|
14226
14271
|
});
|
|
14227
14272
|
};
|
|
14228
|
-
/**
|
|
14229
|
-
* The largest video publish dimension across the current publish options.
|
|
14230
|
-
*
|
|
14231
|
-
* @internal
|
|
14232
|
-
*/
|
|
14233
|
-
this.getMaxVideoPublishDimension = () => {
|
|
14234
|
-
if (!this.currentPublishOptions)
|
|
14235
|
-
return undefined;
|
|
14236
|
-
let maxDimension;
|
|
14237
|
-
let maxArea = 0;
|
|
14238
|
-
for (const opt of this.currentPublishOptions) {
|
|
14239
|
-
if (opt.trackType !== TrackType.VIDEO)
|
|
14240
|
-
continue;
|
|
14241
|
-
const dim = opt.videoDimension;
|
|
14242
|
-
if (!dim || !dim.width || !dim.height)
|
|
14243
|
-
continue;
|
|
14244
|
-
const area = dim.width * dim.height;
|
|
14245
|
-
if (area > maxArea) {
|
|
14246
|
-
maxDimension = dim;
|
|
14247
|
-
maxArea = area;
|
|
14248
|
-
}
|
|
14249
|
-
}
|
|
14250
|
-
return maxDimension;
|
|
14251
|
-
};
|
|
14252
14273
|
/**
|
|
14253
14274
|
* Update from the call response from the "call.ring" event
|
|
14254
14275
|
* @internal
|
|
@@ -14395,7 +14416,7 @@ class Call {
|
|
|
14395
14416
|
*
|
|
14396
14417
|
* @returns a promise which resolves once the call join-flow has finished.
|
|
14397
14418
|
*/
|
|
14398
|
-
this.join = async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout,
|
|
14419
|
+
this.join = async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, ...data } = {}) => {
|
|
14399
14420
|
const callingState = this.state.callingState;
|
|
14400
14421
|
if ([exports.CallingState.JOINED, exports.CallingState.JOINING].includes(callingState)) {
|
|
14401
14422
|
throw new Error(`Illegal State: call.join() shall be called only once`);
|
|
@@ -14403,15 +14424,19 @@ class Call {
|
|
|
14403
14424
|
if (data?.ring) {
|
|
14404
14425
|
this.ringingSubject.next(true);
|
|
14405
14426
|
}
|
|
14406
|
-
// we need this to be set before the callingx.joinCall() is
|
|
14407
|
-
// called to avoid registering the test call in the CallKit/Telecom
|
|
14408
|
-
this.selfSubEnabled = selfSubEnabled;
|
|
14409
14427
|
const callingX = globalThis.streamRNVideoSDK?.callingX;
|
|
14410
14428
|
if (callingX) {
|
|
14411
14429
|
// for Android/iOS, we need to start the call in the callingx library as soon as possible
|
|
14412
14430
|
await callingX.joinCall(this, this.clientStore.calls);
|
|
14413
14431
|
}
|
|
14414
14432
|
await this.setup();
|
|
14433
|
+
this.clientEventReporter.registerCall(this.cid, {
|
|
14434
|
+
callType: this.type,
|
|
14435
|
+
callId: this.id,
|
|
14436
|
+
getCallSessionId: () => this.state.session?.id ?? '',
|
|
14437
|
+
getSfuId: () => this.credentials?.server.edge_name ?? '',
|
|
14438
|
+
getUserSessionId: () => this.sfuClient?.sessionId ?? '',
|
|
14439
|
+
});
|
|
14415
14440
|
this.joinResponseTimeout = joinResponseTimeout;
|
|
14416
14441
|
this.rpcRequestTimeout = rpcRequestTimeout;
|
|
14417
14442
|
// we will count the number of join failures per SFU.
|
|
@@ -14421,39 +14446,42 @@ class Call {
|
|
|
14421
14446
|
const joinData = data;
|
|
14422
14447
|
maxJoinRetries = Math.max(maxJoinRetries, 1);
|
|
14423
14448
|
try {
|
|
14424
|
-
|
|
14425
|
-
|
|
14426
|
-
|
|
14427
|
-
|
|
14428
|
-
|
|
14429
|
-
|
|
14430
|
-
|
|
14431
|
-
|
|
14432
|
-
catch (err) {
|
|
14433
|
-
this.logger.warn(`Failed to join call (${attempt})`, this.cid);
|
|
14434
|
-
if ((err instanceof ErrorFromResponse && err.unrecoverable) ||
|
|
14435
|
-
(err instanceof SfuJoinError && err.unrecoverable)) {
|
|
14436
|
-
// if the error is unrecoverable, we should not retry as that signals
|
|
14437
|
-
// that connectivity is good, but the coordinator doesn't allow the user
|
|
14438
|
-
// to join the call due to some reason (e.g., ended call, expired token...)
|
|
14439
|
-
throw err;
|
|
14440
|
-
}
|
|
14441
|
-
// immediately switch to a different SFU in case of recoverable join error
|
|
14442
|
-
const switchSfu = err instanceof SfuJoinError &&
|
|
14443
|
-
SfuJoinError.isJoinErrorCode(err.errorEvent);
|
|
14444
|
-
const sfuId = this.credentials?.server.edge_name || '';
|
|
14445
|
-
const failures = (sfuJoinFailures.get(sfuId) || 0) + 1;
|
|
14446
|
-
sfuJoinFailures.set(sfuId, failures);
|
|
14447
|
-
if (switchSfu || failures >= 2) {
|
|
14448
|
-
joinData.migrating_from = sfuId;
|
|
14449
|
-
joinData.migrating_from_list = Array.from(sfuJoinFailures.keys());
|
|
14449
|
+
await this.clientEventReporter.withJoinLifecycle(this.cid, 'first-attempt', async () => {
|
|
14450
|
+
for (let attempt = 0; attempt < maxJoinRetries; attempt++) {
|
|
14451
|
+
try {
|
|
14452
|
+
this.logger.trace(`Joining call (${attempt})`, this.cid);
|
|
14453
|
+
await this.doJoin(data);
|
|
14454
|
+
delete joinData.migrating_from;
|
|
14455
|
+
delete joinData.migrating_from_list;
|
|
14456
|
+
return;
|
|
14450
14457
|
}
|
|
14451
|
-
|
|
14452
|
-
|
|
14458
|
+
catch (err) {
|
|
14459
|
+
this.logger.warn(`Failed to join call (${attempt})`, this.cid);
|
|
14460
|
+
if ((err instanceof ErrorFromResponse && err.unrecoverable) ||
|
|
14461
|
+
(err instanceof SfuJoinError && err.unrecoverable)) {
|
|
14462
|
+
throw err;
|
|
14463
|
+
}
|
|
14464
|
+
const switchSfu = err instanceof SfuJoinError &&
|
|
14465
|
+
SfuJoinError.isJoinErrorCode(err.errorEvent);
|
|
14466
|
+
const sfuId = this.credentials?.server.edge_name;
|
|
14467
|
+
if (sfuId) {
|
|
14468
|
+
const failures = (sfuJoinFailures.get(sfuId) || 0) + 1;
|
|
14469
|
+
sfuJoinFailures.set(sfuId, failures);
|
|
14470
|
+
if (switchSfu || failures >= 2) {
|
|
14471
|
+
joinData.migrating_from = sfuId;
|
|
14472
|
+
joinData.migrating_from_list = Array.from(sfuJoinFailures.keys());
|
|
14473
|
+
if (attempt < maxJoinRetries - 1) {
|
|
14474
|
+
this.clientEventReporter.startCorrelation(this.cid, 'first-attempt');
|
|
14475
|
+
}
|
|
14476
|
+
}
|
|
14477
|
+
}
|
|
14478
|
+
if (attempt === maxJoinRetries - 1) {
|
|
14479
|
+
throw err;
|
|
14480
|
+
}
|
|
14453
14481
|
}
|
|
14482
|
+
await sleep(retryInterval(attempt));
|
|
14454
14483
|
}
|
|
14455
|
-
|
|
14456
|
-
}
|
|
14484
|
+
});
|
|
14457
14485
|
}
|
|
14458
14486
|
catch (error) {
|
|
14459
14487
|
callingX?.endCall(this, 'error');
|
|
@@ -14482,7 +14510,7 @@ class Call {
|
|
|
14482
14510
|
performingMigration ||
|
|
14483
14511
|
data?.migrating_from) {
|
|
14484
14512
|
try {
|
|
14485
|
-
const joinResponse = await this.doJoinRequest(data);
|
|
14513
|
+
const joinResponse = await this.clientEventReporter.track(this.cid, 'CoordinatorJoin', () => this.doJoinRequest(data));
|
|
14486
14514
|
this.credentials = joinResponse.credentials;
|
|
14487
14515
|
statsOptions = joinResponse.stats_options;
|
|
14488
14516
|
this.lastStatsOptions = statsOptions;
|
|
@@ -14540,9 +14568,11 @@ class Call {
|
|
|
14540
14568
|
const preferredSubscribeOptions = !isReconnecting
|
|
14541
14569
|
? this.getPreferredSubscribeOptions()
|
|
14542
14570
|
: [];
|
|
14571
|
+
const unifiedSessionId = this.unifiedSessionId;
|
|
14572
|
+
const capabilities = Array.from(this.clientCapabilities);
|
|
14543
14573
|
try {
|
|
14544
|
-
const { callState, fastReconnectDeadlineSeconds, publishOptions } = await sfuClient.join({
|
|
14545
|
-
unifiedSessionId
|
|
14574
|
+
const { callState, fastReconnectDeadlineSeconds, publishOptions } = await this.clientEventReporter.track(this.cid, 'WSJoin', () => sfuClient.join({
|
|
14575
|
+
unifiedSessionId,
|
|
14546
14576
|
subscriberSdp,
|
|
14547
14577
|
publisherSdp,
|
|
14548
14578
|
clientDetails,
|
|
@@ -14550,9 +14580,9 @@ class Call {
|
|
|
14550
14580
|
reconnectDetails,
|
|
14551
14581
|
preferredPublishOptions,
|
|
14552
14582
|
preferredSubscribeOptions,
|
|
14553
|
-
capabilities
|
|
14583
|
+
capabilities,
|
|
14554
14584
|
source: ParticipantSource.WEBRTC_UNSPECIFIED,
|
|
14555
|
-
});
|
|
14585
|
+
}));
|
|
14556
14586
|
this.currentPublishOptions = publishOptions;
|
|
14557
14587
|
this.fastReconnectDeadlineSeconds = fastReconnectDeadlineSeconds;
|
|
14558
14588
|
if (callState) {
|
|
@@ -14764,6 +14794,16 @@ class Call {
|
|
|
14764
14794
|
// "ICE never connected" failure budget can be cleared.
|
|
14765
14795
|
this.iceFailuresWithoutConnect = 0;
|
|
14766
14796
|
},
|
|
14797
|
+
onPeerConnectionStateChange: (event) => {
|
|
14798
|
+
this.clientEventReporter.onPeerConnectionStateChange(this.cid, event);
|
|
14799
|
+
},
|
|
14800
|
+
onRemoteTrackUnmute: (trackType, trackId) => {
|
|
14801
|
+
const reportable = trackType === TrackType.AUDIO ||
|
|
14802
|
+
(isReactNative() && trackType === TrackType.VIDEO);
|
|
14803
|
+
if (!reportable)
|
|
14804
|
+
return;
|
|
14805
|
+
this.clientEventReporter.reportFirstFrame(this.cid, trackType, trackId);
|
|
14806
|
+
},
|
|
14767
14807
|
};
|
|
14768
14808
|
this.subscriber = new Subscriber(basePeerConnectionOptions);
|
|
14769
14809
|
// anonymous users can't publish anything hence, there is no need
|
|
@@ -14773,9 +14813,7 @@ class Call {
|
|
|
14773
14813
|
if (closePreviousInstances && this.publisher) {
|
|
14774
14814
|
await this.publisher.dispose();
|
|
14775
14815
|
}
|
|
14776
|
-
this.publisher = new Publisher(basePeerConnectionOptions, publishOptions
|
|
14777
|
-
selfSubEnabled: this.selfSubEnabled,
|
|
14778
|
-
});
|
|
14816
|
+
this.publisher = new Publisher(basePeerConnectionOptions, publishOptions);
|
|
14779
14817
|
}
|
|
14780
14818
|
this.statsReporter?.stop();
|
|
14781
14819
|
if (this.statsReportingIntervalInMs > 0) {
|
|
@@ -15042,7 +15080,10 @@ class Call {
|
|
|
15042
15080
|
const reconnectStartTime = Date.now();
|
|
15043
15081
|
this.reconnectStrategy = WebsocketReconnectStrategy.REJOIN;
|
|
15044
15082
|
this.state.setCallingState(exports.CallingState.RECONNECTING);
|
|
15045
|
-
|
|
15083
|
+
const joinReason = this.reconnectReason === ReconnectReason.NETWORK_BACK_ONLINE
|
|
15084
|
+
? 'network-available'
|
|
15085
|
+
: 'full-rejoin';
|
|
15086
|
+
await this.clientEventReporter.withJoinLifecycle(this.cid, joinReason, () => this.doJoin(this.joinCallData));
|
|
15046
15087
|
await this.restorePublishedTracks();
|
|
15047
15088
|
this.restoreSubscribedTracks();
|
|
15048
15089
|
this.sfuStatsReporter?.sendReconnectionTime(WebsocketReconnectStrategy.REJOIN, (Date.now() - reconnectStartTime) / 1000);
|
|
@@ -15066,11 +15107,11 @@ class Call {
|
|
|
15066
15107
|
const migrationTask = makeSafePromise(currentSfuClient.enterMigration());
|
|
15067
15108
|
try {
|
|
15068
15109
|
const currentSfu = currentSfuClient.edgeName;
|
|
15069
|
-
await this.doJoin({
|
|
15110
|
+
await this.clientEventReporter.withJoinLifecycle(this.cid, 'migration', () => this.doJoin({
|
|
15070
15111
|
...this.joinCallData,
|
|
15071
15112
|
migrating_from: currentSfu,
|
|
15072
15113
|
migrating_from_list: [currentSfu],
|
|
15073
|
-
});
|
|
15114
|
+
}));
|
|
15074
15115
|
}
|
|
15075
15116
|
finally {
|
|
15076
15117
|
// cleanup the migration_from field after the migration is complete or failed
|
|
@@ -15106,11 +15147,22 @@ class Call {
|
|
|
15106
15147
|
this.registerReconnectHandlers = () => {
|
|
15107
15148
|
// handles the legacy "goAway" event
|
|
15108
15149
|
const unregisterGoAway = this.on('goAway', () => {
|
|
15150
|
+
this.clientEventReporter.captureWsError(this.cid, {
|
|
15151
|
+
code: 'SFU_GO_AWAY',
|
|
15152
|
+
reason: 'SFU goAway received during WS join',
|
|
15153
|
+
});
|
|
15109
15154
|
this.reconnect(WebsocketReconnectStrategy.MIGRATE, ReconnectReason.GO_AWAY).catch((err) => this.logger.warn('[Reconnect] Error reconnecting', err));
|
|
15110
15155
|
});
|
|
15111
15156
|
// handles the "error" event, through which the SFU can request a reconnect
|
|
15112
15157
|
const unregisterOnError = this.on('error', (e) => {
|
|
15113
15158
|
const { reconnectStrategy: strategy, error } = e;
|
|
15159
|
+
if (!SfuJoinError.isJoinErrorCode(e)) {
|
|
15160
|
+
const code = error?.code ? ErrorCode[error.code] : undefined;
|
|
15161
|
+
this.clientEventReporter.captureWsError(this.cid, {
|
|
15162
|
+
code: code ?? 'SFU_ERROR',
|
|
15163
|
+
reason: error?.message || 'SFU error during WS join',
|
|
15164
|
+
});
|
|
15165
|
+
}
|
|
15114
15166
|
// SFU_FULL is a join error, and when emitted, although it specifies a
|
|
15115
15167
|
// `migrate` strategy, we should actually perform a REJOIN to a new SFU.
|
|
15116
15168
|
// This is now handled separately in the `call.join()` method.
|
|
@@ -15805,7 +15857,9 @@ class Call {
|
|
|
15805
15857
|
this.leave({
|
|
15806
15858
|
reject: true,
|
|
15807
15859
|
reason: 'timeout',
|
|
15808
|
-
message: `ringing timeout - ${this.isCreatedByMe
|
|
15860
|
+
message: `ringing timeout - ${this.isCreatedByMe
|
|
15861
|
+
? 'no one accepted'
|
|
15862
|
+
: `user didn't interact with incoming call screen`}`,
|
|
15809
15863
|
}).catch((err) => {
|
|
15810
15864
|
this.logger.error('Failed to drop call', err);
|
|
15811
15865
|
});
|
|
@@ -16011,15 +16065,36 @@ class Call {
|
|
|
16011
16065
|
* @param trackType the kind of video.
|
|
16012
16066
|
*/
|
|
16013
16067
|
this.bindVideoElement = (videoElement, sessionId, trackType) => {
|
|
16014
|
-
const
|
|
16015
|
-
|
|
16068
|
+
const unbindDynascale = this.dynascaleManager?.bindVideoElement(videoElement, sessionId, trackType);
|
|
16069
|
+
const stopFirstFrameDetector = this.bindFirstVideoFrameDetector(videoElement, sessionId, trackType);
|
|
16070
|
+
if (!unbindDynascale && !stopFirstFrameDetector)
|
|
16016
16071
|
return;
|
|
16072
|
+
const unbind = () => {
|
|
16073
|
+
stopFirstFrameDetector?.();
|
|
16074
|
+
unbindDynascale?.();
|
|
16075
|
+
};
|
|
16017
16076
|
this.leaveCallHooks.add(unbind);
|
|
16018
16077
|
return () => {
|
|
16019
16078
|
this.leaveCallHooks.delete(unbind);
|
|
16020
16079
|
unbind();
|
|
16021
16080
|
};
|
|
16022
16081
|
};
|
|
16082
|
+
this.bindFirstVideoFrameDetector = (videoElement, sessionId, trackType) => {
|
|
16083
|
+
if (trackType !== 'videoTrack')
|
|
16084
|
+
return;
|
|
16085
|
+
return createFirstVideoFrameDetector(videoElement, () => {
|
|
16086
|
+
this.reportFirstRenderedVideoFrame(sessionId);
|
|
16087
|
+
});
|
|
16088
|
+
};
|
|
16089
|
+
this.reportFirstRenderedVideoFrame = (sessionId) => {
|
|
16090
|
+
const participant = this.state.findParticipantBySessionId(sessionId);
|
|
16091
|
+
if (participant?.isLocalParticipant)
|
|
16092
|
+
return;
|
|
16093
|
+
const trackId = participant?.videoStream?.getVideoTracks()[0]?.id;
|
|
16094
|
+
if (!trackId)
|
|
16095
|
+
return;
|
|
16096
|
+
this.clientEventReporter.reportFirstFrame(this.cid, TrackType.VIDEO, trackId);
|
|
16097
|
+
};
|
|
16023
16098
|
/**
|
|
16024
16099
|
* Binds a DOM <audio> element to the given session id.
|
|
16025
16100
|
*
|
|
@@ -16169,6 +16244,7 @@ class Call {
|
|
|
16169
16244
|
this.ringingSubject = new rxjs.BehaviorSubject(ringing);
|
|
16170
16245
|
this.watching = watching;
|
|
16171
16246
|
this.streamClient = streamClient;
|
|
16247
|
+
this.clientEventReporter = clientEventReporter;
|
|
16172
16248
|
this.clientStore = clientStore;
|
|
16173
16249
|
this.streamClientBasePath = `/call/${this.type}/${this.id}`;
|
|
16174
16250
|
this.logger = videoLoggerSystem.getLogger('Call');
|
|
@@ -16205,12 +16281,6 @@ class Call {
|
|
|
16205
16281
|
get currentUserId() {
|
|
16206
16282
|
return this.clientStore.connectedUser?.id;
|
|
16207
16283
|
}
|
|
16208
|
-
/**
|
|
16209
|
-
* A flag indicating whether self-subscription is enabled for the call.
|
|
16210
|
-
*/
|
|
16211
|
-
get isSelfSubEnabled() {
|
|
16212
|
-
return this.selfSubEnabled;
|
|
16213
|
-
}
|
|
16214
16284
|
/**
|
|
16215
16285
|
* A flag indicating whether the call was created by the current user.
|
|
16216
16286
|
*/
|
|
@@ -17397,10 +17467,12 @@ class StreamClient {
|
|
|
17397
17467
|
this.logger.info('StreamClient.connect: this.wsConnection.connect()');
|
|
17398
17468
|
return await this.wsConnection.connect(this.defaultWSTimeout);
|
|
17399
17469
|
};
|
|
17470
|
+
this.getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
|
|
17471
|
+
"1.53.0";
|
|
17400
17472
|
this.getUserAgent = () => {
|
|
17401
17473
|
if (!this.cachedUserAgent) {
|
|
17402
17474
|
const { clientAppIdentifier = {} } = this.options;
|
|
17403
|
-
const { sdkName = 'js', sdkVersion = "1.
|
|
17475
|
+
const { sdkName = 'js', sdkVersion = "1.53.0", ...extras } = clientAppIdentifier;
|
|
17404
17476
|
this.cachedUserAgent = [
|
|
17405
17477
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
17406
17478
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|
|
@@ -17577,6 +17649,602 @@ const createTokenOrProvider = (options) => {
|
|
|
17577
17649
|
return token || tokenProvider;
|
|
17578
17650
|
};
|
|
17579
17651
|
|
|
17652
|
+
const pcKey = (cid, role) => `${cid}:${role}`;
|
|
17653
|
+
class ClientEventReporter {
|
|
17654
|
+
constructor(options) {
|
|
17655
|
+
this.logger = videoLoggerSystem.getLogger('ClientEventReporter');
|
|
17656
|
+
this.callContexts = new Map();
|
|
17657
|
+
this.joinAttemptIds = new Map();
|
|
17658
|
+
this.joinReasons = new Map();
|
|
17659
|
+
this.coordinatorPairs = new Map();
|
|
17660
|
+
this.wsPairs = new Map();
|
|
17661
|
+
this.peerConnectionPairs = new Map();
|
|
17662
|
+
this.pcEverConnected = new Map();
|
|
17663
|
+
this.firstFrameReported = new Set();
|
|
17664
|
+
/**
|
|
17665
|
+
* Starts a new coordinator connection correlation scope.
|
|
17666
|
+
*
|
|
17667
|
+
* @param userId the id of the user being connected. Captured here because
|
|
17668
|
+
* the `CoordinatorWS` stage emits before the connection flow assigns
|
|
17669
|
+
* the user to the client, so it can't be read from the client yet.
|
|
17670
|
+
*/
|
|
17671
|
+
this.startCoordinatorConnection = (userId) => {
|
|
17672
|
+
this.coordinatorConnectId = generateUUIDv4();
|
|
17673
|
+
this.coordinatorConnectUserId = userId;
|
|
17674
|
+
return this.coordinatorConnectId;
|
|
17675
|
+
};
|
|
17676
|
+
this.trackCoordinatorWs = async (op) => {
|
|
17677
|
+
this.beginCoordinatorWs();
|
|
17678
|
+
try {
|
|
17679
|
+
const result = await op();
|
|
17680
|
+
this.succeedCoordinatorWs();
|
|
17681
|
+
return result;
|
|
17682
|
+
}
|
|
17683
|
+
catch (err) {
|
|
17684
|
+
applyError(this.coordinatorWsPair, mapCoordinatorWsError(err));
|
|
17685
|
+
throw err;
|
|
17686
|
+
}
|
|
17687
|
+
};
|
|
17688
|
+
this.beginCoordinatorWs = () => {
|
|
17689
|
+
if (!this.coordinatorWsPair) {
|
|
17690
|
+
this.coordinatorWsPair = {
|
|
17691
|
+
sid: generateUUIDv4(),
|
|
17692
|
+
attempts: 0,
|
|
17693
|
+
startedAt: Date.now(),
|
|
17694
|
+
userIdSnapshot: this.coordinatorConnectUserId,
|
|
17695
|
+
};
|
|
17696
|
+
this.send({
|
|
17697
|
+
...this.buildCoordinatorWsCommon(this.coordinatorWsPair),
|
|
17698
|
+
event_type: 'initiated',
|
|
17699
|
+
});
|
|
17700
|
+
}
|
|
17701
|
+
this.coordinatorWsPair.attempts++;
|
|
17702
|
+
};
|
|
17703
|
+
this.succeedCoordinatorWs = () => {
|
|
17704
|
+
const pair = this.coordinatorWsPair;
|
|
17705
|
+
if (!pair)
|
|
17706
|
+
return;
|
|
17707
|
+
this.send({
|
|
17708
|
+
...this.buildCoordinatorWsCommon(pair),
|
|
17709
|
+
event_type: 'completed',
|
|
17710
|
+
outcome: 'success',
|
|
17711
|
+
retry_count_attempt: pair.attempts - 1,
|
|
17712
|
+
elapsed_time: Date.now() - pair.startedAt,
|
|
17713
|
+
});
|
|
17714
|
+
this.coordinatorWsPair = undefined;
|
|
17715
|
+
};
|
|
17716
|
+
this.closeCoordinatorWs = () => {
|
|
17717
|
+
const pair = this.coordinatorWsPair;
|
|
17718
|
+
if (!pair || !pair.lastError) {
|
|
17719
|
+
this.coordinatorWsPair = undefined;
|
|
17720
|
+
return;
|
|
17721
|
+
}
|
|
17722
|
+
const { reason, code } = pair.lastError;
|
|
17723
|
+
this.send({
|
|
17724
|
+
...this.buildCoordinatorWsCommon(pair),
|
|
17725
|
+
event_type: 'completed',
|
|
17726
|
+
outcome: 'failure',
|
|
17727
|
+
retry_count_attempt: pair.attempts - 1,
|
|
17728
|
+
elapsed_time: Date.now() - pair.startedAt,
|
|
17729
|
+
retry_failure_reason: reason,
|
|
17730
|
+
retry_failure_code: code,
|
|
17731
|
+
});
|
|
17732
|
+
this.coordinatorWsPair = undefined;
|
|
17733
|
+
};
|
|
17734
|
+
this.buildCoordinatorWsCommon = (pair) => ({
|
|
17735
|
+
user_id: pair.userIdSnapshot ?? this.streamClient.userID,
|
|
17736
|
+
stage: 'CoordinatorWS',
|
|
17737
|
+
stage_id: pair.sid,
|
|
17738
|
+
...(this.coordinatorConnectId && {
|
|
17739
|
+
coordinator_connect_id: this.coordinatorConnectId,
|
|
17740
|
+
}),
|
|
17741
|
+
timestamp: new Date().toISOString(),
|
|
17742
|
+
user_agent: this.streamClient.getUserAgent(),
|
|
17743
|
+
sdk_version: this.streamClient.getSdkVersion(),
|
|
17744
|
+
});
|
|
17745
|
+
this.emitMediaPermission = (cid) => {
|
|
17746
|
+
if (isReactNative() || !this.callContexts.has(cid))
|
|
17747
|
+
return;
|
|
17748
|
+
const pair = {
|
|
17749
|
+
sid: generateUUIDv4(),
|
|
17750
|
+
attempts: 0,
|
|
17751
|
+
startedAt: Date.now(),
|
|
17752
|
+
joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
|
|
17753
|
+
};
|
|
17754
|
+
this.send({
|
|
17755
|
+
...this.buildCommon(cid, 'MediaDevicePermission', pair),
|
|
17756
|
+
...this.sessionIdField(cid),
|
|
17757
|
+
microphone_permission_status: readPermissionStatus(getAudioBrowserPermission()),
|
|
17758
|
+
camera_permission_status: readPermissionStatus(getVideoBrowserPermission()),
|
|
17759
|
+
event_type: 'initiated',
|
|
17760
|
+
});
|
|
17761
|
+
};
|
|
17762
|
+
this.registerCall = (cid, ctx) => {
|
|
17763
|
+
this.callContexts.set(cid, ctx);
|
|
17764
|
+
};
|
|
17765
|
+
this.unregisterCall = (cid) => {
|
|
17766
|
+
this.callContexts.delete(cid);
|
|
17767
|
+
this.joinAttemptIds.delete(cid);
|
|
17768
|
+
this.joinReasons.delete(cid);
|
|
17769
|
+
this.coordinatorPairs.delete(cid);
|
|
17770
|
+
this.wsPairs.delete(cid);
|
|
17771
|
+
this.firstFrameReported.delete(`${cid}:FirstVideoFrame`);
|
|
17772
|
+
this.firstFrameReported.delete(`${cid}:FirstAudioFrame`);
|
|
17773
|
+
for (const role of ['publish', 'subscribe']) {
|
|
17774
|
+
const key = pcKey(cid, role);
|
|
17775
|
+
this.peerConnectionPairs.delete(key);
|
|
17776
|
+
this.pcEverConnected.delete(key);
|
|
17777
|
+
}
|
|
17778
|
+
};
|
|
17779
|
+
this.startCorrelation = (cid, joinReason) => {
|
|
17780
|
+
try {
|
|
17781
|
+
this.closeCallPairs(cid);
|
|
17782
|
+
this.joinAttemptIds.set(cid, generateUUIDv4());
|
|
17783
|
+
this.joinReasons.set(cid, joinReason);
|
|
17784
|
+
this.firstFrameReported.delete(`${cid}:FirstVideoFrame`);
|
|
17785
|
+
this.firstFrameReported.delete(`${cid}:FirstAudioFrame`);
|
|
17786
|
+
this.emitJoinInitiated(cid);
|
|
17787
|
+
this.emitMediaPermission(cid);
|
|
17788
|
+
}
|
|
17789
|
+
catch (err) {
|
|
17790
|
+
this.logger.warn('Failed to start join correlation', err);
|
|
17791
|
+
}
|
|
17792
|
+
};
|
|
17793
|
+
this.withJoinLifecycle = async (cid, joinReason, op) => {
|
|
17794
|
+
this.startCorrelation(cid, joinReason);
|
|
17795
|
+
try {
|
|
17796
|
+
return await op();
|
|
17797
|
+
}
|
|
17798
|
+
catch (err) {
|
|
17799
|
+
this.closeCallPairs(cid);
|
|
17800
|
+
throw err;
|
|
17801
|
+
}
|
|
17802
|
+
};
|
|
17803
|
+
this.track = async (cid, stage, op) => {
|
|
17804
|
+
this.beginAttempt(cid, stage);
|
|
17805
|
+
try {
|
|
17806
|
+
const result = await op();
|
|
17807
|
+
this.succeedAttempt(cid, stage);
|
|
17808
|
+
return result;
|
|
17809
|
+
}
|
|
17810
|
+
catch (err) {
|
|
17811
|
+
this.applyStageError(cid, stage, err);
|
|
17812
|
+
throw err;
|
|
17813
|
+
}
|
|
17814
|
+
};
|
|
17815
|
+
this.reportFirstFrame = (cid, trackType, trackId) => {
|
|
17816
|
+
const stage = trackType === TrackType.VIDEO
|
|
17817
|
+
? 'FirstVideoFrame'
|
|
17818
|
+
: trackType === TrackType.AUDIO
|
|
17819
|
+
? 'FirstAudioFrame'
|
|
17820
|
+
: undefined;
|
|
17821
|
+
if (!stage)
|
|
17822
|
+
return;
|
|
17823
|
+
const key = `${cid}:${stage}`;
|
|
17824
|
+
if (this.firstFrameReported.has(key))
|
|
17825
|
+
return;
|
|
17826
|
+
this.firstFrameReported.add(key);
|
|
17827
|
+
const pair = {
|
|
17828
|
+
sid: generateUUIDv4(),
|
|
17829
|
+
attempts: 0,
|
|
17830
|
+
startedAt: Date.now(),
|
|
17831
|
+
joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
|
|
17832
|
+
};
|
|
17833
|
+
const resolvedSfuId = this.getSfuId(cid);
|
|
17834
|
+
this.send({
|
|
17835
|
+
...this.buildCommon(cid, stage, pair),
|
|
17836
|
+
...this.sessionIdField(cid),
|
|
17837
|
+
...(resolvedSfuId && { sfu_id: resolvedSfuId }),
|
|
17838
|
+
track_id: trackId,
|
|
17839
|
+
event_type: 'initiated',
|
|
17840
|
+
});
|
|
17841
|
+
};
|
|
17842
|
+
this.captureWsError = (cid, opts) => {
|
|
17843
|
+
const pair = this.wsPairs.get(cid);
|
|
17844
|
+
if (!pair)
|
|
17845
|
+
return;
|
|
17846
|
+
applyError(pair, { reason: opts.reason, code: opts.code });
|
|
17847
|
+
};
|
|
17848
|
+
this.close = (cid) => {
|
|
17849
|
+
this.closeCallPairs(cid);
|
|
17850
|
+
};
|
|
17851
|
+
this.abort = (cid, opts) => {
|
|
17852
|
+
try {
|
|
17853
|
+
const { code, reason } = opts;
|
|
17854
|
+
const stageError = { code, reason };
|
|
17855
|
+
applyErrorIfAbsent(this.coordinatorPairs.get(cid), stageError);
|
|
17856
|
+
applyErrorIfAbsent(this.wsPairs.get(cid), stageError);
|
|
17857
|
+
this.failCoordinator(cid);
|
|
17858
|
+
this.failWs(cid);
|
|
17859
|
+
this.emitPeerConnectionFailure(cid, 'publish', code, reason, 'NOT_CONNECTED');
|
|
17860
|
+
this.emitPeerConnectionFailure(cid, 'subscribe', code, reason, 'NOT_CONNECTED');
|
|
17861
|
+
}
|
|
17862
|
+
catch (err) {
|
|
17863
|
+
this.logger.warn('Failed to report abort', err);
|
|
17864
|
+
}
|
|
17865
|
+
};
|
|
17866
|
+
this.closeCallPairs = (cid) => {
|
|
17867
|
+
if (this.coordinatorPairs.get(cid))
|
|
17868
|
+
this.failCoordinator(cid);
|
|
17869
|
+
if (this.wsPairs.get(cid))
|
|
17870
|
+
this.failWs(cid);
|
|
17871
|
+
for (const role of ['publish', 'subscribe']) {
|
|
17872
|
+
this.emitPeerConnectionFailure(cid, role, 'CLIENT_ABORTED', 'superseded by a new join attempt', 'NOT_CONNECTED');
|
|
17873
|
+
}
|
|
17874
|
+
};
|
|
17875
|
+
this.emitJoinInitiated = (cid) => {
|
|
17876
|
+
const joinAttemptId = this.joinAttemptIds.get(cid);
|
|
17877
|
+
if (!joinAttemptId)
|
|
17878
|
+
return;
|
|
17879
|
+
const coordinatorConnectId = this.coordinatorConnectId;
|
|
17880
|
+
this.send({
|
|
17881
|
+
user_id: this.streamClient.userID,
|
|
17882
|
+
stage: 'JoinInitiated',
|
|
17883
|
+
join_attempt_id: joinAttemptId,
|
|
17884
|
+
...(coordinatorConnectId && {
|
|
17885
|
+
coordinator_connect_id: coordinatorConnectId,
|
|
17886
|
+
}),
|
|
17887
|
+
timestamp: new Date().toISOString(),
|
|
17888
|
+
user_agent: this.streamClient.getUserAgent(),
|
|
17889
|
+
sdk_version: this.streamClient.getSdkVersion(),
|
|
17890
|
+
event_type: 'initiated',
|
|
17891
|
+
});
|
|
17892
|
+
};
|
|
17893
|
+
this.beginAttempt = (cid, stage) => {
|
|
17894
|
+
if (stage === 'CoordinatorJoin')
|
|
17895
|
+
this.beginCoordinatorAttempt(cid);
|
|
17896
|
+
else
|
|
17897
|
+
this.beginWsAttempt(cid);
|
|
17898
|
+
};
|
|
17899
|
+
this.succeedAttempt = (cid, stage) => {
|
|
17900
|
+
if (stage === 'CoordinatorJoin')
|
|
17901
|
+
this.succeedCoordinator(cid);
|
|
17902
|
+
else
|
|
17903
|
+
this.succeedWs(cid);
|
|
17904
|
+
};
|
|
17905
|
+
this.applyStageError = (cid, stage, err) => {
|
|
17906
|
+
const pair = stage === 'CoordinatorJoin'
|
|
17907
|
+
? this.coordinatorPairs.get(cid)
|
|
17908
|
+
: this.wsPairs.get(cid);
|
|
17909
|
+
applyErrorIfAbsent(pair, stage === 'CoordinatorJoin'
|
|
17910
|
+
? mapCoordinatorHttpError(err)
|
|
17911
|
+
: mapWsJoinError(err));
|
|
17912
|
+
};
|
|
17913
|
+
this.beginCoordinatorAttempt = (cid) => {
|
|
17914
|
+
let pair = this.coordinatorPairs.get(cid);
|
|
17915
|
+
if (!pair) {
|
|
17916
|
+
pair = {
|
|
17917
|
+
sid: generateUUIDv4(),
|
|
17918
|
+
attempts: 0,
|
|
17919
|
+
startedAt: Date.now(),
|
|
17920
|
+
joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
|
|
17921
|
+
joinReasonSnapshot: this.joinReasons.get(cid),
|
|
17922
|
+
};
|
|
17923
|
+
this.coordinatorPairs.set(cid, pair);
|
|
17924
|
+
this.send({
|
|
17925
|
+
...this.buildCommon(cid, 'CoordinatorJoin', pair),
|
|
17926
|
+
...(pair.joinReasonSnapshot && {
|
|
17927
|
+
join_reason: pair.joinReasonSnapshot,
|
|
17928
|
+
}),
|
|
17929
|
+
event_type: 'initiated',
|
|
17930
|
+
});
|
|
17931
|
+
}
|
|
17932
|
+
pair.lastError = undefined;
|
|
17933
|
+
pair.attempts++;
|
|
17934
|
+
};
|
|
17935
|
+
this.succeedCoordinator = (cid) => {
|
|
17936
|
+
const pair = this.coordinatorPairs.get(cid);
|
|
17937
|
+
if (!pair)
|
|
17938
|
+
return;
|
|
17939
|
+
this.send({
|
|
17940
|
+
...this.buildCommon(cid, 'CoordinatorJoin', pair),
|
|
17941
|
+
...this.sessionIdField(cid),
|
|
17942
|
+
...(pair.joinReasonSnapshot && { join_reason: pair.joinReasonSnapshot }),
|
|
17943
|
+
event_type: 'completed',
|
|
17944
|
+
outcome: 'success',
|
|
17945
|
+
retry_count_attempt: pair.attempts - 1,
|
|
17946
|
+
elapsed_time: Date.now() - pair.startedAt,
|
|
17947
|
+
});
|
|
17948
|
+
this.coordinatorPairs.delete(cid);
|
|
17949
|
+
};
|
|
17950
|
+
this.failCoordinator = (cid) => {
|
|
17951
|
+
const pair = this.coordinatorPairs.get(cid);
|
|
17952
|
+
if (!pair || !pair.lastError) {
|
|
17953
|
+
this.coordinatorPairs.delete(cid);
|
|
17954
|
+
return;
|
|
17955
|
+
}
|
|
17956
|
+
const { reason, code } = pair.lastError;
|
|
17957
|
+
this.send({
|
|
17958
|
+
...this.buildCommon(cid, 'CoordinatorJoin', pair),
|
|
17959
|
+
...this.sessionIdField(cid),
|
|
17960
|
+
...(pair.joinReasonSnapshot && { join_reason: pair.joinReasonSnapshot }),
|
|
17961
|
+
event_type: 'completed',
|
|
17962
|
+
outcome: 'failure',
|
|
17963
|
+
retry_count_attempt: pair.attempts - 1,
|
|
17964
|
+
elapsed_time: Date.now() - pair.startedAt,
|
|
17965
|
+
retry_failure_reason: reason,
|
|
17966
|
+
retry_failure_code: code,
|
|
17967
|
+
});
|
|
17968
|
+
this.coordinatorPairs.delete(cid);
|
|
17969
|
+
};
|
|
17970
|
+
this.beginWsAttempt = (cid) => {
|
|
17971
|
+
let pair = this.wsPairs.get(cid);
|
|
17972
|
+
if (!pair) {
|
|
17973
|
+
pair = {
|
|
17974
|
+
sid: generateUUIDv4(),
|
|
17975
|
+
attempts: 0,
|
|
17976
|
+
startedAt: Date.now(),
|
|
17977
|
+
joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
|
|
17978
|
+
};
|
|
17979
|
+
this.wsPairs.set(cid, pair);
|
|
17980
|
+
const sfuId = this.getSfuId(cid);
|
|
17981
|
+
this.send({
|
|
17982
|
+
...this.buildCommon(cid, 'WSJoin', pair),
|
|
17983
|
+
...this.sessionIdField(cid),
|
|
17984
|
+
...(sfuId && { sfu_id: sfuId }),
|
|
17985
|
+
event_type: 'initiated',
|
|
17986
|
+
});
|
|
17987
|
+
}
|
|
17988
|
+
pair.lastError = undefined;
|
|
17989
|
+
pair.attempts++;
|
|
17990
|
+
};
|
|
17991
|
+
this.succeedWs = (cid) => {
|
|
17992
|
+
const pair = this.wsPairs.get(cid);
|
|
17993
|
+
if (!pair)
|
|
17994
|
+
return;
|
|
17995
|
+
const sfuId = this.getSfuId(cid);
|
|
17996
|
+
this.send({
|
|
17997
|
+
...this.buildCommon(cid, 'WSJoin', pair),
|
|
17998
|
+
...this.sessionIdField(cid),
|
|
17999
|
+
...(sfuId && { sfu_id: sfuId }),
|
|
18000
|
+
event_type: 'completed',
|
|
18001
|
+
outcome: 'success',
|
|
18002
|
+
retry_count_attempt: pair.attempts - 1,
|
|
18003
|
+
elapsed_time: Date.now() - pair.startedAt,
|
|
18004
|
+
});
|
|
18005
|
+
this.wsPairs.delete(cid);
|
|
18006
|
+
};
|
|
18007
|
+
this.failWs = (cid) => {
|
|
18008
|
+
const pair = this.wsPairs.get(cid);
|
|
18009
|
+
if (!pair || !pair.lastError) {
|
|
18010
|
+
this.wsPairs.delete(cid);
|
|
18011
|
+
return;
|
|
18012
|
+
}
|
|
18013
|
+
const { reason, code } = pair.lastError;
|
|
18014
|
+
const sfuId = this.getSfuId(cid);
|
|
18015
|
+
this.send({
|
|
18016
|
+
...this.buildCommon(cid, 'WSJoin', pair),
|
|
18017
|
+
...this.sessionIdField(cid),
|
|
18018
|
+
event_type: 'completed',
|
|
18019
|
+
outcome: 'failure',
|
|
18020
|
+
retry_count_attempt: pair.attempts - 1,
|
|
18021
|
+
elapsed_time: Date.now() - pair.startedAt,
|
|
18022
|
+
...(sfuId && { sfu_id: sfuId }),
|
|
18023
|
+
retry_failure_reason: reason,
|
|
18024
|
+
retry_failure_code: code,
|
|
18025
|
+
});
|
|
18026
|
+
this.wsPairs.delete(cid);
|
|
18027
|
+
};
|
|
18028
|
+
this.onPeerConnectionStateChange = (cid, event) => {
|
|
18029
|
+
const role = event.peerType === PeerType.SUBSCRIBER ? 'subscribe' : 'publish';
|
|
18030
|
+
if (event.stateType === 'ice' && event.state === 'failed') {
|
|
18031
|
+
this.emitPeerConnectionFailure(cid, role, 'ICE_CONNECTIVITY_FAILED', 'ICE connectivity checks failed', 'FAILED');
|
|
18032
|
+
return;
|
|
18033
|
+
}
|
|
18034
|
+
if (event.stateType === 'peerConnection' && event.state === 'failed') {
|
|
18035
|
+
this.emitPeerConnectionFailure(cid, role, 'DTLS_CONNECTIVITY_FAILED', 'DTLS connectivity checks failed', 'CONNECTED');
|
|
18036
|
+
return;
|
|
18037
|
+
}
|
|
18038
|
+
if (event.stateType !== 'peerConnection')
|
|
18039
|
+
return;
|
|
18040
|
+
switch (event.state) {
|
|
18041
|
+
case 'connecting':
|
|
18042
|
+
if (this.peerConnectionPairs.has(pcKey(cid, role)))
|
|
18043
|
+
return;
|
|
18044
|
+
this.openPeerConnectionPair(cid, role);
|
|
18045
|
+
break;
|
|
18046
|
+
case 'connected':
|
|
18047
|
+
this.emitPeerConnectionSuccess(cid, role);
|
|
18048
|
+
this.pcEverConnected.set(pcKey(cid, role), true);
|
|
18049
|
+
break;
|
|
18050
|
+
}
|
|
18051
|
+
};
|
|
18052
|
+
this.openPeerConnectionPair = (cid, role) => {
|
|
18053
|
+
const key = pcKey(cid, role);
|
|
18054
|
+
const pair = {
|
|
18055
|
+
sid: generateUUIDv4(),
|
|
18056
|
+
attempts: 0,
|
|
18057
|
+
startedAt: Date.now(),
|
|
18058
|
+
joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
|
|
18059
|
+
sfuId: this.getSfuId(cid),
|
|
18060
|
+
userSessionId: this.getUserSessionId(cid),
|
|
18061
|
+
wasPreviouslyConnected: this.pcEverConnected.get(key) === true,
|
|
18062
|
+
};
|
|
18063
|
+
this.peerConnectionPairs.set(key, pair);
|
|
18064
|
+
this.send({
|
|
18065
|
+
...this.buildCommon(cid, 'PeerConnectionConnect', pair),
|
|
18066
|
+
...this.sessionIdField(cid),
|
|
18067
|
+
peer_connection: role,
|
|
18068
|
+
was_previously_connected: pair.wasPreviouslyConnected,
|
|
18069
|
+
...(pair.sfuId && { sfu_id: pair.sfuId }),
|
|
18070
|
+
...(pair.userSessionId && {
|
|
18071
|
+
user_session_id: pair.userSessionId,
|
|
18072
|
+
}),
|
|
18073
|
+
event_type: 'initiated',
|
|
18074
|
+
});
|
|
18075
|
+
};
|
|
18076
|
+
this.emitPeerConnectionSuccess = (cid, role) => {
|
|
18077
|
+
const key = pcKey(cid, role);
|
|
18078
|
+
const pair = this.peerConnectionPairs.get(key);
|
|
18079
|
+
if (!pair)
|
|
18080
|
+
return;
|
|
18081
|
+
this.send({
|
|
18082
|
+
...this.buildCommon(cid, 'PeerConnectionConnect', pair),
|
|
18083
|
+
...this.sessionIdField(cid),
|
|
18084
|
+
peer_connection: role,
|
|
18085
|
+
was_previously_connected: pair.wasPreviouslyConnected,
|
|
18086
|
+
...(pair.sfuId && { sfu_id: pair.sfuId }),
|
|
18087
|
+
...(pair.userSessionId && {
|
|
18088
|
+
user_session_id: pair.userSessionId,
|
|
18089
|
+
}),
|
|
18090
|
+
event_type: 'completed',
|
|
18091
|
+
outcome: 'success',
|
|
18092
|
+
retry_count_attempt: 0,
|
|
18093
|
+
elapsed_time: Date.now() - pair.startedAt,
|
|
18094
|
+
});
|
|
18095
|
+
this.peerConnectionPairs.delete(key);
|
|
18096
|
+
};
|
|
18097
|
+
this.emitPeerConnectionFailure = (cid, role, code, reason, iceState) => {
|
|
18098
|
+
const key = pcKey(cid, role);
|
|
18099
|
+
const pair = this.peerConnectionPairs.get(key);
|
|
18100
|
+
if (!pair)
|
|
18101
|
+
return;
|
|
18102
|
+
this.send({
|
|
18103
|
+
...this.buildCommon(cid, 'PeerConnectionConnect', pair),
|
|
18104
|
+
...this.sessionIdField(cid),
|
|
18105
|
+
peer_connection: role,
|
|
18106
|
+
was_previously_connected: pair.wasPreviouslyConnected,
|
|
18107
|
+
...(pair.userSessionId && {
|
|
18108
|
+
user_session_id: pair.userSessionId,
|
|
18109
|
+
}),
|
|
18110
|
+
...(pair.sfuId && { sfu_id: pair.sfuId }),
|
|
18111
|
+
event_type: 'completed',
|
|
18112
|
+
outcome: 'failure',
|
|
18113
|
+
retry_count_attempt: 0,
|
|
18114
|
+
elapsed_time: Date.now() - pair.startedAt,
|
|
18115
|
+
ice_state: iceState,
|
|
18116
|
+
retry_failure_reason: reason,
|
|
18117
|
+
retry_failure_code: code,
|
|
18118
|
+
});
|
|
18119
|
+
this.peerConnectionPairs.delete(key);
|
|
18120
|
+
};
|
|
18121
|
+
this.getSfuId = (cid) => this.callContexts.get(cid)?.getSfuId() ?? '';
|
|
18122
|
+
this.getUserSessionId = (cid) => this.callContexts.get(cid)?.getUserSessionId() ?? '';
|
|
18123
|
+
this.sessionIdField = (cid) => {
|
|
18124
|
+
const callSessionId = this.callContexts.get(cid)?.getCallSessionId() ?? '';
|
|
18125
|
+
return callSessionId ? { call_session_id: callSessionId } : {};
|
|
18126
|
+
};
|
|
18127
|
+
this.buildCommon = (cid, stage, pair) => {
|
|
18128
|
+
const ctx = this.callContexts.get(cid);
|
|
18129
|
+
const coordinatorConnectId = this.coordinatorConnectId;
|
|
18130
|
+
return {
|
|
18131
|
+
user_id: this.streamClient.userID,
|
|
18132
|
+
type: ctx?.callType ?? '',
|
|
18133
|
+
id: ctx?.callId ?? '',
|
|
18134
|
+
call_cid: cid,
|
|
18135
|
+
stage,
|
|
18136
|
+
stage_id: pair.sid,
|
|
18137
|
+
...(pair.joinAttemptIdSnapshot && {
|
|
18138
|
+
join_attempt_id: pair.joinAttemptIdSnapshot,
|
|
18139
|
+
}),
|
|
18140
|
+
...(coordinatorConnectId && {
|
|
18141
|
+
coordinator_connect_id: coordinatorConnectId,
|
|
18142
|
+
}),
|
|
18143
|
+
timestamp: new Date().toISOString(),
|
|
18144
|
+
user_agent: this.streamClient.getUserAgent(),
|
|
18145
|
+
sdk_version: this.streamClient.getSdkVersion(),
|
|
18146
|
+
};
|
|
18147
|
+
};
|
|
18148
|
+
this.send = (body) => {
|
|
18149
|
+
void this.sendWithRetry(body);
|
|
18150
|
+
};
|
|
18151
|
+
this.sendWithRetry = async (body) => {
|
|
18152
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
18153
|
+
try {
|
|
18154
|
+
await this.streamClient.doAxiosRequest('post', '/call_client_event', { events: [body] }, { publicEndpoint: true });
|
|
18155
|
+
return true;
|
|
18156
|
+
}
|
|
18157
|
+
catch (err) {
|
|
18158
|
+
const status = err?.response
|
|
18159
|
+
?.status;
|
|
18160
|
+
if (typeof status === 'number' && status >= 400 && status < 500) {
|
|
18161
|
+
this.logger.debug(`Client event rejected (${status}), not retrying`, body.stage, body.event_type);
|
|
18162
|
+
return false;
|
|
18163
|
+
}
|
|
18164
|
+
if (attempt === 4) {
|
|
18165
|
+
this.logger.debug('Client event delivery failed after retries', body.stage, body.event_type, err);
|
|
18166
|
+
return false;
|
|
18167
|
+
}
|
|
18168
|
+
await sleep(retryInterval(attempt));
|
|
18169
|
+
}
|
|
18170
|
+
}
|
|
18171
|
+
return false;
|
|
18172
|
+
};
|
|
18173
|
+
this.streamClient = options.streamClient;
|
|
18174
|
+
}
|
|
18175
|
+
}
|
|
18176
|
+
const readPermissionStatus = (permission) => {
|
|
18177
|
+
const state = getCurrentValue(permission.asStateObservable());
|
|
18178
|
+
switch (state) {
|
|
18179
|
+
case 'granted':
|
|
18180
|
+
return 'GRANTED';
|
|
18181
|
+
case 'denied':
|
|
18182
|
+
return 'FAILED';
|
|
18183
|
+
case 'prompting':
|
|
18184
|
+
return 'INITIATED';
|
|
18185
|
+
case 'prompt':
|
|
18186
|
+
default:
|
|
18187
|
+
return 'NOT_INITIATED';
|
|
18188
|
+
}
|
|
18189
|
+
};
|
|
18190
|
+
const errorMessage = (err) => err instanceof Error ? err.message : String(err);
|
|
18191
|
+
const applyError = (pair, next) => {
|
|
18192
|
+
if (!pair)
|
|
18193
|
+
return;
|
|
18194
|
+
pair.lastError = next;
|
|
18195
|
+
};
|
|
18196
|
+
const applyErrorIfAbsent = (pair, next) => {
|
|
18197
|
+
if (!pair || pair.lastError)
|
|
18198
|
+
return;
|
|
18199
|
+
pair.lastError = next;
|
|
18200
|
+
};
|
|
18201
|
+
const mapCoordinatorHttpError = (err) => {
|
|
18202
|
+
if (err instanceof ErrorFromResponse) {
|
|
18203
|
+
return {
|
|
18204
|
+
reason: err.message,
|
|
18205
|
+
code: err.code != null ? String(err.code) : 'SERVER_ERROR',
|
|
18206
|
+
};
|
|
18207
|
+
}
|
|
18208
|
+
return { reason: errorMessage(err), code: 'SERVER_ERROR' };
|
|
18209
|
+
};
|
|
18210
|
+
const mapCoordinatorWsError = (err) => {
|
|
18211
|
+
if (err instanceof ErrorFromResponse) {
|
|
18212
|
+
return {
|
|
18213
|
+
reason: err.message,
|
|
18214
|
+
code: err.code != null ? String(err.code) : 'SERVER_ERROR',
|
|
18215
|
+
};
|
|
18216
|
+
}
|
|
18217
|
+
if (err instanceof Error) {
|
|
18218
|
+
try {
|
|
18219
|
+
const parsed = JSON.parse(err.message);
|
|
18220
|
+
if (typeof parsed.isWSFailure === 'boolean') {
|
|
18221
|
+
return {
|
|
18222
|
+
reason: parsed.message || err.message,
|
|
18223
|
+
code: !parsed.isWSFailure && parsed.code
|
|
18224
|
+
? String(parsed.code)
|
|
18225
|
+
: 'SERVER_ERROR',
|
|
18226
|
+
};
|
|
18227
|
+
}
|
|
18228
|
+
}
|
|
18229
|
+
catch { }
|
|
18230
|
+
}
|
|
18231
|
+
return { reason: errorMessage(err), code: 'SERVER_ERROR' };
|
|
18232
|
+
};
|
|
18233
|
+
const mapWsJoinError = (err) => {
|
|
18234
|
+
if (err instanceof SfuJoinError) {
|
|
18235
|
+
const sfuError = err.errorEvent.error;
|
|
18236
|
+
return {
|
|
18237
|
+
reason: sfuError?.message || err.message,
|
|
18238
|
+
code: (sfuError && ErrorCode[sfuError.code]) || 'SFU_ERROR',
|
|
18239
|
+
};
|
|
18240
|
+
}
|
|
18241
|
+
const reason = errorMessage(err);
|
|
18242
|
+
if (err instanceof SfuTimeoutError) {
|
|
18243
|
+
return { reason, code: 'REQUEST_TIMEOUT' };
|
|
18244
|
+
}
|
|
18245
|
+
return { reason, code: 'SFU_ERROR' };
|
|
18246
|
+
};
|
|
18247
|
+
|
|
17580
18248
|
/**
|
|
17581
18249
|
* A `StreamVideoClient` instance lets you communicate with our API, and authenticate users.
|
|
17582
18250
|
*/
|
|
@@ -17640,6 +18308,7 @@ class StreamVideoClient {
|
|
|
17640
18308
|
}
|
|
17641
18309
|
call = new Call({
|
|
17642
18310
|
streamClient: this.streamClient,
|
|
18311
|
+
clientEventReporter: this.clientEventReporter,
|
|
17643
18312
|
type: e.call.type,
|
|
17644
18313
|
id: e.call.id,
|
|
17645
18314
|
members: e.members,
|
|
@@ -17709,6 +18378,8 @@ class StreamVideoClient {
|
|
|
17709
18378
|
user.id = '!anon';
|
|
17710
18379
|
return this.connectAnonymousUser(user, tokenOrProvider);
|
|
17711
18380
|
}
|
|
18381
|
+
const reporter = this.clientEventReporter;
|
|
18382
|
+
reporter.startCoordinatorConnection(user.id);
|
|
17712
18383
|
const connectUserResponse = await withoutConcurrency(this.connectionConcurrencyTag, async () => {
|
|
17713
18384
|
const client = this.streamClient;
|
|
17714
18385
|
const { onConnectUserError, persistUserOnConnectionFailure } = client.options;
|
|
@@ -17718,14 +18389,15 @@ class StreamVideoClient {
|
|
|
17718
18389
|
for (let attempt = 0; attempt < maxConnectUserRetries; attempt++) {
|
|
17719
18390
|
try {
|
|
17720
18391
|
this.logger.trace(`Connecting user (${attempt})`, user);
|
|
17721
|
-
return user.type === 'guest'
|
|
17722
|
-
?
|
|
17723
|
-
:
|
|
18392
|
+
return await reporter.trackCoordinatorWs(() => user.type === 'guest'
|
|
18393
|
+
? client.connectGuestUser(user)
|
|
18394
|
+
: client.connectUser(user, tokenOrProvider));
|
|
17724
18395
|
}
|
|
17725
18396
|
catch (err) {
|
|
17726
18397
|
this.logger.warn(`Failed to connect a user (${attempt})`, err);
|
|
17727
18398
|
errorQueue.push(err);
|
|
17728
18399
|
if (attempt === maxConnectUserRetries - 1) {
|
|
18400
|
+
reporter.closeCoordinatorWs();
|
|
17729
18401
|
onConnectUserError?.(err, errorQueue);
|
|
17730
18402
|
throw err;
|
|
17731
18403
|
}
|
|
@@ -17803,6 +18475,7 @@ class StreamVideoClient {
|
|
|
17803
18475
|
return (call ??
|
|
17804
18476
|
new Call({
|
|
17805
18477
|
streamClient: this.streamClient,
|
|
18478
|
+
clientEventReporter: this.clientEventReporter,
|
|
17806
18479
|
id: id,
|
|
17807
18480
|
type: type,
|
|
17808
18481
|
clientStore: this.writeableStateStore,
|
|
@@ -17827,6 +18500,7 @@ class StreamVideoClient {
|
|
|
17827
18500
|
for (const c of response.calls) {
|
|
17828
18501
|
const call = new Call({
|
|
17829
18502
|
streamClient: this.streamClient,
|
|
18503
|
+
clientEventReporter: this.clientEventReporter,
|
|
17830
18504
|
id: c.call.id,
|
|
17831
18505
|
type: c.call.type,
|
|
17832
18506
|
members: c.members,
|
|
@@ -17934,6 +18608,7 @@ class StreamVideoClient {
|
|
|
17934
18608
|
const [callType, callId] = call_cid.split(':');
|
|
17935
18609
|
call = new Call({
|
|
17936
18610
|
streamClient: this.streamClient,
|
|
18611
|
+
clientEventReporter: this.clientEventReporter,
|
|
17937
18612
|
type: callType,
|
|
17938
18613
|
id: callId,
|
|
17939
18614
|
clientStore: this.writeableStateStore,
|
|
@@ -17974,6 +18649,9 @@ class StreamVideoClient {
|
|
|
17974
18649
|
this.logger = videoLoggerSystem.getLogger('client');
|
|
17975
18650
|
this.rejectCallWhenBusy = clientOptions?.rejectCallWhenBusy ?? false;
|
|
17976
18651
|
this.streamClient = createCoordinatorClient(apiKey, clientOptions);
|
|
18652
|
+
this.clientEventReporter = new ClientEventReporter({
|
|
18653
|
+
streamClient: this.streamClient,
|
|
18654
|
+
});
|
|
17977
18655
|
this.writeableStateStore = new StreamVideoWriteableStateStore();
|
|
17978
18656
|
this.readOnlyStateStore = new StreamVideoReadOnlyStateStore(this.writeableStateStore);
|
|
17979
18657
|
if (typeof apiKeyOrArgs !== 'string' && apiKeyOrArgs.user) {
|
|
@@ -18087,6 +18765,7 @@ exports.ScreenShareState = ScreenShareState;
|
|
|
18087
18765
|
exports.SfuEvents = events;
|
|
18088
18766
|
exports.SfuJoinError = SfuJoinError;
|
|
18089
18767
|
exports.SfuModels = models;
|
|
18768
|
+
exports.SfuTimeoutError = SfuTimeoutError;
|
|
18090
18769
|
exports.SpeakerManager = SpeakerManager;
|
|
18091
18770
|
exports.SpeakerState = SpeakerState;
|
|
18092
18771
|
exports.StartClosedCaptionsRequestLanguageEnum = StartClosedCaptionsRequestLanguageEnum;
|