@torrent-tv/proxy 2.87.3 → 2.87.4

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 (465) hide show
  1. package/.claude/commands/opsx/apply.md +155 -155
  2. package/.claude/commands/opsx/archive.md +160 -160
  3. package/.claude/commands/opsx/explore.md +174 -174
  4. package/.claude/commands/opsx/propose.md +109 -109
  5. package/.claude/commands/opsx/sync.md +143 -143
  6. package/.claude/skills/openspec-apply-change/SKILL.md +159 -159
  7. package/.claude/skills/openspec-archive-change/SKILL.md +117 -117
  8. package/.claude/skills/openspec-explore/SKILL.md +289 -289
  9. package/.claude/skills/openspec-propose/SKILL.md +113 -113
  10. package/.claude/skills/openspec-sync-specs/SKILL.md +147 -147
  11. package/.dockerignore +5 -5
  12. package/AGENTS.md +322 -322
  13. package/CHANGELOG.md +2157 -2153
  14. package/Dockerfile +30 -30
  15. package/LICENSE +16 -16
  16. package/README.md +369 -369
  17. package/assets/calibration/NOTICE.md +100 -100
  18. package/assets/diagnostics/sctpstate.gdb +31 -31
  19. package/bin/cli.js +684 -684
  20. package/bin/thread-pool.js +30 -30
  21. package/biome.json +411 -411
  22. package/docs/browser-proxy-contract.md +94 -94
  23. package/docs/container-architecture.md +218 -218
  24. package/docs/download-architecture.md +261 -261
  25. package/docs/encode-architecture.md +729 -729
  26. package/docs/encode-run-state.md +96 -96
  27. package/docs/logs.md +97 -97
  28. package/docs/socks5-egress-research.md +114 -114
  29. package/docs/storage-architecture.md +267 -267
  30. package/knip.json +14 -14
  31. package/openspec/changes/adaptive-bitrate/design.md +92 -92
  32. package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
  33. package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
  34. package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
  35. package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
  36. package/openspec/changes/binary-distribution/proposal.md +53 -53
  37. package/openspec/changes/chunked-request-bodies/design.md +81 -81
  38. package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
  39. package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
  40. package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
  41. package/openspec/changes/cold-start/design.md +98 -98
  42. package/openspec/changes/cold-start/proposal.md +62 -62
  43. package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
  44. package/openspec/changes/cold-start/tasks.md +48 -48
  45. package/openspec/changes/disk-cap/.openspec.yaml +2 -2
  46. package/openspec/changes/disk-cap/proposal.md +37 -37
  47. package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
  48. package/openspec/changes/disk-cap/tasks.md +19 -19
  49. package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
  50. package/openspec/changes/proxy-observability/design.md +38 -38
  51. package/openspec/changes/proxy-observability/proposal.md +49 -49
  52. package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
  53. package/openspec/changes/proxy-observability/tasks.md +21 -21
  54. package/openspec/changes/seek-debounce/design.md +118 -118
  55. package/openspec/changes/seek-debounce/proposal.md +59 -59
  56. package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
  57. package/openspec/changes/seek-debounce/tasks.md +33 -33
  58. package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
  59. package/openspec/changes/subtitle-language/proposal.md +50 -50
  60. package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
  61. package/openspec/changes/subtitle-language/tasks.md +26 -26
  62. package/openspec/changes/track-selection/.openspec.yaml +2 -2
  63. package/openspec/changes/track-selection/design.md +47 -47
  64. package/openspec/changes/track-selection/proposal.md +45 -45
  65. package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
  66. package/openspec/changes/track-selection/tasks.md +20 -20
  67. package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
  68. package/openspec/changes/transcode-quality/proposal.md +51 -51
  69. package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
  70. package/openspec/changes/transcode-quality/tasks.md +63 -63
  71. package/openspec/config.yaml +38 -38
  72. package/package.json +50 -50
  73. package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
  74. package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
  75. package/research/double-spawn-2026-09-10.md +171 -171
  76. package/research/handover-2026-09-13.md +209 -209
  77. package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
  78. package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
  79. package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
  80. package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
  81. package/routes/api/client-logs/post.js +86 -86
  82. package/routes/api/delivery-sink/get.js +72 -72
  83. package/routes/api/link-probe/get.js +51 -51
  84. package/routes/api/playback-plan/post.js +53 -53
  85. package/routes/api/sources/files/get.js +100 -100
  86. package/routes/api/sources/post.js +36 -36
  87. package/routes/api/sources/stats/get.js +132 -132
  88. package/routes/api/sources/warm/post.js +217 -181
  89. package/routes/api/subtitles/get.js +230 -230
  90. package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
  91. package/routes/api/transcode-sessions/net-report/post.js +129 -129
  92. package/routes/api/transcode-sessions/post.js +160 -160
  93. package/routes/api/transcode-sessions/progress/get.js +27 -27
  94. package/routes/api/transcode-sessions/release/post.js +41 -41
  95. package/routes/api/transcode-sessions/seek/post.js +63 -63
  96. package/routes/health/get.js +13 -13
  97. package/routes/healthz/get.js +13 -13
  98. package/routes/stream/get.js +361 -361
  99. package/routes/transcode/audio-file/get.js +64 -64
  100. package/routes/transcode/audio-warm/get.js +84 -84
  101. package/routes/transcode/session-file/get.js +52 -52
  102. package/routes/transcode/variant-file/get.js +95 -95
  103. package/routes/transcode/variant-warm/get.js +84 -84
  104. package/scripts/render-run-graph.js +173 -173
  105. package/server.js +721 -720
  106. package/services/encode/CoverageMap.js +428 -428
  107. package/services/encode/CushionReport.js +345 -345
  108. package/services/encode/EncodeAdmission.js +332 -332
  109. package/services/encode/EncodeOrchestrator.js +1217 -1217
  110. package/services/encode/EncodePlan.js +1366 -1366
  111. package/services/encode/EncodeRun.js +776 -776
  112. package/services/encode/EncodeRuns.js +1229 -1229
  113. package/services/encode/Encoder.js +142 -142
  114. package/services/encode/EncoderSelection.js +38 -38
  115. package/services/encode/LocalObservations.js +359 -359
  116. package/services/encode/NvencEncoder.js +50 -50
  117. package/services/encode/OutputOpening.js +755 -755
  118. package/services/encode/OutputTimes.js +579 -579
  119. package/services/encode/QsvEncoder.js +58 -58
  120. package/services/encode/Renditions.js +2162 -2162
  121. package/services/encode/RunProgress.js +84 -84
  122. package/services/encode/SegmentDemand.js +95 -95
  123. package/services/encode/SoftwareEncoder.js +121 -121
  124. package/services/encode/V4l2m2mEncoder.js +53 -53
  125. package/services/encode/VaapiEncoder.js +66 -66
  126. package/services/encode/args.js +416 -416
  127. package/services/encode/available-share.js +89 -89
  128. package/services/encode/calibration.js +361 -361
  129. package/services/encode/contention.js +128 -128
  130. package/services/encode/decode-cost-fit.js +363 -363
  131. package/services/encode/encode-exit.js +114 -114
  132. package/services/encode/encode-run-state.js +551 -551
  133. package/services/encode/encoder-readings.js +45 -45
  134. package/services/encode/fingerprint.js +322 -322
  135. package/services/encode/host-load.js +174 -174
  136. package/services/encode/hwaccel.js +1894 -1894
  137. package/services/encode/index.js +9 -9
  138. package/services/encode/learned-median.js +90 -90
  139. package/services/encode/output/EncodedOutput.js +57 -57
  140. package/services/encode/output/Output.js +131 -131
  141. package/services/encode/output/OutputCatalog.js +414 -414
  142. package/services/encode/output/OutputSpec.js +390 -390
  143. package/services/encode/output/Timeline.js +308 -308
  144. package/services/encode/output/cut-grid.js +107 -107
  145. package/services/encode/output/index.js +20 -20
  146. package/services/encode/output/ladder.js +32 -32
  147. package/services/encode/output/playlists.js +284 -284
  148. package/services/encode/output/rates.js +125 -125
  149. package/services/encode/output-key-format.js +21 -21
  150. package/services/encode/quality/EncodeCost.js +1083 -1083
  151. package/services/encode/quality/HostLoad.js +525 -525
  152. package/services/encode/quality/HostTimings.js +249 -249
  153. package/services/encode/quality/OutputQualityState.js +28 -28
  154. package/services/encode/quality/QualityController.js +935 -935
  155. package/services/encode/quality/QualityOffer.js +416 -416
  156. package/services/encode/quality/drain-threat.js +47 -47
  157. package/services/encode/quality/link-budget.js +254 -254
  158. package/services/encode/quality/output-format.js +266 -266
  159. package/services/encode/quality/serving-output.js +216 -216
  160. package/services/encode/quality/visible-rung.js +57 -57
  161. package/services/encode/run-budget.js +97 -97
  162. package/services/encode/run-command.js +747 -747
  163. package/services/encode/run-costs.js +155 -155
  164. package/services/encode/run-inputs.js +42 -42
  165. package/services/encode/segment-formats/fmp4.js +437 -437
  166. package/services/encode/segment-formats/index.js +77 -77
  167. package/services/encode/segment-formats/init-compat.js +324 -324
  168. package/services/encode/segment-formats/mp4-boxes.js +311 -311
  169. package/services/encode/segment-formats/mpegts.js +134 -134
  170. package/services/encode/start-stop-cost.js +178 -178
  171. package/services/encode/throughput.js +97 -97
  172. package/services/encode/torrent-cost.js +88 -88
  173. package/services/identity/installation-id.js +38 -38
  174. package/services/media/ContainerOrchestrator.js +149 -149
  175. package/services/media/KeyframeTables.js +316 -316
  176. package/services/media/SourceFile.js +298 -298
  177. package/services/media/SubtitleCues.js +520 -520
  178. package/services/media/SubtitleOrchestrator.js +167 -167
  179. package/services/media/audio-inventory.js +212 -212
  180. package/services/media/container/AviContainer.js +266 -266
  181. package/services/media/container/Container.js +603 -603
  182. package/services/media/container/ContainerFactory.js +56 -56
  183. package/services/media/container/KeyframeTable.js +272 -272
  184. package/services/media/container/MatroskaContainer.js +1494 -1494
  185. package/services/media/container/Mp4Container.js +1244 -1244
  186. package/services/media/container/SubtitleFileContainer.js +322 -322
  187. package/services/media/container/ebml-reader.js +159 -159
  188. package/services/media/ffmpeg-banner.js +260 -260
  189. package/services/media/keyframe-probe.js +157 -157
  190. package/services/media/media-info-probe.js +93 -93
  191. package/services/media/playback-planner.js +856 -848
  192. package/services/media/tracks/AudioTrack.js +131 -131
  193. package/services/media/tracks/ContainerTrack.js +75 -75
  194. package/services/media/tracks/ImageSubtitleTrack.js +19 -19
  195. package/services/media/tracks/SubtitleTrack.js +38 -38
  196. package/services/media/tracks/TextSubtitleTrack.js +287 -287
  197. package/services/media/tracks/VideoTrack.js +33 -33
  198. package/services/media/tracks/index.js +15 -15
  199. package/services/media/tracks/language-detect.js +174 -174
  200. package/services/server/OutputLifecycle.js +418 -418
  201. package/services/server/SegmentServing.js +1146 -1146
  202. package/services/server/ViewerRequests.js +522 -522
  203. package/services/server/WaitLedger.js +142 -142
  204. package/services/server/controllers/PlaybackController.js +36 -36
  205. package/services/server/controllers/SubtitleController.js +109 -109
  206. package/services/server/http-responses.js +60 -60
  207. package/services/server/transcode-session-files.js +325 -325
  208. package/services/server/wire-outputs.js +983 -983
  209. package/services/storage/Diagnostics.js +202 -202
  210. package/services/storage/MachineBudget.js +235 -235
  211. package/services/storage/allowance.js +107 -107
  212. package/services/storage/core-dumps.js +106 -106
  213. package/services/storage/files/CompletedFiles.js +391 -391
  214. package/services/storage/files/piece-from-whole-file.js +118 -118
  215. package/services/storage/free.js +71 -71
  216. package/services/storage/keep.js +48 -48
  217. package/services/storage/machine-memory.js +56 -56
  218. package/services/storage/memory-report.js +575 -575
  219. package/services/storage/piece-store/piece-disk-store.js +516 -516
  220. package/services/storage/piece-store/piece-lru.js +495 -495
  221. package/services/storage/piece-store/shared-piece-store.js +1880 -1880
  222. package/services/storage/returns.js +103 -103
  223. package/services/storage/segment-store/SegmentStore.js +1196 -1196
  224. package/services/storage/wire.js +175 -175
  225. package/services/torrent/Contents.js +324 -324
  226. package/services/torrent/demand/DemandRegister.js +202 -202
  227. package/services/torrent/demand/Urgency.js +137 -137
  228. package/services/torrent/demand/Window.js +118 -118
  229. package/services/torrent/demand/index.js +8 -8
  230. package/services/torrent/demand/pieces.js +140 -140
  231. package/services/torrent/download/SwarmSelection.js +386 -386
  232. package/services/torrent/download/registry.js +102 -102
  233. package/services/torrent/download/withdraw-claim.js +80 -80
  234. package/services/torrent/files.js +455 -455
  235. package/services/torrent/naming.js +619 -619
  236. package/services/torrent/piece-store-of.js +29 -29
  237. package/services/torrent/supply-margin.js +262 -262
  238. package/services/torrent/torrent-pool.js +2675 -2635
  239. package/services/torrent/torrent-source-key.js +38 -38
  240. package/services/torrent/worker/background-fill.js +213 -213
  241. package/services/torrent/worker/channel.js +271 -271
  242. package/services/torrent/worker/client.js +765 -760
  243. package/services/torrent/worker/fastest-wires.js +342 -342
  244. package/services/torrent/worker/handle-state.js +23 -23
  245. package/services/torrent/worker/held-bytes.js +155 -155
  246. package/services/torrent/worker/install-webrtc-shim.js +32 -32
  247. package/services/torrent/worker/piece-reader.js +1078 -1078
  248. package/services/torrent/worker/pool-adapter.js +365 -350
  249. package/services/torrent/worker/protocol.js +184 -182
  250. package/services/torrent/worker/resume-warm.js +107 -107
  251. package/services/torrent/worker/webrtc-shim.js +219 -219
  252. package/services/torrent/worker/worker.js +1129 -1116
  253. package/services/transport/data-channel-handler.js +1356 -1356
  254. package/services/transport/delivery-probe.js +842 -842
  255. package/services/transport/health-collector.js +53 -53
  256. package/services/transport/nat-classifier.js +231 -231
  257. package/services/transport/packet-witness.js +808 -808
  258. package/services/transport/port-mapper.js +235 -235
  259. package/services/transport/registry-api.js +81 -81
  260. package/services/transport/tunnel-client.js +447 -447
  261. package/services/transport/usrsctp-state.js +175 -175
  262. package/services/transport/webrtc-manager.js +484 -484
  263. package/services/viewer/Assignments.js +340 -340
  264. package/services/viewer/PriorityMap.js +390 -390
  265. package/services/viewer/PriorityOrchestrator.js +325 -325
  266. package/services/viewer/Viewer.js +657 -657
  267. package/services/viewer/Viewers.js +368 -368
  268. package/services/viewer/active-output.js +37 -37
  269. package/services/viewer/audio-start.js +46 -46
  270. package/services/viewer/buffer-trend.js +81 -81
  271. package/services/viewer/choices.js +647 -647
  272. package/services/viewer/positions.js +126 -126
  273. package/services/viewer/report-intake.js +46 -46
  274. package/store/source-registry.js +73 -73
  275. package/test/asset-paths.test.js +28 -28
  276. package/test/audio-inventory.test.js +176 -176
  277. package/test/audio-track-bitrate.test.js +40 -40
  278. package/test/auto-quality-step.test.js +707 -707
  279. package/test/available-share.test.js +76 -76
  280. package/test/background-fill.test.js +190 -190
  281. package/test/client-log-file.test.js +110 -110
  282. package/test/completed-files.test.js +115 -115
  283. package/test/concurrent-cost.test.js +148 -148
  284. package/test/container-media-info.test.js +228 -228
  285. package/test/contention.test.js +76 -76
  286. package/test/core-dumps.test.js +57 -57
  287. package/test/coverage-follows-the-disk.test.js +228 -228
  288. package/test/coverage-map.test.js +195 -195
  289. package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
  290. package/test/cut-times-timeline.test.js +65 -65
  291. package/test/cuts-follow-published-grid.test.js +109 -109
  292. package/test/data-channel-frame.test.js +63 -63
  293. package/test/dead-torrent-handle.test.js +52 -52
  294. package/test/declared-rates.test.js +116 -116
  295. package/test/declared-tracks.test.js +35 -35
  296. package/test/decode-cost-fit.test.js +202 -202
  297. package/test/decode-cost.test.js +462 -462
  298. package/test/delivery-probe.test.js +393 -393
  299. package/test/demand-register.test.js +206 -206
  300. package/test/dht-bootstrap.test.js +72 -72
  301. package/test/drain-threat.test.js +65 -65
  302. package/test/duplicate-add.test.js +48 -48
  303. package/test/encode-admission.test.js +146 -146
  304. package/test/encode-cost.test.js +283 -283
  305. package/test/encode-exit.test.js +111 -111
  306. package/test/encode-orchestrator.test.js +419 -419
  307. package/test/encode-plan-viewers.test.js +718 -718
  308. package/test/encode-plan.test.js +545 -545
  309. package/test/encode-run-state.test.js +314 -314
  310. package/test/encode-run.test.js +161 -161
  311. package/test/encode-slope.test.js +46 -46
  312. package/test/encoded-output.test.js +39 -39
  313. package/test/encoder-kinds.test.js +122 -122
  314. package/test/encoder-readings.test.js +46 -46
  315. package/test/every-claimant-is-told-its-share.test.js +152 -152
  316. package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
  317. package/test/failed-start-breaker.test.js +202 -202
  318. package/test/fastest-wires.test.js +143 -143
  319. package/test/ffmpeg-stream-counts.test.js +78 -78
  320. package/test/file-edges.test.js +191 -191
  321. package/test/fmp4-self-contained.test.js +80 -80
  322. package/test/fmp4-timestamps.test.js +140 -140
  323. package/test/fmp4-track-completeness.test.js +41 -41
  324. package/test/format-keyed-outputs.test.js +78 -78
  325. package/test/health-metrics.test.js +38 -38
  326. package/test/held-request-width.test.js +157 -157
  327. package/test/helpers/encode-run.js +136 -136
  328. package/test/helpers/held-file.js +29 -29
  329. package/test/helpers/manager.js +57 -57
  330. package/test/helpers/output-spec.js +63 -63
  331. package/test/host-load.test.js +68 -68
  332. package/test/index-accuracy-is-about-the-file.test.js +128 -128
  333. package/test/init-compat.test.js +219 -219
  334. package/test/input-lost-quiets-the-plan.test.js +261 -261
  335. package/test/input-unavailable.test.js +48 -48
  336. package/test/installation-id.test.js +39 -39
  337. package/test/keeping-period.test.js +83 -83
  338. package/test/keyframe-index-accuracy.test.js +188 -188
  339. package/test/keyframe-table-budget.test.js +102 -102
  340. package/test/keyframes-belong-to-the-file.test.js +249 -249
  341. package/test/learned-median.test.js +37 -37
  342. package/test/letting-go-asks-who-is-watching.test.js +191 -191
  343. package/test/limit-row-by-area.test.js +60 -60
  344. package/test/link-budget.test.js +111 -111
  345. package/test/link-probe.test.js +56 -56
  346. package/test/local-encoding-data.test.js +273 -273
  347. package/test/logger-repeats.test.js +134 -134
  348. package/test/look-ahead-coverage.test.js +56 -56
  349. package/test/manager-surface.test.js +292 -292
  350. package/test/matroska-cues-track.test.js +192 -192
  351. package/test/memory-budget.test.js +390 -390
  352. package/test/move-cost.test.js +216 -216
  353. package/test/mp4-subtitles.test.js +173 -173
  354. package/test/one-authority.test.js +285 -285
  355. package/test/one-budget.test.js +149 -149
  356. package/test/one-output-one-session.test.js +204 -204
  357. package/test/one-resource-divided.test.js +185 -185
  358. package/test/one-serving-output-per-height.test.js +458 -458
  359. package/test/orchestrator-wired.test.js +206 -206
  360. package/test/output-catalog.test.js +39 -39
  361. package/test/output-shape.test.js +21 -21
  362. package/test/output-spec.test.js +251 -251
  363. package/test/output-speed.test.js +88 -88
  364. package/test/outputs-being-prepared.test.js +40 -40
  365. package/test/packet-witness-ring.test.js +236 -236
  366. package/test/packet-witness.test.js +148 -148
  367. package/test/piece-disk-store.test.js +381 -381
  368. package/test/piece-from-whole-file.test.js +129 -129
  369. package/test/piece-lru.test.js +319 -319
  370. package/test/piece-reader.test.js +162 -162
  371. package/test/piece-selection-offset.test.js +85 -85
  372. package/test/piece-store-eviction.test.js +560 -560
  373. package/test/piece-store-never-refuses.test.js +153 -153
  374. package/test/piece-store-reservations.test.js +318 -318
  375. package/test/piece-store-slow-disk.test.js +159 -159
  376. package/test/piece-tail.test.js +97 -97
  377. package/test/plan-host-timings.test.js +68 -68
  378. package/test/plan-is-steady.test.js +225 -225
  379. package/test/priority-map-download.test.js +174 -174
  380. package/test/priority-map-emptied.test.js +204 -204
  381. package/test/priority-map-per-output.test.js +215 -215
  382. package/test/priority-map.test.js +163 -163
  383. package/test/probe-wedge-certainty.test.js +60 -60
  384. package/test/produced-copy-choice.test.js +213 -213
  385. package/test/published-timeline.test.js +83 -83
  386. package/test/quality-offer.test.js +162 -162
  387. package/test/quality-variants.test.js +1229 -1229
  388. package/test/rate-control-in-the-output.test.js +180 -180
  389. package/test/read-survives-withdrawal.test.js +164 -164
  390. package/test/read-window.test.js +365 -365
  391. package/test/resume-warm.test.js +39 -39
  392. package/test/run-graph-drift.test.js +37 -37
  393. package/test/run-inputs.test.js +80 -80
  394. package/test/run-intervals.test.js +100 -100
  395. package/test/run-position-follows-published-grid.test.js +89 -89
  396. package/test/run-progress.test.js +65 -65
  397. package/test/same-height-switch.test.js +664 -664
  398. package/test/seek-frees-the-viewers-requests.test.js +217 -217
  399. package/test/seek-landing.test.js +109 -109
  400. package/test/seek-target-not-superseded.test.js +195 -195
  401. package/test/segment-closed-on-both-branches.test.js +96 -96
  402. package/test/segment-cut-times.test.js +63 -63
  403. package/test/segment-demand.test.js +80 -80
  404. package/test/segment-hold-supersede.test.js +91 -91
  405. package/test/segment-judged-by-its-own-header.test.js +221 -221
  406. package/test/segment-serve-wiring.test.js +475 -475
  407. package/test/segment-store-eviction.test.js +247 -247
  408. package/test/segment-store.test.js +323 -323
  409. package/test/segment-true-start.test.js +86 -86
  410. package/test/segments-are-shared.test.js +176 -176
  411. package/test/send-chunk-bytes.test.js +86 -86
  412. package/test/serving-existing-output.test.js +102 -102
  413. package/test/serving-output.test.js +102 -102
  414. package/test/shared-piece-store.test.js +310 -310
  415. package/test/sidecar-naming.test.js +142 -142
  416. package/test/source-file.test.js +133 -133
  417. package/test/source-files-route.test.js +103 -103
  418. package/test/split-a-stretch.test.js +139 -139
  419. package/test/stale-request-after-seek.test.js +158 -158
  420. package/test/startup-readings.test.js +124 -124
  421. package/test/stream-route.test.js +182 -182
  422. package/test/subtitle-cue-framing.test.js +200 -200
  423. package/test/subtitle-cue-source.test.js +102 -102
  424. package/test/subtitle-cue-walk.test.js +370 -370
  425. package/test/subtitle-cursor.test.js +63 -63
  426. package/test/subtitle-defaults.test.js +118 -118
  427. package/test/subtitle-language.test.js +252 -252
  428. package/test/subtitle-track-numbering.test.js +386 -386
  429. package/test/supply-margin.test.js +177 -177
  430. package/test/swarm-follows-readers.test.js +174 -174
  431. package/test/swarm-reach.test.js +130 -130
  432. package/test/swarm-selection.test.js +220 -220
  433. package/test/tail-duplication.test.js +167 -167
  434. package/test/the-channel-has-both-ends.test.js +91 -91
  435. package/test/the-torrent-thread-serves-bytes.test.js +107 -107
  436. package/test/thread-pool.test.js +53 -53
  437. package/test/timeline.test.js +84 -84
  438. package/test/torrent-contents.test.js +235 -235
  439. package/test/torrent-cost.test.js +71 -71
  440. package/test/torrent-files.test.js +221 -221
  441. package/test/torrent-naming.test.js +255 -255
  442. package/test/tracks-begin-together.test.js +209 -209
  443. package/test/tunnel-renewal.test.js +167 -167
  444. package/test/two-viewers-one-picture.test.js +407 -407
  445. package/test/upload-hurry.test.js +186 -186
  446. package/test/usrsctp-state.test.js +131 -131
  447. package/test/video-facts.test.js +102 -102
  448. package/test/viewer-assignments.test.js +143 -143
  449. package/test/viewer-outputs.test.js +285 -285
  450. package/test/viewer-presence.test.js +116 -116
  451. package/test/viewer-requests-admission.test.js +89 -89
  452. package/test/viewer-states.test.js +189 -189
  453. package/test/viewer-subtitle-subscription.test.js +96 -96
  454. package/test/viewer.test.js +139 -139
  455. package/test/viewing-generations.test.js +390 -390
  456. package/test/visible-rung.test.js +37 -37
  457. package/test/wait-ledger.test.js +100 -100
  458. package/test/webrtc-shim.test.js +149 -149
  459. package/test/wedge-certainty.test.js +131 -131
  460. package/test/withdraw-piece-claim.test.js +212 -212
  461. package/test/worker-channel.test.js +158 -158
  462. package/utils/client-log-file.js +151 -151
  463. package/utils/logger.js +312 -312
  464. package/utils/parse-range.js +25 -25
  465. package/utils/perf.js +59 -59
