@torrent-tv/proxy 2.87.10 → 2.87.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.
Files changed (469) 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 +2191 -2181
  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 +124 -101
  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 -217
  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 +136 -129
  92. package/routes/api/transcode-sessions/post.js +173 -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 +725 -725
  106. package/services/encode/CoverageMap.js +428 -428
  107. package/services/encode/CushionReport.js +345 -345
  108. package/services/encode/EncodeAdmission.js +376 -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 +758 -755
  118. package/services/encode/OutputTimes.js +579 -579
  119. package/services/encode/QsvEncoder.js +58 -58
  120. package/services/encode/Renditions.js +2184 -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 +1141 -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 +315 -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 +245 -245
  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 +931 -931
  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 +727 -522
  203. package/services/server/WaitLedger.js +142 -142
  204. package/services/server/controllers/PlaybackController.js +40 -40
  205. package/services/server/controllers/SubtitleController.js +109 -109
  206. package/services/server/http-responses.js +60 -60
  207. package/services/server/playback-readiness.js +620 -0
  208. package/services/server/transcode-session-files.js +325 -325
  209. package/services/server/wire-outputs.js +992 -983
  210. package/services/storage/Diagnostics.js +202 -202
  211. package/services/storage/MachineBudget.js +235 -235
  212. package/services/storage/allowance.js +107 -107
  213. package/services/storage/core-dumps.js +106 -106
  214. package/services/storage/files/CompletedFiles.js +391 -391
  215. package/services/storage/files/piece-from-whole-file.js +118 -118
  216. package/services/storage/free.js +71 -71
  217. package/services/storage/keep.js +48 -48
  218. package/services/storage/machine-memory.js +56 -56
  219. package/services/storage/memory-report.js +575 -575
  220. package/services/storage/piece-store/piece-disk-store.js +516 -516
  221. package/services/storage/piece-store/piece-lru.js +495 -495
  222. package/services/storage/piece-store/shared-piece-store.js +1880 -1880
  223. package/services/storage/returns.js +103 -103
  224. package/services/storage/segment-store/SegmentStore.js +1196 -1196
  225. package/services/storage/wire.js +175 -175
  226. package/services/torrent/Contents.js +324 -324
  227. package/services/torrent/demand/DemandRegister.js +202 -202
  228. package/services/torrent/demand/Urgency.js +137 -137
  229. package/services/torrent/demand/Window.js +118 -118
  230. package/services/torrent/demand/index.js +8 -8
  231. package/services/torrent/demand/pieces.js +140 -140
  232. package/services/torrent/download/SwarmSelection.js +386 -386
  233. package/services/torrent/download/registry.js +102 -102
  234. package/services/torrent/download/withdraw-claim.js +80 -80
  235. package/services/torrent/files.js +455 -455
  236. package/services/torrent/naming.js +619 -619
  237. package/services/torrent/piece-store-of.js +29 -29
  238. package/services/torrent/supply-margin.js +262 -262
  239. package/services/torrent/torrent-pool.js +2675 -2675
  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 -765
  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 -365
  249. package/services/torrent/worker/protocol.js +184 -184
  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 -1129
  253. package/services/transport/data-channel-handler.js +1369 -1369
  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 +682 -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 +71 -71
  275. package/test/asset-paths.test.js +28 -28
  276. package/test/audio-inventory.test.js +212 -212
  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/data-channel-viewer-lifecycle.test.js +92 -92
  294. package/test/dead-torrent-handle.test.js +52 -52
  295. package/test/declared-rates.test.js +116 -116
  296. package/test/declared-tracks.test.js +43 -43
  297. package/test/decode-cost-fit.test.js +202 -202
  298. package/test/decode-cost.test.js +462 -462
  299. package/test/delivery-probe.test.js +393 -393
  300. package/test/demand-register.test.js +206 -206
  301. package/test/dht-bootstrap.test.js +72 -72
  302. package/test/drain-threat.test.js +65 -65
  303. package/test/duplicate-add.test.js +48 -48
  304. package/test/encode-admission.test.js +146 -146
  305. package/test/encode-cost.test.js +283 -283
  306. package/test/encode-exit.test.js +111 -111
  307. package/test/encode-orchestrator.test.js +419 -419
  308. package/test/encode-plan-viewers.test.js +718 -718
  309. package/test/encode-plan.test.js +545 -545
  310. package/test/encode-run-state.test.js +314 -314
  311. package/test/encode-run.test.js +161 -161
  312. package/test/encode-slope.test.js +46 -46
  313. package/test/encoded-output.test.js +39 -39
  314. package/test/encoder-kinds.test.js +122 -122
  315. package/test/encoder-readings.test.js +46 -46
  316. package/test/every-claimant-is-told-its-share.test.js +152 -152
  317. package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
  318. package/test/failed-start-breaker.test.js +202 -202
  319. package/test/fastest-wires.test.js +143 -143
  320. package/test/ffmpeg-stream-counts.test.js +78 -78
  321. package/test/file-edges.test.js +191 -191
  322. package/test/fmp4-self-contained.test.js +80 -80
  323. package/test/fmp4-timestamps.test.js +140 -140
  324. package/test/fmp4-track-completeness.test.js +41 -41
  325. package/test/format-keyed-outputs.test.js +78 -78
  326. package/test/health-metrics.test.js +38 -38
  327. package/test/held-request-width.test.js +157 -157
  328. package/test/helpers/encode-run.js +136 -136
  329. package/test/helpers/held-file.js +29 -29
  330. package/test/helpers/manager.js +57 -57
  331. package/test/helpers/output-spec.js +63 -63
  332. package/test/host-load.test.js +68 -68
  333. package/test/index-accuracy-is-about-the-file.test.js +128 -128
  334. package/test/init-compat.test.js +219 -219
  335. package/test/input-lost-quiets-the-plan.test.js +261 -261
  336. package/test/input-unavailable.test.js +48 -48
  337. package/test/installation-id.test.js +39 -39
  338. package/test/keeping-period.test.js +83 -83
  339. package/test/keyframe-index-accuracy.test.js +188 -188
  340. package/test/keyframe-table-budget.test.js +102 -102
  341. package/test/keyframes-belong-to-the-file.test.js +249 -249
  342. package/test/learned-median.test.js +37 -37
  343. package/test/letting-go-asks-who-is-watching.test.js +191 -191
  344. package/test/limit-row-by-area.test.js +60 -60
  345. package/test/link-budget.test.js +111 -111
  346. package/test/link-probe.test.js +56 -56
  347. package/test/local-encoding-data.test.js +273 -273
  348. package/test/logger-repeats.test.js +134 -134
  349. package/test/look-ahead-coverage.test.js +56 -56
  350. package/test/manager-surface.test.js +292 -292
  351. package/test/matroska-cues-track.test.js +192 -192
  352. package/test/memory-budget.test.js +390 -390
  353. package/test/move-cost.test.js +216 -216
  354. package/test/mp4-subtitles.test.js +173 -173
  355. package/test/one-authority.test.js +285 -285
  356. package/test/one-budget.test.js +149 -149
  357. package/test/one-output-one-session.test.js +204 -204
  358. package/test/one-resource-divided.test.js +185 -185
  359. package/test/one-serving-output-per-height.test.js +458 -458
  360. package/test/orchestrator-wired.test.js +206 -206
  361. package/test/output-catalog.test.js +39 -39
  362. package/test/output-shape.test.js +21 -21
  363. package/test/output-spec.test.js +251 -251
  364. package/test/output-speed.test.js +88 -88
  365. package/test/outputs-being-prepared.test.js +40 -40
  366. package/test/packet-witness-ring.test.js +236 -236
  367. package/test/packet-witness.test.js +148 -148
  368. package/test/piece-disk-store.test.js +381 -381
  369. package/test/piece-from-whole-file.test.js +129 -129
  370. package/test/piece-lru.test.js +319 -319
  371. package/test/piece-reader.test.js +162 -162
  372. package/test/piece-selection-offset.test.js +85 -85
  373. package/test/piece-store-eviction.test.js +560 -560
  374. package/test/piece-store-never-refuses.test.js +153 -153
  375. package/test/piece-store-reservations.test.js +318 -318
  376. package/test/piece-store-slow-disk.test.js +159 -159
  377. package/test/piece-tail.test.js +97 -97
  378. package/test/plan-host-timings.test.js +68 -68
  379. package/test/plan-is-steady.test.js +225 -225
  380. package/test/playback-readiness.test.js +323 -0
  381. package/test/priority-map-download.test.js +174 -174
  382. package/test/priority-map-emptied.test.js +204 -204
  383. package/test/priority-map-per-output.test.js +215 -215
  384. package/test/priority-map.test.js +163 -163
  385. package/test/probe-wedge-certainty.test.js +60 -60
  386. package/test/produced-copy-choice.test.js +213 -213
  387. package/test/published-timeline.test.js +83 -83
  388. package/test/quality-offer.test.js +162 -162
  389. package/test/quality-variants.test.js +1229 -1229
  390. package/test/rate-control-in-the-output.test.js +180 -180
  391. package/test/read-survives-withdrawal.test.js +164 -164
  392. package/test/read-window.test.js +365 -365
  393. package/test/resume-warm.test.js +39 -39
  394. package/test/run-graph-drift.test.js +37 -37
  395. package/test/run-inputs.test.js +80 -80
  396. package/test/run-intervals.test.js +100 -100
  397. package/test/run-position-follows-published-grid.test.js +89 -89
  398. package/test/run-progress.test.js +65 -65
  399. package/test/same-height-switch.test.js +664 -664
  400. package/test/seek-frees-the-viewers-requests.test.js +217 -217
  401. package/test/seek-landing.test.js +109 -109
  402. package/test/seek-target-not-superseded.test.js +195 -195
  403. package/test/segment-closed-on-both-branches.test.js +96 -96
  404. package/test/segment-cut-times.test.js +63 -63
  405. package/test/segment-demand.test.js +80 -80
  406. package/test/segment-hold-supersede.test.js +91 -91
  407. package/test/segment-judged-by-its-own-header.test.js +221 -221
  408. package/test/segment-serve-wiring.test.js +475 -475
  409. package/test/segment-store-eviction.test.js +247 -247
  410. package/test/segment-store.test.js +323 -323
  411. package/test/segment-true-start.test.js +86 -86
  412. package/test/segments-are-shared.test.js +176 -176
  413. package/test/send-chunk-bytes.test.js +86 -86
  414. package/test/serving-existing-output.test.js +102 -102
  415. package/test/serving-output.test.js +102 -102
  416. package/test/shared-piece-store.test.js +310 -310
  417. package/test/sidecar-naming.test.js +142 -142
  418. package/test/source-file.test.js +133 -133
  419. package/test/source-files-route.test.js +103 -103
  420. package/test/source-registry.test.js +18 -18
  421. package/test/split-a-stretch.test.js +139 -139
  422. package/test/stale-request-after-seek.test.js +158 -158
  423. package/test/startup-readings.test.js +124 -124
  424. package/test/stream-route.test.js +182 -182
  425. package/test/subtitle-cue-framing.test.js +200 -200
  426. package/test/subtitle-cue-source.test.js +102 -102
  427. package/test/subtitle-cue-walk.test.js +370 -370
  428. package/test/subtitle-cursor.test.js +63 -63
  429. package/test/subtitle-defaults.test.js +118 -118
  430. package/test/subtitle-language.test.js +252 -252
  431. package/test/subtitle-track-numbering.test.js +386 -386
  432. package/test/supply-margin.test.js +177 -177
  433. package/test/swarm-follows-readers.test.js +174 -174
  434. package/test/swarm-reach.test.js +130 -130
  435. package/test/swarm-selection.test.js +220 -220
  436. package/test/tail-duplication.test.js +167 -167
  437. package/test/the-channel-has-both-ends.test.js +91 -91
  438. package/test/the-torrent-thread-serves-bytes.test.js +107 -107
  439. package/test/thread-pool.test.js +53 -53
  440. package/test/timeline.test.js +84 -84
  441. package/test/torrent-contents.test.js +235 -235
  442. package/test/torrent-cost.test.js +71 -71
  443. package/test/torrent-files.test.js +221 -221
  444. package/test/torrent-naming.test.js +255 -255
  445. package/test/tracks-begin-together.test.js +209 -209
  446. package/test/tunnel-renewal.test.js +167 -167
  447. package/test/two-viewers-one-picture.test.js +407 -407
  448. package/test/upload-hurry.test.js +186 -186
  449. package/test/usrsctp-state.test.js +131 -131
  450. package/test/video-facts.test.js +102 -102
  451. package/test/viewer-assignments.test.js +143 -143
  452. package/test/viewer-outputs.test.js +285 -285
  453. package/test/viewer-presence.test.js +116 -116
  454. package/test/viewer-requests-admission.test.js +89 -89
  455. package/test/viewer-states.test.js +189 -189
  456. package/test/viewer-subtitle-subscription.test.js +96 -96
  457. package/test/viewer.test.js +139 -139
  458. package/test/viewing-generations.test.js +390 -390
  459. package/test/visible-rung.test.js +37 -37
  460. package/test/wait-ledger.test.js +100 -100
  461. package/test/webrtc-shim.test.js +149 -149
  462. package/test/wedge-certainty.test.js +131 -131
  463. package/test/withdraw-piece-claim.test.js +212 -212
  464. package/test/worker-channel.test.js +158 -158
  465. package/utils/client-log-file.js +151 -151
  466. package/utils/logger.js +312 -312
  467. package/utils/parse-range.js +25 -25
  468. package/utils/perf.js +59 -59
  469. package/utils/torrent-source-key.js +38 -38
@@ -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).