@stream-io/video-client 1.42.1 → 1.42.2
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 +6 -0
- package/dist/index.browser.es.js +116 -48
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +116 -48
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +116 -48
- package/dist/index.es.js.map +1 -1
- package/dist/src/StreamSfuClient.d.ts +1 -6
- package/dist/src/errors/SfuJoinError.d.ts +7 -0
- package/dist/src/errors/index.d.ts +1 -0
- package/dist/src/gen/coordinator/index.d.ts +6 -0
- package/dist/src/rtc/BasePeerConnection.d.ts +1 -0
- package/dist/src/rtc/Dispatcher.d.ts +36 -2
- package/index.ts +1 -0
- package/package.json +1 -1
- package/src/Call.ts +22 -5
- package/src/StreamSfuClient.ts +36 -37
- package/src/errors/SfuJoinError.ts +26 -0
- package/src/errors/index.ts +1 -0
- package/src/events/internal.ts +4 -4
- package/src/events/speaker.ts +2 -2
- package/src/gen/coordinator/index.ts +6 -0
- package/src/rtc/BasePeerConnection.ts +3 -1
- package/src/rtc/Dispatcher.ts +55 -13
- package/src/rtc/__tests__/Dispatcher.test.ts +28 -0
- package/src/rtc/__tests__/Publisher.test.ts +3 -0
- package/src/rtc/__tests__/Subscriber.test.ts +2 -1
package/dist/index.cjs.js
CHANGED
|
@@ -4291,6 +4291,12 @@ const sfuEventKinds = {
|
|
|
4291
4291
|
changePublishOptions: undefined,
|
|
4292
4292
|
inboundStateNotification: undefined,
|
|
4293
4293
|
};
|
|
4294
|
+
/**
|
|
4295
|
+
* Determines if a given event name belongs to the category of SFU events.
|
|
4296
|
+
*
|
|
4297
|
+
* @param eventName the name of the event to check.
|
|
4298
|
+
* @returns true if the event name is an SFU event, otherwise false.
|
|
4299
|
+
*/
|
|
4294
4300
|
const isSfuEvent = (eventName) => {
|
|
4295
4301
|
return Object.prototype.hasOwnProperty.call(sfuEventKinds, eventName);
|
|
4296
4302
|
};
|
|
@@ -4298,33 +4304,70 @@ class Dispatcher {
|
|
|
4298
4304
|
constructor() {
|
|
4299
4305
|
this.logger = videoLoggerSystem.getLogger('Dispatcher');
|
|
4300
4306
|
this.subscribers = {};
|
|
4301
|
-
|
|
4307
|
+
/**
|
|
4308
|
+
* Dispatch an event to all subscribers.
|
|
4309
|
+
*
|
|
4310
|
+
* @param message the event payload to dispatch.
|
|
4311
|
+
* @param tag for scoping events to a specific tag. Use `*` dispatch to every tag.
|
|
4312
|
+
*/
|
|
4313
|
+
this.dispatch = (message, tag = '*') => {
|
|
4302
4314
|
const eventKind = message.eventPayload.oneofKind;
|
|
4303
4315
|
if (!eventKind)
|
|
4304
4316
|
return;
|
|
4305
4317
|
const payload = message.eventPayload[eventKind];
|
|
4306
4318
|
this.logger.debug(`Dispatching ${eventKind}, tag=${tag}`, payload);
|
|
4307
|
-
const
|
|
4308
|
-
if (!
|
|
4319
|
+
const handlers = this.subscribers[eventKind];
|
|
4320
|
+
if (!handlers)
|
|
4309
4321
|
return;
|
|
4310
|
-
|
|
4322
|
+
this.emit(payload, handlers[tag]);
|
|
4323
|
+
if (tag !== '*')
|
|
4324
|
+
this.emit(payload, handlers['*']);
|
|
4325
|
+
};
|
|
4326
|
+
/**
|
|
4327
|
+
* Emit an event to a list of listeners.
|
|
4328
|
+
*
|
|
4329
|
+
* @param payload the event payload to emit.
|
|
4330
|
+
* @param listeners the list of listeners to emit the event to.
|
|
4331
|
+
*/
|
|
4332
|
+
this.emit = (payload, listeners = []) => {
|
|
4333
|
+
for (const listener of listeners) {
|
|
4311
4334
|
try {
|
|
4312
|
-
|
|
4335
|
+
listener(payload);
|
|
4313
4336
|
}
|
|
4314
4337
|
catch (e) {
|
|
4315
4338
|
this.logger.warn('Listener failed with error', e);
|
|
4316
4339
|
}
|
|
4317
4340
|
}
|
|
4318
4341
|
};
|
|
4319
|
-
|
|
4342
|
+
/**
|
|
4343
|
+
* Subscribe to an event.
|
|
4344
|
+
*
|
|
4345
|
+
* @param eventName the name of the event to subscribe to.
|
|
4346
|
+
* @param tag for scoping events to a specific tag. Use `*` dispatch to every tag.
|
|
4347
|
+
* @param fn the callback function to invoke when the event is emitted.
|
|
4348
|
+
* @returns a function that can be called to unsubscribe from the event.
|
|
4349
|
+
*/
|
|
4350
|
+
this.on = (eventName, tag, fn) => {
|
|
4320
4351
|
var _a;
|
|
4321
|
-
((_a = this.subscribers)[eventName] ?? (_a[eventName] =
|
|
4352
|
+
const bucket = ((_a = this.subscribers)[eventName] ?? (_a[eventName] = {}));
|
|
4353
|
+
(bucket[tag] ?? (bucket[tag] = [])).push(fn);
|
|
4322
4354
|
return () => {
|
|
4323
|
-
this.off(eventName, fn);
|
|
4355
|
+
this.off(eventName, tag, fn);
|
|
4324
4356
|
};
|
|
4325
4357
|
};
|
|
4326
|
-
|
|
4327
|
-
|
|
4358
|
+
/**
|
|
4359
|
+
* Unsubscribe from an event.
|
|
4360
|
+
*
|
|
4361
|
+
* @param eventName the name of the event to unsubscribe from.
|
|
4362
|
+
* @param tag for scoping events to a specific tag. Use `*` dispatch to every tag.
|
|
4363
|
+
* @param fn the callback function to remove from the event listeners.
|
|
4364
|
+
*/
|
|
4365
|
+
this.off = (eventName, tag, fn) => {
|
|
4366
|
+
const bucket = this.subscribers[eventName];
|
|
4367
|
+
const listeners = bucket?.[tag];
|
|
4368
|
+
if (!listeners)
|
|
4369
|
+
return;
|
|
4370
|
+
bucket[tag] = listeners.filter((f) => f !== fn);
|
|
4328
4371
|
};
|
|
4329
4372
|
}
|
|
4330
4373
|
}
|
|
@@ -6208,7 +6251,7 @@ const getSdkVersion = (sdk) => {
|
|
|
6208
6251
|
return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
|
|
6209
6252
|
};
|
|
6210
6253
|
|
|
6211
|
-
const version = "1.42.
|
|
6254
|
+
const version = "1.42.2";
|
|
6212
6255
|
const [major, minor, patch] = version.split('.');
|
|
6213
6256
|
let sdkInfo = {
|
|
6214
6257
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -7275,7 +7318,7 @@ class BasePeerConnection {
|
|
|
7275
7318
|
* Consecutive events are queued and executed one after the other.
|
|
7276
7319
|
*/
|
|
7277
7320
|
this.on = (event, fn) => {
|
|
7278
|
-
this.subscriptions.push(this.dispatcher.on(event, (e) => {
|
|
7321
|
+
this.subscriptions.push(this.dispatcher.on(event, this.tag, (e) => {
|
|
7279
7322
|
const lockKey = `pc.${this.lock}.${event}`;
|
|
7280
7323
|
withoutConcurrency(lockKey, async () => fn(e)).catch((err) => {
|
|
7281
7324
|
if (this.isDisposed)
|
|
@@ -7460,6 +7503,7 @@ class BasePeerConnection {
|
|
|
7460
7503
|
this.dispatcher = dispatcher;
|
|
7461
7504
|
this.iceRestartDelay = iceRestartDelay;
|
|
7462
7505
|
this.clientPublishOptions = clientPublishOptions;
|
|
7506
|
+
this.tag = tag;
|
|
7463
7507
|
this.onReconnectionNeeded = onReconnectionNeeded;
|
|
7464
7508
|
this.logger = videoLoggerSystem.getLogger(peerType === PeerType.SUBSCRIBER ? 'Subscriber' : 'Publisher', { tags: [tag] });
|
|
7465
7509
|
this.pc = this.createPeerConnection(connectionConfig);
|
|
@@ -8462,6 +8506,21 @@ const getTimers = lazy(() => {
|
|
|
8462
8506
|
return new workerTimer.WorkerTimer({ useWorker: timerWorkerEnabled });
|
|
8463
8507
|
});
|
|
8464
8508
|
|
|
8509
|
+
class SfuJoinError extends Error {
|
|
8510
|
+
constructor(event) {
|
|
8511
|
+
super(event.error?.message || 'Join Error');
|
|
8512
|
+
this.errorEvent = event;
|
|
8513
|
+
this.unrecoverable =
|
|
8514
|
+
event.reconnectStrategy === WebsocketReconnectStrategy.DISCONNECT;
|
|
8515
|
+
}
|
|
8516
|
+
static isJoinErrorCode(event) {
|
|
8517
|
+
const code = event.error?.code;
|
|
8518
|
+
return (code === ErrorCode.SFU_FULL ||
|
|
8519
|
+
code === ErrorCode.SFU_SHUTTING_DOWN ||
|
|
8520
|
+
code === ErrorCode.CALL_PARTICIPANT_LIMIT_REACHED);
|
|
8521
|
+
}
|
|
8522
|
+
}
|
|
8523
|
+
|
|
8465
8524
|
/**
|
|
8466
8525
|
* The client used for exchanging information with the SFU.
|
|
8467
8526
|
*/
|
|
@@ -8638,7 +8697,7 @@ class StreamSfuClient {
|
|
|
8638
8697
|
const { timeout = 7 * 1000 } = opts;
|
|
8639
8698
|
this.migrationTask?.reject(new Error('Cancelled previous migration'));
|
|
8640
8699
|
const task = (this.migrationTask = promiseWithResolvers());
|
|
8641
|
-
const unsubscribe = this.dispatcher.on('participantMigrationComplete', () => {
|
|
8700
|
+
const unsubscribe = this.dispatcher.on('participantMigrationComplete', this.tag, () => {
|
|
8642
8701
|
unsubscribe();
|
|
8643
8702
|
clearTimeout(this.migrateAwayTimeout);
|
|
8644
8703
|
task.resolve();
|
|
@@ -8664,27 +8723,29 @@ class StreamSfuClient {
|
|
|
8664
8723
|
// be replaced with a new one in case a second join request is made
|
|
8665
8724
|
const current = this.joinResponseTask;
|
|
8666
8725
|
let timeoutId = undefined;
|
|
8667
|
-
|
|
8668
|
-
|
|
8669
|
-
|
|
8670
|
-
|
|
8671
|
-
|
|
8672
|
-
|
|
8673
|
-
|
|
8674
|
-
|
|
8726
|
+
let unsubscribeJoinResponse = undefined;
|
|
8727
|
+
let unsubscribeJoinErrorEvents = undefined;
|
|
8728
|
+
const cleanupJoinSubscriptions = () => {
|
|
8729
|
+
clearTimeout(timeoutId);
|
|
8730
|
+
timeoutId = undefined;
|
|
8731
|
+
unsubscribeJoinErrorEvents?.();
|
|
8732
|
+
unsubscribeJoinErrorEvents = undefined;
|
|
8733
|
+
unsubscribeJoinResponse?.();
|
|
8734
|
+
unsubscribeJoinResponse = undefined;
|
|
8735
|
+
};
|
|
8736
|
+
unsubscribeJoinErrorEvents = this.dispatcher.on('error', this.tag, (event) => {
|
|
8737
|
+
if (SfuJoinError.isJoinErrorCode(event)) {
|
|
8738
|
+
cleanupJoinSubscriptions();
|
|
8675
8739
|
current.reject(new SfuJoinError(event));
|
|
8676
8740
|
}
|
|
8677
8741
|
});
|
|
8678
|
-
|
|
8679
|
-
|
|
8680
|
-
unsubscribe();
|
|
8681
|
-
unsubscribeJoinErrorEvents();
|
|
8742
|
+
unsubscribeJoinResponse = this.dispatcher.on('joinResponse', this.tag, (joinResponse) => {
|
|
8743
|
+
cleanupJoinSubscriptions();
|
|
8682
8744
|
this.keepAlive();
|
|
8683
8745
|
current.resolve(joinResponse);
|
|
8684
8746
|
});
|
|
8685
8747
|
timeoutId = setTimeout(() => {
|
|
8686
|
-
|
|
8687
|
-
unsubscribeJoinErrorEvents();
|
|
8748
|
+
cleanupJoinSubscriptions();
|
|
8688
8749
|
const message = `Waiting for "joinResponse" has timed out after ${this.joinResponseTimeout}ms`;
|
|
8689
8750
|
this.tracer?.trace('joinRequestTimeout', message);
|
|
8690
8751
|
current.reject(new Error(message));
|
|
@@ -8779,8 +8840,8 @@ class StreamSfuClient {
|
|
|
8779
8840
|
// In that case, those events (ICE candidates) need to be buffered
|
|
8780
8841
|
// and later added to the appropriate PeerConnection
|
|
8781
8842
|
// once the remoteDescription is known and set.
|
|
8782
|
-
this.unsubscribeIceTrickle = dispatcher.on('iceTrickle', (
|
|
8783
|
-
this.iceTrickleBuffer.push(
|
|
8843
|
+
this.unsubscribeIceTrickle = dispatcher.on('iceTrickle', tag, (t) => {
|
|
8844
|
+
this.iceTrickleBuffer.push(t);
|
|
8784
8845
|
});
|
|
8785
8846
|
// listen to network changes to handle offline state
|
|
8786
8847
|
// we shouldn't attempt to recover websocket connection when offline
|
|
@@ -8829,14 +8890,6 @@ StreamSfuClient.DISPOSE_OLD_SOCKET = 4100;
|
|
|
8829
8890
|
* The close code used when the client fails to join the call (on the SFU).
|
|
8830
8891
|
*/
|
|
8831
8892
|
StreamSfuClient.JOIN_FAILED = 4101;
|
|
8832
|
-
class SfuJoinError extends Error {
|
|
8833
|
-
constructor(event) {
|
|
8834
|
-
super(event.error?.message || 'Join Error');
|
|
8835
|
-
this.errorEvent = event;
|
|
8836
|
-
this.unrecoverable =
|
|
8837
|
-
event.reconnectStrategy === WebsocketReconnectStrategy.DISCONNECT;
|
|
8838
|
-
}
|
|
8839
|
-
}
|
|
8840
8893
|
|
|
8841
8894
|
/**
|
|
8842
8895
|
* Event handler that watched the delivery of `call.accepted`.
|
|
@@ -8985,7 +9038,7 @@ const removeFromIfPresent = (arr, ...values) => {
|
|
|
8985
9038
|
};
|
|
8986
9039
|
|
|
8987
9040
|
const watchConnectionQualityChanged = (dispatcher, state) => {
|
|
8988
|
-
return dispatcher.on('connectionQualityChanged', (e) => {
|
|
9041
|
+
return dispatcher.on('connectionQualityChanged', '*', (e) => {
|
|
8989
9042
|
const { connectionQualityUpdates } = e;
|
|
8990
9043
|
if (!connectionQualityUpdates)
|
|
8991
9044
|
return;
|
|
@@ -9003,7 +9056,7 @@ const watchConnectionQualityChanged = (dispatcher, state) => {
|
|
|
9003
9056
|
* health check events that our SFU sends.
|
|
9004
9057
|
*/
|
|
9005
9058
|
const watchParticipantCountChanged = (dispatcher, state) => {
|
|
9006
|
-
return dispatcher.on('healthCheckResponse', (e) => {
|
|
9059
|
+
return dispatcher.on('healthCheckResponse', '*', (e) => {
|
|
9007
9060
|
const { participantCount } = e;
|
|
9008
9061
|
if (participantCount) {
|
|
9009
9062
|
state.setParticipantCount(participantCount.total);
|
|
@@ -9012,7 +9065,7 @@ const watchParticipantCountChanged = (dispatcher, state) => {
|
|
|
9012
9065
|
});
|
|
9013
9066
|
};
|
|
9014
9067
|
const watchLiveEnded = (dispatcher, call) => {
|
|
9015
|
-
return dispatcher.on('error', (e) => {
|
|
9068
|
+
return dispatcher.on('error', '*', (e) => {
|
|
9016
9069
|
if (e.error && e.error.code !== ErrorCode.LIVE_ENDED)
|
|
9017
9070
|
return;
|
|
9018
9071
|
call.state.setBackstage(true);
|
|
@@ -9027,7 +9080,7 @@ const watchLiveEnded = (dispatcher, call) => {
|
|
|
9027
9080
|
* Watches and logs the errors reported by the currently connected SFU.
|
|
9028
9081
|
*/
|
|
9029
9082
|
const watchSfuErrorReports = (dispatcher) => {
|
|
9030
|
-
return dispatcher.on('error', (e) => {
|
|
9083
|
+
return dispatcher.on('error', '*', (e) => {
|
|
9031
9084
|
if (!e.error)
|
|
9032
9085
|
return;
|
|
9033
9086
|
const logger = videoLoggerSystem.getLogger('SfuClient');
|
|
@@ -9226,7 +9279,7 @@ const reconcileOrphanedTracks = (state, participant) => {
|
|
|
9226
9279
|
* Watches for `dominantSpeakerChanged` events.
|
|
9227
9280
|
*/
|
|
9228
9281
|
const watchDominantSpeakerChanged = (dispatcher, state) => {
|
|
9229
|
-
return dispatcher.on('dominantSpeakerChanged', (e) => {
|
|
9282
|
+
return dispatcher.on('dominantSpeakerChanged', '*', (e) => {
|
|
9230
9283
|
const { sessionId } = e;
|
|
9231
9284
|
if (sessionId === state.dominantSpeaker?.sessionId)
|
|
9232
9285
|
return;
|
|
@@ -9253,7 +9306,7 @@ const watchDominantSpeakerChanged = (dispatcher, state) => {
|
|
|
9253
9306
|
* Watches for `audioLevelChanged` events.
|
|
9254
9307
|
*/
|
|
9255
9308
|
const watchAudioLevelChanged = (dispatcher, state) => {
|
|
9256
|
-
return dispatcher.on('audioLevelChanged', (e) => {
|
|
9309
|
+
return dispatcher.on('audioLevelChanged', '*', (e) => {
|
|
9257
9310
|
const { audioLevels } = e;
|
|
9258
9311
|
state.updateParticipants(audioLevels.reduce((patches, current) => {
|
|
9259
9312
|
patches[current.sessionId] = {
|
|
@@ -12609,7 +12662,7 @@ class Call {
|
|
|
12609
12662
|
*/
|
|
12610
12663
|
this.on = (eventName, fn) => {
|
|
12611
12664
|
if (isSfuEvent(eventName)) {
|
|
12612
|
-
return this.dispatcher.on(eventName, fn);
|
|
12665
|
+
return this.dispatcher.on(eventName, '*', fn);
|
|
12613
12666
|
}
|
|
12614
12667
|
const offHandler = this.streamClient.on(eventName, (e) => {
|
|
12615
12668
|
const event = e;
|
|
@@ -12631,7 +12684,7 @@ class Call {
|
|
|
12631
12684
|
*/
|
|
12632
12685
|
this.off = (eventName, fn) => {
|
|
12633
12686
|
if (isSfuEvent(eventName)) {
|
|
12634
|
-
return this.dispatcher.off(eventName, fn);
|
|
12687
|
+
return this.dispatcher.off(eventName, '*', fn);
|
|
12635
12688
|
}
|
|
12636
12689
|
// unsubscribe from the stream client event by using the 'off' reference
|
|
12637
12690
|
const registeredOffHandler = this.streamClientEventHandlers.get(fn);
|
|
@@ -12867,6 +12920,7 @@ class Call {
|
|
|
12867
12920
|
this.logger.trace(`Joining call (${attempt})`, this.cid);
|
|
12868
12921
|
await this.doJoin(data);
|
|
12869
12922
|
delete joinData.migrating_from;
|
|
12923
|
+
delete joinData.migrating_from_list;
|
|
12870
12924
|
break;
|
|
12871
12925
|
}
|
|
12872
12926
|
catch (err) {
|
|
@@ -12878,11 +12932,15 @@ class Call {
|
|
|
12878
12932
|
// to join the call due to some reason (e.g., ended call, expired token...)
|
|
12879
12933
|
throw err;
|
|
12880
12934
|
}
|
|
12935
|
+
// immediately switch to a different SFU in case of recoverable join error
|
|
12936
|
+
const switchSfu = err instanceof SfuJoinError &&
|
|
12937
|
+
SfuJoinError.isJoinErrorCode(err.errorEvent);
|
|
12881
12938
|
const sfuId = this.credentials?.server.edge_name || '';
|
|
12882
12939
|
const failures = (sfuJoinFailures.get(sfuId) || 0) + 1;
|
|
12883
12940
|
sfuJoinFailures.set(sfuId, failures);
|
|
12884
|
-
if (failures >= 2) {
|
|
12941
|
+
if (switchSfu || failures >= 2) {
|
|
12885
12942
|
joinData.migrating_from = sfuId;
|
|
12943
|
+
joinData.migrating_from_list = Array.from(sfuJoinFailures.keys());
|
|
12886
12944
|
}
|
|
12887
12945
|
if (attempt === maxJoinRetries - 1) {
|
|
12888
12946
|
throw err;
|
|
@@ -13410,12 +13468,17 @@ class Call {
|
|
|
13410
13468
|
const migrationTask = makeSafePromise(currentSfuClient.enterMigration());
|
|
13411
13469
|
try {
|
|
13412
13470
|
const currentSfu = currentSfuClient.edgeName;
|
|
13413
|
-
await this.doJoin({
|
|
13471
|
+
await this.doJoin({
|
|
13472
|
+
...this.joinCallData,
|
|
13473
|
+
migrating_from: currentSfu,
|
|
13474
|
+
migrating_from_list: [currentSfu],
|
|
13475
|
+
});
|
|
13414
13476
|
}
|
|
13415
13477
|
finally {
|
|
13416
13478
|
// cleanup the migration_from field after the migration is complete or failed
|
|
13417
13479
|
// as we don't want to keep dirty data in the join call data
|
|
13418
13480
|
delete this.joinCallData?.migrating_from;
|
|
13481
|
+
delete this.joinCallData?.migrating_from_list;
|
|
13419
13482
|
}
|
|
13420
13483
|
await this.restorePublishedTracks();
|
|
13421
13484
|
this.restoreSubscribedTracks();
|
|
@@ -13450,6 +13513,11 @@ class Call {
|
|
|
13450
13513
|
// handles the "error" event, through which the SFU can request a reconnect
|
|
13451
13514
|
const unregisterOnError = this.on('error', (e) => {
|
|
13452
13515
|
const { reconnectStrategy: strategy, error } = e;
|
|
13516
|
+
// SFU_FULL is a join error, and when emitted, although it specifies a
|
|
13517
|
+
// `migrate` strategy, we should actually perform a REJOIN to a new SFU.
|
|
13518
|
+
// This is now handled separately in the `call.join()` method.
|
|
13519
|
+
if (SfuJoinError.isJoinErrorCode(e))
|
|
13520
|
+
return;
|
|
13453
13521
|
if (strategy === WebsocketReconnectStrategy.UNSPECIFIED)
|
|
13454
13522
|
return;
|
|
13455
13523
|
if (strategy === WebsocketReconnectStrategy.DISCONNECT) {
|
|
@@ -15546,7 +15614,7 @@ class StreamClient {
|
|
|
15546
15614
|
this.getUserAgent = () => {
|
|
15547
15615
|
if (!this.cachedUserAgent) {
|
|
15548
15616
|
const { clientAppIdentifier = {} } = this.options;
|
|
15549
|
-
const { sdkName = 'js', sdkVersion = "1.42.
|
|
15617
|
+
const { sdkName = 'js', sdkVersion = "1.42.2", ...extras } = clientAppIdentifier;
|
|
15550
15618
|
this.cachedUserAgent = [
|
|
15551
15619
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
15552
15620
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|