@torrent-tv/proxy 2.89.7 → 2.89.9

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 (498) 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 +363 -338
  13. package/CHANGELOG.md +2345 -2335
  14. package/Dockerfile +30 -30
  15. package/LICENSE +16 -16
  16. package/README.md +369 -369
  17. package/assets/calibration/NOTICE.md +100 -100
  18. package/assets/calibration/audio/NOTICE.md +19 -19
  19. package/assets/calibration/audio/manifest.json +98 -98
  20. package/assets/diagnostics/sctpstate.gdb +31 -31
  21. package/bin/cli.js +684 -684
  22. package/bin/thread-pool.js +30 -30
  23. package/biome.json +412 -412
  24. package/docs/browser-proxy-contract.md +155 -155
  25. package/docs/container-architecture.md +273 -273
  26. package/docs/download-architecture.md +261 -261
  27. package/docs/encode-architecture.md +765 -765
  28. package/docs/encode-run-state.md +96 -96
  29. package/docs/logs.md +97 -97
  30. package/docs/playback-start-forecast.md +113 -113
  31. package/docs/socks5-egress-research.md +114 -114
  32. package/docs/storage-architecture.md +273 -273
  33. package/knip.json +14 -14
  34. package/openspec/changes/adaptive-bitrate/design.md +92 -92
  35. package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
  36. package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
  37. package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
  38. package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
  39. package/openspec/changes/binary-distribution/proposal.md +53 -53
  40. package/openspec/changes/chunked-request-bodies/design.md +81 -81
  41. package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
  42. package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
  43. package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
  44. package/openspec/changes/cold-start/design.md +98 -98
  45. package/openspec/changes/cold-start/proposal.md +62 -62
  46. package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
  47. package/openspec/changes/cold-start/tasks.md +48 -48
  48. package/openspec/changes/disk-cap/.openspec.yaml +2 -2
  49. package/openspec/changes/disk-cap/proposal.md +37 -37
  50. package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
  51. package/openspec/changes/disk-cap/tasks.md +19 -19
  52. package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
  53. package/openspec/changes/proxy-observability/design.md +38 -38
  54. package/openspec/changes/proxy-observability/proposal.md +49 -49
  55. package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
  56. package/openspec/changes/proxy-observability/tasks.md +21 -21
  57. package/openspec/changes/seek-debounce/design.md +118 -118
  58. package/openspec/changes/seek-debounce/proposal.md +59 -59
  59. package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
  60. package/openspec/changes/seek-debounce/tasks.md +33 -33
  61. package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
  62. package/openspec/changes/subtitle-language/proposal.md +50 -50
  63. package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
  64. package/openspec/changes/subtitle-language/tasks.md +26 -26
  65. package/openspec/changes/track-selection/.openspec.yaml +2 -2
  66. package/openspec/changes/track-selection/design.md +47 -47
  67. package/openspec/changes/track-selection/proposal.md +45 -45
  68. package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
  69. package/openspec/changes/track-selection/tasks.md +20 -20
  70. package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
  71. package/openspec/changes/transcode-quality/proposal.md +51 -51
  72. package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
  73. package/openspec/changes/transcode-quality/tasks.md +63 -63
  74. package/openspec/config.yaml +38 -38
  75. package/package.json +51 -50
  76. package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
  77. package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
  78. package/research/double-spawn-2026-09-10.md +171 -171
  79. package/research/handover-2026-09-13.md +209 -209
  80. package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
  81. package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
  82. package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
  83. package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
  84. package/routes/api/client-logs/post.js +86 -86
  85. package/routes/api/delivery-sink/get.js +72 -72
  86. package/routes/api/link-probe/get.js +51 -51
  87. package/routes/api/playback-plan/audio-tracks/post.js +29 -29
  88. package/routes/api/playback-plan/post.js +53 -53
  89. package/routes/api/sources/files/get.js +106 -106
  90. package/routes/api/sources/post.js +36 -36
  91. package/routes/api/sources/stats/get.js +132 -132
  92. package/routes/api/sources/warm/post.js +217 -217
  93. package/routes/api/subtitles/get.js +230 -230
  94. package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
  95. package/routes/api/transcode-sessions/net-report/post.js +138 -138
  96. package/routes/api/transcode-sessions/post.js +178 -178
  97. package/routes/api/transcode-sessions/progress/get.js +27 -27
  98. package/routes/api/transcode-sessions/release/post.js +41 -41
  99. package/routes/api/transcode-sessions/seek/post.js +63 -63
  100. package/routes/health/get.js +13 -13
  101. package/routes/healthz/get.js +13 -13
  102. package/routes/stream/get.js +416 -416
  103. package/routes/transcode/audio-file/get.js +64 -64
  104. package/routes/transcode/audio-warm/get.js +95 -95
  105. package/routes/transcode/session-file/get.js +52 -52
  106. package/routes/transcode/variant-file/get.js +95 -95
  107. package/routes/transcode/variant-warm/get.js +84 -84
  108. package/scripts/build-audio-samples.mjs +31 -31
  109. package/scripts/render-run-graph.js +173 -173
  110. package/server.js +748 -748
  111. package/services/encode/CoverageMap.js +428 -428
  112. package/services/encode/CushionReport.js +345 -345
  113. package/services/encode/EncodeAdmission.js +376 -376
  114. package/services/encode/EncodeOrchestrator.js +1192 -1192
  115. package/services/encode/EncodePlan.js +1368 -1368
  116. package/services/encode/EncodeRun.js +36 -12
  117. package/services/encode/Encoder.js +142 -142
  118. package/services/encode/EncoderSelection.js +38 -38
  119. package/services/encode/LocalObservations.js +359 -359
  120. package/services/encode/NvencEncoder.js +50 -50
  121. package/services/encode/OutputOpening.js +784 -784
  122. package/services/encode/OutputTimes.js +579 -579
  123. package/services/encode/QsvEncoder.js +58 -58
  124. package/services/encode/Renditions.js +2283 -2283
  125. package/services/encode/RunClock.js +87 -87
  126. package/services/encode/RunProgress.js +79 -79
  127. package/services/encode/SegmentDemand.js +95 -95
  128. package/services/encode/SoftwareEncoder.js +121 -121
  129. package/services/encode/V4l2m2mEncoder.js +53 -53
  130. package/services/encode/VaapiEncoder.js +66 -66
  131. package/services/encode/args.js +418 -418
  132. package/services/encode/audio-calibration.js +67 -67
  133. package/services/encode/audio-work-rate.js +15 -15
  134. package/services/encode/available-share.js +89 -89
  135. package/services/encode/calibration.js +361 -361
  136. package/services/encode/contention.js +128 -128
  137. package/services/encode/decode-cost-fit.js +363 -363
  138. package/services/encode/encode-exit.js +116 -116
  139. package/services/encode/encode-run-state.js +551 -551
  140. package/services/encode/encoder-readings.js +43 -43
  141. package/services/encode/fingerprint.js +322 -322
  142. package/services/encode/host-load.js +174 -174
  143. package/services/encode/hwaccel.js +1894 -1894
  144. package/services/encode/index.js +9 -9
  145. package/services/encode/learned-median.js +90 -90
  146. package/services/encode/output/EncodedOutput.js +57 -57
  147. package/services/encode/output/Output.js +131 -131
  148. package/services/encode/output/OutputCatalog.js +414 -414
  149. package/services/encode/output/OutputSpec.js +390 -390
  150. package/services/encode/output/Timeline.js +308 -308
  151. package/services/encode/output/cut-grid.js +107 -107
  152. package/services/encode/output/index.js +24 -24
  153. package/services/encode/output/ladder.js +32 -32
  154. package/services/encode/output/playlists.js +284 -284
  155. package/services/encode/output/rates.js +125 -125
  156. package/services/encode/output-key-format.js +21 -21
  157. package/services/encode/quality/EncodeCost.js +1043 -1043
  158. package/services/encode/quality/HostLoad.js +525 -525
  159. package/services/encode/quality/HostTimings.js +249 -249
  160. package/services/encode/quality/OutputQualityState.js +24 -24
  161. package/services/encode/quality/QualityController.js +758 -758
  162. package/services/encode/quality/QualityOffer.js +416 -416
  163. package/services/encode/quality/drain-threat.js +47 -47
  164. package/services/encode/quality/link-budget.js +337 -337
  165. package/services/encode/quality/output-format.js +319 -319
  166. package/services/encode/quality/serving-output.js +216 -216
  167. package/services/encode/quality/visible-rung.js +57 -57
  168. package/services/encode/run-budget.js +97 -97
  169. package/services/encode/run-command.js +757 -757
  170. package/services/encode/run-costs.js +155 -155
  171. package/services/encode/run-inputs.js +53 -53
  172. package/services/encode/segment-formats/fmp4.js +476 -476
  173. package/services/encode/segment-formats/init-compat.js +324 -324
  174. package/services/encode/segment-formats/mpegts.js +134 -134
  175. package/services/encode/start-stop-cost.js +178 -178
  176. package/services/encode/throughput.js +97 -97
  177. package/services/encode/torrent-cost.js +88 -88
  178. package/services/identity/installation-id.js +38 -38
  179. package/services/media/ContainerOrchestrator.js +202 -202
  180. package/services/media/KeyframeTables.js +356 -356
  181. package/services/media/SourceFile.js +298 -298
  182. package/services/media/SubtitleCues.js +592 -592
  183. package/services/media/SubtitleOrchestrator.js +167 -167
  184. package/services/media/audio-inventory.js +299 -299
  185. package/services/media/container/AviContainer.js +268 -268
  186. package/services/media/container/Container.js +632 -632
  187. package/services/media/container/ContainerFactory.js +38 -38
  188. package/services/media/container/KeyframeTable.js +272 -272
  189. package/services/media/container/MatroskaContainer.js +1031 -1031
  190. package/services/media/container/Mp4Container.js +1354 -1354
  191. package/services/media/container/SubtitleFileContainer.js +339 -339
  192. package/services/media/container/ebml-reader.js +159 -159
  193. package/services/media/container/ebml-stream.js +174 -174
  194. package/services/media/container/matroska-clusters.js +1165 -1165
  195. package/services/media/container/unavailable.js +77 -77
  196. package/services/media/ffmpeg-banner.js +260 -260
  197. package/services/media/keyframe-probe.js +157 -157
  198. package/services/media/media-info-probe.js +93 -93
  199. package/services/media/playback-planner.js +931 -931
  200. package/services/media/tracks/AudioTrack.js +143 -143
  201. package/services/media/tracks/ContainerTrack.js +84 -84
  202. package/services/media/tracks/ImageSubtitleTrack.js +19 -19
  203. package/services/media/tracks/SubtitleTrack.js +38 -38
  204. package/services/media/tracks/TextSubtitleTrack.js +295 -295
  205. package/services/media/tracks/VideoTrack.js +33 -33
  206. package/services/media/tracks/audio-bound.js +233 -233
  207. package/services/media/tracks/index.js +15 -15
  208. package/services/media/tracks/language-detect.js +174 -174
  209. package/services/server/OutputLifecycle.js +418 -418
  210. package/services/server/SegmentServing.js +1152 -1152
  211. package/services/server/ViewerRequests.js +785 -785
  212. package/services/server/WaitLedger.js +142 -142
  213. package/services/server/controllers/PlaybackController.js +40 -40
  214. package/services/server/controllers/SubtitleController.js +109 -109
  215. package/services/server/http-responses.js +60 -60
  216. package/services/server/transcode-session-files.js +325 -325
  217. package/services/server/wire-outputs.js +1029 -1029
  218. package/services/storage/Diagnostics.js +202 -202
  219. package/services/storage/MachineBudget.js +235 -235
  220. package/services/storage/allowance.js +107 -107
  221. package/services/storage/core-dumps.js +106 -106
  222. package/services/storage/files/CompletedFiles.js +391 -391
  223. package/services/storage/files/piece-from-whole-file.js +118 -118
  224. package/services/storage/free.js +71 -71
  225. package/services/storage/keep.js +48 -48
  226. package/services/storage/machine-memory.js +56 -56
  227. package/services/storage/memory-report.js +575 -575
  228. package/services/storage/piece-store/piece-disk-store.js +539 -539
  229. package/services/storage/piece-store/piece-lru.js +495 -495
  230. package/services/storage/piece-store/shared-piece-store.js +1937 -1937
  231. package/services/storage/returns.js +103 -103
  232. package/services/storage/segment-store/SegmentStore.js +1294 -1294
  233. package/services/storage/wire.js +175 -175
  234. package/services/torrent/Contents.js +382 -382
  235. package/services/torrent/demand/DemandRegister.js +202 -202
  236. package/services/torrent/demand/Urgency.js +137 -137
  237. package/services/torrent/demand/Window.js +118 -118
  238. package/services/torrent/demand/index.js +8 -8
  239. package/services/torrent/demand/pieces.js +140 -140
  240. package/services/torrent/download/SwarmSelection.js +386 -386
  241. package/services/torrent/download/registry.js +102 -102
  242. package/services/torrent/download/withdraw-claim.js +80 -80
  243. package/services/torrent/episode-naming.js +182 -182
  244. package/services/torrent/files.js +455 -455
  245. package/services/torrent/naming.js +619 -619
  246. package/services/torrent/piece-store-of.js +29 -29
  247. package/services/torrent/supply-margin.js +264 -264
  248. package/services/torrent/torrent-pool.js +2688 -2688
  249. package/services/torrent/worker/background-fill.js +213 -213
  250. package/services/torrent/worker/channel.js +271 -271
  251. package/services/torrent/worker/client.js +836 -836
  252. package/services/torrent/worker/fastest-wires.js +342 -342
  253. package/services/torrent/worker/handle-state.js +23 -23
  254. package/services/torrent/worker/held-bytes.js +231 -231
  255. package/services/torrent/worker/install-webrtc-shim.js +32 -32
  256. package/services/torrent/worker/piece-reader.js +1078 -1078
  257. package/services/torrent/worker/pool-adapter.js +371 -371
  258. package/services/torrent/worker/protocol.js +184 -184
  259. package/services/torrent/worker/resume-warm.js +107 -107
  260. package/services/torrent/worker/webrtc-shim.js +219 -219
  261. package/services/torrent/worker/worker.js +1137 -1137
  262. package/services/transport/data-channel-handler.js +1406 -1373
  263. package/services/transport/delivery-probe.js +798 -842
  264. package/services/transport/health-collector.js +53 -53
  265. package/services/transport/nat-classifier.js +231 -231
  266. package/services/transport/packet-witness.js +808 -808
  267. package/services/transport/port-mapper.js +235 -235
  268. package/services/transport/registry-api.js +81 -81
  269. package/services/transport/tunnel-client.js +447 -447
  270. package/services/transport/usrsctp-state.js +175 -175
  271. package/services/transport/webrtc-manager.js +484 -484
  272. package/services/viewer/Assignments.js +340 -340
  273. package/services/viewer/PriorityMap.js +421 -421
  274. package/services/viewer/PriorityOrchestrator.js +325 -325
  275. package/services/viewer/Viewer.js +691 -691
  276. package/services/viewer/Viewers.js +368 -368
  277. package/services/viewer/active-output.js +37 -37
  278. package/services/viewer/audio-start.js +46 -46
  279. package/services/viewer/buffer-trend.js +81 -81
  280. package/services/viewer/choices.js +647 -647
  281. package/services/viewer/media-time.js +355 -355
  282. package/services/viewer/playback-readiness.js +635 -635
  283. package/services/viewer/positions.js +126 -126
  284. package/services/viewer/report-intake.js +46 -46
  285. package/store/source-registry.js +71 -71
  286. package/test/asset-paths.test.js +28 -28
  287. package/test/audio-buffer-continuity.test.js +77 -77
  288. package/test/audio-calibration.test.js +23 -23
  289. package/test/audio-inventory.test.js +212 -212
  290. package/test/audio-track-bitrate.test.js +40 -40
  291. package/test/auto-quality-step.test.js +762 -762
  292. package/test/available-share.test.js +76 -76
  293. package/test/background-fill.test.js +190 -190
  294. package/test/client-log-file.test.js +110 -110
  295. package/test/coalesce.test.js +39 -39
  296. package/test/completed-files.test.js +115 -115
  297. package/test/concurrent-cost.test.js +148 -148
  298. package/test/container-media-info.test.js +228 -228
  299. package/test/contention.test.js +76 -76
  300. package/test/core-dumps.test.js +57 -57
  301. package/test/coverage-follows-the-disk.test.js +233 -228
  302. package/test/coverage-map.test.js +195 -195
  303. package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
  304. package/test/cut-times-timeline.test.js +65 -65
  305. package/test/cuts-follow-published-grid.test.js +109 -109
  306. package/test/data-channel-frame.test.js +63 -63
  307. package/test/data-channel-viewer-lifecycle.test.js +162 -92
  308. package/test/dead-torrent-handle.test.js +52 -52
  309. package/test/declared-rates.test.js +116 -116
  310. package/test/declared-tracks.test.js +43 -43
  311. package/test/decode-cost-fit.test.js +202 -202
  312. package/test/decode-cost.test.js +475 -475
  313. package/test/delivery-probe-field.test.js +173 -0
  314. package/test/delivery-probe.test.js +382 -393
  315. package/test/demand-register.test.js +206 -206
  316. package/test/dht-bootstrap.test.js +72 -72
  317. package/test/drain-threat.test.js +65 -65
  318. package/test/duplicate-add.test.js +48 -48
  319. package/test/encode-admission.test.js +146 -146
  320. package/test/encode-cost.test.js +275 -275
  321. package/test/encode-exit.test.js +111 -111
  322. package/test/encode-orchestrator.test.js +426 -426
  323. package/test/encode-plan-viewers.test.js +725 -725
  324. package/test/encode-plan.test.js +545 -545
  325. package/test/encode-run-state.test.js +314 -314
  326. package/test/encode-run.test.js +20 -0
  327. package/test/encode-slope.test.js +46 -46
  328. package/test/encoded-output.test.js +39 -39
  329. package/test/encoder-kinds.test.js +122 -122
  330. package/test/encoder-readings.test.js +68 -68
  331. package/test/episode-naming.test.js +131 -131
  332. package/test/every-claimant-is-told-its-share.test.js +152 -152
  333. package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
  334. package/test/failed-start-breaker.test.js +202 -202
  335. package/test/fastest-wires.test.js +143 -143
  336. package/test/ffmpeg-stream-counts.test.js +78 -78
  337. package/test/file-edges.test.js +191 -191
  338. package/test/fixtures/audio-frame-boundary.json +1 -1
  339. package/test/fmp4-self-contained.test.js +80 -80
  340. package/test/fmp4-timestamps.test.js +140 -140
  341. package/test/fmp4-track-completeness.test.js +41 -41
  342. package/test/format-keyed-outputs.test.js +78 -78
  343. package/test/health-metrics.test.js +36 -38
  344. package/test/held-read-ownership.test.js +77 -77
  345. package/test/held-request-width.test.js +157 -157
  346. package/test/helpers/encode-run.js +248 -240
  347. package/test/helpers/held-file.js +77 -77
  348. package/test/helpers/manager.js +62 -62
  349. package/test/helpers/matroska-file.js +296 -296
  350. package/test/helpers/output-spec.js +63 -63
  351. package/test/host-load.test.js +68 -68
  352. package/test/index-accuracy-is-about-the-file.test.js +128 -128
  353. package/test/init-compat.test.js +219 -219
  354. package/test/input-lost-quiets-the-plan.test.js +261 -261
  355. package/test/input-unavailable.test.js +48 -48
  356. package/test/installation-id.test.js +39 -39
  357. package/test/keeping-period.test.js +83 -83
  358. package/test/keyframe-index-accuracy.test.js +188 -188
  359. package/test/keyframe-table-budget.test.js +123 -123
  360. package/test/keyframes-belong-to-the-file.test.js +249 -249
  361. package/test/learned-median.test.js +37 -37
  362. package/test/letting-go-asks-who-is-watching.test.js +191 -191
  363. package/test/limit-row-by-area.test.js +60 -60
  364. package/test/link-budget.test.js +111 -111
  365. package/test/link-probe.test.js +56 -56
  366. package/test/local-encoding-data.test.js +273 -273
  367. package/test/logger-repeats.test.js +134 -134
  368. package/test/look-ahead-coverage.test.js +56 -56
  369. package/test/manager-surface.test.js +291 -291
  370. package/test/matroska-cluster-discovery.test.js +323 -323
  371. package/test/matroska-cues-track.test.js +192 -192
  372. package/test/media-time.test.js +62 -62
  373. package/test/memory-budget.test.js +390 -390
  374. package/test/move-cost.test.js +216 -216
  375. package/test/mp4-subtitles.test.js +173 -173
  376. package/test/one-authority.test.js +285 -285
  377. package/test/one-budget.test.js +149 -149
  378. package/test/one-output-one-session.test.js +273 -273
  379. package/test/one-resource-divided.test.js +185 -185
  380. package/test/one-serving-output-per-height.test.js +458 -458
  381. package/test/orchestrator-wired.test.js +207 -207
  382. package/test/output-catalog.test.js +39 -39
  383. package/test/output-shape.test.js +21 -21
  384. package/test/output-spec.test.js +251 -251
  385. package/test/output-speed.test.js +92 -87
  386. package/test/outputs-being-prepared.test.js +40 -40
  387. package/test/packet-witness-ring.test.js +236 -236
  388. package/test/packet-witness.test.js +148 -148
  389. package/test/piece-disk-store.test.js +401 -401
  390. package/test/piece-from-whole-file.test.js +129 -129
  391. package/test/piece-lru.test.js +319 -319
  392. package/test/piece-reader.test.js +162 -162
  393. package/test/piece-selection-offset.test.js +85 -85
  394. package/test/piece-store-eviction.test.js +595 -595
  395. package/test/piece-store-never-refuses.test.js +153 -153
  396. package/test/piece-store-reservations.test.js +403 -403
  397. package/test/piece-store-slow-disk.test.js +159 -159
  398. package/test/piece-tail.test.js +97 -97
  399. package/test/plan-host-timings.test.js +68 -68
  400. package/test/plan-is-steady.test.js +225 -225
  401. package/test/playback-readiness.test.js +518 -518
  402. package/test/playback-start-regressions.test.js +101 -101
  403. package/test/priority-map-download.test.js +174 -174
  404. package/test/priority-map-emptied.test.js +204 -204
  405. package/test/priority-map-per-output.test.js +215 -215
  406. package/test/priority-map.test.js +163 -163
  407. package/test/probe-wedge-certainty.test.js +60 -60
  408. package/test/produced-copy-choice.test.js +213 -213
  409. package/test/published-timeline.test.js +83 -83
  410. package/test/quality-offer.test.js +160 -160
  411. package/test/quality-variants.test.js +1274 -1270
  412. package/test/rate-control-in-the-output.test.js +181 -180
  413. package/test/read-survives-withdrawal.test.js +164 -164
  414. package/test/read-window.test.js +365 -365
  415. package/test/reads-not-yet-arrived.test.js +218 -218
  416. package/test/resume-warm.test.js +39 -39
  417. package/test/run-graph-drift.test.js +37 -37
  418. package/test/run-inputs.test.js +80 -80
  419. package/test/run-intervals.test.js +100 -100
  420. package/test/run-position-follows-published-grid.test.js +89 -89
  421. package/test/run-progress.test.js +64 -64
  422. package/test/same-height-switch.test.js +663 -663
  423. package/test/seek-frees-the-viewers-requests.test.js +217 -217
  424. package/test/seek-landing.test.js +109 -109
  425. package/test/seek-target-not-superseded.test.js +195 -195
  426. package/test/segment-closed-on-both-branches.test.js +114 -114
  427. package/test/segment-cut-times.test.js +63 -63
  428. package/test/segment-demand.test.js +80 -80
  429. package/test/segment-hold-supersede.test.js +91 -91
  430. package/test/segment-judged-by-its-own-header.test.js +222 -222
  431. package/test/segment-serve-wiring.test.js +481 -481
  432. package/test/segment-store-eviction.test.js +247 -247
  433. package/test/segment-store.test.js +352 -352
  434. package/test/segment-true-start.test.js +86 -86
  435. package/test/segments-are-shared.test.js +176 -176
  436. package/test/send-chunk-bytes.test.js +86 -86
  437. package/test/serving-existing-output.test.js +111 -111
  438. package/test/serving-output.test.js +102 -102
  439. package/test/shared-piece-store.test.js +310 -310
  440. package/test/sidecar-naming.test.js +142 -142
  441. package/test/soundtrack-rate-bound.test.js +289 -289
  442. package/test/source-file.test.js +133 -133
  443. package/test/source-files-route.test.js +113 -113
  444. package/test/source-registry.test.js +18 -18
  445. package/test/split-a-stretch.test.js +139 -139
  446. package/test/stale-request-after-seek.test.js +158 -158
  447. package/test/startup-readings.test.js +124 -124
  448. package/test/stream-route.test.js +237 -237
  449. package/test/subtitle-cue-framing.test.js +200 -200
  450. package/test/subtitle-cue-source.test.js +102 -102
  451. package/test/subtitle-cue-walk.test.js +385 -385
  452. package/test/subtitle-cursor.test.js +63 -63
  453. package/test/subtitle-defaults.test.js +118 -118
  454. package/test/subtitle-language.test.js +252 -252
  455. package/test/subtitle-metadata.test.js +15 -15
  456. package/test/subtitle-plan-waits.test.js +125 -125
  457. package/test/subtitle-track-numbering.test.js +391 -391
  458. package/test/supply-margin.test.js +190 -190
  459. package/test/swarm-follows-readers.test.js +174 -174
  460. package/test/swarm-reach.test.js +130 -130
  461. package/test/swarm-selection.test.js +220 -220
  462. package/test/tail-duplication.test.js +167 -167
  463. package/test/the-channel-has-both-ends.test.js +91 -91
  464. package/test/the-torrent-thread-serves-bytes.test.js +107 -107
  465. package/test/thread-pool.test.js +53 -53
  466. package/test/timeline.test.js +84 -84
  467. package/test/torrent-contents.test.js +235 -235
  468. package/test/torrent-cost.test.js +71 -71
  469. package/test/torrent-files.test.js +221 -221
  470. package/test/torrent-naming.test.js +255 -255
  471. package/test/tracks-begin-together.test.js +209 -209
  472. package/test/tunnel-renewal.test.js +167 -167
  473. package/test/two-viewers-one-picture.test.js +508 -508
  474. package/test/upload-hurry.test.js +186 -186
  475. package/test/usrsctp-state.test.js +131 -131
  476. package/test/video-facts.test.js +102 -102
  477. package/test/viewer-assignments.test.js +143 -143
  478. package/test/viewer-outputs.test.js +285 -285
  479. package/test/viewer-presence.test.js +116 -116
  480. package/test/viewer-requests-admission.test.js +92 -92
  481. package/test/viewer-starts-on-map.test.js +27 -27
  482. package/test/viewer-states.test.js +192 -189
  483. package/test/viewer-subtitle-subscription.test.js +96 -96
  484. package/test/viewer.test.js +139 -139
  485. package/test/viewing-generations.test.js +390 -390
  486. package/test/visible-rung.test.js +37 -37
  487. package/test/wait-ledger.test.js +100 -100
  488. package/test/webrtc-shim.test.js +149 -149
  489. package/test/wedge-certainty.test.js +131 -131
  490. package/test/whole-file-held-read.test.js +30 -30
  491. package/test/withdraw-piece-claim.test.js +212 -212
  492. package/test/worker-channel.test.js +158 -158
  493. package/utils/client-log-file.js +151 -151
  494. package/utils/coalesce.js +41 -41
  495. package/utils/logger.js +312 -312
  496. package/utils/parse-range.js +25 -25
  497. package/utils/perf.js +59 -59
  498. package/utils/torrent-source-key.js +38 -38
