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