@torrent-tv/proxy 2.89.5 → 2.89.7

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 (447) 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 +338 -338
  13. package/CHANGELOG.md +16 -0
  14. package/Dockerfile +30 -30
  15. package/LICENSE +16 -16
  16. package/README.md +369 -369
  17. package/assets/calibration/NOTICE.md +100 -100
  18. package/assets/diagnostics/sctpstate.gdb +31 -31
  19. package/bin/cli.js +684 -684
  20. package/bin/thread-pool.js +30 -30
  21. package/docs/download-architecture.md +261 -261
  22. package/docs/encode-architecture.md +765 -765
  23. package/docs/encode-run-state.md +96 -96
  24. package/docs/logs.md +97 -97
  25. package/docs/playback-start-forecast.md +17 -2
  26. package/docs/socks5-egress-research.md +114 -114
  27. package/docs/storage-architecture.md +273 -273
  28. package/knip.json +14 -14
  29. package/openspec/changes/adaptive-bitrate/design.md +92 -92
  30. package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
  31. package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
  32. package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
  33. package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
  34. package/openspec/changes/binary-distribution/proposal.md +53 -53
  35. package/openspec/changes/chunked-request-bodies/design.md +81 -81
  36. package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
  37. package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
  38. package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
  39. package/openspec/changes/cold-start/design.md +98 -98
  40. package/openspec/changes/cold-start/proposal.md +62 -62
  41. package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
  42. package/openspec/changes/cold-start/tasks.md +48 -48
  43. package/openspec/changes/disk-cap/.openspec.yaml +2 -2
  44. package/openspec/changes/disk-cap/proposal.md +37 -37
  45. package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
  46. package/openspec/changes/disk-cap/tasks.md +19 -19
  47. package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
  48. package/openspec/changes/proxy-observability/design.md +38 -38
  49. package/openspec/changes/proxy-observability/proposal.md +49 -49
  50. package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
  51. package/openspec/changes/proxy-observability/tasks.md +21 -21
  52. package/openspec/changes/seek-debounce/design.md +118 -118
  53. package/openspec/changes/seek-debounce/proposal.md +59 -59
  54. package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
  55. package/openspec/changes/seek-debounce/tasks.md +33 -33
  56. package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
  57. package/openspec/changes/subtitle-language/proposal.md +50 -50
  58. package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
  59. package/openspec/changes/subtitle-language/tasks.md +26 -26
  60. package/openspec/changes/track-selection/.openspec.yaml +2 -2
  61. package/openspec/changes/track-selection/design.md +47 -47
  62. package/openspec/changes/track-selection/proposal.md +45 -45
  63. package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
  64. package/openspec/changes/track-selection/tasks.md +20 -20
  65. package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
  66. package/openspec/changes/transcode-quality/proposal.md +51 -51
  67. package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
  68. package/openspec/changes/transcode-quality/tasks.md +63 -63
  69. package/openspec/config.yaml +38 -38
  70. package/package.json +1 -1
  71. package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
  72. package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
  73. package/research/double-spawn-2026-09-10.md +171 -171
  74. package/research/handover-2026-09-13.md +209 -209
  75. package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
  76. package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
  77. package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
  78. package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
  79. package/routes/api/client-logs/post.js +86 -86
  80. package/routes/api/delivery-sink/get.js +72 -72
  81. package/routes/api/link-probe/get.js +51 -51
  82. package/routes/api/playback-plan/audio-tracks/post.js +29 -29
  83. package/routes/api/playback-plan/post.js +53 -53
  84. package/routes/api/sources/post.js +36 -36
  85. package/routes/api/sources/stats/get.js +132 -132
  86. package/routes/api/sources/warm/post.js +217 -217
  87. package/routes/api/subtitles/get.js +230 -230
  88. package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
  89. package/routes/api/transcode-sessions/net-report/post.js +138 -138
  90. package/routes/api/transcode-sessions/post.js +178 -178
  91. package/routes/api/transcode-sessions/progress/get.js +27 -27
  92. package/routes/api/transcode-sessions/release/post.js +41 -41
  93. package/routes/api/transcode-sessions/seek/post.js +63 -63
  94. package/routes/health/get.js +13 -13
  95. package/routes/healthz/get.js +13 -13
  96. package/routes/stream/get.js +416 -416
  97. package/routes/transcode/audio-file/get.js +64 -64
  98. package/routes/transcode/audio-warm/get.js +95 -95
  99. package/routes/transcode/session-file/get.js +52 -52
  100. package/routes/transcode/variant-file/get.js +95 -95
  101. package/routes/transcode/variant-warm/get.js +84 -84
  102. package/scripts/render-run-graph.js +173 -173
  103. package/server.js +748 -748
  104. package/services/encode/CoverageMap.js +428 -428
  105. package/services/encode/CushionReport.js +345 -345
  106. package/services/encode/EncodeAdmission.js +376 -376
  107. package/services/encode/EncodeOrchestrator.js +1192 -1192
  108. package/services/encode/EncodePlan.js +1368 -1368
  109. package/services/encode/EncodeRun.js +32 -4
  110. package/services/encode/EncodeRuns.js +19 -7
  111. package/services/encode/Encoder.js +142 -142
  112. package/services/encode/EncoderSelection.js +38 -38
  113. package/services/encode/LocalObservations.js +359 -359
  114. package/services/encode/NvencEncoder.js +50 -50
  115. package/services/encode/OutputTimes.js +579 -579
  116. package/services/encode/QsvEncoder.js +58 -58
  117. package/services/encode/Renditions.js +2283 -2283
  118. package/services/encode/RunClock.js +87 -87
  119. package/services/encode/RunProgress.js +79 -79
  120. package/services/encode/SegmentDemand.js +95 -95
  121. package/services/encode/SoftwareEncoder.js +121 -121
  122. package/services/encode/V4l2m2mEncoder.js +53 -53
  123. package/services/encode/VaapiEncoder.js +66 -66
  124. package/services/encode/args.js +418 -418
  125. package/services/encode/available-share.js +89 -89
  126. package/services/encode/calibration.js +361 -361
  127. package/services/encode/contention.js +128 -128
  128. package/services/encode/decode-cost-fit.js +363 -363
  129. package/services/encode/encode-exit.js +116 -114
  130. package/services/encode/encode-run-state.js +551 -551
  131. package/services/encode/encoder-readings.js +43 -43
  132. package/services/encode/fingerprint.js +322 -322
  133. package/services/encode/host-load.js +174 -174
  134. package/services/encode/index.js +9 -9
  135. package/services/encode/learned-median.js +90 -90
  136. package/services/encode/output/EncodedOutput.js +57 -57
  137. package/services/encode/output/Output.js +131 -131
  138. package/services/encode/output/OutputCatalog.js +414 -414
  139. package/services/encode/output/OutputSpec.js +390 -390
  140. package/services/encode/output/Timeline.js +308 -308
  141. package/services/encode/output/cut-grid.js +107 -107
  142. package/services/encode/output/ladder.js +32 -32
  143. package/services/encode/output/playlists.js +284 -284
  144. package/services/encode/output/rates.js +125 -125
  145. package/services/encode/output-key-format.js +21 -21
  146. package/services/encode/piece-completeness.js +60 -2
  147. package/services/encode/quality/EncodeCost.js +1043 -1043
  148. package/services/encode/quality/HostLoad.js +525 -525
  149. package/services/encode/quality/HostTimings.js +249 -249
  150. package/services/encode/quality/OutputQualityState.js +24 -24
  151. package/services/encode/quality/QualityController.js +758 -758
  152. package/services/encode/quality/QualityOffer.js +416 -416
  153. package/services/encode/quality/drain-threat.js +47 -47
  154. package/services/encode/quality/link-budget.js +337 -337
  155. package/services/encode/quality/output-format.js +319 -319
  156. package/services/encode/quality/serving-output.js +216 -216
  157. package/services/encode/quality/visible-rung.js +57 -57
  158. package/services/encode/run-budget.js +97 -97
  159. package/services/encode/run-command.js +3 -1
  160. package/services/encode/run-costs.js +155 -155
  161. package/services/encode/run-inputs.js +53 -53
  162. package/services/encode/segment-formats/fmp4.js +476 -476
  163. package/services/encode/segment-formats/index.js +1 -0
  164. package/services/encode/segment-formats/init-compat.js +324 -324
  165. package/services/encode/segment-formats/mp4-boxes.js +8 -1
  166. package/services/encode/segment-formats/mpegts.js +134 -134
  167. package/services/encode/start-stop-cost.js +178 -178
  168. package/services/encode/throughput.js +97 -97
  169. package/services/encode/torrent-cost.js +88 -88
  170. package/services/identity/installation-id.js +38 -38
  171. package/services/media/SourceFile.js +298 -298
  172. package/services/media/SubtitleOrchestrator.js +167 -167
  173. package/services/media/container/KeyframeTable.js +272 -272
  174. package/services/media/container/ebml-reader.js +159 -159
  175. package/services/media/ffmpeg-banner.js +260 -260
  176. package/services/media/keyframe-probe.js +157 -157
  177. package/services/media/media-info-probe.js +93 -93
  178. package/services/media/playback-planner.js +931 -931
  179. package/services/media/tracks/AudioTrack.js +143 -143
  180. package/services/media/tracks/ImageSubtitleTrack.js +19 -19
  181. package/services/media/tracks/SubtitleTrack.js +38 -38
  182. package/services/media/tracks/VideoTrack.js +33 -33
  183. package/services/media/tracks/audio-bound.js +233 -233
  184. package/services/media/tracks/index.js +15 -15
  185. package/services/media/tracks/language-detect.js +174 -174
  186. package/services/server/OutputLifecycle.js +418 -418
  187. package/services/server/SegmentServing.js +1152 -1152
  188. package/services/server/ViewerRequests.js +785 -785
  189. package/services/server/WaitLedger.js +142 -142
  190. package/services/server/controllers/PlaybackController.js +40 -40
  191. package/services/server/controllers/SubtitleController.js +109 -109
  192. package/services/server/http-responses.js +60 -60
  193. package/services/server/transcode-session-files.js +325 -325
  194. package/services/server/wire-outputs.js +1029 -1029
  195. package/services/storage/Diagnostics.js +202 -202
  196. package/services/storage/MachineBudget.js +235 -235
  197. package/services/storage/allowance.js +107 -107
  198. package/services/storage/core-dumps.js +106 -106
  199. package/services/storage/files/CompletedFiles.js +391 -391
  200. package/services/storage/files/piece-from-whole-file.js +118 -118
  201. package/services/storage/free.js +71 -71
  202. package/services/storage/keep.js +48 -48
  203. package/services/storage/machine-memory.js +56 -56
  204. package/services/storage/memory-report.js +575 -575
  205. package/services/storage/piece-store/piece-disk-store.js +539 -539
  206. package/services/storage/piece-store/piece-lru.js +495 -495
  207. package/services/storage/returns.js +103 -103
  208. package/services/storage/wire.js +175 -175
  209. package/services/torrent/demand/DemandRegister.js +202 -202
  210. package/services/torrent/demand/Urgency.js +137 -137
  211. package/services/torrent/demand/Window.js +118 -118
  212. package/services/torrent/demand/index.js +8 -8
  213. package/services/torrent/demand/pieces.js +140 -140
  214. package/services/torrent/download/SwarmSelection.js +386 -386
  215. package/services/torrent/download/registry.js +102 -102
  216. package/services/torrent/download/withdraw-claim.js +80 -80
  217. package/services/torrent/files.js +455 -455
  218. package/services/torrent/naming.js +619 -619
  219. package/services/torrent/piece-store-of.js +29 -29
  220. package/services/torrent/supply-margin.js +264 -264
  221. package/services/torrent/worker/background-fill.js +213 -213
  222. package/services/torrent/worker/channel.js +271 -271
  223. package/services/torrent/worker/fastest-wires.js +342 -342
  224. package/services/torrent/worker/handle-state.js +23 -23
  225. package/services/torrent/worker/install-webrtc-shim.js +32 -32
  226. package/services/torrent/worker/piece-reader.js +1078 -1078
  227. package/services/torrent/worker/pool-adapter.js +371 -371
  228. package/services/torrent/worker/protocol.js +184 -184
  229. package/services/torrent/worker/resume-warm.js +107 -107
  230. package/services/torrent/worker/webrtc-shim.js +219 -219
  231. package/services/transport/delivery-probe.js +842 -842
  232. package/services/transport/health-collector.js +53 -53
  233. package/services/transport/nat-classifier.js +231 -231
  234. package/services/transport/packet-witness.js +808 -808
  235. package/services/transport/port-mapper.js +235 -235
  236. package/services/transport/registry-api.js +81 -81
  237. package/services/transport/tunnel-client.js +447 -447
  238. package/services/transport/usrsctp-state.js +175 -175
  239. package/services/transport/webrtc-manager.js +484 -484
  240. package/services/viewer/Assignments.js +340 -340
  241. package/services/viewer/PriorityOrchestrator.js +325 -325
  242. package/services/viewer/Viewer.js +691 -691
  243. package/services/viewer/Viewers.js +368 -368
  244. package/services/viewer/active-output.js +37 -37
  245. package/services/viewer/audio-start.js +46 -46
  246. package/services/viewer/buffer-trend.js +81 -81
  247. package/services/viewer/choices.js +647 -647
  248. package/services/viewer/media-time.js +355 -355
  249. package/services/viewer/playback-readiness.js +635 -635
  250. package/services/viewer/positions.js +126 -126
  251. package/services/viewer/report-intake.js +46 -46
  252. package/store/source-registry.js +71 -71
  253. package/test/asset-paths.test.js +28 -28
  254. package/test/audio-buffer-continuity.test.js +77 -77
  255. package/test/audio-inventory.test.js +212 -212
  256. package/test/audio-track-bitrate.test.js +40 -40
  257. package/test/auto-quality-step.test.js +762 -753
  258. package/test/available-share.test.js +76 -76
  259. package/test/background-fill.test.js +190 -190
  260. package/test/client-log-file.test.js +110 -110
  261. package/test/completed-files.test.js +115 -115
  262. package/test/concurrent-cost.test.js +148 -148
  263. package/test/container-media-info.test.js +228 -228
  264. package/test/contention.test.js +76 -76
  265. package/test/core-dumps.test.js +57 -57
  266. package/test/coverage-follows-the-disk.test.js +228 -228
  267. package/test/coverage-map.test.js +195 -195
  268. package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
  269. package/test/cut-times-timeline.test.js +65 -65
  270. package/test/cuts-follow-published-grid.test.js +109 -109
  271. package/test/data-channel-frame.test.js +63 -63
  272. package/test/data-channel-viewer-lifecycle.test.js +92 -92
  273. package/test/dead-torrent-handle.test.js +52 -52
  274. package/test/declared-rates.test.js +116 -116
  275. package/test/declared-tracks.test.js +43 -43
  276. package/test/decode-cost-fit.test.js +202 -202
  277. package/test/decode-cost.test.js +475 -462
  278. package/test/delivery-probe.test.js +393 -393
  279. package/test/demand-register.test.js +206 -206
  280. package/test/dht-bootstrap.test.js +72 -72
  281. package/test/drain-threat.test.js +65 -65
  282. package/test/duplicate-add.test.js +48 -48
  283. package/test/encode-admission.test.js +146 -146
  284. package/test/encode-cost.test.js +275 -275
  285. package/test/encode-exit.test.js +111 -111
  286. package/test/encode-orchestrator.test.js +426 -426
  287. package/test/encode-plan-viewers.test.js +725 -725
  288. package/test/encode-plan.test.js +545 -545
  289. package/test/encode-run-state.test.js +314 -314
  290. package/test/encode-run.test.js +38 -0
  291. package/test/encode-slope.test.js +46 -46
  292. package/test/encoded-output.test.js +39 -39
  293. package/test/encoder-kinds.test.js +122 -122
  294. package/test/encoder-readings.test.js +68 -68
  295. package/test/every-claimant-is-told-its-share.test.js +152 -152
  296. package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
  297. package/test/failed-start-breaker.test.js +202 -202
  298. package/test/fastest-wires.test.js +143 -143
  299. package/test/ffmpeg-stream-counts.test.js +78 -78
  300. package/test/file-edges.test.js +191 -191
  301. package/test/fixtures/refused-audio-20261002.json +10 -0
  302. package/test/fmp4-presentation-clock.test.js +4 -2
  303. package/test/fmp4-self-contained.test.js +80 -80
  304. package/test/fmp4-timestamps.test.js +140 -140
  305. package/test/fmp4-track-completeness.test.js +41 -41
  306. package/test/format-keyed-outputs.test.js +78 -78
  307. package/test/health-metrics.test.js +38 -38
  308. package/test/held-request-width.test.js +157 -157
  309. package/test/helpers/encode-run.js +240 -240
  310. package/test/helpers/manager.js +62 -57
  311. package/test/helpers/output-spec.js +63 -63
  312. package/test/host-load.test.js +68 -68
  313. package/test/index-accuracy-is-about-the-file.test.js +128 -128
  314. package/test/init-compat.test.js +219 -219
  315. package/test/input-lost-quiets-the-plan.test.js +261 -261
  316. package/test/input-unavailable.test.js +48 -48
  317. package/test/installation-id.test.js +39 -39
  318. package/test/keeping-period.test.js +83 -83
  319. package/test/keyframe-index-accuracy.test.js +188 -188
  320. package/test/keyframes-belong-to-the-file.test.js +249 -249
  321. package/test/learned-median.test.js +37 -37
  322. package/test/letting-go-asks-who-is-watching.test.js +191 -191
  323. package/test/limit-row-by-area.test.js +60 -60
  324. package/test/link-budget.test.js +111 -111
  325. package/test/link-probe.test.js +56 -56
  326. package/test/local-encoding-data.test.js +273 -273
  327. package/test/logger-repeats.test.js +134 -134
  328. package/test/look-ahead-coverage.test.js +56 -56
  329. package/test/manager-surface.test.js +291 -291
  330. package/test/matroska-cues-track.test.js +192 -192
  331. package/test/media-time.test.js +62 -62
  332. package/test/memory-budget.test.js +390 -390
  333. package/test/move-cost.test.js +216 -216
  334. package/test/mp4-subtitles.test.js +173 -173
  335. package/test/one-authority.test.js +285 -285
  336. package/test/one-budget.test.js +149 -149
  337. package/test/one-resource-divided.test.js +185 -185
  338. package/test/one-serving-output-per-height.test.js +458 -458
  339. package/test/orchestrator-wired.test.js +207 -207
  340. package/test/output-catalog.test.js +39 -39
  341. package/test/output-shape.test.js +21 -21
  342. package/test/output-spec.test.js +251 -251
  343. package/test/output-speed.test.js +87 -87
  344. package/test/outputs-being-prepared.test.js +40 -40
  345. package/test/packet-witness-ring.test.js +236 -236
  346. package/test/packet-witness.test.js +148 -148
  347. package/test/piece-completeness.test.js +37 -1
  348. package/test/piece-disk-store.test.js +401 -401
  349. package/test/piece-from-whole-file.test.js +129 -129
  350. package/test/piece-lru.test.js +319 -319
  351. package/test/piece-reader.test.js +162 -162
  352. package/test/piece-selection-offset.test.js +85 -85
  353. package/test/piece-store-eviction.test.js +595 -595
  354. package/test/piece-store-never-refuses.test.js +153 -153
  355. package/test/piece-store-reservations.test.js +403 -403
  356. package/test/piece-store-slow-disk.test.js +159 -159
  357. package/test/piece-tail.test.js +97 -97
  358. package/test/plan-host-timings.test.js +68 -68
  359. package/test/plan-is-steady.test.js +225 -225
  360. package/test/playback-readiness.test.js +518 -518
  361. package/test/playback-start-regressions.test.js +101 -101
  362. package/test/priority-map-download.test.js +174 -174
  363. package/test/priority-map-emptied.test.js +204 -204
  364. package/test/priority-map-per-output.test.js +215 -215
  365. package/test/priority-map.test.js +163 -163
  366. package/test/probe-wedge-certainty.test.js +60 -60
  367. package/test/produced-copy-choice.test.js +213 -213
  368. package/test/published-timeline.test.js +83 -83
  369. package/test/quality-offer.test.js +160 -160
  370. package/test/quality-variants.test.js +1270 -1257
  371. package/test/rate-control-in-the-output.test.js +180 -180
  372. package/test/read-survives-withdrawal.test.js +164 -164
  373. package/test/read-window.test.js +365 -365
  374. package/test/resume-warm.test.js +39 -39
  375. package/test/run-graph-drift.test.js +37 -37
  376. package/test/run-inputs.test.js +80 -80
  377. package/test/run-intervals.test.js +100 -100
  378. package/test/run-position-follows-published-grid.test.js +89 -89
  379. package/test/run-progress.test.js +64 -64
  380. package/test/same-height-switch.test.js +663 -663
  381. package/test/seek-frees-the-viewers-requests.test.js +217 -217
  382. package/test/seek-landing.test.js +109 -109
  383. package/test/seek-target-not-superseded.test.js +195 -195
  384. package/test/segment-cut-times.test.js +63 -63
  385. package/test/segment-demand.test.js +80 -80
  386. package/test/segment-hold-supersede.test.js +91 -91
  387. package/test/segment-judged-by-its-own-header.test.js +222 -221
  388. package/test/segment-serve-wiring.test.js +9 -3
  389. package/test/segment-store-eviction.test.js +247 -247
  390. package/test/segment-store.test.js +352 -352
  391. package/test/segment-true-start.test.js +86 -86
  392. package/test/segments-are-shared.test.js +176 -176
  393. package/test/send-chunk-bytes.test.js +86 -86
  394. package/test/serving-existing-output.test.js +111 -105
  395. package/test/serving-output.test.js +102 -102
  396. package/test/shared-piece-store.test.js +310 -310
  397. package/test/sidecar-naming.test.js +142 -142
  398. package/test/soundtrack-rate-bound.test.js +289 -289
  399. package/test/source-file.test.js +133 -133
  400. package/test/source-registry.test.js +18 -18
  401. package/test/split-a-stretch.test.js +139 -139
  402. package/test/stale-request-after-seek.test.js +158 -158
  403. package/test/startup-readings.test.js +124 -124
  404. package/test/stream-route.test.js +237 -237
  405. package/test/subtitle-cue-framing.test.js +200 -200
  406. package/test/subtitle-cue-source.test.js +102 -102
  407. package/test/subtitle-cursor.test.js +63 -63
  408. package/test/subtitle-defaults.test.js +118 -118
  409. package/test/subtitle-language.test.js +252 -252
  410. package/test/supply-margin.test.js +190 -190
  411. package/test/swarm-follows-readers.test.js +174 -174
  412. package/test/swarm-reach.test.js +130 -130
  413. package/test/swarm-selection.test.js +220 -220
  414. package/test/tail-duplication.test.js +167 -167
  415. package/test/the-channel-has-both-ends.test.js +91 -91
  416. package/test/the-torrent-thread-serves-bytes.test.js +107 -107
  417. package/test/thread-pool.test.js +53 -53
  418. package/test/timeline.test.js +84 -84
  419. package/test/torrent-contents.test.js +235 -235
  420. package/test/torrent-cost.test.js +71 -71
  421. package/test/torrent-files.test.js +221 -221
  422. package/test/torrent-naming.test.js +255 -255
  423. package/test/tracks-begin-together.test.js +209 -209
  424. package/test/tunnel-renewal.test.js +167 -167
  425. package/test/two-viewers-one-picture.test.js +508 -504
  426. package/test/upload-hurry.test.js +186 -186
  427. package/test/usrsctp-state.test.js +131 -131
  428. package/test/video-facts.test.js +102 -102
  429. package/test/viewer-assignments.test.js +143 -143
  430. package/test/viewer-outputs.test.js +285 -285
  431. package/test/viewer-presence.test.js +116 -116
  432. package/test/viewer-requests-admission.test.js +92 -89
  433. package/test/viewer-states.test.js +189 -189
  434. package/test/viewer-subtitle-subscription.test.js +96 -96
  435. package/test/viewer.test.js +139 -139
  436. package/test/viewing-generations.test.js +390 -390
  437. package/test/visible-rung.test.js +37 -37
  438. package/test/wait-ledger.test.js +100 -100
  439. package/test/webrtc-shim.test.js +149 -149
  440. package/test/wedge-certainty.test.js +131 -131
  441. package/test/withdraw-piece-claim.test.js +212 -212
  442. package/test/worker-channel.test.js +158 -158
  443. package/utils/client-log-file.js +151 -151
  444. package/utils/logger.js +312 -312
  445. package/utils/parse-range.js +25 -25
  446. package/utils/perf.js +59 -59
  447. package/utils/torrent-source-key.js +38 -38
