@mebius-io/web 0.5.4 → 0.6.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.d.cts CHANGED
@@ -149,6 +149,13 @@ interface CaptionSegment {
149
149
  durationMs: number;
150
150
  /** Original transcript, in the source language. */
151
151
  text: string;
152
+ /**
153
+ * Language `text` is actually in, as the recogniser identified it. With an
154
+ * "auto" source this varies per sentence, so a client showing `text` as a
155
+ * subtitle must check this rather than assume it matches what the viewer
156
+ * asked for.
157
+ */
158
+ srcLang?: string;
152
159
  /** The requested {@link CaptionsOptions.lang} translation, if produced yet. */
153
160
  translation?: string;
154
161
  /** Always `true`. Render a machine-generated indicator — never as a direct quote. */
@@ -417,13 +424,37 @@ declare class MebiusCaptions extends TypedEmitter<CaptionsEventMap> {
417
424
  private es;
418
425
  private timer;
419
426
  private readonly pending;
427
+ /**
428
+ * segmentId -> the rev already handed to listeners. Not a Set: with interim
429
+ * revisions on, a segment stays pending while it is being corrected, and a
430
+ * plain "have I shown this id" check re-emits the same unchanged text on
431
+ * every 100ms tick. Comparing revs emits exactly once per actual revision.
432
+ */
420
433
  private readonly shown;
434
+ /** Kept so the feed can be reopened after a hidden-tab suspend. */
435
+ private streamId;
436
+ private hiddenTimer;
437
+ private onVisibility;
421
438
  /** @internal */
422
439
  constructor(signaling: SignalingClient, player: MebiusPlayer, opts: CaptionsOptions);
423
440
  /** Open the SSE connection and begin emitting segments for `streamId`. */
424
441
  start(streamId: string): void;
425
442
  /** Close the connection and drop all buffered segments. */
426
443
  stop(): void;
444
+ private openFeed;
445
+ private closeFeed;
446
+ /**
447
+ * Drop the feed while the page is hidden, restore it when it comes back.
448
+ *
449
+ * Nothing here is about rendering — a hidden tab shows no captions either
450
+ * way. It is about not holding a subscriber open, since that subscriber is
451
+ * what keeps a billed session alive on the engine.
452
+ *
453
+ * Buffered segments are deliberately kept across a suspend: they age out on
454
+ * their own (STALE_MS) and clearing them would blank the screen on return
455
+ * for no benefit.
456
+ */
457
+ private watchVisibility;
427
458
  private onFrame;
428
459
  private tick;
429
460
  }
package/dist/index.d.ts CHANGED
@@ -149,6 +149,13 @@ interface CaptionSegment {
149
149
  durationMs: number;
150
150
  /** Original transcript, in the source language. */
151
151
  text: string;
152
+ /**
153
+ * Language `text` is actually in, as the recogniser identified it. With an
154
+ * "auto" source this varies per sentence, so a client showing `text` as a
155
+ * subtitle must check this rather than assume it matches what the viewer
156
+ * asked for.
157
+ */
158
+ srcLang?: string;
152
159
  /** The requested {@link CaptionsOptions.lang} translation, if produced yet. */
153
160
  translation?: string;
154
161
  /** Always `true`. Render a machine-generated indicator — never as a direct quote. */
@@ -417,13 +424,37 @@ declare class MebiusCaptions extends TypedEmitter<CaptionsEventMap> {
417
424
  private es;
418
425
  private timer;
419
426
  private readonly pending;
427
+ /**
428
+ * segmentId -> the rev already handed to listeners. Not a Set: with interim
429
+ * revisions on, a segment stays pending while it is being corrected, and a
430
+ * plain "have I shown this id" check re-emits the same unchanged text on
431
+ * every 100ms tick. Comparing revs emits exactly once per actual revision.
432
+ */
420
433
  private readonly shown;
434
+ /** Kept so the feed can be reopened after a hidden-tab suspend. */
435
+ private streamId;
436
+ private hiddenTimer;
437
+ private onVisibility;
421
438
  /** @internal */
422
439
  constructor(signaling: SignalingClient, player: MebiusPlayer, opts: CaptionsOptions);
423
440
  /** Open the SSE connection and begin emitting segments for `streamId`. */
424
441
  start(streamId: string): void;
425
442
  /** Close the connection and drop all buffered segments. */
426
443
  stop(): void;
444
+ private openFeed;
445
+ private closeFeed;
446
+ /**
447
+ * Drop the feed while the page is hidden, restore it when it comes back.
448
+ *
449
+ * Nothing here is about rendering — a hidden tab shows no captions either
450
+ * way. It is about not holding a subscriber open, since that subscriber is
451
+ * what keeps a billed session alive on the engine.
452
+ *
453
+ * Buffered segments are deliberately kept across a suspend: they age out on
454
+ * their own (STALE_MS) and clearing them would blank the screen on return
455
+ * for no benefit.
456
+ */
457
+ private watchVisibility;
427
458
  private onFrame;
428
459
  private tick;
429
460
  }
@@ -43428,6 +43428,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43428
43428
  var TICK_MS = 100;
43429
43429
  var STALE_MS = 5e3;
43430
43430
  var MAX_QUEUE_MS = 1500;
43431
+ var HIDDEN_GRACE_MS = 6e4;
43431
43432
  var MebiusCaptions = class extends TypedEmitter {
43432
43433
  /** @internal */
43433
43434
  constructor(signaling, player, opts) {
@@ -43438,26 +43439,81 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43438
43439
  this.es = null;
43439
43440
  this.timer = null;
43440
43441
  this.pending = /* @__PURE__ */ new Map();
43441
- this.shown = /* @__PURE__ */ new Set();
43442
+ /**
43443
+ * segmentId -> the rev already handed to listeners. Not a Set: with interim
43444
+ * revisions on, a segment stays pending while it is being corrected, and a
43445
+ * plain "have I shown this id" check re-emits the same unchanged text on
43446
+ * every 100ms tick. Comparing revs emits exactly once per actual revision.
43447
+ */
43448
+ this.shown = /* @__PURE__ */ new Map();
43449
+ /** Kept so the feed can be reopened after a hidden-tab suspend. */
43450
+ this.streamId = null;
43451
+ this.hiddenTimer = null;
43452
+ this.onVisibility = null;
43442
43453
  }
43443
43454
  /** Open the SSE connection and begin emitting segments for `streamId`. */
43444
43455
  start(streamId) {
43445
43456
  if (this.es) return;
43446
- const url = this.signaling.captionsUrl(streamId, this.opts.lang);
43447
- const es = new EventSource(url);
43457
+ this.streamId = streamId;
43458
+ this.openFeed();
43459
+ this.watchVisibility();
43460
+ }
43461
+ /** Close the connection and drop all buffered segments. */
43462
+ stop() {
43463
+ this.streamId = null;
43464
+ if (this.onVisibility && typeof document !== "undefined") {
43465
+ document.removeEventListener("visibilitychange", this.onVisibility);
43466
+ }
43467
+ this.onVisibility = null;
43468
+ if (this.hiddenTimer) clearTimeout(this.hiddenTimer);
43469
+ this.hiddenTimer = null;
43470
+ this.closeFeed();
43471
+ this.pending.clear();
43472
+ this.shown.clear();
43473
+ }
43474
+ openFeed() {
43475
+ if (this.es || !this.streamId) return;
43476
+ const es = new EventSource(this.signaling.captionsUrl(this.streamId, this.opts.lang));
43448
43477
  es.onmessage = (ev) => this.onFrame(ev);
43449
43478
  es.onerror = () => this.emit("error", void 0);
43450
43479
  this.es = es;
43451
43480
  this.timer = setInterval(() => this.tick(), TICK_MS);
43452
43481
  }
43453
- /** Close the connection and drop all buffered segments. */
43454
- stop() {
43482
+ closeFeed() {
43455
43483
  this.es?.close();
43456
43484
  this.es = null;
43457
43485
  if (this.timer) clearInterval(this.timer);
43458
43486
  this.timer = null;
43459
- this.pending.clear();
43460
- this.shown.clear();
43487
+ }
43488
+ /**
43489
+ * Drop the feed while the page is hidden, restore it when it comes back.
43490
+ *
43491
+ * Nothing here is about rendering — a hidden tab shows no captions either
43492
+ * way. It is about not holding a subscriber open, since that subscriber is
43493
+ * what keeps a billed session alive on the engine.
43494
+ *
43495
+ * Buffered segments are deliberately kept across a suspend: they age out on
43496
+ * their own (STALE_MS) and clearing them would blank the screen on return
43497
+ * for no benefit.
43498
+ */
43499
+ watchVisibility() {
43500
+ if (typeof document === "undefined") return;
43501
+ this.onVisibility = () => {
43502
+ if (document.visibilityState === "hidden") {
43503
+ if (this.hiddenTimer) return;
43504
+ this.hiddenTimer = setTimeout(() => {
43505
+ this.hiddenTimer = null;
43506
+ this.closeFeed();
43507
+ }, HIDDEN_GRACE_MS);
43508
+ return;
43509
+ }
43510
+ if (this.hiddenTimer) {
43511
+ clearTimeout(this.hiddenTimer);
43512
+ this.hiddenTimer = null;
43513
+ }
43514
+ this.openFeed();
43515
+ };
43516
+ document.addEventListener("visibilitychange", this.onVisibility);
43461
43517
  }
43462
43518
  onFrame(ev) {
43463
43519
  let frame;
@@ -43487,8 +43543,9 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43487
43543
  this.emit("cleared", { segmentId: id });
43488
43544
  continue;
43489
43545
  }
43490
- if (this.shown.has(id) && frame.state === "final") continue;
43491
- this.shown.add(id);
43546
+ const rev = frame.rev ?? 0;
43547
+ if (this.shown.get(id) === rev) continue;
43548
+ this.shown.set(id, rev);
43492
43549
  this.emit("segment", toSegment(id, frame, this.opts.lang));
43493
43550
  }
43494
43551
  }
@@ -43501,6 +43558,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
43501
43558
  epochMs: frame.epochMs ?? 0,
43502
43559
  durationMs: frame.durationMs ?? 0,
43503
43560
  text: frame.text ?? "",
43561
+ srcLang: frame.srcLang,
43504
43562
  translation: frame.translations?.[lang],
43505
43563
  machineGenerated: true
43506
43564
  };