@torrent-tv/proxy 2.89.7 → 2.89.8

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 (497) 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 +358 -338
  13. package/CHANGELOG.md +2340 -2335
  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/calibration/audio/NOTICE.md +19 -19
  19. package/assets/calibration/audio/manifest.json +98 -98
  20. package/assets/diagnostics/sctpstate.gdb +31 -31
  21. package/bin/cli.js +684 -684
  22. package/bin/thread-pool.js +30 -30
  23. package/biome.json +412 -412
  24. package/docs/browser-proxy-contract.md +155 -155
  25. package/docs/container-architecture.md +273 -273
  26. package/docs/download-architecture.md +261 -261
  27. package/docs/encode-architecture.md +765 -765
  28. package/docs/encode-run-state.md +96 -96
  29. package/docs/logs.md +97 -97
  30. package/docs/playback-start-forecast.md +113 -113
  31. package/docs/socks5-egress-research.md +114 -114
  32. package/docs/storage-architecture.md +273 -273
  33. package/knip.json +14 -14
  34. package/openspec/changes/adaptive-bitrate/design.md +92 -92
  35. package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
  36. package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
  37. package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
  38. package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
  39. package/openspec/changes/binary-distribution/proposal.md +53 -53
  40. package/openspec/changes/chunked-request-bodies/design.md +81 -81
  41. package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
  42. package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
  43. package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
  44. package/openspec/changes/cold-start/design.md +98 -98
  45. package/openspec/changes/cold-start/proposal.md +62 -62
  46. package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
  47. package/openspec/changes/cold-start/tasks.md +48 -48
  48. package/openspec/changes/disk-cap/.openspec.yaml +2 -2
  49. package/openspec/changes/disk-cap/proposal.md +37 -37
  50. package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
  51. package/openspec/changes/disk-cap/tasks.md +19 -19
  52. package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
  53. package/openspec/changes/proxy-observability/design.md +38 -38
  54. package/openspec/changes/proxy-observability/proposal.md +49 -49
  55. package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
  56. package/openspec/changes/proxy-observability/tasks.md +21 -21
  57. package/openspec/changes/seek-debounce/design.md +118 -118
  58. package/openspec/changes/seek-debounce/proposal.md +59 -59
  59. package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
  60. package/openspec/changes/seek-debounce/tasks.md +33 -33
  61. package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
  62. package/openspec/changes/subtitle-language/proposal.md +50 -50
  63. package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
  64. package/openspec/changes/subtitle-language/tasks.md +26 -26
  65. package/openspec/changes/track-selection/.openspec.yaml +2 -2
  66. package/openspec/changes/track-selection/design.md +47 -47
  67. package/openspec/changes/track-selection/proposal.md +45 -45
  68. package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
  69. package/openspec/changes/track-selection/tasks.md +20 -20
  70. package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
  71. package/openspec/changes/transcode-quality/proposal.md +51 -51
  72. package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
  73. package/openspec/changes/transcode-quality/tasks.md +63 -63
  74. package/openspec/config.yaml +38 -38
  75. package/package.json +51 -50
  76. package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
  77. package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
  78. package/research/double-spawn-2026-09-10.md +171 -171
  79. package/research/handover-2026-09-13.md +209 -209
  80. package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
  81. package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
  82. package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
  83. package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
  84. package/routes/api/client-logs/post.js +86 -86
  85. package/routes/api/delivery-sink/get.js +72 -72
  86. package/routes/api/link-probe/get.js +51 -51
  87. package/routes/api/playback-plan/audio-tracks/post.js +29 -29
  88. package/routes/api/playback-plan/post.js +53 -53
  89. package/routes/api/sources/files/get.js +106 -106
  90. package/routes/api/sources/post.js +36 -36
  91. package/routes/api/sources/stats/get.js +132 -132
  92. package/routes/api/sources/warm/post.js +217 -217
  93. package/routes/api/subtitles/get.js +230 -230
  94. package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
  95. package/routes/api/transcode-sessions/net-report/post.js +138 -138
  96. package/routes/api/transcode-sessions/post.js +178 -178
  97. package/routes/api/transcode-sessions/progress/get.js +27 -27
  98. package/routes/api/transcode-sessions/release/post.js +41 -41
  99. package/routes/api/transcode-sessions/seek/post.js +63 -63
  100. package/routes/health/get.js +13 -13
  101. package/routes/healthz/get.js +13 -13
  102. package/routes/stream/get.js +416 -416
  103. package/routes/transcode/audio-file/get.js +64 -64
  104. package/routes/transcode/audio-warm/get.js +95 -95
  105. package/routes/transcode/session-file/get.js +52 -52
  106. package/routes/transcode/variant-file/get.js +95 -95
  107. package/routes/transcode/variant-warm/get.js +84 -84
  108. package/scripts/build-audio-samples.mjs +31 -31
  109. package/scripts/render-run-graph.js +173 -173
  110. package/server.js +748 -748
  111. package/services/encode/CoverageMap.js +428 -428
  112. package/services/encode/CushionReport.js +345 -345
  113. package/services/encode/EncodeAdmission.js +376 -376
  114. package/services/encode/EncodeOrchestrator.js +1192 -1192
  115. package/services/encode/EncodePlan.js +1368 -1368
  116. package/services/encode/EncodeRun.js +36 -12
  117. package/services/encode/Encoder.js +142 -142
  118. package/services/encode/EncoderSelection.js +38 -38
  119. package/services/encode/LocalObservations.js +359 -359
  120. package/services/encode/NvencEncoder.js +50 -50
  121. package/services/encode/OutputOpening.js +784 -784
  122. package/services/encode/OutputTimes.js +579 -579
  123. package/services/encode/QsvEncoder.js +58 -58
  124. package/services/encode/Renditions.js +2283 -2283
  125. package/services/encode/RunClock.js +87 -87
  126. package/services/encode/RunProgress.js +79 -79
  127. package/services/encode/SegmentDemand.js +95 -95
  128. package/services/encode/SoftwareEncoder.js +121 -121
  129. package/services/encode/V4l2m2mEncoder.js +53 -53
  130. package/services/encode/VaapiEncoder.js +66 -66
  131. package/services/encode/args.js +418 -418
  132. package/services/encode/audio-calibration.js +67 -67
  133. package/services/encode/audio-work-rate.js +15 -15
  134. package/services/encode/available-share.js +89 -89
  135. package/services/encode/calibration.js +361 -361
  136. package/services/encode/contention.js +128 -128
  137. package/services/encode/decode-cost-fit.js +363 -363
  138. package/services/encode/encode-exit.js +116 -116
  139. package/services/encode/encode-run-state.js +551 -551
  140. package/services/encode/encoder-readings.js +43 -43
  141. package/services/encode/fingerprint.js +322 -322
  142. package/services/encode/host-load.js +174 -174
  143. package/services/encode/hwaccel.js +1894 -1894
  144. package/services/encode/index.js +9 -9
  145. package/services/encode/learned-median.js +90 -90
  146. package/services/encode/output/EncodedOutput.js +57 -57
  147. package/services/encode/output/Output.js +131 -131
  148. package/services/encode/output/OutputCatalog.js +414 -414
  149. package/services/encode/output/OutputSpec.js +390 -390
  150. package/services/encode/output/Timeline.js +308 -308
  151. package/services/encode/output/cut-grid.js +107 -107
  152. package/services/encode/output/index.js +24 -24
  153. package/services/encode/output/ladder.js +32 -32
  154. package/services/encode/output/playlists.js +284 -284
  155. package/services/encode/output/rates.js +125 -125
  156. package/services/encode/output-key-format.js +21 -21
  157. package/services/encode/quality/EncodeCost.js +1043 -1043
  158. package/services/encode/quality/HostLoad.js +525 -525
  159. package/services/encode/quality/HostTimings.js +249 -249
  160. package/services/encode/quality/OutputQualityState.js +24 -24
  161. package/services/encode/quality/QualityController.js +758 -758
  162. package/services/encode/quality/QualityOffer.js +416 -416
  163. package/services/encode/quality/drain-threat.js +47 -47
  164. package/services/encode/quality/link-budget.js +337 -337
  165. package/services/encode/quality/output-format.js +319 -319
  166. package/services/encode/quality/serving-output.js +216 -216
  167. package/services/encode/quality/visible-rung.js +57 -57
  168. package/services/encode/run-budget.js +97 -97
  169. package/services/encode/run-command.js +757 -757
  170. package/services/encode/run-costs.js +155 -155
  171. package/services/encode/run-inputs.js +53 -53
  172. package/services/encode/segment-formats/fmp4.js +476 -476
  173. package/services/encode/segment-formats/init-compat.js +324 -324
  174. package/services/encode/segment-formats/mpegts.js +134 -134
  175. package/services/encode/start-stop-cost.js +178 -178
  176. package/services/encode/throughput.js +97 -97
  177. package/services/encode/torrent-cost.js +88 -88
  178. package/services/identity/installation-id.js +38 -38
  179. package/services/media/ContainerOrchestrator.js +202 -202
  180. package/services/media/KeyframeTables.js +356 -356
  181. package/services/media/SourceFile.js +298 -298
  182. package/services/media/SubtitleCues.js +592 -592
  183. package/services/media/SubtitleOrchestrator.js +167 -167
  184. package/services/media/audio-inventory.js +299 -299
  185. package/services/media/container/AviContainer.js +268 -268
  186. package/services/media/container/Container.js +632 -632
  187. package/services/media/container/ContainerFactory.js +38 -38
  188. package/services/media/container/KeyframeTable.js +272 -272
  189. package/services/media/container/MatroskaContainer.js +1031 -1031
  190. package/services/media/container/Mp4Container.js +1354 -1354
  191. package/services/media/container/SubtitleFileContainer.js +339 -339
  192. package/services/media/container/ebml-reader.js +159 -159
  193. package/services/media/container/ebml-stream.js +174 -174
  194. package/services/media/container/matroska-clusters.js +1165 -1165
  195. package/services/media/container/unavailable.js +77 -77
  196. package/services/media/ffmpeg-banner.js +260 -260
  197. package/services/media/keyframe-probe.js +157 -157
  198. package/services/media/media-info-probe.js +93 -93
  199. package/services/media/playback-planner.js +931 -931
  200. package/services/media/tracks/AudioTrack.js +143 -143
  201. package/services/media/tracks/ContainerTrack.js +84 -84
  202. package/services/media/tracks/ImageSubtitleTrack.js +19 -19
  203. package/services/media/tracks/SubtitleTrack.js +38 -38
  204. package/services/media/tracks/TextSubtitleTrack.js +295 -295
  205. package/services/media/tracks/VideoTrack.js +33 -33
  206. package/services/media/tracks/audio-bound.js +233 -233
  207. package/services/media/tracks/index.js +15 -15
  208. package/services/media/tracks/language-detect.js +174 -174
  209. package/services/server/OutputLifecycle.js +418 -418
  210. package/services/server/SegmentServing.js +1152 -1152
  211. package/services/server/ViewerRequests.js +785 -785
  212. package/services/server/WaitLedger.js +142 -142
  213. package/services/server/controllers/PlaybackController.js +40 -40
  214. package/services/server/controllers/SubtitleController.js +109 -109
  215. package/services/server/http-responses.js +60 -60
  216. package/services/server/transcode-session-files.js +325 -325
  217. package/services/server/wire-outputs.js +1029 -1029
  218. package/services/storage/Diagnostics.js +202 -202
  219. package/services/storage/MachineBudget.js +235 -235
  220. package/services/storage/allowance.js +107 -107
  221. package/services/storage/core-dumps.js +106 -106
  222. package/services/storage/files/CompletedFiles.js +391 -391
  223. package/services/storage/files/piece-from-whole-file.js +118 -118
  224. package/services/storage/free.js +71 -71
  225. package/services/storage/keep.js +48 -48
  226. package/services/storage/machine-memory.js +56 -56
  227. package/services/storage/memory-report.js +575 -575
  228. package/services/storage/piece-store/piece-disk-store.js +539 -539
  229. package/services/storage/piece-store/piece-lru.js +495 -495
  230. package/services/storage/piece-store/shared-piece-store.js +1937 -1937
  231. package/services/storage/returns.js +103 -103
  232. package/services/storage/segment-store/SegmentStore.js +1294 -1294
  233. package/services/storage/wire.js +175 -175
  234. package/services/torrent/Contents.js +382 -382
  235. package/services/torrent/demand/DemandRegister.js +202 -202
  236. package/services/torrent/demand/Urgency.js +137 -137
  237. package/services/torrent/demand/Window.js +118 -118
  238. package/services/torrent/demand/index.js +8 -8
  239. package/services/torrent/demand/pieces.js +140 -140
  240. package/services/torrent/download/SwarmSelection.js +386 -386
  241. package/services/torrent/download/registry.js +102 -102
  242. package/services/torrent/download/withdraw-claim.js +80 -80
  243. package/services/torrent/episode-naming.js +182 -182
  244. package/services/torrent/files.js +455 -455
  245. package/services/torrent/naming.js +619 -619
  246. package/services/torrent/piece-store-of.js +29 -29
  247. package/services/torrent/supply-margin.js +264 -264
  248. package/services/torrent/torrent-pool.js +2688 -2688
  249. package/services/torrent/worker/background-fill.js +213 -213
  250. package/services/torrent/worker/channel.js +271 -271
  251. package/services/torrent/worker/client.js +836 -836
  252. package/services/torrent/worker/fastest-wires.js +342 -342
  253. package/services/torrent/worker/handle-state.js +23 -23
  254. package/services/torrent/worker/held-bytes.js +231 -231
  255. package/services/torrent/worker/install-webrtc-shim.js +32 -32
  256. package/services/torrent/worker/piece-reader.js +1078 -1078
  257. package/services/torrent/worker/pool-adapter.js +371 -371
  258. package/services/torrent/worker/protocol.js +184 -184
  259. package/services/torrent/worker/resume-warm.js +107 -107
  260. package/services/torrent/worker/webrtc-shim.js +219 -219
  261. package/services/torrent/worker/worker.js +1137 -1137
  262. package/services/transport/data-channel-handler.js +1373 -1373
  263. package/services/transport/delivery-probe.js +842 -842
  264. package/services/transport/health-collector.js +53 -53
  265. package/services/transport/nat-classifier.js +231 -231
  266. package/services/transport/packet-witness.js +808 -808
  267. package/services/transport/port-mapper.js +235 -235
  268. package/services/transport/registry-api.js +81 -81
  269. package/services/transport/tunnel-client.js +447 -447
  270. package/services/transport/usrsctp-state.js +175 -175
  271. package/services/transport/webrtc-manager.js +484 -484
  272. package/services/viewer/Assignments.js +340 -340
  273. package/services/viewer/PriorityMap.js +421 -421
  274. package/services/viewer/PriorityOrchestrator.js +325 -325
  275. package/services/viewer/Viewer.js +691 -691
  276. package/services/viewer/Viewers.js +368 -368
  277. package/services/viewer/active-output.js +37 -37
  278. package/services/viewer/audio-start.js +46 -46
  279. package/services/viewer/buffer-trend.js +81 -81
  280. package/services/viewer/choices.js +647 -647
  281. package/services/viewer/media-time.js +355 -355
  282. package/services/viewer/playback-readiness.js +635 -635
  283. package/services/viewer/positions.js +126 -126
  284. package/services/viewer/report-intake.js +46 -46
  285. package/store/source-registry.js +71 -71
  286. package/test/asset-paths.test.js +28 -28
  287. package/test/audio-buffer-continuity.test.js +77 -77
  288. package/test/audio-calibration.test.js +23 -23
  289. package/test/audio-inventory.test.js +212 -212
  290. package/test/audio-track-bitrate.test.js +40 -40
  291. package/test/auto-quality-step.test.js +762 -762
  292. package/test/available-share.test.js +76 -76
  293. package/test/background-fill.test.js +190 -190
  294. package/test/client-log-file.test.js +110 -110
  295. package/test/coalesce.test.js +39 -39
  296. package/test/completed-files.test.js +115 -115
  297. package/test/concurrent-cost.test.js +148 -148
  298. package/test/container-media-info.test.js +228 -228
  299. package/test/contention.test.js +76 -76
  300. package/test/core-dumps.test.js +57 -57
  301. package/test/coverage-follows-the-disk.test.js +233 -228
  302. package/test/coverage-map.test.js +195 -195
  303. package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
  304. package/test/cut-times-timeline.test.js +65 -65
  305. package/test/cuts-follow-published-grid.test.js +109 -109
  306. package/test/data-channel-frame.test.js +63 -63
  307. package/test/data-channel-viewer-lifecycle.test.js +92 -92
  308. package/test/dead-torrent-handle.test.js +52 -52
  309. package/test/declared-rates.test.js +116 -116
  310. package/test/declared-tracks.test.js +43 -43
  311. package/test/decode-cost-fit.test.js +202 -202
  312. package/test/decode-cost.test.js +475 -475
  313. package/test/delivery-probe.test.js +393 -393
  314. package/test/demand-register.test.js +206 -206
  315. package/test/dht-bootstrap.test.js +72 -72
  316. package/test/drain-threat.test.js +65 -65
  317. package/test/duplicate-add.test.js +48 -48
  318. package/test/encode-admission.test.js +146 -146
  319. package/test/encode-cost.test.js +275 -275
  320. package/test/encode-exit.test.js +111 -111
  321. package/test/encode-orchestrator.test.js +426 -426
  322. package/test/encode-plan-viewers.test.js +725 -725
  323. package/test/encode-plan.test.js +545 -545
  324. package/test/encode-run-state.test.js +314 -314
  325. package/test/encode-run.test.js +20 -0
  326. package/test/encode-slope.test.js +46 -46
  327. package/test/encoded-output.test.js +39 -39
  328. package/test/encoder-kinds.test.js +122 -122
  329. package/test/encoder-readings.test.js +68 -68
  330. package/test/episode-naming.test.js +131 -131
  331. package/test/every-claimant-is-told-its-share.test.js +152 -152
  332. package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
  333. package/test/failed-start-breaker.test.js +202 -202
  334. package/test/fastest-wires.test.js +143 -143
  335. package/test/ffmpeg-stream-counts.test.js +78 -78
  336. package/test/file-edges.test.js +191 -191
  337. package/test/fixtures/audio-frame-boundary.json +1 -1
  338. package/test/fmp4-self-contained.test.js +80 -80
  339. package/test/fmp4-timestamps.test.js +140 -140
  340. package/test/fmp4-track-completeness.test.js +41 -41
  341. package/test/format-keyed-outputs.test.js +78 -78
  342. package/test/health-metrics.test.js +36 -38
  343. package/test/held-read-ownership.test.js +77 -77
  344. package/test/held-request-width.test.js +157 -157
  345. package/test/helpers/encode-run.js +248 -240
  346. package/test/helpers/held-file.js +77 -77
  347. package/test/helpers/manager.js +62 -62
  348. package/test/helpers/matroska-file.js +296 -296
  349. package/test/helpers/output-spec.js +63 -63
  350. package/test/host-load.test.js +68 -68
  351. package/test/index-accuracy-is-about-the-file.test.js +128 -128
  352. package/test/init-compat.test.js +219 -219
  353. package/test/input-lost-quiets-the-plan.test.js +261 -261
  354. package/test/input-unavailable.test.js +48 -48
  355. package/test/installation-id.test.js +39 -39
  356. package/test/keeping-period.test.js +83 -83
  357. package/test/keyframe-index-accuracy.test.js +188 -188
  358. package/test/keyframe-table-budget.test.js +123 -123
  359. package/test/keyframes-belong-to-the-file.test.js +249 -249
  360. package/test/learned-median.test.js +37 -37
  361. package/test/letting-go-asks-who-is-watching.test.js +191 -191
  362. package/test/limit-row-by-area.test.js +60 -60
  363. package/test/link-budget.test.js +111 -111
  364. package/test/link-probe.test.js +56 -56
  365. package/test/local-encoding-data.test.js +273 -273
  366. package/test/logger-repeats.test.js +134 -134
  367. package/test/look-ahead-coverage.test.js +56 -56
  368. package/test/manager-surface.test.js +291 -291
  369. package/test/matroska-cluster-discovery.test.js +323 -323
  370. package/test/matroska-cues-track.test.js +192 -192
  371. package/test/media-time.test.js +62 -62
  372. package/test/memory-budget.test.js +390 -390
  373. package/test/move-cost.test.js +216 -216
  374. package/test/mp4-subtitles.test.js +173 -173
  375. package/test/one-authority.test.js +285 -285
  376. package/test/one-budget.test.js +149 -149
  377. package/test/one-output-one-session.test.js +273 -273
  378. package/test/one-resource-divided.test.js +185 -185
  379. package/test/one-serving-output-per-height.test.js +458 -458
  380. package/test/orchestrator-wired.test.js +207 -207
  381. package/test/output-catalog.test.js +39 -39
  382. package/test/output-shape.test.js +21 -21
  383. package/test/output-spec.test.js +251 -251
  384. package/test/output-speed.test.js +92 -87
  385. package/test/outputs-being-prepared.test.js +40 -40
  386. package/test/packet-witness-ring.test.js +236 -236
  387. package/test/packet-witness.test.js +148 -148
  388. package/test/piece-disk-store.test.js +401 -401
  389. package/test/piece-from-whole-file.test.js +129 -129
  390. package/test/piece-lru.test.js +319 -319
  391. package/test/piece-reader.test.js +162 -162
  392. package/test/piece-selection-offset.test.js +85 -85
  393. package/test/piece-store-eviction.test.js +595 -595
  394. package/test/piece-store-never-refuses.test.js +153 -153
  395. package/test/piece-store-reservations.test.js +403 -403
  396. package/test/piece-store-slow-disk.test.js +159 -159
  397. package/test/piece-tail.test.js +97 -97
  398. package/test/plan-host-timings.test.js +68 -68
  399. package/test/plan-is-steady.test.js +225 -225
  400. package/test/playback-readiness.test.js +518 -518
  401. package/test/playback-start-regressions.test.js +101 -101
  402. package/test/priority-map-download.test.js +174 -174
  403. package/test/priority-map-emptied.test.js +204 -204
  404. package/test/priority-map-per-output.test.js +215 -215
  405. package/test/priority-map.test.js +163 -163
  406. package/test/probe-wedge-certainty.test.js +60 -60
  407. package/test/produced-copy-choice.test.js +213 -213
  408. package/test/published-timeline.test.js +83 -83
  409. package/test/quality-offer.test.js +160 -160
  410. package/test/quality-variants.test.js +1274 -1270
  411. package/test/rate-control-in-the-output.test.js +181 -180
  412. package/test/read-survives-withdrawal.test.js +164 -164
  413. package/test/read-window.test.js +365 -365
  414. package/test/reads-not-yet-arrived.test.js +218 -218
  415. package/test/resume-warm.test.js +39 -39
  416. package/test/run-graph-drift.test.js +37 -37
  417. package/test/run-inputs.test.js +80 -80
  418. package/test/run-intervals.test.js +100 -100
  419. package/test/run-position-follows-published-grid.test.js +89 -89
  420. package/test/run-progress.test.js +64 -64
  421. package/test/same-height-switch.test.js +663 -663
  422. package/test/seek-frees-the-viewers-requests.test.js +217 -217
  423. package/test/seek-landing.test.js +109 -109
  424. package/test/seek-target-not-superseded.test.js +195 -195
  425. package/test/segment-closed-on-both-branches.test.js +114 -114
  426. package/test/segment-cut-times.test.js +63 -63
  427. package/test/segment-demand.test.js +80 -80
  428. package/test/segment-hold-supersede.test.js +91 -91
  429. package/test/segment-judged-by-its-own-header.test.js +222 -222
  430. package/test/segment-serve-wiring.test.js +481 -481
  431. package/test/segment-store-eviction.test.js +247 -247
  432. package/test/segment-store.test.js +352 -352
  433. package/test/segment-true-start.test.js +86 -86
  434. package/test/segments-are-shared.test.js +176 -176
  435. package/test/send-chunk-bytes.test.js +86 -86
  436. package/test/serving-existing-output.test.js +111 -111
  437. package/test/serving-output.test.js +102 -102
  438. package/test/shared-piece-store.test.js +310 -310
  439. package/test/sidecar-naming.test.js +142 -142
  440. package/test/soundtrack-rate-bound.test.js +289 -289
  441. package/test/source-file.test.js +133 -133
  442. package/test/source-files-route.test.js +113 -113
  443. package/test/source-registry.test.js +18 -18
  444. package/test/split-a-stretch.test.js +139 -139
  445. package/test/stale-request-after-seek.test.js +158 -158
  446. package/test/startup-readings.test.js +124 -124
  447. package/test/stream-route.test.js +237 -237
  448. package/test/subtitle-cue-framing.test.js +200 -200
  449. package/test/subtitle-cue-source.test.js +102 -102
  450. package/test/subtitle-cue-walk.test.js +385 -385
  451. package/test/subtitle-cursor.test.js +63 -63
  452. package/test/subtitle-defaults.test.js +118 -118
  453. package/test/subtitle-language.test.js +252 -252
  454. package/test/subtitle-metadata.test.js +15 -15
  455. package/test/subtitle-plan-waits.test.js +125 -125
  456. package/test/subtitle-track-numbering.test.js +391 -391
  457. package/test/supply-margin.test.js +190 -190
  458. package/test/swarm-follows-readers.test.js +174 -174
  459. package/test/swarm-reach.test.js +130 -130
  460. package/test/swarm-selection.test.js +220 -220
  461. package/test/tail-duplication.test.js +167 -167
  462. package/test/the-channel-has-both-ends.test.js +91 -91
  463. package/test/the-torrent-thread-serves-bytes.test.js +107 -107
  464. package/test/thread-pool.test.js +53 -53
  465. package/test/timeline.test.js +84 -84
  466. package/test/torrent-contents.test.js +235 -235
  467. package/test/torrent-cost.test.js +71 -71
  468. package/test/torrent-files.test.js +221 -221
  469. package/test/torrent-naming.test.js +255 -255
  470. package/test/tracks-begin-together.test.js +209 -209
  471. package/test/tunnel-renewal.test.js +167 -167
  472. package/test/two-viewers-one-picture.test.js +508 -508
  473. package/test/upload-hurry.test.js +186 -186
  474. package/test/usrsctp-state.test.js +131 -131
  475. package/test/video-facts.test.js +102 -102
  476. package/test/viewer-assignments.test.js +143 -143
  477. package/test/viewer-outputs.test.js +285 -285
  478. package/test/viewer-presence.test.js +116 -116
  479. package/test/viewer-requests-admission.test.js +92 -92
  480. package/test/viewer-starts-on-map.test.js +27 -27
  481. package/test/viewer-states.test.js +192 -189
  482. package/test/viewer-subtitle-subscription.test.js +96 -96
  483. package/test/viewer.test.js +139 -139
  484. package/test/viewing-generations.test.js +390 -390
  485. package/test/visible-rung.test.js +37 -37
  486. package/test/wait-ledger.test.js +100 -100
  487. package/test/webrtc-shim.test.js +149 -149
  488. package/test/wedge-certainty.test.js +131 -131
  489. package/test/whole-file-held-read.test.js +30 -30
  490. package/test/withdraw-piece-claim.test.js +212 -212
  491. package/test/worker-channel.test.js +158 -158
  492. package/utils/client-log-file.js +151 -151
  493. package/utils/coalesce.js +41 -41
  494. package/utils/logger.js +312 -312
  495. package/utils/parse-range.js +25 -25
  496. package/utils/perf.js +59 -59
  497. 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 session manager 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,113 +1,113 @@
