@torrent-tv/proxy 2.80.8 → 2.80.10
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 +1675 -1658
- package/bin/cli.js +606 -596
- package/package.json +51 -50
- package/services/data-channel-handler.js +2 -4
- package/services/encode/CoverageMap.js +428 -401
- package/services/encode/EncodePlan.js +0 -3
- package/services/encode/EncodeRun.js +14 -0
- package/services/encode/SegmentStore.js +718 -669
- package/services/hls-session-manager.js +44 -162
- package/services/hwaccel.js +2 -2
- package/services/memory-report.js +596 -592
- package/services/orchestrators/EncodeOrchestrator.js +143 -11
- package/services/quality/EncodeCost.js +555 -555
- package/test/auto-quality-step.test.js +514 -507
- package/test/contention.test.js +1 -1
- package/test/coverage-follows-the-disk.test.js +187 -0
- package/test/coverage-map.test.js +195 -178
- package/test/encode-orchestrator.test.js +1 -2
- package/test/encode-plan-viewers.test.js +18 -19
- package/test/encode-plan.test.js +3 -4
- package/test/held-request-width.test.js +155 -154
- package/test/helpers/encode-run.js +136 -128
- package/test/orchestrator-wired.test.js +16 -11
- package/test/produced-copy-choice.test.js +358 -327
- package/test/run-intervals.test.js +100 -100
- package/test/segment-serve-wiring.test.js +76 -20
- package/test/segment-store.test.js +216 -187
- package/test/stale-request-after-seek.test.js +51 -77
- package/test/tracks-begin-together.test.js +176 -153
- package/test/viewer-outputs.test.js +278 -273
- package/test/behind-head-repair.test.js +0 -261
- /package/services/{contention.js → encode/contention.js} +0 -0
- /package/{test/decode-cost.test.js → test-measured/decode-cost.measured.js} +0 -0
- /package/{test/decode-measurement.test.js → test-measured/decode-measurement.measured.js} +0 -0
|
@@ -1,100 +1,100 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file How far a run may work, and who decides it.
|
|
3
|
-
*
|
|
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.
|
|
13
|
-
*
|
|
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.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import test from "node:test";
|
|
21
|
-
import assert from "node:assert/strict";
|
|
22
|
-
import { CoverageMap } from "../services/encode/CoverageMap.js";
|
|
23
|
-
|
|
24
|
-
/** A stand-in for a run: the map identifies one by being it. */
|
|
25
|
-
function run(name) {
|
|
26
|
-
return { name };
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
test("with nothing made, a run gets everything from where it was asked", () => {
|
|
30
|
-
const coverage = new CoverageMap({ segmentCount: 100 });
|
|
31
|
-
|
|
32
|
-
assert.equal(coverage.freeRunFrom(0), 100, "the whole track is free");
|
|
33
|
-
assert.equal(coverage.firstGapFrom(0), 0);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test("a run stops before material that is already made", () => {
|
|
37
|
-
const coverage = new CoverageMap({ segmentCount: 100 });
|
|
38
|
-
coverage.
|
|
39
|
-
|
|
40
|
-
assert.equal(coverage.freeRunFrom(0), 10, "0..9, and it stops where 10 begins");
|
|
41
|
-
});
|
|
42
|
-
|
|
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);
|
|
47
|
-
|
|
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");
|
|
50
|
-
});
|
|
51
|
-
|
|
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);
|
|
56
|
-
|
|
57
|
-
assert.equal(
|
|
58
|
-
coverage.freeRunFrom(40, mine),
|
|
59
|
-
60,
|
|
60
|
-
"asked by the run that holds it, the stretch is its own to work through"
|
|
61
|
-
);
|
|
62
|
-
});
|
|
63
|
-
|
|
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);
|
|
69
|
-
|
|
70
|
-
assert.equal(coverage.freeRunFrom(0), 100, "what it did not finish is free again");
|
|
71
|
-
});
|
|
72
|
-
|
|
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.
|
|
78
|
-
coverage.release(dead);
|
|
79
|
-
|
|
80
|
-
assert.equal(coverage.firstGapFrom(40), 43, "a closed file is closed whoever made it");
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
test("nothing left to make is answered with nothing", () => {
|
|
84
|
-
const coverage = new CoverageMap({ segmentCount: 5 });
|
|
85
|
-
coverage.
|
|
86
|
-
|
|
87
|
-
assert.equal(coverage.firstGapFrom(0), null, "and a run started here would only repeat somebody's work");
|
|
88
|
-
});
|
|
89
|
-
|
|
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);
|
|
97
|
-
|
|
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");
|
|
100
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file How far a run may work, and who decides it.
|
|
3
|
+
*
|
|
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.
|
|
13
|
+
*
|
|
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.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import test from "node:test";
|
|
21
|
+
import assert from "node:assert/strict";
|
|
22
|
+
import { CoverageMap } from "../services/encode/CoverageMap.js";
|
|
23
|
+
|
|
24
|
+
/** A stand-in for a run: the map identifies one by being it. */
|
|
25
|
+
function run(name) {
|
|
26
|
+
return { name };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
test("with nothing made, a run gets everything from where it was asked", () => {
|
|
30
|
+
const coverage = new CoverageMap({ segmentCount: 100 });
|
|
31
|
+
|
|
32
|
+
assert.equal(coverage.freeRunFrom(0), 100, "the whole track is free");
|
|
33
|
+
assert.equal(coverage.firstGapFrom(0), 0);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("a run stops before material that is already made", () => {
|
|
37
|
+
const coverage = new CoverageMap({ segmentCount: 100 });
|
|
38
|
+
coverage.setReady([10, 11, 12, 13, 14, 15, 16, 17, 18, 19]);
|
|
39
|
+
|
|
40
|
+
assert.equal(coverage.freeRunFrom(0), 10, "0..9, and it stops where 10 begins");
|
|
41
|
+
});
|
|
42
|
+
|
|
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);
|
|
47
|
+
|
|
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");
|
|
50
|
+
});
|
|
51
|
+
|
|
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);
|
|
56
|
+
|
|
57
|
+
assert.equal(
|
|
58
|
+
coverage.freeRunFrom(40, mine),
|
|
59
|
+
60,
|
|
60
|
+
"asked by the run that holds it, the stretch is its own to work through"
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
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);
|
|
69
|
+
|
|
70
|
+
assert.equal(coverage.freeRunFrom(0), 100, "what it did not finish is free again");
|
|
71
|
+
});
|
|
72
|
+
|
|
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.setReady([40, 41, 42]);
|
|
78
|
+
coverage.release(dead);
|
|
79
|
+
|
|
80
|
+
assert.equal(coverage.firstGapFrom(40), 43, "a closed file is closed whoever made it");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("nothing left to make is answered with nothing", () => {
|
|
84
|
+
const coverage = new CoverageMap({ segmentCount: 5 });
|
|
85
|
+
coverage.setReady([0, 1, 2, 3, 4]);
|
|
86
|
+
|
|
87
|
+
assert.equal(coverage.firstGapFrom(0), null, "and a run started here would only repeat somebody's work");
|
|
88
|
+
});
|
|
89
|
+
|
|
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);
|
|
97
|
+
|
|
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");
|
|
100
|
+
});
|
|
@@ -19,8 +19,7 @@ import test from "node:test";
|
|
|
19
19
|
import assert from "node:assert/strict";
|
|
20
20
|
import { SourceFile } from "../services/source/SourceFile.js";
|
|
21
21
|
import { Timeline } from "../services/output/Timeline.js";
|
|
22
|
-
import {
|
|
23
|
-
import os from "node:os";
|
|
22
|
+
import { rm, writeFile } from "node:fs/promises";
|
|
24
23
|
import path from "node:path";
|
|
25
24
|
import { HlsSessionManager } from "../services/hls-session-manager.js";
|
|
26
25
|
import { ENCODE_RUN_STATE } from "../services/encode/encode-run-state.js";
|
|
@@ -32,6 +31,9 @@ const VIDEO_TIMESCALE = 90_000;
|
|
|
32
31
|
const AUDIO_TIMESCALE = 48_000;
|
|
33
32
|
const SEGMENT_START_SECONDS = 12.5;
|
|
34
33
|
const SESSION_ID = "11111111-2222-3333-4444-555555555555";
|
|
34
|
+
// Its own address, so this file's directory in the shared store is nobody
|
|
35
|
+
// else's.
|
|
36
|
+
const OUTPUT_KEY = "serve-wiring:fmt=fmp4:grid=uniform:video-only:v=0/copy";
|
|
35
37
|
|
|
36
38
|
/**
|
|
37
39
|
* @param {string} type
|
|
@@ -120,39 +122,62 @@ function selfContainedPiece(offsetSeconds) {
|
|
|
120
122
|
* @returns {Promise<{ manager: HlsSessionManager, session: object, dirPath: string }>}
|
|
121
123
|
*/
|
|
122
124
|
async function managerWithReadySegment(overrides = {}) {
|
|
123
|
-
const dirPath = await mkdtemp(path.join(os.tmpdir(), "segment-serve-"));
|
|
124
|
-
const piece = selfContainedPiece(SEGMENT_START_SECONDS);
|
|
125
|
-
// Two segments, because a piece is only finished once the next one exists.
|
|
126
|
-
await writeFile(path.join(dirPath, "segment-00000.mp4"), piece);
|
|
127
|
-
await writeFile(path.join(dirPath, "segment-00001.mp4"), piece);
|
|
128
|
-
|
|
129
125
|
const manager = new HlsSessionManager({
|
|
130
126
|
enabled: true,
|
|
131
127
|
ffmpegBin: "ffmpeg",
|
|
132
128
|
localBindHost: "127.0.0.1",
|
|
133
129
|
localPort: 9090
|
|
134
130
|
});
|
|
131
|
+
// ADDRESSED THE WAY PRODUCTION ADDRESSES IT. A session's segments live in the
|
|
132
|
+
// store's directory for its OUTPUT, and whether one is finished is asked of
|
|
133
|
+
// the store by that same address. This fixture used to make a directory of its
|
|
134
|
+
// own and leave the session with no output address at all, so it described a
|
|
135
|
+
// proxy that no longer exists and asked for a segment the store had never
|
|
136
|
+
// heard of.
|
|
137
|
+
manager.segmentStore.useFormat(OUTPUT_KEY, overrides.segmentFormat ?? fmp4Format);
|
|
138
|
+
const dirPath = manager.segmentStore.directoryFor(OUTPUT_KEY);
|
|
139
|
+
const piece = selfContainedPiece(SEGMENT_START_SECONDS);
|
|
140
|
+
// Two segments, because a piece is only finished once the next one exists.
|
|
141
|
+
await writeFile(path.join(dirPath, "segment-00000.mp4"), piece);
|
|
142
|
+
await writeFile(path.join(dirPath, "segment-00001.mp4"), piece);
|
|
143
|
+
|
|
135
144
|
const session = {
|
|
136
145
|
id: SESSION_ID,
|
|
146
|
+
outputKey: OUTPUT_KEY,
|
|
137
147
|
dirPath,
|
|
138
148
|
// Where this file is cut, held by the file. A fixture that stated it
|
|
139
149
|
// on the session was describing what production no longer does.
|
|
150
|
+
// LONGER THAN WHAT IS ON DISK. Two segments exist; the film runs to five, so
|
|
151
|
+
// there is film left to make and an encoder on it is warranted. With the
|
|
152
|
+
// whole film already made, an encoder is not — the plan stops it, correctly,
|
|
153
|
+
// and a fixture that wants a live run has to give it something to do.
|
|
140
154
|
timeline: new Timeline({
|
|
141
|
-
boundaries: [0, SEGMENT_START_SECONDS, 25],
|
|
155
|
+
boundaries: [0, SEGMENT_START_SECONDS, 25, 37.5, 50, 62.5],
|
|
142
156
|
cutGrid: "uniform"
|
|
143
157
|
}),
|
|
144
158
|
state: "ready",
|
|
145
|
-
file: new SourceFile({ sourceKey: "source-1", fileIndex: 0, name: "video.mkv" }),
|
|
159
|
+
file: new SourceFile({ sourceKey: "source-1", fileIndex: 0, name: "video.mkv" }).learn({ durationSeconds: 62.5 }),
|
|
146
160
|
// An ordinary session reads its own file, and its sound is inside it. The
|
|
147
161
|
// three differ only for a soundtrack shipped as a file of its own.
|
|
148
162
|
get inputFile() { return this.file; },
|
|
149
163
|
get audioFile() { return this.file; },
|
|
164
|
+
// WHAT THIS HOST ENCODES AT. Every real host measures it before a viewer
|
|
165
|
+
// exists, and the plan compares arrivals — which cannot be computed without
|
|
166
|
+
// it. A fixture that leaves it out describes a machine that has never
|
|
167
|
+
// measured itself: every arrangement is then equally hopeless, they all tie,
|
|
168
|
+
// and the plan takes the encoder away for changing nothing.
|
|
169
|
+
lastAloneSpeed: 2,
|
|
150
170
|
startedAt: Date.now(),
|
|
151
171
|
createEntryMs: Date.now(),
|
|
152
172
|
lastAccessedAt: Date.now(),
|
|
153
173
|
runs: new Set(),
|
|
154
174
|
lastError: "",
|
|
155
175
|
consumers: new Set(),
|
|
176
|
+
// Who is watching this output, and how long it is. Without the first the
|
|
177
|
+
// output has no viewers to build a priority map from, so it states no zones
|
|
178
|
+
// at all — which reads as nobody watching and stops every encoder on it.
|
|
179
|
+
viewers: new Map(),
|
|
180
|
+
segmentCount: 5,
|
|
156
181
|
segmentFormat: overrides.segmentFormat ?? fmp4Format,
|
|
157
182
|
useSyntheticPlaylist: true,
|
|
158
183
|
playlistText: "#EXTM3U\n",
|
|
@@ -161,9 +186,20 @@ async function managerWithReadySegment(overrides = {}) {
|
|
|
161
186
|
waitEpoch: 0
|
|
162
187
|
};
|
|
163
188
|
manager.sessionsById.set(SESSION_ID, session);
|
|
189
|
+
// SOMEBODY IS WATCHING IT. A segment is requested by a viewer, so a fixture
|
|
190
|
+
// that asks for one without stating a viewer describes a state production
|
|
191
|
+
// never reaches — and an output nobody is watching has every encoder on it
|
|
192
|
+
// stopped, which is right and is what left the run here reading STOPPED.
|
|
193
|
+
// NO PLAN RUNS HERE. This file is about the path that answers a request for a
|
|
194
|
+
// segment. Deciding how many encoders there should be and where is a separate
|
|
195
|
+
// question with its own checks, and letting it run inside these would spawn
|
|
196
|
+
// real ffmpeg processes and replace the very run each test is watching — the
|
|
197
|
+
// test would then be about the plan, and about it at its least stable.
|
|
198
|
+
manager.planEncodersNow = () => {};
|
|
199
|
+
manager.planEncodersSoon = () => {};
|
|
164
200
|
// The run in force. The fixture's segments live directly in the session
|
|
165
201
|
// directory, which is exactly what one run's directory is here.
|
|
166
|
-
startRunOn(session, { from: 0, usesExplicitCuts: true });
|
|
202
|
+
startRunOn(session, { from: 0, usesExplicitCuts: true, speedX: 2 });
|
|
167
203
|
return { manager, session, dirPath };
|
|
168
204
|
}
|
|
169
205
|
|
|
@@ -245,7 +281,7 @@ test("a fault while preparing an existing segment is named, not turned into a wa
|
|
|
245
281
|
});
|
|
246
282
|
|
|
247
283
|
test("a run's FIRST segment is served once the encoder has passed it, without waiting for a next one", async (t) => {
|
|
248
|
-
const { manager,
|
|
284
|
+
const { manager, dirPath } = await managerWithReadySegment();
|
|
249
285
|
t.after(async () => {
|
|
250
286
|
await manager.disposeAll();
|
|
251
287
|
await rm(dirPath, { recursive: true, force: true });
|
|
@@ -254,7 +290,12 @@ test("a run's FIRST segment is served once the encoder has passed it, without wa
|
|
|
254
290
|
// successor and nothing is producing one. Waiting for that successor is what
|
|
255
291
|
// held #317 for 46 s and then answered 404 to a browser that had given up.
|
|
256
292
|
await rm(path.join(dirPath, "segment-00001.mp4"));
|
|
257
|
-
|
|
293
|
+
// WHAT PROVES IT INSTEAD: the encoder named it. ffmpeg writes the name of each
|
|
294
|
+
// piece on a channel of its own as it closes it, so a run's own first piece is
|
|
295
|
+
// proven the moment it is finished and the absence of a next one says nothing.
|
|
296
|
+
// This used to be inferred from the encoder's reported position instead, which
|
|
297
|
+
// is a different question — where it has read to, not what it has closed.
|
|
298
|
+
manager.segmentStore.markClosed(OUTPUT_KEY, 0);
|
|
258
299
|
|
|
259
300
|
const result = await manager.getFileStream(SESSION_ID, "segment-00000.mp4", { requestSeq: 1 });
|
|
260
301
|
|
|
@@ -301,12 +342,26 @@ test("serving a run's own segment moves the run out of STARTING", async (t) => {
|
|
|
301
342
|
// let go first, because this test is about ONE run and what serving does to
|
|
302
343
|
// it.
|
|
303
344
|
session.runs.clear();
|
|
304
|
-
|
|
345
|
+
// ITS OWN PIECE, AND FILM STILL TO MAKE BEHIND IT. A run standing on material
|
|
346
|
+
// that already exists is moved forward — correctly — so a fixture that wants a
|
|
347
|
+
// live run has to put it where the work is. #1 is what it is making; #2..#4
|
|
348
|
+
// are unmade, so it is wanted; and the encoder has named #1 on its ready
|
|
349
|
+
// channel, which is what makes the piece servable at all.
|
|
350
|
+
const run = startRunOn(session, { from: 1, producing: false, usesExplicitCuts: true, speedX: 2 });
|
|
351
|
+
manager.segmentStore.markClosed(OUTPUT_KEY, 1);
|
|
305
352
|
assert.equal(run.state, ENCODE_RUN_STATE.STARTING);
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
353
|
+
assert.deepEqual(run.produced, [], "it has made nothing yet");
|
|
354
|
+
|
|
355
|
+
await manager.getFileStream(SESSION_ID, "segment-00001.mp4", { requestSeq: 1 });
|
|
356
|
+
|
|
357
|
+
// ASKED OF WHAT THE SERVE WROTE, not of the state that happens to follow it.
|
|
358
|
+
// The plan owns a run's life and may end it in the same turn for reasons of
|
|
359
|
+
// its own — the machine's budget, a viewer leaving, nothing left to make —
|
|
360
|
+
// and a check that reads the state afterwards is really asking what the plan
|
|
361
|
+
// decided. What the SERVE does is tell the run the piece is finished, and that
|
|
362
|
+
// is what moves it out of starting.
|
|
363
|
+
assert.deepEqual(run.produced, [1], "the serve told the run its piece is finished");
|
|
364
|
+
assert.equal(run.head, 2, "so the run stands one past it");
|
|
310
365
|
});
|
|
311
366
|
|
|
312
367
|
test("a segment behind a run's own start does not claim that run has produced", async (t) => {
|
|
@@ -321,9 +376,10 @@ test("a segment behind a run's own start does not claim that run has produced",
|
|
|
321
376
|
// the number alone, and a run believed to be producing is one the look-ahead
|
|
322
377
|
// may suspend and the seek path may wave through as "already covered".
|
|
323
378
|
session.runs.clear();
|
|
324
|
-
const run = startRunOn(session, { from: 5, producing: false, usesExplicitCuts: true });
|
|
379
|
+
const run = startRunOn(session, { from: 5, producing: false, usesExplicitCuts: true, speedX: 2 });
|
|
325
380
|
|
|
326
381
|
await manager.getFileStream(SESSION_ID, "segment-00000.mp4", { requestSeq: 1 });
|
|
327
382
|
|
|
328
|
-
assert.
|
|
383
|
+
assert.deepEqual(run.produced, [], "#0 is somebody else's work and says nothing about this run");
|
|
384
|
+
assert.equal(run.head, 5, "which is still where it began");
|
|
329
385
|
});
|