@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.js
CHANGED
|
@@ -398,6 +398,22 @@ var HlsViewTransport = class {
|
|
|
398
398
|
framesPerSecond: 0
|
|
399
399
|
};
|
|
400
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
|
+
}
|
|
401
417
|
};
|
|
402
418
|
|
|
403
419
|
// src/internal/balanced-view-transport.ts
|
|
@@ -769,6 +785,82 @@ function normalize(c, fallback) {
|
|
|
769
785
|
return c;
|
|
770
786
|
}
|
|
771
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
|
+
|
|
772
864
|
// src/internal/freeze-clock.ts
|
|
773
865
|
var FreezeClock = class {
|
|
774
866
|
constructor(now = Date.now) {
|
|
@@ -938,6 +1030,19 @@ var MebiusPlayer = class extends TypedEmitter {
|
|
|
938
1030
|
this.video.volume = v;
|
|
939
1031
|
this.video.muted = v === 0;
|
|
940
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
|
+
}
|
|
941
1046
|
attach(transport) {
|
|
942
1047
|
transport.onEnded(() => {
|
|
943
1048
|
if (this.transport !== transport) return;
|
|
@@ -1032,6 +1137,23 @@ var SignalingClient = class {
|
|
|
1032
1137
|
scalePlaylistUrl(streamId) {
|
|
1033
1138
|
return this.withToken(`${this.base()}/live/${encodeURIComponent(streamId)}/index.m3u8`);
|
|
1034
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
|
+
* `/api/v1/live/...`, NOT `/live/...`: unlike {@link scalePlaylistUrl}, which
|
|
1146
|
+
* hits the engine's bare `/live/*` media-edge catch-all, captions are mounted
|
|
1147
|
+
* under the versioned control-API group (mebius-stream-engine
|
|
1148
|
+
* internal/api/routes.go) with the play-token middleware, not the proxy.
|
|
1149
|
+
* Copying the media-edge prefix here 401s every request — the catch-all
|
|
1150
|
+
* doesn't recognise the path and never reaches the captions handler at all.
|
|
1151
|
+
*/
|
|
1152
|
+
captionsUrl(streamId, lang) {
|
|
1153
|
+
return this.withToken(
|
|
1154
|
+
`${this.base()}/api/v1/live/${encodeURIComponent(streamId)}/captions?lang=${encodeURIComponent(lang)}`
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1035
1157
|
// Maps a neutral session kind to the concrete signaling path segment. This
|
|
1036
1158
|
// mapping (publish -> WHIP, view -> WHEP) lives ONLY in this method body, so
|
|
1037
1159
|
// the protocol names never appear in any exported type signature.
|
|
@@ -1155,6 +1277,22 @@ var MebiusClient = class extends TypedEmitter {
|
|
|
1155
1277
|
this.assertConnected();
|
|
1156
1278
|
return new MebiusPlayer(this.signaling, { mode: "low-latency" }, this.deliveries, this.telemetry, this.userId);
|
|
1157
1279
|
}
|
|
1280
|
+
/**
|
|
1281
|
+
* Subscribe to a stream's realtime captions.
|
|
1282
|
+
*
|
|
1283
|
+
* Reads the same feed a session already produces — it does NOT start the
|
|
1284
|
+
* caption session itself. `captions/start` spends money and requires an API
|
|
1285
|
+
* key, so it belongs to your own backend (see
|
|
1286
|
+
* mebius-stream-engine/docs/API.md §5.1), called once when you want captions
|
|
1287
|
+
* on for a stream. This only ever consumes what that call turned on.
|
|
1288
|
+
*
|
|
1289
|
+
* `player` must be the one showing `streamId`: captions are timed against its
|
|
1290
|
+
* playhead, and a mismatched player would compare against the wrong clock.
|
|
1291
|
+
*/
|
|
1292
|
+
createCaptions(player, options) {
|
|
1293
|
+
this.assertConnected();
|
|
1294
|
+
return new MebiusCaptions(this.signaling, player, options);
|
|
1295
|
+
}
|
|
1158
1296
|
/** Close the connection and release resources. */
|
|
1159
1297
|
disconnect(reason) {
|
|
1160
1298
|
if (this.expiryTimer) clearTimeout(this.expiryTimer);
|
|
@@ -1199,6 +1337,7 @@ var Mebius = {
|
|
|
1199
1337
|
export {
|
|
1200
1338
|
Mebius,
|
|
1201
1339
|
MebiusBroadcaster,
|
|
1340
|
+
MebiusCaptions,
|
|
1202
1341
|
MebiusClient,
|
|
1203
1342
|
MebiusError,
|
|
1204
1343
|
MebiusPlayer,
|