@torrent-tv/proxy 2.23.0 → 2.24.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 +14 -0
- package/package.json +1 -1
- package/services/hls-session-manager.js +7540 -7079
- package/test/concurrent-cost.test.js +138 -82
- package/test/published-timeline.test.js +83 -0
- package/test/quality-variants.test.js +67 -1
- package/test/segment-serve-wiring.test.js +19 -5
|
@@ -1,82 +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
|
-
});
|
|
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
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file A segment must be stamped where the playlist the player holds says.
|
|
3
|
+
*
|
|
4
|
+
* The field case this pins, 2026-08-17: a seek to 1590.4 s restarted the audio
|
|
5
|
+
* rendition at #291; the produced segments #292 and #293 carried their own
|
|
6
|
+
* positions, 1587.892 s and 1592.692 s, while the playlist the browser was
|
|
7
|
+
* holding said 1585.376 s and 1590.585 s. The browser fetched those two
|
|
8
|
+
* segments **1908 times each** over ten minutes, every one served in 4 ms, and
|
|
9
|
+
* the film never moved: a fragment appended more than `maxBufferHole` away from
|
|
10
|
+
* where the playlist put it is not recognised as buffered, so the player asks
|
|
11
|
+
* for it again.
|
|
12
|
+
*
|
|
13
|
+
* The rule, and the reason it is not simply "always use the file's own time":
|
|
14
|
+
* the boundary table keeps being corrected from produced segments, and the
|
|
15
|
+
* playlist does not. Whichever is right about the FILE, only one of them was
|
|
16
|
+
* sent to the player.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import assert from "node:assert/strict";
|
|
20
|
+
import test from "node:test";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* hls.js's own default. A gap smaller than this is skipped; a larger one is a
|
|
24
|
+
* hole, and the fragment across it counts as not loaded.
|
|
25
|
+
*/
|
|
26
|
+
const PLAYER_BUFFER_HOLE_SEC = 0.5;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The decision under test, in the shape the session manager applies it.
|
|
30
|
+
*
|
|
31
|
+
* @param {{ trueStart: number | null, publishedStart: number }} reading
|
|
32
|
+
* @returns {{ stamp: number, followedPlaylist: boolean }}
|
|
33
|
+
*/
|
|
34
|
+
function stampFor({ trueStart, publishedStart }) {
|
|
35
|
+
if (trueStart === null) {
|
|
36
|
+
return { stamp: publishedStart, followedPlaylist: true };
|
|
37
|
+
}
|
|
38
|
+
if (Math.abs(trueStart - publishedStart) > PLAYER_BUFFER_HOLE_SEC) {
|
|
39
|
+
return { stamp: publishedStart, followedPlaylist: true };
|
|
40
|
+
}
|
|
41
|
+
return { stamp: trueStart, followedPlaylist: false };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
test("the field case: a segment far from its playlist position is stamped where the playlist says", () => {
|
|
45
|
+
const segment292 = stampFor({ trueStart: 1587.892, publishedStart: 1585.376 });
|
|
46
|
+
assert.equal(segment292.stamp, 1585.376);
|
|
47
|
+
assert.equal(segment292.followedPlaylist, true);
|
|
48
|
+
|
|
49
|
+
const segment293 = stampFor({ trueStart: 1592.692, publishedStart: 1590.585 });
|
|
50
|
+
assert.equal(segment293.stamp, 1590.585);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("a file whose index is honest keeps its own position", () => {
|
|
54
|
+
// Within what a player bridges, the file's own figure is the truthful one and
|
|
55
|
+
// it is what keeps speech and subtitles together (2026-08-06, 4.17 s of drift
|
|
56
|
+
// when the picture was stamped from a lying index).
|
|
57
|
+
const reading = stampFor({ trueStart: 120.13, publishedStart: 120.0 });
|
|
58
|
+
assert.equal(reading.stamp, 120.13);
|
|
59
|
+
assert.equal(reading.followedPlaylist, false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("the boundary is exactly the player's own tolerance", () => {
|
|
63
|
+
assert.equal(stampFor({ trueStart: 10.5, publishedStart: 10.0 }).stamp, 10.5);
|
|
64
|
+
assert.equal(stampFor({ trueStart: 10.51, publishedStart: 10.0 }).stamp, 10.0);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("a segment that does not say where it begins is stamped from the playlist", () => {
|
|
68
|
+
assert.equal(stampFor({ trueStart: null, publishedStart: 42.0 }).stamp, 42.0);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("a corrected boundary does not move the stamp of a session already playing", () => {
|
|
72
|
+
// The correction writes into the live table; the published one is frozen. If
|
|
73
|
+
// the stamp followed the live table, every correction would move segments
|
|
74
|
+
// under a player holding the original playlist — which is the same failure
|
|
75
|
+
// seen from the other side.
|
|
76
|
+
const published = [0, 4, 8, 12];
|
|
77
|
+
const live = [...published];
|
|
78
|
+
live[2] = 9.7; // corrected from a produced segment
|
|
79
|
+
const publishedStart = published[2];
|
|
80
|
+
const reading = stampFor({ trueStart: live[2], publishedStart });
|
|
81
|
+
assert.equal(publishedStart, 8);
|
|
82
|
+
assert.equal(reading.stamp, 8, "the player was told 8 s and must be given 8 s");
|
|
83
|
+
});
|
|
@@ -15,7 +15,7 @@ import assert from "node:assert/strict";
|
|
|
15
15
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
16
16
|
import os from "node:os";
|
|
17
17
|
import path from "node:path";
|
|
18
|
-
import { computeSegmentBoundaries, HlsSessionManager } from "../services/hls-session-manager.js";
|
|
18
|
+
import { computeSegmentBoundaries, costKindForSession, HlsSessionManager } from "../services/hls-session-manager.js";
|
|
19
19
|
import { fmp4Format } from "../services/segment-formats/fmp4.js";
|
|
20
20
|
|
|
21
21
|
const BASE_ID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
|
|
@@ -736,3 +736,69 @@ test("an audio track is prepared at the position the switch will land on", async
|
|
|
736
736
|
"and the track is pointed at the switch position, one back for the preceding keyframe"
|
|
737
737
|
);
|
|
738
738
|
});
|
|
739
|
+
|
|
740
|
+
test("a reading from a soundtrack is priced as one", () => {
|
|
741
|
+
// The fault this pins: an audio rendition reached no learner at all. One
|
|
742
|
+
// guard refused renditions a reading, and the only call that would have
|
|
743
|
+
// priced one sat behind a second guard its caller had already made — so a
|
|
744
|
+
// soundtrack ran for the whole film and was charged at nothing, which is the
|
|
745
|
+
// half of roadmap item 6 that was left owing.
|
|
746
|
+
assert.equal(costKindForSession({ audioOnly: true, transcodeVideo: false }), "audio");
|
|
747
|
+
assert.equal(
|
|
748
|
+
costKindForSession({ audioOnly: true, transcodeVideo: true }),
|
|
749
|
+
"audio",
|
|
750
|
+
"a rendition carries no picture, whatever the flag it inherited says"
|
|
751
|
+
);
|
|
752
|
+
assert.equal(costKindForSession({ transcodeVideo: true }), "decode");
|
|
753
|
+
assert.equal(costKindForSession({ transcodeVideo: false }), "copy");
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
test("a quality step being warmed is not refused by its own cost", async (t) => {
|
|
757
|
+
const dirPath = await mkdtemp(path.join(os.tmpdir(), "warm-cost-"));
|
|
758
|
+
// The addon host's own figures, so the arithmetic below is the field's and
|
|
759
|
+
// not an invention. Without a benchmark the check returns every height
|
|
760
|
+
// untouched and a test over it would pass while proving nothing.
|
|
761
|
+
const manager = new HlsSessionManager({
|
|
762
|
+
enabled: true,
|
|
763
|
+
ffmpegBin: "ffmpeg",
|
|
764
|
+
localBindHost: "127.0.0.1",
|
|
765
|
+
localPort: 9090,
|
|
766
|
+
softwarePresetBenchmark: [
|
|
767
|
+
{ preset: "fast", pixelsPerSec: 17.0e6 },
|
|
768
|
+
{ preset: "ultrafast", pixelsPerSec: 67.5e6 }
|
|
769
|
+
],
|
|
770
|
+
decodeCostModel: { pixelTerm: 0.007742, bitrateTerm: 0, constantTerm: 0 }
|
|
771
|
+
});
|
|
772
|
+
t.after(async () => {
|
|
773
|
+
await manager.disposeAll();
|
|
774
|
+
await rm(dirPath, { recursive: true, force: true });
|
|
775
|
+
});
|
|
776
|
+
// A copied 1080p picture, and a 240p step warmed beside it for a switch —
|
|
777
|
+
// which is what every quality change does, two encoders on purpose.
|
|
778
|
+
const base = fakeSession({ id: BASE_ID, encodeHeight: 0, dirPath, transcodeVideo: false });
|
|
779
|
+
base.variantHeight = 1080;
|
|
780
|
+
// What this source costs to decode — 1080p24 at 8 Mbit/s, the field file.
|
|
781
|
+
// Without it nothing can be priced and every height comes back offerable for
|
|
782
|
+
// want of a measurement, which would make the assertion hold for the wrong
|
|
783
|
+
// reason.
|
|
784
|
+
base.sourceDecode = { megapixelsPerSecond: (1920 * 1080 * 24) / 1e6, megabitsPerSecond: 8 };
|
|
785
|
+
const warming = fakeSession({ id: VARIANT_ID, encodeHeight: 240, dirPath });
|
|
786
|
+
warming.variantHeight = 240;
|
|
787
|
+
warming.sourceDecode = base.sourceDecode;
|
|
788
|
+
base.variants = new Map([[240, VARIANT_ID]]);
|
|
789
|
+
warming.variantBases = new Set([BASE_ID]);
|
|
790
|
+
// Running, and running well: it says of itself that it holds twice realtime,
|
|
791
|
+
// i.e. half a second of work per second of video.
|
|
792
|
+
warming.ffmpeg = fakeEncoder();
|
|
793
|
+
warming.lastAloneSpeed = 2;
|
|
794
|
+
manager.sessionsById.set(BASE_ID, base);
|
|
795
|
+
manager.sessionsById.set(VARIANT_ID, warming);
|
|
796
|
+
|
|
797
|
+
const offered = manager.offeredHeights(base);
|
|
798
|
+
|
|
799
|
+
assert.ok(
|
|
800
|
+
offered.includes(240),
|
|
801
|
+
"charged its own cost while being judged, the step the viewer just asked for is dropped from the " +
|
|
802
|
+
"offer by the act of warming it — and its next segment 404s on a stream that is playing"
|
|
803
|
+
);
|
|
804
|
+
});
|
|
@@ -171,7 +171,7 @@ test("serving a segment records what its real start says about the container's i
|
|
|
171
171
|
});
|
|
172
172
|
|
|
173
173
|
test("a segment that exists is served, not reported as still being produced", async (t) => {
|
|
174
|
-
const { manager, dirPath } = await managerWithReadySegment();
|
|
174
|
+
const { manager, dirPath, session } = await managerWithReadySegment();
|
|
175
175
|
t.after(async () => {
|
|
176
176
|
await manager.disposeAll();
|
|
177
177
|
await rm(dirPath, { recursive: true, force: true });
|
|
@@ -189,12 +189,26 @@ test("a segment that exists is served, not reported as still being produced", as
|
|
|
189
189
|
const served = Buffer.concat(chunks);
|
|
190
190
|
assert.equal(served.toString("latin1", 4, 8), "moof", "the init header must be stripped off a media segment");
|
|
191
191
|
|
|
192
|
-
//
|
|
193
|
-
//
|
|
192
|
+
// Where the PLAYLIST the player holds puts this segment — which is what the
|
|
193
|
+
// fragment must be stamped with whenever the piece's own position is further
|
|
194
|
+
// away than a player will bridge. This fixture's piece says 12.5 s while the
|
|
195
|
+
// playlist says 0, and stamping the piece's figure is what killed a film on
|
|
196
|
+
// 2026-08-17: the browser asked for two segments 1908 times each over ten
|
|
197
|
+
// minutes, each served in 4 ms, because a fragment landing 2.5 s from where
|
|
198
|
+
// it was expected is not recognised as buffered. Reading the piece's own
|
|
199
|
+
// position still happens — it is the step that threw in 2.9.124, it feeds the
|
|
200
|
+
// index tally and it corrects the grid for rungs made later — it just no
|
|
201
|
+
// longer contradicts the timeline the player was sent. See
|
|
202
|
+
// test/published-timeline.test.js for the rule itself.
|
|
194
203
|
assert.equal(
|
|
195
204
|
Number(served.readBigUInt64BE(served.indexOf("tfdt") + 8)),
|
|
196
|
-
|
|
197
|
-
"the segment must be stamped
|
|
205
|
+
0,
|
|
206
|
+
"the segment must be stamped where the playlist the player holds says it begins"
|
|
207
|
+
);
|
|
208
|
+
assert.equal(
|
|
209
|
+
session.indexCheck.checked,
|
|
210
|
+
1,
|
|
211
|
+
"and the piece's own position must still have been read, or nothing measures the index"
|
|
198
212
|
);
|
|
199
213
|
});
|
|
200
214
|
|