@torrent-tv/proxy 2.91.0 → 2.92.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.
- package/bin/cli.js +3 -2
- package/package.json +18 -2
- package/routes/api/playback-plan/post.js +21 -4
- package/routes/api/sources/files/get.js +20 -28
- package/routes/api/sources/post.js +4 -2
- package/routes/api/sources/stats/get.js +4 -11
- package/routes/api/sources/viewer/post.js +16 -0
- package/routes/api/sources/warm/post.js +6 -215
- package/routes/api/subtitles/get.js +22 -125
- package/routes/api/subtitles/post.js +26 -0
- package/routes/api/transcode-sessions/post.js +24 -2
- package/routes/media/get.js +80 -0
- package/routes/stream/get.js +26 -94
- package/routes/transcode/audio-warm/get.js +9 -21
- package/routes/transcode/variant-warm/get.js +8 -14
- package/server.js +410 -46
- package/services/encode/AdmittedInput.js +136 -0
- package/services/encode/CushionReport.js +1 -76
- package/services/encode/EncodeInputs.js +142 -0
- package/services/encode/EncodeOrchestrator.js +4 -1
- package/services/encode/EncodeRun.js +13 -4
- package/services/encode/EncodeRuns.js +132 -52
- package/services/encode/InputFailures.js +37 -0
- package/services/encode/MatroskaInput.js +158 -0
- package/services/encode/OutputOpening.js +16 -64
- package/services/encode/Renditions.js +1 -17
- package/services/encode/admitted-command.js +60 -0
- package/services/encode/annex-b-input.js +37 -0
- package/services/encode/bit-payload.js +29 -0
- package/services/encode/output/OutputCatalog.js +1 -1
- package/services/encode/output/Timeline.js +11 -0
- package/services/encode/pcm-normalization.js +30 -0
- package/services/encode/piece-completeness.js +54 -1
- package/services/encode/quality/HostLoad.js +19 -73
- package/services/encode/quality/QualityOffer.js +1 -1
- package/services/encode/run-command.js +1 -589
- package/services/encode/segment-formats/mp4-boxes.js +3 -1
- package/services/encode/segment-formats/presentation-segment.js +114 -0
- package/services/media/ContainerOrchestrator.js +118 -48
- package/services/media/KeyframeTables.js +16 -115
- package/services/media/MediaReadRequests.js +120 -0
- package/services/media/ProbeRequests.js +135 -0
- package/services/media/SegmentInputs.js +48 -0
- package/services/media/SourceFile.js +6 -0
- package/services/media/SubtitleOrchestrator.js +190 -4
- package/services/media/await-plan.js +47 -0
- package/services/media/container/AsfContainer.js +217 -0
- package/services/media/container/AviContainer.js +343 -34
- package/services/media/container/CadencedPictureOrder.js +45 -0
- package/services/media/container/Container.js +7 -3
- package/services/media/container/ContainerFactory.js +10 -3
- package/services/media/container/FfprobeContainer.js +222 -0
- package/services/media/container/MatroskaContainer.js +72 -46
- package/services/media/container/Mp4Container.js +472 -139
- package/services/media/container/MpegPsContainer.js +281 -0
- package/services/media/container/MpegTsContainer.js +277 -0
- package/services/media/container/PacketIndex.js +270 -0
- package/services/media/container/PacketRecords.js +237 -0
- package/services/media/container/RetainedBytes.js +39 -0
- package/services/media/container/RetainedReads.js +51 -0
- package/services/media/container/ac3-frame.js +46 -0
- package/services/media/container/asf-packets.js +153 -0
- package/services/media/container/avc-picture-order.js +50 -0
- package/services/media/container/avi-open-dml.js +47 -0
- package/services/media/container/dts-frame.js +51 -0
- package/services/media/container/ebml-stream.js +12 -1
- package/services/media/container/ffprobe-record.js +45 -0
- package/services/media/container/flv-packet-ranges.js +27 -0
- package/services/media/container/following-packet-time.js +37 -0
- package/services/media/container/h264-configuration.js +183 -0
- package/services/media/container/h264-elementary-index.js +245 -0
- package/services/media/container/hevc-configuration.js +165 -0
- package/services/media/container/hevc-elementary-index.js +132 -0
- package/services/media/container/hevc-picture-order.js +66 -0
- package/services/media/container/latm-frame.js +99 -0
- package/services/media/container/matroska-packets.js +207 -0
- package/services/media/container/memory-unavailable.js +8 -0
- package/services/media/container/mp3-packet-dependencies.js +41 -0
- package/services/media/container/mp4-fragments.js +148 -0
- package/services/media/container/mpeg-audio-frame.js +19 -0
- package/services/media/container/mpeg-audio-reservoir.js +27 -0
- package/services/media/container/mpeg-elementary-index.js +196 -0
- package/services/media/container/mpeg-sequence-reader.js +69 -0
- package/services/media/container/mpeg-ts-packets.js +93 -0
- package/services/media/container/mpeg-video-configuration.js +50 -0
- package/services/media/container/mpeg4-picture-timing.js +108 -0
- package/services/media/container/pes-header.js +34 -0
- package/services/media/playback-declarations.js +57 -0
- package/services/media/playback-planner.js +72 -230
- package/services/media/probe-packets.js +61 -0
- package/services/media/read-playback-declarations.js +18 -0
- package/services/media/tracks/AudioTrack.js +9 -0
- package/services/media/tracks/TextSubtitleTrack.js +4 -1
- package/services/server/OutputLifecycle.js +2 -0
- package/services/server/SegmentServing.js +40 -33
- package/services/server/ViewerRequests.js +22 -41
- package/services/server/controllers/PlaybackController.js +4 -0
- package/services/server/controllers/SubtitleController.js +8 -29
- package/services/server/transcode-session-files.js +58 -99
- package/services/server/wire-outputs.js +58 -15
- package/services/storage/IndexMemory.js +85 -0
- package/services/storage/MachineBudget.js +27 -3
- package/services/storage/allowance.js +8 -1
- package/services/storage/piece-store/piece-disk-store.js +18 -3
- package/services/storage/piece-store/piece-lru.js +8 -4
- package/services/storage/piece-store/shared-piece-store.js +26 -11
- package/services/storage/segment-store/SegmentStore.js +49 -18
- package/services/storage/wire.js +7 -0
- package/services/torrent/Contents.js +52 -20
- package/services/torrent/demand/DemandRegister.js +13 -4
- package/services/torrent/demand/Window.js +13 -2
- package/services/torrent/demand-progress.js +25 -0
- package/services/torrent/download/DeadlineScheduler.js +167 -0
- package/services/torrent/download/FutureDownload.js +166 -0
- package/services/torrent/download/SwarmSelection.js +30 -59
- package/services/torrent/download/registry.js +45 -5
- package/services/torrent/download/withdraw-claim.js +2 -0
- package/services/torrent/torrent-pool.js +39 -600
- package/services/torrent/worker/channel.js +8 -1
- package/services/torrent/worker/client.js +74 -72
- package/services/torrent/worker/files-in-use.js +9 -0
- package/services/torrent/worker/handle-state.js +12 -0
- package/services/torrent/worker/held-bytes.js +82 -99
- package/services/torrent/worker/held-file-bytes.js +17 -0
- package/services/torrent/worker/piece-reader.js +67 -683
- package/services/torrent/worker/pool-adapter.js +37 -123
- package/services/torrent/worker/protocol.js +6 -17
- package/services/torrent/worker/whole-file-stats.js +2 -0
- package/services/torrent/worker/worker.js +86 -156
- package/services/transport/data-channel-handler.js +56 -13
- package/services/viewer/DownloadMaps.js +168 -0
- package/services/viewer/NativeSourceMap.js +33 -0
- package/services/viewer/PriorityMap.js +49 -40
- package/services/viewer/PriorityOrchestrator.js +42 -13
- package/services/viewer/SourcePreparation.js +278 -0
- package/services/viewer/Viewer.js +11 -0
- package/services/viewer/Viewers.js +109 -2
- package/services/viewer/playback-readiness.js +50 -63
- package/services/viewer/segment-demands.js +37 -0
- package/services/viewer/urgent-output-ready.js +26 -0
- package/services/encode/run-inputs.js +0 -53
- package/services/media/keyframe-probe.js +0 -157
- package/services/media/media-info-probe.js +0 -93
- package/services/torrent/worker/background-fill.js +0 -213
- package/services/torrent/worker/resume-warm.js +0 -107
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { matroskaInput, matroskaTrackIdentity } from "./MatroskaInput.js";
|
|
3
|
+
import { pcmNormalization, normalizePcmSlices } from "./pcm-normalization.js";
|
|
4
|
+
|
|
5
|
+
/** Acquire complete source ranges before any process can consume the input. */
|
|
6
|
+
export async function admitInput({ sources, reserve, readRanges }) {
|
|
7
|
+
if (!Array.isArray(sources) || sources.length === 0 || typeof reserve !== "function" || typeof readRanges !== "function") {
|
|
8
|
+
throw new TypeError("Admission requires resolved source inputs, memory reservation and an available-only reader.");
|
|
9
|
+
}
|
|
10
|
+
const bytes = sources.reduce((total, source) => total + source.input.ranges.reduce((sum, [start, end]) => sum + end - start + 1, 0) +
|
|
11
|
+
source.input.tracks.reduce((sum, input) => sum + (pcmNormalization(input.track)
|
|
12
|
+
? input.packets.reduce((size, packet) => size + packet.ranges.reduce((n, [start, end]) => n + end - start + 1, 0), 0) : 0), 0), 0);
|
|
13
|
+
if (!Number.isSafeInteger(bytes) || bytes <= 0) throw new TypeError("Input byte count must be a positive safe integer.");
|
|
14
|
+
const release = await reserve(bytes);
|
|
15
|
+
if (typeof release !== "function") return release?.kind === "terminal" ? release : { kind: "needs-memory", bytes };
|
|
16
|
+
let retained = false;
|
|
17
|
+
try {
|
|
18
|
+
const hash = createHash("sha256");
|
|
19
|
+
const tracks = [];
|
|
20
|
+
const held = new Map();
|
|
21
|
+
const normalized = new Map();
|
|
22
|
+
for (const source of sources) {
|
|
23
|
+
const buffers = await readRanges(source, source.input.ranges, bytes);
|
|
24
|
+
if (buffers === null) return { kind: "needs-bytes", sourceKey: source.sourceKey, fileIndex: source.fileIndex, ranges: source.input.ranges };
|
|
25
|
+
if (!Array.isArray(buffers) || buffers.length !== source.input.ranges.length || buffers.some((buffer, index) =>
|
|
26
|
+
!Buffer.isBuffer(buffer) || buffer.length !== source.input.ranges[index][1] - source.input.ranges[index][0] + 1)) {
|
|
27
|
+
throw new Error("Available-only input reader returned incomplete ranges.");
|
|
28
|
+
}
|
|
29
|
+
hash.update(JSON.stringify(source.input.ranges));
|
|
30
|
+
for (const buffer of buffers) {
|
|
31
|
+
hash.update(String(buffer.length));
|
|
32
|
+
hash.update(":");
|
|
33
|
+
hash.update(buffer);
|
|
34
|
+
}
|
|
35
|
+
for (const input of source.input.tracks) {
|
|
36
|
+
const shifted = { ...input,
|
|
37
|
+
...(Number.isFinite(input.sourceEndSeconds) ? { sourceEndSeconds: input.sourceEndSeconds - (source.timeShiftSeconds ?? 0) } : {}),
|
|
38
|
+
packets: input.packets.map(packet => ({ ...packet,
|
|
39
|
+
pts: packet.pts - (source.timeShiftSeconds ?? 0),
|
|
40
|
+
...(packet.dts === undefined ? {} : { dts: packet.dts - (source.timeShiftSeconds ?? 0) }) })) };
|
|
41
|
+
tracks.push(shifted);
|
|
42
|
+
hash.update(matroskaTrackIdentity(input.track));
|
|
43
|
+
hash.update(JSON.stringify(shifted.packets.map(packet => [packet.pts, packet.dts ?? null,
|
|
44
|
+
packet.duration, packet.keyframe === true, packet.ranges, packet.discardPaddingSeconds ?? 0,
|
|
45
|
+
packet.bitOffset ?? null, packet.bitLength ?? null])));
|
|
46
|
+
held.set(shifted, { ranges: source.input.ranges, buffers });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
let released = false;
|
|
50
|
+
const originSeconds = tracks.reduce((origin, input) => input.packets.reduce((value, packet) => Math.min(value, packet.pts), origin), Infinity);
|
|
51
|
+
if (!Number.isFinite(originSeconds)) throw new Error("Admitted input contains no packets.");
|
|
52
|
+
const packetSlices = (input, packet) => {
|
|
53
|
+
if (released) throw new Error("Admitted input has been released.");
|
|
54
|
+
const source = held.get(input);
|
|
55
|
+
if (!source) throw new Error("Packet does not belong to this admitted input.");
|
|
56
|
+
return packet.ranges.map(([start, end]) => {
|
|
57
|
+
const position = source.ranges.findIndex(([from, to]) => from <= start && end <= to);
|
|
58
|
+
if (position < 0) throw new Error("Packet is outside its admitted ranges.");
|
|
59
|
+
return source.buffers[position].subarray(start - source.ranges[position][0], end - source.ranges[position][0] + 1);
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
const readPacket = (input, packet) => {
|
|
63
|
+
const slices = packetSlices(input, packet);
|
|
64
|
+
return slices.length === 1 ? slices[0] : Buffer.concat(slices);
|
|
65
|
+
};
|
|
66
|
+
for (const input of tracks) {
|
|
67
|
+
for (const packet of input.packets) {
|
|
68
|
+
const slices = packetSlices(input, packet);
|
|
69
|
+
if (packet.expectedHash !== undefined) {
|
|
70
|
+
const payloadHash = createHash("sha256");
|
|
71
|
+
for (const slice of slices) payloadHash.update(slice);
|
|
72
|
+
if (payloadHash.digest("hex") !== packet.expectedHash) {
|
|
73
|
+
return { kind: "terminal", reason: "packet-address-does-not-match-payload", trackId: input.track.trackNumber,
|
|
74
|
+
ranges: packet.ranges };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const format = pcmNormalization(input.track);
|
|
78
|
+
if (format) normalized.set(packet, normalizePcmSlices(slices, format));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
retained = true;
|
|
82
|
+
return {
|
|
83
|
+
kind: "result", bytes, fingerprint: hash.digest("hex"), originSeconds, tracks, readPacket,
|
|
84
|
+
stream: options => matroskaInput({ tracks,
|
|
85
|
+
readPacket: (input, packet) => normalized.has(packet) ? [normalized.get(packet)] : packetSlices(input, packet),
|
|
86
|
+
originSeconds, ...options }),
|
|
87
|
+
release: () => {
|
|
88
|
+
if (released) return;
|
|
89
|
+
released = true;
|
|
90
|
+
held.clear();
|
|
91
|
+
normalized.clear();
|
|
92
|
+
release();
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
} finally {
|
|
96
|
+
if (!retained) release();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Respect stdin backpressure and release input on every completion path. */
|
|
101
|
+
export async function writeAdmittedInput(input, stdin, { next = null } = {}) {
|
|
102
|
+
if (input?.kind !== "result") throw new TypeError("Only complete input may be written to an encoder.");
|
|
103
|
+
const originSeconds = input.originSeconds;
|
|
104
|
+
const declarations = JSON.stringify(input.tracks.map(input => matroskaTrackIdentity(input.track)));
|
|
105
|
+
const seen = new Map();
|
|
106
|
+
let current = input, includeHeader = true;
|
|
107
|
+
try {
|
|
108
|
+
while (current) {
|
|
109
|
+
if (JSON.stringify(current.tracks.map(input => matroskaTrackIdentity(input.track))) !== declarations) {
|
|
110
|
+
throw new Error("Consecutive encoder input changed its declared tracks.");
|
|
111
|
+
}
|
|
112
|
+
for (let index = 0; index < current.tracks.length; index++) {
|
|
113
|
+
const end = current.tracks[index].sourceEndSeconds;
|
|
114
|
+
if (Number.isFinite(end)) input.tracks[index].sourceEndSeconds = end;
|
|
115
|
+
}
|
|
116
|
+
for await (const chunk of current.stream({ originSeconds, includeHeader, seen })) {
|
|
117
|
+
await new Promise((resolve, reject) => {
|
|
118
|
+
const failed = error => { stdin.off("error", failed); reject(error); };
|
|
119
|
+
stdin.once("error", failed);
|
|
120
|
+
stdin.write(chunk, error => {
|
|
121
|
+
stdin.off("error", failed);
|
|
122
|
+
if (error) reject(error);
|
|
123
|
+
else resolve();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
current.release();
|
|
128
|
+
current = next ? await next() : null;
|
|
129
|
+
includeHeader = false;
|
|
130
|
+
}
|
|
131
|
+
stdin.end();
|
|
132
|
+
} finally {
|
|
133
|
+
current?.release();
|
|
134
|
+
input.release();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -42,13 +42,11 @@ export class CushionReport {
|
|
|
42
42
|
/** What this reads and asks of the rest of the proxy, and nothing else. @type {object} */
|
|
43
43
|
#host;
|
|
44
44
|
|
|
45
|
-
/** Files whose spare soundtracks have been fetched whole, once each. @type {Set<string>} */
|
|
46
|
-
#spareSoundtracksFetched = new Set();
|
|
47
45
|
#lastSaidAt = new WeakMap();
|
|
48
46
|
#lookAheadDisagreementSince = new WeakMap();
|
|
49
47
|
|
|
50
48
|
/**
|
|
51
|
-
* @param {object} host - `viewerSecondsOn`, `viewersOf`, `SourceFiles`, `logger`, `producedNumbers`, `encodeRuns`, `
|
|
49
|
+
* @param {object} host - `viewerSecondsOn`, `viewersOf`, `SourceFiles`, `logger`, `producedNumbers`, `encodeRuns`, `hostLoad`, `lookaheadSeconds`, `outputTimes`, `outputs`
|
|
52
50
|
*/
|
|
53
51
|
constructor(host) {
|
|
54
52
|
this.#host = host;
|
|
@@ -201,79 +199,6 @@ export class CushionReport {
|
|
|
201
199
|
`${viewers} viewer(s) holding up to ` +
|
|
202
200
|
`${deepestBuffer === null ? "?" : deepestBuffer.toFixed(1)}s`
|
|
203
201
|
);
|
|
204
|
-
this.#fetchSpareSoundtracks(session, aheadOfPicture);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Fetch the soundtracks that ship beside this picture, whole, while the swarm
|
|
209
|
-
* has capacity to spare.
|
|
210
|
-
*
|
|
211
|
-
* WHY IT WAITS FOR THE CUSHION. A soundtrack nobody has chosen is worth having
|
|
212
|
-
* on disk — it is a twentieth of the picture (30 MB against 566 MB on the
|
|
213
|
-
* field torrent) and having it makes every later switch instant instead of
|
|
214
|
-
* paying for its first pieces. But fetching it takes swarm capacity from the
|
|
215
|
-
* picture, and there is exactly one moment when that capacity is demonstrably
|
|
216
|
-
* spare: when the encoder is already as far ahead of the viewer as it is
|
|
217
|
-
* allowed to get. That is not a guess about the swarm — it is the measurement
|
|
218
|
-
* the line above just printed.
|
|
219
|
-
*
|
|
220
|
-
* WHY IT IS A READ AND NOT A SELECTION. `file.select()` claims every piece of
|
|
221
|
-
* a file at once, and `#syncSelections` in `torrent/torrent-pool.js` records what that
|
|
222
|
-
* cost when it was done alongside the readers' own windows: a claim covering
|
|
223
|
-
* everything always outranked the window, and a seek to 89.1% of a 4.7 GB film
|
|
224
|
-
* waited 93 s while the swarm fetched 2.47 GB in file order. So this goes
|
|
225
|
-
* through the same bounded read the edge warm-up uses, which claims a moving
|
|
226
|
-
* window like any other reader and gives it back when it ends.
|
|
227
|
-
*
|
|
228
|
-
* Once per file, and only for a soundtrack in a file of its own — the
|
|
229
|
-
* picture's own tracks are already in the bytes being played.
|
|
230
|
-
*
|
|
231
|
-
* @param {HlsSession} session
|
|
232
|
-
* @param {number} aheadOfPicture - Seconds of film ready ahead of the viewer.
|
|
233
|
-
* @returns {void}
|
|
234
|
-
*/
|
|
235
|
-
#fetchSpareSoundtracks(session, aheadOfPicture) {
|
|
236
|
-
if (typeof this.#host.fetchWholeFile !== "function") {
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
// The encoder is held at this distance and no further, so reaching it is the
|
|
240
|
-
// signal that nothing more is being asked of the swarm on the picture's
|
|
241
|
-
// behalf.
|
|
242
|
-
if (!(aheadOfPicture >= this.#host.lookaheadSeconds)) {
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
const inventory = this.#host.getCachedAudioTracks?.({
|
|
246
|
-
sourceKey: session.file.sourceKey,
|
|
247
|
-
fileIndex: session.file.fileIndex
|
|
248
|
-
}) ?? [];
|
|
249
|
-
const wanted = new Set(
|
|
250
|
-
inventory
|
|
251
|
-
.filter((entry) => entry?.kind === "sidecar" && Number.isInteger(entry.fileIndex))
|
|
252
|
-
.map((entry) => entry.fileIndex)
|
|
253
|
-
);
|
|
254
|
-
for (const fileIndex of wanted) {
|
|
255
|
-
const key = this.#host.SourceFiles.keyFor(session.file.sourceKey, fileIndex);
|
|
256
|
-
if (this.#spareSoundtracksFetched.has(key)) {
|
|
257
|
-
continue;
|
|
258
|
-
}
|
|
259
|
-
this.#spareSoundtracksFetched.add(key);
|
|
260
|
-
this.#host.logger.info(
|
|
261
|
-
`transcode ${session.id.slice(0, 8)} the picture is ${Math.round(aheadOfPicture)}s ahead of ` +
|
|
262
|
-
`the viewer, so file ${fileIndex} — a soundtrack beside it — is fetched whole now; ` +
|
|
263
|
-
"a switch to it will not wait for the swarm"
|
|
264
|
-
);
|
|
265
|
-
// Not awaited: nothing depends on it finishing, and a failure costs only
|
|
266
|
-
// that the switch pays for its own pieces, as it did before this existed.
|
|
267
|
-
Promise.resolve(this.#host.fetchWholeFile({ sourceKey: session.file.sourceKey, fileIndex })).catch(
|
|
268
|
-
(error) => {
|
|
269
|
-
this.#host.logger.info(
|
|
270
|
-
`transcode: fetching soundtrack file ${fileIndex} whole failed ` +
|
|
271
|
-
`(${error instanceof Error ? error.message : String(error)}) — ` +
|
|
272
|
-
"it will be read when it is played"
|
|
273
|
-
);
|
|
274
|
-
}
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
202
|
}
|
|
278
203
|
|
|
279
204
|
/**
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { admitInput } from "./AdmittedInput.js";
|
|
2
|
+
|
|
3
|
+
/** Event-driven preparation; a synchronous run request can take only ready input. */
|
|
4
|
+
export class EncodeInputs {
|
|
5
|
+
#requests = new Map();
|
|
6
|
+
#revision = 0;
|
|
7
|
+
#held = 0;
|
|
8
|
+
#allowed = 0;
|
|
9
|
+
#wanted = new Map();
|
|
10
|
+
#resolve;
|
|
11
|
+
#read;
|
|
12
|
+
#revise;
|
|
13
|
+
#changed;
|
|
14
|
+
#failed;
|
|
15
|
+
#capacity;
|
|
16
|
+
#urgent;
|
|
17
|
+
|
|
18
|
+
constructor({ resolve, readRanges, reviseBudget, changed, failed, capacity = () => null, urgent = () => true }) {
|
|
19
|
+
this.#resolve = resolve;
|
|
20
|
+
this.#read = readRanges;
|
|
21
|
+
this.#revise = reviseBudget;
|
|
22
|
+
this.#changed = changed;
|
|
23
|
+
this.#failed = failed;
|
|
24
|
+
this.#capacity = capacity;
|
|
25
|
+
this.#urgent = urgent;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
held() { return this.#held; }
|
|
29
|
+
wanted() { return this.#held + [...this.#wanted.values()].reduce((sum, bytes) => sum + bytes, 0); }
|
|
30
|
+
allow(bytes) {
|
|
31
|
+
const changed = this.#allowed !== Math.max(0, bytes);
|
|
32
|
+
this.#allowed = Math.max(0, bytes);
|
|
33
|
+
if (changed) this.#retryMemory();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#retryMemory() {
|
|
37
|
+
for (const request of this.#requests.values()) {
|
|
38
|
+
if (!request.pending && request.result?.kind === "needs-memory") this.#prepare(request);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
failureOf(output) {
|
|
43
|
+
for (const request of this.#requests.values()) {
|
|
44
|
+
if (request.output === output && request.result?.kind === "terminal") return request.result;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
retain(output, windows) {
|
|
50
|
+
for (const [key, request] of this.#requests) {
|
|
51
|
+
if (request.output !== output || windows.some(window => request.from <= window.to && request.to >= window.from)) continue;
|
|
52
|
+
this.#requests.delete(key);
|
|
53
|
+
this.#wanted.delete(key);
|
|
54
|
+
request.result?.release?.();
|
|
55
|
+
}
|
|
56
|
+
this.#retryMemory();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
take(output, from, to) {
|
|
60
|
+
const key = `${output.outputKey ?? output.id}:${from}:${to}`;
|
|
61
|
+
const existing = this.#requests.get(key);
|
|
62
|
+
if (existing?.result?.kind === "result") {
|
|
63
|
+
this.#requests.delete(key);
|
|
64
|
+
return existing.result;
|
|
65
|
+
}
|
|
66
|
+
if (existing?.pending || existing?.revision === this.#revision || existing?.result?.kind === "terminal") return null;
|
|
67
|
+
const request = existing ?? { output, from, to, key };
|
|
68
|
+
this.#requests.set(key, request);
|
|
69
|
+
this.#prepare(request);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async acquire(output, from, to) {
|
|
74
|
+
const key = `${output.outputKey ?? output.id}:${from}:${to}`;
|
|
75
|
+
const ready = this.take(output, from, to);
|
|
76
|
+
if (ready) return ready;
|
|
77
|
+
await this.#requests.get(key)?.promise;
|
|
78
|
+
const request = this.#requests.get(key);
|
|
79
|
+
if (request?.result?.kind !== "result") return null;
|
|
80
|
+
return this.take(output, from, to);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
bytesChanged() {
|
|
84
|
+
this.#revision++;
|
|
85
|
+
for (const request of this.#requests.values()) {
|
|
86
|
+
if (!request.pending && request.result?.kind !== "result" && request.result?.kind !== "terminal") this.#prepare(request);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
forget(output) {
|
|
91
|
+
for (const [key, request] of this.#requests) {
|
|
92
|
+
if (request.output !== output) continue;
|
|
93
|
+
this.#requests.delete(key);
|
|
94
|
+
this.#wanted.delete(key);
|
|
95
|
+
request.result?.release?.();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
#prepare(request) {
|
|
100
|
+
request.pending = true;
|
|
101
|
+
request.revision = this.#revision;
|
|
102
|
+
request.promise = (async () => {
|
|
103
|
+
const resolved = await this.#resolve(request.output, request.from, request.to);
|
|
104
|
+
if (this.#requests.get(request.key) !== request) return;
|
|
105
|
+
const result = resolved.kind === "result" ? await admitInput({
|
|
106
|
+
sources: resolved.sources,
|
|
107
|
+
readRanges: this.#read,
|
|
108
|
+
reserve: async bytes => {
|
|
109
|
+
this.#wanted.set(request.key, bytes);
|
|
110
|
+
await this.#revise();
|
|
111
|
+
const capacity = this.#capacity();
|
|
112
|
+
if (capacity !== null && bytes > capacity && this.#urgent(request.output, request.from)) return {
|
|
113
|
+
kind: "terminal", reason: "source-input-exceeds-memory-capacity", bytes, capacity
|
|
114
|
+
};
|
|
115
|
+
if (this.#requests.get(request.key) !== request || this.#allowed - this.#held < bytes) return null;
|
|
116
|
+
this.#wanted.delete(request.key);
|
|
117
|
+
this.#held += bytes;
|
|
118
|
+
let released = false;
|
|
119
|
+
return () => {
|
|
120
|
+
if (!released) {
|
|
121
|
+
released = true; this.#held -= bytes;
|
|
122
|
+
queueMicrotask(() => this.#retryMemory());
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}) : resolved;
|
|
127
|
+
if (this.#requests.get(request.key) !== request) { result.release?.(); return; }
|
|
128
|
+
request.result = result;
|
|
129
|
+
if (result.kind === "result" || result.kind === "terminal") this.#changed(request.output, result);
|
|
130
|
+
})().catch(error => {
|
|
131
|
+
if (this.#requests.get(request.key) === request) {
|
|
132
|
+
request.result = { kind: "terminal", reason: "input-preparation-failed", message: error.message };
|
|
133
|
+
this.#failed(request.output, error);
|
|
134
|
+
}
|
|
135
|
+
}).finally(() => {
|
|
136
|
+
request.pending = false;
|
|
137
|
+
if (request.result?.kind !== "needs-memory") this.#wanted.delete(request.key);
|
|
138
|
+
if (this.#requests.get(request.key) === request && request.revision !== this.#revision &&
|
|
139
|
+
request.result?.kind !== "result" && request.result?.kind !== "terminal") this.#prepare(request);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -167,6 +167,7 @@ export class EncodeOrchestrator {
|
|
|
167
167
|
startingSpeedFor = () => 0,
|
|
168
168
|
segmentCoverage = null,
|
|
169
169
|
planSoon = null,
|
|
170
|
+
inputDemandChanged = null,
|
|
170
171
|
describeWaits = null,
|
|
171
172
|
admission = null,
|
|
172
173
|
logger,
|
|
@@ -202,6 +203,7 @@ export class EncodeOrchestrator {
|
|
|
202
203
|
// Asked for, not commanded: this class decides, and something else owns the
|
|
203
204
|
// loop that calls it. Absent in a test, where the clock is the test's own.
|
|
204
205
|
this.planSoon = typeof planSoon === "function" ? planSoon : () => undefined;
|
|
206
|
+
this.inputDemandChanged = typeof inputDemandChanged === "function" ? inputDemandChanged : () => undefined;
|
|
205
207
|
}
|
|
206
208
|
|
|
207
209
|
/**
|
|
@@ -536,6 +538,7 @@ export class EncodeOrchestrator {
|
|
|
536
538
|
*/
|
|
537
539
|
notePriorityMap(address, zones) {
|
|
538
540
|
this.#demand.state(address, zones);
|
|
541
|
+
this.inputDemandChanged(address, this.#demand.mapOn(address));
|
|
539
542
|
// NOBODY IS COMING HERE, so what was remembered about this output's input
|
|
540
543
|
// is about nobody. Kept, those three entries would stay for the life of the
|
|
541
544
|
// process, and the wait they describe would greet whoever opens this output
|
|
@@ -994,7 +997,7 @@ export class EncodeOrchestrator {
|
|
|
994
997
|
// is a name match: no stretch to search and no bytes to judge. Done for
|
|
995
998
|
// every ending, the normal one included, since a run that finished cleanly
|
|
996
999
|
// has nothing under a working name and the sweep then removes nothing.
|
|
997
|
-
this.#segmentCoverage?.clearUpAfter(ended.address, ended.from);
|
|
1000
|
+
this.#segmentCoverage?.clearUpAfter(ended.address, ended.run?.makingTag ?? ended.from);
|
|
998
1001
|
this.coverageOf(ended.address).release(ended.run);
|
|
999
1002
|
const remaining = this.runsOn(ended.address).filter((run) => run !== ended.run);
|
|
1000
1003
|
if (remaining.length === 0) {
|
|
@@ -199,7 +199,7 @@ export class EncodeRun {
|
|
|
199
199
|
* @param {(reading: { speed: number, at: number }) => void} [params.onSpeedMeasured] -
|
|
200
200
|
* Told each time this run has measured its processing speed, once per
|
|
201
201
|
* reading.
|
|
202
|
-
* @param {(name: string) => string | null} [params.onClosed] - Called with the
|
|
202
|
+
* @param {(name: string, following: string | null) => string | null} [params.onClosed] - Called with the
|
|
203
203
|
* WORKING name of every piece the encoder has finished writing, as the
|
|
204
204
|
* encoder itself names it on its own channel, and answers with the name that
|
|
205
205
|
* piece is served under — because making it servable is a rename, and only
|
|
@@ -230,6 +230,7 @@ export class EncodeRun {
|
|
|
230
230
|
encoder,
|
|
231
231
|
from,
|
|
232
232
|
to,
|
|
233
|
+
makingTag = String(from),
|
|
233
234
|
buildArgs,
|
|
234
235
|
spawn,
|
|
235
236
|
logger,
|
|
@@ -251,6 +252,7 @@ export class EncodeRun {
|
|
|
251
252
|
this.encoder = encoder;
|
|
252
253
|
this.from = from;
|
|
253
254
|
this.to = to;
|
|
255
|
+
this.makingTag = makingTag;
|
|
254
256
|
this.buildArgs = buildArgs;
|
|
255
257
|
this.spawnProcess = spawn;
|
|
256
258
|
this.logger = logger;
|
|
@@ -452,6 +454,12 @@ export class EncodeRun {
|
|
|
452
454
|
* @param {import("node:child_process").ChildProcess} process
|
|
453
455
|
*/
|
|
454
456
|
#wire(process) {
|
|
457
|
+
// The process owns this listener even between admitted input writes.
|
|
458
|
+
process.stdin?.on("error", (error) => {
|
|
459
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
460
|
+
this.lastError ||= message;
|
|
461
|
+
this.logger.warn(`ffmpeg input #${this.from}..#${this.to} of ${this.address}: ${message}`);
|
|
462
|
+
});
|
|
455
463
|
process.stdout?.on("data", (chunk) => this.#readProgress(String(chunk)));
|
|
456
464
|
// THE CHANNEL THE ENCODER NAMES ITS FINISHED PIECES ON.
|
|
457
465
|
//
|
|
@@ -567,14 +575,14 @@ export class EncodeRun {
|
|
|
567
575
|
// A subsequent closed file proves the previous one reached a cut.
|
|
568
576
|
// The last file can instead have been flushed by an input failure.
|
|
569
577
|
if (this.#pendingClosed !== null) {
|
|
570
|
-
this.#publishClosed(this.#pendingClosed);
|
|
578
|
+
this.#publishClosed(this.#pendingClosed, name);
|
|
571
579
|
}
|
|
572
580
|
if (this.#publicationError) break;
|
|
573
581
|
this.#pendingClosed = name;
|
|
574
582
|
}
|
|
575
583
|
}
|
|
576
584
|
|
|
577
|
-
#publishClosed(name) {
|
|
585
|
+
#publishClosed(name, following = null) {
|
|
578
586
|
this.#measureSpeed();
|
|
579
587
|
// ITS SERVED NAME, which is what whoever owns the disk gives it in answer.
|
|
580
588
|
// ffmpeg writes a piece under a working name and reports that; the piece
|
|
@@ -582,7 +590,7 @@ export class EncodeRun {
|
|
|
582
590
|
// name a request can actually ask for.
|
|
583
591
|
let served;
|
|
584
592
|
try {
|
|
585
|
-
served = this.onClosed(name, this.#closedTimings.get(name) ?? null);
|
|
593
|
+
served = this.onClosed(name, following, this.#closedTimings.get(name) ?? null);
|
|
586
594
|
} catch (error) {
|
|
587
595
|
this.#publicationError = error instanceof Error ? error.message : String(error);
|
|
588
596
|
this.lastError = this.#publicationError;
|
|
@@ -594,6 +602,7 @@ export class EncodeRun {
|
|
|
594
602
|
} finally {
|
|
595
603
|
this.#closedTimings.delete(name);
|
|
596
604
|
}
|
|
605
|
+
|
|
597
606
|
if (!served) return false;
|
|
598
607
|
if (!this.#stopping) {
|
|
599
608
|
this.#provenName = served;
|