@stream-io/video-client 1.11.3 → 1.11.5
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 +14 -0
- package/dist/index.browser.es.js +128 -45
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +128 -45
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +128 -45
- package/dist/index.es.js.map +1 -1
- package/dist/src/StreamVideoClient.d.ts +2 -3
- package/dist/src/coordinator/connection/client.d.ts +11 -8
- package/dist/src/coordinator/connection/connection.d.ts +5 -4
- package/dist/src/helpers/promise.d.ts +18 -0
- package/dist/src/helpers/sdp-munging.d.ts +4 -0
- package/dist/src/rtc/Publisher.d.ts +1 -0
- package/dist/src/types.d.ts +6 -0
- package/package.json +1 -1
- package/src/StreamVideoClient.ts +14 -19
- package/src/__tests__/Call.test.ts +2 -2
- package/src/coordinator/connection/client.ts +33 -14
- package/src/coordinator/connection/connection.ts +14 -19
- package/src/helpers/__tests__/sdp-munging.test.ts +168 -1
- package/src/helpers/promise.ts +47 -0
- package/src/helpers/sdp-munging.ts +55 -0
- package/src/rtc/Publisher.ts +24 -0
- package/src/rtc/codecs.ts +1 -1
- package/src/types.ts +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
|
|
4
4
|
|
|
5
|
+
## [1.11.5](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.11.4...@stream-io/video-client-1.11.5) (2024-11-22)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* unhandled promise rejections during reconnect ([#1585](https://github.com/GetStream/stream-video-js/issues/1585)) ([920c4ea](https://github.com/GetStream/stream-video-js/commit/920c4ea3b3f622430b35ac1bade74a6206ee17e5)), closes [/github.com/GetStream/stream-video-js/pull/1585/files#diff-420f6ddab47c1be72fd9ce8c99e1fa2b9f5f0495b7c367546ee0ff634beaed81](https://github.com/GetStream//github.com/GetStream/stream-video-js/pull/1585/files/issues/diff-420f6ddab47c1be72fd9ce8c99e1fa2b9f5f0495b7c367546ee0ff634beaed81)
|
|
11
|
+
|
|
12
|
+
## [1.11.4](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.11.3...@stream-io/video-client-1.11.4) (2024-11-21)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* experimental option to force single codec preference in the SDP ([#1581](https://github.com/GetStream/stream-video-js/issues/1581)) ([894a86e](https://github.com/GetStream/stream-video-js/commit/894a86e407dc0dd36b7463bb964c86da0c3055d1))
|
|
18
|
+
|
|
5
19
|
## [1.11.3](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.11.2...@stream-io/video-client-1.11.3) (2024-11-20)
|
|
6
20
|
|
|
7
21
|
|
package/dist/index.browser.es.js
CHANGED
|
@@ -3190,13 +3190,6 @@ function convertErrorToJson(err) {
|
|
|
3190
3190
|
}
|
|
3191
3191
|
return jsonObj;
|
|
3192
3192
|
}
|
|
3193
|
-
/**
|
|
3194
|
-
* Informs if a promise is yet to be resolved or rejected
|
|
3195
|
-
*/
|
|
3196
|
-
async function isPromisePending(promise) {
|
|
3197
|
-
const emptyObj = {};
|
|
3198
|
-
return Promise.race([promise, emptyObj]).then((value) => (value === emptyObj ? true : false), () => false);
|
|
3199
|
-
}
|
|
3200
3193
|
/**
|
|
3201
3194
|
* isOnline safely return the navigator.online value for browser env
|
|
3202
3195
|
* if navigator is not in global object, it always return true
|
|
@@ -3335,7 +3328,7 @@ const retryable = async (rpc, signal) => {
|
|
|
3335
3328
|
return result;
|
|
3336
3329
|
};
|
|
3337
3330
|
|
|
3338
|
-
const version = "1.11.
|
|
3331
|
+
const version = "1.11.5";
|
|
3339
3332
|
const [major, minor, patch] = version.split('.');
|
|
3340
3333
|
let sdkInfo = {
|
|
3341
3334
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -3470,7 +3463,7 @@ const getPreferredCodecs = (kind, preferredCodec, codecToRemove) => {
|
|
|
3470
3463
|
continue;
|
|
3471
3464
|
}
|
|
3472
3465
|
const sdpFmtpLine = codec.sdpFmtpLine;
|
|
3473
|
-
if (!sdpFmtpLine || !sdpFmtpLine.includes('profile-level-id=
|
|
3466
|
+
if (!sdpFmtpLine || !sdpFmtpLine.includes('profile-level-id=42')) {
|
|
3474
3467
|
// this is not the baseline h264 codec, prioritize it lower
|
|
3475
3468
|
partiallyPreferred.push(codec);
|
|
3476
3469
|
continue;
|
|
@@ -5497,6 +5490,50 @@ const toggleDtx = (sdp, enable) => {
|
|
|
5497
5490
|
: `${opusFmtp.original};${requiredDtxConfig}`;
|
|
5498
5491
|
return sdp.replace(opusFmtp.original, newFmtp);
|
|
5499
5492
|
};
|
|
5493
|
+
/**
|
|
5494
|
+
* Returns and SDP with all the codecs except the given codec removed.
|
|
5495
|
+
*/
|
|
5496
|
+
const preserveCodec = (sdp, mid, codec) => {
|
|
5497
|
+
const [kind, codecName] = codec.mimeType.toLowerCase().split('/');
|
|
5498
|
+
const toSet = (fmtpLine) => new Set(fmtpLine.split(';').map((f) => f.trim().toLowerCase()));
|
|
5499
|
+
const equal = (a, b) => {
|
|
5500
|
+
if (a.size !== b.size)
|
|
5501
|
+
return false;
|
|
5502
|
+
for (const item of a)
|
|
5503
|
+
if (!b.has(item))
|
|
5504
|
+
return false;
|
|
5505
|
+
return true;
|
|
5506
|
+
};
|
|
5507
|
+
const codecFmtp = toSet(codec.sdpFmtpLine || '');
|
|
5508
|
+
const parsedSdp = SDP.parse(sdp);
|
|
5509
|
+
for (const media of parsedSdp.media) {
|
|
5510
|
+
if (media.type !== kind || String(media.mid) !== mid)
|
|
5511
|
+
continue;
|
|
5512
|
+
// find the payload id of the desired codec
|
|
5513
|
+
const payloads = new Set();
|
|
5514
|
+
for (const rtp of media.rtp) {
|
|
5515
|
+
if (rtp.codec.toLowerCase() === codecName &&
|
|
5516
|
+
media.fmtp.some((f) => f.payload === rtp.payload && equal(toSet(f.config), codecFmtp))) {
|
|
5517
|
+
payloads.add(rtp.payload);
|
|
5518
|
+
}
|
|
5519
|
+
}
|
|
5520
|
+
// find the corresponding rtx codec by matching apt=<preserved-codec-payload>
|
|
5521
|
+
for (const fmtp of media.fmtp) {
|
|
5522
|
+
const match = fmtp.config.match(/(apt)=(\d+)/);
|
|
5523
|
+
if (!match)
|
|
5524
|
+
continue;
|
|
5525
|
+
const [, , preservedCodecPayload] = match;
|
|
5526
|
+
if (payloads.has(Number(preservedCodecPayload))) {
|
|
5527
|
+
payloads.add(fmtp.payload);
|
|
5528
|
+
}
|
|
5529
|
+
}
|
|
5530
|
+
media.rtp = media.rtp.filter((r) => payloads.has(r.payload));
|
|
5531
|
+
media.fmtp = media.fmtp.filter((f) => payloads.has(f.payload));
|
|
5532
|
+
media.rtcpFb = media.rtcpFb?.filter((f) => payloads.has(f.payload));
|
|
5533
|
+
media.payloads = Array.from(payloads).join(' ');
|
|
5534
|
+
}
|
|
5535
|
+
return SDP.write(parsedSdp);
|
|
5536
|
+
};
|
|
5500
5537
|
/**
|
|
5501
5538
|
* Enables high-quality audio through SDP munging for the given trackMid.
|
|
5502
5539
|
*
|
|
@@ -5900,6 +5937,12 @@ class Publisher {
|
|
|
5900
5937
|
if (this.isPublishing(TrackType.SCREEN_SHARE_AUDIO)) {
|
|
5901
5938
|
offer.sdp = this.enableHighQualityAudio(offer.sdp);
|
|
5902
5939
|
}
|
|
5940
|
+
if (this.isPublishing(TrackType.VIDEO)) {
|
|
5941
|
+
// Hotfix for platforms that don't respect the ordered codec list
|
|
5942
|
+
// (Firefox, Android, Linux, etc...).
|
|
5943
|
+
// We remove all the codecs from the SDP except the one we want to use.
|
|
5944
|
+
offer.sdp = this.removeUnpreferredCodecs(offer.sdp, TrackType.VIDEO);
|
|
5945
|
+
}
|
|
5903
5946
|
}
|
|
5904
5947
|
const trackInfos = this.getAnnouncedTracks(offer.sdp);
|
|
5905
5948
|
if (trackInfos.length === 0) {
|
|
@@ -6061,6 +6104,22 @@ class Publisher {
|
|
|
6061
6104
|
});
|
|
6062
6105
|
});
|
|
6063
6106
|
}
|
|
6107
|
+
removeUnpreferredCodecs(sdp, trackType) {
|
|
6108
|
+
const opts = this.publishOptsForTrack.get(trackType);
|
|
6109
|
+
if (!opts || !opts.forceSingleCodec)
|
|
6110
|
+
return sdp;
|
|
6111
|
+
const codec = opts.forceCodec || opts.preferredCodec;
|
|
6112
|
+
const orderedCodecs = this.getCodecPreferences(trackType, codec);
|
|
6113
|
+
if (!orderedCodecs || orderedCodecs.length === 0)
|
|
6114
|
+
return sdp;
|
|
6115
|
+
const transceiver = this.transceiverCache.get(trackType);
|
|
6116
|
+
if (!transceiver)
|
|
6117
|
+
return sdp;
|
|
6118
|
+
const index = this.transceiverInitOrder.indexOf(trackType);
|
|
6119
|
+
const mid = extractMid(transceiver, index, sdp);
|
|
6120
|
+
const [codecToPreserve] = orderedCodecs;
|
|
6121
|
+
return preserveCodec(sdp, mid, codecToPreserve);
|
|
6122
|
+
}
|
|
6064
6123
|
}
|
|
6065
6124
|
|
|
6066
6125
|
/**
|
|
@@ -11503,6 +11562,33 @@ function buildWsSuccessAfterFailureInsight(connection) {
|
|
|
11503
11562
|
return buildWsBaseInsight(connection);
|
|
11504
11563
|
}
|
|
11505
11564
|
|
|
11565
|
+
/**
|
|
11566
|
+
* Saving a long-lived reference to a promise that can reject can be unsafe,
|
|
11567
|
+
* since rejecting the promise causes an unhandled rejection error (even if the
|
|
11568
|
+
* rejection is handled everywhere promise result is expected).
|
|
11569
|
+
*
|
|
11570
|
+
* To avoid that, we add both resolution and rejection handlers to the promise.
|
|
11571
|
+
* That way, the saved promise never rejects. A callback is provided as return
|
|
11572
|
+
* value to build a *new* promise, that resolves and rejects along with
|
|
11573
|
+
* the original promise.
|
|
11574
|
+
* @param promise Promise to wrap, which possibly rejects
|
|
11575
|
+
* @returns Callback to build a new promise, which resolves and rejects along
|
|
11576
|
+
* with the original promise
|
|
11577
|
+
*/
|
|
11578
|
+
function makeSafePromise(promise) {
|
|
11579
|
+
let isPending = true;
|
|
11580
|
+
const safePromise = promise
|
|
11581
|
+
.then((result) => ({ status: 'resolved', result }), (error) => ({ status: 'rejected', error }))
|
|
11582
|
+
.finally(() => (isPending = false));
|
|
11583
|
+
const unwrapPromise = () => safePromise.then((fulfillment) => {
|
|
11584
|
+
if (fulfillment.status === 'rejected')
|
|
11585
|
+
throw fulfillment.error;
|
|
11586
|
+
return fulfillment.result;
|
|
11587
|
+
});
|
|
11588
|
+
unwrapPromise.checkPending = () => isPending;
|
|
11589
|
+
return unwrapPromise;
|
|
11590
|
+
}
|
|
11591
|
+
|
|
11506
11592
|
// Type guards to check WebSocket error type
|
|
11507
11593
|
const isCloseEvent = (res) => res.code !== undefined;
|
|
11508
11594
|
const isErrorEvent = (res) => res.error !== undefined;
|
|
@@ -11751,10 +11837,10 @@ class StableWSConnection {
|
|
|
11751
11837
|
this._setupConnectionPromise = () => {
|
|
11752
11838
|
this.isResolved = false;
|
|
11753
11839
|
/** a promise that is resolved once ws.open is called */
|
|
11754
|
-
this.
|
|
11840
|
+
this.connectionOpenSafe = makeSafePromise(new Promise((resolve, reject) => {
|
|
11755
11841
|
this.resolvePromise = resolve;
|
|
11756
11842
|
this.rejectPromise = reject;
|
|
11757
|
-
});
|
|
11843
|
+
}));
|
|
11758
11844
|
};
|
|
11759
11845
|
/**
|
|
11760
11846
|
* Schedules a next health check ping for websocket.
|
|
@@ -11975,13 +12061,7 @@ class StableWSConnection {
|
|
|
11975
12061
|
this._log(`_connect() - tokenProvider failed before, so going to retry`);
|
|
11976
12062
|
await this.client.tokenManager.loadToken();
|
|
11977
12063
|
}
|
|
11978
|
-
|
|
11979
|
-
if (this.client.connectionIdPromise) {
|
|
11980
|
-
if (await isPromisePending(this.client.connectionIdPromise)) {
|
|
11981
|
-
mustSetupConnectionIdPromise = false;
|
|
11982
|
-
}
|
|
11983
|
-
}
|
|
11984
|
-
if (mustSetupConnectionIdPromise) {
|
|
12064
|
+
if (!this.client.isConnectionIsPromisePending) {
|
|
11985
12065
|
this.client._setupConnectionIdPromise();
|
|
11986
12066
|
}
|
|
11987
12067
|
this._setupConnectionPromise();
|
|
@@ -12101,6 +12181,9 @@ class StableWSConnection {
|
|
|
12101
12181
|
// we don't care
|
|
12102
12182
|
}
|
|
12103
12183
|
}
|
|
12184
|
+
get connectionOpen() {
|
|
12185
|
+
return this.connectionOpenSafe?.();
|
|
12186
|
+
}
|
|
12104
12187
|
}
|
|
12105
12188
|
|
|
12106
12189
|
function isString(arrayOrString) {
|
|
@@ -12664,19 +12747,21 @@ class StreamClient {
|
|
|
12664
12747
|
if (!this.userID) {
|
|
12665
12748
|
throw Error('UserWithId is not set on client, use client.connectUser or client.connectAnonymousUser instead');
|
|
12666
12749
|
}
|
|
12667
|
-
|
|
12750
|
+
const wsPromise = this.wsPromiseSafe?.();
|
|
12751
|
+
if (this.wsConnection?.isConnecting && wsPromise) {
|
|
12668
12752
|
this.logger('info', 'client:openConnection() - connection already in progress');
|
|
12669
|
-
return
|
|
12753
|
+
return await wsPromise;
|
|
12670
12754
|
}
|
|
12671
12755
|
if ((this.wsConnection?.isHealthy || this.wsFallback?.isHealthy()) &&
|
|
12672
12756
|
this._hasConnectionID()) {
|
|
12673
12757
|
this.logger('info', 'client:openConnection() - openConnection called twice, healthy connection already exists');
|
|
12674
|
-
return
|
|
12758
|
+
return;
|
|
12675
12759
|
}
|
|
12676
12760
|
this._setupConnectionIdPromise();
|
|
12677
12761
|
this.clientID = `${this.userID}--${randomId()}`;
|
|
12678
|
-
|
|
12679
|
-
|
|
12762
|
+
const newWsPromise = this.connect();
|
|
12763
|
+
this.wsPromiseSafe = makeSafePromise(newWsPromise);
|
|
12764
|
+
return await newWsPromise;
|
|
12680
12765
|
};
|
|
12681
12766
|
/**
|
|
12682
12767
|
* Disconnects the websocket and removes the user from client.
|
|
@@ -12694,7 +12779,7 @@ class StreamClient {
|
|
|
12694
12779
|
await this.closeConnection(timeout);
|
|
12695
12780
|
removeConnectionEventListeners(this.updateNetworkConnectionStatus);
|
|
12696
12781
|
this.tokenManager.reset();
|
|
12697
|
-
this.
|
|
12782
|
+
this.connectionIdPromiseSafe = undefined;
|
|
12698
12783
|
this.rejectConnectionId = undefined;
|
|
12699
12784
|
this.resolveConnectionId = undefined;
|
|
12700
12785
|
};
|
|
@@ -12755,12 +12840,12 @@ class StreamClient {
|
|
|
12755
12840
|
/**
|
|
12756
12841
|
* sets up the this.connectionIdPromise
|
|
12757
12842
|
*/
|
|
12758
|
-
this._setupConnectionIdPromise =
|
|
12843
|
+
this._setupConnectionIdPromise = () => {
|
|
12759
12844
|
/** a promise that is resolved once connection id is set */
|
|
12760
|
-
this.
|
|
12845
|
+
this.connectionIdPromiseSafe = makeSafePromise(new Promise((resolve, reject) => {
|
|
12761
12846
|
this.resolveConnectionId = resolve;
|
|
12762
12847
|
this.rejectConnectionId = reject;
|
|
12763
|
-
});
|
|
12848
|
+
}));
|
|
12764
12849
|
};
|
|
12765
12850
|
this._logApiRequest = (type, url, data, config) => {
|
|
12766
12851
|
this.logger('trace', `client: ${type} - Request - ${url}`, {
|
|
@@ -12978,7 +13063,7 @@ class StreamClient {
|
|
|
12978
13063
|
});
|
|
12979
13064
|
};
|
|
12980
13065
|
this.getUserAgent = () => {
|
|
12981
|
-
const version = "1.11.
|
|
13066
|
+
const version = "1.11.5";
|
|
12982
13067
|
return (this.userAgent ||
|
|
12983
13068
|
`stream-video-javascript-client-${this.node ? 'node' : 'browser'}-${version}`);
|
|
12984
13069
|
};
|
|
@@ -13092,7 +13177,7 @@ class StreamClient {
|
|
|
13092
13177
|
});
|
|
13093
13178
|
// WS connection is initialized when setUser is called
|
|
13094
13179
|
this.wsConnection = null;
|
|
13095
|
-
this.
|
|
13180
|
+
this.wsPromiseSafe = null;
|
|
13096
13181
|
this.setUserPromise = null;
|
|
13097
13182
|
// mapping between channel groups and configs
|
|
13098
13183
|
this.anonymous = false;
|
|
@@ -13109,6 +13194,15 @@ class StreamClient {
|
|
|
13109
13194
|
? inputOptions.logger
|
|
13110
13195
|
: () => null;
|
|
13111
13196
|
}
|
|
13197
|
+
get connectionIdPromise() {
|
|
13198
|
+
return this.connectionIdPromiseSafe?.();
|
|
13199
|
+
}
|
|
13200
|
+
get isConnectionIsPromisePending() {
|
|
13201
|
+
return this.connectionIdPromiseSafe?.checkPending() ?? false;
|
|
13202
|
+
}
|
|
13203
|
+
get wsPromise() {
|
|
13204
|
+
return this.wsPromiseSafe?.();
|
|
13205
|
+
}
|
|
13112
13206
|
}
|
|
13113
13207
|
|
|
13114
13208
|
/**
|
|
@@ -13118,6 +13212,7 @@ class StreamVideoClient {
|
|
|
13118
13212
|
constructor(apiKeyOrArgs, opts) {
|
|
13119
13213
|
this.logLevel = 'warn';
|
|
13120
13214
|
this.eventHandlersToUnregister = [];
|
|
13215
|
+
this.connectionConcurrencyTag = Symbol('connectionConcurrencyTag');
|
|
13121
13216
|
/**
|
|
13122
13217
|
* Connects the given user to the client.
|
|
13123
13218
|
* Only one user can connect at a time, if you want to change users, call `disconnectUser` before connecting a new user.
|
|
@@ -13139,11 +13234,7 @@ class StreamVideoClient {
|
|
|
13139
13234
|
return this.streamClient.connectGuestUser(user);
|
|
13140
13235
|
};
|
|
13141
13236
|
}
|
|
13142
|
-
|
|
13143
|
-
? this.disconnectionPromise.then(() => connectUser())
|
|
13144
|
-
: connectUser();
|
|
13145
|
-
this.connectionPromise?.finally(() => (this.connectionPromise = undefined));
|
|
13146
|
-
const connectUserResponse = await this.connectionPromise;
|
|
13237
|
+
const connectUserResponse = await withoutConcurrency(this.connectionConcurrencyTag, () => connectUser());
|
|
13147
13238
|
// connectUserResponse will be void if connectUser called twice for the same user
|
|
13148
13239
|
if (connectUserResponse?.me) {
|
|
13149
13240
|
this.writeableStateStore.setConnectedUser(connectUserResponse.me);
|
|
@@ -13220,17 +13311,13 @@ class StreamVideoClient {
|
|
|
13220
13311
|
* https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent
|
|
13221
13312
|
*/
|
|
13222
13313
|
this.disconnectUser = async (timeout) => {
|
|
13223
|
-
if (!this.streamClient.user
|
|
13314
|
+
if (!this.streamClient.user) {
|
|
13224
13315
|
return;
|
|
13225
13316
|
}
|
|
13226
13317
|
const userId = this.streamClient.user?.id;
|
|
13227
13318
|
const apiKey = this.streamClient.key;
|
|
13228
13319
|
const disconnectUser = () => this.streamClient.disconnectUser(timeout);
|
|
13229
|
-
this.
|
|
13230
|
-
? this.connectionPromise.then(() => disconnectUser())
|
|
13231
|
-
: disconnectUser();
|
|
13232
|
-
this.disconnectionPromise.finally(() => (this.disconnectionPromise = undefined));
|
|
13233
|
-
await this.disconnectionPromise;
|
|
13320
|
+
await withoutConcurrency(this.connectionConcurrencyTag, () => disconnectUser());
|
|
13234
13321
|
if (userId) {
|
|
13235
13322
|
StreamVideoClient._instanceMap.delete(apiKey + userId);
|
|
13236
13323
|
}
|
|
@@ -13405,11 +13492,7 @@ class StreamVideoClient {
|
|
|
13405
13492
|
*/
|
|
13406
13493
|
this.connectAnonymousUser = async (user, tokenOrProvider) => {
|
|
13407
13494
|
const connectAnonymousUser = () => this.streamClient.connectAnonymousUser(user, tokenOrProvider);
|
|
13408
|
-
this.
|
|
13409
|
-
? this.disconnectionPromise.then(() => connectAnonymousUser())
|
|
13410
|
-
: connectAnonymousUser();
|
|
13411
|
-
this.connectionPromise.finally(() => (this.connectionPromise = undefined));
|
|
13412
|
-
return this.connectionPromise;
|
|
13495
|
+
return await withoutConcurrency(this.connectionConcurrencyTag, () => connectAnonymousUser());
|
|
13413
13496
|
};
|
|
13414
13497
|
let logger = logToConsole;
|
|
13415
13498
|
let logLevel = 'warn';
|