@torrent-tv/proxy 2.88.4 → 2.88.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (489) hide show
  1. package/.claude/commands/opsx/apply.md +155 -155
  2. package/.claude/commands/opsx/archive.md +160 -160
  3. package/.claude/commands/opsx/explore.md +174 -174
  4. package/.claude/commands/opsx/propose.md +109 -109
  5. package/.claude/commands/opsx/sync.md +143 -143
  6. package/.claude/skills/openspec-apply-change/SKILL.md +159 -159
  7. package/.claude/skills/openspec-archive-change/SKILL.md +117 -117
  8. package/.claude/skills/openspec-explore/SKILL.md +289 -289
  9. package/.claude/skills/openspec-propose/SKILL.md +113 -113
  10. package/.claude/skills/openspec-sync-specs/SKILL.md +147 -147
  11. package/.dockerignore +5 -5
  12. package/AGENTS.md +322 -322
  13. package/CHANGELOG.md +2266 -2259
  14. package/Dockerfile +30 -30
  15. package/LICENSE +16 -16
  16. package/README.md +369 -369
  17. package/assets/calibration/NOTICE.md +100 -100
  18. package/assets/calibration/audio/NOTICE.md +19 -0
  19. package/assets/calibration/audio/aac.m4a +0 -0
  20. package/assets/calibration/audio/ac3.ac3 +0 -0
  21. package/assets/calibration/audio/alac.m4a +0 -0
  22. package/assets/calibration/audio/dts.dts +0 -0
  23. package/assets/calibration/audio/eac3.eac3 +0 -0
  24. package/assets/calibration/audio/flac.flac +0 -0
  25. package/assets/calibration/audio/manifest.json +98 -0
  26. package/assets/calibration/audio/mp2.mp2 +0 -0
  27. package/assets/calibration/audio/mp3.mp3 +0 -0
  28. package/assets/calibration/audio/opus.opus +0 -0
  29. package/assets/calibration/audio/pcm.wav +0 -0
  30. package/assets/calibration/audio/truehd.thd +0 -0
  31. package/assets/calibration/audio/vorbis.ogg +0 -0
  32. package/assets/diagnostics/sctpstate.gdb +31 -31
  33. package/bin/cli.js +684 -684
  34. package/bin/thread-pool.js +30 -30
  35. package/biome.json +412 -411
  36. package/docs/browser-proxy-contract.md +155 -152
  37. package/docs/container-architecture.md +218 -218
  38. package/docs/download-architecture.md +261 -261
  39. package/docs/encode-architecture.md +729 -729
  40. package/docs/encode-run-state.md +96 -96
  41. package/docs/logs.md +97 -97
  42. package/docs/playback-start-forecast.md +56 -0
  43. package/docs/socks5-egress-research.md +114 -114
  44. package/docs/storage-architecture.md +267 -267
  45. package/knip.json +14 -14
  46. package/openspec/changes/adaptive-bitrate/design.md +92 -92
  47. package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
  48. package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
  49. package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
  50. package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
  51. package/openspec/changes/binary-distribution/proposal.md +53 -53
  52. package/openspec/changes/chunked-request-bodies/design.md +81 -81
  53. package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
  54. package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
  55. package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
  56. package/openspec/changes/cold-start/design.md +98 -98
  57. package/openspec/changes/cold-start/proposal.md +62 -62
  58. package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
  59. package/openspec/changes/cold-start/tasks.md +48 -48
  60. package/openspec/changes/disk-cap/.openspec.yaml +2 -2
  61. package/openspec/changes/disk-cap/proposal.md +37 -37
  62. package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
  63. package/openspec/changes/disk-cap/tasks.md +19 -19
  64. package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
  65. package/openspec/changes/proxy-observability/design.md +38 -38
  66. package/openspec/changes/proxy-observability/proposal.md +49 -49
  67. package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
  68. package/openspec/changes/proxy-observability/tasks.md +21 -21
  69. package/openspec/changes/seek-debounce/design.md +118 -118
  70. package/openspec/changes/seek-debounce/proposal.md +59 -59
  71. package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
  72. package/openspec/changes/seek-debounce/tasks.md +33 -33
  73. package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
  74. package/openspec/changes/subtitle-language/proposal.md +50 -50
  75. package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
  76. package/openspec/changes/subtitle-language/tasks.md +26 -26
  77. package/openspec/changes/track-selection/.openspec.yaml +2 -2
  78. package/openspec/changes/track-selection/design.md +47 -47
  79. package/openspec/changes/track-selection/proposal.md +45 -45
  80. package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
  81. package/openspec/changes/track-selection/tasks.md +20 -20
  82. package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
  83. package/openspec/changes/transcode-quality/proposal.md +51 -51
  84. package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
  85. package/openspec/changes/transcode-quality/tasks.md +63 -63
  86. package/openspec/config.yaml +38 -38
  87. package/package.json +1 -1
  88. package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
  89. package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
  90. package/research/double-spawn-2026-09-10.md +171 -171
  91. package/research/handover-2026-09-13.md +209 -209
  92. package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
  93. package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
  94. package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
  95. package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
  96. package/routes/api/client-logs/post.js +86 -86
  97. package/routes/api/delivery-sink/get.js +72 -72
  98. package/routes/api/link-probe/get.js +51 -51
  99. package/routes/api/playback-plan/audio-tracks/post.js +29 -29
  100. package/routes/api/playback-plan/post.js +53 -53
  101. package/routes/api/sources/post.js +36 -36
  102. package/routes/api/sources/stats/get.js +132 -132
  103. package/routes/api/sources/warm/post.js +217 -217
  104. package/routes/api/subtitles/get.js +230 -230
  105. package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
  106. package/routes/api/transcode-sessions/net-report/post.js +138 -138
  107. package/routes/api/transcode-sessions/post.js +178 -178
  108. package/routes/api/transcode-sessions/progress/get.js +27 -27
  109. package/routes/api/transcode-sessions/release/post.js +41 -41
  110. package/routes/api/transcode-sessions/seek/post.js +63 -63
  111. package/routes/health/get.js +13 -13
  112. package/routes/healthz/get.js +13 -13
  113. package/routes/stream/get.js +380 -380
  114. package/routes/transcode/audio-file/get.js +64 -64
  115. package/routes/transcode/audio-warm/get.js +95 -95
  116. package/routes/transcode/session-file/get.js +52 -52
  117. package/routes/transcode/variant-file/get.js +95 -95
  118. package/routes/transcode/variant-warm/get.js +84 -84
  119. package/scripts/build-audio-samples.mjs +31 -0
  120. package/scripts/render-run-graph.js +173 -173
  121. package/server.js +730 -725
  122. package/services/encode/CoverageMap.js +428 -428
  123. package/services/encode/CushionReport.js +345 -345
  124. package/services/encode/EncodeAdmission.js +376 -376
  125. package/services/encode/EncodeOrchestrator.js +1217 -1217
  126. package/services/encode/EncodePlan.js +1366 -1366
  127. package/services/encode/EncodeRun.js +813 -813
  128. package/services/encode/EncodeRuns.js +1237 -1235
  129. package/services/encode/Encoder.js +142 -142
  130. package/services/encode/EncoderSelection.js +38 -38
  131. package/services/encode/LocalObservations.js +359 -359
  132. package/services/encode/NvencEncoder.js +50 -50
  133. package/services/encode/OutputOpening.js +783 -783
  134. package/services/encode/OutputTimes.js +579 -579
  135. package/services/encode/QsvEncoder.js +58 -58
  136. package/services/encode/Renditions.js +2282 -2282
  137. package/services/encode/RunProgress.js +84 -84
  138. package/services/encode/SegmentDemand.js +95 -95
  139. package/services/encode/SoftwareEncoder.js +121 -121
  140. package/services/encode/V4l2m2mEncoder.js +53 -53
  141. package/services/encode/VaapiEncoder.js +66 -66
  142. package/services/encode/args.js +418 -418
  143. package/services/encode/audio-calibration.js +67 -0
  144. package/services/encode/audio-work-rate.js +15 -0
  145. package/services/encode/available-share.js +89 -89
  146. package/services/encode/calibration.js +361 -361
  147. package/services/encode/contention.js +128 -128
  148. package/services/encode/decode-cost-fit.js +363 -363
  149. package/services/encode/encode-exit.js +114 -114
  150. package/services/encode/encode-run-state.js +551 -551
  151. package/services/encode/encoder-readings.js +45 -45
  152. package/services/encode/fingerprint.js +322 -322
  153. package/services/encode/host-load.js +174 -174
  154. package/services/encode/hwaccel.js +1894 -1894
  155. package/services/encode/index.js +9 -9
  156. package/services/encode/learned-median.js +90 -90
  157. package/services/encode/output/EncodedOutput.js +57 -57
  158. package/services/encode/output/Output.js +131 -131
  159. package/services/encode/output/OutputCatalog.js +414 -414
  160. package/services/encode/output/OutputSpec.js +390 -390
  161. package/services/encode/output/Timeline.js +308 -308
  162. package/services/encode/output/cut-grid.js +107 -107
  163. package/services/encode/output/index.js +20 -20
  164. package/services/encode/output/ladder.js +32 -32
  165. package/services/encode/output/playlists.js +284 -284
  166. package/services/encode/output/rates.js +125 -125
  167. package/services/encode/output-key-format.js +21 -21
  168. package/services/encode/quality/EncodeCost.js +1167 -1150
  169. package/services/encode/quality/HostLoad.js +525 -525
  170. package/services/encode/quality/HostTimings.js +249 -249
  171. package/services/encode/quality/OutputQualityState.js +28 -28
  172. package/services/encode/quality/QualityController.js +935 -935
  173. package/services/encode/quality/QualityOffer.js +416 -416
  174. package/services/encode/quality/drain-threat.js +47 -47
  175. package/services/encode/quality/link-budget.js +349 -349
  176. package/services/encode/quality/output-format.js +319 -319
  177. package/services/encode/quality/serving-output.js +216 -216
  178. package/services/encode/quality/visible-rung.js +57 -57
  179. package/services/encode/run-budget.js +97 -97
  180. package/services/encode/run-command.js +754 -754
  181. package/services/encode/run-costs.js +155 -155
  182. package/services/encode/run-inputs.js +42 -42
  183. package/services/encode/segment-formats/fmp4.js +458 -456
  184. package/services/encode/segment-formats/index.js +85 -85
  185. package/services/encode/segment-formats/init-compat.js +324 -324
  186. package/services/encode/segment-formats/mp4-boxes.js +12 -1
  187. package/services/encode/segment-formats/mpegts.js +134 -134
  188. package/services/encode/start-stop-cost.js +178 -178
  189. package/services/encode/throughput.js +97 -97
  190. package/services/encode/torrent-cost.js +88 -88
  191. package/services/identity/installation-id.js +38 -38
  192. package/services/media/ContainerOrchestrator.js +149 -149
  193. package/services/media/KeyframeTables.js +316 -316
  194. package/services/media/SourceFile.js +298 -298
  195. package/services/media/SubtitleCues.js +520 -520
  196. package/services/media/SubtitleOrchestrator.js +167 -167
  197. package/services/media/audio-inventory.js +299 -298
  198. package/services/media/container/AviContainer.js +266 -266
  199. package/services/media/container/Container.js +609 -609
  200. package/services/media/container/ContainerFactory.js +56 -56
  201. package/services/media/container/KeyframeTable.js +272 -272
  202. package/services/media/container/MatroskaContainer.js +1494 -1494
  203. package/services/media/container/Mp4Container.js +1344 -1344
  204. package/services/media/container/ebml-reader.js +159 -159
  205. package/services/media/ffmpeg-banner.js +260 -260
  206. package/services/media/keyframe-probe.js +157 -157
  207. package/services/media/media-info-probe.js +93 -93
  208. package/services/media/playback-planner.js +931 -931
  209. package/services/media/tracks/AudioTrack.js +143 -143
  210. package/services/media/tracks/ContainerTrack.js +75 -75
  211. package/services/media/tracks/ImageSubtitleTrack.js +19 -19
  212. package/services/media/tracks/SubtitleTrack.js +38 -38
  213. package/services/media/tracks/TextSubtitleTrack.js +287 -287
  214. package/services/media/tracks/VideoTrack.js +33 -33
  215. package/services/media/tracks/audio-bound.js +233 -233
  216. package/services/media/tracks/index.js +15 -15
  217. package/services/media/tracks/language-detect.js +174 -174
  218. package/services/server/OutputLifecycle.js +418 -418
  219. package/services/server/SegmentServing.js +1150 -1150
  220. package/services/server/ViewerRequests.js +785 -760
  221. package/services/server/WaitLedger.js +142 -142
  222. package/services/server/controllers/PlaybackController.js +40 -40
  223. package/services/server/controllers/SubtitleController.js +109 -109
  224. package/services/server/http-responses.js +60 -60
  225. package/services/server/playback-readiness.js +516 -708
  226. package/services/server/transcode-session-files.js +325 -325
  227. package/services/server/wire-outputs.js +1020 -995
  228. package/services/storage/Diagnostics.js +202 -202
  229. package/services/storage/MachineBudget.js +235 -235
  230. package/services/storage/allowance.js +107 -107
  231. package/services/storage/core-dumps.js +106 -106
  232. package/services/storage/files/CompletedFiles.js +391 -391
  233. package/services/storage/files/piece-from-whole-file.js +118 -118
  234. package/services/storage/free.js +71 -71
  235. package/services/storage/keep.js +48 -48
  236. package/services/storage/machine-memory.js +56 -56
  237. package/services/storage/memory-report.js +575 -575
  238. package/services/storage/piece-store/piece-disk-store.js +539 -539
  239. package/services/storage/piece-store/piece-lru.js +495 -495
  240. package/services/storage/piece-store/shared-piece-store.js +1937 -1931
  241. package/services/storage/returns.js +103 -103
  242. package/services/storage/segment-store/SegmentStore.js +1255 -1240
  243. package/services/storage/wire.js +175 -175
  244. package/services/torrent/demand/DemandRegister.js +202 -202
  245. package/services/torrent/demand/Urgency.js +137 -137
  246. package/services/torrent/demand/Window.js +118 -118
  247. package/services/torrent/demand/index.js +8 -8
  248. package/services/torrent/demand/pieces.js +140 -140
  249. package/services/torrent/download/SwarmSelection.js +386 -386
  250. package/services/torrent/download/registry.js +102 -102
  251. package/services/torrent/download/withdraw-claim.js +80 -80
  252. package/services/torrent/files.js +455 -455
  253. package/services/torrent/naming.js +619 -619
  254. package/services/torrent/piece-store-of.js +29 -29
  255. package/services/torrent/supply-margin.js +262 -262
  256. package/services/torrent/torrent-pool.js +2688 -2675
  257. package/services/torrent/worker/background-fill.js +213 -213
  258. package/services/torrent/worker/channel.js +271 -271
  259. package/services/torrent/worker/client.js +765 -765
  260. package/services/torrent/worker/fastest-wires.js +342 -342
  261. package/services/torrent/worker/handle-state.js +23 -23
  262. package/services/torrent/worker/held-bytes.js +155 -155
  263. package/services/torrent/worker/install-webrtc-shim.js +32 -32
  264. package/services/torrent/worker/piece-reader.js +1078 -1078
  265. package/services/torrent/worker/pool-adapter.js +371 -371
  266. package/services/torrent/worker/protocol.js +184 -184
  267. package/services/torrent/worker/resume-warm.js +107 -107
  268. package/services/torrent/worker/webrtc-shim.js +219 -219
  269. package/services/torrent/worker/worker.js +1129 -1129
  270. package/services/transport/data-channel-handler.js +1369 -1369
  271. package/services/transport/delivery-probe.js +842 -842
  272. package/services/transport/health-collector.js +53 -53
  273. package/services/transport/nat-classifier.js +231 -231
  274. package/services/transport/packet-witness.js +808 -808
  275. package/services/transport/port-mapper.js +235 -235
  276. package/services/transport/registry-api.js +81 -81
  277. package/services/transport/tunnel-client.js +447 -447
  278. package/services/transport/usrsctp-state.js +175 -175
  279. package/services/transport/webrtc-manager.js +484 -484
  280. package/services/viewer/Assignments.js +340 -340
  281. package/services/viewer/PriorityMap.js +390 -390
  282. package/services/viewer/PriorityOrchestrator.js +325 -325
  283. package/services/viewer/Viewer.js +691 -691
  284. package/services/viewer/Viewers.js +368 -368
  285. package/services/viewer/active-output.js +37 -37
  286. package/services/viewer/audio-start.js +46 -46
  287. package/services/viewer/buffer-trend.js +81 -81
  288. package/services/viewer/choices.js +647 -647
  289. package/services/viewer/positions.js +126 -126
  290. package/services/viewer/report-intake.js +46 -46
  291. package/store/source-registry.js +71 -71
  292. package/test/asset-paths.test.js +28 -28
  293. package/test/audio-calibration.test.js +23 -0
  294. package/test/audio-inventory.test.js +212 -212
  295. package/test/audio-track-bitrate.test.js +40 -40
  296. package/test/auto-quality-step.test.js +707 -707
  297. package/test/available-share.test.js +76 -76
  298. package/test/background-fill.test.js +190 -190
  299. package/test/client-log-file.test.js +110 -110
  300. package/test/completed-files.test.js +115 -115
  301. package/test/concurrent-cost.test.js +148 -148
  302. package/test/container-media-info.test.js +228 -228
  303. package/test/contention.test.js +76 -76
  304. package/test/core-dumps.test.js +57 -57
  305. package/test/coverage-follows-the-disk.test.js +228 -228
  306. package/test/coverage-map.test.js +195 -195
  307. package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
  308. package/test/cut-times-timeline.test.js +65 -65
  309. package/test/cuts-follow-published-grid.test.js +109 -109
  310. package/test/data-channel-frame.test.js +63 -63
  311. package/test/data-channel-viewer-lifecycle.test.js +92 -92
  312. package/test/dead-torrent-handle.test.js +52 -52
  313. package/test/declared-rates.test.js +116 -116
  314. package/test/declared-tracks.test.js +43 -43
  315. package/test/decode-cost-fit.test.js +202 -202
  316. package/test/decode-cost.test.js +462 -462
  317. package/test/delivery-probe.test.js +393 -393
  318. package/test/demand-register.test.js +206 -206
  319. package/test/dht-bootstrap.test.js +72 -72
  320. package/test/drain-threat.test.js +65 -65
  321. package/test/duplicate-add.test.js +48 -48
  322. package/test/encode-admission.test.js +146 -146
  323. package/test/encode-cost.test.js +283 -283
  324. package/test/encode-exit.test.js +111 -111
  325. package/test/encode-orchestrator.test.js +419 -419
  326. package/test/encode-plan-viewers.test.js +718 -718
  327. package/test/encode-plan.test.js +545 -545
  328. package/test/encode-run-state.test.js +314 -314
  329. package/test/encode-run.test.js +162 -162
  330. package/test/encode-slope.test.js +46 -46
  331. package/test/encoded-output.test.js +39 -39
  332. package/test/encoder-kinds.test.js +122 -122
  333. package/test/encoder-readings.test.js +46 -46
  334. package/test/every-claimant-is-told-its-share.test.js +152 -152
  335. package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
  336. package/test/failed-start-breaker.test.js +202 -202
  337. package/test/fastest-wires.test.js +143 -143
  338. package/test/ffmpeg-stream-counts.test.js +78 -78
  339. package/test/file-edges.test.js +191 -191
  340. package/test/fmp4-presentation-clock.test.js +162 -147
  341. package/test/fmp4-self-contained.test.js +80 -80
  342. package/test/fmp4-timestamps.test.js +140 -140
  343. package/test/fmp4-track-completeness.test.js +41 -41
  344. package/test/format-keyed-outputs.test.js +78 -78
  345. package/test/health-metrics.test.js +38 -38
  346. package/test/held-request-width.test.js +157 -157
  347. package/test/helpers/encode-run.js +136 -136
  348. package/test/helpers/held-file.js +29 -29
  349. package/test/helpers/manager.js +57 -57
  350. package/test/helpers/output-spec.js +63 -63
  351. package/test/host-load.test.js +68 -68
  352. package/test/index-accuracy-is-about-the-file.test.js +128 -128
  353. package/test/init-compat.test.js +219 -219
  354. package/test/input-lost-quiets-the-plan.test.js +261 -261
  355. package/test/input-unavailable.test.js +48 -48
  356. package/test/installation-id.test.js +39 -39
  357. package/test/keeping-period.test.js +83 -83
  358. package/test/keyframe-index-accuracy.test.js +188 -188
  359. package/test/keyframe-table-budget.test.js +102 -102
  360. package/test/keyframes-belong-to-the-file.test.js +249 -249
  361. package/test/learned-median.test.js +37 -37
  362. package/test/letting-go-asks-who-is-watching.test.js +191 -191
  363. package/test/limit-row-by-area.test.js +60 -60
  364. package/test/link-budget.test.js +112 -112
  365. package/test/link-probe.test.js +56 -56
  366. package/test/local-encoding-data.test.js +273 -273
  367. package/test/logger-repeats.test.js +134 -134
  368. package/test/look-ahead-coverage.test.js +56 -56
  369. package/test/manager-surface.test.js +292 -292
  370. package/test/matroska-cues-track.test.js +192 -192
  371. package/test/memory-budget.test.js +390 -390
  372. package/test/move-cost.test.js +216 -216
  373. package/test/mp4-subtitles.test.js +173 -173
  374. package/test/one-authority.test.js +285 -285
  375. package/test/one-budget.test.js +149 -149
  376. package/test/one-output-one-session.test.js +273 -270
  377. package/test/one-resource-divided.test.js +185 -185
  378. package/test/one-serving-output-per-height.test.js +458 -458
  379. package/test/orchestrator-wired.test.js +206 -206
  380. package/test/output-catalog.test.js +39 -39
  381. package/test/output-shape.test.js +21 -21
  382. package/test/output-spec.test.js +251 -251
  383. package/test/output-speed.test.js +88 -88
  384. package/test/outputs-being-prepared.test.js +40 -40
  385. package/test/packet-witness-ring.test.js +236 -236
  386. package/test/packet-witness.test.js +148 -148
  387. package/test/piece-disk-store.test.js +401 -401
  388. package/test/piece-from-whole-file.test.js +129 -129
  389. package/test/piece-lru.test.js +319 -319
  390. package/test/piece-reader.test.js +162 -162
  391. package/test/piece-selection-offset.test.js +85 -85
  392. package/test/piece-store-eviction.test.js +595 -595
  393. package/test/piece-store-never-refuses.test.js +153 -153
  394. package/test/piece-store-reservations.test.js +403 -403
  395. package/test/piece-store-slow-disk.test.js +159 -159
  396. package/test/piece-tail.test.js +97 -97
  397. package/test/plan-host-timings.test.js +68 -68
  398. package/test/plan-is-steady.test.js +225 -225
  399. package/test/playback-readiness.test.js +483 -453
  400. package/test/playback-start-regressions.test.js +99 -99
  401. package/test/priority-map-download.test.js +174 -174
  402. package/test/priority-map-emptied.test.js +204 -204
  403. package/test/priority-map-per-output.test.js +215 -215
  404. package/test/priority-map.test.js +163 -163
  405. package/test/probe-wedge-certainty.test.js +60 -60
  406. package/test/produced-copy-choice.test.js +213 -213
  407. package/test/published-timeline.test.js +83 -83
  408. package/test/quality-offer.test.js +162 -162
  409. package/test/quality-variants.test.js +1257 -1257
  410. package/test/rate-control-in-the-output.test.js +180 -180
  411. package/test/read-survives-withdrawal.test.js +164 -164
  412. package/test/read-window.test.js +365 -365
  413. package/test/resume-warm.test.js +39 -39
  414. package/test/run-graph-drift.test.js +37 -37
  415. package/test/run-inputs.test.js +80 -80
  416. package/test/run-intervals.test.js +100 -100
  417. package/test/run-position-follows-published-grid.test.js +89 -89
  418. package/test/run-progress.test.js +65 -65
  419. package/test/same-height-switch.test.js +664 -664
  420. package/test/seek-frees-the-viewers-requests.test.js +217 -217
  421. package/test/seek-landing.test.js +109 -109
  422. package/test/seek-target-not-superseded.test.js +195 -195
  423. package/test/segment-closed-on-both-branches.test.js +96 -96
  424. package/test/segment-cut-times.test.js +63 -63
  425. package/test/segment-demand.test.js +80 -80
  426. package/test/segment-hold-supersede.test.js +91 -91
  427. package/test/segment-judged-by-its-own-header.test.js +221 -221
  428. package/test/segment-serve-wiring.test.js +475 -475
  429. package/test/segment-store-eviction.test.js +247 -247
  430. package/test/segment-store.test.js +338 -323
  431. package/test/segment-true-start.test.js +86 -86
  432. package/test/segments-are-shared.test.js +176 -176
  433. package/test/send-chunk-bytes.test.js +86 -86
  434. package/test/serving-existing-output.test.js +105 -105
  435. package/test/serving-output.test.js +102 -102
  436. package/test/shared-piece-store.test.js +310 -310
  437. package/test/sidecar-naming.test.js +142 -142
  438. package/test/soundtrack-rate-bound.test.js +289 -289
  439. package/test/source-file.test.js +133 -133
  440. package/test/source-registry.test.js +18 -18
  441. package/test/split-a-stretch.test.js +139 -139
  442. package/test/stale-request-after-seek.test.js +158 -158
  443. package/test/startup-readings.test.js +124 -124
  444. package/test/stream-route.test.js +209 -209
  445. package/test/subtitle-cue-framing.test.js +200 -200
  446. package/test/subtitle-cue-source.test.js +102 -102
  447. package/test/subtitle-cue-walk.test.js +370 -370
  448. package/test/subtitle-cursor.test.js +63 -63
  449. package/test/subtitle-defaults.test.js +118 -118
  450. package/test/subtitle-language.test.js +252 -252
  451. package/test/subtitle-track-numbering.test.js +386 -386
  452. package/test/supply-margin.test.js +177 -177
  453. package/test/swarm-follows-readers.test.js +174 -174
  454. package/test/swarm-reach.test.js +130 -130
  455. package/test/swarm-selection.test.js +220 -220
  456. package/test/tail-duplication.test.js +167 -167
  457. package/test/the-channel-has-both-ends.test.js +91 -91
  458. package/test/the-torrent-thread-serves-bytes.test.js +107 -107
  459. package/test/thread-pool.test.js +53 -53
  460. package/test/timeline.test.js +84 -84
  461. package/test/torrent-contents.test.js +235 -235
  462. package/test/torrent-cost.test.js +71 -71
  463. package/test/torrent-files.test.js +221 -221
  464. package/test/torrent-naming.test.js +255 -255
  465. package/test/tracks-begin-together.test.js +209 -209
  466. package/test/tunnel-renewal.test.js +167 -167
  467. package/test/two-viewers-one-picture.test.js +504 -504
  468. package/test/upload-hurry.test.js +186 -186
  469. package/test/usrsctp-state.test.js +131 -131
  470. package/test/video-facts.test.js +102 -102
  471. package/test/viewer-assignments.test.js +143 -143
  472. package/test/viewer-outputs.test.js +285 -285
  473. package/test/viewer-presence.test.js +116 -116
  474. package/test/viewer-requests-admission.test.js +89 -89
  475. package/test/viewer-states.test.js +189 -189
  476. package/test/viewer-subtitle-subscription.test.js +96 -96
  477. package/test/viewer.test.js +139 -139
  478. package/test/viewing-generations.test.js +390 -390
  479. package/test/visible-rung.test.js +37 -37
  480. package/test/wait-ledger.test.js +100 -100
  481. package/test/webrtc-shim.test.js +149 -149
  482. package/test/wedge-certainty.test.js +131 -131
  483. package/test/withdraw-piece-claim.test.js +212 -212
  484. package/test/worker-channel.test.js +158 -158
  485. package/utils/client-log-file.js +151 -151
  486. package/utils/logger.js +312 -312
  487. package/utils/parse-range.js +25 -25
  488. package/utils/perf.js +59 -59
  489. package/utils/torrent-source-key.js +38 -38
