@torrent-tv/proxy 2.80.18 → 2.81.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 (67) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/docs/encode-architecture.md +51 -2
  3. package/package.json +1 -1
  4. package/research/double-spawn-2026-09-10.md +171 -0
  5. package/services/disk/DiskSpace.js +146 -0
  6. package/services/disk/wire.js +60 -0
  7. package/services/encode/EncodeRun.js +37 -9
  8. package/services/encode/SegmentStore.js +284 -232
  9. package/services/encode/run-command.js +16 -1
  10. package/services/hls-session-manager.js +31 -128
  11. package/services/orchestrators/EncodeOrchestrator.js +52 -38
  12. package/services/piece-store/allowance.js +107 -0
  13. package/services/piece-store/piece-disk-store.js +365 -0
  14. package/services/piece-store/shared-piece-store.js +1549 -1535
  15. package/services/segment-formats/fmp4.js +54 -0
  16. package/services/segment-formats/mpegts.js +54 -0
  17. package/services/torrent-worker/client.js +32 -0
  18. package/services/torrent-worker/pool-adapter.js +15 -0
  19. package/services/torrent-worker/protocol.js +9 -0
  20. package/services/torrent-worker/worker.js +8 -1
  21. package/services/viewer/positions.js +48 -0
  22. package/test/audio-inventory.test.js +176 -176
  23. package/test/auto-quality-step.test.js +514 -514
  24. package/test/concurrent-cost.test.js +138 -138
  25. package/test/coverage-follows-the-disk.test.js +191 -187
  26. package/test/coverage-map.test.js +195 -195
  27. package/test/declared-tracks.test.js +35 -35
  28. package/test/disk-space.test.js +138 -0
  29. package/test/encode-orchestrator.test.js +0 -3
  30. package/test/encode-run.test.js +5 -12
  31. package/test/held-request-width.test.js +155 -155
  32. package/test/helpers/encode-run.js +2 -2
  33. package/test/matroska-blocks.test.js +0 -0
  34. package/test/matroska-cues-track.test.js +192 -192
  35. package/test/mp4-composition-times.test.js +0 -0
  36. package/test/mp4-subtitles.test.js +173 -173
  37. package/test/one-authority.test.js +281 -220
  38. package/test/orchestrator-wired.test.js +199 -195
  39. package/test/packet-witness-ring.test.js +236 -236
  40. package/test/packet-witness.test.js +148 -148
  41. package/test/piece-disk-store.test.js +267 -0
  42. package/test/piece-reader.test.js +4 -4
  43. package/test/piece-store-eviction.test.js +17 -17
  44. package/test/piece-store-reservations.test.js +20 -1
  45. package/test/piece-store-slow-disk.test.js +16 -1
  46. package/test/produced-copy-choice.test.js +258 -358
  47. package/test/read-window.test.js +6 -6
  48. package/test/run-intervals.test.js +100 -100
  49. package/test/seek-landing.test.js +109 -109
  50. package/test/segment-serve-wiring.test.js +8 -9
  51. package/test/segment-store-eviction.test.js +232 -0
  52. package/test/segment-store.test.js +238 -216
  53. package/test/segments-are-shared.test.js +1 -1
  54. package/test/shared-piece-store.test.js +12 -12
  55. package/test/sidecar-naming.test.js +142 -142
  56. package/test/subtitle-cue-framing.test.js +200 -200
  57. package/test/subtitle-cue-walk.test.js +369 -369
  58. package/test/subtitle-defaults.test.js +97 -97
  59. package/test/subtitle-track-numbering.test.js +370 -370
  60. package/test/tail-duplication.test.js +167 -167
  61. package/test/tracks-begin-together.test.js +195 -195
  62. package/test/two-viewers-one-picture.test.js +374 -374
  63. package/test/video-facts.test.js +102 -102
  64. package/test/wedge-certainty.test.js +131 -131
  65. package/services/encode/open-piece.js +0 -135
  66. package/services/piece-store/disk-tier.js +0 -151
  67. package/test/open-piece.test.js +0 -152
