@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.cjs
CHANGED
|
@@ -32,6 +32,7 @@ var index_exports = {};
|
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
Mebius: () => Mebius,
|
|
34
34
|
MebiusBroadcaster: () => MebiusBroadcaster,
|
|
35
|
+
MebiusCaptions: () => MebiusCaptions,
|
|
35
36
|
MebiusClient: () => MebiusClient,
|
|
36
37
|
MebiusError: () => MebiusError,
|
|
37
38
|
MebiusPlayer: () => MebiusPlayer,
|
|
@@ -346,6 +347,9 @@ var WhepViewTransport = class {
|
|
|
346
347
|
};
|
|
347
348
|
|
|
348
349
|
// src/internal/scale-view-transport.ts
|
|
350
|
+
function retryWarmupNotFound(cfg, retryCount, res, retry) {
|
|
351
|
+
return retry || retryCount < (cfg?.maxNumRetry ?? 0) && res?.code === 404;
|
|
352
|
+
}
|
|
349
353
|
var HlsViewTransport = class {
|
|
350
354
|
/**
|
|
351
355
|
* deliveryPath, when given, is a gateway-relative path from the gateway's own
|
|
@@ -393,7 +397,22 @@ var HlsViewTransport = class {
|
|
|
393
397
|
this.mutedByPolicy = (await playWithAutoplayFallback(video)).mutedByPolicy;
|
|
394
398
|
return;
|
|
395
399
|
}
|
|
396
|
-
const hls = new Hls({
|
|
400
|
+
const hls = new Hls({
|
|
401
|
+
maxLiveSyncPlaybackRate: 1.1,
|
|
402
|
+
manifestLoadPolicy: {
|
|
403
|
+
default: {
|
|
404
|
+
maxTimeToFirstByteMs: 1e4,
|
|
405
|
+
maxLoadTimeMs: 2e4,
|
|
406
|
+
timeoutRetry: { maxNumRetry: 2, retryDelayMs: 0, maxRetryDelayMs: 0 },
|
|
407
|
+
errorRetry: {
|
|
408
|
+
maxNumRetry: 5,
|
|
409
|
+
retryDelayMs: 500,
|
|
410
|
+
maxRetryDelayMs: 2e3,
|
|
411
|
+
shouldRetry: (cfg, retryCount, _isTimeout, res, retry) => retryWarmupNotFound(cfg, retryCount, res, retry)
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
});
|
|
397
416
|
this.hls = hls;
|
|
398
417
|
hls.on(Hls.Events.ERROR, (_evt, data) => {
|
|
399
418
|
if (data.fatal) this.bufferingCb?.();
|
|
@@ -421,6 +440,22 @@ var HlsViewTransport = class {
|
|
|
421
440
|
framesPerSecond: 0
|
|
422
441
|
};
|
|
423
442
|
}
|
|
443
|
+
/**
|
|
444
|
+
* hls.js exposes `playingDate` straight from the segment the element is
|
|
445
|
+
* currently rendering, derived from the playlist's `EXT-X-PROGRAM-DATE-TIME`
|
|
446
|
+
* (MediaMTX writes it). Safari's native player has no such property, but
|
|
447
|
+
* `getStartDate()` (the wall-clock time of the playlist's first segment) plus
|
|
448
|
+
* elapsed `currentTime` is the same clock by construction.
|
|
449
|
+
*/
|
|
450
|
+
playheadEpochMs() {
|
|
451
|
+
if (this.hls) return this.hls.playingDate?.getTime() ?? null;
|
|
452
|
+
const video = this.video;
|
|
453
|
+
if (video?.getStartDate) {
|
|
454
|
+
const start = video.getStartDate().getTime();
|
|
455
|
+
if (Number.isFinite(start)) return start + video.currentTime * 1e3;
|
|
456
|
+
}
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
424
459
|
};
|
|
425
460
|
|
|
426
461
|
// src/internal/balanced-view-transport.ts
|
|
@@ -792,6 +827,82 @@ function normalize(c, fallback) {
|
|
|
792
827
|
return c;
|
|
793
828
|
}
|
|
794
829
|
|
|
830
|
+
// src/captions.ts
|
|
831
|
+
var TICK_MS = 100;
|
|
832
|
+
var STALE_MS = 5e3;
|
|
833
|
+
var MebiusCaptions = class extends TypedEmitter {
|
|
834
|
+
/** @internal */
|
|
835
|
+
constructor(signaling, player, opts) {
|
|
836
|
+
super();
|
|
837
|
+
this.signaling = signaling;
|
|
838
|
+
this.player = player;
|
|
839
|
+
this.opts = opts;
|
|
840
|
+
this.es = null;
|
|
841
|
+
this.timer = null;
|
|
842
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
843
|
+
this.shown = /* @__PURE__ */ new Set();
|
|
844
|
+
}
|
|
845
|
+
/** Open the SSE connection and begin emitting segments for `streamId`. */
|
|
846
|
+
start(streamId) {
|
|
847
|
+
if (this.es) return;
|
|
848
|
+
const url = this.signaling.captionsUrl(streamId, this.opts.lang);
|
|
849
|
+
const es = new EventSource(url);
|
|
850
|
+
es.onmessage = (ev) => this.onFrame(ev);
|
|
851
|
+
es.onerror = () => this.emit("error", void 0);
|
|
852
|
+
this.es = es;
|
|
853
|
+
this.timer = setInterval(() => this.tick(), TICK_MS);
|
|
854
|
+
}
|
|
855
|
+
/** Close the connection and drop all buffered segments. */
|
|
856
|
+
stop() {
|
|
857
|
+
this.es?.close();
|
|
858
|
+
this.es = null;
|
|
859
|
+
if (this.timer) clearInterval(this.timer);
|
|
860
|
+
this.timer = null;
|
|
861
|
+
this.pending.clear();
|
|
862
|
+
this.shown.clear();
|
|
863
|
+
}
|
|
864
|
+
onFrame(ev) {
|
|
865
|
+
let frame;
|
|
866
|
+
try {
|
|
867
|
+
frame = JSON.parse(ev.data);
|
|
868
|
+
} catch {
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
if (frame.type !== "caption" || !frame.segmentId) return;
|
|
872
|
+
const prev = this.pending.get(frame.segmentId);
|
|
873
|
+
if (prev && (frame.rev ?? 0) < (prev.rev ?? 0)) return;
|
|
874
|
+
this.pending.set(frame.segmentId, frame);
|
|
875
|
+
}
|
|
876
|
+
tick() {
|
|
877
|
+
const now = this.player.currentEpochMs();
|
|
878
|
+
if (now == null) return;
|
|
879
|
+
for (const [id, frame] of this.pending) {
|
|
880
|
+
const due = frame.epochMs ?? 0;
|
|
881
|
+
if (due > now) continue;
|
|
882
|
+
if (due < now - STALE_MS) {
|
|
883
|
+
this.pending.delete(id);
|
|
884
|
+
if (this.shown.delete(id)) this.emit("cleared", { segmentId: id });
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
this.shown.add(id);
|
|
888
|
+
this.emit("segment", toSegment(id, frame, this.opts.lang));
|
|
889
|
+
if (frame.state === "final") this.pending.delete(id);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
};
|
|
893
|
+
function toSegment(segmentId, frame, lang) {
|
|
894
|
+
return {
|
|
895
|
+
segmentId,
|
|
896
|
+
rev: frame.rev ?? 0,
|
|
897
|
+
state: frame.state === "final" ? "final" : "interim",
|
|
898
|
+
epochMs: frame.epochMs ?? 0,
|
|
899
|
+
durationMs: frame.durationMs ?? 0,
|
|
900
|
+
text: frame.text ?? "",
|
|
901
|
+
translation: frame.translations?.[lang],
|
|
902
|
+
machineGenerated: true
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
|
|
795
906
|
// src/internal/freeze-clock.ts
|
|
796
907
|
var FreezeClock = class {
|
|
797
908
|
constructor(now = Date.now) {
|
|
@@ -961,6 +1072,19 @@ var MebiusPlayer = class extends TypedEmitter {
|
|
|
961
1072
|
this.video.volume = v;
|
|
962
1073
|
this.video.muted = v === 0;
|
|
963
1074
|
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Wall-clock time (Unix ms) currently on screen, or `null` when the active
|
|
1077
|
+
* route cannot produce one (HTTP-FLV, WHEP — see {@link ViewTransport}).
|
|
1078
|
+
*
|
|
1079
|
+
* This is what {@link MebiusClient.createCaptions} compares against a
|
|
1080
|
+
* segment's `epochMs` to know when it is due. Delegating to the transport
|
|
1081
|
+
* rather than reading the element directly is what keeps this correct across
|
|
1082
|
+
* a route failover: the player may switch from HLS to FLV mid-session, and
|
|
1083
|
+
* the clock source has to follow.
|
|
1084
|
+
*/
|
|
1085
|
+
currentEpochMs() {
|
|
1086
|
+
return this.transport?.playheadEpochMs?.() ?? null;
|
|
1087
|
+
}
|
|
964
1088
|
attach(transport) {
|
|
965
1089
|
transport.onEnded(() => {
|
|
966
1090
|
if (this.transport !== transport) return;
|
|
@@ -1055,6 +1179,16 @@ var SignalingClient = class {
|
|
|
1055
1179
|
scalePlaylistUrl(streamId) {
|
|
1056
1180
|
return this.withToken(`${this.base()}/live/${encodeURIComponent(streamId)}/index.m3u8`);
|
|
1057
1181
|
}
|
|
1182
|
+
/**
|
|
1183
|
+
* Realtime captions SSE URL. Same play token as media — the engine's
|
|
1184
|
+
* `PlayVerifier` gates both, so a viewer who can watch the stream can already
|
|
1185
|
+
* read its captions with zero extra credential.
|
|
1186
|
+
*/
|
|
1187
|
+
captionsUrl(streamId, lang) {
|
|
1188
|
+
return this.withToken(
|
|
1189
|
+
`${this.base()}/live/${encodeURIComponent(streamId)}/captions?lang=${encodeURIComponent(lang)}`
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1058
1192
|
// Maps a neutral session kind to the concrete signaling path segment. This
|
|
1059
1193
|
// mapping (publish -> WHIP, view -> WHEP) lives ONLY in this method body, so
|
|
1060
1194
|
// the protocol names never appear in any exported type signature.
|
|
@@ -1178,6 +1312,22 @@ var MebiusClient = class extends TypedEmitter {
|
|
|
1178
1312
|
this.assertConnected();
|
|
1179
1313
|
return new MebiusPlayer(this.signaling, { mode: "low-latency" }, this.deliveries, this.telemetry, this.userId);
|
|
1180
1314
|
}
|
|
1315
|
+
/**
|
|
1316
|
+
* Subscribe to a stream's realtime captions.
|
|
1317
|
+
*
|
|
1318
|
+
* Reads the same feed a session already produces — it does NOT start the
|
|
1319
|
+
* caption session itself. `captions/start` spends money and requires an API
|
|
1320
|
+
* key, so it belongs to your own backend (see
|
|
1321
|
+
* mebius-stream-engine/docs/API.md §5.1), called once when you want captions
|
|
1322
|
+
* on for a stream. This only ever consumes what that call turned on.
|
|
1323
|
+
*
|
|
1324
|
+
* `player` must be the one showing `streamId`: captions are timed against its
|
|
1325
|
+
* playhead, and a mismatched player would compare against the wrong clock.
|
|
1326
|
+
*/
|
|
1327
|
+
createCaptions(player, options) {
|
|
1328
|
+
this.assertConnected();
|
|
1329
|
+
return new MebiusCaptions(this.signaling, player, options);
|
|
1330
|
+
}
|
|
1181
1331
|
/** Close the connection and release resources. */
|
|
1182
1332
|
disconnect(reason) {
|
|
1183
1333
|
if (this.expiryTimer) clearTimeout(this.expiryTimer);
|
|
@@ -1223,6 +1373,7 @@ var Mebius = {
|
|
|
1223
1373
|
0 && (module.exports = {
|
|
1224
1374
|
Mebius,
|
|
1225
1375
|
MebiusBroadcaster,
|
|
1376
|
+
MebiusCaptions,
|
|
1226
1377
|
MebiusClient,
|
|
1227
1378
|
MebiusError,
|
|
1228
1379
|
MebiusPlayer,
|