@torrent-tv/proxy 2.88.4 → 2.88.5

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 (489) 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 +2266 -2259
  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 -0
  19. package/assets/calibration/audio/aac.m4a +0 -0
  20. package/assets/calibration/audio/ac3.ac3 +0 -0
  21. package/assets/calibration/audio/alac.m4a +0 -0
  22. package/assets/calibration/audio/dts.dts +0 -0
  23. package/assets/calibration/audio/eac3.eac3 +0 -0
  24. package/assets/calibration/audio/flac.flac +0 -0
  25. package/assets/calibration/audio/manifest.json +98 -0
  26. package/assets/calibration/audio/mp2.mp2 +0 -0
  27. package/assets/calibration/audio/mp3.mp3 +0 -0
  28. package/assets/calibration/audio/opus.opus +0 -0
  29. package/assets/calibration/audio/pcm.wav +0 -0
  30. package/assets/calibration/audio/truehd.thd +0 -0
  31. package/assets/calibration/audio/vorbis.ogg +0 -0
  32. package/assets/diagnostics/sctpstate.gdb +31 -31
  33. package/bin/cli.js +684 -684
  34. package/bin/thread-pool.js +30 -30
  35. package/biome.json +412 -411
  36. package/docs/browser-proxy-contract.md +155 -152
  37. package/docs/container-architecture.md +218 -218
  38. package/docs/download-architecture.md +261 -261
  39. package/docs/encode-architecture.md +729 -729
  40. package/docs/encode-run-state.md +96 -96
  41. package/docs/logs.md +97 -97
  42. package/docs/playback-start-forecast.md +56 -0
  43. package/docs/socks5-egress-research.md +114 -114
  44. package/docs/storage-architecture.md +267 -267
  45. package/knip.json +14 -14
  46. package/openspec/changes/adaptive-bitrate/design.md +92 -92
  47. package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
  48. package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
  49. package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
  50. package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
  51. package/openspec/changes/binary-distribution/proposal.md +53 -53
  52. package/openspec/changes/chunked-request-bodies/design.md +81 -81
  53. package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
  54. package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
  55. package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
  56. package/openspec/changes/cold-start/design.md +98 -98
  57. package/openspec/changes/cold-start/proposal.md +62 -62
  58. package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
  59. package/openspec/changes/cold-start/tasks.md +48 -48
  60. package/openspec/changes/disk-cap/.openspec.yaml +2 -2
  61. package/openspec/changes/disk-cap/proposal.md +37 -37
  62. package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
  63. package/openspec/changes/disk-cap/tasks.md +19 -19
  64. package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
  65. package/openspec/changes/proxy-observability/design.md +38 -38
  66. package/openspec/changes/proxy-observability/proposal.md +49 -49
  67. package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
  68. package/openspec/changes/proxy-observability/tasks.md +21 -21
  69. package/openspec/changes/seek-debounce/design.md +118 -118
  70. package/openspec/changes/seek-debounce/proposal.md +59 -59
  71. package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
  72. package/openspec/changes/seek-debounce/tasks.md +33 -33
  73. package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
  74. package/openspec/changes/subtitle-language/proposal.md +50 -50
  75. package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
  76. package/openspec/changes/subtitle-language/tasks.md +26 -26
  77. package/openspec/changes/track-selection/.openspec.yaml +2 -2
  78. package/openspec/changes/track-selection/design.md +47 -47
  79. package/openspec/changes/track-selection/proposal.md +45 -45
  80. package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
  81. package/openspec/changes/track-selection/tasks.md +20 -20
  82. package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
  83. package/openspec/changes/transcode-quality/proposal.md +51 -51
  84. package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
  85. package/openspec/changes/transcode-quality/tasks.md +63 -63
  86. package/openspec/config.yaml +38 -38
  87. package/package.json +1 -1
  88. package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
  89. package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
  90. package/research/double-spawn-2026-09-10.md +171 -171
  91. package/research/handover-2026-09-13.md +209 -209
  92. package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
  93. package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
  94. package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
  95. package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
  96. package/routes/api/client-logs/post.js +86 -86
  97. package/routes/api/delivery-sink/get.js +72 -72
  98. package/routes/api/link-probe/get.js +51 -51
  99. package/routes/api/playback-plan/audio-tracks/post.js +29 -29
  100. package/routes/api/playback-plan/post.js +53 -53
  101. package/routes/api/sources/post.js +36 -36
  102. package/routes/api/sources/stats/get.js +132 -132
  103. package/routes/api/sources/warm/post.js +217 -217
  104. package/routes/api/subtitles/get.js +230 -230
  105. package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
  106. package/routes/api/transcode-sessions/net-report/post.js +138 -138
  107. package/routes/api/transcode-sessions/post.js +178 -178
  108. package/routes/api/transcode-sessions/progress/get.js +27 -27
  109. package/routes/api/transcode-sessions/release/post.js +41 -41
  110. package/routes/api/transcode-sessions/seek/post.js +63 -63
  111. package/routes/health/get.js +13 -13
  112. package/routes/healthz/get.js +13 -13
  113. package/routes/stream/get.js +380 -380
  114. package/routes/transcode/audio-file/get.js +64 -64
  115. package/routes/transcode/audio-warm/get.js +95 -95
  116. package/routes/transcode/session-file/get.js +52 -52
  117. package/routes/transcode/variant-file/get.js +95 -95
  118. package/routes/transcode/variant-warm/get.js +84 -84
  119. package/scripts/build-audio-samples.mjs +31 -0
  120. package/scripts/render-run-graph.js +173 -173
  121. package/server.js +730 -725
  122. package/services/encode/CoverageMap.js +428 -428
  123. package/services/encode/CushionReport.js +345 -345
  124. package/services/encode/EncodeAdmission.js +376 -376
  125. package/services/encode/EncodeOrchestrator.js +1217 -1217
  126. package/services/encode/EncodePlan.js +1366 -1366
  127. package/services/encode/EncodeRun.js +813 -813
  128. package/services/encode/EncodeRuns.js +1237 -1235
  129. package/services/encode/Encoder.js +142 -142
  130. package/services/encode/EncoderSelection.js +38 -38
  131. package/services/encode/LocalObservations.js +359 -359
  132. package/services/encode/NvencEncoder.js +50 -50
  133. package/services/encode/OutputOpening.js +783 -783
  134. package/services/encode/OutputTimes.js +579 -579
  135. package/services/encode/QsvEncoder.js +58 -58
  136. package/services/encode/Renditions.js +2282 -2282
  137. package/services/encode/RunProgress.js +84 -84
  138. package/services/encode/SegmentDemand.js +95 -95
  139. package/services/encode/SoftwareEncoder.js +121 -121
  140. package/services/encode/V4l2m2mEncoder.js +53 -53
  141. package/services/encode/VaapiEncoder.js +66 -66
  142. package/services/encode/args.js +418 -418
  143. package/services/encode/audio-calibration.js +67 -0
  144. package/services/encode/audio-work-rate.js +15 -0
  145. package/services/encode/available-share.js +89 -89
  146. package/services/encode/calibration.js +361 -361
  147. package/services/encode/contention.js +128 -128
  148. package/services/encode/decode-cost-fit.js +363 -363
  149. package/services/encode/encode-exit.js +114 -114
  150. package/services/encode/encode-run-state.js +551 -551
  151. package/services/encode/encoder-readings.js +45 -45
  152. package/services/encode/fingerprint.js +322 -322
  153. package/services/encode/host-load.js +174 -174
  154. package/services/encode/hwaccel.js +1894 -1894
  155. package/services/encode/index.js +9 -9
  156. package/services/encode/learned-median.js +90 -90
  157. package/services/encode/output/EncodedOutput.js +57 -57
  158. package/services/encode/output/Output.js +131 -131
  159. package/services/encode/output/OutputCatalog.js +414 -414
  160. package/services/encode/output/OutputSpec.js +390 -390
  161. package/services/encode/output/Timeline.js +308 -308
  162. package/services/encode/output/cut-grid.js +107 -107
  163. package/services/encode/output/index.js +20 -20
  164. package/services/encode/output/ladder.js +32 -32
  165. package/services/encode/output/playlists.js +284 -284
  166. package/services/encode/output/rates.js +125 -125
  167. package/services/encode/output-key-format.js +21 -21
  168. package/services/encode/quality/EncodeCost.js +1167 -1150
  169. package/services/encode/quality/HostLoad.js +525 -525
  170. package/services/encode/quality/HostTimings.js +249 -249
  171. package/services/encode/quality/OutputQualityState.js +28 -28
  172. package/services/encode/quality/QualityController.js +935 -935
  173. package/services/encode/quality/QualityOffer.js +416 -416
  174. package/services/encode/quality/drain-threat.js +47 -47
  175. package/services/encode/quality/link-budget.js +349 -349
  176. package/services/encode/quality/output-format.js +319 -319
  177. package/services/encode/quality/serving-output.js +216 -216
  178. package/services/encode/quality/visible-rung.js +57 -57
  179. package/services/encode/run-budget.js +97 -97
  180. package/services/encode/run-command.js +754 -754
  181. package/services/encode/run-costs.js +155 -155
  182. package/services/encode/run-inputs.js +42 -42
  183. package/services/encode/segment-formats/fmp4.js +458 -456
  184. package/services/encode/segment-formats/index.js +85 -85
  185. package/services/encode/segment-formats/init-compat.js +324 -324
  186. package/services/encode/segment-formats/mp4-boxes.js +12 -1
  187. package/services/encode/segment-formats/mpegts.js +134 -134
  188. package/services/encode/start-stop-cost.js +178 -178
  189. package/services/encode/throughput.js +97 -97
  190. package/services/encode/torrent-cost.js +88 -88
  191. package/services/identity/installation-id.js +38 -38
  192. package/services/media/ContainerOrchestrator.js +149 -149
  193. package/services/media/KeyframeTables.js +316 -316
  194. package/services/media/SourceFile.js +298 -298
  195. package/services/media/SubtitleCues.js +520 -520
  196. package/services/media/SubtitleOrchestrator.js +167 -167
  197. package/services/media/audio-inventory.js +299 -298
  198. package/services/media/container/AviContainer.js +266 -266
  199. package/services/media/container/Container.js +609 -609
  200. package/services/media/container/ContainerFactory.js +56 -56
  201. package/services/media/container/KeyframeTable.js +272 -272
  202. package/services/media/container/MatroskaContainer.js +1494 -1494
  203. package/services/media/container/Mp4Container.js +1344 -1344
  204. package/services/media/container/ebml-reader.js +159 -159
  205. package/services/media/ffmpeg-banner.js +260 -260
  206. package/services/media/keyframe-probe.js +157 -157
  207. package/services/media/media-info-probe.js +93 -93
  208. package/services/media/playback-planner.js +931 -931
  209. package/services/media/tracks/AudioTrack.js +143 -143
  210. package/services/media/tracks/ContainerTrack.js +75 -75
  211. package/services/media/tracks/ImageSubtitleTrack.js +19 -19
  212. package/services/media/tracks/SubtitleTrack.js +38 -38
  213. package/services/media/tracks/TextSubtitleTrack.js +287 -287
  214. package/services/media/tracks/VideoTrack.js +33 -33
  215. package/services/media/tracks/audio-bound.js +233 -233
  216. package/services/media/tracks/index.js +15 -15
  217. package/services/media/tracks/language-detect.js +174 -174
  218. package/services/server/OutputLifecycle.js +418 -418
  219. package/services/server/SegmentServing.js +1150 -1150
  220. package/services/server/ViewerRequests.js +785 -760
  221. package/services/server/WaitLedger.js +142 -142
  222. package/services/server/controllers/PlaybackController.js +40 -40
  223. package/services/server/controllers/SubtitleController.js +109 -109
  224. package/services/server/http-responses.js +60 -60
  225. package/services/server/playback-readiness.js +516 -708
  226. package/services/server/transcode-session-files.js +325 -325
  227. package/services/server/wire-outputs.js +1020 -995
  228. package/services/storage/Diagnostics.js +202 -202
  229. package/services/storage/MachineBudget.js +235 -235
  230. package/services/storage/allowance.js +107 -107
  231. package/services/storage/core-dumps.js +106 -106
  232. package/services/storage/files/CompletedFiles.js +391 -391
  233. package/services/storage/files/piece-from-whole-file.js +118 -118
  234. package/services/storage/free.js +71 -71
  235. package/services/storage/keep.js +48 -48
  236. package/services/storage/machine-memory.js +56 -56
  237. package/services/storage/memory-report.js +575 -575
  238. package/services/storage/piece-store/piece-disk-store.js +539 -539
  239. package/services/storage/piece-store/piece-lru.js +495 -495
  240. package/services/storage/piece-store/shared-piece-store.js +1937 -1931
  241. package/services/storage/returns.js +103 -103
  242. package/services/storage/segment-store/SegmentStore.js +1255 -1240
  243. package/services/storage/wire.js +175 -175
  244. package/services/torrent/demand/DemandRegister.js +202 -202
  245. package/services/torrent/demand/Urgency.js +137 -137
  246. package/services/torrent/demand/Window.js +118 -118
  247. package/services/torrent/demand/index.js +8 -8
  248. package/services/torrent/demand/pieces.js +140 -140
  249. package/services/torrent/download/SwarmSelection.js +386 -386
  250. package/services/torrent/download/registry.js +102 -102
  251. package/services/torrent/download/withdraw-claim.js +80 -80
  252. package/services/torrent/files.js +455 -455
  253. package/services/torrent/naming.js +619 -619
  254. package/services/torrent/piece-store-of.js +29 -29
  255. package/services/torrent/supply-margin.js +262 -262
  256. package/services/torrent/torrent-pool.js +2688 -2675
  257. package/services/torrent/worker/background-fill.js +213 -213
  258. package/services/torrent/worker/channel.js +271 -271
  259. package/services/torrent/worker/client.js +765 -765
  260. package/services/torrent/worker/fastest-wires.js +342 -342
  261. package/services/torrent/worker/handle-state.js +23 -23
  262. package/services/torrent/worker/held-bytes.js +155 -155
  263. package/services/torrent/worker/install-webrtc-shim.js +32 -32
  264. package/services/torrent/worker/piece-reader.js +1078 -1078
  265. package/services/torrent/worker/pool-adapter.js +371 -371
  266. package/services/torrent/worker/protocol.js +184 -184
  267. package/services/torrent/worker/resume-warm.js +107 -107
  268. package/services/torrent/worker/webrtc-shim.js +219 -219
  269. package/services/torrent/worker/worker.js +1129 -1129
  270. package/services/transport/data-channel-handler.js +1369 -1369
  271. package/services/transport/delivery-probe.js +842 -842
  272. package/services/transport/health-collector.js +53 -53
  273. package/services/transport/nat-classifier.js +231 -231
  274. package/services/transport/packet-witness.js +808 -808
  275. package/services/transport/port-mapper.js +235 -235
  276. package/services/transport/registry-api.js +81 -81
  277. package/services/transport/tunnel-client.js +447 -447
  278. package/services/transport/usrsctp-state.js +175 -175
  279. package/services/transport/webrtc-manager.js +484 -484
  280. package/services/viewer/Assignments.js +340 -340
  281. package/services/viewer/PriorityMap.js +390 -390
  282. package/services/viewer/PriorityOrchestrator.js +325 -325
  283. package/services/viewer/Viewer.js +691 -691
  284. package/services/viewer/Viewers.js +368 -368
  285. package/services/viewer/active-output.js +37 -37
  286. package/services/viewer/audio-start.js +46 -46
  287. package/services/viewer/buffer-trend.js +81 -81
  288. package/services/viewer/choices.js +647 -647
  289. package/services/viewer/positions.js +126 -126
  290. package/services/viewer/report-intake.js +46 -46
  291. package/store/source-registry.js +71 -71
  292. package/test/asset-paths.test.js +28 -28
  293. package/test/audio-calibration.test.js +23 -0
  294. package/test/audio-inventory.test.js +212 -212
  295. package/test/audio-track-bitrate.test.js +40 -40
  296. package/test/auto-quality-step.test.js +707 -707
  297. package/test/available-share.test.js +76 -76
  298. package/test/background-fill.test.js +190 -190
  299. package/test/client-log-file.test.js +110 -110
  300. package/test/completed-files.test.js +115 -115
  301. package/test/concurrent-cost.test.js +148 -148
  302. package/test/container-media-info.test.js +228 -228
  303. package/test/contention.test.js +76 -76
  304. package/test/core-dumps.test.js +57 -57
  305. package/test/coverage-follows-the-disk.test.js +228 -228
  306. package/test/coverage-map.test.js +195 -195
  307. package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
  308. package/test/cut-times-timeline.test.js +65 -65
  309. package/test/cuts-follow-published-grid.test.js +109 -109
  310. package/test/data-channel-frame.test.js +63 -63
  311. package/test/data-channel-viewer-lifecycle.test.js +92 -92
  312. package/test/dead-torrent-handle.test.js +52 -52
  313. package/test/declared-rates.test.js +116 -116
  314. package/test/declared-tracks.test.js +43 -43
  315. package/test/decode-cost-fit.test.js +202 -202
  316. package/test/decode-cost.test.js +462 -462
  317. package/test/delivery-probe.test.js +393 -393
  318. package/test/demand-register.test.js +206 -206
  319. package/test/dht-bootstrap.test.js +72 -72
  320. package/test/drain-threat.test.js +65 -65
  321. package/test/duplicate-add.test.js +48 -48
  322. package/test/encode-admission.test.js +146 -146
  323. package/test/encode-cost.test.js +283 -283
  324. package/test/encode-exit.test.js +111 -111
  325. package/test/encode-orchestrator.test.js +419 -419
  326. package/test/encode-plan-viewers.test.js +718 -718
  327. package/test/encode-plan.test.js +545 -545
  328. package/test/encode-run-state.test.js +314 -314
  329. package/test/encode-run.test.js +162 -162
  330. package/test/encode-slope.test.js +46 -46
  331. package/test/encoded-output.test.js +39 -39
  332. package/test/encoder-kinds.test.js +122 -122
  333. package/test/encoder-readings.test.js +46 -46
  334. package/test/every-claimant-is-told-its-share.test.js +152 -152
  335. package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
  336. package/test/failed-start-breaker.test.js +202 -202
  337. package/test/fastest-wires.test.js +143 -143
  338. package/test/ffmpeg-stream-counts.test.js +78 -78
  339. package/test/file-edges.test.js +191 -191
  340. package/test/fmp4-presentation-clock.test.js +162 -147
  341. package/test/fmp4-self-contained.test.js +80 -80
  342. package/test/fmp4-timestamps.test.js +140 -140
  343. package/test/fmp4-track-completeness.test.js +41 -41
  344. package/test/format-keyed-outputs.test.js +78 -78
  345. package/test/health-metrics.test.js +38 -38
  346. package/test/held-request-width.test.js +157 -157
  347. package/test/helpers/encode-run.js +136 -136
  348. package/test/helpers/held-file.js +29 -29
  349. package/test/helpers/manager.js +57 -57
  350. package/test/helpers/output-spec.js +63 -63
  351. package/test/host-load.test.js +68 -68
  352. package/test/index-accuracy-is-about-the-file.test.js +128 -128
  353. package/test/init-compat.test.js +219 -219
  354. package/test/input-lost-quiets-the-plan.test.js +261 -261
  355. package/test/input-unavailable.test.js +48 -48
  356. package/test/installation-id.test.js +39 -39
  357. package/test/keeping-period.test.js +83 -83
  358. package/test/keyframe-index-accuracy.test.js +188 -188
  359. package/test/keyframe-table-budget.test.js +102 -102
  360. package/test/keyframes-belong-to-the-file.test.js +249 -249
  361. package/test/learned-median.test.js +37 -37
  362. package/test/letting-go-asks-who-is-watching.test.js +191 -191
  363. package/test/limit-row-by-area.test.js +60 -60
  364. package/test/link-budget.test.js +112 -112
  365. package/test/link-probe.test.js +56 -56
  366. package/test/local-encoding-data.test.js +273 -273
  367. package/test/logger-repeats.test.js +134 -134
  368. package/test/look-ahead-coverage.test.js +56 -56
  369. package/test/manager-surface.test.js +292 -292
  370. package/test/matroska-cues-track.test.js +192 -192
  371. package/test/memory-budget.test.js +390 -390
  372. package/test/move-cost.test.js +216 -216
  373. package/test/mp4-subtitles.test.js +173 -173
  374. package/test/one-authority.test.js +285 -285
  375. package/test/one-budget.test.js +149 -149
  376. package/test/one-output-one-session.test.js +273 -270
  377. package/test/one-resource-divided.test.js +185 -185
  378. package/test/one-serving-output-per-height.test.js +458 -458
  379. package/test/orchestrator-wired.test.js +206 -206
  380. package/test/output-catalog.test.js +39 -39
  381. package/test/output-shape.test.js +21 -21
  382. package/test/output-spec.test.js +251 -251
  383. package/test/output-speed.test.js +88 -88
  384. package/test/outputs-being-prepared.test.js +40 -40
  385. package/test/packet-witness-ring.test.js +236 -236
  386. package/test/packet-witness.test.js +148 -148
  387. package/test/piece-disk-store.test.js +401 -401
  388. package/test/piece-from-whole-file.test.js +129 -129
  389. package/test/piece-lru.test.js +319 -319
  390. package/test/piece-reader.test.js +162 -162
  391. package/test/piece-selection-offset.test.js +85 -85
  392. package/test/piece-store-eviction.test.js +595 -595
  393. package/test/piece-store-never-refuses.test.js +153 -153
  394. package/test/piece-store-reservations.test.js +403 -403
  395. package/test/piece-store-slow-disk.test.js +159 -159
  396. package/test/piece-tail.test.js +97 -97
  397. package/test/plan-host-timings.test.js +68 -68
  398. package/test/plan-is-steady.test.js +225 -225
  399. package/test/playback-readiness.test.js +483 -453
  400. package/test/playback-start-regressions.test.js +99 -99
  401. package/test/priority-map-download.test.js +174 -174
  402. package/test/priority-map-emptied.test.js +204 -204
  403. package/test/priority-map-per-output.test.js +215 -215
  404. package/test/priority-map.test.js +163 -163
  405. package/test/probe-wedge-certainty.test.js +60 -60
  406. package/test/produced-copy-choice.test.js +213 -213
  407. package/test/published-timeline.test.js +83 -83
  408. package/test/quality-offer.test.js +162 -162
  409. package/test/quality-variants.test.js +1257 -1257
  410. package/test/rate-control-in-the-output.test.js +180 -180
  411. package/test/read-survives-withdrawal.test.js +164 -164
  412. package/test/read-window.test.js +365 -365
  413. package/test/resume-warm.test.js +39 -39
  414. package/test/run-graph-drift.test.js +37 -37
  415. package/test/run-inputs.test.js +80 -80
  416. package/test/run-intervals.test.js +100 -100
  417. package/test/run-position-follows-published-grid.test.js +89 -89
  418. package/test/run-progress.test.js +65 -65
  419. package/test/same-height-switch.test.js +664 -664
  420. package/test/seek-frees-the-viewers-requests.test.js +217 -217
  421. package/test/seek-landing.test.js +109 -109
  422. package/test/seek-target-not-superseded.test.js +195 -195
  423. package/test/segment-closed-on-both-branches.test.js +96 -96
  424. package/test/segment-cut-times.test.js +63 -63
  425. package/test/segment-demand.test.js +80 -80
  426. package/test/segment-hold-supersede.test.js +91 -91
  427. package/test/segment-judged-by-its-own-header.test.js +221 -221
  428. package/test/segment-serve-wiring.test.js +475 -475
  429. package/test/segment-store-eviction.test.js +247 -247
  430. package/test/segment-store.test.js +338 -323
  431. package/test/segment-true-start.test.js +86 -86
  432. package/test/segments-are-shared.test.js +176 -176
  433. package/test/send-chunk-bytes.test.js +86 -86
  434. package/test/serving-existing-output.test.js +105 -105
  435. package/test/serving-output.test.js +102 -102
  436. package/test/shared-piece-store.test.js +310 -310
  437. package/test/sidecar-naming.test.js +142 -142
  438. package/test/soundtrack-rate-bound.test.js +289 -289
  439. package/test/source-file.test.js +133 -133
  440. package/test/source-registry.test.js +18 -18
  441. package/test/split-a-stretch.test.js +139 -139
  442. package/test/stale-request-after-seek.test.js +158 -158
  443. package/test/startup-readings.test.js +124 -124
  444. package/test/stream-route.test.js +209 -209
  445. package/test/subtitle-cue-framing.test.js +200 -200
  446. package/test/subtitle-cue-source.test.js +102 -102
  447. package/test/subtitle-cue-walk.test.js +370 -370
  448. package/test/subtitle-cursor.test.js +63 -63
  449. package/test/subtitle-defaults.test.js +118 -118
  450. package/test/subtitle-language.test.js +252 -252
  451. package/test/subtitle-track-numbering.test.js +386 -386
  452. package/test/supply-margin.test.js +177 -177
  453. package/test/swarm-follows-readers.test.js +174 -174
  454. package/test/swarm-reach.test.js +130 -130
  455. package/test/swarm-selection.test.js +220 -220
  456. package/test/tail-duplication.test.js +167 -167
  457. package/test/the-channel-has-both-ends.test.js +91 -91
  458. package/test/the-torrent-thread-serves-bytes.test.js +107 -107
  459. package/test/thread-pool.test.js +53 -53
  460. package/test/timeline.test.js +84 -84
  461. package/test/torrent-contents.test.js +235 -235
  462. package/test/torrent-cost.test.js +71 -71
  463. package/test/torrent-files.test.js +221 -221
  464. package/test/torrent-naming.test.js +255 -255
  465. package/test/tracks-begin-together.test.js +209 -209
  466. package/test/tunnel-renewal.test.js +167 -167
  467. package/test/two-viewers-one-picture.test.js +504 -504
  468. package/test/upload-hurry.test.js +186 -186
  469. package/test/usrsctp-state.test.js +131 -131
  470. package/test/video-facts.test.js +102 -102
  471. package/test/viewer-assignments.test.js +143 -143
  472. package/test/viewer-outputs.test.js +285 -285
  473. package/test/viewer-presence.test.js +116 -116
  474. package/test/viewer-requests-admission.test.js +89 -89
  475. package/test/viewer-states.test.js +189 -189
  476. package/test/viewer-subtitle-subscription.test.js +96 -96
  477. package/test/viewer.test.js +139 -139
  478. package/test/viewing-generations.test.js +390 -390
  479. package/test/visible-rung.test.js +37 -37
  480. package/test/wait-ledger.test.js +100 -100
  481. package/test/webrtc-shim.test.js +149 -149
  482. package/test/wedge-certainty.test.js +131 -131
  483. package/test/withdraw-piece-claim.test.js +212 -212
  484. package/test/worker-channel.test.js +158 -158
  485. package/utils/client-log-file.js +151 -151
  486. package/utils/logger.js +312 -312
  487. package/utils/parse-range.js +25 -25
  488. package/utils/perf.js +59 -59
  489. package/utils/torrent-source-key.js +38 -38
