@torrent-tv/proxy 2.77.0 → 2.79.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 +29 -0
- package/biome.json +1 -0
- package/package.json +1 -1
- package/routes/api/transcode-sessions/net-report/post.js +5 -0
- package/services/encode/EncodePlan.js +309 -272
- package/services/encode/EncodeRun.js +70 -0
- package/services/encode/SegmentDemand.js +0 -0
- package/services/encode/SegmentStore.js +59 -0
- package/services/encode/open-piece.js +88 -0
- package/services/encode/run-budget.js +89 -0
- package/services/encode/run-command.js +15 -0
- package/services/encode/run-costs.js +94 -0
- package/services/hls-session-manager.js +10657 -10846
- package/services/orchestrators/EncodeOrchestrator.js +147 -20
- package/services/priority/PriorityMap.js +252 -0
- package/services/viewer/Viewer.js +83 -0
- package/test/encode-orchestrator.test.js +36 -8
- package/test/encode-plan.test.js +111 -7
- package/test/one-authority.test.js +105 -0
- package/test/priority-map.test.js +139 -0
- package/test/run-intervals.test.js +67 -303
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,32 @@
|
|
|
1
|
+
## 2.79.0
|
|
2
|
+
|
|
3
|
+
- **Fix**: Starting an encoder no longer stops another one. The line that did it looked for a live run whose own start was not below the new one's and killed it — a rule left from when a session held exactly one run and "the previous one" meant "the only one". Once a session could hold several, it began killing runs the plan had decided to keep: 294 stops for that reason in eight minutes of field 2026-09-05, against four starts a viewer had asked for. Who is stopped is decided in one place now, and starting is not it.
|
|
4
|
+
- **Fix**: The stretch an encoder is given reaches it. The plan computed where a run may work to, passed it, and the receiving parameter list did not name it — so it was dropped and the end was computed a second time somewhere else. That was the last remaining second answer to "how far may this run work".
|
|
5
|
+
- **Fix**: An encoder is built and handed back in the same call, so the stretch it is making is held from that instant. It used to answer with nothing and start the encoder behind the answer, leaving the stretch free in the map for as long as that took — and every pass in between started another encoder for it. Measured in the field: 684 starts in 482 seconds, of which 973 answers said the encoder was not there yet, which is every start without exception. Nothing is waited for on that path any more; the one thing it used to wait for was the death of the run it was replacing.
|
|
6
|
+
- **Fix**: Whether to move a running encoder is a comparison with both sides counted, and the terms are measured on this host rather than written into the code. Driving through material that exists costs this run's own encode time for it and costs the swarm the same bytes a second time; moving costs the death of the run, the start of another and the wait for its first output. A run whose speed nothing has measured is now KEPT: moving costs a known amount for an unknown gain, and a run that has produced nothing has nothing worth taking away. It used to answer the other way, and with the whole film ahead already made, every just-started encoder was moved before it produced anything.
|
|
7
|
+
- **New**: What a stop and a start cost is measured (`encode/run-costs.js`). A run times its own death from the moment it is told to stop, and the appearance of its first output from the moment it started. Both were absent: the start was one reading taken once on one machine and written in as a constant, and the other two were not counted at all, so the comparison priced one side of itself.
|
|
8
|
+
- **New**: The encoding orchestrator says what it believes — what is ready, which zones were stated and in what order, which encoders are running and at what speed, and the lowest number a viewer is waiting for. The line was written and called from nowhere, so on 2026-09-05 the question "why did the plan not see the gap the viewer was stopped at" had to be answered by inference and was not answered at all. Printed when it changes rather than on a timer.
|
|
9
|
+
- **Chore**: The piece a run had open when it ended is thrown away by the layer that owns the directories (`encode/open-piece.js`, `SegmentStore.discardOpenPieceOf`), on every ending but the one normal one. It used to be done by the single place that killed a run, and that place is gone.
|
|
10
|
+
|
|
11
|
+
- **Fix**: A seek does one thing — it puts the viewer where they now are. It used to do eleven, and wrote that position into five places: two fields on the picture's session, two on the soundtrack's, and the viewer. It also asked whether the jump would drag another viewer back, started an encoder itself, cancelled outstanding requests, backed off a segment to the preceding keyframe and set a timer to restart ffmpeg — so it was a third authority over the encoders, and not one of its branches ever asked what had already been made. A viewer jumping into a stretch that was finished and on disk got a fresh encoder for it.
|
|
12
|
+
- **Fix**: A viewer's position is held in SECONDS of film and nowhere else. It used to carry a segment number too, and the picture and the soundtrack of one film are cut independently and into different numbers of pieces — 454 against 401 on the field file of 2026-09-05 — so piece 48 of one is not the same moment as piece 48 of the other. Whoever holds a cut grid turns the seconds into their own numbers.
|
|
13
|
+
- **New**: The page says whether the picture is moving, and says it the moment it changes rather than at the next ten-second report. A viewer who has stopped consumes nothing, so nothing in front of them ever falls due and the work goes to whoever is watching — which follows from one boolean without any rule about pauses anywhere. Inferring it from a position that has not moved takes two reports and is wrong whenever a browser holding a full cushion goes quiet between segments, which it does.
|
|
14
|
+
- **Fix**: Below realtime the priority map says how many encoders are needed, and the zones grow rather than being equal. An encoder starting at `q` stays ahead of a viewer at `p` for `(q - p) x s / (1 - s)`, which grows with its distance from them — so equal shares are the wrong division, and by a wide margin: on the addon host with 2400 s of film left, 120 s held and 0.5x, equal shares need twenty encoders and growing ones need five (120, 240, 480, 960, 1920). Each zone is exactly as long as its bound allows, which makes the count the smallest that holds the viewer.
|
|
15
|
+
- **Fix**: How many encoders may run is the smallest of three measured limits, not one. The processor was the only one asked. Every encoder reads the same torrent, so together they cannot consume faster than it is delivered — on the field file a second of film weighs 842 KB against 2119-3347 KB/s from the swarm, so the speeds together may reach 2.5-4x, and a copy the processor would run at 8x runs at four. Encoders placed apart hold reading windows that do not overlap, so the piece store must hold their sum; on the addon host it was already full on the readers of one viewer. Which limit decided is said in the line.
|
|
16
|
+
- **New**: A piece is proven finished by the encoder's own word for it: ffmpeg names each file on a channel of its own the moment it closes it, and that name is kept. What stood there was the existence of the NEXT file, with two exceptions bolted on because it is not true — it is never true of the last piece of a run, so the first segment of every run was held from the viewer. Measured 2026-08-09: #807 held while it lay on disk, and in August the same shape held #317 for 46 seconds and then answered 404 to a browser that had given up.
|
|
17
|
+
- **Chore**: The priority map is a layer of its own (`services/priority/`), below both orchestrators and depending on nothing. It knows only priorities: what is downloaded is the download orchestrator's own knowledge, what is encoded is the encoding orchestrator's, and neither is visible from the map.
|
|
18
|
+
- **Chore**: A check on the shape rather than the behaviour (`test/one-authority.test.js`): one place stops an encoder for scheduling reasons, one place builds one, a start stops nothing, a seek starts nothing, and the stretch computed by the plan is the one handed over. Every rule in it is one that was broken on 2026-09-05.
|
|
19
|
+
|
|
20
|
+
## 2.78.0
|
|
21
|
+
|
|
22
|
+
- **Fix**: One authority over the encoders, where there were three. A viewer watching an episode on 2026-09-05 had their picture stop for 125 seconds while the proxy spawned and killed an encoder every 350-700ms, producing nothing. Two decisions were contradicting each other on every pass: the plan commanded a start inside the viewer's window, at #46; `planRunInterval` in the session manager moved that start to #78, because it counted every live run as claiming up to its head plus the look-ahead; and the plan then saw a run at #78 against a window of [27, 57], found no overlap and killed it as "nothing it was given is wanted". Neither coverage nor demand changed between two such passes, so the same start was commanded again, for as long as anybody watched. `planRunInterval` is deleted: where a run starts is the plan's decision and nothing moves it, and how far it may work is read off the one coverage map the plan itself uses.
|
|
23
|
+
- **Fix**: A run is never stopped for standing outside a viewer's window. While a file is being encoded it is encoded whole; a viewer decides the ORDER the work is taken in and, through the machine's budget, how many processes take it — never whether a run may go on living. The stop this replaced was the other half of the contradiction above, and with it went the last two uses of "does this run overlap a window" in the file.
|
|
24
|
+
- **New**: What a viewer needs is a map, and the maps of several viewers merge into one (`services/encode/DemandMap.js`). Three zones per viewer, in seconds of film: what must be ready before they set off so that they never stop, what the machine reaches while they watch that, and the rest of the track. Merging takes the highest priority per second, so film two people want is as urgent as the more urgent of them and is made once. Which viewer asked never reaches the encoders.
|
|
25
|
+
- **Fix**: The size of that first zone is measured rather than chosen. Below realtime an encoder loses `1 - speed` of a second of film for every second played, so over the film in front of a viewer the shortfall is `remaining × (1 - speed)`, and that much has to exist before they start or they meet a stall partway through — at 0.25x on 400 seconds of film, 300 seconds. Above realtime only the allowance this file's own recent interruptions have shown to be necessary is needed. The 120 seconds that used to size this window were `LOOKAHEAD_PAUSE_SECONDS`, the threshold of a suspended encoder, one chosen number answering seven unrelated questions.
|
|
26
|
+
- **Fix**: The look-ahead limiter is gone, with both of its chosen numbers. It suspended a run 120 seconds in front of the viewer and woke it at 60 — deliberately pushing a run past the window the plan was asking about, which is what the plan then killed it for. What remains of it is a reading: how much film is ready in front of the earliest viewer, which commands nothing.
|
|
27
|
+
- **Fix**: A zone's priority reaches the register instead of being dropped on the way. `want()` took only the two ends, so the order the map states was computed and then discarded. It is a property of the window now, and the claimant's name stays the person's, so a viewer who leaves takes all three of their zones with them.
|
|
28
|
+
- **Fix**: The measured encode speed is read from the field that exists. `progress.speedX` appears nowhere in this codebase, so the speed was always zero and the middle zone was never built at all; the reading the budget already trusts is `recentSpeed.speed`.
|
|
29
|
+
|
|
1
30
|
## 2.77.0
|
|
2
31
|
|
|
3
32
|
- **Fix**: A viewer who has just arrived is watching. Presence and position were one field — the segment they last asked for — so a viewer who had asked for nothing counted as absent, and an output all of whose viewers count as absent has every encoder on it stopped. Field 2026-09-05: a soundtrack's encoder was stopped 1.25 s after it started, with nothing produced, so its `init.mp4` was never made; the picture could not be played without it, the browser waited sixty seconds and was told the proxy had sent no video. The viewer could not have rescued themselves — the segment request that would have marked them present needs the `init.mp4` the stopped encoder was going to make. They are now two facts: position is set the moment they arrive, from the position their own request names, and presence is a fact of their connection.
|
package/biome.json
CHANGED
package/package.json
CHANGED
|
@@ -33,11 +33,16 @@ export async function handleApiTranscodeSessionNetReportPost(req, reply, { hlsSe
|
|
|
33
33
|
// the proxy uses to tell the viewers of one session apart, and a report
|
|
34
34
|
// without them is still a truthful reading of somebody's link.
|
|
35
35
|
const consumerId = typeof body.consumerId === "string" ? body.consumerId.trim() : "";
|
|
36
|
+
// Whether the picture is moving. Absent from a page that does not say, and
|
|
37
|
+
// then the viewer counts as playing, which is what every page meant before it
|
|
38
|
+
// could say otherwise.
|
|
39
|
+
const playing = typeof body.playing === "boolean" ? body.playing : undefined;
|
|
36
40
|
const positionSeconds = Number(body.positionSeconds);
|
|
37
41
|
const recorded = hlsSessionManager.recordNetReport(sessionId, {
|
|
38
42
|
linkMbps,
|
|
39
43
|
bufferedAheadSec,
|
|
40
44
|
consumerId,
|
|
45
|
+
playing,
|
|
41
46
|
positionSeconds:
|
|
42
47
|
Number.isFinite(positionSeconds) && positionSeconds >= 0 ? positionSeconds : undefined
|
|
43
48
|
});
|