@torrent-tv/proxy 2.107.56 → 2.107.58
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 +24 -15
- package/package.json +1 -1
- package/server.js +195 -1139
- package/services/encode/EncodeInputResolution.js +157 -0
- package/services/encode/LocalObservations.js +20 -2
- package/services/encode/measure-host.js +147 -0
- package/services/identity/installation-id.js +17 -6
- package/services/media/SegmentSourceRanges.js +95 -0
- package/services/media/SubtitleWalk.js +172 -0
- package/services/media/WorkTagsReport.js +30 -0
- package/services/media/segment-reading.js +36 -0
- package/services/server/MediaReadParams.js +134 -0
- package/services/server/PreparationReads.js +175 -0
- package/services/server/forwarded-response-log.js +55 -0
- package/services/server/start-playback.js +34 -0
- package/services/server/torrent-pool-ports.js +105 -0
- package/services/server/wire-outputs.js +62 -49
- package/services/storage/files/CompletedFiles.js +6 -1
- package/services/storage/memory-report.js +5 -5
- package/services/storage/processor-share.js +24 -0
- package/services/storage/segment-store/SegmentStore.js +2 -1
- package/services/torrent/PlaybackResolution.js +397 -0
- package/services/torrent/worker/worker.js +1 -6
- package/services/viewer/watched-addresses.js +51 -0
- package/utils/atomic-json.js +46 -0
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* `server.js` to give each route the ones that route asks.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { EventEmitter } from "node:events";
|
|
12
|
+
import { writeJsonAtomic } from "../../utils/atomic-json.js";
|
|
11
13
|
import { logger } from "../../utils/logger.js";
|
|
12
14
|
import { KeyframeTables } from "../media/KeyframeTables.js";
|
|
13
15
|
import { bandOf, WaitLedger } from "../encode/OutputWaitLedger.js";
|
|
@@ -15,6 +17,7 @@ import { readEndedChildrenCpuSeconds, readMachineState, readProcessCpuSeconds, r
|
|
|
15
17
|
import { minimumBufferFrom } from "../torrent/supply-margin.js";
|
|
16
18
|
import { PriorityOrchestrator } from "../viewer/PriorityOrchestrator.js";
|
|
17
19
|
import { urgentOutputsReady } from "../viewer/urgent-output-ready.js";
|
|
20
|
+
import { trackChoicesOf } from "../viewer/segment-demands.js";
|
|
18
21
|
import { softwareDescriptor } from "../encode/hwaccel.js";
|
|
19
22
|
import { resolveSegmentFormat } from "../encode/segment-formats/index.js";
|
|
20
23
|
import { Timelines } from "../encode/output/Timeline.js";
|
|
@@ -50,7 +53,6 @@ import {
|
|
|
50
53
|
noteServingVerdict,
|
|
51
54
|
noteStepBeingWarmed,
|
|
52
55
|
noteStepOnScreen,
|
|
53
|
-
outputsBeingPrepared,
|
|
54
56
|
placeInitially,
|
|
55
57
|
placeOn,
|
|
56
58
|
presentOn,
|
|
@@ -99,10 +101,11 @@ import { freeBytesFor } from "../storage/free.js";
|
|
|
99
101
|
import { PlaybackStorageDemand } from "../storage/PlaybackStorageDemand.js";
|
|
100
102
|
import { inputCacheRoot, probeInputCache } from "../encode/input-cache.js";
|
|
101
103
|
import path from "node:path";
|
|
102
|
-
import { contentOf, LocalObservations
|
|
104
|
+
import { contentOf, LocalObservations } from "../encode/LocalObservations.js";
|
|
103
105
|
import { configurationKeyOf } from "../encode/fingerprint.js";
|
|
104
106
|
import { qualityStateOf } from "../encode/quality/OutputQualityState.js";
|
|
105
|
-
import {
|
|
107
|
+
import { preparedAddresses, watchedAddresses } from "../viewer/watched-addresses.js";
|
|
108
|
+
import { downloadShare, watchedFiles } from "../storage/processor-share.js";
|
|
106
109
|
import { describeProcessor, processorBetween, sampleProcessor } from "../storage/machine-processor.js";
|
|
107
110
|
import { ProcessorBudget } from "../encode/ProcessorBudget.js";
|
|
108
111
|
import { ENCODE_RUN_STATE, processCanBeSignalled } from "../encode/encode-run-state.js";
|
|
@@ -232,19 +235,17 @@ export function wireOutputs({
|
|
|
232
235
|
diagnostics = null,
|
|
233
236
|
diagnosticsRoot = "",
|
|
234
237
|
memoryClaimant = null,
|
|
235
|
-
|
|
238
|
+
encodeInputResolution = null,
|
|
236
239
|
repriceEncodeDemand = null,
|
|
237
240
|
readSourceMedia = null,
|
|
238
241
|
readMetadataActivity = null,
|
|
239
242
|
sourceInputsFor = null,
|
|
240
243
|
readEncodeRanges = null,
|
|
241
244
|
openEncodeWholeFile = null,
|
|
242
|
-
stretchCopied = () => {},
|
|
243
245
|
readEncodeHeldRanges = null,
|
|
244
246
|
encodeInputs = null,
|
|
245
247
|
indexMemory = null,
|
|
246
248
|
budgetPolicy = null,
|
|
247
|
-
onViewerChanged = null,
|
|
248
249
|
// Told when an encoder starts or ends: the room this host has for one more
|
|
249
250
|
// encode changes with it, and the server chooses proxies by that room.
|
|
250
251
|
onEncodersChanged = null}) {
|
|
@@ -774,19 +775,15 @@ export function wireOutputs({
|
|
|
774
775
|
// watched file of every torrent.
|
|
775
776
|
parts.applyDownloadShare = () => {
|
|
776
777
|
if (!parts.setDownloadRate || !parts.hostLoad || !parts.encodeCost) return;
|
|
777
|
-
const
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
if (processCanBeSignalled(state) && state !== ENCODE_RUN_STATE.SUSPENDED) {
|
|
785
|
-
file.encodeCostSec += parts.encodeCost.loadOfOutput(output.outputKey)?.costSec ?? 0;
|
|
778
|
+
const watched = watchedFiles(parts.outputs.values(), {
|
|
779
|
+
watched: (output) => parts.viewers?.forOutput(output)?.size > 0,
|
|
780
|
+
lengthOf: (output) => Number(output.file.lengthBytes ?? parts.hostLoad.fileLengthByKey.get(output.file.key)),
|
|
781
|
+
encodeCostOf: (output) => {
|
|
782
|
+
const state = parts.encodeRuns.runStateOf(output);
|
|
783
|
+
return processCanBeSignalled(state) && state !== ENCODE_RUN_STATE.SUSPENDED
|
|
784
|
+
? parts.encodeCost.loadOfOutput(output.outputKey)?.costSec ?? 0 : 0;
|
|
786
785
|
}
|
|
787
|
-
|
|
788
|
-
}
|
|
789
|
-
const watched = [...files.values()].filter((file) => Number.isFinite(file.bytesPerFilmSecond) && file.bytesPerFilmSecond > 0);
|
|
786
|
+
});
|
|
790
787
|
const share = downloadShare({ availableShare: parts.processor?.availability()?.share ?? 1,
|
|
791
788
|
baseDraw: parts.hostLoad.baseDraw, costPerMegabyte: parts.hostLoad.observedTorrentCostPerMegabyte, files: watched });
|
|
792
789
|
// Kept beside the readings it is judged against: what the torrents moved
|
|
@@ -879,7 +876,9 @@ export function wireOutputs({
|
|
|
879
876
|
if (calibration?.fingerprint) {
|
|
880
877
|
parts.localObservations = new LocalObservations({
|
|
881
878
|
fingerprint: calibration.fingerprint,
|
|
882
|
-
|
|
879
|
+
// The state directory arrives resolved; none keeps the observations in memory only.
|
|
880
|
+
filePath: typeof stateDir === "string" && stateDir.length > 0 ? path.join(stateDir, "local-observations.json") : "",
|
|
881
|
+
writeJson: writeJsonAtomic,
|
|
883
882
|
logger
|
|
884
883
|
});
|
|
885
884
|
parts.localObservations.load(calibration.kinds().map((kind) => configurationKeyOf(calibration.fingerprint, {
|
|
@@ -979,10 +978,13 @@ export function wireOutputs({
|
|
|
979
978
|
// be re-taken on a five-second timer instead, which made a just-created
|
|
980
979
|
// output wait up to five seconds before anything noticed it had a viewer.
|
|
981
980
|
parts.retention = new OutputRetention();
|
|
981
|
+
// Who listens to viewer changes subscribes here (`startPlayback`); a throw
|
|
982
|
+
// of a listener reaches the code that changed the viewers.
|
|
983
|
+
parts.viewerChanges = new EventEmitter();
|
|
982
984
|
parts.viewers = new Viewers({ onChange: () => {
|
|
983
985
|
parts.lifecycle.observeUse();
|
|
984
986
|
parts.encodeRuns.planEncodersSoon();
|
|
985
|
-
|
|
987
|
+
parts.viewerChanges.emit("change");
|
|
986
988
|
} });
|
|
987
989
|
// The outputs that exist, and every question about them: the picture a step
|
|
988
990
|
// belongs to, the steps, the soundtracks, the height a step is named by.
|
|
@@ -1124,36 +1126,12 @@ export function wireOutputs({
|
|
|
1124
1126
|
// cost) and how much of the machine nobody has priced (the host's reading).
|
|
1125
1127
|
parts.admission = new EncodeAdmission({
|
|
1126
1128
|
liveRunsByAddress: () => parts.encodeOrchestrator.liveRunsByAddress(),
|
|
1127
|
-
preparedAddresses: () =>
|
|
1128
|
-
const addresses = new Set();
|
|
1129
|
-
for (const id of outputsBeingPrepared(parts.viewers)) {
|
|
1130
|
-
const key = parts.outputs.get(id)?.outputKey;
|
|
1131
|
-
if (key) {
|
|
1132
|
-
addresses.add(key);
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
return addresses;
|
|
1136
|
-
},
|
|
1129
|
+
preparedAddresses: () => preparedAddresses(parts.viewers, parts.outputs),
|
|
1137
1130
|
// Every output a present viewer RECEIVES — by the same rule the per-output
|
|
1138
1131
|
// priority map is built by — that still has something left to make. The
|
|
1139
1132
|
// place an output is opened on is held from the moment its viewer is put
|
|
1140
1133
|
// on it, not from when the plan gets round to its first encoder.
|
|
1141
|
-
watchedAddresses: () =>
|
|
1142
|
-
const addresses = new Set();
|
|
1143
|
-
for (const session of parts.outputs.values()) {
|
|
1144
|
-
const address = session.outputKey;
|
|
1145
|
-
if (!address || addresses.has(address) || finishedOutput(session)) {
|
|
1146
|
-
continue;
|
|
1147
|
-
}
|
|
1148
|
-
for (const viewer of parts.viewers.forOutput(session).values()) {
|
|
1149
|
-
if (viewer.isPresent() && !parts.outputs.supersededBy(session, viewer.activeVariantId ?? null)) {
|
|
1150
|
-
addresses.add(address);
|
|
1151
|
-
break;
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
}
|
|
1155
|
-
return addresses;
|
|
1156
|
-
},
|
|
1134
|
+
watchedAddresses: () => watchedAddresses(parts.viewers, parts.outputs, finishedOutput),
|
|
1157
1135
|
finished: (address) => parts.outputs.outputsOn(address).some((session) => finishedOutput(session)),
|
|
1158
1136
|
loadOf: (address) => parts.encodeCost.loadOfOutput(address),
|
|
1159
1137
|
loadForCandidate: (spec, file) => parts.encodeCost.loadForCandidate(spec, file),
|
|
@@ -1216,12 +1194,12 @@ export function wireOutputs({
|
|
|
1216
1194
|
void parts.machineBudget.revise(parts.machineBudget.storageResource, "playback stock");
|
|
1217
1195
|
}
|
|
1218
1196
|
});
|
|
1219
|
-
parts.encodeInputs = encodeInputs ?? (typeof
|
|
1220
|
-
resolve:
|
|
1197
|
+
parts.encodeInputs = encodeInputs ?? (typeof encodeInputResolution?.resolve === "function" ? new EncodeInputs({
|
|
1198
|
+
resolve: (...args) => encodeInputResolution.resolve(...args),
|
|
1221
1199
|
readRanges: readEncodeRanges,
|
|
1222
1200
|
openWholeFile: openEncodeWholeFile,
|
|
1223
1201
|
heldRanges: readEncodeHeldRanges,
|
|
1224
|
-
stretchCopied,
|
|
1202
|
+
stretchCopied: (...args) => encodeInputResolution.stretchCopied(...args),
|
|
1225
1203
|
reviseBudget: () => parts.machineBudget.revise("memory", "admitted encode inputs"),
|
|
1226
1204
|
capacity: () => parts.machineBudget.capacityOf("memory"),
|
|
1227
1205
|
// How long this output's input takes to open, from the one owner of start
|
|
@@ -1376,5 +1354,40 @@ export function wireOutputs({
|
|
|
1376
1354
|
void parts.quality.runQualityBudgetOnce();
|
|
1377
1355
|
}, BUDGET_CHECK_INTERVAL_MS);
|
|
1378
1356
|
parts.budgetTimer.unref();
|
|
1357
|
+
/**
|
|
1358
|
+
* What the download map's pass reads of the outputs: each output of a source
|
|
1359
|
+
* that a present viewer watches and that reads the file, in catalog order,
|
|
1360
|
+
* with what decides how long its segments take to make and deliver and what
|
|
1361
|
+
* it has made (`PlaybackResolution`).
|
|
1362
|
+
*
|
|
1363
|
+
* @param {string} sourceKey
|
|
1364
|
+
* @param {number} fileIndex
|
|
1365
|
+
* @returns {Array<{ outputKey: string, timeline: object, choices: object[], revision: unknown,
|
|
1366
|
+
* sizes: () => Map<number, number>, encodeSpeed: number | undefined, linkMbps: number | null, target: number | null }>}
|
|
1367
|
+
*/
|
|
1368
|
+
parts.playedOutputsOf = (sourceKey, fileIndex) => {
|
|
1369
|
+
const played = [];
|
|
1370
|
+
for (const output of parts.outputs.values()) {
|
|
1371
|
+
if (output.file.sourceKey !== sourceKey) continue;
|
|
1372
|
+
const consumers = [...parts.viewers.forOutput(output).values()].filter(viewer =>
|
|
1373
|
+
viewer.isPresent() && !parts.outputs.supersededBy(output, viewer.activeVariantId ?? null));
|
|
1374
|
+
if (consumers.length === 0) continue;
|
|
1375
|
+
const choices = trackChoicesOf(output, fileIndex);
|
|
1376
|
+
if (choices.length === 0) continue;
|
|
1377
|
+
// An output with no cut grid is left out by the pass (`PlaybackResolution`);
|
|
1378
|
+
// none of what follows is worth working out for it.
|
|
1379
|
+
const timeline = output.timeline;
|
|
1380
|
+
if (!timeline || typeof timeline !== "object" || !Array.isArray(timeline.published ?? timeline.boundaries)) continue;
|
|
1381
|
+
const links = consumers.map(viewer => viewer.linkReading()?.linkMbps)
|
|
1382
|
+
.filter(value => Number.isFinite(value) && value > 0);
|
|
1383
|
+
played.push({ outputKey: output.outputKey, timeline: output.timeline, choices,
|
|
1384
|
+
revision: parts.segmentStore.revisionOf(output.outputKey),
|
|
1385
|
+
sizes: () => parts.segmentStore.sizesOf(output.outputKey),
|
|
1386
|
+
encodeSpeed: parts.encodeCost.latestSpeedReadingOf(output)?.speed,
|
|
1387
|
+
linkMbps: links.length ? Math.min(...links) : null,
|
|
1388
|
+
target: parts.encodeInputs?.targetInputBytes?.(output) ?? null });
|
|
1389
|
+
}
|
|
1390
|
+
return played;
|
|
1391
|
+
};
|
|
1379
1392
|
return parts;
|
|
1380
1393
|
}
|
|
@@ -27,6 +27,7 @@ import fs from "node:fs/promises";
|
|
|
27
27
|
import { randomUUID } from "node:crypto";
|
|
28
28
|
import os from "node:os";
|
|
29
29
|
import path from "node:path";
|
|
30
|
+
import { writeJsonAtomic } from "../../../utils/atomic-json.js";
|
|
30
31
|
import { IDLE_KEEP_MS } from "../keep.js";
|
|
31
32
|
|
|
32
33
|
/**
|
|
@@ -541,7 +542,11 @@ export class CompletedFiles {
|
|
|
541
542
|
await fs.rm(directory, { ...REMOVAL, recursive: true }).catch(() => undefined);
|
|
542
543
|
return;
|
|
543
544
|
}
|
|
544
|
-
|
|
545
|
+
try {
|
|
546
|
+
writeJsonAtomic(path.join(directory, "manifest.json"), named);
|
|
547
|
+
} catch {
|
|
548
|
+
// The manifest is a convenience for the next start; a failed write is not a failed keep.
|
|
549
|
+
}
|
|
545
550
|
}
|
|
546
551
|
|
|
547
552
|
/** Remove an empty or partial-only directory discovered during startup. */
|
|
@@ -459,13 +459,13 @@ export function startMemoryReport({
|
|
|
459
459
|
label = "",
|
|
460
460
|
diskPath = "",
|
|
461
461
|
intervalMs = MEMORY_REPORT_INTERVAL_MS,
|
|
462
|
-
quietMs =
|
|
463
|
-
changeBytes =
|
|
462
|
+
quietMs = 60_000,
|
|
463
|
+
changeBytes = 25 * 1024 * 1024,
|
|
464
464
|
snapshotDir = "",
|
|
465
465
|
mayKeep = null,
|
|
466
|
-
snapshotFloorBytes =
|
|
467
|
-
snapshotGrowthBytes =
|
|
468
|
-
keepSnapshots =
|
|
466
|
+
snapshotFloorBytes = 400 * 1024 * 1024,
|
|
467
|
+
snapshotGrowthBytes = 400 * 1024 * 1024,
|
|
468
|
+
keepSnapshots = 3,
|
|
469
469
|
heapSnapshots = false,
|
|
470
470
|
bufferSnapshotFloorBytes = 0,
|
|
471
471
|
bufferSnapshotGrowthBytes = 500 * 1024 * 1024
|
|
@@ -55,3 +55,27 @@ export function downloadShare({ availableShare, baseDraw, costPerMegabyte, files
|
|
|
55
55
|
const bytesPerSecond = perByte > 0 ? Math.max(filmSpeed, 1) * bytes : Infinity;
|
|
56
56
|
return { filmSpeed, capacity, bytesPerSecond: Number.isFinite(bytesPerSecond) && bytes > 0 ? bytesPerSecond : Infinity };
|
|
57
57
|
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The watched outputs grouped by the file they read: what the share is worked
|
|
61
|
+
* out over. Moved out of `applyDownloadShare` verbatim (torrent-tv/meta#280).
|
|
62
|
+
*
|
|
63
|
+
* @param {Iterable<{ file: { key: string, durationSeconds: number } }>} outputs
|
|
64
|
+
* @param {object} ports
|
|
65
|
+
* @param {(output: object) => boolean} ports.watched - Whether a viewer receives the output.
|
|
66
|
+
* @param {(output: object) => number} ports.lengthOf - The file's length in bytes.
|
|
67
|
+
* @param {(output: object) => number} ports.encodeCostOf - What its encoder costs, 0 when none runs.
|
|
68
|
+
* @returns {{ bytesPerFilmSecond: number, encodeCostSec: number }[]}
|
|
69
|
+
* One per file in the order first seen; only finite positive rates kept.
|
|
70
|
+
*/
|
|
71
|
+
export function watchedFiles(outputs, { watched, lengthOf, encodeCostOf }) {
|
|
72
|
+
const files = new Map();
|
|
73
|
+
for (const output of outputs) {
|
|
74
|
+
if (!watched(output)) continue;
|
|
75
|
+
const key = output.file.key;
|
|
76
|
+
const file = files.get(key) ?? { bytesPerFilmSecond: lengthOf(output) / Number(output.file.durationSeconds), encodeCostSec: 0 };
|
|
77
|
+
file.encodeCostSec += encodeCostOf(output);
|
|
78
|
+
files.set(key, file);
|
|
79
|
+
}
|
|
80
|
+
return [...files.values()].filter((file) => Number.isFinite(file.bytesPerFilmSecond) && file.bytesPerFilmSecond > 0);
|
|
81
|
+
}
|
|
@@ -44,6 +44,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync,
|
|
|
44
44
|
import { rmSync } from "node:fs";
|
|
45
45
|
import os from "node:os";
|
|
46
46
|
import path from "node:path";
|
|
47
|
+
import { writeFileAtomic } from "../../../utils/atomic-json.js";
|
|
47
48
|
|
|
48
49
|
/** Where every output's segments live. One root for the process. */
|
|
49
50
|
const DEFAULT_STORE_ROOT = path.join(os.tmpdir(), "torrent-tv-hls");
|
|
@@ -235,7 +236,7 @@ export class SegmentStore {
|
|
|
235
236
|
mkdirSync(dir, { recursive: true });
|
|
236
237
|
// What this directory is, for whoever finds it after this process has
|
|
237
238
|
// been killed. Without it the sweep can only throw everything away.
|
|
238
|
-
|
|
239
|
+
writeFileAtomic(path.join(dir, KEY_FILE), `${key}\n`);
|
|
239
240
|
}
|
|
240
241
|
this.#touched.set(key, this.#now());
|
|
241
242
|
// Production is about to begin here, and a piece closing in this directory
|