@torrent-tv/proxy 2.105.6 → 2.105.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.105.6",
3
+ "version": "2.105.8",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
package/server.js CHANGED
@@ -22,7 +22,7 @@ import { probePackets } from "./services/media/probe-packets.js";
22
22
  import { trackChoicesOf } from "./services/viewer/segment-demands.js";
23
23
  import { PlaybackProjection } from "./services/torrent/PlaybackProjection.js";
24
24
  import { SegmentInputTable } from "./services/viewer/segment-input-table.js";
25
- import { SourcePreparation } from "./services/viewer/SourcePreparation.js";
25
+ import { SourcePreparation, preparationInterval } from "./services/viewer/SourcePreparation.js";
26
26
  import { handleHealthzGet } from "./routes/healthz/get.js";
27
27
  import { handleApiDeliverySinkGet } from "./routes/api/delivery-sink/get.js";
28
28
  import { handleApiLinkProbeGet } from "./routes/api/link-probe/get.js";
@@ -1403,7 +1403,7 @@ export async function startProxyServer({
1403
1403
  (work.ownerId === undefined || viewer.id === work.ownerId))
1404
1404
  .map(viewer => viewer.positionSeconds() ?? 0);
1405
1405
  const from = (positions.length ? Math.min(...positions) : 0) + (Number(info.value?.startTimeSeconds) || 0);
1406
- params.packetInterval = { from, to: from + outputParts.segmentDurationSec };
1406
+ params.packetInterval = preparationInterval(work.role, from, outputParts.segmentDurationSec);
1407
1407
  if (work.role === "subtitle-embedded") {
1408
1408
  const tracks = await containerOrchestrator.containerFor(params).then(container => container.readTracks());
1409
1409
  const track = tracks.find(track => track.type === "subtitle" && track.declaredIndex === work.trackIndex);
@@ -76,7 +76,14 @@ export class SubtitleOrchestrator {
76
76
  endSeconds: packet.duration > 0 ? packet.pts + packet.duration : null, text };
77
77
  pending.push({ identity, cue });
78
78
  }
79
- if (params.isCurrent?.() === false || this.#packetCues.get(key) !== state) {
79
+ // A cue read from the file is a fact about the file, not about where
80
+ // the viewer stood when the read began. A position report moves that
81
+ // stand every 1.5 s, so a read that crossed one used to throw away the
82
+ // cues it had read, and those lines never reached the page
83
+ // (torrent-tv/meta#7). They are kept and published; only a forgotten
84
+ // source cannot take them.
85
+ const obsolete = params.isCurrent?.() === false;
86
+ if (this.#packetCues.get(key) !== state) {
80
87
  return { kind: "terminal", reason: "request-obsolete", requestId };
81
88
  }
82
89
  state.ready = true;
@@ -96,7 +103,7 @@ export class SubtitleOrchestrator {
96
103
  }
97
104
  }
98
105
  const fresh = TextSubtitleTrack.finalizeCues(ordered(), track.codecId).filter(cue => !previous.has(cue.seq));
99
- if (fresh.length && params.isCurrent?.() !== false) {
106
+ if (fresh.length) {
100
107
  const cues = fresh;
101
108
  this.cues.publish?.({ sourceKey: params.sourceKey, fileIndex: params.fileIndex,
102
109
  trackIndex: track.declaredIndex, cues, withdrawn, language: track.language ?? "",
@@ -104,6 +111,7 @@ export class SubtitleOrchestrator {
104
111
  cursor: state.cursor, spanStartSeconds: cues[0]?.startSeconds ?? null,
105
112
  spanEndSeconds: cues.at(-1)?.endSeconds ?? null, walkedClusters: 0, indexedClusters: 0 });
106
113
  }
114
+ if (obsolete) return { kind: "terminal", reason: "request-obsolete", requestId };
107
115
  result = { kind: "result", value: { cues: [...state.packets.values()], track, coveredClusters: 0, indexedClusters: 0 }, requestId };
108
116
  }
109
117
  } catch (error) {
@@ -1,3 +1,31 @@
1
+ /**
2
+ * How many segments ahead of the viewer a chosen embedded subtitle is read.
3
+ *
4
+ * A reading window is taken again only when the viewer is planned again, which
5
+ * is when their report says they entered the next segment, and it starts where
6
+ * that report put them. Reports come every 1.5 s, so a window one segment long
7
+ * ended before the next one began whenever that report came later in its
8
+ * segment than the one before: a cue between the two was never read, and a
9
+ * cue just past a window was read only once the viewer had reached it (field
10
+ * 2026-10-22, Drifters 07: 1 of 27 cues missing, 7 late by 0.2-1.6 s,
11
+ * torrent-tv/meta#7). Two segments let each window reach past the start of the
12
+ * next while the viewer is planned at least once a segment.
13
+ */
14
+ const SUBTITLE_SEGMENTS_AHEAD = 2;
15
+
16
+ /**
17
+ * The stretch of film one preparation read covers from where its viewers stand.
18
+ *
19
+ * @param {string | undefined} role
20
+ * @param {number} from - Seconds on the file's own clock.
21
+ * @param {number} segmentDurationSec
22
+ * @returns {{ from: number, to: number }}
23
+ */
24
+ export function preparationInterval(role, from, segmentDurationSec) {
25
+ const segments = role === "subtitle-embedded" ? SUBTITLE_SEGMENTS_AHEAD : 1;
26
+ return { from, to: from + segments * segmentDurationSec };
27
+ }
28
+
1
29
  /** Source preparation is derived from viewers before any output exists. */
2
30
  export class SourcePreparation {
3
31
  #viewers;