@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.js
CHANGED
|
@@ -222,10 +222,21 @@ function resetVideoElement(video) {
|
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
// src/internal/ll-view-transport.ts
|
|
225
|
+
var DEFAULT_REALTIME_TARGET_MS = 300;
|
|
226
|
+
function holdBuffer(receiver, targetMs) {
|
|
227
|
+
const r = receiver;
|
|
228
|
+
try {
|
|
229
|
+
if ("jitterBufferTarget" in r) r.jitterBufferTarget = targetMs;
|
|
230
|
+
if ("playoutDelayHint" in r) r.playoutDelayHint = targetMs / 1e3;
|
|
231
|
+
} catch {
|
|
232
|
+
}
|
|
233
|
+
}
|
|
225
234
|
var WhepViewTransport = class {
|
|
226
|
-
constructor(signaling) {
|
|
235
|
+
constructor(signaling, targetLatencyMs = DEFAULT_REALTIME_TARGET_MS) {
|
|
227
236
|
this.signaling = signaling;
|
|
237
|
+
this.targetLatencyMs = targetLatencyMs;
|
|
228
238
|
this.kind = "whep";
|
|
239
|
+
this.cursor = null;
|
|
229
240
|
this.pc = null;
|
|
230
241
|
this.resourceUrl = null;
|
|
231
242
|
this.endedCb = null;
|
|
@@ -249,6 +260,7 @@ var WhepViewTransport = class {
|
|
|
249
260
|
pc.addTransceiver("video", { direction: "recvonly" });
|
|
250
261
|
pc.addTransceiver("audio", { direction: "recvonly" });
|
|
251
262
|
pc.ontrack = (ev) => {
|
|
263
|
+
holdBuffer(ev.receiver, this.targetLatencyMs);
|
|
252
264
|
remote.addTrack(ev.track);
|
|
253
265
|
video.srcObject = remote;
|
|
254
266
|
void playWithAutoplayFallback(video).then((o) => {
|
|
@@ -276,6 +288,7 @@ var WhepViewTransport = class {
|
|
|
276
288
|
await pc.setRemoteDescription({ type: "answer", sdp: answer });
|
|
277
289
|
}
|
|
278
290
|
async stop() {
|
|
291
|
+
this.cursor = null;
|
|
279
292
|
await this.signaling.deleteResource(this.resourceUrl);
|
|
280
293
|
this.resourceUrl = null;
|
|
281
294
|
this.pc?.close();
|
|
@@ -283,24 +296,59 @@ var WhepViewTransport = class {
|
|
|
283
296
|
if (this.video) resetVideoElement(this.video);
|
|
284
297
|
this.video = null;
|
|
285
298
|
}
|
|
299
|
+
/**
|
|
300
|
+
* Read what the connection actually did since the last reading.
|
|
301
|
+
*
|
|
302
|
+
* This route needs to measure its own freezes, because nothing else can. A
|
|
303
|
+
* frozen real-time picture still reports a healthy connection and raises no
|
|
304
|
+
* event on the video element — so from the outside the session looks flawless
|
|
305
|
+
* while the viewer stares at a still frame. Every freeze on this route used to
|
|
306
|
+
* be recorded as zero, which is why the problem could be felt and never seen.
|
|
307
|
+
*/
|
|
286
308
|
async getStats() {
|
|
287
309
|
if (!this.pc) return null;
|
|
288
310
|
const report = await this.pc.getStats();
|
|
289
|
-
let
|
|
290
|
-
let
|
|
291
|
-
let
|
|
311
|
+
let framesPerSecond;
|
|
312
|
+
let freezeS = 0;
|
|
313
|
+
let packetsLost = 0;
|
|
314
|
+
let packetsReceived = 0;
|
|
315
|
+
let bytesReceived = 0;
|
|
316
|
+
let bufferDelayS = 0;
|
|
317
|
+
let bufferEmitted = 0;
|
|
318
|
+
let rttMs;
|
|
292
319
|
report.forEach((stat) => {
|
|
293
|
-
if (stat.type === "inbound-rtp") {
|
|
294
|
-
|
|
295
|
-
if (typeof
|
|
320
|
+
if (stat.type === "inbound-rtp" && stat.kind === "video") {
|
|
321
|
+
const v = stat;
|
|
322
|
+
if (typeof v.framesPerSecond === "number") framesPerSecond = v.framesPerSecond;
|
|
323
|
+
if (typeof v.totalFreezesDuration === "number") freezeS = v.totalFreezesDuration;
|
|
324
|
+
if (typeof v.packetsLost === "number") packetsLost = v.packetsLost;
|
|
325
|
+
if (typeof v.packetsReceived === "number") packetsReceived = v.packetsReceived;
|
|
326
|
+
if (typeof v.bytesReceived === "number") bytesReceived = v.bytesReceived;
|
|
327
|
+
if (typeof v.jitterBufferDelay === "number") bufferDelayS = v.jitterBufferDelay;
|
|
328
|
+
if (typeof v.jitterBufferEmittedCount === "number") bufferEmitted = v.jitterBufferEmittedCount;
|
|
296
329
|
}
|
|
297
330
|
if (stat.type === "candidate-pair" && stat.state === "succeeded") {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
331
|
+
const p = stat;
|
|
332
|
+
if (typeof p.currentRoundTripTime === "number") rttMs = Math.round(p.currentRoundTripTime * 1e3);
|
|
301
333
|
}
|
|
302
334
|
});
|
|
303
|
-
|
|
335
|
+
const atMs = Date.now();
|
|
336
|
+
const previous = this.cursor;
|
|
337
|
+
this.cursor = { atMs, freezeS, packetsLost, packetsReceived, bytesReceived, bufferDelayS, bufferEmitted };
|
|
338
|
+
if (!previous) return { framesPerSecond, rttMs };
|
|
339
|
+
const elapsedS = Math.max(1e-3, (atMs - previous.atMs) / 1e3);
|
|
340
|
+
const deltaLost = Math.max(0, packetsLost - previous.packetsLost);
|
|
341
|
+
const deltaReceived = Math.max(0, packetsReceived - previous.packetsReceived);
|
|
342
|
+
const deltaEmitted = bufferEmitted - previous.bufferEmitted;
|
|
343
|
+
const heldMs = deltaEmitted > 0 ? (bufferDelayS - previous.bufferDelayS) / deltaEmitted * 1e3 : void 0;
|
|
344
|
+
return {
|
|
345
|
+
bitrateKbps: Math.round((bytesReceived - previous.bytesReceived) * 8 / elapsedS / 1e3),
|
|
346
|
+
framesPerSecond,
|
|
347
|
+
latencyMs: heldMs === void 0 ? void 0 : Math.round(heldMs + (rttMs !== void 0 ? rttMs / 2 : 0)),
|
|
348
|
+
rttMs,
|
|
349
|
+
packetLossPct: deltaLost + deltaReceived > 0 ? Number((deltaLost / (deltaLost + deltaReceived) * 100).toFixed(2)) : 0,
|
|
350
|
+
freezeMs: Math.max(0, Math.round((freezeS - previous.freezeS) * 1e3))
|
|
351
|
+
};
|
|
304
352
|
}
|
|
305
353
|
};
|
|
306
354
|
|
|
@@ -308,6 +356,19 @@ var WhepViewTransport = class {
|
|
|
308
356
|
function retryWarmupNotFound(cfg, retryCount, res, retry) {
|
|
309
357
|
return retry || retryCount < (cfg?.maxNumRetry ?? 0) && res?.code === 404;
|
|
310
358
|
}
|
|
359
|
+
var TOKEN_PARAM = /([?&]token=)[^&#]*/;
|
|
360
|
+
function withCurrentToken(url, token) {
|
|
361
|
+
return url.replace(TOKEN_PARAM, (_match, prefix) => prefix + encodeURIComponent(token));
|
|
362
|
+
}
|
|
363
|
+
function tokenRestampingLoader(Hls, currentToken) {
|
|
364
|
+
const Base = Hls.DefaultConfig.loader;
|
|
365
|
+
return class TokenRestampingLoader extends Base {
|
|
366
|
+
load(context, config2, callbacks) {
|
|
367
|
+
context.url = withCurrentToken(context.url, currentToken());
|
|
368
|
+
super.load(context, config2, callbacks);
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
}
|
|
311
372
|
var HlsViewTransport = class {
|
|
312
373
|
/**
|
|
313
374
|
* deliveryPath, when given, is a gateway-relative path from the gateway's own
|
|
@@ -315,9 +376,10 @@ var HlsViewTransport = class {
|
|
|
315
376
|
* origin one. Without it this falls back to the origin playlist, which is
|
|
316
377
|
* still correct, just served from our own bandwidth.
|
|
317
378
|
*/
|
|
318
|
-
constructor(signaling, deliveryPath) {
|
|
379
|
+
constructor(signaling, deliveryPath, targetS) {
|
|
319
380
|
this.signaling = signaling;
|
|
320
381
|
this.deliveryPath = deliveryPath;
|
|
382
|
+
this.targetS = targetS;
|
|
321
383
|
this.kind = "hls";
|
|
322
384
|
this.hls = null;
|
|
323
385
|
this.video = null;
|
|
@@ -357,6 +419,11 @@ var HlsViewTransport = class {
|
|
|
357
419
|
}
|
|
358
420
|
const hls = new Hls({
|
|
359
421
|
maxLiveSyncPlaybackRate: 1.1,
|
|
422
|
+
loader: tokenRestampingLoader(Hls, () => this.signaling.accessToken()),
|
|
423
|
+
// Only when the app actually asked. Left alone, the library follows the
|
|
424
|
+
// playlist's own HOLD-BACK, which the server measured from the segments it
|
|
425
|
+
// is producing — a number guessed here would only override a real one.
|
|
426
|
+
...this.targetS === void 0 ? {} : { liveSyncDuration: this.targetS },
|
|
360
427
|
manifestLoadPolicy: {
|
|
361
428
|
default: {
|
|
362
429
|
maxTimeToFirstByteMs: 1e4,
|
|
@@ -426,8 +493,12 @@ var LIVE_FLV_CONFIG = {
|
|
|
426
493
|
autoCleanupMinBackwardDuration: 10,
|
|
427
494
|
reuseRedirectedURL: true
|
|
428
495
|
};
|
|
429
|
-
var
|
|
430
|
-
var
|
|
496
|
+
var DEFAULT_BALANCED_TARGET_S = 2;
|
|
497
|
+
var CATCH_UP_RATE = 1.05;
|
|
498
|
+
var BUILD_UP_RATE = 0.98;
|
|
499
|
+
var SEEK_AT = 4;
|
|
500
|
+
var CATCH_UP_ABOVE = 1.5;
|
|
501
|
+
var BUILD_UP_BELOW = 0.9;
|
|
431
502
|
var AUDIO_RETRY_MS = 2500;
|
|
432
503
|
var UPSTREAM_LATENCY_MS = 800;
|
|
433
504
|
function stalledWithData(video, ms) {
|
|
@@ -445,17 +516,31 @@ function stalledWithData(video, ms) {
|
|
|
445
516
|
video.addEventListener("timeupdate", onTime);
|
|
446
517
|
});
|
|
447
518
|
}
|
|
448
|
-
function
|
|
519
|
+
function syncLiveEdge(video, targetS = DEFAULT_BALANCED_TARGET_S) {
|
|
449
520
|
const ranges = video.buffered;
|
|
450
521
|
if (ranges.length === 0) return;
|
|
451
522
|
const edge = ranges.end(ranges.length - 1);
|
|
452
|
-
|
|
453
|
-
|
|
523
|
+
const drift = edge - video.currentTime;
|
|
524
|
+
if (drift > targetS * SEEK_AT) {
|
|
525
|
+
video.currentTime = edge - targetS;
|
|
526
|
+
video.playbackRate = 1;
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
if (drift > targetS * CATCH_UP_ABOVE) {
|
|
530
|
+
video.playbackRate = CATCH_UP_RATE;
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
if (drift < targetS * BUILD_UP_BELOW) {
|
|
534
|
+
video.playbackRate = BUILD_UP_RATE;
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
video.playbackRate = 1;
|
|
454
538
|
}
|
|
455
539
|
var FlvViewTransport = class {
|
|
456
|
-
constructor(signaling, deliveryPath) {
|
|
540
|
+
constructor(signaling, deliveryPath, targetS = DEFAULT_BALANCED_TARGET_S) {
|
|
457
541
|
this.signaling = signaling;
|
|
458
542
|
this.deliveryPath = deliveryPath;
|
|
543
|
+
this.targetS = targetS;
|
|
459
544
|
this.kind = "flv_js";
|
|
460
545
|
this.player = null;
|
|
461
546
|
this.video = null;
|
|
@@ -481,7 +566,7 @@ var FlvViewTransport = class {
|
|
|
481
566
|
const { signal } = this.listeners;
|
|
482
567
|
video.addEventListener("ended", () => this.endedCb?.(), { signal });
|
|
483
568
|
video.addEventListener("waiting", () => this.bufferingCb?.(), { signal });
|
|
484
|
-
video.addEventListener("timeupdate", () =>
|
|
569
|
+
video.addEventListener("timeupdate", () => syncLiveEdge(video, this.targetS), { signal });
|
|
485
570
|
let mod;
|
|
486
571
|
try {
|
|
487
572
|
mod = await import("flv.js");
|
|
@@ -525,6 +610,7 @@ var FlvViewTransport = class {
|
|
|
525
610
|
this.listeners = null;
|
|
526
611
|
this.teardownPlayer();
|
|
527
612
|
if (this.video) {
|
|
613
|
+
this.video.playbackRate = 1;
|
|
528
614
|
this.video.removeAttribute("src");
|
|
529
615
|
this.video.load();
|
|
530
616
|
}
|
|
@@ -590,18 +676,29 @@ var KIND_LOCAL = "local";
|
|
|
590
676
|
function canPlayBuffered() {
|
|
591
677
|
return typeof MediaSource !== "undefined";
|
|
592
678
|
}
|
|
593
|
-
function transportFor(kind, path, signaling) {
|
|
594
|
-
|
|
595
|
-
if (kind ===
|
|
679
|
+
function transportFor(kind, path, signaling, targetLatencyMs) {
|
|
680
|
+
const targetS = targetLatencyMs === void 0 ? void 0 : targetLatencyMs / 1e3;
|
|
681
|
+
if (kind === KIND_FAST)
|
|
682
|
+
return canPlayBuffered() ? new FlvViewTransport(signaling, path, targetS) : null;
|
|
683
|
+
if (kind === KIND_WIDE || kind === KIND_LOCAL)
|
|
684
|
+
return new HlsViewTransport(signaling, path, targetS);
|
|
596
685
|
return null;
|
|
597
686
|
}
|
|
598
|
-
function createViewCandidates(mode, signaling, deliveries = []) {
|
|
599
|
-
const fromGateway = (kinds) => deliveries.filter((d) => kinds.includes(d.kind)).map((d) => transportFor(d.kind, d.path, signaling)).filter((t) => t !== null);
|
|
600
|
-
const originFallback = new HlsViewTransport(
|
|
687
|
+
function createViewCandidates(mode, signaling, deliveries = [], targetLatencyMs) {
|
|
688
|
+
const fromGateway = (kinds) => deliveries.filter((d) => kinds.includes(d.kind)).map((d) => transportFor(d.kind, d.path, signaling, targetLatencyMs)).filter((t) => t !== null);
|
|
689
|
+
const originFallback = new HlsViewTransport(
|
|
690
|
+
signaling,
|
|
691
|
+
void 0,
|
|
692
|
+
targetLatencyMs === void 0 ? void 0 : targetLatencyMs / 1e3
|
|
693
|
+
);
|
|
601
694
|
const allKinds = [KIND_FAST, KIND_WIDE, KIND_LOCAL];
|
|
602
695
|
switch (mode) {
|
|
603
696
|
case "low-latency":
|
|
604
|
-
return [
|
|
697
|
+
return [
|
|
698
|
+
new WhepViewTransport(signaling, targetLatencyMs ?? DEFAULT_REALTIME_TARGET_MS),
|
|
699
|
+
...fromGateway(allKinds),
|
|
700
|
+
originFallback
|
|
701
|
+
];
|
|
605
702
|
case "balanced":
|
|
606
703
|
return [...fromGateway(allKinds), originFallback];
|
|
607
704
|
case "scale":
|
|
@@ -612,7 +709,7 @@ function createViewCandidates(mode, signaling, deliveries = []) {
|
|
|
612
709
|
}
|
|
613
710
|
|
|
614
711
|
// src/internal/telemetry.ts
|
|
615
|
-
var SDK_VERSION = "
|
|
712
|
+
var SDK_VERSION = true ? `web/${"0.7.0"}` : "web/dev";
|
|
616
713
|
var FLUSH_INTERVAL_MS = 15e3;
|
|
617
714
|
var MAX_BATCH = 64;
|
|
618
715
|
function describeDevice() {
|
|
@@ -1020,7 +1117,12 @@ var MebiusPlayer = class extends TypedEmitter {
|
|
|
1020
1117
|
this.freeze = new FreezeClock();
|
|
1021
1118
|
/** Cancels element listeners bound for the lifetime of one play(). */
|
|
1022
1119
|
this.elementListeners = null;
|
|
1023
|
-
this.candidates = createViewCandidates(
|
|
1120
|
+
this.candidates = createViewCandidates(
|
|
1121
|
+
options.mode ?? "auto",
|
|
1122
|
+
signaling,
|
|
1123
|
+
deliveries,
|
|
1124
|
+
options.targetLatencyMs
|
|
1125
|
+
);
|
|
1024
1126
|
}
|
|
1025
1127
|
/** Start playing `streamId` into the given video element or selector. */
|
|
1026
1128
|
async play(streamId, viewTarget) {
|
|
@@ -1117,13 +1219,15 @@ var MebiusPlayer = class extends TypedEmitter {
|
|
|
1117
1219
|
}
|
|
1118
1220
|
/**
|
|
1119
1221
|
* Wall-clock time (Unix ms) currently on screen, or `null` when the active
|
|
1120
|
-
* route cannot produce one
|
|
1222
|
+
* route cannot produce one. A real-time route carries no wall clock at all,
|
|
1223
|
+
* and a segmented route has none until its first timestamped segment arrives
|
|
1224
|
+
* (see {@link ViewTransport}).
|
|
1121
1225
|
*
|
|
1122
1226
|
* This is what {@link MebiusClient.createCaptions} compares against a
|
|
1123
1227
|
* segment's `epochMs` to know when it is due. Delegating to the transport
|
|
1124
1228
|
* rather than reading the element directly is what keeps this correct across
|
|
1125
|
-
* a route failover: the player may
|
|
1126
|
-
*
|
|
1229
|
+
* a route failover: the player may change route mid-session, and the clock
|
|
1230
|
+
* source has to follow.
|
|
1127
1231
|
*/
|
|
1128
1232
|
currentEpochMs() {
|
|
1129
1233
|
return this.transport?.playheadEpochMs?.() ?? null;
|
|
@@ -1147,9 +1251,11 @@ var MebiusPlayer = class extends TypedEmitter {
|
|
|
1147
1251
|
startStats() {
|
|
1148
1252
|
this.statsTimer = setInterval(async () => {
|
|
1149
1253
|
const stats = await this.transport?.getStats();
|
|
1150
|
-
const
|
|
1254
|
+
const elementFreezeMs = this.freeze.take();
|
|
1151
1255
|
if (!stats) {
|
|
1152
|
-
if (
|
|
1256
|
+
if (elementFreezeMs > 0) {
|
|
1257
|
+
this.reporter?.add({ ts: Math.floor(Date.now() / 1e3), freezeMs: elementFreezeMs });
|
|
1258
|
+
}
|
|
1153
1259
|
return;
|
|
1154
1260
|
}
|
|
1155
1261
|
this.emit("stats", stats);
|
|
@@ -1157,7 +1263,9 @@ var MebiusPlayer = class extends TypedEmitter {
|
|
|
1157
1263
|
ts: Math.floor(Date.now() / 1e3),
|
|
1158
1264
|
bitrateKbps: stats.bitrateKbps,
|
|
1159
1265
|
fps: stats.framesPerSecond,
|
|
1160
|
-
|
|
1266
|
+
rttMs: stats.rttMs,
|
|
1267
|
+
packetLossPct: stats.packetLossPct,
|
|
1268
|
+
freezeMs: elementFreezeMs + (stats.freezeMs ?? 0)
|
|
1161
1269
|
});
|
|
1162
1270
|
}, STATS_INTERVAL_MS2);
|
|
1163
1271
|
}
|
|
@@ -1188,6 +1296,21 @@ var SignalingClient = class {
|
|
|
1188
1296
|
this.gateway = gateway;
|
|
1189
1297
|
this.token = token;
|
|
1190
1298
|
}
|
|
1299
|
+
/**
|
|
1300
|
+
* Swap in a freshly-minted access token.
|
|
1301
|
+
*
|
|
1302
|
+
* Every URL this class builds is built at call time, so a session that starts
|
|
1303
|
+
* a new request after this point uses the new token with no further wiring.
|
|
1304
|
+
* What it does NOT reach is a request already in flight or a media URL another
|
|
1305
|
+
* library has memorised — see the scale route's loader for that half.
|
|
1306
|
+
*/
|
|
1307
|
+
setToken(token) {
|
|
1308
|
+
this.token = token;
|
|
1309
|
+
}
|
|
1310
|
+
/** The current access token, for transports that must re-stamp their own URLs. */
|
|
1311
|
+
accessToken() {
|
|
1312
|
+
return this.token;
|
|
1313
|
+
}
|
|
1191
1314
|
base() {
|
|
1192
1315
|
return this.gateway.replace(/\/+$/, "");
|
|
1193
1316
|
}
|
|
@@ -1308,16 +1431,21 @@ function readToken(token) {
|
|
|
1308
1431
|
}
|
|
1309
1432
|
|
|
1310
1433
|
// src/client.ts
|
|
1434
|
+
var REFRESH_MARGIN_MS = 6e4;
|
|
1435
|
+
var REFRESH_RETRY_BASE_MS = 2e3;
|
|
1436
|
+
var REFRESH_RETRY_MAX_MS = 3e4;
|
|
1311
1437
|
var MebiusClient = class extends TypedEmitter {
|
|
1312
1438
|
/** @internal */
|
|
1313
|
-
constructor(config2, token, deliveries = [], telemetry = null, userId) {
|
|
1439
|
+
constructor(config2, token, deliveries = [], telemetry = null, userId, getToken) {
|
|
1314
1440
|
super();
|
|
1315
1441
|
this.token = token;
|
|
1316
1442
|
this.deliveries = deliveries;
|
|
1317
1443
|
this.telemetry = telemetry;
|
|
1318
1444
|
this.userId = userId;
|
|
1445
|
+
this.getToken = getToken;
|
|
1319
1446
|
this.expiryTimer = null;
|
|
1320
1447
|
this.connected = false;
|
|
1448
|
+
this.refreshFailures = 0;
|
|
1321
1449
|
this.signaling = new SignalingClient(config2.gateway, token);
|
|
1322
1450
|
}
|
|
1323
1451
|
/** @internal Called by {@link Mebius.connect}. */
|
|
@@ -1329,13 +1457,82 @@ var MebiusClient = class extends TypedEmitter {
|
|
|
1329
1457
|
return;
|
|
1330
1458
|
}
|
|
1331
1459
|
this.connected = true;
|
|
1332
|
-
|
|
1460
|
+
this.scheduleTokenWork(expiresAtMs);
|
|
1461
|
+
queueMicrotask(() => this.emit("connected", void 0));
|
|
1462
|
+
}
|
|
1463
|
+
/**
|
|
1464
|
+
* Arm whatever has to happen as this token approaches its expiry: renew it if
|
|
1465
|
+
* the app gave us a way to, otherwise report that the session is over.
|
|
1466
|
+
*
|
|
1467
|
+
* Renewing is what makes an unattended session possible at all. The gateway
|
|
1468
|
+
* checks the token on every media request, so without a fresh one playback
|
|
1469
|
+
* stops the moment it expires — no matter how healthy the stream is.
|
|
1470
|
+
*/
|
|
1471
|
+
scheduleTokenWork(expiresAtMs) {
|
|
1472
|
+
this.clearTimer();
|
|
1473
|
+
if (expiresAtMs === null) return;
|
|
1474
|
+
const remaining = expiresAtMs - Date.now();
|
|
1475
|
+
if (!this.getToken) {
|
|
1333
1476
|
this.expiryTimer = setTimeout(
|
|
1334
1477
|
() => this.emit("error", mebiusError("TOKEN_EXPIRED")),
|
|
1335
|
-
Math.max(0,
|
|
1478
|
+
Math.max(0, remaining)
|
|
1336
1479
|
);
|
|
1480
|
+
return;
|
|
1337
1481
|
}
|
|
1338
|
-
|
|
1482
|
+
this.expiryTimer = setTimeout(
|
|
1483
|
+
() => void this.refreshToken(expiresAtMs),
|
|
1484
|
+
Math.max(0, remaining - REFRESH_MARGIN_MS)
|
|
1485
|
+
);
|
|
1486
|
+
}
|
|
1487
|
+
async refreshToken(previousExpiryMs) {
|
|
1488
|
+
if (!this.connected || !this.getToken) return;
|
|
1489
|
+
let next;
|
|
1490
|
+
try {
|
|
1491
|
+
next = await this.getToken();
|
|
1492
|
+
} catch (cause) {
|
|
1493
|
+
this.onRefreshFailed(previousExpiryMs, cause);
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
if (!this.connected) return;
|
|
1497
|
+
const { expiresAtMs } = readToken(next);
|
|
1498
|
+
if (expiresAtMs !== null && expiresAtMs <= previousExpiryMs) {
|
|
1499
|
+
this.emit(
|
|
1500
|
+
"error",
|
|
1501
|
+
mebiusError("TOKEN_EXPIRED", "Mebius token refresh returned a token that is not newer.")
|
|
1502
|
+
);
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1505
|
+
this.refreshFailures = 0;
|
|
1506
|
+
this.token = next;
|
|
1507
|
+
this.signaling.setToken(next);
|
|
1508
|
+
this.emit("token-refreshed", void 0);
|
|
1509
|
+
this.scheduleTokenWork(expiresAtMs);
|
|
1510
|
+
}
|
|
1511
|
+
/**
|
|
1512
|
+
* A failed refresh is not a dead session: the current token is still valid
|
|
1513
|
+
* until `expiryMs`, and the viewer is still watching. Retry inside that window
|
|
1514
|
+
* and only report expiry once it has actually run out.
|
|
1515
|
+
*/
|
|
1516
|
+
onRefreshFailed(expiryMs, cause) {
|
|
1517
|
+
const remaining = expiryMs - Date.now();
|
|
1518
|
+
if (remaining <= 0) {
|
|
1519
|
+
this.emit("error", mebiusError("TOKEN_EXPIRED", void 0, cause));
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
this.refreshFailures += 1;
|
|
1523
|
+
const backoff = Math.min(
|
|
1524
|
+
REFRESH_RETRY_MAX_MS,
|
|
1525
|
+
REFRESH_RETRY_BASE_MS * 2 ** (this.refreshFailures - 1)
|
|
1526
|
+
);
|
|
1527
|
+
this.clearTimer();
|
|
1528
|
+
this.expiryTimer = setTimeout(
|
|
1529
|
+
() => void this.refreshToken(expiryMs),
|
|
1530
|
+
Math.min(backoff, remaining)
|
|
1531
|
+
);
|
|
1532
|
+
}
|
|
1533
|
+
clearTimer() {
|
|
1534
|
+
if (this.expiryTimer) clearTimeout(this.expiryTimer);
|
|
1535
|
+
this.expiryTimer = null;
|
|
1339
1536
|
}
|
|
1340
1537
|
/** Create a broadcaster bound to this connection. */
|
|
1341
1538
|
createBroadcaster(options = {}) {
|
|
@@ -1380,8 +1577,7 @@ var MebiusClient = class extends TypedEmitter {
|
|
|
1380
1577
|
}
|
|
1381
1578
|
/** Close the connection and release resources. */
|
|
1382
1579
|
disconnect(reason) {
|
|
1383
|
-
|
|
1384
|
-
this.expiryTimer = null;
|
|
1580
|
+
this.clearTimer();
|
|
1385
1581
|
this.connected = false;
|
|
1386
1582
|
this.emit("disconnected", { reason });
|
|
1387
1583
|
this.removeAllListeners();
|
|
@@ -1410,7 +1606,14 @@ var Mebius = {
|
|
|
1410
1606
|
}
|
|
1411
1607
|
if (!options.token) throw mebiusError("UNKNOWN", "Mebius.connect requires a token.");
|
|
1412
1608
|
const telemetry = options.beaconToken && options.beaconUrl ? { token: options.beaconToken, url: options.beaconUrl } : null;
|
|
1413
|
-
const client = new MebiusClient(
|
|
1609
|
+
const client = new MebiusClient(
|
|
1610
|
+
config,
|
|
1611
|
+
options.token,
|
|
1612
|
+
options.deliveries ?? [],
|
|
1613
|
+
telemetry,
|
|
1614
|
+
options.userId,
|
|
1615
|
+
options.getToken
|
|
1616
|
+
);
|
|
1414
1617
|
client.open();
|
|
1415
1618
|
return client;
|
|
1416
1619
|
},
|