@torrent-tv/proxy 2.88.6 → 2.89.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.
Files changed (45) hide show
  1. package/AGENTS.md +21 -12
  2. package/CHANGELOG.md +17 -3
  3. package/assets/calibration/audio/NOTICE.md +19 -19
  4. package/assets/calibration/audio/manifest.json +98 -98
  5. package/docs/container-architecture.md +77 -22
  6. package/docs/playback-start-forecast.md +56 -56
  7. package/main2.cpuprofile +1 -0
  8. package/package.json +1 -1
  9. package/scripts/build-audio-samples.mjs +31 -31
  10. package/server.js +42 -27
  11. package/services/encode/OutputOpening.js +5 -4
  12. package/services/encode/audio-calibration.js +67 -67
  13. package/services/encode/audio-work-rate.js +15 -15
  14. package/services/media/ContainerOrchestrator.js +99 -46
  15. package/services/media/KeyframeTables.js +61 -21
  16. package/services/media/SubtitleCues.js +255 -183
  17. package/services/media/container/AviContainer.js +5 -3
  18. package/services/media/container/Container.js +66 -43
  19. package/services/media/container/ContainerFactory.js +8 -26
  20. package/services/media/container/MatroskaContainer.js +719 -1182
  21. package/services/media/container/Mp4Container.js +83 -73
  22. package/services/media/container/ebml-stream.js +174 -0
  23. package/services/media/container/matroska-clusters.js +1165 -0
  24. package/services/media/container/unavailable.js +77 -0
  25. package/services/media/tracks/ContainerTrack.js +9 -0
  26. package/services/media/tracks/TextSubtitleTrack.js +9 -1
  27. package/services/server/playback-readiness.js +516 -516
  28. package/services/torrent/worker/client.js +6 -2
  29. package/services/torrent/worker/held-bytes.js +84 -8
  30. package/services/torrent/worker/worker.js +10 -2
  31. package/services/transport/data-channel-handler.js +7 -3
  32. package/services/viewer/PriorityMap.js +31 -0
  33. package/test/audio-calibration.test.js +23 -23
  34. package/test/coalesce.test.js +39 -0
  35. package/test/held-read-ownership.test.js +77 -0
  36. package/test/helpers/held-file.js +50 -2
  37. package/test/helpers/matroska-file.js +296 -0
  38. package/test/keyframe-table-budget.test.js +21 -0
  39. package/test/matroska-cluster-discovery.test.js +323 -0
  40. package/test/reads-not-yet-arrived.test.js +218 -0
  41. package/test/subtitle-cue-walk.test.js +16 -1
  42. package/test/subtitle-plan-waits.test.js +125 -0
  43. package/test/subtitle-track-numbering.test.js +8 -3
  44. package/test/viewer-starts-on-map.test.js +27 -0
  45. package/utils/coalesce.js +41 -0
@@ -16,7 +16,12 @@
16
16
  * it. Cancelling it would make every later viewer pay the whole wait again;
17
17
  * 3. **a read that THREW is not an answer.** "The head is not downloaded" says
18
18
  * nothing about the file; turning it into "this file has no keyframes" makes
19
- * every picture of it a re-encode for as long as the process lives.
19
+ * every picture of it a re-encode for as long as the process lives. Bytes
20
+ * that have not arrived reach here as `BytesUnavailable` — until 2026-10-01
21
+ * they came back as a reading of no times, which `KeyframeTable.learn`
22
+ * records as answered, so point 3 held only for reads that crashed. When
23
+ * pieces of such a file arrive the read is made again
24
+ * (`readAgainIfUnanswered`), and its answer lands in the same table.
20
25
  *
21
26
  * **What it does NOT know.** Where the bytes come from. The reader is one
22
27
  * function handed in at construction — on this proxy it crosses to the torrent
@@ -27,20 +32,16 @@
27
32
  * transport inside a layer that must not have one, and a second answer to a
28
33
  * question with one owner.
29
34
  *
30
- * **Why this is not `ContainerOrchestrator`, which is the same shape and sits
31
- * in this directory: the THREAD, not the responsibility.** That one holds a
32
- * container per file and needs byte ranges to build it, so it can only live
33
- * where the torrent is — the worker. This lives on the main thread, where the
34
- * sessions are, and reaches the other across the channel. The table is
35
- * therefore held twice, once per thread, and the two are not equals: the
36
- * worker's is the parse, memoized on the container it parsed, and the main
37
- * thread's is the answer, which nothing in the worker ever reads back. Three
38
- * directories of the storage layer are kept apart for the same reason, and it
39
- * is stated here for the same reason too — an unexplained second home reads as
40
- * a second owner.
35
+ * **Why this is not `ContainerOrchestrator`.** Both live on the main thread
36
+ * (the parse moved there on 2026-09-15). That one holds the file's one
37
+ * container, which keeps its own reading of the Cues table; this keeps the
38
+ * ANSWER the sessions hold — the keyframe times taken from that reading, and
39
+ * the wait around it, which a container cannot have because it does not know
40
+ * there is a viewer.
41
41
  */
