@torrent-tv/proxy 2.64.6 → 2.64.7

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/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## 2.64.7
2
+
3
+ - **Fix**: A file opened at a position starts its SOUND at that position. The audio rendition's start is worked out from the picture's read head less the buffer the viewer reports holding — sound, because a read head is the furthest request of any viewer and the picture sits behind it by however deep that buffer is. At a cold open there is no report yet, and the fallback subtracted the WHOLE 120 s look-ahead from a buffer that does not exist: field 2026-08-31, a page opened at 588 s started its sound at 460 s, 131 seconds of film nobody would hear, and the segment the viewer needed took 38.8 s to appear against the picture's 8.4 s — the audio encoder healthy at 2.3-3.1x throughout, simply given a running start it did not need. The reading now says WHICH of its three sources answered (`viewerPositionSource`): a seek and a served segment are request edges and keep the subtraction, while the opening position is not an edge — nothing has been asked for since the session was made, and a browser that has just opened holds nothing by construction (`research/cold-open-audio-start-2026-08-31.md`).
4
+ - **Fix**: The first encode run is positioned from the position the viewer ASKED for, not from the figure rounded to ten seconds. That rounding exists to answer one question — whether two viewers share a session — and `Math.round` can move a position FORWARD: 588 s became 590 s, which falls in segment #85 while the viewer at 588 s is inside #84. The player asked for a segment behind the run, the run was restarted onto it, and the 4.5 s it had already produced were thrown away.
5
+
1
6
  ## 2.64.6
2
7
 
3
8
  - **New**: The torrent worker reads its own memory once a SECOND, and writes a line only when something moved. A minute cannot see what kills it: three times — 2026-08-30 14:00 and 23:19, 2026-08-31 13:27 — the worker's own line read `heap=28-36MB`, and by the sample after next the thread had been terminated for reaching its heap ceiling, with the whole rise fitting inside a single sixty-second gap. The reading and the line are now separate cadences: taken every second, written when the heap has moved by 25 MB or when a quiet minute is up, so a healthy session costs the same one line a minute it costs today and a runaway is a curve rather than a step.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.64.6",
