@torrent-tv/proxy 2.89.3 → 2.89.4
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 +12 -0
- 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 +46 -29
- 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-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
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## 2.89.4
|
|
2
|
+
|
|
3
|
+
- **Fix**: An encoder's speed is read over its own working time. The stream route says when a run's input starts and stops waiting for bytes, each input read carries the number of the run it feeds, and a run's clock (`RunClock`) takes out that waiting and the time it was stopped. Field 2026-10-01: a copy waited 32.93 s and 43.91 s for two pieces and read 0.21x, which the start forecast then carried over the whole remaining film while it charged the same download separately. The forecast's processing rate and the learned price of copying, decoding and soundtracks now measure the machine.
|
|
4
|
+
- **Fix**: Every quality step is judged on the viewer's report, with every term measured. Down: on its trend their buffer runs dry before another output could have the piece they need, and either their link carries less than the stream or this machine makes the picture slower than realtime over its own working time; a shortfall of the swarm is not answered with a smaller picture, which reads the same input. Up: the machine makes the picture at realtime or faster, their buffer is not draining and holds the time another output takes to be ready, and their link carries the next height. Gone: the 0.95x threshold and fifteen-second window of slowness, the sixty-second window before a step back up, the 80 % share of a measured link (`LINK_SAFETY`), and the download-bound test built on `fileProgress >= 0.999` and a download rate against the file's byte rate. While a viewer waits for playback to start nothing steps down for the machine: their buffer is not draining.
|
|
5
|
+
- **Fix**: An encoder's price and the forecast's processing rate are learned each time it closes a piece, on every host. The five-second timer only samples the host's load and the torrents' download rates; hardware hosts used to learn nothing, because the timer stopped before its learning part there.
|
|
6
|
+
- **Fix**: The stream an output sends is measured over every finished piece, each over its own length on the cut table, from the sizes the segment store already holds. It used to be the last five pieces but the newest, timed as if every piece were one length — wrong for a copy, which is cut at the source's keyframes — and read from the disk on every report.
|
|
7
|
+
- **Fix**: A request to move a viewer's quality stands for as long as the conditions it was asked under hold, not for a chosen 45 s. Each judgement of the viewer's report asks for what it needs again; a request it does not repeat — their buffer stopped draining, the room for a step up went, the smaller picture is no longer smaller — is let go and said once.
|
|
8
|
+
- **Fix**: The speed a file's supply asks for is no longer cut down at a lost share of 0.99. The share is strictly below one wherever a figure is given — at least one gap of delivery lies inside the span — so the speed is finite; a supply that delivered for a thousandth of the time now asks a thousand times realtime instead of a hundred, which no re-encode meets and which leaves the copy.
|
|
9
|
+
- **Fix**: An encoder's speed has one owner, the run. It measures itself between two closed pieces over its own working time and announces each reading once; the plan, the quality budget, the playback forecast and the learned price all read that one figure. The plan and the orchestrator used ffmpeg's cumulative `speed=` instead, which divides by every second the encoder was stopped or its input starved, so a slow swarm read as a slow machine and the plan bought encoders that read the same input. Gone with it: the orchestrator's per-output copy of the fastest speed (which nothing filled any more), the quality state's copy of the last reading, and the `speed` field of a progress report, which no page reads.
|
|
10
|
+
- **Chore**: Whether a closed piece holds the whole of its stretch is decided by the encoding (`encode/piece-completeness.js`), which knows the cut table: `EncodeRuns` judges a closed file before it is published and a stored piece left by an earlier process when the forecast reads it. The segment store keeps the bytes and the media intervals read from them, and takes a piece off the disk only at the encoding's word. Behaviour is unchanged. The store used to decide it, which made it read the cut table, a fact of the encoding.
|
|
11
|
+
- **Chore**: The three-second shortest window a speed was read over is gone: a speed is the difference of two samples taken at the encoder's own progress reports. So is the check that kept a starved reading out of the learned price, which judged starvation by two chosen numbers (`fileProgress >= 0.999` and a download rate against the file's byte rate); the waiting is now subtracted instead of guessed at.
|
|
12
|
+
|
|
1
13
|
## 2.89.3
|
|
2
14
|
|
|
3
15
|
- **Chore**: The same release as 2.89.2, which the registry accepted as a staged version and never published (`409 Cannot publish over previously staged version` with no staged version listed).
|
|
@@ -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;
|