@stream-io/video-client 1.52.1-beta.0 → 1.53.1
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 +17 -0
- package/dist/index.browser.es.js +819 -123
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +819 -122
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +819 -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/devices/MicrophoneManager.d.ts +6 -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 +16 -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/MicrophoneManager.ts +16 -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 +78 -2
- 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 +864 -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 +18 -0
package/dist/index.es.js
CHANGED
|
@@ -508,7 +508,6 @@ class ErrorFromResponse extends Error {
|
|
|
508
508
|
}
|
|
509
509
|
}
|
|
510
510
|
|
|
511
|
-
/* eslint-disable */
|
|
512
511
|
// @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
|
|
513
512
|
// @generated from protobuf file "google/protobuf/struct.proto" (package "google.protobuf", syntax proto3)
|
|
514
513
|
// tslint:disable
|
|
@@ -774,7 +773,6 @@ class ListValue$Type extends MessageType {
|
|
|
774
773
|
*/
|
|
775
774
|
const ListValue = new ListValue$Type();
|
|
776
775
|
|
|
777
|
-
/* eslint-disable */
|
|
778
776
|
// @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
|
|
779
777
|
// @generated from protobuf file "google/protobuf/timestamp.proto" (package "google.protobuf", syntax proto3)
|
|
780
778
|
// tslint:disable
|
|
@@ -1841,12 +1839,6 @@ class TrackInfo$Type extends MessageType {
|
|
|
1841
1839
|
kind: 'scalar',
|
|
1842
1840
|
T: 5 /*ScalarType.INT32*/,
|
|
1843
1841
|
},
|
|
1844
|
-
{
|
|
1845
|
-
no: 13,
|
|
1846
|
-
name: 'self_sub_audio_video',
|
|
1847
|
-
kind: 'scalar',
|
|
1848
|
-
T: 8 /*ScalarType.BOOL*/,
|
|
1849
|
-
},
|
|
1850
1842
|
]);
|
|
1851
1843
|
}
|
|
1852
1844
|
}
|
|
@@ -6649,7 +6641,7 @@ const getSdkVersion = (sdk) => {
|
|
|
6649
6641
|
return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
|
|
6650
6642
|
};
|
|
6651
6643
|
|
|
6652
|
-
const version = "1.
|
|
6644
|
+
const version = "1.53.1";
|
|
6653
6645
|
const [major, minor, patch] = version.split('.');
|
|
6654
6646
|
let sdkInfo = {
|
|
6655
6647
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -6793,7 +6785,7 @@ const getClientDetails = async () => {
|
|
|
6793
6785
|
.join(' '),
|
|
6794
6786
|
version: '',
|
|
6795
6787
|
},
|
|
6796
|
-
webrtcVersion:
|
|
6788
|
+
webrtcVersion: browserVersion,
|
|
6797
6789
|
};
|
|
6798
6790
|
};
|
|
6799
6791
|
|
|
@@ -7743,7 +7735,7 @@ class BasePeerConnection {
|
|
|
7743
7735
|
/**
|
|
7744
7736
|
* Constructs a new `BasePeerConnection` instance.
|
|
7745
7737
|
*/
|
|
7746
|
-
constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, onIceConnected, tag, enableTracing, clientPublishOptions, iceRestartDelay = 2500, statsTimestampDriftThresholdMs = 0, }) {
|
|
7738
|
+
constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, onIceConnected, onPeerConnectionStateChange, onRemoteTrackUnmute, tag, enableTracing, clientPublishOptions, iceRestartDelay = 2500, statsTimestampDriftThresholdMs = 0, }) {
|
|
7747
7739
|
this.iceHasEverConnected = false;
|
|
7748
7740
|
this.isIceRestarting = false;
|
|
7749
7741
|
this.isDisposed = false;
|
|
@@ -7897,6 +7889,10 @@ class BasePeerConnection {
|
|
|
7897
7889
|
this.onConnectionStateChange = async () => {
|
|
7898
7890
|
const state = this.pc.connectionState;
|
|
7899
7891
|
this.logger.debug(`Connection state changed`, state);
|
|
7892
|
+
this.fireOnPeerConnectionStateChange({
|
|
7893
|
+
stateType: 'peerConnection',
|
|
7894
|
+
state,
|
|
7895
|
+
});
|
|
7900
7896
|
if (this.tracer && (state === 'connected' || state === 'failed')) {
|
|
7901
7897
|
try {
|
|
7902
7898
|
const stats = await this.stats.get();
|
|
@@ -7919,8 +7915,20 @@ class BasePeerConnection {
|
|
|
7919
7915
|
this.onIceConnectionStateChange = () => {
|
|
7920
7916
|
const state = this.pc.iceConnectionState;
|
|
7921
7917
|
this.logger.debug(`ICE connection state changed`, state);
|
|
7918
|
+
this.fireOnPeerConnectionStateChange({ stateType: 'ice', state });
|
|
7922
7919
|
this.handleConnectionStateUpdate(state);
|
|
7923
7920
|
};
|
|
7921
|
+
this.fireOnPeerConnectionStateChange = (event) => {
|
|
7922
|
+
try {
|
|
7923
|
+
this.onPeerConnectionStateChange?.({
|
|
7924
|
+
peerType: this.peerType,
|
|
7925
|
+
...event,
|
|
7926
|
+
});
|
|
7927
|
+
}
|
|
7928
|
+
catch (err) {
|
|
7929
|
+
this.logger.warn('onPeerConnectionStateChange listener threw', err);
|
|
7930
|
+
}
|
|
7931
|
+
};
|
|
7924
7932
|
this.handleConnectionStateUpdate = (state) => {
|
|
7925
7933
|
const { callingState } = this.state;
|
|
7926
7934
|
if (callingState === CallingState.OFFLINE)
|
|
@@ -8035,6 +8043,8 @@ class BasePeerConnection {
|
|
|
8035
8043
|
this.tag = tag;
|
|
8036
8044
|
this.onReconnectionNeeded = onReconnectionNeeded;
|
|
8037
8045
|
this.onIceConnected = onIceConnected;
|
|
8046
|
+
this.onPeerConnectionStateChange = onPeerConnectionStateChange;
|
|
8047
|
+
this.onRemoteTrackUnmute = onRemoteTrackUnmute;
|
|
8038
8048
|
this.logger = videoLoggerSystem.getLogger(peerType === PeerType.SUBSCRIBER ? 'Subscriber' : 'Publisher', { tags: [tag] });
|
|
8039
8049
|
this.pc = this.createPeerConnection(connectionConfig);
|
|
8040
8050
|
this.stats = new StatsTracer(this.pc, peerType, this.trackIdToTrackType, statsTimestampDriftThresholdMs);
|
|
@@ -8057,6 +8067,8 @@ class BasePeerConnection {
|
|
|
8057
8067
|
this.preConnectStuckTimeout = undefined;
|
|
8058
8068
|
this.onReconnectionNeeded = undefined;
|
|
8059
8069
|
this.onIceConnected = undefined;
|
|
8070
|
+
this.onPeerConnectionStateChange = undefined;
|
|
8071
|
+
this.onRemoteTrackUnmute = undefined;
|
|
8060
8072
|
this.isDisposed = true;
|
|
8061
8073
|
this.detachEventHandlers();
|
|
8062
8074
|
this.pc.close();
|
|
@@ -8417,7 +8429,7 @@ class Publisher extends BasePeerConnection {
|
|
|
8417
8429
|
/**
|
|
8418
8430
|
* Constructs a new `Publisher` instance.
|
|
8419
8431
|
*/
|
|
8420
|
-
constructor(baseOptions, publishOptions
|
|
8432
|
+
constructor(baseOptions, publishOptions) {
|
|
8421
8433
|
super(PeerType.PUBLISHER_UNSPECIFIED, baseOptions);
|
|
8422
8434
|
this.transceiverCache = new TransceiverCache();
|
|
8423
8435
|
this.clonedTracks = new Set();
|
|
@@ -8838,7 +8850,6 @@ class Publisher extends BasePeerConnection {
|
|
|
8838
8850
|
muted: !isTrackLive,
|
|
8839
8851
|
codec: publishOption.codec,
|
|
8840
8852
|
publishOptionId: publishOption.id,
|
|
8841
|
-
selfSubAudioVideo: this.selfSubEnabled,
|
|
8842
8853
|
};
|
|
8843
8854
|
};
|
|
8844
8855
|
this.cloneTrack = (track) => {
|
|
@@ -8919,7 +8930,6 @@ class Publisher extends BasePeerConnection {
|
|
|
8919
8930
|
});
|
|
8920
8931
|
};
|
|
8921
8932
|
this.publishOptions = publishOptions;
|
|
8922
|
-
this.selfSubEnabled = opts.selfSubEnabled ?? false;
|
|
8923
8933
|
this.on('iceRestart', (iceRestart) => {
|
|
8924
8934
|
if (iceRestart.peerType !== PeerType.PUBLISHER_UNSPECIFIED)
|
|
8925
8935
|
return;
|
|
@@ -9001,13 +9011,6 @@ class Subscriber extends BasePeerConnection {
|
|
|
9001
9011
|
*/
|
|
9002
9012
|
constructor(opts) {
|
|
9003
9013
|
super(PeerType.SUBSCRIBER, opts);
|
|
9004
|
-
/**
|
|
9005
|
-
* Remote streams received from the SFU. For a self-sub case
|
|
9006
|
-
* we need to be able to distinguish between the local capture stream.
|
|
9007
|
-
* The map will never contain local streams so we can safely use it to
|
|
9008
|
-
* check if the stream is remote and dispose it when needed.
|
|
9009
|
-
*/
|
|
9010
|
-
this.trackedStreams = new WeakSet();
|
|
9011
9014
|
/**
|
|
9012
9015
|
* Restarts the ICE connection and renegotiates with the SFU.
|
|
9013
9016
|
*/
|
|
@@ -9042,7 +9045,6 @@ class Subscriber extends BasePeerConnection {
|
|
|
9042
9045
|
// example: `e3f6aaf8-b03d-4911-be36-83f47d37a76a:TRACK_TYPE_VIDEO`
|
|
9043
9046
|
const [trackId, rawTrackType] = primaryStream.id.split(':');
|
|
9044
9047
|
const participantToUpdate = this.state.participants.find((p) => p.trackLookupPrefix === trackId);
|
|
9045
|
-
const isSelfSub = !!participantToUpdate?.isLocalParticipant;
|
|
9046
9048
|
this.logger.debug(`[onTrack]: Got remote ${rawTrackType} track for userId: ${participantToUpdate?.userId}`, track.id, track);
|
|
9047
9049
|
const trackType = toTrackType(rawTrackType);
|
|
9048
9050
|
if (!trackType) {
|
|
@@ -9056,6 +9058,7 @@ class Subscriber extends BasePeerConnection {
|
|
|
9056
9058
|
track.addEventListener('unmute', () => {
|
|
9057
9059
|
this.logger.info(`[onTrack]: Track unmuted: ${trackDebugInfo}`);
|
|
9058
9060
|
this.setRemoteTrackInterrupted(trackId, trackType, false);
|
|
9061
|
+
this.onRemoteTrackUnmute?.(trackType, track.id);
|
|
9059
9062
|
});
|
|
9060
9063
|
track.addEventListener('ended', () => {
|
|
9061
9064
|
this.logger.info(`[onTrack]: Track ended: ${trackDebugInfo}`);
|
|
@@ -9066,9 +9069,6 @@ class Subscriber extends BasePeerConnection {
|
|
|
9066
9069
|
this.setRemoteTrackInterrupted(trackId, trackType, true);
|
|
9067
9070
|
}
|
|
9068
9071
|
this.trackIdToTrackType.set(track.id, trackType);
|
|
9069
|
-
if (isSelfSub) {
|
|
9070
|
-
this.trackedStreams.add(primaryStream);
|
|
9071
|
-
}
|
|
9072
9072
|
if (!participantToUpdate) {
|
|
9073
9073
|
this.logger.warn(`[onTrack]: Received track for unknown participant: ${trackId}`, e);
|
|
9074
9074
|
this.state.registerOrphanedTrack({
|
|
@@ -9084,12 +9084,6 @@ class Subscriber extends BasePeerConnection {
|
|
|
9084
9084
|
this.logger.error(`Unknown track type: ${rawTrackType}`);
|
|
9085
9085
|
return;
|
|
9086
9086
|
}
|
|
9087
|
-
// Self-sub loopback audio routes to the speaker by default, which
|
|
9088
|
-
// would echo the local user's voice. Default-mute here; consumers
|
|
9089
|
-
// (the loopback recording hook) re-enable explicitly when needed.
|
|
9090
|
-
if (isSelfSub && e.track.kind === 'audio') {
|
|
9091
|
-
e.track.enabled = false;
|
|
9092
|
-
}
|
|
9093
9087
|
// get the previous stream to dispose it later
|
|
9094
9088
|
// usually this happens during migration, when the stream is replaced
|
|
9095
9089
|
// with a new one but the old one is still in the state
|
|
@@ -9098,12 +9092,8 @@ class Subscriber extends BasePeerConnection {
|
|
|
9098
9092
|
this.state.updateParticipant(participantToUpdate.sessionId, {
|
|
9099
9093
|
[streamKindProp]: primaryStream,
|
|
9100
9094
|
});
|
|
9095
|
+
// now, dispose the previous stream if it exists
|
|
9101
9096
|
if (previousStream) {
|
|
9102
|
-
if (isSelfSub && !this.trackedStreams.has(previousStream)) {
|
|
9103
|
-
// this is the local capture stream, we don't want to dispose it
|
|
9104
|
-
this.logger.debug(`[onTrack]: Skipping cleanup of previous ${e.track.kind} stream for userId: ${participantToUpdate.userId} because it is not tracked`);
|
|
9105
|
-
return;
|
|
9106
|
-
}
|
|
9107
9097
|
this.logger.info(`[onTrack]: Cleaning up previous remote ${track.kind} tracks for userId: ${participantToUpdate.userId}`);
|
|
9108
9098
|
previousStream.getTracks().forEach((t) => {
|
|
9109
9099
|
t.stop();
|
|
@@ -9308,6 +9298,15 @@ class SfuJoinError extends Error {
|
|
|
9308
9298
|
}
|
|
9309
9299
|
}
|
|
9310
9300
|
|
|
9301
|
+
/**
|
|
9302
|
+
* An error thrown when a client-side SFU deadline (e.g., waiting for the
|
|
9303
|
+
* signaling WS to open or for the `joinResponse` to arrive) fires before
|
|
9304
|
+
* the awaited operation resolves. Allows consumers (e.g., the client event
|
|
9305
|
+
* reporter) to classify timeouts without relying on message wording.
|
|
9306
|
+
*/
|
|
9307
|
+
class SfuTimeoutError extends Error {
|
|
9308
|
+
}
|
|
9309
|
+
|
|
9311
9310
|
/**
|
|
9312
9311
|
* Creates a fresh `joinResponseTask` with a no-op rejection handler attached
|
|
9313
9312
|
* to the underlying promise. The handler marks the rejection path as handled
|
|
@@ -9413,7 +9412,7 @@ class StreamSfuClient {
|
|
|
9413
9412
|
timeoutId = setTimeout(() => {
|
|
9414
9413
|
const message = `SFU WS connection failed to open after ${this.joinResponseTimeout}ms`;
|
|
9415
9414
|
this.tracer?.trace('signal.timeout', message);
|
|
9416
|
-
reject(new
|
|
9415
|
+
reject(new SfuTimeoutError(message));
|
|
9417
9416
|
}, this.joinResponseTimeout);
|
|
9418
9417
|
}),
|
|
9419
9418
|
]));
|
|
@@ -9583,7 +9582,7 @@ class StreamSfuClient {
|
|
|
9583
9582
|
cleanupJoinSubscriptions();
|
|
9584
9583
|
const message = `Waiting for "joinResponse" has timed out after ${this.joinResponseTimeout}ms`;
|
|
9585
9584
|
this.tracer?.trace('joinRequestTimeout', message);
|
|
9586
|
-
current.reject(new
|
|
9585
|
+
current.reject(new SfuTimeoutError(message));
|
|
9587
9586
|
}, this.joinResponseTimeout);
|
|
9588
9587
|
const joinRequest = SfuRequest.create({
|
|
9589
9588
|
requestPayload: {
|
|
@@ -9800,6 +9799,10 @@ const watchCallEnded = (call) => {
|
|
|
9800
9799
|
const { callingState } = call.state;
|
|
9801
9800
|
if (callingState !== CallingState.IDLE &&
|
|
9802
9801
|
callingState !== CallingState.LEFT) {
|
|
9802
|
+
call.clientEventReporter.abort(call.cid, {
|
|
9803
|
+
code: 'BACKEND_LEAVE',
|
|
9804
|
+
reason: 'call.ended event received',
|
|
9805
|
+
});
|
|
9803
9806
|
call
|
|
9804
9807
|
.leave({ message: 'call.ended event received', reject: false })
|
|
9805
9808
|
.catch((err) => {
|
|
@@ -9829,6 +9832,10 @@ const watchSfuCallEnded = (call) => {
|
|
|
9829
9832
|
call.state.setEndedAt(new Date());
|
|
9830
9833
|
const reason = CallEndedReason[e.reason];
|
|
9831
9834
|
globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
|
|
9835
|
+
call.clientEventReporter.abort(call.cid, {
|
|
9836
|
+
code: 'BACKEND_LEAVE',
|
|
9837
|
+
reason: `callEnded received: ${reason}`,
|
|
9838
|
+
});
|
|
9832
9839
|
await call.leave({ message: `callEnded received: ${reason}` });
|
|
9833
9840
|
}
|
|
9834
9841
|
catch (err) {
|
|
@@ -10981,6 +10988,40 @@ class DynascaleManager {
|
|
|
10981
10988
|
}
|
|
10982
10989
|
}
|
|
10983
10990
|
|
|
10991
|
+
/**
|
|
10992
|
+
* Invokes `onFirstFrame` once when the video element renders a frame.
|
|
10993
|
+
*
|
|
10994
|
+
* Uses `requestVideoFrameCallback` when available, falling back to `loadeddata`
|
|
10995
|
+
* for browsers that don't support it.
|
|
10996
|
+
*/
|
|
10997
|
+
const createFirstVideoFrameDetector = (videoElement, onFirstFrame) => {
|
|
10998
|
+
let done = false;
|
|
10999
|
+
const notify = () => {
|
|
11000
|
+
if (done)
|
|
11001
|
+
return;
|
|
11002
|
+
done = true;
|
|
11003
|
+
onFirstFrame();
|
|
11004
|
+
};
|
|
11005
|
+
if (typeof videoElement.requestVideoFrameCallback === 'function') {
|
|
11006
|
+
const handle = videoElement.requestVideoFrameCallback(notify);
|
|
11007
|
+
return () => {
|
|
11008
|
+
done = true;
|
|
11009
|
+
videoElement.cancelVideoFrameCallback(handle);
|
|
11010
|
+
};
|
|
11011
|
+
}
|
|
11012
|
+
if (videoElement.readyState >= videoElement.HAVE_CURRENT_DATA) {
|
|
11013
|
+
queueMicrotask(notify);
|
|
11014
|
+
return () => {
|
|
11015
|
+
done = true;
|
|
11016
|
+
};
|
|
11017
|
+
}
|
|
11018
|
+
videoElement.addEventListener('loadeddata', notify, { once: true });
|
|
11019
|
+
return () => {
|
|
11020
|
+
done = true;
|
|
11021
|
+
videoElement.removeEventListener('loadeddata', notify);
|
|
11022
|
+
};
|
|
11023
|
+
};
|
|
11024
|
+
|
|
10984
11025
|
const DEFAULT_THRESHOLD = 0.35;
|
|
10985
11026
|
const DEFAULT_VIEWPORT_VISIBILITY_STATE = {
|
|
10986
11027
|
videoTrack: VisibilityState.UNKNOWN,
|
|
@@ -13099,6 +13140,7 @@ class MicrophoneManager extends AudioDeviceManager {
|
|
|
13099
13140
|
]), async ([callingState, ownCapabilities, deviceId, status, permissionState,]) => {
|
|
13100
13141
|
try {
|
|
13101
13142
|
if (callingState === CallingState.LEFT) {
|
|
13143
|
+
this.setMutedRecordingPrepared(false);
|
|
13102
13144
|
await this.stopSpeakingWhileMutedDetection();
|
|
13103
13145
|
}
|
|
13104
13146
|
if (callingState !== CallingState.JOINED)
|
|
@@ -13108,13 +13150,16 @@ class MicrophoneManager extends AudioDeviceManager {
|
|
|
13108
13150
|
if (ownCapabilities.includes(OwnCapability.SEND_AUDIO)) {
|
|
13109
13151
|
const hasPermission = await this.hasPermission(permissionState);
|
|
13110
13152
|
if (hasPermission && status !== 'enabled') {
|
|
13153
|
+
this.setMutedRecordingPrepared(true);
|
|
13111
13154
|
await this.startSpeakingWhileMutedDetection(deviceId);
|
|
13112
13155
|
}
|
|
13113
13156
|
else {
|
|
13157
|
+
this.setMutedRecordingPrepared(false);
|
|
13114
13158
|
await this.stopSpeakingWhileMutedDetection();
|
|
13115
13159
|
}
|
|
13116
13160
|
}
|
|
13117
13161
|
else {
|
|
13162
|
+
this.setMutedRecordingPrepared(false);
|
|
13118
13163
|
await this.stopSpeakingWhileMutedDetection();
|
|
13119
13164
|
}
|
|
13120
13165
|
}
|
|
@@ -13432,6 +13477,16 @@ class MicrophoneManager extends AudioDeviceManager {
|
|
|
13432
13477
|
this.logger.warn('Failed to stop speaking while muted detector', err);
|
|
13433
13478
|
});
|
|
13434
13479
|
}
|
|
13480
|
+
/**
|
|
13481
|
+
* iOS-only: keep the mic-input chain prepared while muted
|
|
13482
|
+
* so the `AVAudioEngine` stays full-duplex and remote audio renders on a
|
|
13483
|
+
* muted join.
|
|
13484
|
+
*/
|
|
13485
|
+
setMutedRecordingPrepared(enabled) {
|
|
13486
|
+
if (!isReactNative())
|
|
13487
|
+
return;
|
|
13488
|
+
globalThis.streamRNVideoSDK?.callManager.setMutedRecordingPrepared?.(enabled);
|
|
13489
|
+
}
|
|
13435
13490
|
async hasPermission(permissionState) {
|
|
13436
13491
|
if (!isReactNative())
|
|
13437
13492
|
return permissionState === 'granted';
|
|
@@ -13824,7 +13879,7 @@ class Call {
|
|
|
13824
13879
|
* Use the [`StreamVideoClient.call`](./StreamVideoClient.md/#call)
|
|
13825
13880
|
* method to construct a `Call` instance.
|
|
13826
13881
|
*/
|
|
13827
|
-
constructor({ type, id, streamClient, members, ownCapabilities, sortParticipantsBy, clientStore, ringing = false, watching = false, }) {
|
|
13882
|
+
constructor({ type, id, streamClient, clientEventReporter, members, ownCapabilities, sortParticipantsBy, clientStore, ringing = false, watching = false, }) {
|
|
13828
13883
|
/**
|
|
13829
13884
|
* The state of this call.
|
|
13830
13885
|
*/
|
|
@@ -13861,7 +13916,6 @@ class Call {
|
|
|
13861
13916
|
// maintain the order of publishing tracks to restore them after a reconnection
|
|
13862
13917
|
// it shouldn't contain duplicates
|
|
13863
13918
|
this.trackPublishOrder = [];
|
|
13864
|
-
this.selfSubEnabled = false;
|
|
13865
13919
|
this.hasJoinedOnce = false;
|
|
13866
13920
|
this.deviceSettingsAppliedOnce = false;
|
|
13867
13921
|
this.initialized = false;
|
|
@@ -14152,9 +14206,14 @@ class Call {
|
|
|
14152
14206
|
this.sfuStatsReporter = undefined;
|
|
14153
14207
|
this.lastStatsOptions = undefined;
|
|
14154
14208
|
await this.subscriber?.dispose();
|
|
14209
|
+
this.clientEventReporter.abort(this.cid, {
|
|
14210
|
+
code: 'CLIENT_ABORTED',
|
|
14211
|
+
reason: leaveReason,
|
|
14212
|
+
});
|
|
14155
14213
|
this.subscriber = undefined;
|
|
14156
14214
|
await this.publisher?.dispose();
|
|
14157
14215
|
this.publisher = undefined;
|
|
14216
|
+
this.clientEventReporter.unregisterCall(this.cid);
|
|
14158
14217
|
await this.sfuClient?.leaveAndClose(leaveReason);
|
|
14159
14218
|
this.sfuClient = undefined;
|
|
14160
14219
|
this.trackSubscriptionManager.setSfuClient(undefined);
|
|
@@ -14206,30 +14265,6 @@ class Call {
|
|
|
14206
14265
|
await Promise.all(stopOnLeavePromises);
|
|
14207
14266
|
});
|
|
14208
14267
|
};
|
|
14209
|
-
/**
|
|
14210
|
-
* The largest video publish dimension across the current publish options.
|
|
14211
|
-
*
|
|
14212
|
-
* @internal
|
|
14213
|
-
*/
|
|
14214
|
-
this.getMaxVideoPublishDimension = () => {
|
|
14215
|
-
if (!this.currentPublishOptions)
|
|
14216
|
-
return undefined;
|
|
14217
|
-
let maxDimension;
|
|
14218
|
-
let maxArea = 0;
|
|
14219
|
-
for (const opt of this.currentPublishOptions) {
|
|
14220
|
-
if (opt.trackType !== TrackType.VIDEO)
|
|
14221
|
-
continue;
|
|
14222
|
-
const dim = opt.videoDimension;
|
|
14223
|
-
if (!dim || !dim.width || !dim.height)
|
|
14224
|
-
continue;
|
|
14225
|
-
const area = dim.width * dim.height;
|
|
14226
|
-
if (area > maxArea) {
|
|
14227
|
-
maxDimension = dim;
|
|
14228
|
-
maxArea = area;
|
|
14229
|
-
}
|
|
14230
|
-
}
|
|
14231
|
-
return maxDimension;
|
|
14232
|
-
};
|
|
14233
14268
|
/**
|
|
14234
14269
|
* Update from the call response from the "call.ring" event
|
|
14235
14270
|
* @internal
|
|
@@ -14376,7 +14411,7 @@ class Call {
|
|
|
14376
14411
|
*
|
|
14377
14412
|
* @returns a promise which resolves once the call join-flow has finished.
|
|
14378
14413
|
*/
|
|
14379
|
-
this.join = async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout,
|
|
14414
|
+
this.join = async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, ...data } = {}) => {
|
|
14380
14415
|
const callingState = this.state.callingState;
|
|
14381
14416
|
if ([CallingState.JOINED, CallingState.JOINING].includes(callingState)) {
|
|
14382
14417
|
throw new Error(`Illegal State: call.join() shall be called only once`);
|
|
@@ -14384,15 +14419,19 @@ class Call {
|
|
|
14384
14419
|
if (data?.ring) {
|
|
14385
14420
|
this.ringingSubject.next(true);
|
|
14386
14421
|
}
|
|
14387
|
-
// we need this to be set before the callingx.joinCall() is
|
|
14388
|
-
// called to avoid registering the test call in the CallKit/Telecom
|
|
14389
|
-
this.selfSubEnabled = selfSubEnabled;
|
|
14390
14422
|
const callingX = globalThis.streamRNVideoSDK?.callingX;
|
|
14391
14423
|
if (callingX) {
|
|
14392
14424
|
// for Android/iOS, we need to start the call in the callingx library as soon as possible
|
|
14393
14425
|
await callingX.joinCall(this, this.clientStore.calls);
|
|
14394
14426
|
}
|
|
14395
14427
|
await this.setup();
|
|
14428
|
+
this.clientEventReporter.registerCall(this.cid, {
|
|
14429
|
+
callType: this.type,
|
|
14430
|
+
callId: this.id,
|
|
14431
|
+
getCallSessionId: () => this.state.session?.id ?? '',
|
|
14432
|
+
getSfuId: () => this.credentials?.server.edge_name ?? '',
|
|
14433
|
+
getUserSessionId: () => this.sfuClient?.sessionId ?? '',
|
|
14434
|
+
});
|
|
14396
14435
|
this.joinResponseTimeout = joinResponseTimeout;
|
|
14397
14436
|
this.rpcRequestTimeout = rpcRequestTimeout;
|
|
14398
14437
|
// we will count the number of join failures per SFU.
|
|
@@ -14402,39 +14441,42 @@ class Call {
|
|
|
14402
14441
|
const joinData = data;
|
|
14403
14442
|
maxJoinRetries = Math.max(maxJoinRetries, 1);
|
|
14404
14443
|
try {
|
|
14405
|
-
|
|
14406
|
-
|
|
14407
|
-
|
|
14408
|
-
|
|
14409
|
-
|
|
14410
|
-
|
|
14411
|
-
|
|
14412
|
-
|
|
14413
|
-
catch (err) {
|
|
14414
|
-
this.logger.warn(`Failed to join call (${attempt})`, this.cid);
|
|
14415
|
-
if ((err instanceof ErrorFromResponse && err.unrecoverable) ||
|
|
14416
|
-
(err instanceof SfuJoinError && err.unrecoverable)) {
|
|
14417
|
-
// if the error is unrecoverable, we should not retry as that signals
|
|
14418
|
-
// that connectivity is good, but the coordinator doesn't allow the user
|
|
14419
|
-
// to join the call due to some reason (e.g., ended call, expired token...)
|
|
14420
|
-
throw err;
|
|
14421
|
-
}
|
|
14422
|
-
// immediately switch to a different SFU in case of recoverable join error
|
|
14423
|
-
const switchSfu = err instanceof SfuJoinError &&
|
|
14424
|
-
SfuJoinError.isJoinErrorCode(err.errorEvent);
|
|
14425
|
-
const sfuId = this.credentials?.server.edge_name || '';
|
|
14426
|
-
const failures = (sfuJoinFailures.get(sfuId) || 0) + 1;
|
|
14427
|
-
sfuJoinFailures.set(sfuId, failures);
|
|
14428
|
-
if (switchSfu || failures >= 2) {
|
|
14429
|
-
joinData.migrating_from = sfuId;
|
|
14430
|
-
joinData.migrating_from_list = Array.from(sfuJoinFailures.keys());
|
|
14444
|
+
await this.clientEventReporter.withJoinLifecycle(this.cid, 'first-attempt', async () => {
|
|
14445
|
+
for (let attempt = 0; attempt < maxJoinRetries; attempt++) {
|
|
14446
|
+
try {
|
|
14447
|
+
this.logger.trace(`Joining call (${attempt})`, this.cid);
|
|
14448
|
+
await this.doJoin(data);
|
|
14449
|
+
delete joinData.migrating_from;
|
|
14450
|
+
delete joinData.migrating_from_list;
|
|
14451
|
+
return;
|
|
14431
14452
|
}
|
|
14432
|
-
|
|
14433
|
-
|
|
14453
|
+
catch (err) {
|
|
14454
|
+
this.logger.warn(`Failed to join call (${attempt})`, this.cid);
|
|
14455
|
+
if ((err instanceof ErrorFromResponse && err.unrecoverable) ||
|
|
14456
|
+
(err instanceof SfuJoinError && err.unrecoverable)) {
|
|
14457
|
+
throw err;
|
|
14458
|
+
}
|
|
14459
|
+
const switchSfu = err instanceof SfuJoinError &&
|
|
14460
|
+
SfuJoinError.isJoinErrorCode(err.errorEvent);
|
|
14461
|
+
const sfuId = this.credentials?.server.edge_name;
|
|
14462
|
+
if (sfuId) {
|
|
14463
|
+
const failures = (sfuJoinFailures.get(sfuId) || 0) + 1;
|
|
14464
|
+
sfuJoinFailures.set(sfuId, failures);
|
|
14465
|
+
if (switchSfu || failures >= 2) {
|
|
14466
|
+
joinData.migrating_from = sfuId;
|
|
14467
|
+
joinData.migrating_from_list = Array.from(sfuJoinFailures.keys());
|
|
14468
|
+
if (attempt < maxJoinRetries - 1) {
|
|
14469
|
+
this.clientEventReporter.startCorrelation(this.cid, 'first-attempt');
|
|
14470
|
+
}
|
|
14471
|
+
}
|
|
14472
|
+
}
|
|
14473
|
+
if (attempt === maxJoinRetries - 1) {
|
|
14474
|
+
throw err;
|
|
14475
|
+
}
|
|
14434
14476
|
}
|
|
14477
|
+
await sleep(retryInterval(attempt));
|
|
14435
14478
|
}
|
|
14436
|
-
|
|
14437
|
-
}
|
|
14479
|
+
});
|
|
14438
14480
|
}
|
|
14439
14481
|
catch (error) {
|
|
14440
14482
|
callingX?.endCall(this, 'error');
|
|
@@ -14463,7 +14505,7 @@ class Call {
|
|
|
14463
14505
|
performingMigration ||
|
|
14464
14506
|
data?.migrating_from) {
|
|
14465
14507
|
try {
|
|
14466
|
-
const joinResponse = await this.doJoinRequest(data);
|
|
14508
|
+
const joinResponse = await this.clientEventReporter.track(this.cid, 'CoordinatorJoin', () => this.doJoinRequest(data));
|
|
14467
14509
|
this.credentials = joinResponse.credentials;
|
|
14468
14510
|
statsOptions = joinResponse.stats_options;
|
|
14469
14511
|
this.lastStatsOptions = statsOptions;
|
|
@@ -14521,9 +14563,11 @@ class Call {
|
|
|
14521
14563
|
const preferredSubscribeOptions = !isReconnecting
|
|
14522
14564
|
? this.getPreferredSubscribeOptions()
|
|
14523
14565
|
: [];
|
|
14566
|
+
const unifiedSessionId = this.unifiedSessionId;
|
|
14567
|
+
const capabilities = Array.from(this.clientCapabilities);
|
|
14524
14568
|
try {
|
|
14525
|
-
const { callState, fastReconnectDeadlineSeconds, publishOptions } = await sfuClient.join({
|
|
14526
|
-
unifiedSessionId
|
|
14569
|
+
const { callState, fastReconnectDeadlineSeconds, publishOptions } = await this.clientEventReporter.track(this.cid, 'WSJoin', () => sfuClient.join({
|
|
14570
|
+
unifiedSessionId,
|
|
14527
14571
|
subscriberSdp,
|
|
14528
14572
|
publisherSdp,
|
|
14529
14573
|
clientDetails,
|
|
@@ -14531,9 +14575,9 @@ class Call {
|
|
|
14531
14575
|
reconnectDetails,
|
|
14532
14576
|
preferredPublishOptions,
|
|
14533
14577
|
preferredSubscribeOptions,
|
|
14534
|
-
capabilities
|
|
14578
|
+
capabilities,
|
|
14535
14579
|
source: ParticipantSource.WEBRTC_UNSPECIFIED,
|
|
14536
|
-
});
|
|
14580
|
+
}));
|
|
14537
14581
|
this.currentPublishOptions = publishOptions;
|
|
14538
14582
|
this.fastReconnectDeadlineSeconds = fastReconnectDeadlineSeconds;
|
|
14539
14583
|
if (callState) {
|
|
@@ -14745,6 +14789,16 @@ class Call {
|
|
|
14745
14789
|
// "ICE never connected" failure budget can be cleared.
|
|
14746
14790
|
this.iceFailuresWithoutConnect = 0;
|
|
14747
14791
|
},
|
|
14792
|
+
onPeerConnectionStateChange: (event) => {
|
|
14793
|
+
this.clientEventReporter.onPeerConnectionStateChange(this.cid, event);
|
|
14794
|
+
},
|
|
14795
|
+
onRemoteTrackUnmute: (trackType, trackId) => {
|
|
14796
|
+
const reportable = trackType === TrackType.AUDIO ||
|
|
14797
|
+
(isReactNative() && trackType === TrackType.VIDEO);
|
|
14798
|
+
if (!reportable)
|
|
14799
|
+
return;
|
|
14800
|
+
this.clientEventReporter.reportFirstFrame(this.cid, trackType, trackId);
|
|
14801
|
+
},
|
|
14748
14802
|
};
|
|
14749
14803
|
this.subscriber = new Subscriber(basePeerConnectionOptions);
|
|
14750
14804
|
// anonymous users can't publish anything hence, there is no need
|
|
@@ -14754,9 +14808,7 @@ class Call {
|
|
|
14754
14808
|
if (closePreviousInstances && this.publisher) {
|
|
14755
14809
|
await this.publisher.dispose();
|
|
14756
14810
|
}
|
|
14757
|
-
this.publisher = new Publisher(basePeerConnectionOptions, publishOptions
|
|
14758
|
-
selfSubEnabled: this.selfSubEnabled,
|
|
14759
|
-
});
|
|
14811
|
+
this.publisher = new Publisher(basePeerConnectionOptions, publishOptions);
|
|
14760
14812
|
}
|
|
14761
14813
|
this.statsReporter?.stop();
|
|
14762
14814
|
if (this.statsReportingIntervalInMs > 0) {
|
|
@@ -15023,7 +15075,10 @@ class Call {
|
|
|
15023
15075
|
const reconnectStartTime = Date.now();
|
|
15024
15076
|
this.reconnectStrategy = WebsocketReconnectStrategy.REJOIN;
|
|
15025
15077
|
this.state.setCallingState(CallingState.RECONNECTING);
|
|
15026
|
-
|
|
15078
|
+
const joinReason = this.reconnectReason === ReconnectReason.NETWORK_BACK_ONLINE
|
|
15079
|
+
? 'network-available'
|
|
15080
|
+
: 'full-rejoin';
|
|
15081
|
+
await this.clientEventReporter.withJoinLifecycle(this.cid, joinReason, () => this.doJoin(this.joinCallData));
|
|
15027
15082
|
await this.restorePublishedTracks();
|
|
15028
15083
|
this.restoreSubscribedTracks();
|
|
15029
15084
|
this.sfuStatsReporter?.sendReconnectionTime(WebsocketReconnectStrategy.REJOIN, (Date.now() - reconnectStartTime) / 1000);
|
|
@@ -15047,11 +15102,11 @@ class Call {
|
|
|
15047
15102
|
const migrationTask = makeSafePromise(currentSfuClient.enterMigration());
|
|
15048
15103
|
try {
|
|
15049
15104
|
const currentSfu = currentSfuClient.edgeName;
|
|
15050
|
-
await this.doJoin({
|
|
15105
|
+
await this.clientEventReporter.withJoinLifecycle(this.cid, 'migration', () => this.doJoin({
|
|
15051
15106
|
...this.joinCallData,
|
|
15052
15107
|
migrating_from: currentSfu,
|
|
15053
15108
|
migrating_from_list: [currentSfu],
|
|
15054
|
-
});
|
|
15109
|
+
}));
|
|
15055
15110
|
}
|
|
15056
15111
|
finally {
|
|
15057
15112
|
// cleanup the migration_from field after the migration is complete or failed
|
|
@@ -15087,11 +15142,22 @@ class Call {
|
|
|
15087
15142
|
this.registerReconnectHandlers = () => {
|
|
15088
15143
|
// handles the legacy "goAway" event
|
|
15089
15144
|
const unregisterGoAway = this.on('goAway', () => {
|
|
15145
|
+
this.clientEventReporter.captureWsError(this.cid, {
|
|
15146
|
+
code: 'SFU_GO_AWAY',
|
|
15147
|
+
reason: 'SFU goAway received during WS join',
|
|
15148
|
+
});
|
|
15090
15149
|
this.reconnect(WebsocketReconnectStrategy.MIGRATE, ReconnectReason.GO_AWAY).catch((err) => this.logger.warn('[Reconnect] Error reconnecting', err));
|
|
15091
15150
|
});
|
|
15092
15151
|
// handles the "error" event, through which the SFU can request a reconnect
|
|
15093
15152
|
const unregisterOnError = this.on('error', (e) => {
|
|
15094
15153
|
const { reconnectStrategy: strategy, error } = e;
|
|
15154
|
+
if (!SfuJoinError.isJoinErrorCode(e)) {
|
|
15155
|
+
const code = error?.code ? ErrorCode[error.code] : undefined;
|
|
15156
|
+
this.clientEventReporter.captureWsError(this.cid, {
|
|
15157
|
+
code: code ?? 'SFU_ERROR',
|
|
15158
|
+
reason: error?.message || 'SFU error during WS join',
|
|
15159
|
+
});
|
|
15160
|
+
}
|
|
15095
15161
|
// SFU_FULL is a join error, and when emitted, although it specifies a
|
|
15096
15162
|
// `migrate` strategy, we should actually perform a REJOIN to a new SFU.
|
|
15097
15163
|
// This is now handled separately in the `call.join()` method.
|
|
@@ -15786,7 +15852,9 @@ class Call {
|
|
|
15786
15852
|
this.leave({
|
|
15787
15853
|
reject: true,
|
|
15788
15854
|
reason: 'timeout',
|
|
15789
|
-
message: `ringing timeout - ${this.isCreatedByMe
|
|
15855
|
+
message: `ringing timeout - ${this.isCreatedByMe
|
|
15856
|
+
? 'no one accepted'
|
|
15857
|
+
: `user didn't interact with incoming call screen`}`,
|
|
15790
15858
|
}).catch((err) => {
|
|
15791
15859
|
this.logger.error('Failed to drop call', err);
|
|
15792
15860
|
});
|
|
@@ -15992,15 +16060,36 @@ class Call {
|
|
|
15992
16060
|
* @param trackType the kind of video.
|
|
15993
16061
|
*/
|
|
15994
16062
|
this.bindVideoElement = (videoElement, sessionId, trackType) => {
|
|
15995
|
-
const
|
|
15996
|
-
|
|
16063
|
+
const unbindDynascale = this.dynascaleManager?.bindVideoElement(videoElement, sessionId, trackType);
|
|
16064
|
+
const stopFirstFrameDetector = this.bindFirstVideoFrameDetector(videoElement, sessionId, trackType);
|
|
16065
|
+
if (!unbindDynascale && !stopFirstFrameDetector)
|
|
15997
16066
|
return;
|
|
16067
|
+
const unbind = () => {
|
|
16068
|
+
stopFirstFrameDetector?.();
|
|
16069
|
+
unbindDynascale?.();
|
|
16070
|
+
};
|
|
15998
16071
|
this.leaveCallHooks.add(unbind);
|
|
15999
16072
|
return () => {
|
|
16000
16073
|
this.leaveCallHooks.delete(unbind);
|
|
16001
16074
|
unbind();
|
|
16002
16075
|
};
|
|
16003
16076
|
};
|
|
16077
|
+
this.bindFirstVideoFrameDetector = (videoElement, sessionId, trackType) => {
|
|
16078
|
+
if (trackType !== 'videoTrack')
|
|
16079
|
+
return;
|
|
16080
|
+
return createFirstVideoFrameDetector(videoElement, () => {
|
|
16081
|
+
this.reportFirstRenderedVideoFrame(sessionId);
|
|
16082
|
+
});
|
|
16083
|
+
};
|
|
16084
|
+
this.reportFirstRenderedVideoFrame = (sessionId) => {
|
|
16085
|
+
const participant = this.state.findParticipantBySessionId(sessionId);
|
|
16086
|
+
if (participant?.isLocalParticipant)
|
|
16087
|
+
return;
|
|
16088
|
+
const trackId = participant?.videoStream?.getVideoTracks()[0]?.id;
|
|
16089
|
+
if (!trackId)
|
|
16090
|
+
return;
|
|
16091
|
+
this.clientEventReporter.reportFirstFrame(this.cid, TrackType.VIDEO, trackId);
|
|
16092
|
+
};
|
|
16004
16093
|
/**
|
|
16005
16094
|
* Binds a DOM <audio> element to the given session id.
|
|
16006
16095
|
*
|
|
@@ -16150,6 +16239,7 @@ class Call {
|
|
|
16150
16239
|
this.ringingSubject = new BehaviorSubject(ringing);
|
|
16151
16240
|
this.watching = watching;
|
|
16152
16241
|
this.streamClient = streamClient;
|
|
16242
|
+
this.clientEventReporter = clientEventReporter;
|
|
16153
16243
|
this.clientStore = clientStore;
|
|
16154
16244
|
this.streamClientBasePath = `/call/${this.type}/${this.id}`;
|
|
16155
16245
|
this.logger = videoLoggerSystem.getLogger('Call');
|
|
@@ -16186,12 +16276,6 @@ class Call {
|
|
|
16186
16276
|
get currentUserId() {
|
|
16187
16277
|
return this.clientStore.connectedUser?.id;
|
|
16188
16278
|
}
|
|
16189
|
-
/**
|
|
16190
|
-
* A flag indicating whether self-subscription is enabled for the call.
|
|
16191
|
-
*/
|
|
16192
|
-
get isSelfSubEnabled() {
|
|
16193
|
-
return this.selfSubEnabled;
|
|
16194
|
-
}
|
|
16195
16279
|
/**
|
|
16196
16280
|
* A flag indicating whether the call was created by the current user.
|
|
16197
16281
|
*/
|
|
@@ -17378,10 +17462,12 @@ class StreamClient {
|
|
|
17378
17462
|
this.logger.info('StreamClient.connect: this.wsConnection.connect()');
|
|
17379
17463
|
return await this.wsConnection.connect(this.defaultWSTimeout);
|
|
17380
17464
|
};
|
|
17465
|
+
this.getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
|
|
17466
|
+
"1.53.1";
|
|
17381
17467
|
this.getUserAgent = () => {
|
|
17382
17468
|
if (!this.cachedUserAgent) {
|
|
17383
17469
|
const { clientAppIdentifier = {} } = this.options;
|
|
17384
|
-
const { sdkName = 'js', sdkVersion = "1.
|
|
17470
|
+
const { sdkName = 'js', sdkVersion = "1.53.1", ...extras } = clientAppIdentifier;
|
|
17385
17471
|
this.cachedUserAgent = [
|
|
17386
17472
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
17387
17473
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|
|
@@ -17558,6 +17644,606 @@ const createTokenOrProvider = (options) => {
|
|
|
17558
17644
|
return token || tokenProvider;
|
|
17559
17645
|
};
|
|
17560
17646
|
|
|
17647
|
+
const pcKey = (cid, role) => `${cid}:${role}`;
|
|
17648
|
+
class ClientEventReporter {
|
|
17649
|
+
constructor(options) {
|
|
17650
|
+
this.logger = videoLoggerSystem.getLogger('ClientEventReporter');
|
|
17651
|
+
this.callContexts = new Map();
|
|
17652
|
+
this.joinAttemptIds = new Map();
|
|
17653
|
+
this.joinReasons = new Map();
|
|
17654
|
+
this.coordinatorPairs = new Map();
|
|
17655
|
+
this.wsPairs = new Map();
|
|
17656
|
+
this.peerConnectionPairs = new Map();
|
|
17657
|
+
this.pcEverConnected = new Map();
|
|
17658
|
+
this.firstFrameReported = new Set();
|
|
17659
|
+
/**
|
|
17660
|
+
* Starts a new coordinator connection correlation scope.
|
|
17661
|
+
*
|
|
17662
|
+
* @param userId the id of the user being connected. Captured here because
|
|
17663
|
+
* the `CoordinatorWS` stage emits before the connection flow assigns
|
|
17664
|
+
* the user to the client, so it can't be read from the client yet.
|
|
17665
|
+
*/
|
|
17666
|
+
this.startCoordinatorConnection = (userId) => {
|
|
17667
|
+
this.coordinatorConnectId = generateUUIDv4();
|
|
17668
|
+
this.coordinatorConnectUserId = userId;
|
|
17669
|
+
return this.coordinatorConnectId;
|
|
17670
|
+
};
|
|
17671
|
+
this.trackCoordinatorWs = async (op) => {
|
|
17672
|
+
this.beginCoordinatorWs();
|
|
17673
|
+
try {
|
|
17674
|
+
const result = await op();
|
|
17675
|
+
this.succeedCoordinatorWs();
|
|
17676
|
+
return result;
|
|
17677
|
+
}
|
|
17678
|
+
catch (err) {
|
|
17679
|
+
applyError(this.coordinatorWsPair, mapCoordinatorWsError(err));
|
|
17680
|
+
throw err;
|
|
17681
|
+
}
|
|
17682
|
+
};
|
|
17683
|
+
this.beginCoordinatorWs = () => {
|
|
17684
|
+
if (!this.coordinatorWsPair) {
|
|
17685
|
+
this.coordinatorWsPair = {
|
|
17686
|
+
sid: generateUUIDv4(),
|
|
17687
|
+
attempts: 0,
|
|
17688
|
+
startedAt: Date.now(),
|
|
17689
|
+
userIdSnapshot: this.coordinatorConnectUserId,
|
|
17690
|
+
};
|
|
17691
|
+
this.send({
|
|
17692
|
+
...this.buildCoordinatorWsCommon(this.coordinatorWsPair),
|
|
17693
|
+
event_type: 'initiated',
|
|
17694
|
+
});
|
|
17695
|
+
}
|
|
17696
|
+
this.coordinatorWsPair.attempts++;
|
|
17697
|
+
};
|
|
17698
|
+
this.succeedCoordinatorWs = () => {
|
|
17699
|
+
const pair = this.coordinatorWsPair;
|
|
17700
|
+
if (!pair)
|
|
17701
|
+
return;
|
|
17702
|
+
this.send({
|
|
17703
|
+
...this.buildCoordinatorWsCommon(pair),
|
|
17704
|
+
event_type: 'completed',
|
|
17705
|
+
outcome: 'success',
|
|
17706
|
+
retry_count_attempt: pair.attempts - 1,
|
|
17707
|
+
elapsed_time: Date.now() - pair.startedAt,
|
|
17708
|
+
});
|
|
17709
|
+
this.coordinatorWsPair = undefined;
|
|
17710
|
+
};
|
|
17711
|
+
this.closeCoordinatorWs = () => {
|
|
17712
|
+
const pair = this.coordinatorWsPair;
|
|
17713
|
+
if (!pair || !pair.lastError) {
|
|
17714
|
+
this.coordinatorWsPair = undefined;
|
|
17715
|
+
return;
|
|
17716
|
+
}
|
|
17717
|
+
const { reason, code } = pair.lastError;
|
|
17718
|
+
this.send({
|
|
17719
|
+
...this.buildCoordinatorWsCommon(pair),
|
|
17720
|
+
event_type: 'completed',
|
|
17721
|
+
outcome: 'failure',
|
|
17722
|
+
retry_count_attempt: pair.attempts - 1,
|
|
17723
|
+
elapsed_time: Date.now() - pair.startedAt,
|
|
17724
|
+
retry_failure_reason: reason,
|
|
17725
|
+
retry_failure_code: code,
|
|
17726
|
+
});
|
|
17727
|
+
this.coordinatorWsPair = undefined;
|
|
17728
|
+
};
|
|
17729
|
+
this.buildCoordinatorWsCommon = (pair) => ({
|
|
17730
|
+
user_id: pair.userIdSnapshot ?? this.streamClient.userID,
|
|
17731
|
+
stage: 'CoordinatorWS',
|
|
17732
|
+
stage_id: pair.sid,
|
|
17733
|
+
...(this.coordinatorConnectId && {
|
|
17734
|
+
coordinator_connect_id: this.coordinatorConnectId,
|
|
17735
|
+
}),
|
|
17736
|
+
timestamp: new Date().toISOString(),
|
|
17737
|
+
user_agent: this.streamClient.getUserAgent(),
|
|
17738
|
+
sdk_version: this.streamClient.getSdkVersion(),
|
|
17739
|
+
});
|
|
17740
|
+
this.emitMediaPermission = (cid) => {
|
|
17741
|
+
if (isReactNative() || !this.callContexts.has(cid))
|
|
17742
|
+
return;
|
|
17743
|
+
const pair = {
|
|
17744
|
+
sid: generateUUIDv4(),
|
|
17745
|
+
attempts: 0,
|
|
17746
|
+
startedAt: Date.now(),
|
|
17747
|
+
joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
|
|
17748
|
+
};
|
|
17749
|
+
this.send({
|
|
17750
|
+
...this.buildCommon(cid, 'MediaDevicePermission', pair),
|
|
17751
|
+
...this.sessionIdField(cid),
|
|
17752
|
+
microphone_permission_status: readPermissionStatus(getAudioBrowserPermission()),
|
|
17753
|
+
camera_permission_status: readPermissionStatus(getVideoBrowserPermission()),
|
|
17754
|
+
event_type: 'initiated',
|
|
17755
|
+
});
|
|
17756
|
+
};
|
|
17757
|
+
this.registerCall = (cid, ctx) => {
|
|
17758
|
+
this.callContexts.set(cid, ctx);
|
|
17759
|
+
};
|
|
17760
|
+
this.unregisterCall = (cid) => {
|
|
17761
|
+
this.callContexts.delete(cid);
|
|
17762
|
+
this.joinAttemptIds.delete(cid);
|
|
17763
|
+
this.joinReasons.delete(cid);
|
|
17764
|
+
this.coordinatorPairs.delete(cid);
|
|
17765
|
+
this.wsPairs.delete(cid);
|
|
17766
|
+
this.firstFrameReported.delete(`${cid}:FirstVideoFrame`);
|
|
17767
|
+
this.firstFrameReported.delete(`${cid}:FirstAudioFrame`);
|
|
17768
|
+
for (const role of ['publish', 'subscribe']) {
|
|
17769
|
+
const key = pcKey(cid, role);
|
|
17770
|
+
this.peerConnectionPairs.delete(key);
|
|
17771
|
+
this.pcEverConnected.delete(key);
|
|
17772
|
+
}
|
|
17773
|
+
};
|
|
17774
|
+
this.startCorrelation = (cid, joinReason) => {
|
|
17775
|
+
try {
|
|
17776
|
+
this.closeCallPairs(cid);
|
|
17777
|
+
this.joinAttemptIds.set(cid, generateUUIDv4());
|
|
17778
|
+
this.joinReasons.set(cid, joinReason);
|
|
17779
|
+
this.firstFrameReported.delete(`${cid}:FirstVideoFrame`);
|
|
17780
|
+
this.firstFrameReported.delete(`${cid}:FirstAudioFrame`);
|
|
17781
|
+
this.emitJoinInitiated(cid);
|
|
17782
|
+
this.emitMediaPermission(cid);
|
|
17783
|
+
}
|
|
17784
|
+
catch (err) {
|
|
17785
|
+
this.logger.warn('Failed to start join correlation', err);
|
|
17786
|
+
}
|
|
17787
|
+
};
|
|
17788
|
+
this.withJoinLifecycle = async (cid, joinReason, op) => {
|
|
17789
|
+
this.startCorrelation(cid, joinReason);
|
|
17790
|
+
try {
|
|
17791
|
+
return await op();
|
|
17792
|
+
}
|
|
17793
|
+
catch (err) {
|
|
17794
|
+
this.closeCallPairs(cid);
|
|
17795
|
+
throw err;
|
|
17796
|
+
}
|
|
17797
|
+
};
|
|
17798
|
+
this.track = async (cid, stage, op) => {
|
|
17799
|
+
this.beginAttempt(cid, stage);
|
|
17800
|
+
try {
|
|
17801
|
+
const result = await op();
|
|
17802
|
+
this.succeedAttempt(cid, stage);
|
|
17803
|
+
return result;
|
|
17804
|
+
}
|
|
17805
|
+
catch (err) {
|
|
17806
|
+
this.applyStageError(cid, stage, err);
|
|
17807
|
+
throw err;
|
|
17808
|
+
}
|
|
17809
|
+
};
|
|
17810
|
+
this.reportFirstFrame = (cid, trackType, trackId) => {
|
|
17811
|
+
const stage = trackType === TrackType.VIDEO
|
|
17812
|
+
? 'FirstVideoFrame'
|
|
17813
|
+
: trackType === TrackType.AUDIO
|
|
17814
|
+
? 'FirstAudioFrame'
|
|
17815
|
+
: undefined;
|
|
17816
|
+
if (!stage)
|
|
17817
|
+
return;
|
|
17818
|
+
const key = `${cid}:${stage}`;
|
|
17819
|
+
if (this.firstFrameReported.has(key))
|
|
17820
|
+
return;
|
|
17821
|
+
this.firstFrameReported.add(key);
|
|
17822
|
+
const pair = {
|
|
17823
|
+
sid: generateUUIDv4(),
|
|
17824
|
+
attempts: 0,
|
|
17825
|
+
startedAt: Date.now(),
|
|
17826
|
+
joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
|
|
17827
|
+
};
|
|
17828
|
+
const resolvedSfuId = this.getSfuId(cid);
|
|
17829
|
+
this.send({
|
|
17830
|
+
...this.buildCommon(cid, stage, pair),
|
|
17831
|
+
...this.sessionIdField(cid),
|
|
17832
|
+
...(resolvedSfuId && { sfu_id: resolvedSfuId }),
|
|
17833
|
+
track_id: trackId,
|
|
17834
|
+
event_type: 'initiated',
|
|
17835
|
+
});
|
|
17836
|
+
};
|
|
17837
|
+
this.captureWsError = (cid, opts) => {
|
|
17838
|
+
const pair = this.wsPairs.get(cid);
|
|
17839
|
+
if (!pair)
|
|
17840
|
+
return;
|
|
17841
|
+
applyError(pair, { reason: opts.reason, code: opts.code });
|
|
17842
|
+
};
|
|
17843
|
+
this.close = (cid) => {
|
|
17844
|
+
this.closeCallPairs(cid);
|
|
17845
|
+
};
|
|
17846
|
+
this.abort = (cid, opts) => {
|
|
17847
|
+
try {
|
|
17848
|
+
const { code, reason } = opts;
|
|
17849
|
+
const stageError = { code, reason };
|
|
17850
|
+
applyErrorIfAbsent(this.coordinatorPairs.get(cid), stageError);
|
|
17851
|
+
applyErrorIfAbsent(this.wsPairs.get(cid), stageError);
|
|
17852
|
+
this.failCoordinator(cid);
|
|
17853
|
+
this.failWs(cid);
|
|
17854
|
+
this.emitPeerConnectionFailure(cid, 'publish', code, reason, 'NOT_CONNECTED');
|
|
17855
|
+
this.emitPeerConnectionFailure(cid, 'subscribe', code, reason, 'NOT_CONNECTED');
|
|
17856
|
+
}
|
|
17857
|
+
catch (err) {
|
|
17858
|
+
this.logger.warn('Failed to report abort', err);
|
|
17859
|
+
}
|
|
17860
|
+
};
|
|
17861
|
+
this.closeCallPairs = (cid) => {
|
|
17862
|
+
if (this.coordinatorPairs.get(cid))
|
|
17863
|
+
this.failCoordinator(cid);
|
|
17864
|
+
if (this.wsPairs.get(cid))
|
|
17865
|
+
this.failWs(cid);
|
|
17866
|
+
for (const role of ['publish', 'subscribe']) {
|
|
17867
|
+
this.emitPeerConnectionFailure(cid, role, 'CLIENT_ABORTED', 'superseded by a new join attempt', 'NOT_CONNECTED');
|
|
17868
|
+
}
|
|
17869
|
+
};
|
|
17870
|
+
this.emitJoinInitiated = (cid) => {
|
|
17871
|
+
const joinAttemptId = this.joinAttemptIds.get(cid);
|
|
17872
|
+
if (!joinAttemptId)
|
|
17873
|
+
return;
|
|
17874
|
+
const coordinatorConnectId = this.coordinatorConnectId;
|
|
17875
|
+
const ctx = this.callContexts.get(cid);
|
|
17876
|
+
this.send({
|
|
17877
|
+
user_id: this.streamClient.userID,
|
|
17878
|
+
type: ctx?.callType,
|
|
17879
|
+
id: ctx?.callId,
|
|
17880
|
+
call_cid: cid,
|
|
17881
|
+
stage: 'JoinInitiated',
|
|
17882
|
+
join_attempt_id: joinAttemptId,
|
|
17883
|
+
...(coordinatorConnectId && {
|
|
17884
|
+
coordinator_connect_id: coordinatorConnectId,
|
|
17885
|
+
}),
|
|
17886
|
+
timestamp: new Date().toISOString(),
|
|
17887
|
+
user_agent: this.streamClient.getUserAgent(),
|
|
17888
|
+
sdk_version: this.streamClient.getSdkVersion(),
|
|
17889
|
+
event_type: 'initiated',
|
|
17890
|
+
});
|
|
17891
|
+
};
|
|
17892
|
+
this.beginAttempt = (cid, stage) => {
|
|
17893
|
+
if (stage === 'CoordinatorJoin')
|
|
17894
|
+
this.beginCoordinatorAttempt(cid);
|
|
17895
|
+
else
|
|
17896
|
+
this.beginWsAttempt(cid);
|
|
17897
|
+
};
|
|
17898
|
+
this.succeedAttempt = (cid, stage) => {
|
|
17899
|
+
if (stage === 'CoordinatorJoin')
|
|
17900
|
+
this.succeedCoordinator(cid);
|
|
17901
|
+
else
|
|
17902
|
+
this.succeedWs(cid);
|
|
17903
|
+
};
|
|
17904
|
+
this.applyStageError = (cid, stage, err) => {
|
|
17905
|
+
const pair = stage === 'CoordinatorJoin'
|
|
17906
|
+
? this.coordinatorPairs.get(cid)
|
|
17907
|
+
: this.wsPairs.get(cid);
|
|
17908
|
+
applyErrorIfAbsent(pair, stage === 'CoordinatorJoin'
|
|
17909
|
+
? mapCoordinatorHttpError(err)
|
|
17910
|
+
: mapWsJoinError(err));
|
|
17911
|
+
};
|
|
17912
|
+
this.beginCoordinatorAttempt = (cid) => {
|
|
17913
|
+
let pair = this.coordinatorPairs.get(cid);
|
|
17914
|
+
if (!pair) {
|
|
17915
|
+
pair = {
|
|
17916
|
+
sid: generateUUIDv4(),
|
|
17917
|
+
attempts: 0,
|
|
17918
|
+
startedAt: Date.now(),
|
|
17919
|
+
joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
|
|
17920
|
+
joinReasonSnapshot: this.joinReasons.get(cid),
|
|
17921
|
+
};
|
|
17922
|
+
this.coordinatorPairs.set(cid, pair);
|
|
17923
|
+
this.send({
|
|
17924
|
+
...this.buildCommon(cid, 'CoordinatorJoin', pair),
|
|
17925
|
+
...(pair.joinReasonSnapshot && {
|
|
17926
|
+
join_reason: pair.joinReasonSnapshot,
|
|
17927
|
+
}),
|
|
17928
|
+
event_type: 'initiated',
|
|
17929
|
+
});
|
|
17930
|
+
}
|
|
17931
|
+
pair.lastError = undefined;
|
|
17932
|
+
pair.attempts++;
|
|
17933
|
+
};
|
|
17934
|
+
this.succeedCoordinator = (cid) => {
|
|
17935
|
+
const pair = this.coordinatorPairs.get(cid);
|
|
17936
|
+
if (!pair)
|
|
17937
|
+
return;
|
|
17938
|
+
this.send({
|
|
17939
|
+
...this.buildCommon(cid, 'CoordinatorJoin', pair),
|
|
17940
|
+
...this.sessionIdField(cid),
|
|
17941
|
+
...(pair.joinReasonSnapshot && { join_reason: pair.joinReasonSnapshot }),
|
|
17942
|
+
event_type: 'completed',
|
|
17943
|
+
outcome: 'success',
|
|
17944
|
+
retry_count_attempt: pair.attempts - 1,
|
|
17945
|
+
elapsed_time: Date.now() - pair.startedAt,
|
|
17946
|
+
});
|
|
17947
|
+
this.coordinatorPairs.delete(cid);
|
|
17948
|
+
};
|
|
17949
|
+
this.failCoordinator = (cid) => {
|
|
17950
|
+
const pair = this.coordinatorPairs.get(cid);
|
|
17951
|
+
if (!pair || !pair.lastError) {
|
|
17952
|
+
this.coordinatorPairs.delete(cid);
|
|
17953
|
+
return;
|
|
17954
|
+
}
|
|
17955
|
+
const { reason, code } = pair.lastError;
|
|
17956
|
+
this.send({
|
|
17957
|
+
...this.buildCommon(cid, 'CoordinatorJoin', pair),
|
|
17958
|
+
...this.sessionIdField(cid),
|
|
17959
|
+
...(pair.joinReasonSnapshot && { join_reason: pair.joinReasonSnapshot }),
|
|
17960
|
+
event_type: 'completed',
|
|
17961
|
+
outcome: 'failure',
|
|
17962
|
+
retry_count_attempt: pair.attempts - 1,
|
|
17963
|
+
elapsed_time: Date.now() - pair.startedAt,
|
|
17964
|
+
retry_failure_reason: reason,
|
|
17965
|
+
retry_failure_code: code,
|
|
17966
|
+
});
|
|
17967
|
+
this.coordinatorPairs.delete(cid);
|
|
17968
|
+
};
|
|
17969
|
+
this.beginWsAttempt = (cid) => {
|
|
17970
|
+
let pair = this.wsPairs.get(cid);
|
|
17971
|
+
if (!pair) {
|
|
17972
|
+
pair = {
|
|
17973
|
+
sid: generateUUIDv4(),
|
|
17974
|
+
attempts: 0,
|
|
17975
|
+
startedAt: Date.now(),
|
|
17976
|
+
joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
|
|
17977
|
+
};
|
|
17978
|
+
this.wsPairs.set(cid, pair);
|
|
17979
|
+
const sfuId = this.getSfuId(cid);
|
|
17980
|
+
this.send({
|
|
17981
|
+
...this.buildCommon(cid, 'WSJoin', pair),
|
|
17982
|
+
...this.sessionIdField(cid),
|
|
17983
|
+
...(sfuId && { sfu_id: sfuId }),
|
|
17984
|
+
event_type: 'initiated',
|
|
17985
|
+
});
|
|
17986
|
+
}
|
|
17987
|
+
pair.lastError = undefined;
|
|
17988
|
+
pair.attempts++;
|
|
17989
|
+
};
|
|
17990
|
+
this.succeedWs = (cid) => {
|
|
17991
|
+
const pair = this.wsPairs.get(cid);
|
|
17992
|
+
if (!pair)
|
|
17993
|
+
return;
|
|
17994
|
+
const sfuId = this.getSfuId(cid);
|
|
17995
|
+
this.send({
|
|
17996
|
+
...this.buildCommon(cid, 'WSJoin', pair),
|
|
17997
|
+
...this.sessionIdField(cid),
|
|
17998
|
+
...(sfuId && { sfu_id: sfuId }),
|
|
17999
|
+
event_type: 'completed',
|
|
18000
|
+
outcome: 'success',
|
|
18001
|
+
retry_count_attempt: pair.attempts - 1,
|
|
18002
|
+
elapsed_time: Date.now() - pair.startedAt,
|
|
18003
|
+
});
|
|
18004
|
+
this.wsPairs.delete(cid);
|
|
18005
|
+
};
|
|
18006
|
+
this.failWs = (cid) => {
|
|
18007
|
+
const pair = this.wsPairs.get(cid);
|
|
18008
|
+
if (!pair || !pair.lastError) {
|
|
18009
|
+
this.wsPairs.delete(cid);
|
|
18010
|
+
return;
|
|
18011
|
+
}
|
|
18012
|
+
const { reason, code } = pair.lastError;
|
|
18013
|
+
const sfuId = this.getSfuId(cid);
|
|
18014
|
+
this.send({
|
|
18015
|
+
...this.buildCommon(cid, 'WSJoin', pair),
|
|
18016
|
+
...this.sessionIdField(cid),
|
|
18017
|
+
event_type: 'completed',
|
|
18018
|
+
outcome: 'failure',
|
|
18019
|
+
retry_count_attempt: pair.attempts - 1,
|
|
18020
|
+
elapsed_time: Date.now() - pair.startedAt,
|
|
18021
|
+
...(sfuId && { sfu_id: sfuId }),
|
|
18022
|
+
retry_failure_reason: reason,
|
|
18023
|
+
retry_failure_code: code,
|
|
18024
|
+
});
|
|
18025
|
+
this.wsPairs.delete(cid);
|
|
18026
|
+
};
|
|
18027
|
+
this.onPeerConnectionStateChange = (cid, event) => {
|
|
18028
|
+
const role = event.peerType === PeerType.SUBSCRIBER ? 'subscribe' : 'publish';
|
|
18029
|
+
if (event.stateType === 'ice' && event.state === 'failed') {
|
|
18030
|
+
this.emitPeerConnectionFailure(cid, role, 'ICE_CONNECTIVITY_FAILED', 'ICE connectivity checks failed', 'FAILED');
|
|
18031
|
+
return;
|
|
18032
|
+
}
|
|
18033
|
+
if (event.stateType === 'peerConnection' && event.state === 'failed') {
|
|
18034
|
+
this.emitPeerConnectionFailure(cid, role, 'DTLS_CONNECTIVITY_FAILED', 'DTLS connectivity checks failed', 'CONNECTED');
|
|
18035
|
+
return;
|
|
18036
|
+
}
|
|
18037
|
+
if (event.stateType !== 'peerConnection')
|
|
18038
|
+
return;
|
|
18039
|
+
switch (event.state) {
|
|
18040
|
+
case 'connecting':
|
|
18041
|
+
if (this.peerConnectionPairs.has(pcKey(cid, role)))
|
|
18042
|
+
return;
|
|
18043
|
+
this.openPeerConnectionPair(cid, role);
|
|
18044
|
+
break;
|
|
18045
|
+
case 'connected':
|
|
18046
|
+
this.emitPeerConnectionSuccess(cid, role);
|
|
18047
|
+
this.pcEverConnected.set(pcKey(cid, role), true);
|
|
18048
|
+
break;
|
|
18049
|
+
}
|
|
18050
|
+
};
|
|
18051
|
+
this.openPeerConnectionPair = (cid, role) => {
|
|
18052
|
+
const key = pcKey(cid, role);
|
|
18053
|
+
const pair = {
|
|
18054
|
+
sid: generateUUIDv4(),
|
|
18055
|
+
attempts: 0,
|
|
18056
|
+
startedAt: Date.now(),
|
|
18057
|
+
joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
|
|
18058
|
+
sfuId: this.getSfuId(cid),
|
|
18059
|
+
userSessionId: this.getUserSessionId(cid),
|
|
18060
|
+
wasPreviouslyConnected: this.pcEverConnected.get(key) === true,
|
|
18061
|
+
};
|
|
18062
|
+
this.peerConnectionPairs.set(key, pair);
|
|
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: 'initiated',
|
|
18073
|
+
});
|
|
18074
|
+
};
|
|
18075
|
+
this.emitPeerConnectionSuccess = (cid, role) => {
|
|
18076
|
+
const key = pcKey(cid, role);
|
|
18077
|
+
const pair = this.peerConnectionPairs.get(key);
|
|
18078
|
+
if (!pair)
|
|
18079
|
+
return;
|
|
18080
|
+
this.send({
|
|
18081
|
+
...this.buildCommon(cid, 'PeerConnectionConnect', pair),
|
|
18082
|
+
...this.sessionIdField(cid),
|
|
18083
|
+
peer_connection: role,
|
|
18084
|
+
was_previously_connected: pair.wasPreviouslyConnected,
|
|
18085
|
+
...(pair.sfuId && { sfu_id: pair.sfuId }),
|
|
18086
|
+
...(pair.userSessionId && {
|
|
18087
|
+
user_session_id: pair.userSessionId,
|
|
18088
|
+
}),
|
|
18089
|
+
event_type: 'completed',
|
|
18090
|
+
outcome: 'success',
|
|
18091
|
+
retry_count_attempt: 0,
|
|
18092
|
+
elapsed_time: Date.now() - pair.startedAt,
|
|
18093
|
+
});
|
|
18094
|
+
this.peerConnectionPairs.delete(key);
|
|
18095
|
+
};
|
|
18096
|
+
this.emitPeerConnectionFailure = (cid, role, code, reason, iceState) => {
|
|
18097
|
+
const key = pcKey(cid, role);
|
|
18098
|
+
const pair = this.peerConnectionPairs.get(key);
|
|
18099
|
+
if (!pair)
|
|
18100
|
+
return;
|
|
18101
|
+
this.send({
|
|
18102
|
+
...this.buildCommon(cid, 'PeerConnectionConnect', pair),
|
|
18103
|
+
...this.sessionIdField(cid),
|
|
18104
|
+
peer_connection: role,
|
|
18105
|
+
was_previously_connected: pair.wasPreviouslyConnected,
|
|
18106
|
+
...(pair.userSessionId && {
|
|
18107
|
+
user_session_id: pair.userSessionId,
|
|
18108
|
+
}),
|
|
18109
|
+
...(pair.sfuId && { sfu_id: pair.sfuId }),
|
|
18110
|
+
event_type: 'completed',
|
|
18111
|
+
outcome: 'failure',
|
|
18112
|
+
retry_count_attempt: 0,
|
|
18113
|
+
elapsed_time: Date.now() - pair.startedAt,
|
|
18114
|
+
ice_state: iceState,
|
|
18115
|
+
retry_failure_reason: reason,
|
|
18116
|
+
retry_failure_code: code,
|
|
18117
|
+
});
|
|
18118
|
+
this.peerConnectionPairs.delete(key);
|
|
18119
|
+
};
|
|
18120
|
+
this.getSfuId = (cid) => this.callContexts.get(cid)?.getSfuId() ?? '';
|
|
18121
|
+
this.getUserSessionId = (cid) => this.callContexts.get(cid)?.getUserSessionId() ?? '';
|
|
18122
|
+
this.sessionIdField = (cid) => {
|
|
18123
|
+
const callSessionId = this.callContexts.get(cid)?.getCallSessionId() ?? '';
|
|
18124
|
+
return callSessionId ? { call_session_id: callSessionId } : {};
|
|
18125
|
+
};
|
|
18126
|
+
this.buildCommon = (cid, stage, pair) => {
|
|
18127
|
+
const ctx = this.callContexts.get(cid);
|
|
18128
|
+
const coordinatorConnectId = this.coordinatorConnectId;
|
|
18129
|
+
return {
|
|
18130
|
+
user_id: this.streamClient.userID,
|
|
18131
|
+
type: ctx?.callType ?? '',
|
|
18132
|
+
id: ctx?.callId ?? '',
|
|
18133
|
+
call_cid: cid,
|
|
18134
|
+
stage,
|
|
18135
|
+
stage_id: pair.sid,
|
|
18136
|
+
...(pair.joinAttemptIdSnapshot && {
|
|
18137
|
+
join_attempt_id: pair.joinAttemptIdSnapshot,
|
|
18138
|
+
}),
|
|
18139
|
+
...(coordinatorConnectId && {
|
|
18140
|
+
coordinator_connect_id: coordinatorConnectId,
|
|
18141
|
+
}),
|
|
18142
|
+
timestamp: new Date().toISOString(),
|
|
18143
|
+
user_agent: this.streamClient.getUserAgent(),
|
|
18144
|
+
sdk_version: this.streamClient.getSdkVersion(),
|
|
18145
|
+
};
|
|
18146
|
+
};
|
|
18147
|
+
this.send = (body) => {
|
|
18148
|
+
void this.sendWithRetry(body);
|
|
18149
|
+
};
|
|
18150
|
+
this.sendWithRetry = async (body) => {
|
|
18151
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
18152
|
+
try {
|
|
18153
|
+
await this.streamClient.doAxiosRequest('post', '/call_client_event', { events: [body] }, { publicEndpoint: true });
|
|
18154
|
+
return true;
|
|
18155
|
+
}
|
|
18156
|
+
catch (err) {
|
|
18157
|
+
const status = err?.response
|
|
18158
|
+
?.status;
|
|
18159
|
+
if (typeof status === 'number' && status >= 400 && status < 500) {
|
|
18160
|
+
this.logger.debug(`Client event rejected (${status}), not retrying`, body.stage, body.event_type);
|
|
18161
|
+
return false;
|
|
18162
|
+
}
|
|
18163
|
+
if (attempt === 4) {
|
|
18164
|
+
this.logger.debug('Client event delivery failed after retries', body.stage, body.event_type, err);
|
|
18165
|
+
return false;
|
|
18166
|
+
}
|
|
18167
|
+
await sleep(retryInterval(attempt));
|
|
18168
|
+
}
|
|
18169
|
+
}
|
|
18170
|
+
return false;
|
|
18171
|
+
};
|
|
18172
|
+
this.streamClient = options.streamClient;
|
|
18173
|
+
}
|
|
18174
|
+
}
|
|
18175
|
+
const readPermissionStatus = (permission) => {
|
|
18176
|
+
const state = getCurrentValue(permission.asStateObservable());
|
|
18177
|
+
switch (state) {
|
|
18178
|
+
case 'granted':
|
|
18179
|
+
return 'GRANTED';
|
|
18180
|
+
case 'denied':
|
|
18181
|
+
return 'FAILED';
|
|
18182
|
+
case 'prompting':
|
|
18183
|
+
return 'INITIATED';
|
|
18184
|
+
case 'prompt':
|
|
18185
|
+
default:
|
|
18186
|
+
return 'NOT_INITIATED';
|
|
18187
|
+
}
|
|
18188
|
+
};
|
|
18189
|
+
const errorMessage = (err) => err instanceof Error ? err.message : String(err);
|
|
18190
|
+
const applyError = (pair, next) => {
|
|
18191
|
+
if (!pair)
|
|
18192
|
+
return;
|
|
18193
|
+
pair.lastError = next;
|
|
18194
|
+
};
|
|
18195
|
+
const applyErrorIfAbsent = (pair, next) => {
|
|
18196
|
+
if (!pair || pair.lastError)
|
|
18197
|
+
return;
|
|
18198
|
+
pair.lastError = next;
|
|
18199
|
+
};
|
|
18200
|
+
const mapCoordinatorHttpError = (err) => {
|
|
18201
|
+
if (err instanceof ErrorFromResponse) {
|
|
18202
|
+
return {
|
|
18203
|
+
reason: err.message,
|
|
18204
|
+
code: err.code != null ? String(err.code) : 'SERVER_ERROR',
|
|
18205
|
+
};
|
|
18206
|
+
}
|
|
18207
|
+
return { reason: errorMessage(err), code: 'SERVER_ERROR' };
|
|
18208
|
+
};
|
|
18209
|
+
const mapCoordinatorWsError = (err) => {
|
|
18210
|
+
if (err instanceof ErrorFromResponse) {
|
|
18211
|
+
return {
|
|
18212
|
+
reason: err.message,
|
|
18213
|
+
code: err.code != null ? String(err.code) : 'SERVER_ERROR',
|
|
18214
|
+
};
|
|
18215
|
+
}
|
|
18216
|
+
if (err instanceof Error) {
|
|
18217
|
+
try {
|
|
18218
|
+
const parsed = JSON.parse(err.message);
|
|
18219
|
+
if (typeof parsed.isWSFailure === 'boolean') {
|
|
18220
|
+
return {
|
|
18221
|
+
reason: parsed.message || err.message,
|
|
18222
|
+
code: !parsed.isWSFailure && parsed.code
|
|
18223
|
+
? String(parsed.code)
|
|
18224
|
+
: 'SERVER_ERROR',
|
|
18225
|
+
};
|
|
18226
|
+
}
|
|
18227
|
+
}
|
|
18228
|
+
catch { }
|
|
18229
|
+
}
|
|
18230
|
+
return { reason: errorMessage(err), code: 'SERVER_ERROR' };
|
|
18231
|
+
};
|
|
18232
|
+
const mapWsJoinError = (err) => {
|
|
18233
|
+
if (err instanceof SfuJoinError) {
|
|
18234
|
+
const sfuError = err.errorEvent.error;
|
|
18235
|
+
return {
|
|
18236
|
+
reason: sfuError?.message || err.message,
|
|
18237
|
+
code: (sfuError && ErrorCode[sfuError.code]) || 'SFU_ERROR',
|
|
18238
|
+
};
|
|
18239
|
+
}
|
|
18240
|
+
const reason = errorMessage(err);
|
|
18241
|
+
if (err instanceof SfuTimeoutError) {
|
|
18242
|
+
return { reason, code: 'REQUEST_TIMEOUT' };
|
|
18243
|
+
}
|
|
18244
|
+
return { reason, code: 'SFU_ERROR' };
|
|
18245
|
+
};
|
|
18246
|
+
|
|
17561
18247
|
/**
|
|
17562
18248
|
* A `StreamVideoClient` instance lets you communicate with our API, and authenticate users.
|
|
17563
18249
|
*/
|
|
@@ -17621,6 +18307,7 @@ class StreamVideoClient {
|
|
|
17621
18307
|
}
|
|
17622
18308
|
call = new Call({
|
|
17623
18309
|
streamClient: this.streamClient,
|
|
18310
|
+
clientEventReporter: this.clientEventReporter,
|
|
17624
18311
|
type: e.call.type,
|
|
17625
18312
|
id: e.call.id,
|
|
17626
18313
|
members: e.members,
|
|
@@ -17690,6 +18377,8 @@ class StreamVideoClient {
|
|
|
17690
18377
|
user.id = '!anon';
|
|
17691
18378
|
return this.connectAnonymousUser(user, tokenOrProvider);
|
|
17692
18379
|
}
|
|
18380
|
+
const reporter = this.clientEventReporter;
|
|
18381
|
+
reporter.startCoordinatorConnection(user.id);
|
|
17693
18382
|
const connectUserResponse = await withoutConcurrency(this.connectionConcurrencyTag, async () => {
|
|
17694
18383
|
const client = this.streamClient;
|
|
17695
18384
|
const { onConnectUserError, persistUserOnConnectionFailure } = client.options;
|
|
@@ -17699,14 +18388,15 @@ class StreamVideoClient {
|
|
|
17699
18388
|
for (let attempt = 0; attempt < maxConnectUserRetries; attempt++) {
|
|
17700
18389
|
try {
|
|
17701
18390
|
this.logger.trace(`Connecting user (${attempt})`, user);
|
|
17702
|
-
return user.type === 'guest'
|
|
17703
|
-
?
|
|
17704
|
-
:
|
|
18391
|
+
return await reporter.trackCoordinatorWs(() => user.type === 'guest'
|
|
18392
|
+
? client.connectGuestUser(user)
|
|
18393
|
+
: client.connectUser(user, tokenOrProvider));
|
|
17705
18394
|
}
|
|
17706
18395
|
catch (err) {
|
|
17707
18396
|
this.logger.warn(`Failed to connect a user (${attempt})`, err);
|
|
17708
18397
|
errorQueue.push(err);
|
|
17709
18398
|
if (attempt === maxConnectUserRetries - 1) {
|
|
18399
|
+
reporter.closeCoordinatorWs();
|
|
17710
18400
|
onConnectUserError?.(err, errorQueue);
|
|
17711
18401
|
throw err;
|
|
17712
18402
|
}
|
|
@@ -17784,6 +18474,7 @@ class StreamVideoClient {
|
|
|
17784
18474
|
return (call ??
|
|
17785
18475
|
new Call({
|
|
17786
18476
|
streamClient: this.streamClient,
|
|
18477
|
+
clientEventReporter: this.clientEventReporter,
|
|
17787
18478
|
id: id,
|
|
17788
18479
|
type: type,
|
|
17789
18480
|
clientStore: this.writeableStateStore,
|
|
@@ -17808,6 +18499,7 @@ class StreamVideoClient {
|
|
|
17808
18499
|
for (const c of response.calls) {
|
|
17809
18500
|
const call = new Call({
|
|
17810
18501
|
streamClient: this.streamClient,
|
|
18502
|
+
clientEventReporter: this.clientEventReporter,
|
|
17811
18503
|
id: c.call.id,
|
|
17812
18504
|
type: c.call.type,
|
|
17813
18505
|
members: c.members,
|
|
@@ -17915,6 +18607,7 @@ class StreamVideoClient {
|
|
|
17915
18607
|
const [callType, callId] = call_cid.split(':');
|
|
17916
18608
|
call = new Call({
|
|
17917
18609
|
streamClient: this.streamClient,
|
|
18610
|
+
clientEventReporter: this.clientEventReporter,
|
|
17918
18611
|
type: callType,
|
|
17919
18612
|
id: callId,
|
|
17920
18613
|
clientStore: this.writeableStateStore,
|
|
@@ -17955,6 +18648,9 @@ class StreamVideoClient {
|
|
|
17955
18648
|
this.logger = videoLoggerSystem.getLogger('client');
|
|
17956
18649
|
this.rejectCallWhenBusy = clientOptions?.rejectCallWhenBusy ?? false;
|
|
17957
18650
|
this.streamClient = createCoordinatorClient(apiKey, clientOptions);
|
|
18651
|
+
this.clientEventReporter = new ClientEventReporter({
|
|
18652
|
+
streamClient: this.streamClient,
|
|
18653
|
+
});
|
|
17958
18654
|
this.writeableStateStore = new StreamVideoWriteableStateStore();
|
|
17959
18655
|
this.readOnlyStateStore = new StreamVideoReadOnlyStateStore(this.writeableStateStore);
|
|
17960
18656
|
if (typeof apiKeyOrArgs !== 'string' && apiKeyOrArgs.user) {
|
|
@@ -18017,5 +18713,5 @@ const humanize = (n) => {
|
|
|
18017
18713
|
return String(n);
|
|
18018
18714
|
};
|
|
18019
18715
|
|
|
18020
|
-
export { AudioSettingsRequestDefaultDeviceEnum, AudioSettingsResponseDefaultDeviceEnum, browsers as Browsers, Call, CallRecordingFailedEventRecordingTypeEnum, CallRecordingReadyEventRecordingTypeEnum, CallRecordingStartedEventRecordingTypeEnum, CallRecordingStoppedEventRecordingTypeEnum, CallState, CallType, CallTypes, CallingState, CameraManager, CameraManagerState, CreateDeviceRequestPushProviderEnum, DebounceType, DeviceManager, DeviceManagerState, DynascaleManager, ErrorFromResponse, FrameRecordingSettingsRequestModeEnum, FrameRecordingSettingsRequestQualityEnum, FrameRecordingSettingsResponseModeEnum, IndividualRecordingSettingsRequestModeEnum, IndividualRecordingSettingsResponseModeEnum, IngressAudioEncodingOptionsRequestChannelsEnum, IngressSourceRequestFpsEnum, IngressVideoLayerRequestCodecEnum, LayoutSettingsRequestNameEnum, MicrophoneManager, MicrophoneManagerState, NoiseCancellationSettingsModeEnum, OwnCapability, RTMPBroadcastRequestQualityEnum, RTMPSettingsRequestQualityEnum, RawRecordingSettingsRequestModeEnum, RawRecordingSettingsResponseModeEnum, RecordSettingsRequestModeEnum, RecordSettingsRequestQualityEnum, rxUtils as RxUtils, ScreenShareManager, ScreenShareState, events as SfuEvents, SfuJoinError, models as SfuModels, SpeakerManager, SpeakerState, StartClosedCaptionsRequestLanguageEnum, StartTranscriptionRequestLanguageEnum, StreamSfuClient, StreamVideoClient, StreamVideoReadOnlyStateStore, StreamVideoWriteableStateStore, TranscriptionSettingsRequestClosedCaptionModeEnum, TranscriptionSettingsRequestLanguageEnum, TranscriptionSettingsRequestModeEnum, TranscriptionSettingsResponseClosedCaptionModeEnum, TranscriptionSettingsResponseLanguageEnum, TranscriptionSettingsResponseModeEnum, VideoSettingsRequestCameraFacingEnum, VideoSettingsResponseCameraFacingEnum, ViewportTracker, VisibilityState, checkIfAudioOutputChangeSupported, combineComparators, conditional, createSoundDetector, defaultSortPreset, descending, deviceIds$, disposeOfMediaStream, dominantSpeaker, getAudioBrowserPermission, getAudioDevices, getAudioOutputDevices, getAudioStream, getClientDetails, getDeviceState, getScreenShareStream, getSdkInfo, getVideoBrowserPermission, getVideoDevices, getVideoStream, getWebRTCInfo, hasAudio$1 as hasAudio, hasInterruptedTrack, hasPausedTrack, hasScreenShare, hasScreenShareAudio, hasVideo, humanize, isPinned, livestreamOrAudioRoomSortPreset, logToConsole, name, noopComparator, paginatedLayoutSortPreset, pinned, publishingAudio, publishingVideo, reactionType, resolveDeviceId, role, screenSharing, setDeviceInfo, setOSInfo, setPowerState, setSdkInfo, setThermalState, setWebRTCInfo, speakerLayoutSortPreset, speaking, videoLoggerSystem, withParticipantSource };
|
|
18716
|
+
export { AudioSettingsRequestDefaultDeviceEnum, AudioSettingsResponseDefaultDeviceEnum, browsers as Browsers, Call, CallRecordingFailedEventRecordingTypeEnum, CallRecordingReadyEventRecordingTypeEnum, CallRecordingStartedEventRecordingTypeEnum, CallRecordingStoppedEventRecordingTypeEnum, CallState, CallType, CallTypes, CallingState, CameraManager, CameraManagerState, CreateDeviceRequestPushProviderEnum, DebounceType, DeviceManager, DeviceManagerState, DynascaleManager, ErrorFromResponse, FrameRecordingSettingsRequestModeEnum, FrameRecordingSettingsRequestQualityEnum, FrameRecordingSettingsResponseModeEnum, IndividualRecordingSettingsRequestModeEnum, IndividualRecordingSettingsResponseModeEnum, IngressAudioEncodingOptionsRequestChannelsEnum, IngressSourceRequestFpsEnum, IngressVideoLayerRequestCodecEnum, LayoutSettingsRequestNameEnum, MicrophoneManager, MicrophoneManagerState, NoiseCancellationSettingsModeEnum, OwnCapability, RTMPBroadcastRequestQualityEnum, RTMPSettingsRequestQualityEnum, RawRecordingSettingsRequestModeEnum, RawRecordingSettingsResponseModeEnum, RecordSettingsRequestModeEnum, RecordSettingsRequestQualityEnum, rxUtils as RxUtils, ScreenShareManager, ScreenShareState, events as SfuEvents, SfuJoinError, models as SfuModels, SfuTimeoutError, SpeakerManager, SpeakerState, StartClosedCaptionsRequestLanguageEnum, StartTranscriptionRequestLanguageEnum, StreamSfuClient, StreamVideoClient, StreamVideoReadOnlyStateStore, StreamVideoWriteableStateStore, TranscriptionSettingsRequestClosedCaptionModeEnum, TranscriptionSettingsRequestLanguageEnum, TranscriptionSettingsRequestModeEnum, TranscriptionSettingsResponseClosedCaptionModeEnum, TranscriptionSettingsResponseLanguageEnum, TranscriptionSettingsResponseModeEnum, VideoSettingsRequestCameraFacingEnum, VideoSettingsResponseCameraFacingEnum, ViewportTracker, VisibilityState, checkIfAudioOutputChangeSupported, combineComparators, conditional, createSoundDetector, defaultSortPreset, descending, deviceIds$, disposeOfMediaStream, dominantSpeaker, getAudioBrowserPermission, getAudioDevices, getAudioOutputDevices, getAudioStream, getClientDetails, getDeviceState, getScreenShareStream, getSdkInfo, getVideoBrowserPermission, getVideoDevices, getVideoStream, getWebRTCInfo, hasAudio$1 as hasAudio, hasInterruptedTrack, hasPausedTrack, hasScreenShare, hasScreenShareAudio, hasVideo, humanize, isPinned, livestreamOrAudioRoomSortPreset, logToConsole, name, noopComparator, paginatedLayoutSortPreset, pinned, publishingAudio, publishingVideo, reactionType, resolveDeviceId, role, screenSharing, setDeviceInfo, setOSInfo, setPowerState, setSdkInfo, setThermalState, setWebRTCInfo, speakerLayoutSortPreset, speaking, videoLoggerSystem, withParticipantSource };
|
|
18021
18717
|
//# sourceMappingURL=index.es.js.map
|