@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.browser.es.js
CHANGED
|
@@ -507,7 +507,6 @@ class ErrorFromResponse extends Error {
|
|
|
507
507
|
}
|
|
508
508
|
}
|
|
509
509
|
|
|
510
|
-
/* eslint-disable */
|
|
511
510
|
// @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
|
|
512
511
|
// @generated from protobuf file "google/protobuf/struct.proto" (package "google.protobuf", syntax proto3)
|
|
513
512
|
// tslint:disable
|
|
@@ -773,7 +772,6 @@ class ListValue$Type extends MessageType {
|
|
|
773
772
|
*/
|
|
774
773
|
const ListValue = new ListValue$Type();
|
|
775
774
|
|
|
776
|
-
/* eslint-disable */
|
|
777
775
|
// @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
|
|
778
776
|
// @generated from protobuf file "google/protobuf/timestamp.proto" (package "google.protobuf", syntax proto3)
|
|
779
777
|
// tslint:disable
|
|
@@ -1840,12 +1838,6 @@ class TrackInfo$Type extends MessageType {
|
|
|
1840
1838
|
kind: 'scalar',
|
|
1841
1839
|
T: 5 /*ScalarType.INT32*/,
|
|
1842
1840
|
},
|
|
1843
|
-
{
|
|
1844
|
-
no: 13,
|
|
1845
|
-
name: 'self_sub_audio_video',
|
|
1846
|
-
kind: 'scalar',
|
|
1847
|
-
T: 8 /*ScalarType.BOOL*/,
|
|
1848
|
-
},
|
|
1849
1841
|
]);
|
|
1850
1842
|
}
|
|
1851
1843
|
}
|
|
@@ -6648,7 +6640,7 @@ const getSdkVersion = (sdk) => {
|
|
|
6648
6640
|
return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
|
|
6649
6641
|
};
|
|
6650
6642
|
|
|
6651
|
-
const version = "1.
|
|
6643
|
+
const version = "1.53.0";
|
|
6652
6644
|
const [major, minor, patch] = version.split('.');
|
|
6653
6645
|
let sdkInfo = {
|
|
6654
6646
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -6792,7 +6784,7 @@ const getClientDetails = async () => {
|
|
|
6792
6784
|
.join(' '),
|
|
6793
6785
|
version: '',
|
|
6794
6786
|
},
|
|
6795
|
-
webrtcVersion:
|
|
6787
|
+
webrtcVersion: browserVersion,
|
|
6796
6788
|
};
|
|
6797
6789
|
};
|
|
6798
6790
|
|
|
@@ -7742,7 +7734,7 @@ class BasePeerConnection {
|
|
|
7742
7734
|
/**
|
|
7743
7735
|
* Constructs a new `BasePeerConnection` instance.
|
|
7744
7736
|
*/
|
|
7745
|
-
constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, onIceConnected, tag, enableTracing, clientPublishOptions, iceRestartDelay = 2500, statsTimestampDriftThresholdMs = 0, }) {
|
|
7737
|
+
constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, onIceConnected, onPeerConnectionStateChange, onRemoteTrackUnmute, tag, enableTracing, clientPublishOptions, iceRestartDelay = 2500, statsTimestampDriftThresholdMs = 0, }) {
|
|
7746
7738
|
this.iceHasEverConnected = false;
|
|
7747
7739
|
this.isIceRestarting = false;
|
|
7748
7740
|
this.isDisposed = false;
|
|
@@ -7896,6 +7888,10 @@ class BasePeerConnection {
|
|
|
7896
7888
|
this.onConnectionStateChange = async () => {
|
|
7897
7889
|
const state = this.pc.connectionState;
|
|
7898
7890
|
this.logger.debug(`Connection state changed`, state);
|
|
7891
|
+
this.fireOnPeerConnectionStateChange({
|
|
7892
|
+
stateType: 'peerConnection',
|
|
7893
|
+
state,
|
|
7894
|
+
});
|
|
7899
7895
|
if (this.tracer && (state === 'connected' || state === 'failed')) {
|
|
7900
7896
|
try {
|
|
7901
7897
|
const stats = await this.stats.get();
|
|
@@ -7918,8 +7914,20 @@ class BasePeerConnection {
|
|
|
7918
7914
|
this.onIceConnectionStateChange = () => {
|
|
7919
7915
|
const state = this.pc.iceConnectionState;
|
|
7920
7916
|
this.logger.debug(`ICE connection state changed`, state);
|
|
7917
|
+
this.fireOnPeerConnectionStateChange({ stateType: 'ice', state });
|
|
7921
7918
|
this.handleConnectionStateUpdate(state);
|
|
7922
7919
|
};
|
|
7920
|
+
this.fireOnPeerConnectionStateChange = (event) => {
|
|
7921
|
+
try {
|
|
7922
|
+
this.onPeerConnectionStateChange?.({
|
|
7923
|
+
peerType: this.peerType,
|
|
7924
|
+
...event,
|
|
7925
|
+
});
|
|
7926
|
+
}
|
|
7927
|
+
catch (err) {
|
|
7928
|
+
this.logger.warn('onPeerConnectionStateChange listener threw', err);
|
|
7929
|
+
}
|
|
7930
|
+
};
|
|
7923
7931
|
this.handleConnectionStateUpdate = (state) => {
|
|
7924
7932
|
const { callingState } = this.state;
|
|
7925
7933
|
if (callingState === CallingState.OFFLINE)
|
|
@@ -8034,6 +8042,8 @@ class BasePeerConnection {
|
|
|
8034
8042
|
this.tag = tag;
|
|
8035
8043
|
this.onReconnectionNeeded = onReconnectionNeeded;
|
|
8036
8044
|
this.onIceConnected = onIceConnected;
|
|
8045
|
+
this.onPeerConnectionStateChange = onPeerConnectionStateChange;
|
|
8046
|
+
this.onRemoteTrackUnmute = onRemoteTrackUnmute;
|
|
8037
8047
|
this.logger = videoLoggerSystem.getLogger(peerType === PeerType.SUBSCRIBER ? 'Subscriber' : 'Publisher', { tags: [tag] });
|
|
8038
8048
|
this.pc = this.createPeerConnection(connectionConfig);
|
|
8039
8049
|
this.stats = new StatsTracer(this.pc, peerType, this.trackIdToTrackType, statsTimestampDriftThresholdMs);
|
|
@@ -8056,6 +8066,8 @@ class BasePeerConnection {
|
|
|
8056
8066
|
this.preConnectStuckTimeout = undefined;
|
|
8057
8067
|
this.onReconnectionNeeded = undefined;
|
|
8058
8068
|
this.onIceConnected = undefined;
|
|
8069
|
+
this.onPeerConnectionStateChange = undefined;
|
|
8070
|
+
this.onRemoteTrackUnmute = undefined;
|
|
8059
8071
|
this.isDisposed = true;
|
|
8060
8072
|
this.detachEventHandlers();
|
|
8061
8073
|
this.pc.close();
|
|
@@ -8416,7 +8428,7 @@ class Publisher extends BasePeerConnection {
|
|
|
8416
8428
|
/**
|
|
8417
8429
|
* Constructs a new `Publisher` instance.
|
|
8418
8430
|
*/
|
|
8419
|
-
constructor(baseOptions, publishOptions
|
|
8431
|
+
constructor(baseOptions, publishOptions) {
|
|
8420
8432
|
super(PeerType.PUBLISHER_UNSPECIFIED, baseOptions);
|
|
8421
8433
|
this.transceiverCache = new TransceiverCache();
|
|
8422
8434
|
this.clonedTracks = new Set();
|
|
@@ -8837,7 +8849,6 @@ class Publisher extends BasePeerConnection {
|
|
|
8837
8849
|
muted: !isTrackLive,
|
|
8838
8850
|
codec: publishOption.codec,
|
|
8839
8851
|
publishOptionId: publishOption.id,
|
|
8840
|
-
selfSubAudioVideo: this.selfSubEnabled,
|
|
8841
8852
|
};
|
|
8842
8853
|
};
|
|
8843
8854
|
this.cloneTrack = (track) => {
|
|
@@ -8918,7 +8929,6 @@ class Publisher extends BasePeerConnection {
|
|
|
8918
8929
|
});
|
|
8919
8930
|
};
|
|
8920
8931
|
this.publishOptions = publishOptions;
|
|
8921
|
-
this.selfSubEnabled = opts.selfSubEnabled ?? false;
|
|
8922
8932
|
this.on('iceRestart', (iceRestart) => {
|
|
8923
8933
|
if (iceRestart.peerType !== PeerType.PUBLISHER_UNSPECIFIED)
|
|
8924
8934
|
return;
|
|
@@ -9000,13 +9010,6 @@ class Subscriber extends BasePeerConnection {
|
|
|
9000
9010
|
*/
|
|
9001
9011
|
constructor(opts) {
|
|
9002
9012
|
super(PeerType.SUBSCRIBER, opts);
|
|
9003
|
-
/**
|
|
9004
|
-
* Remote streams received from the SFU. For a self-sub case
|
|
9005
|
-
* we need to be able to distinguish between the local capture stream.
|
|
9006
|
-
* The map will never contain local streams so we can safely use it to
|
|
9007
|
-
* check if the stream is remote and dispose it when needed.
|
|
9008
|
-
*/
|
|
9009
|
-
this.trackedStreams = new WeakSet();
|
|
9010
9013
|
/**
|
|
9011
9014
|
* Restarts the ICE connection and renegotiates with the SFU.
|
|
9012
9015
|
*/
|
|
@@ -9041,7 +9044,6 @@ class Subscriber extends BasePeerConnection {
|
|
|
9041
9044
|
// example: `e3f6aaf8-b03d-4911-be36-83f47d37a76a:TRACK_TYPE_VIDEO`
|
|
9042
9045
|
const [trackId, rawTrackType] = primaryStream.id.split(':');
|
|
9043
9046
|
const participantToUpdate = this.state.participants.find((p) => p.trackLookupPrefix === trackId);
|
|
9044
|
-
const isSelfSub = !!participantToUpdate?.isLocalParticipant;
|
|
9045
9047
|
this.logger.debug(`[onTrack]: Got remote ${rawTrackType} track for userId: ${participantToUpdate?.userId}`, track.id, track);
|
|
9046
9048
|
const trackType = toTrackType(rawTrackType);
|
|
9047
9049
|
if (!trackType) {
|
|
@@ -9055,6 +9057,7 @@ class Subscriber extends BasePeerConnection {
|
|
|
9055
9057
|
track.addEventListener('unmute', () => {
|
|
9056
9058
|
this.logger.info(`[onTrack]: Track unmuted: ${trackDebugInfo}`);
|
|
9057
9059
|
this.setRemoteTrackInterrupted(trackId, trackType, false);
|
|
9060
|
+
this.onRemoteTrackUnmute?.(trackType, track.id);
|
|
9058
9061
|
});
|
|
9059
9062
|
track.addEventListener('ended', () => {
|
|
9060
9063
|
this.logger.info(`[onTrack]: Track ended: ${trackDebugInfo}`);
|
|
@@ -9065,9 +9068,6 @@ class Subscriber extends BasePeerConnection {
|
|
|
9065
9068
|
this.setRemoteTrackInterrupted(trackId, trackType, true);
|
|
9066
9069
|
}
|
|
9067
9070
|
this.trackIdToTrackType.set(track.id, trackType);
|
|
9068
|
-
if (isSelfSub) {
|
|
9069
|
-
this.trackedStreams.add(primaryStream);
|
|
9070
|
-
}
|
|
9071
9071
|
if (!participantToUpdate) {
|
|
9072
9072
|
this.logger.warn(`[onTrack]: Received track for unknown participant: ${trackId}`, e);
|
|
9073
9073
|
this.state.registerOrphanedTrack({
|
|
@@ -9083,12 +9083,6 @@ class Subscriber extends BasePeerConnection {
|
|
|
9083
9083
|
this.logger.error(`Unknown track type: ${rawTrackType}`);
|
|
9084
9084
|
return;
|
|
9085
9085
|
}
|
|
9086
|
-
// Self-sub loopback audio routes to the speaker by default, which
|
|
9087
|
-
// would echo the local user's voice. Default-mute here; consumers
|
|
9088
|
-
// (the loopback recording hook) re-enable explicitly when needed.
|
|
9089
|
-
if (isSelfSub && e.track.kind === 'audio') {
|
|
9090
|
-
e.track.enabled = false;
|
|
9091
|
-
}
|
|
9092
9086
|
// get the previous stream to dispose it later
|
|
9093
9087
|
// usually this happens during migration, when the stream is replaced
|
|
9094
9088
|
// with a new one but the old one is still in the state
|
|
@@ -9097,12 +9091,8 @@ class Subscriber extends BasePeerConnection {
|
|
|
9097
9091
|
this.state.updateParticipant(participantToUpdate.sessionId, {
|
|
9098
9092
|
[streamKindProp]: primaryStream,
|
|
9099
9093
|
});
|
|
9094
|
+
// now, dispose the previous stream if it exists
|
|
9100
9095
|
if (previousStream) {
|
|
9101
|
-
if (isSelfSub && !this.trackedStreams.has(previousStream)) {
|
|
9102
|
-
// this is the local capture stream, we don't want to dispose it
|
|
9103
|
-
this.logger.debug(`[onTrack]: Skipping cleanup of previous ${e.track.kind} stream for userId: ${participantToUpdate.userId} because it is not tracked`);
|
|
9104
|
-
return;
|
|
9105
|
-
}
|
|
9106
9096
|
this.logger.info(`[onTrack]: Cleaning up previous remote ${track.kind} tracks for userId: ${participantToUpdate.userId}`);
|
|
9107
9097
|
previousStream.getTracks().forEach((t) => {
|
|
9108
9098
|
t.stop();
|
|
@@ -9307,6 +9297,15 @@ class SfuJoinError extends Error {
|
|
|
9307
9297
|
}
|
|
9308
9298
|
}
|
|
9309
9299
|
|
|
9300
|
+
/**
|
|
9301
|
+
* An error thrown when a client-side SFU deadline (e.g., waiting for the
|
|
9302
|
+
* signaling WS to open or for the `joinResponse` to arrive) fires before
|
|
9303
|
+
* the awaited operation resolves. Allows consumers (e.g., the client event
|
|
9304
|
+
* reporter) to classify timeouts without relying on message wording.
|
|
9305
|
+
*/
|
|
9306
|
+
class SfuTimeoutError extends Error {
|
|
9307
|
+
}
|
|
9308
|
+
|
|
9310
9309
|
/**
|
|
9311
9310
|
* Creates a fresh `joinResponseTask` with a no-op rejection handler attached
|
|
9312
9311
|
* to the underlying promise. The handler marks the rejection path as handled
|
|
@@ -9412,7 +9411,7 @@ class StreamSfuClient {
|
|
|
9412
9411
|
timeoutId = setTimeout(() => {
|
|
9413
9412
|
const message = `SFU WS connection failed to open after ${this.joinResponseTimeout}ms`;
|
|
9414
9413
|
this.tracer?.trace('signal.timeout', message);
|
|
9415
|
-
reject(new
|
|
9414
|
+
reject(new SfuTimeoutError(message));
|
|
9416
9415
|
}, this.joinResponseTimeout);
|
|
9417
9416
|
}),
|
|
9418
9417
|
]));
|
|
@@ -9582,7 +9581,7 @@ class StreamSfuClient {
|
|
|
9582
9581
|
cleanupJoinSubscriptions();
|
|
9583
9582
|
const message = `Waiting for "joinResponse" has timed out after ${this.joinResponseTimeout}ms`;
|
|
9584
9583
|
this.tracer?.trace('joinRequestTimeout', message);
|
|
9585
|
-
current.reject(new
|
|
9584
|
+
current.reject(new SfuTimeoutError(message));
|
|
9586
9585
|
}, this.joinResponseTimeout);
|
|
9587
9586
|
const joinRequest = SfuRequest.create({
|
|
9588
9587
|
requestPayload: {
|
|
@@ -9799,6 +9798,10 @@ const watchCallEnded = (call) => {
|
|
|
9799
9798
|
const { callingState } = call.state;
|
|
9800
9799
|
if (callingState !== CallingState.IDLE &&
|
|
9801
9800
|
callingState !== CallingState.LEFT) {
|
|
9801
|
+
call.clientEventReporter.abort(call.cid, {
|
|
9802
|
+
code: 'BACKEND_LEAVE',
|
|
9803
|
+
reason: 'call.ended event received',
|
|
9804
|
+
});
|
|
9802
9805
|
call
|
|
9803
9806
|
.leave({ message: 'call.ended event received', reject: false })
|
|
9804
9807
|
.catch((err) => {
|
|
@@ -9828,6 +9831,10 @@ const watchSfuCallEnded = (call) => {
|
|
|
9828
9831
|
call.state.setEndedAt(new Date());
|
|
9829
9832
|
const reason = CallEndedReason[e.reason];
|
|
9830
9833
|
globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
|
|
9834
|
+
call.clientEventReporter.abort(call.cid, {
|
|
9835
|
+
code: 'BACKEND_LEAVE',
|
|
9836
|
+
reason: `callEnded received: ${reason}`,
|
|
9837
|
+
});
|
|
9831
9838
|
await call.leave({ message: `callEnded received: ${reason}` });
|
|
9832
9839
|
}
|
|
9833
9840
|
catch (err) {
|
|
@@ -10980,6 +10987,40 @@ class DynascaleManager {
|
|
|
10980
10987
|
}
|
|
10981
10988
|
}
|
|
10982
10989
|
|
|
10990
|
+
/**
|
|
10991
|
+
* Invokes `onFirstFrame` once when the video element renders a frame.
|
|
10992
|
+
*
|
|
10993
|
+
* Uses `requestVideoFrameCallback` when available, falling back to `loadeddata`
|
|
10994
|
+
* for browsers that don't support it.
|
|
10995
|
+
*/
|
|
10996
|
+
const createFirstVideoFrameDetector = (videoElement, onFirstFrame) => {
|
|
10997
|
+
let done = false;
|
|
10998
|
+
const notify = () => {
|
|
10999
|
+
if (done)
|
|
11000
|
+
return;
|
|
11001
|
+
done = true;
|
|
11002
|
+
onFirstFrame();
|
|
11003
|
+
};
|
|
11004
|
+
if (typeof videoElement.requestVideoFrameCallback === 'function') {
|
|
11005
|
+
const handle = videoElement.requestVideoFrameCallback(notify);
|
|
11006
|
+
return () => {
|
|
11007
|
+
done = true;
|
|
11008
|
+
videoElement.cancelVideoFrameCallback(handle);
|
|
11009
|
+
};
|
|
11010
|
+
}
|
|
11011
|
+
if (videoElement.readyState >= videoElement.HAVE_CURRENT_DATA) {
|
|
11012
|
+
queueMicrotask(notify);
|
|
11013
|
+
return () => {
|
|
11014
|
+
done = true;
|
|
11015
|
+
};
|
|
11016
|
+
}
|
|
11017
|
+
videoElement.addEventListener('loadeddata', notify, { once: true });
|
|
11018
|
+
return () => {
|
|
11019
|
+
done = true;
|
|
11020
|
+
videoElement.removeEventListener('loadeddata', notify);
|
|
11021
|
+
};
|
|
11022
|
+
};
|
|
11023
|
+
|
|
10983
11024
|
const DEFAULT_THRESHOLD = 0.35;
|
|
10984
11025
|
const DEFAULT_VIEWPORT_VISIBILITY_STATE = {
|
|
10985
11026
|
videoTrack: VisibilityState.UNKNOWN,
|
|
@@ -13823,7 +13864,7 @@ class Call {
|
|
|
13823
13864
|
* Use the [`StreamVideoClient.call`](./StreamVideoClient.md/#call)
|
|
13824
13865
|
* method to construct a `Call` instance.
|
|
13825
13866
|
*/
|
|
13826
|
-
constructor({ type, id, streamClient, members, ownCapabilities, sortParticipantsBy, clientStore, ringing = false, watching = false, }) {
|
|
13867
|
+
constructor({ type, id, streamClient, clientEventReporter, members, ownCapabilities, sortParticipantsBy, clientStore, ringing = false, watching = false, }) {
|
|
13827
13868
|
/**
|
|
13828
13869
|
* The state of this call.
|
|
13829
13870
|
*/
|
|
@@ -13860,7 +13901,6 @@ class Call {
|
|
|
13860
13901
|
// maintain the order of publishing tracks to restore them after a reconnection
|
|
13861
13902
|
// it shouldn't contain duplicates
|
|
13862
13903
|
this.trackPublishOrder = [];
|
|
13863
|
-
this.selfSubEnabled = false;
|
|
13864
13904
|
this.hasJoinedOnce = false;
|
|
13865
13905
|
this.deviceSettingsAppliedOnce = false;
|
|
13866
13906
|
this.initialized = false;
|
|
@@ -14151,9 +14191,14 @@ class Call {
|
|
|
14151
14191
|
this.sfuStatsReporter = undefined;
|
|
14152
14192
|
this.lastStatsOptions = undefined;
|
|
14153
14193
|
await this.subscriber?.dispose();
|
|
14194
|
+
this.clientEventReporter.abort(this.cid, {
|
|
14195
|
+
code: 'CLIENT_ABORTED',
|
|
14196
|
+
reason: leaveReason,
|
|
14197
|
+
});
|
|
14154
14198
|
this.subscriber = undefined;
|
|
14155
14199
|
await this.publisher?.dispose();
|
|
14156
14200
|
this.publisher = undefined;
|
|
14201
|
+
this.clientEventReporter.unregisterCall(this.cid);
|
|
14157
14202
|
await this.sfuClient?.leaveAndClose(leaveReason);
|
|
14158
14203
|
this.sfuClient = undefined;
|
|
14159
14204
|
this.trackSubscriptionManager.setSfuClient(undefined);
|
|
@@ -14205,30 +14250,6 @@ class Call {
|
|
|
14205
14250
|
await Promise.all(stopOnLeavePromises);
|
|
14206
14251
|
});
|
|
14207
14252
|
};
|
|
14208
|
-
/**
|
|
14209
|
-
* The largest video publish dimension across the current publish options.
|
|
14210
|
-
*
|
|
14211
|
-
* @internal
|
|
14212
|
-
*/
|
|
14213
|
-
this.getMaxVideoPublishDimension = () => {
|
|
14214
|
-
if (!this.currentPublishOptions)
|
|
14215
|
-
return undefined;
|
|
14216
|
-
let maxDimension;
|
|
14217
|
-
let maxArea = 0;
|
|
14218
|
-
for (const opt of this.currentPublishOptions) {
|
|
14219
|
-
if (opt.trackType !== TrackType.VIDEO)
|
|
14220
|
-
continue;
|
|
14221
|
-
const dim = opt.videoDimension;
|
|
14222
|
-
if (!dim || !dim.width || !dim.height)
|
|
14223
|
-
continue;
|
|
14224
|
-
const area = dim.width * dim.height;
|
|
14225
|
-
if (area > maxArea) {
|
|
14226
|
-
maxDimension = dim;
|
|
14227
|
-
maxArea = area;
|
|
14228
|
-
}
|
|
14229
|
-
}
|
|
14230
|
-
return maxDimension;
|
|
14231
|
-
};
|
|
14232
14253
|
/**
|
|
14233
14254
|
* Update from the call response from the "call.ring" event
|
|
14234
14255
|
* @internal
|
|
@@ -14375,7 +14396,7 @@ class Call {
|
|
|
14375
14396
|
*
|
|
14376
14397
|
* @returns a promise which resolves once the call join-flow has finished.
|
|
14377
14398
|
*/
|
|
14378
|
-
this.join = async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout,
|
|
14399
|
+
this.join = async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, ...data } = {}) => {
|
|
14379
14400
|
const callingState = this.state.callingState;
|
|
14380
14401
|
if ([CallingState.JOINED, CallingState.JOINING].includes(callingState)) {
|
|
14381
14402
|
throw new Error(`Illegal State: call.join() shall be called only once`);
|
|
@@ -14383,15 +14404,19 @@ class Call {
|
|
|
14383
14404
|
if (data?.ring) {
|
|
14384
14405
|
this.ringingSubject.next(true);
|
|
14385
14406
|
}
|
|
14386
|
-
// we need this to be set before the callingx.joinCall() is
|
|
14387
|
-
// called to avoid registering the test call in the CallKit/Telecom
|
|
14388
|
-
this.selfSubEnabled = selfSubEnabled;
|
|
14389
14407
|
const callingX = globalThis.streamRNVideoSDK?.callingX;
|
|
14390
14408
|
if (callingX) {
|
|
14391
14409
|
// for Android/iOS, we need to start the call in the callingx library as soon as possible
|
|
14392
14410
|
await callingX.joinCall(this, this.clientStore.calls);
|
|
14393
14411
|
}
|
|
14394
14412
|
await this.setup();
|
|
14413
|
+
this.clientEventReporter.registerCall(this.cid, {
|
|
14414
|
+
callType: this.type,
|
|
14415
|
+
callId: this.id,
|
|
14416
|
+
getCallSessionId: () => this.state.session?.id ?? '',
|
|
14417
|
+
getSfuId: () => this.credentials?.server.edge_name ?? '',
|
|
14418
|
+
getUserSessionId: () => this.sfuClient?.sessionId ?? '',
|
|
14419
|
+
});
|
|
14395
14420
|
this.joinResponseTimeout = joinResponseTimeout;
|
|
14396
14421
|
this.rpcRequestTimeout = rpcRequestTimeout;
|
|
14397
14422
|
// we will count the number of join failures per SFU.
|
|
@@ -14401,39 +14426,42 @@ class Call {
|
|
|
14401
14426
|
const joinData = data;
|
|
14402
14427
|
maxJoinRetries = Math.max(maxJoinRetries, 1);
|
|
14403
14428
|
try {
|
|
14404
|
-
|
|
14405
|
-
|
|
14406
|
-
|
|
14407
|
-
|
|
14408
|
-
|
|
14409
|
-
|
|
14410
|
-
|
|
14411
|
-
|
|
14412
|
-
catch (err) {
|
|
14413
|
-
this.logger.warn(`Failed to join call (${attempt})`, this.cid);
|
|
14414
|
-
if ((err instanceof ErrorFromResponse && err.unrecoverable) ||
|
|
14415
|
-
(err instanceof SfuJoinError && err.unrecoverable)) {
|
|
14416
|
-
// if the error is unrecoverable, we should not retry as that signals
|
|
14417
|
-
// that connectivity is good, but the coordinator doesn't allow the user
|
|
14418
|
-
// to join the call due to some reason (e.g., ended call, expired token...)
|
|
14419
|
-
throw err;
|
|
14420
|
-
}
|
|
14421
|
-
// immediately switch to a different SFU in case of recoverable join error
|
|
14422
|
-
const switchSfu = err instanceof SfuJoinError &&
|
|
14423
|
-
SfuJoinError.isJoinErrorCode(err.errorEvent);
|
|
14424
|
-
const sfuId = this.credentials?.server.edge_name || '';
|
|
14425
|
-
const failures = (sfuJoinFailures.get(sfuId) || 0) + 1;
|
|
14426
|
-
sfuJoinFailures.set(sfuId, failures);
|
|
14427
|
-
if (switchSfu || failures >= 2) {
|
|
14428
|
-
joinData.migrating_from = sfuId;
|
|
14429
|
-
joinData.migrating_from_list = Array.from(sfuJoinFailures.keys());
|
|
14429
|
+
await this.clientEventReporter.withJoinLifecycle(this.cid, 'first-attempt', async () => {
|
|
14430
|
+
for (let attempt = 0; attempt < maxJoinRetries; attempt++) {
|
|
14431
|
+
try {
|
|
14432
|
+
this.logger.trace(`Joining call (${attempt})`, this.cid);
|
|
14433
|
+
await this.doJoin(data);
|
|
14434
|
+
delete joinData.migrating_from;
|
|
14435
|
+
delete joinData.migrating_from_list;
|
|
14436
|
+
return;
|
|
14430
14437
|
}
|
|
14431
|
-
|
|
14432
|
-
|
|
14438
|
+
catch (err) {
|
|
14439
|
+
this.logger.warn(`Failed to join call (${attempt})`, this.cid);
|
|
14440
|
+
if ((err instanceof ErrorFromResponse && err.unrecoverable) ||
|
|
14441
|
+
(err instanceof SfuJoinError && err.unrecoverable)) {
|
|
14442
|
+
throw err;
|
|
14443
|
+
}
|
|
14444
|
+
const switchSfu = err instanceof SfuJoinError &&
|
|
14445
|
+
SfuJoinError.isJoinErrorCode(err.errorEvent);
|
|
14446
|
+
const sfuId = this.credentials?.server.edge_name;
|
|
14447
|
+
if (sfuId) {
|
|
14448
|
+
const failures = (sfuJoinFailures.get(sfuId) || 0) + 1;
|
|
14449
|
+
sfuJoinFailures.set(sfuId, failures);
|
|
14450
|
+
if (switchSfu || failures >= 2) {
|
|
14451
|
+
joinData.migrating_from = sfuId;
|
|
14452
|
+
joinData.migrating_from_list = Array.from(sfuJoinFailures.keys());
|
|
14453
|
+
if (attempt < maxJoinRetries - 1) {
|
|
14454
|
+
this.clientEventReporter.startCorrelation(this.cid, 'first-attempt');
|
|
14455
|
+
}
|
|
14456
|
+
}
|
|
14457
|
+
}
|
|
14458
|
+
if (attempt === maxJoinRetries - 1) {
|
|
14459
|
+
throw err;
|
|
14460
|
+
}
|
|
14433
14461
|
}
|
|
14462
|
+
await sleep(retryInterval(attempt));
|
|
14434
14463
|
}
|
|
14435
|
-
|
|
14436
|
-
}
|
|
14464
|
+
});
|
|
14437
14465
|
}
|
|
14438
14466
|
catch (error) {
|
|
14439
14467
|
callingX?.endCall(this, 'error');
|
|
@@ -14462,7 +14490,7 @@ class Call {
|
|
|
14462
14490
|
performingMigration ||
|
|
14463
14491
|
data?.migrating_from) {
|
|
14464
14492
|
try {
|
|
14465
|
-
const joinResponse = await this.doJoinRequest(data);
|
|
14493
|
+
const joinResponse = await this.clientEventReporter.track(this.cid, 'CoordinatorJoin', () => this.doJoinRequest(data));
|
|
14466
14494
|
this.credentials = joinResponse.credentials;
|
|
14467
14495
|
statsOptions = joinResponse.stats_options;
|
|
14468
14496
|
this.lastStatsOptions = statsOptions;
|
|
@@ -14520,9 +14548,11 @@ class Call {
|
|
|
14520
14548
|
const preferredSubscribeOptions = !isReconnecting
|
|
14521
14549
|
? this.getPreferredSubscribeOptions()
|
|
14522
14550
|
: [];
|
|
14551
|
+
const unifiedSessionId = this.unifiedSessionId;
|
|
14552
|
+
const capabilities = Array.from(this.clientCapabilities);
|
|
14523
14553
|
try {
|
|
14524
|
-
const { callState, fastReconnectDeadlineSeconds, publishOptions } = await sfuClient.join({
|
|
14525
|
-
unifiedSessionId
|
|
14554
|
+
const { callState, fastReconnectDeadlineSeconds, publishOptions } = await this.clientEventReporter.track(this.cid, 'WSJoin', () => sfuClient.join({
|
|
14555
|
+
unifiedSessionId,
|
|
14526
14556
|
subscriberSdp,
|
|
14527
14557
|
publisherSdp,
|
|
14528
14558
|
clientDetails,
|
|
@@ -14530,9 +14560,9 @@ class Call {
|
|
|
14530
14560
|
reconnectDetails,
|
|
14531
14561
|
preferredPublishOptions,
|
|
14532
14562
|
preferredSubscribeOptions,
|
|
14533
|
-
capabilities
|
|
14563
|
+
capabilities,
|
|
14534
14564
|
source: ParticipantSource.WEBRTC_UNSPECIFIED,
|
|
14535
|
-
});
|
|
14565
|
+
}));
|
|
14536
14566
|
this.currentPublishOptions = publishOptions;
|
|
14537
14567
|
this.fastReconnectDeadlineSeconds = fastReconnectDeadlineSeconds;
|
|
14538
14568
|
if (callState) {
|
|
@@ -14744,6 +14774,16 @@ class Call {
|
|
|
14744
14774
|
// "ICE never connected" failure budget can be cleared.
|
|
14745
14775
|
this.iceFailuresWithoutConnect = 0;
|
|
14746
14776
|
},
|
|
14777
|
+
onPeerConnectionStateChange: (event) => {
|
|
14778
|
+
this.clientEventReporter.onPeerConnectionStateChange(this.cid, event);
|
|
14779
|
+
},
|
|
14780
|
+
onRemoteTrackUnmute: (trackType, trackId) => {
|
|
14781
|
+
const reportable = trackType === TrackType.AUDIO ||
|
|
14782
|
+
(isReactNative() && trackType === TrackType.VIDEO);
|
|
14783
|
+
if (!reportable)
|
|
14784
|
+
return;
|
|
14785
|
+
this.clientEventReporter.reportFirstFrame(this.cid, trackType, trackId);
|
|
14786
|
+
},
|
|
14747
14787
|
};
|
|
14748
14788
|
this.subscriber = new Subscriber(basePeerConnectionOptions);
|
|
14749
14789
|
// anonymous users can't publish anything hence, there is no need
|
|
@@ -14753,9 +14793,7 @@ class Call {
|
|
|
14753
14793
|
if (closePreviousInstances && this.publisher) {
|
|
14754
14794
|
await this.publisher.dispose();
|
|
14755
14795
|
}
|
|
14756
|
-
this.publisher = new Publisher(basePeerConnectionOptions, publishOptions
|
|
14757
|
-
selfSubEnabled: this.selfSubEnabled,
|
|
14758
|
-
});
|
|
14796
|
+
this.publisher = new Publisher(basePeerConnectionOptions, publishOptions);
|
|
14759
14797
|
}
|
|
14760
14798
|
this.statsReporter?.stop();
|
|
14761
14799
|
if (this.statsReportingIntervalInMs > 0) {
|
|
@@ -15022,7 +15060,10 @@ class Call {
|
|
|
15022
15060
|
const reconnectStartTime = Date.now();
|
|
15023
15061
|
this.reconnectStrategy = WebsocketReconnectStrategy.REJOIN;
|
|
15024
15062
|
this.state.setCallingState(CallingState.RECONNECTING);
|
|
15025
|
-
|
|
15063
|
+
const joinReason = this.reconnectReason === ReconnectReason.NETWORK_BACK_ONLINE
|
|
15064
|
+
? 'network-available'
|
|
15065
|
+
: 'full-rejoin';
|
|
15066
|
+
await this.clientEventReporter.withJoinLifecycle(this.cid, joinReason, () => this.doJoin(this.joinCallData));
|
|
15026
15067
|
await this.restorePublishedTracks();
|
|
15027
15068
|
this.restoreSubscribedTracks();
|
|
15028
15069
|
this.sfuStatsReporter?.sendReconnectionTime(WebsocketReconnectStrategy.REJOIN, (Date.now() - reconnectStartTime) / 1000);
|
|
@@ -15046,11 +15087,11 @@ class Call {
|
|
|
15046
15087
|
const migrationTask = makeSafePromise(currentSfuClient.enterMigration());
|
|
15047
15088
|
try {
|
|
15048
15089
|
const currentSfu = currentSfuClient.edgeName;
|
|
15049
|
-
await this.doJoin({
|
|
15090
|
+
await this.clientEventReporter.withJoinLifecycle(this.cid, 'migration', () => this.doJoin({
|
|
15050
15091
|
...this.joinCallData,
|
|
15051
15092
|
migrating_from: currentSfu,
|
|
15052
15093
|
migrating_from_list: [currentSfu],
|
|
15053
|
-
});
|
|
15094
|
+
}));
|
|
15054
15095
|
}
|
|
15055
15096
|
finally {
|
|
15056
15097
|
// cleanup the migration_from field after the migration is complete or failed
|
|
@@ -15086,11 +15127,22 @@ class Call {
|
|
|
15086
15127
|
this.registerReconnectHandlers = () => {
|
|
15087
15128
|
// handles the legacy "goAway" event
|
|
15088
15129
|
const unregisterGoAway = this.on('goAway', () => {
|
|
15130
|
+
this.clientEventReporter.captureWsError(this.cid, {
|
|
15131
|
+
code: 'SFU_GO_AWAY',
|
|
15132
|
+
reason: 'SFU goAway received during WS join',
|
|
15133
|
+
});
|
|
15089
15134
|
this.reconnect(WebsocketReconnectStrategy.MIGRATE, ReconnectReason.GO_AWAY).catch((err) => this.logger.warn('[Reconnect] Error reconnecting', err));
|
|
15090
15135
|
});
|
|
15091
15136
|
// handles the "error" event, through which the SFU can request a reconnect
|
|
15092
15137
|
const unregisterOnError = this.on('error', (e) => {
|
|
15093
15138
|
const { reconnectStrategy: strategy, error } = e;
|
|
15139
|
+
if (!SfuJoinError.isJoinErrorCode(e)) {
|
|
15140
|
+
const code = error?.code ? ErrorCode[error.code] : undefined;
|
|
15141
|
+
this.clientEventReporter.captureWsError(this.cid, {
|
|
15142
|
+
code: code ?? 'SFU_ERROR',
|
|
15143
|
+
reason: error?.message || 'SFU error during WS join',
|
|
15144
|
+
});
|
|
15145
|
+
}
|
|
15094
15146
|
// SFU_FULL is a join error, and when emitted, although it specifies a
|
|
15095
15147
|
// `migrate` strategy, we should actually perform a REJOIN to a new SFU.
|
|
15096
15148
|
// This is now handled separately in the `call.join()` method.
|
|
@@ -15785,7 +15837,9 @@ class Call {
|
|
|
15785
15837
|
this.leave({
|
|
15786
15838
|
reject: true,
|
|
15787
15839
|
reason: 'timeout',
|
|
15788
|
-
message: `ringing timeout - ${this.isCreatedByMe
|
|
15840
|
+
message: `ringing timeout - ${this.isCreatedByMe
|
|
15841
|
+
? 'no one accepted'
|
|
15842
|
+
: `user didn't interact with incoming call screen`}`,
|
|
15789
15843
|
}).catch((err) => {
|
|
15790
15844
|
this.logger.error('Failed to drop call', err);
|
|
15791
15845
|
});
|
|
@@ -15991,15 +16045,36 @@ class Call {
|
|
|
15991
16045
|
* @param trackType the kind of video.
|
|
15992
16046
|
*/
|
|
15993
16047
|
this.bindVideoElement = (videoElement, sessionId, trackType) => {
|
|
15994
|
-
const
|
|
15995
|
-
|
|
16048
|
+
const unbindDynascale = this.dynascaleManager?.bindVideoElement(videoElement, sessionId, trackType);
|
|
16049
|
+
const stopFirstFrameDetector = this.bindFirstVideoFrameDetector(videoElement, sessionId, trackType);
|
|
16050
|
+
if (!unbindDynascale && !stopFirstFrameDetector)
|
|
15996
16051
|
return;
|
|
16052
|
+
const unbind = () => {
|
|
16053
|
+
stopFirstFrameDetector?.();
|
|
16054
|
+
unbindDynascale?.();
|
|
16055
|
+
};
|
|
15997
16056
|
this.leaveCallHooks.add(unbind);
|
|
15998
16057
|
return () => {
|
|
15999
16058
|
this.leaveCallHooks.delete(unbind);
|
|
16000
16059
|
unbind();
|
|
16001
16060
|
};
|
|
16002
16061
|
};
|
|
16062
|
+
this.bindFirstVideoFrameDetector = (videoElement, sessionId, trackType) => {
|
|
16063
|
+
if (trackType !== 'videoTrack')
|
|
16064
|
+
return;
|
|
16065
|
+
return createFirstVideoFrameDetector(videoElement, () => {
|
|
16066
|
+
this.reportFirstRenderedVideoFrame(sessionId);
|
|
16067
|
+
});
|
|
16068
|
+
};
|
|
16069
|
+
this.reportFirstRenderedVideoFrame = (sessionId) => {
|
|
16070
|
+
const participant = this.state.findParticipantBySessionId(sessionId);
|
|
16071
|
+
if (participant?.isLocalParticipant)
|
|
16072
|
+
return;
|
|
16073
|
+
const trackId = participant?.videoStream?.getVideoTracks()[0]?.id;
|
|
16074
|
+
if (!trackId)
|
|
16075
|
+
return;
|
|
16076
|
+
this.clientEventReporter.reportFirstFrame(this.cid, TrackType.VIDEO, trackId);
|
|
16077
|
+
};
|
|
16003
16078
|
/**
|
|
16004
16079
|
* Binds a DOM <audio> element to the given session id.
|
|
16005
16080
|
*
|
|
@@ -16149,6 +16224,7 @@ class Call {
|
|
|
16149
16224
|
this.ringingSubject = new BehaviorSubject(ringing);
|
|
16150
16225
|
this.watching = watching;
|
|
16151
16226
|
this.streamClient = streamClient;
|
|
16227
|
+
this.clientEventReporter = clientEventReporter;
|
|
16152
16228
|
this.clientStore = clientStore;
|
|
16153
16229
|
this.streamClientBasePath = `/call/${this.type}/${this.id}`;
|
|
16154
16230
|
this.logger = videoLoggerSystem.getLogger('Call');
|
|
@@ -16185,12 +16261,6 @@ class Call {
|
|
|
16185
16261
|
get currentUserId() {
|
|
16186
16262
|
return this.clientStore.connectedUser?.id;
|
|
16187
16263
|
}
|
|
16188
|
-
/**
|
|
16189
|
-
* A flag indicating whether self-subscription is enabled for the call.
|
|
16190
|
-
*/
|
|
16191
|
-
get isSelfSubEnabled() {
|
|
16192
|
-
return this.selfSubEnabled;
|
|
16193
|
-
}
|
|
16194
16264
|
/**
|
|
16195
16265
|
* A flag indicating whether the call was created by the current user.
|
|
16196
16266
|
*/
|
|
@@ -17379,10 +17449,12 @@ class StreamClient {
|
|
|
17379
17449
|
this.logger.info('StreamClient.connect: this.wsConnection.connect()');
|
|
17380
17450
|
return await this.wsConnection.connect(this.defaultWSTimeout);
|
|
17381
17451
|
};
|
|
17452
|
+
this.getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
|
|
17453
|
+
"1.53.0";
|
|
17382
17454
|
this.getUserAgent = () => {
|
|
17383
17455
|
if (!this.cachedUserAgent) {
|
|
17384
17456
|
const { clientAppIdentifier = {} } = this.options;
|
|
17385
|
-
const { sdkName = 'js', sdkVersion = "1.
|
|
17457
|
+
const { sdkName = 'js', sdkVersion = "1.53.0", ...extras } = clientAppIdentifier;
|
|
17386
17458
|
this.cachedUserAgent = [
|
|
17387
17459
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
17388
17460
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|
|
@@ -17559,6 +17631,602 @@ const createTokenOrProvider = (options) => {
|
|
|
17559
17631
|
return token || tokenProvider;
|
|
17560
17632
|
};
|
|
17561
17633
|
|
|
17634
|
+
const pcKey = (cid, role) => `${cid}:${role}`;
|
|
17635
|
+
class ClientEventReporter {
|
|
17636
|
+
constructor(options) {
|
|
17637
|
+
this.logger = videoLoggerSystem.getLogger('ClientEventReporter');
|
|
17638
|
+
this.callContexts = new Map();
|
|
17639
|
+
this.joinAttemptIds = new Map();
|
|
17640
|
+
this.joinReasons = new Map();
|
|
17641
|
+
this.coordinatorPairs = new Map();
|
|
17642
|
+
this.wsPairs = new Map();
|
|
17643
|
+
this.peerConnectionPairs = new Map();
|
|
17644
|
+
this.pcEverConnected = new Map();
|
|
17645
|
+
this.firstFrameReported = new Set();
|
|
17646
|
+
/**
|
|
17647
|
+
* Starts a new coordinator connection correlation scope.
|
|
17648
|
+
*
|
|
17649
|
+
* @param userId the id of the user being connected. Captured here because
|
|
17650
|
+
* the `CoordinatorWS` stage emits before the connection flow assigns
|
|
17651
|
+
* the user to the client, so it can't be read from the client yet.
|
|
17652
|
+
*/
|
|
17653
|
+
this.startCoordinatorConnection = (userId) => {
|
|
17654
|
+
this.coordinatorConnectId = generateUUIDv4();
|
|
17655
|
+
this.coordinatorConnectUserId = userId;
|
|
17656
|
+
return this.coordinatorConnectId;
|
|
17657
|
+
};
|
|
17658
|
+
this.trackCoordinatorWs = async (op) => {
|
|
17659
|
+
this.beginCoordinatorWs();
|
|
17660
|
+
try {
|
|
17661
|
+
const result = await op();
|
|
17662
|
+
this.succeedCoordinatorWs();
|
|
17663
|
+
return result;
|
|
17664
|
+
}
|
|
17665
|
+
catch (err) {
|
|
17666
|
+
applyError(this.coordinatorWsPair, mapCoordinatorWsError(err));
|
|
17667
|
+
throw err;
|
|
17668
|
+
}
|
|
17669
|
+
};
|
|
17670
|
+
this.beginCoordinatorWs = () => {
|
|
17671
|
+
if (!this.coordinatorWsPair) {
|
|
17672
|
+
this.coordinatorWsPair = {
|
|
17673
|
+
sid: generateUUIDv4(),
|
|
17674
|
+
attempts: 0,
|
|
17675
|
+
startedAt: Date.now(),
|
|
17676
|
+
userIdSnapshot: this.coordinatorConnectUserId,
|
|
17677
|
+
};
|
|
17678
|
+
this.send({
|
|
17679
|
+
...this.buildCoordinatorWsCommon(this.coordinatorWsPair),
|
|
17680
|
+
event_type: 'initiated',
|
|
17681
|
+
});
|
|
17682
|
+
}
|
|
17683
|
+
this.coordinatorWsPair.attempts++;
|
|
17684
|
+
};
|
|
17685
|
+
this.succeedCoordinatorWs = () => {
|
|
17686
|
+
const pair = this.coordinatorWsPair;
|
|
17687
|
+
if (!pair)
|
|
17688
|
+
return;
|
|
17689
|
+
this.send({
|
|
17690
|
+
...this.buildCoordinatorWsCommon(pair),
|
|
17691
|
+
event_type: 'completed',
|
|
17692
|
+
outcome: 'success',
|
|
17693
|
+
retry_count_attempt: pair.attempts - 1,
|
|
17694
|
+
elapsed_time: Date.now() - pair.startedAt,
|
|
17695
|
+
});
|
|
17696
|
+
this.coordinatorWsPair = undefined;
|
|
17697
|
+
};
|
|
17698
|
+
this.closeCoordinatorWs = () => {
|
|
17699
|
+
const pair = this.coordinatorWsPair;
|
|
17700
|
+
if (!pair || !pair.lastError) {
|
|
17701
|
+
this.coordinatorWsPair = undefined;
|
|
17702
|
+
return;
|
|
17703
|
+
}
|
|
17704
|
+
const { reason, code } = pair.lastError;
|
|
17705
|
+
this.send({
|
|
17706
|
+
...this.buildCoordinatorWsCommon(pair),
|
|
17707
|
+
event_type: 'completed',
|
|
17708
|
+
outcome: 'failure',
|
|
17709
|
+
retry_count_attempt: pair.attempts - 1,
|
|
17710
|
+
elapsed_time: Date.now() - pair.startedAt,
|
|
17711
|
+
retry_failure_reason: reason,
|
|
17712
|
+
retry_failure_code: code,
|
|
17713
|
+
});
|
|
17714
|
+
this.coordinatorWsPair = undefined;
|
|
17715
|
+
};
|
|
17716
|
+
this.buildCoordinatorWsCommon = (pair) => ({
|
|
17717
|
+
user_id: pair.userIdSnapshot ?? this.streamClient.userID,
|
|
17718
|
+
stage: 'CoordinatorWS',
|
|
17719
|
+
stage_id: pair.sid,
|
|
17720
|
+
...(this.coordinatorConnectId && {
|
|
17721
|
+
coordinator_connect_id: this.coordinatorConnectId,
|
|
17722
|
+
}),
|
|
17723
|
+
timestamp: new Date().toISOString(),
|
|
17724
|
+
user_agent: this.streamClient.getUserAgent(),
|
|
17725
|
+
sdk_version: this.streamClient.getSdkVersion(),
|
|
17726
|
+
});
|
|
17727
|
+
this.emitMediaPermission = (cid) => {
|
|
17728
|
+
if (isReactNative() || !this.callContexts.has(cid))
|
|
17729
|
+
return;
|
|
17730
|
+
const pair = {
|
|
17731
|
+
sid: generateUUIDv4(),
|
|
17732
|
+
attempts: 0,
|
|
17733
|
+
startedAt: Date.now(),
|
|
17734
|
+
joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
|
|
17735
|
+
};
|
|
17736
|
+
this.send({
|
|
17737
|
+
...this.buildCommon(cid, 'MediaDevicePermission', pair),
|
|
17738
|
+
...this.sessionIdField(cid),
|
|
17739
|
+
microphone_permission_status: readPermissionStatus(getAudioBrowserPermission()),
|
|
17740
|
+
camera_permission_status: readPermissionStatus(getVideoBrowserPermission()),
|
|
17741
|
+
event_type: 'initiated',
|
|
17742
|
+
});
|
|
17743
|
+
};
|
|
17744
|
+
this.registerCall = (cid, ctx) => {
|
|
17745
|
+
this.callContexts.set(cid, ctx);
|
|
17746
|
+
};
|
|
17747
|
+
this.unregisterCall = (cid) => {
|
|
17748
|
+
this.callContexts.delete(cid);
|
|
17749
|
+
this.joinAttemptIds.delete(cid);
|
|
17750
|
+
this.joinReasons.delete(cid);
|
|
17751
|
+
this.coordinatorPairs.delete(cid);
|
|
17752
|
+
this.wsPairs.delete(cid);
|
|
17753
|
+
this.firstFrameReported.delete(`${cid}:FirstVideoFrame`);
|
|
17754
|
+
this.firstFrameReported.delete(`${cid}:FirstAudioFrame`);
|
|
17755
|
+
for (const role of ['publish', 'subscribe']) {
|
|
17756
|
+
const key = pcKey(cid, role);
|
|
17757
|
+
this.peerConnectionPairs.delete(key);
|
|
17758
|
+
this.pcEverConnected.delete(key);
|
|
17759
|
+
}
|
|
17760
|
+
};
|
|
17761
|
+
this.startCorrelation = (cid, joinReason) => {
|
|
17762
|
+
try {
|
|
17763
|
+
this.closeCallPairs(cid);
|
|
17764
|
+
this.joinAttemptIds.set(cid, generateUUIDv4());
|
|
17765
|
+
this.joinReasons.set(cid, joinReason);
|
|
17766
|
+
this.firstFrameReported.delete(`${cid}:FirstVideoFrame`);
|
|
17767
|
+
this.firstFrameReported.delete(`${cid}:FirstAudioFrame`);
|
|
17768
|
+
this.emitJoinInitiated(cid);
|
|
17769
|
+
this.emitMediaPermission(cid);
|
|
17770
|
+
}
|
|
17771
|
+
catch (err) {
|
|
17772
|
+
this.logger.warn('Failed to start join correlation', err);
|
|
17773
|
+
}
|
|
17774
|
+
};
|
|
17775
|
+
this.withJoinLifecycle = async (cid, joinReason, op) => {
|
|
17776
|
+
this.startCorrelation(cid, joinReason);
|
|
17777
|
+
try {
|
|
17778
|
+
return await op();
|
|
17779
|
+
}
|
|
17780
|
+
catch (err) {
|
|
17781
|
+
this.closeCallPairs(cid);
|
|
17782
|
+
throw err;
|
|
17783
|
+
}
|
|
17784
|
+
};
|
|
17785
|
+
this.track = async (cid, stage, op) => {
|
|
17786
|
+
this.beginAttempt(cid, stage);
|
|
17787
|
+
try {
|
|
17788
|
+
const result = await op();
|
|
17789
|
+
this.succeedAttempt(cid, stage);
|
|
17790
|
+
return result;
|
|
17791
|
+
}
|
|
17792
|
+
catch (err) {
|
|
17793
|
+
this.applyStageError(cid, stage, err);
|
|
17794
|
+
throw err;
|
|
17795
|
+
}
|
|
17796
|
+
};
|
|
17797
|
+
this.reportFirstFrame = (cid, trackType, trackId) => {
|
|
17798
|
+
const stage = trackType === TrackType.VIDEO
|
|
17799
|
+
? 'FirstVideoFrame'
|
|
17800
|
+
: trackType === TrackType.AUDIO
|
|
17801
|
+
? 'FirstAudioFrame'
|
|
17802
|
+
: undefined;
|
|
17803
|
+
if (!stage)
|
|
17804
|
+
return;
|
|
17805
|
+
const key = `${cid}:${stage}`;
|
|
17806
|
+
if (this.firstFrameReported.has(key))
|
|
17807
|
+
return;
|
|
17808
|
+
this.firstFrameReported.add(key);
|
|
17809
|
+
const pair = {
|
|
17810
|
+
sid: generateUUIDv4(),
|
|
17811
|
+
attempts: 0,
|
|
17812
|
+
startedAt: Date.now(),
|
|
17813
|
+
joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
|
|
17814
|
+
};
|
|
17815
|
+
const resolvedSfuId = this.getSfuId(cid);
|
|
17816
|
+
this.send({
|
|
17817
|
+
...this.buildCommon(cid, stage, pair),
|
|
17818
|
+
...this.sessionIdField(cid),
|
|
17819
|
+
...(resolvedSfuId && { sfu_id: resolvedSfuId }),
|
|
17820
|
+
track_id: trackId,
|
|
17821
|
+
event_type: 'initiated',
|
|
17822
|
+
});
|
|
17823
|
+
};
|
|
17824
|
+
this.captureWsError = (cid, opts) => {
|
|
17825
|
+
const pair = this.wsPairs.get(cid);
|
|
17826
|
+
if (!pair)
|
|
17827
|
+
return;
|
|
17828
|
+
applyError(pair, { reason: opts.reason, code: opts.code });
|
|
17829
|
+
};
|
|
17830
|
+
this.close = (cid) => {
|
|
17831
|
+
this.closeCallPairs(cid);
|
|
17832
|
+
};
|
|
17833
|
+
this.abort = (cid, opts) => {
|
|
17834
|
+
try {
|
|
17835
|
+
const { code, reason } = opts;
|
|
17836
|
+
const stageError = { code, reason };
|
|
17837
|
+
applyErrorIfAbsent(this.coordinatorPairs.get(cid), stageError);
|
|
17838
|
+
applyErrorIfAbsent(this.wsPairs.get(cid), stageError);
|
|
17839
|
+
this.failCoordinator(cid);
|
|
17840
|
+
this.failWs(cid);
|
|
17841
|
+
this.emitPeerConnectionFailure(cid, 'publish', code, reason, 'NOT_CONNECTED');
|
|
17842
|
+
this.emitPeerConnectionFailure(cid, 'subscribe', code, reason, 'NOT_CONNECTED');
|
|
17843
|
+
}
|
|
17844
|
+
catch (err) {
|
|
17845
|
+
this.logger.warn('Failed to report abort', err);
|
|
17846
|
+
}
|
|
17847
|
+
};
|
|
17848
|
+
this.closeCallPairs = (cid) => {
|
|
17849
|
+
if (this.coordinatorPairs.get(cid))
|
|
17850
|
+
this.failCoordinator(cid);
|
|
17851
|
+
if (this.wsPairs.get(cid))
|
|
17852
|
+
this.failWs(cid);
|
|
17853
|
+
for (const role of ['publish', 'subscribe']) {
|
|
17854
|
+
this.emitPeerConnectionFailure(cid, role, 'CLIENT_ABORTED', 'superseded by a new join attempt', 'NOT_CONNECTED');
|
|
17855
|
+
}
|
|
17856
|
+
};
|
|
17857
|
+
this.emitJoinInitiated = (cid) => {
|
|
17858
|
+
const joinAttemptId = this.joinAttemptIds.get(cid);
|
|
17859
|
+
if (!joinAttemptId)
|
|
17860
|
+
return;
|
|
17861
|
+
const coordinatorConnectId = this.coordinatorConnectId;
|
|
17862
|
+
this.send({
|
|
17863
|
+
user_id: this.streamClient.userID,
|
|
17864
|
+
stage: 'JoinInitiated',
|
|
17865
|
+
join_attempt_id: joinAttemptId,
|
|
17866
|
+
...(coordinatorConnectId && {
|
|
17867
|
+
coordinator_connect_id: coordinatorConnectId,
|
|
17868
|
+
}),
|
|
17869
|
+
timestamp: new Date().toISOString(),
|
|
17870
|
+
user_agent: this.streamClient.getUserAgent(),
|
|
17871
|
+
sdk_version: this.streamClient.getSdkVersion(),
|
|
17872
|
+
event_type: 'initiated',
|
|
17873
|
+
});
|
|
17874
|
+
};
|
|
17875
|
+
this.beginAttempt = (cid, stage) => {
|
|
17876
|
+
if (stage === 'CoordinatorJoin')
|
|
17877
|
+
this.beginCoordinatorAttempt(cid);
|
|
17878
|
+
else
|
|
17879
|
+
this.beginWsAttempt(cid);
|
|
17880
|
+
};
|
|
17881
|
+
this.succeedAttempt = (cid, stage) => {
|
|
17882
|
+
if (stage === 'CoordinatorJoin')
|
|
17883
|
+
this.succeedCoordinator(cid);
|
|
17884
|
+
else
|
|
17885
|
+
this.succeedWs(cid);
|
|
17886
|
+
};
|
|
17887
|
+
this.applyStageError = (cid, stage, err) => {
|
|
17888
|
+
const pair = stage === 'CoordinatorJoin'
|
|
17889
|
+
? this.coordinatorPairs.get(cid)
|
|
17890
|
+
: this.wsPairs.get(cid);
|
|
17891
|
+
applyErrorIfAbsent(pair, stage === 'CoordinatorJoin'
|
|
17892
|
+
? mapCoordinatorHttpError(err)
|
|
17893
|
+
: mapWsJoinError(err));
|
|
17894
|
+
};
|
|
17895
|
+
this.beginCoordinatorAttempt = (cid) => {
|
|
17896
|
+
let pair = this.coordinatorPairs.get(cid);
|
|
17897
|
+
if (!pair) {
|
|
17898
|
+
pair = {
|
|
17899
|
+
sid: generateUUIDv4(),
|
|
17900
|
+
attempts: 0,
|
|
17901
|
+
startedAt: Date.now(),
|
|
17902
|
+
joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
|
|
17903
|
+
joinReasonSnapshot: this.joinReasons.get(cid),
|
|
17904
|
+
};
|
|
17905
|
+
this.coordinatorPairs.set(cid, pair);
|
|
17906
|
+
this.send({
|
|
17907
|
+
...this.buildCommon(cid, 'CoordinatorJoin', pair),
|
|
17908
|
+
...(pair.joinReasonSnapshot && {
|
|
17909
|
+
join_reason: pair.joinReasonSnapshot,
|
|
17910
|
+
}),
|
|
17911
|
+
event_type: 'initiated',
|
|
17912
|
+
});
|
|
17913
|
+
}
|
|
17914
|
+
pair.lastError = undefined;
|
|
17915
|
+
pair.attempts++;
|
|
17916
|
+
};
|
|
17917
|
+
this.succeedCoordinator = (cid) => {
|
|
17918
|
+
const pair = this.coordinatorPairs.get(cid);
|
|
17919
|
+
if (!pair)
|
|
17920
|
+
return;
|
|
17921
|
+
this.send({
|
|
17922
|
+
...this.buildCommon(cid, 'CoordinatorJoin', pair),
|
|
17923
|
+
...this.sessionIdField(cid),
|
|
17924
|
+
...(pair.joinReasonSnapshot && { join_reason: pair.joinReasonSnapshot }),
|
|
17925
|
+
event_type: 'completed',
|
|
17926
|
+
outcome: 'success',
|
|
17927
|
+
retry_count_attempt: pair.attempts - 1,
|
|
17928
|
+
elapsed_time: Date.now() - pair.startedAt,
|
|
17929
|
+
});
|
|
17930
|
+
this.coordinatorPairs.delete(cid);
|
|
17931
|
+
};
|
|
17932
|
+
this.failCoordinator = (cid) => {
|
|
17933
|
+
const pair = this.coordinatorPairs.get(cid);
|
|
17934
|
+
if (!pair || !pair.lastError) {
|
|
17935
|
+
this.coordinatorPairs.delete(cid);
|
|
17936
|
+
return;
|
|
17937
|
+
}
|
|
17938
|
+
const { reason, code } = pair.lastError;
|
|
17939
|
+
this.send({
|
|
17940
|
+
...this.buildCommon(cid, 'CoordinatorJoin', pair),
|
|
17941
|
+
...this.sessionIdField(cid),
|
|
17942
|
+
...(pair.joinReasonSnapshot && { join_reason: pair.joinReasonSnapshot }),
|
|
17943
|
+
event_type: 'completed',
|
|
17944
|
+
outcome: 'failure',
|
|
17945
|
+
retry_count_attempt: pair.attempts - 1,
|
|
17946
|
+
elapsed_time: Date.now() - pair.startedAt,
|
|
17947
|
+
retry_failure_reason: reason,
|
|
17948
|
+
retry_failure_code: code,
|
|
17949
|
+
});
|
|
17950
|
+
this.coordinatorPairs.delete(cid);
|
|
17951
|
+
};
|
|
17952
|
+
this.beginWsAttempt = (cid) => {
|
|
17953
|
+
let pair = this.wsPairs.get(cid);
|
|
17954
|
+
if (!pair) {
|
|
17955
|
+
pair = {
|
|
17956
|
+
sid: generateUUIDv4(),
|
|
17957
|
+
attempts: 0,
|
|
17958
|
+
startedAt: Date.now(),
|
|
17959
|
+
joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
|
|
17960
|
+
};
|
|
17961
|
+
this.wsPairs.set(cid, pair);
|
|
17962
|
+
const sfuId = this.getSfuId(cid);
|
|
17963
|
+
this.send({
|
|
17964
|
+
...this.buildCommon(cid, 'WSJoin', pair),
|
|
17965
|
+
...this.sessionIdField(cid),
|
|
17966
|
+
...(sfuId && { sfu_id: sfuId }),
|
|
17967
|
+
event_type: 'initiated',
|
|
17968
|
+
});
|
|
17969
|
+
}
|
|
17970
|
+
pair.lastError = undefined;
|
|
17971
|
+
pair.attempts++;
|
|
17972
|
+
};
|
|
17973
|
+
this.succeedWs = (cid) => {
|
|
17974
|
+
const pair = this.wsPairs.get(cid);
|
|
17975
|
+
if (!pair)
|
|
17976
|
+
return;
|
|
17977
|
+
const sfuId = this.getSfuId(cid);
|
|
17978
|
+
this.send({
|
|
17979
|
+
...this.buildCommon(cid, 'WSJoin', pair),
|
|
17980
|
+
...this.sessionIdField(cid),
|
|
17981
|
+
...(sfuId && { sfu_id: sfuId }),
|
|
17982
|
+
event_type: 'completed',
|
|
17983
|
+
outcome: 'success',
|
|
17984
|
+
retry_count_attempt: pair.attempts - 1,
|
|
17985
|
+
elapsed_time: Date.now() - pair.startedAt,
|
|
17986
|
+
});
|
|
17987
|
+
this.wsPairs.delete(cid);
|
|
17988
|
+
};
|
|
17989
|
+
this.failWs = (cid) => {
|
|
17990
|
+
const pair = this.wsPairs.get(cid);
|
|
17991
|
+
if (!pair || !pair.lastError) {
|
|
17992
|
+
this.wsPairs.delete(cid);
|
|
17993
|
+
return;
|
|
17994
|
+
}
|
|
17995
|
+
const { reason, code } = pair.lastError;
|
|
17996
|
+
const sfuId = this.getSfuId(cid);
|
|
17997
|
+
this.send({
|
|
17998
|
+
...this.buildCommon(cid, 'WSJoin', pair),
|
|
17999
|
+
...this.sessionIdField(cid),
|
|
18000
|
+
event_type: 'completed',
|
|
18001
|
+
outcome: 'failure',
|
|
18002
|
+
retry_count_attempt: pair.attempts - 1,
|
|
18003
|
+
elapsed_time: Date.now() - pair.startedAt,
|
|
18004
|
+
...(sfuId && { sfu_id: sfuId }),
|
|
18005
|
+
retry_failure_reason: reason,
|
|
18006
|
+
retry_failure_code: code,
|
|
18007
|
+
});
|
|
18008
|
+
this.wsPairs.delete(cid);
|
|
18009
|
+
};
|
|
18010
|
+
this.onPeerConnectionStateChange = (cid, event) => {
|
|
18011
|
+
const role = event.peerType === PeerType.SUBSCRIBER ? 'subscribe' : 'publish';
|
|
18012
|
+
if (event.stateType === 'ice' && event.state === 'failed') {
|
|
18013
|
+
this.emitPeerConnectionFailure(cid, role, 'ICE_CONNECTIVITY_FAILED', 'ICE connectivity checks failed', 'FAILED');
|
|
18014
|
+
return;
|
|
18015
|
+
}
|
|
18016
|
+
if (event.stateType === 'peerConnection' && event.state === 'failed') {
|
|
18017
|
+
this.emitPeerConnectionFailure(cid, role, 'DTLS_CONNECTIVITY_FAILED', 'DTLS connectivity checks failed', 'CONNECTED');
|
|
18018
|
+
return;
|
|
18019
|
+
}
|
|
18020
|
+
if (event.stateType !== 'peerConnection')
|
|
18021
|
+
return;
|
|
18022
|
+
switch (event.state) {
|
|
18023
|
+
case 'connecting':
|
|
18024
|
+
if (this.peerConnectionPairs.has(pcKey(cid, role)))
|
|
18025
|
+
return;
|
|
18026
|
+
this.openPeerConnectionPair(cid, role);
|
|
18027
|
+
break;
|
|
18028
|
+
case 'connected':
|
|
18029
|
+
this.emitPeerConnectionSuccess(cid, role);
|
|
18030
|
+
this.pcEverConnected.set(pcKey(cid, role), true);
|
|
18031
|
+
break;
|
|
18032
|
+
}
|
|
18033
|
+
};
|
|
18034
|
+
this.openPeerConnectionPair = (cid, role) => {
|
|
18035
|
+
const key = pcKey(cid, role);
|
|
18036
|
+
const pair = {
|
|
18037
|
+
sid: generateUUIDv4(),
|
|
18038
|
+
attempts: 0,
|
|
18039
|
+
startedAt: Date.now(),
|
|
18040
|
+
joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
|
|
18041
|
+
sfuId: this.getSfuId(cid),
|
|
18042
|
+
userSessionId: this.getUserSessionId(cid),
|
|
18043
|
+
wasPreviouslyConnected: this.pcEverConnected.get(key) === true,
|
|
18044
|
+
};
|
|
18045
|
+
this.peerConnectionPairs.set(key, pair);
|
|
18046
|
+
this.send({
|
|
18047
|
+
...this.buildCommon(cid, 'PeerConnectionConnect', pair),
|
|
18048
|
+
...this.sessionIdField(cid),
|
|
18049
|
+
peer_connection: role,
|
|
18050
|
+
was_previously_connected: pair.wasPreviouslyConnected,
|
|
18051
|
+
...(pair.sfuId && { sfu_id: pair.sfuId }),
|
|
18052
|
+
...(pair.userSessionId && {
|
|
18053
|
+
user_session_id: pair.userSessionId,
|
|
18054
|
+
}),
|
|
18055
|
+
event_type: 'initiated',
|
|
18056
|
+
});
|
|
18057
|
+
};
|
|
18058
|
+
this.emitPeerConnectionSuccess = (cid, role) => {
|
|
18059
|
+
const key = pcKey(cid, role);
|
|
18060
|
+
const pair = this.peerConnectionPairs.get(key);
|
|
18061
|
+
if (!pair)
|
|
18062
|
+
return;
|
|
18063
|
+
this.send({
|
|
18064
|
+
...this.buildCommon(cid, 'PeerConnectionConnect', pair),
|
|
18065
|
+
...this.sessionIdField(cid),
|
|
18066
|
+
peer_connection: role,
|
|
18067
|
+
was_previously_connected: pair.wasPreviouslyConnected,
|
|
18068
|
+
...(pair.sfuId && { sfu_id: pair.sfuId }),
|
|
18069
|
+
...(pair.userSessionId && {
|
|
18070
|
+
user_session_id: pair.userSessionId,
|
|
18071
|
+
}),
|
|
18072
|
+
event_type: 'completed',
|
|
18073
|
+
outcome: 'success',
|
|
18074
|
+
retry_count_attempt: 0,
|
|
18075
|
+
elapsed_time: Date.now() - pair.startedAt,
|
|
18076
|
+
});
|
|
18077
|
+
this.peerConnectionPairs.delete(key);
|
|
18078
|
+
};
|
|
18079
|
+
this.emitPeerConnectionFailure = (cid, role, code, reason, iceState) => {
|
|
18080
|
+
const key = pcKey(cid, role);
|
|
18081
|
+
const pair = this.peerConnectionPairs.get(key);
|
|
18082
|
+
if (!pair)
|
|
18083
|
+
return;
|
|
18084
|
+
this.send({
|
|
18085
|
+
...this.buildCommon(cid, 'PeerConnectionConnect', pair),
|
|
18086
|
+
...this.sessionIdField(cid),
|
|
18087
|
+
peer_connection: role,
|
|
18088
|
+
was_previously_connected: pair.wasPreviouslyConnected,
|
|
18089
|
+
...(pair.userSessionId && {
|
|
18090
|
+
user_session_id: pair.userSessionId,
|
|
18091
|
+
}),
|
|
18092
|
+
...(pair.sfuId && { sfu_id: pair.sfuId }),
|
|
18093
|
+
event_type: 'completed',
|
|
18094
|
+
outcome: 'failure',
|
|
18095
|
+
retry_count_attempt: 0,
|
|
18096
|
+
elapsed_time: Date.now() - pair.startedAt,
|
|
18097
|
+
ice_state: iceState,
|
|
18098
|
+
retry_failure_reason: reason,
|
|
18099
|
+
retry_failure_code: code,
|
|
18100
|
+
});
|
|
18101
|
+
this.peerConnectionPairs.delete(key);
|
|
18102
|
+
};
|
|
18103
|
+
this.getSfuId = (cid) => this.callContexts.get(cid)?.getSfuId() ?? '';
|
|
18104
|
+
this.getUserSessionId = (cid) => this.callContexts.get(cid)?.getUserSessionId() ?? '';
|
|
18105
|
+
this.sessionIdField = (cid) => {
|
|
18106
|
+
const callSessionId = this.callContexts.get(cid)?.getCallSessionId() ?? '';
|
|
18107
|
+
return callSessionId ? { call_session_id: callSessionId } : {};
|
|
18108
|
+
};
|
|
18109
|
+
this.buildCommon = (cid, stage, pair) => {
|
|
18110
|
+
const ctx = this.callContexts.get(cid);
|
|
18111
|
+
const coordinatorConnectId = this.coordinatorConnectId;
|
|
18112
|
+
return {
|
|
18113
|
+
user_id: this.streamClient.userID,
|
|
18114
|
+
type: ctx?.callType ?? '',
|
|
18115
|
+
id: ctx?.callId ?? '',
|
|
18116
|
+
call_cid: cid,
|
|
18117
|
+
stage,
|
|
18118
|
+
stage_id: pair.sid,
|
|
18119
|
+
...(pair.joinAttemptIdSnapshot && {
|
|
18120
|
+
join_attempt_id: pair.joinAttemptIdSnapshot,
|
|
18121
|
+
}),
|
|
18122
|
+
...(coordinatorConnectId && {
|
|
18123
|
+
coordinator_connect_id: coordinatorConnectId,
|
|
18124
|
+
}),
|
|
18125
|
+
timestamp: new Date().toISOString(),
|
|
18126
|
+
user_agent: this.streamClient.getUserAgent(),
|
|
18127
|
+
sdk_version: this.streamClient.getSdkVersion(),
|
|
18128
|
+
};
|
|
18129
|
+
};
|
|
18130
|
+
this.send = (body) => {
|
|
18131
|
+
void this.sendWithRetry(body);
|
|
18132
|
+
};
|
|
18133
|
+
this.sendWithRetry = async (body) => {
|
|
18134
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
18135
|
+
try {
|
|
18136
|
+
await this.streamClient.doAxiosRequest('post', '/call_client_event', { events: [body] }, { publicEndpoint: true });
|
|
18137
|
+
return true;
|
|
18138
|
+
}
|
|
18139
|
+
catch (err) {
|
|
18140
|
+
const status = err?.response
|
|
18141
|
+
?.status;
|
|
18142
|
+
if (typeof status === 'number' && status >= 400 && status < 500) {
|
|
18143
|
+
this.logger.debug(`Client event rejected (${status}), not retrying`, body.stage, body.event_type);
|
|
18144
|
+
return false;
|
|
18145
|
+
}
|
|
18146
|
+
if (attempt === 4) {
|
|
18147
|
+
this.logger.debug('Client event delivery failed after retries', body.stage, body.event_type, err);
|
|
18148
|
+
return false;
|
|
18149
|
+
}
|
|
18150
|
+
await sleep(retryInterval(attempt));
|
|
18151
|
+
}
|
|
18152
|
+
}
|
|
18153
|
+
return false;
|
|
18154
|
+
};
|
|
18155
|
+
this.streamClient = options.streamClient;
|
|
18156
|
+
}
|
|
18157
|
+
}
|
|
18158
|
+
const readPermissionStatus = (permission) => {
|
|
18159
|
+
const state = getCurrentValue(permission.asStateObservable());
|
|
18160
|
+
switch (state) {
|
|
18161
|
+
case 'granted':
|
|
18162
|
+
return 'GRANTED';
|
|
18163
|
+
case 'denied':
|
|
18164
|
+
return 'FAILED';
|
|
18165
|
+
case 'prompting':
|
|
18166
|
+
return 'INITIATED';
|
|
18167
|
+
case 'prompt':
|
|
18168
|
+
default:
|
|
18169
|
+
return 'NOT_INITIATED';
|
|
18170
|
+
}
|
|
18171
|
+
};
|
|
18172
|
+
const errorMessage = (err) => err instanceof Error ? err.message : String(err);
|
|
18173
|
+
const applyError = (pair, next) => {
|
|
18174
|
+
if (!pair)
|
|
18175
|
+
return;
|
|
18176
|
+
pair.lastError = next;
|
|
18177
|
+
};
|
|
18178
|
+
const applyErrorIfAbsent = (pair, next) => {
|
|
18179
|
+
if (!pair || pair.lastError)
|
|
18180
|
+
return;
|
|
18181
|
+
pair.lastError = next;
|
|
18182
|
+
};
|
|
18183
|
+
const mapCoordinatorHttpError = (err) => {
|
|
18184
|
+
if (err instanceof ErrorFromResponse) {
|
|
18185
|
+
return {
|
|
18186
|
+
reason: err.message,
|
|
18187
|
+
code: err.code != null ? String(err.code) : 'SERVER_ERROR',
|
|
18188
|
+
};
|
|
18189
|
+
}
|
|
18190
|
+
return { reason: errorMessage(err), code: 'SERVER_ERROR' };
|
|
18191
|
+
};
|
|
18192
|
+
const mapCoordinatorWsError = (err) => {
|
|
18193
|
+
if (err instanceof ErrorFromResponse) {
|
|
18194
|
+
return {
|
|
18195
|
+
reason: err.message,
|
|
18196
|
+
code: err.code != null ? String(err.code) : 'SERVER_ERROR',
|
|
18197
|
+
};
|
|
18198
|
+
}
|
|
18199
|
+
if (err instanceof Error) {
|
|
18200
|
+
try {
|
|
18201
|
+
const parsed = JSON.parse(err.message);
|
|
18202
|
+
if (typeof parsed.isWSFailure === 'boolean') {
|
|
18203
|
+
return {
|
|
18204
|
+
reason: parsed.message || err.message,
|
|
18205
|
+
code: !parsed.isWSFailure && parsed.code
|
|
18206
|
+
? String(parsed.code)
|
|
18207
|
+
: 'SERVER_ERROR',
|
|
18208
|
+
};
|
|
18209
|
+
}
|
|
18210
|
+
}
|
|
18211
|
+
catch { }
|
|
18212
|
+
}
|
|
18213
|
+
return { reason: errorMessage(err), code: 'SERVER_ERROR' };
|
|
18214
|
+
};
|
|
18215
|
+
const mapWsJoinError = (err) => {
|
|
18216
|
+
if (err instanceof SfuJoinError) {
|
|
18217
|
+
const sfuError = err.errorEvent.error;
|
|
18218
|
+
return {
|
|
18219
|
+
reason: sfuError?.message || err.message,
|
|
18220
|
+
code: (sfuError && ErrorCode[sfuError.code]) || 'SFU_ERROR',
|
|
18221
|
+
};
|
|
18222
|
+
}
|
|
18223
|
+
const reason = errorMessage(err);
|
|
18224
|
+
if (err instanceof SfuTimeoutError) {
|
|
18225
|
+
return { reason, code: 'REQUEST_TIMEOUT' };
|
|
18226
|
+
}
|
|
18227
|
+
return { reason, code: 'SFU_ERROR' };
|
|
18228
|
+
};
|
|
18229
|
+
|
|
17562
18230
|
/**
|
|
17563
18231
|
* A `StreamVideoClient` instance lets you communicate with our API, and authenticate users.
|
|
17564
18232
|
*/
|
|
@@ -17622,6 +18290,7 @@ class StreamVideoClient {
|
|
|
17622
18290
|
}
|
|
17623
18291
|
call = new Call({
|
|
17624
18292
|
streamClient: this.streamClient,
|
|
18293
|
+
clientEventReporter: this.clientEventReporter,
|
|
17625
18294
|
type: e.call.type,
|
|
17626
18295
|
id: e.call.id,
|
|
17627
18296
|
members: e.members,
|
|
@@ -17691,6 +18360,8 @@ class StreamVideoClient {
|
|
|
17691
18360
|
user.id = '!anon';
|
|
17692
18361
|
return this.connectAnonymousUser(user, tokenOrProvider);
|
|
17693
18362
|
}
|
|
18363
|
+
const reporter = this.clientEventReporter;
|
|
18364
|
+
reporter.startCoordinatorConnection(user.id);
|
|
17694
18365
|
const connectUserResponse = await withoutConcurrency(this.connectionConcurrencyTag, async () => {
|
|
17695
18366
|
const client = this.streamClient;
|
|
17696
18367
|
const { onConnectUserError, persistUserOnConnectionFailure } = client.options;
|
|
@@ -17700,14 +18371,15 @@ class StreamVideoClient {
|
|
|
17700
18371
|
for (let attempt = 0; attempt < maxConnectUserRetries; attempt++) {
|
|
17701
18372
|
try {
|
|
17702
18373
|
this.logger.trace(`Connecting user (${attempt})`, user);
|
|
17703
|
-
return user.type === 'guest'
|
|
17704
|
-
?
|
|
17705
|
-
:
|
|
18374
|
+
return await reporter.trackCoordinatorWs(() => user.type === 'guest'
|
|
18375
|
+
? client.connectGuestUser(user)
|
|
18376
|
+
: client.connectUser(user, tokenOrProvider));
|
|
17706
18377
|
}
|
|
17707
18378
|
catch (err) {
|
|
17708
18379
|
this.logger.warn(`Failed to connect a user (${attempt})`, err);
|
|
17709
18380
|
errorQueue.push(err);
|
|
17710
18381
|
if (attempt === maxConnectUserRetries - 1) {
|
|
18382
|
+
reporter.closeCoordinatorWs();
|
|
17711
18383
|
onConnectUserError?.(err, errorQueue);
|
|
17712
18384
|
throw err;
|
|
17713
18385
|
}
|
|
@@ -17785,6 +18457,7 @@ class StreamVideoClient {
|
|
|
17785
18457
|
return (call ??
|
|
17786
18458
|
new Call({
|
|
17787
18459
|
streamClient: this.streamClient,
|
|
18460
|
+
clientEventReporter: this.clientEventReporter,
|
|
17788
18461
|
id: id,
|
|
17789
18462
|
type: type,
|
|
17790
18463
|
clientStore: this.writeableStateStore,
|
|
@@ -17809,6 +18482,7 @@ class StreamVideoClient {
|
|
|
17809
18482
|
for (const c of response.calls) {
|
|
17810
18483
|
const call = new Call({
|
|
17811
18484
|
streamClient: this.streamClient,
|
|
18485
|
+
clientEventReporter: this.clientEventReporter,
|
|
17812
18486
|
id: c.call.id,
|
|
17813
18487
|
type: c.call.type,
|
|
17814
18488
|
members: c.members,
|
|
@@ -17916,6 +18590,7 @@ class StreamVideoClient {
|
|
|
17916
18590
|
const [callType, callId] = call_cid.split(':');
|
|
17917
18591
|
call = new Call({
|
|
17918
18592
|
streamClient: this.streamClient,
|
|
18593
|
+
clientEventReporter: this.clientEventReporter,
|
|
17919
18594
|
type: callType,
|
|
17920
18595
|
id: callId,
|
|
17921
18596
|
clientStore: this.writeableStateStore,
|
|
@@ -17956,6 +18631,9 @@ class StreamVideoClient {
|
|
|
17956
18631
|
this.logger = videoLoggerSystem.getLogger('client');
|
|
17957
18632
|
this.rejectCallWhenBusy = clientOptions?.rejectCallWhenBusy ?? false;
|
|
17958
18633
|
this.streamClient = createCoordinatorClient(apiKey, clientOptions);
|
|
18634
|
+
this.clientEventReporter = new ClientEventReporter({
|
|
18635
|
+
streamClient: this.streamClient,
|
|
18636
|
+
});
|
|
17959
18637
|
this.writeableStateStore = new StreamVideoWriteableStateStore();
|
|
17960
18638
|
this.readOnlyStateStore = new StreamVideoReadOnlyStateStore(this.writeableStateStore);
|
|
17961
18639
|
if (typeof apiKeyOrArgs !== 'string' && apiKeyOrArgs.user) {
|
|
@@ -18018,5 +18696,5 @@ const humanize = (n) => {
|
|
|
18018
18696
|
return String(n);
|
|
18019
18697
|
};
|
|
18020
18698
|
|
|
18021
|
-
export { AudioSettingsRequestDefaultDeviceEnum, AudioSettingsResponseDefaultDeviceEnum, browsers as Browsers, Call, CallRecordingFailedEventRecordingTypeEnum, CallRecordingReadyEventRecordingTypeEnum, CallRecordingStartedEventRecordingTypeEnum, CallRecordingStoppedEventRecordingTypeEnum, CallState, CallType, CallTypes, CallingState, CameraManager, CameraManagerState, CreateDeviceRequestPushProviderEnum, DebounceType, DeviceManager, DeviceManagerState, DynascaleManager, ErrorFromResponse, FrameRecordingSettingsRequestModeEnum, FrameRecordingSettingsRequestQualityEnum, FrameRecordingSettingsResponseModeEnum, IndividualRecordingSettingsRequestModeEnum, IndividualRecordingSettingsResponseModeEnum, IngressAudioEncodingOptionsRequestChannelsEnum, IngressSourceRequestFpsEnum, IngressVideoLayerRequestCodecEnum, LayoutSettingsRequestNameEnum, MicrophoneManager, MicrophoneManagerState, NoiseCancellationSettingsModeEnum, OwnCapability, RTMPBroadcastRequestQualityEnum, RTMPSettingsRequestQualityEnum, RawRecordingSettingsRequestModeEnum, RawRecordingSettingsResponseModeEnum, RecordSettingsRequestModeEnum, RecordSettingsRequestQualityEnum, rxUtils as RxUtils, ScreenShareManager, ScreenShareState, events as SfuEvents, SfuJoinError, models as SfuModels, SpeakerManager, SpeakerState, StartClosedCaptionsRequestLanguageEnum, StartTranscriptionRequestLanguageEnum, StreamSfuClient, StreamVideoClient, StreamVideoReadOnlyStateStore, StreamVideoWriteableStateStore, TranscriptionSettingsRequestClosedCaptionModeEnum, TranscriptionSettingsRequestLanguageEnum, TranscriptionSettingsRequestModeEnum, TranscriptionSettingsResponseClosedCaptionModeEnum, TranscriptionSettingsResponseLanguageEnum, TranscriptionSettingsResponseModeEnum, VideoSettingsRequestCameraFacingEnum, VideoSettingsResponseCameraFacingEnum, ViewportTracker, VisibilityState, checkIfAudioOutputChangeSupported, combineComparators, conditional, createSoundDetector, defaultSortPreset, descending, deviceIds$, disposeOfMediaStream, dominantSpeaker, getAudioBrowserPermission, getAudioDevices, getAudioOutputDevices, getAudioStream, getClientDetails, getDeviceState, getScreenShareStream, getSdkInfo, getVideoBrowserPermission, getVideoDevices, getVideoStream, getWebRTCInfo, hasAudio$1 as hasAudio, hasInterruptedTrack, hasPausedTrack, hasScreenShare, hasScreenShareAudio, hasVideo, humanize, isPinned, livestreamOrAudioRoomSortPreset, logToConsole, name, noopComparator, paginatedLayoutSortPreset, pinned, publishingAudio, publishingVideo, reactionType, resolveDeviceId, role, screenSharing, setDeviceInfo, setOSInfo, setPowerState, setSdkInfo, setThermalState, setWebRTCInfo, speakerLayoutSortPreset, speaking, videoLoggerSystem, withParticipantSource };
|
|
18699
|
+
export { AudioSettingsRequestDefaultDeviceEnum, AudioSettingsResponseDefaultDeviceEnum, browsers as Browsers, Call, CallRecordingFailedEventRecordingTypeEnum, CallRecordingReadyEventRecordingTypeEnum, CallRecordingStartedEventRecordingTypeEnum, CallRecordingStoppedEventRecordingTypeEnum, CallState, CallType, CallTypes, CallingState, CameraManager, CameraManagerState, CreateDeviceRequestPushProviderEnum, DebounceType, DeviceManager, DeviceManagerState, DynascaleManager, ErrorFromResponse, FrameRecordingSettingsRequestModeEnum, FrameRecordingSettingsRequestQualityEnum, FrameRecordingSettingsResponseModeEnum, IndividualRecordingSettingsRequestModeEnum, IndividualRecordingSettingsResponseModeEnum, IngressAudioEncodingOptionsRequestChannelsEnum, IngressSourceRequestFpsEnum, IngressVideoLayerRequestCodecEnum, LayoutSettingsRequestNameEnum, MicrophoneManager, MicrophoneManagerState, NoiseCancellationSettingsModeEnum, OwnCapability, RTMPBroadcastRequestQualityEnum, RTMPSettingsRequestQualityEnum, RawRecordingSettingsRequestModeEnum, RawRecordingSettingsResponseModeEnum, RecordSettingsRequestModeEnum, RecordSettingsRequestQualityEnum, rxUtils as RxUtils, ScreenShareManager, ScreenShareState, events as SfuEvents, SfuJoinError, models as SfuModels, SfuTimeoutError, SpeakerManager, SpeakerState, StartClosedCaptionsRequestLanguageEnum, StartTranscriptionRequestLanguageEnum, StreamSfuClient, StreamVideoClient, StreamVideoReadOnlyStateStore, StreamVideoWriteableStateStore, TranscriptionSettingsRequestClosedCaptionModeEnum, TranscriptionSettingsRequestLanguageEnum, TranscriptionSettingsRequestModeEnum, TranscriptionSettingsResponseClosedCaptionModeEnum, TranscriptionSettingsResponseLanguageEnum, TranscriptionSettingsResponseModeEnum, VideoSettingsRequestCameraFacingEnum, VideoSettingsResponseCameraFacingEnum, ViewportTracker, VisibilityState, checkIfAudioOutputChangeSupported, combineComparators, conditional, createSoundDetector, defaultSortPreset, descending, deviceIds$, disposeOfMediaStream, dominantSpeaker, getAudioBrowserPermission, getAudioDevices, getAudioOutputDevices, getAudioStream, getClientDetails, getDeviceState, getScreenShareStream, getSdkInfo, getVideoBrowserPermission, getVideoDevices, getVideoStream, getWebRTCInfo, hasAudio$1 as hasAudio, hasInterruptedTrack, hasPausedTrack, hasScreenShare, hasScreenShareAudio, hasVideo, humanize, isPinned, livestreamOrAudioRoomSortPreset, logToConsole, name, noopComparator, paginatedLayoutSortPreset, pinned, publishingAudio, publishingVideo, reactionType, resolveDeviceId, role, screenSharing, setDeviceInfo, setOSInfo, setPowerState, setSdkInfo, setThermalState, setWebRTCInfo, speakerLayoutSortPreset, speaking, videoLoggerSystem, withParticipantSource };
|
|
18022
18700
|
//# sourceMappingURL=index.browser.es.js.map
|