@@ -1,92 +1,92 @@
1
- # Design: Adaptive bitrate (proxy)
2
-
3
- Read before coding: `services/encode/hwaccel.js` (`chooseSoftwareEncodeSettings`,
4
- the software descriptor's `buildVideoArgs`), `services/hls-session-manager.js`
5
- (the `BUDGET_*` constants, `#chooseEncodeBudget`, the budget interval check
6
- `#enforceRealtimeBudget`, `#classifyTranscodeBound`, `#applyBudgetDownshift`,
7
- session fields around `budgetLadder`), `routes/api/transcode-sessions/`
8
- (route file conventions), `server.js` (route wiring, deps injection).
9
-
10
- ## (a) Caps — constrained CRF
11
-
12
- Nominal H.264 rates by rung height (nearest rung wins for odd heights):
13
-
14
- RUNG_NOMINAL_KBPS = { 1080: 5000, 720: 2800, 480: 1400, 360: 800, 240: 400 }
15
- CAP_MAXRATE_FACTOR = 1.3
16
- CAP_BUFSIZE_FACTOR = 1.5
17
-
18
- In the software descriptor's video args (where `-crf`/`-preset` are emitted),
19
- add for encode height H (the actual encode height after budget/manual
20
- selection, not the source height):
21
-
22
- -maxrate <round(nominal(H) * 1.3)>k
23
- -bufsize <round(nominal(H) * 1.5)>k
24
-
25
- CRF stays the quality driver; the caps only bound peaks (standard
26
- constrained-CRF). Do NOT touch hardware descriptors in this change.
27
-
28
- ## (b) Net report intake
29
-
30
- Route: `POST /api/transcode-sessions/:id/net-report` (data-channel path, so
31
- add the prefix to the allowlist regex in `transport/data-channel-handler.js` if not
32
- covered by the existing `/api/` rule — verify). Body:
33
-
34
- { linkMbps: number, bufferedAheadSec: number }
35
-
36
- Validation: both finite numbers, `linkMbps > 0`, else 400. Unknown session →
37
- 404. On success 204. Handler stores on the session:
38
-
39
- session.netReport = { linkMbps, bufferedAheadSec, at: Date.now() }
40
-
41
- ## (b) Budget-loop trigger
42
-
43
- Constants (near the other BUDGET_ constants):
44
-
45
- LINK_REPORT_FRESH_MS = 30_000 // ignore stale reports
46
- LINK_SAFETY = 0.8 // usable share of reported link
47
- LINK_SLOW_WINDOW_MS = 15_000 // sustained deficit before acting
48
- LINK_LOW_BUFFER_SEC = 10 // only act while the viewer runs dry
49
-
50
- In the periodic budget check, for each active software-transcode session,
51
- alongside the CPU check:
52
-
53
- report fresh (now - at < LINK_REPORT_FRESH_MS)?
54
- manualQuality not set?
55
- observed = observed stream bitrate, Mbit/s — recent produced segment
56
- bytes / segment duration (reuse/extend whatever the CPU path
57
- reads; a rolling average over the last ~5 segments)
58
- deficit = report.linkMbps * LINK_SAFETY < observed
59
- if deficit AND report.bufferedAheadSec < LINK_LOW_BUFFER_SEC:
60
- accumulate slow-time (same pattern as the CPU slow window)
61
- if slow ≥ LINK_SLOW_WINDOW_MS → #applyBudgetDownshift(session,
62
- reason "link") // existing cooldown + step cap + floor apply
63
- else: reset the link slow window
64
-
65
- `#applyBudgetDownshift` is reused as-is except the log line carries the
66
- reason: `budget downshift (link) …` vs the current CPU wording, so the
67
- client-log pipeline can tell them apart in the field.
68
-
69
- No upswitch in v1 (mirrors the CPU budget's conservatism). A downshifted
70
- session stays down until re-opened.
71
-
72
- ## Interactions
73
-
74
- - Caps make `observed` honest: without (a) a complex scene can spike far
75
- above nominal and flap the trigger; with caps observed ≤ maxrate.
76
- - CPU trigger and link trigger share the cooldown inside
77
- `#applyBudgetDownshift` — they cannot double-fire.
78
- - `manualQuality` set → link trigger skipped entirely (user pinned quality
79
- explicitly; the proxy respects it, matching the manual-quality contract).
80
- - Old browsers never POST reports → trigger never fires → behaviour
81
- identical to today plus caps.
82
-
83
- ## Verification
84
-
85
- - Unit: nominal table lookup (exact rungs + nearest for odd heights); ffmpeg
86
- arg assembly contains the caps; trigger logic with a fake clock (fresh vs
87
- stale report, deficit accumulating to a downshift, buffer-high suppresses,
88
- manualQuality suppresses).
89
- - `node --check` on touched files.
90
- - Field (after the server change ships): cellular session shows
91
- `budget downshift (link)` and the stream settles at a rung whose bitrate
92
- fits the link; segment sizes bounded (~maxrate × segDur / 8 max).
1
+ # Design: Adaptive bitrate (proxy)
2
+
3
+ Read before coding: `services/encode/hwaccel.js` (`chooseSoftwareEncodeSettings`,
4
+ the software descriptor's `buildVideoArgs`), `services/hls-session-manager.js`
5
+ (the `BUDGET_*` constants, `#chooseEncodeBudget`, the budget interval check
6
+ `#enforceRealtimeBudget`, `#classifyTranscodeBound`, `#applyBudgetDownshift`,
7
+ session fields around `budgetLadder`), `routes/api/transcode-sessions/`
8
+ (route file conventions), `server.js` (route wiring, deps injection).
9
+
10
+ ## (a) Caps — constrained CRF
11
+
12
+ Nominal H.264 rates by rung height (nearest rung wins for odd heights):
13
+
14
+ RUNG_NOMINAL_KBPS = { 1080: 5000, 720: 2800, 480: 1400, 360: 800, 240: 400 }
15
+ CAP_MAXRATE_FACTOR = 1.3
16
+ CAP_BUFSIZE_FACTOR = 1.5
17
+
18
+ In the software descriptor's video args (where `-crf`/`-preset` are emitted),
19
+ add for encode height H (the actual encode height after budget/manual
20
+ selection, not the source height):
21
+
22
+ -maxrate <round(nominal(H) * 1.3)>k
23
+ -bufsize <round(nominal(H) * 1.5)>k
24
+
25
+ CRF stays the quality driver; the caps only bound peaks (standard
26
+ constrained-CRF). Do NOT touch hardware descriptors in this change.
27
+
28
+ ## (b) Net report intake
29
+
30
+ Route: `POST /api/transcode-sessions/:id/net-report` (data-channel path, so
31
+ add the prefix to the allowlist regex in `transport/data-channel-handler.js` if not
32
+ covered by the existing `/api/` rule — verify). Body:
33
+
34
+ { linkMbps: number, bufferedAheadSec: number }
35
+
36
+ Validation: both finite numbers, `linkMbps > 0`, else 400. Unknown session →
37
+ 404. On success 204. Handler stores on the session:
38
+
39
+ session.netReport = { linkMbps, bufferedAheadSec, at: Date.now() }
40
+
41
+ ## (b) Budget-loop trigger
42
+
43
+ Constants (near the other BUDGET_ constants):
44
+
45
+ LINK_REPORT_FRESH_MS = 30_000 // ignore stale reports
46
+ LINK_SAFETY = 0.8 // usable share of reported link
47
+ LINK_SLOW_WINDOW_MS = 15_000 // sustained deficit before acting
48
+ LINK_LOW_BUFFER_SEC = 10 // only act while the viewer runs dry
49
+
50
+ In the periodic budget check, for each active software-transcode session,
51
+ alongside the CPU check:
52
+
53
+ report fresh (now - at < LINK_REPORT_FRESH_MS)?
54
+ manualQuality not set?
55
+ observed = observed stream bitrate, Mbit/s — recent produced segment
56
+ bytes / segment duration (reuse/extend whatever the CPU path
57
+ reads; a rolling average over the last ~5 segments)
58
+ deficit = report.linkMbps * LINK_SAFETY < observed
59
+ if deficit AND report.bufferedAheadSec < LINK_LOW_BUFFER_SEC:
60
+ accumulate slow-time (same pattern as the CPU slow window)
61
+ if slow ≥ LINK_SLOW_WINDOW_MS → #applyBudgetDownshift(session,
62
+ reason "link") // existing cooldown + step cap + floor apply
63
+ else: reset the link slow window
64
+
65
+ `#applyBudgetDownshift` is reused as-is except the log line carries the
66
+ reason: `budget downshift (link) …` vs the current CPU wording, so the
67
+ client-log pipeline can tell them apart in the field.
68
+
69
+ No upswitch in v1 (mirrors the CPU budget's conservatism). A downshifted
70
+ session stays down until re-opened.
71
+
72
+ ## Interactions
73
+
74
+ - Caps make `observed` honest: without (a) a complex scene can spike far
75
+ above nominal and flap the trigger; with caps observed ≤ maxrate.
76
+ - CPU trigger and link trigger share the cooldown inside
77
+ `#applyBudgetDownshift` — they cannot double-fire.
78
+ - `manualQuality` set → link trigger skipped entirely (user pinned quality
79
+ explicitly; the proxy respects it, matching the manual-quality contract).
80
+ - Old browsers never POST reports → trigger never fires → behaviour
81
+ identical to today plus caps.
82
+
83
+ ## Verification
84
+
85
+ - Unit: nominal table lookup (exact rungs + nearest for odd heights); ffmpeg
86
+ arg assembly contains the caps; trigger logic with a fake clock (fresh vs
87
+ stale report, deficit accumulating to a downshift, buffer-high suppresses,
88
+ manualQuality suppresses).
89
+ - `node --check` on touched files.
90
+ - Field (after the server change ships): cellular session shows
91
+ `budget downshift (link)` and the stream settles at a rung whose bitrate
92
+ fits the link; segment sizes bounded (~maxrate × segDur / 8 max).
@@ -1,56 +1,56 @@
1
- # Proposal: Adaptive bitrate for thin viewer links (proxy side)
2
-
3
- ## Why
4
-
5
- Field evidence (iPhone on cellular, 2026-07-10, session `14cc1017`): encode
6
- bitrate is unbounded — transcoded segments reached 9 MB per 4 s (~18 Mbit/s)
7
- against a measured 1–5.8 Mbit/s cellular link. Result: 45 s prebuffer, buffer
8
- draining during playback (`bottleneck delta=-7.1s`), "было так себе". The
9
- realtime budget protects the PROXY's CPU but nothing protects the VIEWER's
10
- link: quality never adapts to how fast the viewer can actually download.
11
-
12
- ## What Changes
13
-
14
- Two parts, one release:
15
-
16
- - **(a) Bitrate caps (constrained CRF).** Software video encodes gain
17
- `-maxrate`/`-bufsize` sized per resolution rung from a nominal-rate table
18
- (H.264 ladder: 1080p→5000K, 720p→2800K, 480p→1400K, 360p→800K, 240p→400K;
19
- nearest rung by encode height). Multipliers from webtor's production
20
- ladder: `maxrate = 1.3×` nominal, `bufsize = 1.5×` nominal. CRF/preset
21
- selection stays — the caps only bound the peaks that killed the cellular
22
- session. Applies to the software (libx264) path; hardware encoders keep
23
- their current args (follow-up — their rate-control flags differ and any
24
- change must pass the strict startup test on real hardware).
25
- - **(b) Viewer-link downshift trigger.** A new data-channel route lets the
26
- browser report its measured link state (`linkMbps` — rolling median of
27
- per-segment transfer throughput; `bufferedAheadSec`) every ~10 s. The
28
- existing realtime-budget loop gains a SECOND downshift trigger: when
29
- reports are fresh, `manualQuality` is not set, the viewer's link is
30
- sustainedly slower than the produced stream (`linkMbps × 0.8 <` observed
31
- segment bitrate for ≥ 15 s) AND the viewer's buffer is low (< 10 s), step
32
- one rung down via the existing `#applyBudgetDownshift` (same cooldown,
33
- step cap, no upswitch v1). Distinct log reason (`reason=link`) so field
34
- logs distinguish CPU-bound from link-bound downshifts.
35
-
36
- Missing/stale reports change nothing (old clients keep working — the trigger
37
- simply never fires). Manual quality pins win: when `manualQuality` is set the
38
- trigger is skipped.
39
-
40
- ## Capabilities
41
-
42
- ### Modified Capabilities
43
-
44
- - `transcode-quality`: encodes are bitrate-capped per rung; the budget loop
45
- adapts to the viewer's link, not only to the proxy's CPU.
46
-
47
- ## Impact
48
-
49
- - `services/encode/hwaccel.js` — nominal-rate table + caps in the software
50
- descriptor's video args.
51
- - `services/hls-session-manager.js` — accept/store net reports per session;
52
- link trigger in the budget check; log line.
53
- - `routes/api/transcode-sessions/net-report/post.js` (new) + `server.js`
54
- wiring — report intake.
55
- - Proxy release + ha-addon bump. The server-side reporter is a separate
56
- change (`server/viewer-net-report`); release proxy+addon FIRST.
1
+ # Proposal: Adaptive bitrate for thin viewer links (proxy side)
2
+
3
+ ## Why
4
+
5
+ Field evidence (iPhone on cellular, 2026-07-10, session `14cc1017`): encode
6
+ bitrate is unbounded — transcoded segments reached 9 MB per 4 s (~18 Mbit/s)
7
+ against a measured 1–5.8 Mbit/s cellular link. Result: 45 s prebuffer, buffer
8
+ draining during playback (`bottleneck delta=-7.1s`), "было так себе". The
9
+ realtime budget protects the PROXY's CPU but nothing protects the VIEWER's
10
+ link: quality never adapts to how fast the viewer can actually download.
11
+
12
+ ## What Changes
13
+
14
+ Two parts, one release:
15
+
16
+ - **(a) Bitrate caps (constrained CRF).** Software video encodes gain
17
+ `-maxrate`/`-bufsize` sized per resolution rung from a nominal-rate table
18
+ (H.264 ladder: 1080p→5000K, 720p→2800K, 480p→1400K, 360p→800K, 240p→400K;
19
+ nearest rung by encode height). Multipliers from webtor's production
20
+ ladder: `maxrate = 1.3×` nominal, `bufsize = 1.5×` nominal. CRF/preset
21
+ selection stays — the caps only bound the peaks that killed the cellular
22
+ session. Applies to the software (libx264) path; hardware encoders keep
23
+ their current args (follow-up — their rate-control flags differ and any
24
+ change must pass the strict startup test on real hardware).
25
+ - **(b) Viewer-link downshift trigger.** A new data-channel route lets the
26
+ browser report its measured link state (`linkMbps` — rolling median of
27
+ per-segment transfer throughput; `bufferedAheadSec`) every ~10 s. The
28
+ existing realtime-budget loop gains a SECOND downshift trigger: when
29
+ reports are fresh, `manualQuality` is not set, the viewer's link is
30
+ sustainedly slower than the produced stream (`linkMbps × 0.8 <` observed
31
+ segment bitrate for ≥ 15 s) AND the viewer's buffer is low (< 10 s), step
32
+ one rung down via the existing `#applyBudgetDownshift` (same cooldown,
33
+ step cap, no upswitch v1). Distinct log reason (`reason=link`) so field
34
+ logs distinguish CPU-bound from link-bound downshifts.
35
+
36
+ Missing/stale reports change nothing (old clients keep working — the trigger
37
+ simply never fires). Manual quality pins win: when `manualQuality` is set the
38
+ trigger is skipped.
39
+
40
+ ## Capabilities
41
+
42
+ ### Modified Capabilities
43
+
44
+ - `transcode-quality`: encodes are bitrate-capped per rung; the budget loop
45
+ adapts to the viewer's link, not only to the proxy's CPU.
46
+
47
+ ## Impact
48
+
49
+ - `services/encode/hwaccel.js` — nominal-rate table + caps in the software
50
+ descriptor's video args.
51
+ - `services/hls-session-manager.js` — accept/store net reports per session;
52
+ link trigger in the budget check; log line.
53
+ - `routes/api/transcode-sessions/net-report/post.js` (new) + `server.js`
54
+ wiring — report intake.
55
+ - Proxy release + ha-addon bump. The server-side reporter is a separate
56
+ change (`server/viewer-net-report`); release proxy+addon FIRST.
@@ -1,51 +1,51 @@
1
- # adaptive-bitrate — delta spec (proxy)
2
-
3
- ## ADDED Requirements
4
-
5
- ### Requirement: Software encodes are bitrate-capped per resolution rung
6
-
7
- Software video encodes SHALL carry `-maxrate`/`-bufsize` derived from a
8
- per-rung nominal-rate table (1.3× / 1.5× of the rung's nominal), keeping the
9
- existing CRF/preset quality selection (constrained CRF). The cap follows the
10
- ACTUAL encode height after budget or manual selection.
11
-
12
- #### Scenario: Complex scene on a capped encode
13
- - **WHEN** a transcoded scene would spike far above the rung's nominal rate
14
- - **THEN** the produced segments stay bounded by the cap instead of reaching
15
- multi-megabyte sizes that a thin viewer link cannot download in time
16
-
17
- ### Requirement: The proxy accepts viewer link reports
18
-
19
- A data-channel route SHALL accept periodic reports
20
- `{ linkMbps, bufferedAheadSec }` for an active transcode session and store
21
- the latest report with its arrival time. Invalid bodies are rejected;
22
- unknown sessions get 404; missing reports are not an error condition.
23
-
24
- #### Scenario: Old client
25
- - **WHEN** a browser never sends net reports
26
- - **THEN** the session behaves exactly as before this change (plus caps)
27
-
28
- ### Requirement: The budget loop downshifts on a sustained link deficit
29
-
30
- When a fresh report shows the viewer's usable link (reported × safety
31
- margin) sustainedly below the observed produced bitrate AND the viewer's
32
- buffer is low, the existing budget downshift SHALL step the encode one rung
33
- down (same cooldown, step cap and floor as the CPU trigger; no upswitch).
34
- The trigger SHALL be skipped when `manualQuality` is set, when reports are
35
- stale, or when the viewer's buffer is comfortable. The downshift log line
36
- SHALL name the reason (`link`) distinctly from the CPU reason.
37
-
38
- #### Scenario: Cellular viewer, stream too fat
39
- - **WHEN** reports show 3 Mbit/s usable link against 6 Mbit/s produced
40
- bitrate for longer than the slow window, with a draining buffer
41
- - **THEN** the encode steps down one rung and the log shows a link-reason
42
- downshift
43
-
44
- #### Scenario: Slow link but full buffer
45
- - **WHEN** the link is slower than the stream but the viewer's buffer stays
46
- comfortable (e.g. paused playback filling ahead)
47
- - **THEN** no downshift happens
48
-
49
- #### Scenario: Manual quality pinned
50
- - **WHEN** the viewer picked a forced quality
51
- - **THEN** link reports never trigger a downshift
1
+ # adaptive-bitrate — delta spec (proxy)
2
+
3
+ ## ADDED Requirements
4
+
5
+ ### Requirement: Software encodes are bitrate-capped per resolution rung
6
+
7
+ Software video encodes SHALL carry `-maxrate`/`-bufsize` derived from a
8
+ per-rung nominal-rate table (1.3× / 1.5× of the rung's nominal), keeping the
9
+ existing CRF/preset quality selection (constrained CRF). The cap follows the
10
+ ACTUAL encode height after budget or manual selection.
11
+
12
+ #### Scenario: Complex scene on a capped encode
13
+ - **WHEN** a transcoded scene would spike far above the rung's nominal rate
14
+ - **THEN** the produced segments stay bounded by the cap instead of reaching
15
+ multi-megabyte sizes that a thin viewer link cannot download in time
16
+
17
+ ### Requirement: The proxy accepts viewer link reports
18
+
19
+ A data-channel route SHALL accept periodic reports
20
+ `{ linkMbps, bufferedAheadSec }` for an active transcode session and store
21
+ the latest report with its arrival time. Invalid bodies are rejected;
22
+ unknown sessions get 404; missing reports are not an error condition.
23
+
24
+ #### Scenario: Old client
25
+ - **WHEN** a browser never sends net reports
26
+ - **THEN** the session behaves exactly as before this change (plus caps)
27
+
28
+ ### Requirement: The budget loop downshifts on a sustained link deficit
29
+
30
+ When a fresh report shows the viewer's usable link (reported × safety
31
+ margin) sustainedly below the observed produced bitrate AND the viewer's
32
+ buffer is low, the existing budget downshift SHALL step the encode one rung
33
+ down (same cooldown, step cap and floor as the CPU trigger; no upswitch).
34
+ The trigger SHALL be skipped when `manualQuality` is set, when reports are
35
+ stale, or when the viewer's buffer is comfortable. The downshift log line
36
+ SHALL name the reason (`link`) distinctly from the CPU reason.
37
+
38
+ #### Scenario: Cellular viewer, stream too fat
39
+ - **WHEN** reports show 3 Mbit/s usable link against 6 Mbit/s produced
40
+ bitrate for longer than the slow window, with a draining buffer
41
+ - **THEN** the encode steps down one rung and the log shows a link-reason
42
+ downshift
43
+
44
+ #### Scenario: Slow link but full buffer
45
+ - **WHEN** the link is slower than the stream but the viewer's buffer stays
46
+ comfortable (e.g. paused playback filling ahead)
47
+ - **THEN** no downshift happens
48
+
49
+ #### Scenario: Manual quality pinned
50
+ - **WHEN** the viewer picked a forced quality
51
+ - **THEN** link reports never trigger a downshift
@@ -1,40 +1,40 @@
1
- # Tasks: Adaptive bitrate (proxy)
2
-
3
- ## 1. Caps
4
-
5
- - [x] 1.1 `encode/hwaccel.js`: `RUNG_NOMINAL_KBPS` table + nearest-rung lookup;
6
- software descriptor's video args emit `-maxrate`/`-bufsize`
7
- (1.3× / 1.5× nominal for the actual encode height). Hardware
8
- descriptors untouched.
9
-
10
- ## 2. Net report intake
11
-
12
- - [x] 2.1 `routes/api/transcode-sessions/net-report/post.js`: validate
13
- `{ linkMbps, bufferedAheadSec }`, store
14
- `session.netReport = { linkMbps, bufferedAheadSec, at }`, 204/400/404.
15
- - [x] 2.2 Wire in `server.js`; verify the data-channel path allowlist covers
16
- the route.
17
-
18
- ## 3. Budget trigger
19
-
20
- - [x] 3.1 Constants `LINK_REPORT_FRESH_MS`, `LINK_SAFETY`,
21
- `LINK_SLOW_WINDOW_MS`, `LINK_LOW_BUFFER_SEC`.
22
- - [x] 3.2 Observed produced bitrate (rolling, last ~5 segments) available to
23
- the budget check.
24
- - [x] 3.3 Link deficit accumulation + `#applyBudgetDownshift(session,
25
- reason "link")`; skip on manualQuality/stale/comfortable buffer; log
26
- reason distinctly.
27
-
28
- ## 4. Verification
29
-
30
- - [x] 4.1 Unit: table lookup, arg assembly, trigger logic (fake clock):
31
- deficit→downshift, stale report→no-op, high buffer→no-op,
32
- manualQuality→no-op.
33
- - [x] 4.2 `node --check` on touched files.
34
-
35
- ## 5. Release
36
-
37
- - [ ] 5.1 CHANGELOG (next patch) + `npm run patch` (user OTP), then addon
38
- bump + push + HA update. Release BEFORE the server reporter change.
39
- - [ ] 5.2 Field: cellular Poirot/Mavka run shows `budget downshift (link)`
40
- and bounded segment sizes; correlate via client-log pipeline.
1
+ # Tasks: Adaptive bitrate (proxy)
2
+
3
+ ## 1. Caps
4
+
5
+ - [x] 1.1 `encode/hwaccel.js`: `RUNG_NOMINAL_KBPS` table + nearest-rung lookup;
6
+ software descriptor's video args emit `-maxrate`/`-bufsize`
7
+ (1.3× / 1.5× nominal for the actual encode height). Hardware
8
+ descriptors untouched.
9
+
10
+ ## 2. Net report intake
11
+
12
+ - [x] 2.1 `routes/api/transcode-sessions/net-report/post.js`: validate
13
+ `{ linkMbps, bufferedAheadSec }`, store
14
+ `session.netReport = { linkMbps, bufferedAheadSec, at }`, 204/400/404.
15
+ - [x] 2.2 Wire in `server.js`; verify the data-channel path allowlist covers
16
+ the route.
17
+
18
+ ## 3. Budget trigger
19
+
20
+ - [x] 3.1 Constants `LINK_REPORT_FRESH_MS`, `LINK_SAFETY`,
21
+ `LINK_SLOW_WINDOW_MS`, `LINK_LOW_BUFFER_SEC`.
22
+ - [x] 3.2 Observed produced bitrate (rolling, last ~5 segments) available to
23
+ the budget check.
24
+ - [x] 3.3 Link deficit accumulation + `#applyBudgetDownshift(session,
25
+ reason "link")`; skip on manualQuality/stale/comfortable buffer; log
26
+ reason distinctly.
27
+
28
+ ## 4. Verification
29
+
30
+ - [x] 4.1 Unit: table lookup, arg assembly, trigger logic (fake clock):
31
+ deficit→downshift, stale report→no-op, high buffer→no-op,
32
+ manualQuality→no-op.
33
+ - [x] 4.2 `node --check` on touched files.
34
+
35
+ ## 5. Release
36
+
37
+ - [ ] 5.1 CHANGELOG (next patch) + `npm run patch` (user OTP), then addon
38
+ bump + push + HA update. Release BEFORE the server reporter change.
39
+ - [ ] 5.2 Field: cellular Poirot/Mavka run shows `budget downshift (link)`
40
+ and bounded segment sizes; correlate via client-log pipeline.
@@ -1,2 +1,2 @@
1
- schema: spec-driven
2
- created: 2026-07-06
1
+ schema: spec-driven
2
+ created: 2026-07-06
@@ -1,53 +1,53 @@
1
- # Proposal: Proxy as a single binary + service registration (research, queued)
2
-
3
- ## Why
4
-
5
- Bare-npm installation assumes Node.js on the host — a real barrier for the
6
- non-technical proxy owners the pool model targets. Node's Single Executable
7
- Applications (SEA, stable-ish since Node 20/21) allow shipping the proxy as
8
- one self-contained binary per platform (win/linux/macOS, x64/arm64): download,
9
- run, done. A binary can also offer — with the user's explicit consent — to
10
- register itself as an auto-starting service, closing the "survives reboot"
11
- gap that the HA addon solves today.
12
-
13
- ## What Changes (research scope first)
14
-
15
- - **SEA build pipeline**: `node --experimental-sea-config` + postject-injected
16
- blob, one artifact per platform/arch, published as GitHub release assets
17
- alongside the npm package.
18
- - **Known hard parts to resolve in research**:
19
- - Native addons (`node-datachannel`, `utp-native`) cannot live inside the
20
- SEA blob — they must ship next to the binary or self-extract on first
21
- run to a data dir.
22
- - `ffmpeg-static`'s binary likewise ships alongside (or the system ffmpeg
23
- is required, as the HA addon already does with `--ffmpeg-bin`).
24
- - **Service registration (opt-in, explicit user action)**:
25
- - Linux: `proxy install-service` writes a systemd unit and runs
26
- `systemctl enable --now` (requires sudo — the consent step).
27
- - Windows: register via `sc create` / a service wrapper (admin prompt =
28
- consent).
29
- - macOS: launchd plist in `~/Library/LaunchAgents` (user-level, no admin).
30
- - Uninstall counterpart mandatory (`proxy uninstall-service`).
31
- - Stays deployment-agnostic: the binary is a fourth distribution channel
32
- next to HA addon, bare npm and Docker; no code paths may assume it.
33
-
34
- ## Capabilities
35
-
36
- ### New Capabilities
37
-
38
- - `distribution`: how the proxy is packaged and installed on bare hosts.
39
-
40
- ### Modified Capabilities
41
-
42
- <!-- none -->
43
-
44
- ## Impact
45
-
46
- - Build/release tooling in this repo; a `service` CLI subcommand.
47
- - No changes to runtime behaviour for existing channels.
48
-
49
- ## Priority
50
-
51
- MINIMAL — explicitly NOT part of the POC (owner decision, 2026-07-07).
52
- Research for the eventual "pool of non-technical owners" product goal; do
53
- not pick up while any POC work remains.
1
+ # Proposal: Proxy as a single binary + service registration (research, queued)
2
+
3
+ ## Why
4
+
5
+ Bare-npm installation assumes Node.js on the host — a real barrier for the
6
+ non-technical proxy owners the pool model targets. Node's Single Executable
7
+ Applications (SEA, stable-ish since Node 20/21) allow shipping the proxy as
8
+ one self-contained binary per platform (win/linux/macOS, x64/arm64): download,
9
+ run, done. A binary can also offer — with the user's explicit consent — to
10
+ register itself as an auto-starting service, closing the "survives reboot"
11
+ gap that the HA addon solves today.
12
+
13
+ ## What Changes (research scope first)
14
+
15
+ - **SEA build pipeline**: `node --experimental-sea-config` + postject-injected
16
+ blob, one artifact per platform/arch, published as GitHub release assets
17
+ alongside the npm package.
18
+ - **Known hard parts to resolve in research**:
19
+ - Native addons (`node-datachannel`, `utp-native`) cannot live inside the
20
+ SEA blob — they must ship next to the binary or self-extract on first
21
+ run to a data dir.
22
+ - `ffmpeg-static`'s binary likewise ships alongside (or the system ffmpeg
23
+ is required, as the HA addon already does with `--ffmpeg-bin`).
24
+ - **Service registration (opt-in, explicit user action)**:
25
+ - Linux: `proxy install-service` writes a systemd unit and runs
26
+ `systemctl enable --now` (requires sudo — the consent step).
27
+ - Windows: register via `sc create` / a service wrapper (admin prompt =
28
+ consent).
29
+ - macOS: launchd plist in `~/Library/LaunchAgents` (user-level, no admin).
30
+ - Uninstall counterpart mandatory (`proxy uninstall-service`).
31
+ - Stays deployment-agnostic: the binary is a fourth distribution channel
32
+ next to HA addon, bare npm and Docker; no code paths may assume it.
33
+
34
+ ## Capabilities
35
+
36
+ ### New Capabilities
37
+
38
+ - `distribution`: how the proxy is packaged and installed on bare hosts.
39
+
40
+ ### Modified Capabilities
41
+
42
+ <!-- none -->
43
+
44
+ ## Impact
45
+
46
+ - Build/release tooling in this repo; a `service` CLI subcommand.
47
+ - No changes to runtime behaviour for existing channels.
48
+
49
+ ## Priority
50
+
51
+ MINIMAL — explicitly NOT part of the POC (owner decision, 2026-07-07).
52
+ Research for the eventual "pool of non-technical owners" product goal; do
53
+ not pick up while any POC work remains.