@torrent-tv/proxy 2.89.7 → 2.89.8

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 (497) 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 +358 -338
  13. package/CHANGELOG.md +2340 -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 +1373 -1373
  263. package/services/transport/delivery-probe.js +842 -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 +92 -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.test.js +393 -393
  314. package/test/demand-register.test.js +206 -206
  315. package/test/dht-bootstrap.test.js +72 -72
  316. package/test/drain-threat.test.js +65 -65
  317. package/test/duplicate-add.test.js +48 -48
  318. package/test/encode-admission.test.js +146 -146
  319. package/test/encode-cost.test.js +275 -275
  320. package/test/encode-exit.test.js +111 -111
  321. package/test/encode-orchestrator.test.js +426 -426
  322. package/test/encode-plan-viewers.test.js +725 -725
  323. package/test/encode-plan.test.js +545 -545
  324. package/test/encode-run-state.test.js +314 -314
  325. package/test/encode-run.test.js +20 -0
  326. package/test/encode-slope.test.js +46 -46
  327. package/test/encoded-output.test.js +39 -39
  328. package/test/encoder-kinds.test.js +122 -122
  329. package/test/encoder-readings.test.js +68 -68
  330. package/test/episode-naming.test.js +131 -131
  331. package/test/every-claimant-is-told-its-share.test.js +152 -152
  332. package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
  333. package/test/failed-start-breaker.test.js +202 -202
  334. package/test/fastest-wires.test.js +143 -143
  335. package/test/ffmpeg-stream-counts.test.js +78 -78
  336. package/test/file-edges.test.js +191 -191
  337. package/test/fixtures/audio-frame-boundary.json +1 -1
  338. package/test/fmp4-self-contained.test.js +80 -80
  339. package/test/fmp4-timestamps.test.js +140 -140
  340. package/test/fmp4-track-completeness.test.js +41 -41
  341. package/test/format-keyed-outputs.test.js +78 -78
  342. package/test/health-metrics.test.js +36 -38
  343. package/test/held-read-ownership.test.js +77 -77
  344. package/test/held-request-width.test.js +157 -157
  345. package/test/helpers/encode-run.js +248 -240
  346. package/test/helpers/held-file.js +77 -77
  347. package/test/helpers/manager.js +62 -62
  348. package/test/helpers/matroska-file.js +296 -296
  349. package/test/helpers/output-spec.js +63 -63
  350. package/test/host-load.test.js +68 -68
  351. package/test/index-accuracy-is-about-the-file.test.js +128 -128
  352. package/test/init-compat.test.js +219 -219
  353. package/test/input-lost-quiets-the-plan.test.js +261 -261
  354. package/test/input-unavailable.test.js +48 -48
  355. package/test/installation-id.test.js +39 -39
  356. package/test/keeping-period.test.js +83 -83
  357. package/test/keyframe-index-accuracy.test.js +188 -188
  358. package/test/keyframe-table-budget.test.js +123 -123
  359. package/test/keyframes-belong-to-the-file.test.js +249 -249
  360. package/test/learned-median.test.js +37 -37
  361. package/test/letting-go-asks-who-is-watching.test.js +191 -191
  362. package/test/limit-row-by-area.test.js +60 -60
  363. package/test/link-budget.test.js +111 -111
  364. package/test/link-probe.test.js +56 -56
  365. package/test/local-encoding-data.test.js +273 -273
  366. package/test/logger-repeats.test.js +134 -134
  367. package/test/look-ahead-coverage.test.js +56 -56
  368. package/test/manager-surface.test.js +291 -291
  369. package/test/matroska-cluster-discovery.test.js +323 -323
  370. package/test/matroska-cues-track.test.js +192 -192
  371. package/test/media-time.test.js +62 -62
  372. package/test/memory-budget.test.js +390 -390
  373. package/test/move-cost.test.js +216 -216
  374. package/test/mp4-subtitles.test.js +173 -173
  375. package/test/one-authority.test.js +285 -285
  376. package/test/one-budget.test.js +149 -149
  377. package/test/one-output-one-session.test.js +273 -273
  378. package/test/one-resource-divided.test.js +185 -185
  379. package/test/one-serving-output-per-height.test.js +458 -458
  380. package/test/orchestrator-wired.test.js +207 -207
  381. package/test/output-catalog.test.js +39 -39
  382. package/test/output-shape.test.js +21 -21
  383. package/test/output-spec.test.js +251 -251
  384. package/test/output-speed.test.js +92 -87
  385. package/test/outputs-being-prepared.test.js +40 -40
  386. package/test/packet-witness-ring.test.js +236 -236
  387. package/test/packet-witness.test.js +148 -148
  388. package/test/piece-disk-store.test.js +401 -401
  389. package/test/piece-from-whole-file.test.js +129 -129
  390. package/test/piece-lru.test.js +319 -319
  391. package/test/piece-reader.test.js +162 -162
  392. package/test/piece-selection-offset.test.js +85 -85
  393. package/test/piece-store-eviction.test.js +595 -595
  394. package/test/piece-store-never-refuses.test.js +153 -153
  395. package/test/piece-store-reservations.test.js +403 -403
  396. package/test/piece-store-slow-disk.test.js +159 -159
  397. package/test/piece-tail.test.js +97 -97
  398. package/test/plan-host-timings.test.js +68 -68
  399. package/test/plan-is-steady.test.js +225 -225
  400. package/test/playback-readiness.test.js +518 -518
  401. package/test/playback-start-regressions.test.js +101 -101
  402. package/test/priority-map-download.test.js +174 -174
  403. package/test/priority-map-emptied.test.js +204 -204
  404. package/test/priority-map-per-output.test.js +215 -215
  405. package/test/priority-map.test.js +163 -163
  406. package/test/probe-wedge-certainty.test.js +60 -60
  407. package/test/produced-copy-choice.test.js +213 -213
  408. package/test/published-timeline.test.js +83 -83
  409. package/test/quality-offer.test.js +160 -160
  410. package/test/quality-variants.test.js +1274 -1270
  411. package/test/rate-control-in-the-output.test.js +181 -180
  412. package/test/read-survives-withdrawal.test.js +164 -164
  413. package/test/read-window.test.js +365 -365
  414. package/test/reads-not-yet-arrived.test.js +218 -218
  415. package/test/resume-warm.test.js +39 -39
  416. package/test/run-graph-drift.test.js +37 -37
  417. package/test/run-inputs.test.js +80 -80
  418. package/test/run-intervals.test.js +100 -100
  419. package/test/run-position-follows-published-grid.test.js +89 -89
  420. package/test/run-progress.test.js +64 -64
  421. package/test/same-height-switch.test.js +663 -663
  422. package/test/seek-frees-the-viewers-requests.test.js +217 -217
  423. package/test/seek-landing.test.js +109 -109
  424. package/test/seek-target-not-superseded.test.js +195 -195
  425. package/test/segment-closed-on-both-branches.test.js +114 -114
  426. package/test/segment-cut-times.test.js +63 -63
  427. package/test/segment-demand.test.js +80 -80
  428. package/test/segment-hold-supersede.test.js +91 -91
  429. package/test/segment-judged-by-its-own-header.test.js +222 -222
  430. package/test/segment-serve-wiring.test.js +481 -481
  431. package/test/segment-store-eviction.test.js +247 -247
  432. package/test/segment-store.test.js +352 -352
  433. package/test/segment-true-start.test.js +86 -86
  434. package/test/segments-are-shared.test.js +176 -176
  435. package/test/send-chunk-bytes.test.js +86 -86
  436. package/test/serving-existing-output.test.js +111 -111
  437. package/test/serving-output.test.js +102 -102
  438. package/test/shared-piece-store.test.js +310 -310
  439. package/test/sidecar-naming.test.js +142 -142
  440. package/test/soundtrack-rate-bound.test.js +289 -289
  441. package/test/source-file.test.js +133 -133
  442. package/test/source-files-route.test.js +113 -113
  443. package/test/source-registry.test.js +18 -18
  444. package/test/split-a-stretch.test.js +139 -139
  445. package/test/stale-request-after-seek.test.js +158 -158
  446. package/test/startup-readings.test.js +124 -124
  447. package/test/stream-route.test.js +237 -237
  448. package/test/subtitle-cue-framing.test.js +200 -200
  449. package/test/subtitle-cue-source.test.js +102 -102
  450. package/test/subtitle-cue-walk.test.js +385 -385
  451. package/test/subtitle-cursor.test.js +63 -63
  452. package/test/subtitle-defaults.test.js +118 -118
  453. package/test/subtitle-language.test.js +252 -252
  454. package/test/subtitle-metadata.test.js +15 -15
  455. package/test/subtitle-plan-waits.test.js +125 -125
  456. package/test/subtitle-track-numbering.test.js +391 -391
  457. package/test/supply-margin.test.js +190 -190
  458. package/test/swarm-follows-readers.test.js +174 -174
  459. package/test/swarm-reach.test.js +130 -130
  460. package/test/swarm-selection.test.js +220 -220
  461. package/test/tail-duplication.test.js +167 -167
  462. package/test/the-channel-has-both-ends.test.js +91 -91
  463. package/test/the-torrent-thread-serves-bytes.test.js +107 -107
  464. package/test/thread-pool.test.js +53 -53
  465. package/test/timeline.test.js +84 -84
  466. package/test/torrent-contents.test.js +235 -235
  467. package/test/torrent-cost.test.js +71 -71
  468. package/test/torrent-files.test.js +221 -221
  469. package/test/torrent-naming.test.js +255 -255
  470. package/test/tracks-begin-together.test.js +209 -209
  471. package/test/tunnel-renewal.test.js +167 -167
  472. package/test/two-viewers-one-picture.test.js +508 -508
  473. package/test/upload-hurry.test.js +186 -186
  474. package/test/usrsctp-state.test.js +131 -131
  475. package/test/video-facts.test.js +102 -102
  476. package/test/viewer-assignments.test.js +143 -143
  477. package/test/viewer-outputs.test.js +285 -285
  478. package/test/viewer-presence.test.js +116 -116
  479. package/test/viewer-requests-admission.test.js +92 -92
  480. package/test/viewer-starts-on-map.test.js +27 -27
  481. package/test/viewer-states.test.js +192 -189
  482. package/test/viewer-subtitle-subscription.test.js +96 -96
  483. package/test/viewer.test.js +139 -139
  484. package/test/viewing-generations.test.js +390 -390
  485. package/test/visible-rung.test.js +37 -37
  486. package/test/wait-ledger.test.js +100 -100
  487. package/test/webrtc-shim.test.js +149 -149
  488. package/test/wedge-certainty.test.js +131 -131
  489. package/test/whole-file-held-read.test.js +30 -30
  490. package/test/withdraw-piece-claim.test.js +212 -212
  491. package/test/worker-channel.test.js +158 -158
  492. package/utils/client-log-file.js +151 -151
  493. package/utils/coalesce.js +41 -41
  494. package/utils/logger.js +312 -312
  495. package/utils/parse-range.js +25 -25
  496. package/utils/perf.js +59 -59
  497. package/utils/torrent-source-key.js +38 -38