3
+ "version": "2.64.7",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "publishConfig": {
@@ -1257,6 +1257,34 @@ function headsOf(session) {
1257
1257
  return session.consumerHeads;
1258
1258
  }
1259
1259
 
1260
+ /**
1261
+ * WHICH of the three readings answered, which is a different question from what
1262
+ * the answer was.
1263
+ *
1264
+ * It matters for one thing: `openedAt` is not a request edge. The other two are
1265
+ * — a seek and a requested segment are both places a viewer has moved to while
1266
+ * holding a buffer, so the picture is behind them by however deep that buffer
1267
+ * is. `openedAt` is where a session was created and nothing has been asked for
1268
+ * since, so the picture is exactly there and there is nothing to subtract.
1269
+ * Reading the number without knowing which of the three it was is what started
1270
+ * a cold open's audio two minutes early on 2026-08-31.
1271
+ *
1272
+ * @param {{ seeked?: number, lastRequestedStart?: number | null, openedAt?: number }} readings
1273
+ * @returns {"seeked" | "requested" | "opened" | "none"}
1274
+ */
1275
+ export function viewerPositionSource({ seeked, lastRequestedStart, openedAt }) {
1276
+ if (Number.isFinite(seeked) && seeked > 0) {
1277
+ return "seeked";
1278
+ }
1279
+ if (Number.isFinite(lastRequestedStart) && lastRequestedStart > 0) {
1280
+ return "requested";
1281
+ }
1282
+ if (Number.isFinite(openedAt) && openedAt > 0) {
1283
+ return "opened";
1284
+ }
1285
+ return "none";
1286
+ }
1287
+
1260
1288
  export function resolveViewerPosition({ seeked, lastRequestedStart, openedAt }) {
1261
1289
  if (Number.isFinite(seeked) && seeked > 0) {
1262
1290
  return seeked;
@@ -2408,8 +2436,19 @@ export class HlsSessionManager {
2408
2436
  // asked for #152, and 45 s later the browser gave up with "no data arrived
2409
2437
  // from the proxy" while the transcode ran happily at 9.9x through the
2410
2438
  // opening credits.
2411
- const firstIndex = normalizedStartPosition > 0
2412
- ? this.#segmentIndexForTime(session, normalizedStartPosition)
2439
+ // From what the viewer ASKED for, not from the rounded figure. The rounding
2440
+ // exists to answer one question — is this the same session as somebody
2441
+ // else's — and it is the wrong number for this one, because `Math.round`
2442
+ // can move the position FORWARD: 588s became 590s, which falls in segment
2443
+ // #85 while the viewer at 588s is inside #84. The player then asked for a
2444
+ // segment behind the run, the run was restarted onto it, and the 4.5s it
2445
+ // had produced were thrown away (field 2026-08-31,
2446
+ // `research/cold-open-audio-start-2026-08-31.md`).
2447
+ const requestedStart = Number.isFinite(startPositionSeconds) && startPositionSeconds > 0
2448
+ ? startPositionSeconds
2449
+ : 0;
2450
+ const firstIndex = requestedStart > 0
2451
+ ? this.#segmentIndexForTime(session, requestedStart)
2413
2452
  : 0;
2414
2453
  await this.#startEncodeRun(session, firstIndex);
2415
2454
 
@@ -7824,10 +7863,40 @@ export class HlsSessionManager {
7824
7863
  // would start the run where no request can ever reach it.
7825
7864
  return Math.max(0, Math.min(earliestStated, readHead) - this.segmentDurationSec);
7826
7865
  }
7866
+ if (this.#viewerPositionSourceOf(watching) === "opened") {
7867
+ // The session has not started. Nobody has seeked, nobody has asked for a
7868
+ // segment, and nobody has reported anything — so the read head is not a
7869
+ // request edge at all, it is where the viewer opened, and a browser that
7870
+ // has just opened holds no buffer by construction. Subtracting one here
7871
+ // is not erring "early, the cheap direction": it is the whole of the
7872
+ // start-up cost. Field 2026-08-31: a page opened at 588s started its
7873
+ // sound at 460s, 131 seconds of film nobody would hear, and the segment
7874
+ // the viewer needed took 38.8s to appear against the picture's 8.4s
7875
+ // (`research/cold-open-audio-start-2026-08-31.md`).
7876
+ return Math.max(0, readHead - this.segmentDurationSec);
7877
+ }
7827
7878
  const buffered = deepestBuffer === null ? LOOKAHEAD_PAUSE_SECONDS : deepestBuffer;
7828
7879
  return Math.max(0, readHead - buffered - this.segmentDurationSec);
7829
7880
  }
7830
7881
 
7882
+ /**
7883
+ * Which reading gave this session's viewer position — see
7884
+ * {@link viewerPositionSource}.
7885
+ *
7886
+ * @param {HlsSession} session
7887
+ * @returns {"seeked" | "requested" | "opened" | "none"}
7888
+ */
7889
+ #viewerPositionSourceOf(session) {
7890
+ const lastRequestedStart = Number.isInteger(session.lastRequestedSegment) && session.lastRequestedSegment > 0
7891
+ ? this.#segmentStartTime(session, session.lastRequestedSegment)
7892
+ : null;
7893
+ return viewerPositionSource({
7894
+ seeked: session.viewerPositionSeconds,
7895
+ lastRequestedStart,
7896
+ openedAt: session.progress?.startPositionSeconds
7897
+ });
7898
+ }
7899
+
7831
7900
  #viewerPositionOf(session) {
7832
7901
  const lastRequestedStart = Number.isInteger(session.lastRequestedSegment) && session.lastRequestedSegment > 0
7833
7902
  ? this.#segmentStartTime(session, session.lastRequestedSegment)
@@ -1071,3 +1071,42 @@ test("a rung is never served from the COPY, whatever height the copy happens to
1071
1071
 
1072
1072
  assert.equal(asked.id, VARIANT_ID, "the re-encoded rung is its own session, not the copy");
1073
1073
  });
1074
+
1075
+ test("a file opened at a position starts its sound THERE, not a look-ahead earlier", async (t) => {
1076
+ const { manager, base, dirPath } = await managerWithBase();
1077
+ t.after(async () => {
1078
+ await manager.disposeAll();
1079
+ await rm(dirPath, { recursive: true, force: true });
1080
+ });
1081
+ base.audioSeparate = true;
1082
+ // The state at the instant a page is opened at a position: nothing seeked,
1083
+ // no segment served, no report from anybody. The read head is then not a
1084
+ // request edge — it is where the session was made — and a browser that has
1085
+ // just opened holds no buffer at all.
1086
+ base.viewerPositionSeconds = null;
1087
+ base.lastRequestedSegment = null;
1088
+ base.netReports.clear();
1089
+ base.progress.startPositionSeconds = 588;
1090
+ manager.getCachedAudioTracks = () => [
1091
+ { index: 0, language: "rus", title: "", isDefault: true },
1092
+ { index: 1, language: "eng", title: "", isDefault: false }
1093
+ ];
1094
+ const created = [];
1095
+ manager.createOrGetSession = async (params) => {
1096
+ created.push(params);
1097
+ const rendition = fakeSession({ id: VARIANT_ID, encodeHeight: 0, dirPath });
1098
+ rendition.audioOnly = true;
1099
+ return { sessionId: VARIANT_ID, session: rendition };
1100
+ };
1101
+
1102
+ await manager.resolveAudioRenditionFile(BASE_ID, 1, "segment-00010.mp4");
1103
+
1104
+ // Field 2026-08-31: this answered 460 for a page opened at 588 — the whole
1105
+ // 120 s look-ahead subtracted from a buffer that did not exist — and the
1106
+ // segment the viewer needed took 38.8 s to appear against the picture's 8.4 s.
1107
+ assert.equal(
1108
+ created[0].startPositionSeconds,
1109
+ 584,
1110
+ "where the viewer opened, less one segment of margin, and nothing else"
1111
+ );
1112
+ });
@@ -22,7 +22,7 @@
22
22
  import assert from "node:assert/strict";
23
23
  import test from "node:test";
24
24
 
25
- import { resolveViewerPosition } from "../services/hls-session-manager.js";
25
+ import { resolveViewerPosition, viewerPositionSource } from "../services/hls-session-manager.js";
26
26
 
27
27
  test("a file opened at a position has its viewer at that position", () => {
28
28
  // Nothing has been seeked and nothing served yet — the state at the instant
@@ -51,3 +51,42 @@ test("with nothing to go on the answer is the beginning", () => {
51
51
  assert.equal(resolveViewerPosition({ seeked: -5, openedAt: -5 }), 0);
52
52
  assert.equal(resolveViewerPosition({ lastRequestedStart: null, openedAt: undefined }), 0);
53
53
  });
54
+
55
+ /**
56
+ * Which of the three answered is a separate question, and the audio start needs
57
+ * it. A seek and a served segment are request edges — the picture is behind
58
+ * them by however deep the viewer's buffer is, which is what the subtraction in
59
+ * `#audioStartSecondsFor` converts. The opening position is not an edge: it is
60
+ * where the session was made, nothing has been asked for since, and a browser
61
+ * that has just opened holds nothing.
62
+ *
63
+ * Field 2026-08-31: a page opened at 588s, no report yet, and the whole 120 s
64
+ * look-ahead was subtracted — the sound started at 460s and its first segment
65
+ * took 38.8 s to appear against the picture's 8.4 s.
66
+ */
67
+ test("the reading says which of the three it came from", () => {
68
+ assert.equal(viewerPositionSource({ seeked: 900, lastRequestedStart: 400, openedAt: 3130 }), "seeked");
69
+ assert.equal(viewerPositionSource({ lastRequestedStart: 400, openedAt: 3130 }), "requested");
70
+ assert.equal(viewerPositionSource({ openedAt: 3130 }), "opened");
71
+ assert.equal(viewerPositionSource({}), "none");
72
+ });
73
+
74
+ test("the source agrees with the position, reading for reading", () => {
75
+ const readings = [
76
+ { seeked: 900, lastRequestedStart: 400, openedAt: 3130 },
77
+ { lastRequestedStart: 400, openedAt: 3130 },
78
+ { openedAt: 3130 },
79
+ { seeked: Number.NaN, openedAt: Number.NaN },
80
+ { seeked: -5, openedAt: -5 },
81
+ {}
82
+ ];
83
+ for (const reading of readings) {
84
+ const position = resolveViewerPosition(reading);
85
+ const source = viewerPositionSource(reading);
86
+ assert.equal(
87
+ source === "none",
88
+ position === 0,
89
+ `no source must mean no position, and the other way round: ${JSON.stringify(reading)}`
90
+ );
91
+ }
92
+ });