@@ -1,102 +1,102 @@
1
- /**
2
- * @file The picture's facts, from the two readings that state them.
3
- *
4
- * Audio and subtitles have had this reconciliation since their flags were first
5
- * read from the file. Video never did: every figure the encode is planned from
6
- * came from ffmpeg's `-i` banner alone, and the `VideoTrack` the container
7
- * declares was read and then used for nothing but a line in the log — so a file
8
- * that states its bit depth or its HDR signalling, against a probe that does
9
- * not print them, disagreed with nobody watching.
10
- *
11
- * What each check pins is WHICH reading answers, and why: the size and the
12
- * frame rate are what the decoder will produce, so the probe answers; the bit
13
- * depth and the HDR signalling are the file saying how its samples are to be
14
- * read, so the container answers.
15
- */
16
-
17
- import test from "node:test";
18
- import assert from "node:assert/strict";
19
-
20
- import { Container } from "../services/container/Container.js";
21
-
22
- const banner = (fields) => ({ width: null, height: null, fps: null, isHdr: false, bitDepth: null, ...fields });
23
-
24
- test("with no container reading the probe answers for everything", () => {
25
- const facts = Container.mergeVideoFacts(
26
- banner({ width: 1920, height: 1080, fps: 23.976, isHdr: true, bitDepth: 10 }),
27
- null
28
- );
29
- assert.equal(facts.width, 1920);
30
- assert.equal(facts.height, 1080);
31
- assert.equal(facts.fps, 23.976);
32
- assert.equal(facts.isHdr, true);
33
- assert.equal(facts.bitDepth, 10);
34
- assert.deepEqual(facts.disagreements, []);
35
- });
36
-
37
- test("the size and the frame rate are the probe's, because that is what will be decoded", () => {
38
- const facts = Container.mergeVideoFacts(
39
- banner({ width: 1920, height: 1080, fps: 24 }),
40
- { width: 1440, height: 1080, fps: 25, displayWidth: 1920, displayHeight: 1080 }
41
- );
42
- assert.equal(facts.width, 1920, "the ladder and the scale filter are sized to the decoded frame");
43
- assert.equal(facts.height, 1080);
44
- assert.equal(facts.fps, 24);
45
- });
46
-
47
- test("the bit depth and the HDR signalling are the file's, because the file states them", () => {
48
- // A ten-bit HEVC whose probe printed neither: this is the case that decides
49
- // whether tone mapping runs and how the decode cost is priced.
50
- const facts = Container.mergeVideoFacts(
51
- banner({ width: 3840, height: 2160, fps: 24 }),
52
- { width: 3840, height: 2160, bitDepth: 10, isHdr: true }
53
- );
54
- assert.equal(facts.bitDepth, 10);
55
- assert.equal(facts.isHdr, true);
56
- assert.deepEqual(facts.disagreements, [], "one side saying nothing is not a disagreement");
57
- });
58
-
59
- test("a field only the probe states is still answered", () => {
60
- const facts = Container.mergeVideoFacts(
61
- banner({ width: 1280, height: 720, fps: 30, bitDepth: 8 }),
62
- { width: null, height: null, fps: null, bitDepth: null }
63
- );
64
- assert.equal(facts.width, 1280);
65
- assert.equal(facts.bitDepth, 8);
66
- });
67
-
68
- test("a real disagreement is reported, with both values and which is which", () => {
69
- const facts = Container.mergeVideoFacts(
70
- banner({ width: 1920, height: 1080, bitDepth: 8, isHdr: false }),
71
- { width: 1280, height: 720, bitDepth: 10, isHdr: true }
72
- );
73
- assert.equal(facts.disagreements.length, 3);
74
- assert.ok(facts.disagreements.some((line) => /width 1280 in the container against 1920 in the probe/.test(line)));
75
- assert.ok(facts.disagreements.some((line) => /bit depth 10 in the container against 8 in the probe/.test(line)));
76
- // HDR is not among them, and cannot be: see `mergeVideoFacts`.
77
- assert.ok(!facts.disagreements.some((line) => /HDR/.test(line)));
78
- // And the rule still decides: the probe for the size, the file for the rest.
79
- assert.equal(facts.width, 1920);
80
- assert.equal(facts.bitDepth, 10);
81
- assert.equal(facts.isHdr, true);
82
- });
83
-
84
- test("the display size is the container's alone — the probe has no such field", () => {
85
- const facts = Container.mergeVideoFacts(
86
- banner({ width: 1440, height: 1080 }),
87
- { width: 1440, height: 1080, displayWidth: 1920, displayHeight: 1080 }
88
- );
89
- assert.equal(facts.displayWidth, 1920);
90
- assert.equal(facts.displayHeight, 1080);
91
- });
92
-
93
- test("zero and nonsense are not values", () => {
94
- const facts = Container.mergeVideoFacts(
95
- banner({ width: 0, height: 0, fps: 0, bitDepth: 0 }),
96
- { width: 1920, height: 1080, fps: 24, bitDepth: 8 }
97
- );
98
- assert.equal(facts.width, 1920, "a probe that printed nothing does not outrank a file that speaks");
99
- assert.equal(facts.fps, 24);
100
- assert.equal(facts.bitDepth, 8);
101
- assert.deepEqual(facts.disagreements, []);
102
- });
1
+ /**
2
+ * @file The picture's facts, from the two readings that state them.
3
+ *
4
+ * Audio and subtitles have had this reconciliation since their flags were first
5
+ * read from the file. Video never did: every figure the encode is planned from
6
+ * came from ffmpeg's `-i` banner alone, and the `VideoTrack` the container
7
+ * declares was read and then used for nothing but a line in the log — so a file
8
+ * that states its bit depth or its HDR signalling, against a probe that does
9
+ * not print them, disagreed with nobody watching.
10
+ *
11
+ * What each check pins is WHICH reading answers, and why: the size and the
12
+ * frame rate are what the decoder will produce, so the probe answers; the bit
13
+ * depth and the HDR signalling are the file saying how its samples are to be
14
+ * read, so the container answers.
15
+ */
16
+
17
+ import test from "node:test";
18
+ import assert from "node:assert/strict";
19
+
20
+ import { Container } from "../services/container/Container.js";
21
+
22
+ const banner = (fields) => ({ width: null, height: null, fps: null, isHdr: false, bitDepth: null, ...fields });
23
+
24
+ test("with no container reading the probe answers for everything", () => {
25
+ const facts = Container.mergeVideoFacts(
26
+ banner({ width: 1920, height: 1080, fps: 23.976, isHdr: true, bitDepth: 10 }),
27
+ null
28
+ );
29
+ assert.equal(facts.width, 1920);
30
+ assert.equal(facts.height, 1080);
31
+ assert.equal(facts.fps, 23.976);
32
+ assert.equal(facts.isHdr, true);
33
+ assert.equal(facts.bitDepth, 10);
34
+ assert.deepEqual(facts.disagreements, []);
35
+ });
36
+
37
+ test("the size and the frame rate are the probe's, because that is what will be decoded", () => {
38
+ const facts = Container.mergeVideoFacts(
39
+ banner({ width: 1920, height: 1080, fps: 24 }),
40
+ { width: 1440, height: 1080, fps: 25, displayWidth: 1920, displayHeight: 1080 }
41
+ );
42
+ assert.equal(facts.width, 1920, "the ladder and the scale filter are sized to the decoded frame");
43
+ assert.equal(facts.height, 1080);
44
+ assert.equal(facts.fps, 24);
45
+ });
46
+
47
+ test("the bit depth and the HDR signalling are the file's, because the file states them", () => {
48
+ // A ten-bit HEVC whose probe printed neither: this is the case that decides
49
+ // whether tone mapping runs and how the decode cost is priced.
50
+ const facts = Container.mergeVideoFacts(
51
+ banner({ width: 3840, height: 2160, fps: 24 }),
52
+ { width: 3840, height: 2160, bitDepth: 10, isHdr: true }
53
+ );
54
+ assert.equal(facts.bitDepth, 10);
55
+ assert.equal(facts.isHdr, true);
56
+ assert.deepEqual(facts.disagreements, [], "one side saying nothing is not a disagreement");
57
+ });
58
+
59
+ test("a field only the probe states is still answered", () => {
60
+ const facts = Container.mergeVideoFacts(
61
+ banner({ width: 1280, height: 720, fps: 30, bitDepth: 8 }),
62
+ { width: null, height: null, fps: null, bitDepth: null }
63
+ );
64
+ assert.equal(facts.width, 1280);
65
+ assert.equal(facts.bitDepth, 8);
66
+ });
67
+
68
+ test("a real disagreement is reported, with both values and which is which", () => {
69
+ const facts = Container.mergeVideoFacts(
70
+ banner({ width: 1920, height: 1080, bitDepth: 8, isHdr: false }),
71
+ { width: 1280, height: 720, bitDepth: 10, isHdr: true }
72
+ );
73
+ assert.equal(facts.disagreements.length, 3);
74
+ assert.ok(facts.disagreements.some((line) => /width 1280 in the container against 1920 in the probe/.test(line)));
75
+ assert.ok(facts.disagreements.some((line) => /bit depth 10 in the container against 8 in the probe/.test(line)));
76
+ // HDR is not among them, and cannot be: see `mergeVideoFacts`.
77
+ assert.ok(!facts.disagreements.some((line) => /HDR/.test(line)));
78
+ // And the rule still decides: the probe for the size, the file for the rest.
79
+ assert.equal(facts.width, 1920);
80
+ assert.equal(facts.bitDepth, 10);
81
+ assert.equal(facts.isHdr, true);
82
+ });
83
+
84
+ test("the display size is the container's alone — the probe has no such field", () => {
85
+ const facts = Container.mergeVideoFacts(
86
+ banner({ width: 1440, height: 1080 }),
87
+ { width: 1440, height: 1080, displayWidth: 1920, displayHeight: 1080 }
88
+ );
89
+ assert.equal(facts.displayWidth, 1920);
90
+ assert.equal(facts.displayHeight, 1080);
91
+ });
92
+
93
+ test("zero and nonsense are not values", () => {
94
+ const facts = Container.mergeVideoFacts(
95
+ banner({ width: 0, height: 0, fps: 0, bitDepth: 0 }),
96
+ { width: 1920, height: 1080, fps: 24, bitDepth: 8 }
97
+ );
98
+ assert.equal(facts.width, 1920, "a probe that printed nothing does not outrank a file that speaks");
99
+ assert.equal(facts.fps, 24);
100
+ assert.equal(facts.bitDepth, 8);
101
+ assert.deepEqual(facts.disagreements, []);
102
+ });
@@ -1,131 +1,131 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
-
4
- import { wedgeIsCertain } from "../services/data-channel-handler.js";
5
- import { PROBE_INTERVAL_MS } from "../services/delivery-probe.js";
6
-
7
- const MEGABYTE = 1024 * 1024;
8
-
9
- test("a queue draining at the rate this link was measured at is not a wedge", () => {
10
- // 8 MB at 16 MB/s is half a second of draining. Half a second in is normal.
11
- const verdict = wedgeIsCertain({
12
- queuedBytes: 8 * MEGABYTE,
13
- bytesPerSecond: 16 * MEGABYTE,
14
- flatForMs: 200
15
- });
16
- assert.equal(verdict.certain, false);
17
- });
18
-
19
- test("the counter standing still past the queue's own drain time is a wedge", () => {
20
- const verdict = wedgeIsCertain({
21
- queuedBytes: 8 * MEGABYTE,
22
- bytesPerSecond: 16 * MEGABYTE,
23
- flatForMs: 4000
24
- });
25
- assert.equal(verdict.certain, true);
26
- assert.equal(verdict.needMs, 500);
27
- });
28
-
29
- test("a big queue on a slow link is given the time it genuinely needs", () => {
30
- // 67 MB at 1 MB/s is 67 seconds of honest draining — the field's own numbers
31
- // for the wedged channel, at a rate a thin link could really be running at.
32
- const patient = wedgeIsCertain({
33
- queuedBytes: 67 * MEGABYTE,
34
- bytesPerSecond: MEGABYTE,
35
- flatForMs: 30_000
36
- });
37
- assert.equal(patient.certain, false);
38
- const later = wedgeIsCertain({
39
- queuedBytes: 67 * MEGABYTE,
40
- bytesPerSecond: MEGABYTE,
41
- flatForMs: 70_000
42
- });
43
- assert.equal(later.certain, true);
44
- });
45
-
46
- test("the same queue on the link the field actually had is called quickly", () => {
47
- // 67 MB at 18 MB/s is under four seconds. The field episode stood still for
48
- // 3217 s; the previous rule waited a flat 30 s before recording anything.
49
- const verdict = wedgeIsCertain({
50
- queuedBytes: 67 * MEGABYTE,
51
- bytesPerSecond: 18 * MEGABYTE,
52
- flatForMs: 5000
53
- });
54
- assert.equal(verdict.certain, true);
55
- assert.ok(verdict.needMs < 30_000);
56
- });
57
-
58
- test("a tiny queue still waits for one round of probes", () => {
59
- const verdict = wedgeIsCertain({
60
- queuedBytes: 512,
61
- bytesPerSecond: 16 * MEGABYTE,
62
- flatForMs: PROBE_INTERVAL_MS - 1
63
- });
64
- assert.equal(verdict.certain, false);
65
- assert.equal(verdict.needMs, PROBE_INTERVAL_MS);
66
- });
67
-
68
- test("nothing queued is still a wedge when the counter has been flat long enough — the channel queue was 0 on the real wedge while usrsctp held 399 MB", () => {
69
- const verdict = wedgeIsCertain({
70
- queuedBytes: 0,
71
- bytesPerSecond: 16 * MEGABYTE,
72
- flatForMs: 600_000
73
- });
74
- assert.equal(verdict.certain, true);
75
- assert.equal(verdict.needMs, PROBE_INTERVAL_MS);
76
- });
77
-
78
- test("with no rate measured the answer is that nothing can be said", () => {
79
- const verdict = wedgeIsCertain({
80
- queuedBytes: 8 * MEGABYTE,
81
- bytesPerSecond: 0,
82
- flatForMs: 600_000
83
- });
84
- assert.equal(verdict.certain, false);
85
- assert.equal(verdict.needMs, null);
86
- });
87
-
88
- test("a pause no longer than this link's own longest healthy pause is not a wedge", () => {
89
- // A retransmission timeout stops the accepted-byte counter dead: a full send
90
- // buffer accepts nothing. If this connection has already paused 3 s while
91
- // healthy, a 3 s pause says nothing.
92
- const verdict = wedgeIsCertain({
93
- queuedBytes: 8 * MEGABYTE,
94
- bytesPerSecond: 18 * MEGABYTE,
95
- flatForMs: 2500,
96
- longestHealthyFlatMs: 3000
97
- });
98
- assert.equal(verdict.certain, false);
99
- assert.equal(verdict.needMs, 3000);
100
- });
101
-
102
- test("a pause longer than any this link has shown is a wedge", () => {
103
- const verdict = wedgeIsCertain({
104
- queuedBytes: 8 * MEGABYTE,
105
- bytesPerSecond: 18 * MEGABYTE,
106
- flatForMs: 3500,
107
- longestHealthyFlatMs: 3000
108
- });
109
- assert.equal(verdict.certain, true);
110
- });
111
-
112
- test("the quiet-probe rate cannot be what the queue is divided by", () => {
113
- // With the browser's buffer full, the only traffic is the probe: three
114
- // channels, a few dozen bytes, twice a second. Dividing 8 MB by that gives
115
- // six hours, and the wedge would never be called. The BEST rate seen is what
116
- // the watcher keeps, so this case must not arise — pinned here as the
117
- // arithmetic that made it matter.
118
- const wrong = wedgeIsCertain({
119
- queuedBytes: 8 * MEGABYTE,
120
- bytesPerSecond: 360,
121
- flatForMs: 60_000
122
- });
123
- assert.equal(wrong.certain, false);
124
- assert.ok(wrong.needMs > 6 * 60 * 60 * 1000 - 1);
125
- const right = wedgeIsCertain({
126
- queuedBytes: 8 * MEGABYTE,
127
- bytesPerSecond: 18 * MEGABYTE,
128
- flatForMs: 60_000
129
- });
130
- assert.equal(right.certain, true);
131
- });
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ import { wedgeIsCertain } from "../services/data-channel-handler.js";
5
+ import { PROBE_INTERVAL_MS } from "../services/delivery-probe.js";
6
+
7
+ const MEGABYTE = 1024 * 1024;
8
+
9
+ test("a queue draining at the rate this link was measured at is not a wedge", () => {
10
+ // 8 MB at 16 MB/s is half a second of draining. Half a second in is normal.
11
+ const verdict = wedgeIsCertain({
12
+ queuedBytes: 8 * MEGABYTE,
13
+ bytesPerSecond: 16 * MEGABYTE,
14
+ flatForMs: 200
15
+ });
16
+ assert.equal(verdict.certain, false);
17
+ });
18
+
19
+ test("the counter standing still past the queue's own drain time is a wedge", () => {
20
+ const verdict = wedgeIsCertain({
21
+ queuedBytes: 8 * MEGABYTE,
22
+ bytesPerSecond: 16 * MEGABYTE,
23
+ flatForMs: 4000
24
+ });
25
+ assert.equal(verdict.certain, true);
26
+ assert.equal(verdict.needMs, 500);
27
+ });
28
+
29
+ test("a big queue on a slow link is given the time it genuinely needs", () => {
30
+ // 67 MB at 1 MB/s is 67 seconds of honest draining — the field's own numbers
31
+ // for the wedged channel, at a rate a thin link could really be running at.
32
+ const patient = wedgeIsCertain({
33
+ queuedBytes: 67 * MEGABYTE,
34
+ bytesPerSecond: MEGABYTE,
35
+ flatForMs: 30_000
36
+ });
37
+ assert.equal(patient.certain, false);
38
+ const later = wedgeIsCertain({
39
+ queuedBytes: 67 * MEGABYTE,
40
+ bytesPerSecond: MEGABYTE,
41
+ flatForMs: 70_000
42
+ });
43
+ assert.equal(later.certain, true);
44
+ });
45
+
46
+ test("the same queue on the link the field actually had is called quickly", () => {
47
+ // 67 MB at 18 MB/s is under four seconds. The field episode stood still for
48
+ // 3217 s; the previous rule waited a flat 30 s before recording anything.
49
+ const verdict = wedgeIsCertain({
50
+ queuedBytes: 67 * MEGABYTE,
51
+ bytesPerSecond: 18 * MEGABYTE,
52
+ flatForMs: 5000
53
+ });
54
+ assert.equal(verdict.certain, true);
55
+ assert.ok(verdict.needMs < 30_000);
56
+ });
57
+
58
+ test("a tiny queue still waits for one round of probes", () => {
59
+ const verdict = wedgeIsCertain({
60
+ queuedBytes: 512,
61
+ bytesPerSecond: 16 * MEGABYTE,
62
+ flatForMs: PROBE_INTERVAL_MS - 1
63
+ });
64
+ assert.equal(verdict.certain, false);
65
+ assert.equal(verdict.needMs, PROBE_INTERVAL_MS);
66
+ });
67
+
68
+ test("nothing queued is still a wedge when the counter has been flat long enough — the channel queue was 0 on the real wedge while usrsctp held 399 MB", () => {
69
+ const verdict = wedgeIsCertain({
70
+ queuedBytes: 0,
71
+ bytesPerSecond: 16 * MEGABYTE,
72
+ flatForMs: 600_000
73
+ });
74
+ assert.equal(verdict.certain, true);
75
+ assert.equal(verdict.needMs, PROBE_INTERVAL_MS);
76
+ });
77
+
78
+ test("with no rate measured the answer is that nothing can be said", () => {
79
+ const verdict = wedgeIsCertain({
80
+ queuedBytes: 8 * MEGABYTE,
81
+ bytesPerSecond: 0,
82
+ flatForMs: 600_000
83
+ });
84
+ assert.equal(verdict.certain, false);
85
+ assert.equal(verdict.needMs, null);
86
+ });
87
+
88
+ test("a pause no longer than this link's own longest healthy pause is not a wedge", () => {
89
+ // A retransmission timeout stops the accepted-byte counter dead: a full send
90
+ // buffer accepts nothing. If this connection has already paused 3 s while
91
+ // healthy, a 3 s pause says nothing.
92
+ const verdict = wedgeIsCertain({
93
+ queuedBytes: 8 * MEGABYTE,
94
+ bytesPerSecond: 18 * MEGABYTE,
95
+ flatForMs: 2500,
96
+ longestHealthyFlatMs: 3000
97
+ });
98
+ assert.equal(verdict.certain, false);
99
+ assert.equal(verdict.needMs, 3000);
100
+ });
101
+
102
+ test("a pause longer than any this link has shown is a wedge", () => {
103
+ const verdict = wedgeIsCertain({
104
+ queuedBytes: 8 * MEGABYTE,
105
+ bytesPerSecond: 18 * MEGABYTE,
106
+ flatForMs: 3500,
107
+ longestHealthyFlatMs: 3000
108
+ });
109
+ assert.equal(verdict.certain, true);
110
+ });
111
+
112
+ test("the quiet-probe rate cannot be what the queue is divided by", () => {
113
+ // With the browser's buffer full, the only traffic is the probe: three
114
+ // channels, a few dozen bytes, twice a second. Dividing 8 MB by that gives
115
+ // six hours, and the wedge would never be called. The BEST rate seen is what
116
+ // the watcher keeps, so this case must not arise — pinned here as the
117
+ // arithmetic that made it matter.
118
+ const wrong = wedgeIsCertain({
119
+ queuedBytes: 8 * MEGABYTE,
120
+ bytesPerSecond: 360,
121
+ flatForMs: 60_000
122
+ });
123
+ assert.equal(wrong.certain, false);
124
+ assert.ok(wrong.needMs > 6 * 60 * 60 * 1000 - 1);
125
+ const right = wedgeIsCertain({
126
+ queuedBytes: 8 * MEGABYTE,
127
+ bytesPerSecond: 18 * MEGABYTE,
128
+ flatForMs: 60_000
129
+ });
130
+ assert.equal(right.certain, true);
131
+ });
@@ -1,135 +0,0 @@
1
- /**
2
- * @file The piece a run had open when it ended.
3
- *
4
- * A fact of a run's output directory, and therefore of the encoding layer. It
5
- * lived in the eleven-thousand-line file that is being taken apart, where it was
6
- * called by the one place that killed a run; stopping is decided in one place
7
- * now and carried out in another, so the cleanup belongs to the layer that owns
8
- * the directories rather than to whoever happened to do the killing.
9
- */
10
-
11
- import { readdir, readFile, stat, unlink } from "node:fs/promises";
12
- import path from "node:path";
13
-
14
- /**
15
- * Remove the piece a run had open when it ended, if that piece is unusable.
16
- *
17
- * The `segment` muxer creates its output file when it OPENS it and writes into
18
- * it until the next cut, so at any instant exactly one file in a run's
19
- * directory is unfinished: the highest-numbered one. A run that reaches the end
20
- * of its work closes that file and it is a good piece; a run killed for a seek
21
- * does not — measured 2026-09-03, ffmpeg exited 19 ms after SIGTERM and left
22
- * `segment-00025.mp4` at zero bytes, which then closed the only hole in the
23
- * numbering and convinced the look-ahead to keep the encoder stopped for having
24
- * "produced" it.
25
- *
26
- * **A piece is whole only if the run PROVED it, and reading it proves nothing.**
27
- *
28
- * The highest-numbered file in a run's stretch is the one it had open. How a
29
- * run ends decides what became of that file, and all three outcomes leave it
30
- * readable-looking: stopped with SIGTERM, ffmpeg writes it out and names it on
31
- * the ready channel exactly as it names a finished one; killed harder, or dying
32
- * on its own, it leaves the bytes it had written with no name at all. In every
33
- * case the file decodes and holds film only up to the instant the run ended,
34
- * under a number whose playlist entry promises a whole span. Field 2026-09-06:
35
- * `segment-00010.mp4` held 3.92 s of its declared 5.589 s — 96 frames — and the
36
- * picture jumped 1.5 s at 1:02; the soundtrack did the same at 17.5 s, 2.8 s
37
- * wide, in the same session. Both decoded, which is how both reached the viewer.
38
- *
39
- * So the question asked here is not what the file contains but whether the run
40
- * named it while it was still running normally. That is a fact the run holds,
41
- * so there is no span to measure and no tolerance to choose.
42
- *
43
- * A piece finished in the moment between the last such name and the end is
44
- * then made a second time. That is the cheaper error: the other is a hole the
45
- * viewer sees.
46
- *
47
- * @param {string | null | undefined} runDirPath
48
- * @param {{ isSegmentFileName: (name: string) => boolean, segmentIndexFromName: (name: string) => number }} segmentFormat
49
- * @param {((raw: Buffer) => boolean) | null} judgeUsable - Whether a non-empty
50
- * piece carries what it should. Null where nothing can say, and then only an
51
- * empty file is removed.
52
- * @param {string | null} [provenName] - The last piece the run named while it
53
- * was running normally. A file beyond it was open when the run ended and goes
54
- * whether or not it reads. Null where the run proved nothing, and then every
55
- * piece it left is unproven.
56
- * @returns {Promise<number | null>} The segment number removed, or null.
57
- */
58
- export async function discardOpenPiece(runDirPath, segmentFormat, within, judgeUsable, provenName = null) {
59
- if (!runDirPath || typeof segmentFormat?.isSegmentFileName !== "function") {
60
- return null;
61
- }
62
- // Only inside the stretch the ended run was given. Every run of an output
63
- // writes into one directory now — they are kept apart by their intervals
64
- // rather than by a directory each — so the highest-numbered file in there may
65
- // belong to a run that is still going, and removing it would take away a
66
- // piece somebody is producing.
67
- const from = Number.isInteger(within?.from) ? within.from : 0;
68
- // THE RUN'S OWN REACH, which is a fact it holds and needs nothing measured.
69
- //
70
- // A run cannot have opened a file above the one just past the last it named:
71
- // ffmpeg names a piece when it closes it and opens the next, so the open piece
72
- // is at most `proven + 1`, and where it named nothing at all the open one is
73
- // the first it was given.
74
- //
75
- // Used only where no end was declared — `to` below `from`, which is how "to
76
- // the end of the track" is written everywhere here. Such a run had no bound at
77
- // all: the search covered the whole directory and took the highest-numbered
78
- // file in it. Every run of an output writes into that one
79
- // directory, so what it took was a piece a LIVE run had just finished. Field
80
- // 2026-09-06: the piece holding 2:47-2:57 went that way, its number is spent
81
- // for good because names only grow, and the picture stood still for 647 s.
82
- const provenIndex = typeof provenName === "string" && segmentFormat.isSegmentFileName(provenName)
83
- ? segmentFormat.segmentIndexFromName(provenName)
84
- : null;
85
- const reach = Number.isInteger(provenIndex) && provenIndex >= from ? provenIndex + 1 : from;
86
- // The declared end is the bound wherever there is one. The run's own reach is
87
- // the bound of LAST RESORT, for a run given none: before it, such a run had no
88
- // bound at all and the search covered the whole directory.
89
- const to = Number.isInteger(within?.to) && within.to >= from ? within.to : reach;
90
- let highest = null;
91
- try {
92
- for (const name of await readdir(runDirPath)) {
93
- if (!segmentFormat.isSegmentFileName(name)) {
94
- continue;
95
- }
96
- const index = segmentFormat.segmentIndexFromName(name);
97
- if (index < from || index > to) {
98
- continue;
99
- }
100
- if (index >= 0 && (highest === null || index > highest.index)) {
101
- highest = { index, name };
102
- }
103
- }
104
- } catch {
105
- return null; // The run wrote nothing, or its directory is already gone.
106
- }
107
- if (highest === null) {
108
- return null;
109
- }
110
- const filePath = path.join(runDirPath, highest.name);
111
- // Proven finished only if the run said so while it was running. Anything
112
- // beyond that name was open when the run ended, and its contents cannot say
113
- // so — it decodes.
114
- const proven = typeof provenName === "string" && provenName === highest.name;
115
- let unusable = !proven;
116
- try {
117
- const info = await stat(filePath);
118
- if (info.size === 0) {
119
- unusable = true;
120
- } else if (!unusable && typeof judgeUsable === "function") {
121
- unusable = !judgeUsable(await readFile(filePath));
122
- }
123
- } catch {
124
- return null; // Gone between the listing and the question.
125
- }
126
- if (!unusable) {
127
- return null;
128
- }
129
- try {
130
- await unlink(filePath);
131
- return highest.index;
132
- } catch {
133
- return null; // Already removed.
134
- }
135
- }