@stream-io/video-client 1.53.2 → 1.54.1-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/dist/index.browser.es.js +111 -12
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +111 -12
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +111 -12
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +13 -1
- package/dist/src/coordinator/connection/types.d.ts +6 -0
- package/dist/src/gen/google/protobuf/struct.d.ts +3 -1
- package/dist/src/gen/google/protobuf/timestamp.d.ts +3 -1
- package/dist/src/gen/video/sfu/event/events.d.ts +22 -1
- package/dist/src/gen/video/sfu/models/models.d.ts +4 -0
- package/dist/src/gen/video/sfu/signal_rpc/signal.client.d.ts +23 -2
- package/dist/src/reporting/ClientEventReporter.d.ts +2 -0
- package/dist/src/rtc/Publisher.d.ts +4 -1
- package/dist/src/rtc/Subscriber.d.ts +7 -0
- package/package.json +1 -1
- package/src/Call.ts +86 -6
- package/src/StreamVideoClient.ts +1 -0
- package/src/coordinator/connection/types.ts +7 -0
- package/src/gen/google/protobuf/struct.ts +7 -12
- package/src/gen/google/protobuf/timestamp.ts +6 -7
- package/src/gen/video/sfu/event/events.ts +23 -25
- package/src/gen/video/sfu/models/models.ts +11 -1
- package/src/gen/video/sfu/signal_rpc/signal.client.ts +25 -29
- package/src/gen/video/sfu/signal_rpc/signal.ts +1 -0
- package/src/helpers/client-details.ts +1 -1
- package/src/reporting/ClientEventReporter.ts +4 -0
- package/src/reporting/__tests__/ClientEventReporter.test.ts +33 -0
- package/src/rtc/Publisher.ts +4 -0
- package/src/rtc/Subscriber.ts +28 -1
- package/src/rtc/__tests__/Call.reconnect.test.ts +149 -2
package/dist/index.es.js
CHANGED
|
@@ -508,6 +508,7 @@ class ErrorFromResponse extends Error {
|
|
|
508
508
|
}
|
|
509
509
|
}
|
|
510
510
|
|
|
511
|
+
/* eslint-disable */
|
|
511
512
|
// @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
|
|
512
513
|
// @generated from protobuf file "google/protobuf/struct.proto" (package "google.protobuf", syntax proto3)
|
|
513
514
|
// tslint:disable
|
|
@@ -773,6 +774,7 @@ class ListValue$Type extends MessageType {
|
|
|
773
774
|
*/
|
|
774
775
|
const ListValue = new ListValue$Type();
|
|
775
776
|
|
|
777
|
+
/* eslint-disable */
|
|
776
778
|
// @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
|
|
777
779
|
// @generated from protobuf file "google/protobuf/timestamp.proto" (package "google.protobuf", syntax proto3)
|
|
778
780
|
// tslint:disable
|
|
@@ -1839,6 +1841,12 @@ class TrackInfo$Type extends MessageType {
|
|
|
1839
1841
|
kind: 'scalar',
|
|
1840
1842
|
T: 5 /*ScalarType.INT32*/,
|
|
1841
1843
|
},
|
|
1844
|
+
{
|
|
1845
|
+
no: 13,
|
|
1846
|
+
name: 'self_sub_audio_video',
|
|
1847
|
+
kind: 'scalar',
|
|
1848
|
+
T: 8 /*ScalarType.BOOL*/,
|
|
1849
|
+
},
|
|
1842
1850
|
]);
|
|
1843
1851
|
}
|
|
1844
1852
|
}
|
|
@@ -6641,7 +6649,7 @@ const getSdkVersion = (sdk) => {
|
|
|
6641
6649
|
return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
|
|
6642
6650
|
};
|
|
6643
6651
|
|
|
6644
|
-
const version = "1.
|
|
6652
|
+
const version = "1.54.1-beta.0";
|
|
6645
6653
|
const [major, minor, patch] = version.split('.');
|
|
6646
6654
|
let sdkInfo = {
|
|
6647
6655
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -6785,7 +6793,7 @@ const getClientDetails = async () => {
|
|
|
6785
6793
|
.join(' '),
|
|
6786
6794
|
version: '',
|
|
6787
6795
|
},
|
|
6788
|
-
webrtcVersion:
|
|
6796
|
+
webrtcVersion: webRtcInfo?.version || '',
|
|
6789
6797
|
};
|
|
6790
6798
|
};
|
|
6791
6799
|
|
|
@@ -8429,7 +8437,7 @@ class Publisher extends BasePeerConnection {
|
|
|
8429
8437
|
/**
|
|
8430
8438
|
* Constructs a new `Publisher` instance.
|
|
8431
8439
|
*/
|
|
8432
|
-
constructor(baseOptions, publishOptions) {
|
|
8440
|
+
constructor(baseOptions, publishOptions, opts = {}) {
|
|
8433
8441
|
super(PeerType.PUBLISHER_UNSPECIFIED, baseOptions);
|
|
8434
8442
|
this.transceiverCache = new TransceiverCache();
|
|
8435
8443
|
this.clonedTracks = new Set();
|
|
@@ -8850,6 +8858,7 @@ class Publisher extends BasePeerConnection {
|
|
|
8850
8858
|
muted: !isTrackLive,
|
|
8851
8859
|
codec: publishOption.codec,
|
|
8852
8860
|
publishOptionId: publishOption.id,
|
|
8861
|
+
selfSubAudioVideo: this.selfSubEnabled,
|
|
8853
8862
|
};
|
|
8854
8863
|
};
|
|
8855
8864
|
this.cloneTrack = (track) => {
|
|
@@ -8930,6 +8939,7 @@ class Publisher extends BasePeerConnection {
|
|
|
8930
8939
|
});
|
|
8931
8940
|
};
|
|
8932
8941
|
this.publishOptions = publishOptions;
|
|
8942
|
+
this.selfSubEnabled = opts.selfSubEnabled ?? false;
|
|
8933
8943
|
this.on('iceRestart', (iceRestart) => {
|
|
8934
8944
|
if (iceRestart.peerType !== PeerType.PUBLISHER_UNSPECIFIED)
|
|
8935
8945
|
return;
|
|
@@ -9011,6 +9021,13 @@ class Subscriber extends BasePeerConnection {
|
|
|
9011
9021
|
*/
|
|
9012
9022
|
constructor(opts) {
|
|
9013
9023
|
super(PeerType.SUBSCRIBER, opts);
|
|
9024
|
+
/**
|
|
9025
|
+
* Remote streams received from the SFU. For a self-sub case
|
|
9026
|
+
* we need to be able to distinguish between the local capture stream.
|
|
9027
|
+
* The map will never contain local streams so we can safely use it to
|
|
9028
|
+
* check if the stream is remote and dispose it when needed.
|
|
9029
|
+
*/
|
|
9030
|
+
this.trackedStreams = new WeakSet();
|
|
9014
9031
|
/**
|
|
9015
9032
|
* Restarts the ICE connection and renegotiates with the SFU.
|
|
9016
9033
|
*/
|
|
@@ -9045,6 +9062,7 @@ class Subscriber extends BasePeerConnection {
|
|
|
9045
9062
|
// example: `e3f6aaf8-b03d-4911-be36-83f47d37a76a:TRACK_TYPE_VIDEO`
|
|
9046
9063
|
const [trackId, rawTrackType] = primaryStream.id.split(':');
|
|
9047
9064
|
const participantToUpdate = this.state.participants.find((p) => p.trackLookupPrefix === trackId);
|
|
9065
|
+
const isSelfSub = !!participantToUpdate?.isLocalParticipant;
|
|
9048
9066
|
this.logger.debug(`[onTrack]: Got remote ${rawTrackType} track for userId: ${participantToUpdate?.userId}`, track.id, track);
|
|
9049
9067
|
const trackType = toTrackType(rawTrackType);
|
|
9050
9068
|
if (!trackType) {
|
|
@@ -9069,6 +9087,9 @@ class Subscriber extends BasePeerConnection {
|
|
|
9069
9087
|
this.setRemoteTrackInterrupted(trackId, trackType, true);
|
|
9070
9088
|
}
|
|
9071
9089
|
this.trackIdToTrackType.set(track.id, trackType);
|
|
9090
|
+
if (isSelfSub) {
|
|
9091
|
+
this.trackedStreams.add(primaryStream);
|
|
9092
|
+
}
|
|
9072
9093
|
if (!participantToUpdate) {
|
|
9073
9094
|
this.logger.warn(`[onTrack]: Received track for unknown participant: ${trackId}`, e);
|
|
9074
9095
|
this.state.registerOrphanedTrack({
|
|
@@ -9084,6 +9105,12 @@ class Subscriber extends BasePeerConnection {
|
|
|
9084
9105
|
this.logger.error(`Unknown track type: ${rawTrackType}`);
|
|
9085
9106
|
return;
|
|
9086
9107
|
}
|
|
9108
|
+
// Self-sub loopback audio routes to the speaker by default, which
|
|
9109
|
+
// would echo the local user's voice. Default-mute here; consumers
|
|
9110
|
+
// (the loopback recording hook) re-enable explicitly when needed.
|
|
9111
|
+
if (isSelfSub && e.track.kind === 'audio') {
|
|
9112
|
+
e.track.enabled = false;
|
|
9113
|
+
}
|
|
9087
9114
|
// get the previous stream to dispose it later
|
|
9088
9115
|
// usually this happens during migration, when the stream is replaced
|
|
9089
9116
|
// with a new one but the old one is still in the state
|
|
@@ -9092,8 +9119,12 @@ class Subscriber extends BasePeerConnection {
|
|
|
9092
9119
|
this.state.updateParticipant(participantToUpdate.sessionId, {
|
|
9093
9120
|
[streamKindProp]: primaryStream,
|
|
9094
9121
|
});
|
|
9095
|
-
// now, dispose the previous stream if it exists
|
|
9096
9122
|
if (previousStream) {
|
|
9123
|
+
if (isSelfSub && !this.trackedStreams.has(previousStream)) {
|
|
9124
|
+
// this is the local capture stream, we don't want to dispose it
|
|
9125
|
+
this.logger.debug(`[onTrack]: Skipping cleanup of previous ${e.track.kind} stream for userId: ${participantToUpdate.userId} because it is not tracked`);
|
|
9126
|
+
return;
|
|
9127
|
+
}
|
|
9097
9128
|
this.logger.info(`[onTrack]: Cleaning up previous remote ${track.kind} tracks for userId: ${participantToUpdate.userId}`);
|
|
9098
9129
|
previousStream.getTracks().forEach((t) => {
|
|
9099
9130
|
t.stop();
|
|
@@ -13916,6 +13947,7 @@ class Call {
|
|
|
13916
13947
|
// maintain the order of publishing tracks to restore them after a reconnection
|
|
13917
13948
|
// it shouldn't contain duplicates
|
|
13918
13949
|
this.trackPublishOrder = [];
|
|
13950
|
+
this.selfSubEnabled = false;
|
|
13919
13951
|
this.hasJoinedOnce = false;
|
|
13920
13952
|
this.deviceSettingsAppliedOnce = false;
|
|
13921
13953
|
this.initialized = false;
|
|
@@ -14265,6 +14297,30 @@ class Call {
|
|
|
14265
14297
|
await Promise.all(stopOnLeavePromises);
|
|
14266
14298
|
});
|
|
14267
14299
|
};
|
|
14300
|
+
/**
|
|
14301
|
+
* The largest video publish dimension across the current publish options.
|
|
14302
|
+
*
|
|
14303
|
+
* @internal
|
|
14304
|
+
*/
|
|
14305
|
+
this.getMaxVideoPublishDimension = () => {
|
|
14306
|
+
if (!this.currentPublishOptions)
|
|
14307
|
+
return undefined;
|
|
14308
|
+
let maxDimension;
|
|
14309
|
+
let maxArea = 0;
|
|
14310
|
+
for (const opt of this.currentPublishOptions) {
|
|
14311
|
+
if (opt.trackType !== TrackType.VIDEO)
|
|
14312
|
+
continue;
|
|
14313
|
+
const dim = opt.videoDimension;
|
|
14314
|
+
if (!dim || !dim.width || !dim.height)
|
|
14315
|
+
continue;
|
|
14316
|
+
const area = dim.width * dim.height;
|
|
14317
|
+
if (area > maxArea) {
|
|
14318
|
+
maxDimension = dim;
|
|
14319
|
+
maxArea = area;
|
|
14320
|
+
}
|
|
14321
|
+
}
|
|
14322
|
+
return maxDimension;
|
|
14323
|
+
};
|
|
14268
14324
|
/**
|
|
14269
14325
|
* Update from the call response from the "call.ring" event
|
|
14270
14326
|
* @internal
|
|
@@ -14411,7 +14467,7 @@ class Call {
|
|
|
14411
14467
|
*
|
|
14412
14468
|
* @returns a promise which resolves once the call join-flow has finished.
|
|
14413
14469
|
*/
|
|
14414
|
-
this.join = async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, ...data } = {}) => {
|
|
14470
|
+
this.join = async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, selfSubEnabled = false, ...data } = {}) => {
|
|
14415
14471
|
const callingState = this.state.callingState;
|
|
14416
14472
|
if ([CallingState.JOINED, CallingState.JOINING].includes(callingState)) {
|
|
14417
14473
|
throw new Error(`Illegal State: call.join() shall be called only once`);
|
|
@@ -14419,6 +14475,9 @@ class Call {
|
|
|
14419
14475
|
if (data?.ring) {
|
|
14420
14476
|
this.ringingSubject.next(true);
|
|
14421
14477
|
}
|
|
14478
|
+
// we need this to be set before the callingx.joinCall() is
|
|
14479
|
+
// called to avoid registering the test call in the CallKit/Telecom
|
|
14480
|
+
this.selfSubEnabled = selfSubEnabled;
|
|
14422
14481
|
const callingX = globalThis.streamRNVideoSDK?.callingX;
|
|
14423
14482
|
if (callingX) {
|
|
14424
14483
|
// for Android/iOS, we need to start the call in the callingx library as soon as possible
|
|
@@ -14555,7 +14614,7 @@ class Call {
|
|
|
14555
14614
|
]);
|
|
14556
14615
|
const isReconnecting = this.reconnectStrategy !== WebsocketReconnectStrategy.UNSPECIFIED;
|
|
14557
14616
|
const reconnectDetails = isReconnecting
|
|
14558
|
-
? this.getReconnectDetails(
|
|
14617
|
+
? this.getReconnectDetails(previousSfuClient?.edgeName, previousSessionId)
|
|
14559
14618
|
: undefined;
|
|
14560
14619
|
const preferredPublishOptions = !isReconnecting
|
|
14561
14620
|
? this.getPreferredPublishOptions()
|
|
@@ -14808,7 +14867,9 @@ class Call {
|
|
|
14808
14867
|
if (closePreviousInstances && this.publisher) {
|
|
14809
14868
|
await this.publisher.dispose();
|
|
14810
14869
|
}
|
|
14811
|
-
this.publisher = new Publisher(basePeerConnectionOptions, publishOptions
|
|
14870
|
+
this.publisher = new Publisher(basePeerConnectionOptions, publishOptions, {
|
|
14871
|
+
selfSubEnabled: this.selfSubEnabled,
|
|
14872
|
+
});
|
|
14812
14873
|
}
|
|
14813
14874
|
this.statsReporter?.stop();
|
|
14814
14875
|
if (this.statsReportingIntervalInMs > 0) {
|
|
@@ -14924,6 +14985,7 @@ class Call {
|
|
|
14924
14985
|
const reconnectStartTime = Date.now();
|
|
14925
14986
|
this.reconnectStrategy = strategy;
|
|
14926
14987
|
this.reconnectReason = reason;
|
|
14988
|
+
const sfuRejoinFailures = new Map();
|
|
14927
14989
|
const markAsReconnectingFailed = async () => {
|
|
14928
14990
|
try {
|
|
14929
14991
|
// attempt to fetch the call data from the server, as the call
|
|
@@ -14982,7 +15044,8 @@ class Call {
|
|
|
14982
15044
|
if (this.reconnectStrategy !== WebsocketReconnectStrategy.FAST) {
|
|
14983
15045
|
this.reconnectAttempts++;
|
|
14984
15046
|
}
|
|
14985
|
-
const
|
|
15047
|
+
const attemptedStrategy = this.reconnectStrategy;
|
|
15048
|
+
const currentStrategy = WebsocketReconnectStrategy[attemptedStrategy];
|
|
14986
15049
|
try {
|
|
14987
15050
|
// wait until the network is available
|
|
14988
15051
|
await this.networkAvailableTask?.promise;
|
|
@@ -14995,9 +15058,24 @@ class Call {
|
|
|
14995
15058
|
case WebsocketReconnectStrategy.FAST:
|
|
14996
15059
|
await this.reconnectFast();
|
|
14997
15060
|
break;
|
|
14998
|
-
case WebsocketReconnectStrategy.REJOIN:
|
|
14999
|
-
|
|
15061
|
+
case WebsocketReconnectStrategy.REJOIN: {
|
|
15062
|
+
const confirmedBadSfus = Array.from(sfuRejoinFailures)
|
|
15063
|
+
.filter(([, failures]) => failures >= 2)
|
|
15064
|
+
.map(([sfu]) => sfu);
|
|
15065
|
+
if (this.joinCallData && confirmedBadSfus.length) {
|
|
15066
|
+
this.joinCallData.migrating_from =
|
|
15067
|
+
confirmedBadSfus[confirmedBadSfus.length - 1];
|
|
15068
|
+
this.joinCallData.migrating_from_list = confirmedBadSfus;
|
|
15069
|
+
}
|
|
15070
|
+
try {
|
|
15071
|
+
await this.reconnectRejoin();
|
|
15072
|
+
}
|
|
15073
|
+
finally {
|
|
15074
|
+
delete this.joinCallData?.migrating_from;
|
|
15075
|
+
delete this.joinCallData?.migrating_from_list;
|
|
15076
|
+
}
|
|
15000
15077
|
break;
|
|
15078
|
+
}
|
|
15001
15079
|
case WebsocketReconnectStrategy.MIGRATE:
|
|
15002
15080
|
await this.reconnectMigrate();
|
|
15003
15081
|
break;
|
|
@@ -15010,6 +15088,15 @@ class Call {
|
|
|
15010
15088
|
break; // do-while loop, reconnection worked, exit the loop
|
|
15011
15089
|
}
|
|
15012
15090
|
catch (error) {
|
|
15091
|
+
if (attemptedStrategy === WebsocketReconnectStrategy.REJOIN) {
|
|
15092
|
+
const failedSfu = this.credentials?.server.edge_name;
|
|
15093
|
+
if (failedSfu) {
|
|
15094
|
+
const switchSfu = error instanceof SfuJoinError &&
|
|
15095
|
+
SfuJoinError.isJoinErrorCode(error.errorEvent);
|
|
15096
|
+
const failures = (sfuRejoinFailures.get(failedSfu) ?? 0) + 1;
|
|
15097
|
+
sfuRejoinFailures.set(failedSfu, switchSfu ? Math.max(failures, 2) : failures);
|
|
15098
|
+
}
|
|
15099
|
+
}
|
|
15013
15100
|
if (this.state.callingState === CallingState.OFFLINE) {
|
|
15014
15101
|
this.logger.debug(`[Reconnect] Can't reconnect while offline, stopping reconnection attempts`);
|
|
15015
15102
|
break;
|
|
@@ -15205,6 +15292,8 @@ class Call {
|
|
|
15205
15292
|
this.state.setCallingState(CallingState.OFFLINE);
|
|
15206
15293
|
}
|
|
15207
15294
|
else {
|
|
15295
|
+
if (!this.networkAvailableTask)
|
|
15296
|
+
return;
|
|
15208
15297
|
this.logger.debug('[Reconnect] Going online');
|
|
15209
15298
|
this.sfuClient?.close(StreamSfuClient.DISPOSE_OLD_SOCKET, 'Closing WS to reconnect after going online');
|
|
15210
15299
|
// we went online, release the previous waiters and reset the state
|
|
@@ -16276,6 +16365,12 @@ class Call {
|
|
|
16276
16365
|
get currentUserId() {
|
|
16277
16366
|
return this.clientStore.connectedUser?.id;
|
|
16278
16367
|
}
|
|
16368
|
+
/**
|
|
16369
|
+
* A flag indicating whether self-subscription is enabled for the call.
|
|
16370
|
+
*/
|
|
16371
|
+
get isSelfSubEnabled() {
|
|
16372
|
+
return this.selfSubEnabled;
|
|
16373
|
+
}
|
|
16279
16374
|
/**
|
|
16280
16375
|
* A flag indicating whether the call was created by the current user.
|
|
16281
16376
|
*/
|
|
@@ -17463,11 +17558,11 @@ class StreamClient {
|
|
|
17463
17558
|
return await this.wsConnection.connect(this.defaultWSTimeout);
|
|
17464
17559
|
};
|
|
17465
17560
|
this.getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
|
|
17466
|
-
"1.
|
|
17561
|
+
"1.54.1-beta.0";
|
|
17467
17562
|
this.getUserAgent = () => {
|
|
17468
17563
|
if (!this.cachedUserAgent) {
|
|
17469
17564
|
const { clientAppIdentifier = {} } = this.options;
|
|
17470
|
-
const { sdkName = 'js', sdkVersion = "1.
|
|
17565
|
+
const { sdkName = 'js', sdkVersion = "1.54.1-beta.0", ...extras } = clientAppIdentifier;
|
|
17471
17566
|
this.cachedUserAgent = [
|
|
17472
17567
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
17473
17568
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|
|
@@ -18145,6 +18240,8 @@ class ClientEventReporter {
|
|
|
18145
18240
|
};
|
|
18146
18241
|
};
|
|
18147
18242
|
this.send = (body) => {
|
|
18243
|
+
if (!this.enabled)
|
|
18244
|
+
return;
|
|
18148
18245
|
void this.sendWithRetry(body);
|
|
18149
18246
|
};
|
|
18150
18247
|
this.sendWithRetry = async (body) => {
|
|
@@ -18170,6 +18267,7 @@ class ClientEventReporter {
|
|
|
18170
18267
|
return false;
|
|
18171
18268
|
};
|
|
18172
18269
|
this.streamClient = options.streamClient;
|
|
18270
|
+
this.enabled = options.enabled ?? true;
|
|
18173
18271
|
}
|
|
18174
18272
|
}
|
|
18175
18273
|
const readPermissionStatus = (permission) => {
|
|
@@ -18650,6 +18748,7 @@ class StreamVideoClient {
|
|
|
18650
18748
|
this.streamClient = createCoordinatorClient(apiKey, clientOptions);
|
|
18651
18749
|
this.clientEventReporter = new ClientEventReporter({
|
|
18652
18750
|
streamClient: this.streamClient,
|
|
18751
|
+
enabled: clientOptions?.clientEventsReportingEnabled ?? true,
|
|
18653
18752
|
});
|
|
18654
18753
|
this.writeableStateStore = new StreamVideoWriteableStateStore();
|
|
18655
18754
|
this.readOnlyStateStore = new StreamVideoReadOnlyStateStore(this.writeableStateStore);
|