@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.
- package/CHANGELOG.md +22 -0
- package/docs/encode-architecture.md +51 -2
- package/package.json +1 -1
- package/research/double-spawn-2026-09-10.md +171 -0
- package/services/disk/DiskSpace.js +146 -0
- package/services/disk/wire.js +60 -0
- package/services/encode/EncodeRun.js +37 -9
- package/services/encode/SegmentStore.js +284 -232
- package/services/encode/run-command.js +16 -1
- package/services/hls-session-manager.js +31 -128
- package/services/orchestrators/EncodeOrchestrator.js +52 -38
- 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/segment-formats/fmp4.js +54 -0
- package/services/segment-formats/mpegts.js +54 -0
- 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 -187
- package/test/coverage-map.test.js +195 -195
- package/test/declared-tracks.test.js +35 -35
- package/test/disk-space.test.js +138 -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 -195
- 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/produced-copy-choice.test.js +258 -358
- 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-serve-wiring.test.js +8 -9
- package/test/segment-store-eviction.test.js +232 -0
- package/test/segment-store.test.js +238 -216
- 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/encode/open-piece.js +0 -135
- package/services/piece-store/disk-tier.js +0 -151
- package/test/open-piece.test.js +0 -152
package/test/video-facts.test.js
CHANGED
|
@@ -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
|
-
}
|