@torrent-tv/proxy 2.97.20 → 2.97.22
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/package.json +1 -1
- package/services/encode/EncodeRuns.js +15 -0
- package/services/encode/source-command.js +9 -1
- package/services/media/container/AviContainer.js +24 -8
- package/services/media/container/MatroskaContainer.js +39 -1
- package/services/media/container/Mp4Container.js +10 -0
- package/services/media/container/matroska-packets.js +6 -6
- package/services/media/container/mpeg-elementary-index.js +15 -0
- package/services/server/wire-outputs.js +17 -1
- package/services/storage/wire.js +25 -0
package/package.json
CHANGED
|
@@ -281,6 +281,21 @@ export class EncodeRuns {
|
|
|
281
281
|
return this.#runsByInputToken.get(runToken)?.originalInput ?? null;
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
+
/**
|
|
285
|
+
* What FFmpeg's caches may hold for the live runs that read an original
|
|
286
|
+
* file: at most what each was admitted, since a cache keeps what its run
|
|
287
|
+
* read of it (`source-command.js`).
|
|
288
|
+
*
|
|
289
|
+
* @returns {number}
|
|
290
|
+
*/
|
|
291
|
+
inputCacheBytes() {
|
|
292
|
+
let bytes = 0;
|
|
293
|
+
for (const run of this.#runsByInputToken.values()) {
|
|
294
|
+
if (run.isAlive && run.originalInput) bytes += Math.max(0, Number(run.originalInput.bytes) || 0);
|
|
295
|
+
}
|
|
296
|
+
return bytes;
|
|
297
|
+
}
|
|
298
|
+
|
|
284
299
|
/**
|
|
285
300
|
* @param {object} output
|
|
286
301
|
* @returns {number}
|
|
@@ -270,7 +270,15 @@ export function buildOriginalCommand(params) {
|
|
|
270
270
|
const primary = sourceFor(audioOnly ? "audio" : "video");
|
|
271
271
|
const audio = sourceFor("audio") ?? primary;
|
|
272
272
|
if (!primary) throw new Error("The selected source track is absent from original input.");
|
|
273
|
-
|
|
273
|
+
// READ THROUGH FFMPEG'S CACHE, so a return to bytes already read is not a
|
|
274
|
+
// new request. An OpenDML AVI's open seeks from its super index in the header
|
|
275
|
+
// to each standard index and back: on the field's two-hour AVI, 195 indexes
|
|
276
|
+
// made 390 requests and 636-1243 MB of answers, and the first piece came 4.8 s
|
|
277
|
+
// after the start against 2.8 s through the cache, which made 3 requests;
|
|
278
|
+
// the pieces were the same to the byte, on FFmpeg 6.1 and 8.1.2 (Home
|
|
279
|
+
// Assistant 2026-10-13, torrent-tv/meta#178). The cache's file holds what
|
|
280
|
+
// FFmpeg read and is counted by the budget (`EncodeRuns.inputCacheBytes`).
|
|
281
|
+
const url = source => `cache:${new URL(`/encode-input/${inputToken}/${source.fileIndex}`, baseUrl).href}`;
|
|
274
282
|
// The container named the bytes FFmpeg's stream search reads for this much
|
|
275
283
|
// media time; the search is told the same, whatever its own default.
|
|
276
284
|
const openArgs = source => Number.isFinite(source.input.streamSearchSeconds)
|
|
@@ -304,9 +304,19 @@ export class AviContainer extends Container {
|
|
|
304
304
|
}
|
|
305
305
|
|
|
306
306
|
/**
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
307
|
+
* The length of the film, by the rule FFmpeg's AVI demuxer reads it with
|
|
308
|
+
* (libavformat/avidec.c n8.1, the `strh` case): a stream whose samples vary
|
|
309
|
+
* in size — every picture, and sound such as MP3 declared with
|
|
310
|
+
* `dwSampleSize` 0 — lasts `dwLength` units of its own `dwScale/dwRate`, and
|
|
311
|
+
* a stream of fixed-size samples is given no length by its header. The film
|
|
312
|
+
* lasts as long as the longest stream that has one.
|
|
313
|
+
*
|
|
314
|
+
* The main header's frame count is not that length. OpenDML has
|
|
315
|
+
* `avih.dwTotalFrames` count the first RIFF only, so a film with a second
|
|
316
|
+
* RIFF read as 8136.0 s of its 8998.1 and its last fourteen minutes were
|
|
317
|
+
* neither listed nor reachable (Home Assistant 2026-10-13,
|
|
318
|
+
* torrent-tv/meta#178). FFmpeg skips `dmlh` as well. The main header is
|
|
319
|
+
* read only when no stream states a length.
|
|
310
320
|
*
|
|
311
321
|
* An AVI has no edit list and no timeline offset of any kind, so its start is
|
|
312
322
|
* zero — a declaration of the format itself, not an absence.
|
|
@@ -319,11 +329,17 @@ export class AviContainer extends Container {
|
|
|
319
329
|
}
|
|
320
330
|
/** @type {import("./Container.js").ContainerMediaInfo} */
|
|
321
331
|
const info = { format: this.formatName, durationSeconds: null, startTimeSeconds: 0 };
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
332
|
+
const stated = (await this.readTracks())
|
|
333
|
+
.filter(track => ["video", "audio"].includes(track.type) && (track.type === "video" || track.sampleSize === 0))
|
|
334
|
+
.map(track => track.durationSeconds)
|
|
335
|
+
.filter(seconds => seconds > 0);
|
|
336
|
+
if (stated.length > 0) {
|
|
337
|
+
info.durationSeconds = Math.max(...stated);
|
|
338
|
+
} else {
|
|
339
|
+
const { avih } = await this.#readHeaders();
|
|
340
|
+
const microsecondsPerFrame = avih?.readUInt32LE(0) ?? 0;
|
|
341
|
+
const totalFrames = avih?.readUInt32LE(16) ?? 0;
|
|
342
|
+
if (microsecondsPerFrame > 0 && totalFrames > 0) info.durationSeconds = (microsecondsPerFrame * totalFrames) / 1e6;
|
|
327
343
|
}
|
|
328
344
|
this.mediaInfo = info;
|
|
329
345
|
return info;
|
|
@@ -363,10 +363,20 @@ export class MatroskaContainer extends Container {
|
|
|
363
363
|
* Kept only once it has been read: a head that had not arrived used to leave
|
|
364
364
|
* an empty answer here for the life of the container.
|
|
365
365
|
*
|
|
366
|
+
* `Duration` is optional (RFC 9559 §5.1.2.4), and a file written as it was
|
|
367
|
+
* streamed states none. Such a film lasts to the end of its last frame, which
|
|
368
|
+
* the complete packet index knows: the plan asks for that index when no
|
|
369
|
+
* length is stated and then asks here again, and an answer kept from before
|
|
370
|
+
* the index ended the opening with `media-duration-unavailable`
|
|
371
|
+
* (torrent-tv/meta#178).
|
|
372
|
+
*
|
|
366
373
|
* @returns {Promise<import("./Container.js").ContainerMediaInfo>}
|
|
367
374
|
*/
|
|
368
375
|
async readMediaInfo() {
|
|
369
376
|
if (this.mediaInfo) {
|
|
377
|
+
if (this.mediaInfo.durationSeconds === null && this.#packets) {
|
|
378
|
+
this.mediaInfo = { ...this.mediaInfo, durationSeconds: await this.#lastFrameEnd(this.mediaInfo.startTimeSeconds) };
|
|
379
|
+
}
|
|
370
380
|
return this.mediaInfo;
|
|
371
381
|
}
|
|
372
382
|
/** @type {import("./Container.js").ContainerMediaInfo} */
|
|
@@ -394,10 +404,36 @@ export class MatroskaContainer extends Container {
|
|
|
394
404
|
if (layout.firstClusterAt !== null) {
|
|
395
405
|
info.startTimeSeconds = await clusterTimestampSeconds(reader, layout.firstClusterAt, layout);
|
|
396
406
|
}
|
|
407
|
+
// `Duration` runs from the Segment's timestamp zero, and a muxer that keeps
|
|
408
|
+
// its source's timestamps (FFmpeg's does) puts the first Cluster later. The
|
|
409
|
+
// plan's timeline begins at that Cluster, as every other container states
|
|
410
|
+
// its length, so what lies before it is not the film's: counted in, a file
|
|
411
|
+
// starting at 5 s listed five seconds past its last frame
|
|
412
|
+
// (torrent-tv/meta#178).
|
|
413
|
+
if (info.durationSeconds !== null && info.startTimeSeconds > 0) {
|
|
414
|
+
const shown = info.durationSeconds - info.startTimeSeconds;
|
|
415
|
+
info.durationSeconds = shown > 0 ? shown : null;
|
|
416
|
+
}
|
|
417
|
+
if (info.durationSeconds === null && this.#packets) info.durationSeconds = await this.#lastFrameEnd(info.startTimeSeconds);
|
|
397
418
|
this.mediaInfo = info;
|
|
398
419
|
return info;
|
|
399
420
|
}
|
|
400
421
|
|
|
422
|
+
/**
|
|
423
|
+
* How long after the start of the timeline the last picture or sound frame
|
|
424
|
+
* of the complete packet index ends; null when none does.
|
|
425
|
+
*
|
|
426
|
+
* @param {number | null} startSeconds
|
|
427
|
+
*/
|
|
428
|
+
async #lastFrameEnd(startSeconds) {
|
|
429
|
+
const ends = (await this.readTracks())
|
|
430
|
+
.filter(track => ["video", "audio"].includes(track.type))
|
|
431
|
+
.map(track => this.#packets.boundsOf(track.trackNumber)?.end)
|
|
432
|
+
.filter(Number.isFinite);
|
|
433
|
+
const length = ends.length > 0 ? Math.max(...ends) - (startSeconds ?? 0) : 0;
|
|
434
|
+
return length > 0 ? length : null;
|
|
435
|
+
}
|
|
436
|
+
|
|
401
437
|
/**
|
|
402
438
|
* What the file states about the work: `Info/Title`, the `Tags` that apply
|
|
403
439
|
* to the whole file, the titles of its chapters and tracks, and whether it
|
|
@@ -712,7 +748,9 @@ export class MatroskaContainer extends Container {
|
|
|
712
748
|
}
|
|
713
749
|
const index = await readMatroskaPackets({ readRange: this.readRange,
|
|
714
750
|
fileSize: this.fileSize, portionBytes: this.portionBytes, layout, tracks,
|
|
715
|
-
|
|
751
|
+
// The last frame ends where the film does, on the packets' own clock.
|
|
752
|
+
endSeconds: info.durationSeconds === null ? null : info.durationSeconds + (info.startTimeSeconds ?? 0),
|
|
753
|
+
interval, state, packetMemory: this.packetMemory });
|
|
716
754
|
if (index.isComplete() && state.startAt === layout.firstClusterAt) {
|
|
717
755
|
this.#packets = index;
|
|
718
756
|
}
|
|
@@ -620,6 +620,16 @@ export class Mp4Container extends Container {
|
|
|
620
620
|
info.startTimeSeconds = movieTimescale > 0
|
|
621
621
|
? Mp4Container.#emptyEditSeconds(moov, header, movieTimescale)
|
|
622
622
|
: null;
|
|
623
|
+
// `mvhd` states the length of the longest track, and a track lasts as long
|
|
624
|
+
// as its edits, the empty one included (ISO/IEC 14496-12, 8.2.2 and 8.6.6).
|
|
625
|
+
// The film is shown from the end of that empty edit, and the plan's
|
|
626
|
+
// timeline begins there — as every other container states its length —
|
|
627
|
+
// so the edit is not the film's. Counted in, a file starting at 5 s listed
|
|
628
|
+
// five seconds past its last frame (torrent-tv/meta#178).
|
|
629
|
+
if (info.durationSeconds !== null && info.startTimeSeconds > 0) {
|
|
630
|
+
const shown = info.durationSeconds - info.startTimeSeconds;
|
|
631
|
+
info.durationSeconds = shown > 0 ? shown : null;
|
|
632
|
+
}
|
|
623
633
|
return info;
|
|
624
634
|
}
|
|
625
635
|
|
|
@@ -10,7 +10,7 @@ const CLUSTER = 0x1f43b675;
|
|
|
10
10
|
const LEVEL_ONE = new Set([CLUSTER, 0x1c53bb6b, 0x114d9b74, 0x1549a966, 0x1654ae6b, 0x1043a770, 0x1941a469, 0x1254c367]);
|
|
11
11
|
|
|
12
12
|
/** Index frame addresses by reading structural headers, never frame bodies. */
|
|
13
|
-
export async function readMatroskaPackets({ readRange, fileSize, portionBytes, layout, tracks,
|
|
13
|
+
export async function readMatroskaPackets({ readRange, fileSize, portionBytes, layout, tracks, endSeconds, interval, packetMemory, state = {} }) {
|
|
14
14
|
const reader = new ElementReader({ read: readRange, fileSize, portionBytes });
|
|
15
15
|
const declared = new Map(tracks.filter(track => ["video", "audio", "subtitle"].includes(track.type)).map(track => [track.trackNumber, track]));
|
|
16
16
|
if (!state.packets) {
|
|
@@ -18,7 +18,7 @@ export async function readMatroskaPackets({ readRange, fileSize, portionBytes, l
|
|
|
18
18
|
state.at ??= layout.firstClusterAt ?? layout.segmentEnd;
|
|
19
19
|
}
|
|
20
20
|
const packets = state.packets;
|
|
21
|
-
if (intervalCovered(declared, packets, interval)) return buildIndex(declared, packets,
|
|
21
|
+
if (intervalCovered(declared, packets, interval)) return buildIndex(declared, packets, endSeconds, false, interval, packetMemory);
|
|
22
22
|
let at = state.at;
|
|
23
23
|
while (at < layout.segmentEnd) {
|
|
24
24
|
const element = state.cluster?.element ?? await validHeader(reader, at, layout.segmentEnd);
|
|
@@ -80,14 +80,14 @@ export async function readMatroskaPackets({ readRange, fileSize, portionBytes, l
|
|
|
80
80
|
cursor = child.end;
|
|
81
81
|
state.cluster.cursor = cursor;
|
|
82
82
|
state.cluster.clusterTicks = clusterTicks;
|
|
83
|
-
if (intervalCovered(declared, packets, interval)) return buildIndex(declared, packets,
|
|
83
|
+
if (intervalCovered(declared, packets, interval)) return buildIndex(declared, packets, endSeconds, false, interval, packetMemory);
|
|
84
84
|
}
|
|
85
85
|
if (cursor <= at) throw new Error("Matroska packet walk did not advance.");
|
|
86
86
|
at = element.end ?? cursor;
|
|
87
87
|
state.at = at;
|
|
88
88
|
state.cluster = null;
|
|
89
89
|
}
|
|
90
|
-
return buildIndex(declared, packets,
|
|
90
|
+
return buildIndex(declared, packets, endSeconds, true, undefined, packetMemory);
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
function paddingForFrame(seconds, duration, count, frame) {
|
|
@@ -110,7 +110,7 @@ function intervalCovered(declared, packets, interval) {
|
|
|
110
110
|
});
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
function buildIndex(declared, packets,
|
|
113
|
+
function buildIndex(declared, packets, endSeconds, complete, interval, packetMemory) {
|
|
114
114
|
const index = new PacketIndex();
|
|
115
115
|
for (const [id, track] of declared) {
|
|
116
116
|
index.declareTrack(id, { type: track.type, codecId: track.codecId, codecRanges: track.codecRanges ?? [], prerollSeconds: track.seekPrerollSeconds ?? 0, reorderDepth: track.reorderDepth ?? 0 });
|
|
@@ -120,7 +120,7 @@ function buildIndex(declared, packets, durationSeconds, complete, interval, pack
|
|
|
120
120
|
if (stream.durationAt(position) === 0 || stream.derivedDurationAt(position)) { needsFollowing = true; break; }
|
|
121
121
|
}
|
|
122
122
|
if (needsFollowing) {
|
|
123
|
-
const endFor = followingPacketTime(stream, complete ?
|
|
123
|
+
const endFor = followingPacketTime(stream, complete ? endSeconds : null, packetMemory);
|
|
124
124
|
try { for (let position = 0; position < stream.length; position++) {
|
|
125
125
|
if (stream.durationAt(position) !== 0 && !stream.derivedDurationAt(position)) continue;
|
|
126
126
|
const pts = stream.ptsAt(position), end = endFor(pts);
|
|
@@ -28,6 +28,7 @@ export class MpegElementaryIndex {
|
|
|
28
28
|
nalIndex: track.codecId === "h264" ? new H264ElementaryIndex(track, this.index)
|
|
29
29
|
: track.codecId === "hevc" ? new HevcElementaryIndex(track, this.index) : null,
|
|
30
30
|
picture: null, pendingStart: 0, lastPicture: -1, presentationBase: null, lastDts: null, referencePts: null,
|
|
31
|
+
lastGroup: -1, groupBegins: false, shownUntil: null,
|
|
31
32
|
audioBytes: Buffer.alloc(0), audioStart: 0, audioPts: null, lastTimestamp: null, clockShift: 0,
|
|
32
33
|
reservoir: new MpegAudioReservoir(), packetCount: 0,
|
|
33
34
|
lastExtension: -1, requiresStamp: false });
|
|
@@ -97,6 +98,10 @@ export class MpegElementaryIndex {
|
|
|
97
98
|
if ((code === 0xb3 || code === 0xb8) && offset > state.lastPicture) {
|
|
98
99
|
if (state.pendingStart === null) state.pendingStart = offset;
|
|
99
100
|
}
|
|
101
|
+
if (code === 0xb8 && offset > state.lastGroup) {
|
|
102
|
+
state.lastGroup = offset;
|
|
103
|
+
state.groupBegins = true;
|
|
104
|
+
}
|
|
100
105
|
if (code !== 0 || at + 6 > bytes.length || offset <= state.lastPicture) continue;
|
|
101
106
|
if (!(state.track.fps > 0)) throw new Error("MPEG video cadence is not declared.");
|
|
102
107
|
const reference = (bytes[at + 4] << 2) | (bytes[at + 5] >> 6);
|
|
@@ -107,7 +112,16 @@ export class MpegElementaryIndex {
|
|
|
107
112
|
const stamp = stampAt(state, offset);
|
|
108
113
|
if (state.requiresStamp && !stamp) throw new Error("MPEG repeated frames require an explicit following timestamp.");
|
|
109
114
|
state.requiresStamp = false;
|
|
115
|
+
// `temporal_reference` counts the pictures of one group of pictures in
|
|
116
|
+
// the order they are shown, from 0 (ISO/IEC 13818-2, 6.3.9), so a new
|
|
117
|
+
// group shows its first picture where the last one shown ended. A muxer
|
|
118
|
+
// stamps a PES packet, not every picture: a group whose first picture
|
|
119
|
+
// came unstamped was placed on the previous group's base, up to a group
|
|
120
|
+
// earlier, and the index refused to go back: none of four program
|
|
121
|
+
// streams FFmpeg wrote with more than one group opened (torrent-tv/meta#178).
|
|
110
122
|
if (stamp) state.presentationBase = stamp.pts - reference / state.track.fps;
|
|
123
|
+
else if (state.groupBegins && state.shownUntil !== null) state.presentationBase = state.shownUntil;
|
|
124
|
+
state.groupBegins = false;
|
|
111
125
|
if (state.presentationBase === null) throw new Error("MPEG picture has no presentation timestamp.");
|
|
112
126
|
const pts = stamp?.pts ?? state.presentationBase + reference / state.track.fps;
|
|
113
127
|
const dts = stamp?.dts ?? (state.lastDts === null ? pts : state.lastDts + 1 / state.track.fps);
|
|
@@ -129,6 +143,7 @@ export class MpegElementaryIndex {
|
|
|
129
143
|
const { start, ...packet } = state.picture;
|
|
130
144
|
if (!(end > start)) throw new Error("MPEG picture has no bytes.");
|
|
131
145
|
this.index.append(state.track.trackNumber, { ...packet, ranges: addresses(state, start, end) });
|
|
146
|
+
state.shownUntil = Math.max(state.shownUntil ?? -Infinity, packet.pts + packet.duration);
|
|
132
147
|
state.mappings = state.mappings.filter(mapping => mapping.to > end);
|
|
133
148
|
}
|
|
134
149
|
|
|
@@ -93,6 +93,7 @@ import { audioReadingFor } from "../encode/audio-work-rate.js";
|
|
|
93
93
|
import { wireMachineBudget } from "../storage/wire.js";
|
|
94
94
|
import { Returns } from "../storage/returns.js";
|
|
95
95
|
import { freeBytesFor } from "../storage/free.js";
|
|
96
|
+
import { existsSync } from "node:fs";
|
|
96
97
|
import path from "node:path";
|
|
97
98
|
import { contentOf, LocalObservations, PROXY_ROOT } from "../encode/LocalObservations.js";
|
|
98
99
|
import { configurationKeyOf } from "../encode/fingerprint.js";
|
|
@@ -345,7 +346,7 @@ export function wireOutputs({
|
|
|
345
346
|
get encoders() { return parts.encoders; },
|
|
346
347
|
invalidateWaits: (output) => parts.serving.invalidateWaits(output),
|
|
347
348
|
productionFailed: (output) => parts.renditions.noteProductionFailed(output),
|
|
348
|
-
encodersChanged: () => { parts.applyDownloadShare?.(); onEncodersChanged?.(); },
|
|
349
|
+
encodersChanged: () => { parts.applyDownloadShare?.(); reviseInputCaches(); onEncodersChanged?.(); },
|
|
349
350
|
});
|
|
350
351
|
// How much film is ready in front of the viewers, said; and the spare soundtracks fetched once it is full.
|
|
351
352
|
parts.cushion = new CushionReport({
|
|
@@ -1044,10 +1045,25 @@ export function wireOutputs({
|
|
|
1044
1045
|
memory: parts.memoryClaimant,
|
|
1045
1046
|
encodeInputs: parts.encodeInputs,
|
|
1046
1047
|
indexMemory,
|
|
1048
|
+
// FFmpeg puts its temporary files in /tmp, or where it runs when there is
|
|
1049
|
+
// none — an output directory of the segments.
|
|
1050
|
+
inputCaches: { held: () => parts.encodeRuns?.inputCacheBytes() ?? 0, root: existsSync("/tmp") ? "/tmp" : null },
|
|
1047
1051
|
policy: parts.budgetPolicy ?? undefined,
|
|
1048
1052
|
readFree: freeBytesFor,
|
|
1049
1053
|
logger
|
|
1050
1054
|
});
|
|
1055
|
+
/**
|
|
1056
|
+
* A run reading an original file started or ended, so the disk its cache is
|
|
1057
|
+
* on is divided again — only when what the caches may hold has changed.
|
|
1058
|
+
*/
|
|
1059
|
+
let inputCachesDivided = 0;
|
|
1060
|
+
function reviseInputCaches() {
|
|
1061
|
+
const bytes = parts.encodeRuns?.inputCacheBytes() ?? 0;
|
|
1062
|
+
const on = parts.machineBudget?.inputCachesOn;
|
|
1063
|
+
if (!on || bytes === inputCachesDivided) return;
|
|
1064
|
+
inputCachesDivided = bytes;
|
|
1065
|
+
void parts.machineBudget.revise(on, "encoder input caches").catch(error => logger.warn(`input cache budget: ${error.message}`));
|
|
1066
|
+
}
|
|
1051
1067
|
/**
|
|
1052
1068
|
* What starting a run costs this output before its first piece is encoded:
|
|
1053
1069
|
* its own measured open where it has one (`RunCosts.openOf`), else the
|
package/services/storage/wire.js
CHANGED
|
@@ -44,6 +44,9 @@ import { availableMemoryBytes } from "./machine-memory.js";
|
|
|
44
44
|
* not fit in memory is spilled, so this share decides how much disk is needed.
|
|
45
45
|
* @param {{ held: () => number, allow: (bytes: number) => unknown, root?: string }} [params.wholeFiles] -
|
|
46
46
|
* Files downloaded whole and kept as files, on the torrent thread as well.
|
|
47
|
+
* @param {{ held: () => number, root?: string | null }} [params.inputCaches] -
|
|
48
|
+
* What FFmpeg's caches of original input hold, and the directory FFmpeg puts
|
|
49
|
+
* them in (the segments' disk when none).
|
|
47
50
|
* @param {import("./Diagnostics.js").Diagnostics} [params.diagnostics] - The
|
|
48
51
|
* evidence. Built where the collectors are, so one object is both registered
|
|
49
52
|
* here and consulted by them.
|
|
@@ -64,6 +67,7 @@ export function wireMachineBudget({
|
|
|
64
67
|
encodeInputs,
|
|
65
68
|
indexMemory,
|
|
66
69
|
wholeFiles,
|
|
70
|
+
inputCaches,
|
|
67
71
|
diagnostics,
|
|
68
72
|
diagnosticsRoot = "",
|
|
69
73
|
readFree,
|
|
@@ -153,6 +157,25 @@ export function wireMachineBudget({
|
|
|
153
157
|
});
|
|
154
158
|
}
|
|
155
159
|
|
|
160
|
+
// FFMPEG'S CACHE OF WHAT A RUN READ from an original file (`cache:`,
|
|
161
|
+
// `encode/source-command.js`): a temporary file where FFmpeg puts its own,
|
|
162
|
+
// as large as what the run has read and at most what it was admitted. Held
|
|
163
|
+
// while the run lives and given back by nobody else, so it is required in
|
|
164
|
+
// full and the rest of its disk is divided after it.
|
|
165
|
+
let inputCachesOn = null;
|
|
166
|
+
if (typeof inputCaches?.held === "function") {
|
|
167
|
+
inputCachesOn = inputCaches.root ? deviceOf(inputCaches.root) : segmentsOn;
|
|
168
|
+
defineDisk(inputCachesOn, inputCaches.root || segmentStore.root);
|
|
169
|
+
budget.register({
|
|
170
|
+
name: "encoder input caches",
|
|
171
|
+
resource: inputCachesOn,
|
|
172
|
+
held: () => inputCaches.held(),
|
|
173
|
+
wanted: () => inputCaches.held(),
|
|
174
|
+
required: () => inputCaches.held(),
|
|
175
|
+
allow: () => {}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
156
179
|
let evidenceOn = null;
|
|
157
180
|
if (diagnostics) {
|
|
158
181
|
evidenceOn = diagnosticsRoot ? deviceOf(diagnosticsRoot) : segmentsOn;
|
|
@@ -188,6 +211,8 @@ export function wireMachineBudget({
|
|
|
188
211
|
return budget.revise(resource, because);
|
|
189
212
|
},
|
|
190
213
|
segmentBytes: () => segmentBytes,
|
|
214
|
+
/** The disk FFmpeg's input caches are counted on, or null when none are. */
|
|
215
|
+
inputCachesOn,
|
|
191
216
|
diagnostics: diagnostics ?? null,
|
|
192
217
|
describe: () => budget.describe()
|
|
193
218
|
};
|