@torrent-tv/proxy 2.107.26 → 2.107.28
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/server.js +4 -1
- package/services/encode/EncodeAdmission.js +14 -4
- package/services/encode/EncodeInputs.js +4 -1
- package/services/encode/quality/output-format.js +15 -12
- package/services/encode/quality/serving-output.js +4 -2
- package/services/media/container/AsfContainer.js +26 -12
- package/services/media/container/asf-packets.js +12 -3
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -1045,10 +1045,13 @@ export async function startProxyServer({
|
|
|
1045
1045
|
if (!params) return { kind: "needs-source" };
|
|
1046
1046
|
if (timing) {
|
|
1047
1047
|
params.walkAccount = timing.walk ??= { lockMs: 0, reads: 0, bytes: 0 };
|
|
1048
|
-
params.onTimed = (
|
|
1048
|
+
params.onTimed = (statement, waitedMs, readMs) => {
|
|
1049
1049
|
timing.reads += 1;
|
|
1050
1050
|
timing.queuedMs += waitedMs;
|
|
1051
1051
|
timing.readMs += readMs;
|
|
1052
|
+
const measured = timing.readsByStatement[statement] ??= { queuedMs: 0, readMs: 0 };
|
|
1053
|
+
measured.queuedMs += waitedMs;
|
|
1054
|
+
measured.readMs += readMs;
|
|
1052
1055
|
};
|
|
1053
1056
|
}
|
|
1054
1057
|
const tracks = await containerOrchestrator.inspect(params, "tracks");
|
|
@@ -194,9 +194,10 @@ export class EncodeAdmission {
|
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
/**
|
|
197
|
-
* Whether
|
|
198
|
-
*
|
|
199
|
-
*
|
|
197
|
+
* Whether an output candidate can take a place on the whole machine. A
|
|
198
|
+
* partial output already in the catalogue uses its measured cost; a
|
|
199
|
+
* not-yet-opened output uses the same candidate estimate as before. The final
|
|
200
|
+
* claim uses `admitsWatching` after registering the output.
|
|
200
201
|
*
|
|
201
202
|
* @param {object} spec
|
|
202
203
|
* @param {object} file
|
|
@@ -207,13 +208,22 @@ export class EncodeAdmission {
|
|
|
207
208
|
if (!address) {
|
|
208
209
|
return { admitted: false, reason: "the output has no address", speedX: null };
|
|
209
210
|
}
|
|
211
|
+
if (this.#host.finished?.(address) === true) {
|
|
212
|
+
return { admitted: true, reason: "everything it serves is already made", speedX: null };
|
|
213
|
+
}
|
|
210
214
|
if ((this.#host.liveRunsByAddress().get(address) ?? 0) > 0) {
|
|
211
215
|
return { admitted: true, reason: "an encoder already runs there", speedX: null };
|
|
212
216
|
}
|
|
213
217
|
if (this.#heldWithoutRun().has(address)) {
|
|
214
218
|
return { admitted: true, reason: "it already holds a place for another viewer", speedX: null };
|
|
215
219
|
}
|
|
216
|
-
|
|
220
|
+
// A partial output already in the catalogue has its measured rate. Use it
|
|
221
|
+
// before the candidate estimate; completed outputs took the return above.
|
|
222
|
+
const existing = this.#host.loadOf(address);
|
|
223
|
+
const hasMeasuredExistingCost = Number.isFinite(existing?.costSec) && existing.costSec > 0;
|
|
224
|
+
const load = hasMeasuredExistingCost
|
|
225
|
+
? existing
|
|
226
|
+
: this.#host.loadForCandidate?.(spec, file) ?? existing;
|
|
217
227
|
return this.#assessCandidate(address, load);
|
|
218
228
|
}
|
|
219
229
|
|
|
@@ -413,7 +413,7 @@ export class EncodeInputs {
|
|
|
413
413
|
// field 2026-10-10, a picture's request took 38 s before its copy began and
|
|
414
414
|
// nothing said on what (torrent-tv/meta#166).
|
|
415
415
|
request.timing ??= { askedAt: this.#now(), attempts: 0, resolutions: 0, resolveMs: 0,
|
|
416
|
-
reads: 0, queuedMs: 0, readMs: 0, heldMs: 0, budgetMs: 0 };
|
|
416
|
+
reads: 0, queuedMs: 0, readMs: 0, readsByStatement: {}, heldMs: 0, budgetMs: 0 };
|
|
417
417
|
request.timing.attempts += 1;
|
|
418
418
|
this.#observe(request, "attempt", { request: request.key, attempt: request.timing.attempts });
|
|
419
419
|
request.promise = (async () => {
|
|
@@ -742,9 +742,12 @@ function awaitedIn(waiting, arrived) {
|
|
|
742
742
|
*/
|
|
743
743
|
function describeTiming(timing, now) {
|
|
744
744
|
if (!timing) return "";
|
|
745
|
+
const statements = Object.entries(timing.readsByStatement ?? {}).map(([statement, measured]) =>
|
|
746
|
+
`${statement} queued ${measured.queuedMs}ms/read ${measured.readMs}ms`).join(", ");
|
|
745
747
|
return `; asked ${Math.max(0, now - timing.askedAt)}ms ago over ${timing.attempts} attempt(s): ` +
|
|
746
748
|
`${timing.resolutions} resolution(s) ${timing.resolveMs}ms ` +
|
|
747
749
|
`(${timing.reads} file read(s): queued ${timing.queuedMs}ms, reading ${timing.readMs}ms), ` +
|
|
750
|
+
(statements ? `by statement: ${statements}, ` : "") +
|
|
748
751
|
`held ranges ${timing.heldMs}ms, budget ${timing.budgetMs}ms` +
|
|
749
752
|
// Where a packet input's walk spent its read: waiting for the same
|
|
750
753
|
// track's earlier walks, or reading the file (r62, torrent-tv/meta#178).
|
|
@@ -228,6 +228,20 @@ export function decideOutputFormat({
|
|
|
228
228
|
rateControl: limit === null ? null : softwareRateControlFor({ width: w, height: h, fps, source, capKbps: limit })
|
|
229
229
|
}));
|
|
230
230
|
};
|
|
231
|
+
let machineRefusal = null;
|
|
232
|
+
const machineAdmissionFor = (spec) => typeof machineAdmission === "function" ? machineAdmission(spec) : null;
|
|
233
|
+
const machineCanContinue = (spec) => machineAdmissionFor(spec)?.admitted !== false;
|
|
234
|
+
const admittedByMachine = (spec) => {
|
|
235
|
+
if (!machineCan(spec.video?.encode?.width ?? 0, spec.video?.encode?.height ?? 0)) {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
const answer = machineAdmissionFor(spec);
|
|
239
|
+
if (answer?.admitted === false) {
|
|
240
|
+
machineRefusal ??= answer;
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
return true;
|
|
244
|
+
};
|
|
231
245
|
const wantedFormats = formatsAt(width, height);
|
|
232
246
|
const wantedSpec = wantedFormats[0];
|
|
233
247
|
const wantedKey = wantedSpec.toKey();
|
|
@@ -247,6 +261,7 @@ export function decideOutputFormat({
|
|
|
247
261
|
wanted: { width, height },
|
|
248
262
|
nextLowerArea: nextRungAreaBelow(buildResolutionLadder(ceiling.w, ceiling.h), height),
|
|
249
263
|
atRisk: measured && !canSustainOutput({ benchmark, ...priced, outputPixelsPerSec: width * height * fps, frame: { width, height } }).sustainable,
|
|
264
|
+
canProduce: (candidate) => machineCanContinue(candidate.spec),
|
|
250
265
|
judge: (candidate) => judge(candidate.spec),
|
|
251
266
|
candidates: servingCandidates({
|
|
252
267
|
wanted: wantedSpec,
|
|
@@ -262,18 +277,6 @@ export function decideOutputFormat({
|
|
|
262
277
|
}
|
|
263
278
|
|
|
264
279
|
let anySizeHeld = false;
|
|
265
|
-
let machineRefusal = null;
|
|
266
|
-
const admittedByMachine = (spec) => {
|
|
267
|
-
if (!machineCan(spec.video?.encode?.width ?? 0, spec.video?.encode?.height ?? 0)) {
|
|
268
|
-
return false;
|
|
269
|
-
}
|
|
270
|
-
const answer = typeof machineAdmission === "function" ? machineAdmission(spec) : null;
|
|
271
|
-
if (answer?.admitted === false) {
|
|
272
|
-
machineRefusal ??= answer;
|
|
273
|
-
return false;
|
|
274
|
-
}
|
|
275
|
-
return true;
|
|
276
|
-
};
|
|
277
280
|
if (machineCan(width, height)) {
|
|
278
281
|
for (const spec of wantedFormats) {
|
|
279
282
|
if (!admittedByMachine(spec)) {
|
|
@@ -48,15 +48,17 @@ import { admissionRank } from "./link-budget.js";
|
|
|
48
48
|
* on this viewer's own ladder; zero when there is none.
|
|
49
49
|
* @param {boolean} params.atRisk - Producing `wanted` here is measured not to
|
|
50
50
|
* keep up with what the file needs. False where nothing measured it.
|
|
51
|
+
* @param {(candidate: Candidate) => boolean} [params.canProduce] - Whether
|
|
52
|
+
* this machine can keep producing an output that is not finished.
|
|
51
53
|
* @param {(candidate: Candidate) => { admitted: boolean, load: object }} params.judge -
|
|
52
54
|
* What this viewer's link does with the candidate's whole load.
|
|
53
55
|
* @param {Candidate[]} params.candidates
|
|
54
56
|
* @returns {{ key: string, answer: object } | null} The output to serve, or
|
|
55
57
|
* null to produce `wanted`.
|
|
56
58
|
*/
|
|
57
|
-
export function chooseServingOutput({ mode, wanted, nextLowerArea, atRisk, judge, candidates }) {
|
|
59
|
+
export function chooseServingOutput({ mode, wanted, nextLowerArea, atRisk, canProduce = () => true, judge, candidates }) {
|
|
58
60
|
const admitted = (candidates ?? [])
|
|
59
|
-
.filter((candidate) => candidate.readyHere === true)
|
|
61
|
+
.filter((candidate) => candidate.readyHere === true && canProduce(candidate))
|
|
60
62
|
.map((candidate) => ({ candidate, answer: judge(candidate) }))
|
|
61
63
|
.filter((one) => one.answer.admitted);
|
|
62
64
|
const areaOf = (one) => one.candidate.width * one.candidate.height;
|
|
@@ -212,15 +212,20 @@ export class AsfContainer extends Container {
|
|
|
212
212
|
const index = await this.readPacketIndex();
|
|
213
213
|
return { times: index.keyframesOf(track.trackNumber), tolerance: 0 };
|
|
214
214
|
}
|
|
215
|
-
async readPacketIndex(interval) {
|
|
215
|
+
async readPacketIndex(interval, { isCurrent } = {}) {
|
|
216
|
+
if (isCurrent?.() === false) return null;
|
|
216
217
|
await this.#readHeader();
|
|
218
|
+
if (isCurrent?.() === false) return null;
|
|
217
219
|
if (!this.#packets && Number.isFinite(interval?.from) && Number.isFinite(interval?.to) && interval.to > interval.from) {
|
|
218
|
-
const window = await this.#readWindow(interval);
|
|
220
|
+
const window = await this.#readWindow(interval, isCurrent);
|
|
221
|
+
if (isCurrent?.() === false) return null;
|
|
219
222
|
if (window) return window;
|
|
220
223
|
}
|
|
224
|
+
if (isCurrent?.() === false) return null;
|
|
221
225
|
if (!this.#packets) {
|
|
222
226
|
const index = await readAsfPackets({ readRange: this.readRange, fileSize: this.fileSize, tracks: this.#tracks,
|
|
223
|
-
info: { ...this.#info, durationSeconds: this.#duration }, state: this.#packetState, packetMemory: this.packetMemory });
|
|
227
|
+
info: { ...this.#info, durationSeconds: this.#duration }, state: this.#packetState, packetMemory: this.packetMemory, isCurrent });
|
|
228
|
+
if (index === null) return null;
|
|
224
229
|
const timeline = this.#tracks.find(track => track.type === "video") ?? this.#tracks.find(track => track.type === "audio");
|
|
225
230
|
const bounds = timeline ? index.boundsOf(timeline.trackNumber) : null;
|
|
226
231
|
if (bounds) {
|
|
@@ -246,17 +251,21 @@ export class AsfContainer extends Container {
|
|
|
246
251
|
* interval, is read again from twice as far back, and from the first packet
|
|
247
252
|
* at the latest; which it reaches is checked on what it read, not assumed.
|
|
248
253
|
*/
|
|
249
|
-
async #readWindow(interval) {
|
|
254
|
+
async #readWindow(interval, isCurrent) {
|
|
250
255
|
const data = await this.#dataLayout();
|
|
256
|
+
if (isCurrent?.() === false) return null;
|
|
251
257
|
if (!data) return null;
|
|
252
258
|
const requested = this.#tracks.filter(track => ["video", "audio"].includes(track.type) &&
|
|
253
259
|
(!interval.trackIds?.length || interval.trackIds.includes(track.trackNumber)));
|
|
254
260
|
if (!requested.length) return null;
|
|
255
261
|
const info = { ...this.#info, durationSeconds: this.#duration };
|
|
256
262
|
for (let lead = this.#info.prerollSeconds + WINDOW_LEAD_SECONDS; ; lead *= 2) {
|
|
263
|
+
if (isCurrent?.() === false) return null;
|
|
257
264
|
const sentFrom = interval.from - lead;
|
|
258
|
-
const first = await this.#packetSentBefore(sentFrom, data, interval.to + WINDOW_LEAD_SECONDS);
|
|
259
|
-
|
|
265
|
+
const first = await this.#packetSentBefore(sentFrom, data, interval.to + WINDOW_LEAD_SECONDS, isCurrent);
|
|
266
|
+
if (isCurrent?.() === false) return null;
|
|
267
|
+
const window = await readAsfWindow({ readRange: this.readRange, data, tracks: this.#tracks, requested, info, first, interval, sentFrom, isCurrent });
|
|
268
|
+
if (isCurrent?.() === false) return null;
|
|
260
269
|
if (window.startCovered || first === 0) return window.index;
|
|
261
270
|
}
|
|
262
271
|
}
|
|
@@ -291,14 +300,16 @@ export class AsfContainer extends Container {
|
|
|
291
300
|
* bytes the window from it to `until` is estimated to take, so the swarm is
|
|
292
301
|
* asked for the packet and the window it begins at once.
|
|
293
302
|
*/
|
|
294
|
-
async #packetSentBefore(seconds, data, until) {
|
|
295
|
-
|
|
303
|
+
async #packetSentBefore(seconds, data, until, isCurrent) {
|
|
304
|
+
if (isCurrent?.() === false) return null;
|
|
305
|
+
let low = 0, high = data.count, lowSent = await this.#sentAt(0, data, 1, isCurrent), highSent = this.#duration;
|
|
306
|
+
if (isCurrent?.() === false) return null;
|
|
296
307
|
if (lowSent > seconds) return 0;
|
|
297
308
|
// Past the last packet nothing is sent before the film ends; the last
|
|
298
309
|
// packet itself is read only when the search needs it.
|
|
299
310
|
if (!(highSent > seconds)) {
|
|
300
311
|
high = data.count - 1;
|
|
301
|
-
highSent = await this.#sentAt(high, data);
|
|
312
|
+
highSent = await this.#sentAt(high, data, 1, isCurrent);
|
|
302
313
|
if (highSent <= seconds) return high;
|
|
303
314
|
}
|
|
304
315
|
for (const [number, at] of this.#sent) {
|
|
@@ -307,12 +318,14 @@ export class AsfContainer extends Container {
|
|
|
307
318
|
else { high = number; highSent = at; }
|
|
308
319
|
}
|
|
309
320
|
for (let guesses = 0; high - low > 1 && seconds - lowSent > WINDOW_SEARCH_SECONDS; guesses++) {
|
|
321
|
+
if (isCurrent?.() === false) return null;
|
|
310
322
|
const perSecond = (high - low) / (highSent - lowSent);
|
|
311
323
|
const aim = seconds - WINDOW_SEARCH_SECONDS / 2;
|
|
312
324
|
const guess = guesses >= 3 || !(perSecond > 0) ? Math.floor((low + high) / 2) : low + Math.floor((aim - lowSent) * perSecond);
|
|
313
325
|
const probe = Math.min(high - 1, Math.max(low + 1, guess));
|
|
314
326
|
const window = perSecond > 0 ? Math.ceil((until - aim) * perSecond) : 1;
|
|
315
|
-
const at = await this.#sentAt(probe, data, Math.max(1, window));
|
|
327
|
+
const at = await this.#sentAt(probe, data, Math.max(1, window), isCurrent);
|
|
328
|
+
if (isCurrent?.() === false) return null;
|
|
316
329
|
if (at <= seconds) { low = probe; lowSent = at; }
|
|
317
330
|
else { high = probe; highSent = at; }
|
|
318
331
|
}
|
|
@@ -323,7 +336,8 @@ export class AsfContainer extends Container {
|
|
|
323
336
|
* When packet `number` is sent. A packet that is not held is said as
|
|
324
337
|
* missing with the `packets` packets from it.
|
|
325
338
|
*/
|
|
326
|
-
async #sentAt(number, data, packets = 1) {
|
|
339
|
+
async #sentAt(number, data, packets = 1, isCurrent) {
|
|
340
|
+
if (isCurrent?.() === false) return null;
|
|
327
341
|
if (!this.#sent.has(number)) {
|
|
328
342
|
const at = data.start + number * data.length;
|
|
329
343
|
try {
|
|
@@ -333,6 +347,6 @@ export class AsfContainer extends Container {
|
|
|
333
347
|
throw new BytesUnavailable(at, Math.min(data.start + data.count * data.length, at + packets * data.length) - 1, 0);
|
|
334
348
|
}
|
|
335
349
|
}
|
|
336
|
-
return this.#sent.get(number);
|
|
350
|
+
return isCurrent?.() === false ? null : this.#sent.get(number);
|
|
337
351
|
}
|
|
338
352
|
}
|
|
@@ -20,7 +20,7 @@ export async function readAsfData({ readRange, fileSize, info }) {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
/** ASF payload addresses, including fragmented and compressed media objects. */
|
|
23
|
-
export async function readAsfPackets({ readRange, fileSize, tracks, info, firstPicture = false, packetMemory, state = {} }) {
|
|
23
|
+
export async function readAsfPackets({ readRange, fileSize, tracks, info, firstPicture = false, packetMemory, state = {}, isCurrent }) {
|
|
24
24
|
if (!state.initialized) {
|
|
25
25
|
const data = await readAsfData({ readRange, fileSize, info });
|
|
26
26
|
Object.assign(state, { initialized: true, at: data.start, end: data.end, count: data.count, packets: 0,
|
|
@@ -30,6 +30,7 @@ export async function readAsfPackets({ readRange, fileSize, tracks, info, firstP
|
|
|
30
30
|
const { end, count, frames, assembling } = state;
|
|
31
31
|
const declared = new Map(tracks.map(track => [track.trackNumber, track]));
|
|
32
32
|
while (at < end && (!count || packets < count)) {
|
|
33
|
+
if (isCurrent?.() === false) return null;
|
|
33
34
|
const previousObjects = new Map([...assembling].map(([id, object]) => [id, { ...object, ranges: [...object.ranges] }]));
|
|
34
35
|
const previousLengths = new Map([...frames].map(([id, stream]) => [id, stream.length]));
|
|
35
36
|
try {
|
|
@@ -37,6 +38,7 @@ export async function readAsfPackets({ readRange, fileSize, tracks, info, firstP
|
|
|
37
38
|
packets++;
|
|
38
39
|
state.at = at;
|
|
39
40
|
state.packets = packets;
|
|
41
|
+
if (isCurrent?.() === false) return null;
|
|
40
42
|
if (firstPicture) {
|
|
41
43
|
const video = tracks.find(track => track.type === "video");
|
|
42
44
|
const stream = video && frames.get(video.trackNumber);
|
|
@@ -105,7 +107,7 @@ export async function readAsfPacketTime({ readRange, at, end, info }) {
|
|
|
105
107
|
* @param {number} params.sentFrom - When packet `first` is sent, as far as the caller knows.
|
|
106
108
|
* @returns {Promise<{ index: PacketIndex, startCovered: boolean }>}
|
|
107
109
|
*/
|
|
108
|
-
export async function readAsfWindow({ readRange, data, tracks, requested, info, first, interval, sentFrom }) {
|
|
110
|
+
export async function readAsfWindow({ readRange, data, tracks, requested, info, first, interval, sentFrom, isCurrent }) {
|
|
109
111
|
const declared = new Map(tracks.map(track => [track.trackNumber, track]));
|
|
110
112
|
// Not admitted to IndexMemory: one segment's records, a few hundred, held
|
|
111
113
|
// by the request that asked and dropped with it. Nothing owns a window to
|
|
@@ -125,23 +127,30 @@ export async function readAsfWindow({ readRange, data, tracks, requested, info,
|
|
|
125
127
|
const held = (start, end) => buffer !== null && start >= bufferAt && end < bufferAt + buffer.length;
|
|
126
128
|
const read = (start, end) => held(start, end) ? buffer.subarray(start - bufferAt, end - bufferAt + 1) : readRange(start, end);
|
|
127
129
|
const load = async () => {
|
|
130
|
+
if (isCurrent?.() === false) return false;
|
|
128
131
|
try {
|
|
129
132
|
buffer = await readRange(at, Math.min(dataEnd - 1, at + WINDOW_PORTION_PACKETS * data.length - 1));
|
|
130
133
|
} catch (error) {
|
|
134
|
+
if (isCurrent?.() === false) return false;
|
|
131
135
|
if (!isUnavailable(error)) throw error;
|
|
132
136
|
try {
|
|
133
137
|
buffer = await readRange(at, Math.min(dataEnd - 1, at + data.length - 1));
|
|
134
138
|
} catch (missing) {
|
|
139
|
+
if (isCurrent?.() === false) return false;
|
|
135
140
|
if (!isUnavailable(missing)) throw missing;
|
|
136
141
|
const ahead = Math.ceil(Math.max(0, interval.to + WINDOW_LEAD_SECONDS - sent) * rate);
|
|
137
142
|
throw new BytesUnavailable(at, Math.min(dataEnd - 1, Math.max(at + data.length, at + ahead) - 1), 0);
|
|
138
143
|
}
|
|
139
144
|
}
|
|
145
|
+
if (isCurrent?.() === false) return false;
|
|
140
146
|
bufferAt = at;
|
|
147
|
+
return true;
|
|
141
148
|
};
|
|
142
149
|
for (;;) {
|
|
150
|
+
if (isCurrent?.() === false) return null;
|
|
143
151
|
if (at >= dataEnd) { reachedEnd = true; break; }
|
|
144
|
-
if (!held(at, Math.min(dataEnd, at + data.length) - 1)
|
|
152
|
+
if (!held(at, Math.min(dataEnd, at + data.length) - 1) && !(await load())) return null;
|
|
153
|
+
if (isCurrent?.() === false) return null;
|
|
145
154
|
({ next: at, sent } = await readPacket({ readRange: read, at, end: data.end, info, declared, frames, assembling, started }));
|
|
146
155
|
for (const id of ids) {
|
|
147
156
|
const stream = frames.get(id), times = after.get(id);
|