1
- # Playback start forecast
2
-
3
- For resource r, measured service is S_r(t) = rate_r * t. New observations
4
- replace the conditional future rate; polling cannot invent acceleration,
5
- exponential recovery or a new physical measurement.
6
-
7
- Source work uses the file's measured byte density, missing local byte ranges
8
- and the observed torrent download rate. Shared input ranges are downloaded
9
- once. Files on the same torrent share a queue; distinct torrents use distinct
10
- observed services. Local input reading is included in effective processing
11
- speed, rather than charged again as a separate disk or memory delay.
12
-
13
- Each output's remaining processing work subtracts progress only within its
14
- actual live run intervals. Initial video rates use existing codec/size/encoder
15
- calibration. Initial audio rates use short packaged per-codec measurements and
16
- sample work (encoding) or encoded bytes (copying). Actual output progress
17
- replaces these initial rates. That rate is the run's processing speed: film
18
- made over the run's own working time, with the time its input waited for the
19
- swarm and the time it was stopped taken out (`RunClock`, fed by the stream
20
- route, which marks each wait of a run's input read). The download is charged
21
- once, by the source service, and not a second time inside the processing rate. Startup is charged once, separately from processing
22
- throughput, and disappears once actual run progress establishes processing.
23
-
24
- Input arrival and processing overlap. With arrival time D(m), processing rate
25
- e and previous completion C, completion for an interval [a,b] is
26
-
27
- max(C + (b-a)/e, sup_m(D(m) + (b-m)/e)).
28
-
29
- Between service boundaries these functions are linear, so only endpoints are
30
- needed. Client delivery follows production and shares the observed link queue.
31
- Measured client ranges and prepared media establish each track's deadlines.
32
-
33
- The minimum start delay for this conditional trajectory is
34
-
35
- max(0, max_j(completion_j - presentation_deadline_j)).
36
-
37
- This covers the remaining finite movie, including rates below real time. Slow
38
- positive service gives a finite preproduction delay; it is not rejected by a
39
- browser-capacity threshold or a capped search. Observed supply-interruption
40
- coverage is held in prepared proxy media and client media, without requiring
41
- the browser to hold it all.
42
-
43
- Critical review: future rates can change, average byte density is an initial
44
- mapping rather than an exact packet index, initial audio sample-work scaling
45
- does not model every decoder's nonlinear cost, and client receive/append time
46
- is not separately measured. These are approximation limits, not hidden weights.
47
- Actual progress, segment sizes and link observations refine the prediction.
48
- The client countdown uses elapsed local monotonic time; expiration cannot
49
- authorize playback. Only the proxy's readiness and real track coverage can.
50
-
51
- Zero service, missing media facts and a genuine timestamp gap remain explicit
52
- noncomputable states. The UI reports their causes instead of an estimating
53
- placeholder or a fabricated duration.
54
-
55
- ## Continuity is decided in exact time
56
-
57
- The forecast lives in the viewer component (`services/viewer/`), because what
58
- it predicts is how the viewer's browser will play; the request operation
59
- (`ViewerRequests`) is handed it by `wire-outputs.js`.
60
-
61
- 1. **The file states integers.** A piece's media is read as presentation
62
- intervals in the integer ticks of each track's timescale
63
- (`readPresentationRanges`, `services/encode/segment-formats/mp4-boxes.js`).
64
- Intervals are joined there only where they touch or overlap; the format
65
- states what the samples say and nothing about browsers.
66
- 2. **One function places a piece.** The position written into `tfdt` when a
67
- piece is served and the position its coverage is read at both come from
68
- `readTrackEdits`, which converts the movie-timescale edit into track ticks
69
- with one stated rounding. The reader and the writer cannot disagree.
70
- 3. **Seconds from the page are exact fractions.** A buffered range, a position
71
- or a cut time arrives as a JavaScript number, which is a binary fraction;
72
- `services/viewer/media-time.js` converts it to that fraction exactly. No unit
73
- is chosen. A buffered range is widened by `REPORTED_TIME_ERROR`, the bound of
74
- Chromium's three truncations to whole microseconds, so that it denotes every
75
- exact time the report can stand for.
76
- 4. **The join rule holds in every engine.** The Media Source specification
77
- leaves the threshold to the implementation, so a gap counts as joined only
78
- when Chromium, Gecko and WebKit all join it: twice the gap within the two
79
- ranges' longest frames since a keyframe (Gecko's fuzz), and the gap within
80
- 2002/24000 s (WebKit's `timeFudgeFactor`). Chromium's own bound follows from
81
- the first. Old iOS without Media Source plays HLS in a closed player, and
82
- nothing is derived for it.
83
-
84
- Seconds appear again only for rates, schedules and the answer.
85
-
86
- ## A piece is finished when its frames reach its cut
87
-
88
- A closed non-final fragment is reusable only if the end of its last frame, on
89
- every track, reaches its muxer's cut less `SEGMENT_CUT_TIME_DELTA_SECONDS`,
90
- the delta the muxer is configured with. That is a fact of production, so no
91
- browser frame allowance is used to decide completeness. For explicit cuts the
92
- closure channel is CSV: its reference packet end has not lost the fraction of
93
- a movie tick that MP4's empty edit loses. The first reference packet's time is
94
- recovered from that end minus the first piece's exact sample span (the first
95
- CSV start is zero even after a seek), and is added to the relative cut times
96
- handed to FFmpeg. Comparisons use integer microseconds, as FFmpeg's segment
97
- muxer does. A stored piece without this closure reading carries the upper
98
- uncertainty of its empty edit, read from the movie and track timescales.
99
-
100
- The last frame must still reach the cut on every track. No playable media, a
101
- missing full frame, or a failed rename remains a publication failure. Such a
102
- failure stops the run, records that cause and releases its claimed interval;
103
- the existing repeated-start failure limit applies to subsequent attempts. A
104
- refused file cannot stay claimed while the encoder finishes the whole film.
105
-
106
- The encoding decides completeness
107
- (`encode/piece-completeness.js`): `EncodeRuns` judges a closed file before it is
108
- published, and judges a stored piece left by an earlier process when the
109
- forecast asks for it, taking a short one off the disk. The segment store keeps
110
- the bytes and the media intervals read from them and judges nothing. Interrupted
111
- fragments are therefore never served or counted, so the production schedule
112
- treats their media as unfinished work. The final fragment uses its actual media
113
- end rather than the container's approximate duration.
1
+ # Playback start forecast
2
+
3
+ For resource r, measured service is S_r(t) = rate_r * t. New observations
4
+ replace the conditional future rate; polling cannot invent acceleration,
5
+ exponential recovery or a new physical measurement.
6
+
7
+ Source work uses the file's measured byte density, missing local byte ranges
8
+ and the observed torrent download rate. Shared input ranges are downloaded
9
+ once. Files on the same torrent share a queue; distinct torrents use distinct
10
+ observed services. Local input reading is included in effective processing
11
+ speed, rather than charged again as a separate disk or memory delay.
12
+
13
+ Each output's remaining processing work subtracts progress only within its
14
+ actual live run intervals. Initial video rates use existing codec/size/encoder
15
+ calibration. Initial audio rates use short packaged per-codec measurements and
16
+ sample work (encoding) or encoded bytes (copying). Actual output progress
17
+ replaces these initial rates. That rate is the run's processing speed: film
18
+ made over the run's own working time, with the time its input waited for the
19
+ swarm and the time it was stopped taken out (`RunClock`, fed by the stream
20
+ route, which marks each wait of a run's input read). The download is charged
21
+ once, by the source service, and not a second time inside the processing rate. Startup is charged once, separately from processing
22
+ throughput, and disappears once actual run progress establishes processing.
23
+
24
+ Input arrival and processing overlap. With arrival time D(m), processing rate
25
+ e and previous completion C, completion for an interval [a,b] is
26
+
27
+ max(C + (b-a)/e, sup_m(D(m) + (b-m)/e)).
28
+
29
+ Between service boundaries these functions are linear, so only endpoints are
30
+ needed. Client delivery follows production and shares the observed link queue.
31
+ Measured client ranges and prepared media establish each track's deadlines.
32
+
33
+ The minimum start delay for this conditional trajectory is
34
+
35
+ max(0, max_j(completion_j - presentation_deadline_j)).
36
+
37
+ This covers the remaining finite movie, including rates below real time. Slow
38
+ positive service gives a finite preproduction delay; it is not rejected by a
39
+ browser-capacity threshold or a capped search. Observed supply-interruption
40
+ coverage is held in prepared proxy media and client media, without requiring
41
+ the browser to hold it all.
42
+
43
+ Critical review: future rates can change, average byte density is an initial
44
+ mapping rather than an exact packet index, initial audio sample-work scaling
45
+ does not model every decoder's nonlinear cost, and client receive/append time
46
+ is not separately measured. These are approximation limits, not hidden weights.
47
+ Actual progress, segment sizes and link observations refine the prediction.
48
+ The client countdown uses elapsed local monotonic time; expiration cannot
49
+ authorize playback. Only the proxy's readiness and real track coverage can.
50
+
51
+ Zero service, missing media facts and a genuine timestamp gap remain explicit
52
+ noncomputable states. The UI reports their causes instead of an estimating
53
+ placeholder or a fabricated duration.
54
+
55
+ ## Continuity is decided in exact time
56
+
57
+ The forecast lives in the viewer component (`services/viewer/`), because what
58
+ it predicts is how the viewer's browser will play; the request operation
59
+ (`ViewerRequests`) is handed it by `wire-outputs.js`.
60
+
61
+ 1. **The file states integers.** A piece's media is read as presentation
62
+ intervals in the integer ticks of each track's timescale
63
+ (`readPresentationRanges`, `services/encode/segment-formats/mp4-boxes.js`).
64
+ Intervals are joined there only where they touch or overlap; the format
65
+ states what the samples say and nothing about browsers.
66
+ 2. **One function places a piece.** The position written into `tfdt` when a
67
+ piece is served and the position its coverage is read at both come from
68
+ `readTrackEdits`, which converts the movie-timescale edit into track ticks
69
+ with one stated rounding. The reader and the writer cannot disagree.
70
+ 3. **Seconds from the page are exact fractions.** A buffered range, a position
71
+ or a cut time arrives as a JavaScript number, which is a binary fraction;
72
+ `services/viewer/media-time.js` converts it to that fraction exactly. No unit
73
+ is chosen. A buffered range is widened by `REPORTED_TIME_ERROR`, the bound of
74
+ Chromium's three truncations to whole microseconds, so that it denotes every
75
+ exact time the report can stand for.
76
+ 4. **The join rule holds in every engine.** The Media Source specification
77
+ leaves the threshold to the implementation, so a gap counts as joined only
78
+ when Chromium, Gecko and WebKit all join it: twice the gap within the two
79
+ ranges' longest frames since a keyframe (Gecko's fuzz), and the gap within
80
+ 2002/24000 s (WebKit's `timeFudgeFactor`). Chromium's own bound follows from
81
+ the first. Old iOS without Media Source plays HLS in a closed player, and
82
+ nothing is derived for it.
83
+
84
+ Seconds appear again only for rates, schedules and the answer.
85
+
86
+ ## A piece is finished when its frames reach its cut
87
+
88
+ A closed non-final fragment is reusable only if the end of its last frame, on
89
+ every track, reaches its muxer's cut less `SEGMENT_CUT_TIME_DELTA_SECONDS`,
90
+ the delta the muxer is configured with. That is a fact of production, so no
91
+ browser frame allowance is used to decide completeness. For explicit cuts the
92
+ closure channel is CSV: its reference packet end has not lost the fraction of
93
+ a movie tick that MP4's empty edit loses. The first reference packet's time is
94
+ recovered from that end minus the first piece's exact sample span (the first
95
+ CSV start is zero even after a seek), and is added to the relative cut times
96
+ handed to FFmpeg. Comparisons use integer microseconds, as FFmpeg's segment
97
+ muxer does. A stored piece without this closure reading carries the upper
98
+ uncertainty of its empty edit, read from the movie and track timescales.
99
+
100
+ The last frame must still reach the cut on every track. No playable media, a
101
+ missing full frame, or a failed rename remains a publication failure. Such a
102
+ failure stops the run, records that cause and releases its claimed interval;
103
+ the existing repeated-start failure limit applies to subsequent attempts. A
104
+ refused file cannot stay claimed while the encoder finishes the whole film.
105
+
106
+ The encoding decides completeness
107
+ (`encode/piece-completeness.js`): `EncodeRuns` judges a closed file before it is
108
+ published, and judges a stored piece left by an earlier process when the
109
+ forecast asks for it, taking a short one off the disk. The segment store keeps
110
+ the bytes and the media intervals read from them and judges nothing. Interrupted
111
+ fragments are therefore never served or counted, so the production schedule
112
+ treats their media as unfinished work. The final fragment uses its actual media
113
+ end rather than the container's approximate duration.