@torrent-tv/proxy 2.107.34 → 2.107.36
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
CHANGED
package/server.js
CHANGED
|
@@ -649,6 +649,7 @@ export async function startProxyServer({
|
|
|
649
649
|
return downloadMaps.refresh(sourceKey, fileIndex);
|
|
650
650
|
}).catch(error => logger.warn(`download map after a packet table: ${error?.message ?? error}`));
|
|
651
651
|
};
|
|
652
|
+
let outputPartsReady = false;
|
|
652
653
|
const downloadMaps = new DownloadMaps({
|
|
653
654
|
// The ranges a picture reads at open are named again by every segment;
|
|
654
655
|
// only the zone the swarm would rank first is sent (torrent-tv/meta#166).
|
|
@@ -665,6 +666,12 @@ export async function startProxyServer({
|
|
|
665
666
|
pack: (zones) => coalescedZones(zones),
|
|
666
667
|
log: (line) => logger.info(line),
|
|
667
668
|
observeWait: (sourceKey, event, fields) => waitTrace.source(sourceKey, event, fields),
|
|
669
|
+
// A file's own reads are wanted while a present viewer has the file chosen
|
|
670
|
+
// or on screen. The viewers are assembled below; until they exist no read
|
|
671
|
+
// can have been asked for by one.
|
|
672
|
+
fileWanted: (sourceKey, fileIndex) => (outputPartsReady ? outputParts.viewers.forSource(sourceKey) : [])
|
|
673
|
+
.some(viewer => viewer.source?.selectedFileIndex === fileIndex ||
|
|
674
|
+
viewer.source?.visibleFileIndices?.includes(fileIndex)),
|
|
668
675
|
resolvePlayback: async (map) => {
|
|
669
676
|
const fileKey = `${map.sourceKey}:${map.fileIndex}`;
|
|
670
677
|
if (map.zones.length === 0) {
|
|
@@ -1000,7 +1007,10 @@ export async function startProxyServer({
|
|
|
1000
1007
|
...(demand ? { priority: demand.priority, urgent: demand.urgent, deadlineAt: demand.deadlineAt, interval: demand, leadSeconds: demand.leadSeconds ?? 0 } : {}),
|
|
1001
1008
|
...(readPriority?.requestId ? { priority: readPriority.rank, urgent: readPriority.urgent === true,
|
|
1002
1009
|
deadlineAt: Number.isFinite(readPriority.deadlineAt) ? readPriority.deadlineAt : Number.POSITIVE_INFINITY,
|
|
1003
|
-
scope: "preparation" } : {})
|
|
1010
|
+
scope: "preparation" } : {}),
|
|
1011
|
+
// Neither a viewer's interval nor an encode input asked for it: the
|
|
1012
|
+
// file read itself, and that read is wanted while the file is.
|
|
1013
|
+
...(!demand && !readPriority?.requestId ? { owner: "file" } : {}) });
|
|
1004
1014
|
}
|
|
1005
1015
|
if (finished && result.kind === "result") {
|
|
1006
1016
|
void downloadMaps.refresh(sourceKey, fileIndex).catch(error =>
|
|
@@ -1039,7 +1049,11 @@ export async function startProxyServer({
|
|
|
1039
1049
|
pinnedPreset,
|
|
1040
1050
|
indexMemory,
|
|
1041
1051
|
readMetadataActivity: () => containerOrchestrator.activity(),
|
|
1042
|
-
onViewerChanged: () => {
|
|
1052
|
+
onViewerChanged: () => {
|
|
1053
|
+
void sourcePreparation?.refresh();
|
|
1054
|
+
void downloadMaps.viewersChanged().catch(error => logger.warn(`download maps after a viewer change: ${error.message}`));
|
|
1055
|
+
onStateChanged();
|
|
1056
|
+
},
|
|
1043
1057
|
onEncodersChanged: () => onStateChanged(),
|
|
1044
1058
|
sourceInputsFor: (output, index) => segmentInputs.inputsFor(output.file.sourceKey, output.outputKey, index),
|
|
1045
1059
|
readSourceMedia: async (params) => {
|
|
@@ -1333,6 +1347,7 @@ export async function startProxyServer({
|
|
|
1333
1347
|
await torrentPool.fillFileInBackground?.(torrent, fileIndex);
|
|
1334
1348
|
}
|
|
1335
1349
|
});
|
|
1350
|
+
outputPartsReady = true;
|
|
1336
1351
|
// Where the processor goes, and at a cold start what the main thread does
|
|
1337
1352
|
// with it, when the operator asks (torrent-tv/meta#177).
|
|
1338
1353
|
const startupProfile = startupCpuProfile({
|
|
@@ -89,6 +89,9 @@ export class SwarmSelection {
|
|
|
89
89
|
const strictFirstClass = first !== null && !isConditional(first.urgency);
|
|
90
90
|
/** @type {Map<string, { from: number, to: number, priority: number }>} */
|
|
91
91
|
const wanted = new Map();
|
|
92
|
+
// Who asked for the first class here, for the line that says what the
|
|
93
|
+
// swarm is told first: which file and which request holds it.
|
|
94
|
+
const holders = new Set();
|
|
92
95
|
for (const priority of [1, 0]) {
|
|
93
96
|
const ranges = [];
|
|
94
97
|
for (const window of missing) {
|
|
@@ -99,6 +102,7 @@ export class SwarmSelection {
|
|
|
99
102
|
&& window.urgency === first.urgency && window.priority >= first.priority;
|
|
100
103
|
if (strictFirstClass && !inFirstClass) continue;
|
|
101
104
|
if ((inFirstClass ? 1 : 0) !== priority) continue;
|
|
105
|
+
if (priority === 1) holders.add(`${window.fileIndex}:${window.requestId}`);
|
|
102
106
|
const range = this.#piecesFor(window);
|
|
103
107
|
if (range) ranges.push(range);
|
|
104
108
|
}
|
|
@@ -114,6 +118,7 @@ export class SwarmSelection {
|
|
|
114
118
|
for (const range of union) wanted.set(`${range.from}-${range.to}-${priority}`, range);
|
|
115
119
|
if (priority === 1) {
|
|
116
120
|
this.firstClassPieces = union.map(range => range.from === range.to ? `${range.from}` : `${range.from}-${range.to}`);
|
|
121
|
+
this.firstClassHolders = [...holders].sort();
|
|
117
122
|
this.#required = strictFirstClass ? union.map(({ from, to }) => ({ from, to })) : [];
|
|
118
123
|
}
|
|
119
124
|
}
|
|
@@ -285,9 +285,16 @@ export function reconcileAll() {
|
|
|
285
285
|
for (const entry of entries) entry.limit.sample();
|
|
286
286
|
// What the swarm is told to fetch FIRST, said when it changes. Lower classes
|
|
287
287
|
// are withheld because a wire with no first-class piece would fall through.
|
|
288
|
+
// And who asked for it, as file:request: a class one torrent holds is what
|
|
289
|
+
// every other torrent waits behind, so the line names the request holding it.
|
|
290
|
+
const holdersOf = entry => {
|
|
291
|
+
const holders = entry.selection.firstClassPieces?.length ? entry.selection.firstClassHolders ?? [] : [];
|
|
292
|
+
return holders.length ? ` (held by ${holders.slice(0, 4).join(", ")}${holders.length > 4 ? ` +${holders.length - 4} more` : ""})` : "";
|
|
293
|
+
};
|
|
288
294
|
const said = entries.map(entry => `${String(entry.torrent.infoHash ?? "").slice(0, 8)}: ` +
|
|
289
295
|
`${(entry.selection.firstClassPieces ?? []).slice(0, 8).join(",") || "nothing"}` +
|
|
290
|
-
`${(entry.selection.firstClassPieces?.length ?? 0) > 8 ? ` +${entry.selection.firstClassPieces.length - 8} more` : ""}`
|
|
296
|
+
`${(entry.selection.firstClassPieces?.length ?? 0) > 8 ? ` +${entry.selection.firstClassPieces.length - 8} more` : ""}` +
|
|
297
|
+
holdersOf(entry)).join("; ");
|
|
291
298
|
const head = firstClass ? `${urgencyName(firstClass.urgency)} priority ${firstClass.priority}` : "none";
|
|
292
299
|
firstClassStated = `${head} — pieces ${said}`;
|
|
293
300
|
return { torrents: entries.length, speculativeAllowed, stated, withdrawn };
|
|
@@ -21,13 +21,21 @@ export class DownloadMaps {
|
|
|
21
21
|
/** What the torrent is told of the playback zones: as many windows as say the same thing. */
|
|
22
22
|
#pack;
|
|
23
23
|
#observeWait;
|
|
24
|
+
/**
|
|
25
|
+
* Whether a present viewer holds this file, chosen or listed. A read the
|
|
26
|
+
* file itself made — its track table, media info, keyframes, cover — has no
|
|
27
|
+
* owner of its own, so it is wanted exactly while the file is.
|
|
28
|
+
*/
|
|
29
|
+
#fileWanted;
|
|
24
30
|
|
|
25
31
|
constructor({ publish, resolvePlayback = async map => map.zones, log = null, now = Date.now,
|
|
26
|
-
clock = () => performance.timeOrigin + performance.now(), pack = zones => zones, observeWait = null
|
|
32
|
+
clock = () => performance.timeOrigin + performance.now(), pack = zones => zones, observeWait = null,
|
|
33
|
+
fileWanted = () => true }) {
|
|
27
34
|
if (typeof publish !== "function") throw new TypeError("Download map publication is required.");
|
|
28
35
|
this.#publish = publish;
|
|
29
36
|
this.#resolvePlayback = resolvePlayback;
|
|
30
37
|
this.#pack = pack;
|
|
38
|
+
this.#fileWanted = fileWanted;
|
|
31
39
|
this.#log = typeof log === "function" ? log : null;
|
|
32
40
|
this.#now = now;
|
|
33
41
|
this.#clock = clock;
|
|
@@ -178,7 +186,14 @@ export class DownloadMaps {
|
|
|
178
186
|
return Boolean(file && zoneIndexOf(file).exactly({ from, to }));
|
|
179
187
|
}
|
|
180
188
|
|
|
181
|
-
|
|
189
|
+
/**
|
|
190
|
+
* @param {object} params
|
|
191
|
+
* @param {"file"} [params.owner] - `"file"` for a read the file made for
|
|
192
|
+
* itself with no viewer's demand behind it. Such a statement is published
|
|
193
|
+
* only while {@link DownloadMaps#viewersChanged}'s question says the file
|
|
194
|
+
* is held by somebody.
|
|
195
|
+
*/
|
|
196
|
+
metadata({ sourceKey, fileIndex, statement, result, priority = 100, urgent = true, deadlineAt = 0, interval, leadSeconds = 0, scope = "playback", order = 0, owner }) {
|
|
182
197
|
if (!Number.isFinite(leadSeconds) || leadSeconds < 0) throw new TypeError("Metadata preparation time must be finite and nonnegative.");
|
|
183
198
|
const file = this.#file(sourceKey, fileIndex);
|
|
184
199
|
if (result.kind === "needs-ranges") {
|
|
@@ -189,7 +204,8 @@ export class DownloadMaps {
|
|
|
189
204
|
urgent = current.urgent;
|
|
190
205
|
deadlineAt = Number.isFinite(current.deadlineAt) ? current.deadlineAt - leadSeconds * 1000 : current.deadlineAt;
|
|
191
206
|
}
|
|
192
|
-
const entry = { ranges: result.ranges, requestId: result.requestId, priority, urgent, deadlineAt, interval, leadSeconds, scope, order
|
|
207
|
+
const entry = { ranges: result.ranges, requestId: result.requestId, priority, urgent, deadlineAt, interval, leadSeconds, scope, order,
|
|
208
|
+
...(owner === "file" ? { owner } : {}) };
|
|
193
209
|
const previous = file.metadata.get(statement);
|
|
194
210
|
if (previous && sameMetadata(previous, entry) && this.#standing(file)) return this.#unchanged(file);
|
|
195
211
|
file.metadata.set(statement, entry);
|
|
@@ -254,6 +270,46 @@ export class DownloadMaps {
|
|
|
254
270
|
return Promise.all(changed);
|
|
255
271
|
}
|
|
256
272
|
|
|
273
|
+
/**
|
|
274
|
+
* The viewers changed: a file's own reads are published again where whether
|
|
275
|
+
* anybody holds the file has changed.
|
|
276
|
+
*
|
|
277
|
+
* A read the file made for itself states its bytes at the urgency of a
|
|
278
|
+
* stopped viewer, and the swarm fetches only the most urgent class across
|
|
279
|
+
* every torrent. Kept after its viewer had gone, the head of a film nobody
|
|
280
|
+
* was opening any more held that class for the whole proxy: its swarm gave
|
|
281
|
+
* nothing, and every other film was told to fetch only the piece its viewer
|
|
282
|
+
* stood on (Home Assistant 2026-10-26, PPPD-452, torrent-tv/meta#239).
|
|
283
|
+
*
|
|
284
|
+
* @returns {Promise<unknown[]>}
|
|
285
|
+
*/
|
|
286
|
+
viewersChanged() {
|
|
287
|
+
const sent = [];
|
|
288
|
+
for (const file of this.#files.values()) {
|
|
289
|
+
if (![...file.metadata.values()].some(metadata => metadata.owner === "file")) continue;
|
|
290
|
+
if (file.ownReadsWanted === (this.#fileWanted(file.sourceKey, file.fileIndex) === true)) continue;
|
|
291
|
+
sent.push(this.#emit(file));
|
|
292
|
+
}
|
|
293
|
+
return Promise.all(sent);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Whether a present viewer holds this file, decided where the map is built
|
|
298
|
+
* and said when it changes for a file that has reads of its own.
|
|
299
|
+
*
|
|
300
|
+
* @returns {boolean}
|
|
301
|
+
*/
|
|
302
|
+
#ownReadsWanted(file) {
|
|
303
|
+
const wanted = this.#fileWanted(file.sourceKey, file.fileIndex) === true;
|
|
304
|
+
const own = [...file.metadata].filter(([, metadata]) => metadata.owner === "file").map(([statement]) => statement);
|
|
305
|
+
if (own.length && wanted !== file.ownReadsWanted && (file.ownReadsWanted !== undefined || !wanted)) {
|
|
306
|
+
this.#log?.(`download map ${file.sourceKey}:${file.fileIndex}: ${own.length} read(s) of the file itself ` +
|
|
307
|
+
`${wanted ? "stated again — a viewer holds the file" : "withheld — no present viewer holds the file"} [${own.join(", ")}]`);
|
|
308
|
+
}
|
|
309
|
+
file.ownReadsWanted = wanted;
|
|
310
|
+
return wanted;
|
|
311
|
+
}
|
|
312
|
+
|
|
257
313
|
forget(sourceKey, fileIndex, { keepPreparation = false } = {}) {
|
|
258
314
|
const file = this.#file(sourceKey, fileIndex);
|
|
259
315
|
this.#setZones(file, []);
|
|
@@ -302,7 +358,9 @@ export class DownloadMaps {
|
|
|
302
358
|
// Compared and sent as the torrent is to act on it: the playback zones
|
|
303
359
|
// packed into as few windows as say the same (`coalescedZones`).
|
|
304
360
|
const zones = [...this.#packedPlayback(file)];
|
|
305
|
-
|
|
361
|
+
const ownReadsWanted = this.#ownReadsWanted(file);
|
|
362
|
+
for (const { ranges, requestId, priority, urgent, deadlineAt, order, owner } of file.metadata.values()) {
|
|
363
|
+
if (owner === "file" && !ownReadsWanted) continue;
|
|
306
364
|
for (const [byteStart, byteEnd] of ranges) {
|
|
307
365
|
zones.push({ byteStart, byteEnd, priority, urgent, downloadOnly: true, requestId, deadlineAt, ...(order ? { order } : {}) });
|
|
308
366
|
}
|
|
@@ -520,6 +578,7 @@ function sameMetadata(left, right) {
|
|
|
520
578
|
return left.requestId === right.requestId && Object.is(left.priority, right.priority) &&
|
|
521
579
|
left.urgent === right.urgent && Object.is(left.deadlineAt, right.deadlineAt) &&
|
|
522
580
|
Object.is(left.leadSeconds, right.leadSeconds) && left.scope === right.scope && left.order === right.order &&
|
|
581
|
+
left.owner === right.owner &&
|
|
523
582
|
(left.interval?.from ?? null) === (right.interval?.from ?? null) &&
|
|
524
583
|
(left.interval?.to ?? null) === (right.interval?.to ?? null) &&
|
|
525
584
|
left.ranges.length === right.ranges.length &&
|