@@ -1,96 +1,96 @@
1
- <!-- GENERATED from services/encode/encode-run-state.js by scripts/render-run-graph.js.
2
- Do not edit by hand: change the table and run `npm run graph`. -->
3
-
4
- # The encoder run — states and transitions
5
-
6
- One run of one ffmpeg inside one transcode session. The table this is drawn
7
- from is executed by `services/encode/EncodeRuns.js`; every transition a real
8
- run makes is logged as state, event and target, so a run that takes an edge
9
- absent here is a violation the log names.
10
-
11
- ```mermaid
12
- stateDiagram-v2
13
- state RUN {
14
- [*] --> IDLE
15
- state WORKING {
16
- state ALIVE {
17
- STARTING
18
- PRODUCING
19
- SUSPENDED
20
- }
21
- RETRY_WAIT
22
- }
23
- IDLE
24
- STOPPED
25
- ENDED_COMPLETE
26
- ENDED_FAILED
27
- }
28
-
29
- RUN --> STARTING : SPAWNED
30
- WORKING --> STOPPED : STOP_ORDERED
31
- ALIVE --> SUSPENDED : SUSPEND_ORDERED
32
- ALIVE --> ENDED_COMPLETE : EXITED_COMPLETE
33
- ALIVE --> ENDED_FAILED : EXITED_SHORT
34
- ALIVE --> RETRY_WAIT : EXITED_INPUT_LOST
35
- ALIVE --> ENDED_FAILED : EXITED_FAILED
36
- STARTING --> PRODUCING : FIRST_SEGMENT
37
- SUSPENDED --> PRODUCING : RESUME_ORDERED
38
- RETRY_WAIT --> IDLE : RETRY_DUE
39
- ```
40
-
41
- ## States
42
-
43
- | state | means |
44
- |---|---|
45
- | `IDLE` | No process, and one is expected — before the first run, or after a retry timer fired. |
46
- | `STARTING` | Spawned; this run has produced nothing servable yet. |
47
- | `PRODUCING` | This run has produced at least one servable segment. |
48
- | `SUSPENDED` | SIGSTOP delivered, process alive — and nothing is reading the input. |
49
- | `RETRY_WAIT` | The input went away; a restart is timed. Requests are held, not refused. |
50
- | `STOPPED` | Stopped on purpose with no replacement — a rung the viewer switched away from. |
51
- | `ENDED_COMPLETE` | Ran through the last segment of the file. Nothing is owed. |
52
- | `ENDED_FAILED` | Terminal for this target: requests are answered as failures. |
53
-
54
- ## Events
55
-
56
- | event | means |
57
- |---|---|
58
- | `SPAWNED` | A process was spawned for this session. |
59
- | `FIRST_SEGMENT` | The first servable segment of this run was served. |
60
- | `SUSPEND_ORDERED` | SIGSTOP was delivered. |
61
- | `RESUME_ORDERED` | SIGCONT was sent. |
62
- | `STOP_ORDERED` | The run was stopped with no replacement. |
63
- | `EXITED_COMPLETE` | Exit 0, having produced through the last segment. |
64
- | `EXITED_SHORT` | Exit 0, short of the last segment — the input dried up. |
65
- | `EXITED_INPUT_LOST` | Died because the input was not there. Recoverable. |
66
- | `EXITED_FAILED` | Died for any other reason, or could not be spawned. |
67
- | `RETRY_DUE` | The input-retry timer fired. |
68
-
69
- ## What each state answers
70
-
71
- Outputs depend on the state alone — computed here by calling the same
72
- functions the encoding component (`EncodeRuns`) calls.
73
-
74
- | state | reads its input | can be signalled | a missing segment | on the wire | may restart |
75
- |---|---|---|---|---|---|
76
- | `IDLE` | no | no | hold | `starting` | yes |
77
- | `STARTING` | yes | yes | hold | `running` | yes |
78
- | `PRODUCING` | yes | yes | hold | `running` | yes |
79
- | `SUSPENDED` | no | yes | hold | `running` | yes |
80
- | `RETRY_WAIT` | no | no | hold | `starting` | yes |
81
- | `STOPPED` | no | no | hold | `running` | yes |
82
- | `ENDED_COMPLETE` | no | no | hold | `ready` | no |
83
- | `ENDED_FAILED` | no | no | fail | `failed` | yes |
84
-
85
- ## Edges that must never exist
86
-
87
- The machine's real content: a near-complete digraph asserts nothing.
88
-
89
- | from | event | must not reach | because |
90
- |---|---|---|---|
91
- | `SUSPENDED` | `FIRST_SEGMENT` | `PRODUCING` | a segment request must not release a suspended encoder — 2.9.93, where any request did, and the run sawtoothed from 155 s to 922 s ahead of the viewer in three minutes |
92
- | `ENDED_FAILED` | `FIRST_SEGMENT` | `PRODUCING` | a dead run is not a producing one — 2.9.93, where the handle pointed at a corpse and every later seek was waved through as already covered, so the session answered 500 for ever |
93
- | `ENDED_FAILED` | `RESUME_ORDERED` | `PRODUCING` | there is no process to continue; only a spawn leads out of a failure |
94
- | `IDLE` | `FIRST_SEGMENT` | `PRODUCING` | nothing produces before a process exists |
95
- | `RETRY_WAIT` | `FIRST_SEGMENT` | `PRODUCING` | a run waiting for its input back has no process; only a spawn resumes production |
96
- | `STOPPED` | `RESUME_ORDERED` | `PRODUCING` | a rung the viewer switched away from must not be revived by its own held requests — the host has one encoder's worth of capacity and the rung on screen needs it |
1
+ <!-- GENERATED from services/encode/encode-run-state.js by scripts/render-run-graph.js.
2
+ Do not edit by hand: change the table and run `npm run graph`. -->
3
+
4
+ # The encoder run — states and transitions
5
+
6
+ One run of one ffmpeg inside one transcode session. The table this is drawn
7
+ from is executed by `services/encode/EncodeRuns.js`; every transition a real
8
+ run makes is logged as state, event and target, so a run that takes an edge
9
+ absent here is a violation the log names.
10
+
11
+ ```mermaid
12
+ stateDiagram-v2
13
+ state RUN {
14
+ [*] --> IDLE
15
+ state WORKING {
16
+ state ALIVE {
17
+ STARTING
18
+ PRODUCING
19
+ SUSPENDED
20
+ }
21
+ RETRY_WAIT
22
+ }
23
+ IDLE
24
+ STOPPED
25
+ ENDED_COMPLETE
26
+ ENDED_FAILED
27
+ }
28
+
29
+ RUN --> STARTING : SPAWNED
30
+ WORKING --> STOPPED : STOP_ORDERED
31
+ ALIVE --> SUSPENDED : SUSPEND_ORDERED
32
+ ALIVE --> ENDED_COMPLETE : EXITED_COMPLETE
33
+ ALIVE --> ENDED_FAILED : EXITED_SHORT
34
+ ALIVE --> RETRY_WAIT : EXITED_INPUT_LOST
35
+ ALIVE --> ENDED_FAILED : EXITED_FAILED
36
+ STARTING --> PRODUCING : FIRST_SEGMENT
37
+ SUSPENDED --> PRODUCING : RESUME_ORDERED
38
+ RETRY_WAIT --> IDLE : RETRY_DUE
39
+ ```
40
+
41
+ ## States
42
+
43
+ | state | means |
44
+ |---|---|
45
+ | `IDLE` | No process, and one is expected — before the first run, or after a retry timer fired. |
46
+ | `STARTING` | Spawned; this run has produced nothing servable yet. |
47
+ | `PRODUCING` | This run has produced at least one servable segment. |
48
+ | `SUSPENDED` | SIGSTOP delivered, process alive — and nothing is reading the input. |
49
+ | `RETRY_WAIT` | The input went away; a restart is timed. Requests are held, not refused. |
50
+ | `STOPPED` | Stopped on purpose with no replacement — a rung the viewer switched away from. |
51
+ | `ENDED_COMPLETE` | Ran through the last segment of the file. Nothing is owed. |
52
+ | `ENDED_FAILED` | Terminal for this target: requests are answered as failures. |
53
+
54
+ ## Events
55
+
56
+ | event | means |
57
+ |---|---|
58
+ | `SPAWNED` | A process was spawned for this session. |
59
+ | `FIRST_SEGMENT` | The first servable segment of this run was served. |
60
+ | `SUSPEND_ORDERED` | SIGSTOP was delivered. |
61
+ | `RESUME_ORDERED` | SIGCONT was sent. |
62
+ | `STOP_ORDERED` | The run was stopped with no replacement. |
63
+ | `EXITED_COMPLETE` | Exit 0, having produced through the last segment. |
64
+ | `EXITED_SHORT` | Exit 0, short of the last segment — the input dried up. |
65
+ | `EXITED_INPUT_LOST` | Died because the input was not there. Recoverable. |
66
+ | `EXITED_FAILED` | Died for any other reason, or could not be spawned. |
67
+ | `RETRY_DUE` | The input-retry timer fired. |
68
+
69
+ ## What each state answers
70
+
71
+ Outputs depend on the state alone — computed here by calling the same
72
+ functions the encoding component (`EncodeRuns`) calls.
73
+
74
+ | state | reads its input | can be signalled | a missing segment | on the wire | may restart |
75
+ |---|---|---|---|---|---|
76
+ | `IDLE` | no | no | hold | `starting` | yes |
77
+ | `STARTING` | yes | yes | hold | `running` | yes |
78
+ | `PRODUCING` | yes | yes | hold | `running` | yes |
79
+ | `SUSPENDED` | no | yes | hold | `running` | yes |
80
+ | `RETRY_WAIT` | no | no | hold | `starting` | yes |
81
+ | `STOPPED` | no | no | hold | `running` | yes |
82
+ | `ENDED_COMPLETE` | no | no | hold | `ready` | no |
83
+ | `ENDED_FAILED` | no | no | fail | `failed` | yes |
84
+
85
+ ## Edges that must never exist
86
+
87
+ The machine's real content: a near-complete digraph asserts nothing.
88
+
89
+ | from | event | must not reach | because |
90
+ |---|---|---|---|
91
+ | `SUSPENDED` | `FIRST_SEGMENT` | `PRODUCING` | a segment request must not release a suspended encoder — 2.9.93, where any request did, and the run sawtoothed from 155 s to 922 s ahead of the viewer in three minutes |
92
+ | `ENDED_FAILED` | `FIRST_SEGMENT` | `PRODUCING` | a dead run is not a producing one — 2.9.93, where the handle pointed at a corpse and every later seek was waved through as already covered, so the session answered 500 for ever |
93
+ | `ENDED_FAILED` | `RESUME_ORDERED` | `PRODUCING` | there is no process to continue; only a spawn leads out of a failure |
94
+ | `IDLE` | `FIRST_SEGMENT` | `PRODUCING` | nothing produces before a process exists |
95
+ | `RETRY_WAIT` | `FIRST_SEGMENT` | `PRODUCING` | a run waiting for its input back has no process; only a spawn resumes production |
96
+ | `STOPPED` | `RESUME_ORDERED` | `PRODUCING` | a rung the viewer switched away from must not be revived by its own held requests — the host has one encoder's worth of capacity and the rung on screen needs it |
package/docs/logs.md CHANGED
@@ -1,97 +1,97 @@
1
- # Logs — where to look
2
-
3
- No browser console copy-paste is needed: the page forwards its own log. But it
4
- lands in TWO places depending on the phase of the session, and `docker logs` is
5
- only one of them — see "Browser logs are in TWO places" below before concluding
6
- anything from a half-session.
7
-
8
- ## HA proxy (aarch64, addon `b34a1737_torrent_tv_proxy`)
9
-
10
- Image `b34a1737/aarch64-addon-torrent_tv_proxy:<version>` = `@torrent-tv/proxy` `<version>` (see `ha-addon/torrent_tv_proxy/config.yaml`).
11
-
12
- - Console + file are the same: the proxy logs to both `stdout` and `/data/proxy.log` on start (`logging to /data/proxy.log as well as the console`).
13
- - **Since 2.71.0 that is true of the torrent thread too, and it was not before.**
14
- A worker thread loads its own instance of every module, so the logger's file
15
- handle — set once, on the main thread — was null in the worker for the life of
16
- the process. Measured over a whole 49 938-line file: zero lines from the piece
17
- reader (`read "…"`, `supply "…"`, every wait and its cause) and zero from the
18
- torrent pool, against 52 and 36 of them in `docker logs`, which every release
19
- destroys. The worker now sends its lines to the main thread, which is the only
20
- writer; a line from there reads `torrent-worker: …`.
21
- - Via container:
22
- ```bash
23
- ssh ha "sudo docker logs app_b34a1737_torrent_tv_proxy --tail 200"
24
- ssh ha "sudo docker exec app_b34a1737_torrent_tv_proxy cat /data/proxy.log | tail -n 300"
25
- ```
26
- - Filter by session: every transcode session logs its id, e.g. `003ed2fd-7c9b-4cd5-9d05-ff875ff2be23`, `hold segment-00068.mp4 failed`, `encode-run failed`, `EXITED_*`.
27
- - Host file `/data/proxy.log` survives `docker logs` rotation; `core dumps: 1 present` line shows kept dump under `/data`.
28
-
29
- SSH to HA requires `MACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,umac-128-etm@openssh.com` — server `OpenSSH_10.3` on `homeassistant.local` offers only `*-etm` (`Unable to negotiate` / `Corrupted MAC` otherwise). Already in `~/.ssh/config` `Host ha`.
30
-
31
- ## DO server (webauth.courses, `infra-server-1`)
32
-
33
- - Server is `infra-server-1` (`ghcr.io/torrent-tv/server:latest`) on `do` (`206.189.97.152`).
34
- - Frontend logs reach here only in the phases the proxy cannot be reached in — see "Browser logs are in TWO places" below. When they do, the server `console.log`s them with the prefix `[client <tag> <id> sig=<webrtcSessionId>]` (`server/routes/api/client-logs/post.js`), in the same container as its own lines.
35
- - Via container:
36
- ```bash
37
- ssh do "docker logs infra-server-1 --tail 200 | cat"
38
- # filter a single viewing:
39
- ssh do "docker logs infra-server-1 --tail 500 | grep 003ed2fd"
40
- ssh do "docker logs infra-server-1 --tail 500 | grep '\[client'"
41
- ```
42
- - No need to open eruda or copy the browser console on a phone — it is forwarded.
43
-
44
- ## Browser logs are in TWO places, split by phase — read both
45
-
46
- Until 2026-09-13 the page's whole log went to the droplet's standard output,
47
- which every release of the server destroys. A viewer reported a desync and a
48
- frozen picture that day and the analysis ran on half the evidence, so the log
49
- moved to the proxy — beside the proxy's own, on the same durable directory.
50
-
51
- The split is by PHASE, and neither half is the whole session:
52
-
53
- 1. **Before a transport exists** — the page opening, the proxy being chosen, a
54
- connection failing — there is nowhere else to send it, so it goes to the
55
- SERVER (`infra-server-1`);
56
- 2. **From the moment the data channel is up**, the page's logger is given a
57
- proxy sink (`loading.js`, `setProxySink`) and every batch goes to the PROXY
58
- over that channel. This is the bulk of a viewing;
59
- 3. **At unload** it goes to the server again: `navigator.sendBeacon` is the only
60
- thing a page that is going away can use, and a data channel is not;
61
- 4. **A batch that cannot reach the proxy** falls back to the server.
62
-
63
- So a session that failed while connecting is on the droplet, a session that
64
- failed while playing is on the addon host, and a complete reading of a long
65
- failure needs both.
66
-
67
- ### The files on the proxy
68
-
69
- One file per viewing session, in the proxy log's own directory (`/data` on the
70
- addon host), named so the two halves join without guessing:
71
-
72
- ```
73
- client-<YYYYMMDD-HHMMSS UTC of the session start>-<sessionId:8>-<torrent name:60>-<infoHash:8>.log
74
- ```
75
-
76
- A session that has not chosen a torrent yet writes `no-torrent-yet` in that
77
- last part — the file is keyed by the session id, not by the name, so choosing a
78
- torrent later does NOT start a second file. Rotated at 16 MB to `<name>.log.1`.
79
-
80
- ```bash
81
- ssh ha "sudo docker exec app_b34a1737_torrent_tv_proxy sh -c 'ls -la /data/client-*.log'"
82
- ssh ha "sudo docker exec app_b34a1737_torrent_tv_proxy sh -c 'cat /data/client-20260913-*.log'"
83
- ```
84
-
85
- Implementation: `utils/client-log-file.js`, route `routes/api/client-logs/post.js`.
86
-
87
- ## Quick triage
88
-
89
- - Rewind/seek bug (hold 0ms → 500): HA `hold ... failed after 0ms → 500` + `encode-run` lines for the same `<sessionId>`, and DO `[client ...] fragLoadError / levelLoadError 500` for same `sn`.
90
- - Cushion / link budget: HA `memory: rss=... anon=...` and `cushion` lines; DO `[eta]` / `[cushion]` from client.
91
- - Update check: `ssh ha "sudo docker exec hassio_cli ha apps info b34a1737_torrent_tv_proxy | grep version"` and `ssh ha "sudo docker ps --filter name=app_b34a1737_torrent_tv_proxy"`.
92
-
93
- ## Related
94
-
95
- - `ha-addon/CLAUDE.md` — addon build/update detour via `hassio_cli`, cache-bust via `config.yaml` version.
96
- - `docs/container-architecture.md` — what container/track classes log and where.
97
- - `routes/api/client-logs/post.js` and `server/routes/api/client-logs/post.js` — the two receivers; both sanitize the same way (control chars → space, `MAX_LINES`, `MAX_MSG_LEN`).
1
+ # Logs — where to look
2
+
3
+ No browser console copy-paste is needed: the page forwards its own log. But it
4
+ lands in TWO places depending on the phase of the session, and `docker logs` is
5
+ only one of them — see "Browser logs are in TWO places" below before concluding
6
+ anything from a half-session.
7
+
8
+ ## HA proxy (aarch64, addon `b34a1737_torrent_tv_proxy`)
9
+
10
+ Image `b34a1737/aarch64-addon-torrent_tv_proxy:<version>` = `@torrent-tv/proxy` `<version>` (see `ha-addon/torrent_tv_proxy/config.yaml`).
11
+
12
+ - Console + file are the same: the proxy logs to both `stdout` and `/data/proxy.log` on start (`logging to /data/proxy.log as well as the console`).
13
+ - **Since 2.71.0 that is true of the torrent thread too, and it was not before.**
14
+ A worker thread loads its own instance of every module, so the logger's file
15
+ handle — set once, on the main thread — was null in the worker for the life of
16
+ the process. Measured over a whole 49 938-line file: zero lines from the piece
17
+ reader (`read "…"`, `supply "…"`, every wait and its cause) and zero from the
18
+ torrent pool, against 52 and 36 of them in `docker logs`, which every release
19
+ destroys. The worker now sends its lines to the main thread, which is the only
20
+ writer; a line from there reads `torrent-worker: …`.
21
+ - Via container:
22
+ ```bash
23
+ ssh ha "sudo docker logs app_b34a1737_torrent_tv_proxy --tail 200"
24
+ ssh ha "sudo docker exec app_b34a1737_torrent_tv_proxy cat /data/proxy.log | tail -n 300"
25
+ ```
26
+ - Filter by session: every transcode session logs its id, e.g. `003ed2fd-7c9b-4cd5-9d05-ff875ff2be23`, `hold segment-00068.mp4 failed`, `encode-run failed`, `EXITED_*`.
27
+ - Host file `/data/proxy.log` survives `docker logs` rotation; `core dumps: 1 present` line shows kept dump under `/data`.
28
+
29
+ SSH to HA requires `MACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,umac-128-etm@openssh.com` — server `OpenSSH_10.3` on `homeassistant.local` offers only `*-etm` (`Unable to negotiate` / `Corrupted MAC` otherwise). Already in `~/.ssh/config` `Host ha`.
30
+
31
+ ## DO server (webauth.courses, `infra-server-1`)
32
+
33
+ - Server is `infra-server-1` (`ghcr.io/torrent-tv/server:latest`) on `do` (`206.189.97.152`).
34
+ - Frontend logs reach here only in the phases the proxy cannot be reached in — see "Browser logs are in TWO places" below. When they do, the server `console.log`s them with the prefix `[client <tag> <id> sig=<webrtcSessionId>]` (`server/routes/api/client-logs/post.js`), in the same container as its own lines.
35
+ - Via container:
36
+ ```bash
37
+ ssh do "docker logs infra-server-1 --tail 200 | cat"
38
+ # filter a single viewing:
39
+ ssh do "docker logs infra-server-1 --tail 500 | grep 003ed2fd"
40
+ ssh do "docker logs infra-server-1 --tail 500 | grep '\[client'"
41
+ ```
42
+ - No need to open eruda or copy the browser console on a phone — it is forwarded.
43
+
44
+ ## Browser logs are in TWO places, split by phase — read both
45
+
46
+ Until 2026-09-13 the page's whole log went to the droplet's standard output,
47
+ which every release of the server destroys. A viewer reported a desync and a
48
+ frozen picture that day and the analysis ran on half the evidence, so the log
49
+ moved to the proxy — beside the proxy's own, on the same durable directory.
50
+
51
+ The split is by PHASE, and neither half is the whole session:
52
+
53
+ 1. **Before a transport exists** — the page opening, the proxy being chosen, a
54
+ connection failing — there is nowhere else to send it, so it goes to the
55
+ SERVER (`infra-server-1`);
56
+ 2. **From the moment the data channel is up**, the page's logger is given a
57
+ proxy sink (`loading.js`, `setProxySink`) and every batch goes to the PROXY
58
+ over that channel. This is the bulk of a viewing;
59
+ 3. **At unload** it goes to the server again: `navigator.sendBeacon` is the only
60
+ thing a page that is going away can use, and a data channel is not;
61
+ 4. **A batch that cannot reach the proxy** falls back to the server.
62
+
63
+ So a session that failed while connecting is on the droplet, a session that
64
+ failed while playing is on the addon host, and a complete reading of a long
65
+ failure needs both.
66
+
67
+ ### The files on the proxy
68
+
69
+ One file per viewing session, in the proxy log's own directory (`/data` on the
70
+ addon host), named so the two halves join without guessing:
71
+
72
+ ```
73
+ client-<YYYYMMDD-HHMMSS UTC of the session start>-<sessionId:8>-<torrent name:60>-<infoHash:8>.log
74
+ ```
75
+
76
+ A session that has not chosen a torrent yet writes `no-torrent-yet` in that
77
+ last part — the file is keyed by the session id, not by the name, so choosing a
78
+ torrent later does NOT start a second file. Rotated at 16 MB to `<name>.log.1`.
79
+
80
+ ```bash
81
+ ssh ha "sudo docker exec app_b34a1737_torrent_tv_proxy sh -c 'ls -la /data/client-*.log'"
82
+ ssh ha "sudo docker exec app_b34a1737_torrent_tv_proxy sh -c 'cat /data/client-20260913-*.log'"
83
+ ```
84
+
85
+ Implementation: `utils/client-log-file.js`, route `routes/api/client-logs/post.js`.
86
+
87
+ ## Quick triage
88
+
89
+ - Rewind/seek bug (hold 0ms → 500): HA `hold ... failed after 0ms → 500` + `encode-run` lines for the same `<sessionId>`, and DO `[client ...] fragLoadError / levelLoadError 500` for same `sn`.
90
+ - Cushion / link budget: HA `memory: rss=... anon=...` and `cushion` lines; DO `[eta]` / `[cushion]` from client.
91
+ - Update check: `ssh ha "sudo docker exec hassio_cli ha apps info b34a1737_torrent_tv_proxy | grep version"` and `ssh ha "sudo docker ps --filter name=app_b34a1737_torrent_tv_proxy"`.
92
+
93
+ ## Related
94
+
95
+ - `ha-addon/CLAUDE.md` — addon build/update detour via `hassio_cli`, cache-bust via `config.yaml` version.
96
+ - `docs/container-architecture.md` — what container/track classes log and where.
97
+ - `routes/api/client-logs/post.js` and `server/routes/api/client-logs/post.js` — the two receivers; both sanitize the same way (control chars → space, `MAX_LINES`, `MAX_MSG_LEN`).
@@ -1,114 +1,114 @@
1
- # SOCKS5 torrent egress — feasibility research (NOT implemented)
2
-
3
- Research only (2026-07-10), no code. Question: can the proxy route its
4
- **torrent** traffic (peers, trackers, DHT) through an owner-configured SOCKS5
5
- endpoint (e.g. a VPN provider's) to hide the owner's home IP from the swarm,
6
- while WebRTC to the viewer stays direct? Roadmap item 8 in the root
7
- `CLAUDE.md`.
8
-
9
- Stack: WebTorrent 2.8.5, Node. Verified against
10
- `node_modules/webtorrent/lib/*.js` and `index.js`.
11
-
12
- ## What "torrent traffic" actually is (and which parts can be SOCKS5'd)
13
-
14
- SOCKS5 forwards a connection through a relay so the destination sees the
15
- relay's IP, not ours. It supports TCP (CONNECT) and, in theory, UDP (UDP
16
- ASSOCIATE) — but UDP-ASSOCIATE is rarely usable from Node libraries that bind
17
- their own UDP sockets. So per traffic type:
18
-
19
- | Traffic | Transport | Where in WebTorrent | SOCKS5? |
20
- |---|---|---|---|
21
- | Peer wire (outgoing) | TCP | `torrent.js` `net.connect(opts)` (~L2112) | **Yes** (TCP CONNECT) |
22
- | Peer wire (outgoing) | µTP (UDP) | `torrent.js` `utp.connect()` (~L2110) | No (UDP, own socket) → **disable** |
23
- | HTTP/HTTPS trackers | TCP | `bittorrent-tracker` (HTTP agent) | **Yes** (agent) |
24
- | WSS trackers | TCP (WebSocket) | `bittorrent-tracker` (ws agent) | **Yes** (agent) |
25
- | UDP trackers (`udp://`) | UDP | `bittorrent-tracker` | No → **drop** (filter announce list) |
26
- | DHT | UDP | `bittorrent-dht` | No → **disable** (`dht:false`) |
27
- | LSD (local peer discovery) | UDP multicast | conn-pool | LAN-only leak → **disable** (`lsd:false`) |
28
- | Incoming peers | TCP/µTP listener | conn-pool | N/A (inbound; see note) |
29
-
30
- Conclusion: a clean, leak-free config is **TCP-only through the SOCKS5 relay**
31
- with every UDP path turned OFF. This is the standard "torrent proxy" model and
32
- its standard caveat: you lose µTP peers, UDP trackers, and DHT peer discovery —
33
- peer counts drop, but HTTP/WSS-tracker swarms (like the rutracker trackers our
34
- test torrents use) still work, and TCP peers are plentiful.
35
-
36
- WebTorrent already exposes the needed off-switches (verified in `index.js`):
37
- `utp: false` (L82), `dht: false` (L127), `lsd: false` (L76), `webSeeds`
38
- (L161). Announce-list filtering to `http(s)://`/`wss://` is ours to do.
39
-
40
- ## The hard part: no dialer hook for TCP peers
41
-
42
- WebTorrent has **no public option** to supply a custom socket/dialer for
43
- outgoing peer connections — it calls `net.connect(opts)` directly inside
44
- `torrent.js`. Three ways around it:
45
-
46
- ### Approach A — userland, scoped monkeypatch of `net.connect` (+ disable UDP)
47
- Wrap `net.connect` so that for a **peer destination** it returns a socket
48
- tunneled through SOCKS5 (via the already-present `socks` package —
49
- `SocksClient.createConnection({ proxy, command:'connect', destination })`),
50
- and for everything else (our loopback HTTP server, the signalling tunnel,
51
- ffmpeg's `127.0.0.1` fetch, DNS) it calls the real `net.connect`.
52
- - The `socks` dep is ALREADY in `node_modules` (currently transitive) — would
53
- become a direct dependency.
54
- - Shim detail: `net.connect` returns a socket synchronously and connects
55
- async; `SocksClient.createConnection` is Promise-based. Need a small adapter
56
- that returns a `net.Socket`-like duplex immediately and attaches the real
57
- tunneled socket once the SOCKS handshake resolves (or emits `error`). The
58
- `socks` package's event API supports this.
59
- - Scoping rule: SOCKS only for non-local IPv4/IPv6 destinations; NEVER for
60
- `127.0.0.1`/`::1`/the tunnel host — otherwise we'd route our own control
61
- plane through the VPN.
62
- - Trackers/webseeds (HTTP/WSS): pass a `socks-proxy-agent` as the HTTP/ws
63
- agent to `bittorrent-tracker` / webseed fetches (need to confirm WebTorrent
64
- threads an `agent` option through; may need a small patch).
65
- - Pros: no fork, no build-chain changes, deployment-agnostic. Cons: global
66
- monkeypatch is delicate; must be airtight on the scoping rule.
67
-
68
- ### Approach B — patch/fork WebTorrent to add a `createConnection` hook
69
- Add an option so `torrent.js` uses `opts.createConnection ?? net.connect`.
70
- Cleaner and explicit, but a fork to maintain (or an upstream PR — issue
71
- webtorrent#807 "SOCKS Proxy Support" is open and unresolved, so upstream is
72
- unlikely soon). More correct long-term if A proves too fragile.
73
-
74
- ### Approach C — network-level (VPN container / namespace), NOT in-proxy
75
- Run the whole proxy process behind a VPN with a kill-switch (e.g. a `gluetun`
76
- sidecar container, or a Linux network namespace bound to the VPN). Catches
77
- ALL traffic — TCP, µTP, DHT, UDP trackers — leak-proof, zero proxy code.
78
- Trade-off: not per-app (the whole proxy egresses via VPN, including the
79
- signalling tunnel unless split), heavier ops, and awkward for the HA addon
80
- (`host_network: true`). This is the ROBUST alternative to document alongside;
81
- for a bare-Docker/npm operator it may actually be the better answer than
82
- in-proxy SOCKS5.
83
-
84
- ## Kill-switch (fail-closed) — mandatory for the feature to mean anything
85
- If the SOCKS5 relay is down, torrent traffic must NOT silently fall back to a
86
- direct connection (that leaks the exact IP we set out to hide). With Approach
87
- A this is natural: a failed SOCKS handshake fails the peer connection with no
88
- direct retry, AND all UDP paths are already disabled, so nothing bypasses the
89
- relay. Must add a startup connectivity self-test + a clear log line
90
- (BitPlay's "test proxy" pattern), and surface "proxy down → torrent paused"
91
- rather than leaking.
92
-
93
- ## Recommended direction (when we build it)
94
- 1. Primary: **Approach A** — `socks` for TCP peers via a scoped `net.connect`
95
- shim + `socks-proxy-agent` for HTTP/WSS trackers + webseeds, with
96
- `utp:false`, `dht:false`, `lsd:false`, and announce-list filtered to
97
- TCP-based trackers. Fail-closed by construction.
98
- 2. Document **Approach C** (VPN container) in the addon/deployment docs as the
99
- leak-proof alternative for operators who prefer it.
100
- 3. Approach B only if A's monkeypatch proves too fragile in practice.
101
-
102
- ## Open questions to resolve at build time (not now)
103
- - Does WebTorrent 2.8.x thread an `agent`/proxy option to `bittorrent-tracker`
104
- and webseed fetches, or is a small patch needed there too?
105
- - `socks` socket-shim: cleanest way to present a pre-connect `net.Socket`
106
- (custom Duplex vs a paused real socket) without confusing
107
- `bittorrent-protocol`'s wire setup.
108
- - Incoming peers: with UDP off and outbound via SOCKS, do we still accept
109
- inbound TCP peers directly (that inbound listener exposes the home IP to
110
- connecting peers)? For full hiding, inbound may need to be disabled too
111
- (accept that peer discovery becomes outbound-only) — decide per privacy goal.
112
- - Interaction with the UPnP port mapping / WebRTC UDP port (those are for the
113
- viewer transport, NOT torrent egress — must stay direct; confirm the shim's
114
- scoping never touches them).
1
+ # SOCKS5 torrent egress — feasibility research (NOT implemented)
2
+
3
+ Research only (2026-07-10), no code. Question: can the proxy route its
4
+ **torrent** traffic (peers, trackers, DHT) through an owner-configured SOCKS5
5
+ endpoint (e.g. a VPN provider's) to hide the owner's home IP from the swarm,
6
+ while WebRTC to the viewer stays direct? Roadmap item 8 in the root
7
+ `CLAUDE.md`.
8
+
9
+ Stack: WebTorrent 2.8.5, Node. Verified against
10
+ `node_modules/webtorrent/lib/*.js` and `index.js`.
11
+
12
+ ## What "torrent traffic" actually is (and which parts can be SOCKS5'd)
13
+
14
+ SOCKS5 forwards a connection through a relay so the destination sees the
15
+ relay's IP, not ours. It supports TCP (CONNECT) and, in theory, UDP (UDP
16
+ ASSOCIATE) — but UDP-ASSOCIATE is rarely usable from Node libraries that bind
17
+ their own UDP sockets. So per traffic type:
18
+
19
+ | Traffic | Transport | Where in WebTorrent | SOCKS5? |
20
+ |---|---|---|---|
21
+ | Peer wire (outgoing) | TCP | `torrent.js` `net.connect(opts)` (~L2112) | **Yes** (TCP CONNECT) |
22
+ | Peer wire (outgoing) | µTP (UDP) | `torrent.js` `utp.connect()` (~L2110) | No (UDP, own socket) → **disable** |
23
+ | HTTP/HTTPS trackers | TCP | `bittorrent-tracker` (HTTP agent) | **Yes** (agent) |
24
+ | WSS trackers | TCP (WebSocket) | `bittorrent-tracker` (ws agent) | **Yes** (agent) |
25
+ | UDP trackers (`udp://`) | UDP | `bittorrent-tracker` | No → **drop** (filter announce list) |
26
+ | DHT | UDP | `bittorrent-dht` | No → **disable** (`dht:false`) |
27
+ | LSD (local peer discovery) | UDP multicast | conn-pool | LAN-only leak → **disable** (`lsd:false`) |
28
+ | Incoming peers | TCP/µTP listener | conn-pool | N/A (inbound; see note) |
29
+
30
+ Conclusion: a clean, leak-free config is **TCP-only through the SOCKS5 relay**
31
+ with every UDP path turned OFF. This is the standard "torrent proxy" model and
32
+ its standard caveat: you lose µTP peers, UDP trackers, and DHT peer discovery —
33
+ peer counts drop, but HTTP/WSS-tracker swarms (like the rutracker trackers our
34
+ test torrents use) still work, and TCP peers are plentiful.
35
+
36
+ WebTorrent already exposes the needed off-switches (verified in `index.js`):
37
+ `utp: false` (L82), `dht: false` (L127), `lsd: false` (L76), `webSeeds`
38
+ (L161). Announce-list filtering to `http(s)://`/`wss://` is ours to do.
39
+
40
+ ## The hard part: no dialer hook for TCP peers
41
+
42
+ WebTorrent has **no public option** to supply a custom socket/dialer for
43
+ outgoing peer connections — it calls `net.connect(opts)` directly inside
44
+ `torrent.js`. Three ways around it:
45
+
46
+ ### Approach A — userland, scoped monkeypatch of `net.connect` (+ disable UDP)
47
+ Wrap `net.connect` so that for a **peer destination** it returns a socket
48
+ tunneled through SOCKS5 (via the already-present `socks` package —
49
+ `SocksClient.createConnection({ proxy, command:'connect', destination })`),
50
+ and for everything else (our loopback HTTP server, the signalling tunnel,
51
+ ffmpeg's `127.0.0.1` fetch, DNS) it calls the real `net.connect`.
52
+ - The `socks` dep is ALREADY in `node_modules` (currently transitive) — would
53
+ become a direct dependency.
54
+ - Shim detail: `net.connect` returns a socket synchronously and connects
55
+ async; `SocksClient.createConnection` is Promise-based. Need a small adapter
56
+ that returns a `net.Socket`-like duplex immediately and attaches the real
57
+ tunneled socket once the SOCKS handshake resolves (or emits `error`). The
58
+ `socks` package's event API supports this.
59
+ - Scoping rule: SOCKS only for non-local IPv4/IPv6 destinations; NEVER for
60
+ `127.0.0.1`/`::1`/the tunnel host — otherwise we'd route our own control
61
+ plane through the VPN.
62
+ - Trackers/webseeds (HTTP/WSS): pass a `socks-proxy-agent` as the HTTP/ws
63
+ agent to `bittorrent-tracker` / webseed fetches (need to confirm WebTorrent
64
+ threads an `agent` option through; may need a small patch).
65
+ - Pros: no fork, no build-chain changes, deployment-agnostic. Cons: global
66
+ monkeypatch is delicate; must be airtight on the scoping rule.
67
+
68
+ ### Approach B — patch/fork WebTorrent to add a `createConnection` hook
69
+ Add an option so `torrent.js` uses `opts.createConnection ?? net.connect`.
70
+ Cleaner and explicit, but a fork to maintain (or an upstream PR — issue
71
+ webtorrent#807 "SOCKS Proxy Support" is open and unresolved, so upstream is
72
+ unlikely soon). More correct long-term if A proves too fragile.
73
+
74
+ ### Approach C — network-level (VPN container / namespace), NOT in-proxy
75
+ Run the whole proxy process behind a VPN with a kill-switch (e.g. a `gluetun`
76
+ sidecar container, or a Linux network namespace bound to the VPN). Catches
77
+ ALL traffic — TCP, µTP, DHT, UDP trackers — leak-proof, zero proxy code.
78
+ Trade-off: not per-app (the whole proxy egresses via VPN, including the
79
+ signalling tunnel unless split), heavier ops, and awkward for the HA addon
80
+ (`host_network: true`). This is the ROBUST alternative to document alongside;
81
+ for a bare-Docker/npm operator it may actually be the better answer than
82
+ in-proxy SOCKS5.
83
+
84
+ ## Kill-switch (fail-closed) — mandatory for the feature to mean anything
85
+ If the SOCKS5 relay is down, torrent traffic must NOT silently fall back to a
86
+ direct connection (that leaks the exact IP we set out to hide). With Approach
87
+ A this is natural: a failed SOCKS handshake fails the peer connection with no
88
+ direct retry, AND all UDP paths are already disabled, so nothing bypasses the
89
+ relay. Must add a startup connectivity self-test + a clear log line
90
+ (BitPlay's "test proxy" pattern), and surface "proxy down → torrent paused"
91
+ rather than leaking.
92
+
93
+ ## Recommended direction (when we build it)
94
+ 1. Primary: **Approach A** — `socks` for TCP peers via a scoped `net.connect`
95
+ shim + `socks-proxy-agent` for HTTP/WSS trackers + webseeds, with
96
+ `utp:false`, `dht:false`, `lsd:false`, and announce-list filtered to
97
+ TCP-based trackers. Fail-closed by construction.
98
+ 2. Document **Approach C** (VPN container) in the addon/deployment docs as the
99
+ leak-proof alternative for operators who prefer it.
100
+ 3. Approach B only if A's monkeypatch proves too fragile in practice.
101
+
102
+ ## Open questions to resolve at build time (not now)
103
+ - Does WebTorrent 2.8.x thread an `agent`/proxy option to `bittorrent-tracker`
104
+ and webseed fetches, or is a small patch needed there too?
105
+ - `socks` socket-shim: cleanest way to present a pre-connect `net.Socket`
106
+ (custom Duplex vs a paused real socket) without confusing
107
+ `bittorrent-protocol`'s wire setup.
108
+ - Incoming peers: with UDP off and outbound via SOCKS, do we still accept
109
+ inbound TCP peers directly (that inbound listener exposes the home IP to
110
+ connecting peers)? For full hiding, inbound may need to be disabled too
111
+ (accept that peer discovery becomes outbound-only) — decide per privacy goal.
112
+ - Interaction with the UPnP port mapping / WebRTC UDP port (those are for the
113
+ viewer transport, NOT torrent egress — must stay direct; confirm the shim's
114
+ scoping never touches them).