@torrent-tv/proxy 2.87.21 → 2.88.1

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