@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 +244 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +100 -7
- package/dist/index.d.ts +100 -7
- package/dist/index.global.js +244 -41
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +244 -41
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
332
|
-
let
|
|
333
|
-
let
|
|
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
|
-
|
|
337
|
-
if (typeof
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
}
|
|
373
|
+
const p = stat;
|
|
374
|
+
if (typeof p.currentRoundTripTime === "number") rttMs = Math.round(p.currentRoundTripTime * 1e3);
|
|
343
375
|
}
|
|
344
376
|
});
|
|
345
|
-
|
|
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
|
|
472
|
-
var
|
|
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
|
|
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
|
-
|
|
495
|
-
|
|
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", () =>
|
|
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
|
-
|
|
637
|
-
if (kind ===
|
|
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(
|
|
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 [
|
|
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 = "
|
|
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(
|
|
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
|
|
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
|
|
1168
|
-
*
|
|
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
|
|
1296
|
+
const elementFreezeMs = this.freeze.take();
|
|
1193
1297
|
if (!stats) {
|
|
1194
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
1520
|
+
Math.max(0, remaining)
|
|
1378
1521
|
);
|
|
1522
|
+
return;
|
|
1379
1523
|
}
|
|
1380
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
},
|