@torrent-tv/proxy 2.74.1 → 2.76.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.
Files changed (124) hide show
  1. package/CHANGELOG.md +1504 -1453
  2. package/CLAUDE.md +19 -6
  3. package/biome.json +182 -1
  4. package/docs/container-architecture.md +27 -6
  5. package/docs/encode-run-state.md +1 -1
  6. package/knip.json +14 -0
  7. package/package.json +1 -1
  8. package/routes/api/sources/warm/post.js +1 -1
  9. package/routes/api/transcode-sessions/post.js +185 -185
  10. package/routes/api/transcode-sessions/progress/get.js +5 -1
  11. package/routes/transcode/audio-file/get.js +11 -1
  12. package/routes/transcode/audio-warm/get.js +11 -1
  13. package/routes/transcode/session-file/get.js +1 -1
  14. package/routes/transcode/variant-file/get.js +10 -1
  15. package/scripts/render-run-graph.js +2 -2
  16. package/server.js +25 -0
  17. package/services/audio-inventory.js +9 -214
  18. package/services/container/AviContainer.js +266 -81
  19. package/services/container/Container.js +281 -80
  20. package/services/container/ContainerFactory.js +67 -0
  21. package/services/container/MatroskaContainer.js +327 -8
  22. package/services/container/Mp4Container.js +373 -27
  23. package/services/container/SubtitleFileContainer.js +0 -1
  24. package/services/controllers/SubtitleController.js +128 -128
  25. package/services/demand/index.js +7 -10
  26. package/services/download/registry.js +0 -14
  27. package/services/encode/CoverageMap.js +281 -0
  28. package/services/encode/EncodePlan.js +255 -0
  29. package/services/encode/EncodeRun.js +587 -0
  30. package/services/encode/Encoder.js +84 -0
  31. package/services/encode/NvencEncoder.js +45 -0
  32. package/services/encode/QsvEncoder.js +47 -0
  33. package/services/encode/SegmentDemand.js +0 -0
  34. package/services/encode/SegmentStore.js +529 -0
  35. package/services/encode/SoftwareEncoder.js +111 -0
  36. package/services/encode/V4l2m2mEncoder.js +53 -0
  37. package/services/encode/VaapiEncoder.js +53 -0
  38. package/services/encode/args.js +200 -0
  39. package/services/{encode-exit.js → encode/encode-exit.js} +17 -0
  40. package/services/encode/index.js +9 -0
  41. package/services/encode/run-command.js +647 -0
  42. package/services/hls-session-manager.js +11073 -10711
  43. package/services/hwaccel.js +1688 -1992
  44. package/services/orchestrators/EncodeOrchestrator.js +359 -0
  45. package/services/output/LiveOutputs.js +213 -0
  46. package/services/output/Output.js +94 -0
  47. package/services/output/OutputSpec.js +195 -0
  48. package/services/output/Timeline.js +220 -0
  49. package/services/output/index.js +1 -0
  50. package/services/output/ladder.js +26 -0
  51. package/services/playback-planner.js +806 -747
  52. package/services/produced-index.js +222 -300
  53. package/services/source/SourceFile.js +346 -0
  54. package/services/{sidecar-files.js → torrent/files.js} +107 -11
  55. package/services/torrent/naming.js +619 -0
  56. package/services/torrent-worker/client.js +10 -0
  57. package/services/torrent-worker/container-tracks.js +71 -43
  58. package/services/torrent-worker/pool-adapter.js +370 -333
  59. package/services/torrent-worker/protocol.js +7 -0
  60. package/services/torrent-worker/subtitle-cues.js +549 -549
  61. package/services/torrent-worker/worker.js +18 -0
  62. package/services/tracks/AudioTrack.js +131 -40
  63. package/services/tracks/TextSubtitleTrack.js +287 -287
  64. package/services/tracks/index.js +15 -14
  65. package/services/viewer/Viewer.js +145 -0
  66. package/services/viewer/Viewers.js +124 -0
  67. package/test/audio-inventory.test.js +176 -177
  68. package/test/auto-quality-step.test.js +508 -506
  69. package/test/behind-head-repair.test.js +17 -7
  70. package/test/coverage-map.test.js +153 -0
  71. package/test/cut-times-timeline.test.js +6 -5
  72. package/test/cuts-follow-published-grid.test.js +4 -4
  73. package/test/decode-cost.test.js +31 -12
  74. package/test/encode-exit.test.js +1 -1
  75. package/test/encode-orchestrator.test.js +196 -0
  76. package/test/encode-plan.test.js +245 -0
  77. package/test/encode-run-state.test.js +2 -2
  78. package/test/encode-run.test.js +168 -0
  79. package/test/encoder-kinds.test.js +122 -0
  80. package/test/held-request-width.test.js +9 -3
  81. package/test/helpers/encode-run.js +128 -0
  82. package/test/keyframe-index-accuracy.test.js +19 -12
  83. package/test/keyframes-belong-to-the-file.test.js +132 -0
  84. package/test/matroska-cues-track.test.js +192 -192
  85. package/test/mp4-composition-times.test.js +0 -0
  86. package/test/orchestrator-wired.test.js +164 -0
  87. package/test/output-shape.test.js +68 -0
  88. package/test/output-spec.test.js +157 -0
  89. package/test/produced-copy-choice.test.js +58 -92
  90. package/test/produced-index.test.js +142 -188
  91. package/test/quality-variants.test.js +1079 -1075
  92. package/test/run-graph-drift.test.js +1 -1
  93. package/test/run-intervals.test.js +329 -0
  94. package/test/run-position-follows-published-grid.test.js +4 -4
  95. package/test/seek-landing.test.js +8 -8
  96. package/test/seek-target-not-superseded.test.js +21 -9
  97. package/test/segment-demand.test.js +82 -0
  98. package/test/segment-serve-wiring.test.js +47 -52
  99. package/test/segment-store.test.js +187 -0
  100. package/test/segments-are-shared.test.js +175 -0
  101. package/test/sidecar-naming.test.js +142 -0
  102. package/test/source-file.test.js +133 -0
  103. package/test/stale-request-after-seek.test.js +18 -12
  104. package/test/subtitle-language.test.js +252 -252
  105. package/test/timeline.test.js +95 -0
  106. package/test/{sidecar-files.test.js → torrent-files.test.js} +44 -1
  107. package/test/torrent-naming.test.js +255 -0
  108. package/test/tracks-begin-together.test.js +44 -32
  109. package/test/two-viewers-one-picture.test.js +347 -0
  110. package/test/video-facts.test.js +102 -0
  111. package/test/viewer-outputs.test.js +273 -0
  112. package/test/viewer.test.js +91 -0
  113. package/utils/perf.js +1 -63
  114. package/services/container/index.js +0 -6
  115. package/services/container-index/avi.js +0 -167
  116. package/services/container-index/index.js +0 -118
  117. package/services/container-index/matroska.js +0 -336
  118. package/services/container-index/mp4.js +0 -358
  119. package/services/controllers/index.js +0 -2
  120. package/services/download/index.js +0 -8
  121. package/services/orchestrators/index.js +0 -2
  122. /package/services/{container-index → container}/ebml-reader.js +0 -0
  123. /package/services/{encode-run-state.js → encode/encode-run-state.js} +0 -0
  124. /package/services/{language-detect.js → tracks/language-detect.js} +0 -0
