@torrent-tv/proxy 2.89.6 → 2.89.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.
Files changed (92) hide show
  1. package/AGENTS.md +32 -12
  2. package/CHANGELOG.md +2340 -2330
  3. package/assets/calibration/audio/NOTICE.md +19 -19
  4. package/assets/calibration/audio/manifest.json +98 -98
  5. package/biome.json +412 -412
  6. package/docs/browser-proxy-contract.md +155 -155
  7. package/docs/container-architecture.md +273 -273
  8. package/docs/encode-run-state.md +1 -1
  9. package/docs/playback-start-forecast.md +113 -98
  10. package/package.json +51 -50
  11. package/routes/api/sources/files/get.js +106 -106
  12. package/scripts/build-audio-samples.mjs +31 -31
  13. package/services/encode/EncodeRun.js +67 -15
  14. package/services/encode/EncodeRuns.js +19 -7
  15. package/services/encode/OutputOpening.js +784 -784
  16. package/services/encode/audio-calibration.js +67 -67
  17. package/services/encode/audio-work-rate.js +15 -15
  18. package/services/encode/encode-exit.js +2 -0
  19. package/services/encode/hwaccel.js +1894 -1894
  20. package/services/encode/output/index.js +24 -24
  21. package/services/encode/piece-completeness.js +60 -2
  22. package/services/encode/run-command.js +757 -755
  23. package/services/encode/segment-formats/index.js +1 -0
  24. package/services/encode/segment-formats/mp4-boxes.js +8 -1
  25. package/services/media/ContainerOrchestrator.js +202 -202
  26. package/services/media/KeyframeTables.js +356 -356
  27. package/services/media/SubtitleCues.js +592 -592
  28. package/services/media/audio-inventory.js +299 -299
  29. package/services/media/container/AviContainer.js +268 -268
  30. package/services/media/container/Container.js +632 -632
  31. package/services/media/container/ContainerFactory.js +38 -38
  32. package/services/media/container/MatroskaContainer.js +1031 -1031
  33. package/services/media/container/Mp4Container.js +1354 -1354
  34. package/services/media/container/SubtitleFileContainer.js +339 -339
  35. package/services/media/container/ebml-stream.js +174 -174
  36. package/services/media/container/matroska-clusters.js +1165 -1165
  37. package/services/media/container/unavailable.js +77 -77
  38. package/services/media/tracks/ContainerTrack.js +84 -84
  39. package/services/media/tracks/TextSubtitleTrack.js +295 -295
  40. package/services/storage/piece-store/shared-piece-store.js +1937 -1937
  41. package/services/storage/segment-store/SegmentStore.js +1294 -1294
  42. package/services/torrent/Contents.js +382 -382
  43. package/services/torrent/episode-naming.js +182 -182
  44. package/services/torrent/torrent-pool.js +2688 -2688
  45. package/services/torrent/worker/client.js +836 -836
  46. package/services/torrent/worker/held-bytes.js +231 -231
  47. package/services/torrent/worker/worker.js +1137 -1137
  48. package/services/transport/data-channel-handler.js +1373 -1373
  49. package/services/viewer/PriorityMap.js +421 -421
  50. package/test/audio-calibration.test.js +23 -23
  51. package/test/auto-quality-step.test.js +762 -762
  52. package/test/coalesce.test.js +39 -39
  53. package/test/coverage-follows-the-disk.test.js +5 -0
  54. package/test/decode-cost.test.js +475 -475
  55. package/test/encode-run.test.js +58 -0
  56. package/test/episode-naming.test.js +131 -131
  57. package/test/failed-encoder-still-serves-what-exists.test.js +1 -1
  58. package/test/fixtures/audio-frame-boundary.json +1 -1
  59. package/test/fixtures/refused-audio-20261002.json +10 -0
  60. package/test/fmp4-presentation-clock.test.js +4 -2
  61. package/test/format-keyed-outputs.test.js +78 -78
  62. package/test/health-metrics.test.js +11 -13
  63. package/test/held-read-ownership.test.js +77 -77
  64. package/test/helpers/encode-run.js +14 -6
  65. package/test/helpers/held-file.js +77 -77
  66. package/test/helpers/manager.js +62 -62
  67. package/test/helpers/matroska-file.js +296 -296
  68. package/test/keyframe-table-budget.test.js +123 -123
  69. package/test/matroska-cluster-discovery.test.js +323 -323
  70. package/test/one-output-one-session.test.js +273 -273
  71. package/test/output-speed.test.js +6 -1
  72. package/test/piece-completeness.test.js +37 -1
  73. package/test/quality-variants.test.js +1274 -1270
  74. package/test/rate-control-in-the-output.test.js +4 -3
  75. package/test/reads-not-yet-arrived.test.js +218 -218
  76. package/test/seek-frees-the-viewers-requests.test.js +1 -1
  77. package/test/segment-closed-on-both-branches.test.js +114 -114
  78. package/test/segment-judged-by-its-own-header.test.js +222 -222
  79. package/test/segment-serve-wiring.test.js +481 -481
  80. package/test/serving-existing-output.test.js +111 -111
  81. package/test/source-files-route.test.js +113 -113
  82. package/test/subtitle-cue-walk.test.js +385 -385
  83. package/test/subtitle-metadata.test.js +15 -15
  84. package/test/subtitle-plan-waits.test.js +125 -125
  85. package/test/subtitle-track-numbering.test.js +391 -391
  86. package/test/two-viewers-one-picture.test.js +508 -508
  87. package/test/viewer-requests-admission.test.js +92 -92
  88. package/test/viewer-starts-on-map.test.js +27 -27
  89. package/test/viewer-states.test.js +5 -2
  90. package/test/viewing-generations.test.js +390 -390
  91. package/test/whole-file-held-read.test.js +30 -30
  92. package/utils/coalesce.js +41 -41
