@mebius-io/web 0.4.8 → 0.5.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 +152 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +115 -1
- package/dist/index.d.ts +115 -1
- package/dist/index.global.js +164 -14
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +151 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -305,6 +305,9 @@ var WhepViewTransport = class {
|
|
|
305
305
|
};
|
|
306
306
|
|
|
307
307
|
// src/internal/scale-view-transport.ts
|
|
308
|
+
function retryWarmupNotFound(cfg, retryCount, res, retry) {
|
|
309
|
+
return retry || retryCount < (cfg?.maxNumRetry ?? 0) && res?.code === 404;
|
|
310
|
+
}
|
|
308
311
|
var HlsViewTransport = class {
|
|
309
312
|
/**
|
|
310
313
|
* deliveryPath, when given, is a gateway-relative path from the gateway's own
|
|
@@ -352,7 +355,22 @@ var HlsViewTransport = class {
|
|
|
352
355
|
this.mutedByPolicy = (await playWithAutoplayFallback(video)).mutedByPolicy;
|
|
353
356
|
return;
|
|
354
357
|
}
|
|
355
|
-
const hls = new Hls({
|
|
358
|
+
const hls = new Hls({
|
|
359
|
+
maxLiveSyncPlaybackRate: 1.1,
|
|
360
|
+
manifestLoadPolicy: {
|
|
361
|
+
default: {
|
|
362
|
+
maxTimeToFirstByteMs: 1e4,
|
|
363
|
+
maxLoadTimeMs: 2e4,
|
|
364
|
+
timeoutRetry: { maxNumRetry: 2, retryDelayMs: 0, maxRetryDelayMs: 0 },
|
|
365
|
+
errorRetry: {
|
|
366
|
+
maxNumRetry: 5,
|
|
367
|
+
retryDelayMs: 500,
|
|
368
|
+
maxRetryDelayMs: 2e3,
|
|
369
|
+
shouldRetry: (cfg, retryCount, _isTimeout, res, retry) => retryWarmupNotFound(cfg, retryCount, res, retry)
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
});
|
|
356
374
|
this.hls = hls;
|
|
357
375
|
hls.on(Hls.Events.ERROR, (_evt, data) => {
|
|
358
376
|
if (data.fatal) this.bufferingCb?.();
|
|
@@ -380,6 +398,22 @@ var HlsViewTransport = class {
|
|
|
380
398
|
framesPerSecond: 0
|
|
381
399
|
};
|
|
382
400
|
}
|
|
401
|
+
/**
|
|
402
|
+
* hls.js exposes `playingDate` straight from the segment the element is
|
|
403
|
+
* currently rendering, derived from the playlist's `EXT-X-PROGRAM-DATE-TIME`
|
|
404
|
+
* (MediaMTX writes it). Safari's native player has no such property, but
|
|
405
|
+
* `getStartDate()` (the wall-clock time of the playlist's first segment) plus
|
|
406
|
+
* elapsed `currentTime` is the same clock by construction.
|
|
407
|
+
*/
|
|
408
|
+
playheadEpochMs() {
|
|
409
|
+
if (this.hls) return this.hls.playingDate?.getTime() ?? null;
|
|
410
|
+
const video = this.video;
|
|
411
|
+
if (video?.getStartDate) {
|
|
412
|
+
const start = video.getStartDate().getTime();
|
|
413
|
+
if (Number.isFinite(start)) return start + video.currentTime * 1e3;
|
|
414
|
+
}
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
383
417
|
};
|
|
384
418
|
|
|
385
419
|
// src/internal/balanced-view-transport.ts
|
|
@@ -751,6 +785,82 @@ function normalize(c, fallback) {
|
|
|
751
785
|
return c;
|
|
752
786
|
}
|
|
753
787
|
|
|
788
|
+
// src/captions.ts
|
|
789
|
+
var TICK_MS = 100;
|
|
790
|
+
var STALE_MS = 5e3;
|
|
791
|
+
var MebiusCaptions = class extends TypedEmitter {
|
|
792
|
+
/** @internal */
|
|
793
|
+
constructor(signaling, player, opts) {
|
|
794
|
+
super();
|
|
795
|
+
this.signaling = signaling;
|
|
796
|
+
this.player = player;
|
|
797
|
+
this.opts = opts;
|
|
798
|
+
this.es = null;
|
|
799
|
+
this.timer = null;
|
|
800
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
801
|
+
this.shown = /* @__PURE__ */ new Set();
|
|
802
|
+
}
|
|
803
|
+
/** Open the SSE connection and begin emitting segments for `streamId`. */
|
|
804
|
+
start(streamId) {
|
|
805
|
+
if (this.es) return;
|
|
806
|
+
const url = this.signaling.captionsUrl(streamId, this.opts.lang);
|
|
807
|
+
const es = new EventSource(url);
|
|
808
|
+
es.onmessage = (ev) => this.onFrame(ev);
|
|
809
|
+
es.onerror = () => this.emit("error", void 0);
|
|
810
|
+
this.es = es;
|
|
811
|
+
this.timer = setInterval(() => this.tick(), TICK_MS);
|
|
812
|
+
}
|
|
813
|
+
/** Close the connection and drop all buffered segments. */
|
|
814
|
+
stop() {
|
|
815
|
+
this.es?.close();
|
|
816
|
+
this.es = null;
|
|
817
|
+
if (this.timer) clearInterval(this.timer);
|
|
818
|
+
this.timer = null;
|
|
819
|
+
this.pending.clear();
|
|
820
|
+
this.shown.clear();
|
|
821
|
+
}
|
|
822
|
+
onFrame(ev) {
|
|
823
|
+
let frame;
|
|
824
|
+
try {
|
|
825
|
+
frame = JSON.parse(ev.data);
|
|
826
|
+
} catch {
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
if (frame.type !== "caption" || !frame.segmentId) return;
|
|
830
|
+
const prev = this.pending.get(frame.segmentId);
|
|
831
|
+
if (prev && (frame.rev ?? 0) < (prev.rev ?? 0)) return;
|
|
832
|
+
this.pending.set(frame.segmentId, frame);
|
|
833
|
+
}
|
|
834
|
+
tick() {
|
|
835
|
+
const now = this.player.currentEpochMs();
|
|
836
|
+
if (now == null) return;
|
|
837
|
+
for (const [id, frame] of this.pending) {
|
|
838
|
+
const due = frame.epochMs ?? 0;
|
|
839
|
+
if (due > now) continue;
|
|
840
|
+
if (due < now - STALE_MS) {
|
|
841
|
+
this.pending.delete(id);
|
|
842
|
+
if (this.shown.delete(id)) this.emit("cleared", { segmentId: id });
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
this.shown.add(id);
|
|
846
|
+
this.emit("segment", toSegment(id, frame, this.opts.lang));
|
|
847
|
+
if (frame.state === "final") this.pending.delete(id);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
function toSegment(segmentId, frame, lang) {
|
|
852
|
+
return {
|
|
853
|
+
segmentId,
|
|
854
|
+
rev: frame.rev ?? 0,
|
|
855
|
+
state: frame.state === "final" ? "final" : "interim",
|
|
856
|
+
epochMs: frame.epochMs ?? 0,
|
|
857
|
+
durationMs: frame.durationMs ?? 0,
|
|
858
|
+
text: frame.text ?? "",
|
|
859
|
+
translation: frame.translations?.[lang],
|
|
860
|
+
machineGenerated: true
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
|
|
754
864
|
// src/internal/freeze-clock.ts
|
|
755
865
|
var FreezeClock = class {
|
|
756
866
|
constructor(now = Date.now) {
|
|
@@ -920,6 +1030,19 @@ var MebiusPlayer = class extends TypedEmitter {
|
|
|
920
1030
|
this.video.volume = v;
|
|
921
1031
|
this.video.muted = v === 0;
|
|
922
1032
|
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Wall-clock time (Unix ms) currently on screen, or `null` when the active
|
|
1035
|
+
* route cannot produce one (HTTP-FLV, WHEP — see {@link ViewTransport}).
|
|
1036
|
+
*
|
|
1037
|
+
* This is what {@link MebiusClient.createCaptions} compares against a
|
|
1038
|
+
* segment's `epochMs` to know when it is due. Delegating to the transport
|
|
1039
|
+
* rather than reading the element directly is what keeps this correct across
|
|
1040
|
+
* a route failover: the player may switch from HLS to FLV mid-session, and
|
|
1041
|
+
* the clock source has to follow.
|
|
1042
|
+
*/
|
|
1043
|
+
currentEpochMs() {
|
|
1044
|
+
return this.transport?.playheadEpochMs?.() ?? null;
|
|
1045
|
+
}
|
|
923
1046
|
attach(transport) {
|
|
924
1047
|
transport.onEnded(() => {
|
|
925
1048
|
if (this.transport !== transport) return;
|
|
@@ -1014,6 +1137,16 @@ var SignalingClient = class {
|
|
|
1014
1137
|
scalePlaylistUrl(streamId) {
|
|
1015
1138
|
return this.withToken(`${this.base()}/live/${encodeURIComponent(streamId)}/index.m3u8`);
|
|
1016
1139
|
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Realtime captions SSE URL. Same play token as media — the engine's
|
|
1142
|
+
* `PlayVerifier` gates both, so a viewer who can watch the stream can already
|
|
1143
|
+
* read its captions with zero extra credential.
|
|
1144
|
+
*/
|
|
1145
|
+
captionsUrl(streamId, lang) {
|
|
1146
|
+
return this.withToken(
|
|
1147
|
+
`${this.base()}/live/${encodeURIComponent(streamId)}/captions?lang=${encodeURIComponent(lang)}`
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1017
1150
|
// Maps a neutral session kind to the concrete signaling path segment. This
|
|
1018
1151
|
// mapping (publish -> WHIP, view -> WHEP) lives ONLY in this method body, so
|
|
1019
1152
|
// the protocol names never appear in any exported type signature.
|
|
@@ -1137,6 +1270,22 @@ var MebiusClient = class extends TypedEmitter {
|
|
|
1137
1270
|
this.assertConnected();
|
|
1138
1271
|
return new MebiusPlayer(this.signaling, { mode: "low-latency" }, this.deliveries, this.telemetry, this.userId);
|
|
1139
1272
|
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Subscribe to a stream's realtime captions.
|
|
1275
|
+
*
|
|
1276
|
+
* Reads the same feed a session already produces — it does NOT start the
|
|
1277
|
+
* caption session itself. `captions/start` spends money and requires an API
|
|
1278
|
+
* key, so it belongs to your own backend (see
|
|
1279
|
+
* mebius-stream-engine/docs/API.md §5.1), called once when you want captions
|
|
1280
|
+
* on for a stream. This only ever consumes what that call turned on.
|
|
1281
|
+
*
|
|
1282
|
+
* `player` must be the one showing `streamId`: captions are timed against its
|
|
1283
|
+
* playhead, and a mismatched player would compare against the wrong clock.
|
|
1284
|
+
*/
|
|
1285
|
+
createCaptions(player, options) {
|
|
1286
|
+
this.assertConnected();
|
|
1287
|
+
return new MebiusCaptions(this.signaling, player, options);
|
|
1288
|
+
}
|
|
1140
1289
|
/** Close the connection and release resources. */
|
|
1141
1290
|
disconnect(reason) {
|
|
1142
1291
|
if (this.expiryTimer) clearTimeout(this.expiryTimer);
|
|
@@ -1181,6 +1330,7 @@ var Mebius = {
|
|
|
1181
1330
|
export {
|
|
1182
1331
|
Mebius,
|
|
1183
1332
|
MebiusBroadcaster,
|
|
1333
|
+
MebiusCaptions,
|
|
1184
1334
|
MebiusClient,
|
|
1185
1335
|
MebiusError,
|
|
1186
1336
|
MebiusPlayer,
|