@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.
@@ -1,112 +1,163 @@
1
1
  /**
2
- * @file A map per viewer, merged into one, in the order the work is taken.
2
+ * @file The priority map: one number per second of film.
3
3
  *
4
- * In seconds of film throughout: it is the only unit every term of the
5
- * arithmetic is already stated in, and the one two consumers with different
6
- * states can both be translated from.
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 { mapForViewer, mergeMaps, inWorkingOrder } from "../services/priority/PriorityMap.js";
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
- test("the rest of the track is still wanted, and wanted last", () => {
14
- const map = mapForViewer({ atSeconds: 0, durationSeconds: 1000, allowanceSeconds: 4, encodeSpeedX: 2 });
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
- assert.equal(map[map.length - 1].to, 1000, "the map reaches the end of the film");
17
- assert.ok(map[map.length - 1].priority > 0, "and the far end is still wanted");
18
- let previousEnd = 0;
19
- for (const zone of map) {
20
- assert.equal(zone.from, previousEnd, "no gaps and no overlaps");
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("two viewers merge to the highest priority per second, with no overlaps", () => {
26
- const first = mapForViewer({ atSeconds: 0, durationSeconds: 1000, allowanceSeconds: 8, encodeSpeedX: 2 });
27
- const second = mapForViewer({ atSeconds: 500, durationSeconds: 1000, allowanceSeconds: 8, encodeSpeedX: 2 });
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
- const merged = mergeMaps([first, second]);
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
- let previousEnd = merged[0].from;
32
- for (const zone of merged) {
33
- assert.equal(zone.from, previousEnd, "no gaps and no overlaps");
34
- previousEnd = zone.to;
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("the second viewer's position is not buried under the first viewer's far zone", () => {
42
- // The case that used to leave a viewer opening the same film further in with
43
- // no encoder at all, because the first run claimed everything in front of it.
44
- const first = mapForViewer({ atSeconds: 0, durationSeconds: 4000, allowanceSeconds: 8, encodeSpeedX: 2 });
45
- const second = mapForViewer({ atSeconds: 2000, durationSeconds: 4000, allowanceSeconds: 8, encodeSpeedX: 2 });
46
-
47
- const order = inWorkingOrder(mergeMaps([first, second]));
48
- const firstTwo = order.slice(0, 2).map((zone) => zone.from);
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("within one priority the earliest film goes first that is where somebody is stopped", () => {
55
- const order = inWorkingOrder([
56
- { from: 900, to: 1000, priority: 2 },
57
- { from: 100, to: 200, priority: 2 },
58
- { from: 0, to: 10, priority: 3 }
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
- assert.deepEqual(order.map((zone) => zone.from), [0, 100, 900]);
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("the nearer a viewer is to a second, the higher its number", () => {
65
- // The number is a reading of how far they still have to travel, and nothing
66
- // else. That is what makes two viewers comparable at all.
67
- const map = mapForViewer({ atSeconds: 300, durationSeconds: 3000, allowanceSeconds: 10 });
68
- const ahead = map.filter((zone) => zone.from >= 300);
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
- assert.equal(ahead[0].from, 300, "it starts where they are");
71
- assert.equal(ahead[0].to, 310, "and the first band is the measured allowance");
72
- for (let index = 1; index < ahead.length; index += 1) {
73
- assert.ok(ahead[index].priority < ahead[index - 1].priority, "further off is less urgent");
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 bands widen, so any film is described by a handful of them", () => {
78
- // Near the viewer the difference between now and ten seconds away decides
79
- // what is made first; twenty minutes out it changes nothing.
80
- const map = mapForViewer({ atSeconds: 0, durationSeconds: 3000, allowanceSeconds: 10 });
81
-
82
- assert.ok(map.length <= 12, `a fifty-minute film in ${map.length} bands`);
83
- assert.equal(map[0].to - map[0].from, 10);
84
- assert.equal(map[1].to - map[1].from, 20);
85
- assert.equal(map[2].to - map[2].from, 40);
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("what is behind a viewer is wanted, and wanted last", () => {
89
- const map = mapForViewer({ atSeconds: 300, durationSeconds: 3000, allowanceSeconds: 10 });
90
- const behind = map.find((zone) => zone.from === 0);
91
-
92
- assert.ok(behind, "it is still in the map");
93
- assert.equal(behind.to, 300);
94
- assert.ok(
95
- map.filter((zone) => zone.from >= 300).every((zone) => zone.priority > behind.priority),
96
- "and everything anybody is approaching outranks it"
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 viewer who has stopped the picture is going nowhere", () => {
101
- // Nothing is nearer to them than anything else, so nothing in the film is
102
- // wanted sooner than the rest — and the work goes to whoever is watching.
103
- const map = mapForViewer({
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 viewers want of an output, stated once each and read as a union.
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/1/copy";
20
+ const SOUND = "torrent:abc:fmt=fmp4:grid=kf@0:audio-only:a=0/0/aac";
11
21
 
12
- test("a viewer restating a window replaces it rather than adding to it", () => {
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.state({ claimant: "one", address: PICTURE, from: 0, to: 10, statedAt: 1 });
17
- demand.state({ claimant: "one", address: PICTURE, from: 30, to: 40, statedAt: 2 });
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("one viewer states a window per output, and both stand", () => {
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({ claimant: "one", address: PICTURE, from: 0, to: 10, statedAt: 1 });
25
- demand.state({ claimant: "one", address: SOUND, from: 0, to: 10, statedAt: 1 });
26
- assert.equal(demand.stats().windows, 2);
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("two viewers far apart make a span that covers the ground between them", () => {
40
- // Not so it is all made so that a search for a gap considers all of it.
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({ claimant: "one", address: PICTURE, from: 0, to: 5, statedAt: 1 });
43
- demand.state({ claimant: "two", address: PICTURE, from: 900, to: 905, statedAt: 1 });
44
- assert.deepEqual(demand.spanOn(PICTURE), { from: 0, to: 905 });
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("a viewer who leaves takes every window they stated", () => {
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({ claimant: "one", address: PICTURE, from: 0, to: 5, statedAt: 1 });
50
- demand.state({ claimant: "one", address: SOUND, from: 0, to: 5, statedAt: 1 });
51
- demand.state({ claimant: "two", address: PICTURE, from: 0, to: 5, statedAt: 1 });
52
- assert.equal(demand.forget("one"), 2);
53
- assert.equal(demand.stats().claimants, 1);
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("a window that is not a window is refused rather than stored", () => {
62
+ test("an output can be forgotten entirely", () => {
71
63
  const demand = new SegmentDemand();
72
- assert.equal(demand.state({ claimant: "one", address: PICTURE, from: 5, to: 1, statedAt: 1 }), null);
73
- assert.equal(demand.state({ claimant: "", address: PICTURE, from: 0, to: 1, statedAt: 1 }), null);
74
- assert.equal(demand.state({ claimant: "one", address: "", from: 0, to: 1, statedAt: 1 }), null);
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 wanted on an output answers null rather than an empty span", () => {
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.spanOn(PICTURE), null);
81
- assert.deepEqual(demand.wantedOn(PICTURE), []);
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 not a wedge however long the counter has been still", () => {
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, false);
75
- assert.equal(verdict.needMs, null);
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", () => {