@torrent-tv/proxy 2.80.10 → 2.80.12

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/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
- PR[piece-reader<br/>four bands per read]
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,25 @@ 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
+
54
75
  ## The five levels
55
76
 
56
77
  | level | what it is | stated |
@@ -0,0 +1,150 @@
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
+ ## What is checked
140
+
141
+ `test/one-authority.test.js` holds the shape: one caller of `#startEncodeRun`,
142
+ no encoder stopped for being unwatched, the settle machinery absent, each output
143
+ reading its own map, and the soundtrack's start instant read off the table
144
+ rather than handed in.
145
+
146
+ `test/priority-map-per-output.test.js` holds the two scopes, over the real
147
+ viewer registry, the real `LiveOutputs` and the real `PriorityOrchestrator`.
148
+
149
+ `test/encode-plan.test.js` holds the arithmetic, including that every encoder
150
+ stops when nobody is watching the output.
package/package.json CHANGED
@@ -1,51 +1,50 @@
1
- {
2
- "name": "@torrent-tv/proxy",
3
- "version": "2.80.10",
4
- "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
- "license": "GPL-3.0-or-later",
6
- "publishConfig": {
7
- "access": "public"
8
- },
9
- "type": "module",
10
- "engines": {
11
- "node": ">=24 <25"
12
- },
13
- "bin": {
14
- "torrent-tv-proxy": "./bin/cli.js"
15
- },
16
- "scripts": {
17
- "patch": "npm whoami && npm version patch && npm publish && git push --follow-tags",
18
- "minor": "npm whoami && npm version minor && npm publish && git push --follow-tags",
19
- "major": "npm whoami && npm version major && npm publish && git push --follow-tags",
20
- "start": "node ./bin/cli.js",
21
- "dev": "node --inspect=0 --experimental-network-inspection ./bin/cli.js",
22
- "test": "npm run lint && node --test && npm run test:measured",
23
- "graph": "node scripts/render-run-graph.js",
24
- "lint": "biome lint .",
25
- "test:measured": "node --test --test-concurrency=1 test-measured/decode-cost.measured.js test-measured/decode-measurement.measured.js"
26
- },
27
- "dependencies": {
28
- "@fastify/cors": "^11.2.0",
29
- "@fastify/helmet": "^13.0.2",
30
- "@fastify/static": "^10.1.2",
31
- "@silentbot1/nat-api": "^0.4.9",
32
- "chalk": "^5.4.1",
33
- "commander": "^12.1.0",
34
- "fastify": "^5.8.5",
35
- "ffmpeg-static": "^5.3.0",
36
- "franc": "^6.2.0",
37
- "get-port": "^7.1.0",
38
- "node-datachannel": "^0.32.0",
39
- "parse-torrent": "^11.0.23",
40
- "uint8-util": "2.2.6",
41
- "webtorrent": "2.8.5",
42
- "werift": "^0.24.2",
43
- "ws": "^8.18.2"
44
- },
45
- "devDependencies": {
46
- "@biomejs/biome": "^2.5.7"
47
- },
48
- "overrides": {
49
- "utp-native": "npm:@torrent-tv/utp-native@2.5.3-ttv.8"
50
- }
51
- }
1
+ {
2
+ "name": "@torrent-tv/proxy",
3
+ "version": "2.80.12",
4
+ "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
+ "license": "GPL-3.0-or-later",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "type": "module",
10
+ "engines": {
11
+ "node": ">=24 <25"
12
+ },
13
+ "bin": {
14
+ "torrent-tv-proxy": "./bin/cli.js"
15
+ },
16
+ "scripts": {
17
+ "patch": "npm whoami && npm version patch && npm publish && git push --follow-tags",
18
+ "minor": "npm whoami && npm version minor && npm publish && git push --follow-tags",
19
+ "major": "npm whoami && npm version major && npm publish && git push --follow-tags",
20
+ "start": "node ./bin/cli.js",
21
+ "dev": "node --inspect=0 --experimental-network-inspection ./bin/cli.js",
22
+ "test": "npm run lint && node --test",
23
+ "graph": "node scripts/render-run-graph.js",
24
+ "lint": "biome lint ."
25
+ },
26
+ "dependencies": {
27
+ "@fastify/cors": "^11.2.0",
28
+ "@fastify/helmet": "^13.0.2",
29
+ "@fastify/static": "^10.1.2",
30
+ "@silentbot1/nat-api": "^0.4.9",
31
+ "chalk": "^5.4.1",
32
+ "commander": "^12.1.0",
33
+ "fastify": "^5.8.5",
34
+ "ffmpeg-static": "^5.3.0",
35
+ "franc": "^6.2.0",
36
+ "get-port": "^7.1.0",
37
+ "node-datachannel": "^0.32.0",
38
+ "parse-torrent": "^11.0.23",
39
+ "uint8-util": "2.2.6",
40
+ "webtorrent": "2.8.5",
41
+ "werift": "^0.24.2",
42
+ "ws": "^8.18.2"
43
+ },
44
+ "devDependencies": {
45
+ "@biomejs/biome": "^2.5.7"
46
+ },
47
+ "overrides": {
48
+ "utp-native": "npm:@torrent-tv/utp-native@2.5.3-ttv.8"
49
+ }
50
+ }