@mebius-io/web 0.6.2 → 0.7.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/dist/index.cjs CHANGED
@@ -264,10 +264,21 @@ function resetVideoElement(video) {
264
264
  }
265
265
 
266
266
  // src/internal/ll-view-transport.ts
267
+ var DEFAULT_REALTIME_TARGET_MS = 300;
268
+ function holdBuffer(receiver, targetMs) {
269
+ const r = receiver;
270
+ try {
271
+ if ("jitterBufferTarget" in r) r.jitterBufferTarget = targetMs;
272
+ if ("playoutDelayHint" in r) r.playoutDelayHint = targetMs / 1e3;
273
+ } catch {
274
+ }
275
+ }
267
276
  var WhepViewTransport = class {
268
- constructor(signaling) {
277
+ constructor(signaling, targetLatencyMs = DEFAULT_REALTIME_TARGET_MS) {
269
278
  this.signaling = signaling;
279
+ this.targetLatencyMs = targetLatencyMs;
270
280
  this.kind = "whep";
281
+ this.cursor = null;
271
282
  this.pc = null;
272
283
  this.resourceUrl = null;
273
284
  this.endedCb = null;
@@ -291,6 +302,7 @@ var WhepViewTransport = class {
291
302
  pc.addTransceiver("video", { direction: "recvonly" });
292
303
  pc.addTransceiver("audio", { direction: "recvonly" });
293
304
  pc.ontrack = (ev) => {
305
+ holdBuffer(ev.receiver, this.targetLatencyMs);
294
306
  remote.addTrack(ev.track);
295
307
  video.srcObject = remote;
296
308
  void playWithAutoplayFallback(video).then((o) => {
@@ -318,6 +330,7 @@ var WhepViewTransport = class {
318
330
  await pc.setRemoteDescription({ type: "answer", sdp: answer });
319
331
  }
320
332
  async stop() {
333
+ this.cursor = null;
321
334
  await this.signaling.deleteResource(this.resourceUrl);
322
335
  this.resourceUrl = null;
323
336
  this.pc?.close();
@@ -325,24 +338,59 @@ var WhepViewTransport = class {
325
338
  if (this.video) resetVideoElement(this.video);
326
339
  this.video = null;
327
340
  }
341
+ /**
342
+ * Read what the connection actually did since the last reading.
343
+ *
344
+ * This route needs to measure its own freezes, because nothing else can. A
345
+ * frozen real-time picture still reports a healthy connection and raises no
346
+ * event on the video element — so from the outside the session looks flawless
347
+ * while the viewer stares at a still frame. Every freeze on this route used to
348
+ * be recorded as zero, which is why the problem could be felt and never seen.
349
+ */
328
350
  async getStats() {
329
351
  if (!this.pc) return null;
330
352
  const report = await this.pc.getStats();
331
- let bitrateKbps = 0;
332
- let framesPerSecond = 0;
333
- let latencyMs;
353
+ let framesPerSecond;
354
+ let freezeS = 0;
355
+ let packetsLost = 0;
356
+ let packetsReceived = 0;
357
+ let bytesReceived = 0;
358
+ let bufferDelayS = 0;
359
+ let bufferEmitted = 0;
360
+ let rttMs;
334
361
  report.forEach((stat) => {
335
- if (stat.type === "inbound-rtp") {
336
- if (typeof stat.framesPerSecond === "number") framesPerSecond = stat.framesPerSecond;
337
- if (typeof stat.jitter === "number") latencyMs = Math.round(stat.jitter * 1e3);
362
+ if (stat.type === "inbound-rtp" && stat.kind === "video") {
363
+ const v = stat;
364
+ if (typeof v.framesPerSecond === "number") framesPerSecond = v.framesPerSecond;
365
+ if (typeof v.totalFreezesDuration === "number") freezeS = v.totalFreezesDuration;
366
+ if (typeof v.packetsLost === "number") packetsLost = v.packetsLost;
367
+ if (typeof v.packetsReceived === "number") packetsReceived = v.packetsReceived;
368
+ if (typeof v.bytesReceived === "number") bytesReceived = v.bytesReceived;
369
+ if (typeof v.jitterBufferDelay === "number") bufferDelayS = v.jitterBufferDelay;
370
+ if (typeof v.jitterBufferEmittedCount === "number") bufferEmitted = v.jitterBufferEmittedCount;
338
371
  }
339
372
  if (stat.type === "candidate-pair" && stat.state === "succeeded") {
340
- if (typeof stat.availableIncomingBitrate === "number") {
341
- bitrateKbps = Math.round(stat.availableIncomingBitrate / 1e3);
342
- }
373
+ const p = stat;
374
+ if (typeof p.currentRoundTripTime === "number") rttMs = Math.round(p.currentRoundTripTime * 1e3);
343
375
  }
344
376
  });
345
- return { bitrateKbps, framesPerSecond, latencyMs };
377
+ const atMs = Date.now();
378
+ const previous = this.cursor;
379
+ this.cursor = { atMs, freezeS, packetsLost, packetsReceived, bytesReceived, bufferDelayS, bufferEmitted };
380
+ if (!previous) return { framesPerSecond, rttMs };
381
+ const elapsedS = Math.max(1e-3, (atMs - previous.atMs) / 1e3);
382
+ const deltaLost = Math.max(0, packetsLost - previous.packetsLost);
383
+ const deltaReceived = Math.max(0, packetsReceived - previous.packetsReceived);
384
+ const deltaEmitted = bufferEmitted - previous.bufferEmitted;
385
+ const heldMs = deltaEmitted > 0 ? (bufferDelayS - previous.bufferDelayS) / deltaEmitted * 1e3 : void 0;
386
+ return {
387
+ bitrateKbps: Math.round((bytesReceived - previous.bytesReceived) * 8 / elapsedS / 1e3),
388
+ framesPerSecond,
389
+ latencyMs: heldMs === void 0 ? void 0 : Math.round(heldMs + (rttMs !== void 0 ? rttMs / 2 : 0)),
390
+ rttMs,
391
+ packetLossPct: deltaLost + deltaReceived > 0 ? Number((deltaLost / (deltaLost + deltaReceived) * 100).toFixed(2)) : 0,
392
+ freezeMs: Math.max(0, Math.round((freezeS - previous.freezeS) * 1e3))
393
+ };
346
394
  }
347
395
  };
348
396
 
@@ -350,6 +398,19 @@ var WhepViewTransport = class {
350
398
  function retryWarmupNotFound(cfg, retryCount, res, retry) {
351
399
  return retry || retryCount < (cfg?.maxNumRetry ?? 0) && res?.code === 404;
352
400
  }
401
+ var TOKEN_PARAM = /([?&]token=)[^&#]*/;
402
+ function withCurrentToken(url, token) {
403
+ return url.replace(TOKEN_PARAM, (_match, prefix) => prefix + encodeURIComponent(token));
404
+ }
405
+ function tokenRestampingLoader(Hls, currentToken) {
406
+ const Base = Hls.DefaultConfig.loader;
407
+ return class TokenRestampingLoader extends Base {
408
+ load(context, config2, callbacks) {
409
+ context.url = withCurrentToken(context.url, currentToken());
410
+ super.load(context, config2, callbacks);
411
+ }
412
+ };
413
+ }
353
414
  var HlsViewTransport = class {
354
415
  /**
355
416
  * deliveryPath, when given, is a gateway-relative path from the gateway's own
@@ -357,9 +418,10 @@ var HlsViewTransport = class {
357
418
  * origin one. Without it this falls back to the origin playlist, which is
358
419
  * still correct, just served from our own bandwidth.
359
420
  */
360
- constructor(signaling, deliveryPath) {
421
+ constructor(signaling, deliveryPath, targetS) {
361
422
  this.signaling = signaling;
362
423
  this.deliveryPath = deliveryPath;
424
+ this.targetS = targetS;
363
425
  this.kind = "hls";
364
426
  this.hls = null;
365
427
  this.video = null;
@@ -399,6 +461,11 @@ var HlsViewTransport = class {
399
461
  }
400
462
  const hls = new Hls({
401
463
  maxLiveSyncPlaybackRate: 1.1,
464
+ loader: tokenRestampingLoader(Hls, () => this.signaling.accessToken()),
465
+ // Only when the app actually asked. Left alone, the library follows the
466
+ // playlist's own HOLD-BACK, which the server measured from the segments it
467
+ // is producing — a number guessed here would only override a real one.
468
+ ...this.targetS === void 0 ? {} : { liveSyncDuration: this.targetS },
402
469
  manifestLoadPolicy: {
403
470
  default: {
404
471
  maxTimeToFirstByteMs: 1e4,
@@ -468,8 +535,12 @@ var LIVE_FLV_CONFIG = {
468
535
  autoCleanupMinBackwardDuration: 10,
469
536
  reuseRedirectedURL: true
470
537
  };
471
- var MAX_DRIFT_S = 2;
472
- var EDGE_MARGIN_S = 0.4;
538
+ var DEFAULT_BALANCED_TARGET_S = 2;
539
+ var CATCH_UP_RATE = 1.05;
540
+ var BUILD_UP_RATE = 0.98;
541
+ var SEEK_AT = 4;
542
+ var CATCH_UP_ABOVE = 1.5;
543
+ var BUILD_UP_BELOW = 0.9;
473
544
  var AUDIO_RETRY_MS = 2500;
474
545
  var UPSTREAM_LATENCY_MS = 800;
475
546
  function stalledWithData(video, ms) {
@@ -487,17 +558,31 @@ function stalledWithData(video, ms) {
487
558
  video.addEventListener("timeupdate", onTime);
488
559
  });
489
560
  }
490
- function chaseLiveEdge(video) {
561
+ function syncLiveEdge(video, targetS = DEFAULT_BALANCED_TARGET_S) {
491
562
  const ranges = video.buffered;
492
563
  if (ranges.length === 0) return;
493
564
  const edge = ranges.end(ranges.length - 1);
494
- if (edge - video.currentTime <= MAX_DRIFT_S) return;
495
- video.currentTime = edge - EDGE_MARGIN_S;
565
+ const drift = edge - video.currentTime;
566
+ if (drift > targetS * SEEK_AT) {
567
+ video.currentTime = edge - targetS;
568
+ video.playbackRate = 1;
569
+ return;
570
+ }
571
+ if (drift > targetS * CATCH_UP_ABOVE) {
572
+ video.playbackRate = CATCH_UP_RATE;
573
+ return;
574
+ }
575
+ if (drift < targetS * BUILD_UP_BELOW) {
576
+ video.playbackRate = BUILD_UP_RATE;
577
+ return;
578
+ }
579
+ video.playbackRate = 1;
496
580
  }
497
581
  var FlvViewTransport = class {
498
- constructor(signaling, deliveryPath) {
582
+ constructor(signaling, deliveryPath, targetS = DEFAULT_BALANCED_TARGET_S) {
499
583
  this.signaling = signaling;
500
584
  this.deliveryPath = deliveryPath;
585
+ this.targetS = targetS;
501
586
  this.kind = "flv_js";
502
587
  this.player = null;
503
588
  this.video = null;
@@ -523,7 +608,7 @@ var FlvViewTransport = class {
523
608
  const { signal } = this.listeners;
524
609
  video.addEventListener("ended", () => this.endedCb?.(), { signal });
525
610
  video.addEventListener("waiting", () => this.bufferingCb?.(), { signal });
526
- video.addEventListener("timeupdate", () => chaseLiveEdge(video), { signal });
611
+ video.addEventListener("timeupdate", () => syncLiveEdge(video, this.targetS), { signal });
527
612
  let mod;
528
613
  try {
529
614
  mod = await import("flv.js");
@@ -567,6 +652,7 @@ var FlvViewTransport = class {
567
652
  this.listeners = null;
568
653
  this.teardownPlayer();
569
654
  if (this.video) {
655
+ this.video.playbackRate = 1;
570
656
  this.video.removeAttribute("src");
571
657
  this.video.load();
572
658
  }
@@ -632,18 +718,29 @@ var KIND_LOCAL = "local";
632
718
  function canPlayBuffered() {
633
719
  return typeof MediaSource !== "undefined";
634
720
  }
635
- function transportFor(kind, path, signaling) {
636
- if (kind === KIND_FAST) return canPlayBuffered() ? new FlvViewTransport(signaling, path) : null;
637
- if (kind === KIND_WIDE || kind === KIND_LOCAL) return new HlsViewTransport(signaling, path);
721
+ function transportFor(kind, path, signaling, targetLatencyMs) {
722
+ const targetS = targetLatencyMs === void 0 ? void 0 : targetLatencyMs / 1e3;
723
+ if (kind === KIND_FAST)
724
+ return canPlayBuffered() ? new FlvViewTransport(signaling, path, targetS) : null;
725
+ if (kind === KIND_WIDE || kind === KIND_LOCAL)
726
+ return new HlsViewTransport(signaling, path, targetS);
638
727
  return null;
639
728
  }
640
- function createViewCandidates(mode, signaling, deliveries = []) {
641
- const fromGateway = (kinds) => deliveries.filter((d) => kinds.includes(d.kind)).map((d) => transportFor(d.kind, d.path, signaling)).filter((t) => t !== null);
642
- const originFallback = new HlsViewTransport(signaling);
729
+ function createViewCandidates(mode, signaling, deliveries = [], targetLatencyMs) {
730
+ const fromGateway = (kinds) => deliveries.filter((d) => kinds.includes(d.kind)).map((d) => transportFor(d.kind, d.path, signaling, targetLatencyMs)).filter((t) => t !== null);
731
+ const originFallback = new HlsViewTransport(
732
+ signaling,
733
+ void 0,
734
+ targetLatencyMs === void 0 ? void 0 : targetLatencyMs / 1e3
735
+ );
643
736
  const allKinds = [KIND_FAST, KIND_WIDE, KIND_LOCAL];
644
737
  switch (mode) {
645
738
  case "low-latency":
646
- return [new WhepViewTransport(signaling), ...fromGateway(allKinds), originFallback];
739
+ return [
740
+ new WhepViewTransport(signaling, targetLatencyMs ?? DEFAULT_REALTIME_TARGET_MS),
741
+ ...fromGateway(allKinds),
742
+ originFallback
743
+ ];
647
744
  case "balanced":
648
745
  return [...fromGateway(allKinds), originFallback];
649
746
  case "scale":
@@ -654,7 +751,7 @@ function createViewCandidates(mode, signaling, deliveries = []) {
654
751
  }
655
752
 
656
753
  // src/internal/telemetry.ts
657
- var SDK_VERSION = "web/0.4.8";
754
+ var SDK_VERSION = true ? `web/${"0.7.0"}` : "web/dev";
658
755
  var FLUSH_INTERVAL_MS = 15e3;
659
756
  var MAX_BATCH = 64;
660
757
  function describeDevice() {
@@ -1062,7 +1159,12 @@ var MebiusPlayer = class extends TypedEmitter {
1062
1159
  this.freeze = new FreezeClock();
1063
1160
  /** Cancels element listeners bound for the lifetime of one play(). */
1064
1161
  this.elementListeners = null;
1065
- this.candidates = createViewCandidates(options.mode ?? "auto", signaling, deliveries);
1162
+ this.candidates = createViewCandidates(
1163
+ options.mode ?? "auto",
1164
+ signaling,
1165
+ deliveries,
1166
+ options.targetLatencyMs
1167
+ );
1066
1168
  }
1067
1169
  /** Start playing `streamId` into the given video element or selector. */
1068
1170
  async play(streamId, viewTarget) {
@@ -1159,13 +1261,15 @@ var MebiusPlayer = class extends TypedEmitter {
1159
1261
  }
1160
1262
  /**
1161
1263
  * Wall-clock time (Unix ms) currently on screen, or `null` when the active
1162
- * route cannot produce one (HTTP-FLV, WHEP see {@link ViewTransport}).
1264
+ * route cannot produce one. A real-time route carries no wall clock at all,
1265
+ * and a segmented route has none until its first timestamped segment arrives
1266
+ * (see {@link ViewTransport}).
1163
1267
  *
1164
1268
  * This is what {@link MebiusClient.createCaptions} compares against a
1165
1269
  * segment's `epochMs` to know when it is due. Delegating to the transport
1166
1270
  * rather than reading the element directly is what keeps this correct across
1167
- * a route failover: the player may switch from HLS to FLV mid-session, and
1168
- * the clock source has to follow.
1271
+ * a route failover: the player may change route mid-session, and the clock
1272
+ * source has to follow.
1169
1273
  */
1170
1274
  currentEpochMs() {
1171
1275
  return this.transport?.playheadEpochMs?.() ?? null;
@@ -1189,9 +1293,11 @@ var MebiusPlayer = class extends TypedEmitter {
1189
1293
  startStats() {
1190
1294
  this.statsTimer = setInterval(async () => {
1191
1295
  const stats = await this.transport?.getStats();
1192
- const freezeMs = this.freeze.take();
1296
+ const elementFreezeMs = this.freeze.take();
1193
1297
  if (!stats) {
1194
- if (freezeMs > 0) this.reporter?.add({ ts: Math.floor(Date.now() / 1e3), freezeMs });
1298
+ if (elementFreezeMs > 0) {
1299
+ this.reporter?.add({ ts: Math.floor(Date.now() / 1e3), freezeMs: elementFreezeMs });
1300
+ }
1195
1301
  return;
1196
1302
  }
1197
1303
  this.emit("stats", stats);
@@ -1199,7 +1305,9 @@ var MebiusPlayer = class extends TypedEmitter {
1199
1305
  ts: Math.floor(Date.now() / 1e3),
1200
1306
  bitrateKbps: stats.bitrateKbps,
1201
1307
  fps: stats.framesPerSecond,
1202
- freezeMs
1308
+ rttMs: stats.rttMs,
1309
+ packetLossPct: stats.packetLossPct,
1310
+ freezeMs: elementFreezeMs + (stats.freezeMs ?? 0)
1203
1311
  });
1204
1312
  }, STATS_INTERVAL_MS2);
1205
1313
  }
@@ -1230,6 +1338,21 @@ var SignalingClient = class {
1230
1338
  this.gateway = gateway;
1231
1339
  this.token = token;
1232
1340
  }
1341
+ /**
1342
+ * Swap in a freshly-minted access token.
1343
+ *
1344
+ * Every URL this class builds is built at call time, so a session that starts
1345
+ * a new request after this point uses the new token with no further wiring.
1346
+ * What it does NOT reach is a request already in flight or a media URL another
1347
+ * library has memorised — see the scale route's loader for that half.
1348
+ */
1349
+ setToken(token) {
1350
+ this.token = token;
1351
+ }
1352
+ /** The current access token, for transports that must re-stamp their own URLs. */
1353
+ accessToken() {
1354
+ return this.token;
1355
+ }
1233
1356
  base() {
1234
1357
  return this.gateway.replace(/\/+$/, "");
1235
1358
  }
@@ -1350,16 +1473,21 @@ function readToken(token) {
1350
1473
  }
1351
1474
 
1352
1475
  // src/client.ts
1476
+ var REFRESH_MARGIN_MS = 6e4;
1477
+ var REFRESH_RETRY_BASE_MS = 2e3;
1478
+ var REFRESH_RETRY_MAX_MS = 3e4;
1353
1479
  var MebiusClient = class extends TypedEmitter {
1354
1480
  /** @internal */
1355
- constructor(config2, token, deliveries = [], telemetry = null, userId) {
1481
+ constructor(config2, token, deliveries = [], telemetry = null, userId, getToken) {
1356
1482
  super();
1357
1483
  this.token = token;
1358
1484
  this.deliveries = deliveries;
1359
1485
  this.telemetry = telemetry;
1360
1486
  this.userId = userId;
1487
+ this.getToken = getToken;
1361
1488
  this.expiryTimer = null;
1362
1489
  this.connected = false;
1490
+ this.refreshFailures = 0;
1363
1491
  this.signaling = new SignalingClient(config2.gateway, token);
1364
1492
  }
1365
1493
  /** @internal Called by {@link Mebius.connect}. */
@@ -1371,13 +1499,82 @@ var MebiusClient = class extends TypedEmitter {
1371
1499
  return;
1372
1500
  }
1373
1501
  this.connected = true;
1374
- if (expiresAtMs !== null) {
1502
+ this.scheduleTokenWork(expiresAtMs);
1503
+ queueMicrotask(() => this.emit("connected", void 0));
1504
+ }
1505
+ /**
1506
+ * Arm whatever has to happen as this token approaches its expiry: renew it if
1507
+ * the app gave us a way to, otherwise report that the session is over.
1508
+ *
1509
+ * Renewing is what makes an unattended session possible at all. The gateway
1510
+ * checks the token on every media request, so without a fresh one playback
1511
+ * stops the moment it expires — no matter how healthy the stream is.
1512
+ */
1513
+ scheduleTokenWork(expiresAtMs) {
1514
+ this.clearTimer();
1515
+ if (expiresAtMs === null) return;
1516
+ const remaining = expiresAtMs - Date.now();
1517
+ if (!this.getToken) {
1375
1518
  this.expiryTimer = setTimeout(
1376
1519
  () => this.emit("error", mebiusError("TOKEN_EXPIRED")),
1377
- Math.max(0, expiresAtMs - now)
1520
+ Math.max(0, remaining)
1378
1521
  );
1522
+ return;
1379
1523
  }
1380
- queueMicrotask(() => this.emit("connected", void 0));
1524
+ this.expiryTimer = setTimeout(
1525
+ () => void this.refreshToken(expiresAtMs),
1526
+ Math.max(0, remaining - REFRESH_MARGIN_MS)
1527
+ );
1528
+ }
1529
+ async refreshToken(previousExpiryMs) {
1530
+ if (!this.connected || !this.getToken) return;
1531
+ let next;
1532
+ try {
1533
+ next = await this.getToken();
1534
+ } catch (cause) {
1535
+ this.onRefreshFailed(previousExpiryMs, cause);
1536
+ return;
1537
+ }
1538
+ if (!this.connected) return;
1539
+ const { expiresAtMs } = readToken(next);
1540
+ if (expiresAtMs !== null && expiresAtMs <= previousExpiryMs) {
1541
+ this.emit(
1542
+ "error",
1543
+ mebiusError("TOKEN_EXPIRED", "Mebius token refresh returned a token that is not newer.")
1544
+ );
1545
+ return;
1546
+ }
1547
+ this.refreshFailures = 0;
1548
+ this.token = next;
1549
+ this.signaling.setToken(next);
1550
+ this.emit("token-refreshed", void 0);
1551
+ this.scheduleTokenWork(expiresAtMs);
1552
+ }
1553
+ /**
1554
+ * A failed refresh is not a dead session: the current token is still valid
1555
+ * until `expiryMs`, and the viewer is still watching. Retry inside that window
1556
+ * and only report expiry once it has actually run out.
1557
+ */
1558
+ onRefreshFailed(expiryMs, cause) {
1559
+ const remaining = expiryMs - Date.now();
1560
+ if (remaining <= 0) {
1561
+ this.emit("error", mebiusError("TOKEN_EXPIRED", void 0, cause));
1562
+ return;
1563
+ }
1564
+ this.refreshFailures += 1;
1565
+ const backoff = Math.min(
1566
+ REFRESH_RETRY_MAX_MS,
1567
+ REFRESH_RETRY_BASE_MS * 2 ** (this.refreshFailures - 1)
1568
+ );
1569
+ this.clearTimer();
1570
+ this.expiryTimer = setTimeout(
1571
+ () => void this.refreshToken(expiryMs),
1572
+ Math.min(backoff, remaining)
1573
+ );
1574
+ }
1575
+ clearTimer() {
1576
+ if (this.expiryTimer) clearTimeout(this.expiryTimer);
1577
+ this.expiryTimer = null;
1381
1578
  }
1382
1579
  /** Create a broadcaster bound to this connection. */
1383
1580
  createBroadcaster(options = {}) {
@@ -1422,8 +1619,7 @@ var MebiusClient = class extends TypedEmitter {
1422
1619
  }
1423
1620
  /** Close the connection and release resources. */
1424
1621
  disconnect(reason) {
1425
- if (this.expiryTimer) clearTimeout(this.expiryTimer);
1426
- this.expiryTimer = null;
1622
+ this.clearTimer();
1427
1623
  this.connected = false;
1428
1624
  this.emit("disconnected", { reason });
1429
1625
  this.removeAllListeners();
@@ -1452,7 +1648,14 @@ var Mebius = {
1452
1648
  }
1453
1649
  if (!options.token) throw mebiusError("UNKNOWN", "Mebius.connect requires a token.");
1454
1650
  const telemetry = options.beaconToken && options.beaconUrl ? { token: options.beaconToken, url: options.beaconUrl } : null;
1455
- const client = new MebiusClient(config, options.token, options.deliveries ?? [], telemetry, options.userId);
1651
+ const client = new MebiusClient(
1652
+ config,
1653
+ options.token,
1654
+ options.deliveries ?? [],
1655
+ telemetry,
1656
+ options.userId,
1657
+ options.getToken
1658
+ );
1456
1659
  client.open();
1457
1660
  return client;
1458
1661
  },