@torrent-tv/proxy 2.77.0 → 2.78.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.
@@ -121,9 +121,14 @@ export class EncodeOrchestrator {
121
121
  * @param {string} params.address
122
122
  * @param {number} params.from
123
123
  * @param {number} params.to
124
+ * @param {number} [params.priority] - Higher is sooner. One viewer states
125
+ * several stretches at once — what must be ready before they set off, what
126
+ * is reachable while they watch it, the rest of the track — and the filling
127
+ * takes them in this order. Absent means one undifferentiated want, which
128
+ * is what a caller that knows only a position states.
124
129
  */
125
- want({ claimant, address, from, to }) {
126
- this.demand.state({ claimant, address, from, to, statedAt: this.now() });
130
+ want({ claimant, address, from, to, priority = 0 }) {
131
+ this.demand.state({ claimant, address, from, to, priority, statedAt: this.now() });
127
132
  }
128
133
 
129
134
  /**
@@ -196,9 +201,14 @@ export class EncodeOrchestrator {
196
201
  });
197
202
  }
198
203
  }
204
+ // Carrying the priority through, because the filling takes the work in that
205
+ // order: what a viewer must have before they set off comes before what is
206
+ // merely in front of them, which comes before the rest of the track. Passed
207
+ // as a plain number so the plan stays arithmetic.
199
208
  const windows = this.demand.windowsOn(address).map((window) => ({
200
209
  from: window.from,
201
- to: window.to
210
+ to: window.to,
211
+ priority: Number(window.priority) || 0
202
212
  }));
203
213
  const live = this.runsOn(address).filter((run) => run.isAlive);
204
214
  const actions = planEncoders({
@@ -0,0 +1,102 @@
1
+ /**
2
+ * @file A map per viewer, merged into one, in the order the work is taken.
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.
7
+ */
8
+
9
+ import test from "node:test";
10
+ import assert from "node:assert/strict";
11
+ import { mapForViewer, mergeMaps, inWorkingOrder } from "../services/encode/DemandMap.js";
12
+
13
+ test("a viewer's own position is the most urgent thing there is", () => {
14
+ const map = mapForViewer({
15
+ atSeconds: 100,
16
+ durationSeconds: 1000,
17
+ allowanceSeconds: 8,
18
+ encodeSpeedX: 2
19
+ });
20
+
21
+ assert.equal(map[0].from, 100);
22
+ assert.equal(map[0].to, 108, "above realtime, only the measured allowance");
23
+ for (const zone of map.slice(1)) {
24
+ assert.ok(zone.priority < map[0].priority);
25
+ }
26
+ });
27
+
28
+ test("a machine that cannot keep up must have more ready before the viewer sets off", () => {
29
+ // 900s of film in front. At 0.25x the encoder loses three seconds of film per
30
+ // second played, so 675s must exist first, or the viewer stalls partway.
31
+ const slow = mapForViewer({ atSeconds: 100, durationSeconds: 1000, allowanceSeconds: 0, encodeSpeedX: 0.25 });
32
+ const fast = mapForViewer({ atSeconds: 100, durationSeconds: 1000, allowanceSeconds: 0, encodeSpeedX: 2 });
33
+
34
+ assert.equal(slow[0].to, 775, "100 + 900 x 0.75");
35
+ assert.ok(
36
+ slow[0].to - slow[0].from > fast[0].to - fast[0].from,
37
+ "the slower the machine, the more of the film must be made in advance"
38
+ );
39
+ });
40
+
41
+ test("the allowance is added to the shortfall, not chosen instead of it", () => {
42
+ const map = mapForViewer({ atSeconds: 0, durationSeconds: 100, allowanceSeconds: 10, encodeSpeedX: 0.5 });
43
+
44
+ assert.equal(map[0].to, 60, "50s of shortfall plus 10s of measured allowance");
45
+ });
46
+
47
+ test("the rest of the track is still wanted, and wanted last", () => {
48
+ const map = mapForViewer({ atSeconds: 0, durationSeconds: 1000, allowanceSeconds: 4, encodeSpeedX: 2 });
49
+
50
+ assert.equal(map[map.length - 1].to, 1000, "the map reaches the end of the film");
51
+ assert.ok(map[map.length - 1].priority > 0, "and the far end is still wanted");
52
+ let previousEnd = 0;
53
+ for (const zone of map) {
54
+ assert.equal(zone.from, previousEnd, "no gaps and no overlaps");
55
+ previousEnd = zone.to;
56
+ }
57
+ });
58
+
59
+ test("nothing is measured yet: the middle zone is left out rather than invented", () => {
60
+ const map = mapForViewer({ atSeconds: 0, durationSeconds: 100, allowanceSeconds: 4, encodeSpeedX: 0 });
61
+
62
+ assert.equal(map.length, 2, "what must be ready, and the rest");
63
+ });
64
+
65
+ test("two viewers merge to the highest priority per second, with no overlaps", () => {
66
+ const first = mapForViewer({ atSeconds: 0, durationSeconds: 1000, allowanceSeconds: 8, encodeSpeedX: 2 });
67
+ const second = mapForViewer({ atSeconds: 500, durationSeconds: 1000, allowanceSeconds: 8, encodeSpeedX: 2 });
68
+
69
+ const merged = mergeMaps([first, second]);
70
+
71
+ let previousEnd = merged[0].from;
72
+ for (const zone of merged) {
73
+ assert.equal(zone.from, previousEnd, "no gaps and no overlaps");
74
+ previousEnd = zone.to;
75
+ }
76
+ const priorityAt = (at) => merged.find((zone) => at >= zone.from && at < zone.to)?.priority;
77
+ assert.equal(priorityAt(0), priorityAt(500), "both viewers' own positions are equally urgent");
78
+ assert.ok(priorityAt(500) > priorityAt(300), "a viewer at 500 outranks the far zone of the one at 0");
79
+ });
80
+
81
+ test("the second viewer's position is not buried under the first viewer's far zone", () => {
82
+ // The case that used to leave a viewer opening the same film further in with
83
+ // no encoder at all, because the first run claimed everything in front of it.
84
+ const first = mapForViewer({ atSeconds: 0, durationSeconds: 4000, allowanceSeconds: 8, encodeSpeedX: 2 });
85
+ const second = mapForViewer({ atSeconds: 2000, durationSeconds: 4000, allowanceSeconds: 8, encodeSpeedX: 2 });
86
+
87
+ const order = inWorkingOrder(mergeMaps([first, second]));
88
+ const firstTwo = order.slice(0, 2).map((zone) => zone.from);
89
+
90
+ assert.ok(firstTwo.includes(0), "the first viewer's own position is taken first");
91
+ assert.ok(firstTwo.includes(2000), "so is the second viewer's, before anything less urgent");
92
+ });
93
+
94
+ test("within one priority the earliest film goes first — that is where somebody is stopped", () => {
95
+ const order = inWorkingOrder([
96
+ { from: 900, to: 1000, priority: 2 },
97
+ { from: 100, to: 200, priority: 2 },
98
+ { from: 0, to: 10, priority: 3 }
99
+ ]);
100
+
101
+ assert.deepEqual(order.map((zone) => zone.from), [0, 100, 900]);
102
+ });
@@ -176,7 +176,12 @@ test("every encoder stops when nobody is watching the output", () => {
176
176
  );
177
177
  });
178
178
 
179
- test("a run making material nobody asked for is stopped", () => {
179
+ test("a run standing outside every window keeps working: the file is encoded whole", () => {
180
+ // The rule, stated by the user 2026-09-05: while a file is being encoded it
181
+ // is encoded whole, and a viewer decides the ORDER, not whether a run may
182
+ // live. Stopping a run for standing outside a window is what produced the
183
+ // field oscillation of that day — placed by one rule, killed by another,
184
+ // 350-700ms per cycle, nothing ever produced.
180
185
  const coverage = new CoverageMap({ segmentCount: 1000 });
181
186
  const runA = run({ from: 500, to: 600, head: 520 });
182
187
  coverage.claim(runA, 500, 600);
@@ -186,7 +191,44 @@ test("a run making material nobody asked for is stopped", () => {
186
191
  runs: [runA],
187
192
  ...HOST
188
193
  });
189
- assert.ok(actions.some((action) => action.type === "stop" && action.run === runA));
194
+ assert.ok(
195
+ !actions.some((action) => action.type === "stop" && action.run === runA),
196
+ "it is making film that will be wanted, and nothing else is making it"
197
+ );
198
+ });
199
+
200
+ test("the same plan run twice on an unchanged state gives the same answer", () => {
201
+ // What the oscillation actually was: two passes over one state disagreeing
202
+ // with each other. Nothing about the state changes between them here.
203
+ const coverage = new CoverageMap({ segmentCount: 1000 });
204
+ const runA = run({ from: 500, to: 600, head: 520 });
205
+ coverage.claim(runA, 500, 600);
206
+ const input = { coverage, windows: [{ from: 0, to: 40 }], runs: [runA], ...HOST };
207
+
208
+ const first = planEncoders(input).map((action) => action.type);
209
+ const second = planEncoders(input).map((action) => action.type);
210
+
211
+ assert.deepEqual(first, second);
212
+ assert.ok(!first.includes("stop"), "and neither pass kills what the other would start");
213
+ });
214
+
215
+ test("a viewer's most urgent zone is filled before a less urgent one", () => {
216
+ const coverage = new CoverageMap({ segmentCount: 1000 });
217
+ const actions = planEncoders({
218
+ coverage,
219
+ // The far zone is lower in number and lower in priority: the order must
220
+ // come from the priority, not from the number.
221
+ windows: [
222
+ { from: 0, to: 100, priority: 1 },
223
+ { from: 500, to: 530, priority: 3 }
224
+ ],
225
+ runs: [],
226
+ ...HOST,
227
+ maxRuns: 1
228
+ });
229
+ const started = actions.filter((action) => action.type === "start").map((action) => action.from);
230
+
231
+ assert.deepEqual(started, [500], "the one machine goes where somebody is stopped");
190
232
  });
191
233
 
192
234
  test("two viewers far apart get an encoder each, when the machine can hold two", () => {
@@ -1,336 +1,100 @@
1
1
  /**
2
- * @file A run is given a stretch nobody else holds, and stops at the end of it.
2
+ * @file How far a run may work, and who decides it.
3
3
  *
4
- * This is what removes the per-run directories. Runs used to be kept apart by a
5
- * directory each, because two of them writing one segment name at the same time
6
- * produce a file belonging to neither which was also the only reason a restart
7
- * ever had to wait for its predecessor to die. Intervals remove that by
8
- * construction, so one flat directory per output is correct.
4
+ * These cases used to be asked of `planRunInterval`, a second authority inside
5
+ * the session manager that answered by its own rules: it walked the whole track
6
+ * for the first free number, MOVED the start there, and counted every live run
7
+ * as claiming up to `head + look-ahead`. It contradicted the plan directly, and
8
+ * the two together produced the field oscillation of 2026-09-05 — the plan
9
+ * commanded a start at #46, this moved it to #78, the plan killed the run for
10
+ * standing outside the window it had asked for, and the same start was
11
+ * commanded again, 350-700ms per cycle, no segment ever produced, the viewer's
12
+ * picture stopped for 125 seconds.
9
13
  *
10
- * The other half is the argument that states the end, and WHICH argument it is
11
- * belongs to the branch measured, not reasoned:
12
- * `research/encoder-layer-2026-09-04.md` §11.
14
+ * It is gone. WHERE a run starts is the plan's decision and nothing moves it;
15
+ * HOW FAR it may work is a fact of the one coverage map, and that is what these
16
+ * cases now ask. The map is the same object the plan reads, so there is no
17
+ * second set of rules for the two to disagree about.
13
18
  */
14
19
 
15
20
  import test from "node:test";
16
21
  import assert from "node:assert/strict";
17
- import { SourceFile } from "../services/source/SourceFile.js";
18
- import { startRunOn } from "./helpers/encode-run.js";
19
- import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
20
- import os from "node:os";
21
- import path from "node:path";
22
- import { HlsSessionManager } from "../services/hls-session-manager.js";
23
- import { SegmentStore } from "../services/encode/SegmentStore.js";
24
- import { Timeline } from "../services/output/Timeline.js";
25
- import { fmp4Format } from "../services/segment-formats/fmp4.js";
26
- import { viewerOf } from "../services/viewer/Viewer.js";
22
+ import { CoverageMap } from "../services/encode/CoverageMap.js";
27
23
 
28
- const KEY = "torrent:abc:fmt=fmp4:grid=kf@0:video-only:v=0/copy";
29
-
30
- /**
31
- * @returns {{ manager: HlsSessionManager, store: SegmentStore, root: string, dirPath: string }}
32
- */
33
- function managerWithAnEmptyOutput() {
34
- const root = mkdtempSync(path.join(os.tmpdir(), "run-intervals-"));
35
- const store = new SegmentStore({ root });
36
- const manager = new HlsSessionManager({
37
- enabled: true,
38
- ffmpegBin: "ffmpeg",
39
- localBindHost: "127.0.0.1",
40
- localPort: 9090,
41
- segmentStore: store
42
- });
43
- const dirPath = store.directoryFor(KEY);
44
- store.useFormat(KEY, fmp4Format);
45
- return { manager, store, root, dirPath };
46
- }
47
-
48
- /**
49
- * @param {object} params
50
- * @returns {object}
51
- */
52
- function sessionOn({ id, dirPath, segmentCount = 100, runState = null, encodeStartIndex = 0, runEndIndex = -1 }) {
53
- const session = {
54
- id,
55
- outputKey: KEY,
56
- dirPath,
57
- state: "ready",
58
- file: new SourceFile({ sourceKey: "source-1", fileIndex: 0, name: "video.mkv" }),
59
- // An ordinary session reads its own file, and its sound is inside it. The
60
- // three differ only for a soundtrack shipped as a file of its own.
61
- get inputFile() { return this.file; },
62
- get audioFile() { return this.file; },
63
- segmentFormat: fmp4Format,
64
- // How the file is cut, held by the TIMELINE. A fixture that stated it on the
65
- // session was describing a shape the product had left, and it kept a defect
66
- // alive for a release: `session.segmentCount` is undefined on every real
67
- // session, so runs were given no end and the coverage map had no length.
68
- timeline: new Timeline({
69
- boundaries: Array.from({ length: segmentCount + 1 }, (_, index) => index * 4),
70
- cutGrid: "uniform"
71
- }),
72
- runs: new Set(),
73
- consumers: new Set(),
74
- lastAccessedAt: Date.now()
75
- };
76
- if (runState !== null) {
77
- const run = startRunOn(session, { from: encodeStartIndex, to: runEndIndex });
78
- if (runState === "ENDED_FAILED") {
79
- // A run that failed, said so, and left what it had made behind.
80
- run.process?.exit(255, null);
81
- }
82
- }
83
- return session;
84
- }
85
-
86
- /**
87
- * @param {string} dirPath
88
- * @param {number[]} indexes
89
- */
90
- function alreadyMade(dirPath, indexes) {
91
- for (const index of indexes) {
92
- writeFileSync(path.join(dirPath, fmp4Format.segmentFileName(index)), Buffer.alloc(16, 1));
93
- }
24
+ /** A stand-in for a run: the map identifies one by being it. */
25
+ function run(name) {
26
+ return { name };
94
27
  }
95
28
 
96
- test("with nothing made, a run gets everything from where it was asked", (t) => {
97
- const { manager, root, dirPath } = managerWithAnEmptyOutput();
98
- t.after(() => rmSync(root, { recursive: true, force: true }));
99
-
100
- const session = sessionOn({ id: "s", dirPath });
101
- // -1 for the end means "to the end of the film", which is what every run had
102
- // before ends existed — and the only case where that is still right.
103
- assert.deepEqual(manager.planRunInterval(session, 0), { from: 0, to: -1 });
104
- });
105
-
106
- test("a run stops before material that is already made", (t) => {
107
- const { manager, root, dirPath } = managerWithAnEmptyOutput();
108
- t.after(() => rmSync(root, { recursive: true, force: true }));
109
-
110
- // 10..20 on the disk. Only 10..19 are PROVEN — the highest has no successor,
111
- // so nothing shows whether it was closed or was being written when its run
112
- // died.
113
- alreadyMade(dirPath, [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]);
114
- const session = sessionOn({ id: "s", dirPath });
115
-
116
- assert.deepEqual(manager.planRunInterval(session, 0), { from: 0, to: 9 });
117
- });
118
-
119
- test("a run asked to start inside made material is moved forward to the gap", (t) => {
120
- const { manager, root, dirPath } = managerWithAnEmptyOutput();
121
- t.after(() => rmSync(root, { recursive: true, force: true }));
122
-
123
- alreadyMade(dirPath, [10, 11, 12, 13, 14, 15]);
124
- const session = sessionOn({ id: "s", dirPath });
125
-
126
- // Asked for 11, which is made, and so is everything to 14. The first thing
127
- // worth encoding is 15 — unproven, because 16 does not exist.
128
- assert.deepEqual(manager.planRunInterval(session, 11), { from: 15, to: -1 });
129
- });
130
-
131
- test("a run stops before a stretch another live run was given", (t) => {
132
- const { manager, root, dirPath } = managerWithAnEmptyOutput();
133
- t.after(() => rmSync(root, { recursive: true, force: true }));
134
-
135
- const other = sessionOn({
136
- id: "other",
137
- dirPath,
138
- runState: "PRODUCING",
139
- encodeStartIndex: 40,
140
- runEndIndex: 60
141
- });
142
- manager.sessionsById.set(other.id, other);
143
- const session = sessionOn({ id: "s", dirPath });
29
+ test("with nothing made, a run gets everything from where it was asked", () => {
30
+ const coverage = new CoverageMap({ segmentCount: 100 });
144
31
 
145
- // Two viewers of one film, one already encoding 40..60. The second run takes
146
- // what is free in front of it and stops where the other one begins, so
147
- // neither ever writes a name the other wants.
148
- assert.deepEqual(manager.planRunInterval(session, 0), { from: 0, to: 39 });
149
- assert.deepEqual(manager.planRunInterval(session, 45), { from: 61, to: -1 });
32
+ assert.equal(coverage.freeRunFrom(0), 100, "the whole track is free");
33
+ assert.equal(coverage.firstGapFrom(0), 0);
150
34
  });
151
35
 
152
- test("a run that has died holds nothing back", (t) => {
153
- const { manager, root, dirPath } = managerWithAnEmptyOutput();
154
- t.after(() => rmSync(root, { recursive: true, force: true }));
155
-
156
- // The same stretch, but the process cannot be signalled — it has ended. What
157
- // it did not finish is free again, without anything having to release it.
158
- const dead = sessionOn({
159
- id: "dead",
160
- dirPath,
161
- runState: "ENDED_FAILED",
162
- encodeStartIndex: 40,
163
- runEndIndex: 60
164
- });
165
- manager.sessionsById.set(dead.id, dead);
166
- const session = sessionOn({ id: "s", dirPath });
36
+ test("a run stops before material that is already made", () => {
37
+ const coverage = new CoverageMap({ segmentCount: 100 });
38
+ coverage.markReadyAll([10, 11, 12, 13, 14, 15, 16, 17, 18, 19]);
167
39
 
168
- assert.deepEqual(manager.planRunInterval(session, 0), { from: 0, to: -1 });
40
+ assert.equal(coverage.freeRunFrom(0), 10, "0..9, and it stops where 10 begins");
169
41
  });
170
42
 
171
- test("nothing left to make is answered with nothing, not with a run", (t) => {
172
- const { manager, root, dirPath } = managerWithAnEmptyOutput();
173
- t.after(() => rmSync(root, { recursive: true, force: true }));
43
+ test("a run stops before a stretch another live run was given", () => {
44
+ const coverage = new CoverageMap({ segmentCount: 100 });
45
+ const other = run("other");
46
+ coverage.claim(other, 40, 60);
174
47
 
175
- const session = sessionOn({ id: "s", dirPath, segmentCount: 5 });
176
- alreadyMade(dirPath, [0, 1, 2, 3, 4]);
177
- const other = sessionOn({
178
- id: "other",
179
- dirPath,
180
- segmentCount: 5,
181
- runState: "PRODUCING",
182
- encodeStartIndex: 4,
183
- runEndIndex: 4
184
- });
185
- manager.sessionsById.set(other.id, other);
186
-
187
- // Starting an encoder here would only repeat somebody else's work, which is
188
- // what three ffmpeg processes making one identical picture cost on a CM4.
189
- assert.equal(manager.planRunInterval(session, 0), null);
48
+ assert.equal(coverage.freeRunFrom(0), 40, "0..39, and the other run's claim begins at 40");
49
+ assert.equal(coverage.firstGapFrom(45), 61, "the first thing nobody holds after it");
190
50
  });
191
51
 
192
- test("a session with no address keeps the old shape: start here, no end", (t) => {
193
- const { manager, root, dirPath } = managerWithAnEmptyOutput();
194
- t.after(() => rmSync(root, { recursive: true, force: true }));
195
-
196
- const session = sessionOn({ id: "s", dirPath });
197
- session.outputKey = "";
198
-
199
- assert.deepEqual(manager.planRunInterval(session, 7), { from: 7, to: -1 });
200
- });
201
-
202
- test("a run without an end does not claim the whole film away from a later viewer", (t) => {
203
- const { manager, root, dirPath } = managerWithAnEmptyOutput();
204
- t.after(() => rmSync(root, { recursive: true, force: true }));
205
-
206
- // The first viewer's run, at the beginning, with no end — which is every run
207
- // whose output was empty when it started.
208
- const first = sessionOn({
209
- id: "first",
210
- dirPath,
211
- segmentCount: 1000,
212
- runState: "PRODUCING",
213
- encodeStartIndex: 0,
214
- runEndIndex: -1
215
- });
216
- manager.sessionsById.set(first.id, first);
217
- const second = sessionOn({ id: "second", dirPath, segmentCount: 1000 });
218
-
219
- // A second viewer opens the same film in the middle. Counting the first run's
220
- // claim as the whole film would leave them with no encoder at all, waiting for
221
- // it to encode its way there — an hour on a long film. It only claims as far
222
- // as it will actually get, which is its head plus the look-ahead.
223
- const planned = manager.planRunInterval(second, 500);
224
- assert.notEqual(planned, null);
225
- assert.equal(planned.from, 500);
226
- });
52
+ test("a run's own claim does not hold it back", () => {
53
+ const coverage = new CoverageMap({ segmentCount: 100 });
54
+ const mine = run("mine");
55
+ coverage.claim(mine, 40, 60);
227
56
 
228
- test("a run stops when it reaches a stretch another run was given", async (t) => {
229
- const { manager, root, dirPath } = managerWithAnEmptyOutput();
230
- t.after(() => rmSync(root, { recursive: true, force: true }));
231
-
232
- const stopped = [];
233
- const ahead = sessionOn({
234
- id: "ahead",
235
- dirPath,
236
- runState: "PRODUCING",
237
- encodeStartIndex: 500,
238
- runEndIndex: 600
239
- });
240
- manager.sessionsById.set(ahead.id, ahead);
241
- const behind = sessionOn({
242
- id: "behind",
243
- dirPath,
244
- runState: "PRODUCING",
245
- encodeStartIndex: 0,
246
- runEndIndex: -1
247
- });
248
- manager.sessionsById.set(behind.id, behind);
249
-
250
- // Its own end was set from the gaps of the moment it began, and the viewer who
251
- // opened the film further on was not in that picture. Walking into their
252
- // stretch means writing names they are writing.
253
- const behindRun = [...behind.runs][0];
254
- const aheadRun = [...ahead.runs][0];
255
- assert.equal(manager.runMakingSegment(behind, 550, behindRun), aheadRun);
256
- assert.equal(manager.runMakingSegment(behind, 499, behindRun), null);
257
57
  assert.equal(
258
- manager.runMakingSegment(ahead, 550, aheadRun),
259
- null,
260
- "a run without an end owns only as far as it will get, not the rest of the film"
58
+ coverage.freeRunFrom(40, mine),
59
+ 60,
60
+ "asked by the run that holds it, the stretch is its own to work through"
261
61
  );
262
- void stopped;
263
62
  });
264
63
 
265
- test("a seek backwards does not drag the picture away from a viewer watching ahead", (t) => {
266
- const { manager, root, dirPath } = managerWithAnEmptyOutput();
267
- t.after(() => rmSync(root, { recursive: true, force: true }));
268
-
269
- const shared = sessionOn({
270
- id: "shared",
271
- dirPath,
272
- segmentCount: 1000,
273
- runState: "PRODUCING",
274
- encodeStartIndex: 200,
275
- runEndIndex: -1
276
- });
277
- shared.sourceKey = "torrent:abc";
278
- shared.fileIndex = 0;
279
- shared.timeline = new Timeline({ boundaries: Array.from({ length: 1001 }, (_, index) => index * 4), cutGrid: "uniform" });
280
- shared.acquireSource = null;
281
- shared.progress = { processedSeconds: 900, startPositionSeconds: 800 };
282
- manager.sessionsById.set(shared.id, shared);
64
+ test("a run that has ended holds nothing back", () => {
65
+ const coverage = new CoverageMap({ segmentCount: 100 });
66
+ const dead = run("dead");
67
+ coverage.claim(dead, 40, 60);
68
+ coverage.release(dead);
283
69
 
284
- // One viewer is at segment 250 and being served. The other jumps back an hour.
285
- viewerOf(shared, "ahead").position = { segment: 250, seconds: 1000, at: Date.now() };
286
- viewerOf(shared, "back").position = { segment: 250, seconds: 1000, at: Date.now() };
70
+ assert.equal(coverage.freeRunFrom(0), 100, "what it did not finish is free again");
71
+ });
287
72
 
288
- const servingAhead = [...shared.runs][0];
289
- const startedAt = servingAhead.from;
290
- manager.requestSeek("shared", 40, "back");
73
+ test("what a dead run DID finish stays made", () => {
74
+ const coverage = new CoverageMap({ segmentCount: 100 });
75
+ const dead = run("dead");
76
+ coverage.claim(dead, 40, 60);
77
+ coverage.markReadyAll([40, 41, 42]);
78
+ coverage.release(dead);
291
79
 
292
- assert.equal(
293
- servingAhead.from,
294
- startedAt,
295
- "the run serving the viewer in front is left exactly where it was"
296
- );
297
- assert.ok(shared.runs.has(servingAhead), "and it is not stopped");
298
- // A run start claims its attempt before it awaits anything, so the assertion
299
- // holds without the test needing an ffmpeg. 40 s is segment #10 on this grid.
300
- assert.equal(
301
- shared.pendingRun?.startIndex,
302
- 10,
303
- "and the one who jumped is given a run of their own, there"
304
- );
80
+ assert.equal(coverage.firstGapFrom(40), 43, "a closed file is closed whoever made it");
305
81
  });
306
82
 
307
- test("a seek backwards with nobody else watching still moves the run", (t) => {
308
- const { manager, root, dirPath } = managerWithAnEmptyOutput();
309
- t.after(() => rmSync(root, { recursive: true, force: true }));
83
+ test("nothing left to make is answered with nothing", () => {
84
+ const coverage = new CoverageMap({ segmentCount: 5 });
85
+ coverage.markReadyAll([0, 1, 2, 3, 4]);
310
86
 
311
- let created = 0;
312
- manager.createOrGetSession = async () => {
313
- created += 1;
314
- return null;
315
- };
316
-
317
- const alone = sessionOn({
318
- id: "alone",
319
- dirPath,
320
- segmentCount: 1000,
321
- runState: "PRODUCING",
322
- encodeStartIndex: 200,
323
- runEndIndex: -1
324
- });
325
- alone.timeline = new Timeline({ boundaries: Array.from({ length: 1001 }, (_, index) => index * 4), cutGrid: "uniform" });
326
- alone.progress = { processedSeconds: 900, startPositionSeconds: 800 };
327
- manager.sessionsById.set(alone.id, alone);
328
- viewerOf(alone, "only").position = { segment: 250, seconds: 1000, at: Date.now() };
87
+ assert.equal(coverage.firstGapFrom(0), null, "and a run started here would only repeat somebody's work");
88
+ });
329
89
 
330
- manager.requestSeek("alone", 40, "only");
90
+ test("a run without an end does not take the film away from a viewer further in", () => {
91
+ // The case that used to need a second authority's `head + look-ahead` guess.
92
+ // A run's claim is now the stretch the plan GAVE it, so a viewer opening the
93
+ // same film in the middle finds their own position free.
94
+ const coverage = new CoverageMap({ segmentCount: 1000 });
95
+ const first = run("first");
96
+ coverage.claim(first, 0, 499);
331
97
 
332
- // Nobody is left behind, so a second run would be an encoder for one viewer
333
- // who is not there any more.
334
- assert.equal(created, 0);
335
- assert.equal(alone.seekTarget !== undefined, true, "the run itself is repositioned, as before");
98
+ assert.equal(coverage.firstGapFrom(500), 500, "the second viewer's own position is free");
99
+ assert.equal(coverage.freeRunFrom(500), 500, "and everything from there to the end");
336
100
  });