@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,1366 +1,1366 @@
1
- /**
2
- * @file How many encoders there should be on one output, and where each of them
3
- * belongs — decided from numbers alone.
4
- *
5
- * The decision is separated from carrying it out on purpose. Every rule below
6
- * was previously a condition somewhere inside an eleven-thousand-line file,
7
- * reachable only by starting a real ffmpeg, and each of them was written for
8
- * one viewer:
9
- *
10
- * - a run was placed at the position of whoever asked, and never at the first
11
- * thing missing, so a viewer moving into a stretch already on disk restarted
12
- * an encoder to make it a second time;
13
- * - a run had no end at all — neither `-to` nor `-t` appeared anywhere — so it
14
- * ran until something killed it, and two runs on one output could not exist
15
- * without writing over each other;
16
- * - nothing stopped a run that had caught up with material somebody else had
17
- * already made.
18
- *
19
- * The rule this file exists to express, stated by the user 2026-09-04:
20
- *
21
- * > Viewers are always independent and always reuse what can be reused. The
22
- * > number of encoders is however many are needed; how many are needed follows
23
- * > from which sets of output parameters are wanted and where the viewers stand
24
- * > inside each. Segments produced by ANY encoder are available to ANY viewer,
25
- * > and which viewer asked never enters the question.
26
- *
27
- * So no name of a viewer reaches this file. It is given what is wanted, what
28
- * exists, what is being made, and what the machine can afford.
29
- *
30
- * **A viewer decides the ORDER the map is walked in and, through the budget, how
31
- * many processes walk it. Nothing else.** Stated by the user 2026-09-05, and it
32
- * is the rule the rest of this file now follows: while a file is being encoded
33
- * it is encoded WHOLE, in the order the map dictates. Who wants which segment
34
- * decides which gap is closed first, never whether a run may go on living.
35
- *
36
- * **A run is therefore never stopped for standing outside a viewer's window.**
37
- * It used to be, and the two decisions that produced that were in direct
38
- * contradiction — measured in the field 2026-09-05 on a viewer watching an
39
- * episode:
40
- *
41
- * 1. this file commanded a start inside the window, at #46;
42
- * 2. `planRunInterval` in the session manager moved the start to #78, because
43
- * it counted a suspended run's claim as reaching `head + look-ahead`;
44
- * 3. this file then saw a run at #78 against a window of [27, 57], found no
45
- * overlap, and killed it as "nothing it was given is wanted";
46
- * 4. neither coverage nor demand had changed, so the same start was commanded
47
- * again — 350-700ms per cycle, dozens of times, no segment ever produced,
48
- * the viewer's picture stopped for 125 seconds.
49
- *
50
- * Both of those other authorities are gone (roadmap item 76, step 5). What is
51
- * left is this file, and the only reasons it stops a run are: nobody is
52
- * watching the output at all; the machine affords fewer processes; or there is
53
- * nothing left unmade anywhere in the track.
54
- *
55
- * **A run's end comes from the coverage**, never from a window: it runs until
56
- * it meets material somebody else has made or is making, or until the end of
57
- * the film.
58
- */
59
-
60
- /**
61
- * One encoder that is running now.
62
- *
63
- * @typedef {object} LiveRun
64
- * @property {string} id
65
- * @property {number} from - The first number it was given.
66
- * @property {number} to - The last number it was given, inclusive.
67
- * @property {number} head - The next number it will produce. Its position.
68
- * @property {number} speedX - Measured encode speed against realtime, from
69
- * ffmpeg's own progress. Zero or less means nothing has measured it yet, and
70
- * then no comparison involving its speed can be made.
71
- */
72
-
73
- /**
74
- * What a viewer is waiting for. Which viewer is deliberately absent.
75
- *
76
- * @typedef {object} WantedSpan
77
- * @property {number} from
78
- * @property {number} to
79
- */
80
-
81
- /**
82
- * @typedef {{ type: "start", from: number, to: number, because: string }
83
- * | { type: "move", run: object, from: number, to: number, because: string }
84
- * | { type: "stop", run: object, because: string }
85
- * | { type: "keep", run: object, from: number, to: number }} PlanAction
86
- */
87
-
88
- /**
89
- * Decide what to do with the encoders on one output.
90
- *
91
- * @param {object} params
92
- * @param {import("./CoverageMap.js").CoverageMap} params.coverage - What has
93
- * been made and what is being made.
94
- * @param {WantedSpan[]} params.windows - What viewers are waiting for, one
95
- * window each. Empty means nobody is watching this output.
96
- * @param {LiveRun[]} params.runs - The encoders running on it now.
97
- * @param {number} params.maxRuns - How many encoders this machine can afford on
98
- * this output. Comes from the same arithmetic that decides the quality offer;
99
- * it is measured per host and never chosen here.
100
- * @param {number} params.segmentSeconds - How much film one segment holds.
101
- * @param {number} [params.killCostSec] - How long stopping an encoder takes,
102
- * measured on this host from its own runs. Zero until something has measured
103
- * it, which makes moving one look cheaper than it is and is said here so the
104
- * bias is known.
105
- * @param {number} [params.now] - The clock, injected. This layer is arithmetic
106
- * and reads no clock of its own; how old a run is is one of its inputs.
107
- * @param {number} [params.firstByteWaitSec] - How long a fresh encoder takes to
108
- * produce anything: process start, opening the input, and the first piece.
109
- * Measured the same way. It replaced a constant of 0.12 s taken from one
110
- * host and charged to every other.
111
- * @param {(others: number) => number} [params.contentionPenaltyFor] - How much
112
- * slower ONE encoder runs with that many others beside it, measured on this
113
- * host. Without it every extra process looks free, and the score then wants an
114
- * encoder per piece: at exactly realtime each next piece is marginally late
115
- * however many are running, so another one always seemed to help a little.
116
- * Unmeasured is 1, and then the budget is the only thing bounding the count.
117
- * @returns {PlanAction[]} Stops first, then moves, then starts, so that a plan
118
- * carried out in order never holds two encoders where it means to hold one.
119
- */
120
- export function planEncoders({
121
- coverage,
122
- windows,
123
- runs,
124
- maxRuns,
125
- segmentSeconds,
126
- killCostSec = 0,
127
- firstByteWaitSec = 0,
128
- now = Date.now(),
129
- refetchSecPerFilmSecond = 0,
130
- contentionPenaltyFor = () => 1,
131
- speedX = 0
132
- }) {
133
- /** @type {PlanAction[]} */
134
- const stops = [];
135
- /** @type {PlanAction[]} */
136
- const moves = [];
137
- /** @type {PlanAction[]} */
138
- const starts = [];
139
- /** @type {PlanAction[]} */
140
- const keeps = [];
141
-
142
- const wanted = Array.isArray(windows) ? windows : [];
143
- const live = Array.isArray(runs) ? runs : [];
144
-
145
- // Nobody is watching this output: every encoder on it is making segments for
146
- // no one. This is the case a look-ahead cannot answer, because look-ahead
147
- // asks how far AHEAD of a viewer a run is and there is no viewer.
148
- if (wanted.length === 0) {
149
- for (const run of live) {
150
- stops.push({ type: "stop", run, because: "nobody is watching this output" });
151
- }
152
- return stops;
153
- }
154
-
155
- const untilNeeded = deadlineReaderFor(wanted, segmentSeconds);
156
- // Segments produced per second, from the fastest measured encoder here.
157
- // Seconds of film per second, divided by the film one piece holds.
158
- //
159
- // A RUN WORKING ON THIS OUTPUT OUTRANKS THE BENCHMARK. The startup figure is
160
- // what this host does on reference clips; a run here is what it does on THIS
161
- // material, and that is the more specific statement. Taken as a floor instead
162
- // — the larger of the two — an encoder reporting half realtime was scored as
163
- // though it ran at twice, and nothing was ever late.
164
- const working = live.reduce((best, run) => Math.max(best, run.speedX || 0), 0);
165
- const rate = segmentSeconds > 0 ? (working > 0 ? working : speedX) / segmentSeconds : 0;
166
- // How long one piece takes AT THE RATE ACTUALLY IN FORCE. Concurrent encoders
167
- // slow each other — measured on this host — so an arrangement's own encoder count
168
- // decides it, and the delays below are computed per arrangement for that
169
- // reason. Taken from the unpenalised rate instead, a piece looked cheaper the
170
- // more encoders there were, which made extra encoders look free: the plan bought a
171
- // second encoder where one served, and the arrivals it was compared on were
172
- // computed at the slower rate all along.
173
- //
174
- // `Infinity` where nothing has been measured, which is what "no speed" means
175
- // and what makes every arrangement equally hopeless rather than equally free.
176
- const pieceAt = (howManyEncoders) => {
177
- const inForce = rate / contentionPenaltyFor(Math.max(0, howManyEncoders - 1));
178
- return inForce > 0 ? 1 / inForce : Number.POSITIVE_INFINITY;
179
- };
180
- // What a encoder costs to take away from where it stands and put somewhere else:
181
- // its death, the start of another, and the wait for the first bytes there.
182
- // Taking an encoder somewhere else is stopping this one and waiting for the
183
- // next to produce. Both halves are measured on this host.
184
- // WHAT A START AND A MOVE OWE BEFORE THE PIECE THEY STAND ON EXISTS, and
185
- // neither is infinite.
186
- //
187
- // The measured `firstByteWaitSec` is spawn to first piece, so it already
188
- // contains one piece's encoding. Separated, because the two scale differently:
189
- // the piece costs more when encoders share the machine, the spawn does not.
190
- //
191
- // spawn overhead = measured first output - what one piece costs alone
192
- // a fresh encoder owes spawn overhead + the piece at the rate in force
193
- // a moved one owes the kill, and then the same
194
- //
195
- // With nothing measured the overhead is zero and a fresh encoder owes exactly
196
- // one piece — which is the honest floor rather than a guess: a piece cannot
197
- // appear before it is encoded, and how fast this host encodes is measured
198
- // before any viewer exists. There was an `Infinity` here for the cost of a
199
- // move, on the reasoning that an unmeasured price must not license an
200
- // irreversible act; it was an exception in a model that needs none, and this
201
- // is the same statement made by arithmetic.
202
- const spawnOverheadSec = Math.max(0, firstByteWaitSec - (rate > 0 ? 1 / rate : 0));
203
- const moveSec = killCostSec + spawnOverheadSec;
204
-
205
- // WHAT A RUN STILL HAS TO GO BEFORE IT PRODUCES ANYTHING — the measured time
206
- // to a first piece, less the time it has already been alive.
207
- //
208
- // This is the memory the score was missing. It is computed afresh whenever
209
- // anything changes, and every arrangement used to be priced as though it were
210
- // the last decision anybody would take: a run that started 10 ms ago was
211
- // assumed to produce instantly, so killing it and starting another looked like
212
- // a straight gain. A move is justified by a benefit that arrives when the
213
- // moved run produces something; taken again before it has, the benefit is
214
- // never collected and the cost is paid twice, three times, forty times.
215
- //
216
- // A run 0.8 s old has 0.14 s left to go against 0.94 s to move it, so it is
217
- // left alone; one working for half a minute has nothing left, and a move
218
- // happens exactly when the film it would reach sooner is worth the restart. No
219
- // state to keep and nothing to choose: one measured figure minus elapsed time.
220
- const finishesItsPieceIn = (run, perPiece) => {
221
- // WHEN THIS RUN FINISHES THE PIECE IT IS STANDING ON.
222
- //
223
- // Two cases and neither is a choice: a run that has produced something is
224
- // working at a known rate, so the piece under it lands within one piece's
225
- // worth of time; a run that has produced nothing is still in its warm-up,
226
- // and what is left of that is the measured time to a first piece less the
227
- // time it has already been alive.
228
- //
229
- // This is the memory the score was missing. It is computed afresh whenever
230
- // anything changes, and each arrangement used to be priced as though it were
231
- // the last decision anybody would take: a run 0.8 s into a 0.9 s piece was
232
- // charged a whole piece for it, the same as one about to start, so killing
233
- // it and beginning again elsewhere looked cheaper by ten milliseconds. A
234
- // move is justified by a benefit that arrives when the moved run produces
235
- // something; taken again before it has, the benefit is never collected and
236
- // the cost is paid twice, three times, forty times.
237
- //
238
- // One measured figure minus elapsed time. Nothing to keep and nothing to
239
- // choose.
240
- if (Number(run.head) !== Number(run.from)) {
241
- return perPiece;
242
- }
243
- const startedAt = Number(run.startedAt);
244
- if (!Number.isFinite(startedAt) || startedAt <= 0) {
245
- return firstByteWaitSec;
246
- }
247
- return Math.max(0, firstByteWaitSec - (now - startedAt) / 1000);
248
- };
249
-
250
- // WHAT EACH BODY OWES BEFORE THE PIECE IT STANDS ON EXISTS, priced at the rate
251
- // the arrangement itself puts in force.
252
- //
253
- // Each encoder states its own debt where it is created, as a function of what one
254
- // piece costs — because only there is it known what the encoder IS, and here only
255
- // how many of them there are. So there is nothing to dispatch on: no kind, no
256
- // tag, no case analysis. The count is known before any debt is needed, which
257
- // is why this is one pass over the encoders rather than a figure computed once
258
- // outside.
259
- const priced = (encoders) => {
260
- const perPiece = pieceAt(encoders.length);
261
- return encoders.map((encoder) => ({ at: encoder.at, delaySec: encoder.owes(perPiece) }));
262
- };
263
-
264
-
265
- // WHERE A SECOND ENCODER MEETS THE ONE ALREADY ON A STRETCH, and whether it is
266
- // worth having.
267
- //
268
- // A stretch of unmade film runs from `from` to `to`. Whoever is already on it
269
- // stands at `from` and owes `w` before the piece under it exists; a fresh one
270
- // placed at `x` owes `d` — its start, and then a whole piece — and both then
271
- // work at the rate two encoders leave each other, which is measured on this
272
- // host and is not half by assumption.
273
- //
274
- // the one already there closes [from, x-1]: w + (x - 1 - from) / r
275
- // the fresh one closes [x, to]: d + (to - x) / r
276
- //
277
- // The first rises with `x` and the second falls, so the stretch is closed
278
- // soonest where they cross:
279
- //
280
- // x* = (from + to + 1) / 2 + (d - w) * r / 2
281
- //
282
- // The midpoint, shifted forward by half the difference of what the two owe,
283
- // expressed in pieces. Halving is the special case `d = w`, which holds when
284
- // both are fresh — and it was applied to every case, including the common one
285
- // where `d` is a start plus a piece and `w` is the tail of a piece already
286
- // being made.
287
- //
288
- // AND WHETHER TO SPLIT AT ALL, which halving never asked. Two encoders under
289
- // contention against one at full speed:
290
- //
291
- // one: w + (to - from - 1) / rate
292
- // two: w + (x* - 1 - from) / r
293
- //
294
- // Nothing back where the second does not win. On the addon host at 1920x1080
295
- // the measured penalty for a second encoder is 1.98 — it takes very nearly
296
- // all of the first's speed — so there a second almost never pays, and it was
297
- // being placed regardless.
298
- const splitAt = (from, to) => {
299
- const pieces = to - from + 1;
300
- if (!(pieces > 1) || !(rate > 0)) {
301
- return null;
302
- }
303
- const together = rate / contentionPenaltyFor(live.length);
304
- if (!(together > 0)) {
305
- return null;
306
- }
307
- const perPieceTogether = 1 / together;
308
- // What the one already on this stretch owes, or a fresh one's debt where
309
- // nobody is on it — and then both sides owe the same and the meeting point
310
- // is the middle, as it should be.
311
- const onIt = live.find((run) => Number(run.head) === from) ?? null;
312
- const owedThere = onIt
313
- ? finishesItsPieceIn(onIt, perPieceTogether)
314
- : firstByteWaitSec + perPieceTogether;
315
- const owedFresh = firstByteWaitSec + perPieceTogether;
316
- const meeting = (from + to + 1) / 2 + ((owedFresh - owedThere) * together) / 2;
317
- const x = Math.min(to, Math.max(from + 1, Math.round(meeting)));
318
- const withTwo = owedThere + (x - 1 - from) * perPieceTogether;
319
- const withOne = owedThere + (pieces - 1) / rate;
320
- return withTwo < withOne ? x : null;
321
- };
322
-
323
- // ------------------------------------------------------------------ WHERE
324
- //
325
- // A question about the FILM, and about nothing else: which numbers are
326
- // missing, when each is needed, how fast this machine encodes, how many
327
- // processes it can hold. No encoder that happens to be running enters it,
328
- // which is why it can be answered by arithmetic.
329
- const positions = placeEncoders({
330
- coverage,
331
- windows: wanted,
332
- howMany: maxRuns,
333
- // EVERY LIVE ENCODER IS PRE-PLACED, because that is what "somebody already
334
- // gets here in time" means. A number one of them reaches before it is
335
- // needed is not a position at all; a number none of them reaches is, and
336
- // needs a encoder brought to it. There is no third case, and in particular no
337
- // separate question of whether an encoder should drive on or be moved:
338
- // driving is simply its arrival, and its arrival is priced in one place.
339
- firstGap: gapFinderFor(coverage, new Set(live), rate, segmentSeconds * refetchSecPerFilmSecond),
340
- deadlineAt: untilNeeded,
341
- splitAt
342
- });
343
-
344
- // -------------------------------------------------------------------- WHO
345
- //
346
- // ARGMIN OF THE OBJECTIVE, EVALUATED. Not a rule that approximates it.
347
- //
348
- // Every way of filling the positions is scored by `latenessOf` and the best is
349
- // taken. There are at most a handful of positions and a handful of encoders, so
350
- // the enumeration is exact: no local rule stands in for the objective, and
351
- // none can therefore disagree with another.
352
- //
353
- // Four such rules were written before this and all four had to go — "place
354
- // where a number is late", "take a encoder that serves nothing", "take one whose
355
- // work is needed later than this", "drive on or move, by cost". Each looked
356
- // like a consequence of the model and each approximated it from a different
357
- // side, so together they contradicted one another and the answer depended on
358
- // which ran first.
359
- /** One decision per live encoder, so none can be decided twice. @type {Map<object, PlanAction>} */
360
- const decided = new Map();
361
- const room = Math.max(0, maxRuns - live.length);
362
- const refetchPerSegment = segmentSeconds * refetchSecPerFilmSecond;
363
-
364
- let arrangements = [{ fill: [], used: new Set(), fresh: 0 }];
365
- for (let index = 0; index < positions.length; index += 1) {
366
- const next = [];
367
- for (const arrangement of arrangements) {
368
- next.push({ fill: [...arrangement.fill, null], used: arrangement.used, fresh: arrangement.fresh });
369
- // A FRESH PROCESS IS OFFERED BEFORE ANY WORKING BODY, so that when the two
370
- // score the same the working one is left alone. Taking it is free in the
371
- // arithmetic — its output stays on disk — but it is not free in fact: the
372
- // run it belongs to has a position, a warm input and a measured speed, and
373
- // all three are thrown away for nothing.
374
- if (arrangement.fresh < room) {
375
- next.push({
376
- fill: [...arrangement.fill, "new"],
377
- used: arrangement.used,
378
- fresh: arrangement.fresh + 1
379
- });
380
- }
381
- for (const run of live) {
382
- if (arrangement.used.has(run)) {
383
- continue;
384
- }
385
- next.push({
386
- fill: [...arrangement.fill, run],
387
- used: new Set([...arrangement.used, run]),
388
- fresh: arrangement.fresh
389
- });
390
- }
391
- }
392
- arrangements = next;
393
- }
394
-
395
- let best = null;
396
- let bestScore = null;
397
- for (const arrangement of arrangements) {
398
- const encoders = [];
399
- for (let index = 0; index < positions.length; index += 1) {
400
- const filler = arrangement.fill[index];
401
- if (filler === null) {
402
- continue;
403
- }
404
- if (filler === "new") {
405
- // A encoder that does not exist yet owes its own start and then the piece.
406
- encoders.push({ at: positions[index], owes: (piece) => spawnOverheadSec + piece });
407
- continue;
408
- }
409
- const head = Number(filler.head);
410
- // Left where it stands it owes what is left of the piece under it; taken
411
- // somewhere else it owes the killing, the start and a whole piece.
412
- encoders.push({
413
- at: positions[index],
414
- owes: head === positions[index]
415
- ? (piece) => finishesItsPieceIn(filler, piece)
416
- : (piece) => moveSec + piece
417
- });
418
- }
419
- // Bodies nobody was given a position for go on working where they stand,
420
- // and their coverage counts: the file is encoded whole.
421
- //
422
- // A encoder given no end pays a restart the moment anybody is placed inside the
423
- // road it would drive: where a run stops is fixed when its process starts,
424
- // so it has to be cut and begun again at its own head. That price was
425
- // invisible here, and an arrangement was scored as free when it was not.
426
- for (const run of live) {
427
- if (arrangement.used.has(run)) {
428
- continue;
429
- }
430
- const head = Number(run.head);
431
- const endless = Number(run.to) < Number(run.from);
432
- const cutInFront = arrangement.fill.some((filler, index) =>
433
- filler !== null && positions[index] > head
434
- && (endless || positions[index] <= Number(run.to)));
435
- encoders.push({
436
- at: head,
437
- owes: cutInFront
438
- ? (piece) => moveSec + piece
439
- : (piece) => finishesItsPieceIn(run, piece)
440
- });
441
- }
442
- const scored = latenessOf(priced(encoders), coverage, wanted, untilNeeded, rate / contentionPenaltyFor(Math.max(0, encoders.length - 1)), refetchPerSegment, segmentSeconds);
443
- if (bestScore === null || cheaperThan(scored, bestScore)) {
444
- bestScore = scored;
445
- best = arrangement;
446
- }
447
- }
448
-
449
- // A BODY STANDING ON FILM THAT EXISTS is the one arrangement the enumeration
450
- // above cannot reach: the gap in front of it is nobody's deadline, so it is
451
- // never a position, and the encoder is left to make three hundred pieces a second
452
- // time. Each such encoder is offered its own first gap and the SAME score decides
453
- // — moving costs a restart on everything downstream, staying costs the repeat.
454
- //
455
- // Offered one at a time rather than folded into the enumeration because the
456
- // enumeration is exponential in the number of positions, and this is called
457
- // again on every piece produced. One extra evaluation per encoder against
458
- // several thousand arrangements is the difference between arithmetic and a
459
- // stalled proxy.
460
- const placement = new Map();
461
- for (let index = 0; index < positions.length; index += 1) {
462
- const filler = best ? best.fill[index] : null;
463
- if (filler && filler !== "new") {
464
- placement.set(filler, positions[index]);
465
- }
466
- }
467
- const encodersOf = (override) => {
468
- const encoders = [];
469
- for (const run of live) {
470
- if (override.has(run) && override.get(run) === null) {
471
- // Asked what the film looks like WITHOUT this one.
472
- continue;
473
- }
474
- const at = override.has(run) ? override.get(run) : (placement.get(run) ?? Number(run.head));
475
- const head = Number(run.head);
476
- encoders.push({
477
- at,
478
- owes: at === head
479
- ? (piece) => finishesItsPieceIn(run, piece)
480
- : (piece) => moveSec + piece
481
- });
482
- }
483
- for (let index = 0; index < positions.length; index += 1) {
484
- if ((best ? best.fill[index] : null) === "new") {
485
- encoders.push({ at: positions[index], owes: (piece) => spawnOverheadSec + piece });
486
- }
487
- }
488
- return priced(encoders);
489
- };
490
- const scoreOf = (override) => {
491
- const encoders = encodersOf(override);
492
- return latenessOf(encoders, coverage, wanted, untilNeeded,
493
- rate / contentionPenaltyFor(Math.max(0, encoders.length - 1)), refetchPerSegment, segmentSeconds);
494
- };
495
- for (const run of live) {
496
- if (placement.has(run)) {
497
- continue;
498
- }
499
- const gap = coverage.firstGapFrom(run.head, undefined, run);
500
- if (gap === null || gap === Number(run.head)) {
501
- continue;
502
- }
503
- const asIs = scoreOf(new Map());
504
- const moved = scoreOf(new Map([[run, gap]]));
505
- if (cheaperThan(moved, asIs)) {
506
- placement.set(run, gap);
507
- }
508
- }
509
-
510
- /**
511
- * Would the film be worse off without this encoder? Asked of the same score.
512
- *
513
- * @param {object} run
514
- * @returns {boolean}
515
- */
516
- const worseWithout = (run) => {
517
- const kept = encodersOf(new Map());
518
- const without = encodersOf(new Map([[run, null]]));
519
- const scoreOf_ = (encoders) => latenessOf(encoders, coverage, wanted, untilNeeded,
520
- rate / contentionPenaltyFor(Math.max(0, encoders.length - 1)), refetchPerSegment, segmentSeconds);
521
- return cheaperThan(scoreOf_(kept), scoreOf_(without));
522
- };
523
-
524
- const stretchAt = (from) => endOfStretch(from, Math.min(
525
- coverage.unmadeRunFrom(from),
526
- coverage.freeRunFrom(from, new Set(live))
527
- ));
528
-
529
- for (let index = 0; index < positions.length; index += 1) {
530
- if ((best ? best.fill[index] : null) !== "new") {
531
- continue;
532
- }
533
- const at = positions[index];
534
- starts.push({
535
- type: "start",
536
- from: at,
537
- to: stretchAt(at),
538
- because: `#${at} is wanted and nobody reaches it in time`
539
- });
540
- }
541
-
542
- for (const run of live) {
543
- const head = Number(run.head);
544
- const at = placement.has(run) ? placement.get(run) : head;
545
- if (at !== head) {
546
- decided.set(run, {
547
- type: "move",
548
- run,
549
- from: at,
550
- to: stretchAt(at),
551
- because: `standing at #${head} scores worse than standing at #${at}, counting ` +
552
- "both how late the film would be and the work that would be done twice"
553
- });
554
- continue;
555
- }
556
- // It stays where it is — unless holding it changes nothing.
557
- //
558
- // A encoder left over from where a viewer used to be goes on costing the
559
- // machine a process while another encoder already reaches everything it
560
- // would. The score says so directly: take it away and see. Removing it is
561
- // refused the moment it makes anything later or leaves film abandoned, so
562
- // this cannot quietly drop the encoder somebody is waiting on.
563
- if (!placement.has(run) && !worseWithout(run)) {
564
- stops.push({
565
- type: "stop",
566
- run,
567
- because: "the film is no worse off without it"
568
- });
569
- continue;
570
- }
571
- decided.set(run, { type: "keep", run, from: head, to: run.to });
572
- }
573
-
574
- // THE MACHINE'S LIMIT BINDS, whatever the map wants. It is measured — the
575
- // processor, the swarm and the piece store each give a figure and the smallest
576
- // wins — and an encoder over it is one the host cannot feed. Which of them
577
- // goes is the same question as any other here: the one the film misses least,
578
- // by the same score.
579
- while (decided.size + starts.length > maxRuns) {
580
- let cheapest = null;
581
- let cheapestScore = null;
582
- for (const [run, action] of decided) {
583
- if (action.type !== "keep") {
584
- continue;
585
- }
586
- const without = latenessOf(encodersOf(new Map([[run, null]])), coverage, wanted, untilNeeded,
587
- rate / contentionPenaltyFor(Math.max(0, live.length - 2)), refetchPerSegment, segmentSeconds);
588
- if (cheapestScore === null || cheaperThan(without, cheapestScore)) {
589
- cheapestScore = without;
590
- cheapest = run;
591
- }
592
- }
593
- if (cheapest === null) {
594
- break;
595
- }
596
- decided.delete(cheapest);
597
- placement.delete(cheapest);
598
- stops.push({
599
- type: "stop",
600
- run: cheapest,
601
- because: `the machine holds ${maxRuns} encoder(s) on this output and this is the one ` +
602
- "the film misses least"
603
- });
604
- }
605
-
606
- for (const action of decided.values()) {
607
- if (action.type === "keep") {
608
- keeps.push(action);
609
- } else {
610
- moves.push(action);
611
- }
612
- }
613
-
614
- // ONE ENCODER'S WORK ENDS WHERE THE NEXT ONE'S BEGINS.
615
- //
616
- // A free stretch may run to the end of the track, and an encoder given all of
617
- // it stands in the road of every encoder placed behind it: they write the
618
- // same names, and each one's output is the other's "material somebody else
619
- // made", so they stop one another. Field 2026-09-05: three encoders started
620
- // on one track within 200 ms, each into the road another was already writing.
621
- // Fifteen readers on a piece store that holds sixteen pieces followed, half
622
- // of all evictions took a piece a reader had declared, and `/stream` began
623
- // handing out bytes that were not the file's.
624
- //
625
- // The bound is taken from the NEXT ENCODER'S START, not from a band edge: a
626
- // band edge travels with the viewer, so every step forward would leave a
627
- // sliver just past the previous encoder and buy an encoder for it.
628
- // A RUN THAT IS STAYING IS IN THE SORT TOO, because its road can be taken.
629
- //
630
- // It used to be left out, on the reading that a run staying put keeps what it
631
- // was given. That is true of the stretch it was GIVEN and false of the road it
632
- // will actually drive: a run with no end carries no `-to` and walks to the end
633
- // of the film, so an encoder placed in front of it writes the same names.
634
- const placed = [...moves, ...starts, ...keeps].sort(
635
- (left, right) => /** @type {any} */ (left).from - /** @type {any} */ (right).from
636
- );
637
- for (let index = 0; index < placed.length - 1; index += 1) {
638
- const here = /** @type {{ type: string, run?: object, from: number, to: number }} */ (placed[index]);
639
- const next = /** @type {{ from: number }} */ (placed[index + 1]);
640
- if (here.to >= 0 && here.to < next.from) {
641
- continue;
642
- }
643
- here.to = next.from - 1;
644
- if (here.type !== "keep") {
645
- continue;
646
- }
647
- // SHORTENING A LIVE RUN'S ROAD MEANS STOPPING IT, not merely writing a
648
- // smaller number down. Where a run's end goes is fixed when its process
649
- // starts, so one that was given none keeps producing past any bound decided
650
- // later and would write one piece into the new encoder's road — two
651
- // processes on one name, which is the collision this whole pass exists to
652
- // prevent. So it ends here and begins again at its own head with a real end;
653
- // the viewer in front pays a restart, which is a cost this file already
654
- // prices rather than a interruption nobody counted.
655
- keeps.splice(keeps.indexOf(here), 1);
656
- moves.push({
657
- type: "move",
658
- run: here.run,
659
- from: here.from,
660
- to: here.to,
661
- because:
662
- `an encoder is needed at #${next.from}, which this run would reach only by ` +
663
- "encoding through; it takes the road up to there and ends by itself"
664
- });
665
- }
666
-
667
- return [...stops, ...moves, ...starts, ...keeps];
668
- }
669
-
670
-
671
-
672
- /**
673
- * THE OBJECTIVE, as a value that can be compared.
674
- *
675
- * THREE COUNTS OF SECONDS, COMPARED IN ORDER. The order is the user's, stated
676
- * 2026-09-06, and a later count decides only where the earlier ones tie:
677
- *
678
- * 1. SECONDS ANYBODY SPENDS LOOKING AT A SPINNER. Nothing outranks it, at any
679
- * size. Walked forward in film order rather than summed piece by piece: a
680
- * viewer who is stopped is not watching, so a wait moves every deadline
681
- * behind it by its own length;
682
- *
683
- * 2. WHEN THE FILM IN FRONT OF THE VIEWERS IS FINISHED — the last piece of it to
684
- * be made, whichever encoder makes it. A stretch no encoder will ever reach
685
- * counts as never, which is what stops the front being abandoned;
686
- *
687
- * 3. WHEN THE WHOLE FILE IS FINISHED — the film behind the viewers included,
688
- * plus what the swarm pays to fetch anything a second time. Film nobody is
689
- * waiting for still has value: a viewer seeking back into a part that exists
690
- * starts playing at once, and seeking back is what people do in the first
691
- * minutes while they find their place. So spare capacity goes to finishing
692
- * the file. This is where "the file is encoded WHOLE" lives; it used to be a
693
- * penalty for film below the lowest encoder, which said the same thing as a
694
- * patch and said it about one edge of the track only.
695
- *
696
- * WHY AN ENCODER MAY STAND BEHIND A VIEWER while film in front is still unmade:
697
- * encoders work at the same time, so one in front and one behind can finish the
698
- * file sooner than two in front. Where nobody is stalled and the front is closed
699
- * just as fast, the file being done sooner is the answer — count 3 deciding a
700
- * tie in 1 and 2, which is exactly what the order is for.
701
- *
702
- * WHY THE COUNTS ARE COMPARED AND NOT ADDED: seconds of somebody waiting and
703
- * seconds until a distant stretch exists are not the same thing, and no measured
704
- * quantity says how many of one are worth one of the other. Adding them would
705
- * mean choosing that exchange rate, which is inventing a number.
706
- *
707
- * @param {{ at: number, delaySec: number }[]} encoders - Where each encoder would
708
- * stand, and how long before it produces anything there: nothing where it is
709
- * already standing, a move or a start otherwise.
710
- * @param {import("./CoverageMap.js").CoverageMap} coverage
711
- * @param {WantedSpan[]} wanted
712
- * @param {(index: number) => number} untilNeeded
713
- * @param {number} rate - Segments per second. Always a real figure: this host
714
- * measures what it encodes at on startup, before any viewer exists, and every
715
- * run that works then refines it. There is no "unmeasured" case to answer.
716
- * @param {number} refetchSecPerSegment
717
- * @param {number} segmentSeconds
718
- * @returns {{ stall: number, ahead: number, whole: number }} Three counts of
719
- * seconds, compared in that order by {@link cheaperThan}.
720
- */
721
- function latenessOf(encoders, coverage, wanted, untilNeeded, rate, refetchSecPerSegment, segmentSeconds) {
722
- const first = Math.min(...wanted.map((span) => span.from));
723
- const last = Math.max(...wanted.map((span) => span.to));
724
- // What a number nobody reaches at all counts as. The film's own length is the
725
- // honest bound — nothing can be later than never — and a finite figure is what
726
- // lets two hopeless arrangements still be told apart by the rest of the sum.
727
- const never = (last + 1) * segmentSeconds;
728
-
729
- // WHICH SIDE OF THE VIEWERS a piece is on. The map states it; nothing here
730
- // works it out from positions, and nothing here knows where a viewer stands.
731
- //
732
- // It was read off the deadline before — no time stated meant behind — and that
733
- // is true only of a viewer who is playing. A paused viewer has no times
734
- // anywhere, so their whole film read as behind them, "ahead before behind"
735
- // had nothing to compare, and the encoder was free to wander to the start of
736
- // the file. Which side a stretch is on and how soon it is wanted are two
737
- // different facts, and the map states both.
738
- // WHAT RANK THE MAP GIVES THIS NUMBER — the highest, where zones overlap,
739
- // because a number two viewers want is wanted as much as the more urgent of
740
- // them wants it.
741
- //
742
- // This replaced a boolean, "is it behind everybody", and the boolean was the
743
- // whole of what the objective knew about the map's own order. The map states
744
- // ten ranks on a film — p100 at the number a viewer is stopped on, doubling
745
- // zones down to p91 for the far tail, p1 for what lies behind them — and all
746
- // of that was collapsed into two buckets and then converted to seconds, where
747
- // "never" for the film behind is the film's own length. On a 48-minute file
748
- // that is 2024 s, which outvotes everything: field 2026-09-08, one viewer got
749
- // three encoders, two of them on film behind them, and the run serving them
750
- // was killed to make room for one.
751
- const rankAt = (at) => {
752
- let rank = 0;
753
- for (const span of wanted) {
754
- if (at >= span.from && at <= span.to) {
755
- rank = Math.max(rank, Number(span.priority) || 0);
756
- }
757
- }
758
- return rank;
759
- };
760
- // The ranks the map actually states, most urgent first. The comparison is over
761
- // these and nothing else, so a rank can never be outvoted by a lower one
762
- // however many seconds are at stake there.
763
- const ranks = [...new Set(wanted.map((span) => Number(span.priority) || 0))]
764
- .sort((left, right) => right - left);
765
-
766
- // EVERY COUNT IS OVER THE FILM, NOT OVER THE ENCODERS. When a piece is made
767
- // depends on which encoder reaches it soonest, and the encoder that reaches
768
- // film in front of the viewers may well be standing behind them.
769
- //
770
- // Counted over the encoders instead — each charged to the side it stands on —
771
- // the score had a hole that swallowed everything: an arrangement with every
772
- // encoder BEHIND the viewers had nothing charged to the film in front, so its
773
- // second term was zero, which is the best value there is. The plan then
774
- // abandoned the film in front of a viewer and put both encoders at the start
775
- // of the file, which is the opposite of the rule it is supposed to obey.
776
- //
777
- // AND THE WAITING IS WALKED FORWARD, not summed piece by piece.
778
- //
779
- // Not a sum of each piece's own lateness. A viewer who is stopped is not
780
- // watching, so everything after the piece they are stopped on is needed that
781
- // much later too: one wait moves every deadline behind it by its own length.
782
- //
783
- // Summed independently instead, the far tail of a long file outvoted the film
784
- // under the viewer's feet — measured, and it placed the only encoder at #114
785
- // while the viewer stood at #100, because thirteen pieces of certain waiting
786
- // "cost" less than 886 distant pieces arriving a little later. Walking the
787
- // clock forward makes that trade impossible: abandoning the near film delays
788
- // the far film by at least as much.
789
- let stalled = 0;
790
- let wastedSwarm = 0;
791
- /** Seconds anybody waits past a deadline, per rank. @type {Map<number, number>} */
792
- const lateAt = new Map(ranks.map((rank) => [rank, 0]));
793
- /** When the last number of a rank is made, per rank. @type {Map<number, number>} */
794
- const doneAt = new Map(ranks.map((rank) => [rank, 0]));
795
- for (let index = first; index <= last; index += 1) {
796
- // Which encoder gets to this piece first, and when. One standing on it is
797
- // already there; one behind it must work its way up, re-making anything
798
- // already made on the way, which costs its own time and the swarm's.
799
- let soonest = Number.POSITIVE_INFINITY;
800
- let byWhom = null;
801
- for (const encoder of encoders) {
802
- if (encoder.at > index) {
803
- continue;
804
- }
805
- // WHEN THIS BODY REACHES THIS NUMBER. `delaySec` is when it finishes the
806
- // piece it is STANDING ON, so the pieces after it are the only ones still
807
- // to be encoded at `rate`.
808
- //
809
- // It used to be `(index - at + 1) / rate` on top of the delay, which
810
- // charges every encoder a whole piece for the one it is already working on. A
811
- // fresh encoder does start from nothing, so for it that is right — and it is
812
- // now inside its own delay. A run 0.8 s into a 0.9 s piece does not, and
813
- // charging it 0.94 s for that piece is what made moving it look cheaper
814
- // than leaving it: from #58 it was priced at 1.89 s to reach #59 against
815
- // 1.88 s for a kill and a cold start, a difference of ten milliseconds
816
- // that is nothing but the double charge. Field 2026-09-08: 39 moves in one
817
- // session, 24 of them between three adjacent numbers, and the picture
818
- // stood still for 116.7 s.
819
- const arrival = encoder.delaySec
820
- + (index - encoder.at) / rate
821
- + coverage.madeBetween(encoder.at, index) * refetchSecPerSegment;
822
- if (arrival < soonest) {
823
- soonest = arrival;
824
- byWhom = encoder;
825
- }
826
- }
827
- if (coverage.isReady(index)) {
828
- // It exists. Nobody waits for it and nothing is owed — but whoever passes
829
- // over it makes it a second time, and the swarm fetches its bytes again.
830
- if (byWhom !== null) {
831
- wastedSwarm += refetchSecPerSegment;
832
- }
833
- continue;
834
- }
835
- // A piece nobody is working towards arrives never. There is no third case:
836
- // the host measures what it encodes at, and what it copies at, before any
837
- // viewer exists, so a speed is always a real number and an arrival can
838
- // always be computed.
839
- // Nothing arrives later than never, which is the bound the film's own length
840
- // gives. It is a definition rather than a guard: it also makes the score
841
- // total on a host whose startup measured nothing at all, where every arrival
842
- // is beyond reckoning and every arrangement is therefore equally hopeless.
843
- const when = byWhom === null ? never : Math.min(soonest, never);
844
- const rank = rankAt(index);
845
- doneAt.set(rank, Math.max(doneAt.get(rank) ?? 0, when));
846
- const deadline = untilNeeded(index);
847
- if (Number.isFinite(deadline)) {
848
- const due = deadline + stalled;
849
- const waited = Math.max(0, when - due);
850
- lateAt.set(rank, (lateAt.get(rank) ?? 0) + waited);
851
- stalled += waited;
852
- }
853
- }
854
-
855
- return {
856
- // THE MAP'S OWN ORDER, AS A VECTOR. One pair per rank the map states, most
857
- // urgent rank first: how long anybody waits at that rank, then when the last
858
- // number of it is made.
859
- //
860
- // Compared position by position, so a rank is never outvoted by a lower one
861
- // — which is the whole of what was asked for: nobody stares at a spinner;
862
- // then the film in front of the viewers is encoded as fast as it can be, band
863
- // by band as the map ranks them; then, with whatever is left over and only
864
- // then, the film behind them, in case somebody seeks back.
865
- //
866
- // No weights, and none possible: a weight would let seconds at one rank buy
867
- // seconds at another, and it would be a number nobody measured. The map is
868
- // the source of truth about what matters, and it already says so.
869
- byRank: ranks.flatMap((rank) => [lateAt.get(rank) ?? 0, doneAt.get(rank) ?? 0]),
870
- // HOW MANY ENCODERS IT TAKES. Ranked below every rank of the map and above
871
- // the swarm's bill, so it cannot buy one where the map is indifferent — and
872
- // the map IS indifferent about spare capacity, which is what bought an
873
- // encoder for film nobody waits for.
874
- encoders: encoders.length,
875
- // WHAT THE SWARM PAYS for anything fetched twice, which delays everything.
876
- wasted: wastedSwarm
877
- };
878
- }
879
-
880
- /**
881
- * Is the first arrangement cheaper than the second?
882
- *
883
- * Three things in order, stated by the user 2026-09-06: nobody stares at a
884
- * spinner; then the film in front of the viewers is finished soonest; then the
885
- * whole file is. A later one decides only where the earlier ones tie.
886
- *
887
- * That order is why an encoder may stand BEHIND a viewer while film in front is
888
- * still unmade: encoders work at the same time, so one in front and one behind
889
- * can finish the file sooner than two in front — and where nobody is stalled and
890
- * the front is closed just as fast, the file being done sooner is the answer.
891
- *
892
- * @param {{ stall: number, ahead: number, whole: number }} left
893
- * @param {{ stall: number, ahead: number, whole: number }} right
894
- * @returns {boolean}
895
- */
896
- function cheaperThan(left, right) {
897
- // POSITION BY POSITION, in the map's own order of ranks. A difference at a
898
- // higher rank settles it, and nothing at a lower one can reopen it.
899
- //
900
- // There is no margin here and there must not be one. A threshold — "a move
901
- // must beat staying by at least what moving costs" — was written while the
902
- // arrival arithmetic charged a run for a piece it had already half made, and
903
- // it was a prop under a comparison that was wrong rather than indifferent.
904
- // With the arithmetic right the two are 1.08 s against 1.88 s, and nothing
905
- // needs propping.
906
- const size = Math.max(left.byRank.length, right.byRank.length);
907
- for (let index = 0; index < size; index += 1) {
908
- const here = left.byRank[index] ?? 0;
909
- const there = right.byRank[index] ?? 0;
910
- if (here !== there) {
911
- return here < there;
912
- }
913
- }
914
- // Where every rank is served identically, fewer encoders. The map is
915
- // indifferent, so the machine decides: a process, a reader of the piece store
916
- // and the swarm's bandwidth are all paid by the viewers the ranks above are
917
- // about.
918
- if (left.encoders !== right.encoders) {
919
- return left.encoders < right.encoders;
920
- }
921
- return left.wasted < right.wasted;
922
- }
923
-
924
- /**
925
- * How long until a number is needed, read off the map.
926
- *
927
- * The map states it per stretch, for the stretch's NEAR EDGE, because a stretch
928
- * is met at its beginning. Every number inside is needed no sooner than that, so
929
- * taking the stretch's figure for all of them is the safe reading: it can only
930
- * make the filling earlier than it has to be, never later.
931
- *
932
- * Where the map says nothing, nobody is coming and nothing can be late.
933
- *
934
- * Inside a stretch the time GROWS with the distance, because a viewer covers a
935
- * second of film in a second: the number `n` places past the near edge is
936
- * reached `n` segments of film later. Taking the near edge's figure for every
937
- * number inside instead makes a whole stretch due at once — measured while
938
- * building this: the first stretch is as wide as the measured allowance, so its
939
- * far end was demanded instantly and an encoder was placed on a number another
940
- * one was already writing.
941
- *
942
- * @param {WantedSpan[]} windows
943
- * @param {number} segmentSeconds - How much film one number holds.
944
- * @returns {(index: number) => number}
945
- */
946
- /** @param {WantedSpan[]} windows */
947
- function firstOf(windows) {
948
- return Math.min(...windows.map((span) => span.from));
949
- }
950
-
951
- /** @param {WantedSpan[]} windows */
952
- function lastOf(windows) {
953
- return Math.max(...windows.map((span) => span.to));
954
- }
955
-
956
- function deadlineReaderFor(windows, segmentSeconds) {
957
- const perSegment = segmentSeconds > 0 ? segmentSeconds : 0;
958
- return (index) => {
959
- let soonest = Number.POSITIVE_INFINITY;
960
- for (const span of windows) {
961
- if (index < span.from || index > span.to) {
962
- continue;
963
- }
964
- const stated = /** @type {{ withinSeconds?: number }} */ (span).withinSeconds;
965
- // A stretch stated with no time is somebody waiting at its near edge: that
966
- // is what stating one means. The rest of it grows with the distance, the
967
- // same as a stated one — read as due all at once instead, a window as wide
968
- // as a viewer's cushion demanded its far end instantly and bought an
969
- // encoder to stand beside one already working.
970
- // `null` IS A STATEMENT AND IT SAYS NOBODY IS COMING. `undefined` is the
971
- // absence of one, and a caller that knows only a position is somebody
972
- // waiting at it.
973
- //
974
- // Read through `Number()`, `null` becomes 0 — due NOW — so the film BEHIND
975
- // the viewers, which the map marks with exactly that, was the most urgent
976
- // material in the file. Everything followed from it: it bought encoders,
977
- // it took the run standing in front of the viewer because that run was the
978
- // nearest encoder to it, and it did so again on every pass. Field 2026-09-08:
979
- // 39 moves in one session, 24 between three adjacent numbers, one viewer
980
- // on three encoders, and the picture stood still for 116.7 s in three
981
- // interruptions, the worst of them 91.8 s.
982
- //
983
- // The map has always said it plainly — `{"from":0,"to":57,"priority":1,
984
- // "withinSeconds":null,"behind":true}` is in the log of every session — and
985
- // this line turned it into its opposite. Fourth time in this repository
986
- // that the input to a calculation was not what the calculation assumed.
987
- const within = stated === undefined ? 0 : (stated === null ? Number.NaN : Number(stated));
988
- if (!Number.isFinite(within)) {
989
- // Stated as no time at all: nobody is coming here.
990
- continue;
991
- }
992
- const here = within + (index - span.from) * perSegment;
993
- if (here < soonest) {
994
- soonest = here;
995
- }
996
- }
997
- return soonest;
998
- };
999
- }
1000
-
1001
- /**
1002
- * WHERE ENCODERS BELONG, from the model rather than from a list of cases.
1003
- *
1004
- * The problem this solves, stated exactly:
1005
- *
1006
- * - the track is a line of segment numbers; `M` are the ones not made;
1007
- * - each `x` carries a DEADLINE `D(x)`, the seconds until somebody needs it.
1008
- * That is what the priority map is a reading of — a viewer moving forward
1009
- * covers a second of film in a second, so the time until they are at `x` is
1010
- * the distance to it. `Infinity` where nobody is coming;
1011
- * - an encoder is a SEQUENTIAL producer: placed at `a`, it delivers `a + j` at
1012
- * time `(j + 1) / r`, where `r` is segments per second, measured. It cannot
1013
- * skip, so its whole schedule follows from where it starts;
1014
- * - the machine affords `k` of them, measured.
1015
- *
1016
- * Two consequences fall out and need no rule of their own. Placements
1017
- * `a_1 < ... < a_k` PARTITION the line: encoder `i` is useful only on
1018
- * `[a_i, a_{i+1})`, because past that its neighbour got there first. And a
1019
- * segment served by encoder `i` arrives at `(x - a_i + 1) / r`, which is
1020
- * therefore also the answer to "when would the encoder already placed before it
1021
- * get here" — the second half of the comparison, and the half that was missing.
1022
- *
1023
- * `x` is LATE when it arrives after `D(x)`. The objective is no late segments;
1024
- * where `k` does not stretch to that, lateness beginning as far to the right as
1025
- * possible.
1026
- *
1027
- * THE ALGORITHM is first-fit, left to right:
1028
- *
1029
- * for each missing x with a finite deadline, ascending:
1030
- * if some encoder already placed at a satisfies (x - a + 1)/r <= D(x):
1031
- * it covers x
1032
- * else:
1033
- * place an encoder at x
1034
- *
1035
- * A LIVE run enters as an encoder already placed at its own head. There is no
1036
- * special case for it.
1037
- *
1038
- * WHY IT IS OPTIMAL. The leftmost missing number with a finite deadline must be
1039
- * covered by somebody. An encoder placed exactly on it delivers it at the
1040
- * earliest time any placement can, `1/r`, and covers the longest suffix any
1041
- * placement can — starting further left only re-makes material and arrives
1042
- * later, starting further right does not cover it at all. So the greedy choice
1043
- * is never worse than any other, and the usual exchange argument carries it to
1044
- * the whole line. This is the known result for FIXED-ORDER scheduling with
1045
- * deadlines, where first-fit is optimal at unit processing times, and a segment
1046
- * is one unit. General machine minimisation with release times and deadlines is
1047
- * NP-hard; this case is polynomial because the order is forced and each machine
1048
- * covers a contiguous stretch.
1049
- *
1050
- * WHAT WAS TRIED FIRST AND WAS WRONG, kept because each looked reasonable:
1051
- *
1052
- * - `(h - p) * s / (1 - s)`, how long a run stays in front of a viewer moving
1053
- * forward. It answers a different question: a viewer stopped with an empty
1054
- * buffer needs the segment now, and at exactly realtime that formula says
1055
- * "for ever" while the viewer waits thirteen minutes;
1056
- * - whether a run's head lies inside a wanted band — which ties an encoder to
1057
- * whoever is standing there, and this layer must never know that;
1058
- * - the run's head as a barrier, everything above it placeable. It has no time
1059
- * in it at all, so it cannot tell two segments ahead from two hundred.
1060
- *
1061
- * Each was a case, not a model. The deadline is the model.
1062
- *
1063
- * @param {import("./CoverageMap.js").CoverageMap} coverage
1064
- * @param {Set<object>} surviving - Runs that will still be alive, as encoders
1065
- * already placed at their own heads.
1066
- * @param {number} rate - Segments produced per second by one encoder, measured.
1067
- * Zero when nothing has measured it, and then no arrival time can be computed
1068
- * and every claimed number is left alone.
1069
- * @returns {(at: number, bound: number, deadlineAt: (index: number) => number, alsoPlaced?: number[]) => number | null}
1070
- */
1071
- function gapFinderFor(coverage, surviving, rate, refetchSecPerSegment = 0) {
1072
- /** Encoders already placed: where each stands, and how far its road runs. */
1073
- const placed = [];
1074
- for (const run of surviving) {
1075
- const head = Number(/** @type {{ head?: number }} */ (run).head);
1076
- placed.push({
1077
- at: Number.isFinite(head) ? head : Number(/** @type {{ from: number }} */ (run).from),
1078
- // A live run's road, so that placing inside it can be priced. A run given
1079
- // no end drives to the end of the film, which is what makes the price real.
1080
- // A run given no end drives to the end of the film, which is what makes
1081
- // the price of cutting in front of it real. Written out rather than
1082
- // imported: this file depends on nothing, and that is what lets it be
1083
- // exercised with plain values alone.
1084
- to: Number(/** @type {{ to: number }} */ (run).to) < Number(/** @type {{ from: number }} */ (run).from)
1085
- ? Number.POSITIVE_INFINITY
1086
- : Number(/** @type {{ to: number }} */ (run).to)
1087
- });
1088
- }
1089
- return (at, bound, deadlineAt, alsoPlaced) => {
1090
- const start = Number.isInteger(at) && at > 0 ? at : 0;
1091
- const last = Number.isInteger(bound) ? bound : -1;
1092
- // THE LATE NUMBER THAT IS DUE SOONEST, not the leftmost one.
1093
- //
1094
- // With room for every placement the two are the same answer. With a budget
1095
- // that binds they are not, and the objective decides: lateness pushed as far
1096
- // to the right as possible means the soonest deadline is served first. A
1097
- // walk by number gave the one machine to a viewer due in ten minutes while
1098
- // another stood waiting with an empty buffer.
1099
- //
1100
- // Ties go to the smaller number, so the answer does not depend on the order
1101
- // the map happens to be in.
1102
- let best = null;
1103
- let bestDue = Number.POSITIVE_INFINITY;
1104
- for (let index = start; index <= last; index += 1) {
1105
- if (coverage.isReady(index)) {
1106
- continue;
1107
- }
1108
- const deadline = deadlineAt(index);
1109
- if (!Number.isFinite(deadline)) {
1110
- // NOBODY IS COMING HERE, so nothing can be late — but the film is still
1111
- // wanted, and this is where spare capacity goes. The number is proposed;
1112
- // whether an encoder is actually spent on it is the score's answer, and
1113
- // the score puts anything anybody is waiting for first.
1114
- return index;
1115
- }
1116
- // When would the SOONEST of those already placed get here? Encoders placed
1117
- // EARLIER IN THIS PASS count: the first one placed for a viewer covers the
1118
- // stretch in front of them, and without counting it the walk placed a
1119
- // second and a third on the very next numbers — three processes a segment
1120
- // apart for one person, which is the waste this model exists to refuse.
1121
- let soonest = Number.POSITIVE_INFINITY;
1122
- for (const a of [...placed.map((live) => live.at), ...(alsoPlaced ?? [])]) {
1123
- if (a > index) {
1124
- // Standing past it. Encoders only move forward, so it never will.
1125
- continue;
1126
- }
1127
- if (a === index) {
1128
- // Standing ON it. No placement is faster than the one already made.
1129
- soonest = 0;
1130
- break;
1131
- }
1132
- // WHEN THIS BODY GETS HERE, and both terms of it.
1133
- //
1134
- // Its own encoding of everything between, and the swarm's price for the
1135
- // film it would fetch a SECOND time — every number between that is
1136
- // already made, it makes again. That second term is why "should this
1137
- // encoder drive on or be moved" is not a question of its own: an
1138
- // encoder with three hundred made pieces in front of it is simply slow
1139
- // to arrive, and the model compares arrivals. Asked separately it was a
1140
- // second authority over the same encoder, and the two disagreed.
1141
- const arrival = (index - a + 1) / rate
1142
- + coverage.madeBetween(a, index) * refetchSecPerSegment;
1143
- if (arrival < soonest) {
1144
- soonest = arrival;
1145
- }
1146
- }
1147
- if (soonest <= deadline) {
1148
- // Somebody gets here in time. Nothing to decide.
1149
- continue;
1150
- }
1151
- // IT IS LATE, AND THAT IS ALL THIS DECIDES. Whether filling it is worth
1152
- // the price is not asked here: this only proposes candidates, and the
1153
- // score decides how many of them are taken and by whom. Asked here as
1154
- // well, it was a second cost model beside the objective — with its own
1155
- // idea of what a process costs — and the two disagreed at exactly
1156
- // realtime, where every next piece is marginally late and each looked
1157
- // worth its own encoder.
1158
- if (deadline < bestDue) {
1159
- best = index;
1160
- bestDue = deadline;
1161
- }
1162
- }
1163
- return best;
1164
- };
1165
- }
1166
-
1167
- /**
1168
- * The last number of a stretch that begins at `from` and is `length` long.
1169
- *
1170
- * `-1` when the length is not finite, which is this layer's word for a run with
1171
- * no end: the film's length is not known, so there is nothing to stop it at, and
1172
- * a number invented here would be an end nobody measured.
1173
- *
1174
- * @param {number} from
1175
- * @param {number} length
1176
- * @returns {number}
1177
- */
1178
- function endOfStretch(from, length) {
1179
- return Number.isFinite(length) ? from + Math.max(1, length) - 1 : -1;
1180
- }
1181
-
1182
- /**
1183
- * The lowest number a viewer is waiting for that is not ready — what the plan
1184
- * is judged by.
1185
- *
1186
- * Not used to decide anything: it is the figure a log line carries, so that a
1187
- * plan that keeps producing while a viewer waits is visible rather than
1188
- * inferred.
1189
- *
1190
- * @param {import("./CoverageMap.js").CoverageMap} coverage
1191
- * @param {WantedSpan[]} windows
1192
- * @returns {number | null}
1193
- */
1194
- export function firstUnmetWant(coverage, windows) {
1195
- let lowest = null;
1196
- for (const span of windows ?? []) {
1197
- for (let at = span.from; at <= span.to; at += 1) {
1198
- if (!coverage.isReady(at)) {
1199
- if (lowest === null || at < lowest) {
1200
- lowest = at;
1201
- }
1202
- break;
1203
- }
1204
- }
1205
- }
1206
- return lowest;
1207
- }
1208
-
1209
- /**
1210
- * Where to put the encoders this machine can afford.
1211
- *
1212
- * Two things are wanted of a division of the film, and they are wanted in this
1213
- * order:
1214
- *
1215
- * 1. **the viewer must not stop.** An encoder starting at `q` stays ahead of a
1216
- * viewer at `p` while `y / s <= q + y - p`, so it holds `(q - p) * s / (1-s)`
1217
- * of film and no more. Beyond that the viewer catches it, and the next
1218
- * encoder has to be standing there. That is where the first ones go, and it
1219
- * is why the stretches grow: the further off one starts, the later the
1220
- * viewer arrives and the longer it may work;
1221
- * 2. **the film should be finished as soon as possible.** Once the viewer is
1222
- * safe, whatever is left is divided EQUALLY between the encoders that
1223
- * remain: equal shares finish together, and any other division finishes when
1224
- * its longest share does. That is what makes seeking cheap — the film exists.
1225
- *
1226
- * At or above realtime the first requirement is met by one encoder for the
1227
- * whole film, and every other encoder goes to the second — which is the common
1228
- * case on a copied picture, and is why "one viewer, one encoder" was never the
1229
- * rule.
1230
- *
1231
- * @param {object} params
1232
- * @param {import("./CoverageMap.js").CoverageMap} params.coverage
1233
- * @param {WantedSpan[]} params.windows - The merged map, in this output's own
1234
- * numbering. Its highest-numbered band starts where the viewer is.
1235
- * @param {number} params.howMany - What the machine affords.
1236
- * @param {number} params.speedX - Measured. Zero when nothing has measured it,
1237
- * and then only the first requirement can be served.
1238
- * @param {(at: number, bound: number, deadlineAt: (index: number) => number, placed: number[]) => number | null} [params.firstGap] -
1239
- * Where a gap may be opened. Defaults to the map's own answer; the plan hands
1240
- * in one that also counts a number a live run has claimed but will not reach
1241
- * before it is needed, which is the only way anybody beyond a working encoder
1242
- * is served.
1243
- * @param {(index: number) => number} [params.deadlineAt] - Seconds until that
1244
- * number is needed. `Infinity` where nobody is coming. Absent means every
1245
- * stated want is due now.
1246
- * @returns {number[]} Where to start each encoder, ascending.
1247
- */
1248
- export function placeEncoders({
1249
- coverage,
1250
- windows,
1251
- howMany,
1252
- firstGap = null,
1253
- deadlineAt = null,
1254
- // WHERE TWO ENCODERS SHARE A STRETCH, and whether a second is worth having at
1255
- // all. Derived by whoever holds the measurements — the rates, the contention
1256
- // penalty, what a start costs — because this function is positional and holds
1257
- // none of them. The default is the midpoint, which IS the answer when both are
1258
- // fresh and owe the same, and is what a caller with nothing measured falls
1259
- // back on.
1260
- splitAt = (from, to) => from + Math.floor((to - from + 1) / 2)
1261
- }) {
1262
- if (!(howMany > 0) || windows.length === 0) {
1263
- return [];
1264
- }
1265
- // NOW when the caller says nothing. A stated want with no time is somebody
1266
- // waiting on it — that is what stating one means — so the honest reading is
1267
- // that it is due. `Infinity` is a statement in its own right and has to be
1268
- // made: it says nobody is coming.
1269
- const untilNeeded = deadlineAt ?? (() => 0);
1270
- /** Where this pass has placed so far — each one covers what it can reach. */
1271
- const placedHere = [];
1272
- const gapAt = firstGap
1273
- ? (at, bound) => firstGap(at, bound, untilNeeded, placedHere)
1274
- : (at, bound) => coverage.firstGapFrom(at, bound);
1275
-
1276
-
1277
- // CANDIDATES COME FROM THE PRIORITY MAP, IN THE ORDER THE MAP STATES.
1278
- //
1279
- // The map already answers every question that was being re-derived here. Its
1280
- // ranks say what matters most — the number a viewer is stopped on, then what
1281
- // is in front of them band by band, then the rest of the track, and last of
1282
- // all what lies behind them. A pause flattens those ranks; a seek moves them;
1283
- // a second viewer merges into them. So walking the map in its own order is
1284
- // what "ahead before behind" means, and nothing here has to work out where the
1285
- // viewers are.
1286
- //
1287
- // It was not read that way. This walked the film by number and proposed
1288
- // whatever was late, then a second pass divided the leftovers — an order of
1289
- // its own invention, which put the beginning of the file before the film in
1290
- // front of a viewer and, at one point, proposed #0, #1 and #2 as three
1291
- // separate places.
1292
- //
1293
- // One candidate per zone: the first number in it nobody has and nobody
1294
- // reaches in time. Zones with no deadline can have nothing late in them, so
1295
- // there it is simply the first number nobody has — which is how spare capacity
1296
- // comes to finish the file.
1297
- /** @type {number[]} */
1298
- const places = [];
1299
- const byRank = [...windows].sort(
1300
- (left, right) => (right.priority ?? 0) - (left.priority ?? 0) || left.from - right.from
1301
- );
1302
- for (const zone of byRank) {
1303
- if (places.length >= howMany) {
1304
- break;
1305
- }
1306
- const at = gapAt(zone.from, zone.to);
1307
- if (at === null || places.includes(at)) {
1308
- continue;
1309
- }
1310
- places.push(at);
1311
- placedHere.push(at);
1312
- }
1313
-
1314
- // AND WHERE TO SPLIT WHAT IS LEFT, for the capacity the map has not spent.
1315
- //
1316
- // The search above proposes only what is LATE, so once one encoder covers a
1317
- // zone in time that zone proposes nothing more — and a machine that holds four
1318
- // ran one. Finishing a contiguous stretch soonest with several machines of the
1319
- // same speed means dividing it between them, which is where these come from:
1320
- // the widest run of film between two encoders, split.
1321
- //
1322
- // Proposing is not spending. The score decides whether another process is
1323
- // worth it, and its first term — how late the film is — always outranks its
1324
- // second, so this can never take capacity from somebody waiting.
1325
- while (places.length < howMany) {
1326
- const edges = [...places].sort((left, right) => left - right);
1327
- let widestFrom = null;
1328
- let widest = 0;
1329
- for (let index = 0; index <= edges.length; index += 1) {
1330
- const from = index === 0 ? firstOf(windows) : edges[index - 1] + 1;
1331
- const to = index === edges.length ? lastOf(windows) : edges[index] - 1;
1332
- const room_ = coverage.unmadeRunFrom(from);
1333
- if (to >= from && room_ > widest) {
1334
- // WHERE THE TWO OF THEM MEET, calculated rather than halved. Halving is
1335
- // the answer only when both encoders are fresh and owe the same, and it
1336
- // was applied to every case — one already working is partway through a
1337
- // piece while a new one owes its whole start, so the point where they
1338
- // finish together lies further on. Nothing back means a second encoder
1339
- // does not pay for itself here at all: two under this host's measured
1340
- // contention against one at full speed, a comparison halving never made.
1341
- const meet = splitAt(from, Math.min(from + room_ - 1, to));
1342
- if (meet !== null) {
1343
- widest = room_;
1344
- widestFrom = meet;
1345
- }
1346
- }
1347
- }
1348
- if (widestFrom === null) {
1349
- break;
1350
- }
1351
- const at = coverage.firstGapFrom(widestFrom, lastOf(windows));
1352
- if (at === null || places.includes(at)) {
1353
- break;
1354
- }
1355
- places.push(at);
1356
- placedHere.push(at);
1357
- }
1358
-
1359
- // These are CANDIDATES, not decisions. What is late proposes first, because
1360
- // that is what a viewer feels; what is merely unmade proposes after it. The
1361
- // score decides which of them are worth a process, and a paused viewer, who
1362
- // states no deadline at all, therefore still leaves the file being finished
1363
- // rather than the machine falling idle.
1364
- return places.sort((left, right) => left - right);
1365
- }
1366
-
1
+ /**
2
+ * @file How many encoders there should be on one output, and where each of them
3
+ * belongs — decided from numbers alone.
4
+ *
5
+ * The decision is separated from carrying it out on purpose. Every rule below
6
+ * was previously a condition somewhere inside an eleven-thousand-line file,
7
+ * reachable only by starting a real ffmpeg, and each of them was written for
8
+ * one viewer:
9
+ *
10
+ * - a run was placed at the position of whoever asked, and never at the first
11
+ * thing missing, so a viewer moving into a stretch already on disk restarted
12
+ * an encoder to make it a second time;
13
+ * - a run had no end at all — neither `-to` nor `-t` appeared anywhere — so it
14
+ * ran until something killed it, and two runs on one output could not exist
15
+ * without writing over each other;
16
+ * - nothing stopped a run that had caught up with material somebody else had
17
+ * already made.
18
+ *
19
+ * The rule this file exists to express, stated by the user 2026-09-04:
20
+ *
21
+ * > Viewers are always independent and always reuse what can be reused. The
22
+ * > number of encoders is however many are needed; how many are needed follows
23
+ * > from which sets of output parameters are wanted and where the viewers stand
24
+ * > inside each. Segments produced by ANY encoder are available to ANY viewer,
25
+ * > and which viewer asked never enters the question.
26
+ *
27
+ * So no name of a viewer reaches this file. It is given what is wanted, what
28
+ * exists, what is being made, and what the machine can afford.
29
+ *
30
+ * **A viewer decides the ORDER the map is walked in and, through the budget, how
31
+ * many processes walk it. Nothing else.** Stated by the user 2026-09-05, and it
32
+ * is the rule the rest of this file now follows: while a file is being encoded
33
+ * it is encoded WHOLE, in the order the map dictates. Who wants which segment
34
+ * decides which gap is closed first, never whether a run may go on living.
35
+ *
36
+ * **A run is therefore never stopped for standing outside a viewer's window.**
37
+ * It used to be, and the two decisions that produced that were in direct
38
+ * contradiction — measured in the field 2026-09-05 on a viewer watching an
39
+ * episode:
40
+ *
41
+ * 1. this file commanded a start inside the window, at #46;
42
+ * 2. `planRunInterval` in the session manager moved the start to #78, because
43
+ * it counted a suspended run's claim as reaching `head + look-ahead`;
44
+ * 3. this file then saw a run at #78 against a window of [27, 57], found no
45
+ * overlap, and killed it as "nothing it was given is wanted";
46
+ * 4. neither coverage nor demand had changed, so the same start was commanded
47
+ * again — 350-700ms per cycle, dozens of times, no segment ever produced,
48
+ * the viewer's picture stopped for 125 seconds.
49
+ *
50
+ * Both of those other authorities are gone (roadmap item 76, step 5). What is
51
+ * left is this file, and the only reasons it stops a run are: nobody is
52
+ * watching the output at all; the machine affords fewer processes; or there is
53
+ * nothing left unmade anywhere in the track.
54
+ *
55
+ * **A run's end comes from the coverage**, never from a window: it runs until
56
+ * it meets material somebody else has made or is making, or until the end of
57
+ * the film.
58
+ */
59
+
60
+ /**
61
+ * One encoder that is running now.
62
+ *
63
+ * @typedef {object} LiveRun
64
+ * @property {string} id
65
+ * @property {number} from - The first number it was given.
66
+ * @property {number} to - The last number it was given, inclusive.
67
+ * @property {number} head - The next number it will produce. Its position.
68
+ * @property {number} speedX - Measured encode speed against realtime, from
69
+ * ffmpeg's own progress. Zero or less means nothing has measured it yet, and
70
+ * then no comparison involving its speed can be made.
71
+ */
72
+
73
+ /**
74
+ * What a viewer is waiting for. Which viewer is deliberately absent.
75
+ *
76
+ * @typedef {object} WantedSpan
77
+ * @property {number} from
78
+ * @property {number} to
79
+ */
80
+
81
+ /**
82
+ * @typedef {{ type: "start", from: number, to: number, because: string }
83
+ * | { type: "move", run: object, from: number, to: number, because: string }
84
+ * | { type: "stop", run: object, because: string }
85
+ * | { type: "keep", run: object, from: number, to: number }} PlanAction
86
+ */
87
+
88
+ /**
89
+ * Decide what to do with the encoders on one output.
90
+ *
91
+ * @param {object} params
92
+ * @param {import("./CoverageMap.js").CoverageMap} params.coverage - What has
93
+ * been made and what is being made.
94
+ * @param {WantedSpan[]} params.windows - What viewers are waiting for, one
95
+ * window each. Empty means nobody is watching this output.
96
+ * @param {LiveRun[]} params.runs - The encoders running on it now.
97
+ * @param {number} params.maxRuns - How many encoders this machine can afford on
98
+ * this output. Comes from the same arithmetic that decides the quality offer;
99
+ * it is measured per host and never chosen here.
100
+ * @param {number} params.segmentSeconds - How much film one segment holds.
101
+ * @param {number} [params.killCostSec] - How long stopping an encoder takes,
102
+ * measured on this host from its own runs. Zero until something has measured
103
+ * it, which makes moving one look cheaper than it is and is said here so the
104
+ * bias is known.
105
+ * @param {number} [params.now] - The clock, injected. This layer is arithmetic
106
+ * and reads no clock of its own; how old a run is is one of its inputs.
107
+ * @param {number} [params.firstByteWaitSec] - How long a fresh encoder takes to
108
+ * produce anything: process start, opening the input, and the first piece.
109
+ * Measured the same way. It replaced a constant of 0.12 s taken from one
110
+ * host and charged to every other.
111
+ * @param {(others: number) => number} [params.contentionPenaltyFor] - How much
112
+ * slower ONE encoder runs with that many others beside it, measured on this
113
+ * host. Without it every extra process looks free, and the score then wants an
114
+ * encoder per piece: at exactly realtime each next piece is marginally late
115
+ * however many are running, so another one always seemed to help a little.
116
+ * Unmeasured is 1, and then the budget is the only thing bounding the count.
117
+ * @returns {PlanAction[]} Stops first, then moves, then starts, so that a plan
118
+ * carried out in order never holds two encoders where it means to hold one.
119
+ */
120
+ export function planEncoders({
121
+ coverage,
122
+ windows,
123
+ runs,
124
+ maxRuns,
125
+ segmentSeconds,
126
+ killCostSec = 0,
127
+ firstByteWaitSec = 0,
128
+ now = Date.now(),
129
+ refetchSecPerFilmSecond = 0,
130
+ contentionPenaltyFor = () => 1,
131
+ speedX = 0
132
+ }) {
133
+ /** @type {PlanAction[]} */
134
+ const stops = [];
135
+ /** @type {PlanAction[]} */
136
+ const moves = [];
137
+ /** @type {PlanAction[]} */
138
+ const starts = [];
139
+ /** @type {PlanAction[]} */
140
+ const keeps = [];
141
+
142
+ const wanted = Array.isArray(windows) ? windows : [];
143
+ const live = Array.isArray(runs) ? runs : [];
144
+
145
+ // Nobody is watching this output: every encoder on it is making segments for
146
+ // no one. This is the case a look-ahead cannot answer, because look-ahead
147
+ // asks how far AHEAD of a viewer a run is and there is no viewer.
148
+ if (wanted.length === 0) {
149
+ for (const run of live) {
150
+ stops.push({ type: "stop", run, because: "nobody is watching this output" });
151
+ }
152
+ return stops;
153
+ }
154
+
155
+ const untilNeeded = deadlineReaderFor(wanted, segmentSeconds);
156
+ // Segments produced per second, from the fastest measured encoder here.
157
+ // Seconds of film per second, divided by the film one piece holds.
158
+ //
159
+ // A RUN WORKING ON THIS OUTPUT OUTRANKS THE BENCHMARK. The startup figure is
160
+ // what this host does on reference clips; a run here is what it does on THIS
161
+ // material, and that is the more specific statement. Taken as a floor instead
162
+ // — the larger of the two — an encoder reporting half realtime was scored as
163
+ // though it ran at twice, and nothing was ever late.
164
+ const working = live.reduce((best, run) => Math.max(best, run.speedX || 0), 0);
165
+ const rate = segmentSeconds > 0 ? (working > 0 ? working : speedX) / segmentSeconds : 0;
166
+ // How long one piece takes AT THE RATE ACTUALLY IN FORCE. Concurrent encoders
167
+ // slow each other — measured on this host — so an arrangement's own encoder count
168
+ // decides it, and the delays below are computed per arrangement for that
169
+ // reason. Taken from the unpenalised rate instead, a piece looked cheaper the
170
+ // more encoders there were, which made extra encoders look free: the plan bought a
171
+ // second encoder where one served, and the arrivals it was compared on were
172
+ // computed at the slower rate all along.
173
+ //
174
+ // `Infinity` where nothing has been measured, which is what "no speed" means
175
+ // and what makes every arrangement equally hopeless rather than equally free.
176
+ const pieceAt = (howManyEncoders) => {
177
+ const inForce = rate / contentionPenaltyFor(Math.max(0, howManyEncoders - 1));
178
+ return inForce > 0 ? 1 / inForce : Number.POSITIVE_INFINITY;
179
+ };
180
+ // What a encoder costs to take away from where it stands and put somewhere else:
181
+ // its death, the start of another, and the wait for the first bytes there.
182
+ // Taking an encoder somewhere else is stopping this one and waiting for the
183
+ // next to produce. Both halves are measured on this host.
184
+ // WHAT A START AND A MOVE OWE BEFORE THE PIECE THEY STAND ON EXISTS, and
185
+ // neither is infinite.
186
+ //
187
+ // The measured `firstByteWaitSec` is spawn to first piece, so it already
188
+ // contains one piece's encoding. Separated, because the two scale differently:
189
+ // the piece costs more when encoders share the machine, the spawn does not.
190
+ //
191
+ // spawn overhead = measured first output - what one piece costs alone
192
+ // a fresh encoder owes spawn overhead + the piece at the rate in force
193
+ // a moved one owes the kill, and then the same
194
+ //
195
+ // With nothing measured the overhead is zero and a fresh encoder owes exactly
196
+ // one piece — which is the honest floor rather than a guess: a piece cannot
197
+ // appear before it is encoded, and how fast this host encodes is measured
198
+ // before any viewer exists. There was an `Infinity` here for the cost of a
199
+ // move, on the reasoning that an unmeasured price must not license an
200
+ // irreversible act; it was an exception in a model that needs none, and this
201
+ // is the same statement made by arithmetic.
202
+ const spawnOverheadSec = Math.max(0, firstByteWaitSec - (rate > 0 ? 1 / rate : 0));
203
+ const moveSec = killCostSec + spawnOverheadSec;
204
+
205
+ // WHAT A RUN STILL HAS TO GO BEFORE IT PRODUCES ANYTHING — the measured time
206
+ // to a first piece, less the time it has already been alive.
207
+ //
208
+ // This is the memory the score was missing. It is computed afresh whenever
209
+ // anything changes, and every arrangement used to be priced as though it were
210
+ // the last decision anybody would take: a run that started 10 ms ago was
211
+ // assumed to produce instantly, so killing it and starting another looked like
212
+ // a straight gain. A move is justified by a benefit that arrives when the
213
+ // moved run produces something; taken again before it has, the benefit is
214
+ // never collected and the cost is paid twice, three times, forty times.
215
+ //
216
+ // A run 0.8 s old has 0.14 s left to go against 0.94 s to move it, so it is
217
+ // left alone; one working for half a minute has nothing left, and a move
218
+ // happens exactly when the film it would reach sooner is worth the restart. No
219
+ // state to keep and nothing to choose: one measured figure minus elapsed time.
220
+ const finishesItsPieceIn = (run, perPiece) => {
221
+ // WHEN THIS RUN FINISHES THE PIECE IT IS STANDING ON.
222
+ //
223
+ // Two cases and neither is a choice: a run that has produced something is
224
+ // working at a known rate, so the piece under it lands within one piece's
225
+ // worth of time; a run that has produced nothing is still in its warm-up,
226
+ // and what is left of that is the measured time to a first piece less the
227
+ // time it has already been alive.
228
+ //
229
+ // This is the memory the score was missing. It is computed afresh whenever
230
+ // anything changes, and each arrangement used to be priced as though it were
231
+ // the last decision anybody would take: a run 0.8 s into a 0.9 s piece was
232
+ // charged a whole piece for it, the same as one about to start, so killing
233
+ // it and beginning again elsewhere looked cheaper by ten milliseconds. A
234
+ // move is justified by a benefit that arrives when the moved run produces
235
+ // something; taken again before it has, the benefit is never collected and
236
+ // the cost is paid twice, three times, forty times.
237
+ //
238
+ // One measured figure minus elapsed time. Nothing to keep and nothing to
239
+ // choose.
240
+ if (Number(run.head) !== Number(run.from)) {
241
+ return perPiece;
242
+ }
243
+ const startedAt = Number(run.startedAt);
244
+ if (!Number.isFinite(startedAt) || startedAt <= 0) {
245
+ return firstByteWaitSec;
246
+ }
247
+ return Math.max(0, firstByteWaitSec - (now - startedAt) / 1000);
248
+ };
249
+
250
+ // WHAT EACH BODY OWES BEFORE THE PIECE IT STANDS ON EXISTS, priced at the rate
251
+ // the arrangement itself puts in force.
252
+ //
253
+ // Each encoder states its own debt where it is created, as a function of what one
254
+ // piece costs — because only there is it known what the encoder IS, and here only
255
+ // how many of them there are. So there is nothing to dispatch on: no kind, no
256
+ // tag, no case analysis. The count is known before any debt is needed, which
257
+ // is why this is one pass over the encoders rather than a figure computed once
258
+ // outside.
259
+ const priced = (encoders) => {
260
+ const perPiece = pieceAt(encoders.length);
261
+ return encoders.map((encoder) => ({ at: encoder.at, delaySec: encoder.owes(perPiece) }));
262
+ };
263
+
264
+
265
+ // WHERE A SECOND ENCODER MEETS THE ONE ALREADY ON A STRETCH, and whether it is
266
+ // worth having.
267
+ //
268
+ // A stretch of unmade film runs from `from` to `to`. Whoever is already on it
269
+ // stands at `from` and owes `w` before the piece under it exists; a fresh one
270
+ // placed at `x` owes `d` — its start, and then a whole piece — and both then
271
+ // work at the rate two encoders leave each other, which is measured on this
272
+ // host and is not half by assumption.
273
+ //
274
+ // the one already there closes [from, x-1]: w + (x - 1 - from) / r
275
+ // the fresh one closes [x, to]: d + (to - x) / r
276
+ //
277
+ // The first rises with `x` and the second falls, so the stretch is closed
278
+ // soonest where they cross:
279
+ //
280
+ // x* = (from + to + 1) / 2 + (d - w) * r / 2
281
+ //
282
+ // The midpoint, shifted forward by half the difference of what the two owe,
283
+ // expressed in pieces. Halving is the special case `d = w`, which holds when
284
+ // both are fresh — and it was applied to every case, including the common one
285
+ // where `d` is a start plus a piece and `w` is the tail of a piece already
286
+ // being made.
287
+ //
288
+ // AND WHETHER TO SPLIT AT ALL, which halving never asked. Two encoders under
289
+ // contention against one at full speed:
290
+ //
291
+ // one: w + (to - from - 1) / rate
292
+ // two: w + (x* - 1 - from) / r
293
+ //
294
+ // Nothing back where the second does not win. On the addon host at 1920x1080
295
+ // the measured penalty for a second encoder is 1.98 — it takes very nearly
296
+ // all of the first's speed — so there a second almost never pays, and it was
297
+ // being placed regardless.
298
+ const splitAt = (from, to) => {
299
+ const pieces = to - from + 1;
300
+ if (!(pieces > 1) || !(rate > 0)) {
301
+ return null;
302
+ }
303
+ const together = rate / contentionPenaltyFor(live.length);
304
+ if (!(together > 0)) {
305
+ return null;
306
+ }
307
+ const perPieceTogether = 1 / together;
308
+ // What the one already on this stretch owes, or a fresh one's debt where
309
+ // nobody is on it — and then both sides owe the same and the meeting point
310
+ // is the middle, as it should be.
311
+ const onIt = live.find((run) => Number(run.head) === from) ?? null;
312
+ const owedThere = onIt
313
+ ? finishesItsPieceIn(onIt, perPieceTogether)
314
+ : firstByteWaitSec + perPieceTogether;
315
+ const owedFresh = firstByteWaitSec + perPieceTogether;
316
+ const meeting = (from + to + 1) / 2 + ((owedFresh - owedThere) * together) / 2;
317
+ const x = Math.min(to, Math.max(from + 1, Math.round(meeting)));
318
+ const withTwo = owedThere + (x - 1 - from) * perPieceTogether;
319
+ const withOne = owedThere + (pieces - 1) / rate;
320
+ return withTwo < withOne ? x : null;
321
+ };
322
+
323
+ // ------------------------------------------------------------------ WHERE
324
+ //
325
+ // A question about the FILM, and about nothing else: which numbers are
326
+ // missing, when each is needed, how fast this machine encodes, how many
327
+ // processes it can hold. No encoder that happens to be running enters it,
328
+ // which is why it can be answered by arithmetic.
329
+ const positions = placeEncoders({
330
+ coverage,
331
+ windows: wanted,
332
+ howMany: maxRuns,
333
+ // EVERY LIVE ENCODER IS PRE-PLACED, because that is what "somebody already
334
+ // gets here in time" means. A number one of them reaches before it is
335
+ // needed is not a position at all; a number none of them reaches is, and
336
+ // needs a encoder brought to it. There is no third case, and in particular no
337
+ // separate question of whether an encoder should drive on or be moved:
338
+ // driving is simply its arrival, and its arrival is priced in one place.
339
+ firstGap: gapFinderFor(coverage, new Set(live), rate, segmentSeconds * refetchSecPerFilmSecond),
340
+ deadlineAt: untilNeeded,
341
+ splitAt
342
+ });
343
+
344
+ // -------------------------------------------------------------------- WHO
345
+ //
346
+ // ARGMIN OF THE OBJECTIVE, EVALUATED. Not a rule that approximates it.
347
+ //
348
+ // Every way of filling the positions is scored by `latenessOf` and the best is
349
+ // taken. There are at most a handful of positions and a handful of encoders, so
350
+ // the enumeration is exact: no local rule stands in for the objective, and
351
+ // none can therefore disagree with another.
352
+ //
353
+ // Four such rules were written before this and all four had to go — "place
354
+ // where a number is late", "take a encoder that serves nothing", "take one whose
355
+ // work is needed later than this", "drive on or move, by cost". Each looked
356
+ // like a consequence of the model and each approximated it from a different
357
+ // side, so together they contradicted one another and the answer depended on
358
+ // which ran first.
359
+ /** One decision per live encoder, so none can be decided twice. @type {Map<object, PlanAction>} */
360
+ const decided = new Map();
361
+ const room = Math.max(0, maxRuns - live.length);
362
+ const refetchPerSegment = segmentSeconds * refetchSecPerFilmSecond;
363
+
364
+ let arrangements = [{ fill: [], used: new Set(), fresh: 0 }];
365
+ for (let index = 0; index < positions.length; index += 1) {
366
+ const next = [];
367
+ for (const arrangement of arrangements) {
368
+ next.push({ fill: [...arrangement.fill, null], used: arrangement.used, fresh: arrangement.fresh });
369
+ // A FRESH PROCESS IS OFFERED BEFORE ANY WORKING BODY, so that when the two
370
+ // score the same the working one is left alone. Taking it is free in the
371
+ // arithmetic — its output stays on disk — but it is not free in fact: the
372
+ // run it belongs to has a position, a warm input and a measured speed, and
373
+ // all three are thrown away for nothing.
374
+ if (arrangement.fresh < room) {
375
+ next.push({
376
+ fill: [...arrangement.fill, "new"],
377
+ used: arrangement.used,
378
+ fresh: arrangement.fresh + 1
379
+ });
380
+ }
381
+ for (const run of live) {
382
+ if (arrangement.used.has(run)) {
383
+ continue;
384
+ }
385
+ next.push({
386
+ fill: [...arrangement.fill, run],
387
+ used: new Set([...arrangement.used, run]),
388
+ fresh: arrangement.fresh
389
+ });
390
+ }
391
+ }
392
+ arrangements = next;
393
+ }
394
+
395
+ let best = null;
396
+ let bestScore = null;
397
+ for (const arrangement of arrangements) {
398
+ const encoders = [];
399
+ for (let index = 0; index < positions.length; index += 1) {
400
+ const filler = arrangement.fill[index];
401
+ if (filler === null) {
402
+ continue;
403
+ }
404
+ if (filler === "new") {
405
+ // A encoder that does not exist yet owes its own start and then the piece.
406
+ encoders.push({ at: positions[index], owes: (piece) => spawnOverheadSec + piece });
407
+ continue;
408
+ }
409
+ const head = Number(filler.head);
410
+ // Left where it stands it owes what is left of the piece under it; taken
411
+ // somewhere else it owes the killing, the start and a whole piece.
412
+ encoders.push({
413
+ at: positions[index],
414
+ owes: head === positions[index]
415
+ ? (piece) => finishesItsPieceIn(filler, piece)
416
+ : (piece) => moveSec + piece
417
+ });
418
+ }
419
+ // Bodies nobody was given a position for go on working where they stand,
420
+ // and their coverage counts: the file is encoded whole.
421
+ //
422
+ // A encoder given no end pays a restart the moment anybody is placed inside the
423
+ // road it would drive: where a run stops is fixed when its process starts,
424
+ // so it has to be cut and begun again at its own head. That price was
425
+ // invisible here, and an arrangement was scored as free when it was not.
426
+ for (const run of live) {
427
+ if (arrangement.used.has(run)) {
428
+ continue;
429
+ }
430
+ const head = Number(run.head);
431
+ const endless = Number(run.to) < Number(run.from);
432
+ const cutInFront = arrangement.fill.some((filler, index) =>
433
+ filler !== null && positions[index] > head
434
+ && (endless || positions[index] <= Number(run.to)));
435
+ encoders.push({
436
+ at: head,
437
+ owes: cutInFront
438
+ ? (piece) => moveSec + piece
439
+ : (piece) => finishesItsPieceIn(run, piece)
440
+ });
441
+ }
442
+ const scored = latenessOf(priced(encoders), coverage, wanted, untilNeeded, rate / contentionPenaltyFor(Math.max(0, encoders.length - 1)), refetchPerSegment, segmentSeconds);
443
+ if (bestScore === null || cheaperThan(scored, bestScore)) {
444
+ bestScore = scored;
445
+ best = arrangement;
446
+ }
447
+ }
448
+
449
+ // A BODY STANDING ON FILM THAT EXISTS is the one arrangement the enumeration
450
+ // above cannot reach: the gap in front of it is nobody's deadline, so it is
451
+ // never a position, and the encoder is left to make three hundred pieces a second
452
+ // time. Each such encoder is offered its own first gap and the SAME score decides
453
+ // — moving costs a restart on everything downstream, staying costs the repeat.
454
+ //
455
+ // Offered one at a time rather than folded into the enumeration because the
456
+ // enumeration is exponential in the number of positions, and this is called
457
+ // again on every piece produced. One extra evaluation per encoder against
458
+ // several thousand arrangements is the difference between arithmetic and a
459
+ // stalled proxy.
460
+ const placement = new Map();
461
+ for (let index = 0; index < positions.length; index += 1) {
462
+ const filler = best ? best.fill[index] : null;
463
+ if (filler && filler !== "new") {
464
+ placement.set(filler, positions[index]);
465
+ }
466
+ }
467
+ const encodersOf = (override) => {
468
+ const encoders = [];
469
+ for (const run of live) {
470
+ if (override.has(run) && override.get(run) === null) {
471
+ // Asked what the film looks like WITHOUT this one.
472
+ continue;
473
+ }
474
+ const at = override.has(run) ? override.get(run) : (placement.get(run) ?? Number(run.head));
475
+ const head = Number(run.head);
476
+ encoders.push({
477
+ at,
478
+ owes: at === head
479
+ ? (piece) => finishesItsPieceIn(run, piece)
480
+ : (piece) => moveSec + piece
481
+ });
482
+ }
483
+ for (let index = 0; index < positions.length; index += 1) {
484
+ if ((best ? best.fill[index] : null) === "new") {
485
+ encoders.push({ at: positions[index], owes: (piece) => spawnOverheadSec + piece });
486
+ }
487
+ }
488
+ return priced(encoders);
489
+ };
490
+ const scoreOf = (override) => {
491
+ const encoders = encodersOf(override);
492
+ return latenessOf(encoders, coverage, wanted, untilNeeded,
493
+ rate / contentionPenaltyFor(Math.max(0, encoders.length - 1)), refetchPerSegment, segmentSeconds);
494
+ };
495
+ for (const run of live) {
496
+ if (placement.has(run)) {
497
+ continue;
498
+ }
499
+ const gap = coverage.firstGapFrom(run.head, undefined, run);
500
+ if (gap === null || gap === Number(run.head)) {
501
+ continue;
502
+ }
503
+ const asIs = scoreOf(new Map());
504
+ const moved = scoreOf(new Map([[run, gap]]));
505
+ if (cheaperThan(moved, asIs)) {
506
+ placement.set(run, gap);
507
+ }
508
+ }
509
+
510
+ /**
511
+ * Would the film be worse off without this encoder? Asked of the same score.
512
+ *
513
+ * @param {object} run
514
+ * @returns {boolean}
515
+ */
516
+ const worseWithout = (run) => {
517
+ const kept = encodersOf(new Map());
518
+ const without = encodersOf(new Map([[run, null]]));
519
+ const scoreOf_ = (encoders) => latenessOf(encoders, coverage, wanted, untilNeeded,
520
+ rate / contentionPenaltyFor(Math.max(0, encoders.length - 1)), refetchPerSegment, segmentSeconds);
521
+ return cheaperThan(scoreOf_(kept), scoreOf_(without));
522
+ };
523
+
524
+ const stretchAt = (from) => endOfStretch(from, Math.min(
525
+ coverage.unmadeRunFrom(from),
526
+ coverage.freeRunFrom(from, new Set(live))
527
+ ));
528
+
529
+ for (let index = 0; index < positions.length; index += 1) {
530
+ if ((best ? best.fill[index] : null) !== "new") {
531
+ continue;
532
+ }
533
+ const at = positions[index];
534
+ starts.push({
535
+ type: "start",
536
+ from: at,
537
+ to: stretchAt(at),
538
+ because: `#${at} is wanted and nobody reaches it in time`
539
+ });
540
+ }
541
+
542
+ for (const run of live) {
543
+ const head = Number(run.head);
544
+ const at = placement.has(run) ? placement.get(run) : head;
545
+ if (at !== head) {
546
+ decided.set(run, {
547
+ type: "move",
548
+ run,
549
+ from: at,
550
+ to: stretchAt(at),
551
+ because: `standing at #${head} scores worse than standing at #${at}, counting ` +
552
+ "both how late the film would be and the work that would be done twice"
553
+ });
554
+ continue;
555
+ }
556
+ // It stays where it is — unless holding it changes nothing.
557
+ //
558
+ // A encoder left over from where a viewer used to be goes on costing the
559
+ // machine a process while another encoder already reaches everything it
560
+ // would. The score says so directly: take it away and see. Removing it is
561
+ // refused the moment it makes anything later or leaves film abandoned, so
562
+ // this cannot quietly drop the encoder somebody is waiting on.
563
+ if (!placement.has(run) && !worseWithout(run)) {
564
+ stops.push({
565
+ type: "stop",
566
+ run,
567
+ because: "the film is no worse off without it"
568
+ });
569
+ continue;
570
+ }
571
+ decided.set(run, { type: "keep", run, from: head, to: run.to });
572
+ }
573
+
574
+ // THE MACHINE'S LIMIT BINDS, whatever the map wants. It is measured — the
575
+ // processor, the swarm and the piece store each give a figure and the smallest
576
+ // wins — and an encoder over it is one the host cannot feed. Which of them
577
+ // goes is the same question as any other here: the one the film misses least,
578
+ // by the same score.
579
+ while (decided.size + starts.length > maxRuns) {
580
+ let cheapest = null;
581
+ let cheapestScore = null;
582
+ for (const [run, action] of decided) {
583
+ if (action.type !== "keep") {
584
+ continue;
585
+ }
586
+ const without = latenessOf(encodersOf(new Map([[run, null]])), coverage, wanted, untilNeeded,
587
+ rate / contentionPenaltyFor(Math.max(0, live.length - 2)), refetchPerSegment, segmentSeconds);
588
+ if (cheapestScore === null || cheaperThan(without, cheapestScore)) {
589
+ cheapestScore = without;
590
+ cheapest = run;
591
+ }
592
+ }
593
+ if (cheapest === null) {
594
+ break;
595
+ }
596
+ decided.delete(cheapest);
597
+ placement.delete(cheapest);
598
+ stops.push({
599
+ type: "stop",
600
+ run: cheapest,
601
+ because: `the machine holds ${maxRuns} encoder(s) on this output and this is the one ` +
602
+ "the film misses least"
603
+ });
604
+ }
605
+
606
+ for (const action of decided.values()) {
607
+ if (action.type === "keep") {
608
+ keeps.push(action);
609
+ } else {
610
+ moves.push(action);
611
+ }
612
+ }
613
+
614
+ // ONE ENCODER'S WORK ENDS WHERE THE NEXT ONE'S BEGINS.
615
+ //
616
+ // A free stretch may run to the end of the track, and an encoder given all of
617
+ // it stands in the road of every encoder placed behind it: they write the
618
+ // same names, and each one's output is the other's "material somebody else
619
+ // made", so they stop one another. Field 2026-09-05: three encoders started
620
+ // on one track within 200 ms, each into the road another was already writing.
621
+ // Fifteen readers on a piece store that holds sixteen pieces followed, half
622
+ // of all evictions took a piece a reader had declared, and `/stream` began
623
+ // handing out bytes that were not the file's.
624
+ //
625
+ // The bound is taken from the NEXT ENCODER'S START, not from a band edge: a
626
+ // band edge travels with the viewer, so every step forward would leave a
627
+ // sliver just past the previous encoder and buy an encoder for it.
628
+ // A RUN THAT IS STAYING IS IN THE SORT TOO, because its road can be taken.
629
+ //
630
+ // It used to be left out, on the reading that a run staying put keeps what it
631
+ // was given. That is true of the stretch it was GIVEN and false of the road it
632
+ // will actually drive: a run with no end carries no `-to` and walks to the end
633
+ // of the film, so an encoder placed in front of it writes the same names.
634
+ const placed = [...moves, ...starts, ...keeps].sort(
635
+ (left, right) => /** @type {any} */ (left).from - /** @type {any} */ (right).from
636
+ );
637
+ for (let index = 0; index < placed.length - 1; index += 1) {
638
+ const here = /** @type {{ type: string, run?: object, from: number, to: number }} */ (placed[index]);
639
+ const next = /** @type {{ from: number }} */ (placed[index + 1]);
640
+ if (here.to >= 0 && here.to < next.from) {
641
+ continue;
642
+ }
643
+ here.to = next.from - 1;
644
+ if (here.type !== "keep") {
645
+ continue;
646
+ }
647
+ // SHORTENING A LIVE RUN'S ROAD MEANS STOPPING IT, not merely writing a
648
+ // smaller number down. Where a run's end goes is fixed when its process
649
+ // starts, so one that was given none keeps producing past any bound decided
650
+ // later and would write one piece into the new encoder's road — two
651
+ // processes on one name, which is the collision this whole pass exists to
652
+ // prevent. So it ends here and begins again at its own head with a real end;
653
+ // the viewer in front pays a restart, which is a cost this file already
654
+ // prices rather than a interruption nobody counted.
655
+ keeps.splice(keeps.indexOf(here), 1);
656
+ moves.push({
657
+ type: "move",
658
+ run: here.run,
659
+ from: here.from,
660
+ to: here.to,
661
+ because:
662
+ `an encoder is needed at #${next.from}, which this run would reach only by ` +
663
+ "encoding through; it takes the road up to there and ends by itself"
664
+ });
665
+ }
666
+
667
+ return [...stops, ...moves, ...starts, ...keeps];
668
+ }
669
+
670
+
671
+
672
+ /**
673
+ * THE OBJECTIVE, as a value that can be compared.
674
+ *
675
+ * THREE COUNTS OF SECONDS, COMPARED IN ORDER. The order is the user's, stated
676
+ * 2026-09-06, and a later count decides only where the earlier ones tie:
677
+ *
678
+ * 1. SECONDS ANYBODY SPENDS LOOKING AT A SPINNER. Nothing outranks it, at any
679
+ * size. Walked forward in film order rather than summed piece by piece: a
680
+ * viewer who is stopped is not watching, so a wait moves every deadline
681
+ * behind it by its own length;
682
+ *
683
+ * 2. WHEN THE FILM IN FRONT OF THE VIEWERS IS FINISHED — the last piece of it to
684
+ * be made, whichever encoder makes it. A stretch no encoder will ever reach
685
+ * counts as never, which is what stops the front being abandoned;
686
+ *
687
+ * 3. WHEN THE WHOLE FILE IS FINISHED — the film behind the viewers included,
688
+ * plus what the swarm pays to fetch anything a second time. Film nobody is
689
+ * waiting for still has value: a viewer seeking back into a part that exists
690
+ * starts playing at once, and seeking back is what people do in the first
691
+ * minutes while they find their place. So spare capacity goes to finishing
692
+ * the file. This is where "the file is encoded WHOLE" lives; it used to be a
693
+ * penalty for film below the lowest encoder, which said the same thing as a
694
+ * patch and said it about one edge of the track only.
695
+ *
696
+ * WHY AN ENCODER MAY STAND BEHIND A VIEWER while film in front is still unmade:
697
+ * encoders work at the same time, so one in front and one behind can finish the
698
+ * file sooner than two in front. Where nobody is stalled and the front is closed
699
+ * just as fast, the file being done sooner is the answer — count 3 deciding a
700
+ * tie in 1 and 2, which is exactly what the order is for.
701
+ *
702
+ * WHY THE COUNTS ARE COMPARED AND NOT ADDED: seconds of somebody waiting and
703
+ * seconds until a distant stretch exists are not the same thing, and no measured
704
+ * quantity says how many of one are worth one of the other. Adding them would
705
+ * mean choosing that exchange rate, which is inventing a number.
706
+ *
707
+ * @param {{ at: number, delaySec: number }[]} encoders - Where each encoder would
708
+ * stand, and how long before it produces anything there: nothing where it is
709
+ * already standing, a move or a start otherwise.
710
+ * @param {import("./CoverageMap.js").CoverageMap} coverage
711
+ * @param {WantedSpan[]} wanted
712
+ * @param {(index: number) => number} untilNeeded
713
+ * @param {number} rate - Segments per second. Always a real figure: this host
714
+ * measures what it encodes at on startup, before any viewer exists, and every
715
+ * run that works then refines it. There is no "unmeasured" case to answer.
716
+ * @param {number} refetchSecPerSegment
717
+ * @param {number} segmentSeconds
718
+ * @returns {{ stall: number, ahead: number, whole: number }} Three counts of
719
+ * seconds, compared in that order by {@link cheaperThan}.
720
+ */
721
+ function latenessOf(encoders, coverage, wanted, untilNeeded, rate, refetchSecPerSegment, segmentSeconds) {
722
+ const first = Math.min(...wanted.map((span) => span.from));
723
+ const last = Math.max(...wanted.map((span) => span.to));
724
+ // What a number nobody reaches at all counts as. The film's own length is the
725
+ // honest bound — nothing can be later than never — and a finite figure is what
726
+ // lets two hopeless arrangements still be told apart by the rest of the sum.
727
+ const never = (last + 1) * segmentSeconds;
728
+
729
+ // WHICH SIDE OF THE VIEWERS a piece is on. The map states it; nothing here
730
+ // works it out from positions, and nothing here knows where a viewer stands.
731
+ //
732
+ // It was read off the deadline before — no time stated meant behind — and that
733
+ // is true only of a viewer who is playing. A paused viewer has no times
734
+ // anywhere, so their whole film read as behind them, "ahead before behind"
735
+ // had nothing to compare, and the encoder was free to wander to the start of
736
+ // the file. Which side a stretch is on and how soon it is wanted are two
737
+ // different facts, and the map states both.
738
+ // WHAT RANK THE MAP GIVES THIS NUMBER — the highest, where zones overlap,
739
+ // because a number two viewers want is wanted as much as the more urgent of
740
+ // them wants it.
741
+ //
742
+ // This replaced a boolean, "is it behind everybody", and the boolean was the
743
+ // whole of what the objective knew about the map's own order. The map states
744
+ // ten ranks on a film — p100 at the number a viewer is stopped on, doubling
745
+ // zones down to p91 for the far tail, p1 for what lies behind them — and all
746
+ // of that was collapsed into two buckets and then converted to seconds, where
747
+ // "never" for the film behind is the film's own length. On a 48-minute file
748
+ // that is 2024 s, which outvotes everything: field 2026-09-08, one viewer got
749
+ // three encoders, two of them on film behind them, and the run serving them
750
+ // was killed to make room for one.
751
+ const rankAt = (at) => {
752
+ let rank = 0;
753
+ for (const span of wanted) {
754
+ if (at >= span.from && at <= span.to) {
755
+ rank = Math.max(rank, Number(span.priority) || 0);
756
+ }
757
+ }
758
+ return rank;
759
+ };
760
+ // The ranks the map actually states, most urgent first. The comparison is over
761
+ // these and nothing else, so a rank can never be outvoted by a lower one
762
+ // however many seconds are at stake there.
763
+ const ranks = [...new Set(wanted.map((span) => Number(span.priority) || 0))]
764
+ .sort((left, right) => right - left);
765
+
766
+ // EVERY COUNT IS OVER THE FILM, NOT OVER THE ENCODERS. When a piece is made
767
+ // depends on which encoder reaches it soonest, and the encoder that reaches
768
+ // film in front of the viewers may well be standing behind them.
769
+ //
770
+ // Counted over the encoders instead — each charged to the side it stands on —
771
+ // the score had a hole that swallowed everything: an arrangement with every
772
+ // encoder BEHIND the viewers had nothing charged to the film in front, so its
773
+ // second term was zero, which is the best value there is. The plan then
774
+ // abandoned the film in front of a viewer and put both encoders at the start
775
+ // of the file, which is the opposite of the rule it is supposed to obey.
776
+ //
777
+ // AND THE WAITING IS WALKED FORWARD, not summed piece by piece.
778
+ //
779
+ // Not a sum of each piece's own lateness. A viewer who is stopped is not
780
+ // watching, so everything after the piece they are stopped on is needed that
781
+ // much later too: one wait moves every deadline behind it by its own length.
782
+ //
783
+ // Summed independently instead, the far tail of a long file outvoted the film
784
+ // under the viewer's feet — measured, and it placed the only encoder at #114
785
+ // while the viewer stood at #100, because thirteen pieces of certain waiting
786
+ // "cost" less than 886 distant pieces arriving a little later. Walking the
787
+ // clock forward makes that trade impossible: abandoning the near film delays
788
+ // the far film by at least as much.
789
+ let stalled = 0;
790
+ let wastedSwarm = 0;
791
+ /** Seconds anybody waits past a deadline, per rank. @type {Map<number, number>} */
792
+ const lateAt = new Map(ranks.map((rank) => [rank, 0]));
793
+ /** When the last number of a rank is made, per rank. @type {Map<number, number>} */
794
+ const doneAt = new Map(ranks.map((rank) => [rank, 0]));
795
+ for (let index = first; index <= last; index += 1) {
796
+ // Which encoder gets to this piece first, and when. One standing on it is
797
+ // already there; one behind it must work its way up, re-making anything
798
+ // already made on the way, which costs its own time and the swarm's.
799
+ let soonest = Number.POSITIVE_INFINITY;
800
+ let byWhom = null;
801
+ for (const encoder of encoders) {
802
+ if (encoder.at > index) {
803
+ continue;
804
+ }
805
+ // WHEN THIS BODY REACHES THIS NUMBER. `delaySec` is when it finishes the
806
+ // piece it is STANDING ON, so the pieces after it are the only ones still
807
+ // to be encoded at `rate`.
808
+ //
809
+ // It used to be `(index - at + 1) / rate` on top of the delay, which
810
+ // charges every encoder a whole piece for the one it is already working on. A
811
+ // fresh encoder does start from nothing, so for it that is right — and it is
812
+ // now inside its own delay. A run 0.8 s into a 0.9 s piece does not, and
813
+ // charging it 0.94 s for that piece is what made moving it look cheaper
814
+ // than leaving it: from #58 it was priced at 1.89 s to reach #59 against
815
+ // 1.88 s for a kill and a cold start, a difference of ten milliseconds
816
+ // that is nothing but the double charge. Field 2026-09-08: 39 moves in one
817
+ // session, 24 of them between three adjacent numbers, and the picture
818
+ // stood still for 116.7 s.
819
+ const arrival = encoder.delaySec
820
+ + (index - encoder.at) / rate
821
+ + coverage.madeBetween(encoder.at, index) * refetchSecPerSegment;
822
+ if (arrival < soonest) {
823
+ soonest = arrival;
824
+ byWhom = encoder;
825
+ }
826
+ }
827
+ if (coverage.isReady(index)) {
828
+ // It exists. Nobody waits for it and nothing is owed — but whoever passes
829
+ // over it makes it a second time, and the swarm fetches its bytes again.
830
+ if (byWhom !== null) {
831
+ wastedSwarm += refetchSecPerSegment;
832
+ }
833
+ continue;
834
+ }
835
+ // A piece nobody is working towards arrives never. There is no third case:
836
+ // the host measures what it encodes at, and what it copies at, before any
837
+ // viewer exists, so a speed is always a real number and an arrival can
838
+ // always be computed.
839
+ // Nothing arrives later than never, which is the bound the film's own length
840
+ // gives. It is a definition rather than a guard: it also makes the score
841
+ // total on a host whose startup measured nothing at all, where every arrival
842
+ // is beyond reckoning and every arrangement is therefore equally hopeless.
843
+ const when = byWhom === null ? never : Math.min(soonest, never);
844
+ const rank = rankAt(index);
845
+ doneAt.set(rank, Math.max(doneAt.get(rank) ?? 0, when));
846
+ const deadline = untilNeeded(index);
847
+ if (Number.isFinite(deadline)) {
848
+ const due = deadline + stalled;
849
+ const waited = Math.max(0, when - due);
850
+ lateAt.set(rank, (lateAt.get(rank) ?? 0) + waited);
851
+ stalled += waited;
852
+ }
853
+ }
854
+
855
+ return {
856
+ // THE MAP'S OWN ORDER, AS A VECTOR. One pair per rank the map states, most
857
+ // urgent rank first: how long anybody waits at that rank, then when the last
858
+ // number of it is made.
859
+ //
860
+ // Compared position by position, so a rank is never outvoted by a lower one
861
+ // — which is the whole of what was asked for: nobody stares at a spinner;
862
+ // then the film in front of the viewers is encoded as fast as it can be, band
863
+ // by band as the map ranks them; then, with whatever is left over and only
864
+ // then, the film behind them, in case somebody seeks back.
865
+ //
866
+ // No weights, and none possible: a weight would let seconds at one rank buy
867
+ // seconds at another, and it would be a number nobody measured. The map is
868
+ // the source of truth about what matters, and it already says so.
869
+ byRank: ranks.flatMap((rank) => [lateAt.get(rank) ?? 0, doneAt.get(rank) ?? 0]),
870
+ // HOW MANY ENCODERS IT TAKES. Ranked below every rank of the map and above
871
+ // the swarm's bill, so it cannot buy one where the map is indifferent — and
872
+ // the map IS indifferent about spare capacity, which is what bought an
873
+ // encoder for film nobody waits for.
874
+ encoders: encoders.length,
875
+ // WHAT THE SWARM PAYS for anything fetched twice, which delays everything.
876
+ wasted: wastedSwarm
877
+ };
878
+ }
879
+
880
+ /**
881
+ * Is the first arrangement cheaper than the second?
882
+ *
883
+ * Three things in order, stated by the user 2026-09-06: nobody stares at a
884
+ * spinner; then the film in front of the viewers is finished soonest; then the
885
+ * whole file is. A later one decides only where the earlier ones tie.
886
+ *
887
+ * That order is why an encoder may stand BEHIND a viewer while film in front is
888
+ * still unmade: encoders work at the same time, so one in front and one behind
889
+ * can finish the file sooner than two in front — and where nobody is stalled and
890
+ * the front is closed just as fast, the file being done sooner is the answer.
891
+ *
892
+ * @param {{ stall: number, ahead: number, whole: number }} left
893
+ * @param {{ stall: number, ahead: number, whole: number }} right
894
+ * @returns {boolean}
895
+ */
896
+ function cheaperThan(left, right) {
897
+ // POSITION BY POSITION, in the map's own order of ranks. A difference at a
898
+ // higher rank settles it, and nothing at a lower one can reopen it.
899
+ //
900
+ // There is no margin here and there must not be one. A threshold — "a move
901
+ // must beat staying by at least what moving costs" — was written while the
902
+ // arrival arithmetic charged a run for a piece it had already half made, and
903
+ // it was a prop under a comparison that was wrong rather than indifferent.
904
+ // With the arithmetic right the two are 1.08 s against 1.88 s, and nothing
905
+ // needs propping.
906
+ const size = Math.max(left.byRank.length, right.byRank.length);
907
+ for (let index = 0; index < size; index += 1) {
908
+ const here = left.byRank[index] ?? 0;
909
+ const there = right.byRank[index] ?? 0;
910
+ if (here !== there) {
911
+ return here < there;
912
+ }
913
+ }
914
+ // Where every rank is served identically, fewer encoders. The map is
915
+ // indifferent, so the machine decides: a process, a reader of the piece store
916
+ // and the swarm's bandwidth are all paid by the viewers the ranks above are
917
+ // about.
918
+ if (left.encoders !== right.encoders) {
919
+ return left.encoders < right.encoders;
920
+ }
921
+ return left.wasted < right.wasted;
922
+ }
923
+
924
+ /**
925
+ * How long until a number is needed, read off the map.
926
+ *
927
+ * The map states it per stretch, for the stretch's NEAR EDGE, because a stretch
928
+ * is met at its beginning. Every number inside is needed no sooner than that, so
929
+ * taking the stretch's figure for all of them is the safe reading: it can only
930
+ * make the filling earlier than it has to be, never later.
931
+ *
932
+ * Where the map says nothing, nobody is coming and nothing can be late.
933
+ *
934
+ * Inside a stretch the time GROWS with the distance, because a viewer covers a
935
+ * second of film in a second: the number `n` places past the near edge is
936
+ * reached `n` segments of film later. Taking the near edge's figure for every
937
+ * number inside instead makes a whole stretch due at once — measured while
938
+ * building this: the first stretch is as wide as the measured allowance, so its
939
+ * far end was demanded instantly and an encoder was placed on a number another
940
+ * one was already writing.
941
+ *
942
+ * @param {WantedSpan[]} windows
943
+ * @param {number} segmentSeconds - How much film one number holds.
944
+ * @returns {(index: number) => number}
945
+ */
946
+ /** @param {WantedSpan[]} windows */
947
+ function firstOf(windows) {
948
+ return Math.min(...windows.map((span) => span.from));
949
+ }
950
+
951
+ /** @param {WantedSpan[]} windows */
952
+ function lastOf(windows) {
953
+ return Math.max(...windows.map((span) => span.to));
954
+ }
955
+
956
+ function deadlineReaderFor(windows, segmentSeconds) {
957
+ const perSegment = segmentSeconds > 0 ? segmentSeconds : 0;
958
+ return (index) => {
959
+ let soonest = Number.POSITIVE_INFINITY;
960
+ for (const span of windows) {
961
+ if (index < span.from || index > span.to) {
962
+ continue;
963
+ }
964
+ const stated = /** @type {{ withinSeconds?: number }} */ (span).withinSeconds;
965
+ // A stretch stated with no time is somebody waiting at its near edge: that
966
+ // is what stating one means. The rest of it grows with the distance, the
967
+ // same as a stated one — read as due all at once instead, a window as wide
968
+ // as a viewer's cushion demanded its far end instantly and bought an
969
+ // encoder to stand beside one already working.
970
+ // `null` IS A STATEMENT AND IT SAYS NOBODY IS COMING. `undefined` is the
971
+ // absence of one, and a caller that knows only a position is somebody
972
+ // waiting at it.
973
+ //
974
+ // Read through `Number()`, `null` becomes 0 — due NOW — so the film BEHIND
975
+ // the viewers, which the map marks with exactly that, was the most urgent
976
+ // material in the file. Everything followed from it: it bought encoders,
977
+ // it took the run standing in front of the viewer because that run was the
978
+ // nearest encoder to it, and it did so again on every pass. Field 2026-09-08:
979
+ // 39 moves in one session, 24 between three adjacent numbers, one viewer
980
+ // on three encoders, and the picture stood still for 116.7 s in three
981
+ // interruptions, the worst of them 91.8 s.
982
+ //
983
+ // The map has always said it plainly — `{"from":0,"to":57,"priority":1,
984
+ // "withinSeconds":null,"behind":true}` is in the log of every session — and
985
+ // this line turned it into its opposite. Fourth time in this repository
986
+ // that the input to a calculation was not what the calculation assumed.
987
+ const within = stated === undefined ? 0 : (stated === null ? Number.NaN : Number(stated));
988
+ if (!Number.isFinite(within)) {
989
+ // Stated as no time at all: nobody is coming here.
990
+ continue;
991
+ }
992
+ const here = within + (index - span.from) * perSegment;
993
+ if (here < soonest) {
994
+ soonest = here;
995
+ }
996
+ }
997
+ return soonest;
998
+ };
999
+ }
1000
+
1001
+ /**
1002
+ * WHERE ENCODERS BELONG, from the model rather than from a list of cases.
1003
+ *
1004
+ * The problem this solves, stated exactly:
1005
+ *
1006
+ * - the track is a line of segment numbers; `M` are the ones not made;
1007
+ * - each `x` carries a DEADLINE `D(x)`, the seconds until somebody needs it.
1008
+ * That is what the priority map is a reading of — a viewer moving forward
1009
+ * covers a second of film in a second, so the time until they are at `x` is
1010
+ * the distance to it. `Infinity` where nobody is coming;
1011
+ * - an encoder is a SEQUENTIAL producer: placed at `a`, it delivers `a + j` at
1012
+ * time `(j + 1) / r`, where `r` is segments per second, measured. It cannot
1013
+ * skip, so its whole schedule follows from where it starts;
1014
+ * - the machine affords `k` of them, measured.
1015
+ *
1016
+ * Two consequences fall out and need no rule of their own. Placements
1017
+ * `a_1 < ... < a_k` PARTITION the line: encoder `i` is useful only on
1018
+ * `[a_i, a_{i+1})`, because past that its neighbour got there first. And a
1019
+ * segment served by encoder `i` arrives at `(x - a_i + 1) / r`, which is
1020
+ * therefore also the answer to "when would the encoder already placed before it
1021
+ * get here" — the second half of the comparison, and the half that was missing.
1022
+ *
1023
+ * `x` is LATE when it arrives after `D(x)`. The objective is no late segments;
1024
+ * where `k` does not stretch to that, lateness beginning as far to the right as
1025
+ * possible.
1026
+ *
1027
+ * THE ALGORITHM is first-fit, left to right:
1028
+ *
1029
+ * for each missing x with a finite deadline, ascending:
1030
+ * if some encoder already placed at a satisfies (x - a + 1)/r <= D(x):
1031
+ * it covers x
1032
+ * else:
1033
+ * place an encoder at x
1034
+ *
1035
+ * A LIVE run enters as an encoder already placed at its own head. There is no
1036
+ * special case for it.
1037
+ *
1038
+ * WHY IT IS OPTIMAL. The leftmost missing number with a finite deadline must be
1039
+ * covered by somebody. An encoder placed exactly on it delivers it at the
1040
+ * earliest time any placement can, `1/r`, and covers the longest suffix any
1041
+ * placement can — starting further left only re-makes material and arrives
1042
+ * later, starting further right does not cover it at all. So the greedy choice
1043
+ * is never worse than any other, and the usual exchange argument carries it to
1044
+ * the whole line. This is the known result for FIXED-ORDER scheduling with
1045
+ * deadlines, where first-fit is optimal at unit processing times, and a segment
1046
+ * is one unit. General machine minimisation with release times and deadlines is
1047
+ * NP-hard; this case is polynomial because the order is forced and each machine
1048
+ * covers a contiguous stretch.
1049
+ *
1050
+ * WHAT WAS TRIED FIRST AND WAS WRONG, kept because each looked reasonable:
1051
+ *
1052
+ * - `(h - p) * s / (1 - s)`, how long a run stays in front of a viewer moving
1053
+ * forward. It answers a different question: a viewer stopped with an empty
1054
+ * buffer needs the segment now, and at exactly realtime that formula says
1055
+ * "for ever" while the viewer waits thirteen minutes;
1056
+ * - whether a run's head lies inside a wanted band — which ties an encoder to
1057
+ * whoever is standing there, and this layer must never know that;
1058
+ * - the run's head as a barrier, everything above it placeable. It has no time
1059
+ * in it at all, so it cannot tell two segments ahead from two hundred.
1060
+ *
1061
+ * Each was a case, not a model. The deadline is the model.
1062
+ *
1063
+ * @param {import("./CoverageMap.js").CoverageMap} coverage
1064
+ * @param {Set<object>} surviving - Runs that will still be alive, as encoders
1065
+ * already placed at their own heads.
1066
+ * @param {number} rate - Segments produced per second by one encoder, measured.
1067
+ * Zero when nothing has measured it, and then no arrival time can be computed
1068
+ * and every claimed number is left alone.
1069
+ * @returns {(at: number, bound: number, deadlineAt: (index: number) => number, alsoPlaced?: number[]) => number | null}
1070
+ */
1071
+ function gapFinderFor(coverage, surviving, rate, refetchSecPerSegment = 0) {
1072
+ /** Encoders already placed: where each stands, and how far its road runs. */
1073
+ const placed = [];
1074
+ for (const run of surviving) {
1075
+ const head = Number(/** @type {{ head?: number }} */ (run).head);
1076
+ placed.push({
1077
+ at: Number.isFinite(head) ? head : Number(/** @type {{ from: number }} */ (run).from),
1078
+ // A live run's road, so that placing inside it can be priced. A run given
1079
+ // no end drives to the end of the film, which is what makes the price real.
1080
+ // A run given no end drives to the end of the film, which is what makes
1081
+ // the price of cutting in front of it real. Written out rather than
1082
+ // imported: this file depends on nothing, and that is what lets it be
1083
+ // exercised with plain values alone.
1084
+ to: Number(/** @type {{ to: number }} */ (run).to) < Number(/** @type {{ from: number }} */ (run).from)
1085
+ ? Number.POSITIVE_INFINITY
1086
+ : Number(/** @type {{ to: number }} */ (run).to)
1087
+ });
1088
+ }
1089
+ return (at, bound, deadlineAt, alsoPlaced) => {
1090
+ const start = Number.isInteger(at) && at > 0 ? at : 0;
1091
+ const last = Number.isInteger(bound) ? bound : -1;
1092
+ // THE LATE NUMBER THAT IS DUE SOONEST, not the leftmost one.
1093
+ //
1094
+ // With room for every placement the two are the same answer. With a budget
1095
+ // that binds they are not, and the objective decides: lateness pushed as far
1096
+ // to the right as possible means the soonest deadline is served first. A
1097
+ // walk by number gave the one machine to a viewer due in ten minutes while
1098
+ // another stood waiting with an empty buffer.
1099
+ //
1100
+ // Ties go to the smaller number, so the answer does not depend on the order
1101
+ // the map happens to be in.
1102
+ let best = null;
1103
+ let bestDue = Number.POSITIVE_INFINITY;
1104
+ for (let index = start; index <= last; index += 1) {
1105
+ if (coverage.isReady(index)) {
1106
+ continue;
1107
+ }
1108
+ const deadline = deadlineAt(index);
1109
+ if (!Number.isFinite(deadline)) {
1110
+ // NOBODY IS COMING HERE, so nothing can be late — but the film is still
1111
+ // wanted, and this is where spare capacity goes. The number is proposed;
1112
+ // whether an encoder is actually spent on it is the score's answer, and
1113
+ // the score puts anything anybody is waiting for first.
1114
+ return index;
1115
+ }
1116
+ // When would the SOONEST of those already placed get here? Encoders placed
1117
+ // EARLIER IN THIS PASS count: the first one placed for a viewer covers the
1118
+ // stretch in front of them, and without counting it the walk placed a
1119
+ // second and a third on the very next numbers — three processes a segment
1120
+ // apart for one person, which is the waste this model exists to refuse.
1121
+ let soonest = Number.POSITIVE_INFINITY;
1122
+ for (const a of [...placed.map((live) => live.at), ...(alsoPlaced ?? [])]) {
1123
+ if (a > index) {
1124
+ // Standing past it. Encoders only move forward, so it never will.
1125
+ continue;
1126
+ }
1127
+ if (a === index) {
1128
+ // Standing ON it. No placement is faster than the one already made.
1129
+ soonest = 0;
1130
+ break;
1131
+ }
1132
+ // WHEN THIS BODY GETS HERE, and both terms of it.
1133
+ //
1134
+ // Its own encoding of everything between, and the swarm's price for the
1135
+ // film it would fetch a SECOND time — every number between that is
1136
+ // already made, it makes again. That second term is why "should this
1137
+ // encoder drive on or be moved" is not a question of its own: an
1138
+ // encoder with three hundred made pieces in front of it is simply slow
1139
+ // to arrive, and the model compares arrivals. Asked separately it was a
1140
+ // second authority over the same encoder, and the two disagreed.
1141
+ const arrival = (index - a + 1) / rate
1142
+ + coverage.madeBetween(a, index) * refetchSecPerSegment;
1143
+ if (arrival < soonest) {
1144
+ soonest = arrival;
1145
+ }
1146
+ }
1147
+ if (soonest <= deadline) {
1148
+ // Somebody gets here in time. Nothing to decide.
1149
+ continue;
1150
+ }
1151
+ // IT IS LATE, AND THAT IS ALL THIS DECIDES. Whether filling it is worth
1152
+ // the price is not asked here: this only proposes candidates, and the
1153
+ // score decides how many of them are taken and by whom. Asked here as
1154
+ // well, it was a second cost model beside the objective — with its own
1155
+ // idea of what a process costs — and the two disagreed at exactly
1156
+ // realtime, where every next piece is marginally late and each looked
1157
+ // worth its own encoder.
1158
+ if (deadline < bestDue) {
1159
+ best = index;
1160
+ bestDue = deadline;
1161
+ }
1162
+ }
1163
+ return best;
1164
+ };
1165
+ }
1166
+
1167
+ /**
1168
+ * The last number of a stretch that begins at `from` and is `length` long.
1169
+ *
1170
+ * `-1` when the length is not finite, which is this layer's word for a run with
1171
+ * no end: the film's length is not known, so there is nothing to stop it at, and
1172
+ * a number invented here would be an end nobody measured.
1173
+ *
1174
+ * @param {number} from
1175
+ * @param {number} length
1176
+ * @returns {number}
1177
+ */
1178
+ function endOfStretch(from, length) {
1179
+ return Number.isFinite(length) ? from + Math.max(1, length) - 1 : -1;
1180
+ }
1181
+
1182
+ /**
1183
+ * The lowest number a viewer is waiting for that is not ready — what the plan
1184
+ * is judged by.
1185
+ *
1186
+ * Not used to decide anything: it is the figure a log line carries, so that a
1187
+ * plan that keeps producing while a viewer waits is visible rather than
1188
+ * inferred.
1189
+ *
1190
+ * @param {import("./CoverageMap.js").CoverageMap} coverage
1191
+ * @param {WantedSpan[]} windows
1192
+ * @returns {number | null}
1193
+ */
1194
+ export function firstUnmetWant(coverage, windows) {
1195
+ let lowest = null;
1196
+ for (const span of windows ?? []) {
1197
+ for (let at = span.from; at <= span.to; at += 1) {
1198
+ if (!coverage.isReady(at)) {
1199
+ if (lowest === null || at < lowest) {
1200
+ lowest = at;
1201
+ }
1202
+ break;
1203
+ }
1204
+ }
1205
+ }
1206
+ return lowest;
1207
+ }
1208
+
1209
+ /**
1210
+ * Where to put the encoders this machine can afford.
1211
+ *
1212
+ * Two things are wanted of a division of the film, and they are wanted in this
1213
+ * order:
1214
+ *
1215
+ * 1. **the viewer must not stop.** An encoder starting at `q` stays ahead of a
1216
+ * viewer at `p` while `y / s <= q + y - p`, so it holds `(q - p) * s / (1-s)`
1217
+ * of film and no more. Beyond that the viewer catches it, and the next
1218
+ * encoder has to be standing there. That is where the first ones go, and it
1219
+ * is why the stretches grow: the further off one starts, the later the
1220
+ * viewer arrives and the longer it may work;
1221
+ * 2. **the film should be finished as soon as possible.** Once the viewer is
1222
+ * safe, whatever is left is divided EQUALLY between the encoders that
1223
+ * remain: equal shares finish together, and any other division finishes when
1224
+ * its longest share does. That is what makes seeking cheap — the film exists.
1225
+ *
1226
+ * At or above realtime the first requirement is met by one encoder for the
1227
+ * whole film, and every other encoder goes to the second — which is the common
1228
+ * case on a copied picture, and is why "one viewer, one encoder" was never the
1229
+ * rule.
1230
+ *
1231
+ * @param {object} params
1232
+ * @param {import("./CoverageMap.js").CoverageMap} params.coverage
1233
+ * @param {WantedSpan[]} params.windows - The merged map, in this output's own
1234
+ * numbering. Its highest-numbered band starts where the viewer is.
1235
+ * @param {number} params.howMany - What the machine affords.
1236
+ * @param {number} params.speedX - Measured. Zero when nothing has measured it,
1237
+ * and then only the first requirement can be served.
1238
+ * @param {(at: number, bound: number, deadlineAt: (index: number) => number, placed: number[]) => number | null} [params.firstGap] -
1239
+ * Where a gap may be opened. Defaults to the map's own answer; the plan hands
1240
+ * in one that also counts a number a live run has claimed but will not reach
1241
+ * before it is needed, which is the only way anybody beyond a working encoder
1242
+ * is served.
1243
+ * @param {(index: number) => number} [params.deadlineAt] - Seconds until that
1244
+ * number is needed. `Infinity` where nobody is coming. Absent means every
1245
+ * stated want is due now.
1246
+ * @returns {number[]} Where to start each encoder, ascending.
1247
+ */
1248
+ export function placeEncoders({
1249
+ coverage,
1250
+ windows,
1251
+ howMany,
1252
+ firstGap = null,
1253
+ deadlineAt = null,
1254
+ // WHERE TWO ENCODERS SHARE A STRETCH, and whether a second is worth having at
1255
+ // all. Derived by whoever holds the measurements — the rates, the contention
1256
+ // penalty, what a start costs — because this function is positional and holds
1257
+ // none of them. The default is the midpoint, which IS the answer when both are
1258
+ // fresh and owe the same, and is what a caller with nothing measured falls
1259
+ // back on.
1260
+ splitAt = (from, to) => from + Math.floor((to - from + 1) / 2)
1261
+ }) {
1262
+ if (!(howMany > 0) || windows.length === 0) {
1263
+ return [];
1264
+ }
1265
+ // NOW when the caller says nothing. A stated want with no time is somebody
1266
+ // waiting on it — that is what stating one means — so the honest reading is
1267
+ // that it is due. `Infinity` is a statement in its own right and has to be
1268
+ // made: it says nobody is coming.
1269
+ const untilNeeded = deadlineAt ?? (() => 0);
1270
+ /** Where this pass has placed so far — each one covers what it can reach. */
1271
+ const placedHere = [];
1272
+ const gapAt = firstGap
1273
+ ? (at, bound) => firstGap(at, bound, untilNeeded, placedHere)
1274
+ : (at, bound) => coverage.firstGapFrom(at, bound);
1275
+
1276
+
1277
+ // CANDIDATES COME FROM THE PRIORITY MAP, IN THE ORDER THE MAP STATES.
1278
+ //
1279
+ // The map already answers every question that was being re-derived here. Its
1280
+ // ranks say what matters most — the number a viewer is stopped on, then what
1281
+ // is in front of them band by band, then the rest of the track, and last of
1282
+ // all what lies behind them. A pause flattens those ranks; a seek moves them;
1283
+ // a second viewer merges into them. So walking the map in its own order is
1284
+ // what "ahead before behind" means, and nothing here has to work out where the
1285
+ // viewers are.
1286
+ //
1287
+ // It was not read that way. This walked the film by number and proposed
1288
+ // whatever was late, then a second pass divided the leftovers — an order of
1289
+ // its own invention, which put the beginning of the file before the film in
1290
+ // front of a viewer and, at one point, proposed #0, #1 and #2 as three
1291
+ // separate places.
1292
+ //
1293
+ // One candidate per zone: the first number in it nobody has and nobody
1294
+ // reaches in time. Zones with no deadline can have nothing late in them, so
1295
+ // there it is simply the first number nobody has — which is how spare capacity
1296
+ // comes to finish the file.
1297
+ /** @type {number[]} */
1298
+ const places = [];
1299
+ const byRank = [...windows].sort(
1300
+ (left, right) => (right.priority ?? 0) - (left.priority ?? 0) || left.from - right.from
1301
+ );
1302
+ for (const zone of byRank) {
1303
+ if (places.length >= howMany) {
1304
+ break;
1305
+ }
1306
+ const at = gapAt(zone.from, zone.to);
1307
+ if (at === null || places.includes(at)) {
1308
+ continue;
1309
+ }
1310
+ places.push(at);
1311
+ placedHere.push(at);
1312
+ }
1313
+
1314
+ // AND WHERE TO SPLIT WHAT IS LEFT, for the capacity the map has not spent.
1315
+ //
1316
+ // The search above proposes only what is LATE, so once one encoder covers a
1317
+ // zone in time that zone proposes nothing more — and a machine that holds four
1318
+ // ran one. Finishing a contiguous stretch soonest with several machines of the
1319
+ // same speed means dividing it between them, which is where these come from:
1320
+ // the widest run of film between two encoders, split.
1321
+ //
1322
+ // Proposing is not spending. The score decides whether another process is
1323
+ // worth it, and its first term — how late the film is — always outranks its
1324
+ // second, so this can never take capacity from somebody waiting.
1325
+ while (places.length < howMany) {
1326
+ const edges = [...places].sort((left, right) => left - right);
1327
+ let widestFrom = null;
1328
+ let widest = 0;
1329
+ for (let index = 0; index <= edges.length; index += 1) {
1330
+ const from = index === 0 ? firstOf(windows) : edges[index - 1] + 1;
1331
+ const to = index === edges.length ? lastOf(windows) : edges[index] - 1;
1332
+ const room_ = coverage.unmadeRunFrom(from);
1333
+ if (to >= from && room_ > widest) {
1334
+ // WHERE THE TWO OF THEM MEET, calculated rather than halved. Halving is
1335
+ // the answer only when both encoders are fresh and owe the same, and it
1336
+ // was applied to every case — one already working is partway through a
1337
+ // piece while a new one owes its whole start, so the point where they
1338
+ // finish together lies further on. Nothing back means a second encoder
1339
+ // does not pay for itself here at all: two under this host's measured
1340
+ // contention against one at full speed, a comparison halving never made.
1341
+ const meet = splitAt(from, Math.min(from + room_ - 1, to));
1342
+ if (meet !== null) {
1343
+ widest = room_;
1344
+ widestFrom = meet;
1345
+ }
1346
+ }
1347
+ }
1348
+ if (widestFrom === null) {
1349
+ break;
1350
+ }
1351
+ const at = coverage.firstGapFrom(widestFrom, lastOf(windows));
1352
+ if (at === null || places.includes(at)) {
1353
+ break;
1354
+ }
1355
+ places.push(at);
1356
+ placedHere.push(at);
1357
+ }
1358
+
1359
+ // These are CANDIDATES, not decisions. What is late proposes first, because
1360
+ // that is what a viewer feels; what is merely unmade proposes after it. The
1361
+ // score decides which of them are worth a process, and a paused viewer, who
1362
+ // states no deadline at all, therefore still leaves the file being finished
1363
+ // rather than the machine falling idle.
1364
+ return places.sort((left, right) => left - right);
1365
+ }
1366
+