@torrent-tv/proxy 2.53.0 → 2.55.0
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/CHANGELOG.md +27 -0
- package/package.json +1 -1
- package/services/hls-session-manager.js +697 -169
- package/services/hwaccel.js +396 -49
- package/services/segment-formats/fmp4.js +320 -300
- package/services/segment-formats/mp4-boxes.js +59 -0
- package/test/auto-quality-step.test.js +442 -0
- package/test/decode-measurement.test.js +73 -0
- package/test/quality-variants.test.js +6 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
## 2.55.0
|
|
2
|
+
|
|
3
|
+
- **Fix**: The decode calibration was measuring the loop rather than the decode. It looped each clip with `-stream_loop -1`, and a loop is not free: measured 2026-08-22, a restart costs **0.03 s on the 480p clip and 0.12 s on the 1080p one** — it scales with the picture, so it is the decoder tearing down and re-allocating its frame buffers rather than anything about reading the file. A five-second clip decoded at 55x restarts eleven times a second, and that cost dominated the reading: the same clips measured 53.7x looped against 80.3x in one continuous pass, and 11.8x against 15.8x. Worse, the bias depends on BOTH the clip's own resolution and the host's speed — the two axes the fit exists to separate — so it did not cancel out, it tilted the fit. That is the fast-host failure recorded on 2026-08-20, where a desktop read 1080p at 9.35 Mbit/s as cheaper than 720p at 9.94, an ordering no decoder produces, and the H.264 fit refused to solve at all. A host that could not fit H.264 got no decode figure whatsoever, which is exactly the host most able to serve.
|
|
4
|
+
- **New**: The clip is fed to the decoder as ONE Annex-B elementary stream, written to its stdin over and over. Parameter sets are inline in Annex-B and it can be joined by plain byte concatenation — that is what a broadcast is — so more bytes are simply more stream: nothing re-opens, nothing re-initialises, and there is no restart inside the measured window. The lift out of the container is a copy, not a re-encode, and it goes straight to a pipe: no temporary media is written at any point. Error against the continuous-pass truth is now −0.2 % and −5.5 %, with the readings spread 2-6 %, against −25 % and −33 % for the loop. Every reading on the developer's desktop moved, by up to 66 %, and they are monotonic in both axes for the first time.
|
|
5
|
+
- **New**: The measured window is half a second instead of one. What used to make a long window necessary was the clip restarting inside it; with the stream continuous, the only thing left to average over is the timing jitter of two progress lines, which is milliseconds. Measured at half a second: −3.0 % and +0.6 %.
|
|
6
|
+
- **Fix**: The contention penalty was wrong for the same reason. It compares a decode alone against the same decode beside an encoder, and both readings carried the loop, but not equally — the machine's speed differs between them. Decoding alone now reads 77.5x where it read about 53x.
|
|
7
|
+
- **Chore**: A codec family's clips are lifted out of their containers in ONE ffmpeg run, and the startup is shorter than before rather than longer. The lift is a copy, so its cost is almost entirely the process: one per clip added 11 s here, and running them concurrently did not help — six at once took 4.75 s against 0.89 s for one, so the machine serialises them. One run with many inputs and many outputs costs one process. Measured end to end on the same desktop, alternating old and new: **35.3 s before, 31.6 s after**, with the readings corrected. The contention benchmark lifts its clip once and decodes the same bytes three times instead of lifting it again for each reading. Done before the measurements and never beside them: a remux running next to a decode is a second job on the machine, and this benchmark exists to find out what one job costs.
|
|
8
|
+
- **Fix**: The lift has a time bound and is killed on it. It was the only ffmpeg run in this file without one, and it is awaited before the proxy's tunnel opens — so a remux that never exited was a startup that never finished, with nothing said. Its failures, and the decode's, now carry ffmpeg's own last line instead of "said nothing".
|
|
9
|
+
- **Fix**: A codec family with no Annex-B mapping fails by name instead of being lifted with H.264's filter. This matters for what comes next: AV1 has no Annex-B form at all — its packaging is OBU — and MPEG-2 and VC-1 have no `*_mp4toannexb` filter, so all three of the families the roadmap plans need another route through here, and finding that out as "the clip failed" would send the reader after the clip.
|
|
10
|
+
- **Chore**: `test/decode-measurement.test.js` states the orderings the readings must have — a bigger picture costs more than a smaller one at the same bitrate, a thicker stream more than a thin one at the same size, HEVC more than H.264 — rather than any number, since the numbers belong to whatever machine runs them. Those are the properties the loop inverted, and nothing was checking them.
|
|
11
|
+
|
|
12
|
+
## 2.54.0
|
|
13
|
+
|
|
14
|
+
- **Fix**: The automatic quality step no longer changes the SIZE of the picture underneath a session the browser is already decoding. The fMP4 init segment is fetched once — a player reads `#EXT-X-MAP` and never asks again — and `avc1` keeps SPS and PPS in it rather than in the fragments, so every fragment produced after a size change was decoded against parameter sets describing a picture that was no longer being made. Measured 2026-08-21 across five viewing attempts: both re-encoded sessions of the five were destroyed by it. On `LXH-12.TS` the encoder left 1280x720 for 960x540 at 13:30:06 and the browser went on reporting `decode … size=1280x720` for the next three and a half minutes — 67 readings, not one of them 960x540 — while the viewer watched a band of macroblock garbage over a smeared field. On `c0930.com_chijyo0073.wmv` the same act at 13:35:36 made the element error on the first mismatched fragment, close the MediaSource, throw `bufferAppendError InvalidStateError` on both tracks and sit at `size=0x0 readyState=0` for four and a half minutes. Which of the two happens is the decoder's choice, not ours, and no layer reported an error either time. A change of resolution is a change of VARIANT, as the standard has it: every height is already published in the master with its own init, so the proxy now ASKS the browser to move — the same act the manual menu performs, which has never had this fault.
|
|
15
|
+
- **Fix**: The step is decided on the rate the encoder is making NOW, read as the slope between two progress reports. ffmpeg's `-progress speed=` is cumulative — output time over wall time since the run began — so a run starved early carries that average for life. `LXH-12.TS` spent its first four minutes on a swarm giving ~100 KB/s and the budget correctly refused to act while it could see that, five times. The download recovered at 13:29:56; ten seconds later the cumulative figure still read 0.39x, the machine was now genuinely busy, the classifier answered CPU, and it stepped down a rung that the progress lines themselves show running at **1.30x** — 13 s of video in 10.02 s of wall clock. The same mistake was found and solved once already: the startup decode benchmark reads the slope between two reports for exactly this reason.
|
|
16
|
+
- **New**: The step BACK UP exists. `budgetRungIndex` was written in exactly one place in this codebase's whole life, `nextIndex = session.budgetRungIndex + 1`, so there was no way up from anywhere. A session whose encoder has stayed ahead of realtime and whose viewer's link can carry the next rung's allowed peak, unbroken for four times the window a step down needs, is asked back up one rung at a time — never above the source. A bitrate cap is lifted before the picture is enlarged, because it is the cheaper of the two and the one the viewer notices first.
|
|
17
|
+
- **New**: The quality step exists on the COPY path, which had no automatic behaviour at all — the budget loop left on `!session.transcodeVideo`, so a copied picture too thick for the viewer's link had nothing to answer with. A copy is not being encoded, so it has no rate to lower; the only way to send fewer bits is another rendering of the film, which is a re-encoded rung and therefore a change of variant. A copy is never stepped down for the PROCESSOR, because moving that viewer to a re-encoded rung costs the machine more, not less.
|
|
18
|
+
- **New**: A picture that the viewer's measured link cannot carry is bounded by that measurement, at the size it is already being made. `-maxrate`, `-bufsize` and CRF do not appear in the SPS — x264 writes no HRD parameters unless asked — so one init segment goes on describing every fragment, and there was no separate lever for bitrate before this. The target is not chosen: it is the link the browser reported, less the share protocol overhead and measurement noise take out of it. The preset is deliberately NOT part of this step, and that is a correction to the plan rather than an omission: a preset change moves `profile_idc`, `num_ref_frames`, `entropy_coding_mode_flag` and `transform_8x8_mode_flag`, all of which live in the same init segment as the size, so moving it in place would reproduce the fault this release exists to remove. It rides with the variant change, where the init is that variant's own.
|
|
19
|
+
- **New**: A run about to encode a picture the served init does not describe says so, once per distinct disagreement — the shape 2.48.0 uses for the TIME a run begins at. The size is read out of the init's own bytes rather than taken from our record of what the encoder was told, because those two disagreeing IS the fault. This whole class was silent: the encoder healthy, segments served in milliseconds, and nothing anywhere naming what the viewer was looking at.
|
|
20
|
+
- **Chore**: The per-session resolution ladder state is gone — `budgetLadder`, `budgetRungIndex`, `budgetDownshifts` and the cap on how many steps a session might take. A step is a change of variant now, and a variant is a session with its own init, so there is no rung index to walk. The ladder still chooses the STARTING rung when a session is made, which is untouched.
|
|
21
|
+
- **Fix**: A height this machine has been MEASURED failing at is withdrawn from the offer once the viewer has left it. The base session's own height was exempt from every refusal, which was harmless while a step rewrote the encode inside the base — that height then always WAS the rung on screen. With the step moving the viewer to another variant, the height they left went on being offered, and the way back up would have asked for the one rung the host had just been seen failing at: down, up, down, about every hundred seconds for the length of the film, each move costing a buffer flush and a cold encoder start. The rung on screen keeps its exemption, which is the one that matters; the copied source height cannot reach the refusal at all, since only re-encoding sessions have a reading to be withdrawn on.
|
|
22
|
+
- **Fix**: Lifting a bitrate cap and enlarging the picture are decided by two different questions. Both used to be answered by one — "can the link carry the NEXT rung" — and a session already at the top offered height has no next rung, so the answer was an unconditional yes: the cap came off a link measured at a fifth of what the picture needs, and fifteen seconds later `#checkLinkBudget` put it back. Two ffmpeg restarts every minute and a half, on exactly the thin cellular viewer the cap exists for. Whether to lift a cap is now asked about the picture the cap is ON.
|
|
23
|
+
- **Fix**: The bitrate cap has a floor, and a link report that is not a positive finite number is not a measurement. `linkMbps` reaches an encoder's `-maxrate` from the browser and was taken verbatim: a reading of 0.05 produced `-maxrate 40k` on a 720p encode, and because the cap only ever tightened, one bad reading pinned the session there for the rest of the film. The floor is what the SMALLEST picture this file is offered at is sized to carry — below that the link is not short of bitrate at this size, it is short of the size, and the answer is a smaller variant.
|
|
24
|
+
- **Fix**: One condition decides whether a stream publishes a master playlist, so the builder and the budget cannot disagree about it. A copied stream whose keyframe index could not be read falls back to an even grid ffmpeg does not cut on; the builder refuses it, but the budget looked only at how many heights could in principle be spliced, and recorded requests against a player that has no variants — once per window, for the whole film. It now refuses the same streams, and says why once per session rather than once per window.
|
|
25
|
+
- **Fix**: The line about a run leaving the served init behind is not written when nothing was told. `computeOutputDimensions` reads a zero target width as "no width constraint" while the encoder descriptors read it as their own default of 1280, so on a hardware host — where the budget returns nothing and the width stays zero — a 1920x816 scope source at the 720p variant would have been reported as a size disagreement that does not exist. A line whose whole purpose is to name an otherwise-silent fatal class must not cry wolf on letterboxed content.
|
|
26
|
+
- **Chore**: One pass of the quality budget has a public name, `runQualityBudgetOnce`. A loop that decides what the viewer sees and can only be reached through `setInterval` is a loop nothing can check; it now has nine tests.
|
|
27
|
+
|
|
1
28
|
## 2.53.0
|
|
2
29
|
|
|
3
30
|
- **Fix**: The torrent thread no longer dies of an answer that never came. Our build of `utp-native` moves to 2.5.3-ttv.4, which stops `on_utp_accept` reading a `napi_value` the callback never wrote: it asks JavaScript for the buffer that will hold the NEXT connection and handed the returned handle straight to `napi_get_buffer_info`, while the handle was an uninitialised local and the macro that fills it inspects exactly one failure — `napi_pending_exception` — and even for that one reports the exception and carries on. Every other status is discarded, and in none of those cases does napi write anything. Read from two core dumps on 2026-08-21, at 16:49 and 19:50, both on the thread that owns the uTP socket and both with the same top frames — `v8::Value::IsArrayBufferView` under `napi_get_buffer_info` under `on_utp_accept` — over an ordinary `SpinEventLoopInternal`, so it is NOT the shutdown race that 2.49.0 narrowed. Every live session on the proxy died with the process, five times in three days. The handle is now initialised and every status read; a buffer that never arrives clears `next_connection` instead of leaving it pointing at memory just handed to the connection being accepted, and an accept with no buffer is refused rather than written through null.
|