@@ -1,765 +1,765 @@
1
- # Encode architecture — who decides where encoders go
2
-
3
- One question, one answer, and the answer is arithmetic.
4
-
5
- > Viewers are always independent and always reuse what can be reused. The
6
- > number of encoders is however many are needed; how many are needed follows
7
- > from which sets of output parameters are wanted and where the viewers stand
8
- > inside each. Segments produced by ANY encoder are available to ANY viewer, and
9
- > which viewer asked never enters the question.
10
-
11
- ## The one authority
12
-
13
- `services/encode/EncodePlan.js` decides. Nothing else places an encoder,
14
- nothing else takes one away for scheduling reasons, and there is exactly one
15
- call to `#startEncodeRun` in the whole proxy — the one the plan asks through.
16
-
17
- It decides from four things and no others:
18
-
19
- | what | where it comes from |
20
- |---|---|
21
- | what is already made | the segment store, asked afresh before every decision |
22
- | what is being made | the coverage map's live claims |
23
- | what is wanted | the priority map of that output |
24
- | how many the machine can hold | `run-budget.js`, from measurements of this host |
25
-
26
- **No viewer reaches the decision.** `EncodePlan` and `EncodeOrchestrator` do not
27
- import the viewer layer, name a consumer id or hold a person. What crosses is a
28
- priority map: zones of segment numbers with a rank and a real time, and nobody's
29
- name on it.
30
-
31
- **The rest of `services/encode/` names people, and holds none.** A step and a
32
- soundtrack are chosen, warmed and left per person, so `Renditions`,
33
- `QualityController` and `CushionReport` say WHOSE — by consumer id, which is a
34
- name and not an object. What they may ask about that name and what they may
35
- tell the viewer layer about it is `services/viewer/choices.js`: the mode a size
36
- was picked in, what a link measured, the soundtrack chosen, the step on screen,
37
- what is being warmed, who is present, and the standing quality ask. Values in,
38
- values out. No `Viewer` crosses the boundary and nothing outside the viewer
39
- layer writes a viewer's record — which is the rule a position cost 77 encoder
40
- starts for breaking (field 2026-09-13).
41
-
42
- This paragraph used to claim the whole directory named nobody, which was never
43
- true of `Renditions` and could not be: which soundtrack to make is a fact about
44
- a person. What matters is that it is a NAME, so the viewer's record stays in
45
- one place with one writer.
46
-
47
- **What answered a request is the viewer's, per viewing** (roadmap item 97,
48
- step 9). The page stamps every request with the GENERATION of its viewing — a
49
- number it raises on the viewer's own seek, before it sends anything — and the
50
- proxy reads it rather than counting one, so a request made before a seek and
51
- delivered after it still says which viewing it belongs to. Three things follow,
52
- and each is asked through `choices.js`, by name:
53
-
54
- 1. every file route asks FIRST whether that viewer still takes requests of that
55
- generation, before a step or a soundtrack is resolved — resolving can create
56
- an output and registers the viewer on it, which a request of a viewing
57
- already left must not do;
58
- 2. a repeat of a request within one viewing is answered by the output that
59
- answered it: the step route records the height asked for and the segment
60
- (the init as −1), the picture's own route records under the height that
61
- output is named after. A soundtrack records nothing of this kind;
62
- 3. a response holds its output from its first byte to its end, and no output is
63
- disposed while an assignment holds it — except on a hardware encoder's
64
- failure, where nothing can go on making what it made. The idle expiry asks
65
- only the assignments, not whether anybody is registered, so a paused
66
- viewer's claim on an output stays a separate decision.
67
-
68
- **Which output serves a viewer is one rule, asked by every path, and the
69
- choice is the viewer's own** (step 11). A request for a piece is answered, in
70
- order, by what already answered that address in this viewing (`given`), by the
71
- output chosen for this viewer at that height (`chosen`), and only then by the
72
- rule — and a repeat whose output has gone is served from its stored piece or by
73
- an output with a proven-compatible header, or answered `assignment-lost`. The
74
- rule (`serving-output.js`, `outputSuits`) asks the picture's material, the size
75
- a viewer who picked by hand requires, and what the viewer's OWN link does with
76
- the output's whole load: the picture and the soundtrack they chose, each part
77
- `known` (a limit), `estimated` (a stated average) or `unknown` (no bound), the
78
- load as trustworthy as its worst part (`link-budget.js`). `unknown` is refused
79
- against a measured link; `estimated` is admitted by estimate and reported to
80
- the page as not confirmed; a bound is taken before an average. When nothing
81
- suits, nothing is handed over: the answer is `output-unavailable`, with the
82
- figures, and the page explains it. The shared `OutputCatalog.servedBy` record
83
- and the worst link over all viewers are gone; the quality budget asks each
84
- viewer's link for that viewer alone.
85
-
86
- **A bitrate limit is part of an output, not a figure moved under it** (step
87
- 10). A software encode's `-maxrate`/`-bufsize` are named by its key
88
- (`vbv=<maxrate>k-<bufsize>k@<level>`), so two limits are two outputs and a
89
- slow viewer's limit can no longer land on every viewer of a picture — which is
90
- what it did while it was a field of the output set by the worst of their links.
91
- The level is pinned (`-level:v`) to the level of the NOMINAL output of that
92
- size and frame rate, so every limit at one size writes the same header
93
- and a viewer can be moved between them under the address they already play.
94
- A limit above the nominal one is refused. Hardware encoders are given no limit
95
- and state none (`vbv=-`). Whether the pinned level is the one x264 would choose
96
- for the nominal output is checked before a release
97
- (`stand/segment-compat/level-check.mjs`).
98
-
99
- **Which limits a frame has is decided by its AREA** (step 14, decided with the
100
- user 2026-09-24). The rows of limits are the ladder's frames at 16:9, sized by
101
- the same function that sizes an encode; a frame takes the row nearest by the
102
- ratio of areas, the larger row on the exact border, and a row with no nominal
103
- of its own borrows the nearest row that has one (`limitRowFor`,
104
- `nominalKbpsFor`). Every place that picks a row — opening an output, the move
105
- between limits, the offer's price of a step and the link rule — is handed the
106
- whole frame, so all of them name the same row. By height alone a 1920x800 film
107
- fell in the 720 row although it is nearer the 1080 one. The proposed measured
108
- table of lower limits was dropped on 2026-09-27. Step 14 now qualifies this
109
- machine's encoder modes and does not set lower bitrate limits.
110
-
111
- ## Where a viewer is, and how it reaches the encoders
112
-
113
- A viewer's position has ONE writer — the viewer — and is a function of time:
114
-
115
- ```
116
- position(now) = stated + (playing ? 1 : 0) × (now − statedAt)
117
- ```
118
-
119
- A viewer whose picture moves covers a second of film every second, so between
120
- the moments they speak their position is known exactly; one whose picture is
121
- stopped covers nothing. Nothing else writes it, and in particular a request for
122
- a segment does not: a request says the viewer is still here, and the segment
123
- either exists and is served or does not and is waited for.
124
-
125
- Held as a stored number instead it was a staircase, flat for the ten seconds
126
- between reports and then a jump — and with two writers filling it in turn the
127
- steps went backwards as often as forwards. Field 2026-09-13, one output over one
128
- second: `p100:#9..#10`, `p100:#18..#20`, `p100:#16..#18`, `p100:#18..#20`, while
129
- the second viewer sat frozen at 1673.6 s. The placement follows the map, so the
130
- machine spent six minutes on 77 encoder starts and 141 stops.
131
-
132
- PRESENCE IS A SEPARATE FACT and it is the connection: a viewer is here until
133
- something says otherwise. Silence never says it — a pause, a hidden tab whose
134
- timers are throttled, and a full cushion are all silent and all still watching.
135
-
136
- ## Viewers become a map, and that is the whole crossing
137
-
138
- ```mermaid
139
- flowchart TB
140
- subgraph V["services/viewer — where people are"]
141
- VW[Viewer<br/>position as a function of time, playing, buffered, chosen track]
142
- VS[Viewers<br/>the relation, indexed from both ends]
143
- end
144
-
145
- subgraph P["services/priority — one fact, two scopes"]
146
- PM[PriorityMap<br/>seconds of film to a rank]
147
- PO[PriorityOrchestrator<br/>merge, publish, keep]
148
- end
149
-
150
- subgraph E["services/encode — where encoders go"]
151
- CM[CoverageMap<br/>ready / making / free]
152
- EP[EncodePlan<br/>argmin over arrangements]
153
- RB[run-budget<br/>what this host can hold]
154
- ER[EncodeRun<br/>one process, one interval]
155
- end
156
-
157
- subgraph D["services/torrent/demand + torrent/download — what the swarm is told"]
158
- DR[DemandRegister]
159
- SS[SwarmSelection]
160
- end
161
-
162
- VW --> VS
163
- VS -->|"positions, presence"| PO
164
- PM --> PO
165
- PO -->|"per FILE, in bytes"| DR
166
- DR --> SS
167
- PO -->|"per OUTPUT, in segments"| EO[EncodeOrchestrator]
168
- EO --> EP
169
- CM --> EP
170
- RB --> EP
171
- EP -->|"start / stop / move"| ER
172
- ST[(SegmentStore<br/>the disk)] -->|"what is ready"| CM
173
- ```
174
-
175
- ## Two scopes of one map, and both are right
176
-
177
- The map is built from where the viewers are, once, and read at two scopes.
178
-
179
- **Per FILE, for the swarm.** The picture, a quality step and a soundtrack of one
180
- film read the same bytes, so every viewer of any of them wants that file's
181
- bytes. This is what `PriorityOrchestrator.mapFor(sourceKey, fileIndex)`
182
- answers, and what is published to the download layer.
183
-
184
- **Per OUTPUT, for the encoders.** A person watching 480p wants nothing of the
185
- 1080p output at all. This is `mapForOutput(address)`.
186
-
187
- One map for both was the second authority over encoders. Handed the film's map,
188
- the plan wanted an encoder on every output of the film; what actually stopped
189
- the ones nobody was watching was the session manager killing them by its own
190
- judgement — and since a viewer moving between steps announces itself, the plan
191
- started them again on the very next pass. Two parties answering "should this
192
- encoder exist" by different rules, several times a second.
193
-
194
- An output nobody is on gets a map with **nothing in it**, which is a statement
195
- and not an absence: the walk writes one for every output a session exists for,
196
- including the ones everybody has left. That is how the plan is told to stop what
197
- is on it.
198
-
199
- ## Which output a person is consuming
200
-
201
- A person holds a record on more outputs than they are consuming. The picture is
202
- where their record lives — the browser addresses it, their chosen soundtrack is
203
- written on it, their position is read from it — so they are never let go of it;
204
- but the moment they step down to 480p, the 1080p output is producing for nobody.
205
-
206
- That distinction is answered where the two facts meet, and neither layer is
207
- handed the other:
208
-
209
- - **which step is on their screen** is a fact about a PERSON, read off the
210
- viewer as one field. The page STATES it (`playingHeight` on the viewer's
211
- report, sent the moment hls.js has switched), and `Renditions.viewerPlays`
212
- records it. It used to be inferred from the first segment requested of a
213
- rung, which is the player fetching rather than the picture having moved;
214
- - **which output a step supersedes** is a fact about the FILM'S SHAPE, answered
215
- by `OutputCatalog.supersededBy(session, stepOnScreen)`, which takes a plain id
216
- and has never seen a viewer.
217
-
218
- A step, a soundtrack, and a step being warmed are consumed by whoever is
219
- registered on them — everywhere but the picture, a person who stops watching is
220
- let go of, so being known to an output is consuming it. Whether an output is
221
- wanted at all is asked of the viewer registry and nothing else: an output a
222
- picture made on its own behalf once carried a made-up "family" claim that kept
223
- it alive, which was a second answer to the same question and is gone. Every
224
- viewer has a name; a request that names nobody makes no viewer. Through a warm-up both
225
- the step on screen and the step being made ready are genuinely produced, which
226
- is the price of the switch not being visible.
227
-
228
- ## A move between two limits of one height
229
-
230
- A bitrate limit is part of an output, so a viewer whose link cannot carry the
231
- limit on their screen is given ANOTHER output of the same height at a lower
232
- limit — for them alone, and without their player being told (roadmap item 97,
233
- step 12). The address `v/<height>/…` does not change; what changes is which
234
- output this side answers it with.
235
-
236
- 1. **Prepared, not switched.** `Renditions.prepareSameHeightSwitch` opens (or
237
- finds, by its key) the output of the same size at the next limit of
238
- `limitsFor(height)` that the viewer's link admits, marks it a step of the
239
- picture, and registers the viewer on it — that is what buys it an encoder.
240
- The choice (`Assignments.choose`) is not touched: until the move is made they
241
- are given what they were given. The move being prepared is a field of the
242
- viewer, `Viewer.sameHeightSwitch`, and not `warmingVariantId`, which is a
243
- step their player will switch to itself.
244
- 2. **Made on the store's own event.** `SegmentStore.onPublished` is filtered by
245
- the prepared output's key. The move is made when the segment the viewer will
246
- ask for NEXT is closed on it: one past the highest they were given in the
247
- viewing they are in now, or — with nothing given in it, straight after a
248
- seek — the one where they stand. It is read at the moment of the event, so a
249
- segment closed behind that one, or an event that arrives after a seek, moves
250
- nothing.
251
- 3. **One synchronous stretch.** The choice under each height it was chosen
252
- under, the verdict, the step on screen, the registration on the new output
253
- and off the old one change with no `await` between them.
254
- 4. **The output left is not disposed.** The picture is never left. A step that
255
- is left loses the viewer, the plan stops its encoder by its empty map, and the
256
- idle expiry removes it once no assignment holds it; a repeat of an address it
257
- answered is still answered by it, because `given` is read before `chosen`.
258
- 5. **Cancelled when not wanted.** When the link carries the output on screen
259
- again, when the prepared output no longer suits, when the player moves to
260
- another rung, when encoding the prepared output has failed for good, or when
261
- nothing of the film is left to give from it: the viewer stops watching the
262
- prepared output and nothing else changes for anybody.
263
- 6. **Asked again on three events, never on a timer.** A segment closed on the
264
- prepared output (`SegmentStore.onPublished`); the viewer's own report
265
- (`Renditions.noteViewerReported`, from the net-report route), which is what
266
- carries a change of their link; and encoding the prepared output failing for
267
- good (`Renditions.noteProductionFailed`, told by `EncodeRuns`). Without the
268
- last two, a move onto an output that closes nothing stood for ever, and the
269
- quality budget — which leaves a viewer with a move pending alone — never
270
- decided for them again. Each event may also make the move, on the same one
271
- condition: the segment they will ask for next is closed. A move is not
272
- prepared onto an output whose encoding has failed. The fourth way a move can
273
- wait — no encoder placed on the output — does not arise until the admission
274
- of step 13 can refuse, and that refusal will be its event.
275
-
276
- The quality budget uses this lever first, in both directions: down to the
277
- highest lower limit the link admits, up one limit at a time, and another height
278
- only where the height on screen has no limit left to move to. A copy has no
279
- limit and a hardware encode is given none, so for them the lever is still the
280
- height. The measured table of lower bitrate limits was dropped on 2026-09-27;
281
- the product currently supplies only the nominal limit through `limitsFor`.
282
- Per-machine calibration (step 14) qualifies encoder modes and does not create
283
- lower bitrate limits, so same-height bitrate moves have no target in the current
284
- product configuration.
285
-
286
- 7. **A move up waits for the cushion; a move down does not** (roadmap item
287
- 98). Once the piece asked for next is closed, a move UP is made only when
288
- the viewer holds the cushion this file needs (`minimumBufferSeconds`), and
289
- their next report asks again until they do. A move DOWN was started because
290
- their buffer would run dry, so it is made the moment the piece is ready.
291
-
292
- ## When the quality budget moves a viewer (roadmap item 98)
293
-
294
- The page has no quality control: the quality is always automatic. What moves a
295
- viewer is decided here, per viewer, and each part of it is a measurement.
296
-
297
- 1. **Judged on the viewer's report, not on a timer.** `QualityController.noteViewerReported`
298
- runs from the net-report route, for the step down, the picture they see and
299
- the step up alike. The chosen fifteen-second window of a slow link or a slow
300
- machine, the 0.95x threshold of slowness, the sixty-second window before a
301
- step back up, the ten-second buffer threshold, the thirty-second wait after
302
- every action and the 80 % share of the link are gone. The five-second timer
303
- only samples the host's load and the torrents' download rates; an encoder's
304
- price is learned when it closes a piece.
305
- 2. **Down when the buffer would run dry first, for a reason a smaller output
306
- removes.** On the buffer's trend it ends sooner than another output could
307
- close the piece they need, counting the time until their next report
308
- (`quality/drain-threat.js`), AND either their link carries less than the
309
- stream they are given, or this machine makes the picture slower than
310
- realtime over its run's own working time (`encode/RunClock.js`: the time its
311
- input waited for the swarm and the time it was stopped are taken out). A
312
- threat neither explains — the swarm is short — is not answered with a
313
- smaller picture, which reads the same input. The trend is a least-squares fit
314
- over the shortest run of their reports spanning one segment
315
- (`viewer/buffer-trend.js`), because the buffer rises a segment at a time and
316
- two readings catch only the rise or the fall. The time to readiness is the
317
- observed preparation time of another output of this mode, else this host's
318
- measured first-segment time. The levers: for the link, a lower limit of the
319
- same height, then a lower height; for the machine, a lower height. A lower
320
- height is asked of the player as URGENT — their page switches as soon as the
321
- rung is ready, without waiting for a cushion. Where nothing can be prepared
322
- they stay on what they are given, with no message.
323
- 3. **The picture the viewer sees bounds a re-encode.** The page sends the
324
- frame it would show without enlarging, in physical pixels; it is kept on
325
- `Viewer.visiblePicture`. The bound is the smallest rung of the source's
326
- ladder whose frame is not smaller (`quality/visible-rung.js`). It sets the
327
- box an output is opened at (`OutputOpening`), the ceiling of every step up,
328
- and the rung a step down prefers. A copy of the source is never re-encoded
329
- for being taller than that.
330
- 4. **A smaller picture moves the viewer only onto a READY rung**
331
- (`Renditions.heightReadyFor`: the piece they ask for next closed on it);
332
- with none ready, the next judgement of their link or of the machine applies
333
- the bound.
334
- 5. **Up one rung when there is room, every term measured**: the picture on
335
- their screen is made at least at realtime over its run's own working time (a
336
- copy is not limited by an encoder), their buffer is not draining and holds
337
- at least the time another output takes to be ready, their link carries the
338
- next height, and the picture they see is not already served. A rung this
339
- host has been measured failing at is not offered, so the step back up cannot
340
- return to it; no window has to pass.
341
- 6. **A request stands for as long as its conditions hold**: each judgement
342
- of the viewer's report asks for what it needs again, and a request it does
343
- not repeat is let go — a draining buffer for a step up, a buffer that no
344
- longer drains for a step down, no room for the output, a picture that is no
345
- longer smaller. No chosen time ends a request. The page drops the move it
346
- was preparing when the proxy stops asking for it.
347
- 7. **The page switches a variant when the rung is ready**, and for a request
348
- that is not urgent only once it holds `minimumBufferSeconds`.
349
-
350
- ## A place on the machine
351
-
352
- How many encoders ONE output may run is answered per output
353
- (`EncodeRuns.maxRunsForOutput`) and is always at least one. What refuses the
354
- encoder that would slow everybody is `EncodeAdmission`, across every output at
355
- once (roadmap item 97, step 13).
356
-
357
- 1. **The unit is seconds of work per second of film**, the one the quality
358
- offer judges steps in (`EncodeCost.loadOfOutput`). Costs add, and a set of
359
- encoders is affordable when the machine, corrected for the share of it
360
- nobody has priced, still makes a second of film per second. A soundtrack is
361
- a small fraction of a picture; counting processes would refuse a viewer
362
- their sound. On the addon host the sum reproduces the measurement: two 1080p
363
- encodes at 1.96x alone cost 1.02 s/s and make 0.98x, measured 0.99x and
364
- 0.98x.
365
- 2. **A place is held by an OUTPUT**: by every live encoder at its output's
366
- cost, and by every output a present viewer is being prepared onto — a step
367
- being warmed or a move to another limit — once however many wait for it.
368
- The second is read off the viewers' own records (`Viewers.outputsBeingPrepared`),
369
- so a place ends with its record on whichever path that record is cleared,
370
- and there is no register of its own to keep in step.
371
- 3. **Asked before a preparation is recorded**, in the same synchronous stretch
372
- as the record: two preparations onto two different outputs with room for
373
- one cannot both be admitted, because the second is asked after the first is
374
- written. Refused, a move answers `noPlace` and the budget goes to a lower
375
- height (never a higher one); a warm-up answers `output-unavailable`.
376
- 4. **The plan is bounded by it** (`EncodeOrchestrator.#withinMachine`): an
377
- output may run no more encoders than still fit, and a new one on a full
378
- machine gets none until one elsewhere ends.
379
- 5. **An admitted encoder is never taken away**, and a place promised to a
380
- preparation is kept: the machine's answer is never below what already runs
381
- on an output, nor below one for an output a preparation holds.
382
-
383
- Not solved: places go to whoever asks first, so a second encoder catching up on
384
- one output can hold the place a viewer opening another film waits for.
385
-
386
- ## What each of the eight other places used to do
387
-
388
- Before 2026-09-08 the plan was one opinion among nine. Each of these placed or
389
- killed encoders by a rule of its own; each now states the fact it knows.
390
-
391
- | it knows | it used to do | it now says |
392
- |---|---|---|
393
- | a session was created | start a run at the viewer's position, worked out again | the viewer is placed; the plan reads that |
394
- | a viewer joined further in | start a run there if nothing was being made | as above |
395
- | a step or soundtrack is being warmed | point that session at the switch and start it | this person is at N seconds on it |
396
- | a step was switched to | stop the one left, point the new one | this person is on this step now |
397
- | a step or track was abandoned | stop its encoder | this person has left that output |
398
- | the hardware encoder failed | start a run at the dead one's start | what this host encodes with has changed |
399
- | the input came back | start a run at the last requested segment | decide again |
400
- | the cut table was corrected | restart the members at the measured instant | this run is producing in the wrong place; the instant is in the file's table |
401
- | the bitrate cap changed | restart at where the encoder had got to | nothing: a limit is part of an output since 2.87.0, so another limit is another output and no running encoder is ever told a new one |
402
-
403
- A **seek** is not in that list because it was already reduced to one thing: it
404
- puts the viewer where they are. The settle timer behind it, its cooldown and its
405
- one-segment backoff are gone — a second debounce on a signal the browser had
406
- already debounced, and every millisecond of it was dead time in front of the
407
- viewer.
408
-
409
- ## Whether the map is being served in its own order
410
-
411
- The zones say what matters most. What the viewer actually waited for is measured
412
- where a viewer measurably waits — the one place in the proxy that holds a
413
- request for a named segment — and recorded against the rank the map gave that
414
- segment **at the moment it was asked for**. It reaches the `encode:` state line
415
- as `served[...]`:
416
-
417
- ```
418
- served[now 42 wait(s) median 180ms worst 1900ms, soon 6 wait(s) median 90ms worst 240ms, later none]
419
- ```
420
-
421
- Read it like this:
422
-
423
- | what it says | what is wrong |
424
- |---|---|
425
- | long waits at `now` | the urgent zone is not being served first — a fault in whoever acts on the map |
426
- | long waits at `soon`/`later`, none at `now` | the zones are the wrong width: the urgent one too narrow, so the viewer reaches material only ranked "soon" |
427
- | `now none` while the viewer is watching | nothing was ever urgent — the map is not reaching this output |
428
- | a band reading `none` | silence, not a zero, and it is printed as `none` so it cannot be read as "no waits, all good" |
429
-
430
- Ranks are collapsed into three bands because the map's own scale is as long as
431
- the film needs — a hundred ranks on a long file — and a hundred-row table says
432
- nothing a reader can hold. The width of `soon` is a tenth of the top rank, which
433
- is the map's own shape (its zones widen geometrically) rather than a threshold
434
- chosen for the table.
435
-
436
- The download half is measured the same way and on the same scale, so the two are
437
- comparable: `download-architecture.md`.
438
-
439
- ## What the master playlist declares, and why it is not cosmetic
440
-
441
- `BANDWIDTH` and `AVERAGE-BANDWIDTH` per variant, both measured
442
- (`services/encode/output/rates.js`):
443
-
444
- - the average is the file's own length over its duration, exact and known when
445
- the session is created;
446
- - the peak is the biggest piece produced over the span it covers, and equals the
447
- average until a piece exists — a ratio invented meanwhile would be the
448
- fabrication this replaced;
449
- - a re-encoded height is declared at the cap we impose, which is exact;
450
- - a smaller height at the pixel share of the source's rate, which errs high, and
451
- high is the safe direction.
452
-
453
- It used to be `height * height * 3.2`. **The browser sizes its cushion in BYTES
454
- from `BANDWIDTH`**, so a figure five times low makes the cushion five times
455
- shallow: field 2026-09-08, 3.73 Mbit/s declared for a file carrying 18.4, 120 s
456
- asked bought 56 MB — 26 s of that film — and the deepest the browser ever held
457
- was 17.1 s. Inflating it is not the answer either: hls.js compares it against
458
- its own estimate of the link to decide a level is unplayable, and its recovery
459
- then moves level by itself, which does not honour our pinning.
460
-
461
- ## The objective is the map's own rank order
462
-
463
- Not three terms in seconds. One pair per rank the map states, most urgent rank
464
- first, compared position by position:
465
-
466
- ```
467
- [ late(100), done(100), late(99), done(99), … late(1), done(1) ], encoders, wasted
468
- ```
469
-
470
- `late(r)` is how long anybody waits past a deadline at rank `r`; `done(r)` is
471
- when the last number of that rank is made. A difference at a higher rank settles
472
- it and nothing lower can reopen it — which is the stated order: nobody stares at
473
- a spinner; then the film in front is encoded as fast as it can be, band by band
474
- as the map ranks them; then, with what is left over and only then, the film
475
- behind, in case somebody seeks back.
476
-
477
- **No weights, and none possible.** A weight would let seconds at one rank buy
478
- seconds at another, and it would be a figure nobody measured. The map is the
479
- source of truth about what matters and it already says so — ten ranks on a film,
480
- p100 at the number a viewer is stopped on, doubling zones down to p91 for the far
481
- tail, p1 for what lies behind them.
482
-
483
- `encoders` ranks below every rank of the map, so spare capacity cannot buy an
484
- encoder where the map is indifferent. `wasted` is the swarm's bill for anything
485
- fetched twice.
486
-
487
- ## Why an encoder is not moved for nothing, and it is not a rule
488
-
489
- There is no threshold here, and there must not be one. What kept an encoder
490
- moving was three quantities computed wrongly, not a policy that needed tuning.
491
-
492
- **`delaySec` is when a body finishes the piece it STANDS ON.** For a run that has
493
- produced something, one piece at the rate in force; for one still warming up, the
494
- measured time to a first piece less the time it has already been alive. The
495
- arrival of anything further on is that plus the pieces between, and nothing else.
496
-
497
- It used to be `delay + (index - at + 1) / rate`, which charges every body a whole
498
- piece for the one it is already making. That is right for a body that does not
499
- exist yet and wrong for a run 0.8 s into a 0.9 s piece — and the difference is
500
- the whole fault: from #58, reaching #59 was priced at 1.89 s against 1.88 s for a
501
- kill and a cold start, a coin flip lost by ten milliseconds. Priced correctly it
502
- is 1.08 s against 1.88 s.
503
-
504
- **One rate, the one the arrangement puts in force.** Concurrent encoders slow
505
- each other, measured on this host, so a piece costs what it costs at the body
506
- count the arrangement has. Taken from the unpenalised rate while arrivals used
507
- the penalised one, an extra body looked cheaper than it is and the plan bought a
508
- second encoder where one served.
509
-
510
- **Each body states its own debt** where it is created, as a function of what one
511
- piece costs — because only there is it known what the body IS, and at the point
512
- of pricing only how many there are. So there is no kind, no tag and no case
513
- analysis.
514
-
515
- **The measured start is separated from the piece it contains**, because the two
516
- scale differently: a piece costs more when encoders share the machine, a spawn
517
- does not.
518
-
519
- ```
520
- spawn overhead = measured first output - what one piece costs alone
521
- a fresh encoder = spawn overhead + the piece at the rate in force
522
- a moved one = the kill, and then the same
523
- ```
524
-
525
- **Both figures are measured before any viewer exists**, and until 2026-09-10
526
- neither was. They were learned only from runs that had ENDED, so at a cold open
527
- both read zero — and zero does not mean "unknown" here, it means "free".
528
-
529
- That is not a small bias, it is the whole comparison. Subtract keeping from
530
- moving and what is left is exactly the killing plus the time the run has already
531
- lived:
532
-
533
- ```
534
- keep a warming run = firstByteWait - elapsed
535
- move it = kill + spawn overhead + one piece
536
- = kill + firstByteWait (spawn overhead = firstByteWait - one piece)
537
- move - keep = kill + elapsed
538
- ```
539
-
540
- `elapsed` is the warm-up a move throws away — a process started, an input
541
- opened, the first bytes fetched, a decoder filled — and it is the term that
542
- makes moving cost something. Set `firstByteWait` to zero and it cancels out of
543
- both sides along with the kill: keeping and moving then cost the same figure to
544
- the millisecond, the tie falls to position, and any advantage however small wins.
545
-
546
- Field 2026-09-08, the first fifteen seconds of a session: start at #68, a second
547
- later kill and start at #69, half a second later kill and start at #68 again,
548
- each dying having produced nothing. Over two days 153 runs were stopped by the
549
- plan and 68 of them made no segment at all, median life 9.6 s.
550
-
551
- `services/encode/start-stop-cost.js` measures both from **one ffmpeg run at
552
- startup**: spawn to the first piece the encoder itself announces closed, then
553
- SIGTERM to exit. 0.68 s and 0.02 s on the developer's desktop. `RunCosts` starts
554
- from those and replaces them with readings from real runs as they arrive.
555
-
556
- There was an `Infinity` here once for the cost of a move, on the reasoning that
557
- an unmeasured price must not license an irreversible act. It was removed as an
558
- exception in a model that needs none — correctly, but what replaced it was a
559
- floor of zero, which licenses the act rather than forbidding it. A measurement
560
- is what a model like this needs, not an exception and not a floor.
561
-
562
- And a run killed before producing anything is a measurement too — a lower bound
563
- on the first output, and the only reading a thrash can supply, since every run in
564
- one is killed before it finishes anything. So a thrash makes its own moves
565
- progressively dearer until it stops.
566
-
567
- ### What that was for
568
-
569
- Field 2026-09-08: 39 moves in one session, 24 of them between three adjacent
570
- numbers about 0.8 s apart. One viewer on a host affording three runs got three.
571
- The picture stood still for 116.7 s in three interruptions, the worst 91.8 s.
572
-
573
- Compounding it, the map states `withinSeconds: null` for the film behind the
574
- viewers — nobody is waiting there — and `deadlineReaderFor` read that through
575
- `Number()`, where `null` is 0. So the film a viewer had already passed was due
576
- IMMEDIATELY and was the most urgent material in the file: it bought encoders, and
577
- it took the run standing in front of the viewer because that run was the nearest
578
- body to it.
579
-
580
- Checked by simulation over eighty ticks against the map's real shape — ten zones
581
- doubling ahead of the viewer, one behind — at both one and three runs: the
582
- encoder is placed once, left alone, and moved exactly once, at the viewer's own
583
- seek.
584
-
585
- ## Where a second encoder joins a stretch
586
-
587
- Derived, not halved. A stretch of unmade film runs from `from` to `to`; whoever
588
- is already on it stands at `from` and owes `w` before the piece under it exists;
589
- a fresh one placed at `x` owes `d` — its start and then a whole piece — and both
590
- then work at the rate two encoders leave each other, which is measured here.
591
-
592
- ```
593
- the one already there closes [from, x-1]: w + (x - 1 - from) / r rises with x
594
- the fresh one closes [x, to]: d + (to - x) / r falls with x
595
-
596
- x* = (from + to + 1) / 2 + (d - w) * r / 2
597
- ```
598
-
599
- The midpoint, shifted forward by half the difference of what the two owe, in
600
- pieces. Halving is the special case `d = w`, which holds when both are fresh.
601
-
602
- **The shift is under one segment in every measured configuration** — 0.24 of a
603
- piece at the addon host's 1080p contention — so halving was very nearly right,
604
- and saying otherwise would be dressing it up.
605
-
606
- What the derivation adds is the question halving never asked: **is a second
607
- encoder worth having at all?**
608
-
609
- ```
610
- one: w + (to - from - 1) / rate
611
- two: w + (x* - 1 - from) / r
612
- ```
613
-
614
- Nothing is proposed where the second does not win. At 1920x1080 the measured
615
- penalty for a second encoder on the addon host is 1.98 — it takes very nearly
616
- all of the first's speed — and the objective keeps one; where a second is free it
617
- places three.
618
-
619
- ## What the log says when anything is placed or taken away
620
-
621
- ```
622
- encode-plan on <output>: start #58..#481, stop #?..#?
623
- [speed=4.45x firstByte=1.26s kill=0.04s refetch=0.000s/s maxRuns=2 live=1]
624
- ```
625
-
626
- Every action with its INTERVAL, which is what a run is, and then every term the
627
- decision was made from. Printed on any pass that does something — a session
628
- where nothing changes says nothing.
629
-
630
- The terms are there because an interval says WHAT was decided and only these say
631
- WHY. A decision of this plan is
632
-
633
- delay + (index - at) / rate + madeBetween * refetch against a deadline
634
-
635
- so a recorded decision without the rate can be re-read and not recomputed. That
636
- is not hypothetical: the one-piece intervals of 2026-09-08 were diagnosed by
637
- substituting the rate from the speeds the session reported elsewhere — six
638
- different figures, none of which reproduced the answer the plan had given.
639
-
640
- A zero in `firstByte`, `kill` or `refetch` is a measurement nobody has taken, not
641
- a free operation. It is printed so that reading it as free is a choice.
642
-
643
- **`speed` has one owner: the run.** `EncodeRun.speedX` is the film a run made
644
- between two closed pieces over its own working time (`encode/RunClock.js`), so
645
- the time its input waited for the swarm and the time it was stopped are not in
646
- it. The run announces each reading once (`onSpeedMeasured`); `EncodeCost` learns
647
- the price of the output from it, the quality budget and the playback forecast
648
- read the newest reading of the output's runs, and the plan reads the runs
649
- themselves. Before a run has closed two pieces, and after a restart, the plan
650
- uses `startingSpeedFor`: what this output was last measured doing alone, or
651
- what the startup measurements predict. Nothing keeps a copy across restarts,
652
- and ffmpeg's cumulative `speed=` is read by nothing: it divides by every second
653
- the encoder was stopped or starved.
654
-
655
- What the swarm delivers is not in this figure, by construction. It is the
656
- separate supply term `refetch`, and while that is unmeasured (printed as zero)
657
- the plan's arrivals assume the input keeps up.
658
-
659
- It was missing, and its absence cost three wrong diagnoses of one field session.
660
- The line printed the windows, the budget and where the live runs stood; the
661
- intervals the actions carried were the one thing it did not print. What that
662
- session actually did was give every encoder an interval of exactly ONE segment —
663
- 63 runs, each spending 1.26 s reaching its first piece, making that one piece,
664
- reaching the end of its interval and exiting, twelve of them normally. The
665
- reasons printed beside them read as moves back and forth, so the fault was read
666
- as an oscillating placement three times over. An interval of one segment turns
667
- the protection against two encoders writing one name into a mill for processes.
668
-
669
- ## What proves a segment is finished
670
-
671
- Its NAME, and there is nothing else. A piece being written is called
672
- `making-<from>-00042.mp4` — the tag is the first number of the stretch its run
673
- was given — and it takes `segment-00042.mp4` when the encoder says it has closed
674
- it, which it does on a channel of its own (`-segment_list pipe:3`). Making it
675
- servable is therefore one rename inside one directory, performed by the store
676
- because the store owns the disk. The `hls` branch needs nothing extra: its muxer
677
- writes through a temporary name of its own, so its files appear under their final
678
- name whole.
679
-
680
- Three things follow, and each replaced a guess:
681
-
682
- 1. **a request can never reach a half-written piece.** Closure used to be
683
- inferred from the NEXT number existing — sound for one writer walking forward,
684
- false the moment two runs share an output, which is what the plan gives an
685
- output whenever it places a second encoder. Field 2026-09-08:
686
- `segment-00057.mp4` served at 2 268 361 bytes and then at 4 510 940, exactly
687
- half; the browser appended the half and refused the whole for the rest of the
688
- session, with the picture frozen at 319.66 s;
689
- 2. **the last piece of a run is provable.** Under the successor rule nothing
690
- followed it, so it never was — the resume case that held one segment for 46 s
691
- and then answered 404;
692
- 3. **clearing up after a dead run is a name match.** Its unfinished pieces are
693
- the ones carrying its own tag: no stretch to search, no bytes to judge, and no
694
- way to remove a complete piece somebody else closed. `services/encode/
695
- open-piece.js` did all three of those by guessing and is gone, along with the
696
- session manager's copy of it.
697
-
698
- ## How a request for a segment ends
699
-
700
- A request steers no encoder. The file is served if the store holds it whole;
701
- otherwise the request waits for the store's own announcement that the segment
702
- was published (`SegmentStore.waitFor`), for the waits of that output to be
703
- invalidated (a viewer left the rung), or for the requester to go. How long it
704
- may wait is the requester's: the page states it (`X-Hold-Ms`, its own deadline
705
- less the round trip it has measured) and the route answers "retry" by then. A
706
- requester that states nothing is held until it closes the connection. There is
707
- no poll and no hold time chosen by the proxy.
708
-
709
- Which output a request is answered by is decided in the encoding component
710
- (`encode/OutputOpening.js`), without knowing who asked; the server operation
711
- (`server/ViewerRequests.js`) places the requester on it.
712
-
713
- ## What is checked
714
-
715
- `test/one-authority.test.js` holds the shape: one caller of `#startEncodeRun`,
716
- no encoder stopped for being unwatched, the settle machinery absent, each output
717
- reading its own map, and the soundtrack's start instant read off the table
718
- rather than handed in.
719
-
720
- `test/priority-map-per-output.test.js` holds the two scopes, over the real
721
- viewer registry, the real `OutputCatalog` and the real `PriorityOrchestrator`.
722
-
723
- `test/encode-plan.test.js` holds the arithmetic, including that every encoder
724
- stops when nobody is watching the output.
725
-
726
- `test/segment-store.test.js` holds the naming rule: a piece under its served
727
- name is finished — the last one of a run included — one under a working name is
728
- not and cannot be reached, closing it is one rename, and clearing up after one
729
- run leaves every other run's work alone.
730
-
731
- ## Per-machine calibration and admission (roadmap item 97, step 14)
732
-
733
- At startup, `encode/calibration.js` qualifies every mode the proxy may select,
734
- including the software encoder kept for runtime fallback. Each mode uses the
735
- production arguments on a generated test clip, and every segment must decode
736
- independently. `encode/fingerprint.js` keys the results to the FFmpeg and x264
737
- versions, CPU model and thread count for software, or FFmpeg version, device
738
- model and driver for hardware. A changed key does not reuse measurements from
739
- the previous configuration.
740
-
741
- `encode/throughput.js` measures qualified modes at five frame sizes from
742
- 256×144 through 3840×2160. Between measured sizes it interpolates and subtracts
743
- the measured interpolation error; it does not offer a size outside the measured
744
- range. Calibration stops measuring slower modes and larger sizes once a faster
745
- mode cannot sustain realtime. If a detected device has no qualified mode, the
746
- proxy uses software encoding.
747
-
748
- `encode/LocalObservations.js` stores observations from admitted encodes in the
749
- proxy's `local-observations.json`. It keeps speed without competing encodes,
750
- average and peak segment rates, output preparation time and the viewer buffer at
751
- a transition. A matching fingerprint is required. These observations refine
752
- output cost, the peak estimate of an unbounded hardware encoder and transition
753
- preparation timing; an empty or stale file leaves the base policy in force.
754
-
755
- `encode/EncodeAdmission.js` counts running encoders and outputs that viewers are
756
- being placed on or prepared to use. Opening an output checks capacity and
757
- records the viewer in one synchronous call through `OutputOpening` and
758
- `ViewerRequests`; a second simultaneous opening sees the first reservation. If
759
- no measured mode or remaining capacity is available, the proxy answers 409
760
- `no-capacity` before playback. Unknown costs on occupied outputs count as
761
- unknown capacity, not free capacity. The health report exposes safe encoding
762
- headroom, and the pool's file-specific answer uses the same occupied-cost data.
763
- If the opening is refused, the page asks the pool and prepares the file again
764
- through an eligible proxy before showing video. Each refused proxy is excluded
765
- for that opening attempt, so stale pool answers cannot make the page cycle.
1
+ # Encode architecture — who decides where encoders go
2
+
3
+ One question, one answer, and the answer is arithmetic.
4
+
5
+ > Viewers are always independent and always reuse what can be reused. The
6
+ > number of encoders is however many are needed; how many are needed follows
7
+ > from which sets of output parameters are wanted and where the viewers stand
8
+ > inside each. Segments produced by ANY encoder are available to ANY viewer, and
9
+ > which viewer asked never enters the question.
10
+
11
+ ## The one authority
12
+
13
+ `services/encode/EncodePlan.js` decides. Nothing else places an encoder,
14
+ nothing else takes one away for scheduling reasons, and there is exactly one
15
+ call to `#startEncodeRun` in the whole proxy — the one the plan asks through.
16
+
17
+ It decides from four things and no others:
18
+
19
+ | what | where it comes from |
20
+ |---|---|
21
+ | what is already made | the segment store, asked afresh before every decision |
22
+ | what is being made | the coverage map's live claims |
23
+ | what is wanted | the priority map of that output |
24
+ | how many the machine can hold | `run-budget.js`, from measurements of this host |
25
+
26
+ **No viewer reaches the decision.** `EncodePlan` and `EncodeOrchestrator` do not
27
+ import the viewer layer, name a consumer id or hold a person. What crosses is a
28
+ priority map: zones of segment numbers with a rank and a real time, and nobody's
29
+ name on it.
30
+
31
+ **The rest of `services/encode/` names people, and holds none.** A step and a
32
+ soundtrack are chosen, warmed and left per person, so `Renditions`,
33
+ `QualityController` and `CushionReport` say WHOSE — by consumer id, which is a
34
+ name and not an object. What they may ask about that name and what they may
35
+ tell the viewer layer about it is `services/viewer/choices.js`: the mode a size
36
+ was picked in, what a link measured, the soundtrack chosen, the step on screen,
37
+ what is being warmed, who is present, and the standing quality ask. Values in,
38
+ values out. No `Viewer` crosses the boundary and nothing outside the viewer
39
+ layer writes a viewer's record — which is the rule a position cost 77 encoder
40
+ starts for breaking (field 2026-09-13).
41
+
42
+ This paragraph used to claim the whole directory named nobody, which was never
43
+ true of `Renditions` and could not be: which soundtrack to make is a fact about
44
+ a person. What matters is that it is a NAME, so the viewer's record stays in
45
+ one place with one writer.
46
+
47
+ **What answered a request is the viewer's, per viewing** (roadmap item 97,
48
+ step 9). The page stamps every request with the GENERATION of its viewing — a
49
+ number it raises on the viewer's own seek, before it sends anything — and the
50
+ proxy reads it rather than counting one, so a request made before a seek and
51
+ delivered after it still says which viewing it belongs to. Three things follow,
52
+ and each is asked through `choices.js`, by name:
53
+
54
+ 1. every file route asks FIRST whether that viewer still takes requests of that
55
+ generation, before a step or a soundtrack is resolved — resolving can create
56
+ an output and registers the viewer on it, which a request of a viewing
57
+ already left must not do;
58
+ 2. a repeat of a request within one viewing is answered by the output that
59
+ answered it: the step route records the height asked for and the segment
60
+ (the init as −1), the picture's own route records under the height that
61
+ output is named after. A soundtrack records nothing of this kind;
62
+ 3. a response holds its output from its first byte to its end, and no output is
63
+ disposed while an assignment holds it — except on a hardware encoder's
64
+ failure, where nothing can go on making what it made. The idle expiry asks
65
+ only the assignments, not whether anybody is registered, so a paused
66
+ viewer's claim on an output stays a separate decision.
67
+
68
+ **Which output serves a viewer is one rule, asked by every path, and the
69
+ choice is the viewer's own** (step 11). A request for a piece is answered, in
70
+ order, by what already answered that address in this viewing (`given`), by the
71
+ output chosen for this viewer at that height (`chosen`), and only then by the
72
+ rule — and a repeat whose output has gone is served from its stored piece or by
73
+ an output with a proven-compatible header, or answered `assignment-lost`. The
74
+ rule (`serving-output.js`, `outputSuits`) asks the picture's material, the size
75
+ a viewer who picked by hand requires, and what the viewer's OWN link does with
76
+ the output's whole load: the picture and the soundtrack they chose, each part
77
+ `known` (a limit), `estimated` (a stated average) or `unknown` (no bound), the
78
+ load as trustworthy as its worst part (`link-budget.js`). `unknown` is refused
79
+ against a measured link; `estimated` is admitted by estimate and reported to
80
+ the page as not confirmed; a bound is taken before an average. When nothing
81
+ suits, nothing is handed over: the answer is `output-unavailable`, with the
82
+ figures, and the page explains it. The shared `OutputCatalog.servedBy` record
83
+ and the worst link over all viewers are gone; the quality budget asks each
84
+ viewer's link for that viewer alone.
85
+
86
+ **A bitrate limit is part of an output, not a figure moved under it** (step
87
+ 10). A software encode's `-maxrate`/`-bufsize` are named by its key
88
+ (`vbv=<maxrate>k-<bufsize>k@<level>`), so two limits are two outputs and a
89
+ slow viewer's limit can no longer land on every viewer of a picture — which is
90
+ what it did while it was a field of the output set by the worst of their links.
91
+ The level is pinned (`-level:v`) to the level of the NOMINAL output of that
92
+ size and frame rate, so every limit at one size writes the same header
93
+ and a viewer can be moved between them under the address they already play.
94
+ A limit above the nominal one is refused. Hardware encoders are given no limit
95
+ and state none (`vbv=-`). Whether the pinned level is the one x264 would choose
96
+ for the nominal output is checked before a release
97
+ (`stand/segment-compat/level-check.mjs`).
98
+
99
+ **Which limits a frame has is decided by its AREA** (step 14, decided with the
100
+ user 2026-09-24). The rows of limits are the ladder's frames at 16:9, sized by
101
+ the same function that sizes an encode; a frame takes the row nearest by the
102
+ ratio of areas, the larger row on the exact border, and a row with no nominal
103
+ of its own borrows the nearest row that has one (`limitRowFor`,
104
+ `nominalKbpsFor`). Every place that picks a row — opening an output, the move
105
+ between limits, the offer's price of a step and the link rule — is handed the
106
+ whole frame, so all of them name the same row. By height alone a 1920x800 film
107
+ fell in the 720 row although it is nearer the 1080 one. The proposed measured
108
+ table of lower limits was dropped on 2026-09-27. Step 14 now qualifies this
109
+ machine's encoder modes and does not set lower bitrate limits.
110
+
111
+ ## Where a viewer is, and how it reaches the encoders
112
+
113
+ A viewer's position has ONE writer — the viewer — and is a function of time:
114
+
115
+ ```
116
+ position(now) = stated + (playing ? 1 : 0) × (now − statedAt)
117
+ ```
118
+
119
+ A viewer whose picture moves covers a second of film every second, so between
120
+ the moments they speak their position is known exactly; one whose picture is
121
+ stopped covers nothing. Nothing else writes it, and in particular a request for
122
+ a segment does not: a request says the viewer is still here, and the segment
123
+ either exists and is served or does not and is waited for.
124
+
125
+ Held as a stored number instead it was a staircase, flat for the ten seconds
126
+ between reports and then a jump — and with two writers filling it in turn the
127
+ steps went backwards as often as forwards. Field 2026-09-13, one output over one
128
+ second: `p100:#9..#10`, `p100:#18..#20`, `p100:#16..#18`, `p100:#18..#20`, while
129
+ the second viewer sat frozen at 1673.6 s. The placement follows the map, so the
130
+ machine spent six minutes on 77 encoder starts and 141 stops.
131
+
132
+ PRESENCE IS A SEPARATE FACT and it is the connection: a viewer is here until
133
+ something says otherwise. Silence never says it — a pause, a hidden tab whose
134
+ timers are throttled, and a full cushion are all silent and all still watching.
135
+
136
+ ## Viewers become a map, and that is the whole crossing
137
+
138
+ ```mermaid
139
+ flowchart TB
140
+ subgraph V["services/viewer — where people are"]
141
+ VW[Viewer<br/>position as a function of time, playing, buffered, chosen track]
142
+ VS[Viewers<br/>the relation, indexed from both ends]
143
+ end
144
+
145
+ subgraph P["services/priority — one fact, two scopes"]
146
+ PM[PriorityMap<br/>seconds of film to a rank]
147
+ PO[PriorityOrchestrator<br/>merge, publish, keep]
148
+ end
149
+
150
+ subgraph E["services/encode — where encoders go"]
151
+ CM[CoverageMap<br/>ready / making / free]
152
+ EP[EncodePlan<br/>argmin over arrangements]
153
+ RB[run-budget<br/>what this host can hold]
154
+ ER[EncodeRun<br/>one process, one interval]
155
+ end
156
+
157
+ subgraph D["services/torrent/demand + torrent/download — what the swarm is told"]
158
+ DR[DemandRegister]
159
+ SS[SwarmSelection]
160
+ end
161
+
162
+ VW --> VS
163
+ VS -->|"positions, presence"| PO
164
+ PM --> PO
165
+ PO -->|"per FILE, in bytes"| DR
166
+ DR --> SS
167
+ PO -->|"per OUTPUT, in segments"| EO[EncodeOrchestrator]
168
+ EO --> EP
169
+ CM --> EP
170
+ RB --> EP
171
+ EP -->|"start / stop / move"| ER
172
+ ST[(SegmentStore<br/>the disk)] -->|"what is ready"| CM
173
+ ```
174
+
175
+ ## Two scopes of one map, and both are right
176
+
177
+ The map is built from where the viewers are, once, and read at two scopes.
178
+
179
+ **Per FILE, for the swarm.** The picture, a quality step and a soundtrack of one
180
+ film read the same bytes, so every viewer of any of them wants that file's
181
+ bytes. This is what `PriorityOrchestrator.mapFor(sourceKey, fileIndex)`
182
+ answers, and what is published to the download layer.
183
+
184
+ **Per OUTPUT, for the encoders.** A person watching 480p wants nothing of the
185
+ 1080p output at all. This is `mapForOutput(address)`.
186
+
187
+ One map for both was the second authority over encoders. Handed the film's map,
188
+ the plan wanted an encoder on every output of the film; what actually stopped
189
+ the ones nobody was watching was the session manager killing them by its own
190
+ judgement — and since a viewer moving between steps announces itself, the plan
191
+ started them again on the very next pass. Two parties answering "should this
192
+ encoder exist" by different rules, several times a second.
193
+
194
+ An output nobody is on gets a map with **nothing in it**, which is a statement
195
+ and not an absence: the walk writes one for every output a session exists for,
196
+ including the ones everybody has left. That is how the plan is told to stop what
197
+ is on it.
198
+
199
+ ## Which output a person is consuming
200
+
201
+ A person holds a record on more outputs than they are consuming. The picture is
202
+ where their record lives — the browser addresses it, their chosen soundtrack is
203
+ written on it, their position is read from it — so they are never let go of it;
204
+ but the moment they step down to 480p, the 1080p output is producing for nobody.
205
+
206
+ That distinction is answered where the two facts meet, and neither layer is
207
+ handed the other:
208
+
209
+ - **which step is on their screen** is a fact about a PERSON, read off the
210
+ viewer as one field. The page STATES it (`playingHeight` on the viewer's
211
+ report, sent the moment hls.js has switched), and `Renditions.viewerPlays`
212
+ records it. It used to be inferred from the first segment requested of a
213
+ rung, which is the player fetching rather than the picture having moved;
214
+ - **which output a step supersedes** is a fact about the FILM'S SHAPE, answered
215
+ by `OutputCatalog.supersededBy(session, stepOnScreen)`, which takes a plain id
216
+ and has never seen a viewer.
217
+
218
+ A step, a soundtrack, and a step being warmed are consumed by whoever is
219
+ registered on them — everywhere but the picture, a person who stops watching is
220
+ let go of, so being known to an output is consuming it. Whether an output is
221
+ wanted at all is asked of the viewer registry and nothing else: an output a
222
+ picture made on its own behalf once carried a made-up "family" claim that kept
223
+ it alive, which was a second answer to the same question and is gone. Every
224
+ viewer has a name; a request that names nobody makes no viewer. Through a warm-up both
225
+ the step on screen and the step being made ready are genuinely produced, which
226
+ is the price of the switch not being visible.
227
+
228
+ ## A move between two limits of one height
229
+
230
+ A bitrate limit is part of an output, so a viewer whose link cannot carry the
231
+ limit on their screen is given ANOTHER output of the same height at a lower
232
+ limit — for them alone, and without their player being told (roadmap item 97,
233
+ step 12). The address `v/<height>/…` does not change; what changes is which
234
+ output this side answers it with.
235
+
236
+ 1. **Prepared, not switched.** `Renditions.prepareSameHeightSwitch` opens (or
237
+ finds, by its key) the output of the same size at the next limit of
238
+ `limitsFor(height)` that the viewer's link admits, marks it a step of the
239
+ picture, and registers the viewer on it — that is what buys it an encoder.
240
+ The choice (`Assignments.choose`) is not touched: until the move is made they
241
+ are given what they were given. The move being prepared is a field of the
242
+ viewer, `Viewer.sameHeightSwitch`, and not `warmingVariantId`, which is a
243
+ step their player will switch to itself.
244
+ 2. **Made on the store's own event.** `SegmentStore.onPublished` is filtered by
245
+ the prepared output's key. The move is made when the segment the viewer will
246
+ ask for NEXT is closed on it: one past the highest they were given in the
247
+ viewing they are in now, or — with nothing given in it, straight after a
248
+ seek — the one where they stand. It is read at the moment of the event, so a
249
+ segment closed behind that one, or an event that arrives after a seek, moves
250
+ nothing.
251
+ 3. **One synchronous stretch.** The choice under each height it was chosen
252
+ under, the verdict, the step on screen, the registration on the new output
253
+ and off the old one change with no `await` between them.
254
+ 4. **The output left is not disposed.** The picture is never left. A step that
255
+ is left loses the viewer, the plan stops its encoder by its empty map, and the
256
+ idle expiry removes it once no assignment holds it; a repeat of an address it
257
+ answered is still answered by it, because `given` is read before `chosen`.
258
+ 5. **Cancelled when not wanted.** When the link carries the output on screen
259
+ again, when the prepared output no longer suits, when the player moves to
260
+ another rung, when encoding the prepared output has failed for good, or when
261
+ nothing of the film is left to give from it: the viewer stops watching the
262
+ prepared output and nothing else changes for anybody.
263
+ 6. **Asked again on three events, never on a timer.** A segment closed on the
264
+ prepared output (`SegmentStore.onPublished`); the viewer's own report
265
+ (`Renditions.noteViewerReported`, from the net-report route), which is what
266
+ carries a change of their link; and encoding the prepared output failing for
267
+ good (`Renditions.noteProductionFailed`, told by `EncodeRuns`). Without the
268
+ last two, a move onto an output that closes nothing stood for ever, and the
269
+ quality budget — which leaves a viewer with a move pending alone — never
270
+ decided for them again. Each event may also make the move, on the same one
271
+ condition: the segment they will ask for next is closed. A move is not
272
+ prepared onto an output whose encoding has failed. The fourth way a move can
273
+ wait — no encoder placed on the output — does not arise until the admission
274
+ of step 13 can refuse, and that refusal will be its event.
275
+
276
+ The quality budget uses this lever first, in both directions: down to the
277
+ highest lower limit the link admits, up one limit at a time, and another height
278
+ only where the height on screen has no limit left to move to. A copy has no
279
+ limit and a hardware encode is given none, so for them the lever is still the
280
+ height. The measured table of lower bitrate limits was dropped on 2026-09-27;
281
+ the product currently supplies only the nominal limit through `limitsFor`.
282
+ Per-machine calibration (step 14) qualifies encoder modes and does not create
283
+ lower bitrate limits, so same-height bitrate moves have no target in the current
284
+ product configuration.
285
+
286
+ 7. **A move up waits for the cushion; a move down does not** (roadmap item
287
+ 98). Once the piece asked for next is closed, a move UP is made only when
288
+ the viewer holds the cushion this file needs (`minimumBufferSeconds`), and
289
+ their next report asks again until they do. A move DOWN was started because
290
+ their buffer would run dry, so it is made the moment the piece is ready.
291
+
292
+ ## When the quality budget moves a viewer (roadmap item 98)
293
+
294
+ The page has no quality control: the quality is always automatic. What moves a
295
+ viewer is decided here, per viewer, and each part of it is a measurement.
296
+
297
+ 1. **Judged on the viewer's report, not on a timer.** `QualityController.noteViewerReported`
298
+ runs from the net-report route, for the step down, the picture they see and
299
+ the step up alike. The chosen fifteen-second window of a slow link or a slow
300
+ machine, the 0.95x threshold of slowness, the sixty-second window before a
301
+ step back up, the ten-second buffer threshold, the thirty-second wait after
302
+ every action and the 80 % share of the link are gone. The five-second timer
303
+ only samples the host's load and the torrents' download rates; an encoder's
304
+ price is learned when it closes a piece.
305
+ 2. **Down when the buffer would run dry first, for a reason a smaller output
306
+ removes.** On the buffer's trend it ends sooner than another output could
307
+ close the piece they need, counting the time until their next report
308
+ (`quality/drain-threat.js`), AND either their link carries less than the
309
+ stream they are given, or this machine makes the picture slower than
310
+ realtime over its run's own working time (`encode/RunClock.js`: the time its
311
+ input waited for the swarm and the time it was stopped are taken out). A
312
+ threat neither explains — the swarm is short — is not answered with a
313
+ smaller picture, which reads the same input. The trend is a least-squares fit
314
+ over the shortest run of their reports spanning one segment
315
+ (`viewer/buffer-trend.js`), because the buffer rises a segment at a time and
316
+ two readings catch only the rise or the fall. The time to readiness is the
317
+ observed preparation time of another output of this mode, else this host's
318
+ measured first-segment time. The levers: for the link, a lower limit of the
319
+ same height, then a lower height; for the machine, a lower height. A lower
320
+ height is asked of the player as URGENT — their page switches as soon as the
321
+ rung is ready, without waiting for a cushion. Where nothing can be prepared
322
+ they stay on what they are given, with no message.
323
+ 3. **The picture the viewer sees bounds a re-encode.** The page sends the
324
+ frame it would show without enlarging, in physical pixels; it is kept on
325
+ `Viewer.visiblePicture`. The bound is the smallest rung of the source's
326
+ ladder whose frame is not smaller (`quality/visible-rung.js`). It sets the
327
+ box an output is opened at (`OutputOpening`), the ceiling of every step up,
328
+ and the rung a step down prefers. A copy of the source is never re-encoded
329
+ for being taller than that.
330
+ 4. **A smaller picture moves the viewer only onto a READY rung**
331
+ (`Renditions.heightReadyFor`: the piece they ask for next closed on it);
332
+ with none ready, the next judgement of their link or of the machine applies
333
+ the bound.
334
+ 5. **Up one rung when there is room, every term measured**: the picture on
335
+ their screen is made at least at realtime over its run's own working time (a
336
+ copy is not limited by an encoder), their buffer is not draining and holds
337
+ at least the time another output takes to be ready, their link carries the
338
+ next height, and the picture they see is not already served. A rung this
339
+ host has been measured failing at is not offered, so the step back up cannot
340
+ return to it; no window has to pass.
341
+ 6. **A request stands for as long as its conditions hold**: each judgement
342
+ of the viewer's report asks for what it needs again, and a request it does
343
+ not repeat is let go — a draining buffer for a step up, a buffer that no
344
+ longer drains for a step down, no room for the output, a picture that is no
345
+ longer smaller. No chosen time ends a request. The page drops the move it
346
+ was preparing when the proxy stops asking for it.
347
+ 7. **The page switches a variant when the rung is ready**, and for a request
348
+ that is not urgent only once it holds `minimumBufferSeconds`.
349
+
350
+ ## A place on the machine
351
+
352
+ How many encoders ONE output may run is answered per output
353
+ (`EncodeRuns.maxRunsForOutput`) and is always at least one. What refuses the
354
+ encoder that would slow everybody is `EncodeAdmission`, across every output at
355
+ once (roadmap item 97, step 13).
356
+
357
+ 1. **The unit is seconds of work per second of film**, the one the quality
358
+ offer judges steps in (`EncodeCost.loadOfOutput`). Costs add, and a set of
359
+ encoders is affordable when the machine, corrected for the share of it
360
+ nobody has priced, still makes a second of film per second. A soundtrack is
361
+ a small fraction of a picture; counting processes would refuse a viewer
362
+ their sound. On the addon host the sum reproduces the measurement: two 1080p
363
+ encodes at 1.96x alone cost 1.02 s/s and make 0.98x, measured 0.99x and
364
+ 0.98x.
365
+ 2. **A place is held by an OUTPUT**: by every live encoder at its output's
366
+ cost, and by every output a present viewer is being prepared onto — a step
367
+ being warmed or a move to another limit — once however many wait for it.
368
+ The second is read off the viewers' own records (`Viewers.outputsBeingPrepared`),
369
+ so a place ends with its record on whichever path that record is cleared,
370
+ and there is no register of its own to keep in step.
371
+ 3. **Asked before a preparation is recorded**, in the same synchronous stretch
372
+ as the record: two preparations onto two different outputs with room for
373
+ one cannot both be admitted, because the second is asked after the first is
374
+ written. Refused, a move answers `noPlace` and the budget goes to a lower
375
+ height (never a higher one); a warm-up answers `output-unavailable`.
376
+ 4. **The plan is bounded by it** (`EncodeOrchestrator.#withinMachine`): an
377
+ output may run no more encoders than still fit, and a new one on a full
378
+ machine gets none until one elsewhere ends.
379
+ 5. **An admitted encoder is never taken away**, and a place promised to a
380
+ preparation is kept: the machine's answer is never below what already runs
381
+ on an output, nor below one for an output a preparation holds.
382
+
383
+ Not solved: places go to whoever asks first, so a second encoder catching up on
384
+ one output can hold the place a viewer opening another film waits for.
385
+
386
+ ## What each of the eight other places used to do
387
+
388
+ Before 2026-09-08 the plan was one opinion among nine. Each of these placed or
389
+ killed encoders by a rule of its own; each now states the fact it knows.
390
+
391
+ | it knows | it used to do | it now says |
392
+ |---|---|---|
393
+ | a session was created | start a run at the viewer's position, worked out again | the viewer is placed; the plan reads that |
394
+ | a viewer joined further in | start a run there if nothing was being made | as above |
395
+ | a step or soundtrack is being warmed | point that session at the switch and start it | this person is at N seconds on it |
396
+ | a step was switched to | stop the one left, point the new one | this person is on this step now |
397
+ | a step or track was abandoned | stop its encoder | this person has left that output |
398
+ | the hardware encoder failed | start a run at the dead one's start | what this host encodes with has changed |
399
+ | the input came back | start a run at the last requested segment | decide again |
400
+ | the cut table was corrected | restart the members at the measured instant | this run is producing in the wrong place; the instant is in the file's table |
401
+ | the bitrate cap changed | restart at where the encoder had got to | nothing: a limit is part of an output since 2.87.0, so another limit is another output and no running encoder is ever told a new one |
402
+
403
+ A **seek** is not in that list because it was already reduced to one thing: it
404
+ puts the viewer where they are. The settle timer behind it, its cooldown and its
405
+ one-segment backoff are gone — a second debounce on a signal the browser had
406
+ already debounced, and every millisecond of it was dead time in front of the
407
+ viewer.
408
+
409
+ ## Whether the map is being served in its own order
410
+
411
+ The zones say what matters most. What the viewer actually waited for is measured
412
+ where a viewer measurably waits — the one place in the proxy that holds a
413
+ request for a named segment — and recorded against the rank the map gave that
414
+ segment **at the moment it was asked for**. It reaches the `encode:` state line
415
+ as `served[...]`:
416
+
417
+ ```
418
+ served[now 42 wait(s) median 180ms worst 1900ms, soon 6 wait(s) median 90ms worst 240ms, later none]
419
+ ```
420
+
421
+ Read it like this:
422
+
423
+ | what it says | what is wrong |
424
+ |---|---|
425
+ | long waits at `now` | the urgent zone is not being served first — a fault in whoever acts on the map |
426
+ | long waits at `soon`/`later`, none at `now` | the zones are the wrong width: the urgent one too narrow, so the viewer reaches material only ranked "soon" |
427
+ | `now none` while the viewer is watching | nothing was ever urgent — the map is not reaching this output |
428
+ | a band reading `none` | silence, not a zero, and it is printed as `none` so it cannot be read as "no waits, all good" |
429
+
430
+ Ranks are collapsed into three bands because the map's own scale is as long as
431
+ the film needs — a hundred ranks on a long file — and a hundred-row table says
432
+ nothing a reader can hold. The width of `soon` is a tenth of the top rank, which
433
+ is the map's own shape (its zones widen geometrically) rather than a threshold
434
+ chosen for the table.
435
+
436
+ The download half is measured the same way and on the same scale, so the two are
437
+ comparable: `download-architecture.md`.
438
+
439
+ ## What the master playlist declares, and why it is not cosmetic
440
+
441
+ `BANDWIDTH` and `AVERAGE-BANDWIDTH` per variant, both measured
442
+ (`services/encode/output/rates.js`):
443
+
444
+ - the average is the file's own length over its duration, exact and known when
445
+ the session is created;
446
+ - the peak is the biggest piece produced over the span it covers, and equals the
447
+ average until a piece exists — a ratio invented meanwhile would be the
448
+ fabrication this replaced;
449
+ - a re-encoded height is declared at the cap we impose, which is exact;
450
+ - a smaller height at the pixel share of the source's rate, which errs high, and
451
+ high is the safe direction.
452
+
453
+ It used to be `height * height * 3.2`. **The browser sizes its cushion in BYTES
454
+ from `BANDWIDTH`**, so a figure five times low makes the cushion five times
455
+ shallow: field 2026-09-08, 3.73 Mbit/s declared for a file carrying 18.4, 120 s
456
+ asked bought 56 MB — 26 s of that film — and the deepest the browser ever held
457
+ was 17.1 s. Inflating it is not the answer either: hls.js compares it against
458
+ its own estimate of the link to decide a level is unplayable, and its recovery
459
+ then moves level by itself, which does not honour our pinning.
460
+
461
+ ## The objective is the map's own rank order
462
+
463
+ Not three terms in seconds. One pair per rank the map states, most urgent rank
464
+ first, compared position by position:
465
+
466
+ ```
467
+ [ late(100), done(100), late(99), done(99), … late(1), done(1) ], encoders, wasted
468
+ ```
469
+
470
+ `late(r)` is how long anybody waits past a deadline at rank `r`; `done(r)` is
471
+ when the last number of that rank is made. A difference at a higher rank settles
472
+ it and nothing lower can reopen it — which is the stated order: nobody stares at
473
+ a spinner; then the film in front is encoded as fast as it can be, band by band
474
+ as the map ranks them; then, with what is left over and only then, the film
475
+ behind, in case somebody seeks back.
476
+
477
+ **No weights, and none possible.** A weight would let seconds at one rank buy
478
+ seconds at another, and it would be a figure nobody measured. The map is the
479
+ source of truth about what matters and it already says so — ten ranks on a film,
480
+ p100 at the number a viewer is stopped on, doubling zones down to p91 for the far
481
+ tail, p1 for what lies behind them.
482
+
483
+ `encoders` ranks below every rank of the map, so spare capacity cannot buy an
484
+ encoder where the map is indifferent. `wasted` is the swarm's bill for anything
485
+ fetched twice.
486
+
487
+ ## Why an encoder is not moved for nothing, and it is not a rule
488
+
489
+ There is no threshold here, and there must not be one. What kept an encoder
490
+ moving was three quantities computed wrongly, not a policy that needed tuning.
491
+
492
+ **`delaySec` is when a body finishes the piece it STANDS ON.** For a run that has
493
+ produced something, one piece at the rate in force; for one still warming up, the
494
+ measured time to a first piece less the time it has already been alive. The
495
+ arrival of anything further on is that plus the pieces between, and nothing else.
496
+
497
+ It used to be `delay + (index - at + 1) / rate`, which charges every body a whole
498
+ piece for the one it is already making. That is right for a body that does not
499
+ exist yet and wrong for a run 0.8 s into a 0.9 s piece — and the difference is
500
+ the whole fault: from #58, reaching #59 was priced at 1.89 s against 1.88 s for a
501
+ kill and a cold start, a coin flip lost by ten milliseconds. Priced correctly it
502
+ is 1.08 s against 1.88 s.
503
+
504
+ **One rate, the one the arrangement puts in force.** Concurrent encoders slow
505
+ each other, measured on this host, so a piece costs what it costs at the body
506
+ count the arrangement has. Taken from the unpenalised rate while arrivals used
507
+ the penalised one, an extra body looked cheaper than it is and the plan bought a
508
+ second encoder where one served.
509
+
510
+ **Each body states its own debt** where it is created, as a function of what one
511
+ piece costs — because only there is it known what the body IS, and at the point
512
+ of pricing only how many there are. So there is no kind, no tag and no case
513
+ analysis.
514
+
515
+ **The measured start is separated from the piece it contains**, because the two
516
+ scale differently: a piece costs more when encoders share the machine, a spawn
517
+ does not.
518
+
519
+ ```
520
+ spawn overhead = measured first output - what one piece costs alone
521
+ a fresh encoder = spawn overhead + the piece at the rate in force
522
+ a moved one = the kill, and then the same
523
+ ```
524
+
525
+ **Both figures are measured before any viewer exists**, and until 2026-09-10
526
+ neither was. They were learned only from runs that had ENDED, so at a cold open
527
+ both read zero — and zero does not mean "unknown" here, it means "free".
528
+
529
+ That is not a small bias, it is the whole comparison. Subtract keeping from
530
+ moving and what is left is exactly the killing plus the time the run has already
531
+ lived:
532
+
533
+ ```
534
+ keep a warming run = firstByteWait - elapsed
535
+ move it = kill + spawn overhead + one piece
536
+ = kill + firstByteWait (spawn overhead = firstByteWait - one piece)
537
+ move - keep = kill + elapsed
538
+ ```
539
+
540
+ `elapsed` is the warm-up a move throws away — a process started, an input
541
+ opened, the first bytes fetched, a decoder filled — and it is the term that
542
+ makes moving cost something. Set `firstByteWait` to zero and it cancels out of
543
+ both sides along with the kill: keeping and moving then cost the same figure to
544
+ the millisecond, the tie falls to position, and any advantage however small wins.
545
+
546
+ Field 2026-09-08, the first fifteen seconds of a session: start at #68, a second
547
+ later kill and start at #69, half a second later kill and start at #68 again,
548
+ each dying having produced nothing. Over two days 153 runs were stopped by the
549
+ plan and 68 of them made no segment at all, median life 9.6 s.
550
+
551
+ `services/encode/start-stop-cost.js` measures both from **one ffmpeg run at
552
+ startup**: spawn to the first piece the encoder itself announces closed, then
553
+ SIGTERM to exit. 0.68 s and 0.02 s on the developer's desktop. `RunCosts` starts
554
+ from those and replaces them with readings from real runs as they arrive.
555
+
556
+ There was an `Infinity` here once for the cost of a move, on the reasoning that
557
+ an unmeasured price must not license an irreversible act. It was removed as an
558
+ exception in a model that needs none — correctly, but what replaced it was a
559
+ floor of zero, which licenses the act rather than forbidding it. A measurement
560
+ is what a model like this needs, not an exception and not a floor.
561
+
562
+ And a run killed before producing anything is a measurement too — a lower bound
563
+ on the first output, and the only reading a thrash can supply, since every run in
564
+ one is killed before it finishes anything. So a thrash makes its own moves
565
+ progressively dearer until it stops.
566
+
567
+ ### What that was for
568
+
569
+ Field 2026-09-08: 39 moves in one session, 24 of them between three adjacent
570
+ numbers about 0.8 s apart. One viewer on a host affording three runs got three.
571
+ The picture stood still for 116.7 s in three interruptions, the worst 91.8 s.
572
+
573
+ Compounding it, the map states `withinSeconds: null` for the film behind the
574
+ viewers — nobody is waiting there — and `deadlineReaderFor` read that through
575
+ `Number()`, where `null` is 0. So the film a viewer had already passed was due
576
+ IMMEDIATELY and was the most urgent material in the file: it bought encoders, and
577
+ it took the run standing in front of the viewer because that run was the nearest
578
+ body to it.
579
+
580
+ Checked by simulation over eighty ticks against the map's real shape — ten zones
581
+ doubling ahead of the viewer, one behind — at both one and three runs: the
582
+ encoder is placed once, left alone, and moved exactly once, at the viewer's own
583
+ seek.
584
+
585
+ ## Where a second encoder joins a stretch
586
+
587
+ Derived, not halved. A stretch of unmade film runs from `from` to `to`; whoever
588
+ is already on it stands at `from` and owes `w` before the piece under it exists;
589
+ a fresh one placed at `x` owes `d` — its start and then a whole piece — and both
590
+ then work at the rate two encoders leave each other, which is measured here.
591
+
592
+ ```
593
+ the one already there closes [from, x-1]: w + (x - 1 - from) / r rises with x
594
+ the fresh one closes [x, to]: d + (to - x) / r falls with x
595
+
596
+ x* = (from + to + 1) / 2 + (d - w) * r / 2
597
+ ```
598
+
599
+ The midpoint, shifted forward by half the difference of what the two owe, in
600
+ pieces. Halving is the special case `d = w`, which holds when both are fresh.
601
+
602
+ **The shift is under one segment in every measured configuration** — 0.24 of a
603
+ piece at the addon host's 1080p contention — so halving was very nearly right,
604
+ and saying otherwise would be dressing it up.
605
+
606
+ What the derivation adds is the question halving never asked: **is a second
607
+ encoder worth having at all?**
608
+
609
+ ```
610
+ one: w + (to - from - 1) / rate
611
+ two: w + (x* - 1 - from) / r
612
+ ```
613
+
614
+ Nothing is proposed where the second does not win. At 1920x1080 the measured
615
+ penalty for a second encoder on the addon host is 1.98 — it takes very nearly
616
+ all of the first's speed — and the objective keeps one; where a second is free it
617
+ places three.
618
+
619
+ ## What the log says when anything is placed or taken away
620
+
621
+ ```
622
+ encode-plan on <output>: start #58..#481, stop #?..#?
623
+ [speed=4.45x firstByte=1.26s kill=0.04s refetch=0.000s/s maxRuns=2 live=1]
624
+ ```
625
+
626
+ Every action with its INTERVAL, which is what a run is, and then every term the
627
+ decision was made from. Printed on any pass that does something — a session
628
+ where nothing changes says nothing.
629
+
630
+ The terms are there because an interval says WHAT was decided and only these say
631
+ WHY. A decision of this plan is
632
+
633
+ delay + (index - at) / rate + madeBetween * refetch against a deadline
634
+
635
+ so a recorded decision without the rate can be re-read and not recomputed. That
636
+ is not hypothetical: the one-piece intervals of 2026-09-08 were diagnosed by
637
+ substituting the rate from the speeds the session reported elsewhere — six
638
+ different figures, none of which reproduced the answer the plan had given.
639
+
640
+ A zero in `firstByte`, `kill` or `refetch` is a measurement nobody has taken, not
641
+ a free operation. It is printed so that reading it as free is a choice.
642
+
643
+ **`speed` has one owner: the run.** `EncodeRun.speedX` is the film a run made
644
+ between two closed pieces over its own working time (`encode/RunClock.js`), so
645
+ the time its input waited for the swarm and the time it was stopped are not in
646
+ it. The run announces each reading once (`onSpeedMeasured`); `EncodeCost` learns
647
+ the price of the output from it, the quality budget and the playback forecast
648
+ read the newest reading of the output's runs, and the plan reads the runs
649
+ themselves. Before a run has closed two pieces, and after a restart, the plan
650
+ uses `startingSpeedFor`: what this output was last measured doing alone, or
651
+ what the startup measurements predict. Nothing keeps a copy across restarts,
652
+ and ffmpeg's cumulative `speed=` is read by nothing: it divides by every second
653
+ the encoder was stopped or starved.
654
+
655
+ What the swarm delivers is not in this figure, by construction. It is the
656
+ separate supply term `refetch`, and while that is unmeasured (printed as zero)
657
+ the plan's arrivals assume the input keeps up.
658
+
659
+ It was missing, and its absence cost three wrong diagnoses of one field session.
660
+ The line printed the windows, the budget and where the live runs stood; the
661
+ intervals the actions carried were the one thing it did not print. What that
662
+ session actually did was give every encoder an interval of exactly ONE segment —
663
+ 63 runs, each spending 1.26 s reaching its first piece, making that one piece,
664
+ reaching the end of its interval and exiting, twelve of them normally. The
665
+ reasons printed beside them read as moves back and forth, so the fault was read
666
+ as an oscillating placement three times over. An interval of one segment turns
667
+ the protection against two encoders writing one name into a mill for processes.
668
+
669
+ ## What proves a segment is finished
670
+
671
+ Its NAME, and there is nothing else. A piece being written is called
672
+ `making-<from>-00042.mp4` — the tag is the first number of the stretch its run
673
+ was given — and it takes `segment-00042.mp4` when the encoder says it has closed
674
+ it, which it does on a channel of its own (`-segment_list pipe:3`). Making it
675
+ servable is therefore one rename inside one directory, performed by the store
676
+ because the store owns the disk. The `hls` branch needs nothing extra: its muxer
677
+ writes through a temporary name of its own, so its files appear under their final
678
+ name whole.
679
+
680
+ Three things follow, and each replaced a guess:
681
+
682
+ 1. **a request can never reach a half-written piece.** Closure used to be
683
+ inferred from the NEXT number existing — sound for one writer walking forward,
684
+ false the moment two runs share an output, which is what the plan gives an
685
+ output whenever it places a second encoder. Field 2026-09-08:
686
+ `segment-00057.mp4` served at 2 268 361 bytes and then at 4 510 940, exactly
687
+ half; the browser appended the half and refused the whole for the rest of the
688
+ session, with the picture frozen at 319.66 s;
689
+ 2. **the last piece of a run is provable.** Under the successor rule nothing
690
+ followed it, so it never was — the resume case that held one segment for 46 s
691
+ and then answered 404;
692
+ 3. **clearing up after a dead run is a name match.** Its unfinished pieces are
693
+ the ones carrying its own tag: no stretch to search, no bytes to judge, and no
694
+ way to remove a complete piece somebody else closed. `services/encode/
695
+ open-piece.js` did all three of those by guessing and is gone, along with the
696
+ session manager's copy of it.
697
+
698
+ ## How a request for a segment ends
699
+
700
+ A request steers no encoder. The file is served if the store holds it whole;
701
+ otherwise the request waits for the store's own announcement that the segment
702
+ was published (`SegmentStore.waitFor`), for the waits of that output to be
703
+ invalidated (a viewer left the rung), or for the requester to go. How long it
704
+ may wait is the requester's: the page states it (`X-Hold-Ms`, its own deadline
705
+ less the round trip it has measured) and the route answers "retry" by then. A
706
+ requester that states nothing is held until it closes the connection. There is
707
+ no poll and no hold time chosen by the proxy.
708
+
709
+ Which output a request is answered by is decided in the encoding component
710
+ (`encode/OutputOpening.js`), without knowing who asked; the server operation
711
+ (`server/ViewerRequests.js`) places the requester on it.
712
+
713
+ ## What is checked
714
+
715
+ `test/one-authority.test.js` holds the shape: one caller of `#startEncodeRun`,
716
+ no encoder stopped for being unwatched, the settle machinery absent, each output
717
+ reading its own map, and the soundtrack's start instant read off the table
718
+ rather than handed in.
719
+
720
+ `test/priority-map-per-output.test.js` holds the two scopes, over the real
721
+ viewer registry, the real `OutputCatalog` and the real `PriorityOrchestrator`.
722
+
723
+ `test/encode-plan.test.js` holds the arithmetic, including that every encoder
724
+ stops when nobody is watching the output.
725
+
726
+ `test/segment-store.test.js` holds the naming rule: a piece under its served
727
+ name is finished — the last one of a run included — one under a working name is
728
+ not and cannot be reached, closing it is one rename, and clearing up after one
729
+ run leaves every other run's work alone.
730
+
731
+ ## Per-machine calibration and admission (roadmap item 97, step 14)
732
+
733
+ At startup, `encode/calibration.js` qualifies every mode the proxy may select,
734
+ including the software encoder kept for runtime fallback. Each mode uses the
735
+ production arguments on a generated test clip, and every segment must decode
736
+ independently. `encode/fingerprint.js` keys the results to the FFmpeg and x264
737
+ versions, CPU model and thread count for software, or FFmpeg version, device
738
+ model and driver for hardware. A changed key does not reuse measurements from
739
+ the previous configuration.
740
+
741
+ `encode/throughput.js` measures qualified modes at five frame sizes from
742
+ 256×144 through 3840×2160. Between measured sizes it interpolates and subtracts
743
+ the measured interpolation error; it does not offer a size outside the measured
744
+ range. Calibration stops measuring slower modes and larger sizes once a faster
745
+ mode cannot sustain realtime. If a detected device has no qualified mode, the
746
+ proxy uses software encoding.
747
+
748
+ `encode/LocalObservations.js` stores observations from admitted encodes in the
749
+ proxy's `local-observations.json`. It keeps speed without competing encodes,
750
+ average and peak segment rates, output preparation time and the viewer buffer at
751
+ a transition. A matching fingerprint is required. These observations refine
752
+ output cost, the peak estimate of an unbounded hardware encoder and transition
753
+ preparation timing; an empty or stale file leaves the base policy in force.
754
+
755
+ `encode/EncodeAdmission.js` counts running encoders and outputs that viewers are
756
+ being placed on or prepared to use. Opening an output checks capacity and
757
+ records the viewer in one synchronous call through `OutputOpening` and
758
+ `ViewerRequests`; a second simultaneous opening sees the first reservation. If
759
+ no measured mode or remaining capacity is available, the proxy answers 409
760
+ `no-capacity` before playback. Unknown costs on occupied outputs count as
761
+ unknown capacity, not free capacity. The health report exposes safe encoding
762
+ headroom, and the pool's file-specific answer uses the same occupied-cost data.
763
+ If the opening is refused, the page asks the pool and prepares the file again
764
+ through an eligible proxy before showing video. Each refused proxy is excluded
765
+ for that opening attempt, so stale pool answers cannot make the page cycle.