@torrent-tv/proxy 2.75.0 → 2.76.1
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 +1509 -1461
- package/CLAUDE.md +11 -1
- package/biome.json +182 -1
- package/docs/container-architecture.md +2 -1
- package/docs/encode-run-state.md +1 -1
- package/knip.json +14 -0
- package/package.json +1 -1
- package/routes/api/sources/warm/post.js +1 -1
- package/routes/api/transcode-sessions/post.js +185 -185
- package/routes/api/transcode-sessions/progress/get.js +5 -1
- package/routes/transcode/audio-file/get.js +11 -1
- package/routes/transcode/audio-warm/get.js +11 -1
- package/routes/transcode/session-file/get.js +1 -1
- package/routes/transcode/variant-file/get.js +10 -1
- package/scripts/render-run-graph.js +2 -2
- package/server.js +25 -0
- package/services/audio-inventory.js +206 -201
- package/services/container/AviContainer.js +1 -1
- package/services/container/Container.js +33 -1
- package/services/container/MatroskaContainer.js +1 -1
- package/services/container/Mp4Container.js +1 -1
- package/services/container/SubtitleFileContainer.js +0 -1
- package/services/controllers/SubtitleController.js +128 -128
- package/services/demand/index.js +7 -10
- package/services/download/registry.js +0 -14
- package/services/encode/CoverageMap.js +281 -0
- package/services/encode/EncodePlan.js +255 -0
- package/services/encode/EncodeRun.js +587 -0
- package/services/encode/Encoder.js +84 -0
- package/services/encode/NvencEncoder.js +45 -0
- package/services/encode/QsvEncoder.js +47 -0
- package/services/encode/SegmentDemand.js +0 -0
- package/services/encode/SegmentStore.js +529 -0
- package/services/encode/SoftwareEncoder.js +111 -0
- package/services/encode/V4l2m2mEncoder.js +53 -0
- package/services/encode/VaapiEncoder.js +53 -0
- package/services/encode/args.js +200 -0
- package/services/{encode-exit.js → encode/encode-exit.js} +17 -0
- package/services/encode/index.js +9 -0
- package/services/encode/run-command.js +647 -0
- package/services/hls-session-manager.js +11100 -10711
- package/services/hwaccel.js +1688 -1992
- package/services/orchestrators/EncodeOrchestrator.js +359 -0
- package/services/output/LiveOutputs.js +213 -0
- package/services/output/Output.js +94 -0
- package/services/output/OutputSpec.js +195 -0
- package/services/output/Timeline.js +220 -0
- package/services/output/index.js +1 -0
- package/services/output/ladder.js +26 -0
- package/services/playback-planner.js +806 -775
- package/services/produced-index.js +222 -300
- package/services/source/SourceFile.js +346 -0
- package/services/{sidecar-files.js → torrent/files.js} +107 -11
- package/services/torrent/naming.js +619 -0
- package/services/torrent-worker/client.js +10 -0
- package/services/torrent-worker/container-tracks.js +71 -43
- package/services/torrent-worker/pool-adapter.js +18 -0
- package/services/torrent-worker/protocol.js +7 -0
- package/services/torrent-worker/subtitle-cues.js +549 -549
- package/services/torrent-worker/worker.js +18 -0
- package/services/tracks/TextSubtitleTrack.js +287 -287
- package/services/tracks/index.js +15 -14
- package/services/viewer/Viewer.js +145 -0
- package/services/viewer/Viewers.js +124 -0
- package/test/auto-quality-step.test.js +508 -506
- package/test/behind-head-repair.test.js +17 -7
- package/test/coverage-map.test.js +153 -0
- package/test/cut-times-timeline.test.js +6 -5
- package/test/cuts-follow-published-grid.test.js +4 -4
- package/test/decode-cost.test.js +31 -12
- package/test/encode-exit.test.js +1 -1
- package/test/encode-orchestrator.test.js +196 -0
- package/test/encode-plan.test.js +245 -0
- package/test/encode-run-state.test.js +2 -2
- package/test/encode-run.test.js +168 -0
- package/test/encoder-kinds.test.js +122 -0
- package/test/held-request-width.test.js +9 -3
- package/test/helpers/encode-run.js +128 -0
- package/test/keyframe-index-accuracy.test.js +19 -12
- package/test/keyframes-belong-to-the-file.test.js +132 -0
- package/test/orchestrator-wired.test.js +164 -0
- package/test/output-shape.test.js +68 -0
- package/test/output-spec.test.js +157 -0
- package/test/produced-copy-choice.test.js +58 -92
- package/test/produced-index.test.js +142 -188
- package/test/quality-variants.test.js +1079 -1075
- package/test/run-graph-drift.test.js +1 -1
- package/test/run-intervals.test.js +329 -0
- package/test/run-position-follows-published-grid.test.js +4 -4
- package/test/seek-landing.test.js +8 -8
- package/test/seek-target-not-superseded.test.js +21 -9
- package/test/segment-demand.test.js +82 -0
- package/test/segment-serve-wiring.test.js +47 -52
- package/test/segment-store.test.js +187 -0
- package/test/segments-are-shared.test.js +175 -0
- package/test/sidecar-naming.test.js +142 -0
- package/test/source-file.test.js +133 -0
- package/test/stale-request-after-seek.test.js +18 -12
- package/test/subtitle-language.test.js +252 -252
- package/test/timeline.test.js +95 -0
- package/test/{sidecar-files.test.js → torrent-files.test.js} +44 -1
- package/test/torrent-naming.test.js +255 -0
- package/test/tracks-begin-together.test.js +44 -32
- package/test/two-viewers-one-picture.test.js +347 -0
- package/test/viewer-outputs.test.js +273 -0
- package/test/viewer.test.js +91 -0
- package/utils/perf.js +1 -63
- package/services/container/index.js +0 -6
- package/services/controllers/index.js +0 -2
- package/services/download/index.js +0 -8
- package/services/orchestrators/index.js +0 -2
- /package/services/{encode-run-state.js → encode/encode-run-state.js} +0 -0
- /package/services/{language-detect.js → tracks/language-detect.js} +0 -0
|
@@ -205,22 +205,6 @@ async function readTracks(torrent, fileIndex, sourceKey, options) {
|
|
|
205
205
|
}
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
/**
|
|
209
|
-
* The audio tracks of one file, in the order ffmpeg numbers them `0:a:N`.
|
|
210
|
-
*
|
|
211
|
-
* @param {object} torrent
|
|
212
|
-
* @param {number} fileIndex
|
|
213
|
-
* @param {string} sourceKey
|
|
214
|
-
* @param {{ prefetchEdges?: () => Promise<unknown> }} [options]
|
|
215
|
-
* @returns {Promise<object[]>}
|
|
216
|
-
*/
|
|
217
|
-
export async function containerAudioTracksOf(torrent, fileIndex, sourceKey, options = {}) {
|
|
218
|
-
const tracks = await containerTracksOf(torrent, fileIndex, sourceKey, options);
|
|
219
|
-
return tracks
|
|
220
|
-
.filter((track) => track.type === "audio")
|
|
221
|
-
.sort((left, right) => left.declaredIndex - right.declaredIndex);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
208
|
/** Bytes of a file's head worth fetching before its track table is read. */
|
|
225
209
|
export const CONTAINER_HEAD_BYTES = HEAD_BYTES;
|
|
226
210
|
|
|
@@ -300,16 +284,78 @@ export async function warmResumePosition(torrent, fileIndex, sourceKey, position
|
|
|
300
284
|
}
|
|
301
285
|
|
|
302
286
|
/**
|
|
303
|
-
*
|
|
304
|
-
*
|
|
287
|
+
* Where one file's keyframes are, from the container's own table.
|
|
288
|
+
*
|
|
289
|
+
* A property of immutable bytes, like the duration and the track list, and read
|
|
290
|
+
* here for the same reason they are: the container is cached per file, so the
|
|
291
|
+
* table is read ONCE however many sessions ask for it, and two sessions created
|
|
292
|
+
* in the same moment join one read instead of making two. That is the two-viewer
|
|
293
|
+
* case exactly — measured 2026-09-03, two pictures of one file were created 13 ms
|
|
294
|
+
* apart, and each read the table for itself over the proxy's own HTTP.
|
|
305
295
|
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
296
|
+
* It decides which branch a copy takes: a picture can only be cut where the
|
|
297
|
+
* source already has a keyframe, so a file with no readable index is re-encoded
|
|
298
|
+
* instead. That decision belongs to the FILE, and it is the same for everybody
|
|
299
|
+
* watching it.
|
|
300
|
+
*
|
|
301
|
+
* @param {object} torrent
|
|
302
|
+
* @param {number} fileIndex
|
|
303
|
+
* @param {string} sourceKey
|
|
304
|
+
* @param {{ prefetchEdges?: () => Promise<unknown> }} [options]
|
|
305
|
+
* @returns {Promise<{ times: number[], tolerance: number } | null>}
|
|
306
|
+
*/
|
|
307
|
+
export async function containerKeyframesOf(torrent, fileIndex, sourceKey, options = {}) {
|
|
308
|
+
const file = torrent?.files?.[fileIndex];
|
|
309
|
+
if (!file || !Number.isFinite(file.length) || file.length <= 0) {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
if (typeof options.prefetchEdges === "function") {
|
|
313
|
+
try {
|
|
314
|
+
await options.prefetchEdges();
|
|
315
|
+
} catch {
|
|
316
|
+
// A prefetch that failed is not a reason to skip the read: the read
|
|
317
|
+
// fetches what it needs itself, only more slowly.
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
const readRange = async (start, end) =>
|
|
321
|
+
readFetching(file, start, Math.min(end, file.length - 1));
|
|
322
|
+
const startedAt = Date.now();
|
|
323
|
+
const params = {
|
|
324
|
+
sourceKey,
|
|
325
|
+
fileIndex,
|
|
326
|
+
readRange,
|
|
327
|
+
fileSize: file.length,
|
|
328
|
+
label: String(file.name ?? "")
|
|
329
|
+
};
|
|
330
|
+
try {
|
|
331
|
+
const index = await containerOrchestrator.getKeyframeIndex(params);
|
|
332
|
+
// Which container answered, reported whether or not it produced anything:
|
|
333
|
+
// how often an index disagrees with its own file is a question about the
|
|
334
|
+
// CONTAINER, and a measurement that does not say which one cannot answer
|
|
335
|
+
// it. Free — the container is the cached one that has just read the table.
|
|
336
|
+
const format = (await containerOrchestrator.getContainer(params))?.formatName ?? "unrecognised";
|
|
337
|
+
const times = Array.isArray(index?.times) && index.times.length > 0 ? index.times : null;
|
|
338
|
+
logger.info(
|
|
339
|
+
`container-keyframes: "${String(file.name).slice(0, 40)}" ` +
|
|
340
|
+
(times
|
|
341
|
+
? `${times.length} keyframes from the ${format} index in ${Date.now() - startedAt}ms`
|
|
342
|
+
: `has no readable index (${format}, ${Date.now() - startedAt}ms)`)
|
|
343
|
+
);
|
|
344
|
+
return {
|
|
345
|
+
times,
|
|
346
|
+
format,
|
|
347
|
+
tolerance: Number.isFinite(index?.tolerance) ? index.tolerance : 0
|
|
348
|
+
};
|
|
349
|
+
} catch (error) {
|
|
350
|
+
logger.warn(
|
|
351
|
+
`container-keyframes: "${String(file.name).slice(0, 40)}" could not be read: ${error?.message ?? error}`
|
|
352
|
+
);
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* What one file declares about itself as a whole.
|
|
313
359
|
*
|
|
314
360
|
* @param {object} torrent
|
|
315
361
|
* @param {number} fileIndex
|
|
@@ -357,21 +403,3 @@ export async function containerMediaInfoOf(torrent, fileIndex, sourceKey, option
|
|
|
357
403
|
return null;
|
|
358
404
|
}
|
|
359
405
|
}
|
|
360
|
-
|
|
361
|
-
/**
|
|
362
|
-
* Forget one file's tracks, or every file of a source.
|
|
363
|
-
*
|
|
364
|
-
* @param {string} sourceKey
|
|
365
|
-
* @param {number} [fileIndex]
|
|
366
|
-
*/
|
|
367
|
-
export function forgetContainerTracks(sourceKey, fileIndex) {
|
|
368
|
-
if (fileIndex === undefined) {
|
|
369
|
-
for (const key of [...byFile.keys()]) {
|
|
370
|
-
if (key.startsWith(`${sourceKey}:`)) {
|
|
371
|
-
byFile.delete(key);
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
byFile.delete(`${sourceKey}:${fileIndex}`);
|
|
377
|
-
}
|
|
@@ -239,6 +239,24 @@ export class WorkerTorrentPool {
|
|
|
239
239
|
return answer?.info ?? null;
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
+
/**
|
|
243
|
+
* Where one file's keyframes are, from the container's own table. Null when
|
|
244
|
+
* this file has no readable index — which is a final answer about the file,
|
|
245
|
+
* and the reason a copy of it has to be re-encoded instead.
|
|
246
|
+
*
|
|
247
|
+
* @param {object} torrent
|
|
248
|
+
* @param {number} fileIndex
|
|
249
|
+
* @returns {Promise<{ times: number[], tolerance: number } | null>}
|
|
250
|
+
*/
|
|
251
|
+
async getContainerKeyframes(torrent, fileIndex) {
|
|
252
|
+
const sourceKey = torrent?.sourceKey;
|
|
253
|
+
if (!sourceKey) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
const answer = await this.#client.getContainerKeyframes({ sourceKey, fileIndex });
|
|
257
|
+
return answer?.index ?? null;
|
|
258
|
+
}
|
|
259
|
+
|
|
242
260
|
/**
|
|
243
261
|
* The audio tracks one file declares, in the order ffmpeg numbers them
|
|
244
262
|
* `0:a:N`.
|
|
@@ -88,6 +88,13 @@ export const Command = {
|
|
|
88
88
|
* own HTTP reading the same bytes again.
|
|
89
89
|
*/
|
|
90
90
|
CONTAINER_MEDIA_INFO: "container-media-info",
|
|
91
|
+
/**
|
|
92
|
+
* Where a file's keyframes are, from the container's own table. A property of
|
|
93
|
+
* immutable bytes, read once per file by the same container that answers the
|
|
94
|
+
* two above — and the answer decides whether a picture can be copied at all,
|
|
95
|
+
* which must be the same answer for every viewer of that file.
|
|
96
|
+
*/
|
|
97
|
+
CONTAINER_KEYFRAMES: "container-keyframes",
|
|
91
98
|
/**
|
|
92
99
|
* Start fetching the region a viewer is about to resume at, named in seconds
|
|
93
100
|
* and turned into bytes here, where the file's own duration can be read.
|