@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.global.js
CHANGED
|
@@ -42840,10 +42840,21 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
|
|
|
42840
42840
|
}
|
|
42841
42841
|
|
|
42842
42842
|
// src/internal/ll-view-transport.ts
|
|
42843
|
+
var DEFAULT_REALTIME_TARGET_MS = 300;
|
|
42844
|
+
function holdBuffer(receiver, targetMs) {
|
|
42845
|
+
const r = receiver;
|
|
42846
|
+
try {
|
|
42847
|
+
if ("jitterBufferTarget" in r) r.jitterBufferTarget = targetMs;
|
|
42848
|
+
if ("playoutDelayHint" in r) r.playoutDelayHint = targetMs / 1e3;
|
|
42849
|
+
} catch {
|
|
42850
|
+
}
|
|
42851
|
+
}
|
|
42843
42852
|
var WhepViewTransport = class {
|
|
42844
|
-
constructor(signaling) {
|
|
42853
|
+
constructor(signaling, targetLatencyMs = DEFAULT_REALTIME_TARGET_MS) {
|
|
42845
42854
|
this.signaling = signaling;
|
|
42855
|
+
this.targetLatencyMs = targetLatencyMs;
|
|
42846
42856
|
this.kind = "whep";
|
|
42857
|
+
this.cursor = null;
|
|
42847
42858
|
this.pc = null;
|
|
42848
42859
|
this.resourceUrl = null;
|
|
42849
42860
|
this.endedCb = null;
|
|
@@ -42867,6 +42878,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
|
|
|
42867
42878
|
pc.addTransceiver("video", { direction: "recvonly" });
|
|
42868
42879
|
pc.addTransceiver("audio", { direction: "recvonly" });
|
|
42869
42880
|
pc.ontrack = (ev) => {
|
|
42881
|
+
holdBuffer(ev.receiver, this.targetLatencyMs);
|
|
42870
42882
|
remote.addTrack(ev.track);
|
|
42871
42883
|
video.srcObject = remote;
|
|
42872
42884
|
void playWithAutoplayFallback(video).then((o) => {
|
|
@@ -42894,6 +42906,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
|
|
|
42894
42906
|
await pc.setRemoteDescription({ type: "answer", sdp: answer });
|
|
42895
42907
|
}
|
|
42896
42908
|
async stop() {
|
|
42909
|
+
this.cursor = null;
|
|
42897
42910
|
await this.signaling.deleteResource(this.resourceUrl);
|
|
42898
42911
|
this.resourceUrl = null;
|
|
42899
42912
|
this.pc?.close();
|
|
@@ -42901,24 +42914,59 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
|
|
|
42901
42914
|
if (this.video) resetVideoElement(this.video);
|
|
42902
42915
|
this.video = null;
|
|
42903
42916
|
}
|
|
42917
|
+
/**
|
|
42918
|
+
* Read what the connection actually did since the last reading.
|
|
42919
|
+
*
|
|
42920
|
+
* This route needs to measure its own freezes, because nothing else can. A
|
|
42921
|
+
* frozen real-time picture still reports a healthy connection and raises no
|
|
42922
|
+
* event on the video element — so from the outside the session looks flawless
|
|
42923
|
+
* while the viewer stares at a still frame. Every freeze on this route used to
|
|
42924
|
+
* be recorded as zero, which is why the problem could be felt and never seen.
|
|
42925
|
+
*/
|
|
42904
42926
|
async getStats() {
|
|
42905
42927
|
if (!this.pc) return null;
|
|
42906
42928
|
const report = await this.pc.getStats();
|
|
42907
|
-
let
|
|
42908
|
-
let
|
|
42909
|
-
let
|
|
42929
|
+
let framesPerSecond;
|
|
42930
|
+
let freezeS = 0;
|
|
42931
|
+
let packetsLost = 0;
|
|
42932
|
+
let packetsReceived = 0;
|
|
42933
|
+
let bytesReceived = 0;
|
|
42934
|
+
let bufferDelayS = 0;
|
|
42935
|
+
let bufferEmitted = 0;
|
|
42936
|
+
let rttMs;
|
|
42910
42937
|
report.forEach((stat) => {
|
|
42911
|
-
if (stat.type === "inbound-rtp") {
|
|
42912
|
-
|
|
42913
|
-
if (typeof
|
|
42938
|
+
if (stat.type === "inbound-rtp" && stat.kind === "video") {
|
|
42939
|
+
const v = stat;
|
|
42940
|
+
if (typeof v.framesPerSecond === "number") framesPerSecond = v.framesPerSecond;
|
|
42941
|
+
if (typeof v.totalFreezesDuration === "number") freezeS = v.totalFreezesDuration;
|
|
42942
|
+
if (typeof v.packetsLost === "number") packetsLost = v.packetsLost;
|
|
42943
|
+
if (typeof v.packetsReceived === "number") packetsReceived = v.packetsReceived;
|
|
42944
|
+
if (typeof v.bytesReceived === "number") bytesReceived = v.bytesReceived;
|
|
42945
|
+
if (typeof v.jitterBufferDelay === "number") bufferDelayS = v.jitterBufferDelay;
|
|
42946
|
+
if (typeof v.jitterBufferEmittedCount === "number") bufferEmitted = v.jitterBufferEmittedCount;
|
|
42914
42947
|
}
|
|
42915
42948
|
if (stat.type === "candidate-pair" && stat.state === "succeeded") {
|
|
42916
|
-
|
|
42917
|
-
|
|
42918
|
-
}
|
|
42949
|
+
const p = stat;
|
|
42950
|
+
if (typeof p.currentRoundTripTime === "number") rttMs = Math.round(p.currentRoundTripTime * 1e3);
|
|
42919
42951
|
}
|
|
42920
42952
|
});
|
|
42921
|
-
|
|
42953
|
+
const atMs = Date.now();
|
|
42954
|
+
const previous = this.cursor;
|
|
42955
|
+
this.cursor = { atMs, freezeS, packetsLost, packetsReceived, bytesReceived, bufferDelayS, bufferEmitted };
|
|
42956
|
+
if (!previous) return { framesPerSecond, rttMs };
|
|
42957
|
+
const elapsedS = Math.max(1e-3, (atMs - previous.atMs) / 1e3);
|
|
42958
|
+
const deltaLost = Math.max(0, packetsLost - previous.packetsLost);
|
|
42959
|
+
const deltaReceived = Math.max(0, packetsReceived - previous.packetsReceived);
|
|
42960
|
+
const deltaEmitted = bufferEmitted - previous.bufferEmitted;
|
|
42961
|
+
const heldMs = deltaEmitted > 0 ? (bufferDelayS - previous.bufferDelayS) / deltaEmitted * 1e3 : void 0;
|
|
42962
|
+
return {
|
|
42963
|
+
bitrateKbps: Math.round((bytesReceived - previous.bytesReceived) * 8 / elapsedS / 1e3),
|
|
42964
|
+
framesPerSecond,
|
|
42965
|
+
latencyMs: heldMs === void 0 ? void 0 : Math.round(heldMs + (rttMs !== void 0 ? rttMs / 2 : 0)),
|
|
42966
|
+
rttMs,
|
|
42967
|
+
packetLossPct: deltaLost + deltaReceived > 0 ? Number((deltaLost / (deltaLost + deltaReceived) * 100).toFixed(2)) : 0,
|
|
42968
|
+
freezeMs: Math.max(0, Math.round((freezeS - previous.freezeS) * 1e3))
|
|
42969
|
+
};
|
|
42922
42970
|
}
|
|
42923
42971
|
};
|
|
42924
42972
|
|
|
@@ -42926,6 +42974,19 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
|
|
|
42926
42974
|
function retryWarmupNotFound(cfg, retryCount, res, retry) {
|
|
42927
42975
|
return retry || retryCount < (cfg?.maxNumRetry ?? 0) && res?.code === 404;
|
|
42928
42976
|
}
|
|
42977
|
+
var TOKEN_PARAM = /([?&]token=)[^&#]*/;
|
|
42978
|
+
function withCurrentToken(url, token) {
|
|
42979
|
+
return url.replace(TOKEN_PARAM, (_match, prefix) => prefix + encodeURIComponent(token));
|
|
42980
|
+
}
|
|
42981
|
+
function tokenRestampingLoader(Hls2, currentToken) {
|
|
42982
|
+
const Base = Hls2.DefaultConfig.loader;
|
|
42983
|
+
return class TokenRestampingLoader extends Base {
|
|
42984
|
+
load(context, config2, callbacks) {
|
|
42985
|
+
context.url = withCurrentToken(context.url, currentToken());
|
|
42986
|
+
super.load(context, config2, callbacks);
|
|
42987
|
+
}
|
|
42988
|
+
};
|
|
42989
|
+
}
|
|
42929
42990
|
var HlsViewTransport = class {
|
|
42930
42991
|
/**
|
|
42931
42992
|
* deliveryPath, when given, is a gateway-relative path from the gateway's own
|
|
@@ -42933,9 +42994,10 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
|
|
|
42933
42994
|
* origin one. Without it this falls back to the origin playlist, which is
|
|
42934
42995
|
* still correct, just served from our own bandwidth.
|
|
42935
42996
|
*/
|
|
42936
|
-
constructor(signaling, deliveryPath) {
|
|
42997
|
+
constructor(signaling, deliveryPath, targetS) {
|
|
42937
42998
|
this.signaling = signaling;
|
|
42938
42999
|
this.deliveryPath = deliveryPath;
|
|
43000
|
+
this.targetS = targetS;
|
|
42939
43001
|
this.kind = "hls";
|
|
42940
43002
|
this.hls = null;
|
|
42941
43003
|
this.video = null;
|
|
@@ -42975,6 +43037,11 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
|
|
|
42975
43037
|
}
|
|
42976
43038
|
const hls = new Hls2({
|
|
42977
43039
|
maxLiveSyncPlaybackRate: 1.1,
|
|
43040
|
+
loader: tokenRestampingLoader(Hls2, () => this.signaling.accessToken()),
|
|
43041
|
+
// Only when the app actually asked. Left alone, the library follows the
|
|
43042
|
+
// playlist's own HOLD-BACK, which the server measured from the segments it
|
|
43043
|
+
// is producing — a number guessed here would only override a real one.
|
|
43044
|
+
...this.targetS === void 0 ? {} : { liveSyncDuration: this.targetS },
|
|
42978
43045
|
manifestLoadPolicy: {
|
|
42979
43046
|
default: {
|
|
42980
43047
|
maxTimeToFirstByteMs: 1e4,
|
|
@@ -43044,8 +43111,12 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
|
|
|
43044
43111
|
autoCleanupMinBackwardDuration: 10,
|
|
43045
43112
|
reuseRedirectedURL: true
|
|
43046
43113
|
};
|
|
43047
|
-
var
|
|
43048
|
-
var
|
|
43114
|
+
var DEFAULT_BALANCED_TARGET_S = 2;
|
|
43115
|
+
var CATCH_UP_RATE = 1.05;
|
|
43116
|
+
var BUILD_UP_RATE = 0.98;
|
|
43117
|
+
var SEEK_AT = 4;
|
|
43118
|
+
var CATCH_UP_ABOVE = 1.5;
|
|
43119
|
+
var BUILD_UP_BELOW = 0.9;
|
|
43049
43120
|
var AUDIO_RETRY_MS = 2500;
|
|
43050
43121
|
var UPSTREAM_LATENCY_MS = 800;
|
|
43051
43122
|
function stalledWithData(video, ms) {
|
|
@@ -43063,17 +43134,31 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
|
|
|
43063
43134
|
video.addEventListener("timeupdate", onTime);
|
|
43064
43135
|
});
|
|
43065
43136
|
}
|
|
43066
|
-
function
|
|
43137
|
+
function syncLiveEdge(video, targetS = DEFAULT_BALANCED_TARGET_S) {
|
|
43067
43138
|
const ranges = video.buffered;
|
|
43068
43139
|
if (ranges.length === 0) return;
|
|
43069
43140
|
const edge = ranges.end(ranges.length - 1);
|
|
43070
|
-
|
|
43071
|
-
|
|
43141
|
+
const drift = edge - video.currentTime;
|
|
43142
|
+
if (drift > targetS * SEEK_AT) {
|
|
43143
|
+
video.currentTime = edge - targetS;
|
|
43144
|
+
video.playbackRate = 1;
|
|
43145
|
+
return;
|
|
43146
|
+
}
|
|
43147
|
+
if (drift > targetS * CATCH_UP_ABOVE) {
|
|
43148
|
+
video.playbackRate = CATCH_UP_RATE;
|
|
43149
|
+
return;
|
|
43150
|
+
}
|
|
43151
|
+
if (drift < targetS * BUILD_UP_BELOW) {
|
|
43152
|
+
video.playbackRate = BUILD_UP_RATE;
|
|
43153
|
+
return;
|
|
43154
|
+
}
|
|
43155
|
+
video.playbackRate = 1;
|
|
43072
43156
|
}
|
|
43073
43157
|
var FlvViewTransport = class {
|
|
43074
|
-
constructor(signaling, deliveryPath) {
|
|
43158
|
+
constructor(signaling, deliveryPath, targetS = DEFAULT_BALANCED_TARGET_S) {
|
|
43075
43159
|
this.signaling = signaling;
|
|
43076
43160
|
this.deliveryPath = deliveryPath;
|
|
43161
|
+
this.targetS = targetS;
|
|
43077
43162
|
this.kind = "flv_js";
|
|
43078
43163
|
this.player = null;
|
|
43079
43164
|
this.video = null;
|
|
@@ -43099,7 +43184,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
|
|
|
43099
43184
|
const { signal } = this.listeners;
|
|
43100
43185
|
video.addEventListener("ended", () => this.endedCb?.(), { signal });
|
|
43101
43186
|
video.addEventListener("waiting", () => this.bufferingCb?.(), { signal });
|
|
43102
|
-
video.addEventListener("timeupdate", () =>
|
|
43187
|
+
video.addEventListener("timeupdate", () => syncLiveEdge(video, this.targetS), { signal });
|
|
43103
43188
|
let mod;
|
|
43104
43189
|
try {
|
|
43105
43190
|
mod = await Promise.resolve().then(() => __toESM(require_flv(), 1));
|
|
@@ -43143,6 +43228,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
|
|
|
43143
43228
|
this.listeners = null;
|
|
43144
43229
|
this.teardownPlayer();
|
|
43145
43230
|
if (this.video) {
|
|
43231
|
+
this.video.playbackRate = 1;
|
|
43146
43232
|
this.video.removeAttribute("src");
|
|
43147
43233
|
this.video.load();
|
|
43148
43234
|
}
|
|
@@ -43208,18 +43294,29 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
|
|
|
43208
43294
|
function canPlayBuffered() {
|
|
43209
43295
|
return typeof MediaSource !== "undefined";
|
|
43210
43296
|
}
|
|
43211
|
-
function transportFor(kind, path, signaling) {
|
|
43212
|
-
|
|
43213
|
-
if (kind ===
|
|
43297
|
+
function transportFor(kind, path, signaling, targetLatencyMs) {
|
|
43298
|
+
const targetS = targetLatencyMs === void 0 ? void 0 : targetLatencyMs / 1e3;
|
|
43299
|
+
if (kind === KIND_FAST)
|
|
43300
|
+
return canPlayBuffered() ? new FlvViewTransport(signaling, path, targetS) : null;
|
|
43301
|
+
if (kind === KIND_WIDE || kind === KIND_LOCAL)
|
|
43302
|
+
return new HlsViewTransport(signaling, path, targetS);
|
|
43214
43303
|
return null;
|
|
43215
43304
|
}
|
|
43216
|
-
function createViewCandidates(mode, signaling, deliveries = []) {
|
|
43217
|
-
const fromGateway = (kinds) => deliveries.filter((d) => kinds.includes(d.kind)).map((d) => transportFor(d.kind, d.path, signaling)).filter((t) => t !== null);
|
|
43218
|
-
const originFallback = new HlsViewTransport(
|
|
43305
|
+
function createViewCandidates(mode, signaling, deliveries = [], targetLatencyMs) {
|
|
43306
|
+
const fromGateway = (kinds) => deliveries.filter((d) => kinds.includes(d.kind)).map((d) => transportFor(d.kind, d.path, signaling, targetLatencyMs)).filter((t) => t !== null);
|
|
43307
|
+
const originFallback = new HlsViewTransport(
|
|
43308
|
+
signaling,
|
|
43309
|
+
void 0,
|
|
43310
|
+
targetLatencyMs === void 0 ? void 0 : targetLatencyMs / 1e3
|
|
43311
|
+
);
|
|
43219
43312
|
const allKinds = [KIND_FAST, KIND_WIDE, KIND_LOCAL];
|
|
43220
43313
|
switch (mode) {
|
|
43221
43314
|
case "low-latency":
|
|
43222
|
-
return [
|
|
43315
|
+
return [
|
|
43316
|
+
new WhepViewTransport(signaling, targetLatencyMs ?? DEFAULT_REALTIME_TARGET_MS),
|
|
43317
|
+
...fromGateway(allKinds),
|
|
43318
|
+
originFallback
|
|
43319
|
+
];
|
|
43223
43320
|
case "balanced":
|
|
43224
43321
|
return [...fromGateway(allKinds), originFallback];
|
|
43225
43322
|
case "scale":
|
|
@@ -43230,7 +43327,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
|
|
|
43230
43327
|
}
|
|
43231
43328
|
|
|
43232
43329
|
// src/internal/telemetry.ts
|
|
43233
|
-
var SDK_VERSION = "
|
|
43330
|
+
var SDK_VERSION = true ? `web/${"0.7.0"}` : "web/dev";
|
|
43234
43331
|
var FLUSH_INTERVAL_MS = 15e3;
|
|
43235
43332
|
var MAX_BATCH = 64;
|
|
43236
43333
|
function describeDevice() {
|
|
@@ -43638,7 +43735,12 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
|
|
|
43638
43735
|
this.freeze = new FreezeClock();
|
|
43639
43736
|
/** Cancels element listeners bound for the lifetime of one play(). */
|
|
43640
43737
|
this.elementListeners = null;
|
|
43641
|
-
this.candidates = createViewCandidates(
|
|
43738
|
+
this.candidates = createViewCandidates(
|
|
43739
|
+
options.mode ?? "auto",
|
|
43740
|
+
signaling,
|
|
43741
|
+
deliveries,
|
|
43742
|
+
options.targetLatencyMs
|
|
43743
|
+
);
|
|
43642
43744
|
}
|
|
43643
43745
|
/** Start playing `streamId` into the given video element or selector. */
|
|
43644
43746
|
async play(streamId, viewTarget) {
|
|
@@ -43735,13 +43837,15 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
|
|
|
43735
43837
|
}
|
|
43736
43838
|
/**
|
|
43737
43839
|
* Wall-clock time (Unix ms) currently on screen, or `null` when the active
|
|
43738
|
-
* route cannot produce one
|
|
43840
|
+
* route cannot produce one. A real-time route carries no wall clock at all,
|
|
43841
|
+
* and a segmented route has none until its first timestamped segment arrives
|
|
43842
|
+
* (see {@link ViewTransport}).
|
|
43739
43843
|
*
|
|
43740
43844
|
* This is what {@link MebiusClient.createCaptions} compares against a
|
|
43741
43845
|
* segment's `epochMs` to know when it is due. Delegating to the transport
|
|
43742
43846
|
* rather than reading the element directly is what keeps this correct across
|
|
43743
|
-
* a route failover: the player may
|
|
43744
|
-
*
|
|
43847
|
+
* a route failover: the player may change route mid-session, and the clock
|
|
43848
|
+
* source has to follow.
|
|
43745
43849
|
*/
|
|
43746
43850
|
currentEpochMs() {
|
|
43747
43851
|
return this.transport?.playheadEpochMs?.() ?? null;
|
|
@@ -43765,9 +43869,11 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
|
|
|
43765
43869
|
startStats() {
|
|
43766
43870
|
this.statsTimer = setInterval(async () => {
|
|
43767
43871
|
const stats = await this.transport?.getStats();
|
|
43768
|
-
const
|
|
43872
|
+
const elementFreezeMs = this.freeze.take();
|
|
43769
43873
|
if (!stats) {
|
|
43770
|
-
if (
|
|
43874
|
+
if (elementFreezeMs > 0) {
|
|
43875
|
+
this.reporter?.add({ ts: Math.floor(Date.now() / 1e3), freezeMs: elementFreezeMs });
|
|
43876
|
+
}
|
|
43771
43877
|
return;
|
|
43772
43878
|
}
|
|
43773
43879
|
this.emit("stats", stats);
|
|
@@ -43775,7 +43881,9 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
|
|
|
43775
43881
|
ts: Math.floor(Date.now() / 1e3),
|
|
43776
43882
|
bitrateKbps: stats.bitrateKbps,
|
|
43777
43883
|
fps: stats.framesPerSecond,
|
|
43778
|
-
|
|
43884
|
+
rttMs: stats.rttMs,
|
|
43885
|
+
packetLossPct: stats.packetLossPct,
|
|
43886
|
+
freezeMs: elementFreezeMs + (stats.freezeMs ?? 0)
|
|
43779
43887
|
});
|
|
43780
43888
|
}, STATS_INTERVAL_MS2);
|
|
43781
43889
|
}
|
|
@@ -43806,6 +43914,21 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
|
|
|
43806
43914
|
this.gateway = gateway;
|
|
43807
43915
|
this.token = token;
|
|
43808
43916
|
}
|
|
43917
|
+
/**
|
|
43918
|
+
* Swap in a freshly-minted access token.
|
|
43919
|
+
*
|
|
43920
|
+
* Every URL this class builds is built at call time, so a session that starts
|
|
43921
|
+
* a new request after this point uses the new token with no further wiring.
|
|
43922
|
+
* What it does NOT reach is a request already in flight or a media URL another
|
|
43923
|
+
* library has memorised — see the scale route's loader for that half.
|
|
43924
|
+
*/
|
|
43925
|
+
setToken(token) {
|
|
43926
|
+
this.token = token;
|
|
43927
|
+
}
|
|
43928
|
+
/** The current access token, for transports that must re-stamp their own URLs. */
|
|
43929
|
+
accessToken() {
|
|
43930
|
+
return this.token;
|
|
43931
|
+
}
|
|
43809
43932
|
base() {
|
|
43810
43933
|
return this.gateway.replace(/\/+$/, "");
|
|
43811
43934
|
}
|
|
@@ -43926,16 +44049,21 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
|
|
|
43926
44049
|
}
|
|
43927
44050
|
|
|
43928
44051
|
// src/client.ts
|
|
44052
|
+
var REFRESH_MARGIN_MS = 6e4;
|
|
44053
|
+
var REFRESH_RETRY_BASE_MS = 2e3;
|
|
44054
|
+
var REFRESH_RETRY_MAX_MS = 3e4;
|
|
43929
44055
|
var MebiusClient = class extends TypedEmitter {
|
|
43930
44056
|
/** @internal */
|
|
43931
|
-
constructor(config2, token, deliveries = [], telemetry = null, userId) {
|
|
44057
|
+
constructor(config2, token, deliveries = [], telemetry = null, userId, getToken) {
|
|
43932
44058
|
super();
|
|
43933
44059
|
this.token = token;
|
|
43934
44060
|
this.deliveries = deliveries;
|
|
43935
44061
|
this.telemetry = telemetry;
|
|
43936
44062
|
this.userId = userId;
|
|
44063
|
+
this.getToken = getToken;
|
|
43937
44064
|
this.expiryTimer = null;
|
|
43938
44065
|
this.connected = false;
|
|
44066
|
+
this.refreshFailures = 0;
|
|
43939
44067
|
this.signaling = new SignalingClient(config2.gateway, token);
|
|
43940
44068
|
}
|
|
43941
44069
|
/** @internal Called by {@link Mebius.connect}. */
|
|
@@ -43947,13 +44075,82 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
|
|
|
43947
44075
|
return;
|
|
43948
44076
|
}
|
|
43949
44077
|
this.connected = true;
|
|
43950
|
-
|
|
44078
|
+
this.scheduleTokenWork(expiresAtMs);
|
|
44079
|
+
queueMicrotask(() => this.emit("connected", void 0));
|
|
44080
|
+
}
|
|
44081
|
+
/**
|
|
44082
|
+
* Arm whatever has to happen as this token approaches its expiry: renew it if
|
|
44083
|
+
* the app gave us a way to, otherwise report that the session is over.
|
|
44084
|
+
*
|
|
44085
|
+
* Renewing is what makes an unattended session possible at all. The gateway
|
|
44086
|
+
* checks the token on every media request, so without a fresh one playback
|
|
44087
|
+
* stops the moment it expires — no matter how healthy the stream is.
|
|
44088
|
+
*/
|
|
44089
|
+
scheduleTokenWork(expiresAtMs) {
|
|
44090
|
+
this.clearTimer();
|
|
44091
|
+
if (expiresAtMs === null) return;
|
|
44092
|
+
const remaining = expiresAtMs - Date.now();
|
|
44093
|
+
if (!this.getToken) {
|
|
43951
44094
|
this.expiryTimer = setTimeout(
|
|
43952
44095
|
() => this.emit("error", mebiusError("TOKEN_EXPIRED")),
|
|
43953
|
-
Math.max(0,
|
|
44096
|
+
Math.max(0, remaining)
|
|
43954
44097
|
);
|
|
44098
|
+
return;
|
|
43955
44099
|
}
|
|
43956
|
-
|
|
44100
|
+
this.expiryTimer = setTimeout(
|
|
44101
|
+
() => void this.refreshToken(expiresAtMs),
|
|
44102
|
+
Math.max(0, remaining - REFRESH_MARGIN_MS)
|
|
44103
|
+
);
|
|
44104
|
+
}
|
|
44105
|
+
async refreshToken(previousExpiryMs) {
|
|
44106
|
+
if (!this.connected || !this.getToken) return;
|
|
44107
|
+
let next;
|
|
44108
|
+
try {
|
|
44109
|
+
next = await this.getToken();
|
|
44110
|
+
} catch (cause) {
|
|
44111
|
+
this.onRefreshFailed(previousExpiryMs, cause);
|
|
44112
|
+
return;
|
|
44113
|
+
}
|
|
44114
|
+
if (!this.connected) return;
|
|
44115
|
+
const { expiresAtMs } = readToken(next);
|
|
44116
|
+
if (expiresAtMs !== null && expiresAtMs <= previousExpiryMs) {
|
|
44117
|
+
this.emit(
|
|
44118
|
+
"error",
|
|
44119
|
+
mebiusError("TOKEN_EXPIRED", "Mebius token refresh returned a token that is not newer.")
|
|
44120
|
+
);
|
|
44121
|
+
return;
|
|
44122
|
+
}
|
|
44123
|
+
this.refreshFailures = 0;
|
|
44124
|
+
this.token = next;
|
|
44125
|
+
this.signaling.setToken(next);
|
|
44126
|
+
this.emit("token-refreshed", void 0);
|
|
44127
|
+
this.scheduleTokenWork(expiresAtMs);
|
|
44128
|
+
}
|
|
44129
|
+
/**
|
|
44130
|
+
* A failed refresh is not a dead session: the current token is still valid
|
|
44131
|
+
* until `expiryMs`, and the viewer is still watching. Retry inside that window
|
|
44132
|
+
* and only report expiry once it has actually run out.
|
|
44133
|
+
*/
|
|
44134
|
+
onRefreshFailed(expiryMs, cause) {
|
|
44135
|
+
const remaining = expiryMs - Date.now();
|
|
44136
|
+
if (remaining <= 0) {
|
|
44137
|
+
this.emit("error", mebiusError("TOKEN_EXPIRED", void 0, cause));
|
|
44138
|
+
return;
|
|
44139
|
+
}
|
|
44140
|
+
this.refreshFailures += 1;
|
|
44141
|
+
const backoff = Math.min(
|
|
44142
|
+
REFRESH_RETRY_MAX_MS,
|
|
44143
|
+
REFRESH_RETRY_BASE_MS * 2 ** (this.refreshFailures - 1)
|
|
44144
|
+
);
|
|
44145
|
+
this.clearTimer();
|
|
44146
|
+
this.expiryTimer = setTimeout(
|
|
44147
|
+
() => void this.refreshToken(expiryMs),
|
|
44148
|
+
Math.min(backoff, remaining)
|
|
44149
|
+
);
|
|
44150
|
+
}
|
|
44151
|
+
clearTimer() {
|
|
44152
|
+
if (this.expiryTimer) clearTimeout(this.expiryTimer);
|
|
44153
|
+
this.expiryTimer = null;
|
|
43957
44154
|
}
|
|
43958
44155
|
/** Create a broadcaster bound to this connection. */
|
|
43959
44156
|
createBroadcaster(options = {}) {
|
|
@@ -43998,8 +44195,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
|
|
|
43998
44195
|
}
|
|
43999
44196
|
/** Close the connection and release resources. */
|
|
44000
44197
|
disconnect(reason) {
|
|
44001
|
-
|
|
44002
|
-
this.expiryTimer = null;
|
|
44198
|
+
this.clearTimer();
|
|
44003
44199
|
this.connected = false;
|
|
44004
44200
|
this.emit("disconnected", { reason });
|
|
44005
44201
|
this.removeAllListeners();
|
|
@@ -44028,7 +44224,14 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
|
|
|
44028
44224
|
}
|
|
44029
44225
|
if (!options.token) throw mebiusError("UNKNOWN", "Mebius.connect requires a token.");
|
|
44030
44226
|
const telemetry = options.beaconToken && options.beaconUrl ? { token: options.beaconToken, url: options.beaconUrl } : null;
|
|
44031
|
-
const client = new MebiusClient(
|
|
44227
|
+
const client = new MebiusClient(
|
|
44228
|
+
config,
|
|
44229
|
+
options.token,
|
|
44230
|
+
options.deliveries ?? [],
|
|
44231
|
+
telemetry,
|
|
44232
|
+
options.userId,
|
|
44233
|
+
options.getToken
|
|
44234
|
+
);
|
|
44032
44235
|
client.open();
|
|
44033
44236
|
return client;
|
|
44034
44237
|
},
|