@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.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({ maxLiveSyncPlaybackRate: 1.1 });
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,