@torrent-tv/proxy 2.80.19 → 2.81.1
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 +16 -0
- package/package.json +1 -1
- package/research/double-spawn-2026-09-10.md +171 -0
- package/services/disk/DiskSpace.js +150 -0
- package/services/disk/wire.js +60 -0
- package/services/encode/EncodeRun.js +25 -3
- package/services/encode/SegmentStore.js +137 -9
- package/services/hls-session-manager.js +26 -45
- package/services/orchestrators/EncodeOrchestrator.js +4 -1
- package/services/piece-store/allowance.js +107 -0
- package/services/piece-store/piece-disk-store.js +365 -0
- package/services/piece-store/shared-piece-store.js +1549 -1535
- package/services/torrent-worker/client.js +32 -0
- package/services/torrent-worker/pool-adapter.js +15 -0
- package/services/torrent-worker/protocol.js +9 -0
- package/services/torrent-worker/worker.js +8 -1
- package/services/viewer/positions.js +48 -0
- package/test/audio-inventory.test.js +176 -176
- package/test/auto-quality-step.test.js +514 -514
- package/test/concurrent-cost.test.js +138 -138
- package/test/coverage-follows-the-disk.test.js +191 -191
- package/test/coverage-map.test.js +195 -195
- package/test/declared-tracks.test.js +35 -35
- package/test/disk-space.test.js +150 -0
- package/test/encode-orchestrator.test.js +0 -3
- package/test/encode-run.test.js +5 -12
- package/test/held-request-width.test.js +155 -155
- package/test/helpers/encode-run.js +2 -2
- package/test/matroska-blocks.test.js +0 -0
- package/test/matroska-cues-track.test.js +192 -192
- package/test/mp4-composition-times.test.js +0 -0
- package/test/mp4-subtitles.test.js +173 -173
- package/test/one-authority.test.js +281 -220
- package/test/orchestrator-wired.test.js +199 -199
- package/test/packet-witness-ring.test.js +236 -236
- package/test/packet-witness.test.js +148 -148
- package/test/piece-disk-store.test.js +267 -0
- package/test/piece-reader.test.js +4 -4
- package/test/piece-store-eviction.test.js +17 -17
- package/test/piece-store-reservations.test.js +20 -1
- package/test/piece-store-slow-disk.test.js +16 -1
- package/test/read-window.test.js +6 -6
- package/test/run-intervals.test.js +100 -100
- package/test/seek-landing.test.js +109 -109
- package/test/segment-store-eviction.test.js +232 -0
- package/test/segments-are-shared.test.js +1 -1
- package/test/shared-piece-store.test.js +12 -12
- package/test/sidecar-naming.test.js +142 -142
- package/test/subtitle-cue-framing.test.js +200 -200
- package/test/subtitle-cue-walk.test.js +369 -369
- package/test/subtitle-defaults.test.js +97 -97
- package/test/subtitle-track-numbering.test.js +370 -370
- package/test/tail-duplication.test.js +167 -167
- package/test/tracks-begin-together.test.js +195 -195
- package/test/two-viewers-one-picture.test.js +374 -374
- package/test/video-facts.test.js +102 -102
- package/test/wedge-certainty.test.js +131 -131
- package/services/piece-store/disk-tier.js +0 -151
|
@@ -1,138 +1,138 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file A rung is judged against the machine it will actually have.
|
|
3
|
-
*
|
|
4
|
-
* The field case of 2026-08-15, in arithmetic: a copy of the picture took about
|
|
5
|
-
* 0.125 s of work per second of video (7.9-8.0x measured), a 240p rung needed
|
|
6
|
-
* about 1.05, and the two together are more than the one second per second the
|
|
7
|
-
* machine has. The budget priced the copy at nothing, offered the rung, and the
|
|
8
|
-
* viewer watched it fail.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import assert from "node:assert/strict";
|
|
12
|
-
import test from "node:test";
|
|
13
|
-
|
|
14
|
-
import { canSustainOutput } from "../services/hwaccel.js";
|
|
15
|
-
|
|
16
|
-
// A host that encodes 640x360 at 24 fps about eleven times over, and decodes
|
|
17
|
-
// 1080p24 at 2.6x — the addon host's own figures.
|
|
18
|
-
const BENCHMARK = [
|
|
19
|
-
{ preset: "fast", pixelsPerSec: 17.0e6 },
|
|
20
|
-
{ preset: "ultrafast", pixelsPerSec: 67.5e6 }
|
|
21
|
-
];
|
|
22
|
-
const DECODE_MODEL = { pixelTerm: 0.007742, bitrateTerm: 0, constantTerm: 0 };
|
|
23
|
-
const SOURCE = { megapixelsPerSecond: (1920 * 1080 * 24) / 1e6, megabitsPerSecond: 8 };
|
|
24
|
-
const RUNG_240P = 426 * 240 * 24;
|
|
25
|
-
|
|
26
|
-
test("a rung is judged on the machine it will have, not on an empty one", () => {
|
|
27
|
-
const alone = canSustainOutput({
|
|
28
|
-
benchmark: BENCHMARK,
|
|
29
|
-
decodeModel: DECODE_MODEL,
|
|
30
|
-
source: SOURCE,
|
|
31
|
-
outputPixelsPerSec: RUNG_240P
|
|
32
|
-
});
|
|
33
|
-
const beside = canSustainOutput({
|
|
34
|
-
benchmark: BENCHMARK,
|
|
35
|
-
decodeModel: DECODE_MODEL,
|
|
36
|
-
source: SOURCE,
|
|
37
|
-
outputPixelsPerSec: RUNG_240P,
|
|
38
|
-
// The picture is being copied beside it, at the cost the field measured.
|
|
39
|
-
concurrentCostSec: 0.125
|
|
40
|
-
});
|
|
41
|
-
assert.ok(alone.speed !== null && beside.speed !== null);
|
|
42
|
-
assert.ok(beside.speed < alone.speed, "sharing the machine cannot make a rung faster");
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test("nothing else running leaves the answer exactly as it was", () => {
|
|
46
|
-
const withZero = canSustainOutput({
|
|
47
|
-
benchmark: BENCHMARK,
|
|
48
|
-
decodeModel: DECODE_MODEL,
|
|
49
|
-
source: SOURCE,
|
|
50
|
-
outputPixelsPerSec: RUNG_240P,
|
|
51
|
-
concurrentCostSec: 0
|
|
52
|
-
});
|
|
53
|
-
const without = canSustainOutput({
|
|
54
|
-
benchmark: BENCHMARK,
|
|
55
|
-
decodeModel: DECODE_MODEL,
|
|
56
|
-
source: SOURCE,
|
|
57
|
-
outputPixelsPerSec: RUNG_240P
|
|
58
|
-
});
|
|
59
|
-
assert.equal(withZero.speed, without.speed);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test("a host with nothing measured still refuses nothing", () => {
|
|
63
|
-
const answer = canSustainOutput({
|
|
64
|
-
benchmark: [],
|
|
65
|
-
outputPixelsPerSec: RUNG_240P,
|
|
66
|
-
concurrentCostSec: 0.125
|
|
67
|
-
});
|
|
68
|
-
assert.equal(answer.sustainable, true);
|
|
69
|
-
assert.equal(answer.speed, null);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test("a cost heavy enough to fill the machine puts a rung below realtime", () => {
|
|
73
|
-
const answer = canSustainOutput({
|
|
74
|
-
benchmark: BENCHMARK,
|
|
75
|
-
decodeModel: DECODE_MODEL,
|
|
76
|
-
source: SOURCE,
|
|
77
|
-
outputPixelsPerSec: RUNG_240P,
|
|
78
|
-
concurrentCostSec: 0.9
|
|
79
|
-
});
|
|
80
|
-
assert.ok(answer.speed !== null && answer.speed < 1, "0.9 of the machine spent elsewhere leaves less than realtime");
|
|
81
|
-
assert.equal(answer.sustainable, false);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test("a soundtrack encoder is not free once it has been measured", () => {
|
|
85
|
-
// The second half of roadmap item 6. An audio rendition runs for as long as
|
|
86
|
-
// the picture does — it is a second encoder by construction, not an extra —
|
|
87
|
-
// and the budget counted it at nothing. Small beside a picture, and small is
|
|
88
|
-
// not zero: on a host where a rung needs almost the whole machine, this is
|
|
89
|
-
// the difference between offering it and refusing it.
|
|
90
|
-
const withoutAudio = canSustainOutput({
|
|
91
|
-
benchmark: BENCHMARK,
|
|
92
|
-
decodeModel: DECODE_MODEL,
|
|
93
|
-
source: SOURCE,
|
|
94
|
-
outputPixelsPerSec: RUNG_240P,
|
|
95
|
-
concurrentCostSec: 0.125
|
|
96
|
-
});
|
|
97
|
-
const withAudio = canSustainOutput({
|
|
98
|
-
benchmark: BENCHMARK,
|
|
99
|
-
decodeModel: DECODE_MODEL,
|
|
100
|
-
source: SOURCE,
|
|
101
|
-
outputPixelsPerSec: RUNG_240P,
|
|
102
|
-
// The copy, plus a soundtrack measured at about twenty times realtime.
|
|
103
|
-
concurrentCostSec: 0.125 + 0.05
|
|
104
|
-
});
|
|
105
|
-
assert.ok(
|
|
106
|
-
withAudio.speed < withoutAudio.speed,
|
|
107
|
-
"charging for the soundtrack must lower what the machine has left for the picture"
|
|
108
|
-
);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
test("two pictures encoding at once are charged as two", () => {
|
|
112
|
-
// The warm-up that makes a quality switch seamless runs two encoders on
|
|
113
|
-
// purpose. Measured 2026-08-15: the new rung ran at 0.504x for its first four
|
|
114
|
-
// seconds, against 0.90-0.99x once it had the machine to itself and 1.58x
|
|
115
|
-
// predicted for an idle one. Priced as though it were alone, the budget
|
|
116
|
-
// describes a machine that does not exist at the moment the viewer switches.
|
|
117
|
-
const alone = canSustainOutput({
|
|
118
|
-
benchmark: BENCHMARK,
|
|
119
|
-
decodeModel: DECODE_MODEL,
|
|
120
|
-
source: SOURCE,
|
|
121
|
-
outputPixelsPerSec: RUNG_240P,
|
|
122
|
-
concurrentCostSec: 0
|
|
123
|
-
});
|
|
124
|
-
const besideAnother = canSustainOutput({
|
|
125
|
-
benchmark: BENCHMARK,
|
|
126
|
-
decodeModel: DECODE_MODEL,
|
|
127
|
-
source: SOURCE,
|
|
128
|
-
outputPixelsPerSec: RUNG_240P,
|
|
129
|
-
// Another rung of the same family, already running at about realtime.
|
|
130
|
-
concurrentCostSec: 1 / 1.05
|
|
131
|
-
});
|
|
132
|
-
assert.ok(alone.sustainable, "the rung must be offerable on an idle machine, or the case is not the one meant");
|
|
133
|
-
assert.equal(
|
|
134
|
-
besideAnother.sustainable,
|
|
135
|
-
false,
|
|
136
|
-
"a machine already spending a second per second of video has nothing left for a second picture"
|
|
137
|
-
);
|
|
138
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file A rung is judged against the machine it will actually have.
|
|
3
|
+
*
|
|
4
|
+
* The field case of 2026-08-15, in arithmetic: a copy of the picture took about
|
|
5
|
+
* 0.125 s of work per second of video (7.9-8.0x measured), a 240p rung needed
|
|
6
|
+
* about 1.05, and the two together are more than the one second per second the
|
|
7
|
+
* machine has. The budget priced the copy at nothing, offered the rung, and the
|
|
8
|
+
* viewer watched it fail.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import test from "node:test";
|
|
13
|
+
|
|
14
|
+
import { canSustainOutput } from "../services/hwaccel.js";
|
|
15
|
+
|
|
16
|
+
// A host that encodes 640x360 at 24 fps about eleven times over, and decodes
|
|
17
|
+
// 1080p24 at 2.6x — the addon host's own figures.
|
|
18
|
+
const BENCHMARK = [
|
|
19
|
+
{ preset: "fast", pixelsPerSec: 17.0e6 },
|
|
20
|
+
{ preset: "ultrafast", pixelsPerSec: 67.5e6 }
|
|
21
|
+
];
|
|
22
|
+
const DECODE_MODEL = { pixelTerm: 0.007742, bitrateTerm: 0, constantTerm: 0 };
|
|
23
|
+
const SOURCE = { megapixelsPerSecond: (1920 * 1080 * 24) / 1e6, megabitsPerSecond: 8 };
|
|
24
|
+
const RUNG_240P = 426 * 240 * 24;
|
|
25
|
+
|
|
26
|
+
test("a rung is judged on the machine it will have, not on an empty one", () => {
|
|
27
|
+
const alone = canSustainOutput({
|
|
28
|
+
benchmark: BENCHMARK,
|
|
29
|
+
decodeModel: DECODE_MODEL,
|
|
30
|
+
source: SOURCE,
|
|
31
|
+
outputPixelsPerSec: RUNG_240P
|
|
32
|
+
});
|
|
33
|
+
const beside = canSustainOutput({
|
|
34
|
+
benchmark: BENCHMARK,
|
|
35
|
+
decodeModel: DECODE_MODEL,
|
|
36
|
+
source: SOURCE,
|
|
37
|
+
outputPixelsPerSec: RUNG_240P,
|
|
38
|
+
// The picture is being copied beside it, at the cost the field measured.
|
|
39
|
+
concurrentCostSec: 0.125
|
|
40
|
+
});
|
|
41
|
+
assert.ok(alone.speed !== null && beside.speed !== null);
|
|
42
|
+
assert.ok(beside.speed < alone.speed, "sharing the machine cannot make a rung faster");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("nothing else running leaves the answer exactly as it was", () => {
|
|
46
|
+
const withZero = canSustainOutput({
|
|
47
|
+
benchmark: BENCHMARK,
|
|
48
|
+
decodeModel: DECODE_MODEL,
|
|
49
|
+
source: SOURCE,
|
|
50
|
+
outputPixelsPerSec: RUNG_240P,
|
|
51
|
+
concurrentCostSec: 0
|
|
52
|
+
});
|
|
53
|
+
const without = canSustainOutput({
|
|
54
|
+
benchmark: BENCHMARK,
|
|
55
|
+
decodeModel: DECODE_MODEL,
|
|
56
|
+
source: SOURCE,
|
|
57
|
+
outputPixelsPerSec: RUNG_240P
|
|
58
|
+
});
|
|
59
|
+
assert.equal(withZero.speed, without.speed);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("a host with nothing measured still refuses nothing", () => {
|
|
63
|
+
const answer = canSustainOutput({
|
|
64
|
+
benchmark: [],
|
|
65
|
+
outputPixelsPerSec: RUNG_240P,
|
|
66
|
+
concurrentCostSec: 0.125
|
|
67
|
+
});
|
|
68
|
+
assert.equal(answer.sustainable, true);
|
|
69
|
+
assert.equal(answer.speed, null);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("a cost heavy enough to fill the machine puts a rung below realtime", () => {
|
|
73
|
+
const answer = canSustainOutput({
|
|
74
|
+
benchmark: BENCHMARK,
|
|
75
|
+
decodeModel: DECODE_MODEL,
|
|
76
|
+
source: SOURCE,
|
|
77
|
+
outputPixelsPerSec: RUNG_240P,
|
|
78
|
+
concurrentCostSec: 0.9
|
|
79
|
+
});
|
|
80
|
+
assert.ok(answer.speed !== null && answer.speed < 1, "0.9 of the machine spent elsewhere leaves less than realtime");
|
|
81
|
+
assert.equal(answer.sustainable, false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("a soundtrack encoder is not free once it has been measured", () => {
|
|
85
|
+
// The second half of roadmap item 6. An audio rendition runs for as long as
|
|
86
|
+
// the picture does — it is a second encoder by construction, not an extra —
|
|
87
|
+
// and the budget counted it at nothing. Small beside a picture, and small is
|
|
88
|
+
// not zero: on a host where a rung needs almost the whole machine, this is
|
|
89
|
+
// the difference between offering it and refusing it.
|
|
90
|
+
const withoutAudio = canSustainOutput({
|
|
91
|
+
benchmark: BENCHMARK,
|
|
92
|
+
decodeModel: DECODE_MODEL,
|
|
93
|
+
source: SOURCE,
|
|
94
|
+
outputPixelsPerSec: RUNG_240P,
|
|
95
|
+
concurrentCostSec: 0.125
|
|
96
|
+
});
|
|
97
|
+
const withAudio = canSustainOutput({
|
|
98
|
+
benchmark: BENCHMARK,
|
|
99
|
+
decodeModel: DECODE_MODEL,
|
|
100
|
+
source: SOURCE,
|
|
101
|
+
outputPixelsPerSec: RUNG_240P,
|
|
102
|
+
// The copy, plus a soundtrack measured at about twenty times realtime.
|
|
103
|
+
concurrentCostSec: 0.125 + 0.05
|
|
104
|
+
});
|
|
105
|
+
assert.ok(
|
|
106
|
+
withAudio.speed < withoutAudio.speed,
|
|
107
|
+
"charging for the soundtrack must lower what the machine has left for the picture"
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("two pictures encoding at once are charged as two", () => {
|
|
112
|
+
// The warm-up that makes a quality switch seamless runs two encoders on
|
|
113
|
+
// purpose. Measured 2026-08-15: the new rung ran at 0.504x for its first four
|
|
114
|
+
// seconds, against 0.90-0.99x once it had the machine to itself and 1.58x
|
|
115
|
+
// predicted for an idle one. Priced as though it were alone, the budget
|
|
116
|
+
// describes a machine that does not exist at the moment the viewer switches.
|
|
117
|
+
const alone = canSustainOutput({
|
|
118
|
+
benchmark: BENCHMARK,
|
|
119
|
+
decodeModel: DECODE_MODEL,
|
|
120
|
+
source: SOURCE,
|
|
121
|
+
outputPixelsPerSec: RUNG_240P,
|
|
122
|
+
concurrentCostSec: 0
|
|
123
|
+
});
|
|
124
|
+
const besideAnother = canSustainOutput({
|
|
125
|
+
benchmark: BENCHMARK,
|
|
126
|
+
decodeModel: DECODE_MODEL,
|
|
127
|
+
source: SOURCE,
|
|
128
|
+
outputPixelsPerSec: RUNG_240P,
|
|
129
|
+
// Another rung of the same family, already running at about realtime.
|
|
130
|
+
concurrentCostSec: 1 / 1.05
|
|
131
|
+
});
|
|
132
|
+
assert.ok(alone.sustainable, "the rung must be offerable on an idle machine, or the case is not the one meant");
|
|
133
|
+
assert.equal(
|
|
134
|
+
besideAnother.sustainable,
|
|
135
|
+
false,
|
|
136
|
+
"a machine already spending a second per second of video has nothing left for a second picture"
|
|
137
|
+
);
|
|
138
|
+
});
|
|
@@ -1,191 +1,191 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file What the plan believes exists is what the disk holds — with a real
|
|
3
|
-
* store, a real directory, and no ffmpeg.
|
|
4
|
-
*
|
|
5
|
-
* The failure these are written against, field 2026-09-07, twice in one evening
|
|
6
|
-
* on `Star.Trek.Strange.New.Worlds.S04E03`:
|
|
7
|
-
*
|
|
8
|
-
* > `encode: … ready=482 zones=[p100:#0..#0 …] runs=[#0..#0@0.0x] waiting=nobody`
|
|
9
|
-
* > `encode-run stopped #0..#0 …, reached #-1 (0 segment(s)) after 2778ms,`
|
|
10
|
-
* > `signal SIGTERM: the film is no worse off without it`
|
|
11
|
-
* > `encode: … ready=482 … runs=[] waiting=nobody`
|
|
12
|
-
*
|
|
13
|
-
* The map claimed every one of the film's 482 segments while the directory held
|
|
14
|
-
* nothing a header could be lifted out of, so every arrangement scored perfect,
|
|
15
|
-
* the only encoder was taken away as unnecessary and none was placed again. The
|
|
16
|
-
* viewer's browser asked for the init segment, was never answered, and gave up.
|
|
17
|
-
*
|
|
18
|
-
* Readiness had been accumulating for the life of the process: the map was told
|
|
19
|
-
* what existed and never told what had stopped existing — the word for that
|
|
20
|
-
* existed and was called from no line of the product. So these check the one
|
|
21
|
-
* property that makes the whole class of failure impossible: what the map says
|
|
22
|
-
* is ready is what the store can prove right now, and nothing older.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
import test from "node:test";
|
|
26
|
-
import assert from "node:assert/strict";
|
|
27
|
-
import { EventEmitter } from "node:events";
|
|
28
|
-
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
29
|
-
import os from "node:os";
|
|
30
|
-
import path from "node:path";
|
|
31
|
-
import { EncodeRun } from "../services/encode/EncodeRun.js";
|
|
32
|
-
import { SegmentStore } from "../services/encode/SegmentStore.js";
|
|
33
|
-
import { SoftwareEncoder } from "../services/encode/SoftwareEncoder.js";
|
|
34
|
-
import { EncodeOrchestrator } from "../services/orchestrators/EncodeOrchestrator.js";
|
|
35
|
-
import { fmp4Format } from "../services/segment-formats/fmp4.js";
|
|
36
|
-
|
|
37
|
-
const PICTURE = "torrent:abc:fmt=fmp4:grid=kf@0:video-only:v=0/copy";
|
|
38
|
-
const SEGMENTS = 482;
|
|
39
|
-
|
|
40
|
-
class FakeProcess extends EventEmitter {
|
|
41
|
-
constructor() {
|
|
42
|
-
super();
|
|
43
|
-
this.pid = 1;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
kill(signal) {
|
|
47
|
-
this.emit("exit", null, signal);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* A store on a real directory, and an orchestrator that owns it.
|
|
53
|
-
*
|
|
54
|
-
* @returns {{ made: EncodeOrchestrator, store: SegmentStore, dir: string,
|
|
55
|
-
* root: string, lines: string[] }}
|
|
56
|
-
*/
|
|
57
|
-
function orchestratorWithAStore() {
|
|
58
|
-
const root = mkdtempSync(path.join(os.tmpdir(), "coverage-disk-"));
|
|
59
|
-
const lines = [];
|
|
60
|
-
const logger = { info: (line) => lines.push(line), warn: (line) => lines.push(line) };
|
|
61
|
-
const store = new SegmentStore({ root, logger });
|
|
62
|
-
store.useFormat(PICTURE, fmp4Format);
|
|
63
|
-
const dir = store.directoryFor(PICTURE);
|
|
64
|
-
/** @type {EncodeOrchestrator} */
|
|
65
|
-
let made;
|
|
66
|
-
made = new EncodeOrchestrator({
|
|
67
|
-
maxRunsFor: () => 1,
|
|
68
|
-
segmentSeconds: 6,
|
|
69
|
-
killCostSec: 0,
|
|
70
|
-
firstByteWaitSec: 0.12,
|
|
71
|
-
refetchSecPerFilmSecond: () => 0.25,
|
|
72
|
-
startingSpeedFor: () => 2,
|
|
73
|
-
segmentStore: store,
|
|
74
|
-
now: () => 1000,
|
|
75
|
-
logger,
|
|
76
|
-
makeRun: ({ address, from, to }) => new EncodeRun({
|
|
77
|
-
address,
|
|
78
|
-
encoder: new SoftwareEncoder(),
|
|
79
|
-
from,
|
|
80
|
-
to,
|
|
81
|
-
buildArgs: () => ["-i", "in", "out"],
|
|
82
|
-
spawn: () => new FakeProcess(),
|
|
83
|
-
logger,
|
|
84
|
-
now: () => 1000,
|
|
85
|
-
onEnded: (ended) => made.noteEnded(ended)
|
|
86
|
-
})
|
|
87
|
-
});
|
|
88
|
-
made.setSegmentCount(PICTURE, SEGMENTS);
|
|
89
|
-
return { made, store, dir, root, lines };
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** A viewer stopped at the beginning, which is what a fresh session states. */
|
|
93
|
-
function aViewerAtTheStart(made) {
|
|
94
|
-
made.notePriorityMap(PICTURE, [
|
|
95
|
-
{ from: 0, to: 0, priority: 100, withinSeconds: 0 },
|
|
96
|
-
{ from: 1, to: SEGMENTS - 1, priority: 91, withinSeconds: 6 }
|
|
97
|
-
]);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Every segment of the film, written and reported closed — what the store looked
|
|
102
|
-
* like while the film was being watched.
|
|
103
|
-
*/
|
|
104
|
-
function theWholeFilmIsOnDisk(store, dir) {
|
|
105
|
-
for (let index = 0; index < SEGMENTS; index += 1) {
|
|
106
|
-
// Under the served name, which is the whole of what says a piece is closed:
|
|
107
|
-
// it takes that name only when its writer has said so. There used to be a
|
|
108
|
-
// statement kept beside the disk as well, told by whoever noticed a piece
|
|
109
|
-
// being produced — a second owner of one fact, and the fault this file's own
|
|
110
|
-
// subject is.
|
|
111
|
-
writeFileSync(path.join(dir, fmp4Format.segmentFileName(index)), Buffer.alloc(16));
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
test("a film that is entirely made needs no encoder", (t) => {
|
|
116
|
-
const { made, store, dir, root } = orchestratorWithAStore();
|
|
117
|
-
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
118
|
-
|
|
119
|
-
theWholeFilmIsOnDisk(store, dir);
|
|
120
|
-
aViewerAtTheStart(made);
|
|
121
|
-
made.reconcile();
|
|
122
|
-
|
|
123
|
-
// The other half of the property, and the reason the failure looked
|
|
124
|
-
// reasonable: when the film really is all there, taking the encoder away IS
|
|
125
|
-
// right, and the viewer is served from the disk.
|
|
126
|
-
assert.equal(made.runsOn(PICTURE).length, 0, "nothing left to make");
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
test("segments the store has lost stop being ready, and an encoder is placed again", (t) => {
|
|
130
|
-
const { made, store, dir, root } = orchestratorWithAStore();
|
|
131
|
-
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
132
|
-
|
|
133
|
-
theWholeFilmIsOnDisk(store, dir);
|
|
134
|
-
aViewerAtTheStart(made);
|
|
135
|
-
made.reconcile();
|
|
136
|
-
assert.equal(made.runsOn(PICTURE).length, 0);
|
|
137
|
-
|
|
138
|
-
// THE FIELD FAILURE. The output's directory goes — dropped for room, or by the
|
|
139
|
-
// idle rule, or because a killed process left it and the sweep cleared it. The
|
|
140
|
-
// map used to keep all 482 numbers and the plan went on believing the film was
|
|
141
|
-
// made, for the life of the process.
|
|
142
|
-
store.drop(PICTURE, "the test is taking its files away");
|
|
143
|
-
store.useFormat(PICTURE, fmp4Format);
|
|
144
|
-
store.directoryFor(PICTURE);
|
|
145
|
-
|
|
146
|
-
made.reconcile();
|
|
147
|
-
const runs = made.runsOn(PICTURE);
|
|
148
|
-
assert.equal(runs.length, 1, "the film is unmade again, so somebody makes it");
|
|
149
|
-
assert.equal(runs[0].from, 0, "beginning where the viewer is stopped");
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
test("a piece discarded with the run that had it open is not ready either", (t) => {
|
|
153
|
-
const { made, store, dir, root } = orchestratorWithAStore();
|
|
154
|
-
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
155
|
-
|
|
156
|
-
theWholeFilmIsOnDisk(store, dir);
|
|
157
|
-
aViewerAtTheStart(made);
|
|
158
|
-
made.reconcile();
|
|
159
|
-
assert.equal(made.runsOn(PICTURE).length, 0);
|
|
160
|
-
|
|
161
|
-
// A run stopped the instant after opening #0 leaves a file of no bytes under a
|
|
162
|
-
// name that reads as a segment; the store throws it away. Field 2026-09-07,
|
|
163
|
-
// 20:13:13 and 20:13:57 — printed twice while the map went on saying 482.
|
|
164
|
-
rmSync(path.join(dir, fmp4Format.segmentFileName(0)), { force: true });
|
|
165
|
-
|
|
166
|
-
made.reconcile();
|
|
167
|
-
assert.equal(made.runsOn(PICTURE).length, 1, "one number missing is one encoder");
|
|
168
|
-
assert.equal(made.runsOn(PICTURE)[0].from, 0);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
test("nobody making what somebody waits for is said out loud", (t) => {
|
|
172
|
-
const { made, root, lines } = orchestratorWithAStore();
|
|
173
|
-
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
174
|
-
|
|
175
|
-
// A HOST THAT CANNOT BUILD AN ENCODER. Whatever the reason — and there are
|
|
176
|
-
// several — the state it leaves is the one that cost two sessions on
|
|
177
|
-
// 2026-09-07: somebody is waiting, nothing is being made, and every line
|
|
178
|
-
// above reads as a healthy proxy. It ran for 41 seconds in the field with no
|
|
179
|
-
// word about it on either side, and ended at the browser's own timeout with a
|
|
180
|
-
// message naming no cause.
|
|
181
|
-
made.makeRun = () => null;
|
|
182
|
-
aViewerAtTheStart(made);
|
|
183
|
-
made.reconcile();
|
|
184
|
-
|
|
185
|
-
assert.equal(made.runsOn(PICTURE).length, 0);
|
|
186
|
-
const said = lines.find((line) => line.includes("NO ENCODER IS MAKING IT"));
|
|
187
|
-
assert.ok(said, "the state has a line of its own");
|
|
188
|
-
assert.ok(said.includes("#0"), "which number is wanted");
|
|
189
|
-
assert.ok(said.includes(PICTURE), "of which output, whole — not cut to sixty characters");
|
|
190
|
-
assert.ok(/files=\d+/.test(said), "and what the disk holds, beside what is proven");
|
|
191
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file What the plan believes exists is what the disk holds — with a real
|
|
3
|
+
* store, a real directory, and no ffmpeg.
|
|
4
|
+
*
|
|
5
|
+
* The failure these are written against, field 2026-09-07, twice in one evening
|
|
6
|
+
* on `Star.Trek.Strange.New.Worlds.S04E03`:
|
|
7
|
+
*
|
|
8
|
+
* > `encode: … ready=482 zones=[p100:#0..#0 …] runs=[#0..#0@0.0x] waiting=nobody`
|
|
9
|
+
* > `encode-run stopped #0..#0 …, reached #-1 (0 segment(s)) after 2778ms,`
|
|
10
|
+
* > `signal SIGTERM: the film is no worse off without it`
|
|
11
|
+
* > `encode: … ready=482 … runs=[] waiting=nobody`
|
|
12
|
+
*
|
|
13
|
+
* The map claimed every one of the film's 482 segments while the directory held
|
|
14
|
+
* nothing a header could be lifted out of, so every arrangement scored perfect,
|
|
15
|
+
* the only encoder was taken away as unnecessary and none was placed again. The
|
|
16
|
+
* viewer's browser asked for the init segment, was never answered, and gave up.
|
|
17
|
+
*
|
|
18
|
+
* Readiness had been accumulating for the life of the process: the map was told
|
|
19
|
+
* what existed and never told what had stopped existing — the word for that
|
|
20
|
+
* existed and was called from no line of the product. So these check the one
|
|
21
|
+
* property that makes the whole class of failure impossible: what the map says
|
|
22
|
+
* is ready is what the store can prove right now, and nothing older.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import test from "node:test";
|
|
26
|
+
import assert from "node:assert/strict";
|
|
27
|
+
import { EventEmitter } from "node:events";
|
|
28
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
29
|
+
import os from "node:os";
|
|
30
|
+
import path from "node:path";
|
|
31
|
+
import { EncodeRun } from "../services/encode/EncodeRun.js";
|
|
32
|
+
import { SegmentStore } from "../services/encode/SegmentStore.js";
|
|
33
|
+
import { SoftwareEncoder } from "../services/encode/SoftwareEncoder.js";
|
|
34
|
+
import { EncodeOrchestrator } from "../services/orchestrators/EncodeOrchestrator.js";
|
|
35
|
+
import { fmp4Format } from "../services/segment-formats/fmp4.js";
|
|
36
|
+
|
|
37
|
+
const PICTURE = "torrent:abc:fmt=fmp4:grid=kf@0:video-only:v=0/copy";
|
|
38
|
+
const SEGMENTS = 482;
|
|
39
|
+
|
|
40
|
+
class FakeProcess extends EventEmitter {
|
|
41
|
+
constructor() {
|
|
42
|
+
super();
|
|
43
|
+
this.pid = 1;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
kill(signal) {
|
|
47
|
+
this.emit("exit", null, signal);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A store on a real directory, and an orchestrator that owns it.
|
|
53
|
+
*
|
|
54
|
+
* @returns {{ made: EncodeOrchestrator, store: SegmentStore, dir: string,
|
|
55
|
+
* root: string, lines: string[] }}
|
|
56
|
+
*/
|
|
57
|
+
function orchestratorWithAStore() {
|
|
58
|
+
const root = mkdtempSync(path.join(os.tmpdir(), "coverage-disk-"));
|
|
59
|
+
const lines = [];
|
|
60
|
+
const logger = { info: (line) => lines.push(line), warn: (line) => lines.push(line) };
|
|
61
|
+
const store = new SegmentStore({ root, logger });
|
|
62
|
+
store.useFormat(PICTURE, fmp4Format);
|
|
63
|
+
const dir = store.directoryFor(PICTURE);
|
|
64
|
+
/** @type {EncodeOrchestrator} */
|
|
65
|
+
let made;
|
|
66
|
+
made = new EncodeOrchestrator({
|
|
67
|
+
maxRunsFor: () => 1,
|
|
68
|
+
segmentSeconds: 6,
|
|
69
|
+
killCostSec: 0,
|
|
70
|
+
firstByteWaitSec: 0.12,
|
|
71
|
+
refetchSecPerFilmSecond: () => 0.25,
|
|
72
|
+
startingSpeedFor: () => 2,
|
|
73
|
+
segmentStore: store,
|
|
74
|
+
now: () => 1000,
|
|
75
|
+
logger,
|
|
76
|
+
makeRun: ({ address, from, to }) => new EncodeRun({
|
|
77
|
+
address,
|
|
78
|
+
encoder: new SoftwareEncoder(),
|
|
79
|
+
from,
|
|
80
|
+
to,
|
|
81
|
+
buildArgs: () => ["-i", "in", "out"],
|
|
82
|
+
spawn: () => new FakeProcess(),
|
|
83
|
+
logger,
|
|
84
|
+
now: () => 1000,
|
|
85
|
+
onEnded: (ended) => made.noteEnded(ended)
|
|
86
|
+
})
|
|
87
|
+
});
|
|
88
|
+
made.setSegmentCount(PICTURE, SEGMENTS);
|
|
89
|
+
return { made, store, dir, root, lines };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** A viewer stopped at the beginning, which is what a fresh session states. */
|
|
93
|
+
function aViewerAtTheStart(made) {
|
|
94
|
+
made.notePriorityMap(PICTURE, [
|
|
95
|
+
{ from: 0, to: 0, priority: 100, withinSeconds: 0 },
|
|
96
|
+
{ from: 1, to: SEGMENTS - 1, priority: 91, withinSeconds: 6 }
|
|
97
|
+
]);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Every segment of the film, written and reported closed — what the store looked
|
|
102
|
+
* like while the film was being watched.
|
|
103
|
+
*/
|
|
104
|
+
function theWholeFilmIsOnDisk(store, dir) {
|
|
105
|
+
for (let index = 0; index < SEGMENTS; index += 1) {
|
|
106
|
+
// Under the served name, which is the whole of what says a piece is closed:
|
|
107
|
+
// it takes that name only when its writer has said so. There used to be a
|
|
108
|
+
// statement kept beside the disk as well, told by whoever noticed a piece
|
|
109
|
+
// being produced — a second owner of one fact, and the fault this file's own
|
|
110
|
+
// subject is.
|
|
111
|
+
writeFileSync(path.join(dir, fmp4Format.segmentFileName(index)), Buffer.alloc(16));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
test("a film that is entirely made needs no encoder", (t) => {
|
|
116
|
+
const { made, store, dir, root } = orchestratorWithAStore();
|
|
117
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
118
|
+
|
|
119
|
+
theWholeFilmIsOnDisk(store, dir);
|
|
120
|
+
aViewerAtTheStart(made);
|
|
121
|
+
made.reconcile();
|
|
122
|
+
|
|
123
|
+
// The other half of the property, and the reason the failure looked
|
|
124
|
+
// reasonable: when the film really is all there, taking the encoder away IS
|
|
125
|
+
// right, and the viewer is served from the disk.
|
|
126
|
+
assert.equal(made.runsOn(PICTURE).length, 0, "nothing left to make");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("segments the store has lost stop being ready, and an encoder is placed again", (t) => {
|
|
130
|
+
const { made, store, dir, root } = orchestratorWithAStore();
|
|
131
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
132
|
+
|
|
133
|
+
theWholeFilmIsOnDisk(store, dir);
|
|
134
|
+
aViewerAtTheStart(made);
|
|
135
|
+
made.reconcile();
|
|
136
|
+
assert.equal(made.runsOn(PICTURE).length, 0);
|
|
137
|
+
|
|
138
|
+
// THE FIELD FAILURE. The output's directory goes — dropped for room, or by the
|
|
139
|
+
// idle rule, or because a killed process left it and the sweep cleared it. The
|
|
140
|
+
// map used to keep all 482 numbers and the plan went on believing the film was
|
|
141
|
+
// made, for the life of the process.
|
|
142
|
+
store.drop(PICTURE, "the test is taking its files away");
|
|
143
|
+
store.useFormat(PICTURE, fmp4Format);
|
|
144
|
+
store.directoryFor(PICTURE);
|
|
145
|
+
|
|
146
|
+
made.reconcile();
|
|
147
|
+
const runs = made.runsOn(PICTURE);
|
|
148
|
+
assert.equal(runs.length, 1, "the film is unmade again, so somebody makes it");
|
|
149
|
+
assert.equal(runs[0].from, 0, "beginning where the viewer is stopped");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("a piece discarded with the run that had it open is not ready either", (t) => {
|
|
153
|
+
const { made, store, dir, root } = orchestratorWithAStore();
|
|
154
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
155
|
+
|
|
156
|
+
theWholeFilmIsOnDisk(store, dir);
|
|
157
|
+
aViewerAtTheStart(made);
|
|
158
|
+
made.reconcile();
|
|
159
|
+
assert.equal(made.runsOn(PICTURE).length, 0);
|
|
160
|
+
|
|
161
|
+
// A run stopped the instant after opening #0 leaves a file of no bytes under a
|
|
162
|
+
// name that reads as a segment; the store throws it away. Field 2026-09-07,
|
|
163
|
+
// 20:13:13 and 20:13:57 — printed twice while the map went on saying 482.
|
|
164
|
+
rmSync(path.join(dir, fmp4Format.segmentFileName(0)), { force: true });
|
|
165
|
+
|
|
166
|
+
made.reconcile();
|
|
167
|
+
assert.equal(made.runsOn(PICTURE).length, 1, "one number missing is one encoder");
|
|
168
|
+
assert.equal(made.runsOn(PICTURE)[0].from, 0);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("nobody making what somebody waits for is said out loud", (t) => {
|
|
172
|
+
const { made, root, lines } = orchestratorWithAStore();
|
|
173
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
174
|
+
|
|
175
|
+
// A HOST THAT CANNOT BUILD AN ENCODER. Whatever the reason — and there are
|
|
176
|
+
// several — the state it leaves is the one that cost two sessions on
|
|
177
|
+
// 2026-09-07: somebody is waiting, nothing is being made, and every line
|
|
178
|
+
// above reads as a healthy proxy. It ran for 41 seconds in the field with no
|
|
179
|
+
// word about it on either side, and ended at the browser's own timeout with a
|
|
180
|
+
// message naming no cause.
|
|
181
|
+
made.makeRun = () => null;
|
|
182
|
+
aViewerAtTheStart(made);
|
|
183
|
+
made.reconcile();
|
|
184
|
+
|
|
185
|
+
assert.equal(made.runsOn(PICTURE).length, 0);
|
|
186
|
+
const said = lines.find((line) => line.includes("NO ENCODER IS MAKING IT"));
|
|
187
|
+
assert.ok(said, "the state has a line of its own");
|
|
188
|
+
assert.ok(said.includes("#0"), "which number is wanted");
|
|
189
|
+
assert.ok(said.includes(PICTURE), "of which output, whole — not cut to sixty characters");
|
|
190
|
+
assert.ok(/files=\d+/.test(said), "and what the disk holds, beside what is proven");
|
|
191
|
+
});
|