@@ -1,390 +1,390 @@
1
- /**
2
- * @file THE PRIORITY MAP — what one viewer needs, what all of them need
3
- * together, and in what order the work should be taken.
4
- *
5
- * A layer of its own, below both orchestrators and depending on nothing. It
6
- * knows ONLY priorities: what is downloaded is the download orchestrator's own
7
- * knowledge, what is encoded is the encoding orchestrator's, and neither is
8
- * visible from here. Both read this and recompute their own on every change.
9
- *
10
- * The shape, stated by the user 2026-09-05:
11
- *
12
- * > Usually you make a map for each viewer, then merge the maps, then decide
13
- * > the best way of filling it given the encoders available, where they are now
14
- * > and how many there are.
15
- *
16
- * Three questions, and this file answers the first two. The third — the filling
17
- * — belongs to whoever holds the encoders, and it is handed the merged map
18
- * instead of a list of windows.
19
- *
20
- * **ONE PRIORITISATION, TWO CONSUMERS.** Downloading and encoding keep
21
- * different STATES — made / being made / free for one; downloaded / arriving,
22
- * and which peers hold it at what speed, for the other — but they must agree
23
- * about what matters first, or the swarm fetches what the encoder will not
24
- * reach for another twenty minutes. That agreement is this map.
25
- *
26
- * **THE UNIT IS SECONDS OF FILM.** A map is a set of stretches with sizes and a
27
- * length of its own, so it needs a unit, and seconds are the only one every
28
- * term of the arithmetic is already stated in: encode speed is a ratio of
29
- * seconds to seconds, the measured allowance below which an interruption
30
- * reaches a viewer is seconds, the viewer's position is seconds, the film's
31
- * length is seconds. Bytes cannot serve — how many a second costs is not known
32
- * when a file is opened and is not constant across it, and a soundtrack in a
33
- * file of its own has bytes of its own. Segment numbers cannot serve either:
34
- * they exist only once a cut grid is read, and two outputs of one film number
35
- * differently.
36
- *
37
- * **What this file must NOT know**, and the boundary is the point: nothing about
38
- * containers, cut grids, pieces or bytes. Turning a stretch of seconds into the
39
- * bytes of one track is the container's and the track's business, by whatever
40
- * means suit that file — a Cues table, a sample table, or a walk when the file
41
- * carries neither, which is the same answer they already give in order to play
42
- * it at all. Getting those bytes is the downloader's business; making segments
43
- * out of them is the encoder's.
44
- */
45
-
46
- /**
47
- * THE MAP ITSELF: one number per second of film.
48
- *
49
- * Literally an array as long as the film, where the value at second `x` says how
50
- * urgently that second is wanted. Stated by the user 2026-09-06, and it replaced
51
- * a list of stretches each carrying two numbers.
52
- *
53
- * Two arrays rather than one, because two different questions are asked of the
54
- * map and only one of them can be answered by a rank:
55
- *
56
- * - `priority` orders the work. Only the ORDER between values is meaningful;
57
- * the scale below says what the numbers separate;
58
- * - `secondsUntilPlayed` is a quantity, and it is what makes lateness
59
- * computable at all: whoever fills the map compares "when is this needed"
60
- * against "when would it arrive", and only the second half is theirs to work
61
- * out. `Infinity` where nobody is on their way.
62
- *
63
- * @typedef {object} PriorityMap
64
- * @property {number} durationSeconds - The film's length, in whole seconds.
65
- * @property {Uint8Array} priority - One entry per second. Zero means nothing is
66
- * wanted there, which happens only where nobody is watching at all.
67
- * @property {Float64Array} secondsUntilPlayed - One entry per second.
68
- * @property {Uint8Array} behind - One entry per second, 1 where the second lies
69
- * behind EVERY viewer. Stated rather than worked out from the other two,
70
- * because working it out means knowing this file's scale, and the consumers
71
- * are in other layers. Inferred from the absence of a time, as it was, it was
72
- * wrong for a stopped viewer: their whole film carries no time, so the film in
73
- * front of them counted as behind them and the encoders wandered to the start
74
- * of the file.
75
- */
76
-
77
- /**
78
- * A stretch of the map with one value throughout — the same facts said as a
79
- * range instead of as a run of equal entries.
80
- *
81
- * @typedef {object} DemandZone
82
- * @property {number} from - First second, inclusive.
83
- * @property {number} to - Last second, exclusive.
84
- * @property {number} priority
85
- * @property {number} withinSeconds
86
- * @property {boolean} behind - Whether this lies behind every viewer.
87
- */
88
-
89
- /**
90
- * THE SCALE. Three bands, and the order between them is the whole of what the
91
- * numbers claim.
92
- *
93
- * 1. IN FRONT OF SOMEBODY WHO IS WATCHING — the top value at the second they
94
- * are about to see, falling as the film gets further from them;
95
- * 2. IN FRONT OF SOMEBODY WHO HAS STOPPED — the same shape, and every value of
96
- * it below every value of band 1. A stopped viewer will watch this film when
97
- * they press play, so it is still in front of them; nobody is waiting for it
98
- * now, so it yields to anybody who is;
99
- * 3. BEHIND EVERYBODY — reachable only by a seek back, and nothing measures how
100
- * likely that is, so it is one value rather than a shape.
101
- *
102
- * IN FRONT ALWAYS OUTRANKS BEHIND, at any distance and whoever is stopped. The
103
- * bands make that true by construction rather than by a comparison somebody has
104
- * to remember to write.
105
- *
106
- * The values inside a band are AN ENCODING OF AN ORDER, not a measurement:
107
- * every consumer asks only which of two seconds comes first, so any strictly
108
- * decreasing shape does the same work, and none of these numbers claims
109
- * anything about the world.
110
- */
111
- export const AT_A_WATCHING_VIEWER = 100;
112
- const WATCHING_FLOOR = 70;
113
- const AT_A_STOPPED_VIEWER = 69;
114
- const STOPPED_FLOOR = 39;
115
-
116
- /** Behind everybody. */
117
- export const NOBODY_IS_COMING = 1;
118
-
119
- /** Nothing is wanted here at all. */
120
- const NOTHING = 0;
121
-
122
- /**
123
- * Does this value mean "behind every viewer"?
124
- *
125
- * A read of the scale, so that nobody has to infer the side from the absence of
126
- * a deadline. Inferred that way it was wrong for a stopped viewer, whose whole
127
- * film carries no deadline: the film in front of them then counted as behind
128
- * them, and the encoders were free to wander to the start of the file.
129
- *
130
- * @param {number} priority
131
- * @returns {boolean}
132
- */
133
- export function isBehindEverybody(priority) {
134
- return priority <= NOBODY_IS_COMING;
135
- }
136
-
137
- /**
138
- * Is this second the one a viewer is about to watch?
139
- *
140
- * The top step of the watching band: the film under their feet and the measured
141
- * allowance in front of it, which is the depth below which an interruption
142
- * reaches them.
143
- *
144
- * @param {number} priority
145
- * @returns {boolean}
146
- */
147
- export function isAtAWatchingViewer(priority) {
148
- return priority >= AT_A_WATCHING_VIEWER;
149
- }
150
-
151
- /**
152
- * Is this second in front of somebody who has STOPPED, and of nobody who is
153
- * watching?
154
- *
155
- * Wanted, and wanted after everything anybody is walking towards: they will see
156
- * it when they press play, and nothing here knows when that is.
157
- *
158
- * @param {number} priority
159
- * @returns {boolean}
160
- */
161
- export function isNobodyComingNow(priority) {
162
- return priority > NOBODY_IS_COMING && priority <= AT_A_STOPPED_VIEWER;
163
- }
164
-
165
- /**
166
- * How far into its band a second sits, given its distance from the viewer.
167
- *
168
- * The step doubles: near the viewer the difference between now and ten seconds
169
- * away decides what is made first, while twenty minutes out the difference
170
- * between twenty and twenty-one minutes changes nothing. The first step is the
171
- * MEASURED allowance for this file on this swarm — the depth below which an
172
- * interruption reaches the viewer — so how finely the map divides comes from a
173
- * measurement rather than from a number chosen here.
174
- *
175
- * @param {number} distanceSeconds
176
- * @param {number} allowanceSeconds
177
- * @returns {number} Zero at the viewer, growing with distance.
178
- */
179
- function stepsAway(distanceSeconds, allowanceSeconds) {
180
- if (!(distanceSeconds > 0)) {
181
- return 0;
182
- }
183
- return Math.floor(Math.log2(1 + distanceSeconds / allowanceSeconds));
184
- }
185
-
186
- /**
187
- * An empty map of the right length: nothing wanted anywhere.
188
- *
189
- * @param {number} durationSeconds
190
- * @returns {PriorityMap}
191
- */
192
- export function emptyMap(durationSeconds) {
193
- const seconds = Number.isFinite(durationSeconds) && durationSeconds > 0
194
- ? Math.ceil(durationSeconds)
195
- : 0;
196
- return {
197
- durationSeconds: seconds,
198
- priority: new Uint8Array(seconds),
199
- secondsUntilPlayed: new Float64Array(seconds).fill(Number.POSITIVE_INFINITY),
200
- behind: new Uint8Array(seconds).fill(1)
201
- };
202
- }
203
-
204
- /**
205
- * One viewer's map.
206
- *
207
- * **The number is a reading of how soon they will be there.** A viewer watching
208
- * forward reaches second `x` after `x - p` seconds of film. That distance — not
209
- * a clock time — is what the value is derived from, and it is why two viewers
210
- * can be compared at all: the nearer one wins the second they both want.
211
- *
212
- * **A pause removes the time, not the direction.** A stopped viewer is still
213
- * standing somewhere, and the film in front of them is still the film they will
214
- * watch, so their map keeps its shape and moves into the band below. Collapsed
215
- * to one flat value over the whole film, as it was, their position disappeared
216
- * entirely — and with it the rule that what is in front is made first.
217
- *
218
- * **What must NOT be here**, and the boundary is the point: how fast this
219
- * machine encodes, how many encoders that takes, what a second weighs in bytes,
220
- * where a piece boundary falls, and what has already been made. Those are
221
- * answers the encoding and the downloading work out for themselves, from this
222
- * map and from what each knows about itself.
223
- *
224
- * @param {object} params
225
- * @param {number} params.atSeconds - Where they are watching from.
226
- * @param {number} params.durationSeconds - How long the film is.
227
- * @param {number} params.allowanceSeconds - The measured depth below which an
228
- * interruption reaches this viewer. The width of the first step.
229
- * @param {boolean} [params.playing] - Whether the picture is moving.
230
- * @returns {PriorityMap}
231
- */
232
- export function mapForViewer({ atSeconds, durationSeconds, allowanceSeconds, playing = true }) {
233
- const map = emptyMap(durationSeconds);
234
- if (map.durationSeconds === 0) {
235
- return map;
236
- }
237
- const at = Number.isFinite(atSeconds) && atSeconds > 0 ? Math.floor(atSeconds) : 0;
238
- const allowance = Number.isFinite(allowanceSeconds) && allowanceSeconds > 0
239
- ? allowanceSeconds
240
- : 1;
241
- const watching = playing !== false;
242
- const top = watching ? AT_A_WATCHING_VIEWER : AT_A_STOPPED_VIEWER;
243
- const floor = watching ? WATCHING_FLOOR : STOPPED_FLOOR;
244
- for (let second = 0; second < map.durationSeconds; second += 1) {
245
- if (second < at) {
246
- map.priority[second] = NOBODY_IS_COMING;
247
- continue;
248
- }
249
- map.behind[second] = 0;
250
- const distance = second - at;
251
- map.priority[second] = Math.max(floor, top - stepsAway(distance, allowance));
252
- // A viewer who has stopped is on their way nowhere, so there is no second by
253
- // which any of this must exist. The direction survives the pause, in the
254
- // priority above; the time does not, because there is none to state.
255
- map.secondsUntilPlayed[second] = watching ? distance : Number.POSITIVE_INFINITY;
256
- }
257
- return map;
258
- }
259
-
260
- /**
261
- * Every viewer's map as one.
262
- *
263
- * The highest priority per second wins, and the soonest time: film two people
264
- * want is as urgent as the more urgent of them, and making it once serves both.
265
- * The two are taken separately on purpose — the priority is coarse, so two
266
- * viewers can tie on it while one is genuinely nearer, and whoever schedules
267
- * against the time must be given the nearer one.
268
- *
269
- * In front of ANYBODY is in front, and the third array says so directly.
270
- *
271
- * @param {PriorityMap[]} maps
272
- * @returns {PriorityMap}
273
- */
274
- export function mergeMaps(maps) {
275
- const all = (maps ?? []).filter((map) => map && map.durationSeconds > 0);
276
- if (all.length === 0) {
277
- return emptyMap(0);
278
- }
279
- const merged = emptyMap(Math.max(...all.map((map) => map.durationSeconds)));
280
- for (const map of all) {
281
- for (let second = 0; second < map.durationSeconds; second += 1) {
282
- if (map.priority[second] > merged.priority[second]) {
283
- merged.priority[second] = map.priority[second];
284
- }
285
- if (map.secondsUntilPlayed[second] < merged.secondsUntilPlayed[second]) {
286
- merged.secondsUntilPlayed[second] = map.secondsUntilPlayed[second];
287
- }
288
- // In front of ANYBODY is in front: a stretch one viewer has passed is
289
- // still the film another is walking towards.
290
- if (map.behind[second] === 0) {
291
- merged.behind[second] = 0;
292
- }
293
- }
294
- }
295
- return merged;
296
- }
297
-
298
- /**
299
- * The map as stretches: runs of seconds that agree on both numbers.
300
- *
301
- * A VIEW of the map, never a second copy of it. Consumers that work in ranges —
302
- * the swarm is asked for byte ranges, an encoder is given a stretch of pieces —
303
- * would otherwise each write this walk for themselves.
304
- *
305
- * @param {PriorityMap} map
306
- * @returns {DemandZone[]} Ascending, without gaps or overlaps. Seconds nobody
307
- * wants are left out.
308
- */
309
- export function runsOf(map) {
310
- /** @type {DemandZone[]} */
311
- const runs = [];
312
- if (!map || !(map.durationSeconds > 0)) {
313
- return runs;
314
- }
315
- for (let second = 0; second < map.durationSeconds; second += 1) {
316
- const priority = map.priority[second];
317
- if (priority === NOTHING) {
318
- continue;
319
- }
320
- const behind = map.behind[second] === 1;
321
- const previous = runs[runs.length - 1];
322
- if (
323
- previous
324
- && previous.to === second
325
- && previous.priority === priority
326
- && previous.behind === behind
327
- ) {
328
- previous.to = second + 1;
329
- continue;
330
- }
331
- // THE TIME OF A STRETCH IS THE TIME OF ITS NEAR EDGE. A stretch is met at
332
- // its beginning, so its beginning is when it must exist; whoever needs the
333
- // time of a second inside it walks forward from there, at a second of film
334
- // per second, which is the same arithmetic that put the number here.
335
- //
336
- // Merging on the exact time as well would merge nothing at all: the time is
337
- // the distance, so it differs at every second, and the map came back as one
338
- // stretch per second of film.
339
- const withinSeconds = map.secondsUntilPlayed[second];
340
- runs.push({
341
- from: second,
342
- to: second + 1,
343
- priority,
344
- withinSeconds,
345
- behind
346
- });
347
- }
348
- return runs;
349
- }
350
-
351
- /**
352
- * The map in the order the work is taken: most urgent first, and within one
353
- * priority the earliest film first — that is where somebody is stopped.
354
- *
355
- * @param {DemandZone[]} runs
356
- * @returns {DemandZone[]}
357
- */
358
- export function inWorkingOrder(runs) {
359
- return [...(runs ?? [])].sort(
360
- (left, right) => right.priority - left.priority || left.from - right.from
361
- );
362
- }
363
-
364
- /**
365
- * The earliest second of film anybody is waiting on, or null when nobody is.
366
- *
367
- * WHY A SOUNDTRACK ASKS THE MAP AND NOT A VIEWER. Sound is produced for the
368
- * same reason a picture is — somebody is about to reach it — and the map is
369
- * where that is already stated, in seconds of film, merged over everybody
370
- * watching. Reading it from a viewer instead means the encoding layer holds a
371
- * person, and then every question about which person it is (the earliest of
372
- * two, the one on this rung, the one who has not reported yet) has to be
373
- * answered again in a second place. The map answers it once.
374
- *
375
- * The EARLIEST among the most urgent, because a track started at the leader's
376
- * position has nothing to give the viewer behind them, and the map's own order
377
- * — most urgent first, earliest film first within a priority — already says
378
- * which that is.
379
- *
380
- * @param {DemandZone[]} runs - From {@link runsOf}.
381
- * @returns {number | null} Seconds.
382
- */
383
- export function earliestUrgentSecond(runs) {
384
- const wanted = (runs ?? []).filter((zone) => zone && !zone.behind);
385
- if (wanted.length === 0) {
386
- return null;
387
- }
388
- const top = Math.max(...wanted.map((zone) => Number(zone.priority) || 0));
389
- return Math.min(...wanted.filter((zone) => (Number(zone.priority) || 0) === top).map((zone) => zone.from));
390
- }
1
+ /**
2
+ * @file THE PRIORITY MAP — what one viewer needs, what all of them need
3
+ * together, and in what order the work should be taken.
4
+ *
5
+ * A layer of its own, below both orchestrators and depending on nothing. It
6
+ * knows ONLY priorities: what is downloaded is the download orchestrator's own
7
+ * knowledge, what is encoded is the encoding orchestrator's, and neither is
8
+ * visible from here. Both read this and recompute their own on every change.
9
+ *
10
+ * The shape, stated by the user 2026-09-05:
11
+ *
12
+ * > Usually you make a map for each viewer, then merge the maps, then decide
13
+ * > the best way of filling it given the encoders available, where they are now
14
+ * > and how many there are.
15
+ *
16
+ * Three questions, and this file answers the first two. The third — the filling
17
+ * — belongs to whoever holds the encoders, and it is handed the merged map
18
+ * instead of a list of windows.
19
+ *
20
+ * **ONE PRIORITISATION, TWO CONSUMERS.** Downloading and encoding keep
21
+ * different STATES — made / being made / free for one; downloaded / arriving,
22
+ * and which peers hold it at what speed, for the other — but they must agree
23
+ * about what matters first, or the swarm fetches what the encoder will not
24
+ * reach for another twenty minutes. That agreement is this map.
25
+ *
26
+ * **THE UNIT IS SECONDS OF FILM.** A map is a set of stretches with sizes and a
27
+ * length of its own, so it needs a unit, and seconds are the only one every
28
+ * term of the arithmetic is already stated in: encode speed is a ratio of
29
+ * seconds to seconds, the measured allowance below which an interruption
30
+ * reaches a viewer is seconds, the viewer's position is seconds, the film's
31
+ * length is seconds. Bytes cannot serve — how many a second costs is not known
32
+ * when a file is opened and is not constant across it, and a soundtrack in a
33
+ * file of its own has bytes of its own. Segment numbers cannot serve either:
34
+ * they exist only once a cut grid is read, and two outputs of one film number
35
+ * differently.
36
+ *
37
+ * **What this file must NOT know**, and the boundary is the point: nothing about
38
+ * containers, cut grids, pieces or bytes. Turning a stretch of seconds into the
39
+ * bytes of one track is the container's and the track's business, by whatever
40
+ * means suit that file — a Cues table, a sample table, or a walk when the file
41
+ * carries neither, which is the same answer they already give in order to play
42
+ * it at all. Getting those bytes is the downloader's business; making segments
43
+ * out of them is the encoder's.
44
+ */
45
+
46
+ /**
47
+ * THE MAP ITSELF: one number per second of film.
48
+ *
49
+ * Literally an array as long as the film, where the value at second `x` says how
50
+ * urgently that second is wanted. Stated by the user 2026-09-06, and it replaced
51
+ * a list of stretches each carrying two numbers.
52
+ *
53
+ * Two arrays rather than one, because two different questions are asked of the
54
+ * map and only one of them can be answered by a rank:
55
+ *
56
+ * - `priority` orders the work. Only the ORDER between values is meaningful;
57
+ * the scale below says what the numbers separate;
58
+ * - `secondsUntilPlayed` is a quantity, and it is what makes lateness
59
+ * computable at all: whoever fills the map compares "when is this needed"
60
+ * against "when would it arrive", and only the second half is theirs to work
61
+ * out. `Infinity` where nobody is on their way.
62
+ *
63
+ * @typedef {object} PriorityMap
64
+ * @property {number} durationSeconds - The film's length, in whole seconds.
65
+ * @property {Uint8Array} priority - One entry per second. Zero means nothing is
66
+ * wanted there, which happens only where nobody is watching at all.
67
+ * @property {Float64Array} secondsUntilPlayed - One entry per second.
68
+ * @property {Uint8Array} behind - One entry per second, 1 where the second lies
69
+ * behind EVERY viewer. Stated rather than worked out from the other two,
70
+ * because working it out means knowing this file's scale, and the consumers
71
+ * are in other layers. Inferred from the absence of a time, as it was, it was
72
+ * wrong for a stopped viewer: their whole film carries no time, so the film in
73
+ * front of them counted as behind them and the encoders wandered to the start
74
+ * of the file.
75
+ */
76
+
77
+ /**
78
+ * A stretch of the map with one value throughout — the same facts said as a
79
+ * range instead of as a run of equal entries.
80
+ *
81
+ * @typedef {object} DemandZone
82
+ * @property {number} from - First second, inclusive.
83
+ * @property {number} to - Last second, exclusive.
84
+ * @property {number} priority
85
+ * @property {number} withinSeconds
86
+ * @property {boolean} behind - Whether this lies behind every viewer.
87
+ */
88
+
89
+ /**
90
+ * THE SCALE. Three bands, and the order between them is the whole of what the
91
+ * numbers claim.
92
+ *
93
+ * 1. IN FRONT OF SOMEBODY WHO IS WATCHING — the top value at the second they
94
+ * are about to see, falling as the film gets further from them;
95
+ * 2. IN FRONT OF SOMEBODY WHO HAS STOPPED — the same shape, and every value of
96
+ * it below every value of band 1. A stopped viewer will watch this film when
97
+ * they press play, so it is still in front of them; nobody is waiting for it
98
+ * now, so it yields to anybody who is;
99
+ * 3. BEHIND EVERYBODY — reachable only by a seek back, and nothing measures how
100
+ * likely that is, so it is one value rather than a shape.
101
+ *
102
+ * IN FRONT ALWAYS OUTRANKS BEHIND, at any distance and whoever is stopped. The
103
+ * bands make that true by construction rather than by a comparison somebody has
104
+ * to remember to write.
105
+ *
106
+ * The values inside a band are AN ENCODING OF AN ORDER, not a measurement:
107
+ * every consumer asks only which of two seconds comes first, so any strictly
108
+ * decreasing shape does the same work, and none of these numbers claims
109
+ * anything about the world.
110
+ */
111
+ export const AT_A_WATCHING_VIEWER = 100;
112
+ const WATCHING_FLOOR = 70;
113
+ const AT_A_STOPPED_VIEWER = 69;
114
+ const STOPPED_FLOOR = 39;
115
+
116
+ /** Behind everybody. */
117
+ export const NOBODY_IS_COMING = 1;
118
+
119
+ /** Nothing is wanted here at all. */
120
+ const NOTHING = 0;
121
+
122
+ /**
123
+ * Does this value mean "behind every viewer"?
124
+ *
125
+ * A read of the scale, so that nobody has to infer the side from the absence of
126
+ * a deadline. Inferred that way it was wrong for a stopped viewer, whose whole
127
+ * film carries no deadline: the film in front of them then counted as behind
128
+ * them, and the encoders were free to wander to the start of the file.
129
+ *
130
+ * @param {number} priority
131
+ * @returns {boolean}
132
+ */
133
+ export function isBehindEverybody(priority) {
134
+ return priority <= NOBODY_IS_COMING;
135
+ }
136
+
137
+ /**
138
+ * Is this second the one a viewer is about to watch?
139
+ *
140
+ * The top step of the watching band: the film under their feet and the measured
141
+ * allowance in front of it, which is the depth below which an interruption
142
+ * reaches them.
143
+ *
144
+ * @param {number} priority
145
+ * @returns {boolean}
146
+ */
147
+ export function isAtAWatchingViewer(priority) {
148
+ return priority >= AT_A_WATCHING_VIEWER;
149
+ }
150
+
151
+ /**
152
+ * Is this second in front of somebody who has STOPPED, and of nobody who is
153
+ * watching?
154
+ *
155
+ * Wanted, and wanted after everything anybody is walking towards: they will see
156
+ * it when they press play, and nothing here knows when that is.
157
+ *
158
+ * @param {number} priority
159
+ * @returns {boolean}
160
+ */
161
+ export function isNobodyComingNow(priority) {
162
+ return priority > NOBODY_IS_COMING && priority <= AT_A_STOPPED_VIEWER;
163
+ }
164
+
165
+ /**
166
+ * How far into its band a second sits, given its distance from the viewer.
167
+ *
168
+ * The step doubles: near the viewer the difference between now and ten seconds
169
+ * away decides what is made first, while twenty minutes out the difference
170
+ * between twenty and twenty-one minutes changes nothing. The first step is the
171
+ * MEASURED allowance for this file on this swarm — the depth below which an
172
+ * interruption reaches the viewer — so how finely the map divides comes from a
173
+ * measurement rather than from a number chosen here.
174
+ *
175
+ * @param {number} distanceSeconds
176
+ * @param {number} allowanceSeconds
177
+ * @returns {number} Zero at the viewer, growing with distance.
178
+ */
179
+ function stepsAway(distanceSeconds, allowanceSeconds) {
180
+ if (!(distanceSeconds > 0)) {
181
+ return 0;
182
+ }
183
+ return Math.floor(Math.log2(1 + distanceSeconds / allowanceSeconds));
184
+ }
185
+
186
+ /**
187
+ * An empty map of the right length: nothing wanted anywhere.
188
+ *
189
+ * @param {number} durationSeconds
190
+ * @returns {PriorityMap}
191
+ */
192
+ export function emptyMap(durationSeconds) {
193
+ const seconds = Number.isFinite(durationSeconds) && durationSeconds > 0
194
+ ? Math.ceil(durationSeconds)
195
+ : 0;
196
+ return {
197
+ durationSeconds: seconds,
198
+ priority: new Uint8Array(seconds),
199
+ secondsUntilPlayed: new Float64Array(seconds).fill(Number.POSITIVE_INFINITY),
200
+ behind: new Uint8Array(seconds).fill(1)
201
+ };
202
+ }
203
+
204
+ /**
205
+ * One viewer's map.
206
+ *
207
+ * **The number is a reading of how soon they will be there.** A viewer watching
208
+ * forward reaches second `x` after `x - p` seconds of film. That distance — not
209
+ * a clock time — is what the value is derived from, and it is why two viewers
210
+ * can be compared at all: the nearer one wins the second they both want.
211
+ *
212
+ * **A pause removes the time, not the direction.** A stopped viewer is still
213
+ * standing somewhere, and the film in front of them is still the film they will
214
+ * watch, so their map keeps its shape and moves into the band below. Collapsed
215
+ * to one flat value over the whole film, as it was, their position disappeared
216
+ * entirely — and with it the rule that what is in front is made first.
217
+ *
218
+ * **What must NOT be here**, and the boundary is the point: how fast this
219
+ * machine encodes, how many encoders that takes, what a second weighs in bytes,
220
+ * where a piece boundary falls, and what has already been made. Those are
221
+ * answers the encoding and the downloading work out for themselves, from this
222
+ * map and from what each knows about itself.
223
+ *
224
+ * @param {object} params
225
+ * @param {number} params.atSeconds - Where they are watching from.
226
+ * @param {number} params.durationSeconds - How long the film is.
227
+ * @param {number} params.allowanceSeconds - The measured depth below which an
228
+ * interruption reaches this viewer. The width of the first step.
229
+ * @param {boolean} [params.playing] - Whether the picture is moving.
230
+ * @returns {PriorityMap}
231
+ */
232
+ export function mapForViewer({ atSeconds, durationSeconds, allowanceSeconds, playing = true }) {
233
+ const map = emptyMap(durationSeconds);
234
+ if (map.durationSeconds === 0) {
235
+ return map;
236
+ }
237
+ const at = Number.isFinite(atSeconds) && atSeconds > 0 ? Math.floor(atSeconds) : 0;
238
+ const allowance = Number.isFinite(allowanceSeconds) && allowanceSeconds > 0
239
+ ? allowanceSeconds
240
+ : 1;
241
+ const watching = playing !== false;
242
+ const top = watching ? AT_A_WATCHING_VIEWER : AT_A_STOPPED_VIEWER;
243
+ const floor = watching ? WATCHING_FLOOR : STOPPED_FLOOR;
244
+ for (let second = 0; second < map.durationSeconds; second += 1) {
245
+ if (second < at) {
246
+ map.priority[second] = NOBODY_IS_COMING;
247
+ continue;
248
+ }
249
+ map.behind[second] = 0;
250
+ const distance = second - at;
251
+ map.priority[second] = Math.max(floor, top - stepsAway(distance, allowance));
252
+ // A viewer who has stopped is on their way nowhere, so there is no second by
253
+ // which any of this must exist. The direction survives the pause, in the
254
+ // priority above; the time does not, because there is none to state.
255
+ map.secondsUntilPlayed[second] = watching ? distance : Number.POSITIVE_INFINITY;
256
+ }
257
+ return map;
258
+ }
259
+
260
+ /**
261
+ * Every viewer's map as one.
262
+ *
263
+ * The highest priority per second wins, and the soonest time: film two people
264
+ * want is as urgent as the more urgent of them, and making it once serves both.
265
+ * The two are taken separately on purpose — the priority is coarse, so two
266
+ * viewers can tie on it while one is genuinely nearer, and whoever schedules
267
+ * against the time must be given the nearer one.
268
+ *
269
+ * In front of ANYBODY is in front, and the third array says so directly.
270
+ *
271
+ * @param {PriorityMap[]} maps
272
+ * @returns {PriorityMap}
273
+ */
274
+ export function mergeMaps(maps) {
275
+ const all = (maps ?? []).filter((map) => map && map.durationSeconds > 0);
276
+ if (all.length === 0) {
277
+ return emptyMap(0);
278
+ }
279
+ const merged = emptyMap(Math.max(...all.map((map) => map.durationSeconds)));
280
+ for (const map of all) {
281
+ for (let second = 0; second < map.durationSeconds; second += 1) {
282
+ if (map.priority[second] > merged.priority[second]) {
283
+ merged.priority[second] = map.priority[second];
284
+ }
285
+ if (map.secondsUntilPlayed[second] < merged.secondsUntilPlayed[second]) {
286
+ merged.secondsUntilPlayed[second] = map.secondsUntilPlayed[second];
287
+ }
288
+ // In front of ANYBODY is in front: a stretch one viewer has passed is
289
+ // still the film another is walking towards.
290
+ if (map.behind[second] === 0) {
291
+ merged.behind[second] = 0;
292
+ }
293
+ }
294
+ }
295
+ return merged;
296
+ }
297
+
298
+ /**
299
+ * The map as stretches: runs of seconds that agree on both numbers.
300
+ *
301
+ * A VIEW of the map, never a second copy of it. Consumers that work in ranges —
302
+ * the swarm is asked for byte ranges, an encoder is given a stretch of pieces —
303
+ * would otherwise each write this walk for themselves.
304
+ *
305
+ * @param {PriorityMap} map
306
+ * @returns {DemandZone[]} Ascending, without gaps or overlaps. Seconds nobody
307
+ * wants are left out.
308
+ */
309
+ export function runsOf(map) {
310
+ /** @type {DemandZone[]} */
311
+ const runs = [];
312
+ if (!map || !(map.durationSeconds > 0)) {
313
+ return runs;
314
+ }
315
+ for (let second = 0; second < map.durationSeconds; second += 1) {
316
+ const priority = map.priority[second];
317
+ if (priority === NOTHING) {
318
+ continue;
319
+ }
320
+ const behind = map.behind[second] === 1;
321
+ const previous = runs[runs.length - 1];
322
+ if (
323
+ previous
324
+ && previous.to === second
325
+ && previous.priority === priority
326
+ && previous.behind === behind
327
+ ) {
328
+ previous.to = second + 1;
329
+ continue;
330
+ }
331
+ // THE TIME OF A STRETCH IS THE TIME OF ITS NEAR EDGE. A stretch is met at
332
+ // its beginning, so its beginning is when it must exist; whoever needs the
333
+ // time of a second inside it walks forward from there, at a second of film
334
+ // per second, which is the same arithmetic that put the number here.
335
+ //
336
+ // Merging on the exact time as well would merge nothing at all: the time is
337
+ // the distance, so it differs at every second, and the map came back as one
338
+ // stretch per second of film.
339
+ const withinSeconds = map.secondsUntilPlayed[second];
340
+ runs.push({
341
+ from: second,
342
+ to: second + 1,
343
+ priority,
344
+ withinSeconds,
345
+ behind
346
+ });
347
+ }
348
+ return runs;
349
+ }
350
+
351
+ /**
352
+ * The map in the order the work is taken: most urgent first, and within one
353
+ * priority the earliest film first — that is where somebody is stopped.
354
+ *
355
+ * @param {DemandZone[]} runs
356
+ * @returns {DemandZone[]}
357
+ */
358
+ export function inWorkingOrder(runs) {
359
+ return [...(runs ?? [])].sort(
360
+ (left, right) => right.priority - left.priority || left.from - right.from
361
+ );
362
+ }
363
+
364
+ /**
365
+ * The earliest second of film anybody is waiting on, or null when nobody is.
366
+ *
367
+ * WHY A SOUNDTRACK ASKS THE MAP AND NOT A VIEWER. Sound is produced for the
368
+ * same reason a picture is — somebody is about to reach it — and the map is
369
+ * where that is already stated, in seconds of film, merged over everybody
370
+ * watching. Reading it from a viewer instead means the encoding layer holds a
371
+ * person, and then every question about which person it is (the earliest of
372
+ * two, the one on this rung, the one who has not reported yet) has to be
373
+ * answered again in a second place. The map answers it once.
374
+ *
375
+ * The EARLIEST among the most urgent, because a track started at the leader's
376
+ * position has nothing to give the viewer behind them, and the map's own order
377
+ * — most urgent first, earliest film first within a priority — already says
378
+ * which that is.
379
+ *
380
+ * @param {DemandZone[]} runs - From {@link runsOf}.
381
+ * @returns {number | null} Seconds.
382
+ */
383
+ export function earliestUrgentSecond(runs) {
384
+ const wanted = (runs ?? []).filter((zone) => zone && !zone.behind);
385
+ if (wanted.length === 0) {
386
+ return null;
387
+ }
388
+ const top = Math.max(...wanted.map((zone) => Number(zone.priority) || 0));
389
+ return Math.min(...wanted.filter((zone) => (Number(zone.priority) || 0) === top).map((zone) => zone.from));
390
+ }