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