package/CLAUDE.md CHANGED
@@ -33,6 +33,14 @@ Linux-only host (e.g. POSIX-only signals must degrade elsewhere).
33
33
  `deselect` or `critical`, and `registry.js`, which holds one per torrent and
34
34
  owns the cross-torrent rule that withholds the speculative levels while
35
35
  anything urgent is missing anywhere. See `docs/download-architecture.md`.
36
+ - `output/` — domain layer: `OutputSpec`, `VideoOutput`, `AudioOutput`,
37
+ `CutGrid`. What a session PRODUCES — which tracks, in what form, cut how,
38
+ packaged how — and therefore its identity: two outputs whose parameters
39
+ agree ARE the same output, and the encoded result is reused by definition.
40
+ Nothing about a VIEWER appears in it (not the consumer id, not where they
41
+ started, not their viewport), and nothing about the request that does not
42
+ change a byte of the result. `hls-session-manager` builds one and keys the
43
+ session on it.
36
44
  - `container/` — domain layer: `Container` (abstract, RFC 9559 / ISO 14496-12),
37
45
  `MatroskaContainer` / `Mp4Container` / `AviContainer`, `ContainerFactory`
38
46
  (sniff 16 bytes → precise subclass). See `docs/container-architecture.md`.
@@ -43,18 +51,23 @@ Linux-only host (e.g. POSIX-only signals must degrade elsewhere).
43
51
  RFC 9559 §5.1.4.1, FlagOriginal/Commentary only on audio, tkhd
