@torrent-tv/proxy 2.95.30 → 2.95.31
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 +7 -0
- package/services/encode/EncodeInputs.js +247 -40
- package/services/encode/EncodeOrchestrator.js +18 -1
- package/services/encode/EncodeRun.js +12 -0
- package/services/encode/EncodeRuns.js +46 -5
- package/services/encode/OriginalInput.js +14 -4
- package/services/encode/source-command.js +20 -6
- package/services/server/wire-outputs.js +2 -0
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -690,6 +690,13 @@ export async function startProxyServer({
|
|
|
690
690
|
const torrent = torrentPool.knownTorrent(source.sourceKey);
|
|
691
691
|
return torrent ? torrentPool.readHeldRangesOf(torrent, source.fileIndex, ranges, maxBytes) : null;
|
|
692
692
|
},
|
|
693
|
+
// Which bytes of a file are downloaded whole now — what an original
|
|
694
|
+
// input's stretch may extend over. A snapshot: the copy that follows
|
|
695
|
+
// holds the pieces it reads, and refuses where one has gone since.
|
|
696
|
+
readEncodeHeldRanges: async (source) => {
|
|
697
|
+
const torrent = torrentPool.knownTorrent(source.sourceKey);
|
|
698
|
+
return torrent ? torrentPool.heldRangesOf(torrent, source.fileIndex) : [];
|
|
699
|
+
},
|
|
693
700
|
enabled: transcodeAudio,
|
|
694
701
|
keyframeTables,
|
|
695
702
|
ffmpegBin,
|
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
import { admitInput } from "./AdmittedInput.js";
|
|
2
|
-
import { admitOriginalInput } from "./OriginalInput.js";
|
|
2
|
+
import { admitOriginalInput, originalInputBytes } from "./OriginalInput.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* How many recent readings a learned figure here is taken from: long enough
|
|
6
|
+
* that one reading does not move the answer, short enough that the answer
|
|
7
|
+
* still follows the host. The same count as `run-costs.js`.
|
|
8
|
+
*/
|
|
9
|
+
const RECENT_READINGS = 20;
|
|
10
|
+
|
|
11
|
+
/** @param {number[]} values @returns {number | null} */
|
|
12
|
+
function middleOf(values) {
|
|
13
|
+
if (values.length === 0) return null;
|
|
14
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
15
|
+
const middle = Math.floor(sorted.length / 2);
|
|
16
|
+
return sorted.length % 2 === 0 ? (sorted[middle - 1] + sorted[middle]) / 2 : sorted[middle];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** @param {number[]} readings @param {number} value */
|
|
20
|
+
function keep(readings, value) {
|
|
21
|
+
readings.push(value);
|
|
22
|
+
while (readings.length > RECENT_READINGS) readings.shift();
|
|
23
|
+
}
|
|
3
24
|
|
|
4
25
|
/** Event-driven preparation; a synchronous run request can take only ready input. */
|
|
5
26
|
export class EncodeInputs {
|
|
@@ -8,25 +29,34 @@ export class EncodeInputs {
|
|
|
8
29
|
#memoryRevision = 0;
|
|
9
30
|
#held = 0;
|
|
10
31
|
#allowed = 0;
|
|
32
|
+
/** What each preparing request asks of memory: all it would use, and the least it can run with. */
|
|
11
33
|
#wanted = new Map();
|
|
34
|
+
/** Bytes per millisecond of recent original-input copies, read where the copy is made. */
|
|
35
|
+
#copyRates = [];
|
|
36
|
+
/** Seconds an encoder took to open its original input, per output. */
|
|
37
|
+
#opens = new Map();
|
|
12
38
|
#resolve;
|
|
13
39
|
#read;
|
|
40
|
+
#heldRanges;
|
|
14
41
|
#revise;
|
|
15
42
|
#changed;
|
|
16
43
|
#failed;
|
|
17
44
|
#capacity;
|
|
18
45
|
#urgent;
|
|
19
46
|
#log;
|
|
47
|
+
#now;
|
|
20
48
|
|
|
21
|
-
constructor({ resolve, readRanges, reviseBudget, changed, failed, capacity = () => null, urgent = () => true, log = () => {} }) {
|
|
49
|
+
constructor({ resolve, readRanges, heldRanges = null, reviseBudget, changed, failed, capacity = () => null, urgent = () => true, log = () => {}, now = Date.now }) {
|
|
22
50
|
this.#resolve = resolve;
|
|
23
51
|
this.#read = readRanges;
|
|
52
|
+
this.#heldRanges = heldRanges;
|
|
24
53
|
this.#revise = reviseBudget;
|
|
25
54
|
this.#changed = changed;
|
|
26
55
|
this.#failed = failed;
|
|
27
56
|
this.#capacity = capacity;
|
|
28
57
|
this.#urgent = urgent;
|
|
29
58
|
this.#log = log;
|
|
59
|
+
this.#now = now;
|
|
30
60
|
}
|
|
31
61
|
|
|
32
62
|
/** One line when a request's state changes, so a wait that never ends names what it waits for. */
|
|
@@ -38,16 +68,16 @@ export class EncodeInputs {
|
|
|
38
68
|
}
|
|
39
69
|
|
|
40
70
|
held() { return this.#held; }
|
|
41
|
-
wanted() { return this.#held + [...this.#wanted.values()].reduce((sum,
|
|
71
|
+
wanted() { return this.#held + [...this.#wanted.values()].reduce((sum, asked) => sum + asked.wanted, 0); }
|
|
42
72
|
required() {
|
|
43
73
|
let bytes = this.#held;
|
|
44
|
-
for (const [key,
|
|
45
|
-
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
if (
|
|
74
|
+
for (const [key, asked] of this.#wanted) {
|
|
75
|
+
// The encode plan already chose this finite input. Its least runnable
|
|
76
|
+
// part must fit before production can advance, even when playback has
|
|
77
|
+
// not reached it yet. Sharing against speculative whole-file demand can
|
|
78
|
+
// otherwise deny it forever, so urgency must not decide that minimum
|
|
79
|
+
// again. What a stretch asks beyond it is shared like any other want.
|
|
80
|
+
if (this.#requests.has(key)) bytes += asked.required;
|
|
51
81
|
}
|
|
52
82
|
return bytes;
|
|
53
83
|
}
|
|
@@ -87,9 +117,30 @@ export class EncodeInputs {
|
|
|
87
117
|
this.#retryMemory();
|
|
88
118
|
}
|
|
89
119
|
|
|
120
|
+
/**
|
|
121
|
+
* The input of a run starting at `from`, once it is prepared.
|
|
122
|
+
*
|
|
123
|
+
* `to` is the last number the run may reach. A packet input is prepared for
|
|
124
|
+
* `from` alone; an original input for the longest stretch from `from` that
|
|
125
|
+
* is present, fits memory and copies no longer than an open of that input.
|
|
126
|
+
* The answer carries the stretch it holds as `from` and `to`.
|
|
127
|
+
*/
|
|
90
128
|
take(output, from, to) {
|
|
91
|
-
const key =
|
|
92
|
-
|
|
129
|
+
const key = keyOf(output, from);
|
|
130
|
+
let existing = this.#requests.get(key);
|
|
131
|
+
if (existing && existing.to !== to) {
|
|
132
|
+
// The plan's bound for this start moved. A copy already made beyond it
|
|
133
|
+
// would be a run over somebody else's stretch, so it is given back; one
|
|
134
|
+
// being made is checked when it is done, and one waiting for bytes or
|
|
135
|
+
// memory takes the new bound when its own event prepares it again.
|
|
136
|
+
existing.to = to;
|
|
137
|
+
if (existing.result?.kind === "result" && existing.result.to > to) {
|
|
138
|
+
this.#requests.delete(key);
|
|
139
|
+
this.#wanted.delete(key);
|
|
140
|
+
existing.result.release();
|
|
141
|
+
existing = undefined;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
93
144
|
if (existing?.result?.kind === "result") {
|
|
94
145
|
this.#requests.delete(key);
|
|
95
146
|
return existing.result;
|
|
@@ -102,7 +153,7 @@ export class EncodeInputs {
|
|
|
102
153
|
}
|
|
103
154
|
|
|
104
155
|
async acquire(output, from, to) {
|
|
105
|
-
const key =
|
|
156
|
+
const key = keyOf(output, from);
|
|
106
157
|
const ready = this.take(output, from, to);
|
|
107
158
|
if (ready) return ready;
|
|
108
159
|
await this.#requests.get(key)?.promise;
|
|
@@ -125,41 +176,70 @@ export class EncodeInputs {
|
|
|
125
176
|
this.#wanted.delete(key);
|
|
126
177
|
request.result?.release?.();
|
|
127
178
|
}
|
|
179
|
+
this.#opens.delete(addressOf(output));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* How long an encoder took to open this output's original input: from its
|
|
184
|
+
* spawn to the name of its first closed piece, less what encoding that piece
|
|
185
|
+
* costs. Measured by whoever ran it, kept here where a stretch is sized.
|
|
186
|
+
*
|
|
187
|
+
* @param {object} output
|
|
188
|
+
* @param {number} seconds
|
|
189
|
+
*/
|
|
190
|
+
noteOpen(output, seconds) {
|
|
191
|
+
if (!Number.isFinite(seconds) || seconds < 0) return;
|
|
192
|
+
const readings = this.#opens.get(addressOf(output)) ?? [];
|
|
193
|
+
keep(readings, seconds);
|
|
194
|
+
this.#opens.set(addressOf(output), readings);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* How many bytes a stretch may hold so that copying it takes no longer than
|
|
199
|
+
* opening the input it replaces: the recent copy rate times this output's
|
|
200
|
+
* recent open. Beyond it the open's share of each piece falls below the
|
|
201
|
+
* copy's, which is the same for every piece; a longer stretch buys less and
|
|
202
|
+
* delays the run's start more. Null until both have been measured, and the
|
|
203
|
+
* run then gets one piece, as it did before either reading existed.
|
|
204
|
+
*
|
|
205
|
+
* @param {object} output
|
|
206
|
+
* @returns {number | null}
|
|
207
|
+
*/
|
|
208
|
+
stretchBytesFor(output) {
|
|
209
|
+
const rate = middleOf(this.#copyRates);
|
|
210
|
+
const open = middleOf(this.#opens.get(addressOf(output)) ?? []);
|
|
211
|
+
return rate === null || open === null ? null : Math.floor(rate * open * 1000);
|
|
128
212
|
}
|
|
129
213
|
|
|
130
214
|
#prepare(request) {
|
|
131
215
|
request.pending = true;
|
|
216
|
+
request.stale = false;
|
|
132
217
|
request.revision = this.#revision;
|
|
133
218
|
request.memoryRevision = this.#memoryRevision;
|
|
134
219
|
request.promise = (async () => {
|
|
135
|
-
const resolved = await this.#resolve(request.output, request.from, request.
|
|
220
|
+
const resolved = await this.#resolve(request.output, request.from, request.from);
|
|
136
221
|
if (this.#requests.get(request.key) !== request) return;
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
};
|
|
148
|
-
if (this.#requests.get(request.key) !== request || this.#allowed - this.#held < bytes) return null;
|
|
149
|
-
this.#wanted.delete(request.key);
|
|
150
|
-
this.#held += bytes;
|
|
151
|
-
let released = false;
|
|
152
|
-
return () => {
|
|
153
|
-
if (!released) {
|
|
154
|
-
released = true; this.#held -= bytes;
|
|
155
|
-
queueMicrotask(() => this.memoryChanged());
|
|
156
|
-
}
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
}) : resolved;
|
|
222
|
+
let result;
|
|
223
|
+
if (resolved.kind !== "result") {
|
|
224
|
+
result = resolved;
|
|
225
|
+
} else if (resolved.sources?.every(source => source.input.original === true)) {
|
|
226
|
+
result = await this.#admitStretch(request, resolved.sources);
|
|
227
|
+
if (!result) return;
|
|
228
|
+
} else {
|
|
229
|
+
result = await admitInput({ sources: resolved.sources, readRanges: this.#read, reserve: this.#reserveFor(request) });
|
|
230
|
+
if (result.kind === "result") Object.assign(result, { from: request.from, to: request.from });
|
|
231
|
+
}
|
|
160
232
|
if (this.#requests.get(request.key) !== request) { result.release?.(); return; }
|
|
233
|
+
if (result.kind === "result" && result.to > request.to) {
|
|
234
|
+
// The plan narrowed this start while the copy was being made.
|
|
235
|
+
result.release();
|
|
236
|
+
request.result = null;
|
|
237
|
+
request.stale = true;
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
161
240
|
request.result = result;
|
|
162
|
-
this.#note(request, result.kind === "result" ? `ready, ${result.bytes ?? "?"} bytes`
|
|
241
|
+
this.#note(request, result.kind === "result" ? `ready #${result.from}..#${result.to}, ${result.bytes ?? "?"} bytes` +
|
|
242
|
+
(Number.isFinite(result.copyMs) ? `, copied in ${result.copyMs}ms` : "")
|
|
163
243
|
: `${result.kind}${result.reason ? ` ${result.reason}` : ""}${Number.isFinite(result.bytes) ? ` ${result.bytes} bytes` : ""}`);
|
|
164
244
|
if (result.kind === "result" || result.kind === "terminal") this.#changed(request.output, result);
|
|
165
245
|
})().catch(error => {
|
|
@@ -170,10 +250,137 @@ export class EncodeInputs {
|
|
|
170
250
|
}).finally(() => {
|
|
171
251
|
request.pending = false;
|
|
172
252
|
if (request.result?.kind !== "needs-memory") this.#wanted.delete(request.key);
|
|
173
|
-
const changed = request.result?.kind === "needs-memory"
|
|
174
|
-
? request.memoryRevision !== this.#memoryRevision : request.revision !== this.#revision;
|
|
253
|
+
const changed = request.stale || (request.result?.kind === "needs-memory"
|
|
254
|
+
? request.memoryRevision !== this.#memoryRevision : request.revision !== this.#revision);
|
|
175
255
|
if (this.#requests.get(request.key) === request && changed &&
|
|
176
256
|
request.result?.kind !== "result" && request.result?.kind !== "terminal") this.#prepare(request);
|
|
177
257
|
});
|
|
178
258
|
}
|
|
259
|
+
|
|
260
|
+
/** The reservation a packet input makes: all of it, or nothing. */
|
|
261
|
+
#reserveFor(request) {
|
|
262
|
+
return async bytes => {
|
|
263
|
+
this.#wanted.set(request.key, { wanted: bytes, required: bytes });
|
|
264
|
+
await this.#revise();
|
|
265
|
+
const capacity = this.#capacity();
|
|
266
|
+
if (capacity !== null && bytes > capacity && this.#urgent(request.output, request.from)) return {
|
|
267
|
+
kind: "terminal", reason: "source-input-exceeds-memory-capacity", bytes, capacity
|
|
268
|
+
};
|
|
269
|
+
return this.#hold(request, bytes);
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Take `bytes` of the allowance for this request now, or answer null. */
|
|
274
|
+
#hold(request, bytes) {
|
|
275
|
+
if (this.#requests.get(request.key) !== request || this.#allowed - this.#held < bytes) return null;
|
|
276
|
+
this.#wanted.delete(request.key);
|
|
277
|
+
this.#held += bytes;
|
|
278
|
+
let released = false;
|
|
279
|
+
return () => {
|
|
280
|
+
if (!released) {
|
|
281
|
+
released = true; this.#held -= bytes;
|
|
282
|
+
queueMicrotask(() => this.memoryChanged());
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Choose and copy one finite stretch of an original input, starting at the
|
|
289
|
+
* request's `from` and ending at the largest `e <= to` such that every byte
|
|
290
|
+
* of `from … e` is present now, the stretch copies no longer than an open,
|
|
291
|
+
* and it fits the memory the budget allows. One piece is the least it runs
|
|
292
|
+
* with; only that least is required of the budget, and only that least being
|
|
293
|
+
* larger than the policy's whole capacity is terminal.
|
|
294
|
+
*
|
|
295
|
+
* @returns {Promise<object | undefined>} Undefined when the request was withdrawn.
|
|
296
|
+
*/
|
|
297
|
+
async #admitStretch(request, firstSources) {
|
|
298
|
+
const { output, from } = request;
|
|
299
|
+
const minimum = originalInputBytes(firstSources);
|
|
300
|
+
const stretches = new Map([[from, { sources: firstSources, bytes: minimum }]]);
|
|
301
|
+
const held = new Map();
|
|
302
|
+
const at = async end => {
|
|
303
|
+
if (!stretches.has(end)) {
|
|
304
|
+
// A longer stretch that cannot be stated is not one to copy; the
|
|
305
|
+
// shorter one already stated still runs.
|
|
306
|
+
const resolved = await this.#resolve(output, from, end).catch(() => null);
|
|
307
|
+
stretches.set(end, resolved?.kind === "result" && resolved.sources?.every(source => source.input.original === true)
|
|
308
|
+
? { sources: resolved.sources, bytes: originalInputBytes(resolved.sources) } : null);
|
|
309
|
+
}
|
|
310
|
+
return stretches.get(end);
|
|
311
|
+
};
|
|
312
|
+
const fits = async (end, maxBytes) => {
|
|
313
|
+
const stretch = await at(end);
|
|
314
|
+
return Boolean(stretch) && stretch.bytes <= maxBytes && await this.#present(stretch.sources, held);
|
|
315
|
+
};
|
|
316
|
+
// Grows by doubling and then halves back: a handful of resolutions find
|
|
317
|
+
// the end whether it is a piece away or the rest of the film.
|
|
318
|
+
const longest = async maxBytes => {
|
|
319
|
+
let good = from, bad = null, step = 1;
|
|
320
|
+
while (bad === null) {
|
|
321
|
+
const end = good + step;
|
|
322
|
+
if (end > request.to) bad = request.to + 1;
|
|
323
|
+
else if (await fits(end, maxBytes)) { good = end; step *= 2; }
|
|
324
|
+
else bad = end;
|
|
325
|
+
}
|
|
326
|
+
while (bad - good > 1) {
|
|
327
|
+
const middle = Math.floor((good + bad) / 2);
|
|
328
|
+
if (await fits(middle, maxBytes)) good = middle; else bad = middle;
|
|
329
|
+
}
|
|
330
|
+
return good;
|
|
331
|
+
};
|
|
332
|
+
const target = this.stretchBytesFor(output);
|
|
333
|
+
let end = target === null || this.#heldRanges === null ? from : await longest(target);
|
|
334
|
+
if (this.#requests.get(request.key) !== request) return undefined;
|
|
335
|
+
this.#wanted.set(request.key, { wanted: stretches.get(end).bytes, required: minimum });
|
|
336
|
+
await this.#revise();
|
|
337
|
+
if (this.#requests.get(request.key) !== request) return undefined;
|
|
338
|
+
const capacity = this.#capacity();
|
|
339
|
+
if (capacity !== null && minimum > capacity && this.#urgent(output, from)) {
|
|
340
|
+
return { kind: "terminal", reason: "source-input-exceeds-memory-capacity", bytes: minimum, capacity };
|
|
341
|
+
}
|
|
342
|
+
const roomNow = () => Math.min(capacity ?? Number.POSITIVE_INFINITY, this.#allowed - this.#held);
|
|
343
|
+
let room = roomNow();
|
|
344
|
+
let result;
|
|
345
|
+
for (;;) {
|
|
346
|
+
if (minimum > room) {
|
|
347
|
+
// Wait for memory with the least it can run with, not the larger stretch.
|
|
348
|
+
this.#wanted.set(request.key, { wanted: minimum, required: minimum });
|
|
349
|
+
return { kind: "needs-memory", bytes: minimum };
|
|
350
|
+
}
|
|
351
|
+
if (stretches.get(end).bytes > room) {
|
|
352
|
+
end = await longest(Math.min(target ?? minimum, room));
|
|
353
|
+
if (this.#requests.get(request.key) !== request) return undefined;
|
|
354
|
+
}
|
|
355
|
+
const chosen = stretches.get(end);
|
|
356
|
+
this.#wanted.set(request.key, { wanted: chosen.bytes, required: chosen.bytes });
|
|
357
|
+
result = await admitOriginalInput({ sources: chosen.sources, readRanges: this.#read, now: this.#now,
|
|
358
|
+
reserve: async bytes => this.#hold(request, bytes) });
|
|
359
|
+
// Another output's copy can take the room this one was sized for while
|
|
360
|
+
// it was being chosen. A shorter stretch is tried at once rather than
|
|
361
|
+
// waiting for a memory change that may never come.
|
|
362
|
+
if (result.kind !== "needs-memory" || this.#requests.get(request.key) !== request || roomNow() >= room) break;
|
|
363
|
+
room = roomNow();
|
|
364
|
+
}
|
|
365
|
+
if (result.kind === "needs-memory") this.#wanted.set(request.key, { wanted: minimum, required: minimum });
|
|
366
|
+
if (result.kind === "result") {
|
|
367
|
+
Object.assign(result, { from, to: end });
|
|
368
|
+
if (result.copyMs > 0) keep(this.#copyRates, result.bytes / result.copyMs);
|
|
369
|
+
}
|
|
370
|
+
return result;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** Whether every range of these sources is downloaded whole now. */
|
|
374
|
+
async #present(sources, held) {
|
|
375
|
+
for (const source of sources) {
|
|
376
|
+
const name = `${source.sourceKey}:${source.fileIndex}`;
|
|
377
|
+
if (!held.has(name)) held.set(name, await Promise.resolve().then(() => this.#heldRanges(source)).catch(() => null) ?? []);
|
|
378
|
+
const ranges = held.get(name);
|
|
379
|
+
if (!source.input.ranges.every(([start, end]) => ranges.some(([first, last]) => first <= start && last >= end))) return false;
|
|
380
|
+
}
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
179
383
|
}
|
|
384
|
+
|
|
385
|
+
function addressOf(output) { return output.outputKey ?? output.id; }
|
|
386
|
+
function keyOf(output, from) { return `${addressOf(output)}:${from}`; }
|
|
@@ -847,7 +847,24 @@ export class EncodeOrchestrator {
|
|
|
847
847
|
// between is synchronous, and a process cannot say a word before the next
|
|
848
848
|
// tick. What it removes is the second act — building a run and starting it
|
|
849
849
|
// were two steps, and two owners each performed the second one.
|
|
850
|
-
|
|
850
|
+
//
|
|
851
|
+
// A run whose input is one copied stretch can make nothing past it, so it
|
|
852
|
+
// claims that stretch and not the plan's longer bound: the rest stays free
|
|
853
|
+
// for the next run (torrent-tv/meta#158). Every other run claims as before.
|
|
854
|
+
const through = Number.isInteger(run.inputThrough) ? run.inputThrough : to;
|
|
855
|
+
this.coverageOf(address).claim(run, from, endOfRun({ from, to: through }));
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* The speed one encoder on this output is taken to run at: the fastest live
|
|
860
|
+
* run's measurement, else what this output last measured or the startup
|
|
861
|
+
* figures predict. Zero where none exists.
|
|
862
|
+
*
|
|
863
|
+
* @param {string} address
|
|
864
|
+
* @returns {number}
|
|
865
|
+
*/
|
|
866
|
+
speedOn(address) {
|
|
867
|
+
return this.#speedOn(address, this.runsOn(address).filter((run) => run.isAlive));
|
|
851
868
|
}
|
|
852
869
|
|
|
853
870
|
/**
|
|
@@ -63,6 +63,9 @@ const MICROSECONDS_PER_SECOND = 1_000_000;
|
|
|
63
63
|
* @property {number} reached - The last number it finished, or `from - 1` when
|
|
64
64
|
* it finished none.
|
|
65
65
|
* @property {number} livedMs
|
|
66
|
+
* @property {number | null} [firstNamedMs] - Spawn to the moment the encoder
|
|
67
|
+
* named its first closed piece: the open of its input plus that one piece's
|
|
68
|
+
* encoding, without the wait for the following piece that publication adds.
|
|
66
69
|
* @property {boolean} normal - Whether this ending is the expected one.
|
|
67
70
|
* @property {string} lastError - The last thing ffmpeg said on stderr.
|
|
68
71
|
* @property {string | null} provenName - The last piece this run named while it
|
|
@@ -156,6 +159,13 @@ export class EncodeRun {
|
|
|
156
159
|
*/
|
|
157
160
|
#firstOutputAt = 0;
|
|
158
161
|
|
|
162
|
+
/**
|
|
163
|
+
* When the encoder first NAMED a closed piece of its own stretch. A piece is
|
|
164
|
+
* published only once the next one is named, so the first output above holds
|
|
165
|
+
* two pieces' encoding; this holds one, and is what an open is read from.
|
|
166
|
+
*/
|
|
167
|
+
#firstNamedAt = 0;
|
|
168
|
+
|
|
159
169
|
/** The film made and the run's own working time, as of the last progress report. */
|
|
160
170
|
#workSample = null;
|
|
161
171
|
|
|
@@ -583,6 +593,7 @@ export class EncodeRun {
|
|
|
583
593
|
}
|
|
584
594
|
if (this.#publicationError) break;
|
|
585
595
|
const index = this.indexOfName(name);
|
|
596
|
+
if (this.#firstNamedAt === 0 && Number.isInteger(index) && index >= this.from) this.#firstNamedAt = this.now();
|
|
586
597
|
const endOfWork = Number.isFinite(endOfRun(this)) ? this.to : this.lastSegmentIndex();
|
|
587
598
|
// A packet crossing the final cut can flush a following file. It may
|
|
588
599
|
// complete its predecessor, but no unadmitted interval can be published.
|
|
@@ -899,6 +910,7 @@ export class EncodeRun {
|
|
|
899
910
|
this.#firstOutputAt > 0 && this.#startedAt > 0
|
|
900
911
|
? this.#firstOutputAt - this.#startedAt
|
|
901
912
|
: null,
|
|
913
|
+
firstNamedMs: this.#firstNamedAt > 0 && this.#startedAt > 0 ? this.#firstNamedAt - this.#startedAt : null,
|
|
902
914
|
normal: ending === ENCODE_EXIT.COMPLETE,
|
|
903
915
|
lastError: this.lastError
|
|
904
916
|
};
|
|
@@ -809,12 +809,23 @@ export class EncodeRuns {
|
|
|
809
809
|
return null;
|
|
810
810
|
}
|
|
811
811
|
this.#host.encodeInputs?.retain(session, this.#host.encodeOrchestrator.wantedSegmentsOn(session.outputKey));
|
|
812
|
-
|
|
812
|
+
// THE LAST NUMBER THIS RUN MAY REACH is the plan's; how far its input
|
|
813
|
+
// really reaches is the input's answer. An original input is one copied
|
|
814
|
+
// stretch, and the run is given exactly that stretch (torrent-tv/meta#158).
|
|
815
|
+
const segmentCount = Number(session.timeline?.segmentCount) || 0;
|
|
816
|
+
const mayReach = Number.isInteger(ordered?.to) && ordered.to >= startIndex ? ordered.to
|
|
817
|
+
: segmentCount > startIndex ? segmentCount - 1 : startIndex;
|
|
818
|
+
const admittedInput = this.#host.encodeInputs?.take(session, startIndex, mayReach) ?? null;
|
|
813
819
|
if (!admittedInput) return null;
|
|
820
|
+
const inputEnd = admittedInput.original ? admittedInput.to : startIndex;
|
|
821
|
+
// The stretch is part of an original input's identity: two stretches from
|
|
822
|
+
// one start can hold the same padded bytes, and a stretch that failed is
|
|
823
|
+
// refused only while it is the same stretch.
|
|
814
824
|
const inputParameters = admittedInput ? { outputKey: session.outputKey, encoder: this.#host.videoEncoder.name,
|
|
815
825
|
width: session.output.encodeWidth, height: session.output.encodeHeight, fps: session.output.outputFps,
|
|
816
826
|
preset: session.output.softwarePreset, tonemap: session.output.applyTonemap,
|
|
817
|
-
rateControl: session.spec.video?.encode?.rateControl ?? null
|
|
827
|
+
rateControl: session.spec.video?.encode?.rateControl ?? null,
|
|
828
|
+
...(admittedInput.original ? { interval: [startIndex, inputEnd] } : {}) } : null;
|
|
818
829
|
const inputKey = admittedInput ? this.#inputFailures.key(admittedInput, inputParameters) : null;
|
|
819
830
|
if (inputKey && this.#inputFailures.failure(session.outputKey, startIndex, inputKey)) {
|
|
820
831
|
admittedInput.release();
|
|
@@ -842,7 +853,7 @@ export class EncodeRuns {
|
|
|
842
853
|
// an argument. Reading it off the coverage map a second time was the last
|
|
843
854
|
// remaining second answer to that question: the plan computed `to`, passed
|
|
844
855
|
// it, and the parameter list did not name it.
|
|
845
|
-
const runEnd = admittedInput ?
|
|
856
|
+
const runEnd = admittedInput ? inputEnd : Number.isInteger(ordered?.to)
|
|
846
857
|
? ordered.to
|
|
847
858
|
: this.#runEndFrom(session, startIndex, null);
|
|
848
859
|
// The restart backs off a segment or two from what was asked for, so the
|
|
@@ -914,7 +925,8 @@ export class EncodeRuns {
|
|
|
914
925
|
audioSourceTrackIndex: session.spec.audioSourceTrackIndex,
|
|
915
926
|
rateControl: session.spec.video?.encode?.rateControl ?? null,
|
|
916
927
|
startIndex,
|
|
917
|
-
endIndex:
|
|
928
|
+
endIndex: admittedInput.original ? inputEnd
|
|
929
|
+
: Number.isInteger(ordered?.to) ? ordered.to : runEnd >= startIndex ? runEnd : session.timeline.segmentCount - 1,
|
|
918
930
|
videoEncoder: this.#host.videoEncoder,
|
|
919
931
|
segmentDurationSec: this.#host.segmentDurationSec
|
|
920
932
|
});
|
|
@@ -1017,6 +1029,7 @@ export class EncodeRuns {
|
|
|
1017
1029
|
`${reads.count} read(s), ${reads.bytes} bytes, first at ${since(reads.firstAt)}, last at ${since(reads.lastAt)}, ` +
|
|
1018
1030
|
`ended at ${Date.now() - run.startedAt}ms`);
|
|
1019
1031
|
}
|
|
1032
|
+
this.#noteOpen(session, run, ended);
|
|
1020
1033
|
admittedInput.release();
|
|
1021
1034
|
}
|
|
1022
1035
|
this.noteRunEnded(session, run, ended);
|
|
@@ -1025,7 +1038,13 @@ export class EncodeRuns {
|
|
|
1025
1038
|
if (admittedInput.original) {
|
|
1026
1039
|
run.originalInput = admittedInput;
|
|
1027
1040
|
run.inputFingerprint = admittedInput.fingerprint;
|
|
1028
|
-
|
|
1041
|
+
// Every number of the stretch answers to its one key, so a failure
|
|
1042
|
+
// anywhere inside it is recorded against the stretch that was commanded.
|
|
1043
|
+
run.admittedInputKeys = new Map(Array.from({ length: inputEnd - safeIndex + 1 }, (_, offset) => [safeIndex + offset, inputKey]));
|
|
1044
|
+
// What this run can make is what its input holds, and that is what it
|
|
1045
|
+
// claims; the plan's longer bound would read as made by a run that
|
|
1046
|
+
// cannot reach it.
|
|
1047
|
+
run.inputThrough = inputEnd;
|
|
1029
1048
|
run.process.stdin.end();
|
|
1030
1049
|
} else if (admittedInput) {
|
|
1031
1050
|
run.inputFingerprint = admittedInput.fingerprint;
|
|
@@ -1084,6 +1103,28 @@ export class EncodeRuns {
|
|
|
1084
1103
|
}
|
|
1085
1104
|
}
|
|
1086
1105
|
|
|
1106
|
+
/**
|
|
1107
|
+
* How long this run took to open its original input, for sizing the next
|
|
1108
|
+
* stretch: from its spawn to the name of its first closed piece, less what
|
|
1109
|
+
* encoding that piece costs at the run's speed — its own where it measured
|
|
1110
|
+
* one, else the output's. Nothing is noted where either is unknown.
|
|
1111
|
+
*
|
|
1112
|
+
* @param {object} session
|
|
1113
|
+
* @param {import("./EncodeRun.js").EncodeRun} run
|
|
1114
|
+
* @param {import("./EncodeRun.js").RunEnded} ended
|
|
1115
|
+
* @returns {void}
|
|
1116
|
+
*/
|
|
1117
|
+
#noteOpen(session, run, ended) {
|
|
1118
|
+
const grid = session.timeline?.published ?? session.timeline?.boundaries;
|
|
1119
|
+
const pieceSeconds = Number(grid?.[run.from + 1]) - Number(grid?.[run.from]);
|
|
1120
|
+
const speed = run.speedX > 0 ? run.speedX : this.#host.encodeOrchestrator.speedOn?.(session.outputKey) ?? 0;
|
|
1121
|
+
if (!Number.isFinite(ended.firstNamedMs) || !(pieceSeconds > 0) || !(speed > 0)) return;
|
|
1122
|
+
const openSeconds = Math.max(0, ended.firstNamedMs / 1000 - pieceSeconds / speed);
|
|
1123
|
+
this.#host.encodeInputs?.noteOpen(session, openSeconds);
|
|
1124
|
+
this.#host.logger.info(`encode input of run #${run.from}..#${run.to} on ${session.outputKey}: opened in ` +
|
|
1125
|
+
`${openSeconds.toFixed(2)}s (first piece named at ${ended.firstNamedMs}ms, ${pieceSeconds.toFixed(3)}s of film at ${speed.toFixed(2)}x)`);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1087
1128
|
/**
|
|
1088
1129
|
* Log a report from a run that still belongs to this output. The run owns the
|
|
1089
1130
|
* clock and has already placed its relative ffmpeg time on the source
|
|
@@ -4,18 +4,22 @@ import { createHash } from "node:crypto";
|
|
|
4
4
|
* The available-only reader acquires complete pieces atomically before copying;
|
|
5
5
|
* owned buffers then outlive torrent eviction without keeping duplicate pins.
|
|
6
6
|
*/
|
|
7
|
-
export async function admitOriginalInput({ sources, reserve, readRanges }) {
|
|
8
|
-
const bytes = sources
|
|
9
|
-
.reduce((sum, [start, end]) => sum + end - start + 1, 0), 0);
|
|
7
|
+
export async function admitOriginalInput({ sources, reserve, readRanges, now = Date.now }) {
|
|
8
|
+
const bytes = originalInputBytes(sources);
|
|
10
9
|
if (!Number.isSafeInteger(bytes) || bytes <= 0) throw new TypeError("Original input requires complete finite byte ranges.");
|
|
11
10
|
const releaseBudget = await reserve(bytes);
|
|
12
11
|
if (typeof releaseBudget !== "function") return releaseBudget?.kind === "terminal" ? releaseBudget : { kind: "needs-memory", bytes };
|
|
13
12
|
const held = new Map();
|
|
14
13
|
const hash = createHash("sha256");
|
|
15
14
|
let retained = false;
|
|
15
|
+
// How long the copy itself took, apart from any wait for memory or bytes
|
|
16
|
+
// before it: the figure a run's stretch is sized from, read where the copy is.
|
|
17
|
+
let copyMs = 0;
|
|
16
18
|
try {
|
|
17
19
|
for (const source of sources) {
|
|
20
|
+
const startedAt = now();
|
|
18
21
|
const buffers = await readRanges(source, source.input.ranges, bytes);
|
|
22
|
+
copyMs += Math.max(0, now() - startedAt);
|
|
19
23
|
if (buffers === null) return { kind: "needs-bytes", sourceKey: source.sourceKey, fileIndex: source.fileIndex, ranges: source.input.ranges };
|
|
20
24
|
if (!Array.isArray(buffers) || buffers.length !== source.input.ranges.length || buffers.some((buffer, index) =>
|
|
21
25
|
!Buffer.isBuffer(buffer) || buffer.length !== source.input.ranges[index][1] - source.input.ranges[index][0] + 1)) {
|
|
@@ -32,7 +36,7 @@ export async function admitOriginalInput({ sources, reserve, readRanges }) {
|
|
|
32
36
|
// 1.6 s from a file, and nothing said whether it was waiting on its input.
|
|
33
37
|
const reads = { count: 0, bytes: 0, firstAt: 0, lastAt: 0 };
|
|
34
38
|
return {
|
|
35
|
-
kind: "result", original: true, sources, bytes, fingerprint: hash.digest("hex"),
|
|
39
|
+
kind: "result", original: true, sources, bytes, copyMs, fingerprint: hash.digest("hex"),
|
|
36
40
|
tracks: sources.flatMap(source => (source.input.selections ?? []).map(selection => ({ track: selection.track,
|
|
37
41
|
...(Number.isFinite(source.input.sourceEnds?.[selection.track.trackNumber]) ? {
|
|
38
42
|
sourceEndSeconds: source.input.sourceEnds[selection.track.trackNumber] - source.timeShiftSeconds
|
|
@@ -67,3 +71,9 @@ export async function admitOriginalInput({ sources, reserve, readRanges }) {
|
|
|
67
71
|
if (!retained) releaseBudget();
|
|
68
72
|
}
|
|
69
73
|
}
|
|
74
|
+
|
|
75
|
+
/** The bytes an original input of these sources holds once copied. */
|
|
76
|
+
export function originalInputBytes(sources) {
|
|
77
|
+
return sources.reduce((total, source) => total + source.input.ranges
|
|
78
|
+
.reduce((sum, [start, end]) => sum + end - start + 1, 0), 0);
|
|
79
|
+
}
|
|
@@ -233,20 +233,34 @@ export function buildRunCommand({
|
|
|
233
233
|
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
-
/**
|
|
236
|
+
/**
|
|
237
|
+
* One finite original-source run over the stretch its input holds, `startIndex`
|
|
238
|
+
* through `endIndex`, preserving the published intervals and track choice.
|
|
239
|
+
*
|
|
240
|
+
* The command works on a local timeline holding the stretch's own boundaries
|
|
241
|
+
* plus one beyond its end, so every published boundary inside it, the end
|
|
242
|
+
* included, is a cut, and what the muxer flushes after the end is a piece
|
|
243
|
+
* nobody publishes. Numbering continues from `startIndex`.
|
|
244
|
+
*/
|
|
237
245
|
export function buildOriginalCommand(params) {
|
|
238
246
|
const { admittedInput, timeline, startIndex, inputToken, baseUrl, audioOnly } = params;
|
|
247
|
+
const endIndex = Number.isInteger(params.endIndex) && params.endIndex >= startIndex ? params.endIndex : startIndex;
|
|
239
248
|
const grid = publishedGridFor(timeline);
|
|
240
|
-
const
|
|
241
|
-
|
|
249
|
+
const stretch = grid.slice(startIndex, endIndex + 2);
|
|
250
|
+
const from = stretch[0], to = stretch.at(-1);
|
|
251
|
+
if (stretch.length !== endIndex - startIndex + 2 || !stretch.every((at, index) => index === 0 || at > stretch[index - 1])) {
|
|
252
|
+
throw new Error("Original-source encoding requires a finite published interval.");
|
|
253
|
+
}
|
|
254
|
+
const last = stretch.at(-2);
|
|
242
255
|
const sourceFor = kind => admittedInput.sources.find(source => source.input.selections.some(selection => selection.track.type === kind));
|
|
243
256
|
const primary = sourceFor(audioOnly ? "audio" : "video");
|
|
244
257
|
const audio = sourceFor("audio") ?? primary;
|
|
245
258
|
if (!primary) throw new Error("The selected source track is absent from original input.");
|
|
246
259
|
const url = source => new URL(`/encode-input/${inputToken}/${source.fileIndex}`, baseUrl).href;
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
260
|
+
const local = [...stretch, to + (to - last)];
|
|
261
|
+
const localTimeline = { ...timeline, published: local,
|
|
262
|
+
boundaries: local, sourceStartOf: () => primary.input.from ?? from + primary.timeShiftSeconds };
|
|
263
|
+
const command = buildRunCommand({ ...params, startIndex: 0, endIndex: endIndex - startIndex, timeline: localTimeline,
|
|
250
264
|
videoSourceTrackIndex: primary.input.selections.find(selection => selection.track.type === "video")?.index ?? 0,
|
|
251
265
|
reorderDepth: primary.input.selections.find(selection => selection.track.type === "video")?.track.reorderDepth ?? 0,
|
|
252
266
|
inputFile: { startTime: primary.timeShiftSeconds }, audioFile: { startTime: audio.timeShiftSeconds },
|
|
@@ -218,6 +218,7 @@ export function wireOutputs({
|
|
|
218
218
|
readMetadataActivity = null,
|
|
219
219
|
sourceInputsFor = null,
|
|
220
220
|
readEncodeRanges = null,
|
|
221
|
+
readEncodeHeldRanges = null,
|
|
221
222
|
encodeInputs = null,
|
|
222
223
|
indexMemory = null,
|
|
223
224
|
budgetPolicy = null,
|
|
@@ -964,6 +965,7 @@ export function wireOutputs({
|
|
|
964
965
|
parts.encodeInputs = encodeInputs ?? (typeof resolveEncodeInput === "function" ? new EncodeInputs({
|
|
965
966
|
resolve: resolveEncodeInput,
|
|
966
967
|
readRanges: readEncodeRanges,
|
|
968
|
+
heldRanges: readEncodeHeldRanges,
|
|
967
969
|
reviseBudget: () => parts.machineBudget.revise(),
|
|
968
970
|
capacity: () => parts.machineBudget.capacityOf("memory"),
|
|
969
971
|
urgent: (output, index) => parts.encodeOrchestrator.wantedSegmentsOn(output.outputKey).some(window =>
|