@torrent-tv/proxy 2.80.5 → 2.80.6
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 +32 -0
- package/package.json +1 -1
- package/routes/api/delivery-sink/get.js +8 -4
- package/server.js +415 -403
- package/services/data-channel-handler.js +87 -25
- package/services/delivery-probe.js +38 -5
- package/services/encode/CoverageMap.js +77 -4
- package/services/encode/EncodePlan.js +1025 -358
- package/services/encode/EncodeRun.js +19 -1
- package/services/encode/SegmentDemand.js +0 -0
- package/services/encode/SegmentStore.js +53 -1
- package/services/encode/open-piece.js +22 -1
- package/services/encode/run-command.js +12 -2
- package/services/hls-session-manager.js +38 -158
- package/services/hwaccel.js +182 -54
- package/services/orchestrators/EncodeOrchestrator.js +118 -89
- package/services/output/LiveOutputs.js +233 -213
- package/services/output/Timeline.js +333 -256
- package/services/priority/PriorityMap.js +262 -108
- package/services/priority/PriorityOrchestrator.js +31 -6
- package/services/quality/EncodeCost.js +555 -500
- package/services/torrent-pool.js +9 -4
- package/test/encode-orchestrator.test.js +195 -65
- package/test/encode-plan-viewers.test.js +719 -0
- package/test/encode-plan.test.js +174 -81
- package/test/open-piece.test.js +40 -0
- package/test/output-speed.test.js +86 -0
- package/test/priority-map-download.test.js +25 -7
- package/test/priority-map.test.js +134 -83
- package/test/seek-landing.test.js +109 -76
- package/test/segment-demand.test.js +54 -56
- package/test/wedge-certainty.test.js +3 -3
|
@@ -1,112 +1,163 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file
|
|
2
|
+
* @file The priority map: one number per second of film.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The map is literally a map — an array as long as the film, saying how urgently
|
|
5
|
+
* each second is wanted, and beside it how long until somebody plays it. What is
|
|
6
|
+
* checked here is what the numbers MEAN, since only their order is meaningful:
|
|
7
|
+
* in front of a viewer beats behind them, nearer beats further, and two viewers
|
|
8
|
+
* merge to the more urgent of them.
|
|
9
|
+
*
|
|
10
|
+
* Nothing here spawns anything, reads a disk or looks at a clock: this layer is
|
|
11
|
+
* exercised with plain values alone, which is the layer check made executable.
|
|
7
12
|
*/
|
|
8
13
|
|
|
9
14
|
import test from "node:test";
|
|
10
15
|
import assert from "node:assert/strict";
|
|
11
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
emptyMap,
|
|
18
|
+
inWorkingOrder,
|
|
19
|
+
isBehindEverybody,
|
|
20
|
+
mapForViewer,
|
|
21
|
+
mergeMaps,
|
|
22
|
+
runsOf
|
|
23
|
+
} from "../services/priority/PriorityMap.js";
|
|
24
|
+
|
|
25
|
+
const FILM = 3600;
|
|
26
|
+
const ALLOWANCE = 8;
|
|
12
27
|
|
|
13
|
-
|
|
14
|
-
|
|
28
|
+
/**
|
|
29
|
+
* @param {number} atSeconds
|
|
30
|
+
* @param {boolean} [playing]
|
|
31
|
+
*/
|
|
32
|
+
function viewer(atSeconds, playing = true) {
|
|
33
|
+
return mapForViewer({
|
|
34
|
+
atSeconds,
|
|
35
|
+
durationSeconds: FILM,
|
|
36
|
+
allowanceSeconds: ALLOWANCE,
|
|
37
|
+
playing
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
test("the map is one entry per second of film", () => {
|
|
42
|
+
const map = viewer(600);
|
|
43
|
+
assert.equal(map.durationSeconds, FILM);
|
|
44
|
+
assert.equal(map.priority.length, FILM);
|
|
45
|
+
assert.equal(map.secondsUntilPlayed.length, FILM);
|
|
46
|
+
assert.equal(map.behind.length, FILM);
|
|
47
|
+
});
|
|
15
48
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
previousEnd = zone.to;
|
|
22
|
-
}
|
|
49
|
+
test("the nearer a viewer is to a second, the higher its number", () => {
|
|
50
|
+
// The whole of what a priority means: which of two seconds is wanted first.
|
|
51
|
+
const map = viewer(600);
|
|
52
|
+
assert.ok(map.priority[600] > map.priority[700], "their own second beats one a hundred on");
|
|
53
|
+
assert.ok(map.priority[700] > map.priority[3000], "and that beats one forty minutes on");
|
|
23
54
|
});
|
|
24
55
|
|
|
25
|
-
test("
|
|
26
|
-
const
|
|
27
|
-
|
|
56
|
+
test("the time is the distance, because a viewer covers a second of film in a second", () => {
|
|
57
|
+
const map = viewer(600);
|
|
58
|
+
assert.equal(map.secondsUntilPlayed[600], 0, "they are there now");
|
|
59
|
+
assert.equal(map.secondsUntilPlayed[660], 60, "a minute of film away is a minute away");
|
|
60
|
+
});
|
|
28
61
|
|
|
29
|
-
|
|
62
|
+
test("what is behind a viewer is wanted, and wanted last", () => {
|
|
63
|
+
// Still wanted — a seek back must be cheap — but it yields to everything
|
|
64
|
+
// anybody is walking towards, however far off that is.
|
|
65
|
+
const map = viewer(600);
|
|
66
|
+
assert.equal(map.behind[599], 1);
|
|
67
|
+
assert.equal(map.behind[600], 0);
|
|
68
|
+
assert.ok(isBehindEverybody(map.priority[599]));
|
|
69
|
+
assert.ok(map.priority[599] < map.priority[3599],
|
|
70
|
+
"the last second of the film outranks the second they just watched");
|
|
71
|
+
assert.equal(map.secondsUntilPlayed[599], Number.POSITIVE_INFINITY,
|
|
72
|
+
"nobody is on their way there, so there is no time by which it must exist");
|
|
73
|
+
});
|
|
30
74
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
75
|
+
test("a viewer who has stopped the picture keeps their position, and loses their times", () => {
|
|
76
|
+
// A pause removes the time, not the direction. Collapsed to one flat value
|
|
77
|
+
// over the whole film, as it was, their position disappeared entirely — and
|
|
78
|
+
// with it the rule that what is in front of them is made first.
|
|
79
|
+
const map = viewer(600, false);
|
|
80
|
+
assert.equal(map.behind[599], 1, "what they have watched is still behind them");
|
|
81
|
+
assert.equal(map.behind[600], 0, "and what they have not is still in front");
|
|
82
|
+
assert.ok(map.priority[600] > map.priority[599], "in front still outranks behind");
|
|
83
|
+
assert.ok(map.priority[600] > map.priority[3000], "and nearer still outranks further");
|
|
84
|
+
for (const second of [599, 600, 3000]) {
|
|
85
|
+
assert.equal(map.secondsUntilPlayed[second], Number.POSITIVE_INFINITY,
|
|
86
|
+
"but nothing has a time, because they are on their way nowhere");
|
|
35
87
|
}
|
|
36
|
-
const priorityAt = (at) => merged.find((zone) => at >= zone.from && at < zone.to)?.priority;
|
|
37
|
-
assert.equal(priorityAt(0), priorityAt(500), "both viewers' own positions are equally urgent");
|
|
38
|
-
assert.ok(priorityAt(500) > priorityAt(300), "a viewer at 500 outranks the far zone of the one at 0");
|
|
39
88
|
});
|
|
40
89
|
|
|
41
|
-
test("
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
assert.ok(firstTwo.includes(0), "the first viewer's own position is taken first");
|
|
51
|
-
assert.ok(firstTwo.includes(2000), "so is the second viewer's, before anything less urgent");
|
|
90
|
+
test("anybody who is watching outranks anybody who has stopped", () => {
|
|
91
|
+
// An ordering fact rather than a chosen number: somebody watching needs their
|
|
92
|
+
// next second almost at once, while somebody stopped needs theirs at a time
|
|
93
|
+
// nothing here knows.
|
|
94
|
+
const watching = viewer(600);
|
|
95
|
+
const stopped = viewer(3000, false);
|
|
96
|
+
assert.ok(watching.priority[3599] > stopped.priority[3000],
|
|
97
|
+
"the far tail of a watching viewer beats the very next second of a stopped one");
|
|
52
98
|
});
|
|
53
99
|
|
|
54
|
-
test("
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
]);
|
|
100
|
+
test("two viewers merge to the more urgent of them, second by second", () => {
|
|
101
|
+
const merged = mergeMaps([viewer(600), viewer(1800)]);
|
|
102
|
+
assert.equal(merged.priority[1800], merged.priority[600],
|
|
103
|
+
"each of them is at the top of the scale where they stand");
|
|
104
|
+
assert.equal(merged.secondsUntilPlayed[1800], 0, "the nearer time wins");
|
|
105
|
+
assert.equal(merged.behind[599], 1, "behind both of them is behind");
|
|
106
|
+
assert.equal(merged.behind[700], 0, "and in front of ANYBODY is in front");
|
|
107
|
+
});
|
|
60
108
|
|
|
61
|
-
|
|
109
|
+
test("the second viewer's position is not buried under the first viewer's distance", () => {
|
|
110
|
+
// The failure this guards: one viewer's far film outranking another viewer's
|
|
111
|
+
// own second, so the second of them is served by nobody.
|
|
112
|
+
const merged = mergeMaps([viewer(600), viewer(1800)]);
|
|
113
|
+
assert.ok(merged.priority[1800] > merged.priority[1799],
|
|
114
|
+
"where the second viewer stands beats the film just before them");
|
|
62
115
|
});
|
|
63
116
|
|
|
64
|
-
test("
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
117
|
+
test("a film nobody is watching wants nothing", () => {
|
|
118
|
+
const merged = mergeMaps([]);
|
|
119
|
+
assert.equal(merged.durationSeconds, 0);
|
|
120
|
+
assert.deepEqual(runsOf(merged), []);
|
|
121
|
+
});
|
|
69
122
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
123
|
+
test("the map as stretches says the same thing in fewer numbers", () => {
|
|
124
|
+
const runs = runsOf(viewer(600));
|
|
125
|
+
assert.ok(runs.length > 1 && runs.length < 40, "a handful of stretches, not thousands");
|
|
126
|
+
assert.equal(runs[0].from, 0, "starting at the beginning of the film");
|
|
127
|
+
assert.equal(runs[0].behind, true, "which is behind them");
|
|
128
|
+
assert.equal(runs[runs.length - 1].to, FILM, "and ending at its end");
|
|
129
|
+
for (let index = 0; index < runs.length - 1; index += 1) {
|
|
130
|
+
assert.equal(runs[index].to, runs[index + 1].from, "with no gaps");
|
|
74
131
|
}
|
|
75
132
|
});
|
|
76
133
|
|
|
77
|
-
test("the
|
|
78
|
-
// Near the viewer the difference between now and ten seconds away decides
|
|
79
|
-
//
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
assert.
|
|
134
|
+
test("the stretches widen with distance, so any film is a handful of them", () => {
|
|
135
|
+
// Near the viewer the difference between now and ten seconds away decides what
|
|
136
|
+
// is made first; twenty minutes out, one more division changes no decision.
|
|
137
|
+
const ahead = runsOf(viewer(0)).filter((run) => run.behind === false);
|
|
138
|
+
for (let index = 1; index < ahead.length; index += 1) {
|
|
139
|
+
assert.ok(ahead[index].to - ahead[index].from >= ahead[index - 1].to - ahead[index - 1].from,
|
|
140
|
+
"each stretch is at least as wide as the one before it");
|
|
141
|
+
}
|
|
142
|
+
assert.ok(ahead[0].to - ahead[0].from <= ALLOWANCE + 1,
|
|
143
|
+
"and the first is as wide as the measured allowance");
|
|
86
144
|
});
|
|
87
145
|
|
|
88
|
-
test("
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
146
|
+
test("the working order is most urgent first, earliest film within one priority", () => {
|
|
147
|
+
const ordered = inWorkingOrder(runsOf(viewer(600)));
|
|
148
|
+
for (let index = 0; index < ordered.length - 1; index += 1) {
|
|
149
|
+
const left = ordered[index];
|
|
150
|
+
const right = ordered[index + 1];
|
|
151
|
+
assert.ok(
|
|
152
|
+
left.priority > right.priority || (left.priority === right.priority && left.from < right.from),
|
|
153
|
+
"sorted by priority, then by position"
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
assert.equal(ordered[0].from, 600, "and the first of all is where the viewer is stopped");
|
|
98
157
|
});
|
|
99
158
|
|
|
100
|
-
test("a
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
atSeconds: 300,
|
|
105
|
-
durationSeconds: 3000,
|
|
106
|
-
allowanceSeconds: 10,
|
|
107
|
-
playing: false
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
assert.equal(map.length, 1);
|
|
111
|
-
assert.deepEqual({ from: map[0].from, to: map[0].to }, { from: 0, to: 3000 });
|
|
159
|
+
test("a map of no length is a statement, and it says nothing is wanted", () => {
|
|
160
|
+
const map = emptyMap(0);
|
|
161
|
+
assert.equal(map.durationSeconds, 0);
|
|
162
|
+
assert.deepEqual(runsOf(map), []);
|
|
112
163
|
});
|
|
@@ -1,76 +1,109 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Ask ffmpeg late enough that it lands where we meant.
|
|
3
|
-
*
|
|
4
|
-
* `fftools/ffmpeg_demux.c` moves an input seek back by `3*AV_TIME_BASE / 23` —
|
|
5
|
-
* 130.435 ms — whenever the container does not declare `AVFMT_SEEK_TO_PTS` and
|
|
6
|
-
* a stream carries B-frames. So asking for a keyframe lands on the one before
|
|
7
|
-
* it, and since `-segment_times` is measured from where the run really began,
|
|
8
|
-
* every cut of that run inherits the shift.
|
|
9
|
-
*
|
|
10
|
-
* Measured 2026-08-21 on Matroska with keyframes every 2 s: `-ss 10` produced a
|
|
11
|
-
* first segment starting at 8.000, `-ss 10.130435` one starting at 10.000. On
|
|
12
|
-
* MP4, where the heuristic does not fire, 10, 10.130435 and 10.2 all produced
|
|
13
|
-
* 10.000 — right in one case, harmless in the other.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import assert from "node:assert/strict";
|
|
17
|
-
import { SourceFile } from "../services/source/SourceFile.js";
|
|
18
|
-
import test from "node:test";
|
|
19
|
-
|
|
20
|
-
import { seekLandingOffsetFor } from "../services/hls-session-manager.js";
|
|
21
|
-
|
|
22
|
-
const OFFSET = 3 / 23;
|
|
23
|
-
|
|
24
|
-
test("a copied picture is asked for one heuristic later than the keyframe", () => {
|
|
25
|
-
const session = { transcodeVideo: false, file: new SourceFile({ sourceKey: "s", fileIndex: 0 }).learn({ keyframeTimes: [0, 2.002, 4.004, 6.006] }) };
|
|
26
|
-
assert.equal(seekLandingOffsetFor(session, 2.002), OFFSET);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test("a re-encode is asked for exactly what it should produce", () => {
|
|
30
|
-
// It decodes from the keyframe and discards frames up to the requested time,
|
|
31
|
-
// so pushing the request later would start its output late.
|
|
32
|
-
const session = { transcodeVideo: true, file: new SourceFile({ sourceKey: "s", fileIndex: 0 }).learn({ keyframeTimes: [0, 2.002, 4.004] }) };
|
|
33
|
-
assert.equal(seekLandingOffsetFor(session, 2.002), 0);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test("the offset never reaches the next keyframe", () => {
|
|
37
|
-
// Keyframes 0.1 s apart: half of that is the most that can be added without
|
|
38
|
-
// risking a landing on the NEXT one where the heuristic does not fire.
|
|
39
|
-
const session = { transcodeVideo: false, file: new SourceFile({ sourceKey: "s", fileIndex: 0 }).learn({ keyframeTimes: [0, 0.1, 0.2, 0.3] }) };
|
|
40
|
-
assert.equal(seekLandingOffsetFor(session, 0.1), 0.05);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test("the last keyframe has nothing after it to collide with", () => {
|
|
44
|
-
const session = { transcodeVideo: false, file: new SourceFile({ sourceKey: "s", fileIndex: 0 }).learn({ keyframeTimes: [0, 2.002, 4.004] }) };
|
|
45
|
-
assert.equal(seekLandingOffsetFor(session, 4.004), OFFSET);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test("no keyframe list is still answered", () => {
|
|
49
|
-
assert.equal(seekLandingOffsetFor({ transcodeVideo: false }, 5), OFFSET);
|
|
50
|
-
assert.equal(seekLandingOffsetFor(null, 5), OFFSET);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test("a grid whose times are approximate is asked for that much later again", () => {
|
|
54
|
-
// AVI names a keyframe by its frame NUMBER and the time is that number times
|
|
55
|
-
// the frame duration, so a name can sit just BELOW the keyframe it refers to
|
|
56
|
-
// — measured 2026-08-21, 10-44 ms out on two files, always under one frame.
|
|
57
|
-
// Asking at the name alone would seek to before the real keyframe and land on
|
|
58
|
-
// the one before that, which is the fault this offset exists for.
|
|
59
|
-
const session = {
|
|
60
|
-
transcodeVideo: false,
|
|
61
|
-
file: new SourceFile({ sourceKey: "s", fileIndex: 0 }).learn({ keyframeTimes: [0, 4.004, 8.008, 12.012], keyframeTolerance: 0.04 })
|
|
62
|
-
};
|
|
63
|
-
assert.equal(seekLandingOffsetFor(session, 4.004), OFFSET + 0.04);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test("an exact grid claims no tolerance", () => {
|
|
67
|
-
// Matroska and MP4 state instants outright — measured the same day, nine
|
|
68
|
-
// files and 11 665 keyframes with not one disagreement.
|
|
69
|
-
const session = { transcodeVideo: false, file: new SourceFile({ sourceKey: "s", fileIndex: 0 }).learn({ keyframeTimes: [0, 4.004, 8.008], keyframeTolerance: 0 }) };
|
|
70
|
-
assert.equal(seekLandingOffsetFor(session, 4.004), OFFSET);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test("the bound still holds once a tolerance is added", () => {
|
|
74
|
-
const session = { transcodeVideo: false, file: new SourceFile({ sourceKey: "s", fileIndex: 0 }).learn({ keyframeTimes: [0, 0.1, 0.2], keyframeTolerance: 1 }) };
|
|
75
|
-
assert.equal(seekLandingOffsetFor(session, 0.1), 0.05);
|
|
76
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file Ask ffmpeg late enough that it lands where we meant.
|
|
3
|
+
*
|
|
4
|
+
* `fftools/ffmpeg_demux.c` moves an input seek back by `3*AV_TIME_BASE / 23` —
|
|
5
|
+
* 130.435 ms — whenever the container does not declare `AVFMT_SEEK_TO_PTS` and
|
|
6
|
+
* a stream carries B-frames. So asking for a keyframe lands on the one before
|
|
7
|
+
* it, and since `-segment_times` is measured from where the run really began,
|
|
8
|
+
* every cut of that run inherits the shift.
|
|
9
|
+
*
|
|
10
|
+
* Measured 2026-08-21 on Matroska with keyframes every 2 s: `-ss 10` produced a
|
|
11
|
+
* first segment starting at 8.000, `-ss 10.130435` one starting at 10.000. On
|
|
12
|
+
* MP4, where the heuristic does not fire, 10, 10.130435 and 10.2 all produced
|
|
13
|
+
* 10.000 — right in one case, harmless in the other.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import assert from "node:assert/strict";
|
|
17
|
+
import { SourceFile } from "../services/source/SourceFile.js";
|
|
18
|
+
import test from "node:test";
|
|
19
|
+
|
|
20
|
+
import { seekLandingOffsetFor } from "../services/hls-session-manager.js";
|
|
21
|
+
|
|
22
|
+
const OFFSET = 3 / 23;
|
|
23
|
+
|
|
24
|
+
test("a copied picture is asked for one heuristic later than the keyframe", () => {
|
|
25
|
+
const session = { transcodeVideo: false, file: new SourceFile({ sourceKey: "s", fileIndex: 0 }).learn({ keyframeTimes: [0, 2.002, 4.004, 6.006] }) };
|
|
26
|
+
assert.equal(seekLandingOffsetFor(session, 2.002), OFFSET);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("a re-encode is asked for exactly what it should produce", () => {
|
|
30
|
+
// It decodes from the keyframe and discards frames up to the requested time,
|
|
31
|
+
// so pushing the request later would start its output late.
|
|
32
|
+
const session = { transcodeVideo: true, file: new SourceFile({ sourceKey: "s", fileIndex: 0 }).learn({ keyframeTimes: [0, 2.002, 4.004] }) };
|
|
33
|
+
assert.equal(seekLandingOffsetFor(session, 2.002), 0);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("the offset never reaches the next keyframe", () => {
|
|
37
|
+
// Keyframes 0.1 s apart: half of that is the most that can be added without
|
|
38
|
+
// risking a landing on the NEXT one where the heuristic does not fire.
|
|
39
|
+
const session = { transcodeVideo: false, file: new SourceFile({ sourceKey: "s", fileIndex: 0 }).learn({ keyframeTimes: [0, 0.1, 0.2, 0.3] }) };
|
|
40
|
+
assert.equal(seekLandingOffsetFor(session, 0.1), 0.05);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("the last keyframe has nothing after it to collide with", () => {
|
|
44
|
+
const session = { transcodeVideo: false, file: new SourceFile({ sourceKey: "s", fileIndex: 0 }).learn({ keyframeTimes: [0, 2.002, 4.004] }) };
|
|
45
|
+
assert.equal(seekLandingOffsetFor(session, 4.004), OFFSET);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("no keyframe list is still answered", () => {
|
|
49
|
+
assert.equal(seekLandingOffsetFor({ transcodeVideo: false }, 5), OFFSET);
|
|
50
|
+
assert.equal(seekLandingOffsetFor(null, 5), OFFSET);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("a grid whose times are approximate is asked for that much later again", () => {
|
|
54
|
+
// AVI names a keyframe by its frame NUMBER and the time is that number times
|
|
55
|
+
// the frame duration, so a name can sit just BELOW the keyframe it refers to
|
|
56
|
+
// — measured 2026-08-21, 10-44 ms out on two files, always under one frame.
|
|
57
|
+
// Asking at the name alone would seek to before the real keyframe and land on
|
|
58
|
+
// the one before that, which is the fault this offset exists for.
|
|
59
|
+
const session = {
|
|
60
|
+
transcodeVideo: false,
|
|
61
|
+
file: new SourceFile({ sourceKey: "s", fileIndex: 0 }).learn({ keyframeTimes: [0, 4.004, 8.008, 12.012], keyframeTolerance: 0.04 })
|
|
62
|
+
};
|
|
63
|
+
assert.equal(seekLandingOffsetFor(session, 4.004), OFFSET + 0.04);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("an exact grid claims no tolerance", () => {
|
|
67
|
+
// Matroska and MP4 state instants outright — measured the same day, nine
|
|
68
|
+
// files and 11 665 keyframes with not one disagreement.
|
|
69
|
+
const session = { transcodeVideo: false, file: new SourceFile({ sourceKey: "s", fileIndex: 0 }).learn({ keyframeTimes: [0, 4.004, 8.008], keyframeTolerance: 0 }) };
|
|
70
|
+
assert.equal(seekLandingOffsetFor(session, 4.004), OFFSET);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("the bound still holds once a tolerance is added", () => {
|
|
74
|
+
const session = { transcodeVideo: false, file: new SourceFile({ sourceKey: "s", fileIndex: 0 }).learn({ keyframeTimes: [0, 0.1, 0.2], keyframeTolerance: 1 }) };
|
|
75
|
+
assert.equal(seekLandingOffsetFor(session, 0.1), 0.05);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("an output carrying only sound is not pushed past what it asked for", () => {
|
|
79
|
+
// The field failure of 2026-09-06: the sound played 130 ms ahead of the
|
|
80
|
+
// picture from every restart onward, and the viewer saw lips out of step with
|
|
81
|
+
// the voice from two minutes in.
|
|
82
|
+
//
|
|
83
|
+
// The offset exists because ffmpeg's demuxer moves a seek target back for a
|
|
84
|
+
// container it reads in decode order, after which a COPY lands on the previous
|
|
85
|
+
// keyframe. A re-encode trims to the requested time itself and is excluded —
|
|
86
|
+
// and an output with no picture is exactly that, since its one track is
|
|
87
|
+
// `-c:a aac`. It was not excluded, because the test asked whether the PICTURE
|
|
88
|
+
// is re-encoded and an output with no picture answers no.
|
|
89
|
+
const soundtrack = {
|
|
90
|
+
audioOnly: true,
|
|
91
|
+
transcodeVideo: false,
|
|
92
|
+
file: new SourceFile({ sourceKey: "s", fileIndex: 0 })
|
|
93
|
+
.learn({ keyframeTimes: [0, 4.004, 8.008, 12.012], keyframeTolerance: 0 })
|
|
94
|
+
};
|
|
95
|
+
assert.equal(seekLandingOffsetFor(soundtrack, 4.004), 0);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("the picture of the same film still gets the offset", () => {
|
|
99
|
+
// The pair to the check above: the two outputs are repositioned to one
|
|
100
|
+
// boundary and must be given DIFFERENT requests, because one is copied and one
|
|
101
|
+
// is re-encoded. Given the same request they land 130 ms apart.
|
|
102
|
+
const picture = {
|
|
103
|
+
audioOnly: false,
|
|
104
|
+
transcodeVideo: false,
|
|
105
|
+
file: new SourceFile({ sourceKey: "s", fileIndex: 0 })
|
|
106
|
+
.learn({ keyframeTimes: [0, 4.004, 8.008, 12.012], keyframeTolerance: 0 })
|
|
107
|
+
};
|
|
108
|
+
assert.equal(seekLandingOffsetFor(picture, 4.004), OFFSET);
|
|
109
|
+
});
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file What
|
|
2
|
+
* @file What is wanted of one output, in its own segment numbers.
|
|
3
|
+
*
|
|
4
|
+
* The class held a window per viewer per band and merged them itself. That was
|
|
5
|
+
* the priority layer's work done a second time in the wrong place, and it
|
|
6
|
+
* carried the viewer's name as the key of a claim — against the rule that the
|
|
7
|
+
* encoding and the viewer are not connected at all. The behaviours those checks
|
|
8
|
+
* pinned (a viewer's own bands, two viewers as a union) are the priority map's
|
|
9
|
+
* and are checked there.
|
|
10
|
+
*
|
|
11
|
+
* What is left is a holder: one map per output, replaced whole, and an empty one
|
|
12
|
+
* meaning nobody is coming.
|
|
3
13
|
*/
|
|
4
14
|
|
|
5
15
|
import test from "node:test";
|
|
@@ -7,76 +17,64 @@ import assert from "node:assert/strict";
|
|
|
7
17
|
import { SegmentDemand } from "../services/encode/SegmentDemand.js";
|
|
8
18
|
|
|
9
19
|
const PICTURE = "torrent:abc:fmt=fmp4:grid=kf@0:video-only:v=0/copy";
|
|
10
|
-
const SOUND = "torrent:abc:fmt=fmp4:grid=kf@0:audio-only:a=0/
|
|
20
|
+
const SOUND = "torrent:abc:fmt=fmp4:grid=kf@0:audio-only:a=0/0/aac";
|
|
11
21
|
|
|
12
|
-
test("
|
|
13
|
-
// A player restates its window every few seconds. Accumulated, the demand
|
|
14
|
-
// would grow to the whole film within a minute.
|
|
22
|
+
test("an output nothing has been said about wants nothing", () => {
|
|
15
23
|
const demand = new SegmentDemand();
|
|
16
|
-
demand.
|
|
17
|
-
demand.
|
|
18
|
-
assert.deepEqual(demand.spanOn(PICTURE), { from: 30, to: 40 });
|
|
19
|
-
assert.equal(demand.stats().windows, 1);
|
|
24
|
+
assert.deepEqual(demand.mapOn(PICTURE), []);
|
|
25
|
+
assert.deepEqual(demand.addresses(), []);
|
|
20
26
|
});
|
|
21
27
|
|
|
22
|
-
test("
|
|
28
|
+
test("a map replaces whatever was wanted before, rather than adding to it", () => {
|
|
29
|
+
// The map is a statement of what is wanted NOW, built fresh each time from
|
|
30
|
+
// where the viewers are. Accumulating them would keep serving people who have
|
|
31
|
+
// moved or gone.
|
|
23
32
|
const demand = new SegmentDemand();
|
|
24
|
-
demand.state({
|
|
25
|
-
demand.state({
|
|
26
|
-
assert.
|
|
27
|
-
assert.deepEqual(demand.addresses().sort(), [SOUND, PICTURE].sort());
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test("two viewers of one output are a union, not a sum", () => {
|
|
31
|
-
// Two viewers seconds apart want mostly the same segments; counted twice, a
|
|
32
|
-
// stretch would look twice as wanted as it is.
|
|
33
|
-
const demand = new SegmentDemand();
|
|
34
|
-
demand.state({ claimant: "one", address: PICTURE, from: 10, to: 14, statedAt: 1 });
|
|
35
|
-
demand.state({ claimant: "two", address: PICTURE, from: 12, to: 16, statedAt: 1 });
|
|
36
|
-
assert.deepEqual(demand.wantedOn(PICTURE), [10, 11, 12, 13, 14, 15, 16]);
|
|
33
|
+
demand.state(PICTURE, [{ from: 100, to: 130, priority: 32, withinSeconds: 0 }]);
|
|
34
|
+
demand.state(PICTURE, [{ from: 500, to: 530, priority: 32, withinSeconds: 0 }]);
|
|
35
|
+
assert.deepEqual(demand.mapOn(PICTURE), [{ from: 500, to: 530, priority: 32, withinSeconds: 0 }]);
|
|
37
36
|
});
|
|
38
37
|
|
|
39
|
-
test("
|
|
40
|
-
//
|
|
38
|
+
test("an empty map is a statement, and it is kept as one", () => {
|
|
39
|
+
// It says nobody is coming anywhere in this output, which is what stops the
|
|
40
|
+
// encoders on it. Dropped instead of stored, the output would look like one
|
|
41
|
+
// nothing had ever been said about, and the last map with people in it would
|
|
42
|
+
// stand as current.
|
|
41
43
|
const demand = new SegmentDemand();
|
|
42
|
-
demand.state({
|
|
43
|
-
demand.state(
|
|
44
|
-
assert.deepEqual(demand.
|
|
44
|
+
demand.state(PICTURE, [{ from: 100, to: 130, priority: 32, withinSeconds: 0 }]);
|
|
45
|
+
demand.state(PICTURE, []);
|
|
46
|
+
assert.deepEqual(demand.mapOn(PICTURE), []);
|
|
47
|
+
assert.deepEqual(demand.addresses(), [PICTURE], "the output is still one that has been spoken about");
|
|
45
48
|
});
|
|
46
49
|
|
|
47
|
-
test("
|
|
50
|
+
test("each output holds its own map", () => {
|
|
51
|
+
// Two outputs of one film are cut independently — 454 pieces against 401 on
|
|
52
|
+
// the field file — so the same second is a different number in each, and one
|
|
53
|
+
// map cannot serve both.
|
|
48
54
|
const demand = new SegmentDemand();
|
|
49
|
-
demand.state({
|
|
50
|
-
demand.state({
|
|
51
|
-
demand.
|
|
52
|
-
assert.equal(demand.
|
|
53
|
-
assert.
|
|
54
|
-
assert.equal(demand.addresses().length, 1);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test("expiry is carried out here and decided elsewhere", () => {
|
|
58
|
-
// The register has no clock on purpose: the rule that says how stale is too
|
|
59
|
-
// stale lives with whoever measures it, and this stays exercisable with
|
|
60
|
-
// numbers alone.
|
|
61
|
-
const demand = new SegmentDemand();
|
|
62
|
-
demand.state({ claimant: "one", address: PICTURE, from: 0, to: 5, statedAt: 1000 });
|
|
63
|
-
demand.state({ claimant: "two", address: PICTURE, from: 0, to: 5, statedAt: 9000 });
|
|
64
|
-
const dropped = demand.forgetStatedBefore(5000);
|
|
65
|
-
assert.equal(dropped.length, 1);
|
|
66
|
-
assert.equal(dropped[0].claimant, "one");
|
|
67
|
-
assert.equal(demand.stats().windows, 1);
|
|
55
|
+
demand.state(PICTURE, [{ from: 100, to: 130, priority: 32, withinSeconds: 0 }]);
|
|
56
|
+
demand.state(SOUND, [{ from: 88, to: 115, priority: 32, withinSeconds: 0 }]);
|
|
57
|
+
assert.equal(demand.mapOn(PICTURE)[0].from, 100);
|
|
58
|
+
assert.equal(demand.mapOn(SOUND)[0].from, 88);
|
|
59
|
+
assert.deepEqual(demand.addresses().sort(), [SOUND, PICTURE].sort());
|
|
68
60
|
});
|
|
69
61
|
|
|
70
|
-
test("
|
|
62
|
+
test("an output can be forgotten entirely", () => {
|
|
71
63
|
const demand = new SegmentDemand();
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
assert.
|
|
75
|
-
assert.equal(demand.stats().windows, 0);
|
|
64
|
+
demand.state(PICTURE, [{ from: 100, to: 130, priority: 32, withinSeconds: 0 }]);
|
|
65
|
+
demand.forget(PICTURE);
|
|
66
|
+
assert.deepEqual(demand.addresses(), []);
|
|
76
67
|
});
|
|
77
68
|
|
|
78
|
-
test("nothing
|
|
69
|
+
test("nothing about a viewer can be stated, because nothing about one is held", () => {
|
|
70
|
+
// The check that the rule holds by construction: there is no name to pass and
|
|
71
|
+
// no way to ask about one.
|
|
79
72
|
const demand = new SegmentDemand();
|
|
80
|
-
assert.equal(demand.
|
|
81
|
-
assert.
|
|
73
|
+
assert.equal(typeof (/** @type {any} */ (demand).want), "undefined");
|
|
74
|
+
assert.equal(typeof (/** @type {any} */ (demand).windowsOn), "undefined");
|
|
75
|
+
assert.equal(
|
|
76
|
+
demand.state.length,
|
|
77
|
+
2,
|
|
78
|
+
"an address and a map, and nothing else"
|
|
79
|
+
);
|
|
82
80
|
});
|
|
@@ -65,14 +65,14 @@ test("a tiny queue still waits for one round of probes", () => {
|
|
|
65
65
|
assert.equal(verdict.needMs, PROBE_INTERVAL_MS);
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
-
test("nothing queued is
|
|
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
69
|
const verdict = wedgeIsCertain({
|
|
70
70
|
queuedBytes: 0,
|
|
71
71
|
bytesPerSecond: 16 * MEGABYTE,
|
|
72
72
|
flatForMs: 600_000
|
|
73
73
|
});
|
|
74
|
-
assert.equal(verdict.certain,
|
|
75
|
-
assert.equal(verdict.needMs,
|
|
74
|
+
assert.equal(verdict.certain, true);
|
|
75
|
+
assert.equal(verdict.needMs, PROBE_INTERVAL_MS);
|
|
76
76
|
});
|
|
77
77
|
|
|
78
78
|
test("with no rate measured the answer is that nothing can be said", () => {
|