44
52
  track_enabled / alternate_group, elng BCP47).
45
53
  - `orchestrators/` — application layer: `ContainerOrchestrator` (detect + per-file
46
- cache, `getTracks`/`getKeyframeIndex`), `SubtitleOrchestrator` (wraps
54
+ cache, `getTracks`/`getMediaInfo`/`getKeyframeIndex` the keyframe table is
55
+ a property of immutable bytes like the other two, memoized on the container
56
+ and read once per file by whoever asks), `SubtitleOrchestrator` (wraps
47
57
  `torrent-worker/subtitle-cues.js` + `Container` tracks, warm/push). The walk
48
58
  itself is `MatroskaContainer.walkHeldClusters` / `Mp4Container.readHeldSamples`;
49
59
  `subtitle-cues.js` supplies the torrent's read policy and keeps the cursor.
50
60
  - `controllers/` — interface layer: `PlaybackController` / `SubtitleController`
51
61
  (thin adapters over orchestrators; routes depend on controllers, not services).
52
62
  `routes/*` are now thin HTTP translators.
53
- - `container-index/` what is not specific to one media kind: EBML element
54
- walking and the Matroska/MP4/AVI keyframe tables. Used internally by
55
- `container/*`, deprecated as a direct import. Everything a container states
56
- about its own SUBTITLES — track table, Cues, blocks in a cluster, sample
57
- table, the walk of what is downloaded lives in the container class.
63
+ - Everything a container states about ITSELF lives in its own class: the
64
+ track table, the Cues, the keyframe times they name, the cluster positions,
65
+ the blocks inside a cluster, the MP4 sample table, and the walk of what is
66
+ downloaded. `services/container-index/` is gone; `container/ebml-reader.js`
67
+ is the format's byte-level grammar, not its statements, and is the only
68
+ piece kept apart. `ContainerFactory` is the ONE place that decides what a
69
+ file is — by sniffing the header, since that is what the muxer wrote — and
70
+ it also answers `readKeyframeIndex`.
58
71
  - `docs/download-architecture.md` — the two axes of downloading: what is
59
72
  wanted (`demand/`) against what the swarm is told (`download/`), why urgency
60
73
  is not a number given to the library, and why the speculative levels are
package/biome.json CHANGED
@@ -17,5 +17,186 @@
17
17
  "noPrivateImports": "error"
18
18
  }
19
19
  }
