@torrent-tv/proxy 2.80.5 → 2.80.6
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 +32 -0
- package/package.json +1 -1
- package/routes/api/delivery-sink/get.js +8 -4
- package/server.js +415 -403
- package/services/data-channel-handler.js +87 -25
- package/services/delivery-probe.js +38 -5
- package/services/encode/CoverageMap.js +77 -4
- package/services/encode/EncodePlan.js +1025 -358
- package/services/encode/EncodeRun.js +19 -1
- package/services/encode/SegmentDemand.js +0 -0
- package/services/encode/SegmentStore.js +53 -1
- package/services/encode/open-piece.js +22 -1
- package/services/encode/run-command.js +12 -2
- package/services/hls-session-manager.js +38 -158
- package/services/hwaccel.js +182 -54
- package/services/orchestrators/EncodeOrchestrator.js +118 -89
- package/services/output/LiveOutputs.js +233 -213
- package/services/output/Timeline.js +333 -256
- package/services/priority/PriorityMap.js +262 -108
- package/services/priority/PriorityOrchestrator.js +31 -6
- package/services/quality/EncodeCost.js +555 -500
- package/services/torrent-pool.js +9 -4
- package/test/encode-orchestrator.test.js +195 -65
- package/test/encode-plan-viewers.test.js +719 -0
- package/test/encode-plan.test.js +174 -81
- package/test/open-piece.test.js +40 -0
- package/test/output-speed.test.js +86 -0
- package/test/priority-map-download.test.js +25 -7
- package/test/priority-map.test.js +134 -83
- package/test/seek-landing.test.js +109 -76
- package/test/segment-demand.test.js +54 -56
- package/test/wedge-certainty.test.js +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,35 @@
|
|
|
1
|
+
## 2.80.6
|
|
2
|
+
|
|
3
|
+
- **Fix**: `waitForBufferDrain` resolved after 5 s while `bufferedAmount` was still above `LOW_WATER` and the send loop kept queueing into a wedged association — `399 MB` in `libdatachannel` `rss 455→982 MB` `drainMs=5000` on every line for 6 h 50 min. Now waits until `LOW_WATER` with `50 ms` polling and never resolves while bytes are still held.
|
|
4
|
+
- **Fix**: `wedgeIsCertain` required `queuedBytes>0` — on the real wedge the channel queue was `0` from `10:47:15` to `11:00`, so the declaration was `16` minutes late. Now `queued=0` is allowed, `drainMs=0`, `needMs` is the longest healthy flat and probe interval.
|
|
5
|
+
- **Fix**: `peerStillSending` was read from transport's `bytesReceived` which advances on SACKs `~140 B/s` on a wedge and gave a false `flowing` every other tick — counted `49 295` stuck lines and `13` captures of one wedge. Now sums `report.channels[].bytes` and `peerChannelBytes` where available, with `30 s` rate-limit on the stuck line and single capture per wedge.
|
|
6
|
+
- **Fix**: The sound played 130 ms ahead of the picture from every encoder restart onward, and the viewer saw lips out of step with the voice from two minutes in. The offset that makes ffmpeg land on a keyframe is for a COPIED picture and is excluded for a re-encode — but the exclusion asked whether the picture is re-encoded, and an output carrying only sound has no picture, so it answered no and took the whole offset onto its own seek. Measured in the field 2026-09-06: both outputs repositioned to one boundary, both given `-ss 166.963435`, the copied picture landing on the keyframe at 166.833 and the re-encoded sound beginning where it was asked. Two checks pin it, and the first fails without the change.
|
|
7
|
+
- **New**: The priority map is a map. One number per second of film, in an array as long as the film, beside a second array saying how long until somebody plays that second and a third saying which side of the viewers it lies on. It was a list of stretches carrying two numbers each; ranges are still what the swarm and the encoders are given, and they are now a view derived from the map rather than the map itself.
|
|
8
|
+
- **Fix**: A pause removes the TIME, not the DIRECTION. A stopped viewer's map used to collapse to one flat value over the whole film, so their position disappeared, the film in front of them counted as behind them, and the encoders were free to wander to the start of the file — measured: a viewer at 7:20 with the film in front of them unmade, and both encoders placed at #0 and #50.
|
|
9
|
+
- **Fix**: Which side of the viewers a stretch lies on is STATED by the map instead of guessed from the absence of a deadline. The guess is right only for a viewer who is watching; for a stopped one it inverted every score.
|
|
10
|
+
- **Fix**: Two decisions in the encode plan were made outside the score, and one of them could not work at all: driving an encoder on against moving it compared two scores with `<`, and a score is three numbers, so the comparison was always false and that whole branch was dead. The other kept a run because "there is only one" and stopped it because "nothing is ahead" — both are answers the score gives, and it gives them without a rule to contradict.
|
|
11
|
+
- **New**: This host measures how fast it COPIES a picture, at startup, beside the measurements it already takes of encoding and decoding. A copied picture does neither of those — it reads packets and writes them out again — so that whole branch had no figure at all, and every decision in the encoding layer is made from arrivals, which cannot be computed without one. A copied output was therefore planned with no speed until its own encoder had been running long enough to report one, which is the cold start: the moment the question matters most. Read as a slope over ffmpeg's own progress, like the others, so the process starting is outside the figure; 602x on a developer's desktop, taken in 1.9 s.
|
|
12
|
+
- **Chore**: How fast one output is produced is answered in one place, from three measurements in order of how specific they are: what a run on that output has been seen doing, then the startup benchmark for a re-encoded picture, then the startup copy measurement for a copied one. The plan no longer has a case for "no speed known", because there is no such state.
|
|
13
|
+
- **Fix**: What a second encoder costs the first is READ OFF THIS HOST'S OWN MEASUREMENT rather than assumed. The score compares arrangements of encoders, and without that price every extra process is free — so it always wanted more of them, and more of them on one machine is slower for everybody. The startup measurement already existed and was not reaching this layer; past the range it was measured over it holds the largest reading rather than continuing a curve two points cannot describe.
|
|
14
|
+
- **Fix**: Seconds of waiting are counted by walking forward through the film, not by adding up each piece's own lateness separately. A viewer who is stopped is not watching, so one wait pushes every deadline behind it back by its own length. Added up separately, the far end of a long film outvoted the film under the viewer's feet: measured, the only encoder was placed thirteen pieces PAST a waiting viewer, because thirteen pieces of certain waiting scored less than 886 distant pieces arriving slightly later.
|
|
15
|
+
- **Fix**: When the film in front of the viewers is finished is counted over the FILM, not over the encoders. Charged to whichever side of the viewers each encoder stood on, an arrangement with every encoder behind them had nothing at all charged to the film in front — which is the best value there is — so the plan abandoned the film in front of a viewer and put both encoders at the start of the file.
|
|
16
|
+
- **Chore**: The 0.12 s a restart was priced at, measured on one machine and written into the code for every machine, is gone. What a start and a stop cost is taken from this host's own runs, which was already being measured and was being added to that constant.
|
|
17
|
+
- **Fix**: An encoder claims the whole stretch it was given, not the one piece it is writing this second. A run with no declared end claimed `[from, head]`, so the plan read the road in front of a working encoder as free and put more encoders on it — three processes writing one directory with the same names, field 2026-09-06. The first of them was then cleaned up after, the cleanup had no upper bound for a run with no end, and it took a file the live encoder had just finished; the number is spent for good, because names only grow. Under that name the browser was served 110 698 bytes where its neighbours are 12 MB — 40 ms of film instead of 10.4 s — and the picture stood still for 647 s. Three readings of `-1` in three places are now one.
|
|
18
|
+
- **New**: Where encoders belong and who stands where is decided by ONE score, evaluated. Every arrangement of encoders is priced in seconds — how late the film would be, the work that would be done twice, the film that would be abandoned, what a restart costs and what a second process costs the first — and the cheapest is taken. Four local rules were written first and all four had to go: each approximated the same objective from a different side, so together they contradicted one another and the answer depended on which ran first. The rules were the fault; the score is the fix.
|
|
19
|
+
- **Fix**: How fast this machine encodes an output is kept across restarts. It was read off the running process alone, so a moved encoder began as one whose speed nothing had measured — and an unmeasured speed is read as "no arrival can be computed", which is every decision in this layer. One restart therefore blinded the plan until the new process had run long enough to be measured again.
|
|
20
|
+
- **Fix**: An encoder is held only while the film would be worse off without it. One bought for a place a viewer passed through went on costing the machine a process while another already reached everything it would; the score is asked directly — take it away and see — and refuses the moment anything becomes later or any film is abandoned.
|
|
21
|
+
- **New**: Where encoders belong is decided by a model instead of a list of cases. Every number carries a deadline — the seconds until somebody needs it, which is the distance to them, so a peak moves forward on its own as they watch, jumps on a seek, and flattens on a pause. An encoder placed at `a` delivers `a + j` at `(j + 1) / r`, and that same expression answers when the one already placed would get there. A number is late when it arrives after its deadline; placement is first-fit over the line, taking the soonest deadline first. Non-overlap and the number of processes stop being rules and become consequences. This is the known result for fixed-order scheduling with deadlines, where first-fit is optimal at unit processing times, and a piece is one unit.
|
|
22
|
+
- **Fix**: Placing an encoder is priced against what it costs. A new one does not begin at once — its process starts and its input opens, both measured — so a number one second late no longer buys a process that takes longer than that to produce anything.
|
|
23
|
+
- **Fix**: Nothing is placed where nothing can be late. A stretch nobody is coming to — behind a viewer, past the last of them, or the whole film while everybody is paused — took an encoder from the people who are watching and prevented nothing. A paused viewer now makes nobody work. The film is still encoded whole by the run that exists, which is never stopped for leaving a window.
|
|
24
|
+
- **Fix**: When the machine affords no more encoders, the ones it has stand where something is due. A viewer who seeked far ahead was served by nobody: the run that existed kept its road because nothing covered what lay in front of IT, and the budget left nothing to place.
|
|
25
|
+
- **Fix**: An encoder learns which pieces it made from its own ready channel rather than from a listing of the directory it shares. It used to be handed every number on disk at or above its start, so a run beginning at 3:39 reported reaching the end of the film through 483 files another process wrote — and its head, which the claim and the cleanup both rest on, was not its own.
|
|
26
|
+
- **Fix**: Placing an encoder is priced against what it costs somebody else. Cutting in front of a working run forces that run to stop and start again at its own head, because where a run ends is fixed when its process starts; that price — kill, start, first bytes, all measured — is now in the comparison. Without it the model could call a trade a gain while it was a loss: seconds saved for one viewer, paid for with a restart in front of another. The original scheduling problem has no such term, its machines being free to start and stop.
|
|
27
|
+
- **Fix**: Cleaning up after a dead run reaches no further than the run itself did. A piece is opened only just past the last one named, so `proven + 1` bounds it — and where a run named nothing, its own first number does. The declared end remains the bound wherever there is one; this is the bound of last resort, for a run given none, which before had no bound at all and whose cleanup took the highest-numbered file in a directory every run of the output writes into.
|
|
28
|
+
- **New**: A piece taken as finished only because the next one exists — with nothing from a run to say so — is reported with its size. That is the one way an unfinished piece can still be served, and it is now expected only for pieces left by a previous life of this process.
|
|
29
|
+
- **Chore**: The priority map is built ONCE, in the layer that owns it, and both sides that act on it read that one. It used to be built twice — once per viewer by the session and stated to the encoding as a window each, and once again for the downloading — so one question had two answers. The encoding's copy also carried the VIEWER'S NAME as the key of a claim, which is the last place a viewer appeared inside the encoding at all; there is now no name to pass and no way to ask about one. Nothing is released when somebody leaves: the map that arrives next does not have them in it, and an empty map says nobody is coming anywhere in that output.
|
|
30
|
+
- **Chore**: Turning a map stated in seconds of film into an output's own piece numbers moved to the cut table it converts against. It was in the session manager, which is a place rather than a layer.
|
|
31
|
+
- **Chore**: The encode layer no longer reaches into the priority layer. Flattening overlapping stretches is arithmetic belonging to neither and now lives on its own; the demand register answers for its own units.
|
|
32
|
+
|
|
1
33
|
## 2.80.5
|
|
2
34
|
|
|
3
35
|
- **Fix**: A piece is kept only where the run PROVED it finished, which closes the half 2.80.4 left open. That release recognised the piece ffmpeg writes out on `SIGTERM` and named it by when it arrived on the ready channel — right for our own stop, and blind to every other way a run ends. Killed harder, or dying on its own, a run leaves the same piece half-written and with no name at all; it decodes just as well and is just as short. The question asked of the highest-numbered file in a run's stretch is no longer what it contains but whether the run named it while still running normally, so all three endings are covered by one fact the run already holds.
|
package/package.json
CHANGED
|
@@ -32,11 +32,15 @@ export async function handleApiDeliverySinkGet(req, reply, { enabled } = {}) {
|
|
|
32
32
|
const total = Number.isFinite(requested)
|
|
33
33
|
? Math.min(Math.max(Math.trunc(requested), 0), MAX_SINK_BYTES)
|
|
34
34
|
: DEFAULT_SINK_BYTES;
|
|
35
|
+
const requestedChunk = Number((req.query ?? {}).chunkBytes);
|
|
36
|
+
const chunkBytes = Number.isFinite(requestedChunk)
|
|
37
|
+
? Math.min(Math.max(Math.trunc(requestedChunk), 1024), CHUNK_BYTES)
|
|
38
|
+
: CHUNK_BYTES;
|
|
35
39
|
|
|
36
40
|
// One buffer, reused: allocating a fresh chunk per iteration would make this
|
|
37
41
|
// route measure the allocator as much as the transport.
|
|
38
|
-
const chunk = Buffer.alloc(
|
|
39
|
-
for (let i = 0; i <
|
|
42
|
+
const chunk = Buffer.alloc(chunkBytes);
|
|
43
|
+
for (let i = 0; i < chunkBytes; i += 1) {
|
|
40
44
|
chunk[i] = i % 251; // 251 is prime, so the pattern does not align to any power of two.
|
|
41
45
|
}
|
|
42
46
|
|
|
@@ -51,8 +55,8 @@ export async function handleApiDeliverySinkGet(req, reply, { enabled } = {}) {
|
|
|
51
55
|
controller.close();
|
|
52
56
|
return;
|
|
53
57
|
}
|
|
54
|
-
const size = Math.min(
|
|
55
|
-
controller.enqueue(size ===
|
|
58
|
+
const size = Math.min(chunkBytes, total - sent);
|
|
59
|
+
controller.enqueue(size === chunkBytes ? chunk : chunk.subarray(0, size));
|
|
56
60
|
sent += size;
|
|
57
61
|
}
|
|
58
62
|
});
|