@@ -1,123 +1,123 @@
1
- /**
2
- * @file How long anybody waits for the file's keyframe table.
3
- *
4
- * With the table a picture is copied; without it the whole picture is
5
- * re-encoded, which on a weak host is the difference between almost free and
6
- * more than the machine has. Until 2.76.1 there was no bound on that wait at
7
- * all, and the file comes off a torrent — so the bytes the table lives in may
8
- * still be arriving, and a session could sit there for as long as they took.
9
- *
10
- * Measured on the addon host 2026-09-04 over fifteen torrents
11
- * (`research/keyframe-table-read-2026-09-04.md`): every read whose bytes were
12
- * there finished within 8.8 s, while reads on swarms of one to four peers were
13
- * still waiting at 60-121 s. The bound sits between those two, and what it must
14
- * do is pinned here.
15
- */
16
-
17
- import test from "node:test";
18
- import assert from "node:assert/strict";
19
- import { KeyframeTables } from "../services/media/KeyframeTables.js";
20
- import { BytesUnavailable } from "../services/media/container/unavailable.js";
21
-
22
- const QUIET = { info: () => {}, warn: () => {} };
23
-
24
- /**
25
- * @param {number} budgetMs
26
- * @param {(params: object) => Promise<object | null>} readTable
27
- * @returns {KeyframeTables}
28
- */
29
- function tables(budgetMs, readTable) {
30
- return new KeyframeTables({ readTable, budgetMs, logger: QUIET });
31
- }
32
-
33
- const FILE = { sourceKey: "torrent:abc", fileIndex: 0, logName: "a.mkv" };
34
-
35
- test("a table that arrives inside the budget is the answer", async () => {
36
- const keyframes = tables(1_000, async () => ({ times: [0, 4, 8], tolerance: 0, format: "matroska" }));
37
-
38
- const { table, arrived } = await keyframes.within(FILE);
39
-
40
- assert.deepEqual(table.times, [0, 4, 8]);
41
- assert.equal(arrived, true, "the file has answered, so the picture may be copied");
42
- assert.equal(table.readable, true);
43
- });
44
-
45
- test("a table that has not arrived gives up on copying rather than on the session", async () => {
46
- // The bytes it needs are still coming off the swarm. On the field host this
47
- // is a torrent with one peer: the read was still waiting after two minutes.
48
- const keyframes = tables(60, () => new Promise(() => {}));
49
-
50
- const startedAt = Date.now();
51
- const { table, arrived } = await keyframes.within(FILE);
52
- const waited = Date.now() - startedAt;
53
-
54
- assert.equal(table.readable, false, "no table, so this session cannot copy the picture");
55
- assert.equal(
56
- table.answered,
57
- false,
58
- "and the file has NOT answered — an absence recorded as an answer would make a passing " +
59
- "shortage of bytes look like a property of the bytes"
60
- );
61
- assert.equal(arrived, false);
62
- assert.ok(waited < 2_000, `the wait ended at the bound, not at the read (${waited}ms)`);
63
- });
64
-
65
- test("the read goes on after the budget, so the next session of the file gets the copy", async () => {
66
- let reads = 0;
67
- let answerLate = null;
68
- const keyframes = tables(40, () => {
69
- reads += 1;
70
- return new Promise((resolve) => {
71
- answerLate = resolve;
72
- });
73
- });
74
-
75
- const first = await keyframes.within(FILE);
76
- assert.equal(first.arrived, false);
77
-
78
- answerLate({ times: [0, 5, 10], tolerance: 0, format: "matroska" });
79
- // Every microtask this resolution queues, and not a chosen ten milliseconds.
80
- // `setImmediate` runs after the whole microtask queue of this turn, so the
81
- // answer has been taken in by the time it fires — where the ten milliseconds
82
- // were a guess about how long that takes on whatever machine is running.
83
- // Asking the reader again instead would have counted as another read, which
84
- // is the very thing the last assertion here is about.
85
- await new Promise((resolve) => { setImmediate(resolve); });
86
-
87
- const second = await keyframes.within(FILE);
88
- assert.deepEqual(second.table.times, [0, 5, 10], "the late answer was kept, not thrown away");
89
- assert.equal(second.arrived, true);
90
- assert.equal(reads, 1, "and it was not read a second time");
91
- });
92
-
93
- test("a read that fails is not turned into a bounded wait's silence", async () => {
94
- const keyframes = tables(1_000, async () => {
95
- throw new Error("the head is not downloaded");
96
- });
97
-
98
- await assert.rejects(
99
- () => keyframes.read(FILE),
100
- /the head is not downloaded/,
101
- "a read that threw is a different thing from a read that is still running"
102
- );
103
- });
104
-
105
- test("bytes not downloaded yet are a table that has not arrived, and the next arrival reads again", async () => {
106
- let calls = 0;
107
- const keyframes = tables(1_000, async () => {
108
- calls += 1;
109
- if (calls === 1) {
110
- throw new BytesUnavailable(100, 199, 0);
111
- }
112
- return { times: [0, 4], tolerance: 0, format: "matroska" };
113
- });
114
-
115
- const { table, arrived } = await keyframes.within(FILE);
116
- assert.equal(arrived, false, "the opening goes on and re-encodes; it is not failed over a shortage of bytes");
117
- assert.equal(table.answered, false, "missing bytes say nothing about the file's keyframes");
118
-
119
- keyframes.readAgainIfUnanswered(FILE);
120
- await keyframes.read(FILE);
121
- assert.equal(calls, 2);
122
- assert.deepEqual(keyframes.of(FILE).times, [0, 4]);
123
- });
1
+ /**
2
+ * @file How long anybody waits for the file's keyframe table.
3
+ *
4
+ * With the table a picture is copied; without it the whole picture is
5
+ * re-encoded, which on a weak host is the difference between almost free and
6
+ * more than the machine has. Until 2.76.1 there was no bound on that wait at
7
+ * all, and the file comes off a torrent — so the bytes the table lives in may
8
+ * still be arriving, and a session could sit there for as long as they took.
9
+ *
10
+ * Measured on the addon host 2026-09-04 over fifteen torrents
11
+ * (`research/keyframe-table-read-2026-09-04.md`): every read whose bytes were
12
+ * there finished within 8.8 s, while reads on swarms of one to four peers were
13
+ * still waiting at 60-121 s. The bound sits between those two, and what it must
14
+ * do is pinned here.
15
+ */
16
+
17
+ import test from "node:test";
18
+ import assert from "node:assert/strict";
19
+ import { KeyframeTables } from "../services/media/KeyframeTables.js";
20
+ import { BytesUnavailable } from "../services/media/container/unavailable.js";
21
+
22
+ const QUIET = { info: () => {}, warn: () => {} };
23
+
24
+ /**
25
+ * @param {number} budgetMs
26
+ * @param {(params: object) => Promise<object | null>} readTable
27
+ * @returns {KeyframeTables}
28
+ */
29
+ function tables(budgetMs, readTable) {
30
+ return new KeyframeTables({ readTable, budgetMs, logger: QUIET });
31
+ }
32
+
33
+ const FILE = { sourceKey: "torrent:abc", fileIndex: 0, logName: "a.mkv" };
34
+
35
+ test("a table that arrives inside the budget is the answer", async () => {
36
+ const keyframes = tables(1_000, async () => ({ times: [0, 4, 8], tolerance: 0, format: "matroska" }));
37
+
38
+ const { table, arrived } = await keyframes.within(FILE);
39
+
40
+ assert.deepEqual(table.times, [0, 4, 8]);
41
+ assert.equal(arrived, true, "the file has answered, so the picture may be copied");
42
+ assert.equal(table.readable, true);
43
+ });
44
+
45
+ test("a table that has not arrived gives up on copying rather than on the session", async () => {
46
+ // The bytes it needs are still coming off the swarm. On the field host this
47
+ // is a torrent with one peer: the read was still waiting after two minutes.
48
+ const keyframes = tables(60, () => new Promise(() => {}));
49
+
50
+ const startedAt = Date.now();
51
+ const { table, arrived } = await keyframes.within(FILE);
52
+ const waited = Date.now() - startedAt;
53
+
54
+ assert.equal(table.readable, false, "no table, so this session cannot copy the picture");
55
+ assert.equal(
56
+ table.answered,
57
+ false,
58
+ "and the file has NOT answered — an absence recorded as an answer would make a passing " +
59
+ "shortage of bytes look like a property of the bytes"
60
+ );
61
+ assert.equal(arrived, false);
62
+ assert.ok(waited < 2_000, `the wait ended at the bound, not at the read (${waited}ms)`);
63
+ });
64
+
65
+ test("the read goes on after the budget, so the next session of the file gets the copy", async () => {
66
+ let reads = 0;
67
+ let answerLate = null;
68
+ const keyframes = tables(40, () => {
69
+ reads += 1;
70
+ return new Promise((resolve) => {
71
+ answerLate = resolve;
72
+ });
73
+ });
74
+
75
+ const first = await keyframes.within(FILE);
76
+ assert.equal(first.arrived, false);
77
+
78
+ answerLate({ times: [0, 5, 10], tolerance: 0, format: "matroska" });
79
+ // Every microtask this resolution queues, and not a chosen ten milliseconds.
80
+ // `setImmediate` runs after the whole microtask queue of this turn, so the
81
+ // answer has been taken in by the time it fires — where the ten milliseconds
82
+ // were a guess about how long that takes on whatever machine is running.
83
+ // Asking the reader again instead would have counted as another read, which
84
+ // is the very thing the last assertion here is about.
85
+ await new Promise((resolve) => { setImmediate(resolve); });
86
+
87
+ const second = await keyframes.within(FILE);
88
+ assert.deepEqual(second.table.times, [0, 5, 10], "the late answer was kept, not thrown away");
89
+ assert.equal(second.arrived, true);
90
+ assert.equal(reads, 1, "and it was not read a second time");
91
+ });
92
+
93
+ test("a read that fails is not turned into a bounded wait's silence", async () => {
94
+ const keyframes = tables(1_000, async () => {
95
+ throw new Error("the head is not downloaded");
96
+ });
97
+
98
+ await assert.rejects(
99
+ () => keyframes.read(FILE),
100
+ /the head is not downloaded/,
101
+ "a read that threw is a different thing from a read that is still running"
102
+ );
103
+ });
104
+
105
+ test("bytes not downloaded yet are a table that has not arrived, and the next arrival reads again", async () => {
106
+ let calls = 0;
107
+ const keyframes = tables(1_000, async () => {
108
+ calls += 1;
109
+ if (calls === 1) {
110
+ throw new BytesUnavailable(100, 199, 0);
111
+ }
112
+ return { times: [0, 4], tolerance: 0, format: "matroska" };
113
+ });
114
+
115
+ const { table, arrived } = await keyframes.within(FILE);
116
+ assert.equal(arrived, false, "the opening goes on and re-encodes; it is not failed over a shortage of bytes");
117
+ assert.equal(table.answered, false, "missing bytes say nothing about the file's keyframes");
118
+
119
+ keyframes.readAgainIfUnanswered(FILE);
120
+ await keyframes.read(FILE);
121
+ assert.equal(calls, 2);
122
+ assert.deepEqual(keyframes.of(FILE).times, [0, 4]);
123
+ });