20
- }
20
+ },
21
+ "overrides": [
22
+ {
23
+ "includes": [
24
+ "services/demand/**",
25
+ "services/piece-store/**",
26
+ "services/segment-formats/**",
27
+ "services/tracks/**",
28
+ "services/output/**",
29
+ "services/viewer/**",
30
+ "services/encode/**",
31
+ "services/source/**",
32
+ "services/torrent/**"
33
+ ],
34
+ "linter": {
35
+ "rules": {
36
+ "style": {
37
+ "noRestrictedImports": {
38
+ "level": "error",
39
+ "options": {
40
+ "patterns": [
41
+ {
42
+ "group": ["../**"],
43
+ "message": "This layer states facts and imports nothing above itself. Anything it needs from outside — a logger, a clock, a way to read bytes — is passed in."
44
+ }
45
+ ]
46
+ }
47
+ }
48
+ }
49
+ }
50
+ }
51
+ },
52
+ {
53
+ "includes": ["services/download/**"],
54
+ "linter": {
55
+ "rules": {
56
+ "style": {
57
+ "noRestrictedImports": {
58
+ "level": "error",
59
+ "options": {
60
+ "patterns": [
61
+ {
62
+ "group": [
63
+ "../*",
64
+ "../container/**",
65
+ "../controllers/**",
66
+ "../encode/**",
67
+ "../orchestrators/**",
68
+ "../output/**",
69
+ "../segment-formats/**",
70
+ "../torrent-worker/**",
71
+ "../tracks/**",
72
+ "../viewer/**"
73
+ ],
74
+ "message": "What the swarm is told is decided from what is wanted (demand/) and what is held (piece-store/), and from nothing else."
75
+ }
76
+ ]
77
+ }
78
+ }
79
+ }
80
+ }
81
+ }
82
+ },
83
+ {
84
+ "includes": ["services/container/**"],
85
+ "linter": {
86
+ "rules": {
87
+ "style": {
88
+ "noRestrictedImports": {
89
+ "level": "error",
90
+ "options": {
91
+ "patterns": [
92
+ {
93
+ "group": [
94
+ "../*",
95
+ "../controllers/**",
96
+ "../demand/**",
97
+ "../download/**",
98
+ "../encode/**",
99
+ "../orchestrators/**",
100
+ "../output/**",
101
+ "../piece-store/**",
102
+ "../segment-formats/**",
103
+ "../torrent-worker/**",
104
+ "../viewer/**"
105
+ ],
106
+ "message": "A container answers for itself and hands out tracks/. It knows nothing of torrents, encoders or viewers: the four functions it is built with are its whole outside world."
107
+ }
108
+ ]
109
+ }
110
+ }
111
+ }
112
+ }
113
+ }
114
+ },
115
+ {
116
+ "includes": ["services/orchestrators/**"],
117
+ "linter": {
118
+ "rules": {
119
+ "style": {
120
+ "noRestrictedImports": {
121
+ "level": "error",
122
+ "options": {
123
+ "patterns": [
124
+ {
125
+ "group": [
126
+ "../*",
127
+ "../controllers/**",
128
+ "../demand/**",
129
+ "../download/**",
130
+ "../output/**",
131
+ "../piece-store/**",
132
+ "../segment-formats/**",
133
+ "../viewer/**"
134
+ ],
135
+ "message": "An orchestrator composes the layers below it. Reaching sideways into serving or downloading is what put a scenario in two places before."
136
+ }
137
+ ]
138
+ }
139
+ }
140
+ }
141
+ }
142
+ }
143
+ },
144
+ {
145
+ "includes": ["services/controllers/**"],
146
+ "linter": {
147
+ "rules": {
148
+ "style": {
149
+ "noRestrictedImports": {
150
+ "level": "error",
151
+ "options": {
152
+ "patterns": [
153
+ {
154
+ "group": [
155
+ "../*",
156
+ "../demand/**",
157
+ "../download/**",
158
+ "../encode/**",
159
+ "../output/**",
160
+ "../piece-store/**",
161
+ "../segment-formats/**",
162
+ "../torrent-worker/**",
163
+ "../viewer/**"
164
+ ],
165
+ "message": "A controller translates a request for an orchestrator. Every layer below the orchestrator is reached through it, never around it."
166
+ }
167
+ ]
168
+ }
169
+ }
170
+ }
171
+ }
172
+ }
173
+ },
174
+ {
175
+ "includes": ["services/torrent-worker/**"],
176
+ "linter": {
177
+ "rules": {
178
+ "style": {
179
+ "noRestrictedImports": {
180
+ "level": "error",
181
+ "options": {
182
+ "patterns": [
183
+ {
184
+ "group": [
185
+ "../hls-session-manager.js",
186
+ "../controllers/**",
187
+ "../encode/**",
188
+ "../output/**",
189
+ "../viewer/**",
190
+ "../segment-formats/**"
191
+ ],
192
+ "message": "The torrent thread serves bytes. What is produced from them, and for whom, is decided in the main thread and never asked of this one."
193
+ }
194
+ ]
195
+ }
196
+ }
197
+ }
198
+ }
199
+ }
200
+ }
201
+ ]
21
202
  }
@@ -162,17 +162,38 @@ flowchart TB
162
162
  ## Orchestrators & Controllers
163
163
 
164
164
  - `ContainerFactory.create({readRange,fileSize})` — sniffs 16 bytes, returns precise `Container` subclass. No torrent knowledge.