42
42
 
43
43
  import { KeyframeTable } from "./container/KeyframeTable.js";
44
+ import { isUnavailable } from "./container/unavailable.js";
44
45
  import { logger as defaultLogger } from "../../utils/logger.js";
45
46
 
46
47
  /**
@@ -187,10 +188,17 @@ export class KeyframeTables {
187
188
  return table;
188
189
  })
189
190
  .catch((error) => {
190
- this.#logger.warn(
191
- `keyframe index "${logName}": the read failed after ${Date.now() - startedMs}ms — ` +
192
- `${error?.message ?? error}`
193
- );
191
+ if (isUnavailable(error)) {
192
+ this.#logger.info(
193
+ `keyframe index "${logName}": not downloaded yet after ${Date.now() - startedMs}ms — ` +
194
+ "read again when pieces of the file arrive"
195
+ );
196
+ } else {
197
+ this.#logger.warn(
198
+ `keyframe index "${logName}": the read failed after ${Date.now() - startedMs}ms — ` +
199
+ `${error?.message ?? error}`
200
+ );
201
+ }
194
202
  throw error;
195
203
  })
196
204
  .finally(() => {
@@ -220,6 +228,25 @@ export class KeyframeTables {
220
228
  }
221
229
  }
222
230
 
231
+ /**
232
+ * Read again a table somebody asked for and nobody has answered.
233
+ *
234
+ * Called when pieces of the file arrive. A table nobody asked for is left
235
+ * alone — this is not a reason to read every file of a torrent — and one
236
+ * already read, or being read, is not touched.
237
+ *
238
+ * @param {{ sourceKey: string, fileIndex: number, logName?: string }} params
239
+ * @returns {void}
240
+ */
241
+ readAgainIfUnanswered(params) {
242
+ const key = KeyframeTables.keyFor(params.sourceKey, params.fileIndex);
243
+ const table = this.#byFile.get(key);
244
+ if (!table || table.answered || this.#reading.has(key)) {
245
+ return;
246
+ }
247
+ void this.warm(params);
248
+ }
249
+
223
250
  /**
224
251
  * The same read, with a bound on how long THIS caller waits for it.
225
252
  *
@@ -232,7 +259,8 @@ export class KeyframeTables {
232
259
  * @param {{ sourceKey: string, fileIndex: number, logName?: string }} params
233
260
  * @returns {Promise<{ table: KeyframeTable, arrived: boolean }>} `arrived` is
234
261
  * about THIS wait. False with an unanswered table means the read is still
235
- * running, which is not the same as a file with no keyframes.
262
+ * running, or found the bytes not downloaded yet and will be made again
263
+ * when they arrive — neither is the same as a file with no keyframes.
236
264
  */
237
265
  async within(params) {
238
266
  const read = this.read(params);
@@ -242,11 +270,23 @@ export class KeyframeTables {
242
270
  // A caller must not be held open by this timer alone.
243
271
  timer?.unref?.();
244
272
  });
245
- const answer = await Promise.race([read.then((table) => ({ table, arrived: true })), budget]);
273
+ // Bytes not downloaded yet are "not arrived" to this caller: the table
274
+ // stays unanswered and is read again when pieces of the file arrive.
275
+ // Thrown out of here, a shortage of bytes would fail the opening of an
276
+ // output. Any other failure is still the caller's to see.
277
+ const answer = await Promise.race([
278
+ read.then(
279
+ (table) => ({ table, arrived: true }),
280
+ (error) => {
281
+ if (isUnavailable(error)) {
282
+ return { table: this.of(params), arrived: false };
283
+ }
284
+ throw error;
285
+ }
286
+ ),
287
+ budget
288
+ ]);
246
289
  clearTimeout(timer);
247
- // Nothing is added here to swallow a late rejection: the race is holding a
248
- // handler on that promise already, and a second one would only look like it
249
- // was doing something.
250
290
  return answer ?? { table: this.of(params), arrived: false };
251
291
  }
252
292