@torrent-tv/proxy 2.80.11 → 2.80.13
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 +14 -0
- package/CLAUDE.md +6 -0
- package/docs/download-architecture.md +45 -1
- package/docs/encode-architecture.md +180 -0
- package/package.json +1 -1
- package/routes/stream/get.js +324 -300
- package/routes/transcode/session-file/get.js +22 -2
- package/services/encode/SegmentDemand.js +34 -0
- package/services/hls-session-manager.js +19 -17
- package/services/orchestrators/EncodeOrchestrator.js +735 -726
- package/services/output/LiveOutputs.js +267 -282
- package/services/priority/WaitLedger.js +142 -0
- package/services/torrent-pool.js +38 -1
- package/services/torrent-worker/client.js +27 -12
- package/test/priority-map-per-output.test.js +5 -5
- package/test/quality-variants.test.js +4 -4
- package/test/wait-ledger.test.js +100 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## 2.80.13
|
|
2
|
+
|
|
3
|
+
- **New**: Whether the priority map is being served IN ITS OWN ORDER is measured, for both things that read it. Until now the map's ranks were built, published, and then thrown away by everyone who acted on them — a map read backwards would have looked identical in every line this proxy writes. The measure is one wait recorded against the rank the map gave the thing waited for at the moment it was asked for, on one scale for both layers (`services/priority/WaitLedger.js`, `SegmentDemand.rankOf`, eight checks). The encoding's own state line carries `served[now N wait(s) median Xms worst Yms, soon …, later …]` and each `[hold]` line names the band it belonged to; a band that never waited prints `none` rather than a zero, because silence is not a good result. Read: long waits at `now` mean the urgent zone is not being served first, long waits lower down with none at `now` mean the zones are the wrong width.
|
|
4
|
+
- **New**: What the swarm is ACTUALLY TOLD is said out loud, on change, per level: `the swarm is told, for "film.mkv": NEAR 1 zone(s) 42MB, AHEAD 4 zone(s) 310MB, TAIL 1 zone(s) 1900MB (7 band(s) of the map, over 1218s of film)`. `applyPriorityMap` was silent — that the map had been built was visible in the encoding's line, that the download had received it was visible nowhere, and a whole field session carried not one line about it. Per level rather than per zone, because the register has five levels against the map's however-many bands and the fit between them is the one thing there that can be wrong.
|
|
5
|
+
- **Fix**: A read that ends SHORTER than the `Content-Length` already sent says so, loudly. The header is committed before the first byte and the reader's `close()` ends the iteration with no accounting, so only the `fail()` path was ever logged: a client could receive a truncated body with no way to know it, and ffmpeg's mp4 demuxer — which holds the sample table and asks for samples past what arrived — then parses at wrong offsets. That is exactly the `Invalid NAL unit size (negative)` and `missing picture in access unit` seen 2138 times in one field session on a COPIED picture, where no encoder touches a frame. A single clean read of the same file through the same route produced none, and four concurrent ones produced none; what the field session also had was a piece store whose readers wanted every piece it could hold and 100 of 1395 evictions taking a piece a reader had declared.
|
|
6
|
+
- **Fix**: The reads that feed ffmpeg are failed when the torrent thread dies. Both death handlers walked the ordinary reads and left `#fragmentReaders` untouched — which is what `/stream` uses for an encoder — so an encoder's input neither ended nor errored, it went quiet. Field 2026-08-31, three times: the thread died, both ffmpeg runs stayed alive and stopped producing (167 `holding segment-00085.mp4 … encoder alive` lines), and the viewer was left on a spinner that never ended. Recorded as open since then.
|
|
7
|
+
- **Chore**: `hls-session-manager.js` is 2 lines shorter: the zone walk it did by hand is one call to the map that owns ranks, and the explanation of what an empty map means moved to `SegmentDemand.rankOf`, which is the thing it is about.
|
|
8
|
+
- **Chore**: Both architecture documents gain the measurement and how to read it.
|
|
9
|
+
|
|
10
|
+
## 2.80.12
|
|
11
|
+
|
|
12
|
+
- **Chore**: The predicate that decides whether an output is the one somebody is consuming asks each layer only for what it owns. Written first as `LiveOutputs.watchedBy(session, viewer)`, it had the output layer reading a viewer's fields — a coupling in the wrong direction, however plain the values. It is `supersededBy(session, stepOnScreen)` now, taking a plain id: which step is on somebody's screen belongs to the person, which output a step supersedes belongs to the film's shape, and the one place that legitimately holds both puts them together.
|
|
13
|
+
- **New**: `docs/encode-architecture.md` — who decides where encoders go, with the shape drawn: viewers become a map and the map is the whole crossing, the four things the plan decides from, why the map is read at two scopes, and a table of what each of the eight other places used to do against what it now states. `docs/download-architecture.md` gains the priority map as a claimant, which it has been since 2026-09-05 and was never drawn as, and a section saying what each claimant knows — a read states only the piece it is stopped on, which is a fact and not a forecast.
|
|
14
|
+
|
|
1
15
|
## 2.80.11
|
|
2
16
|
|
|
3
17
|
- **Fix**: ONE authority over where encoders go, and it is the plan. Eight other places placed or killed them, each by a rule of its own: the first run of a session, a viewer joining it further in, a quality step or a soundtrack being warmed, a step being switched to, a hardware encoder falling back to software, an input coming back, the cut table correcting itself, and a settled seek. The plan is arithmetic over what is made, what is being made and what is wanted; beside eight other answers it was one opinion among nine. Every one of them now states the FACT it knows — this person is here, this output is nobody's, this encoder's arguments are stale — and the plan places what follows. `this.#startEncodeRun` has exactly one caller.
|
package/CLAUDE.md
CHANGED
|
@@ -72,6 +72,12 @@ Linux-only host (e.g. POSIX-only signals must degrade elsewhere).
|
|
|
72
72
|
wanted (`demand/`) against what the swarm is told (`download/`), why urgency
|
|
73
73
|
is not a number given to the library, and why the speculative levels are
|
|
74
74
|
withdrawn rather than lowered.
|
|
75
|
+
- `docs/encode-architecture.md` — who decides where encoders go, and the
|
|
76
|
+
answer is one authority: `EncodePlan`, from what is made, what is being
|
|
77
|
+
made, what is wanted and what the host can hold. Why no viewer reaches it,
|
|
78
|
+
why the priority map is read at two scopes (per FILE for the swarm, per
|
|
79
|
+
OUTPUT for the encoders), and what each of the eight other places that used
|
|
80
|
+
to place or kill an encoder now states instead.
|
|
75
81
|
- `docs/logs.md` — where to find logs (HA `docker logs` + `/data/proxy.log`, DO
|
|
76
82
|
`infra-server-1` with forwarded frontend `POST /api/client-logs`). Browser
|
|
77
83
|
console not needed.
|
|
@@ -37,10 +37,12 @@ flowchart TB
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
subgraph Claimants["who states needs"]
|
|
40
|
-
|
|
40
|
+
PM[priority map<br/>where the viewers are, per FILE]
|
|
41
|
+
PR[piece-reader<br/>the piece it is stopped on]
|
|
41
42
|
BF[torrent-pool<br/>background fill, per file]
|
|
42
43
|
end
|
|
43
44
|
|
|
45
|
+
PM -->|state / withdraw| R
|
|
44
46
|
PR -->|state / withdraw| R
|
|
45
47
|
BF -->|state / withdraw| R
|
|
46
48
|
W --> R
|
|
@@ -51,6 +53,48 @@ flowchart TB
|
|
|
51
53
|
S -->|the only caller| WT[(WebTorrent)]
|
|
52
54
|
```
|
|
53
55
|
|
|
56
|
+
## Who the claimants are, and what each of them knows
|
|
57
|
+
|
|
58
|
+
**The priority map** states what should be downloaded AHEAD of the viewers, once
|
|
59
|
+
per file, built in `services/priority/` from where they are and from nothing
|
|
60
|
+
else. It is the same map the encoding reads — the encoding reads it per OUTPUT
|
|
61
|
+
and in segment numbers, this layer reads it per FILE and in bytes, and both
|
|
62
|
+
scopes are right for what asks them (`encode-architecture.md`).
|
|
63
|
+
|
|
64
|
+
**A read** states only the piece it is STOPPED ON. That is not a forecast but the
|
|
65
|
+
fact that somebody is waiting there, and it is what keeps working for the reads
|
|
66
|
+
the viewers' map does not cover: a container header at open, the subtitle walk, a
|
|
67
|
+
soundtrack being fetched whole. Until 2026-09-02 each read built four bands
|
|
68
|
+
around its own head instead, so fifteen reads were fifteen forecasts on a store
|
|
69
|
+
holding sixteen pieces, half of all evictions took a piece a reader had said it
|
|
70
|
+
wanted, and two thirds of reads came back from disk.
|
|
71
|
+
|
|
72
|
+
**The background fill** states one file at a time — a soundtrack pulled whole
|
|
73
|
+
once the cushion is full, and no further.
|
|
74
|
+
|
|
75
|
+
## Whether the map reaches the swarm, and in what shape
|
|
76
|
+
|
|
77
|
+
Applied in silence until 2026-09-08: that the map had been BUILT was visible in
|
|
78
|
+
the encoding's own line, that the download had received it was visible nowhere,
|
|
79
|
+
and a whole field session carried not one line about it. Said now on change, per
|
|
80
|
+
LEVEL rather than per zone — the register has five levels and the map has as many
|
|
81
|
+
bands as the film needs, and the fit between the two is the one thing here that
|
|
82
|
+
could be wrong:
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
torrent-pool: the swarm is told, for "film.mkv": NEAR 1 zone(s) 42MB,
|
|
86
|
+
AHEAD 4 zone(s) 310MB, TAIL 1 zone(s) 1900MB (7 band(s) of the map, over 1218s of film)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
In megabytes, because that is what a swarm delivers. `NEAR` absent while
|
|
90
|
+
somebody is watching means the map is not arriving; every band landing in one
|
|
91
|
+
level means the fit has collapsed.
|
|
92
|
+
|
|
93
|
+
Beside it, where the waits fell: the reader records each wait against the level
|
|
94
|
+
the map put its piece in, and the `supply` line carries the table. Long waits at
|
|
95
|
+
`blocked` mean the urgent zone is too narrow; long waits further out mean the
|
|
96
|
+
lead is.
|
|
97
|
+
|
|
54
98
|
## The five levels
|
|
55
99
|
|
|
56
100
|
| level | what it is | stated |
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Encode architecture — who decides where encoders go
|
|
2
|
+
|
|
3
|
+
One question, one answer, and the answer is arithmetic.
|
|
4
|
+
|
|
5
|
+
> Viewers are always independent and always reuse what can be reused. The
|
|
6
|
+
> number of encoders is however many are needed; how many are needed follows
|
|
7
|
+
> from which sets of output parameters are wanted and where the viewers stand
|
|
8
|
+
> inside each. Segments produced by ANY encoder are available to ANY viewer, and
|
|
9
|
+
> which viewer asked never enters the question.
|
|
10
|
+
|
|
11
|
+
## The one authority
|
|
12
|
+
|
|
13
|
+
`services/encode/EncodePlan.js` decides. Nothing else places an encoder,
|
|
14
|
+
nothing else takes one away for scheduling reasons, and there is exactly one
|
|
15
|
+
call to `#startEncodeRun` in the whole proxy — the one the plan asks through.
|
|
16
|
+
|
|
17
|
+
It decides from four things and no others:
|
|
18
|
+
|
|
19
|
+
| what | where it comes from |
|
|
20
|
+
|---|---|
|
|
21
|
+
| what is already made | the segment store, asked afresh before every decision |
|
|
22
|
+
| what is being made | the coverage map's live claims |
|
|
23
|
+
| what is wanted | the priority map of that output |
|
|
24
|
+
| how many the machine can hold | `run-budget.js`, from measurements of this host |
|
|
25
|
+
|
|
26
|
+
**No viewer reaches it.** `services/encode/` and
|
|
27
|
+
`services/orchestrators/EncodeOrchestrator.js` do not import the viewer layer,
|
|
28
|
+
name a consumer id, or hold a person. What crosses is a priority map: zones of
|
|
29
|
+
segment numbers with a rank and a real time, and nobody's name on it.
|
|
30
|
+
|
|
31
|
+
## Viewers become a map, and that is the whole crossing
|
|
32
|
+
|
|
33
|
+
```mermaid
|
|
34
|
+
flowchart TB
|
|
35
|
+
subgraph V["services/viewer — where people are"]
|
|
36
|
+
VW[Viewer<br/>position, playing, buffered, chosen track]
|
|
37
|
+
VS[Viewers<br/>the relation, indexed from both ends]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
subgraph P["services/priority — one fact, two scopes"]
|
|
41
|
+
PM[PriorityMap<br/>seconds of film to a rank]
|
|
42
|
+
PO[PriorityOrchestrator<br/>merge, publish, keep]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
subgraph E["services/encode — where encoders go"]
|
|
46
|
+
CM[CoverageMap<br/>ready / making / free]
|
|
47
|
+
EP[EncodePlan<br/>argmin over arrangements]
|
|
48
|
+
RB[run-budget<br/>what this host can hold]
|
|
49
|
+
ER[EncodeRun<br/>one process, one interval]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
subgraph D["services/demand + download — what the swarm is told"]
|
|
53
|
+
DR[DemandRegister]
|
|
54
|
+
SS[SwarmSelection]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
VW --> VS
|
|
58
|
+
VS -->|"positions, presence"| PO
|
|
59
|
+
PM --> PO
|
|
60
|
+
PO -->|"per FILE, in bytes"| DR
|
|
61
|
+
DR --> SS
|
|
62
|
+
PO -->|"per OUTPUT, in segments"| EO[EncodeOrchestrator]
|
|
63
|
+
EO --> EP
|
|
64
|
+
CM --> EP
|
|
65
|
+
RB --> EP
|
|
66
|
+
EP -->|"start / stop / move"| ER
|
|
67
|
+
ST[(SegmentStore<br/>the disk)] -->|"what is ready"| CM
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Two scopes of one map, and both are right
|
|
71
|
+
|
|
72
|
+
The map is built from where the viewers are, once, and read at two scopes.
|
|
73
|
+
|
|
74
|
+
**Per FILE, for the swarm.** The picture, a quality step and a soundtrack of one
|
|
75
|
+
film read the same bytes, so every viewer of any of them wants that file's
|
|
76
|
+
bytes. This is what `PriorityOrchestrator.mapFor(sourceKey, fileIndex)`
|
|
77
|
+
answers, and what is published to the download layer.
|
|
78
|
+
|
|
79
|
+
**Per OUTPUT, for the encoders.** A person watching 480p wants nothing of the
|
|
80
|
+
1080p output at all. This is `mapForOutput(address)`.
|
|
81
|
+
|
|
82
|
+
One map for both was the second authority over encoders. Handed the film's map,
|
|
83
|
+
the plan wanted an encoder on every output of the film; what actually stopped
|
|
84
|
+
the ones nobody was watching was the session manager killing them by its own
|
|
85
|
+
judgement — and since a viewer moving between steps announces itself, the plan
|
|
86
|
+
started them again on the very next pass. Two parties answering "should this
|
|
87
|
+
encoder exist" by different rules, several times a second.
|
|
88
|
+
|
|
89
|
+
An output nobody is on gets a map with **nothing in it**, which is a statement
|
|
90
|
+
and not an absence: the walk writes one for every output a session exists for,
|
|
91
|
+
including the ones everybody has left. That is how the plan is told to stop what
|
|
92
|
+
is on it.
|
|
93
|
+
|
|
94
|
+
## Which output a person is consuming
|
|
95
|
+
|
|
96
|
+
A person holds a record on more outputs than they are consuming. The picture is
|
|
97
|
+
where their record lives — the browser addresses it, their chosen soundtrack is
|
|
98
|
+
written on it, their position is read from it — so they are never let go of it;
|
|
99
|
+
but the moment they step down to 480p, the 1080p output is producing for nobody.
|
|
100
|
+
|
|
101
|
+
That distinction is answered where the two facts meet, and neither layer is
|
|
102
|
+
handed the other:
|
|
103
|
+
|
|
104
|
+
- **which step is on their screen** is a fact about a PERSON, read off the
|
|
105
|
+
viewer as one field;
|
|
106
|
+
- **which output a step supersedes** is a fact about the FILM'S SHAPE, answered
|
|
107
|
+
by `LiveOutputs.supersededBy(session, stepOnScreen)`, which takes a plain id
|
|
108
|
+
and has never seen a viewer.
|
|
109
|
+
|
|
110
|
+
A step, a soundtrack, and a step being warmed are consumed by whoever is
|
|
111
|
+
registered on them — everywhere but the picture, a person who stops watching is
|
|
112
|
+
let go of, so being known to an output is consuming it. Through a warm-up both
|
|
113
|
+
the step on screen and the step being made ready are genuinely produced, which
|
|
114
|
+
is the price of the switch not being visible.
|
|
115
|
+
|
|
116
|
+
## What each of the eight other places used to do
|
|
117
|
+
|
|
118
|
+
Before 2026-09-08 the plan was one opinion among nine. Each of these placed or
|
|
119
|
+
killed encoders by a rule of its own; each now states the fact it knows.
|
|
120
|
+
|
|
121
|
+
| it knows | it used to do | it now says |
|
|
122
|
+
|---|---|---|
|
|
123
|
+
| a session was created | start a run at the viewer's position, worked out again | the viewer is placed; the plan reads that |
|
|
124
|
+
| a viewer joined further in | start a run there if nothing was being made | as above |
|
|
125
|
+
| a step or soundtrack is being warmed | point that session at the switch and start it | this person is at N seconds on it |
|
|
126
|
+
| a step was switched to | stop the one left, point the new one | this person is on this step now |
|
|
127
|
+
| a step or track was abandoned | stop its encoder | this person has left that output |
|
|
128
|
+
| the hardware encoder failed | start a run at the dead one's start | what this host encodes with has changed |
|
|
129
|
+
| the input came back | start a run at the last requested segment | decide again |
|
|
130
|
+
| the cut table was corrected | restart the members at the measured instant | this run is producing in the wrong place; the instant is in the file's table |
|
|
131
|
+
| the bitrate cap changed | restart at where the encoder had got to | this run's arguments are stale |
|
|
132
|
+
|
|
133
|
+
A **seek** is not in that list because it was already reduced to one thing: it
|
|
134
|
+
puts the viewer where they are. The settle timer behind it, its cooldown and its
|
|
135
|
+
one-segment backoff are gone — a second debounce on a signal the browser had
|
|
136
|
+
already debounced, and every millisecond of it was dead time in front of the
|
|
137
|
+
viewer.
|
|
138
|
+
|
|
139
|
+
## Whether the map is being served in its own order
|
|
140
|
+
|
|
141
|
+
The zones say what matters most. What the viewer actually waited for is measured
|
|
142
|
+
where a viewer measurably waits — the one place in the proxy that holds a
|
|
143
|
+
request for a named segment — and recorded against the rank the map gave that
|
|
144
|
+
segment **at the moment it was asked for**. It reaches the `encode:` state line
|
|
145
|
+
as `served[...]`:
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
served[now 42 wait(s) median 180ms worst 1900ms, soon 6 wait(s) median 90ms worst 240ms, later none]
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Read it like this:
|
|
152
|
+
|
|
153
|
+
| what it says | what is wrong |
|
|
154
|
+
|---|---|
|
|
155
|
+
| long waits at `now` | the urgent zone is not being served first — a fault in whoever acts on the map |
|
|
156
|
+
| long waits at `soon`/`later`, none at `now` | the zones are the wrong width: the urgent one too narrow, so the viewer reaches material only ranked "soon" |
|
|
157
|
+
| `now none` while the viewer is watching | nothing was ever urgent — the map is not reaching this output |
|
|
158
|
+
| a band reading `none` | silence, not a zero, and it is printed as `none` so it cannot be read as "no waits, all good" |
|
|
159
|
+
|
|
160
|
+
Ranks are collapsed into three bands because the map's own scale is as long as
|
|
161
|
+
the film needs — a hundred ranks on a long file — and a hundred-row table says
|
|
162
|
+
nothing a reader can hold. The width of `soon` is a tenth of the top rank, which
|
|
163
|
+
is the map's own shape (its zones widen geometrically) rather than a threshold
|
|
164
|
+
chosen for the table.
|
|
165
|
+
|
|
166
|
+
The download half is measured the same way and on the same scale, so the two are
|
|
167
|
+
comparable: `download-architecture.md`.
|
|
168
|
+
|
|
169
|
+
## What is checked
|
|
170
|
+
|
|
171
|
+
`test/one-authority.test.js` holds the shape: one caller of `#startEncodeRun`,
|
|
172
|
+
no encoder stopped for being unwatched, the settle machinery absent, each output
|
|
173
|
+
reading its own map, and the soundtrack's start instant read off the table
|
|
174
|
+
rather than handed in.
|
|
175
|
+
|
|
176
|
+
`test/priority-map-per-output.test.js` holds the two scopes, over the real
|
|
177
|
+
viewer registry, the real `LiveOutputs` and the real `PriorityOrchestrator`.
|
|
178
|
+
|
|
179
|
+
`test/encode-plan.test.js` holds the arithmetic, including that every encoder
|
|
180
|
+
stops when nobody is watching the output.
|