165
- - `ContainerOrchestrator` — per-file cache (`sourceKey:fileIndex`), `getTracks()` / `getKeyframeIndex()`. Transport-agnostic.
165
+ - `ContainerOrchestrator` — per-file cache (`sourceKey:fileIndex`), `getTracks()` / `getMediaInfo()` / `getKeyframeIndex()`. Transport-agnostic.
166
+ - **The keyframe table is read once per file**, like the other two. `Container.readKeyframeIndex` memoizes and each format implements `parseKeyframeIndex`, so the wait belongs to the FILE: two sessions created in the same moment join one read rather than making two, which is exactly what two viewers opening one film do. A read that THREW is not remembered — the bytes it needed may simply not have arrived. Who asks: `torrent-worker/container-tracks.js` `containerKeyframesOf`, over the torrent, reached from the session manager by the `container-keyframes` command. The manager's own HTTP read remains for a manager wired without that path (every unit test).
166
167
  - `SubtitleOrchestrator` — wraps `torrent-worker/subtitle-cues.js` (`planFor`, `cuesHeldFor`, `warmSubtitleCues`) behind the `ContainerTrack` abstraction. Routes depend on this, not on the worker directly. The reading itself is the containers' — that module supplies the torrent's read policy and keeps the cursor.
167
168
  - `PlaybackController` / `SubtitleController` — thin interface adapters; `routes/api/*` delegate to them, handle HTTP headers (`X-Subtitle-Language`, `X-Subtitle-Cursor`) only.
168
169
 
169
170
  ## Legacy
170
171
 
171
- `services/container-index/` holds what is NOT specific to one media kind: EBML
172
- element walking, and the Matroska/MP4/AVI keyframe tables. Everything a container
173
- states about its own subtitles — the track table, the Cues, the blocks in a
174
- cluster, the sample table is in the container class itself. Direct imports from
175
- routes are deprecated use `orchestrators/` and `controllers/` instead.
172
+ Everything a container states about ITSELF is in its own class: the track table,
173
+ the Cues, the keyframe times they name, the cluster positions, the blocks inside
174
+ a cluster, the MP4 sample table. `services/container-index/` no longer exists.
175
+ What was kept apart is `container/ebml-reader.js` EBML element walking is the
176
+ format's grammar rather than any of its statements.
177
+
178
+ `ContainerFactory` is the one place that decides what a file IS. It sniffs the
179
+ header, because that is what the muxer wrote while a name is what somebody
180
+ typed, and it answers `readKeyframeIndex` for the same reason: doing it anywhere
181
+ else meant a third place making that decision. `ContainerFactory.byName` exists
182
+ for the one path that cannot sniff — the cue walk asks the swarm for nothing, so
183
+ a file whose head is not downloaded has only its name.
184
+
185
+ **Two readings of one file, and which answers.** ffmpeg's `-i` banner and the
186
+ container's own table both describe a file's tracks, and the rule is stated once
187
+ per media kind on `Container`. `alignWithBanner` lines them up by position and
188
+ CHECKS each pair on language or title; one pair agreeing on neither, or a length
189
+ that differs, drops the container reading whole, because a wrong flag is worse
190
+ than a missing one. `mergeSubtitleFlags`, `mergeAudioFlags` and
191
+ `mergeVideoFacts` then say which side answers for which field: flags the banner
192
+ cannot express come from the container; the coded size and frame rate come from
193
+ the probe, because the encoder receives what the decoder produced.
194
+
195
+ Direct imports from routes are deprecated — use `orchestrators/` and
196
+ `controllers/` instead.
176
197
 
177
198
  ## Flags matrix
178
199
 
@@ -1,4 +1,4 @@
1
- <!-- GENERATED from services/encode-run-state.js by scripts/render-run-graph.js.
1
+ <!-- GENERATED from services/encode/encode-run-state.js by scripts/render-run-graph.js.
2
2
  Do not edit by hand: change the table and run `npm run graph`. -->
3
3
 
4
4
  # The encoder run — states and transitions
package/knip.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "$schema": "https://unpkg.com/knip@6/schema.json",
3
+ "entry": [
4
+ "test/*.test.js"
5
+ ],
6
+ "project": [
7
+ "bin/**/*.js",
8
+ "routes/**/*.js",
9
+ "services/**/*.js",
10
+ "utils/**/*.js",
11
+ "server.js",
12
+ "test/**/*.js"
13
+ ]
14
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.74.1",
3
+ "version": "2.76.0",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "publishConfig": {
@@ -3,7 +3,7 @@ import {
3
3
  countVideoFiles,
4
4
  matchSidecarFiles,
5
5
  TEXT_SUBTITLE_SIDECAR_EXTENSIONS
6
- } from "../../../../services/sidecar-files.js";
6
+ } from "../../../../services/torrent/files.js";
7
7
 
8
8
  /**
9
9
  * Start fetching a source before anyone asks to play it.