@@ -1,1270 +1,1274 @@
1
- /**
2
- * @file Quality variants: the master playlist, and what happens when the viewer
3
- * moves between rungs.
4
- *
5
- * What makes a mid-stream change of quality possible at all is that every
6
- * variant is cut at the SAME times, so a segment produced by one encoder can be
7
- * appended where another encoder's would have gone. The last test here pins
8
- * that property at its source; the rest cover the wiring that a route reaches —
9
- * a module test that imports a function directly cannot see a caller that never
10
- * calls it (2.9.124).
11
- */
12
-
13
- import test from "node:test";
14
- import { fakeProcess as fakeEncoder, startRunOn } from "./helpers/encode-run.js";
15
- import assert from "node:assert/strict";
16
- import { SourceFile } from "../services/media/SourceFile.js";
17
- import { Timeline } from "../services/encode/output/Timeline.js";
18
- import { mkdtemp, rm } from "node:fs/promises";
19
- import os from "node:os";
20
- import path from "node:path";
21
- import { costKindForSession } from "../services/encode/quality/EncodeCost.js";
22
- import { managerWithOwnStore } from "./helpers/manager.js";
23
- import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
24
- import { computeCutGrid } from "../services/encode/output/cut-grid.js";
25
- import { buildRunCommand, nearestKeyframeAtOrBefore } from "../services/encode/run-command.js";
26
- import { Output } from "../services/encode/output/Output.js";
27
- import { outputSpec } from "./helpers/output-spec.js";
28
- import { qualityStateOf } from "../services/encode/quality/OutputQualityState.js";
29
- import { chooseOutput, chosenOutputOf } from "../services/viewer/choices.js";
30
-
31
- const BASE_ID = "aaaaaaaabbbbcccc";
32
- const VARIANT_ID = "1111111122223333";
33
- const SECOND_VARIANT_ID = "9999999988887777";
34
- /** The one person these checks are about. Every viewer has a name. */
35
- const VIEWER = "viewer";
36
-
37
- /**
38
- * The rule has chosen `key` for this viewer at this height. It used to be one
39
- * record per file and height for everybody (`OutputCatalog.servedBy`); it is
40
- * the viewer's own now (roadmap item 97, step 11), so the viewer is registered
41
- * first — a choice kept for nobody is lost.
42
- *
43
- * @param {object} manager
44
- * @param {object} base
45
- * @param {number} height
46
- * @param {string} key
47
- * @returns {void}
48
- */
49
- function chooseFor(manager, base, height, key) {
50
- manager.viewers.of(base, VIEWER);
51
- chooseOutput(manager.viewers, VIEWER, height, key);
52
- }
53
-
54
- /**
55
- * Ask for a height as the viewer does. A request naming nobody is refused.
56
- *
57
- * @param {object} manager
58
- * @param {object} base
59
- * @param {number} height
60
- * @returns {Promise<object | null>}
61
- */
62
- function askFor(manager, base, height) {
63
- manager.viewers.of(base, VIEWER);
64
- return manager.renditions.resolveVariantSession(BASE_ID, height, -1, VIEWER);
65
- }
66
- const SEGMENT_SECONDS = 4;
67
-
68
- /**
69
- * A session shaped like a live one, without the ffmpeg run behind it.
70
- *
71
- * @param {{ id: string, encodeHeight: number, dirPath: string, transcodeVideo?: boolean, audioOnly?: boolean, audioSeparate?: boolean, audioSourceTrackIndex?: number }} params
72
- * @returns {object}
73
- */
74
- function fakeSession({
75
- id,
76
- encodeHeight,
77
- dirPath,
78
- transcodeVideo = true,
79
- audioOnly = false,
80
- audioSeparate = false,
81
- audioSourceTrackIndex = 0
82
- }) {
83
- return {
84
- id,
85
- get outputKey() { return this.spec.toKey(); },
86
- spec: outputSpec({
87
- transcodeVideo,
88
- audioOnly,
89
- audioSeparate,
90
- audioSourceTrackIndex,
91
- height: encodeHeight
92
- }),
93
- dirPath,
94
- // Where this file is cut, held by the file. A fixture that stated it
95
- // on the session was describing what production no longer does.
96
- timeline: new Timeline({
97
- boundaries: Array.from({ length: 101 }, (_, index) => index * SEGMENT_SECONDS),
98
- cutGrid: "uniform"
99
- }),
100
- state: "ready",
101
- // The file's own facts, which is where the size and what decoding it costs
102
- // both come from: 1080p24 at 8 Mbit/s, the field file of 2026-08-17. A
103
- // fixture that assigned a cost object instead was stating an answer the
104
- // file now derives, so nothing it said reached the arithmetic.
105
- file: new SourceFile({ sourceKey: "source-1", fileIndex: 0, name: "video.mkv" }).learn({
106
- width: 1920,
107
- height: 1080,
108
- fps: 24,
109
- bitrateKbps: 8000
110
- }),
111
- // An ordinary session reads its own file, and its sound is inside it. The
112
- // three differ only for a soundtrack shipped as a file of its own.
113
- get inputFile() { return this.file; },
114
- get audioFile() { return this.file; },
115
- startedAt: Date.now(),
116
- lastAccessedAt: Date.now(),
117
- ffmpeg: null,
118
- lastError: "",
119
- segmentFormat: fmp4Format,
120
- transcodeVideo,
121
- transcodeAudio: true,
122
- audioTrackIndex: 0,
123
- // The shape this output is encoded AS, decided once for the output rather
124
- // than once per session.
125
- output: new Output({ encodeWidth: 0, encodeHeight, outputFps: 24, softwarePreset: null, applyTonemap: false }),
126
- encodeRunGeneration: 0,
127
- failedStartAt: -1,
128
- failedStartCount: 0,
129
- waitEpoch: 0,
130
- usesExplicitCuts: false,
131
- useSyntheticPlaylist: true,
132
- playlistText: "#EXTM3U\n",
133
- segmentCount: 100,
134
- progress: { state: "running", processedSeconds: 0, startPositionSeconds: 0 }
135
- };
136
- }
137
-
138
-
139
- /**
140
- * @param {object} [options] - Passed to the manager: the host's own startup
141
- * measurements, which are read through the encoder in use and are not
142
- * assigned afterwards.
143
- * @returns {Promise<{ manager: object, base: object, dirPath: string }>}
144
- */
145
- async function managerWithBase(options = {}) {
146
- const dirPath = await mkdtemp(path.join(os.tmpdir(), "quality-variants-"));
147
- // Its own store root — see `helpers/manager.js` for what sharing one cost.
148
- const { manager } = managerWithOwnStore(options);
149
- // 812p is what a viewport-sized budget actually produces — deliberately not a
150
- // ladder rung, because that is the case the master has to carry.
151
- const base = fakeSession({ id: BASE_ID, encodeHeight: 812, dirPath });
152
- manager.outputs.set(BASE_ID, base);
153
- manager.encodeCost.loadOfOutput = () => ({ costSec: 0.25, fileKey: "", fileCostSec: 0 });
154
- return { manager, base, dirPath };
155
- }
156
-
157
- function startManagedRun(manager, output, options = {}) {
158
- const run = startRunOn(output, options);
159
- manager.encodeOrchestrator.adopt(output.outputKey, run);
160
- return run;
161
- }
162
-
163
- test("the master offers every rung, the session's own height among them", async (t) => {
164
- const { manager, dirPath } = await managerWithBase();
165
- t.after(async () => {
166
- await manager.lifecycle.disposeAll();
167
- await rm(dirPath, { recursive: true, force: true });
168
- });
169
-
170
- const master = manager.renditions.buildMasterPlaylist(BASE_ID);
171
-
172
- assert.ok(master, "a re-encoded 1080p source has rungs to choose between");
173
- const heights = [...master.matchAll(/^v\/(\d+)\/index\.m3u8$/gm)].map((match) => Number(match[1]));
174
- assert.deepEqual(
175
- heights,
176
- [1080, 812, 720, 540, 480, 360, 240],
177
- "the source height, the height already being encoded, and the rungs below it, largest first"
178
- );
179
- assert.match(master, /^#EXT-X-VERSION:7$/m, "the version the segment format requires");
180
- assert.match(master, /RESOLUTION=1280x720/, "each variant states the size it decodes to");
181
- assert.ok(
182
- !master.includes("2160"),
183
- "a rung above the source would be upscaling — invented detail at a higher cost than the source"
184
- );
185
- });
186
-
187
- test("audio is published once for the file, and every rung points at it", async (t) => {
188
- const { manager, base, dirPath } = await managerWithBase();
189
- t.after(async () => {
190
- await manager.lifecycle.disposeAll();
191
- await rm(dirPath, { recursive: true, force: true });
192
- });
193
- // The inventory the plan already probed — the same list the browser's audio
194
- // menu is built from.
195
- manager.getCachedAudioTracks = () => [
196
- { index: 0, language: "rus", title: "Дубляж", isDefault: true },
197
- { index: 1, language: "eng", title: "", isDefault: false }
198
- ];
199
- // Settled at creation in production; set here directly, since this test
200
- // builds its session by hand.
201
- base.spec = outputSpec({ transcodeVideo: true, audioSeparate: true, height: base.output.encodeHeight });
202
- manager.viewers.of(base, VIEWER).audio = { trackIndex: 1, transcode: true };
203
-
204
- // ASKED FOR BY THE VIEWER WHOSE CHOICE IT IS. Which soundtrack a master marks
205
- // DEFAULT is a fact of the person the master is being built for — the route
206
- // passes the name of whoever asked — so a nameless request is answered by the
207
- // session's own parameters instead, which is another track entirely.
208
- const master = manager.renditions.buildMasterPlaylist(BASE_ID, VIEWER);
209
-
210
- assert.match(
211
- master,
212
- /#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud",NAME="Дубляж",LANGUAGE="ru",AUTOSELECT=YES,DEFAULT=NO,URI="a\/0\/index\.m3u8"/,
213
- "a track with a title is named by it"
214
- );
215
- assert.match(
216
- master,
217
- /#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud",NAME="eng",LANGUAGE="en",AUTOSELECT=YES,DEFAULT=YES,URI="a\/1\/index\.m3u8"/,
218
- "the track the session was created with is the default one, and an untitled track is named by its language"
219
- );
220
- const streams = [...master.matchAll(/^#EXT-X-STREAM-INF:.*$/gm)].map((match) => match[0]);
221
- assert.equal(streams.length, 7, "every rung");
222
- assert.ok(
223
- streams.every((line) => line.includes('AUDIO="aud"')),
224
- "each rung plays with the shared audio rather than carrying its own"
225
- );
226
- });
227
-
228
- test("a session that did not ask for renditions gets audio in its stream, as before", async (t) => {
229
- const { manager, dirPath } = await managerWithBase();
230
- t.after(async () => {
231
- await manager.lifecycle.disposeAll();
232
- await rm(dirPath, { recursive: true, force: true });
233
- });
234
- manager.getCachedAudioTracks = () => [{ index: 0, language: "rus", title: "", isDefault: true }];
235
-
236
- const master = manager.renditions.buildMasterPlaylist(BASE_ID);
237
-
238
- assert.ok(!master.includes("EXT-X-MEDIA"), "a browser that does not know about renditions is not sent any");
239
- assert.ok(!master.includes("AUDIO="), "and its rungs still carry their own audio");
240
- });
241
-
242
- test("a copied video is offered variants when its cut grid is real", async (t) => {
243
- const { manager, base, dirPath } = await managerWithBase();
244
- t.after(async () => {
245
- await manager.lifecycle.disposeAll();
246
- await rm(dirPath, { recursive: true, force: true });
247
- });
248
- // A copy is cut at the source's own keyframes — it has no other choice. A
249
- // re-encoded rung CAN be cut there too, by being told those times, and then
250
- // its segments cover the same spans and can stand in the copy's place.
251
- base.spec = outputSpec({ transcodeVideo: false, cutGrid: "keyframe" });
252
- base.timeline = new Timeline({ boundaries: base.timeline?.boundaries ?? [], cutGrid: "keyframe" });
253
-
254
- const master = manager.renditions.buildMasterPlaylist(BASE_ID);
255
-
256
- assert.ok(master, "the obstacle was never the encoder, it was the cut points");
257
- assert.match(master, /^v\/1080\/index\.m3u8$/m, "the copy itself is the top rung — no encoder, no cost");
258
- assert.match(master, /^v\/540\/index\.m3u8$/m);
259
- });
260
-
261
- test("a copied video with no readable keyframe index is offered nothing", async (t) => {
262
- const { manager, base, dirPath } = await managerWithBase();
263
- t.after(async () => {
264
- await manager.lifecycle.disposeAll();
265
- await rm(dirPath, { recursive: true, force: true });
266
- });
267
- // Its playlist claims an even grid that ffmpeg does not cut on. Aligning a
268
- // rung to that is aligning it to a fiction.
269
- base.spec = outputSpec({ transcodeVideo: false, cutGrid: "uniform" });
270
- base.timeline = new Timeline({ boundaries: base.timeline?.boundaries ?? [], cutGrid: "uniform" });
271
-
272
- assert.equal(manager.renditions.buildMasterPlaylist(BASE_ID), null);
273
- });
274
-
275
- test("the session's own height resolves to the session itself", async (t) => {
276
- const { manager, dirPath } = await managerWithBase();
277
- t.after(async () => {
278
- await manager.lifecycle.disposeAll();
279
- await rm(dirPath, { recursive: true, force: true });
280
- });
281
-
282
- assert.deepEqual(
283
- await manager.renditions.resolveVariantFile(BASE_ID, 812, "segment-00000.mp4", VIEWER),
284
- { sessionId: BASE_ID },
285
- "an encoder is already producing this height; making a second one would be a cold start for nothing"
286
- );
287
- });
288
-
289
- test("a height the master does not offer is refused", async (t) => {
290
- const { manager, dirPath } = await managerWithBase();
291
- t.after(async () => {
292
- await manager.lifecycle.disposeAll();
293
- await rm(dirPath, { recursive: true, force: true });
294
- });
295
-
296
- assert.deepEqual(
297
- await manager.renditions.resolveVariantFile(BASE_ID, 999, "index.m3u8", VIEWER),
298
- { sessionId: null },
299
- "honouring an arbitrary height would let a client start encoder runs at will"
300
- );
301
- assert.deepEqual(
302
- await manager.renditions.resolveVariantFile(BASE_ID, 540, "master.m3u8", VIEWER),
303
- { sessionId: null },
304
- "a master under a variant would describe variants of a variant"
305
- );
306
- });
307
-
308
- test("a variant's playlist is answered without starting an encoder for it", async (t) => {
309
- const { manager, dirPath } = await managerWithBase();
310
- t.after(async () => {
311
- await manager.lifecycle.disposeAll();
312
- await rm(dirPath, { recursive: true, force: true });
313
- });
314
-
315
- const resolved = await manager.renditions.resolveVariantFile(BASE_ID, 540, "index.m3u8", VIEWER);
316
-
317
- assert.deepEqual(
318
- resolved,
319
- { sessionId: BASE_ID },
320
- "every variant of a file has the same media playlist — that is what makes them interchangeable"
321
- );
322
- assert.equal(
323
- chosenOutputOf(manager.viewers, VIEWER, 540),
324
- "",
325
- "the player fetches a level's playlist to decide with, and may never switch to it"
326
- );
327
- });
328
-
329
- test("the rung the page says it plays is the viewer's rung, and the encoder goes with it", async (t) => {
330
- const { manager, base, dirPath } = await managerWithBase();
331
- t.after(async () => {
332
- await manager.lifecycle.disposeAll();
333
- await rm(dirPath, { recursive: true, force: true });
334
- });
335
- manager.encodeRuns.planEncodersSoon = () => {};
336
- const variant = fakeSession({ id: VARIANT_ID, encodeHeight: 540, dirPath });
337
- variant.variantHeight = 540;
338
- // A step of the picture: made as one, same file, and a height of its own.
339
- variant.isStep = true;
340
- manager.outputs.markStep(variant);
341
- manager.outputs.set(VARIANT_ID, variant);
342
- chooseFor(manager, base, 540, variant.outputKey);
343
- // The viewer is a hundred seconds in, and the base is the one encoding.
344
- base.lastRequestedSegment = 25;
345
- const encoder = fakeEncoder();
346
- startManagedRun(manager, base, { process: encoder });
347
- const served = await manager.renditions.resolveVariantFile(BASE_ID, 540, "segment-00025.mp4", VIEWER);
348
- assert.equal(served.sessionId, VARIANT_ID, "the file must be served from the variant, not the base");
349
- assert.equal(
350
- manager.viewers.of(base, VIEWER).activeVariantId ?? null,
351
- null,
352
- "a request for a rung's segment is the player fetching, not the person having moved"
353
- );
354
-
355
- // The page, the moment the player has switched: this rung, and the picture at 100 s.
356
- assert.equal(manager.renditions.viewerPlays(BASE_ID, VIEWER, 540, 100), true);
357
-
358
- assert.equal(manager.viewers.of(base, VIEWER).activeVariantId, VARIANT_ID, "the variant the viewer is watching is the active one");
359
- assert.ok(
360
- Math.abs((manager.viewers.of(variant, VIEWER).positionSeconds() ?? -1) - 100) < 1,
361
- "where this person stands on the rung is where their picture is"
362
- );
363
- // AND THE RUNG THEY CAME OFF IS PRODUCING FOR NOBODY, which is one fact and
364
- // not an act. It used to be stopped from this path by hand, and the plan —
365
- // handed the whole film's priority map — started it again on the very next
366
- // pass, which this viewer's own move had just triggered.
367
- assert.equal(
368
- manager.outputs.supersededBy(base, manager.viewers.of(base, VIEWER).activeVariantId),
369
- true,
370
- "the picture they stepped off is nobody's now"
371
- );
372
- assert.equal(
373
- manager.outputs.supersededBy(variant, manager.viewers.of(variant, VIEWER).activeVariantId),
374
- false,
375
- "and the rung they moved to is theirs"
376
- );
377
- assert.deepEqual(encoder.signals, [], "stopping it is the plan's, from that fact, and not this path's");
378
- });
379
-
380
- test("a rung is placed where the picture is, not where the other rung had read to", async (t) => {
381
- const { manager, base, dirPath } = await managerWithBase();
382
- t.after(async () => {
383
- await manager.lifecycle.disposeAll();
384
- await rm(dirPath, { recursive: true, force: true });
385
- });
386
- const variant = fakeSession({ id: VARIANT_ID, encodeHeight: 540, dirPath });
387
- variant.variantHeight = 540;
388
- // A step of the picture: made as one, same file, and a height of its own.
389
- variant.isStep = true;
390
- manager.outputs.markStep(variant);
391
- manager.outputs.set(VARIANT_ID, variant);
392
- chooseFor(manager, base, 540, variant.outputKey);
393
- startManagedRun(manager, base, { process: fakeEncoder() });
394
- // The rung being left had read fourteen segments further than the picture had
395
- // played — an encoder running at several times realtime fills the buffer far
396
- // ahead. Measured 2026-08-11: 56 s of gap, and using the read head placed the
397
- // new run past everything the player then asked for, which no request could
398
- // ever be answered from.
399
- base.lastRequestedSegment = 70;
400
-
401
- await manager.renditions.resolveVariantFile(BASE_ID, 540, "segment-00056.mp4", VIEWER);
402
- manager.renditions.viewerPlays(BASE_ID, VIEWER, 540, 224);
403
-
404
- assert.equal(
405
- manager.viewers.of(variant, VIEWER).positionSeconds(),
406
- 224,
407
- "where the page says the picture is — 224 s, segment 56 on a four-second grid — " +
408
- "not the read head fourteen segments further on"
409
- );
410
- });
411
-
412
- test("warming a rung prepares it without taking the encoder from the one on screen", async (t) => {
413
- const { manager, base, dirPath } = await managerWithBase();
414
- t.after(async () => {
415
- await manager.lifecycle.disposeAll();
416
- await rm(dirPath, { recursive: true, force: true });
417
- });
418
- manager.encodeRuns.planEncodersSoon = () => {};
419
- const variant = fakeSession({ id: VARIANT_ID, encodeHeight: 540, dirPath });
420
- variant.variantHeight = 540;
421
- // A step of the picture: made as one, same file, and a height of its own.
422
- variant.isStep = true;
423
- manager.outputs.markStep(variant);
424
- manager.outputs.set(VARIANT_ID, variant);
425
- chooseFor(manager, base, 540, variant.outputKey);
426
- const encoder = fakeEncoder();
427
- startManagedRun(manager, base, { process: encoder });
428
- const prepared = await manager.renditions.prepareVariant(BASE_ID, 540, 240, VIEWER);
429
-
430
- assert.deepEqual(
431
- prepared,
432
- { sessionId: VARIANT_ID, fileName: "segment-00060.mp4" },
433
- "the caller is told which segment to wait for — 240 s on a four-second grid"
434
- );
435
- assert.equal(
436
- manager.viewers.of(variant, VIEWER).positionSeconds(),
437
- 240,
438
- "the rung being warmed is told where this person is, which is what buys it an encoder"
439
- );
440
- assert.equal(manager.viewers.of(base, VIEWER).activeVariantId, null, "nothing has switched yet");
441
- assert.equal([...base.runs][0]?.process, encoder, "the picture on screen keeps its encoder until the player actually moves");
442
- assert.deepEqual(encoder.signals, [], "stopping it here is what would put the spinner back");
443
- });
444
-
445
- test("a rung warmed at the playhead survives the switch that lands just ahead of it", async (t) => {
446
- const { manager, base, dirPath } = await managerWithBase();
447
- t.after(async () => {
448
- await manager.lifecycle.disposeAll();
449
- await rm(dirPath, { recursive: true, force: true });
450
- });
451
- const variant = fakeSession({ id: VARIANT_ID, encodeHeight: 540, dirPath });
452
- variant.variantHeight = 540;
453
- // A step of the picture: made as one, same file, and a height of its own.
454
- variant.isStep = true;
455
- manager.outputs.markStep(variant);
456
- manager.outputs.set(VARIANT_ID, variant);
457
- chooseFor(manager, base, 540, variant.outputKey);
458
- startManagedRun(manager, base, { process: fakeEncoder() });
459
- // Warmed AT THE PLAYHEAD (240 s = segment #60), which is what the browser
460
- // sends from server 0.10.0 onwards, and the run is alive and has produced a
461
- // few segments past it.
462
- await manager.renditions.prepareVariant(BASE_ID, 540, 240, VIEWER);
463
-
464
- startManagedRun(manager, variant, { from: 59, process: fakeEncoder() });
465
- variant.progress = { ...variant.progress, processedSeconds: 268 };
466
- variant.seekTarget = null;
467
- variant.seekSettleTimer = null;
468
-
469
- // hls.js flushes from the fragment after the one holding
470
- // `currentTime + fetchdelay`, so its first request for the new rung is the
471
- // playhead plus up to one fragment — here #61 against a run that began at
472
- // #59. Warming at the END OF THE BUFFER instead put the run tens of seconds
473
- // AHEAD of this request, which the proxy then read as a seek backwards:
474
- // measured 2026-08-14, that killed a run holding 21.8 s of encoded output.
475
- await manager.renditions.resolveVariantFile(BASE_ID, 540, "segment-00061.mp4", VIEWER);
476
- manager.renditions.viewerPlays(BASE_ID, VIEWER, 540, 244);
477
-
478
- assert.equal(manager.viewers.of(base, VIEWER).activeVariantId, VARIANT_ID, "the viewer has moved to this rung");
479
- assert.equal(
480
- variant.seekTarget,
481
- null,
482
- "the request is inside the warmed run, so nothing is repositioned and the warm-up is kept"
483
- );
484
- assert.equal([...variant.runs][0].from, 59, "the run still begins where it was warmed");
485
- });
486
-
487
- test("a rung warmed PAST the switch is repositioned, which is what warming late costs", async (t) => {
488
- const { manager, base, dirPath } = await managerWithBase();
489
- t.after(async () => {
490
- await manager.lifecycle.disposeAll();
491
- await rm(dirPath, { recursive: true, force: true });
492
- });
493
- const variant = fakeSession({ id: VARIANT_ID, encodeHeight: 540, dirPath });
494
- variant.variantHeight = 540;
495
- // A step of the picture: made as one, same file, and a height of its own.
496
- variant.isStep = true;
497
- manager.outputs.markStep(variant);
498
- manager.outputs.set(VARIANT_ID, variant);
499
- chooseFor(manager, base, 540, variant.outputKey);
500
- startManagedRun(manager, base, { process: fakeEncoder() });
501
- // The same session, warmed where the BUFFER ended rather than where the
502
- // picture was — 60 s further on, which is an ordinary cushion. This is what
503
- // server 0.9.3 sent and 0.11.0 stopped sending.
504
- await manager.renditions.prepareVariant(BASE_ID, 540, 300, VIEWER);
505
- startManagedRun(manager, variant, { from: 74, process: fakeEncoder() });
506
- variant.progress = { ...variant.progress, processedSeconds: 310 };
507
-
508
- // hls.js still lands near the playhead, so the request is far BEHIND the
509
- // warmed run: the proxy reads it as a seek backwards and starts again, and
510
- // everything the warm-up produced is thrown away. Measured in the field
511
- // 2026-08-14 as 21.8 s of encoded output destroyed by the act of using it.
512
- await manager.renditions.resolveVariantFile(BASE_ID, 540, "segment-00061.mp4", VIEWER);
513
- manager.renditions.viewerPlays(BASE_ID, VIEWER, 540, 244);
514
-
515
- assert.equal(
516
- manager.viewers.of(variant, VIEWER).positionSeconds(),
517
- 244,
518
- "where the picture is, not where the warm-up had run to"
519
- );
520
- });
521
-
522
- test("the rung on screen fetching its own segments does not cancel a warm-up", async (t) => {
523
- const { manager, base, dirPath } = await managerWithBase();
524
- t.after(async () => {
525
- await manager.lifecycle.disposeAll();
526
- await rm(dirPath, { recursive: true, force: true });
527
- });
528
- manager.encodeRuns.planEncodersSoon = () => {};
529
- const variant = fakeSession({ id: VARIANT_ID, encodeHeight: 540, dirPath });
530
- variant.variantHeight = 540;
531
- // A step of the picture: made as one, same file, and a height of its own.
532
- variant.isStep = true;
533
- manager.outputs.markStep(variant);
534
- manager.outputs.set(VARIANT_ID, variant);
535
- chooseFor(manager, base, 540, variant.outputKey);
536
- const warmedEncoder = fakeEncoder();
537
- startManagedRun(manager, variant, { process: warmedEncoder });
538
- startManagedRun(manager, base, { process: fakeEncoder() });
539
- await manager.renditions.prepareVariant(BASE_ID, 540, 100, VIEWER);
540
-
541
- // The viewer has not moved: the rung they are watching goes on asking for its
542
- // own segments, every few seconds, for as long as they watch.
543
- await manager.renditions.resolveVariantFile(BASE_ID, 812, "segment-00026.mp4", VIEWER);
544
- manager.renditions.viewerPlays(BASE_ID, VIEWER, 812, 104);
545
- await manager.renditions.resolveVariantFile(BASE_ID, 812, "segment-00027.mp4", VIEWER);
546
- manager.renditions.viewerPlays(BASE_ID, VIEWER, 812, 108);
547
-
548
- // Kept per viewer.
549
- assert.equal(
550
- manager.viewers.forOutput(base).get(VIEWER)?.warmingVariantId ?? null,
551
- VARIANT_ID,
552
- "the rung being prepared is still being prepared"
553
- );
554
- assert.deepEqual(
555
- warmedEncoder.signals,
556
- [],
557
- "cancelling it here left the viewer waiting out the whole warm-up for a segment nobody was making"
558
- );
559
- });
560
-
561
- test("warming the height the base itself serves still points it at the switch", async (t) => {
562
- const { manager, base, dirPath } = await managerWithBase();
563
- t.after(async () => {
564
- await manager.lifecycle.disposeAll();
565
- await rm(dirPath, { recursive: true, force: true });
566
- });
567
- // The viewer is on another rung; the base is parked where they left it, with
568
- // its encoder stopped. Warming its height must bring it back.
569
- const variant = fakeSession({ id: VARIANT_ID, encodeHeight: 540, dirPath });
570
- variant.variantHeight = 540;
571
- manager.outputs.set(VARIANT_ID, variant);
572
- chooseFor(manager, base, 540, variant.outputKey);
573
- manager.viewers.of(base, VIEWER).activeVariantId = VARIANT_ID;
574
- base.runs = new Set();
575
-
576
- await manager.renditions.prepareVariant(BASE_ID, 812, 400, VIEWER);
577
-
578
- // 400 s falls on the boundary between #99 and #100, and a run starts one
579
- // segment back so the player has the preceding keyframe.
580
- assert.equal(
581
- manager.viewers.of(base, VIEWER).positionSeconds(),
582
- 396,
583
- "the base is parked where they left it, so warming its height says where they are like any other rung"
584
- );
585
- });
586
-
587
- test("the viewer's position is kept current by the segments they ask for", async (t) => {
588
- const { manager, base, dirPath } = await managerWithBase();
589
- t.after(async () => {
590
- await manager.lifecycle.disposeAll();
591
- await rm(dirPath, { recursive: true, force: true });
592
- });
593
- // A REQUEST SAYS NOTHING ABOUT WHERE THEY ARE, and asking for one must not
594
- // move them. Where they are is what they stated — a seek, or a report
595
- // carrying their playhead — and it stands until they state otherwise.
596
- manager.viewers.of(base, VIEWER).moveTo(40);
597
-
598
- await manager.serving.getFileStream(BASE_ID, "segment-00090.mp4", { consumerId: VIEWER });
599
-
600
- assert.ok(
601
- Math.abs((manager.viewers.of(base, VIEWER).positionSeconds() ?? -1) - 40) < 1,
602
- "a request for segment #90 leaves the viewer where they said they were"
603
- );
604
- });
605
-
606
- test("a playlist or an init segment does not move the encoder", async (t) => {
607
- const { manager, base, dirPath } = await managerWithBase();
608
- t.after(async () => {
609
- await manager.lifecycle.disposeAll();
610
- await rm(dirPath, { recursive: true, force: true });
611
- });
612
- const variant = fakeSession({ id: VARIANT_ID, encodeHeight: 540, dirPath });
613
- manager.outputs.set(VARIANT_ID, variant);
614
- chooseFor(manager, base, 540, variant.outputKey);
615
- startManagedRun(manager, base, { process: fakeEncoder() });
616
- manager.encodeRuns.planEncodersSoon = () => {};
617
- chooseFor(manager, base, 540, variant.outputKey);
618
- await manager.renditions.resolveVariantFile(BASE_ID, 540, "index.m3u8", VIEWER);
619
- await manager.renditions.resolveVariantFile(BASE_ID, 540, "init.mp4", VIEWER);
620
-
621
- assert.notEqual(
622
- manager.viewers.of(base, VIEWER).activeVariantId,
623
- VARIANT_ID,
624
- "hls.js fetches a level's playlist and init to decide with, and may never switch to it"
625
- );
626
- assert.ok([...base.runs][0]?.process, "the stream on screen must keep its encoder while the player is only looking");
627
- });
628
-
629
- test("the name of a variant is fixed, whatever its encode is later set to", async (t) => {
630
- const { manager, base, dirPath } = await managerWithBase();
631
- t.after(async () => {
632
- await manager.lifecycle.disposeAll();
633
- await rm(dirPath, { recursive: true, force: true });
634
- });
635
- // The player fetched the master once and addresses this variant as 812p for
636
- // the rest of the session, so the name must not follow the encode. Nothing in
637
- // the proxy changes `encodeHeight` mid-session any more — a change of size is
638
- // a change of variant now — but the name and the encode are still two
639
- // different things, and the addressing depends on their staying so.
640
- assert.equal(manager.outputs.variantHeightOf(base), 812);
641
- base.encodeHeight = 540;
642
-
643
- assert.equal(
644
- manager.outputs.variantHeightOf(base),
645
- 812,
646
- "the name stays; renaming it would leave the player addressing a variant nobody answers for"
647
- );
648
- assert.deepEqual(
649
- await manager.renditions.resolveVariantFile(BASE_ID, 812, "segment-00000.mp4", VIEWER),
650
- { sessionId: BASE_ID },
651
- "a second session at a height the host has already failed to manage is the opposite of what a step is for"
652
- );
653
- });
654
-
655
- test("the cut grid follows the grid asked for, not who produces the frames", () => {
656
- // Why a segment from one encoder can stand where another's would have: both
657
- // are cut at the same times. Which times is a property of the SESSION — the
658
- // even grid, or the source's own keyframes — and it must not be re-derived
659
- // from whether the video is copied, because a variant of a copied stream is
660
- // re-encoded and still has to land on the copy's cuts.
661
- const shape = { durationSeconds: 100, segDur: SEGMENT_SECONDS, startTime: 0 };
662
- const keyframeTimes = [0, 3.1, 9.7, 14.2, 21.5, 40, 61.25];
663
-
664
- const even = computeCutGrid({ ...shape, useKeyframeGrid: false, keyframeTimes });
665
- assert.equal(even.boundaries[1], SEGMENT_SECONDS, "the even grid ignores the source's keyframes");
666
- assert.equal(even.boundaries.at(-1), 100);
667
-
668
- const source = computeCutGrid({ ...shape, useKeyframeGrid: true, keyframeTimes });
669
- assert.deepEqual(
670
- source.boundaries,
671
- [0, 9.7, 14.2, 21.5, 40, 61.25, 100],
672
- "the source's own keyframes, kept only where they are at least a segment apart"
673
- );
674
-
675
- // The one that matters: a copy and a re-encoded rung of it, given the same
676
- // grid, produce the SAME table. Segment N then covers the same span in both,
677
- // which is what lets one stand where the other would have.
678
- assert.deepEqual(
679
- computeCutGrid({ ...shape, useKeyframeGrid: true, keyframeTimes }).boundaries,
680
- source.boundaries,
681
- "a variant inherits the grid, so its boundaries are the same values"
682
- );
683
- // And with no index there is nothing to align to — the even grid, whoever asks.
684
- assert.deepEqual(
685
- computeCutGrid({ ...shape, useKeyframeGrid: true, keyframeTimes: null }).boundaries,
686
- even.boundaries,
687
- "no keyframes means no keyframe grid, however the caller asks"
688
- );
689
- });
690
-
691
- test("a boundary carries the keyframe it is, exactly as the container stated it", () => {
692
- // The fault that stopped a viewing on 2026-09-05. A boundary is kept on the
693
- // player's clock, `keyframe - startTime`, rounded; the seek used to add the
694
- // start time back and look the result up in the container's list by value.
695
- // The round trip is lossy — 26.234 - 0.083 + 0.083 is 26.233999999999998 —
696
- // and "the keyframe at or before that" is then the PREVIOUS one, a whole
697
- // keyframe interval earlier.
698
- const startTime = 0.083;
699
- const keyframeTimes = [];
700
- for (let index = 0; index < 60; index += 1) {
701
- keyframeTimes.push(Number((startTime + index * 8.717).toFixed(3)));
702
- }
703
- const grid = computeCutGrid({
704
- useKeyframeGrid: true, durationSeconds: 500, segDur: SEGMENT_SECONDS, keyframeTimes, startTime
705
- });
706
-
707
- assert.equal(grid.sourceTimes[0], startTime, "the first cut is the start of the file");
708
- assert.equal(grid.boundaries.length, grid.sourceTimes.length, "an index must name both clocks");
709
- for (let index = 1; index < grid.boundaries.length - 1; index += 1) {
710
- assert.ok(
711
- keyframeTimes.includes(grid.sourceTimes[index]),
712
- `cut #${index} carries ${grid.sourceTimes[index]}, which the container never stated`
713
- );
714
- }
715
-
716
- // What the round trip does, so the reason this table exists cannot be
717
- // mistaken for caution: at least one boundary fails to find itself.
718
- const lost = grid.boundaries
719
- .slice(1, -1)
720
- .filter((published, at) => nearestKeyframeAtOrBefore(keyframeTimes, published + startTime)
721
- !== grid.sourceTimes[at + 1]);
722
- assert.ok(
723
- lost.length > 0,
724
- "the search agreed everywhere, so this fixture no longer reproduces the fault it was built for"
725
- );
726
- });
727
-
728
- test("a run on the keyframe grid is given no trim to apply", () => {
729
- // The second half of the same fault. That trim is an output-side `-ss`, and
730
- // beside `-copyts` it moves the whole run backwards by its own amount:
731
- // measured 2026-09-06 on a 5 s keyframe interval, a run landing at 15 s and
732
- // asked to trim to the cut at 20 s produced its first file starting at 10 s.
733
- // Every cut of the run inherits it while the numbering, fixed at spawn, does
734
- // not — so the files are named for times they do not hold.
735
- const startTime = 0.083;
736
- const keyframeTimes = [];
737
- for (let index = 0; index < 60; index += 1) {
738
- keyframeTimes.push(Number((startTime + index * 8.717).toFixed(3)));
739
- }
740
- const grid = computeCutGrid({
741
- useKeyframeGrid: true, durationSeconds: 500, segDur: SEGMENT_SECONDS, keyframeTimes, startTime
742
- });
743
- const timeline = new Timeline({
744
- boundaries: grid.boundaries, sourceTimes: grid.sourceTimes, cutGrid: "keyframe"
745
- });
746
-
747
- for (let index = 1; index < grid.boundaries.length - 1; index += 1) {
748
- const { args } = buildRunCommand({
749
- keyframes: { times: keyframeTimes },
750
- inputFile: { startTime },
751
- audioFile: { startTime },
752
- inputUrl: "http://127.0.0.1/stream",
753
- audioInputUrl: "",
754
- outputDir: ".",
755
- timeline,
756
- segmentFormat: fmp4Format,
757
- transcodeVideo: false,
758
- transcodeAudio: true,
759
- audioOnly: false,
760
- audioSeparate: false,
761
- audioSourceTrackIndex: 0,
762
- rateControl: null,
763
- startIndex: index,
764
- endIndex: index,
765
- videoEncoder: { name: "libx264" },
766
- segmentDurationSec: SEGMENT_SECONDS
767
- });
768
- // One input seek establishes decode order in the preceding private cut.
769
- assert.equal(
770
- args.filter((one) => one === "-ss").length,
771
- 1,
772
- `run at #${index} was given a trim beside -copyts`
773
- );
774
- const seek = Number(args[args.indexOf("-ss") + 1]);
775
- assert.ok(
776
- Math.abs(seek - grid.sourceTimes[index - 1]) < 0.2,
777
- `run at #${index} seeks to ${seek}, not to the preceding keyframe ${grid.sourceTimes[index - 1]}`
778
- );
779
- assert.equal(Number(args[args.indexOf("-segment_start_number") + 1]), index - 1);
780
- }
781
- });
782
-
783
- test("a rung served by copy stays offered while a re-encoded rung is on screen", async (t) => {
784
- // Enough to re-encode 240p (1.67x combined) and nowhere near enough for
785
- // 720p (0.45x) — the field's own shape, where the rung the viewer picked was
786
- // offered and everything between it and the copy was not.
787
- const { manager, base, dirPath } = await managerWithBase({
788
- softwarePresetBenchmark: [{ preset: "ultrafast", pixelsPerSec: 12_000_000 }],
789
- decodeCostModel: { pixelTerm: 0.00793, bitrateTerm: 0, constantTerm: 0 }
790
- });
791
- t.after(async () => {
792
- await manager.lifecycle.disposeAll();
793
- await rm(dirPath, { recursive: true, force: true });
794
- });
795
- // The field case of 2026-08-15: a 1080p source served by COPY, the viewer on
796
- // 240p, and a host too weak to re-encode anything above it.
797
- base.spec = outputSpec({ transcodeVideo: false, cutGrid: "keyframe" });
798
- base.encodeHeight = 1080;
799
- base.variantHeight = 1080;
800
- // What decoding this source costs comes from the file's own facts, stated in
801
- // the fixture: 49.766 Mpx/s at 8 Mbit/s.
802
- assert.ok(base.file.decode, "the fixture must state enough for a decode cost to exist");
803
-
804
- const watching = fakeSession({ id: VARIANT_ID, encodeHeight: 240, dirPath });
805
- watching.variantHeight = 240;
806
- // The rung knows the source as well as the base does, because it IS the same
807
- // file. Without that it prices nothing at all — every height comes back
808
- // "sustainable" for want of a measurement — and the assertion below would
809
- // hold for the wrong reason.
810
- watching.file = base.file;
811
- // A step of the picture: same file, and made as a step.
812
- watching.isStep = true;
813
- manager.outputs.markStep(watching);
814
- manager.outputs.set(VARIANT_ID, watching);
815
- chooseFor(manager, base, 240, watching.outputKey);
816
- manager.viewers.of(base, VIEWER).activeVariantId = VARIANT_ID;
817
-
818
- const offered = manager.quality.offeredHeights(watching);
819
-
820
- assert.ok(
821
- offered.includes(1080),
822
- "the height the source is COPIED at costs no encoder, so no measurement of this host can withdraw it"
823
- );
824
- assert.deepEqual(
825
- offered,
826
- manager.quality.offeredHeights(base),
827
- "one answer for the family: a rung asked while watching another must not disagree with the base"
828
- );
829
- });
830
-
831
- test("a separately published audio track starts where the picture is, from the reported buffer", async (t) => {
832
- const { manager, base, dirPath } = await managerWithBase();
833
- t.after(async () => {
834
- await manager.lifecycle.disposeAll();
835
- await rm(dirPath, { recursive: true, force: true });
836
- });
837
- base.spec = outputSpec({ transcodeVideo: true, audioSeparate: true, height: base.output.encodeHeight });
838
- manager.viewers.of(base, VIEWER).audio = { trackIndex: 1, transcode: true };
839
- // The viewer says where their picture is: 100 s. That, less a segment of
840
- // margin, is where the track has to begin — no subtraction of anything,
841
- // because the position is the playhead and not the edge of a buffer.
842
- manager.viewers.of(base, "viewer").moveTo(100);
843
- manager.viewers.of(base, "viewer").playing = false;
844
- manager.getCachedAudioTracks = () => [
845
- { index: 0, language: "rus", title: "", isDefault: true },
846
- { index: 1, language: "eng", title: "", isDefault: false }
847
- ];
848
- const created = [];
849
- manager.viewerRequests.createOrGetSession = async (params) => {
850
- created.push(params);
851
- // The output itself, which is what `createOrGetSession` answers, carrying
852
- // the track it was asked for.
853
- return fakeSession({
854
- id: VARIANT_ID,
855
- encodeHeight: 0,
856
- dirPath,
857
- audioOnly: true,
858
- audioSourceTrackIndex: params.audioTrackIndex
859
- });
860
- };
861
-
862
- await manager.renditions.resolveAudioRenditionFile(BASE_ID, 1, "segment-00010.mp4", VIEWER);
863
-
864
- assert.equal(created.length, 1, "the track's own session was made");
865
- assert.equal(
866
- created[0].startPositionSeconds,
867
- 96,
868
- "140 s served, less the 40 s the player holds, less one segment of margin"
869
- );
870
- });
871
-
872
- test("with two viewers the audio track starts at the EARLIEST picture, not the read head", async (t) => {
873
- const { manager, base, dirPath } = await managerWithBase();
874
- t.after(async () => {
875
- await manager.lifecycle.disposeAll();
876
- await rm(dirPath, { recursive: true, force: true });
877
- });
878
- base.spec = outputSpec({ transcodeVideo: true, audioSeparate: true, height: base.output.encodeHeight });
879
- // A copied picture is one session shared by both of them, and a track begun
880
- // at the leader has nothing to give the one behind.
881
- manager.viewers.of(base, "ahead").moveTo(100);
882
- manager.viewers.of(base, "ahead").playing = false;
883
- manager.viewers.of(base, "behind").moveTo(40);
884
- manager.viewers.of(base, "behind").playing = false;
885
- manager.getCachedAudioTracks = () => [
886
- { index: 0, language: "rus", title: "", isDefault: true },
887
- { index: 1, language: "eng", title: "", isDefault: false }
888
- ];
889
- const created = [];
890
- manager.viewerRequests.createOrGetSession = async (params) => {
891
- created.push(params);
892
- // The output itself, which is what `createOrGetSession` answers, carrying
893
- // the track it was asked for.
894
- return fakeSession({
895
- id: VARIANT_ID,
896
- encodeHeight: 0,
897
- dirPath,
898
- audioOnly: true,
899
- audioSourceTrackIndex: params.audioTrackIndex
900
- });
901
- };
902
-
903
- await manager.renditions.resolveAudioRenditionFile(BASE_ID, 1, "segment-00010.mp4", VIEWER);
904
-
905
- assert.equal(
906
- created[0].startPositionSeconds,
907
- 36,
908
- "the viewer at 40 s, less one segment of margin — a run starting at the leader " +
909
- "has nothing to give the one behind them"
910
- );
911
- });
912
-
913
- test("a soundtrack begins where a viewer says they are, whenever they said it", async (t) => {
914
- const { manager, base, dirPath } = await managerWithBase();
915
- t.after(async () => {
916
- await manager.lifecycle.disposeAll();
917
- await rm(dirPath, { recursive: true, force: true });
918
- });
919
- base.spec = outputSpec({ transcodeVideo: true, audioSeparate: true, height: base.output.encodeHeight });
920
- // TWO RULES WENT WHEN THE QUANTITY STOPPED MEANING TWO THINGS. A position
921
- // "past the read head" had to be clamped, because the read head was a
922
- // request edge and a report could overtake it; and a report older than a few
923
- // seconds had to be discarded, because it was being subtracted from that
924
- // edge. Neither survives: this is where the viewer's PICTURE is, they are the
925
- // only writer of it, and between statements it moves at the rate the film
926
- // moves. An old statement from a stopped viewer is exactly as true as a new
927
- // one.
928
- const viewer = manager.viewers.of(base, "viewer");
929
- viewer.moveTo(900, Date.now() - 600_000);
930
- viewer.playing = false;
931
- manager.getCachedAudioTracks = () => [
932
- { index: 0, language: "rus", title: "", isDefault: true },
933
- { index: 1, language: "eng", title: "", isDefault: false }
934
- ];
935
- const created = [];
936
- manager.viewerRequests.createOrGetSession = async (params) => {
937
- created.push(params);
938
- // The output itself, which is what `createOrGetSession` answers, carrying
939
- // the track it was asked for.
940
- return fakeSession({
941
- id: VARIANT_ID,
942
- encodeHeight: 0,
943
- dirPath,
944
- audioOnly: true,
945
- audioSourceTrackIndex: params.audioTrackIndex
946
- });
947
- };
948
-
949
- await manager.renditions.resolveAudioRenditionFile(BASE_ID, 1, "segment-00010.mp4", VIEWER);
950
-
951
- assert.equal(
952
- created[0].startPositionSeconds,
953
- 896,
954
- "ten minutes of silence from a stopped viewer says they are still at 900 s"
955
- );
956
- });
957
-
958
- test("an audio track is prepared at the position the switch will land on", async (t) => {
959
- const { manager, base, dirPath } = await managerWithBase();
960
- t.after(async () => {
961
- await manager.lifecycle.disposeAll();
962
- await rm(dirPath, { recursive: true, force: true });
963
- });
964
- base.spec = outputSpec({ transcodeVideo: true, audioSeparate: true, height: base.output.encodeHeight });
965
- manager.viewers.of(base, VIEWER).audio = { trackIndex: 1, transcode: true };
966
- manager.getCachedAudioTracks = () => [
967
- { index: 0, language: "rus", title: "", isDefault: true },
968
- { index: 1, language: "eng", title: "", isDefault: false }
969
- ];
970
- const rendition = fakeSession({
971
- id: VARIANT_ID,
972
- encodeHeight: 0,
973
- dirPath,
974
- audioOnly: true,
975
- audioSourceTrackIndex: 1
976
- });
977
- rendition.id = rendition.spec.toName();
978
- // A soundtrack of THIS picture: the same file, published on its own. Found by
979
- // what it is — there is no list of ids to put it on.
980
- rendition.file = base.file;
981
- startManagedRun(manager, rendition, { process: fakeEncoder() });
982
- manager.outputs.set(rendition.id, rendition);
983
-
984
- const prepared = await manager.renditions.prepareAudioTrack(BASE_ID, 1, 240, VIEWER);
985
-
986
- assert.deepEqual(
987
- prepared,
988
- { sessionId: rendition.id, fileName: "segment-00060.mp4" },
989
- "the caller is told which segment to wait for — 240 s on a four-second grid"
990
- );
991
- assert.equal(
992
- manager.viewers.of(rendition, VIEWER).positionSeconds(),
993
- 240,
994
- "and the track is told where this person is, which is what the switch will land on"
995
- );
996
- });
997
-
998
- test("a reading from a soundtrack is priced as one", () => {
999
- // The fault this pins: an audio rendition reached no learner at all. One
1000
- // guard refused renditions a reading, and the only call that would have
1001
- // priced one sat behind a second guard its caller had already made — so a
1002
- // soundtrack ran for the whole film and was charged at nothing, which is the
1003
- // half of roadmap item 6 that was left owing.
1004
- assert.equal(costKindForSession({ spec: outputSpec({ audioOnly: true }) }), "audio");
1005
- assert.equal(
1006
- costKindForSession({ spec: outputSpec({ audioOnly: true, transcodeVideo: true }) }),
1007
- "audio",
1008
- "a rendition carries no picture, whatever the flag it inherited says"
1009
- );
1010
- assert.equal(costKindForSession({ spec: outputSpec({ transcodeVideo: true }) }), "decode");
1011
- assert.equal(costKindForSession({ spec: outputSpec({ transcodeVideo: false }) }), "copy");
1012
- });
1013
-
1014
- test("a quality step being warmed is not refused by its own cost", async (t) => {
1015
- const dirPath = await mkdtemp(path.join(os.tmpdir(), "warm-cost-"));
1016
- // The addon host's own figures, so the arithmetic below is the field's and
1017
- // not an invention. Without a benchmark the check returns every height
1018
- // untouched and a test over it would pass while proving nothing.
1019
- const { manager } = managerWithOwnStore({
1020
- softwarePresetBenchmark: [
1021
- { preset: "fast", pixelsPerSec: 17.0e6 },
1022
- { preset: "ultrafast", pixelsPerSec: 67.5e6 }
1023
- ],
1024
- decodeCostModel: { pixelTerm: 0.007742, bitrateTerm: 0, constantTerm: 0 }
1025
- });
1026
- t.after(async () => {
1027
- await manager.lifecycle.disposeAll();
1028
- await rm(dirPath, { recursive: true, force: true });
1029
- });
1030
- // A copied 1080p picture, and a 240p step warmed beside it for a switch —
1031
- // which is what every quality change does, two encoders on purpose.
1032
- const base = fakeSession({ id: BASE_ID, encodeHeight: 0, dirPath, transcodeVideo: false });
1033
- base.variantHeight = 1080;
1034
- // What this source costs to decode comes from the file's own facts, stated in
1035
- // the fixture: 1080p24 at 8 Mbit/s, the field file. Without them nothing can
1036
- // be priced and every height comes back offerable for want of a measurement,
1037
- // which would make the assertion hold for the wrong reason.
1038
- assert.ok(base.file.decode, "the fixture must state enough for a decode cost to exist");
1039
- const warming = fakeSession({ id: VARIANT_ID, encodeHeight: 240, dirPath });
1040
- warming.variantHeight = 240;
1041
- // Two sessions of ONE file share its facts, which is the whole point of the
1042
- // file being an object: a step warmed beside the picture is not a second file.
1043
- warming.file = base.file;
1044
- chooseFor(manager, base, 240, warming.outputKey);
1045
- // A step of the picture: same file, and a height of its own.
1046
- // Running, and running well: it says of itself that it holds twice realtime,
1047
- // i.e. half a second of work per second of video.
1048
- startManagedRun(manager, warming, { process: fakeEncoder() });
1049
- qualityStateOf(warming).lastAloneSpeed = 2;
1050
- manager.outputs.set(BASE_ID, base);
1051
- manager.outputs.set(VARIANT_ID, warming);
1052
-
1053
- const offered = manager.quality.offeredHeights(base);
1054
-
1055
- assert.ok(
1056
- offered.includes(240),
1057
- "charged its own cost while being judged, the step the viewer just asked for is dropped from the " +
1058
- "offer by the act of warming it — and its next segment 404s on a stream that is playing"
1059
- );
1060
- });
1061
-
1062
- test("the master survives a live offer that has collapsed to one rung", async (t) => {
1063
- // The field case of 2026-08-18, in the smallest form that reproduces it: a
1064
- // host too slow for any re-encoded rung, and a swarm whose interruptions
1065
- // demand far more than realtime. The live offer then holds only the height an
1066
- // encoder is already producing — and until this test existed, that made
1067
- // `buildMasterPlaylist` answer null and the route answer 404 to a session
1068
- // that had just published the address.
1069
- const dirPath = await mkdtemp(path.join(os.tmpdir(), "quality-variants-collapse-"));
1070
- const { manager } = managerWithOwnStore({
1071
- // A megapixel a second: every rung below the source costs more than the
1072
- // machine has.
1073
- softwarePresetBenchmark: [{ preset: "veryfast", pixelsPerSec: 1_000_000 }],
1074
- decodeCostModel: { pixelTerm: 0.01, bitrateTerm: 0, constantTerm: 0 }
1075
- });
1076
- const base = fakeSession({ id: BASE_ID, encodeHeight: 812, dirPath });
1077
- // A thicker source than the fixture's, stated as the file's own bitrate
1078
- // rather than as a cost object the file now derives.
1079
- base.file.learn({ width: 1920, height: 1080, fps: 24, bitrateKbps: 10_000 });
1080
- // What this file's own reader measured: a step must run at eight times
1081
- // realtime to survive this swarm.
1082
- manager.hostLoad.requiredSpeedFor = () => 8;
1083
- manager.outputs.set(BASE_ID, base);
1084
- t.after(async () => {
1085
- await manager.lifecycle.disposeAll();
1086
- await rm(dirPath, { recursive: true, force: true });
1087
- });
1088
-
1089
- assert.deepEqual(
1090
- manager.quality.offeredHeights(base),
1091
- [812],
1092
- "the live judgement is unchanged: nothing but the running height is worth offering"
1093
- );
1094
-
1095
- const master = manager.renditions.buildMasterPlaylist(BASE_ID);
1096
-
1097
- assert.ok(master, "the master is a published document, not a live figure");
1098
- const heights = [...master.matchAll(/^v\/(\d+)\/index\.m3u8$/gm)].map((match) => Number(match[1]));
1099
- assert.deepEqual(
1100
- heights,
1101
- [1080, 812, 720, 540, 480, 360, 240],
1102
- "every rung that can be spliced onto this cut grid stays addressable"
1103
- );
1104
- });
1105
-
1106
- test("two heights that clamp onto one picture share a single encoder", async (t) => {
1107
- const { manager, base, dirPath } = await managerWithBase();
1108
- const spawnedDirs = [];
1109
- t.after(async () => {
1110
- await manager.lifecycle.disposeAll();
1111
- await Promise.all(spawnedDirs.map((dir) => rm(dir, { recursive: true, force: true })));
1112
- await rm(dirPath, { recursive: true, force: true });
1113
- });
1114
-
1115
- // A host that can hold exactly one rung. Whatever height is asked for, the
1116
- // clamp inside createOrGetSession lands the encode on 240p — which is what a
1117
- // CM4 did on 2026-08-28, turning a 360p and a 540p request into two more
1118
- // ffmpeg processes making the same 426x240 picture as the 240p one.
1119
- const madeIds = [VARIANT_ID, SECOND_VARIANT_ID];
1120
- const created = [];
1121
- manager.viewerRequests.createOrGetSession = async (params) => {
1122
- created.push(params);
1123
- // ONE FORMAT, ONE OUTPUT, as `OutputOpening` has it: an output is named by
1124
- // what it carries, so a request clamped onto a picture already made is
1125
- // answered by that output and makes nothing.
1126
- const probe = fakeSession({ id: "probe", encodeHeight: 240, dirPath });
1127
- const existing = [...manager.outputs.values()].find((output) => output.outputKey === probe.outputKey);
1128
- if (existing) {
1129
- return existing;
1130
- }
1131
- const id = madeIds[created.length - 1];
1132
- const variantDir = await mkdtemp(path.join(os.tmpdir(), "quality-variants-clamped-"));
1133
- spawnedDirs.push(variantDir);
1134
- const variant = fakeSession({ id, encodeHeight: 240, dirPath: variantDir });
1135
- manager.outputs.set(id, variant);
1136
- return variant;
1137
- };
1138
-
1139
- const asked360 = await askFor(manager, base, 360);
1140
- const asked540 = await askFor(manager, base, 540);
1141
-
1142
- assert.equal(asked360.id, VARIANT_ID, "the first request made the encoder");
1143
- assert.equal(
1144
- asked540.id,
1145
- VARIANT_ID,
1146
- "and the second is served by it, because it produces the very same picture"
1147
- );
1148
- assert.equal(
1149
- manager.outputs.has(SECOND_VARIANT_ID),
1150
- false,
1151
- "the duplicate was let go as soon as its size was known"
1152
- );
1153
- assert.equal(
1154
- chosenOutputOf(manager.viewers, VIEWER, 540),
1155
- manager.outputs.get(VARIANT_ID)?.outputKey,
1156
- "540p is recorded as answered by the output the machine can hold"
1157
- );
1158
-
1159
- const askedAgain = await askFor(manager, base, 540);
1160
-
1161
- assert.equal(askedAgain.id, VARIANT_ID);
1162
- assert.equal(created.length, 2, "the third request created nothing at all");
1163
- });
1164
-
1165
- test("rungs that really do differ keep their own encoders", async (t) => {
1166
- const { manager, base, dirPath } = await managerWithBase();
1167
- const spawnedDirs = [];
1168
- t.after(async () => {
1169
- await manager.lifecycle.disposeAll();
1170
- await Promise.all(spawnedDirs.map((dir) => rm(dir, { recursive: true, force: true })));
1171
- await rm(dirPath, { recursive: true, force: true });
1172
- });
1173
-
1174
- // A host with room: each request is encoded at the height it named, so
1175
- // nothing may be merged. The sharing above must not become "one encoder for
1176
- // every rung".
1177
- const madeIds = [VARIANT_ID, SECOND_VARIANT_ID];
1178
- const created = [];
1179
- manager.viewerRequests.createOrGetSession = async (params) => {
1180
- const id = madeIds[created.length];
1181
- created.push(params);
1182
- const variantDir = await mkdtemp(path.join(os.tmpdir(), "quality-variants-distinct-"));
1183
- spawnedDirs.push(variantDir);
1184
- const variant = fakeSession({ id, encodeHeight: params.targetHeight, dirPath: variantDir });
1185
- manager.outputs.set(id, variant);
1186
- return variant;
1187
- };
1188
-
1189
- const asked360 = await askFor(manager, base, 360);
1190
- const asked540 = await askFor(manager, base, 540);
1191
-
1192
- assert.equal(asked360.id, VARIANT_ID);
1193
- assert.equal(asked540.id, SECOND_VARIANT_ID, "two different pictures, two encoders");
1194
- assert.equal(chosenOutputOf(manager.viewers, VIEWER, 360), asked360.outputKey);
1195
- assert.equal(chosenOutputOf(manager.viewers, VIEWER, 540), asked540.outputKey);
1196
- });
1197
-
1198
- test("a rung is never served from the COPY, whatever height the copy happens to be", async (t) => {
1199
- const { manager, base, dirPath } = await managerWithBase();
1200
- const spawnedDirs = [];
1201
- t.after(async () => {
1202
- await manager.lifecycle.disposeAll();
1203
- await Promise.all(spawnedDirs.map((dir) => rm(dir, { recursive: true, force: true })));
1204
- await rm(dirPath, { recursive: true, force: true });
1205
- });
1206
-
1207
- // The base is a COPY at 240p — no encoder behind it, and it is the one rung
1208
- // this host can always serve. Handing it to a request for a re-encoded 360p
1209
- // would give away exactly that, and a viewer stranded on a rung the machine
1210
- // cannot hold would have nowhere left to return to.
1211
- base.transcodeVideo = false;
1212
- base.encodeHeight = 240;
1213
- base.variantHeight = 240;
1214
- const created = [];
1215
- manager.viewerRequests.createOrGetSession = async (params) => {
1216
- created.push(params);
1217
- const variantDir = await mkdtemp(path.join(os.tmpdir(), "quality-variants-copy-"));
1218
- spawnedDirs.push(variantDir);
1219
- const variant = fakeSession({ id: VARIANT_ID, encodeHeight: 240, dirPath: variantDir });
1220
- manager.outputs.set(VARIANT_ID, variant);
1221
- return variant;
1222
- };
1223
-
1224
- const asked = await askFor(manager, base, 360);
1225
-
1226
- assert.equal(asked.id, VARIANT_ID, "the re-encoded rung is its own session, not the copy");
1227
- });
1228
-
1229
- test("a file opened at a position starts its sound THERE, not a look-ahead earlier", async (t) => {
1230
- const { manager, base, dirPath } = await managerWithBase();
1231
- t.after(async () => {
1232
- await manager.lifecycle.disposeAll();
1233
- await rm(dirPath, { recursive: true, force: true });
1234
- });
1235
- base.spec = outputSpec({ transcodeVideo: true, audioSeparate: true, height: base.output.encodeHeight });
1236
- // The state at the instant a page is opened at a position: nothing seeked,
1237
- // no segment served, no report from anybody. The read head is then not a
1238
- // request edge — it is where the session was made — and a browser that has
1239
- // just opened holds no buffer at all.
1240
- base.lastRequestedSegment = null;
1241
- manager.viewers.of(base, VIEWER).moveTo(588);
1242
- manager.getCachedAudioTracks = () => [
1243
- { index: 0, language: "rus", title: "", isDefault: true },
1244
- { index: 1, language: "eng", title: "", isDefault: false }
1245
- ];
1246
- const created = [];
1247
- manager.viewerRequests.createOrGetSession = async (params) => {
1248
- created.push(params);
1249
- // The output itself, which is what `createOrGetSession` answers, carrying
1250
- // the track it was asked for.
1251
- return fakeSession({
1252
- id: VARIANT_ID,
1253
- encodeHeight: 0,
1254
- dirPath,
1255
- audioOnly: true,
1256
- audioSourceTrackIndex: params.audioTrackIndex
1257
- });
1258
- };
1259
-
1260
- await manager.renditions.resolveAudioRenditionFile(BASE_ID, 1, "segment-00010.mp4", VIEWER);
1261
-
1262
- // Field 2026-08-31: this answered 460 for a page opened at 588 — the whole
1263
- // 120 s look-ahead subtracted from a buffer that did not exist — and the
1264
- // segment the viewer needed took 38.8 s to appear against the picture's 8.4 s.
1265
- assert.equal(
1266
- created[0].startPositionSeconds,
1267
- 584,
1268
- "where the viewer opened, less one segment of margin, and nothing else"
1269
- );
1270
- });
1
+ /**
2
+ * @file Quality variants: the master playlist, and what happens when the viewer
3
+ * moves between rungs.
4
+ *
5
+ * What makes a mid-stream change of quality possible at all is that every
6
+ * variant is cut at the SAME times, so a segment produced by one encoder can be
7
+ * appended where another encoder's would have gone. The last test here pins
8
+ * that property at its source; the rest cover the wiring that a route reaches —
9
+ * a module test that imports a function directly cannot see a caller that never
10
+ * calls it (2.9.124).
11
+ */
12
+
13
+ import test from "node:test";
14
+ import { fakeProcess as fakeEncoder, startRunOn } from "./helpers/encode-run.js";
15
+ import assert from "node:assert/strict";
16
+ import { SourceFile } from "../services/media/SourceFile.js";
17
+ import { Timeline } from "../services/encode/output/Timeline.js";
18
+ import { mkdtemp, rm } from "node:fs/promises";
19
+ import os from "node:os";
20
+ import path from "node:path";
21
+ import { costKindForSession } from "../services/encode/quality/EncodeCost.js";
22
+ import { managerWithOwnStore } from "./helpers/manager.js";
23
+ import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
24
+ import { computeCutGrid } from "../services/encode/output/cut-grid.js";
25
+ import { buildRunCommand, nearestKeyframeAtOrBefore } from "../services/encode/run-command.js";
26
+ import { Output } from "../services/encode/output/Output.js";
27
+ import { outputSpec } from "./helpers/output-spec.js";
28
+ import { qualityStateOf } from "../services/encode/quality/OutputQualityState.js";
29
+ import { chooseOutput, chosenOutputOf } from "../services/viewer/choices.js";
30
+
31
+ const BASE_ID = "aaaaaaaabbbbcccc";
32
+ const VARIANT_ID = "1111111122223333";
33
+ const SECOND_VARIANT_ID = "9999999988887777";
34
+ /** The one person these checks are about. Every viewer has a name. */
35
+ const VIEWER = "viewer";
36
+
37
+ /**
38
+ * The rule has chosen `key` for this viewer at this height. It used to be one
39
+ * record per file and height for everybody (`OutputCatalog.servedBy`); it is
40
+ * the viewer's own now (roadmap item 97, step 11), so the viewer is registered
41
+ * first — a choice kept for nobody is lost.
42
+ *
43
+ * @param {object} manager
44
+ * @param {object} base
45
+ * @param {number} height
46
+ * @param {string} key
47
+ * @returns {void}
48
+ */
49
+ function chooseFor(manager, base, height, key) {
50
+ manager.viewers.of(base, VIEWER);
51
+ chooseOutput(manager.viewers, VIEWER, height, key);
52
+ }
53
+
54
+ /**
55
+ * Ask for a height as the viewer does. A request naming nobody is refused.
56
+ *
57
+ * @param {object} manager
58
+ * @param {object} base
59
+ * @param {number} height
60
+ * @returns {Promise<object | null>}
61
+ */
62
+ function askFor(manager, base, height) {
63
+ manager.viewers.of(base, VIEWER);
64
+ return manager.renditions.resolveVariantSession(BASE_ID, height, -1, VIEWER);
65
+ }
66
+ const SEGMENT_SECONDS = 4;
67
+
68
+ /**
69
+ * A session shaped like a live one, without the ffmpeg run behind it.
70
+ *
71
+ * @param {{ id: string, encodeHeight: number, dirPath: string, transcodeVideo?: boolean, audioOnly?: boolean, audioSeparate?: boolean, audioSourceTrackIndex?: number }} params
72
+ * @returns {object}
73
+ */
74
+ function fakeSession({
75
+ id,
76
+ encodeHeight,
77
+ dirPath,
78
+ transcodeVideo = true,
79
+ audioOnly = false,
80
+ audioSeparate = false,
81
+ audioSourceTrackIndex = 0
82
+ }) {
83
+ return {
84
+ id,
85
+ get outputKey() { return this.spec.toKey(); },
86
+ spec: outputSpec({
87
+ transcodeVideo,
88
+ audioOnly,
89
+ audioSeparate,
90
+ audioSourceTrackIndex,
91
+ height: encodeHeight
92
+ }),
93
+ dirPath,
94
+ // Where this file is cut, held by the file. A fixture that stated it
95
+ // on the session was describing what production no longer does.
96
+ timeline: new Timeline({
97
+ boundaries: Array.from({ length: 101 }, (_, index) => index * SEGMENT_SECONDS),
98
+ cutGrid: "uniform"
99
+ }),
100
+ state: "ready",
101
+ // The file's own facts, which is where the size and what decoding it costs
102
+ // both come from: 1080p24 at 8 Mbit/s, the field file of 2026-08-17. A
103
+ // fixture that assigned a cost object instead was stating an answer the
104
+ // file now derives, so nothing it said reached the arithmetic.
105
+ file: new SourceFile({ sourceKey: "source-1", fileIndex: 0, name: "video.mkv" }).learn({
106
+ width: 1920,
107
+ height: 1080,
108
+ fps: 24,
109
+ bitrateKbps: 8000
110
+ }),
111
+ // An ordinary session reads its own file, and its sound is inside it. The
112
+ // three differ only for a soundtrack shipped as a file of its own.
113
+ get inputFile() { return this.file; },
114
+ get audioFile() { return this.file; },
115
+ startedAt: Date.now(),
116
+ lastAccessedAt: Date.now(),
117
+ ffmpeg: null,
118
+ lastError: "",
119
+ segmentFormat: fmp4Format,
120
+ transcodeVideo,
121
+ transcodeAudio: true,
122
+ audioTrackIndex: 0,
123
+ // The shape this output is encoded AS, decided once for the output rather
124
+ // than once per session.
125
+ output: new Output({ encodeWidth: 0, encodeHeight, outputFps: 24, softwarePreset: null, applyTonemap: false }),
126
+ encodeRunGeneration: 0,
127
+ failedStartAt: -1,
128
+ failedStartCount: 0,
129
+ waitEpoch: 0,
130
+ usesExplicitCuts: false,
131
+ useSyntheticPlaylist: true,
132
+ playlistText: "#EXTM3U\n",
133
+ segmentCount: 100,
134
+ progress: { state: "running", processedSeconds: 0, startPositionSeconds: 0 }
135
+ };
136
+ }
137
+
138
+
139
+ /**
140
+ * @param {object} [options] - Passed to the manager: the host's own startup
141
+ * measurements, which are read through the encoder in use and are not
142
+ * assigned afterwards.
143
+ * @returns {Promise<{ manager: object, base: object, dirPath: string }>}
144
+ */
145
+ async function managerWithBase(options = {}) {
146
+ const dirPath = await mkdtemp(path.join(os.tmpdir(), "quality-variants-"));
147
+ // Its own store root — see `helpers/manager.js` for what sharing one cost.
148
+ const { manager } = managerWithOwnStore(options);
149
+ // 812p is what a viewport-sized budget actually produces — deliberately not a
150
+ // ladder rung, because that is the case the master has to carry.
151
+ const base = fakeSession({ id: BASE_ID, encodeHeight: 812, dirPath });
152
+ manager.outputs.set(BASE_ID, base);
153
+ manager.encodeCost.loadOfOutput = () => ({ costSec: 0.25, fileKey: "", fileCostSec: 0 });
154
+ return { manager, base, dirPath };
155
+ }
156
+
157
+ function startManagedRun(manager, output, options = {}) {
158
+ const run = startRunOn(output, options);
159
+ manager.encodeOrchestrator.adopt(output.outputKey, run);
160
+ return run;
161
+ }
162
+
163
+ test("the master offers every rung, the session's own height among them", async (t) => {
164
+ const { manager, dirPath } = await managerWithBase();
165
+ t.after(async () => {
166
+ await manager.lifecycle.disposeAll();
167
+ await rm(dirPath, { recursive: true, force: true });
168
+ });
169
+
170
+ const master = manager.renditions.buildMasterPlaylist(BASE_ID);
171
+
172
+ assert.ok(master, "a re-encoded 1080p source has rungs to choose between");
173
+ const heights = [...master.matchAll(/^v\/(\d+)\/index\.m3u8$/gm)].map((match) => Number(match[1]));
174
+ assert.deepEqual(
175
+ heights,
176
+ [1080, 812, 720, 540, 480, 360, 240],
177
+ "the source height, the height already being encoded, and the rungs below it, largest first"
178
+ );
179
+ assert.match(master, /^#EXT-X-VERSION:7$/m, "the version the segment format requires");
180
+ assert.match(master, /RESOLUTION=1280x720/, "each variant states the size it decodes to");
181
+ assert.ok(
182
+ !master.includes("2160"),
183
+ "a rung above the source would be upscaling — invented detail at a higher cost than the source"
184
+ );
185
+ });
186
+
187
+ test("audio is published once for the file, and every rung points at it", async (t) => {
188
+ const { manager, base, dirPath } = await managerWithBase();
189
+ t.after(async () => {
190
+ await manager.lifecycle.disposeAll();
191
+ await rm(dirPath, { recursive: true, force: true });
192
+ });
193
+ // The inventory the plan already probed — the same list the browser's audio
194
+ // menu is built from.
195
+ manager.getCachedAudioTracks = () => [
196
+ { index: 0, language: "rus", title: "Дубляж", isDefault: true },
197
+ { index: 1, language: "eng", title: "", isDefault: false }
198
+ ];
199
+ // Settled at creation in production; set here directly, since this test
200
+ // builds its session by hand.
201
+ base.spec = outputSpec({ transcodeVideo: true, audioSeparate: true, height: base.output.encodeHeight });
202
+ manager.viewers.of(base, VIEWER).audio = { trackIndex: 1, transcode: true };
203
+
204
+ // ASKED FOR BY THE VIEWER WHOSE CHOICE IT IS. Which soundtrack a master marks
205
+ // DEFAULT is a fact of the person the master is being built for — the route
206
+ // passes the name of whoever asked — so a nameless request is answered by the
207
+ // session's own parameters instead, which is another track entirely.
208
+ const master = manager.renditions.buildMasterPlaylist(BASE_ID, VIEWER);
209
+
210
+ assert.match(
211
+ master,
212
+ /#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud",NAME="Дубляж",LANGUAGE="ru",AUTOSELECT=YES,DEFAULT=NO,URI="a\/0\/index\.m3u8"/,
213
+ "a track with a title is named by it"
214
+ );
215
+ assert.match(
216
+ master,
217
+ /#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud",NAME="eng",LANGUAGE="en",AUTOSELECT=YES,DEFAULT=YES,URI="a\/1\/index\.m3u8"/,
218
+ "the track the session was created with is the default one, and an untitled track is named by its language"
219
+ );
220
+ const streams = [...master.matchAll(/^#EXT-X-STREAM-INF:.*$/gm)].map((match) => match[0]);
221
+ assert.equal(streams.length, 7, "every rung");
222
+ assert.ok(
223
+ streams.every((line) => line.includes('AUDIO="aud"')),
224
+ "each rung plays with the shared audio rather than carrying its own"
225
+ );
226
+ });
227
+
228
+ test("a session that did not ask for renditions gets audio in its stream, as before", async (t) => {
229
+ const { manager, dirPath } = await managerWithBase();
230
+ t.after(async () => {
231
+ await manager.lifecycle.disposeAll();
232
+ await rm(dirPath, { recursive: true, force: true });
233
+ });
234
+ manager.getCachedAudioTracks = () => [{ index: 0, language: "rus", title: "", isDefault: true }];
235
+
236
+ const master = manager.renditions.buildMasterPlaylist(BASE_ID);
237
+
238
+ assert.ok(!master.includes("EXT-X-MEDIA"), "a browser that does not know about renditions is not sent any");
239
+ assert.ok(!master.includes("AUDIO="), "and its rungs still carry their own audio");
240
+ });
241
+
242
+ test("a copied video is offered variants when its cut grid is real", async (t) => {
243
+ const { manager, base, dirPath } = await managerWithBase();
244
+ t.after(async () => {
245
+ await manager.lifecycle.disposeAll();
246
+ await rm(dirPath, { recursive: true, force: true });
247
+ });
248
+ // A copy is cut at the source's own keyframes — it has no other choice. A
249
+ // re-encoded rung CAN be cut there too, by being told those times, and then
250
+ // its segments cover the same spans and can stand in the copy's place.
251
+ base.spec = outputSpec({ transcodeVideo: false, cutGrid: "keyframe" });
252
+ base.timeline = new Timeline({ boundaries: base.timeline?.boundaries ?? [], cutGrid: "keyframe" });
253
+
254
+ const master = manager.renditions.buildMasterPlaylist(BASE_ID);
255
+
256
+ assert.ok(master, "the obstacle was never the encoder, it was the cut points");
257
+ assert.match(master, /^v\/1080\/index\.m3u8$/m, "the copy itself is the top rung — no encoder, no cost");
258
+ assert.match(master, /^v\/540\/index\.m3u8$/m);
259
+ });
260
+
261
+ test("a copied video with no readable keyframe index is offered nothing", async (t) => {
262
+ const { manager, base, dirPath } = await managerWithBase();
263
+ t.after(async () => {
264
+ await manager.lifecycle.disposeAll();
265
+ await rm(dirPath, { recursive: true, force: true });
266
+ });
267
+ // Its playlist claims an even grid that ffmpeg does not cut on. Aligning a
268
+ // rung to that is aligning it to a fiction.
269
+ base.spec = outputSpec({ transcodeVideo: false, cutGrid: "uniform" });
270
+ base.timeline = new Timeline({ boundaries: base.timeline?.boundaries ?? [], cutGrid: "uniform" });
271
+
272
+ assert.equal(manager.renditions.buildMasterPlaylist(BASE_ID), null);
273
+ });
274
+
275
+ test("the session's own height resolves to the session itself", async (t) => {
276
+ const { manager, dirPath } = await managerWithBase();
277
+ t.after(async () => {
278
+ await manager.lifecycle.disposeAll();
279
+ await rm(dirPath, { recursive: true, force: true });
280
+ });
281
+
282
+ assert.deepEqual(
283
+ await manager.renditions.resolveVariantFile(BASE_ID, 812, "segment-00000.mp4", VIEWER),
284
+ { sessionId: BASE_ID },
285
+ "an encoder is already producing this height; making a second one would be a cold start for nothing"
286
+ );
287
+ });
288
+
289
+ test("a height the master does not offer is refused", async (t) => {
290
+ const { manager, dirPath } = await managerWithBase();
291
+ t.after(async () => {
292
+ await manager.lifecycle.disposeAll();
293
+ await rm(dirPath, { recursive: true, force: true });
294
+ });
295
+
296
+ assert.deepEqual(
297
+ await manager.renditions.resolveVariantFile(BASE_ID, 999, "index.m3u8", VIEWER),
298
+ { sessionId: null },
299
+ "honouring an arbitrary height would let a client start encoder runs at will"
300
+ );
301
+ assert.deepEqual(
302
+ await manager.renditions.resolveVariantFile(BASE_ID, 540, "master.m3u8", VIEWER),
303
+ { sessionId: null },
304
+ "a master under a variant would describe variants of a variant"
305
+ );
306
+ });
307
+
308
+ test("a variant's playlist is answered without starting an encoder for it", async (t) => {
309
+ const { manager, dirPath } = await managerWithBase();
310
+ t.after(async () => {
311
+ await manager.lifecycle.disposeAll();
312
+ await rm(dirPath, { recursive: true, force: true });
313
+ });
314
+
315
+ const resolved = await manager.renditions.resolveVariantFile(BASE_ID, 540, "index.m3u8", VIEWER);
316
+
317
+ assert.deepEqual(
318
+ resolved,
319
+ { sessionId: BASE_ID },
320
+ "every variant of a file has the same media playlist — that is what makes them interchangeable"
321
+ );
322
+ assert.equal(
323
+ chosenOutputOf(manager.viewers, VIEWER, 540),
324
+ "",
325
+ "the player fetches a level's playlist to decide with, and may never switch to it"
326
+ );
327
+ });
328
+
329
+ test("the rung the page says it plays is the viewer's rung, and the encoder goes with it", async (t) => {
330
+ const { manager, base, dirPath } = await managerWithBase();
331
+ t.after(async () => {
332
+ await manager.lifecycle.disposeAll();
333
+ await rm(dirPath, { recursive: true, force: true });
334
+ });
335
+ manager.encodeRuns.planEncodersSoon = () => {};
336
+ const variant = fakeSession({ id: VARIANT_ID, encodeHeight: 540, dirPath });
337
+ variant.variantHeight = 540;
338
+ // A step of the picture: made as one, same file, and a height of its own.
339
+ variant.isStep = true;
340
+ manager.outputs.markStep(variant);
341
+ manager.outputs.set(VARIANT_ID, variant);
342
+ chooseFor(manager, base, 540, variant.outputKey);
343
+ // The viewer is a hundred seconds in, and the base is the one encoding.
344
+ base.lastRequestedSegment = 25;
345
+ const encoder = fakeEncoder();
346
+ startManagedRun(manager, base, { process: encoder });
347
+ const served = await manager.renditions.resolveVariantFile(BASE_ID, 540, "segment-00025.mp4", VIEWER);
348
+ assert.equal(served.sessionId, VARIANT_ID, "the file must be served from the variant, not the base");
349
+ assert.equal(
350
+ manager.viewers.of(base, VIEWER).activeVariantId ?? null,
351
+ null,
352
+ "a request for a rung's segment is the player fetching, not the person having moved"
353
+ );
354
+
355
+ // The page, the moment the player has switched: this rung, and the picture at 100 s.
356
+ assert.equal(manager.renditions.viewerPlays(BASE_ID, VIEWER, 540, 100), true);
357
+
358
+ assert.equal(manager.viewers.of(base, VIEWER).activeVariantId, VARIANT_ID, "the variant the viewer is watching is the active one");
359
+ assert.ok(
360
+ Math.abs((manager.viewers.of(variant, VIEWER).positionSeconds() ?? -1) - 100) < 1,
361
+ "where this person stands on the rung is where their picture is"
362
+ );
363
+ // AND THE RUNG THEY CAME OFF IS PRODUCING FOR NOBODY, which is one fact and
364
+ // not an act. It used to be stopped from this path by hand, and the plan —
365
+ // handed the whole film's priority map — started it again on the very next
366
+ // pass, which this viewer's own move had just triggered.
367
+ assert.equal(
368
+ manager.outputs.supersededBy(base, manager.viewers.of(base, VIEWER).activeVariantId),
369
+ true,
370
+ "the picture they stepped off is nobody's now"
371
+ );
372
+ assert.equal(
373
+ manager.outputs.supersededBy(variant, manager.viewers.of(variant, VIEWER).activeVariantId),
374
+ false,
375
+ "and the rung they moved to is theirs"
376
+ );
377
+ assert.deepEqual(encoder.signals, [], "stopping it is the plan's, from that fact, and not this path's");
378
+ });
379
+
380
+ test("a rung is placed where the picture is, not where the other rung had read to", async (t) => {
381
+ const { manager, base, dirPath } = await managerWithBase();
382
+ t.after(async () => {
383
+ await manager.lifecycle.disposeAll();
384
+ await rm(dirPath, { recursive: true, force: true });
385
+ });
386
+ const variant = fakeSession({ id: VARIANT_ID, encodeHeight: 540, dirPath });
387
+ variant.variantHeight = 540;
388
+ // A step of the picture: made as one, same file, and a height of its own.
389
+ variant.isStep = true;
390
+ manager.outputs.markStep(variant);
391
+ manager.outputs.set(VARIANT_ID, variant);
392
+ chooseFor(manager, base, 540, variant.outputKey);
393
+ startManagedRun(manager, base, { process: fakeEncoder() });
394
+ // The rung being left had read fourteen segments further than the picture had
395
+ // played — an encoder running at several times realtime fills the buffer far
396
+ // ahead. Measured 2026-08-11: 56 s of gap, and using the read head placed the
397
+ // new run past everything the player then asked for, which no request could
398
+ // ever be answered from.
399
+ base.lastRequestedSegment = 70;
400
+
401
+ await manager.renditions.resolveVariantFile(BASE_ID, 540, "segment-00056.mp4", VIEWER);
402
+ manager.renditions.viewerPlays(BASE_ID, VIEWER, 540, 224);
403
+
404
+ assert.equal(
405
+ manager.viewers.of(variant, VIEWER).positionSeconds(),
406
+ 224,
407
+ "where the page says the picture is — 224 s, segment 56 on a four-second grid — " +
408
+ "not the read head fourteen segments further on"
409
+ );
410
+ });
411
+
412
+ test("warming a rung prepares it without taking the encoder from the one on screen", async (t) => {
413
+ const { manager, base, dirPath } = await managerWithBase();
414
+ t.after(async () => {
415
+ await manager.lifecycle.disposeAll();
416
+ await rm(dirPath, { recursive: true, force: true });
417
+ });
418
+ manager.encodeRuns.planEncodersSoon = () => {};
419
+ const variant = fakeSession({ id: VARIANT_ID, encodeHeight: 540, dirPath });
420
+ variant.variantHeight = 540;
421
+ // A step of the picture: made as one, same file, and a height of its own.
422
+ variant.isStep = true;
423
+ manager.outputs.markStep(variant);
424
+ manager.outputs.set(VARIANT_ID, variant);
425
+ chooseFor(manager, base, 540, variant.outputKey);
426
+ const encoder = fakeEncoder();
427
+ startManagedRun(manager, base, { process: encoder });
428
+ const prepared = await manager.renditions.prepareVariant(BASE_ID, 540, 240, VIEWER);
429
+
430
+ assert.deepEqual(
431
+ prepared,
432
+ { sessionId: VARIANT_ID, fileName: "segment-00060.mp4" },
433
+ "the caller is told which segment to wait for — 240 s on a four-second grid"
434
+ );
435
+ assert.equal(
436
+ manager.viewers.of(variant, VIEWER).positionSeconds(),
437
+ 240,
438
+ "the rung being warmed is told where this person is, which is what buys it an encoder"
439
+ );
440
+ assert.equal(manager.viewers.of(base, VIEWER).activeVariantId, null, "nothing has switched yet");
441
+ assert.equal([...base.runs][0]?.process, encoder, "the picture on screen keeps its encoder until the player actually moves");
442
+ assert.deepEqual(encoder.signals, [], "stopping it here is what would put the spinner back");
443
+ });
444
+
445
+ test("a rung warmed at the playhead survives the switch that lands just ahead of it", async (t) => {
446
+ const { manager, base, dirPath } = await managerWithBase();
447
+ t.after(async () => {
448
+ await manager.lifecycle.disposeAll();
449
+ await rm(dirPath, { recursive: true, force: true });
450
+ });
451
+ const variant = fakeSession({ id: VARIANT_ID, encodeHeight: 540, dirPath });
452
+ variant.variantHeight = 540;
453
+ // A step of the picture: made as one, same file, and a height of its own.
454
+ variant.isStep = true;
455
+ manager.outputs.markStep(variant);
456
+ manager.outputs.set(VARIANT_ID, variant);
457
+ chooseFor(manager, base, 540, variant.outputKey);
458
+ startManagedRun(manager, base, { process: fakeEncoder() });
459
+ // Warmed AT THE PLAYHEAD (240 s = segment #60), which is what the browser
460
+ // sends from server 0.10.0 onwards, and the run is alive and has produced a
461
+ // few segments past it.
462
+ await manager.renditions.prepareVariant(BASE_ID, 540, 240, VIEWER);
463
+
464
+ startManagedRun(manager, variant, { from: 59, process: fakeEncoder() });
465
+ variant.progress = { ...variant.progress, processedSeconds: 268 };
466
+ variant.seekTarget = null;
467
+ variant.seekSettleTimer = null;
468
+
469
+ // hls.js flushes from the fragment after the one holding
470
+ // `currentTime + fetchdelay`, so its first request for the new rung is the
471
+ // playhead plus up to one fragment — here #61 against a run that began at
472
+ // #59. Warming at the END OF THE BUFFER instead put the run tens of seconds
473
+ // AHEAD of this request, which the proxy then read as a seek backwards:
474
+ // measured 2026-08-14, that killed a run holding 21.8 s of encoded output.
475
+ await manager.renditions.resolveVariantFile(BASE_ID, 540, "segment-00061.mp4", VIEWER);
476
+ manager.renditions.viewerPlays(BASE_ID, VIEWER, 540, 244);
477
+
478
+ assert.equal(manager.viewers.of(base, VIEWER).activeVariantId, VARIANT_ID, "the viewer has moved to this rung");
479
+ assert.equal(
480
+ variant.seekTarget,
481
+ null,
482
+ "the request is inside the warmed run, so nothing is repositioned and the warm-up is kept"
483
+ );
484
+ assert.equal([...variant.runs][0].from, 59, "the run still begins where it was warmed");
485
+ });
486
+
487
+ test("a rung warmed PAST the switch is repositioned, which is what warming late costs", async (t) => {
488
+ const { manager, base, dirPath } = await managerWithBase();
489
+ t.after(async () => {
490
+ await manager.lifecycle.disposeAll();
491
+ await rm(dirPath, { recursive: true, force: true });
492
+ });
493
+ const variant = fakeSession({ id: VARIANT_ID, encodeHeight: 540, dirPath });
494
+ variant.variantHeight = 540;
495
+ // A step of the picture: made as one, same file, and a height of its own.
496
+ variant.isStep = true;
497
+ manager.outputs.markStep(variant);
498
+ manager.outputs.set(VARIANT_ID, variant);
499
+ chooseFor(manager, base, 540, variant.outputKey);
500
+ startManagedRun(manager, base, { process: fakeEncoder() });
501
+ // The same session, warmed where the BUFFER ended rather than where the
502
+ // picture was — 60 s further on, which is an ordinary cushion. This is what
503
+ // server 0.9.3 sent and 0.11.0 stopped sending.
504
+ await manager.renditions.prepareVariant(BASE_ID, 540, 300, VIEWER);
505
+ startManagedRun(manager, variant, { from: 74, process: fakeEncoder() });
506
+ variant.progress = { ...variant.progress, processedSeconds: 310 };
507
+
508
+ // hls.js still lands near the playhead, so the request is far BEHIND the
509
+ // warmed run: the proxy reads it as a seek backwards and starts again, and
510
+ // everything the warm-up produced is thrown away. Measured in the field
511
+ // 2026-08-14 as 21.8 s of encoded output destroyed by the act of using it.
512
+ await manager.renditions.resolveVariantFile(BASE_ID, 540, "segment-00061.mp4", VIEWER);
513
+ manager.renditions.viewerPlays(BASE_ID, VIEWER, 540, 244);
514
+
515
+ assert.equal(
516
+ manager.viewers.of(variant, VIEWER).positionSeconds(),
517
+ 244,
518
+ "where the picture is, not where the warm-up had run to"
519
+ );
520
+ });
521
+
522
+ test("the rung on screen fetching its own segments does not cancel a warm-up", async (t) => {
523
+ const { manager, base, dirPath } = await managerWithBase();
524
+ t.after(async () => {
525
+ await manager.lifecycle.disposeAll();
526
+ await rm(dirPath, { recursive: true, force: true });
527
+ });
528
+ manager.encodeRuns.planEncodersSoon = () => {};
529
+ const variant = fakeSession({ id: VARIANT_ID, encodeHeight: 540, dirPath });
530
+ variant.variantHeight = 540;
531
+ // A step of the picture: made as one, same file, and a height of its own.
532
+ variant.isStep = true;
533
+ manager.outputs.markStep(variant);
534
+ manager.outputs.set(VARIANT_ID, variant);
535
+ chooseFor(manager, base, 540, variant.outputKey);
536
+ const warmedEncoder = fakeEncoder();
537
+ startManagedRun(manager, variant, { process: warmedEncoder });
538
+ startManagedRun(manager, base, { process: fakeEncoder() });
539
+ await manager.renditions.prepareVariant(BASE_ID, 540, 100, VIEWER);
540
+
541
+ // The viewer has not moved: the rung they are watching goes on asking for its
542
+ // own segments, every few seconds, for as long as they watch.
543
+ await manager.renditions.resolveVariantFile(BASE_ID, 812, "segment-00026.mp4", VIEWER);
544
+ manager.renditions.viewerPlays(BASE_ID, VIEWER, 812, 104);
545
+ await manager.renditions.resolveVariantFile(BASE_ID, 812, "segment-00027.mp4", VIEWER);
546
+ manager.renditions.viewerPlays(BASE_ID, VIEWER, 812, 108);
547
+
548
+ // Kept per viewer.
549
+ assert.equal(
550
+ manager.viewers.forOutput(base).get(VIEWER)?.warmingVariantId ?? null,
551
+ VARIANT_ID,
552
+ "the rung being prepared is still being prepared"
553
+ );
554
+ assert.deepEqual(
555
+ warmedEncoder.signals,
556
+ [],
557
+ "cancelling it here left the viewer waiting out the whole warm-up for a segment nobody was making"
558
+ );
559
+ });
560
+
561
+ test("warming the height the base itself serves still points it at the switch", async (t) => {
562
+ const { manager, base, dirPath } = await managerWithBase();
563
+ t.after(async () => {
564
+ await manager.lifecycle.disposeAll();
565
+ await rm(dirPath, { recursive: true, force: true });
566
+ });
567
+ // The viewer is on another rung; the base is parked where they left it, with
568
+ // its encoder stopped. Warming its height must bring it back.
569
+ const variant = fakeSession({ id: VARIANT_ID, encodeHeight: 540, dirPath });
570
+ variant.variantHeight = 540;
571
+ manager.outputs.set(VARIANT_ID, variant);
572
+ chooseFor(manager, base, 540, variant.outputKey);
573
+ manager.viewers.of(base, VIEWER).activeVariantId = VARIANT_ID;
574
+ base.runs = new Set();
575
+
576
+ await manager.renditions.prepareVariant(BASE_ID, 812, 400, VIEWER);
577
+
578
+ // 400 s falls on the boundary between #99 and #100, and a run starts one
579
+ // segment back so the player has the preceding keyframe.
580
+ assert.equal(
581
+ manager.viewers.of(base, VIEWER).positionSeconds(),
582
+ 396,
583
+ "the base is parked where they left it, so warming its height says where they are like any other rung"
584
+ );
585
+ });
586
+
587
+ test("the viewer's position is kept current by the segments they ask for", async (t) => {
588
+ const { manager, base, dirPath } = await managerWithBase();
589
+ t.after(async () => {
590
+ await manager.lifecycle.disposeAll();
591
+ await rm(dirPath, { recursive: true, force: true });
592
+ });
593
+ // A REQUEST SAYS NOTHING ABOUT WHERE THEY ARE, and asking for one must not
594
+ // move them. Where they are is what they stated — a seek, or a report
595
+ // carrying their playhead — and it stands until they state otherwise.
596
+ manager.viewers.of(base, VIEWER).moveTo(40);
597
+
598
+ await manager.serving.getFileStream(BASE_ID, "segment-00090.mp4", { consumerId: VIEWER });
599
+
600
+ assert.ok(
601
+ Math.abs((manager.viewers.of(base, VIEWER).positionSeconds() ?? -1) - 40) < 1,
602
+ "a request for segment #90 leaves the viewer where they said they were"
603
+ );
604
+ });
605
+
606
+ test("a playlist or an init segment does not move the encoder", async (t) => {
607
+ const { manager, base, dirPath } = await managerWithBase();
608
+ t.after(async () => {
609
+ await manager.lifecycle.disposeAll();
610
+ await rm(dirPath, { recursive: true, force: true });
611
+ });
612
+ const variant = fakeSession({ id: VARIANT_ID, encodeHeight: 540, dirPath });
613
+ manager.outputs.set(VARIANT_ID, variant);
614
+ // The plan is kept out of this: it is not what is being checked, and a run
615
+ // with no measured speed is one it would take away for changing nothing.
616
+ manager.encodeRuns.planEncodersSoon = () => {};
617
+ chooseFor(manager, base, 540, variant.outputKey);
618
+ const encoder = fakeEncoder();
619
+ startManagedRun(manager, base, { process: encoder });
620
+ chooseFor(manager, base, 540, variant.outputKey);
621
+ await manager.renditions.resolveVariantFile(BASE_ID, 540, "index.m3u8", VIEWER);
622
+ await manager.renditions.resolveVariantFile(BASE_ID, 540, "init.mp4", VIEWER);
623
+
624
+ assert.notEqual(
625
+ manager.viewers.of(base, VIEWER).activeVariantId,
626
+ VARIANT_ID,
627
+ "hls.js fetches a level's playlist and init to decide with, and may never switch to it"
628
+ );
629
+ assert.ok([...base.runs][0]?.process, "the stream on screen must keep its encoder while the player is only looking");
630
+ assert.deepEqual(encoder.signals, [], "and nothing was sent to stop it");
631
+ });
632
+
633
+ test("the name of a variant is fixed, whatever its encode is later set to", async (t) => {
634
+ const { manager, base, dirPath } = await managerWithBase();
635
+ t.after(async () => {
636
+ await manager.lifecycle.disposeAll();
637
+ await rm(dirPath, { recursive: true, force: true });
638
+ });
639
+ // The player fetched the master once and addresses this variant as 812p for
640
+ // the rest of the session, so the name must not follow the encode. Nothing in
641
+ // the proxy changes `encodeHeight` mid-session any more — a change of size is
642
+ // a change of variant now — but the name and the encode are still two
643
+ // different things, and the addressing depends on their staying so.
644
+ assert.equal(manager.outputs.variantHeightOf(base), 812);
645
+ base.encodeHeight = 540;
646
+
647
+ assert.equal(
648
+ manager.outputs.variantHeightOf(base),
649
+ 812,
650
+ "the name stays; renaming it would leave the player addressing a variant nobody answers for"
651
+ );
652
+ assert.deepEqual(
653
+ await manager.renditions.resolveVariantFile(BASE_ID, 812, "segment-00000.mp4", VIEWER),
654
+ { sessionId: BASE_ID },
655
+ "a second session at a height the host has already failed to manage is the opposite of what a step is for"
656
+ );
657
+ });
658
+
659
+ test("the cut grid follows the grid asked for, not who produces the frames", () => {
660
+ // Why a segment from one encoder can stand where another's would have: both
661
+ // are cut at the same times. Which times is a property of the SESSION — the
662
+ // even grid, or the source's own keyframes — and it must not be re-derived
663
+ // from whether the video is copied, because a variant of a copied stream is
664
+ // re-encoded and still has to land on the copy's cuts.
665
+ const shape = { durationSeconds: 100, segDur: SEGMENT_SECONDS, startTime: 0 };
666
+ const keyframeTimes = [0, 3.1, 9.7, 14.2, 21.5, 40, 61.25];
667
+
668
+ const even = computeCutGrid({ ...shape, useKeyframeGrid: false, keyframeTimes });
669
+ assert.equal(even.boundaries[1], SEGMENT_SECONDS, "the even grid ignores the source's keyframes");
670
+ assert.equal(even.boundaries.at(-1), 100);
671
+
672
+ const source = computeCutGrid({ ...shape, useKeyframeGrid: true, keyframeTimes });
673
+ assert.deepEqual(
674
+ source.boundaries,
675
+ [0, 9.7, 14.2, 21.5, 40, 61.25, 100],
676
+ "the source's own keyframes, kept only where they are at least a segment apart"
677
+ );
678
+
679
+ // The one that matters: a copy and a re-encoded rung of it, given the same
680
+ // grid, produce the SAME table. Segment N then covers the same span in both,
681
+ // which is what lets one stand where the other would have.
682
+ assert.deepEqual(
683
+ computeCutGrid({ ...shape, useKeyframeGrid: true, keyframeTimes }).boundaries,
684
+ source.boundaries,
685
+ "a variant inherits the grid, so its boundaries are the same values"
686
+ );
687
+ // And with no index there is nothing to align to — the even grid, whoever asks.
688
+ assert.deepEqual(
689
+ computeCutGrid({ ...shape, useKeyframeGrid: true, keyframeTimes: null }).boundaries,
690
+ even.boundaries,
691
+ "no keyframes means no keyframe grid, however the caller asks"
692
+ );
693
+ });
694
+
695
+ test("a boundary carries the keyframe it is, exactly as the container stated it", () => {
696
+ // The fault that stopped a viewing on 2026-09-05. A boundary is kept on the
697
+ // player's clock, `keyframe - startTime`, rounded; the seek used to add the
698
+ // start time back and look the result up in the container's list by value.
699
+ // The round trip is lossy — 26.234 - 0.083 + 0.083 is 26.233999999999998 —
700
+ // and "the keyframe at or before that" is then the PREVIOUS one, a whole
701
+ // keyframe interval earlier.
702
+ const startTime = 0.083;
703
+ const keyframeTimes = [];
704
+ for (let index = 0; index < 60; index += 1) {
705
+ keyframeTimes.push(Number((startTime + index * 8.717).toFixed(3)));
706
+ }
707
+ const grid = computeCutGrid({
708
+ useKeyframeGrid: true, durationSeconds: 500, segDur: SEGMENT_SECONDS, keyframeTimes, startTime
709
+ });
710
+
711
+ assert.equal(grid.sourceTimes[0], startTime, "the first cut is the start of the file");
712
+ assert.equal(grid.boundaries.length, grid.sourceTimes.length, "an index must name both clocks");
713
+ for (let index = 1; index < grid.boundaries.length - 1; index += 1) {
714
+ assert.ok(
715
+ keyframeTimes.includes(grid.sourceTimes[index]),
716
+ `cut #${index} carries ${grid.sourceTimes[index]}, which the container never stated`
717
+ );
718
+ }
719
+
720
+ // What the round trip does, so the reason this table exists cannot be
721
+ // mistaken for caution: at least one boundary fails to find itself.
722
+ const lost = grid.boundaries
723
+ .slice(1, -1)
724
+ .filter((published, at) => nearestKeyframeAtOrBefore(keyframeTimes, published + startTime)
725
+ !== grid.sourceTimes[at + 1]);
726
+ assert.ok(
727
+ lost.length > 0,
728
+ "the search agreed everywhere, so this fixture no longer reproduces the fault it was built for"
729
+ );
730
+ });
731
+
732
+ test("a run on the keyframe grid is given no trim to apply", () => {
733
+ // The second half of the same fault. That trim is an output-side `-ss`, and
734
+ // beside `-copyts` it moves the whole run backwards by its own amount:
735
+ // measured 2026-09-06 on a 5 s keyframe interval, a run landing at 15 s and
736
+ // asked to trim to the cut at 20 s produced its first file starting at 10 s.
737
+ // Every cut of the run inherits it while the numbering, fixed at spawn, does
738
+ // not — so the files are named for times they do not hold.
739
+ const startTime = 0.083;
740
+ const keyframeTimes = [];
741
+ for (let index = 0; index < 60; index += 1) {
742
+ keyframeTimes.push(Number((startTime + index * 8.717).toFixed(3)));
743
+ }
744
+ const grid = computeCutGrid({
745
+ useKeyframeGrid: true, durationSeconds: 500, segDur: SEGMENT_SECONDS, keyframeTimes, startTime
746
+ });
747
+ const timeline = new Timeline({
748
+ boundaries: grid.boundaries, sourceTimes: grid.sourceTimes, cutGrid: "keyframe"
749
+ });
750
+
751
+ for (let index = 1; index < grid.boundaries.length - 1; index += 1) {
752
+ const { args } = buildRunCommand({
753
+ keyframes: { times: keyframeTimes },
754
+ inputFile: { startTime },
755
+ audioFile: { startTime },
756
+ inputUrl: "http://127.0.0.1/stream",
757
+ audioInputUrl: "",
758
+ outputDir: ".",
759
+ timeline,
760
+ segmentFormat: fmp4Format,
761
+ transcodeVideo: false,
762
+ transcodeAudio: true,
763
+ audioOnly: false,
764
+ audioSeparate: false,
765
+ audioSourceTrackIndex: 0,
766
+ rateControl: null,
767
+ startIndex: index,
768
+ endIndex: index,
769
+ videoEncoder: { name: "libx264" },
770
+ segmentDurationSec: SEGMENT_SECONDS
771
+ });
772
+ // One input seek establishes decode order in the preceding private cut.
773
+ assert.equal(
774
+ args.filter((one) => one === "-ss").length,
775
+ 1,
776
+ `run at #${index} was given a trim beside -copyts`
777
+ );
778
+ const seek = Number(args[args.indexOf("-ss") + 1]);
779
+ assert.ok(
780
+ Math.abs(seek - grid.sourceTimes[index - 1]) < 0.2,
781
+ `run at #${index} seeks to ${seek}, not to the preceding keyframe ${grid.sourceTimes[index - 1]}`
782
+ );
783
+ assert.equal(Number(args[args.indexOf("-segment_start_number") + 1]), index - 1);
784
+ }
785
+ });
786
+
787
+ test("a rung served by copy stays offered while a re-encoded rung is on screen", async (t) => {
788
+ // Enough to re-encode 240p (1.67x combined) and nowhere near enough for
789
+ // 720p (0.45x) — the field's own shape, where the rung the viewer picked was
790
+ // offered and everything between it and the copy was not.
791
+ const { manager, base, dirPath } = await managerWithBase({
792
+ softwarePresetBenchmark: [{ preset: "ultrafast", pixelsPerSec: 12_000_000 }],
793
+ decodeCostModel: { pixelTerm: 0.00793, bitrateTerm: 0, constantTerm: 0 }
794
+ });
795
+ t.after(async () => {
796
+ await manager.lifecycle.disposeAll();
797
+ await rm(dirPath, { recursive: true, force: true });
798
+ });
799
+ // The field case of 2026-08-15: a 1080p source served by COPY, the viewer on
800
+ // 240p, and a host too weak to re-encode anything above it.
801
+ base.spec = outputSpec({ transcodeVideo: false, cutGrid: "keyframe" });
802
+ base.encodeHeight = 1080;
803
+ base.variantHeight = 1080;
804
+ // What decoding this source costs comes from the file's own facts, stated in
805
+ // the fixture: 49.766 Mpx/s at 8 Mbit/s.
806
+ assert.ok(base.file.decode, "the fixture must state enough for a decode cost to exist");
807
+
808
+ const watching = fakeSession({ id: VARIANT_ID, encodeHeight: 240, dirPath });
809
+ watching.variantHeight = 240;
810
+ // The rung knows the source as well as the base does, because it IS the same
811
+ // file. Without that it prices nothing at all — every height comes back
812
+ // "sustainable" for want of a measurement — and the assertion below would
813
+ // hold for the wrong reason.
814
+ watching.file = base.file;
815
+ // A step of the picture: same file, and made as a step.
816
+ watching.isStep = true;
817
+ manager.outputs.markStep(watching);
818
+ manager.outputs.set(VARIANT_ID, watching);
819
+ chooseFor(manager, base, 240, watching.outputKey);
820
+ manager.viewers.of(base, VIEWER).activeVariantId = VARIANT_ID;
821
+
822
+ const offered = manager.quality.offeredHeights(watching);
823
+
824
+ assert.ok(
825
+ offered.includes(1080),
826
+ "the height the source is COPIED at costs no encoder, so no measurement of this host can withdraw it"
827
+ );
828
+ assert.deepEqual(
829
+ offered,
830
+ manager.quality.offeredHeights(base),
831
+ "one answer for the family: a rung asked while watching another must not disagree with the base"
832
+ );
833
+ });
834
+
835
+ test("a separately published audio track starts where the picture is, from the reported buffer", async (t) => {
836
+ const { manager, base, dirPath } = await managerWithBase();
837
+ t.after(async () => {
838
+ await manager.lifecycle.disposeAll();
839
+ await rm(dirPath, { recursive: true, force: true });
840
+ });
841
+ base.spec = outputSpec({ transcodeVideo: true, audioSeparate: true, height: base.output.encodeHeight });
842
+ manager.viewers.of(base, VIEWER).audio = { trackIndex: 1, transcode: true };
843
+ // The viewer says where their picture is: 100 s. That, less a segment of
844
+ // margin, is where the track has to begin — no subtraction of anything,
845
+ // because the position is the playhead and not the edge of a buffer.
846
+ manager.viewers.of(base, "viewer").moveTo(100);
847
+ manager.viewers.of(base, "viewer").playing = false;
848
+ manager.getCachedAudioTracks = () => [
849
+ { index: 0, language: "rus", title: "", isDefault: true },
850
+ { index: 1, language: "eng", title: "", isDefault: false }
851
+ ];
852
+ const created = [];
853
+ manager.viewerRequests.createOrGetSession = async (params) => {
854
+ created.push(params);
855
+ // The output itself, which is what `createOrGetSession` answers, carrying
856
+ // the track it was asked for.
857
+ return fakeSession({
858
+ id: VARIANT_ID,
859
+ encodeHeight: 0,
860
+ dirPath,
861
+ audioOnly: true,
862
+ audioSourceTrackIndex: params.audioTrackIndex
863
+ });
864
+ };
865
+
866
+ await manager.renditions.resolveAudioRenditionFile(BASE_ID, 1, "segment-00010.mp4", VIEWER);
867
+
868
+ assert.equal(created.length, 1, "the track's own session was made");
869
+ assert.equal(
870
+ created[0].startPositionSeconds,
871
+ 96,
872
+ "140 s served, less the 40 s the player holds, less one segment of margin"
873
+ );
874
+ });
875
+
876
+ test("with two viewers the audio track starts at the EARLIEST picture, not the read head", async (t) => {
877
+ const { manager, base, dirPath } = await managerWithBase();
878
+ t.after(async () => {
879
+ await manager.lifecycle.disposeAll();
880
+ await rm(dirPath, { recursive: true, force: true });
881
+ });
882
+ base.spec = outputSpec({ transcodeVideo: true, audioSeparate: true, height: base.output.encodeHeight });
883
+ // A copied picture is one session shared by both of them, and a track begun
884
+ // at the leader has nothing to give the one behind.
885
+ manager.viewers.of(base, "ahead").moveTo(100);
886
+ manager.viewers.of(base, "ahead").playing = false;
887
+ manager.viewers.of(base, "behind").moveTo(40);
888
+ manager.viewers.of(base, "behind").playing = false;
889
+ manager.getCachedAudioTracks = () => [
890
+ { index: 0, language: "rus", title: "", isDefault: true },
891
+ { index: 1, language: "eng", title: "", isDefault: false }
892
+ ];
893
+ const created = [];
894
+ manager.viewerRequests.createOrGetSession = async (params) => {
895
+ created.push(params);
896
+ // The output itself, which is what `createOrGetSession` answers, carrying
897
+ // the track it was asked for.
898
+ return fakeSession({
899
+ id: VARIANT_ID,
900
+ encodeHeight: 0,
901
+ dirPath,
902
+ audioOnly: true,
903
+ audioSourceTrackIndex: params.audioTrackIndex
904
+ });
905
+ };
906
+
907
+ await manager.renditions.resolveAudioRenditionFile(BASE_ID, 1, "segment-00010.mp4", VIEWER);
908
+
909
+ assert.equal(
910
+ created[0].startPositionSeconds,
911
+ 36,
912
+ "the viewer at 40 s, less one segment of margin — a run starting at the leader " +
913
+ "has nothing to give the one behind them"
914
+ );
915
+ });
916
+
917
+ test("a soundtrack begins where a viewer says they are, whenever they said it", async (t) => {
918
+ const { manager, base, dirPath } = await managerWithBase();
919
+ t.after(async () => {
920
+ await manager.lifecycle.disposeAll();
921
+ await rm(dirPath, { recursive: true, force: true });
922
+ });
923
+ base.spec = outputSpec({ transcodeVideo: true, audioSeparate: true, height: base.output.encodeHeight });
924
+ // TWO RULES WENT WHEN THE QUANTITY STOPPED MEANING TWO THINGS. A position
925
+ // "past the read head" had to be clamped, because the read head was a
926
+ // request edge and a report could overtake it; and a report older than a few
927
+ // seconds had to be discarded, because it was being subtracted from that
928
+ // edge. Neither survives: this is where the viewer's PICTURE is, they are the
929
+ // only writer of it, and between statements it moves at the rate the film
930
+ // moves. An old statement from a stopped viewer is exactly as true as a new
931
+ // one.
932
+ const viewer = manager.viewers.of(base, "viewer");
933
+ viewer.moveTo(900, Date.now() - 600_000);
934
+ viewer.playing = false;
935
+ manager.getCachedAudioTracks = () => [
936
+ { index: 0, language: "rus", title: "", isDefault: true },
937
+ { index: 1, language: "eng", title: "", isDefault: false }
938
+ ];
939
+ const created = [];
940
+ manager.viewerRequests.createOrGetSession = async (params) => {
941
+ created.push(params);
942
+ // The output itself, which is what `createOrGetSession` answers, carrying
943
+ // the track it was asked for.
944
+ return fakeSession({
945
+ id: VARIANT_ID,
946
+ encodeHeight: 0,
947
+ dirPath,
948
+ audioOnly: true,
949
+ audioSourceTrackIndex: params.audioTrackIndex
950
+ });
951
+ };
952
+
953
+ await manager.renditions.resolveAudioRenditionFile(BASE_ID, 1, "segment-00010.mp4", VIEWER);
954
+
955
+ assert.equal(
956
+ created[0].startPositionSeconds,
957
+ 896,
958
+ "ten minutes of silence from a stopped viewer says they are still at 900 s"
959
+ );
960
+ });
961
+
962
+ test("an audio track is prepared at the position the switch will land on", async (t) => {
963
+ const { manager, base, dirPath } = await managerWithBase();
964
+ t.after(async () => {
965
+ await manager.lifecycle.disposeAll();
966
+ await rm(dirPath, { recursive: true, force: true });
967
+ });
968
+ base.spec = outputSpec({ transcodeVideo: true, audioSeparate: true, height: base.output.encodeHeight });
969
+ manager.viewers.of(base, VIEWER).audio = { trackIndex: 1, transcode: true };
970
+ manager.getCachedAudioTracks = () => [
971
+ { index: 0, language: "rus", title: "", isDefault: true },
972
+ { index: 1, language: "eng", title: "", isDefault: false }
973
+ ];
974
+ const rendition = fakeSession({
975
+ id: VARIANT_ID,
976
+ encodeHeight: 0,
977
+ dirPath,
978
+ audioOnly: true,
979
+ audioSourceTrackIndex: 1
980
+ });
981
+ rendition.id = rendition.spec.toName();
982
+ // A soundtrack of THIS picture: the same file, published on its own. Found by
983
+ // what it is — there is no list of ids to put it on.
984
+ rendition.file = base.file;
985
+ startManagedRun(manager, rendition, { process: fakeEncoder() });
986
+ manager.outputs.set(rendition.id, rendition);
987
+
988
+ const prepared = await manager.renditions.prepareAudioTrack(BASE_ID, 1, 240, VIEWER);
989
+
990
+ assert.deepEqual(
991
+ prepared,
992
+ { sessionId: rendition.id, fileName: "segment-00060.mp4" },
993
+ "the caller is told which segment to wait for — 240 s on a four-second grid"
994
+ );
995
+ assert.equal(
996
+ manager.viewers.of(rendition, VIEWER).positionSeconds(),
997
+ 240,
998
+ "and the track is told where this person is, which is what the switch will land on"
999
+ );
1000
+ });
1001
+
1002
+ test("a reading from a soundtrack is priced as one", () => {
1003
+ // The fault this pins: an audio rendition reached no learner at all. One
1004
+ // guard refused renditions a reading, and the only call that would have
1005
+ // priced one sat behind a second guard its caller had already made — so a
1006
+ // soundtrack ran for the whole film and was charged at nothing, which is the
1007
+ // half of roadmap item 6 that was left owing.
1008
+ assert.equal(costKindForSession({ spec: outputSpec({ audioOnly: true }) }), "audio");
1009
+ assert.equal(
1010
+ costKindForSession({ spec: outputSpec({ audioOnly: true, transcodeVideo: true }) }),
1011
+ "audio",
1012
+ "a rendition carries no picture, whatever the flag it inherited says"
1013
+ );
1014
+ assert.equal(costKindForSession({ spec: outputSpec({ transcodeVideo: true }) }), "decode");
1015
+ assert.equal(costKindForSession({ spec: outputSpec({ transcodeVideo: false }) }), "copy");
1016
+ });
1017
+
1018
+ test("a quality step being warmed is not refused by its own cost", async (t) => {
1019
+ const dirPath = await mkdtemp(path.join(os.tmpdir(), "warm-cost-"));
1020
+ // The addon host's own figures, so the arithmetic below is the field's and
1021
+ // not an invention. Without a benchmark the check returns every height
1022
+ // untouched and a test over it would pass while proving nothing.
1023
+ const { manager } = managerWithOwnStore({
1024
+ softwarePresetBenchmark: [
1025
+ { preset: "fast", pixelsPerSec: 17.0e6 },
1026
+ { preset: "ultrafast", pixelsPerSec: 67.5e6 }
1027
+ ],
1028
+ decodeCostModel: { pixelTerm: 0.007742, bitrateTerm: 0, constantTerm: 0 }
1029
+ });
1030
+ t.after(async () => {
1031
+ await manager.lifecycle.disposeAll();
1032
+ await rm(dirPath, { recursive: true, force: true });
1033
+ });
1034
+ // A copied 1080p picture, and a 240p step warmed beside it for a switch —
1035
+ // which is what every quality change does, two encoders on purpose.
1036
+ const base = fakeSession({ id: BASE_ID, encodeHeight: 0, dirPath, transcodeVideo: false });
1037
+ base.variantHeight = 1080;
1038
+ // What this source costs to decode comes from the file's own facts, stated in
1039
+ // the fixture: 1080p24 at 8 Mbit/s, the field file. Without them nothing can
1040
+ // be priced and every height comes back offerable for want of a measurement,
1041
+ // which would make the assertion hold for the wrong reason.
1042
+ assert.ok(base.file.decode, "the fixture must state enough for a decode cost to exist");
1043
+ const warming = fakeSession({ id: VARIANT_ID, encodeHeight: 240, dirPath });
1044
+ warming.variantHeight = 240;
1045
+ // Two sessions of ONE file share its facts, which is the whole point of the
1046
+ // file being an object: a step warmed beside the picture is not a second file.
1047
+ warming.file = base.file;
1048
+ chooseFor(manager, base, 240, warming.outputKey);
1049
+ // A step of the picture: same file, and a height of its own.
1050
+ // Running, and running well: it says of itself that it holds twice realtime,
1051
+ // i.e. half a second of work per second of video.
1052
+ startManagedRun(manager, warming, { process: fakeEncoder() });
1053
+ qualityStateOf(warming).lastAloneSpeed = 2;
1054
+ manager.outputs.set(BASE_ID, base);
1055
+ manager.outputs.set(VARIANT_ID, warming);
1056
+
1057
+ const offered = manager.quality.offeredHeights(base);
1058
+
1059
+ assert.ok(
1060
+ offered.includes(240),
1061
+ "charged its own cost while being judged, the step the viewer just asked for is dropped from the " +
1062
+ "offer by the act of warming it — and its next segment 404s on a stream that is playing"
1063
+ );
1064
+ });
1065
+
1066
+ test("the master survives a live offer that has collapsed to one rung", async (t) => {
1067
+ // The field case of 2026-08-18, in the smallest form that reproduces it: a
1068
+ // host too slow for any re-encoded rung, and a swarm whose interruptions
1069
+ // demand far more than realtime. The live offer then holds only the height an
1070
+ // encoder is already producing — and until this test existed, that made
1071
+ // `buildMasterPlaylist` answer null and the route answer 404 to a session
1072
+ // that had just published the address.
1073
+ const dirPath = await mkdtemp(path.join(os.tmpdir(), "quality-variants-collapse-"));
1074
+ const { manager } = managerWithOwnStore({
1075
+ // A megapixel a second: every rung below the source costs more than the
1076
+ // machine has.
1077
+ softwarePresetBenchmark: [{ preset: "veryfast", pixelsPerSec: 1_000_000 }],
1078
+ decodeCostModel: { pixelTerm: 0.01, bitrateTerm: 0, constantTerm: 0 }
1079
+ });
1080
+ const base = fakeSession({ id: BASE_ID, encodeHeight: 812, dirPath });
1081
+ // A thicker source than the fixture's, stated as the file's own bitrate
1082
+ // rather than as a cost object the file now derives.
1083
+ base.file.learn({ width: 1920, height: 1080, fps: 24, bitrateKbps: 10_000 });
1084
+ // What this file's own reader measured: a step must run at eight times
1085
+ // realtime to survive this swarm.
1086
+ manager.hostLoad.requiredSpeedFor = () => 8;
1087
+ manager.outputs.set(BASE_ID, base);
1088
+ t.after(async () => {
1089
+ await manager.lifecycle.disposeAll();
1090
+ await rm(dirPath, { recursive: true, force: true });
1091
+ });
1092
+
1093
+ assert.deepEqual(
1094
+ manager.quality.offeredHeights(base),
1095
+ [812],
1096
+ "the live judgement is unchanged: nothing but the running height is worth offering"
1097
+ );
1098
+
1099
+ const master = manager.renditions.buildMasterPlaylist(BASE_ID);
1100
+
1101
+ assert.ok(master, "the master is a published document, not a live figure");
1102
+ const heights = [...master.matchAll(/^v\/(\d+)\/index\.m3u8$/gm)].map((match) => Number(match[1]));
1103
+ assert.deepEqual(
1104
+ heights,
1105
+ [1080, 812, 720, 540, 480, 360, 240],
1106
+ "every rung that can be spliced onto this cut grid stays addressable"
1107
+ );
1108
+ });
1109
+
1110
+ test("two heights that clamp onto one picture share a single encoder", async (t) => {
1111
+ const { manager, base, dirPath } = await managerWithBase();
1112
+ const spawnedDirs = [];
1113
+ t.after(async () => {
1114
+ await manager.lifecycle.disposeAll();
1115
+ await Promise.all(spawnedDirs.map((dir) => rm(dir, { recursive: true, force: true })));
1116
+ await rm(dirPath, { recursive: true, force: true });
1117
+ });
1118
+
1119
+ // A host that can hold exactly one rung. Whatever height is asked for, the
1120
+ // clamp inside createOrGetSession lands the encode on 240p — which is what a
1121
+ // CM4 did on 2026-08-28, turning a 360p and a 540p request into two more
1122
+ // ffmpeg processes making the same 426x240 picture as the 240p one.
1123
+ const madeIds = [VARIANT_ID, SECOND_VARIANT_ID];
1124
+ const created = [];
1125
+ manager.viewerRequests.createOrGetSession = async (params) => {
1126
+ created.push(params);
1127
+ // ONE FORMAT, ONE OUTPUT, as `OutputOpening` has it: an output is named by
1128
+ // what it carries, so a request clamped onto a picture already made is
1129
+ // answered by that output and makes nothing.
1130
+ const probe = fakeSession({ id: "probe", encodeHeight: 240, dirPath });
1131
+ const existing = [...manager.outputs.values()].find((output) => output.outputKey === probe.outputKey);
1132
+ if (existing) {
1133
+ return existing;
1134
+ }
1135
+ const id = madeIds[created.length - 1];
1136
+ const variantDir = await mkdtemp(path.join(os.tmpdir(), "quality-variants-clamped-"));
1137
+ spawnedDirs.push(variantDir);
1138
+ const variant = fakeSession({ id, encodeHeight: 240, dirPath: variantDir });
1139
+ manager.outputs.set(id, variant);
1140
+ return variant;
1141
+ };
1142
+
1143
+ const asked360 = await askFor(manager, base, 360);
1144
+ const asked540 = await askFor(manager, base, 540);
1145
+
1146
+ assert.equal(asked360.id, VARIANT_ID, "the first request made the encoder");
1147
+ assert.equal(
1148
+ asked540.id,
1149
+ VARIANT_ID,
1150
+ "and the second is served by it, because it produces the very same picture"
1151
+ );
1152
+ assert.equal(
1153
+ manager.outputs.has(SECOND_VARIANT_ID),
1154
+ false,
1155
+ "the duplicate was let go as soon as its size was known"
1156
+ );
1157
+ assert.equal(
1158
+ chosenOutputOf(manager.viewers, VIEWER, 540),
1159
+ manager.outputs.get(VARIANT_ID)?.outputKey,
1160
+ "540p is recorded as answered by the output the machine can hold"
1161
+ );
1162
+
1163
+ const askedAgain = await askFor(manager, base, 540);
1164
+
1165
+ assert.equal(askedAgain.id, VARIANT_ID);
1166
+ assert.equal(created.length, 2, "the third request created nothing at all");
1167
+ });
1168
+
1169
+ test("rungs that really do differ keep their own encoders", async (t) => {
1170
+ const { manager, base, dirPath } = await managerWithBase();
1171
+ const spawnedDirs = [];
1172
+ t.after(async () => {
1173
+ await manager.lifecycle.disposeAll();
1174
+ await Promise.all(spawnedDirs.map((dir) => rm(dir, { recursive: true, force: true })));
1175
+ await rm(dirPath, { recursive: true, force: true });
1176
+ });
1177
+
1178
+ // A host with room: each request is encoded at the height it named, so
1179
+ // nothing may be merged. The sharing above must not become "one encoder for
1180
+ // every rung".
1181
+ const madeIds = [VARIANT_ID, SECOND_VARIANT_ID];
1182
+ const created = [];
1183
+ manager.viewerRequests.createOrGetSession = async (params) => {
1184
+ const id = madeIds[created.length];
1185
+ created.push(params);
1186
+ const variantDir = await mkdtemp(path.join(os.tmpdir(), "quality-variants-distinct-"));
1187
+ spawnedDirs.push(variantDir);
1188
+ const variant = fakeSession({ id, encodeHeight: params.targetHeight, dirPath: variantDir });
1189
+ manager.outputs.set(id, variant);
1190
+ return variant;
1191
+ };
1192
+
1193
+ const asked360 = await askFor(manager, base, 360);
1194
+ const asked540 = await askFor(manager, base, 540);
1195
+
1196
+ assert.equal(asked360.id, VARIANT_ID);
1197
+ assert.equal(asked540.id, SECOND_VARIANT_ID, "two different pictures, two encoders");
1198
+ assert.equal(chosenOutputOf(manager.viewers, VIEWER, 360), asked360.outputKey);
1199
+ assert.equal(chosenOutputOf(manager.viewers, VIEWER, 540), asked540.outputKey);
1200
+ });
1201
+
1202
+ test("a rung is never served from the COPY, whatever height the copy happens to be", async (t) => {
1203
+ const { manager, base, dirPath } = await managerWithBase();
1204
+ const spawnedDirs = [];
1205
+ t.after(async () => {
1206
+ await manager.lifecycle.disposeAll();
1207
+ await Promise.all(spawnedDirs.map((dir) => rm(dir, { recursive: true, force: true })));
1208
+ await rm(dirPath, { recursive: true, force: true });
1209
+ });
1210
+
1211
+ // The base is a COPY at 240p — no encoder behind it, and it is the one rung
1212
+ // this host can always serve. Handing it to a request for a re-encoded 360p
1213
+ // would give away exactly that, and a viewer stranded on a rung the machine
1214
+ // cannot hold would have nowhere left to return to.
1215
+ base.transcodeVideo = false;
1216
+ base.encodeHeight = 240;
1217
+ base.variantHeight = 240;
1218
+ const created = [];
1219
+ manager.viewerRequests.createOrGetSession = async (params) => {
1220
+ created.push(params);
1221
+ const variantDir = await mkdtemp(path.join(os.tmpdir(), "quality-variants-copy-"));
1222
+ spawnedDirs.push(variantDir);
1223
+ const variant = fakeSession({ id: VARIANT_ID, encodeHeight: 240, dirPath: variantDir });
1224
+ manager.outputs.set(VARIANT_ID, variant);
1225
+ return variant;
1226
+ };
1227
+
1228
+ const asked = await askFor(manager, base, 360);
1229
+
1230
+ assert.equal(asked.id, VARIANT_ID, "the re-encoded rung is its own session, not the copy");
1231
+ });
1232
+
1233
+ test("a file opened at a position starts its sound THERE, not a look-ahead earlier", async (t) => {
1234
+ const { manager, base, dirPath } = await managerWithBase();
1235
+ t.after(async () => {
1236
+ await manager.lifecycle.disposeAll();
1237
+ await rm(dirPath, { recursive: true, force: true });
1238
+ });
1239
+ base.spec = outputSpec({ transcodeVideo: true, audioSeparate: true, height: base.output.encodeHeight });
1240
+ // The state at the instant a page is opened at a position: nothing seeked,
1241
+ // no segment served, no report from anybody. The read head is then not a
1242
+ // request edge — it is where the session was made — and a browser that has
1243
+ // just opened holds no buffer at all.
1244
+ base.lastRequestedSegment = null;
1245
+ manager.viewers.of(base, VIEWER).moveTo(588);
1246
+ manager.getCachedAudioTracks = () => [
1247
+ { index: 0, language: "rus", title: "", isDefault: true },
1248
+ { index: 1, language: "eng", title: "", isDefault: false }
1249
+ ];
1250
+ const created = [];
1251
+ manager.viewerRequests.createOrGetSession = async (params) => {
1252
+ created.push(params);
1253
+ // The output itself, which is what `createOrGetSession` answers, carrying
1254
+ // the track it was asked for.
1255
+ return fakeSession({
1256
+ id: VARIANT_ID,
1257
+ encodeHeight: 0,
1258
+ dirPath,
1259
+ audioOnly: true,
1260
+ audioSourceTrackIndex: params.audioTrackIndex
1261
+ });
1262
+ };
1263
+
1264
+ await manager.renditions.resolveAudioRenditionFile(BASE_ID, 1, "segment-00010.mp4", VIEWER);
1265
+
1266
+ // Field 2026-08-31: this answered 460 for a page opened at 588 — the whole
1267
+ // 120 s look-ahead subtracted from a buffer that did not exist — and the
1268
+ // segment the viewer needed took 38.8 s to appear against the picture's 8.4 s.
1269
+ assert.equal(
1270
+ created[0].startPositionSeconds,
1271
+ 584,
1272
+ "where the viewer opened, less one segment of margin, and nothing else"
1273
+ );
1274
+ });