@torrent-tv/proxy 2.89.2 → 2.89.4
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/AGENTS.md +338 -336
- package/CHANGELOG.md +2315 -2299
- package/docs/encode-architecture.md +55 -19
- package/docs/playback-start-forecast.md +13 -5
- package/docs/storage-architecture.md +6 -0
- package/package.json +1 -1
- package/routes/stream/get.js +38 -2
- package/server.js +748 -745
- package/services/encode/EncodeOrchestrator.js +24 -49
- package/services/encode/EncodePlan.js +5 -3
- package/services/encode/EncodeRun.js +80 -17
- package/services/encode/EncodeRuns.js +1339 -1239
- package/services/encode/Renditions.js +2 -1
- package/services/encode/RunClock.js +87 -0
- package/services/encode/RunProgress.js +1 -6
- package/services/encode/encoder-readings.js +26 -28
- package/services/encode/piece-completeness.js +46 -0
- package/services/encode/quality/EncodeCost.js +1043 -1167
- package/services/encode/quality/OutputQualityState.js +1 -5
- package/services/encode/quality/QualityController.js +213 -390
- package/services/encode/quality/link-budget.js +5 -17
- package/services/encode/run-inputs.js +13 -2
- package/services/server/SegmentServing.js +3 -1
- package/services/server/ViewerRequests.js +2 -7
- package/services/server/wire-outputs.js +12 -7
- package/services/storage/segment-store/SegmentStore.js +46 -29
- package/services/torrent/supply-margin.js +7 -5
- package/test/auto-quality-step.test.js +75 -29
- package/test/decode-cost.test.js +1 -1
- package/test/encode-cost.test.js +31 -39
- package/test/encode-orchestrator.test.js +15 -8
- package/test/encode-plan-viewers.test.js +34 -27
- package/test/encode-run.test.js +34 -6
- package/test/encoder-readings.test.js +48 -26
- package/test/helpers/encode-run.js +113 -9
- package/test/link-budget.test.js +4 -5
- package/test/manager-surface.test.js +1 -2
- package/test/orchestrator-wired.test.js +5 -4
- package/test/output-speed.test.js +0 -1
- package/test/piece-completeness.test.js +42 -0
- package/test/quality-offer.test.js +1 -3
- package/test/quality-variants.test.js +1 -1
- package/test/run-progress.test.js +2 -3
- package/test/same-height-switch.test.js +1 -2
- package/test/segment-store.test.js +15 -15
- package/test/stream-route.test.js +28 -0
- package/test/supply-margin.test.js +13 -0
- package/test/two-viewers-one-picture.test.js +1 -1
- package/test/viewer-outputs.test.js +1 -1
|
@@ -15,6 +15,79 @@
|
|
|
15
15
|
|
|
16
16
|
import { EncodeRun } from "../../services/encode/EncodeRun.js";
|
|
17
17
|
|
|
18
|
+
/** The working time between two closed pieces of a test measurement. */
|
|
19
|
+
const MEASURED_STEP_MS = 1000;
|
|
20
|
+
|
|
21
|
+
/** Closed pieces in a test measurement: the last two give the reading. */
|
|
22
|
+
const MEASURED_STEPS = 3;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The clock each run built by `startRunOn` reads: standing while a measurement
|
|
26
|
+
* is said, the wall clock otherwise. @type {WeakMap<EncodeRun, { at: number | null }>}
|
|
27
|
+
*/
|
|
28
|
+
const wallClocks = new WeakMap();
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Make a run measure a speed the way it does in the product: progress reports
|
|
32
|
+
* and the names of closed pieces on the encoder's own channels, one second of
|
|
33
|
+
* the run's own work apart. A closed piece is published when the next name
|
|
34
|
+
* arrives, and each publication is one reading against the one before, so the
|
|
35
|
+
* last of three steps reads exactly `speedX` whatever the run measured before.
|
|
36
|
+
*
|
|
37
|
+
* @param {EncodeRun} run - Its process exposes `stdout` and `stdio[3]`.
|
|
38
|
+
* @param {number} speedX - Film seconds per second of the run's work.
|
|
39
|
+
* @param {{ at: number } | null} [clock] - What the run's `now` reads; advanced
|
|
40
|
+
* here. Absent for a run built by `startRunOn` without one: its clock is
|
|
41
|
+
* stood just behind the wall clock for the measurement and released after,
|
|
42
|
+
* so the reading is dated now.
|
|
43
|
+
* @param {number} [waitedMs] - How long the run's input waits for the swarm in
|
|
44
|
+
* each step, on top of its second of work.
|
|
45
|
+
* @returns {void}
|
|
46
|
+
*/
|
|
47
|
+
export function measureSpeed(run, speedX, clock = null, waitedMs = 0) {
|
|
48
|
+
if (clock === null) {
|
|
49
|
+
const wall = wallClocks.get(run);
|
|
50
|
+
if (!wall) {
|
|
51
|
+
throw new TypeError("measureSpeed needs the clock the run reads");
|
|
52
|
+
}
|
|
53
|
+
// Never behind the run's start or its own last reading: a step back in
|
|
54
|
+
// time is no work.
|
|
55
|
+
wall.at = Math.max(
|
|
56
|
+
Date.now() - (MEASURED_STEP_MS + waitedMs) * MEASURED_STEPS,
|
|
57
|
+
run.startedAt,
|
|
58
|
+
run.speedReading?.at ?? Number.NEGATIVE_INFINITY
|
|
59
|
+
);
|
|
60
|
+
measureSpeed(run, speedX, wall, waitedMs);
|
|
61
|
+
wall.at = null;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const { stdout, stdio } = run.process;
|
|
65
|
+
let produced = run.progress.processedSeconds - run.progress.startPositionSeconds;
|
|
66
|
+
for (let step = 0; step < MEASURED_STEPS; step += 1) {
|
|
67
|
+
if (waitedMs > 0) {
|
|
68
|
+
run.inputWaitBegins();
|
|
69
|
+
clock.at += waitedMs;
|
|
70
|
+
run.inputWaitEnds();
|
|
71
|
+
}
|
|
72
|
+
clock.at += MEASURED_STEP_MS;
|
|
73
|
+
produced += (speedX * MEASURED_STEP_MS) / 1000;
|
|
74
|
+
say(stdout, `out_time_ms=${Math.round(produced * 1_000_000)}\n`);
|
|
75
|
+
say(stdio[3], `measured-${step}\n`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @param {{ write?: (text: string) => void, emit?: (event: string, text: string) => void }} channel
|
|
81
|
+
* @param {string} text
|
|
82
|
+
*/
|
|
83
|
+
function say(channel, text) {
|
|
84
|
+
if (typeof channel.write === "function") {
|
|
85
|
+
channel.write(text);
|
|
86
|
+
} else {
|
|
87
|
+
channel.emit("data", text);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
18
91
|
/**
|
|
19
92
|
* A child process that records what was done to it.
|
|
20
93
|
*
|
|
@@ -26,6 +99,8 @@ import { EncodeRun } from "../../services/encode/EncodeRun.js";
|
|
|
26
99
|
export function fakeProcess({ pid = null, exitsWhenKilled = true } = {}) {
|
|
27
100
|
/** @type {Map<string, (...args: unknown[]) => void>} */
|
|
28
101
|
const listeners = new Map();
|
|
102
|
+
const stdout = channel();
|
|
103
|
+
const closedPieces = channel();
|
|
29
104
|
return {
|
|
30
105
|
pid,
|
|
31
106
|
killed: false,
|
|
@@ -33,8 +108,10 @@ export function fakeProcess({ pid = null, exitsWhenKilled = true } = {}) {
|
|
|
33
108
|
signalCode: null,
|
|
34
109
|
/** Every signal it was sent, in order. */
|
|
35
110
|
signals: [],
|
|
36
|
-
stdout
|
|
111
|
+
stdout,
|
|
37
112
|
stderr: { on() {} },
|
|
113
|
+
// The descriptor the encoder names its closed pieces on (`-segment_list pipe:3`).
|
|
114
|
+
stdio: [null, stdout, null, closedPieces],
|
|
38
115
|
on(event, handler) {
|
|
39
116
|
const kept = listeners.get(event) ?? [];
|
|
40
117
|
kept.push(handler);
|
|
@@ -71,6 +148,24 @@ export function fakeProcess({ pid = null, exitsWhenKilled = true } = {}) {
|
|
|
71
148
|
};
|
|
72
149
|
}
|
|
73
150
|
|
|
151
|
+
/**
|
|
152
|
+
* A readable side of a pipe that a test writes into.
|
|
153
|
+
*
|
|
154
|
+
* @returns {{ on: (event: string, handler: (chunk: string) => void) => void, write: (text: string) => void }}
|
|
155
|
+
*/
|
|
156
|
+
function channel() {
|
|
157
|
+
/** @type {((chunk: string) => void)[]} */
|
|
158
|
+
const handlers = [];
|
|
159
|
+
return {
|
|
160
|
+
on(event, handler) {
|
|
161
|
+
if (event === "data") handlers.push(handler);
|
|
162
|
+
},
|
|
163
|
+
write(text) {
|
|
164
|
+
for (const handler of handlers) handler(text);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
74
169
|
/** A logger that says nothing, for tests that are not about the log. */
|
|
75
170
|
export const silentLogger = { info() {}, warn() {}, error() {} };
|
|
76
171
|
|
|
@@ -100,14 +195,17 @@ export function startRunOn(session, options = {}) {
|
|
|
100
195
|
producing = true,
|
|
101
196
|
lastSegmentIndex = null,
|
|
102
197
|
usesExplicitCuts = false,
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
// for
|
|
109
|
-
|
|
198
|
+
// THE SPEED IT HAS MEASURED: film made over its own working time between
|
|
199
|
+
// two closed pieces. Left at zero, the plan has no speed to compute an
|
|
200
|
+
// arrival from, every arrangement of encoders is equally hopeless, they all
|
|
201
|
+
// tie, and the encoder is taken away for changing nothing.
|
|
202
|
+
speedX = 0,
|
|
203
|
+
// What the run reads as now, for a test that drives time itself. Absent,
|
|
204
|
+
// the run reads the wall clock, which stands still only while a
|
|
205
|
+
// measurement is said.
|
|
206
|
+
clock: given = null
|
|
110
207
|
} = options;
|
|
208
|
+
const clock = given ?? { at: null };
|
|
111
209
|
const run = new EncodeRun({
|
|
112
210
|
address: session.outputKey ?? session.id ?? "output",
|
|
113
211
|
encoder: { name: "libx264", kind: "software" },
|
|
@@ -118,9 +216,15 @@ export function startRunOn(session, options = {}) {
|
|
|
118
216
|
logger: silentLogger,
|
|
119
217
|
lastSegmentIndex: () => lastSegmentIndex,
|
|
120
218
|
usesExplicitCuts,
|
|
219
|
+
now: () => clock.at ?? Date.now(),
|
|
121
220
|
because: "a test asked for it"
|
|
122
221
|
});
|
|
123
|
-
|
|
222
|
+
if (given === null) {
|
|
223
|
+
wallClocks.set(run, clock);
|
|
224
|
+
}
|
|
225
|
+
if (speedX > 0) {
|
|
226
|
+
measureSpeed(run, speedX, given);
|
|
227
|
+
}
|
|
124
228
|
if (producing) {
|
|
125
229
|
// What moves a run out of starting is its first segment, in the product as
|
|
126
230
|
// here — so a run that is producing has made one, and its head stands one
|
package/test/link-budget.test.js
CHANGED
|
@@ -9,7 +9,6 @@ import test from "node:test";
|
|
|
9
9
|
import assert from "node:assert/strict";
|
|
10
10
|
import {
|
|
11
11
|
admissionRank,
|
|
12
|
-
LINK_SAFETY,
|
|
13
12
|
LINK_VERDICT,
|
|
14
13
|
linkCouldCarry,
|
|
15
14
|
loadOf,
|
|
@@ -67,11 +66,11 @@ test("a load is as trustworthy as its least trustworthy part", () => {
|
|
|
67
66
|
assert.ok(Math.abs(loadOf(known(3), known(0.128)).totalMbps - 3.128) < 1e-9, "the sound is added to the picture");
|
|
68
67
|
});
|
|
69
68
|
|
|
70
|
-
test("the sound is counted
|
|
71
|
-
// The picture alone fits the
|
|
72
|
-
//
|
|
69
|
+
test("the sound is counted with the picture against the whole measured link", () => {
|
|
70
|
+
// The picture alone fits the link exactly; with the soundtrack the viewer
|
|
71
|
+
// hears, it does not. No share of the link is held back.
|
|
73
72
|
const link = 10;
|
|
74
|
-
const picture = known(link
|
|
73
|
+
const picture = known(link);
|
|
75
74
|
assert.equal(linkCouldCarry(link, loadOf(picture, null)).verdict, LINK_VERDICT.FITS, "without sound it would pass");
|
|
76
75
|
assert.equal(
|
|
77
76
|
linkCouldCarry(link, loadOf(picture, known(AUDIO_TRANSCODE_KBPS / 1000))).verdict,
|
|
@@ -142,7 +142,7 @@ function fakeSession({ id = SESSION_ID } = {}) {
|
|
|
142
142
|
useSyntheticPlaylist: true,
|
|
143
143
|
playlistText: "#EXTM3U\n",
|
|
144
144
|
segmentCount: 10,
|
|
145
|
-
progress: { state: "running", processedSeconds: 0, startPositionSeconds: 0
|
|
145
|
+
progress: { state: "running", processedSeconds: 0, startPositionSeconds: 0 }
|
|
146
146
|
};
|
|
147
147
|
}
|
|
148
148
|
|
|
@@ -182,7 +182,6 @@ test("the progress report keeps every figure it carries today", async () => {
|
|
|
182
182
|
"percent",
|
|
183
183
|
"remainingSeconds",
|
|
184
184
|
"segmentDurationSec",
|
|
185
|
-
"speed",
|
|
186
185
|
"currentHeight",
|
|
187
186
|
"offeredHeights",
|
|
188
187
|
"requestedHeight",
|
|
@@ -18,7 +18,7 @@ import { wireOutputs } from "../services/server/wire-outputs.js";
|
|
|
18
18
|
import { SegmentStore } from "../services/storage/segment-store/SegmentStore.js";
|
|
19
19
|
import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
|
|
20
20
|
import { qualityStateOf } from "../services/encode/quality/OutputQualityState.js";
|
|
21
|
-
import { startRunOn } from "./helpers/encode-run.js";
|
|
21
|
+
import { measureSpeed, startRunOn } from "./helpers/encode-run.js";
|
|
22
22
|
import { outputSpec } from "./helpers/output-spec.js";
|
|
23
23
|
|
|
24
24
|
const KEY = "torrent:abc:fmt=fmp4:grid=kf@0:video-only:v=0/copy";
|
|
@@ -65,9 +65,10 @@ function sessionOn({ manager, id, dirPath, encodeStartIndex = 0, runEndIndex = -
|
|
|
65
65
|
const run = startRunOn(session, { from: encodeStartIndex, to: runEndIndex });
|
|
66
66
|
manager.encodeOrchestrator.adopt(KEY, run);
|
|
67
67
|
session.testRun = run;
|
|
68
|
-
// A speed is
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
// A speed is the run's own reading, measured from its closed pieces.
|
|
69
|
+
if (speed > 0) {
|
|
70
|
+
measureSpeed(run, speed);
|
|
71
|
+
}
|
|
71
72
|
}
|
|
72
73
|
return session;
|
|
73
74
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Whether a closed piece holds the whole of its stretch of film.
|
|
3
|
+
*
|
|
4
|
+
* Decided by the encoding from the cut table and the piece's own media; the
|
|
5
|
+
* segment store only keeps what it is given.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import test from "node:test";
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import { cutOf, judgePiece } from "../services/encode/piece-completeness.js";
|
|
11
|
+
import { SEGMENT_CUT_TIME_DELTA_SECONDS } from "../services/encode/output/index.js";
|
|
12
|
+
|
|
13
|
+
/** Media ending where stated, in the shape a format's reading has. */
|
|
14
|
+
const endingAt = (seconds) => ({ end: seconds });
|
|
15
|
+
const format = { producedThroughSeconds: (ranges) => ranges?.end ?? null };
|
|
16
|
+
|
|
17
|
+
/** Cuts every ten seconds, ten pieces. */
|
|
18
|
+
const timeline = { segmentCount: 10, publishedStartOf: (index) => index * 10 };
|
|
19
|
+
|
|
20
|
+
test("a piece must reach its next cut less the delta the muxer is given", () => {
|
|
21
|
+
assert.equal(cutOf(timeline, 3), 40 - SEGMENT_CUT_TIME_DELTA_SECONDS);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("the final piece has no next cut", () => {
|
|
25
|
+
assert.equal(cutOf(timeline, 9), undefined);
|
|
26
|
+
assert.deepEqual(judgePiece(format, endingAt(95), cutOf(timeline, 9)), { whole: true, throughSeconds: 95 });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("an audio piece ending within the delta of its cut is whole", () => {
|
|
30
|
+
// Field 2026-10-01: AAC frames end 4.7389 s into a piece cut at 4.755 s.
|
|
31
|
+
const cut = 4.755 - SEGMENT_CUT_TIME_DELTA_SECONDS;
|
|
32
|
+
assert.equal(judgePiece(format, endingAt(4.738913832199547), cut).whole, true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("a piece closed short of its cut is not whole", () => {
|
|
36
|
+
// A run killed mid-piece: 84 ms of film where ten seconds were cut.
|
|
37
|
+
assert.deepEqual(judgePiece(format, endingAt(456.084), 466), { whole: false, throughSeconds: 456.084 });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("a piece with no playable media is not whole, whatever its cut", () => {
|
|
41
|
+
assert.deepEqual(judgePiece(format, null, undefined), { whole: false, throughSeconds: null });
|
|
42
|
+
});
|
|
@@ -58,10 +58,8 @@ function offerOver(
|
|
|
58
58
|
runningEncoders: () => 0,
|
|
59
59
|
encodersRunningNow: () => 0,
|
|
60
60
|
torrentCostSecFor: () => 0,
|
|
61
|
-
boundBy: async () => "cpu",
|
|
62
61
|
runsFor: (session) => [...(session.runs ?? [])],
|
|
63
|
-
stateFor: (session) => runStateOf(session.runs)
|
|
64
|
-
progressFor: (session) => session.progress ?? null
|
|
62
|
+
stateFor: (session) => runStateOf(session.runs)
|
|
65
63
|
});
|
|
66
64
|
let computed = 0;
|
|
67
65
|
const real = cost.sustainableHeights.bind(cost);
|
|
@@ -131,7 +131,7 @@ function fakeSession({
|
|
|
131
131
|
useSyntheticPlaylist: true,
|
|
132
132
|
playlistText: "#EXTM3U\n",
|
|
133
133
|
segmentCount: 100,
|
|
134
|
-
progress: { state: "running", processedSeconds: 0, startPositionSeconds: 0
|
|
134
|
+
progress: { state: "running", processedSeconds: 0, startPositionSeconds: 0 }
|
|
135
135
|
};
|
|
136
136
|
}
|
|
137
137
|
|
|
@@ -16,7 +16,7 @@ test("a run rebases ffmpeg's relative clock onto its own source position", () =>
|
|
|
16
16
|
let now = 10;
|
|
17
17
|
const progress = new RunProgress({ startSeconds: 600, totalSeconds: 1000, now: () => now });
|
|
18
18
|
now = 20;
|
|
19
|
-
progress.note({ processedSeconds: 12.5
|
|
19
|
+
progress.note({ processedSeconds: 12.5 });
|
|
20
20
|
|
|
21
21
|
assert.deepEqual(progress.snapshot(), {
|
|
22
22
|
processedSeconds: 612.5,
|
|
@@ -24,7 +24,6 @@ test("a run rebases ffmpeg's relative clock onto its own source position", () =>
|
|
|
24
24
|
totalSeconds: 1000,
|
|
25
25
|
percent: 3.125,
|
|
26
26
|
remainingSeconds: 387.5,
|
|
27
|
-
speed: "2.0x",
|
|
28
27
|
updatedAt: 20
|
|
29
28
|
});
|
|
30
29
|
});
|
|
@@ -33,7 +32,7 @@ test("two runs of one output retain separate clocks", () => {
|
|
|
33
32
|
const made = orchestrator();
|
|
34
33
|
const first = { from: 0, to: 49, isAlive: true, progress: new RunProgress({ startSeconds: 0 }) };
|
|
35
34
|
const second = { from: 50, to: 99, isAlive: true, progress: new RunProgress({ startSeconds: 200 }) };
|
|
36
|
-
first.progress.note({ processedSeconds: 40
|
|
35
|
+
first.progress.note({ processedSeconds: 40 });
|
|
37
36
|
second.progress.note({ processedSeconds: 8, speed: "2.0x" });
|
|
38
37
|
made.adopt("picture", first);
|
|
39
38
|
made.adopt("picture", second);
|
|
@@ -65,7 +65,6 @@ import { SourceFile } from "../services/media/SourceFile.js";
|
|
|
65
65
|
import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
|
|
66
66
|
import { outputSpec } from "./helpers/output-spec.js";
|
|
67
67
|
import { maxrateKbpsFor, nominalKbpsFor, softwareRateControlFor } from "../services/encode/args.js";
|
|
68
|
-
import { LINK_SAFETY } from "../services/encode/quality/link-budget.js";
|
|
69
68
|
|
|
70
69
|
const BASE_ID = "1111111122223333";
|
|
71
70
|
const SEGMENT_SECONDS = 4;
|
|
@@ -86,7 +85,7 @@ const AUDIO_MBPS = 0.128;
|
|
|
86
85
|
* @returns {number}
|
|
87
86
|
*/
|
|
88
87
|
function linkJustFor(limit) {
|
|
89
|
-
return
|
|
88
|
+
return maxrateKbpsFor(limit) / 1000 + AUDIO_MBPS + 0.01;
|
|
90
89
|
}
|
|
91
90
|
|
|
92
91
|
/**
|
|
@@ -10,7 +10,6 @@ import os from "node:os";
|
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
import { directoryNameFor, SegmentStore } from "../services/storage/segment-store/SegmentStore.js";
|
|
12
12
|
import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
|
|
13
|
-
import { SEGMENT_CUT_TIME_DELTA_SECONDS } from "../services/encode/output/index.js";
|
|
14
13
|
|
|
15
14
|
/**
|
|
16
15
|
* @returns {{ store: SegmentStore, root: string, lines: string[] }}
|
|
@@ -36,32 +35,33 @@ function writeSegment(dir, index, bytes = 16) {
|
|
|
36
35
|
|
|
37
36
|
const KEY = "torrent:abc:fmt=fmp4:grid=kf@0:video-only:v=0/copy";
|
|
38
37
|
|
|
39
|
-
test("a
|
|
38
|
+
test("a published piece keeps the media intervals the encoding read from it", (t) => {
|
|
39
|
+
// The store does not judge a piece; whether it is whole is the encoding's
|
|
40
|
+
// decision (`encode/piece-completeness.js`), taken before it is published.
|
|
40
41
|
const { store, root } = storeInATempRoot();
|
|
41
42
|
t.after(() => { store.dropAll("the check is over"); rmSync(root, { recursive: true, force: true }); });
|
|
42
|
-
const format = { ...fmp4Format, readMediaRanges: () =>
|
|
43
|
-
producedThroughSeconds: (ranges) => ranges.at(-1).end };
|
|
43
|
+
const format = { ...fmp4Format, readMediaRanges: () => assert.fail("the store does not read a piece it was handed") };
|
|
44
44
|
const dir = store.directoryFor(KEY);
|
|
45
45
|
store.useFormat(KEY, format);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
assert.
|
|
46
|
+
writeFileSync(path.join(dir, "making-0-00000.mp4"), Buffer.from("closed"));
|
|
47
|
+
assert.deepEqual(store.closedBytesOf(KEY, "making-0-00000.mp4"), Buffer.from("closed"));
|
|
48
|
+
const mediaRanges = { tracks: [] };
|
|
49
|
+
assert.equal(store.publish(KEY, "making-0-00000.mp4", format, { mediaRanges }), "segment-00000.mp4");
|
|
50
|
+
assert.equal(store.mediaRangesOf(KEY, 0), mediaRanges);
|
|
50
51
|
});
|
|
51
52
|
|
|
52
|
-
test("a
|
|
53
|
-
const { store, root } = storeInATempRoot();
|
|
53
|
+
test("a stored piece is taken off the disk at its owner's word", (t) => {
|
|
54
|
+
const { store, root, lines } = storeInATempRoot();
|
|
54
55
|
t.after(() => { store.dropAll("the check is over"); rmSync(root, { recursive: true, force: true }); });
|
|
55
|
-
const format = { ...fmp4Format, readMediaRanges: () =>
|
|
56
|
-
producedThroughSeconds: (ranges) => ranges.at(-1).end };
|
|
56
|
+
const format = { ...fmp4Format, readMediaRanges: () => ({ tracks: [] }) };
|
|
57
57
|
const dir = store.directoryFor(KEY);
|
|
58
58
|
store.useFormat(KEY, format);
|
|
59
|
-
writeFileSync(path.join(dir, "making-0-00071.mp4"), Buffer.alloc(16));
|
|
60
|
-
assert.equal(store.publish(KEY, "making-0-00071.mp4", format, { endSeconds: 466 }), null);
|
|
61
59
|
writeSegment(dir, 71);
|
|
62
60
|
assert.ok(store.mediaRangesOf(KEY, 71));
|
|
63
|
-
|
|
61
|
+
store.remove(KEY, 71, "short of its cut");
|
|
64
62
|
assert.equal(store.pathOf(KEY, 71), null);
|
|
63
|
+
assert.equal(store.mediaRangesOf(KEY, 71), undefined);
|
|
64
|
+
assert.ok(lines.some((line) => line.includes("segment 71") && line.includes("short of its cut")));
|
|
65
65
|
});
|
|
66
66
|
|
|
67
67
|
test("two viewers of one output are given the same directory", (t) => {
|
|
@@ -207,3 +207,31 @@ test("a file this proxy does not have whole still goes to the torrent", async ()
|
|
|
207
207
|
await handleStreamGet(req, reply, deps);
|
|
208
208
|
assert.equal(state.prioritized.length, 1, "the ordinary path was not taken");
|
|
209
209
|
});
|
|
210
|
+
|
|
211
|
+
test("every wait of an encoder's input read is marked for the run its URL names", async () => {
|
|
212
|
+
// The run's processing speed is read over its own working time; the time its
|
|
213
|
+
// input waits for the swarm is what this marks (RunClock).
|
|
214
|
+
const { req, reply, deps } = harness({ method: "GET", range: "bytes=0-5" });
|
|
215
|
+
const { files: [file] } = await deps.torrentPool.getTorrent();
|
|
216
|
+
const pieces = [Buffer.from("abc"), Buffer.from("def")];
|
|
217
|
+
file.createFragmentReader = () => ({
|
|
218
|
+
cancel() {},
|
|
219
|
+
async *[Symbol.asyncIterator]() {
|
|
220
|
+
for (const bytes of pieces) {
|
|
221
|
+
yield { bytes, release() {} };
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
reply.raw.write = (_bytes, done) => done();
|
|
226
|
+
reply.raw.writableEnded = false;
|
|
227
|
+
reply.raw.destroyed = false;
|
|
228
|
+
req.query.session = "output-1";
|
|
229
|
+
req.query.run = "7";
|
|
230
|
+
const marks = [];
|
|
231
|
+
deps.noteInputWaiting = (runToken, waiting) => marks.push([runToken, waiting]);
|
|
232
|
+
|
|
233
|
+
await handleStreamGet(req, reply, deps);
|
|
234
|
+
|
|
235
|
+
assert.deepEqual(marks, [[7, true], [7, false], [7, true], [7, false], [7, true], [7, false]],
|
|
236
|
+
"a wait before each fragment and one before the end, each closed");
|
|
237
|
+
});
|
|
@@ -175,3 +175,16 @@ test("a term with nothing observed contributes nothing", () => {
|
|
|
175
175
|
assert.equal(minimumBufferFrom({}), null, "without a segment duration nothing can be said");
|
|
176
176
|
assert.equal(minimumBufferFrom({ segmentSeconds: 0 }), null);
|
|
177
177
|
});
|
|
178
|
+
|
|
179
|
+
test("a supply that delivered for a sliver of the time asks for what it measured, uncut", () => {
|
|
180
|
+
// Two stalls of 99.9 s with 0.1 s of delivery between them: the share lost is
|
|
181
|
+
// 0.999, and the speed asked for is a thousand times realtime. A cap at a
|
|
182
|
+
// chosen 0.99 used to report a hundred.
|
|
183
|
+
const answer = requiredSpeedFrom([
|
|
184
|
+
{ waitedMs: 99_900, at: 99_900 },
|
|
185
|
+
{ waitedMs: 99_900, at: 199_900 }
|
|
186
|
+
]);
|
|
187
|
+
assert.ok(answer);
|
|
188
|
+
assert.ok(Math.abs(answer.lostShare - 0.999) < 1e-9, `got ${answer.lostShare}`);
|
|
189
|
+
assert.ok(Math.abs(answer.requiredSpeed - 1000) < 1e-6, `got ${answer.requiredSpeed}`);
|
|
190
|
+
});
|
|
@@ -132,7 +132,7 @@ function fakeSession({
|
|
|
132
132
|
useSyntheticPlaylist: true,
|
|
133
133
|
playlistText: "#EXTM3U\n",
|
|
134
134
|
segmentCount: 100,
|
|
135
|
-
progress: { state: "running", processedSeconds: 0, startPositionSeconds: 0
|
|
135
|
+
progress: { state: "running", processedSeconds: 0, startPositionSeconds: 0 }
|
|
136
136
|
};
|
|
137
137
|
}
|
|
138
138
|
|
|
@@ -98,7 +98,7 @@ function fakeSession({ id, dirPath, file, encodeHeight = 0, audioOnly = false, i
|
|
|
98
98
|
useSyntheticPlaylist: true,
|
|
99
99
|
playlistText: "#EXTM3U\n",
|
|
100
100
|
segmentCount: 100,
|
|
101
|
-
progress: { state: "running", processedSeconds: 0, startPositionSeconds: 0
|
|
101
|
+
progress: { state: "running", processedSeconds: 0, startPositionSeconds: 0 }
|
|
102
102
|
};
|
|
103
103
|
}
|
|
104
104
|
|