@@ -1,725 +1,725 @@
1
- /**
2
- * @file The placement model under one, two and three viewers, each of them
3
- * playing, seeking and paused.
4
- *
5
- * THE MODEL, restated so a failure here can be read against it:
6
- *
7
- * - each segment number carries a deadline `D(x)`, the seconds until somebody
8
- * needs it. A viewer moving forward covers a second of film in a second, so
9
- * `D(x)` is the distance to them. `Infinity` where nobody is coming;
10
- * - an encoder placed at `a` delivers `a + j` at `(j + 1) / r`, `r` segments per
11
- * second. The same expression answers "when would the one already placed get
12
- * here";
13
- * - a number is late when it arrives after its deadline. Placement is first-fit
14
- * left to right: give it to an encoder that arrives in time, else open one
15
- * exactly there.
16
- *
17
- * Three behaviours of the map follow from that and are checked here rather than
18
- * assumed. A peak MOVES FORWARD on its own, because the deadline is a distance
19
- * and the distance shrinks as the viewer watches. It JUMPS on a seek. It
20
- * FLATTENS on a pause, because a viewer who is not moving has no time by which
21
- * anything must exist.
22
- *
23
- * Nothing here spawns ffmpeg, touches a disk or reads a clock. That is the layer
24
- * check, made executable: this layer is exercised with plain values alone.
25
- */
26
-
27
- import test from "node:test";
28
- import assert from "node:assert/strict";
29
- import { EventEmitter } from "node:events";
30
- import { EncodeRun } from "../services/encode/EncodeRun.js";
31
- import { measureSpeed } from "./helpers/encode-run.js";
32
- import { SoftwareEncoder } from "../services/encode/SoftwareEncoder.js";
33
- import { EncodeOrchestrator } from "../services/encode/EncodeOrchestrator.js";
34
- import { mapForViewer, mergeMaps, runsOf } from "../services/viewer/PriorityMap.js";
35
- import { penaltiesFrom } from "../services/encode/contention.js";
36
-
37
- // WHAT A SECOND ENCODER COSTS THE FIRST — measured, never a formula.
38
- //
39
- // Addon host, 2026-09-03: 854x480 through libx264 `ultrafast` ran at 7.12x with
40
- // the machine to itself, and at 4.20x and 4.16x when two ran at once. The
41
- // penalty is read off that reading by the SAME two functions production uses,
42
- // so nothing here invents a shape: beyond what was measured the reading is held
43
- // rather than extrapolated.
44
- const MEASURED_PENALTIES = penaltiesFrom(7.12, [{ others: 1, speed: 4.18 }]);
45
-
46
- // What a start and a stop cost, measured on the same host: a spawn with its
47
- // input open is 0.12 s there.
48
- const RUN_COSTS = { killCostSec: 0, firstByteWaitSec: 0.12 };
49
-
50
-
51
- const PICTURE = "torrent:abc:fmt=fmp4:grid=kf@0:video-only:v=0/copy";
52
- const SEGMENT_SECONDS = 4;
53
- const FILM_SECONDS = 4000;
54
- const SEGMENTS = FILM_SECONDS / SEGMENT_SECONDS;
55
-
56
- /** What every run of these tests reads as now; a measurement advances it. */
57
- const clock = { at: 1000 };
58
-
59
- class FakeProcess extends EventEmitter {
60
- constructor() {
61
- super();
62
- this.pid = 1;
63
- // The progress channel and the channel closed pieces are named on.
64
- this.stdout = new EventEmitter();
65
- this.stdio = [null, this.stdout, null, new EventEmitter()];
66
- }
67
-
68
- kill(signal) {
69
- this.emit("close", null, signal);
70
- }
71
- }
72
-
73
- /** @param {{ maxRuns?: number }} [options] */
74
- function orchestrator({ maxRuns = 3 } = {}) {
75
- /** @type {EncodeOrchestrator} */
76
- let made;
77
- const build = ({ address, from, to }) => new EncodeRun({
78
- address,
79
- encoder: new SoftwareEncoder(),
80
- from,
81
- to,
82
- buildArgs: () => ["-i", "in", "out"],
83
- spawn: () => new FakeProcess(),
84
- logger: { info() {}, warn() {} },
85
- now: () => clock.at,
86
- onEnded: (ended) => made.noteEnded(ended)
87
- });
88
- made = new EncodeOrchestrator({
89
- maxRunsFor: () => maxRuns,
90
- segmentSeconds: SEGMENT_SECONDS,
91
- ...RUN_COSTS,
92
- // A swarm with room for several encoders. The figure is the measured cost
93
- // of fetching a second of film again, in seconds of swarm time; at 0.25 one
94
- // encoder at 6x already takes one and a half swarms, so the budget would be
95
- // one process and the placement would have nothing to place — which is a
96
- // real limit, checked in its own test below, and not the subject of these.
97
- refetchSecPerFilmSecond: () => 0.02,
98
- // What the startup benchmark says this host encodes at. It exists before
99
- // any viewer, so the plan is never without a speed.
100
- startingSpeedFor: () => 2,
101
- // What a second encoder costs the first, measured on the addon host. The
102
- // orchestrator reads the table with the same pure function production uses.
103
- // Without it an extra process is free and the score always wants more of
104
- // them — and fewer encoders can genuinely finish sooner.
105
- contentionPenalties: MEASURED_PENALTIES,
106
- now: () => 1000,
107
- logger: { info() {}, warn() {} },
108
- makeRun: build
109
- });
110
- made.setSegmentCount(PICTURE, SEGMENTS);
111
- /** Who is watching, by name. The map is rebuilt from all of them on change. */
112
- const watching = new Map();
113
- const watches = (who, { atSeconds, playing = true }) => {
114
- watching.set(who, { atSeconds, playing });
115
- stateMap(made, watching);
116
- };
117
- const leaves = (who) => {
118
- watching.delete(who);
119
- stateMap(made, watching);
120
- };
121
- return { made, build, watches, leaves };
122
- }
123
-
124
- /**
125
- * Everybody watching, as ONE map, the way the product builds it.
126
- *
127
- * A viewer's own map is in seconds of film; the maps of all of them are merged
128
- * into one, and only then converted into an output's own numbering. Doing it
129
- * here rather than stating windows by hand is the point: the pause and the seek
130
- * are the MAP's behaviour, and a test that stated windows directly would be
131
- * checking its own arithmetic.
132
- *
133
- * @param {EncodeOrchestrator} made
134
- * @param {Map<string, { atSeconds: number, playing: boolean }>} watching
135
- */
136
- function stateMap(made, watching) {
137
- const map = mergeMaps(
138
- [...watching.values()].map((viewer) => mapForViewer({
139
- atSeconds: viewer.atSeconds,
140
- durationSeconds: FILM_SECONDS,
141
- allowanceSeconds: 8,
142
- playing: viewer.playing
143
- }))
144
- );
145
- // Seconds of film into this output's own piece numbers. The product does it
146
- // with the cut table, which knows where the pieces fall; here the pieces are a
147
- // uniform grid, so it is a division — and doing it at all is the point, since
148
- // the map is per FILM and every consumer numbers differently.
149
- made.notePriorityMap(PICTURE, runsOf(map).map((zone) => ({
150
- from: Math.floor(zone.from / SEGMENT_SECONDS),
151
- to: Math.max(
152
- Math.floor(zone.from / SEGMENT_SECONDS),
153
- Math.ceil(zone.to / SEGMENT_SECONDS) - 1
154
- ),
155
- priority: zone.priority,
156
- withinSeconds: zone.withinSeconds,
157
- behind: zone.behind
158
- })));
159
- }
160
-
161
- /**
162
- * How many encoders are working the stretch around one number.
163
- *
164
- * The count that matters for a viewer is not how many exist — spare capacity is
165
- * meant to be spent finishing the film, so a machine that holds three should be
166
- * holding three — but how many are crowded onto the place they are standing.
167
- *
168
- * @param {EncodeOrchestrator} made
169
- * @param {number} segment
170
- */
171
- function onTheStretchOf(made, segment) {
172
- return made.runsOn(PICTURE).filter((run) => {
173
- const to = run.to < run.from ? Number.POSITIVE_INFINITY : run.to;
174
- return run.from <= segment && segment <= to;
175
- }).length;
176
- }
177
-
178
- /** @param {EncodeOrchestrator} made */
179
- function placements(made) {
180
- return made.runsOn(PICTURE).map((run) => run.from).sort((left, right) => left - right);
181
- }
182
-
183
- /**
184
- * Every live encoder's stretch, so that "they never share a number" can be
185
- * asserted as the consequence it is rather than trusted.
186
- *
187
- * @param {EncodeOrchestrator} made
188
- */
189
- function assertNoOverlap(made) {
190
- const spans = made.runsOn(PICTURE)
191
- .map((run) => [run.from, run.to < run.from ? Number.POSITIVE_INFINITY : run.to])
192
- .sort((left, right) => left[0] - right[0]);
193
- for (let index = 0; index < spans.length - 1; index += 1) {
194
- assert.ok(
195
- spans[index][1] < spans[index + 1][0],
196
- `#${spans[index][0]}..#${spans[index][1]} overlaps #${spans[index + 1][0]}`
197
- );
198
- }
199
- }
200
-
201
- // ---------------------------------------------------------------- one viewer
202
-
203
- test("one viewer playing: one encoder, exactly where they are", () => {
204
- const { made, watches } = orchestrator();
205
- watches("one", { atSeconds: 400 });
206
- made.reconcile();
207
- assert.deepEqual(placements(made), [100], "at their own position, 400s / 4s");
208
- assertNoOverlap(made);
209
- });
210
-
211
- test("one viewer playing: the encoder keeping up buys no second one", () => {
212
- // The peak moves forward on its own as they watch, and an encoder that stays
213
- // in front of it is never late. This is the case that must NOT spend a
214
- // process, and the one the old head-as-a-barrier rule got wrong.
215
- const { made, watches } = orchestrator();
216
- watches("one", { atSeconds: 400 });
217
- made.reconcile();
218
- const [run] = made.runsOn(PICTURE);
219
- measureSpeed(run, 6, clock);
220
- for (let index = 100; index < 130; index += 1) {
221
- made.noteProduced(PICTURE, index);
222
- }
223
- watches("one", { atSeconds: 480 });
224
- made.reconcile();
225
- // Asked of the first number nobody has, not of the one they are standing on:
226
- // that one is already made, and nothing needs to be covering it.
227
- assert.equal(onTheStretchOf(made, 130), 1, "one encoder on the film in front of them");
228
- assertNoOverlap(made);
229
- });
230
-
231
- test("one viewer seeking far ahead: an encoder is placed there", () => {
232
- const { made, watches } = orchestrator();
233
- watches("one", { atSeconds: 400 });
234
- made.reconcile();
235
- measureSpeed(made.runsOn(PICTURE)[0], 6, clock);
236
- made.noteProduced(PICTURE, 100);
237
-
238
- // The peak JUMPS. Nothing about the old place is wanted now.
239
- watches("one", { atSeconds: 3000 });
240
- made.reconcile();
241
-
242
- assert.ok(placements(made).includes(750), "an encoder where they landed, 3000s / 4s");
243
- assertNoOverlap(made);
244
- });
245
-
246
- test("one viewer paused: nothing is late, so no encoder is added", () => {
247
- // A paused viewer states the whole film at one undifferentiated rank and no
248
- // time by which any of it must exist. The encoder already working goes on
249
- // encoding the track; nothing justifies a second process.
250
- const { made, watches } = orchestrator();
251
- watches("one", { atSeconds: 400 });
252
- made.reconcile();
253
- measureSpeed(made.runsOn(PICTURE)[0], 1, clock);
254
- made.noteProduced(PICTURE, 100);
255
-
256
- watches("one", { atSeconds: 404, playing: false });
257
- made.reconcile();
258
-
259
- assert.equal(onTheStretchOf(made, 101), 1, "one encoder where they are standing");
260
- assertNoOverlap(made);
261
- });
262
-
263
- test("one viewer paused states no deadline anywhere", () => {
264
- // The map's own answer, checked directly, because every placement decision
265
- // below rests on it.
266
- const map = mapForViewer({
267
- atSeconds: 400,
268
- durationSeconds: FILM_SECONDS,
269
- allowanceSeconds: 8,
270
- playing: false
271
- });
272
- assert.ok(map.durationSeconds > 0, "a paused viewer still wants the film");
273
- for (let second = 0; second < map.durationSeconds; second += 1) {
274
- assert.equal(map.secondsUntilPlayed[second], Number.POSITIVE_INFINITY,
275
- "but no second of it has a time by which it must exist");
276
- }
277
- // Their POSITION survives the pause, and with it the rule that what is in
278
- // front of them is made before what is behind.
279
- assert.equal(map.behind[399], 1, "what they have watched is behind them");
280
- assert.equal(map.behind[400], 0, "and what they have not is in front");
281
- assert.ok(map.priority[400] > map.priority[399], "which is what the numbers say");
282
- });
283
-
284
- // --------------------------------------------------------------- two viewers
285
-
286
- test("two viewers close together share one encoder", () => {
287
- // Film both of them want is made once. This is what merging the map is for,
288
- // and it must survive the deadline being carried alongside the rank.
289
- const { made, watches } = orchestrator();
290
- watches("one", { atSeconds: 400 });
291
- watches("two", { atSeconds: 408 });
292
- made.reconcile();
293
- // One encoder for the pair — asked of the film they are both about to watch.
294
- // The machine may well be running others further on: what it has spare goes to
295
- // finishing the file, and that is a different question from crowding.
296
- assert.equal(onTheStretchOf(made, 100), 1, "one encoder for the pair");
297
- assert.equal(onTheStretchOf(made, 102), 1, "and one for the film just in front of them");
298
- assertNoOverlap(made);
299
- });
300
-
301
- test("two viewers far apart get an encoder each", () => {
302
- const { made, watches } = orchestrator();
303
- watches("one", { atSeconds: 400 });
304
- made.reconcile();
305
- measureSpeed(made.runsOn(PICTURE)[0], 6, clock);
306
- made.noteProduced(PICTURE, 100);
307
-
308
- watches("two", { atSeconds: 3000 });
309
- made.reconcile();
310
-
311
- assert.ok(placements(made).includes(750), "the far one is served where they stand");
312
- assert.equal(onTheStretchOf(made, 750), 1, "and by one encoder, not a crowd");
313
- assertNoOverlap(made);
314
- });
315
-
316
- test("two viewers: one seeking does not take the other's encoder", () => {
317
- const { made, watches } = orchestrator();
318
- watches("one", { atSeconds: 400 });
319
- made.reconcile();
320
- // A speed has to be measured before a second encoder can be justified: how
321
- // long one takes to reach a number is the whole comparison. Real life
322
- // measures it from the first run; a test that stated both viewers before any
323
- // run existed would be asking the plan to buy a process on no evidence.
324
- measureSpeed(made.runsOn(PICTURE)[0], 6, clock);
325
- made.noteProduced(PICTURE, 100);
326
- watches("two", { atSeconds: 3000 });
327
- made.reconcile();
328
- for (const run of made.runsOn(PICTURE)) {
329
- measureSpeed(run, 6, clock);
330
- }
331
- assert.ok(made.runsOn(PICTURE).length >= 2, "one each to begin with");
332
-
333
- // The far one seeks somewhere else entirely.
334
- watches("two", { atSeconds: 2000 });
335
- made.reconcile();
336
-
337
- // Asked of the coverage, not of a run's first number: a run whose road was
338
- // shortened begins again at its own head, so "is this viewer served" is a
339
- // question about the map and never about where a process happens to start.
340
- const coverage = made.coverageOf(PICTURE);
341
- assert.equal(coverage.stateOf(105), "making", "the one who did not move is still served");
342
- assert.ok(placements(made).includes(500), "and the one who moved is served where they landed");
343
- assertNoOverlap(made);
344
- });
345
-
346
- test("two viewers: one pausing leaves the other served", () => {
347
- const { made, watches } = orchestrator();
348
- watches("one", { atSeconds: 400 });
349
- watches("two", { atSeconds: 3000 });
350
- made.reconcile();
351
- for (const run of made.runsOn(PICTURE)) {
352
- measureSpeed(run, 1, clock);
353
- }
354
-
355
- watches("two", { atSeconds: 3000, playing: false });
356
- made.reconcile();
357
-
358
- assert.equal(made.coverageOf(PICTURE).stateOf(105), "making",
359
- "the one still watching keeps their encoder");
360
- assertNoOverlap(made);
361
- });
362
-
363
- test("two viewers: the one who leaves takes nothing from the one who stays", () => {
364
- const { made, watches, leaves } = orchestrator();
365
- watches("one", { atSeconds: 400 });
366
- watches("two", { atSeconds: 3000 });
367
- made.reconcile();
368
- for (const run of made.runsOn(PICTURE)) {
369
- measureSpeed(run, 1, clock);
370
- }
371
-
372
- leaves("two");
373
- made.reconcile();
374
-
375
- assert.equal(made.coverageOf(PICTURE).stateOf(105), "making",
376
- "the one who stayed is still served");
377
- assertNoOverlap(made);
378
- });
379
-
380
- // ------------------------------------------------------------- three viewers
381
-
382
- test("three viewers far apart get an encoder each when the machine affords it", () => {
383
- const { made, watches } = orchestrator({ maxRuns: 3 });
384
- watches("one", { atSeconds: 400 });
385
- watches("two", { atSeconds: 2000 });
386
- watches("three", { atSeconds: 3600 });
387
- made.reconcile();
388
- for (const run of made.runsOn(PICTURE)) {
389
- measureSpeed(run, 6, clock);
390
- }
391
- made.reconcile();
392
-
393
- const where = placements(made);
394
- for (const at of [100, 500, 900]) {
395
- assert.ok(where.includes(at), `somebody is making what the viewer at #${at} needs`);
396
- assert.equal(onTheStretchOf(made, at), 1, `and one encoder there, not a crowd`);
397
- }
398
- assertNoOverlap(made);
399
- });
400
-
401
- test("three viewers, a machine that affords two: the budget binds, not the map", () => {
402
- // The number of encoders is the smaller of what the map asks and what the
403
- // machine can hold. Which two are served follows from the order the work is
404
- // taken in; what must not happen is a third process on a host that cannot
405
- // hold it, or two processes writing one number.
406
- const { made, watches } = orchestrator({ maxRuns: 2 });
407
- watches("one", { atSeconds: 400 });
408
- watches("two", { atSeconds: 2000 });
409
- watches("three", { atSeconds: 3600 });
410
- made.reconcile();
411
- for (const run of made.runsOn(PICTURE)) {
412
- measureSpeed(run, 1, clock);
413
- }
414
- made.reconcile();
415
-
416
- assert.ok(made.runsOn(PICTURE).length <= 2, "never more than the machine affords");
417
- assert.ok(made.runsOn(PICTURE).length >= 1, "and never nothing while people wait");
418
- assertNoOverlap(made);
419
- });
420
-
421
- test("three viewers: one seeks onto another, and the two of them share", () => {
422
- const { made, watches } = orchestrator({ maxRuns: 3 });
423
- watches("one", { atSeconds: 400 });
424
- made.reconcile();
425
- measureSpeed(made.runsOn(PICTURE)[0], 6, clock);
426
- made.noteProduced(PICTURE, 100);
427
- watches("two", { atSeconds: 2000 });
428
- watches("three", { atSeconds: 3600 });
429
- made.reconcile();
430
- for (const run of made.runsOn(PICTURE)) {
431
- measureSpeed(run, 6, clock);
432
- }
433
- made.reconcile();
434
- assert.equal(made.runsOn(PICTURE).length, 3);
435
-
436
- // The third joins the second. Two peaks where there were three.
437
- watches("three", { atSeconds: 2008 });
438
- made.reconcile();
439
-
440
- assert.ok(made.runsOn(PICTURE).length <= 3, "no process is added by people converging");
441
- assertNoOverlap(made);
442
- });
443
-
444
- test("three viewers: all paused, and no encoder is added for any of them", () => {
445
- const { made, watches } = orchestrator({ maxRuns: 3 });
446
- watches("one", { atSeconds: 400 });
447
- made.reconcile();
448
- measureSpeed(made.runsOn(PICTURE)[0], 1, clock);
449
- const before = made.runsOn(PICTURE).length;
450
-
451
- watches("one", { atSeconds: 400, playing: false });
452
- watches("two", { atSeconds: 2000, playing: false });
453
- watches("three", { atSeconds: 3600, playing: false });
454
- made.reconcile();
455
-
456
- // Nobody is coming anywhere, so nothing can be late — and the machine does not
457
- // therefore fall idle: what it has spare goes on finishing the file, which is
458
- // what makes a seek back into a made part start playing at once.
459
- assert.ok(made.runsOn(PICTURE).length >= before, "the file goes on being finished");
460
- assertNoOverlap(made);
461
- });
462
-
463
- test("the plan is a function of the state: the same state twice gives the same answer", () => {
464
- // What stops a moving map from becoming a thrash: the decision is arithmetic
465
- // over the state, so a pass that finds nothing changed changes nothing.
466
- const { made, watches } = orchestrator({ maxRuns: 3 });
467
- watches("one", { atSeconds: 400 });
468
- watches("two", { atSeconds: 2000 });
469
- made.reconcile();
470
- for (const run of made.runsOn(PICTURE)) {
471
- measureSpeed(run, 1, clock);
472
- }
473
- made.reconcile();
474
- const first = placements(made);
475
- made.reconcile();
476
- made.reconcile();
477
- assert.deepEqual(placements(made), first, "three more passes moved nothing");
478
- });
479
-
480
- // ---------------------------------------------------------------- the edge
481
-
482
- test("exactly realtime arrives exactly on time, and no second encoder is bought", () => {
483
- // The knife edge, and it lands on "in time" by construction: the deadline of
484
- // a number is the distance to it, and an encoder at 1.0x covers that distance
485
- // in exactly that time. So the arithmetic says nothing is late and nothing is
486
- // bought. Worth pinning, because the formula this model replaced said
487
- // something quite different — `speed / (1 - speed)` divides by zero here and
488
- // claims the encoder stays ahead FOR EVER, which is the same answer arrived
489
- // at by nonsense.
490
- const { made, watches } = orchestrator({ maxRuns: 3 });
491
- watches("one", { atSeconds: 400 });
492
- made.reconcile();
493
- measureSpeed(made.runsOn(PICTURE)[0], 1, clock);
494
- made.noteProduced(PICTURE, 100);
495
- made.reconcile();
496
-
497
- assert.equal(onTheStretchOf(made, 150), 1,
498
- "just in time everywhere, so one encoder on the viewer's stretch is enough");
499
- assertNoOverlap(made);
500
- });
501
-
502
- test("below realtime, whether a second encoder helps is arithmetic, and here it does not", () => {
503
- // The side of the edge that matters, and the answer is not the one this check
504
- // asserted when it was written. An encoder at half realtime loses ground on
505
- // the viewer every second, so everything ahead of it is late — and a second
506
- // process does not fix that, because on this host each of two runs at 1.70x
507
- // slower than one. The near film gets worse by more than the far film gets
508
- // better, so the total seconds of waiting go UP, and the score refuses.
509
- //
510
- // What this case actually needs is the answer the user gave for it: a machine
511
- // that cannot serve its viewers without stopping the picture says so, and the
512
- // viewer is moved to another proxy. That is not built. Adding processes is not
513
- // a substitute for it and the model correctly declines to pretend otherwise.
514
- const { made, watches, leaves } = orchestrator({ maxRuns: 3 });
515
- watches("one", { atSeconds: 400 });
516
- made.reconcile();
517
- measureSpeed(made.runsOn(PICTURE)[0], 0.5, clock);
518
- made.noteProduced(PICTURE, 100);
519
- made.reconcile();
520
-
521
- assert.equal(made.runsOn(PICTURE).length, 1,
522
- "one encoder, because two of them would leave the viewer waiting longer");
523
- assert.equal(made.coverageOf(PICTURE).stateOf(101), "making",
524
- "and it is on the film in front of them");
525
- assertNoOverlap(made);
526
- void leaves;
527
- });
528
-
529
- test("two viewers arriving together on a cold output get one encoder, then are measured", () => {
530
- // Nothing has measured how fast this machine encodes, so how long one encoder
531
- // would take to reach the second viewer is not a known quantity. Buying a
532
- // process on that is buying it on no evidence, which is refused; one is placed
533
- // and the speed it reports is what justifies the next.
534
- const { made, watches } = orchestrator({ maxRuns: 3 });
535
- watches("one", { atSeconds: 400 });
536
- watches("two", { atSeconds: 3000 });
537
- made.reconcile();
538
- assert.ok(made.runsOn(PICTURE).length >= 1, "at least one, until something is measured");
539
- assert.equal(onTheStretchOf(made, 100), 1, "and not two on the same place");
540
-
541
- measureSpeed(made.runsOn(PICTURE)[0], 6, clock);
542
- made.noteProduced(PICTURE, 100);
543
- made.reconcile();
544
- assert.ok(placements(made).includes(750), "and now the far one is served too");
545
- assertNoOverlap(made);
546
- });
547
-
548
- test("an encoder comfortably faster than realtime is left to do the whole stretch", () => {
549
- const { made, watches } = orchestrator({ maxRuns: 3 });
550
- watches("one", { atSeconds: 400 });
551
- made.reconcile();
552
- measureSpeed(made.runsOn(PICTURE)[0], 6, clock);
553
- made.noteProduced(PICTURE, 100);
554
- made.reconcile();
555
-
556
- assert.equal(onTheStretchOf(made, 150), 1, "one encoder on the stretch they are watching");
557
- assertNoOverlap(made);
558
- });
559
-
560
- test("a swarm that feeds one encoder moves it to whoever is late, rather than serving nobody", () => {
561
- // The budget binds. The one encoder there is stands where nothing is due —
562
- // the viewer left — so it is moved to the soonest number that IS due. Without
563
- // this the viewer who seeked was served by nobody at all: the run kept its
564
- // road because nothing covered what lay in front of IT.
565
- const { made, watches } = orchestrator({ maxRuns: 3 });
566
- watches("one", { atSeconds: 400 });
567
- made.reconcile();
568
- const [run] = made.runsOn(PICTURE);
569
- // 6x against a swarm charging 0.25 s per second of film: one encoder already
570
- // takes one and a half of what is delivered, so the budget is one.
571
- measureSpeed(run, 6, clock);
572
- made.refetchSecPerFilmSecond = () => 0.25;
573
- made.noteProduced(PICTURE, 100);
574
-
575
- watches("one", { atSeconds: 3000 });
576
- made.reconcile();
577
-
578
- assert.equal(made.runsOn(PICTURE).length, 1, "still one, because that is what the swarm feeds");
579
- assert.deepEqual(placements(made), [750], "and it is where the viewer now stands");
580
- assertNoOverlap(made);
581
- });
582
-
583
- // ------------------------------------------------------- seeking BACKWARD
584
-
585
- test("one viewer seeking back into film that exists is served from it, with no encoder", () => {
586
- // The case the whole layer was built for. Everything behind them has been
587
- // made, so nothing is late anywhere they are going, and the score says the
588
- // cheapest arrangement is the one that changes nothing. A restart here is the
589
- // 647-second stall of 2026-09-06 in miniature.
590
- const { made, watches } = orchestrator();
591
- watches("one", { atSeconds: 400 });
592
- made.reconcile();
593
- measureSpeed(made.runsOn(PICTURE)[0], 6, clock);
594
- for (let index = 100; index <= 160; index += 1) {
595
- made.noteProduced(PICTURE, index);
596
- }
597
- const before = made.runsOn(PICTURE);
598
-
599
- watches("one", { atSeconds: 440 });
600
- made.reconcile();
601
-
602
- // Not "the count is unchanged": with nothing late anywhere, the machine is
603
- // free to spend what it has spare on finishing the file, and that is what the
604
- // user asked for. What must not happen is a process put on film that already
605
- // exists, or the run they were behind restarted — which is the 647-second
606
- // stall of 2026-09-06 in miniature.
607
- for (const was of before) {
608
- assert.ok(made.runsOn(PICTURE).includes(was),
609
- "the encoder they were behind is not restarted");
610
- }
611
- for (const run_ of made.runsOn(PICTURE)) {
612
- // Where it is WORKING, not where it was created: the surviving run was
613
- // started at #100 and has since produced up to #161.
614
- assert.equal(made.coverageOf(PICTURE).isReady(run_.head), false,
615
- `no process is bought for film that exists (#${run_.head})`);
616
- }
617
- assertNoOverlap(made);
618
- });
619
-
620
- test("one viewer seeking back into film nobody has gets an encoder there", () => {
621
- // Behind them is not the same as made. Where the film was never encoded, going
622
- // back is exactly as bare as going forward, and the arithmetic is the same one.
623
- const { made, watches } = orchestrator();
624
- watches("one", { atSeconds: 3000 });
625
- made.reconcile();
626
- measureSpeed(made.runsOn(PICTURE)[0], 6, clock);
627
- made.noteProduced(PICTURE, 750);
628
-
629
- watches("one", { atSeconds: 400 });
630
- made.reconcile();
631
-
632
- const coverage = made.coverageOf(PICTURE);
633
- assert.equal(coverage.stateOf(100), "making", "somebody is making where they landed");
634
- assertNoOverlap(made);
635
- });
636
-
637
- test("two viewers: one seeks back onto film the other already had made", () => {
638
- const { made, watches } = orchestrator({ maxRuns: 3 });
639
- watches("one", { atSeconds: 400 });
640
- made.reconcile();
641
- measureSpeed(made.runsOn(PICTURE)[0], 6, clock);
642
- for (let index = 100; index <= 200; index += 1) {
643
- made.noteProduced(PICTURE, index);
644
- }
645
- watches("two", { atSeconds: 3000 });
646
- made.reconcile();
647
- for (const run of made.runsOn(PICTURE)) {
648
- measureSpeed(run, 6, clock);
649
- }
650
- const before = made.runsOn(PICTURE).length;
651
-
652
- // The far one comes back to where the first one has already been.
653
- watches("two", { atSeconds: 500 });
654
- made.reconcile();
655
-
656
- assert.ok(made.runsOn(PICTURE).length <= before,
657
- "coming back onto made film buys nobody an encoder");
658
- assertNoOverlap(made);
659
- });
660
-
661
- test("three viewers: one forward, one back, one paused", () => {
662
- // All three motions at once, which is the state a real proxy is in most of the
663
- // time. What must hold is what always must: never two encoders on one number,
664
- // never more than the machine affords, and somebody making what the moving
665
- // viewers are about to need.
666
- const { made, watches } = orchestrator({ maxRuns: 3 });
667
- watches("one", { atSeconds: 400 });
668
- watches("two", { atSeconds: 2000 });
669
- watches("three", { atSeconds: 3600 });
670
- made.reconcile();
671
- for (const run of made.runsOn(PICTURE)) {
672
- measureSpeed(run, 6, clock);
673
- }
674
- made.reconcile();
675
-
676
- watches("one", { atSeconds: 800 });
677
- watches("two", { atSeconds: 1200 });
678
- watches("three", { atSeconds: 3600, playing: false });
679
- made.reconcile();
680
-
681
- assert.ok(made.runsOn(PICTURE).length <= 3, "never more than the machine holds");
682
- assertNoOverlap(made);
683
- const coverage = made.coverageOf(PICTURE);
684
- assert.equal(coverage.stateOf(200), "making", "the one who went forward is served");
685
- assert.equal(coverage.stateOf(300), "making", "and so is the one who came back");
686
- });
687
-
688
- test("a viewer scrubbing back and forth does not accumulate encoders", () => {
689
- // A person dragging the time bar states a new position every few hundred
690
- // milliseconds, and each of those is a state the plan answers. What must hold
691
- // is that the answers do not pile up: an encoder bought for a place the viewer
692
- // passed through is not still running when they have gone back.
693
- //
694
- // Not that the arrangement is identical to the one before the scrub — the
695
- // model does not promise that and nothing here should claim it. What it
696
- // promises is that no arrangement costs more than it is worth.
697
- const { made, watches } = orchestrator({ maxRuns: 3 });
698
- watches("one", { atSeconds: 400 });
699
- made.reconcile();
700
- measureSpeed(made.runsOn(PICTURE)[0], 6, clock);
701
- made.noteProduced(PICTURE, 100);
702
- made.reconcile();
703
- const settled = placements(made);
704
-
705
- watches("one", { atSeconds: 2000 });
706
- made.reconcile();
707
- for (const run of made.runsOn(PICTURE)) {
708
- measureSpeed(run, 6, clock);
709
- }
710
- watches("one", { atSeconds: 404 });
711
- made.reconcile();
712
- watches("one", { atSeconds: 400 });
713
- made.reconcile();
714
- made.reconcile();
715
-
716
- // Not "no more encoders" — the machine is allowed to spend what it has spare on
717
- // finishing the file, and after a scrub there is more of the file it has seen.
718
- // What must not happen is two of them on one stretch, or more than the machine
719
- // holds, or the viewer left unserved.
720
- assert.ok(made.runsOn(PICTURE).length <= 3, "never more than the machine holds");
721
- assert.equal(onTheStretchOf(made, 105), 1, "one encoder on the film in front of them");
722
- assert.equal(made.coverageOf(PICTURE).stateOf(105), "making", "and the viewer is served");
723
- assertNoOverlap(made);
724
- void settled;
725
- });
1
+ /**
2
+ * @file The placement model under one, two and three viewers, each of them
3
+ * playing, seeking and paused.
4
+ *
5
+ * THE MODEL, restated so a failure here can be read against it:
6
+ *
7
+ * - each segment number carries a deadline `D(x)`, the seconds until somebody
8
+ * needs it. A viewer moving forward covers a second of film in a second, so
9
+ * `D(x)` is the distance to them. `Infinity` where nobody is coming;
10
+ * - an encoder placed at `a` delivers `a + j` at `(j + 1) / r`, `r` segments per
11
+ * second. The same expression answers "when would the one already placed get
12
+ * here";
13
+ * - a number is late when it arrives after its deadline. Placement is first-fit
14
+ * left to right: give it to an encoder that arrives in time, else open one
15
+ * exactly there.
16
+ *
17
+ * Three behaviours of the map follow from that and are checked here rather than
18
+ * assumed. A peak MOVES FORWARD on its own, because the deadline is a distance
19
+ * and the distance shrinks as the viewer watches. It JUMPS on a seek. It
20
+ * FLATTENS on a pause, because a viewer who is not moving has no time by which
21
+ * anything must exist.
22
+ *
23
+ * Nothing here spawns ffmpeg, touches a disk or reads a clock. That is the layer
24
+ * check, made executable: this layer is exercised with plain values alone.
25
+ */
26
+
27
+ import test from "node:test";
28
+ import assert from "node:assert/strict";
29
+ import { EventEmitter } from "node:events";
30
+ import { EncodeRun } from "../services/encode/EncodeRun.js";
31
+ import { measureSpeed } from "./helpers/encode-run.js";
32
+ import { SoftwareEncoder } from "../services/encode/SoftwareEncoder.js";
33
+ import { EncodeOrchestrator } from "../services/encode/EncodeOrchestrator.js";
34
+ import { mapForViewer, mergeMaps, runsOf } from "../services/viewer/PriorityMap.js";
35
+ import { penaltiesFrom } from "../services/encode/contention.js";
36
+
37
+ // WHAT A SECOND ENCODER COSTS THE FIRST — measured, never a formula.
38
+ //
39
+ // Addon host, 2026-09-03: 854x480 through libx264 `ultrafast` ran at 7.12x with
40
+ // the machine to itself, and at 4.20x and 4.16x when two ran at once. The
41
+ // penalty is read off that reading by the SAME two functions production uses,
42
+ // so nothing here invents a shape: beyond what was measured the reading is held
43
+ // rather than extrapolated.
44
+ const MEASURED_PENALTIES = penaltiesFrom(7.12, [{ others: 1, speed: 4.18 }]);
45
+
46
+ // What a start and a stop cost, measured on the same host: a spawn with its
47
+ // input open is 0.12 s there.
48
+ const RUN_COSTS = { killCostSec: 0, firstByteWaitSec: 0.12 };
49
+
50
+
51
+ const PICTURE = "torrent:abc:fmt=fmp4:grid=kf@0:video-only:v=0/copy";
52
+ const SEGMENT_SECONDS = 4;
53
+ const FILM_SECONDS = 4000;
54
+ const SEGMENTS = FILM_SECONDS / SEGMENT_SECONDS;
55
+
56
+ /** What every run of these tests reads as now; a measurement advances it. */
57
+ const clock = { at: 1000 };
58
+
59
+ class FakeProcess extends EventEmitter {
60
+ constructor() {
61
+ super();
62
+ this.pid = 1;
63
+ // The progress channel and the channel closed pieces are named on.
64
+ this.stdout = new EventEmitter();
65
+ this.stdio = [null, this.stdout, null, new EventEmitter()];
66
+ }
67
+
68
+ kill(signal) {
69
+ this.emit("close", null, signal);
70
+ }
71
+ }
72
+
73
+ /** @param {{ maxRuns?: number }} [options] */
74
+ function orchestrator({ maxRuns = 3 } = {}) {
75
+ /** @type {EncodeOrchestrator} */
76
+ let made;
77
+ const build = ({ address, from, to }) => new EncodeRun({
78
+ address,
79
+ encoder: new SoftwareEncoder(),
80
+ from,
81
+ to,
82
+ buildArgs: () => ["-i", "in", "out"],
83
+ spawn: () => new FakeProcess(),
84
+ logger: { info() {}, warn() {} },
85
+ now: () => clock.at,
86
+ onEnded: (ended) => made.noteEnded(ended)
87
+ });
88
+ made = new EncodeOrchestrator({
89
+ maxRunsFor: () => maxRuns,
90
+ segmentSeconds: SEGMENT_SECONDS,
91
+ ...RUN_COSTS,
92
+ // A swarm with room for several encoders. The figure is the measured cost
93
+ // of fetching a second of film again, in seconds of swarm time; at 0.25 one
94
+ // encoder at 6x already takes one and a half swarms, so the budget would be
95
+ // one process and the placement would have nothing to place — which is a
96
+ // real limit, checked in its own test below, and not the subject of these.
97
+ refetchSecPerFilmSecond: () => 0.02,
98
+ // What the startup benchmark says this host encodes at. It exists before
99
+ // any viewer, so the plan is never without a speed.
100
+ startingSpeedFor: () => 2,
101
+ // What a second encoder costs the first, measured on the addon host. The
102
+ // orchestrator reads the table with the same pure function production uses.
103
+ // Without it an extra process is free and the score always wants more of
104
+ // them — and fewer encoders can genuinely finish sooner.
105
+ contentionPenalties: MEASURED_PENALTIES,
106
+ now: () => 1000,
107
+ logger: { info() {}, warn() {} },
108
+ makeRun: build
109
+ });
110
+ made.setSegmentCount(PICTURE, SEGMENTS);
111
+ /** Who is watching, by name. The map is rebuilt from all of them on change. */
112
+ const watching = new Map();
113
+ const watches = (who, { atSeconds, playing = true }) => {
114
+ watching.set(who, { atSeconds, playing });
115
+ stateMap(made, watching);
116
+ };
117
+ const leaves = (who) => {
118
+ watching.delete(who);
119
+ stateMap(made, watching);
120
+ };
121
+ return { made, build, watches, leaves };
122
+ }
123
+
124
+ /**
125
+ * Everybody watching, as ONE map, the way the product builds it.
126
+ *
127
+ * A viewer's own map is in seconds of film; the maps of all of them are merged
128
+ * into one, and only then converted into an output's own numbering. Doing it
129
+ * here rather than stating windows by hand is the point: the pause and the seek
130
+ * are the MAP's behaviour, and a test that stated windows directly would be
131
+ * checking its own arithmetic.
132
+ *
133
+ * @param {EncodeOrchestrator} made
134
+ * @param {Map<string, { atSeconds: number, playing: boolean }>} watching
135
+ */
136
+ function stateMap(made, watching) {
137
+ const map = mergeMaps(
138
+ [...watching.values()].map((viewer) => mapForViewer({
139
+ atSeconds: viewer.atSeconds,
140
+ durationSeconds: FILM_SECONDS,
141
+ allowanceSeconds: 8,
142
+ playing: viewer.playing
143
+ }))
144
+ );
145
+ // Seconds of film into this output's own piece numbers. The product does it
146
+ // with the cut table, which knows where the pieces fall; here the pieces are a
147
+ // uniform grid, so it is a division — and doing it at all is the point, since
148
+ // the map is per FILM and every consumer numbers differently.
149
+ made.notePriorityMap(PICTURE, runsOf(map).map((zone) => ({
150
+ from: Math.floor(zone.from / SEGMENT_SECONDS),
151
+ to: Math.max(
152
+ Math.floor(zone.from / SEGMENT_SECONDS),
153
+ Math.ceil(zone.to / SEGMENT_SECONDS) - 1
154
+ ),
155
+ priority: zone.priority,
156
+ withinSeconds: zone.withinSeconds,
157
+ behind: zone.behind
158
+ })));
159
+ }
160
+
161
+ /**
162
+ * How many encoders are working the stretch around one number.
163
+ *
164
+ * The count that matters for a viewer is not how many exist — spare capacity is
165
+ * meant to be spent finishing the film, so a machine that holds three should be
166
+ * holding three — but how many are crowded onto the place they are standing.
167
+ *
168
+ * @param {EncodeOrchestrator} made
169
+ * @param {number} segment
170
+ */
171
+ function onTheStretchOf(made, segment) {
172
+ return made.runsOn(PICTURE).filter((run) => {
173
+ const to = run.to < run.from ? Number.POSITIVE_INFINITY : run.to;
174
+ return run.from <= segment && segment <= to;
175
+ }).length;
176
+ }
177
+
178
+ /** @param {EncodeOrchestrator} made */
179
+ function placements(made) {
180
+ return made.runsOn(PICTURE).map((run) => run.from).sort((left, right) => left - right);
181
+ }
182
+
183
+ /**
184
+ * Every live encoder's stretch, so that "they never share a number" can be
185
+ * asserted as the consequence it is rather than trusted.
186
+ *
187
+ * @param {EncodeOrchestrator} made
188
+ */
189
+ function assertNoOverlap(made) {
190
+ const spans = made.runsOn(PICTURE)
191
+ .map((run) => [run.from, run.to < run.from ? Number.POSITIVE_INFINITY : run.to])
192
+ .sort((left, right) => left[0] - right[0]);
193
+ for (let index = 0; index < spans.length - 1; index += 1) {
194
+ assert.ok(
195
+ spans[index][1] < spans[index + 1][0],
196
+ `#${spans[index][0]}..#${spans[index][1]} overlaps #${spans[index + 1][0]}`
197
+ );
198
+ }
199
+ }
200
+
201
+ // ---------------------------------------------------------------- one viewer
202
+
203
+ test("one viewer playing: one encoder, exactly where they are", () => {
204
+ const { made, watches } = orchestrator();
205
+ watches("one", { atSeconds: 400 });
206
+ made.reconcile();
207
+ assert.deepEqual(placements(made), [100], "at their own position, 400s / 4s");
208
+ assertNoOverlap(made);
209
+ });
210
+
211
+ test("one viewer playing: the encoder keeping up buys no second one", () => {
212
+ // The peak moves forward on its own as they watch, and an encoder that stays
213
+ // in front of it is never late. This is the case that must NOT spend a
214
+ // process, and the one the old head-as-a-barrier rule got wrong.
215
+ const { made, watches } = orchestrator();
216
+ watches("one", { atSeconds: 400 });
217
+ made.reconcile();
218
+ const [run] = made.runsOn(PICTURE);
219
+ measureSpeed(run, 6, clock);
220
+ for (let index = 100; index < 130; index += 1) {
221
+ made.noteProduced(PICTURE, index);
222
+ }
223
+ watches("one", { atSeconds: 480 });
224
+ made.reconcile();
225
+ // Asked of the first number nobody has, not of the one they are standing on:
226
+ // that one is already made, and nothing needs to be covering it.
227
+ assert.equal(onTheStretchOf(made, 130), 1, "one encoder on the film in front of them");
228
+ assertNoOverlap(made);
229
+ });
230
+
231
+ test("one viewer seeking far ahead: an encoder is placed there", () => {
232
+ const { made, watches } = orchestrator();
233
+ watches("one", { atSeconds: 400 });
234
+ made.reconcile();
235
+ measureSpeed(made.runsOn(PICTURE)[0], 6, clock);
236
+ made.noteProduced(PICTURE, 100);
237
+
238
+ // The peak JUMPS. Nothing about the old place is wanted now.
239
+ watches("one", { atSeconds: 3000 });
240
+ made.reconcile();
241
+
242
+ assert.ok(placements(made).includes(750), "an encoder where they landed, 3000s / 4s");
243
+ assertNoOverlap(made);
244
+ });
245
+
246
+ test("one viewer paused: nothing is late, so no encoder is added", () => {
247
+ // A paused viewer states the whole film at one undifferentiated rank and no
248
+ // time by which any of it must exist. The encoder already working goes on
249
+ // encoding the track; nothing justifies a second process.
250
+ const { made, watches } = orchestrator();
251
+ watches("one", { atSeconds: 400 });
252
+ made.reconcile();
253
+ measureSpeed(made.runsOn(PICTURE)[0], 1, clock);
254
+ made.noteProduced(PICTURE, 100);
255
+
256
+ watches("one", { atSeconds: 404, playing: false });
257
+ made.reconcile();
258
+
259
+ assert.equal(onTheStretchOf(made, 101), 1, "one encoder where they are standing");
260
+ assertNoOverlap(made);
261
+ });
262
+
263
+ test("one viewer paused states no deadline anywhere", () => {
264
+ // The map's own answer, checked directly, because every placement decision
265
+ // below rests on it.
266
+ const map = mapForViewer({
267
+ atSeconds: 400,
268
+ durationSeconds: FILM_SECONDS,
269
+ allowanceSeconds: 8,
270
+ playing: false
271
+ });
272
+ assert.ok(map.durationSeconds > 0, "a paused viewer still wants the film");
273
+ for (let second = 0; second < map.durationSeconds; second += 1) {
274
+ assert.equal(map.secondsUntilPlayed[second], Number.POSITIVE_INFINITY,
275
+ "but no second of it has a time by which it must exist");
276
+ }
277
+ // Their POSITION survives the pause, and with it the rule that what is in
278
+ // front of them is made before what is behind.
279
+ assert.equal(map.behind[399], 1, "what they have watched is behind them");
280
+ assert.equal(map.behind[400], 0, "and what they have not is in front");
281
+ assert.ok(map.priority[400] > map.priority[399], "which is what the numbers say");
282
+ });
283
+
284
+ // --------------------------------------------------------------- two viewers
285
+
286
+ test("two viewers close together share one encoder", () => {
287
+ // Film both of them want is made once. This is what merging the map is for,
288
+ // and it must survive the deadline being carried alongside the rank.
289
+ const { made, watches } = orchestrator();
290
+ watches("one", { atSeconds: 400 });
291
+ watches("two", { atSeconds: 408 });
292
+ made.reconcile();
293
+ // One encoder for the pair — asked of the film they are both about to watch.
294
+ // The machine may well be running others further on: what it has spare goes to
295
+ // finishing the file, and that is a different question from crowding.
296
+ assert.equal(onTheStretchOf(made, 100), 1, "one encoder for the pair");
297
+ assert.equal(onTheStretchOf(made, 102), 1, "and one for the film just in front of them");
298
+ assertNoOverlap(made);
299
+ });
300
+
301
+ test("two viewers far apart get an encoder each", () => {
302
+ const { made, watches } = orchestrator();
303
+ watches("one", { atSeconds: 400 });
304
+ made.reconcile();
305
+ measureSpeed(made.runsOn(PICTURE)[0], 6, clock);
306
+ made.noteProduced(PICTURE, 100);
307
+
308
+ watches("two", { atSeconds: 3000 });
309
+ made.reconcile();
310
+
311
+ assert.ok(placements(made).includes(750), "the far one is served where they stand");
312
+ assert.equal(onTheStretchOf(made, 750), 1, "and by one encoder, not a crowd");
313
+ assertNoOverlap(made);
314
+ });
315
+
316
+ test("two viewers: one seeking does not take the other's encoder", () => {
317
+ const { made, watches } = orchestrator();
318
+ watches("one", { atSeconds: 400 });
319
+ made.reconcile();
320
+ // A speed has to be measured before a second encoder can be justified: how
321
+ // long one takes to reach a number is the whole comparison. Real life
322
+ // measures it from the first run; a test that stated both viewers before any
323
+ // run existed would be asking the plan to buy a process on no evidence.
324
+ measureSpeed(made.runsOn(PICTURE)[0], 6, clock);
325
+ made.noteProduced(PICTURE, 100);
326
+ watches("two", { atSeconds: 3000 });
327
+ made.reconcile();
328
+ for (const run of made.runsOn(PICTURE)) {
329
+ measureSpeed(run, 6, clock);
330
+ }
331
+ assert.ok(made.runsOn(PICTURE).length >= 2, "one each to begin with");
332
+
333
+ // The far one seeks somewhere else entirely.
334
+ watches("two", { atSeconds: 2000 });
335
+ made.reconcile();
336
+
337
+ // Asked of the coverage, not of a run's first number: a run whose road was
338
+ // shortened begins again at its own head, so "is this viewer served" is a
339
+ // question about the map and never about where a process happens to start.
340
+ const coverage = made.coverageOf(PICTURE);
341
+ assert.equal(coverage.stateOf(105), "making", "the one who did not move is still served");
342
+ assert.ok(placements(made).includes(500), "and the one who moved is served where they landed");
343
+ assertNoOverlap(made);
344
+ });
345
+
346
+ test("two viewers: one pausing leaves the other served", () => {
347
+ const { made, watches } = orchestrator();
348
+ watches("one", { atSeconds: 400 });
349
+ watches("two", { atSeconds: 3000 });
350
+ made.reconcile();
351
+ for (const run of made.runsOn(PICTURE)) {
352
+ measureSpeed(run, 1, clock);
353
+ }
354
+
355
+ watches("two", { atSeconds: 3000, playing: false });
356
+ made.reconcile();
357
+
358
+ assert.equal(made.coverageOf(PICTURE).stateOf(105), "making",
359
+ "the one still watching keeps their encoder");
360
+ assertNoOverlap(made);
361
+ });
362
+
363
+ test("two viewers: the one who leaves takes nothing from the one who stays", () => {
364
+ const { made, watches, leaves } = orchestrator();
365
+ watches("one", { atSeconds: 400 });
366
+ watches("two", { atSeconds: 3000 });
367
+ made.reconcile();
368
+ for (const run of made.runsOn(PICTURE)) {
369
+ measureSpeed(run, 1, clock);
370
+ }
371
+
372
+ leaves("two");
373
+ made.reconcile();
374
+
375
+ assert.equal(made.coverageOf(PICTURE).stateOf(105), "making",
376
+ "the one who stayed is still served");
377
+ assertNoOverlap(made);
378
+ });
379
+
380
+ // ------------------------------------------------------------- three viewers
381
+
382
+ test("three viewers far apart get an encoder each when the machine affords it", () => {
383
+ const { made, watches } = orchestrator({ maxRuns: 3 });
384
+ watches("one", { atSeconds: 400 });
385
+ watches("two", { atSeconds: 2000 });
386
+ watches("three", { atSeconds: 3600 });
387
+ made.reconcile();
388
+ for (const run of made.runsOn(PICTURE)) {
389
+ measureSpeed(run, 6, clock);
390
+ }
391
+ made.reconcile();
392
+
393
+ const where = placements(made);
394
+ for (const at of [100, 500, 900]) {
395
+ assert.ok(where.includes(at), `somebody is making what the viewer at #${at} needs`);
396
+ assert.equal(onTheStretchOf(made, at), 1, `and one encoder there, not a crowd`);
397
+ }
398
+ assertNoOverlap(made);
399
+ });
400
+
401
+ test("three viewers, a machine that affords two: the budget binds, not the map", () => {
402
+ // The number of encoders is the smaller of what the map asks and what the
403
+ // machine can hold. Which two are served follows from the order the work is
404
+ // taken in; what must not happen is a third process on a host that cannot
405
+ // hold it, or two processes writing one number.
406
+ const { made, watches } = orchestrator({ maxRuns: 2 });
407
+ watches("one", { atSeconds: 400 });
408
+ watches("two", { atSeconds: 2000 });
409
+ watches("three", { atSeconds: 3600 });
410
+ made.reconcile();
411
+ for (const run of made.runsOn(PICTURE)) {
412
+ measureSpeed(run, 1, clock);
413
+ }
414
+ made.reconcile();
415
+
416
+ assert.ok(made.runsOn(PICTURE).length <= 2, "never more than the machine affords");
417
+ assert.ok(made.runsOn(PICTURE).length >= 1, "and never nothing while people wait");
418
+ assertNoOverlap(made);
419
+ });
420
+
421
+ test("three viewers: one seeks onto another, and the two of them share", () => {
422
+ const { made, watches } = orchestrator({ maxRuns: 3 });
423
+ watches("one", { atSeconds: 400 });
424
+ made.reconcile();
425
+ measureSpeed(made.runsOn(PICTURE)[0], 6, clock);
426
+ made.noteProduced(PICTURE, 100);
427
+ watches("two", { atSeconds: 2000 });
428
+ watches("three", { atSeconds: 3600 });
429
+ made.reconcile();
430
+ for (const run of made.runsOn(PICTURE)) {
431
+ measureSpeed(run, 6, clock);
432
+ }
433
+ made.reconcile();
434
+ assert.equal(made.runsOn(PICTURE).length, 3);
435
+
436
+ // The third joins the second. Two peaks where there were three.
437
+ watches("three", { atSeconds: 2008 });
438
+ made.reconcile();
439
+
440
+ assert.ok(made.runsOn(PICTURE).length <= 3, "no process is added by people converging");
441
+ assertNoOverlap(made);
442
+ });
443
+
444
+ test("three viewers: all paused, and no encoder is added for any of them", () => {
445
+ const { made, watches } = orchestrator({ maxRuns: 3 });
446
+ watches("one", { atSeconds: 400 });
447
+ made.reconcile();
448
+ measureSpeed(made.runsOn(PICTURE)[0], 1, clock);
449
+ const before = made.runsOn(PICTURE).length;
450
+
451
+ watches("one", { atSeconds: 400, playing: false });
452
+ watches("two", { atSeconds: 2000, playing: false });
453
+ watches("three", { atSeconds: 3600, playing: false });
454
+ made.reconcile();
455
+
456
+ // Nobody is coming anywhere, so nothing can be late — and the machine does not
457
+ // therefore fall idle: what it has spare goes on finishing the file, which is
458
+ // what makes a seek back into a made part start playing at once.
459
+ assert.ok(made.runsOn(PICTURE).length >= before, "the file goes on being finished");
460
+ assertNoOverlap(made);
461
+ });
462
+
463
+ test("the plan is a function of the state: the same state twice gives the same answer", () => {
464
+ // What stops a moving map from becoming a thrash: the decision is arithmetic
465
+ // over the state, so a pass that finds nothing changed changes nothing.
466
+ const { made, watches } = orchestrator({ maxRuns: 3 });
467
+ watches("one", { atSeconds: 400 });
468
+ watches("two", { atSeconds: 2000 });
469
+ made.reconcile();
470
+ for (const run of made.runsOn(PICTURE)) {
471
+ measureSpeed(run, 1, clock);
472
+ }
473
+ made.reconcile();
474
+ const first = placements(made);
475
+ made.reconcile();
476
+ made.reconcile();
477
+ assert.deepEqual(placements(made), first, "three more passes moved nothing");
478
+ });
479
+
480
+ // ---------------------------------------------------------------- the edge
481
+
482
+ test("exactly realtime arrives exactly on time, and no second encoder is bought", () => {
483
+ // The knife edge, and it lands on "in time" by construction: the deadline of
484
+ // a number is the distance to it, and an encoder at 1.0x covers that distance
485
+ // in exactly that time. So the arithmetic says nothing is late and nothing is
486
+ // bought. Worth pinning, because the formula this model replaced said
487
+ // something quite different — `speed / (1 - speed)` divides by zero here and
488
+ // claims the encoder stays ahead FOR EVER, which is the same answer arrived
489
+ // at by nonsense.
490
+ const { made, watches } = orchestrator({ maxRuns: 3 });
491
+ watches("one", { atSeconds: 400 });
492
+ made.reconcile();
493
+ measureSpeed(made.runsOn(PICTURE)[0], 1, clock);
494
+ made.noteProduced(PICTURE, 100);
495
+ made.reconcile();
496
+
497
+ assert.equal(onTheStretchOf(made, 150), 1,
498
+ "just in time everywhere, so one encoder on the viewer's stretch is enough");
499
+ assertNoOverlap(made);
500
+ });
501
+
502
+ test("below realtime, whether a second encoder helps is arithmetic, and here it does not", () => {
503
+ // The side of the edge that matters, and the answer is not the one this check
504
+ // asserted when it was written. An encoder at half realtime loses ground on
505
+ // the viewer every second, so everything ahead of it is late — and a second
506
+ // process does not fix that, because on this host each of two runs at 1.70x
507
+ // slower than one. The near film gets worse by more than the far film gets
508
+ // better, so the total seconds of waiting go UP, and the score refuses.
509
+ //
510
+ // What this case actually needs is the answer the user gave for it: a machine
511
+ // that cannot serve its viewers without stopping the picture says so, and the
512
+ // viewer is moved to another proxy. That is not built. Adding processes is not
513
+ // a substitute for it and the model correctly declines to pretend otherwise.
514
+ const { made, watches, leaves } = orchestrator({ maxRuns: 3 });
515
+ watches("one", { atSeconds: 400 });
516
+ made.reconcile();
517
+ measureSpeed(made.runsOn(PICTURE)[0], 0.5, clock);
518
+ made.noteProduced(PICTURE, 100);
519
+ made.reconcile();
520
+
521
+ assert.equal(made.runsOn(PICTURE).length, 1,
522
+ "one encoder, because two of them would leave the viewer waiting longer");
523
+ assert.equal(made.coverageOf(PICTURE).stateOf(101), "making",
524
+ "and it is on the film in front of them");
525
+ assertNoOverlap(made);
526
+ void leaves;
527
+ });
528
+
529
+ test("two viewers arriving together on a cold output get one encoder, then are measured", () => {
530
+ // Nothing has measured how fast this machine encodes, so how long one encoder
531
+ // would take to reach the second viewer is not a known quantity. Buying a
532
+ // process on that is buying it on no evidence, which is refused; one is placed
533
+ // and the speed it reports is what justifies the next.
534
+ const { made, watches } = orchestrator({ maxRuns: 3 });
535
+ watches("one", { atSeconds: 400 });
536
+ watches("two", { atSeconds: 3000 });
537
+ made.reconcile();
538
+ assert.ok(made.runsOn(PICTURE).length >= 1, "at least one, until something is measured");
539
+ assert.equal(onTheStretchOf(made, 100), 1, "and not two on the same place");
540
+
541
+ measureSpeed(made.runsOn(PICTURE)[0], 6, clock);
542
+ made.noteProduced(PICTURE, 100);
543
+ made.reconcile();
544
+ assert.ok(placements(made).includes(750), "and now the far one is served too");
545
+ assertNoOverlap(made);
546
+ });
547
+
548
+ test("an encoder comfortably faster than realtime is left to do the whole stretch", () => {
549
+ const { made, watches } = orchestrator({ maxRuns: 3 });
550
+ watches("one", { atSeconds: 400 });
551
+ made.reconcile();
552
+ measureSpeed(made.runsOn(PICTURE)[0], 6, clock);
553
+ made.noteProduced(PICTURE, 100);
554
+ made.reconcile();
555
+
556
+ assert.equal(onTheStretchOf(made, 150), 1, "one encoder on the stretch they are watching");
557
+ assertNoOverlap(made);
558
+ });
559
+
560
+ test("a swarm that feeds one encoder moves it to whoever is late, rather than serving nobody", () => {
561
+ // The budget binds. The one encoder there is stands where nothing is due —
562
+ // the viewer left — so it is moved to the soonest number that IS due. Without
563
+ // this the viewer who seeked was served by nobody at all: the run kept its
564
+ // road because nothing covered what lay in front of IT.
565
+ const { made, watches } = orchestrator({ maxRuns: 3 });
566
+ watches("one", { atSeconds: 400 });
567
+ made.reconcile();
568
+ const [run] = made.runsOn(PICTURE);
569
+ // 6x against a swarm charging 0.25 s per second of film: one encoder already
570
+ // takes one and a half of what is delivered, so the budget is one.
571
+ measureSpeed(run, 6, clock);
572
+ made.refetchSecPerFilmSecond = () => 0.25;
573
+ made.noteProduced(PICTURE, 100);
574
+
575
+ watches("one", { atSeconds: 3000 });
576
+ made.reconcile();
577
+
578
+ assert.equal(made.runsOn(PICTURE).length, 1, "still one, because that is what the swarm feeds");
579
+ assert.deepEqual(placements(made), [750], "and it is where the viewer now stands");
580
+ assertNoOverlap(made);
581
+ });
582
+
583
+ // ------------------------------------------------------- seeking BACKWARD
584
+
585
+ test("one viewer seeking back into film that exists is served from it, with no encoder", () => {
586
+ // The case the whole layer was built for. Everything behind them has been
587
+ // made, so nothing is late anywhere they are going, and the score says the
588
+ // cheapest arrangement is the one that changes nothing. A restart here is the
589
+ // 647-second stall of 2026-09-06 in miniature.
590
+ const { made, watches } = orchestrator();
591
+ watches("one", { atSeconds: 400 });
592
+ made.reconcile();
593
+ measureSpeed(made.runsOn(PICTURE)[0], 6, clock);
594
+ for (let index = 100; index <= 160; index += 1) {
595
+ made.noteProduced(PICTURE, index);
596
+ }
597
+ const before = made.runsOn(PICTURE);
598
+
599
+ watches("one", { atSeconds: 440 });
600
+ made.reconcile();
601
+
602
+ // Not "the count is unchanged": with nothing late anywhere, the machine is
603
+ // free to spend what it has spare on finishing the file, and that is what the
604
+ // user asked for. What must not happen is a process put on film that already
605
+ // exists, or the run they were behind restarted — which is the 647-second
606
+ // stall of 2026-09-06 in miniature.
607
+ for (const was of before) {
608
+ assert.ok(made.runsOn(PICTURE).includes(was),
609
+ "the encoder they were behind is not restarted");
610
+ }
611
+ for (const run_ of made.runsOn(PICTURE)) {
612
+ // Where it is WORKING, not where it was created: the surviving run was
613
+ // started at #100 and has since produced up to #161.
614
+ assert.equal(made.coverageOf(PICTURE).isReady(run_.head), false,
615
+ `no process is bought for film that exists (#${run_.head})`);
616
+ }
617
+ assertNoOverlap(made);
618
+ });
619
+
620
+ test("one viewer seeking back into film nobody has gets an encoder there", () => {
621
+ // Behind them is not the same as made. Where the film was never encoded, going
622
+ // back is exactly as bare as going forward, and the arithmetic is the same one.
623
+ const { made, watches } = orchestrator();
624
+ watches("one", { atSeconds: 3000 });
625
+ made.reconcile();
626
+ measureSpeed(made.runsOn(PICTURE)[0], 6, clock);
627
+ made.noteProduced(PICTURE, 750);
628
+
629
+ watches("one", { atSeconds: 400 });
630
+ made.reconcile();
631
+
632
+ const coverage = made.coverageOf(PICTURE);
633
+ assert.equal(coverage.stateOf(100), "making", "somebody is making where they landed");
634
+ assertNoOverlap(made);
635
+ });
636
+
637
+ test("two viewers: one seeks back onto film the other already had made", () => {
638
+ const { made, watches } = orchestrator({ maxRuns: 3 });
639
+ watches("one", { atSeconds: 400 });
640
+ made.reconcile();
641
+ measureSpeed(made.runsOn(PICTURE)[0], 6, clock);
642
+ for (let index = 100; index <= 200; index += 1) {
643
+ made.noteProduced(PICTURE, index);
644
+ }
645
+ watches("two", { atSeconds: 3000 });
646
+ made.reconcile();
647
+ for (const run of made.runsOn(PICTURE)) {
648
+ measureSpeed(run, 6, clock);
649
+ }
650
+ const before = made.runsOn(PICTURE).length;
651
+
652
+ // The far one comes back to where the first one has already been.
653
+ watches("two", { atSeconds: 500 });
654
+ made.reconcile();
655
+
656
+ assert.ok(made.runsOn(PICTURE).length <= before,
657
+ "coming back onto made film buys nobody an encoder");
658
+ assertNoOverlap(made);
659
+ });
660
+
661
+ test("three viewers: one forward, one back, one paused", () => {
662
+ // All three motions at once, which is the state a real proxy is in most of the
663
+ // time. What must hold is what always must: never two encoders on one number,
664
+ // never more than the machine affords, and somebody making what the moving
665
+ // viewers are about to need.
666
+ const { made, watches } = orchestrator({ maxRuns: 3 });
667
+ watches("one", { atSeconds: 400 });
668
+ watches("two", { atSeconds: 2000 });
669
+ watches("three", { atSeconds: 3600 });
670
+ made.reconcile();
671
+ for (const run of made.runsOn(PICTURE)) {
672
+ measureSpeed(run, 6, clock);
673
+ }
674
+ made.reconcile();
675
+
676
+ watches("one", { atSeconds: 800 });
677
+ watches("two", { atSeconds: 1200 });
678
+ watches("three", { atSeconds: 3600, playing: false });
679
+ made.reconcile();
680
+
681
+ assert.ok(made.runsOn(PICTURE).length <= 3, "never more than the machine holds");
682
+ assertNoOverlap(made);
683
+ const coverage = made.coverageOf(PICTURE);
684
+ assert.equal(coverage.stateOf(200), "making", "the one who went forward is served");
685
+ assert.equal(coverage.stateOf(300), "making", "and so is the one who came back");
686
+ });
687
+
688
+ test("a viewer scrubbing back and forth does not accumulate encoders", () => {
689
+ // A person dragging the time bar states a new position every few hundred
690
+ // milliseconds, and each of those is a state the plan answers. What must hold
691
+ // is that the answers do not pile up: an encoder bought for a place the viewer
692
+ // passed through is not still running when they have gone back.
693
+ //
694
+ // Not that the arrangement is identical to the one before the scrub — the
695
+ // model does not promise that and nothing here should claim it. What it
696
+ // promises is that no arrangement costs more than it is worth.
697
+ const { made, watches } = orchestrator({ maxRuns: 3 });
698
+ watches("one", { atSeconds: 400 });
699
+ made.reconcile();
700
+ measureSpeed(made.runsOn(PICTURE)[0], 6, clock);
701
+ made.noteProduced(PICTURE, 100);
702
+ made.reconcile();
703
+ const settled = placements(made);
704
+
705
+ watches("one", { atSeconds: 2000 });
706
+ made.reconcile();
707
+ for (const run of made.runsOn(PICTURE)) {
708
+ measureSpeed(run, 6, clock);
709
+ }
710
+ watches("one", { atSeconds: 404 });
711
+ made.reconcile();
712
+ watches("one", { atSeconds: 400 });
713
+ made.reconcile();
714
+ made.reconcile();
715
+
716
+ // Not "no more encoders" — the machine is allowed to spend what it has spare on
717
+ // finishing the file, and after a scrub there is more of the file it has seen.
718
+ // What must not happen is two of them on one stretch, or more than the machine
719
+ // holds, or the viewer left unserved.
720
+ assert.ok(made.runsOn(PICTURE).length <= 3, "never more than the machine holds");
721
+ assert.equal(onTheStretchOf(made, 105), 1, "one encoder on the film in front of them");
722
+ assert.equal(made.coverageOf(PICTURE).stateOf(105), "making", "and the viewer is served");
723
+ assertNoOverlap(made);
724
+ void settled;
725
+ });