@torrent-tv/proxy 2.89.3 → 2.89.5
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/AGENTS.md +338 -336
- package/CHANGELOG.md +2319 -2303
- package/docs/encode-architecture.md +55 -19
- package/docs/playback-start-forecast.md +13 -5
- package/docs/storage-architecture.md +6 -0
- package/package.json +1 -1
- package/routes/stream/get.js +38 -2
- package/server.js +748 -745
- package/services/encode/EncodeOrchestrator.js +24 -49
- package/services/encode/EncodePlan.js +5 -3
- package/services/encode/EncodeRun.js +80 -17
- package/services/encode/EncodeRuns.js +1339 -1239
- package/services/encode/Renditions.js +2 -1
- package/services/encode/RunClock.js +87 -0
- package/services/encode/RunProgress.js +1 -6
- package/services/encode/encoder-readings.js +26 -28
- package/services/encode/piece-completeness.js +46 -0
- package/services/encode/quality/EncodeCost.js +1043 -1167
- package/services/encode/quality/OutputQualityState.js +1 -5
- package/services/encode/quality/QualityController.js +213 -390
- package/services/encode/quality/link-budget.js +5 -17
- package/services/encode/run-inputs.js +13 -2
- package/services/server/SegmentServing.js +3 -1
- package/services/server/ViewerRequests.js +2 -7
- package/services/server/wire-outputs.js +12 -7
- package/services/storage/segment-store/SegmentStore.js +1294 -1255
- package/services/torrent/supply-margin.js +7 -5
- package/test/auto-quality-step.test.js +75 -29
- package/test/decode-cost.test.js +1 -1
- package/test/encode-cost.test.js +31 -39
- package/test/encode-orchestrator.test.js +15 -8
- package/test/encode-plan-viewers.test.js +34 -27
- package/test/encode-run.test.js +34 -6
- package/test/encoder-readings.test.js +48 -26
- package/test/helpers/encode-run.js +113 -9
- package/test/link-budget.test.js +4 -5
- package/test/manager-surface.test.js +1 -2
- package/test/orchestrator-wired.test.js +5 -4
- package/test/output-speed.test.js +0 -1
- package/test/piece-completeness.test.js +42 -0
- package/test/quality-offer.test.js +1 -3
- package/test/quality-variants.test.js +1 -1
- package/test/run-progress.test.js +2 -3
- package/test/same-height-switch.test.js +1 -2
- package/test/segment-closed-on-both-branches.test.js +114 -96
- package/test/segment-store.test.js +15 -15
- package/test/stream-route.test.js +28 -0
- package/test/supply-margin.test.js +13 -0
- package/test/two-viewers-one-picture.test.js +1 -1
- package/test/viewer-outputs.test.js +1 -1
|
@@ -295,20 +295,31 @@ The page has no quality control: the quality is always automatic. What moves a
|
|
|
295
295
|
viewer is decided here, per viewer, and each part of it is a measurement.
|
|
296
296
|
|
|
297
297
|
1. **Judged on the viewer's report, not on a timer.** `QualityController.noteViewerReported`
|
|
298
|
-
runs from the net-report route
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
298
|
+
runs from the net-report route, for the step down, the picture they see and
|
|
299
|
+
the step up alike. The chosen fifteen-second window of a slow link or a slow
|
|
300
|
+
machine, the 0.95x threshold of slowness, the sixty-second window before a
|
|
301
|
+
step back up, the ten-second buffer threshold, the thirty-second wait after
|
|
302
|
+
every action and the 80 % share of the link are gone. The five-second timer
|
|
303
|
+
only samples the host's load and the torrents' download rates; an encoder's
|
|
304
|
+
price is learned when it closes a piece.
|
|
305
|
+
2. **Down when the buffer would run dry first, for a reason a smaller output
|
|
306
|
+
removes.** On the buffer's trend it ends sooner than another output could
|
|
307
|
+
close the piece they need, counting the time until their next report
|
|
308
|
+
(`quality/drain-threat.js`), AND either their link carries less than the
|
|
309
|
+
stream they are given, or this machine makes the picture slower than
|
|
310
|
+
realtime over its run's own working time (`encode/RunClock.js`: the time its
|
|
311
|
+
input waited for the swarm and the time it was stopped are taken out). A
|
|
312
|
+
threat neither explains — the swarm is short — is not answered with a
|
|
313
|
+
smaller picture, which reads the same input. The trend is a least-squares fit
|
|
314
|
+
over the shortest run of their reports spanning one segment
|
|
306
315
|
(`viewer/buffer-trend.js`), because the buffer rises a segment at a time and
|
|
307
|
-
two readings catch only the rise or the fall. The time to readiness is
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
316
|
+
two readings catch only the rise or the fall. The time to readiness is the
|
|
317
|
+
observed preparation time of another output of this mode, else this host's
|
|
318
|
+
measured first-segment time. The levers: for the link, a lower limit of the
|
|
319
|
+
same height, then a lower height; for the machine, a lower height. A lower
|
|
320
|
+
height is asked of the player as URGENT — their page switches as soon as the
|
|
321
|
+
rung is ready, without waiting for a cushion. Where nothing can be prepared
|
|
322
|
+
they stay on what they are given, with no message.
|
|
312
323
|
3. **The picture the viewer sees bounds a re-encode.** The page sends the
|
|
313
324
|
frame it would show without enlarging, in physical pixels; it is kept on
|
|
314
325
|
`Viewer.visiblePicture`. The bound is the smallest rung of the source's
|
|
@@ -319,12 +330,21 @@ viewer is decided here, per viewer, and each part of it is a measurement.
|
|
|
319
330
|
4. **A smaller picture moves the viewer only onto a READY rung**
|
|
320
331
|
(`Renditions.heightReadyFor`: the piece they ask for next closed on it);
|
|
321
332
|
with none ready, the next judgement of their link or of the machine applies
|
|
322
|
-
the bound.
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
333
|
+
the bound.
|
|
334
|
+
5. **Up one rung when there is room, every term measured**: the picture on
|
|
335
|
+
their screen is made at least at realtime over its run's own working time (a
|
|
336
|
+
copy is not limited by an encoder), their buffer is not draining and holds
|
|
337
|
+
at least the time another output takes to be ready, their link carries the
|
|
338
|
+
next height, and the picture they see is not already served. A rung this
|
|
339
|
+
host has been measured failing at is not offered, so the step back up cannot
|
|
340
|
+
return to it; no window has to pass.
|
|
341
|
+
6. **A request stands for as long as its conditions hold**: each judgement
|
|
342
|
+
of the viewer's report asks for what it needs again, and a request it does
|
|
343
|
+
not repeat is let go — a draining buffer for a step up, a buffer that no
|
|
344
|
+
longer drains for a step down, no room for the output, a picture that is no
|
|
345
|
+
longer smaller. No chosen time ends a request. The page drops the move it
|
|
346
|
+
was preparing when the proxy stops asking for it.
|
|
347
|
+
7. **The page switches a variant when the rung is ready**, and for a request
|
|
328
348
|
that is not urgent only once it holds `minimumBufferSeconds`.
|
|
329
349
|
|
|
330
350
|
## A place on the machine
|
|
@@ -620,6 +640,22 @@ different figures, none of which reproduced the answer the plan had given.
|
|
|
620
640
|
A zero in `firstByte`, `kill` or `refetch` is a measurement nobody has taken, not
|
|
621
641
|
a free operation. It is printed so that reading it as free is a choice.
|
|
622
642
|
|
|
643
|
+
**`speed` has one owner: the run.** `EncodeRun.speedX` is the film a run made
|
|
644
|
+
between two closed pieces over its own working time (`encode/RunClock.js`), so
|
|
645
|
+
the time its input waited for the swarm and the time it was stopped are not in
|
|
646
|
+
it. The run announces each reading once (`onSpeedMeasured`); `EncodeCost` learns
|
|
647
|
+
the price of the output from it, the quality budget and the playback forecast
|
|
648
|
+
read the newest reading of the output's runs, and the plan reads the runs
|
|
649
|
+
themselves. Before a run has closed two pieces, and after a restart, the plan
|
|
650
|
+
uses `startingSpeedFor`: what this output was last measured doing alone, or
|
|
651
|
+
what the startup measurements predict. Nothing keeps a copy across restarts,
|
|
652
|
+
and ffmpeg's cumulative `speed=` is read by nothing: it divides by every second
|
|
653
|
+
the encoder was stopped or starved.
|
|
654
|
+
|
|
655
|
+
What the swarm delivers is not in this figure, by construction. It is the
|
|
656
|
+
separate supply term `refetch`, and while that is unmeasured (printed as zero)
|
|
657
|
+
the plan's arrivals assume the input keeps up.
|
|
658
|
+
|
|
623
659
|
It was missing, and its absence cost three wrong diagnoses of one field session.
|
|
624
660
|
The line printed the windows, the budget and where the live runs stood; the
|
|
625
661
|
intervals the actions carried were the one thing it did not print. What that
|
|
@@ -14,7 +14,11 @@ Each output's remaining processing work subtracts progress only within its
|
|
|
14
14
|
actual live run intervals. Initial video rates use existing codec/size/encoder
|
|
15
15
|
calibration. Initial audio rates use short packaged per-codec measurements and
|
|
16
16
|
sample work (encoding) or encoded bytes (copying). Actual output progress
|
|
17
|
-
replaces these initial rates.
|
|
17
|
+
replaces these initial rates. That rate is the run's processing speed: film
|
|
18
|
+
made over the run's own working time, with the time its input waited for the
|
|
19
|
+
swarm and the time it was stopped taken out (`RunClock`, fed by the stream
|
|
20
|
+
route, which marks each wait of a run's input read). The download is charged
|
|
21
|
+
once, by the source service, and not a second time inside the processing rate. Startup is charged once, separately from processing
|
|
18
22
|
throughput, and disappears once actual run progress establishes processing.
|
|
19
23
|
|
|
20
24
|
Input arrival and processing overlap. With arrival time D(m), processing rate
|
|
@@ -84,7 +88,11 @@ Seconds appear again only for rates, schedules and the answer.
|
|
|
84
88
|
A closed non-final fragment is reusable only if the end of its last frame, on
|
|
85
89
|
every track, reaches its next declared cut less `SEGMENT_CUT_TIME_DELTA_SECONDS`,
|
|
86
90
|
the delta the muxer is configured with. That is a fact of production, so no
|
|
87
|
-
browser rule enters it
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
the
|
|
91
|
+
browser rule enters it, and the encoding decides it
|
|
92
|
+
(`encode/piece-completeness.js`): `EncodeRuns` judges a closed file before it is
|
|
93
|
+
published, and judges a stored piece left by an earlier process when the
|
|
94
|
+
forecast asks for it, taking a short one off the disk. The segment store keeps
|
|
95
|
+
the bytes and the media intervals read from them and judges nothing. Interrupted
|
|
96
|
+
fragments are therefore never served or counted, so the production schedule
|
|
97
|
+
treats their media as unfinished work. The final fragment uses its actual media
|
|
98
|
+
end rather than the container's approximate duration.
|
|
@@ -167,6 +167,12 @@ finished pieces inside the ENCODING layer was the encoder owning its own disk.
|
|
|
167
167
|
It did not move into the room's own directory either: it is a CLAIMANT, and a
|
|
168
168
|
claimant inside the owner of the resource is the same fault one floor down.
|
|
169
169
|
|
|
170
|
+
The store keeps a produced piece's bytes and the media intervals read from them;
|
|
171
|
+
it does not judge whether a piece is whole. That is a fact of production, read
|
|
172
|
+
against the output's cut table, and the encoding decides it
|
|
173
|
+
(`encode/piece-completeness.js`): before a piece is published, and for a stored
|
|
174
|
+
piece left by an earlier process, which it asks the store to `remove`.
|
|
175
|
+
|
|
170
176
|
**Four directories, deliberately.** They share one property — bytes on a medium
|
|
171
177
|
with a limit — and that is what makes them one layer. What they are is three
|
|
172
178
|
different things: a torrent piece is born when it is downloaded and addressed by
|
package/package.json
CHANGED
package/routes/stream/get.js
CHANGED
|
@@ -10,6 +10,37 @@ import { open } from "node:fs/promises";
|
|
|
10
10
|
import { parseRange } from "../../utils/parse-range.js";
|
|
11
11
|
import { logger } from "../../utils/logger.js";
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* The same fragments, with every wait for the next one marked: `mark(true)`
|
|
15
|
+
* before it is asked for and `mark(false)` when it has come. The time between
|
|
16
|
+
* is time the encoder reading this response waits for its input.
|
|
17
|
+
*
|
|
18
|
+
* @template T
|
|
19
|
+
* @param {AsyncIterable<T>} source
|
|
20
|
+
* @param {(waiting: boolean) => void} mark
|
|
21
|
+
* @returns {AsyncGenerator<T>}
|
|
22
|
+
*/
|
|
23
|
+
async function* markingWaits(source, mark) {
|
|
24
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
25
|
+
try {
|
|
26
|
+
while (true) {
|
|
27
|
+
mark(true);
|
|
28
|
+
let step;
|
|
29
|
+
try {
|
|
30
|
+
step = await iterator.next();
|
|
31
|
+
} finally {
|
|
32
|
+
mark(false);
|
|
33
|
+
}
|
|
34
|
+
if (step.done) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
yield step.value;
|
|
38
|
+
}
|
|
39
|
+
} finally {
|
|
40
|
+
await iterator.return?.();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
13
44
|
/**
|
|
14
45
|
* Resolve source parameters from the query string.
|
|
15
46
|
* Prefers a registered `sourceKey`; falls back to inline `sourceType`+`source`.
|
|
@@ -138,7 +169,7 @@ function serveWholeFile(req, reply, file, handle) {
|
|
|
138
169
|
return reply.send(handle.createReadStream({ start, end }));
|
|
139
170
|
}
|
|
140
171
|
|
|
141
|
-
export async function handleStreamGet(req, reply, { sourceRegistry, torrentPool, noteInputBytes = null }) {
|
|
172
|
+
export async function handleStreamGet(req, reply, { sourceRegistry, torrentPool, noteInputBytes = null, noteInputWaiting = null }) {
|
|
142
173
|
const fileIndexRaw = typeof req.query.fileIndex === "string" ? req.query.fileIndex : "";
|
|
143
174
|
const fileIndex = Number(fileIndexRaw);
|
|
144
175
|
const { sourceType, source, sourceKey } = getSourceParams(req.query, sourceRegistry);
|
|
@@ -265,6 +296,11 @@ export async function handleStreamGet(req, reply, { sourceRegistry, torrentPool,
|
|
|
265
296
|
// file — and two sessions can read one file, so the file cannot stand in for
|
|
266
297
|
// the session.
|
|
267
298
|
const sessionId = typeof req.query.session === "string" ? req.query.session : "";
|
|
299
|
+
// Which run of that output, so the time its input waits is that run's.
|
|
300
|
+
const runToken = Number(req.query.run);
|
|
301
|
+
const markWaits = noteInputWaiting && Number.isInteger(runToken)
|
|
302
|
+
? (waiting) => noteInputWaiting(runToken, waiting)
|
|
303
|
+
: null;
|
|
268
304
|
|
|
269
305
|
const fragments = typeof file.createFragmentReader === "function"
|
|
270
306
|
? file.createFragmentReader({ start, end, windowBytes })
|
|
@@ -286,7 +322,7 @@ export async function handleStreamGet(req, reply, { sourceRegistry, torrentPool,
|
|
|
286
322
|
|
|
287
323
|
let sent = 0;
|
|
288
324
|
try {
|
|
289
|
-
for await (const fragment of fragments) {
|
|
325
|
+
for await (const fragment of markWaits ? markingWaits(fragments, markWaits) : fragments) {
|
|
290
326
|
if (reply.raw.writableEnded || reply.raw.destroyed) {
|
|
291
327
|
fragment.release();
|
|
292
328
|
break;
|