@mebius-io/web 0.4.9 → 0.5.1
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 +140 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +122 -1
- package/dist/index.d.ts +122 -1
- package/dist/index.global.js +152 -13
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +139 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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,
|
|
@@ -439,6 +440,22 @@ var HlsViewTransport = class {
|
|
|
439
440
|
framesPerSecond: 0
|
|
440
441
|
};
|
|
441
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
|
+
}
|
|
442
459
|
};
|
|
443
460
|
|
|
444
461
|
// src/internal/balanced-view-transport.ts
|
|
@@ -810,6 +827,82 @@ function normalize(c, fallback) {
|
|
|
810
827
|
return c;
|
|
811
828
|
}
|
|
812
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
|
+
|
|
813
906
|
// src/internal/freeze-clock.ts
|
|
814
907
|
var FreezeClock = class {
|
|
815
908
|
constructor(now = Date.now) {
|
|
@@ -979,6 +1072,19 @@ var MebiusPlayer = class extends TypedEmitter {
|
|
|
979
1072
|
this.video.volume = v;
|
|
980
1073
|
this.video.muted = v === 0;
|
|
981
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
|
+
}
|
|
982
1088
|
attach(transport) {
|
|
983
1089
|
transport.onEnded(() => {
|
|
984
1090
|
if (this.transport !== transport) return;
|
|
@@ -1073,6 +1179,23 @@ var SignalingClient = class {
|
|
|
1073
1179
|
scalePlaylistUrl(streamId) {
|
|
1074
1180
|
return this.withToken(`${this.base()}/live/${encodeURIComponent(streamId)}/index.m3u8`);
|
|
1075
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
|
+
* `/api/v1/live/...`, NOT `/live/...`: unlike {@link scalePlaylistUrl}, which
|
|
1188
|
+
* hits the engine's bare `/live/*` media-edge catch-all, captions are mounted
|
|
1189
|
+
* under the versioned control-API group (mebius-stream-engine
|
|
1190
|
+
* internal/api/routes.go) with the play-token middleware, not the proxy.
|
|
1191
|
+
* Copying the media-edge prefix here 401s every request — the catch-all
|
|
1192
|
+
* doesn't recognise the path and never reaches the captions handler at all.
|
|
1193
|
+
*/
|
|
1194
|
+
captionsUrl(streamId, lang) {
|
|
1195
|
+
return this.withToken(
|
|
1196
|
+
`${this.base()}/api/v1/live/${encodeURIComponent(streamId)}/captions?lang=${encodeURIComponent(lang)}`
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1076
1199
|
// Maps a neutral session kind to the concrete signaling path segment. This
|
|
1077
1200
|
// mapping (publish -> WHIP, view -> WHEP) lives ONLY in this method body, so
|
|
1078
1201
|
// the protocol names never appear in any exported type signature.
|
|
@@ -1196,6 +1319,22 @@ var MebiusClient = class extends TypedEmitter {
|
|
|
1196
1319
|
this.assertConnected();
|
|
1197
1320
|
return new MebiusPlayer(this.signaling, { mode: "low-latency" }, this.deliveries, this.telemetry, this.userId);
|
|
1198
1321
|
}
|
|
1322
|
+
/**
|
|
1323
|
+
* Subscribe to a stream's realtime captions.
|
|
1324
|
+
*
|
|
1325
|
+
* Reads the same feed a session already produces — it does NOT start the
|
|
1326
|
+
* caption session itself. `captions/start` spends money and requires an API
|
|
1327
|
+
* key, so it belongs to your own backend (see
|
|
1328
|
+
* mebius-stream-engine/docs/API.md §5.1), called once when you want captions
|
|
1329
|
+
* on for a stream. This only ever consumes what that call turned on.
|
|
1330
|
+
*
|
|
1331
|
+
* `player` must be the one showing `streamId`: captions are timed against its
|
|
1332
|
+
* playhead, and a mismatched player would compare against the wrong clock.
|
|
1333
|
+
*/
|
|
1334
|
+
createCaptions(player, options) {
|
|
1335
|
+
this.assertConnected();
|
|
1336
|
+
return new MebiusCaptions(this.signaling, player, options);
|
|
1337
|
+
}
|
|
1199
1338
|
/** Close the connection and release resources. */
|
|
1200
1339
|
disconnect(reason) {
|
|
1201
1340
|
if (this.expiryTimer) clearTimeout(this.expiryTimer);
|
|
@@ -1241,6 +1380,7 @@ var Mebius = {
|
|
|
1241
1380
|
0 && (module.exports = {
|
|
1242
1381
|
Mebius,
|
|
1243
1382
|
MebiusBroadcaster,
|
|
1383
|
+
MebiusCaptions,
|
|
1244
1384
|
MebiusClient,
|
|
1245
1385
|
MebiusError,
|
|
1246
1386
|
MebiusPlayer,
|