@torrent-tv/proxy 2.96.14 → 2.97.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/bin/cli.js +2 -0
- package/package.json +1 -1
- package/routes/encode-input/get.js +13 -0
- package/server.js +25 -3
- package/services/encode/EncodeInputs.js +60 -47
- package/services/encode/EncodeOrchestrator.js +29 -38
- package/services/encode/EncodeRun.js +26 -5
- package/services/encode/EncodeRuns.js +88 -48
- package/services/encode/OriginalInput.js +45 -1
- package/services/encode/RunClock.js +27 -0
- package/services/encode/run-costs.js +47 -2
- package/services/server/wire-outputs.js +3 -0
- package/services/storage/MachineBudget.js +19 -1
- package/services/storage/cpu-profile.js +68 -0
- package/services/storage/processor-report.js +186 -0
- package/services/torrent/worker/client.js +3 -2
- package/services/torrent/worker/pool-adapter.js +2 -2
- package/services/torrent/worker/worker.js +6 -2
- package/services/viewer/DownloadMaps.js +53 -6
package/bin/cli.js
CHANGED
|
@@ -88,6 +88,7 @@ program
|
|
|
88
88
|
.option("--no-transcode-audio", "Disable optional HLS AAC audio transcoding")
|
|
89
89
|
.option("--no-port-mapping", "Disable automatic UPnP/NAT-PMP port mapping")
|
|
90
90
|
.option("--delivery-sink", "Serve /api/delivery-sink, a torrent-free byte stream for delivery testing")
|
|
91
|
+
.option("--profile-start <seconds>", "Profile the main thread's CPU for this many seconds from the first source opened, written beside the state (off by default)")
|
|
91
92
|
.option(
|
|
92
93
|
"--send-chunk-bytes <bytes>",
|
|
93
94
|
"Size of one data-channel message when sending a response body (0 = whatever the body read hands over, ~64KB). For measuring whether the delivery wedge depends on that size."
|
|
@@ -392,6 +393,7 @@ try {
|
|
|
392
393
|
budgetPolicy,
|
|
393
394
|
logFile: options.logFile ?? "",
|
|
394
395
|
deliverySink: options.deliverySink === true,
|
|
396
|
+
profileStartSeconds: Number(options.profileStart) || 0,
|
|
395
397
|
// Late-bound the same way `webRtcManager` is below: the torrent pool is
|
|
396
398
|
// built inside `startProxyServer`, before `dataChannelHandler` — the
|
|
397
399
|
// thing that actually owns a channel to push down — exists.
|
package/package.json
CHANGED
|
@@ -36,6 +36,19 @@ export function handleEncodeInputGet(req, reply, { inputOf, refused = () => {} }
|
|
|
36
36
|
refused({ token, fileIndex, start: range.start, runTag: input.runTag ?? null });
|
|
37
37
|
return reply.code(503).send({ error: "Original input bytes are outside the admitted ranges." });
|
|
38
38
|
}
|
|
39
|
+
// What was handed to the system for this answer before its connection
|
|
40
|
+
// closed, and whether all of it was: an answer runs to the end of its
|
|
41
|
+
// admitted range and FFmpeg closes the connection once it has what it wants,
|
|
42
|
+
// so the range answered says nothing about the bytes taken. What FFmpeg read
|
|
43
|
+
// is at most what was handed over; the kernel's buffer is the difference,
|
|
44
|
+
// and it cannot be seen from here (torrent-tv/meta#177).
|
|
45
|
+
const socket = req.raw?.socket;
|
|
46
|
+
const writtenBefore = socket?.bytesWritten ?? 0;
|
|
47
|
+
reply.raw?.once?.("close", () => {
|
|
48
|
+
input.noteAnswer?.({ fileIndex, start: range.start, end: held.end,
|
|
49
|
+
written: Math.max(0, (socket?.bytesWritten ?? writtenBefore) - writtenBefore),
|
|
50
|
+
handedOver: reply.raw?.writableFinished === true });
|
|
51
|
+
});
|
|
39
52
|
return reply.code(206).header("Content-Length", String(held.bytes.length))
|
|
40
53
|
.header("Content-Range", `bytes ${range.start}-${held.end}/${length}`).send(held.bytes);
|
|
41
54
|
}
|
package/server.js
CHANGED
|
@@ -80,6 +80,8 @@ import { measureStartAndStop } from "./services/encode/start-stop-cost.js";
|
|
|
80
80
|
import { benchmarkAudio } from "./services/encode/audio-calibration.js";
|
|
81
81
|
import { logger } from "./utils/logger.js";
|
|
82
82
|
import { completedFilesRoot } from "./services/storage/files/CompletedFiles.js";
|
|
83
|
+
import { startupCpuProfile } from "./services/storage/cpu-profile.js";
|
|
84
|
+
import { startProcessorReport } from "./services/storage/processor-report.js";
|
|
83
85
|
|
|
84
86
|
const require = createRequire(import.meta.url);
|
|
85
87
|
const { version } = require("./package.json");
|
|
@@ -119,6 +121,8 @@ function buildPortCandidates(startPort, maxAttempts = 51) {
|
|
|
119
121
|
export async function startProxyServer({
|
|
120
122
|
host, port, transcodeAudio, ffmpegBin, memoryBytes, segmentFormat, stateDir, onSubtitleCues, diagnostics, budgetPolicy,
|
|
121
123
|
deliverySink = false,
|
|
124
|
+
// Seconds of main-thread CPU profile from the first source opened; 0 is off.
|
|
125
|
+
profileStartSeconds = 0,
|
|
122
126
|
// What viewers' connections carry beyond the film, per byte of film; the
|
|
123
127
|
// transport counts it and is built after this server, so it is late-bound.
|
|
124
128
|
serviceShare = () => null,
|
|
@@ -875,6 +879,22 @@ export async function startProxyServer({
|
|
|
875
879
|
await torrentPool.fillFileInBackground?.(torrent, fileIndex);
|
|
876
880
|
}
|
|
877
881
|
});
|
|
882
|
+
// Where the processor goes, and at a cold start what the main thread does
|
|
883
|
+
// with it, when the operator asks (torrent-tv/meta#177).
|
|
884
|
+
const startupProfile = startupCpuProfile({
|
|
885
|
+
seconds: profileStartSeconds,
|
|
886
|
+
dir: stateDir || "",
|
|
887
|
+
log: (line) => logger.info(line),
|
|
888
|
+
mayKeep: (bytes) => diagnostics?.mayKeep?.({ what: "a CPU profile", bytes }) ?? true
|
|
889
|
+
});
|
|
890
|
+
startProcessorReport({
|
|
891
|
+
log: (line) => logger.info(line),
|
|
892
|
+
ffmpegPids: () => [...outputParts.outputs.values()]
|
|
893
|
+
.flatMap((output) => outputParts.encodeRuns.liveRunsOf(output))
|
|
894
|
+
.map((run) => run.process?.pid)
|
|
895
|
+
.filter((pid) => Number.isInteger(pid)),
|
|
896
|
+
detailed: () => startupProfile.active()
|
|
897
|
+
});
|
|
878
898
|
// What the last life of this process left on the disk. The kernel kills this
|
|
879
899
|
// one often enough for that to be an ordinary state rather than an odd one —
|
|
880
900
|
// twice in a single viewing on 2026-09-02 — and when it does, no exit handler
|
|
@@ -1106,9 +1126,11 @@ export async function startProxyServer({
|
|
|
1106
1126
|
);
|
|
1107
1127
|
// Bytes a browser can time its link with before any film has been chosen.
|
|
1108
1128
|
app.get("/api/link-probe", async (req, reply) => handleApiLinkProbeGet(req, reply));
|
|
1109
|
-
app.post("/api/sources", async (req, reply) =>
|
|
1110
|
-
|
|
1111
|
-
|
|
1129
|
+
app.post("/api/sources", async (req, reply) => {
|
|
1130
|
+
// A cold start begins with the first source opened (torrent-tv/meta#177).
|
|
1131
|
+
startupProfile.begin();
|
|
1132
|
+
return handleApiSourcesPost(req, reply, { sourceRegistry, viewers: outputParts.viewers });
|
|
1133
|
+
});
|
|
1112
1134
|
app.get("/api/sources/:sourceKey/stats", async (req, reply) =>
|
|
1113
1135
|
handleApiSourceStatsGet(req, reply, { sourceRegistry, torrentPool })
|
|
1114
1136
|
);
|
|
@@ -33,8 +33,13 @@ export class EncodeInputs {
|
|
|
33
33
|
#wanted = new Map();
|
|
34
34
|
/** Bytes per millisecond of recent original-input copies, read where the copy is made. */
|
|
35
35
|
#copyRates = [];
|
|
36
|
-
/**
|
|
37
|
-
|
|
36
|
+
/**
|
|
37
|
+
* How long an encoder takes to open an output's original input: asked of
|
|
38
|
+
* the one owner of start readings (`RunCosts`, torrent-tv/meta#184).
|
|
39
|
+
*
|
|
40
|
+
* @type {(output: object) => { seconds: number, measured: boolean } | null}
|
|
41
|
+
*/
|
|
42
|
+
#openOf;
|
|
38
43
|
#resolve;
|
|
39
44
|
#read;
|
|
40
45
|
#heldRanges;
|
|
@@ -46,7 +51,9 @@ export class EncodeInputs {
|
|
|
46
51
|
#log;
|
|
47
52
|
#now;
|
|
48
53
|
|
|
49
|
-
constructor({ resolve, readRanges, heldRanges = null, reviseBudget, changed, failed, capacity = () => null, urgent = () => true,
|
|
54
|
+
constructor({ resolve, readRanges, heldRanges = null, reviseBudget, changed, failed, capacity = () => null, urgent = () => true,
|
|
55
|
+
openOf = () => null, log = () => {}, now = Date.now }) {
|
|
56
|
+
this.#openOf = openOf;
|
|
50
57
|
this.#resolve = resolve;
|
|
51
58
|
this.#read = readRanges;
|
|
52
59
|
this.#heldRanges = heldRanges;
|
|
@@ -130,10 +137,26 @@ export class EncodeInputs {
|
|
|
130
137
|
return null;
|
|
131
138
|
}
|
|
132
139
|
|
|
133
|
-
|
|
140
|
+
/**
|
|
141
|
+
* Withdraw what this output no longer needs: a request no wanted window
|
|
142
|
+
* covers, and — when `starts` is given — a copy already held whose start is
|
|
143
|
+
* neither where a live run ends nor where one is being started. A next
|
|
144
|
+
* stretch is copied for the run working before it (torrent-tv/meta#158);
|
|
145
|
+
* once that run has gone and the plan starts elsewhere, after a seek, the
|
|
146
|
+
* copy held memory nobody would use (torrent-tv/meta#184).
|
|
147
|
+
*
|
|
148
|
+
* @param {object} output
|
|
149
|
+
* @param {{ from: number, to: number }[]} windows
|
|
150
|
+
* @param {Set<number> | null} [starts]
|
|
151
|
+
*/
|
|
152
|
+
retain(output, windows, starts = null) {
|
|
134
153
|
for (const [key, request] of this.#requests) {
|
|
135
|
-
if (request.output !== output
|
|
136
|
-
|
|
154
|
+
if (request.output !== output) continue;
|
|
155
|
+
const covered = windows.some(window => request.from <= window.to && request.to >= window.from);
|
|
156
|
+
const unused = starts !== null && request.result?.kind === "result" && !starts.has(request.from);
|
|
157
|
+
if (covered && !unused) continue;
|
|
158
|
+
this.#note(request, covered ? "withdrawn: no live run ends before it and no start is placed there"
|
|
159
|
+
: "withdrawn: no wanted window covers it");
|
|
137
160
|
this.#requests.delete(key);
|
|
138
161
|
this.#wanted.delete(key);
|
|
139
162
|
request.result?.release?.();
|
|
@@ -154,16 +177,13 @@ export class EncodeInputs {
|
|
|
154
177
|
let existing = this.#requests.get(key);
|
|
155
178
|
if (existing && existing.to !== to) {
|
|
156
179
|
// The plan's bound for this start moved. A copy already made beyond it
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
180
|
+
// holds the shorter stretch whole, so the run is commanded over its
|
|
181
|
+
// prefix and the copy is kept until the run ends: copying it again cost
|
|
182
|
+
// as long as the first copy (torrent-tv/meta#184). One being made is
|
|
183
|
+
// narrowed when it is done, and one waiting for bytes or memory takes the
|
|
184
|
+
// new bound when its own event prepares it again.
|
|
160
185
|
existing.to = to;
|
|
161
|
-
if (existing.result?.kind === "result" && existing.result.to > to)
|
|
162
|
-
this.#requests.delete(key);
|
|
163
|
-
this.#wanted.delete(key);
|
|
164
|
-
existing.result.release();
|
|
165
|
-
existing = undefined;
|
|
166
|
-
}
|
|
186
|
+
if (existing.result?.kind === "result" && existing.result.to > to) existing.result.to = Math.max(existing.from, to);
|
|
167
187
|
}
|
|
168
188
|
if (existing?.result?.kind === "result") {
|
|
169
189
|
this.#requests.delete(key);
|
|
@@ -200,44 +220,38 @@ export class EncodeInputs {
|
|
|
200
220
|
this.#wanted.delete(key);
|
|
201
221
|
request.result?.release?.();
|
|
202
222
|
}
|
|
203
|
-
this.#opens.delete(addressOf(output));
|
|
204
223
|
}
|
|
205
224
|
|
|
206
225
|
/**
|
|
207
|
-
* How
|
|
208
|
-
*
|
|
209
|
-
*
|
|
226
|
+
* How many bytes a stretch may hold so that copying it takes no longer than
|
|
227
|
+
* opening the input it replaces: the recent copy rate times this output's
|
|
228
|
+
* recent open. Beyond it the open's share of each piece falls below the
|
|
229
|
+
* copy's, which is the same for every piece; a longer stretch buys less and
|
|
230
|
+
* delays the run's start more. Null until a copy rate is measured and an
|
|
231
|
+
* open is known, and the run then gets one piece. An output that has not
|
|
232
|
+
* opened yet is sized by the host's startup reading, which is said: at a
|
|
233
|
+
* cold start the second run was one piece too for want of it, and cost
|
|
234
|
+
* another open (6.34 s, torrent-tv/meta#166 run 12).
|
|
210
235
|
*
|
|
211
236
|
* @param {object} output
|
|
212
|
-
* @
|
|
237
|
+
* @returns {{ bytes: number, openMeasured: boolean } | null}
|
|
213
238
|
*/
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
this.#opens.set(addressOf(output), readings);
|
|
239
|
+
stretchFor(output) {
|
|
240
|
+
const rate = middleOf(this.#copyRates);
|
|
241
|
+
const open = this.#openOf(output);
|
|
242
|
+
return rate === null || !open ? null : { bytes: Math.floor(rate * open.seconds * 1000), openMeasured: open.measured };
|
|
219
243
|
}
|
|
220
244
|
|
|
221
245
|
/**
|
|
222
|
-
* How many bytes a stretch may hold so that copying it takes no longer than
|
|
223
|
-
* opening the input it replaces: the recent copy rate times this output's
|
|
224
|
-
* recent open. Beyond it the open's share of each piece falls below the
|
|
225
|
-
* copy's, which is the same for every piece; a longer stretch buys less and
|
|
226
|
-
* delays the run's start more. Null until both have been measured, and the
|
|
227
|
-
* run then gets one piece, as it did before either reading existed.
|
|
228
|
-
*
|
|
229
246
|
* @param {object} output
|
|
230
247
|
* @returns {number | null}
|
|
231
248
|
*/
|
|
232
249
|
stretchBytesFor(output) {
|
|
233
|
-
|
|
234
|
-
const open = middleOf(this.#opens.get(addressOf(output)) ?? []);
|
|
235
|
-
return rate === null || open === null ? null : Math.floor(rate * open * 1000);
|
|
250
|
+
return this.stretchFor(output)?.bytes ?? null;
|
|
236
251
|
}
|
|
237
252
|
|
|
238
253
|
#prepare(request) {
|
|
239
254
|
request.pending = true;
|
|
240
|
-
request.stale = false;
|
|
241
255
|
request.revision = this.#revision;
|
|
242
256
|
request.memoryRevision = this.#memoryRevision;
|
|
243
257
|
// Where the time from the request to its input goes, across every attempt:
|
|
@@ -260,13 +274,9 @@ export class EncodeInputs {
|
|
|
260
274
|
if (result.kind === "result") Object.assign(result, { from: request.from, to: request.from });
|
|
261
275
|
}
|
|
262
276
|
if (this.#requests.get(request.key) !== request) { result.release?.(); return; }
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
request.result = null;
|
|
267
|
-
request.stale = true;
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
277
|
+
// The plan narrowed this start while the copy was being made: the run
|
|
278
|
+
// is commanded over its prefix.
|
|
279
|
+
if (result.kind === "result" && result.to > request.to) result.to = Math.max(request.from, request.to);
|
|
270
280
|
request.result = result;
|
|
271
281
|
this.#note(request, result.kind === "result" ? `ready #${result.from}..#${result.to}, ${result.bytes ?? "?"} bytes` +
|
|
272
282
|
(Number.isFinite(result.copyMs) ? `, copied in ${result.copyMs}ms` : "")
|
|
@@ -281,8 +291,8 @@ export class EncodeInputs {
|
|
|
281
291
|
}).finally(() => {
|
|
282
292
|
request.pending = false;
|
|
283
293
|
if (request.result?.kind !== "needs-memory") this.#wanted.delete(request.key);
|
|
284
|
-
const changed = request.
|
|
285
|
-
? request.memoryRevision !== this.#memoryRevision : request.revision !== this.#revision
|
|
294
|
+
const changed = request.result?.kind === "needs-memory"
|
|
295
|
+
? request.memoryRevision !== this.#memoryRevision : request.revision !== this.#revision;
|
|
286
296
|
if (this.#requests.get(request.key) === request && changed &&
|
|
287
297
|
request.result?.kind !== "result" && request.result?.kind !== "terminal") this.#prepare(request);
|
|
288
298
|
});
|
|
@@ -360,7 +370,9 @@ export class EncodeInputs {
|
|
|
360
370
|
}
|
|
361
371
|
return good;
|
|
362
372
|
};
|
|
363
|
-
const
|
|
373
|
+
const sizing = this.stretchFor(output);
|
|
374
|
+
const target = sizing?.bytes ?? null;
|
|
375
|
+
if (sizing && !sizing.openMeasured) request.timing.openApproximate = true;
|
|
364
376
|
let end = target === null || this.#heldRanges === null ? from : await longest(target);
|
|
365
377
|
if (this.#requests.get(request.key) !== request) return undefined;
|
|
366
378
|
this.#wanted.set(request.key, { wanted: stretches.get(end).bytes, required: minimum });
|
|
@@ -429,7 +441,8 @@ function describeTiming(timing, now) {
|
|
|
429
441
|
return `; asked ${Math.max(0, now - timing.askedAt)}ms ago over ${timing.attempts} attempt(s): ` +
|
|
430
442
|
`${timing.resolutions} resolution(s) ${timing.resolveMs}ms ` +
|
|
431
443
|
`(${timing.reads} file read(s): queued ${timing.queuedMs}ms, reading ${timing.readMs}ms), ` +
|
|
432
|
-
`held ranges ${timing.heldMs}ms, budget ${timing.budgetMs}ms
|
|
444
|
+
`held ranges ${timing.heldMs}ms, budget ${timing.budgetMs}ms` +
|
|
445
|
+
(timing.openApproximate ? "; sized by the host's startup open, this output has not opened yet" : "");
|
|
433
446
|
}
|
|
434
447
|
|
|
435
448
|
function addressOf(output) { return output.outputKey ?? output.id; }
|
|
@@ -145,6 +145,30 @@ export class EncodeOrchestrator {
|
|
|
145
145
|
this.#costs.noteStartup(measured);
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
/**
|
|
149
|
+
* One output's original-input open, and what sizes its stretch: the one
|
|
150
|
+
* owner of start readings is this class's `RunCosts` (torrent-tv/meta#184).
|
|
151
|
+
*
|
|
152
|
+
* @param {string} address
|
|
153
|
+
* @param {number} seconds
|
|
154
|
+
*/
|
|
155
|
+
noteOpen(address, seconds) {
|
|
156
|
+
this.#costs.noteOpen(address, seconds);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* @param {string} address
|
|
161
|
+
* @returns {{ seconds: number, measured: boolean } | null}
|
|
162
|
+
*/
|
|
163
|
+
openOf(address) {
|
|
164
|
+
return this.#costs.openOf(address);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** @param {string} address */
|
|
168
|
+
forgetOpens(address) {
|
|
169
|
+
this.#costs.forgetOpens(address);
|
|
170
|
+
}
|
|
171
|
+
|
|
148
172
|
/**
|
|
149
173
|
* What a stop and a start cost on this host, in seconds, as the plan reads
|
|
150
174
|
* them — for anyone else pricing a fresh encoder by the same figures.
|
|
@@ -289,41 +313,6 @@ export class EncodeOrchestrator {
|
|
|
289
313
|
return coverage;
|
|
290
314
|
}
|
|
291
315
|
|
|
292
|
-
/**
|
|
293
|
-
* Where a run started here must stop: the free stretch in front of it.
|
|
294
|
-
*
|
|
295
|
-
* Asked of the one map, brought up to date first. It used to be worked out by
|
|
296
|
-
* the session manager, which reached into this layer for the map and into the
|
|
297
|
-
* store for what was on the disk and put the two together itself — one fact
|
|
298
|
-
* with two owners and a third party carrying it between them, which is how the
|
|
299
|
-
* two came to disagree.
|
|
300
|
-
*
|
|
301
|
-
* @param {object} params
|
|
302
|
-
* @param {string} params.address
|
|
303
|
-
* @param {number} params.from - Where the run will start.
|
|
304
|
-
* @param {object | null} [params.exceptRun] - The run being replaced, whose
|
|
305
|
-
* own claim is not somebody else's material.
|
|
306
|
-
* @param {number} [params.segmentCount] - The output's length, when known.
|
|
307
|
-
* @returns {number} The last number to work through, or `-1` for the end of
|
|
308
|
-
* the film.
|
|
309
|
-
*/
|
|
310
|
-
freeStretchEnd({ address, from, exceptRun = null, segmentCount = 0 }) {
|
|
311
|
-
if (!address) {
|
|
312
|
-
return -1;
|
|
313
|
-
}
|
|
314
|
-
if (segmentCount > 0) {
|
|
315
|
-
this.coverageOf(address).setSegmentCount(segmentCount);
|
|
316
|
-
}
|
|
317
|
-
const coverage = this.#upToDateCoverage(address);
|
|
318
|
-
const start = Math.max(0, from);
|
|
319
|
-
const free = coverage.freeRunFrom(start, exceptRun);
|
|
320
|
-
if (!Number.isFinite(free)) {
|
|
321
|
-
return -1;
|
|
322
|
-
}
|
|
323
|
-
const end = start + Math.max(1, free) - 1;
|
|
324
|
-
return segmentCount > 0 && end >= segmentCount - 1 ? -1 : end;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
316
|
/**
|
|
328
317
|
* @param {string} address
|
|
329
318
|
* @returns {import("../encode/EncodeRun.js").EncodeRun[]}
|
|
@@ -974,9 +963,11 @@ export class EncodeOrchestrator {
|
|
|
974
963
|
// were two steps, and two owners each performed the second one.
|
|
975
964
|
//
|
|
976
965
|
// A run whose input is one copied stretch can make nothing past it, so it
|
|
977
|
-
// claims that stretch and not the plan's longer bound:
|
|
978
|
-
// for the next run (torrent-tv/meta#158).
|
|
979
|
-
|
|
966
|
+
// claims that stretch — its own `to` — and not the plan's longer bound:
|
|
967
|
+
// the rest stays free for the next run (torrent-tv/meta#158). A run whose
|
|
968
|
+
// input grows a segment at a time claims the plan's bound, as it may reach
|
|
969
|
+
// it (torrent-tv/meta#184).
|
|
970
|
+
const through = run.originalInput ? run.to : to;
|
|
980
971
|
this.coverageOf(address).claim(run, from, endOfRun({ from, to: through }));
|
|
981
972
|
}
|
|
982
973
|
|
|
@@ -64,8 +64,14 @@ const MICROSECONDS_PER_SECOND = 1_000_000;
|
|
|
64
64
|
* it finished none.
|
|
65
65
|
* @property {number} livedMs
|
|
66
66
|
* @property {number | null} [firstNamedMs] - Spawn to the moment the encoder
|
|
67
|
-
* named its first closed piece
|
|
68
|
-
* encoding, without the wait for the
|
|
67
|
+
* named its first closed piece, less the time it was paused before then: the
|
|
68
|
+
* open of its input plus that one piece's encoding, without the wait for the
|
|
69
|
+
* following piece that publication adds.
|
|
70
|
+
* @property {number | null} [firstNamedWallMs] - The same on the wall clock,
|
|
71
|
+
* pauses included: what the viewer waited.
|
|
72
|
+
* @property {number | null} [firstOutputWallMs] - Spawn to the first output on
|
|
73
|
+
* the wall clock; `firstOutputMs` leaves the pauses out.
|
|
74
|
+
* @property {number} [stoppedMs] - How long the run was paused in all.
|
|
69
75
|
* @property {boolean} normal - Whether this ending is the expected one.
|
|
70
76
|
* @property {string} lastError - The last thing ffmpeg said on stderr.
|
|
71
77
|
* @property {string | null} provenName - The last piece this run named while it
|
|
@@ -166,6 +172,10 @@ export class EncodeRun {
|
|
|
166
172
|
*/
|
|
167
173
|
#firstNamedAt = 0;
|
|
168
174
|
|
|
175
|
+
/** How long the run had been paused when each of the two above was taken. */
|
|
176
|
+
#stoppedBeforeFirstOutput = 0;
|
|
177
|
+
#stoppedBeforeFirstNamed = 0;
|
|
178
|
+
|
|
169
179
|
/** The film made and the run's own working time, as of the last progress report. */
|
|
170
180
|
#workSample = null;
|
|
171
181
|
|
|
@@ -569,6 +579,7 @@ export class EncodeRun {
|
|
|
569
579
|
this.#produced.add(index);
|
|
570
580
|
if (this.#firstOutputAt === 0) {
|
|
571
581
|
this.#firstOutputAt = this.now();
|
|
582
|
+
this.#stoppedBeforeFirstOutput = this.clock.stoppedMs(this.#firstOutputAt);
|
|
572
583
|
}
|
|
573
584
|
if (this.#state === ENCODE_RUN_STATE.STARTING) {
|
|
574
585
|
this.#transition(ENCODE_RUN_EVENT.FIRST_SEGMENT);
|
|
@@ -607,7 +618,10 @@ export class EncodeRun {
|
|
|
607
618
|
}
|
|
608
619
|
if (this.#publicationError) break;
|
|
609
620
|
const index = this.indexOfName(name);
|
|
610
|
-
if (this.#firstNamedAt === 0 && Number.isInteger(index) && index >= this.from)
|
|
621
|
+
if (this.#firstNamedAt === 0 && Number.isInteger(index) && index >= this.from) {
|
|
622
|
+
this.#firstNamedAt = this.now();
|
|
623
|
+
this.#stoppedBeforeFirstNamed = this.clock.stoppedMs(this.#firstNamedAt);
|
|
624
|
+
}
|
|
611
625
|
const endOfWork = Number.isFinite(endOfRun(this)) ? this.to : this.lastSegmentIndex();
|
|
612
626
|
// A packet crossing the final cut can flush a following file. It may
|
|
613
627
|
// complete its predecessor, but no unadmitted interval can be published.
|
|
@@ -920,11 +934,18 @@ export class EncodeRun {
|
|
|
920
934
|
// Null where the run was never told to stop, or never produced anything:
|
|
921
935
|
// an absent measurement says so rather than reading as zero.
|
|
922
936
|
dyingMs: this.#stopOrderedAt > 0 ? this.now() - this.#stopOrderedAt : null,
|
|
937
|
+
// Without the time this run was paused before then: a pause is our own
|
|
938
|
+
// decision, not what starting cost (torrent-tv/meta#184). The wall-clock
|
|
939
|
+
// figures beside them are what the viewer waited.
|
|
923
940
|
firstOutputMs:
|
|
924
941
|
this.#firstOutputAt > 0 && this.#startedAt > 0
|
|
925
|
-
? this.#firstOutputAt - this.#startedAt
|
|
942
|
+
? this.#firstOutputAt - this.#startedAt - this.#stoppedBeforeFirstOutput
|
|
926
943
|
: null,
|
|
927
|
-
|
|
944
|
+
firstOutputWallMs: this.#firstOutputAt > 0 && this.#startedAt > 0 ? this.#firstOutputAt - this.#startedAt : null,
|
|
945
|
+
firstNamedMs: this.#firstNamedAt > 0 && this.#startedAt > 0
|
|
946
|
+
? this.#firstNamedAt - this.#startedAt - this.#stoppedBeforeFirstNamed : null,
|
|
947
|
+
firstNamedWallMs: this.#firstNamedAt > 0 && this.#startedAt > 0 ? this.#firstNamedAt - this.#startedAt : null,
|
|
948
|
+
stoppedMs: this.clock.stoppedMs(),
|
|
928
949
|
normal: ending === ENCODE_EXIT.COMPLETE,
|
|
929
950
|
lastError: this.lastError
|
|
930
951
|
};
|