@torrent-tv/proxy 2.80.5 → 2.80.7
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 +36 -0
- package/package.json +1 -1
- package/routes/api/delivery-sink/get.js +8 -4
- package/server.js +415 -403
- package/services/data-channel-handler.js +87 -25
- package/services/delivery-probe.js +38 -5
- package/services/encode/CoverageMap.js +77 -4
- package/services/encode/EncodePlan.js +1025 -358
- package/services/encode/EncodeRun.js +19 -1
- package/services/encode/SegmentDemand.js +0 -0
- package/services/encode/SegmentStore.js +53 -1
- package/services/encode/open-piece.js +22 -1
- package/services/encode/run-command.js +12 -2
- package/services/hls-session-manager.js +38 -158
- package/services/hwaccel.js +182 -54
- package/services/orchestrators/EncodeOrchestrator.js +118 -89
- package/services/output/LiveOutputs.js +233 -213
- package/services/output/Timeline.js +333 -256
- package/services/piece-store/shared-piece-store.js +13 -9
- package/services/priority/PriorityMap.js +262 -108
- package/services/priority/PriorityOrchestrator.js +31 -6
- package/services/quality/EncodeCost.js +555 -500
- package/services/torrent-pool.js +9 -4
- package/test/encode-orchestrator.test.js +195 -65
- package/test/encode-plan-viewers.test.js +719 -0
- package/test/encode-plan.test.js +174 -81
- package/test/open-piece.test.js +40 -0
- package/test/output-speed.test.js +86 -0
- package/test/piece-store-eviction.test.js +26 -0
- package/test/priority-map-download.test.js +25 -7
- package/test/priority-map.test.js +134 -83
- package/test/seek-landing.test.js +109 -76
- package/test/segment-demand.test.js +54 -56
- package/test/wedge-certainty.test.js +3 -3
package/services/hwaccel.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { spawn } from "node:child_process";
|
|
24
|
-
import { mkdtempSync, readdirSync, rmSync, statSync } from "node:fs";
|
|
24
|
+
import { mkdtempSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
25
25
|
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
26
26
|
import os from "node:os";
|
|
27
27
|
import path from "node:path";
|
|
@@ -35,25 +35,25 @@ import {
|
|
|
35
35
|
parseFfmpegVideoFps
|
|
36
36
|
} from "./ffmpeg-banner.js";
|
|
37
37
|
|
|
38
|
-
import { keyFrameArgs, SOFTWARE_CRF, TRANSCODE_FPS } from "./encode/args.js";
|
|
39
|
-
// The five kinds, one class each. Detection and benchmarking stay in this file;
|
|
40
|
-
// how a kind is driven belongs to the kind.
|
|
41
|
-
import {
|
|
42
|
-
NvencEncoder,
|
|
43
|
-
QsvEncoder,
|
|
44
|
-
SoftwareEncoder,
|
|
45
|
-
V4l2m2mEncoder,
|
|
46
|
-
VaapiEncoder
|
|
47
|
-
} from "./encode/index.js";
|
|
48
|
-
// Re-exported so every caller goes on importing these figures from here:
|
|
49
|
-
// the same calculation, moved to sit beside the encoder kinds built from it.
|
|
50
|
-
export {
|
|
51
|
-
chooseOutputFps,
|
|
52
|
-
maxrateKbpsFor,
|
|
53
|
-
nominalKbpsForHeight,
|
|
54
|
-
nominalKbpsForMaxrate,
|
|
55
|
-
TRANSCODE_FPS
|
|
56
|
-
} from "./encode/args.js";
|
|
38
|
+
import { keyFrameArgs, SOFTWARE_CRF, TRANSCODE_FPS } from "./encode/args.js";
|
|
39
|
+
// The five kinds, one class each. Detection and benchmarking stay in this file;
|
|
40
|
+
// how a kind is driven belongs to the kind.
|
|
41
|
+
import {
|
|
42
|
+
NvencEncoder,
|
|
43
|
+
QsvEncoder,
|
|
44
|
+
SoftwareEncoder,
|
|
45
|
+
V4l2m2mEncoder,
|
|
46
|
+
VaapiEncoder
|
|
47
|
+
} from "./encode/index.js";
|
|
48
|
+
// Re-exported so every caller goes on importing these figures from here:
|
|
49
|
+
// the same calculation, moved to sit beside the encoder kinds built from it.
|
|
50
|
+
export {
|
|
51
|
+
chooseOutputFps,
|
|
52
|
+
maxrateKbpsFor,
|
|
53
|
+
nominalKbpsForHeight,
|
|
54
|
+
nominalKbpsForMaxrate,
|
|
55
|
+
TRANSCODE_FPS
|
|
56
|
+
} from "./encode/args.js";
|
|
57
57
|
|
|
58
58
|
// libx264 presets to benchmark, ordered slowest/highest-quality → fastest.
|
|
59
59
|
const BENCHMARK_PRESETS = ["fast", "faster", "veryfast", "superfast", "ultrafast"];
|
|
@@ -78,6 +78,15 @@ const ENCODE_BENCHMARK_MAX_PLAUSIBLE_SPEED = 1000;
|
|
|
78
78
|
* arrive twice a second whatever the encoding speed.
|
|
79
79
|
*/
|
|
80
80
|
const ENCODE_BENCHMARK_TIMEOUT_MS = 10_000;
|
|
81
|
+
/**
|
|
82
|
+
* How many times the calibration clip is joined to itself to measure copying.
|
|
83
|
+
*
|
|
84
|
+
* Not a figure about the machine: it is how much film the reading needs to have
|
|
85
|
+
* in front of it. A copy runs at hundreds of times realtime, and the slope is
|
|
86
|
+
* taken over a window of one second, so the input has to hold more film than the
|
|
87
|
+
* fastest plausible host gets through in that second. Forty laps of a five-second
|
|
88
|
+
* clip is 200 s of film, which covers the ceiling `slopeOf` will accept.
|
|
89
|
+
*/
|
|
81
90
|
/** Progress reports arrive line by line. */
|
|
82
91
|
const NEWLINE = String.fromCharCode(10);
|
|
83
92
|
// Producing one second of video per second of clock. Not a margin and not a
|
|
@@ -94,40 +103,40 @@ const REALTIME = 1;
|
|
|
94
103
|
const UNPRICED_DECODE_BAR = 1.8;
|
|
95
104
|
|
|
96
105
|
|
|
97
|
-
// The five kinds live in `encode/`, one class each, and these keep the names
|
|
98
|
-
// every caller already uses. A kind states its own arguments and its own
|
|
99
|
-
// ladder of speed settings; detection and benchmarking stay here.
|
|
100
|
-
/** @returns {import("./hwaccel.js").VideoEncoderDescriptor} */
|
|
101
|
-
export function softwareDescriptor() {
|
|
102
|
-
return new SoftwareEncoder();
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* @param {string} device
|
|
107
|
-
* @returns {import("./hwaccel.js").VideoEncoderDescriptor}
|
|
108
|
-
*/
|
|
109
|
-
function vaapiDescriptor(device) {
|
|
110
|
-
return new VaapiEncoder(device);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* @param {string} device
|
|
115
|
-
* @returns {import("./hwaccel.js").VideoEncoderDescriptor}
|
|
116
|
-
*/
|
|
117
|
-
function qsvDescriptor(device) {
|
|
118
|
-
return new QsvEncoder(device);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** @returns {import("./hwaccel.js").VideoEncoderDescriptor} */
|
|
122
|
-
function nvencDescriptor() {
|
|
123
|
-
return new NvencEncoder();
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** @returns {import("./hwaccel.js").VideoEncoderDescriptor} */
|
|
127
|
-
function v4l2m2mDescriptor() {
|
|
128
|
-
return new V4l2m2mEncoder();
|
|
129
|
-
}
|
|
130
|
-
|
|
106
|
+
// The five kinds live in `encode/`, one class each, and these keep the names
|
|
107
|
+
// every caller already uses. A kind states its own arguments and its own
|
|
108
|
+
// ladder of speed settings; detection and benchmarking stay here.
|
|
109
|
+
/** @returns {import("./hwaccel.js").VideoEncoderDescriptor} */
|
|
110
|
+
export function softwareDescriptor() {
|
|
111
|
+
return new SoftwareEncoder();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @param {string} device
|
|
116
|
+
* @returns {import("./hwaccel.js").VideoEncoderDescriptor}
|
|
117
|
+
*/
|
|
118
|
+
function vaapiDescriptor(device) {
|
|
119
|
+
return new VaapiEncoder(device);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @param {string} device
|
|
124
|
+
* @returns {import("./hwaccel.js").VideoEncoderDescriptor}
|
|
125
|
+
*/
|
|
126
|
+
function qsvDescriptor(device) {
|
|
127
|
+
return new QsvEncoder(device);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** @returns {import("./hwaccel.js").VideoEncoderDescriptor} */
|
|
131
|
+
function nvencDescriptor() {
|
|
132
|
+
return new NvencEncoder();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** @returns {import("./hwaccel.js").VideoEncoderDescriptor} */
|
|
136
|
+
function v4l2m2mDescriptor() {
|
|
137
|
+
return new V4l2m2mEncoder();
|
|
138
|
+
}
|
|
139
|
+
|
|
131
140
|
|
|
132
141
|
|
|
133
142
|
/**
|
|
@@ -1713,3 +1722,122 @@ export function chooseSoftwareEncodeSettings(benchmark, ceiling, outputFps, cost
|
|
|
1713
1722
|
const preset = pickSoftwarePreset(benchmark, chosen.width * chosen.height * fps, cost);
|
|
1714
1723
|
return { width: chosen.width, height: chosen.height, preset, ladder, rungIndex: chosenIndex };
|
|
1715
1724
|
}
|
|
1725
|
+
|
|
1726
|
+
/**
|
|
1727
|
+
* How fast this machine COPIES a picture, in seconds of film per second.
|
|
1728
|
+
*
|
|
1729
|
+
* The startup measurements price encoding and decoding, and a copied picture
|
|
1730
|
+
* does neither: it reads packets and writes them out again. That left one whole
|
|
1731
|
+
* branch of what this proxy does with no figure at all, and a figure is what
|
|
1732
|
+
* every decision in the encoding layer is made from — where to put an encoder,
|
|
1733
|
+
* how many to run, whether anybody will be left waiting. Without it a copied
|
|
1734
|
+
* output was planned with no speed until its own run had been running long
|
|
1735
|
+
* enough to report one, which is exactly the moment the plan matters most.
|
|
1736
|
+
*
|
|
1737
|
+
* Measured the same way as the others: ffmpeg's own progress, read as a slope
|
|
1738
|
+
* over a window, so the process starting is outside the figure.
|
|
1739
|
+
*
|
|
1740
|
+
* The clip is joined to itself first rather than looped with `-stream_loop`.
|
|
1741
|
+
* Looping charges a re-initialisation per lap — measured on the addon host at
|
|
1742
|
+
* 0.03 s for 480p and 0.12 s for 1080p — and a copy of a five-second clip laps
|
|
1743
|
+
* many times a second, so the reading would have been mostly re-initialisation.
|
|
1744
|
+
*
|
|
1745
|
+
* @param {{ ffmpegBin: string, logger?: { info: (m: string) => void, warn: (m: string) => void }, clipsDir?: string }} params
|
|
1746
|
+
* @returns {Promise<number | null>} Seconds of film per second, or null where
|
|
1747
|
+
* the reading could not be taken. Null means unmeasured and is never a
|
|
1748
|
+
* substitute for a number.
|
|
1749
|
+
*/
|
|
1750
|
+
export async function benchmarkCopySpeed({ ffmpegBin, logger, clipsDir = CALIBRATION_DIR }) {
|
|
1751
|
+
const log = logger ?? { info: () => {}, warn: () => {} };
|
|
1752
|
+
const startedAt = Date.now();
|
|
1753
|
+
// The largest clip in the set. A copy moves BYTES, so what it can do is a
|
|
1754
|
+
// statement about the biggest pictures this host will be asked to pass
|
|
1755
|
+
// through, and the small ones are covered by the same figure.
|
|
1756
|
+
const clip = path.join(clipsDir, "cal-h264-1080-hi.mp4");
|
|
1757
|
+
const speed = await measureCopySlope(ffmpegBin, clip);
|
|
1758
|
+
if (!(speed > 0)) {
|
|
1759
|
+
log.warn("hwaccel: copying could not be measured; a copied picture will be planned from its own run instead");
|
|
1760
|
+
return null;
|
|
1761
|
+
}
|
|
1762
|
+
log.info(
|
|
1763
|
+
`hwaccel: this host copies a picture at ${speed.toFixed(0)}x realtime ` +
|
|
1764
|
+
`(measured in ${((Date.now() - startedAt) / 1000).toFixed(1)}s)`
|
|
1765
|
+
);
|
|
1766
|
+
return speed;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
/**
|
|
1770
|
+
* Seconds of film per second, copying one file, read from ffmpeg's progress.
|
|
1771
|
+
*
|
|
1772
|
+
* @param {string} ffmpegBin
|
|
1773
|
+
* @param {string} filePath
|
|
1774
|
+
* @returns {Promise<number | null>}
|
|
1775
|
+
*/
|
|
1776
|
+
function measureCopySlope(ffmpegBin, filePath) {
|
|
1777
|
+
return new Promise((resolve) => {
|
|
1778
|
+
const args = [
|
|
1779
|
+
"-hide_banner", "-loglevel", "error", "-nostats",
|
|
1780
|
+
// Played over and over, because a copy gets through a five-second clip in
|
|
1781
|
+
// milliseconds and a slope needs a window to be taken over. Looping
|
|
1782
|
+
// charges the demuxer being re-opened once a lap, so what comes out is a
|
|
1783
|
+
// FLOOR on what this host can copy — the safe direction, since a plan made
|
|
1784
|
+
// from it expects copying to be slower than it is.
|
|
1785
|
+
"-stream_loop", "-1", "-i", filePath,
|
|
1786
|
+
// What a copied output does: packets in, packets out, nothing decoded and
|
|
1787
|
+
// nothing encoded. Written nowhere, so the figure is this machine's own
|
|
1788
|
+
// handling and not the disk under a temp directory.
|
|
1789
|
+
"-c", "copy", "-f", "null", "-",
|
|
1790
|
+
// Progress is reported every half second by default, which over a window
|
|
1791
|
+
// of one second is two readings. This asks for twenty.
|
|
1792
|
+
"-stats_period", "0.05",
|
|
1793
|
+
"-progress", "pipe:1"
|
|
1794
|
+
];
|
|
1795
|
+
/** @type {Array<{ wallSec: number, outSec: number }>} */
|
|
1796
|
+
const samples = [];
|
|
1797
|
+
let settled = false;
|
|
1798
|
+
let buffered = "";
|
|
1799
|
+
let child;
|
|
1800
|
+
const startedAt = Date.now();
|
|
1801
|
+
const finish = (value) => {
|
|
1802
|
+
if (settled) {
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
settled = true;
|
|
1806
|
+
clearTimeout(timer);
|
|
1807
|
+
try {
|
|
1808
|
+
child?.kill("SIGKILL");
|
|
1809
|
+
} catch {
|
|
1810
|
+
// already gone
|
|
1811
|
+
}
|
|
1812
|
+
resolve(value);
|
|
1813
|
+
};
|
|
1814
|
+
const timer = setTimeout(() => finish(null), ENCODE_BENCHMARK_TIMEOUT_MS);
|
|
1815
|
+
try {
|
|
1816
|
+
child = spawn(ffmpegBin, args, { stdio: ["ignore", "pipe", "ignore"], windowsHide: true });
|
|
1817
|
+
} catch {
|
|
1818
|
+
finish(null);
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
child.stdout.on("data", (chunk) => {
|
|
1822
|
+
buffered += String(chunk);
|
|
1823
|
+
let newline = buffered.indexOf(NEWLINE);
|
|
1824
|
+
while (newline >= 0) {
|
|
1825
|
+
const line = buffered.slice(0, newline).trim();
|
|
1826
|
+
buffered = buffered.slice(newline + 1);
|
|
1827
|
+
if (line.startsWith("out_time_ms=")) {
|
|
1828
|
+
const outSec = Number(line.slice("out_time_ms=".length)) / 1e6;
|
|
1829
|
+
if (Number.isFinite(outSec) && outSec >= 0) {
|
|
1830
|
+
samples.push({ wallSec: (Date.now() - startedAt) / 1000, outSec });
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
newline = buffered.indexOf(NEWLINE);
|
|
1834
|
+
}
|
|
1835
|
+
const slope = slopeOf(samples);
|
|
1836
|
+
if (slope !== null) {
|
|
1837
|
+
finish(slope);
|
|
1838
|
+
}
|
|
1839
|
+
});
|
|
1840
|
+
child.on("error", () => finish(null));
|
|
1841
|
+
child.on("exit", () => finish(slopeOf(samples, ENCODE_BENCHMARK_MIN_WINDOW_SEC)));
|
|
1842
|
+
});
|
|
1843
|
+
}
|
|
@@ -27,9 +27,9 @@ import { CoverageMap } from "../encode/CoverageMap.js";
|
|
|
27
27
|
import { firstUnmetWant, planEncoders } from "../encode/EncodePlan.js";
|
|
28
28
|
import { endOfRun } from "../encode/EncodeRun.js";
|
|
29
29
|
import { ENCODE_EXIT } from "../encode/encode-exit.js";
|
|
30
|
-
import { mergeMaps } from "../priority/PriorityMap.js";
|
|
31
30
|
import { affordableRuns } from "../encode/run-budget.js";
|
|
32
31
|
import { RunCosts } from "../encode/run-costs.js";
|
|
32
|
+
import { contentionPenalty } from "../contention.js";
|
|
33
33
|
import { SegmentDemand } from "../encode/SegmentDemand.js";
|
|
34
34
|
|
|
35
35
|
export class EncodeOrchestrator {
|
|
@@ -40,6 +40,9 @@ export class EncodeOrchestrator {
|
|
|
40
40
|
#runs = new Map();
|
|
41
41
|
|
|
42
42
|
|
|
43
|
+
/** The fastest speed measured on one output, kept across restarts. @type {Map<string, number>} */
|
|
44
|
+
#lastSpeed = new Map();
|
|
45
|
+
|
|
43
46
|
/** How runs have ended, by cause. @type {Map<string, number>} */
|
|
44
47
|
#endings = new Map();
|
|
45
48
|
|
|
@@ -62,7 +65,12 @@ export class EncodeOrchestrator {
|
|
|
62
65
|
* for a stretch. What to read, what to map and how to cut belong to whoever
|
|
63
66
|
* knows the source.
|
|
64
67
|
* @param {number} params.segmentSeconds
|
|
65
|
-
* @param {
|
|
68
|
+
* @param {import("../contention.js").ContentionPenalties | null}
|
|
69
|
+
* [params.contentionPenalties] - How much slower one encoder runs beside
|
|
70
|
+
* others, MEASURED on this host at startup and keyed by how many others
|
|
71
|
+
* there are. Null until something has measured it, and then the penalty is
|
|
72
|
+
* 1 — a number invented here would be the same mistake as an invented
|
|
73
|
+
* encoding speed.
|
|
66
74
|
* @param {{ info: (line: string) => void, warn: (line: string) => void }} params.logger
|
|
67
75
|
* @param {() => number} [params.now]
|
|
68
76
|
*/
|
|
@@ -70,8 +78,9 @@ export class EncodeOrchestrator {
|
|
|
70
78
|
maxRunsFor,
|
|
71
79
|
makeRun,
|
|
72
80
|
segmentSeconds,
|
|
73
|
-
|
|
81
|
+
contentionPenalties = null,
|
|
74
82
|
refetchSecPerFilmSecond = () => 0,
|
|
83
|
+
startingSpeedFor = () => 0,
|
|
75
84
|
segmentStore = null,
|
|
76
85
|
logger,
|
|
77
86
|
now
|
|
@@ -88,9 +97,16 @@ export class EncodeOrchestrator {
|
|
|
88
97
|
// Injected, because the film's byte rate and the swarm's are measured
|
|
89
98
|
// elsewhere and this class must not reach for them.
|
|
90
99
|
this.refetchSecPerFilmSecond = refetchSecPerFilmSecond;
|
|
100
|
+
// Measured per host: what a second encoder costs the first. Unmeasured is 1,
|
|
101
|
+
// and then only the budget bounds how many there are.
|
|
102
|
+
this.contentionPenalties = contentionPenalties instanceof Map ? contentionPenalties : null;
|
|
103
|
+
// WHAT THIS HOST ENCODES AT BEFORE ANY RUN HAS REPORTED. The startup
|
|
104
|
+
// benchmark measures it — a real pipeline over real clips, before a viewer
|
|
105
|
+
// exists — so the plan is never asked to compare arrivals with no speed to
|
|
106
|
+
// compute them from. Every run that then works refines it.
|
|
107
|
+
this.startingSpeedFor = startingSpeedFor;
|
|
91
108
|
this.makeRun = makeRun;
|
|
92
109
|
this.segmentSeconds = segmentSeconds;
|
|
93
|
-
this.restartCostSec = restartCostSec;
|
|
94
110
|
this.logger = logger;
|
|
95
111
|
this.now = typeof now === "function" ? now : Date.now;
|
|
96
112
|
}
|
|
@@ -154,17 +170,27 @@ export class EncodeOrchestrator {
|
|
|
154
170
|
* takes them in this order. Absent means one undifferentiated want, which
|
|
155
171
|
* is what a caller that knows only a position states.
|
|
156
172
|
*/
|
|
157
|
-
want({ claimant, address, from, to, priority = 0 }) {
|
|
158
|
-
this.demand.state({ claimant, address, from, to, priority, statedAt: this.now() });
|
|
159
|
-
}
|
|
160
|
-
|
|
161
173
|
/**
|
|
162
|
-
*
|
|
174
|
+
* What is wanted of one output, in its own segment numbers.
|
|
175
|
+
*
|
|
176
|
+
* ONE MAP, ALREADY MERGED, AND WITH NOBODY'S NAME ON IT. It is built once per
|
|
177
|
+
* film by the layer that knows where the viewers are; this layer receives it
|
|
178
|
+
* converted into an output's own numbering and never asks who is in it.
|
|
179
|
+
*
|
|
180
|
+
* That replaced a window per viewer per band stated here and merged here,
|
|
181
|
+
* which was the same work done twice in two layers, with the viewer's name as
|
|
182
|
+
* the key of a claim — against the rule that the encoding and the viewer are
|
|
183
|
+
* not connected at all.
|
|
184
|
+
*
|
|
185
|
+
* An empty map says nobody is coming anywhere in this output, and the plan
|
|
186
|
+
* stops its encoders for it. Nothing has to be released when somebody leaves:
|
|
187
|
+
* the map that arrives next simply does not have them in it.
|
|
163
188
|
*
|
|
164
|
-
* @param {string}
|
|
189
|
+
* @param {string} address
|
|
190
|
+
* @param {{ from: number, to: number, priority: number, withinSeconds: number }[]} zones
|
|
165
191
|
*/
|
|
166
|
-
|
|
167
|
-
this.demand.
|
|
192
|
+
notePriorityMap(address, zones) {
|
|
193
|
+
this.demand.state(address, zones);
|
|
168
194
|
}
|
|
169
195
|
|
|
170
196
|
/**
|
|
@@ -191,6 +217,14 @@ export class EncodeOrchestrator {
|
|
|
191
217
|
run.noteSpeed(speedX);
|
|
192
218
|
}
|
|
193
219
|
}
|
|
220
|
+
// HOW FAST THIS MACHINE ENCODES THIS OUTPUT is a property of the machine and
|
|
221
|
+
// the material, not of one process. Read off `run.speedX` alone it was lost
|
|
222
|
+
// at every restart: a moved encoder is a new object that has measured
|
|
223
|
+
// nothing, so the plan fell back to "nothing is known" and stopped comparing
|
|
224
|
+
// arrivals at all — which is every decision in this layer.
|
|
225
|
+
if (speedX > 0 && speedX > (this.#lastSpeed.get(address) ?? 0)) {
|
|
226
|
+
this.#lastSpeed.set(address, speedX);
|
|
227
|
+
}
|
|
194
228
|
}
|
|
195
229
|
|
|
196
230
|
/**
|
|
@@ -241,32 +275,20 @@ export class EncodeOrchestrator {
|
|
|
241
275
|
});
|
|
242
276
|
}
|
|
243
277
|
}
|
|
244
|
-
// Carrying the priority through, because the filling takes the work in that
|
|
245
|
-
// order: what a viewer must have before they set off comes before what is
|
|
246
|
-
// merely in front of them, which comes before the rest of the track. Passed
|
|
247
|
-
// as a plain number so the plan stays arithmetic.
|
|
248
278
|
// ONE MAP, NOT ONE WINDOW PER VIEWER PER ZONE.
|
|
249
279
|
//
|
|
250
|
-
// Two viewers a few seconds apart state stretches that overlap, and the
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
// standing in front of it.
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
to: window.to + 1,
|
|
263
|
-
// A window stated without a number is still a want — one
|
|
264
|
-
// undifferentiated want, which is what a caller that knows only a
|
|
265
|
-
// position states. Zero would read as "nothing wanted here" and the
|
|
266
|
-
// merge would drop it.
|
|
267
|
-
priority: Number(window.priority) || 1
|
|
268
|
-
}))
|
|
269
|
-
]).map((zone) => ({ from: zone.from, to: zone.to - 1, priority: zone.priority }));
|
|
280
|
+
// Two viewers a few seconds apart state stretches that overlap, and the plan
|
|
281
|
+
// puts one encoder on each stretch it is given — so unmerged windows buy an
|
|
282
|
+
// encoder per viewer for film they both want, which is the opposite of what
|
|
283
|
+
// sharing the output is for. Merged, the highest rank and the soonest time
|
|
284
|
+
// per number win and the stretches do not overlap, so one encoder serves
|
|
285
|
+
// everyone standing in front of it.
|
|
286
|
+
//
|
|
287
|
+
// Asked of the register, which is the thing that holds the windows. This
|
|
288
|
+
// used to reach into the layer that STATES them for the same arithmetic,
|
|
289
|
+
// which is the coupling the layer rule forbids; the arithmetic itself now
|
|
290
|
+
// lives where it belongs to nobody.
|
|
291
|
+
const windows = this.demand.mapOn(address);
|
|
270
292
|
const live = this.runsOn(address).filter((run) => run.isAlive);
|
|
271
293
|
const actions = planEncoders({
|
|
272
294
|
coverage,
|
|
@@ -277,15 +299,29 @@ export class EncodeOrchestrator {
|
|
|
277
299
|
runs: live,
|
|
278
300
|
maxRuns: this.#affordableOn(address, live),
|
|
279
301
|
segmentSeconds: this.segmentSeconds,
|
|
280
|
-
|
|
281
|
-
//
|
|
282
|
-
//
|
|
302
|
+
// What a start and a kill cost, measured from this host's own runs rather
|
|
303
|
+
// than written into the code from one machine's reading. Zero until
|
|
304
|
+
// something has been measured, which is the same convention as the
|
|
305
|
+
// refetch price below and is stated so the bias is known.
|
|
283
306
|
...this.#costs.seconds(),
|
|
284
307
|
// What a second of film costs to fetch again, in seconds of swarm time.
|
|
285
308
|
// Answered by whoever measures the film's own byte rate and the swarm's;
|
|
286
309
|
// zero until they have, which makes driving through look cheaper than it
|
|
287
310
|
// is and is stated here so the bias is known.
|
|
288
|
-
refetchSecPerFilmSecond: this.refetchSecPerFilmSecond(address)
|
|
311
|
+
refetchSecPerFilmSecond: this.refetchSecPerFilmSecond(address),
|
|
312
|
+
// How much slower one encoder runs beside others, read off this host's own
|
|
313
|
+
// startup measurement. A pure function over a measured table: beyond what
|
|
314
|
+
// was measured it holds the largest reading rather than continuing a curve
|
|
315
|
+
// nothing observed.
|
|
316
|
+
contentionPenaltyFor: (others) => contentionPenalty(others, this.contentionPenalties).penalty,
|
|
317
|
+
// The best figure this host has: what a run here is doing now, what one
|
|
318
|
+
// was last measured doing, or what the startup benchmark predicted. The
|
|
319
|
+
// first two are this output's own; the third exists before either.
|
|
320
|
+
speedX: Math.max(
|
|
321
|
+
live.reduce((best, run) => Math.max(best, run.speedX || 0), 0),
|
|
322
|
+
this.#lastSpeed.get(address) ?? 0,
|
|
323
|
+
this.startingSpeedFor(address) || 0
|
|
324
|
+
)
|
|
289
325
|
});
|
|
290
326
|
|
|
291
327
|
for (const action of actions) {
|
|
@@ -308,7 +344,22 @@ export class EncodeOrchestrator {
|
|
|
308
344
|
}
|
|
309
345
|
// A run that stays keeps its claim current: the free stretch ahead of it
|
|
310
346
|
// may have shrunk since it was given one.
|
|
311
|
-
|
|
347
|
+
//
|
|
348
|
+
// THE CLAIM IS THE STRETCH IT WAS GIVEN, and there is one rule for that
|
|
349
|
+
// everywhere. It used to be narrowed here to what the run had already
|
|
350
|
+
// MADE whenever the run had no end, which meant a run claimed the single
|
|
351
|
+
// number it was writing and nothing beyond. The plan then read the road
|
|
352
|
+
// in front of a working encoder as free and started more encoders on it:
|
|
353
|
+
// three processes writing one directory with the same names, field
|
|
354
|
+
// 2026-09-06, and a piece of the film lost for good when the first of
|
|
355
|
+
// them was cleaned up after.
|
|
356
|
+
//
|
|
357
|
+
// The worry that narrowing was written for is real and is answered where
|
|
358
|
+
// it belongs — a viewer opening the same film further in must not find
|
|
359
|
+
// every number taken. That is the plan's business, and the plan can take
|
|
360
|
+
// road away from a run that has no end, because such a run carries no
|
|
361
|
+
// `-to` and simply stops when its head meets somebody else's claim.
|
|
362
|
+
coverage.claim(action.run, action.from, endOfRun({ from: action.from, to: action.to }));
|
|
312
363
|
}
|
|
313
364
|
}
|
|
314
365
|
|
|
@@ -340,6 +391,14 @@ export class EncodeOrchestrator {
|
|
|
340
391
|
this.logger.warn(`encode: no encoder could be made for #${from}..#${to} of ${address}`);
|
|
341
392
|
return;
|
|
342
393
|
}
|
|
394
|
+
// What this machine has been measured to do on this output, carried over.
|
|
395
|
+
// A restart does not make the machine slower, and without this every moved
|
|
396
|
+
// encoder began as one whose speed nothing had measured — which the plan
|
|
397
|
+
// reads as "no arrival can be computed" and answers by comparing nothing.
|
|
398
|
+
const known = this.#lastSpeed.get(address) ?? 0;
|
|
399
|
+
if (known > 0) {
|
|
400
|
+
run.noteSpeed(known);
|
|
401
|
+
}
|
|
343
402
|
const onThisOutput = this.#runs.get(address) ?? [];
|
|
344
403
|
onThisOutput.push(run);
|
|
345
404
|
this.#runs.set(address, onThisOutput);
|
|
@@ -369,11 +428,24 @@ export class EncodeOrchestrator {
|
|
|
369
428
|
*/
|
|
370
429
|
#affordableOn(address, live) {
|
|
371
430
|
const byProcessor = Math.max(0, this.maxRunsFor(address));
|
|
372
|
-
|
|
431
|
+
// The best figure this host has: what a run here is doing now, what one was
|
|
432
|
+
// last measured doing, or what the startup benchmark predicted. The first
|
|
433
|
+
// two are this output's own; the third exists before either, so the budget
|
|
434
|
+
// is never asked to price encoders at a speed of zero.
|
|
435
|
+
const fastest = Math.max(
|
|
436
|
+
live.reduce((best, run) => Math.max(best, run.speedX || 0), 0),
|
|
437
|
+
this.#lastSpeed.get(address) ?? 0,
|
|
438
|
+
this.startingSpeedFor(address) || 0
|
|
439
|
+
);
|
|
373
440
|
const budget = affordableRuns({
|
|
374
441
|
byProcessor,
|
|
375
442
|
speedX: fastest,
|
|
376
|
-
refetchSecPerFilmSecond: this.refetchSecPerFilmSecond(address)
|
|
443
|
+
refetchSecPerFilmSecond: this.refetchSecPerFilmSecond(address),
|
|
444
|
+
// How much slower one encoder runs beside others, read off this host's own
|
|
445
|
+
// startup measurement. A pure function over a measured table: beyond what
|
|
446
|
+
// was measured it holds the largest reading rather than continuing a curve
|
|
447
|
+
// nothing observed.
|
|
448
|
+
contentionPenaltyFor: (others) => contentionPenalty(others, this.contentionPenalties).penalty
|
|
377
449
|
});
|
|
378
450
|
if (budget.runs !== byProcessor && budget.because !== this.#lastBudgetReason.get(address)) {
|
|
379
451
|
this.#lastBudgetReason.set(address, budget.because);
|
|
@@ -407,50 +479,7 @@ export class EncodeOrchestrator {
|
|
|
407
479
|
}
|
|
408
480
|
onThisOutput.push(run);
|
|
409
481
|
this.#runs.set(address, onThisOutput);
|
|
410
|
-
this
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
/**
|
|
414
|
-
* What a run holds, as far as the map is concerned.
|
|
415
|
-
*
|
|
416
|
-
* A run given an end holds exactly that stretch. A run given NO end — `to`
|
|
417
|
-
* below `from`, which is how this is written everywhere here — would hold the
|
|
418
|
-
* rest of the film, and that is what must not be claimed: a second viewer
|
|
419
|
-
* opening the same film further in would find every number taken and get no
|
|
420
|
-
* encoder at all, waiting instead for the first run to encode its way there,
|
|
421
|
-
* which on a long film is an hour.
|
|
422
|
-
*
|
|
423
|
-
* What bounds it in practice is the look-ahead: a run is suspended once it is
|
|
424
|
-
* that far in front of the segment its viewer last asked for, and past that it
|
|
425
|
-
* produces nothing until somebody asks. So that is the honest extent of the
|
|
426
|
-
* claim, and it is a measured figure rather than a chosen one — the same
|
|
427
|
-
* allowance the browser sizes its cushion from. `planRunInterval` has applied
|
|
428
|
-
* this rule since runs got intervals; this path did not, which is how an
|
|
429
|
-
* encoder came to be started and killed every five seconds in the field.
|
|
430
|
-
*
|
|
431
|
-
* @param {CoverageMap} coverage
|
|
432
|
-
* @param {object} run
|
|
433
|
-
* @param {number} from
|
|
434
|
-
* @param {number} to
|
|
435
|
-
*/
|
|
436
|
-
#claimFor(coverage, run, from, to) {
|
|
437
|
-
const end = endOfRun({ from, to });
|
|
438
|
-
if (Number.isFinite(end)) {
|
|
439
|
-
coverage.claim(run, from, end);
|
|
440
|
-
return;
|
|
441
|
-
}
|
|
442
|
-
// A RUN WITH NO END HOLDS WHAT IT HAS MADE, NOT WHAT IT MIGHT MAKE.
|
|
443
|
-
//
|
|
444
|
-
// "No end" means the film's length is not known, so there is no last number
|
|
445
|
-
// to claim towards. Claiming the rest of the film would leave a viewer who
|
|
446
|
-
// opens the same film further in with every number taken and no encoder at
|
|
447
|
-
// all. Claiming a fixed distance in front of the head — which is what this
|
|
448
|
-
// did — needs a number nobody measured, and the number it used was the
|
|
449
|
-
// suspended-encoder threshold that no longer exists.
|
|
450
|
-
//
|
|
451
|
-
// What it has made is a fact, and it is the only one available here.
|
|
452
|
-
const head = Number.isFinite(run?.head) ? run.head : from;
|
|
453
|
-
coverage.claim(run, from, Math.max(from, head));
|
|
482
|
+
this.coverageOf(address).claim(run, run.from, endOfRun(run));
|
|
454
483
|
}
|
|
455
484
|
|
|
456
485
|
/**
|
|
@@ -523,8 +552,8 @@ export class EncodeOrchestrator {
|
|
|
523
552
|
const parts = [];
|
|
524
553
|
for (const address of new Set([...this.demand.addresses(), ...this.#runs.keys()])) {
|
|
525
554
|
const coverage = this.coverageOf(address);
|
|
526
|
-
const stated = this.demand.
|
|
527
|
-
const windows = stated.map((
|
|
555
|
+
const stated = this.demand.mapOn(address);
|
|
556
|
+
const windows = stated.map((zone) => ({ from: zone.from, to: zone.to }));
|
|
528
557
|
const waiting = firstUnmetWant(coverage, windows);
|
|
529
558
|
const runs = this.runsOn(address)
|
|
530
559
|
.map((run) => `#${run.head}..#${run.to}@${run.speedX.toFixed(1)}x`)
|