@torrent-tv/proxy 2.88.3 → 2.88.4

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 (473) hide show
  1. package/.claude/commands/opsx/apply.md +155 -155
  2. package/.claude/commands/opsx/archive.md +160 -160
  3. package/.claude/commands/opsx/explore.md +174 -174
  4. package/.claude/commands/opsx/propose.md +109 -109
  5. package/.claude/commands/opsx/sync.md +143 -143
  6. package/.claude/skills/openspec-apply-change/SKILL.md +159 -159
  7. package/.claude/skills/openspec-archive-change/SKILL.md +117 -117
  8. package/.claude/skills/openspec-explore/SKILL.md +289 -289
  9. package/.claude/skills/openspec-propose/SKILL.md +113 -113
  10. package/.claude/skills/openspec-sync-specs/SKILL.md +147 -147
  11. package/.dockerignore +5 -5
  12. package/AGENTS.md +322 -322
  13. package/CHANGELOG.md +2259 -2250
  14. package/Dockerfile +30 -30
  15. package/LICENSE +16 -16
  16. package/README.md +369 -369
  17. package/assets/calibration/NOTICE.md +100 -100
  18. package/assets/diagnostics/sctpstate.gdb +31 -31
  19. package/bin/cli.js +684 -684
  20. package/bin/thread-pool.js +30 -30
  21. package/biome.json +411 -411
  22. package/docs/browser-proxy-contract.md +23 -4
  23. package/docs/container-architecture.md +218 -218
  24. package/docs/download-architecture.md +261 -261
  25. package/docs/encode-architecture.md +729 -729
  26. package/docs/encode-run-state.md +96 -96
  27. package/docs/logs.md +97 -97
  28. package/docs/socks5-egress-research.md +114 -114
  29. package/docs/storage-architecture.md +267 -267
  30. package/knip.json +14 -14
  31. package/openspec/changes/adaptive-bitrate/design.md +92 -92
  32. package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
  33. package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
  34. package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
  35. package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
  36. package/openspec/changes/binary-distribution/proposal.md +53 -53
  37. package/openspec/changes/chunked-request-bodies/design.md +81 -81
  38. package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
  39. package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
  40. package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
  41. package/openspec/changes/cold-start/design.md +98 -98
  42. package/openspec/changes/cold-start/proposal.md +62 -62
  43. package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
  44. package/openspec/changes/cold-start/tasks.md +48 -48
  45. package/openspec/changes/disk-cap/.openspec.yaml +2 -2
  46. package/openspec/changes/disk-cap/proposal.md +37 -37
  47. package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
  48. package/openspec/changes/disk-cap/tasks.md +19 -19
  49. package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
  50. package/openspec/changes/proxy-observability/design.md +38 -38
  51. package/openspec/changes/proxy-observability/proposal.md +49 -49
  52. package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
  53. package/openspec/changes/proxy-observability/tasks.md +21 -21
  54. package/openspec/changes/seek-debounce/design.md +118 -118
  55. package/openspec/changes/seek-debounce/proposal.md +59 -59
  56. package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
  57. package/openspec/changes/seek-debounce/tasks.md +33 -33
  58. package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
  59. package/openspec/changes/subtitle-language/proposal.md +50 -50
  60. package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
  61. package/openspec/changes/subtitle-language/tasks.md +26 -26
  62. package/openspec/changes/track-selection/.openspec.yaml +2 -2
  63. package/openspec/changes/track-selection/design.md +47 -47
  64. package/openspec/changes/track-selection/proposal.md +45 -45
  65. package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
  66. package/openspec/changes/track-selection/tasks.md +20 -20
  67. package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
  68. package/openspec/changes/transcode-quality/proposal.md +51 -51
  69. package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
  70. package/openspec/changes/transcode-quality/tasks.md +63 -63
  71. package/openspec/config.yaml +38 -38
  72. package/package.json +1 -1
  73. package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
  74. package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
  75. package/research/double-spawn-2026-09-10.md +171 -171
  76. package/research/handover-2026-09-13.md +209 -209
  77. package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
  78. package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
  79. package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
  80. package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
  81. package/routes/api/client-logs/post.js +86 -86
  82. package/routes/api/delivery-sink/get.js +72 -72
  83. package/routes/api/link-probe/get.js +51 -51
  84. package/routes/api/playback-plan/audio-tracks/post.js +29 -29
  85. package/routes/api/playback-plan/post.js +53 -53
  86. package/routes/api/sources/post.js +36 -36
  87. package/routes/api/sources/stats/get.js +132 -132
  88. package/routes/api/sources/warm/post.js +217 -217
  89. package/routes/api/subtitles/get.js +230 -230
  90. package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
  91. package/routes/api/transcode-sessions/net-report/post.js +138 -138
  92. package/routes/api/transcode-sessions/post.js +178 -173
  93. package/routes/api/transcode-sessions/progress/get.js +27 -27
  94. package/routes/api/transcode-sessions/release/post.js +41 -41
  95. package/routes/api/transcode-sessions/seek/post.js +63 -63
  96. package/routes/health/get.js +13 -13
  97. package/routes/healthz/get.js +13 -13
  98. package/routes/stream/get.js +380 -380
  99. package/routes/transcode/audio-file/get.js +64 -64
  100. package/routes/transcode/audio-warm/get.js +95 -84
  101. package/routes/transcode/session-file/get.js +52 -52
  102. package/routes/transcode/variant-file/get.js +95 -95
  103. package/routes/transcode/variant-warm/get.js +84 -84
  104. package/scripts/render-run-graph.js +173 -173
  105. package/server.js +725 -725
  106. package/services/encode/CoverageMap.js +428 -428
  107. package/services/encode/CushionReport.js +345 -345
  108. package/services/encode/EncodeAdmission.js +376 -376
  109. package/services/encode/EncodeOrchestrator.js +1217 -1217
  110. package/services/encode/EncodePlan.js +1366 -1366
  111. package/services/encode/EncodeRun.js +813 -813
  112. package/services/encode/EncodeRuns.js +1235 -1235
  113. package/services/encode/Encoder.js +142 -142
  114. package/services/encode/EncoderSelection.js +38 -38
  115. package/services/encode/LocalObservations.js +359 -359
  116. package/services/encode/NvencEncoder.js +50 -50
  117. package/services/encode/OutputOpening.js +783 -758
  118. package/services/encode/OutputTimes.js +579 -579
  119. package/services/encode/QsvEncoder.js +58 -58
  120. package/services/encode/Renditions.js +2282 -2184
  121. package/services/encode/RunProgress.js +84 -84
  122. package/services/encode/SegmentDemand.js +95 -95
  123. package/services/encode/SoftwareEncoder.js +121 -121
  124. package/services/encode/V4l2m2mEncoder.js +53 -53
  125. package/services/encode/VaapiEncoder.js +66 -66
  126. package/services/encode/args.js +418 -416
  127. package/services/encode/available-share.js +89 -89
  128. package/services/encode/calibration.js +361 -361
  129. package/services/encode/contention.js +128 -128
  130. package/services/encode/decode-cost-fit.js +363 -363
  131. package/services/encode/encode-exit.js +114 -114
  132. package/services/encode/encode-run-state.js +551 -551
  133. package/services/encode/encoder-readings.js +45 -45
  134. package/services/encode/fingerprint.js +322 -322
  135. package/services/encode/host-load.js +174 -174
  136. package/services/encode/hwaccel.js +1894 -1894
  137. package/services/encode/index.js +9 -9
  138. package/services/encode/learned-median.js +90 -90
  139. package/services/encode/output/EncodedOutput.js +57 -57
  140. package/services/encode/output/Output.js +131 -131
  141. package/services/encode/output/OutputCatalog.js +414 -414
  142. package/services/encode/output/OutputSpec.js +390 -390
  143. package/services/encode/output/Timeline.js +308 -308
  144. package/services/encode/output/cut-grid.js +107 -107
  145. package/services/encode/output/index.js +20 -20
  146. package/services/encode/output/ladder.js +32 -32
  147. package/services/encode/output/playlists.js +284 -284
  148. package/services/encode/output/rates.js +125 -125
  149. package/services/encode/output-key-format.js +21 -21
  150. package/services/encode/quality/EncodeCost.js +1150 -1141
  151. package/services/encode/quality/HostLoad.js +525 -525
  152. package/services/encode/quality/HostTimings.js +249 -249
  153. package/services/encode/quality/OutputQualityState.js +28 -28
  154. package/services/encode/quality/QualityController.js +935 -935
  155. package/services/encode/quality/QualityOffer.js +416 -416
  156. package/services/encode/quality/drain-threat.js +47 -47
  157. package/services/encode/quality/link-budget.js +349 -254
  158. package/services/encode/quality/output-format.js +319 -315
  159. package/services/encode/quality/serving-output.js +216 -216
  160. package/services/encode/quality/visible-rung.js +57 -57
  161. package/services/encode/run-budget.js +97 -97
  162. package/services/encode/run-command.js +754 -754
  163. package/services/encode/run-costs.js +155 -155
  164. package/services/encode/run-inputs.js +42 -42
  165. package/services/encode/segment-formats/fmp4.js +456 -456
  166. package/services/encode/segment-formats/index.js +85 -85
  167. package/services/encode/segment-formats/init-compat.js +324 -324
  168. package/services/encode/segment-formats/mpegts.js +134 -134
  169. package/services/encode/start-stop-cost.js +178 -178
  170. package/services/encode/throughput.js +97 -97
  171. package/services/encode/torrent-cost.js +88 -88
  172. package/services/identity/installation-id.js +38 -38
  173. package/services/media/ContainerOrchestrator.js +149 -149
  174. package/services/media/KeyframeTables.js +316 -316
  175. package/services/media/SourceFile.js +298 -298
  176. package/services/media/SubtitleCues.js +520 -520
  177. package/services/media/SubtitleOrchestrator.js +167 -167
  178. package/services/media/audio-inventory.js +298 -245
  179. package/services/media/container/AviContainer.js +266 -266
  180. package/services/media/container/Container.js +609 -603
  181. package/services/media/container/ContainerFactory.js +56 -56
  182. package/services/media/container/KeyframeTable.js +272 -272
  183. package/services/media/container/MatroskaContainer.js +1494 -1494
  184. package/services/media/container/Mp4Container.js +1344 -1244
  185. package/services/media/container/ebml-reader.js +159 -159
  186. package/services/media/ffmpeg-banner.js +260 -260
  187. package/services/media/keyframe-probe.js +157 -157
  188. package/services/media/media-info-probe.js +93 -93
  189. package/services/media/playback-planner.js +931 -931
  190. package/services/media/tracks/AudioTrack.js +143 -131
  191. package/services/media/tracks/ContainerTrack.js +75 -75
  192. package/services/media/tracks/ImageSubtitleTrack.js +19 -19
  193. package/services/media/tracks/SubtitleTrack.js +38 -38
  194. package/services/media/tracks/TextSubtitleTrack.js +287 -287
  195. package/services/media/tracks/VideoTrack.js +33 -33
  196. package/services/media/tracks/audio-bound.js +233 -0
  197. package/services/media/tracks/index.js +15 -15
  198. package/services/media/tracks/language-detect.js +174 -174
  199. package/services/server/OutputLifecycle.js +418 -418
  200. package/services/server/SegmentServing.js +1150 -1150
  201. package/services/server/ViewerRequests.js +760 -760
  202. package/services/server/WaitLedger.js +142 -142
  203. package/services/server/controllers/PlaybackController.js +40 -40
  204. package/services/server/controllers/SubtitleController.js +109 -109
  205. package/services/server/http-responses.js +60 -60
  206. package/services/server/transcode-session-files.js +325 -325
  207. package/services/server/wire-outputs.js +995 -992
  208. package/services/storage/Diagnostics.js +202 -202
  209. package/services/storage/MachineBudget.js +235 -235
  210. package/services/storage/allowance.js +107 -107
  211. package/services/storage/core-dumps.js +106 -106
  212. package/services/storage/files/CompletedFiles.js +391 -391
  213. package/services/storage/files/piece-from-whole-file.js +118 -118
  214. package/services/storage/free.js +71 -71
  215. package/services/storage/keep.js +48 -48
  216. package/services/storage/machine-memory.js +56 -56
  217. package/services/storage/memory-report.js +575 -575
  218. package/services/storage/piece-store/piece-disk-store.js +539 -539
  219. package/services/storage/piece-store/piece-lru.js +495 -495
  220. package/services/storage/piece-store/shared-piece-store.js +1931 -1931
  221. package/services/storage/returns.js +103 -103
  222. package/services/storage/segment-store/SegmentStore.js +1240 -1240
  223. package/services/storage/wire.js +175 -175
  224. package/services/torrent/demand/DemandRegister.js +202 -202
  225. package/services/torrent/demand/Urgency.js +137 -137
  226. package/services/torrent/demand/Window.js +118 -118
  227. package/services/torrent/demand/index.js +8 -8
  228. package/services/torrent/demand/pieces.js +140 -140
  229. package/services/torrent/download/SwarmSelection.js +386 -386
  230. package/services/torrent/download/registry.js +102 -102
  231. package/services/torrent/download/withdraw-claim.js +80 -80
  232. package/services/torrent/episode-naming.js +182 -182
  233. package/services/torrent/files.js +455 -455
  234. package/services/torrent/naming.js +619 -619
  235. package/services/torrent/piece-store-of.js +29 -29
  236. package/services/torrent/supply-margin.js +262 -262
  237. package/services/torrent/torrent-pool.js +2675 -2675
  238. package/services/torrent/worker/background-fill.js +213 -213
  239. package/services/torrent/worker/channel.js +271 -271
  240. package/services/torrent/worker/client.js +765 -765
  241. package/services/torrent/worker/fastest-wires.js +342 -342
  242. package/services/torrent/worker/handle-state.js +23 -23
  243. package/services/torrent/worker/held-bytes.js +155 -155
  244. package/services/torrent/worker/install-webrtc-shim.js +32 -32
  245. package/services/torrent/worker/piece-reader.js +1078 -1078
  246. package/services/torrent/worker/pool-adapter.js +371 -371
  247. package/services/torrent/worker/protocol.js +184 -184
  248. package/services/torrent/worker/resume-warm.js +107 -107
  249. package/services/torrent/worker/webrtc-shim.js +219 -219
  250. package/services/torrent/worker/worker.js +1129 -1129
  251. package/services/transport/data-channel-handler.js +1369 -1369
  252. package/services/transport/delivery-probe.js +842 -842
  253. package/services/transport/health-collector.js +53 -53
  254. package/services/transport/nat-classifier.js +231 -231
  255. package/services/transport/packet-witness.js +808 -808
  256. package/services/transport/port-mapper.js +235 -235
  257. package/services/transport/registry-api.js +81 -81
  258. package/services/transport/tunnel-client.js +447 -447
  259. package/services/transport/usrsctp-state.js +175 -175
  260. package/services/transport/webrtc-manager.js +484 -484
  261. package/services/viewer/Assignments.js +340 -340
  262. package/services/viewer/PriorityMap.js +390 -390
  263. package/services/viewer/PriorityOrchestrator.js +325 -325
  264. package/services/viewer/Viewer.js +691 -691
  265. package/services/viewer/Viewers.js +368 -368
  266. package/services/viewer/active-output.js +37 -37
  267. package/services/viewer/audio-start.js +46 -46
  268. package/services/viewer/buffer-trend.js +81 -81
  269. package/services/viewer/choices.js +647 -647
  270. package/services/viewer/positions.js +126 -126
  271. package/services/viewer/report-intake.js +46 -46
  272. package/store/source-registry.js +71 -71
  273. package/test/asset-paths.test.js +28 -28
  274. package/test/audio-buffer-continuity.test.js +70 -70
  275. package/test/audio-inventory.test.js +212 -212
  276. package/test/audio-track-bitrate.test.js +40 -40
  277. package/test/auto-quality-step.test.js +707 -707
  278. package/test/available-share.test.js +76 -76
  279. package/test/background-fill.test.js +190 -190
  280. package/test/client-log-file.test.js +110 -110
  281. package/test/completed-files.test.js +115 -115
  282. package/test/concurrent-cost.test.js +148 -148
  283. package/test/container-media-info.test.js +228 -228
  284. package/test/contention.test.js +76 -76
  285. package/test/core-dumps.test.js +57 -57
  286. package/test/coverage-follows-the-disk.test.js +228 -228
  287. package/test/coverage-map.test.js +195 -195
  288. package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
  289. package/test/cut-times-timeline.test.js +65 -65
  290. package/test/cuts-follow-published-grid.test.js +109 -109
  291. package/test/data-channel-frame.test.js +63 -63
  292. package/test/data-channel-viewer-lifecycle.test.js +92 -92
  293. package/test/dead-torrent-handle.test.js +52 -52
  294. package/test/declared-rates.test.js +116 -116
  295. package/test/declared-tracks.test.js +43 -43
  296. package/test/decode-cost-fit.test.js +202 -202
  297. package/test/decode-cost.test.js +462 -462
  298. package/test/delivery-probe.test.js +393 -393
  299. package/test/demand-register.test.js +206 -206
  300. package/test/dht-bootstrap.test.js +72 -72
  301. package/test/drain-threat.test.js +65 -65
  302. package/test/duplicate-add.test.js +48 -48
  303. package/test/encode-admission.test.js +146 -146
  304. package/test/encode-cost.test.js +283 -283
  305. package/test/encode-exit.test.js +111 -111
  306. package/test/encode-orchestrator.test.js +419 -419
  307. package/test/encode-plan-viewers.test.js +718 -718
  308. package/test/encode-plan.test.js +545 -545
  309. package/test/encode-run-state.test.js +314 -314
  310. package/test/encode-run.test.js +162 -162
  311. package/test/encode-slope.test.js +46 -46
  312. package/test/encoded-output.test.js +39 -39
  313. package/test/encoder-kinds.test.js +122 -122
  314. package/test/encoder-readings.test.js +46 -46
  315. package/test/episode-naming.test.js +131 -131
  316. package/test/every-claimant-is-told-its-share.test.js +152 -152
  317. package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
  318. package/test/failed-start-breaker.test.js +202 -202
  319. package/test/fastest-wires.test.js +143 -143
  320. package/test/ffmpeg-stream-counts.test.js +78 -78
  321. package/test/file-edges.test.js +191 -191
  322. package/test/fixtures/audio-frame-boundary.json +1 -1
  323. package/test/fmp4-presentation-clock.test.js +147 -147
  324. package/test/fmp4-self-contained.test.js +80 -80
  325. package/test/fmp4-timestamps.test.js +140 -140
  326. package/test/fmp4-track-completeness.test.js +41 -41
  327. package/test/format-keyed-outputs.test.js +78 -78
  328. package/test/health-metrics.test.js +38 -38
  329. package/test/held-request-width.test.js +157 -157
  330. package/test/helpers/encode-run.js +136 -136
  331. package/test/helpers/held-file.js +29 -29
  332. package/test/helpers/manager.js +57 -57
  333. package/test/helpers/output-spec.js +63 -63
  334. package/test/host-load.test.js +68 -68
  335. package/test/index-accuracy-is-about-the-file.test.js +128 -128
  336. package/test/init-compat.test.js +219 -219
  337. package/test/input-lost-quiets-the-plan.test.js +261 -261
  338. package/test/input-unavailable.test.js +48 -48
  339. package/test/installation-id.test.js +39 -39
  340. package/test/keeping-period.test.js +83 -83
  341. package/test/keyframe-index-accuracy.test.js +188 -188
  342. package/test/keyframe-table-budget.test.js +102 -102
  343. package/test/keyframes-belong-to-the-file.test.js +249 -249
  344. package/test/learned-median.test.js +37 -37
  345. package/test/letting-go-asks-who-is-watching.test.js +191 -191
  346. package/test/limit-row-by-area.test.js +60 -60
  347. package/test/link-budget.test.js +112 -111
  348. package/test/link-probe.test.js +56 -56
  349. package/test/local-encoding-data.test.js +273 -273
  350. package/test/logger-repeats.test.js +134 -134
  351. package/test/look-ahead-coverage.test.js +56 -56
  352. package/test/manager-surface.test.js +292 -292
  353. package/test/matroska-cues-track.test.js +192 -192
  354. package/test/memory-budget.test.js +390 -390
  355. package/test/move-cost.test.js +216 -216
  356. package/test/mp4-subtitles.test.js +173 -173
  357. package/test/one-authority.test.js +285 -285
  358. package/test/one-budget.test.js +149 -149
  359. package/test/one-output-one-session.test.js +270 -204
  360. package/test/one-resource-divided.test.js +185 -185
  361. package/test/one-serving-output-per-height.test.js +458 -458
  362. package/test/orchestrator-wired.test.js +206 -206
  363. package/test/output-catalog.test.js +39 -39
  364. package/test/output-shape.test.js +21 -21
  365. package/test/output-spec.test.js +251 -251
  366. package/test/output-speed.test.js +88 -88
  367. package/test/outputs-being-prepared.test.js +40 -40
  368. package/test/packet-witness-ring.test.js +236 -236
  369. package/test/packet-witness.test.js +148 -148
  370. package/test/piece-disk-store.test.js +401 -401
  371. package/test/piece-from-whole-file.test.js +129 -129
  372. package/test/piece-lru.test.js +319 -319
  373. package/test/piece-reader.test.js +162 -162
  374. package/test/piece-selection-offset.test.js +85 -85
  375. package/test/piece-store-eviction.test.js +595 -595
  376. package/test/piece-store-never-refuses.test.js +153 -153
  377. package/test/piece-store-reservations.test.js +403 -403
  378. package/test/piece-store-slow-disk.test.js +159 -159
  379. package/test/piece-tail.test.js +97 -97
  380. package/test/plan-host-timings.test.js +68 -68
  381. package/test/plan-is-steady.test.js +225 -225
  382. package/test/playback-readiness.test.js +453 -453
  383. package/test/playback-start-regressions.test.js +99 -99
  384. package/test/priority-map-download.test.js +174 -174
  385. package/test/priority-map-emptied.test.js +204 -204
  386. package/test/priority-map-per-output.test.js +215 -215
  387. package/test/priority-map.test.js +163 -163
  388. package/test/probe-wedge-certainty.test.js +60 -60
  389. package/test/produced-copy-choice.test.js +213 -213
  390. package/test/published-timeline.test.js +83 -83
  391. package/test/quality-offer.test.js +162 -162
  392. package/test/quality-variants.test.js +1257 -1229
  393. package/test/rate-control-in-the-output.test.js +180 -180
  394. package/test/read-survives-withdrawal.test.js +164 -164
  395. package/test/read-window.test.js +365 -365
  396. package/test/resume-warm.test.js +39 -39
  397. package/test/run-graph-drift.test.js +37 -37
  398. package/test/run-inputs.test.js +80 -80
  399. package/test/run-intervals.test.js +100 -100
  400. package/test/run-position-follows-published-grid.test.js +89 -89
  401. package/test/run-progress.test.js +65 -65
  402. package/test/same-height-switch.test.js +664 -664
  403. package/test/seek-frees-the-viewers-requests.test.js +217 -217
  404. package/test/seek-landing.test.js +109 -109
  405. package/test/seek-target-not-superseded.test.js +195 -195
  406. package/test/segment-closed-on-both-branches.test.js +96 -96
  407. package/test/segment-cut-times.test.js +63 -63
  408. package/test/segment-demand.test.js +80 -80
  409. package/test/segment-hold-supersede.test.js +91 -91
  410. package/test/segment-judged-by-its-own-header.test.js +221 -221
  411. package/test/segment-serve-wiring.test.js +475 -475
  412. package/test/segment-store-eviction.test.js +247 -247
  413. package/test/segment-store.test.js +323 -323
  414. package/test/segment-true-start.test.js +86 -86
  415. package/test/segments-are-shared.test.js +176 -176
  416. package/test/send-chunk-bytes.test.js +86 -86
  417. package/test/serving-existing-output.test.js +105 -102
  418. package/test/serving-output.test.js +102 -102
  419. package/test/shared-piece-store.test.js +310 -310
  420. package/test/sidecar-naming.test.js +142 -142
  421. package/test/soundtrack-rate-bound.test.js +289 -0
  422. package/test/source-file.test.js +133 -133
  423. package/test/source-files-route.test.js +113 -113
  424. package/test/source-registry.test.js +18 -18
  425. package/test/split-a-stretch.test.js +139 -139
  426. package/test/stale-request-after-seek.test.js +158 -158
  427. package/test/startup-readings.test.js +124 -124
  428. package/test/stream-route.test.js +209 -209
  429. package/test/subtitle-cue-framing.test.js +200 -200
  430. package/test/subtitle-cue-source.test.js +102 -102
  431. package/test/subtitle-cue-walk.test.js +370 -370
  432. package/test/subtitle-cursor.test.js +63 -63
  433. package/test/subtitle-defaults.test.js +118 -118
  434. package/test/subtitle-language.test.js +252 -252
  435. package/test/subtitle-track-numbering.test.js +386 -386
  436. package/test/supply-margin.test.js +177 -177
  437. package/test/swarm-follows-readers.test.js +174 -174
  438. package/test/swarm-reach.test.js +130 -130
  439. package/test/swarm-selection.test.js +220 -220
  440. package/test/tail-duplication.test.js +167 -167
  441. package/test/the-channel-has-both-ends.test.js +91 -91
  442. package/test/the-torrent-thread-serves-bytes.test.js +107 -107
  443. package/test/thread-pool.test.js +53 -53
  444. package/test/timeline.test.js +84 -84
  445. package/test/torrent-contents.test.js +235 -235
  446. package/test/torrent-cost.test.js +71 -71
  447. package/test/torrent-files.test.js +221 -221
  448. package/test/torrent-naming.test.js +255 -255
  449. package/test/tracks-begin-together.test.js +209 -209
  450. package/test/tunnel-renewal.test.js +167 -167
  451. package/test/two-viewers-one-picture.test.js +504 -407
  452. package/test/upload-hurry.test.js +186 -186
  453. package/test/usrsctp-state.test.js +131 -131
  454. package/test/video-facts.test.js +102 -102
  455. package/test/viewer-assignments.test.js +143 -143
  456. package/test/viewer-outputs.test.js +285 -285
  457. package/test/viewer-presence.test.js +116 -116
  458. package/test/viewer-requests-admission.test.js +89 -89
  459. package/test/viewer-states.test.js +189 -189
  460. package/test/viewer-subtitle-subscription.test.js +96 -96
  461. package/test/viewer.test.js +139 -139
  462. package/test/viewing-generations.test.js +390 -390
  463. package/test/visible-rung.test.js +37 -37
  464. package/test/wait-ledger.test.js +100 -100
  465. package/test/webrtc-shim.test.js +149 -149
  466. package/test/wedge-certainty.test.js +131 -131
  467. package/test/withdraw-piece-claim.test.js +212 -212
  468. package/test/worker-channel.test.js +158 -158
  469. package/utils/client-log-file.js +151 -151
  470. package/utils/logger.js +312 -312
  471. package/utils/parse-range.js +25 -25
  472. package/utils/perf.js +59 -59
  473. package/utils/torrent-source-key.js +38 -38
@@ -1,462 +1,462 @@
1
- /**
2
- * @file What a re-encode costs on this host, decoding included.
3
- *
4
- * The budget used to price the encoder alone. Measured on the addon host
5
- * 2026-08-14, that made it offer a 240p rung it then ran at 0.388-0.947x while
6
- * claiming the host cleared the bar 2.5 times over — the error on that rung was
7
- * 209 %. The decode term brings a controlled measurement to within 5 %.
8
- *
9
- * WHAT IS ASSERTED HERE IS ARITHMETIC, over constants that were measured once
10
- * on the addon host and written down. The live benchmark used to run here too —
11
- * on the reasoning that a fit which only ever meets invented numbers can be
12
- * wrong in every way that matters — and it could not be asserted about: a clip
13
- * that "said nothing" under load makes the whole benchmark answer with nothing,
14
- * so the very first line of that test failed by luck. The orderings it was for
15
- * are held over the fit itself, deterministically, in `decode-cost-fit.test.js`.
16
- *
17
- * What no test covers as a result is the live path — lifting a clip out of its
18
- * container, feeding it through the pipe, reading the slope. That is integration
19
- * over real ffmpeg, and it belongs to a stand run before a release rather than
20
- * to a check that measures whatever else the machine was doing (roadmap item
21
- * 52).
22
- */
23
-
24
- import test from "node:test";
25
- import assert from "node:assert/strict";
26
- import { Timeline } from "../services/encode/output/Timeline.js";
27
- import { createRequire } from "node:module";
28
- import { spawn } from "node:child_process";
29
- import { mkdtemp, rm } from "node:fs/promises";
30
- import { decodeCostOf, decodeFamilyOf } from "../services/encode/decode-cost-fit.js";
31
- import { Output } from "../services/encode/output/Output.js";
32
- import { outputSpec } from "./helpers/output-spec.js";
33
- import os from "node:os";
34
- import path from "node:path";
35
- import {
36
- canSustainOutput,
37
- decodeSpeedFor,
38
- predictedRealtimeSpeed,
39
- speedBar
40
- } from "../services/encode/hwaccel.js";
41
- import { wireOutputs } from "../services/server/wire-outputs.js";
42
- import { SourceFile, sourceDecodeCharacteristics } from "../services/media/SourceFile.js";
43
- import { parseFfmpegBitrateKbps, parseFfmpegVideoDimensions, parseFfmpegVideoFps } from "../services/media/ffmpeg-banner.js";
44
- import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
45
-
46
- const require = createRequire(import.meta.url);
47
- const ffmpegBin = require("ffmpeg-static");
48
- const CALIBRATION_DIR = path.join(import.meta.dirname, "..", "assets", "calibration");
49
-
50
- // The constants measured on the addon host (CM4) on 2026-08-14. Used here as
51
- // a fixed host, so the arithmetic can be checked against figures that were
52
- // measured rather than against figures this test invented.
53
- const ADDON_HOST_MODEL = { pixelTerm: 0.005555, bitrateTerm: 0.00990, constantTerm: 0.0572 };
54
- // The film measured that day: 1920x1080 at 24 fps, about 8 Mbit/s.
55
- const MEASURED_FILM = { megapixelsPerSecond: (1920 * 1080 * 24) / 1e6, megabitsPerSecond: 8 };
56
-
57
- test("decode cost prices the film it was checked against", () => {
58
- const speed = decodeSpeedFor(ADDON_HOST_MODEL, MEASURED_FILM);
59
-
60
- // 0.005555 x 49.77 + 0.00990 x 8 + 0.0572 = 0.4129 s per second of video.
61
- assert.ok(Math.abs(1 / speed - 0.4129) < 0.001, `cost was ${(1 / speed).toFixed(4)} s/s`);
62
- // Measured that day: 0.434 s/s. The claim is 5 %, not exactness.
63
- assert.ok(Math.abs(1 / speed - 0.434) / 0.434 < 0.05);
64
- });
65
-
66
- test("decoding and encoding share the machine, so their speeds combine", () => {
67
- // The rung that broke playback: 240p at 24 fps, encoded at 5.99x by the
68
- // measurement, decoded at 2.31x. Measured combined speed was 1.48x.
69
- const outputPixelsPerSec = 426 * 240 * 24;
70
- const speed = predictedRealtimeSpeed({
71
- decodeModel: { pixelTerm: 0, bitrateTerm: 0, constantTerm: 1 / 2.31 },
72
- encodePixelsPerSec: outputPixelsPerSec * 5.99,
73
- outputPixelsPerSec,
74
- source: MEASURED_FILM
75
- });
76
-
77
- assert.ok(Math.abs(speed - 1.67) < 0.01, `predicted ${speed.toFixed(2)}x`);
78
- // 1.67 against the 1.48x that rung measured under a controlled encode: 12.8 %
79
- // out. The figure is pinned so a change to the combination has to state what
80
- // it does to it.
81
- assert.ok(Math.abs(speed - 1.48) / 1.48 < 0.13);
82
- });
83
-
84
- test("with no decode fit the prediction is the encoder alone — what it was before", () => {
85
- const outputPixelsPerSec = 426 * 240 * 24;
86
- const speed = predictedRealtimeSpeed({
87
- decodeModel: null,
88
- encodePixelsPerSec: outputPixelsPerSec * 5.99,
89
- outputPixelsPerSec,
90
- source: MEASURED_FILM
91
- });
92
-
93
- assert.equal(speed, 5.99, "the old figure, four times the truth on that rung");
94
- });
95
-
96
- test("a rung is refused when the combined speed is under the bar", () => {
97
- // The addon host's fastest preset, read from its own log: 11.2 Mpx/s.
98
- const benchmark = [{ preset: "ultrafast", pixelsPerSec: 11.2e6 }];
99
- const source = MEASURED_FILM;
100
-
101
- const heavy = canSustainOutput({
102
- benchmark,
103
- decodeModel: ADDON_HOST_MODEL,
104
- source,
105
- outputPixelsPerSec: 1280 * 720 * 24
106
- });
107
- assert.equal(heavy.sustainable, false, "720p needs 22 Mpx/s from an 11.2 Mpx/s host");
108
- assert.ok(heavy.speed < 1, `predicted ${heavy.speed.toFixed(2)}x`);
109
-
110
- // The 240p rung of that film predicts 1.58x on an idle machine, and with
111
- // nothing known about the swarm the bar is realtime, so it is offered.
112
- const light = canSustainOutput({
113
- benchmark,
114
- decodeModel: ADDON_HOST_MODEL,
115
- source,
116
- outputPixelsPerSec: 426 * 240 * 24
117
- });
118
- assert.ok(Math.abs(light.speed - 1.58) < 0.01, `predicted ${light.speed.toFixed(2)}x`);
119
- assert.equal(light.sustainable, true);
120
- });
121
-
122
- test("the bar is what this file's own supply demands, when it has been measured", () => {
123
- // The field torrent of 2026-08-17: waits of 1.49 s median arriving every
124
- // 2.22 s demand 1.67x of any step that is to survive them. The same 240p rung
125
- // predicted at 1.58x clears realtime and does not clear that.
126
- const benchmark = [{ preset: "ultrafast", pixelsPerSec: 11.2e6 }];
127
- const rung = {
128
- benchmark,
129
- decodeModel: ADDON_HOST_MODEL,
130
- source: MEASURED_FILM,
131
- outputPixelsPerSec: 426 * 240 * 24
132
- };
133
- assert.equal(canSustainOutput({ ...rung, requiredSpeed: 1.67 }).sustainable, false);
134
- assert.equal(canSustainOutput({ ...rung, requiredSpeed: 1.2 }).sustainable, true);
135
- // A swarm that has not been measured cannot raise the bar, and cannot lower
136
- // it below realtime either.
137
- assert.equal(speedBar(null), 1);
138
- assert.equal(speedBar(0.4), 1);
139
- assert.equal(speedBar(1.67), 1.67);
140
- });
141
-
142
- test("a reading from the running encoder outranks the model of the clips", () => {
143
- // The field case, 2026-08-14: the 240p rung of that film ran at 0.95x at its
144
- // best on a host whose fastest preset benchmarked at 11.2 Mpx/s. Subtracting
145
- // the encode half of that reading leaves what the SOURCE costs to decode
146
- // there — 0.83 s per second of video, i.e. 1.20x — which is four times what
147
- // the H.264 clips predicted for it, and is the truth about this file.
148
- const benchmark = [{ preset: "ultrafast", pixelsPerSec: 11.2e6 }];
149
- const observedDecodeCostSec = 1 / 0.95 - (426 * 240 * 24) / 11.2e6;
150
- assert.ok(Math.abs(observedDecodeCostSec - 0.8335) < 0.001);
151
-
152
- const withObservation = canSustainOutput({
153
- benchmark,
154
- decodeModel: ADDON_HOST_MODEL,
155
- source: MEASURED_FILM,
156
- outputPixelsPerSec: 426 * 240 * 24,
157
- observedDecodeCostSec
158
- });
159
-
160
- // The same rung the clip model called 1.58x and admitted is now priced at
161
- // 0.95x — the speed it was actually seen to run at — and refused.
162
- assert.ok(Math.abs(withObservation.speed - 0.95) < 0.01, `priced ${withObservation.speed.toFixed(2)}x`);
163
- assert.equal(withObservation.sustainable, false);
164
- });
165
-
166
- test("an observation prices a host whose clips were never fitted", () => {
167
- // No model at all — clips missing, or a fit rejected. Before an observation
168
- // nothing can be refused; after one, the same rung is priced and refused.
169
- const benchmark = [{ preset: "ultrafast", pixelsPerSec: 11.2e6 }];
170
- const outputPixelsPerSec = 426 * 240 * 24;
171
-
172
- const unpriced = canSustainOutput({
173
- benchmark,
174
- decodeModel: null,
175
- source: MEASURED_FILM,
176
- outputPixelsPerSec
177
- });
178
- assert.deepEqual(unpriced, { speed: null, sustainable: true });
179
-
180
- const priced = canSustainOutput({
181
- benchmark,
182
- decodeModel: null,
183
- source: MEASURED_FILM,
184
- outputPixelsPerSec,
185
- observedDecodeCostSec: 0.8335
186
- });
187
- assert.equal(priced.sustainable, false);
188
- assert.ok(priced.speed < 1, `priced ${priced.speed?.toFixed(2)}x`);
189
- });
190
-
191
- test("not calibrated means nothing judged; calibrated with nothing qualified means refused", () => {
192
- const uncalibrated = canSustainOutput({
193
- benchmark: null,
194
- decodeModel: null,
195
- source: null,
196
- outputPixelsPerSec: 1920 * 1080 * 24
197
- });
198
- assert.deepEqual(uncalibrated, { speed: null, sustainable: true });
199
-
200
- const nothingQualified = canSustainOutput({
201
- benchmark: [],
202
- decodeModel: null,
203
- source: null,
204
- outputPixelsPerSec: 1920 * 1080 * 24
205
- });
206
- assert.deepEqual(nothingQualified, { speed: null, sustainable: false });
207
- });
208
-
209
- test("a real ffmpeg banner reads into the figures the budget prices", async () => {
210
- // The chain a session actually runs: ffmpeg prints its banner, the four
211
- // readers take the four facts out of it, and `sourceDecodeCharacteristics`
212
- // turns them into the two the fit uses. Run against a real file, because the
213
- // bitrate reader is new and a banner is the one input nobody can invent
214
- // faithfully.
215
- const stderr = await new Promise((resolve) => {
216
- let text = "";
217
- const child = spawn(
218
- ffmpegBin,
219
- ["-hide_banner", "-loglevel", "info", "-i", path.join(CALIBRATION_DIR, "cal-h264-720-hi.mp4"), "-t", "0.1", "-f", "null", "-"],
220
- { stdio: ["ignore", "ignore", "pipe"], windowsHide: true }
221
- );
222
- child.stderr.on("data", (chunk) => {
223
- text += String(chunk);
224
- });
225
- child.on("close", () => resolve(text));
226
- });
227
-
228
- // The VIDEO stream's rate (9940), not the container's (9946). On a clip that
229
- // carries nothing else the two differ only by container overhead; on a film
230
- // with two AC-3 tracks they differ by the whole of the audio, and the fit
231
- // these figures feed was made from clips decoded with `-an`.
232
- assert.equal(parseFfmpegBitrateKbps(stderr), 9940, "the video stream's own bitrate, in kb/s");
233
- const dimensions = parseFfmpegVideoDimensions(stderr);
234
- const figures = sourceDecodeCharacteristics({
235
- width: dimensions.width,
236
- height: dimensions.height,
237
- fps: parseFfmpegVideoFps(stderr),
238
- bitrateKbps: parseFfmpegBitrateKbps(stderr)
239
- });
240
- assert.ok(Math.abs(figures.megapixelsPerSecond - (1280 * 720 * 24) / 1e6) < 0.01);
241
- assert.ok(Math.abs(figures.megabitsPerSecond - 9.94) < 0.001);
242
- });
243
-
244
- test("the video stream's bitrate is preferred, and the output's is never read", () => {
245
- // A real banner states three rates: the container's, the input video
246
- // stream's, and — below "Stream mapping:" — the one ffmpeg is about to
247
- // produce. Only the middle one describes the source.
248
- const banner = [
249
- " Duration: 01:31:19.00, start: 0.000000, bitrate: 9500 kb/s",
250
- " Stream #0:0: Video: h264 (High), yuv420p, 1920x1080, 7800 kb/s, 23.98 fps",
251
- " Stream #0:1: Audio: ac3, 48000 Hz, 5.1, fltp, 640 kb/s",
252
- "Stream mapping:",
253
- " Stream #0:0(und): Video: wrapped_avframe, yuv420p, 1920x1080, q=2-31, 200 kb/s, 24 fps"
254
- ].join("\n");
255
- assert.equal(parseFfmpegBitrateKbps(banner), 7800);
256
-
257
- // No per-stream rate: the container's stands in, as it always did.
258
- const noStreamRate = [
259
- " Duration: 00:10:00.00, bitrate: 4200 kb/s",
260
- " Stream #0:0: Video: hevc (Main 10), yuv420p10le, 1920x1080, 23.98 fps"
261
- ].join("\n");
262
- assert.equal(parseFfmpegBitrateKbps(noStreamRate), 4200);
263
- });
264
-
265
- test("a banner with no bitrate reads as no bitrate, not as zero", () => {
266
- assert.equal(parseFfmpegBitrateKbps("Duration: 00:01:00.00, start: 0.000000\n"), null);
267
- assert.equal(parseFfmpegBitrateKbps(""), null);
268
- assert.equal(parseFfmpegBitrateKbps("Duration: 00:01:00.00, start: 0.000, bitrate: 8000 kb/s"), 8000);
269
- });
270
-
271
- test("the source's decode figures come off the probe, or not at all", () => {
272
- assert.deepEqual(sourceDecodeCharacteristics({ width: 1920, height: 1080, fps: 24, bitrateKbps: 8000 }), {
273
- megapixelsPerSecond: (1920 * 1080 * 24) / 1e6,
274
- megabitsPerSecond: 8,
275
- // Which measurement of this host applies. Absent on the probe means absent
276
- // here — the caller then prices the source as H.264 8-bit, which is what
277
- // every source was priced as before the model was fitted per family.
278
- codec: "",
279
- bitDepth: null
280
- });
281
- assert.deepEqual(
282
- sourceDecodeCharacteristics({ width: 1920, height: 1080, fps: 24, bitrateKbps: 8000, codec: "hevc", bitDepth: 10 }),
283
- {
284
- megapixelsPerSecond: (1920 * 1080 * 24) / 1e6,
285
- megabitsPerSecond: 8,
286
- codec: "hevc",
287
- bitDepth: 10
288
- }
289
- );
290
- assert.equal(sourceDecodeCharacteristics({ width: 1920, height: 1080, fps: 24, bitrateKbps: null }), null);
291
- assert.equal(sourceDecodeCharacteristics(null), null);
292
- });
293
-
294
- test("the OFFER drops the rungs the host cannot hold, and the master keeps addressing them", async (t) => {
295
- const dirPath = await mkdtemp(path.join(os.tmpdir(), "decode-cost-"));
296
- const manager = wireOutputs({
297
- enabled: true,
298
- ffmpegBin: "ffmpeg",
299
- localBindHost: "127.0.0.1",
300
- localPort: 9090,
301
- // A host that can encode a little: 3 Mpx/s, so only the smallest rungs of a
302
- // 1080p source can be produced faster than they are watched.
303
- softwarePresetBenchmark: [{ preset: "ultrafast", pixelsPerSec: 3e6 }],
304
- decodeCostModel: ADDON_HOST_MODEL
305
- });
306
- t.after(async () => {
307
- await manager.lifecycle.disposeAll();
308
- await rm(dirPath, { recursive: true, force: true });
309
- });
310
-
311
- const session = {
312
- id: "ccccccccddddeeee",
313
- spec: outputSpec({ transcodeVideo: false, cutGrid: "keyframe" }),
314
- dirPath,
315
- // Where this file is cut, held by the file. A fixture that stated it
316
- // on the session was describing what production no longer does.
317
- timeline: new Timeline({
318
- boundaries: Array.from({ length: 101 }, (_, index) => index * 4),
319
- cutGrid: "keyframe"
320
- }),
321
- state: "ready",
322
- // The file's own facts: the picture's size, and what decoding it costs,
323
- // which is derived from them rather than stated beside them. These are the
324
- // measured film of MEASURED_FILM above — 1080p24 at 8 Mbit/s.
325
- file: new SourceFile({ sourceKey: "source-1", fileIndex: 0, name: "video.mkv" }).learn({
326
- width: 1920,
327
- height: 1080,
328
- fps: 24,
329
- bitrateKbps: 8000
330
- }),
331
- // An ordinary session reads its own file, and its sound is inside it. The
332
- // three differ only for a soundtrack shipped as a file of its own.
333
- get inputFile() { return this.file; },
334
- get audioFile() { return this.file; },
335
- startedAt: Date.now(),
336
- lastAccessedAt: Date.now(),
337
- ffmpeg: null,
338
- lastError: "",
339
- consumers: new Set(),
340
- segmentFormat: fmp4Format,
341
- transcodeVideo: false,
342
- transcodeAudio: true,
343
- audioTrackIndex: 0,
344
- // A copy can only be cut where the source already has a keyframe, and a
345
- // master is offered only when that grid is real.
346
- // The shape this output is encoded AS, decided once for the output.
347
- output: new Output({
348
- encodeWidth: 0,
349
- encodeHeight: 0,
350
- outputFps: 24,
351
- softwarePreset: null,
352
- applyTonemap: false
353
- }),
354
- usesExplicitCuts: true,
355
- useSyntheticPlaylist: true,
356
- playlistText: "#EXTM3U\n",
357
- segmentCount: 100,
358
- progress: { state: "running", processedSeconds: 0, startPositionSeconds: 0, speed: "1.0x" }
359
- };
360
- manager.outputs.set(session.id, session);
361
-
362
- assert.deepEqual(
363
- manager.quality.offeredHeights(session),
364
- [1080],
365
- "every rung under the copy runs below realtime here, so there is nothing to switch to"
366
- );
367
- // The master is NOT that answer. It says which rungs can be spliced onto this
368
- // cut grid, which is a fact about the file, and it has to hold still for the
369
- // session's life: the browser is handed its address at creation, and a live
370
- // figure that withdrew it left a session answering 404 to itself (field
371
- // 2026-08-18, "Moana (2016).mkv" — nothing played at all).
372
- const weakMaster = manager.renditions.buildMasterPlaylist(session.id);
373
- assert.ok(weakMaster, "published once, whatever the host is managing this second");
374
- assert.deepEqual(
375
- [...weakMaster.matchAll(/^v\/(\d+)\/index\.m3u8$/gm)].map((match) => Number(match[1])),
376
- [1080, 720, 540, 480, 360, 240],
377
- "the ladder of the source, addressable — the menu the viewer sees is offeredHeights"
378
- );
379
-
380
- // A host with a little more encoder keeps the rungs it can actually hold. A
381
- // second session, because the answer is settled once per session.
382
- manager.softwarePresetBenchmark = [{ preset: "ultrafast", pixelsPerSec: 12e6 }];
383
- const stronger = { ...session, id: "ddddddddeeeeffff", offeredHeightsCache: undefined };
384
- manager.outputs.set(stronger.id, stronger);
385
- assert.deepEqual(
386
- manager.quality.offeredHeights(stronger),
387
- [1080, 360, 240],
388
- "nothing is known about this swarm, so the bar is realtime"
389
- );
390
- const master = manager.renditions.buildMasterPlaylist(stronger.id);
391
- assert.ok(master, "1080p copied plus every rung that can be spliced beside it");
392
- assert.deepEqual(
393
- [...master.matchAll(/^v\/(\d+)\/index\.m3u8$/gm)].map((match) => Number(match[1])),
394
- [1080, 720, 540, 480, 360, 240],
395
- "the same published set as before: what the host manages is the offer's business, not the document's"
396
- );
397
-
398
- // The same host, once the reader has measured what this file's supply
399
- // demands: waits arriving as they did on the field torrent of 2026-08-17 ask
400
- // 1.67x of any step, and the rungs that only just cleared realtime go.
401
- const onAThinSwarm = {
402
- ...stronger,
403
- id: "eeeeeeeeffff0000",
404
- offeredHeightsCache: undefined,
405
- supplyFigures: { requiredSpeed: 1.67, worstWaitSec: 1.49, medianIntervalSec: 2.22, samples: 12 }
406
- };
407
- manager.outputs.set(onAThinSwarm.id, onAThinSwarm);
408
- assert.deepEqual(
409
- manager.quality.offeredHeights(onAThinSwarm),
410
- [1080],
411
- "the copied height costs no encoder and stays; nothing re-encoded survives that supply"
412
- );
413
- });
414
-
415
- test("a source is priced by its own codec family when that family was measured", () => {
416
- // A model as the startup benchmark now returns it: H.264 terms at the top
417
- // level, for a caller that knows nothing about codecs, and the measured
418
- // families beside them.
419
- const model = {
420
- pixelTerm: 0.006, bitrateTerm: 0.012, constantTerm: 0,
421
- families: {
422
- h264: { pixelTerm: 0.006, bitrateTerm: 0.012, constantTerm: 0 },
423
- hevc: { pixelTerm: 0.011, bitrateTerm: 0.020, constantTerm: 0 },
424
- hevc10: { pixelTerm: 0.017, bitrateTerm: 0.026, constantTerm: 0 }
425
- }
426
- };
427
- const rates = { megapixelsPerSecond: 50, megabitsPerSecond: 9 };
428
- const asH264 = decodeCostOf(model, { ...rates, codec: "h264", bitDepth: 8 });
429
- const asHevc = decodeCostOf(model, { ...rates, codec: "hevc", bitDepth: 8 });
430
- const asHevc10 = decodeCostOf(model, { ...rates, codec: "hevc", bitDepth: 10 });
431
- assert.equal(Number(asH264.toFixed(4)), 0.408);
432
- assert.equal(Number(asHevc.toFixed(4)), 0.73);
433
- assert.equal(Number(asHevc10.toFixed(4)), 1.084);
434
- // The whole point: the same file costs more as HEVC than as H.264, and more
435
- // again at ten bits. A single fit could not say that.
436
- assert.ok(asHevc > asH264 && asHevc10 > asHevc);
437
- });
438
-
439
- test("a family with no clips is priced as H.264, and a model with no families still works", () => {
440
- const withFamilies = {
441
- pixelTerm: 0.006, bitrateTerm: 0.012, constantTerm: 0,
442
- families: { h264: { pixelTerm: 0.006, bitrateTerm: 0.012, constantTerm: 0 } }
443
- };
444
- const rates = { megapixelsPerSecond: 50, megabitsPerSecond: 9 };
445
- // AV1 has no set of its own yet.
446
- assert.equal(
447
- decodeCostOf(withFamilies, { ...rates, codec: "av1", bitDepth: 8 }),
448
- decodeCostOf(withFamilies, { ...rates, codec: "h264", bitDepth: 8 })
449
- );
450
- // And a flat model — every model before this release — is unchanged.
451
- const flat = { pixelTerm: 0.006, bitrateTerm: 0.012, constantTerm: 0 };
452
- assert.equal(decodeCostOf(flat, { ...rates, codec: "hevc", bitDepth: 10 }), 0.408);
453
- });
454
-
455
- test("the family is chosen by codec and depth, and unknown names fall to H.264", () => {
456
- assert.equal(decodeFamilyOf({ codec: "hevc", bitDepth: 8 }), "hevc");
457
- assert.equal(decodeFamilyOf({ codec: "HEVC", bitDepth: 10 }), "hevc10");
458
- assert.equal(decodeFamilyOf({ codec: "h265", bitDepth: 12 }), "hevc10");
459
- assert.equal(decodeFamilyOf({ codec: "h264", bitDepth: 10 }), "h264");
460
- assert.equal(decodeFamilyOf({ codec: "vc1", bitDepth: null }), "h264");
461
- assert.equal(decodeFamilyOf({}), "h264");
462
- });
1
+ /**
2
+ * @file What a re-encode costs on this host, decoding included.
3
+ *
4
+ * The budget used to price the encoder alone. Measured on the addon host
5
+ * 2026-08-14, that made it offer a 240p rung it then ran at 0.388-0.947x while
6
+ * claiming the host cleared the bar 2.5 times over — the error on that rung was
7
+ * 209 %. The decode term brings a controlled measurement to within 5 %.
8
+ *
9
+ * WHAT IS ASSERTED HERE IS ARITHMETIC, over constants that were measured once
10
+ * on the addon host and written down. The live benchmark used to run here too —
11
+ * on the reasoning that a fit which only ever meets invented numbers can be
12
+ * wrong in every way that matters — and it could not be asserted about: a clip
13
+ * that "said nothing" under load makes the whole benchmark answer with nothing,
14
+ * so the very first line of that test failed by luck. The orderings it was for
15
+ * are held over the fit itself, deterministically, in `decode-cost-fit.test.js`.
16
+ *
17
+ * What no test covers as a result is the live path — lifting a clip out of its
18
+ * container, feeding it through the pipe, reading the slope. That is integration
19
+ * over real ffmpeg, and it belongs to a stand run before a release rather than
20
+ * to a check that measures whatever else the machine was doing (roadmap item
21
+ * 52).
22
+ */
23
+
24
+ import test from "node:test";
25
+ import assert from "node:assert/strict";
26
+ import { Timeline } from "../services/encode/output/Timeline.js";
27
+ import { createRequire } from "node:module";
28
+ import { spawn } from "node:child_process";
29
+ import { mkdtemp, rm } from "node:fs/promises";
30
+ import { decodeCostOf, decodeFamilyOf } from "../services/encode/decode-cost-fit.js";
31
+ import { Output } from "../services/encode/output/Output.js";
32
+ import { outputSpec } from "./helpers/output-spec.js";
33
+ import os from "node:os";
34
+ import path from "node:path";
35
+ import {
36
+ canSustainOutput,
37
+ decodeSpeedFor,
38
+ predictedRealtimeSpeed,
39
+ speedBar
40
+ } from "../services/encode/hwaccel.js";
41
+ import { wireOutputs } from "../services/server/wire-outputs.js";
42
+ import { SourceFile, sourceDecodeCharacteristics } from "../services/media/SourceFile.js";
43
+ import { parseFfmpegBitrateKbps, parseFfmpegVideoDimensions, parseFfmpegVideoFps } from "../services/media/ffmpeg-banner.js";
44
+ import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
45
+
46
+ const require = createRequire(import.meta.url);
47
+ const ffmpegBin = require("ffmpeg-static");
48
+ const CALIBRATION_DIR = path.join(import.meta.dirname, "..", "assets", "calibration");
49
+
50
+ // The constants measured on the addon host (CM4) on 2026-08-14. Used here as
51
+ // a fixed host, so the arithmetic can be checked against figures that were
52
+ // measured rather than against figures this test invented.
53
+ const ADDON_HOST_MODEL = { pixelTerm: 0.005555, bitrateTerm: 0.00990, constantTerm: 0.0572 };
54
+ // The film measured that day: 1920x1080 at 24 fps, about 8 Mbit/s.
55
+ const MEASURED_FILM = { megapixelsPerSecond: (1920 * 1080 * 24) / 1e6, megabitsPerSecond: 8 };
56
+
57
+ test("decode cost prices the film it was checked against", () => {
58
+ const speed = decodeSpeedFor(ADDON_HOST_MODEL, MEASURED_FILM);
59
+
60
+ // 0.005555 x 49.77 + 0.00990 x 8 + 0.0572 = 0.4129 s per second of video.
61
+ assert.ok(Math.abs(1 / speed - 0.4129) < 0.001, `cost was ${(1 / speed).toFixed(4)} s/s`);
62
+ // Measured that day: 0.434 s/s. The claim is 5 %, not exactness.
63
+ assert.ok(Math.abs(1 / speed - 0.434) / 0.434 < 0.05);
64
+ });
65
+
66
+ test("decoding and encoding share the machine, so their speeds combine", () => {
67
+ // The rung that broke playback: 240p at 24 fps, encoded at 5.99x by the
68
+ // measurement, decoded at 2.31x. Measured combined speed was 1.48x.
69
+ const outputPixelsPerSec = 426 * 240 * 24;
70
+ const speed = predictedRealtimeSpeed({
71
+ decodeModel: { pixelTerm: 0, bitrateTerm: 0, constantTerm: 1 / 2.31 },
72
+ encodePixelsPerSec: outputPixelsPerSec * 5.99,
73
+ outputPixelsPerSec,
74
+ source: MEASURED_FILM
75
+ });
76
+
77
+ assert.ok(Math.abs(speed - 1.67) < 0.01, `predicted ${speed.toFixed(2)}x`);
78
+ // 1.67 against the 1.48x that rung measured under a controlled encode: 12.8 %
79
+ // out. The figure is pinned so a change to the combination has to state what
80
+ // it does to it.
81
+ assert.ok(Math.abs(speed - 1.48) / 1.48 < 0.13);
82
+ });
83
+
84
+ test("with no decode fit the prediction is the encoder alone — what it was before", () => {
85
+ const outputPixelsPerSec = 426 * 240 * 24;
86
+ const speed = predictedRealtimeSpeed({
87
+ decodeModel: null,
88
+ encodePixelsPerSec: outputPixelsPerSec * 5.99,
89
+ outputPixelsPerSec,
90
+ source: MEASURED_FILM
91
+ });
92
+
93
+ assert.equal(speed, 5.99, "the old figure, four times the truth on that rung");
94
+ });
95
+
96
+ test("a rung is refused when the combined speed is under the bar", () => {
97
+ // The addon host's fastest preset, read from its own log: 11.2 Mpx/s.
98
+ const benchmark = [{ preset: "ultrafast", pixelsPerSec: 11.2e6 }];
99
+ const source = MEASURED_FILM;
100
+
101
+ const heavy = canSustainOutput({
102
+ benchmark,
103
+ decodeModel: ADDON_HOST_MODEL,
104
+ source,
105
+ outputPixelsPerSec: 1280 * 720 * 24
106
+ });
107
+ assert.equal(heavy.sustainable, false, "720p needs 22 Mpx/s from an 11.2 Mpx/s host");
108
+ assert.ok(heavy.speed < 1, `predicted ${heavy.speed.toFixed(2)}x`);
109
+
110
+ // The 240p rung of that film predicts 1.58x on an idle machine, and with
111
+ // nothing known about the swarm the bar is realtime, so it is offered.
112
+ const light = canSustainOutput({
113
+ benchmark,
114
+ decodeModel: ADDON_HOST_MODEL,
115
+ source,
116
+ outputPixelsPerSec: 426 * 240 * 24
117
+ });
118
+ assert.ok(Math.abs(light.speed - 1.58) < 0.01, `predicted ${light.speed.toFixed(2)}x`);
119
+ assert.equal(light.sustainable, true);
120
+ });
121
+
122
+ test("the bar is what this file's own supply demands, when it has been measured", () => {
123
+ // The field torrent of 2026-08-17: waits of 1.49 s median arriving every
124
+ // 2.22 s demand 1.67x of any step that is to survive them. The same 240p rung
125
+ // predicted at 1.58x clears realtime and does not clear that.
126
+ const benchmark = [{ preset: "ultrafast", pixelsPerSec: 11.2e6 }];
127
+ const rung = {
128
+ benchmark,
129
+ decodeModel: ADDON_HOST_MODEL,
130
+ source: MEASURED_FILM,
131
+ outputPixelsPerSec: 426 * 240 * 24
132
+ };
133
+ assert.equal(canSustainOutput({ ...rung, requiredSpeed: 1.67 }).sustainable, false);
134
+ assert.equal(canSustainOutput({ ...rung, requiredSpeed: 1.2 }).sustainable, true);
135
+ // A swarm that has not been measured cannot raise the bar, and cannot lower
136
+ // it below realtime either.
137
+ assert.equal(speedBar(null), 1);
138
+ assert.equal(speedBar(0.4), 1);
139
+ assert.equal(speedBar(1.67), 1.67);
140
+ });
141
+
142
+ test("a reading from the running encoder outranks the model of the clips", () => {
143
+ // The field case, 2026-08-14: the 240p rung of that film ran at 0.95x at its
144
+ // best on a host whose fastest preset benchmarked at 11.2 Mpx/s. Subtracting
145
+ // the encode half of that reading leaves what the SOURCE costs to decode
146
+ // there — 0.83 s per second of video, i.e. 1.20x — which is four times what
147
+ // the H.264 clips predicted for it, and is the truth about this file.
148
+ const benchmark = [{ preset: "ultrafast", pixelsPerSec: 11.2e6 }];
149
+ const observedDecodeCostSec = 1 / 0.95 - (426 * 240 * 24) / 11.2e6;
150
+ assert.ok(Math.abs(observedDecodeCostSec - 0.8335) < 0.001);
151
+
152
+ const withObservation = canSustainOutput({
153
+ benchmark,
154
+ decodeModel: ADDON_HOST_MODEL,
155
+ source: MEASURED_FILM,
156
+ outputPixelsPerSec: 426 * 240 * 24,
157
+ observedDecodeCostSec
158
+ });
159
+
160
+ // The same rung the clip model called 1.58x and admitted is now priced at
161
+ // 0.95x — the speed it was actually seen to run at — and refused.
162
+ assert.ok(Math.abs(withObservation.speed - 0.95) < 0.01, `priced ${withObservation.speed.toFixed(2)}x`);
163
+ assert.equal(withObservation.sustainable, false);
164
+ });
165
+
166
+ test("an observation prices a host whose clips were never fitted", () => {
167
+ // No model at all — clips missing, or a fit rejected. Before an observation
168
+ // nothing can be refused; after one, the same rung is priced and refused.
169
+ const benchmark = [{ preset: "ultrafast", pixelsPerSec: 11.2e6 }];
170
+ const outputPixelsPerSec = 426 * 240 * 24;
171
+
172
+ const unpriced = canSustainOutput({
173
+ benchmark,
174
+ decodeModel: null,
175
+ source: MEASURED_FILM,
176
+ outputPixelsPerSec
177
+ });
178
+ assert.deepEqual(unpriced, { speed: null, sustainable: true });
179
+
180
+ const priced = canSustainOutput({
181
+ benchmark,
182
+ decodeModel: null,
183
+ source: MEASURED_FILM,
184
+ outputPixelsPerSec,
185
+ observedDecodeCostSec: 0.8335
186
+ });
187
+ assert.equal(priced.sustainable, false);
188
+ assert.ok(priced.speed < 1, `priced ${priced.speed?.toFixed(2)}x`);
189
+ });
190
+
191
+ test("not calibrated means nothing judged; calibrated with nothing qualified means refused", () => {
192
+ const uncalibrated = canSustainOutput({
193
+ benchmark: null,
194
+ decodeModel: null,
195
+ source: null,
196
+ outputPixelsPerSec: 1920 * 1080 * 24
197
+ });
198
+ assert.deepEqual(uncalibrated, { speed: null, sustainable: true });
199
+
200
+ const nothingQualified = canSustainOutput({
201
+ benchmark: [],
202
+ decodeModel: null,
203
+ source: null,
204
+ outputPixelsPerSec: 1920 * 1080 * 24
205
+ });
206
+ assert.deepEqual(nothingQualified, { speed: null, sustainable: false });
207
+ });
208
+
209
+ test("a real ffmpeg banner reads into the figures the budget prices", async () => {
210
+ // The chain a session actually runs: ffmpeg prints its banner, the four
211
+ // readers take the four facts out of it, and `sourceDecodeCharacteristics`
212
+ // turns them into the two the fit uses. Run against a real file, because the
213
+ // bitrate reader is new and a banner is the one input nobody can invent
214
+ // faithfully.
215
+ const stderr = await new Promise((resolve) => {
216
+ let text = "";
217
+ const child = spawn(
218
+ ffmpegBin,
219
+ ["-hide_banner", "-loglevel", "info", "-i", path.join(CALIBRATION_DIR, "cal-h264-720-hi.mp4"), "-t", "0.1", "-f", "null", "-"],
220
+ { stdio: ["ignore", "ignore", "pipe"], windowsHide: true }
221
+ );
222
+ child.stderr.on("data", (chunk) => {
223
+ text += String(chunk);
224
+ });
225
+ child.on("close", () => resolve(text));
226
+ });
227
+
228
+ // The VIDEO stream's rate (9940), not the container's (9946). On a clip that
229
+ // carries nothing else the two differ only by container overhead; on a film
230
+ // with two AC-3 tracks they differ by the whole of the audio, and the fit
231
+ // these figures feed was made from clips decoded with `-an`.
232
+ assert.equal(parseFfmpegBitrateKbps(stderr), 9940, "the video stream's own bitrate, in kb/s");
233
+ const dimensions = parseFfmpegVideoDimensions(stderr);
234
+ const figures = sourceDecodeCharacteristics({
235
+ width: dimensions.width,
236
+ height: dimensions.height,
237
+ fps: parseFfmpegVideoFps(stderr),
238
+ bitrateKbps: parseFfmpegBitrateKbps(stderr)
239
+ });
240
+ assert.ok(Math.abs(figures.megapixelsPerSecond - (1280 * 720 * 24) / 1e6) < 0.01);
241
+ assert.ok(Math.abs(figures.megabitsPerSecond - 9.94) < 0.001);
242
+ });
243
+
244
+ test("the video stream's bitrate is preferred, and the output's is never read", () => {
245
+ // A real banner states three rates: the container's, the input video
246
+ // stream's, and — below "Stream mapping:" — the one ffmpeg is about to
247
+ // produce. Only the middle one describes the source.
248
+ const banner = [
249
+ " Duration: 01:31:19.00, start: 0.000000, bitrate: 9500 kb/s",
250
+ " Stream #0:0: Video: h264 (High), yuv420p, 1920x1080, 7800 kb/s, 23.98 fps",
251
+ " Stream #0:1: Audio: ac3, 48000 Hz, 5.1, fltp, 640 kb/s",
252
+ "Stream mapping:",
253
+ " Stream #0:0(und): Video: wrapped_avframe, yuv420p, 1920x1080, q=2-31, 200 kb/s, 24 fps"
254
+ ].join("\n");
255
+ assert.equal(parseFfmpegBitrateKbps(banner), 7800);
256
+
257
+ // No per-stream rate: the container's stands in, as it always did.
258
+ const noStreamRate = [
259
+ " Duration: 00:10:00.00, bitrate: 4200 kb/s",
260
+ " Stream #0:0: Video: hevc (Main 10), yuv420p10le, 1920x1080, 23.98 fps"
261
+ ].join("\n");
262
+ assert.equal(parseFfmpegBitrateKbps(noStreamRate), 4200);
263
+ });
264
+
265
+ test("a banner with no bitrate reads as no bitrate, not as zero", () => {
266
+ assert.equal(parseFfmpegBitrateKbps("Duration: 00:01:00.00, start: 0.000000\n"), null);
267
+ assert.equal(parseFfmpegBitrateKbps(""), null);
268
+ assert.equal(parseFfmpegBitrateKbps("Duration: 00:01:00.00, start: 0.000, bitrate: 8000 kb/s"), 8000);
269
+ });
270
+
271
+ test("the source's decode figures come off the probe, or not at all", () => {
272
+ assert.deepEqual(sourceDecodeCharacteristics({ width: 1920, height: 1080, fps: 24, bitrateKbps: 8000 }), {
273
+ megapixelsPerSecond: (1920 * 1080 * 24) / 1e6,
274
+ megabitsPerSecond: 8,
275
+ // Which measurement of this host applies. Absent on the probe means absent
276
+ // here — the caller then prices the source as H.264 8-bit, which is what
277
+ // every source was priced as before the model was fitted per family.
278
+ codec: "",
279
+ bitDepth: null
280
+ });
281
+ assert.deepEqual(
282
+ sourceDecodeCharacteristics({ width: 1920, height: 1080, fps: 24, bitrateKbps: 8000, codec: "hevc", bitDepth: 10 }),
283
+ {
284
+ megapixelsPerSecond: (1920 * 1080 * 24) / 1e6,
285
+ megabitsPerSecond: 8,
286
+ codec: "hevc",
287
+ bitDepth: 10
288
+ }
289
+ );
290
+ assert.equal(sourceDecodeCharacteristics({ width: 1920, height: 1080, fps: 24, bitrateKbps: null }), null);
291
+ assert.equal(sourceDecodeCharacteristics(null), null);
292
+ });
293
+
294
+ test("the OFFER drops the rungs the host cannot hold, and the master keeps addressing them", async (t) => {
295
+ const dirPath = await mkdtemp(path.join(os.tmpdir(), "decode-cost-"));
296
+ const manager = wireOutputs({
297
+ enabled: true,
298
+ ffmpegBin: "ffmpeg",
299
+ localBindHost: "127.0.0.1",
300
+ localPort: 9090,
301
+ // A host that can encode a little: 3 Mpx/s, so only the smallest rungs of a
302
+ // 1080p source can be produced faster than they are watched.
303
+ softwarePresetBenchmark: [{ preset: "ultrafast", pixelsPerSec: 3e6 }],
304
+ decodeCostModel: ADDON_HOST_MODEL
305
+ });
306
+ t.after(async () => {
307
+ await manager.lifecycle.disposeAll();
308
+ await rm(dirPath, { recursive: true, force: true });
309
+ });
310
+
311
+ const session = {
312
+ id: "ccccccccddddeeee",
313
+ spec: outputSpec({ transcodeVideo: false, cutGrid: "keyframe" }),
314
+ dirPath,
315
+ // Where this file is cut, held by the file. A fixture that stated it
316
+ // on the session was describing what production no longer does.
317
+ timeline: new Timeline({
318
+ boundaries: Array.from({ length: 101 }, (_, index) => index * 4),
319
+ cutGrid: "keyframe"
320
+ }),
321
+ state: "ready",
322
+ // The file's own facts: the picture's size, and what decoding it costs,
323
+ // which is derived from them rather than stated beside them. These are the
324
+ // measured film of MEASURED_FILM above — 1080p24 at 8 Mbit/s.
325
+ file: new SourceFile({ sourceKey: "source-1", fileIndex: 0, name: "video.mkv" }).learn({
326
+ width: 1920,
327
+ height: 1080,
328
+ fps: 24,
329
+ bitrateKbps: 8000
330
+ }),
331
+ // An ordinary session reads its own file, and its sound is inside it. The
332
+ // three differ only for a soundtrack shipped as a file of its own.
333
+ get inputFile() { return this.file; },
334
+ get audioFile() { return this.file; },
335
+ startedAt: Date.now(),
336
+ lastAccessedAt: Date.now(),
337
+ ffmpeg: null,
338
+ lastError: "",
339
+ consumers: new Set(),
340
+ segmentFormat: fmp4Format,
341
+ transcodeVideo: false,
342
+ transcodeAudio: true,
343
+ audioTrackIndex: 0,
344
+ // A copy can only be cut where the source already has a keyframe, and a
345
+ // master is offered only when that grid is real.
346
+ // The shape this output is encoded AS, decided once for the output.
347
+ output: new Output({
348
+ encodeWidth: 0,
349
+ encodeHeight: 0,
350
+ outputFps: 24,
351
+ softwarePreset: null,
352
+ applyTonemap: false
353
+ }),
354
+ usesExplicitCuts: true,
355
+ useSyntheticPlaylist: true,
356
+ playlistText: "#EXTM3U\n",
357
+ segmentCount: 100,
358
+ progress: { state: "running", processedSeconds: 0, startPositionSeconds: 0, speed: "1.0x" }
359
+ };
360
+ manager.outputs.set(session.id, session);
361
+
362
+ assert.deepEqual(
363
+ manager.quality.offeredHeights(session),
364
+ [1080],
365
+ "every rung under the copy runs below realtime here, so there is nothing to switch to"
366
+ );
367
+ // The master is NOT that answer. It says which rungs can be spliced onto this
368
+ // cut grid, which is a fact about the file, and it has to hold still for the
369
+ // session's life: the browser is handed its address at creation, and a live
370
+ // figure that withdrew it left a session answering 404 to itself (field
371
+ // 2026-08-18, "Moana (2016).mkv" — nothing played at all).
372
+ const weakMaster = manager.renditions.buildMasterPlaylist(session.id);
373
+ assert.ok(weakMaster, "published once, whatever the host is managing this second");
374
+ assert.deepEqual(
375
+ [...weakMaster.matchAll(/^v\/(\d+)\/index\.m3u8$/gm)].map((match) => Number(match[1])),
376
+ [1080, 720, 540, 480, 360, 240],
377
+ "the ladder of the source, addressable — the menu the viewer sees is offeredHeights"
378
+ );
379
+
380
+ // A host with a little more encoder keeps the rungs it can actually hold. A
381
+ // second session, because the answer is settled once per session.
382
+ manager.softwarePresetBenchmark = [{ preset: "ultrafast", pixelsPerSec: 12e6 }];
383
+ const stronger = { ...session, id: "ddddddddeeeeffff", offeredHeightsCache: undefined };
384
+ manager.outputs.set(stronger.id, stronger);
385
+ assert.deepEqual(
386
+ manager.quality.offeredHeights(stronger),
387
+ [1080, 360, 240],
388
+ "nothing is known about this swarm, so the bar is realtime"
389
+ );
390
+ const master = manager.renditions.buildMasterPlaylist(stronger.id);
391
+ assert.ok(master, "1080p copied plus every rung that can be spliced beside it");
392
+ assert.deepEqual(
393
+ [...master.matchAll(/^v\/(\d+)\/index\.m3u8$/gm)].map((match) => Number(match[1])),
394
+ [1080, 720, 540, 480, 360, 240],
395
+ "the same published set as before: what the host manages is the offer's business, not the document's"
396
+ );
397
+
398
+ // The same host, once the reader has measured what this file's supply
399
+ // demands: waits arriving as they did on the field torrent of 2026-08-17 ask
400
+ // 1.67x of any step, and the rungs that only just cleared realtime go.
401
+ const onAThinSwarm = {
402
+ ...stronger,
403
+ id: "eeeeeeeeffff0000",
404
+ offeredHeightsCache: undefined,
405
+ supplyFigures: { requiredSpeed: 1.67, worstWaitSec: 1.49, medianIntervalSec: 2.22, samples: 12 }
406
+ };
407
+ manager.outputs.set(onAThinSwarm.id, onAThinSwarm);
408
+ assert.deepEqual(
409
+ manager.quality.offeredHeights(onAThinSwarm),
410
+ [1080],
411
+ "the copied height costs no encoder and stays; nothing re-encoded survives that supply"
412
+ );
413
+ });
414
+
415
+ test("a source is priced by its own codec family when that family was measured", () => {
416
+ // A model as the startup benchmark now returns it: H.264 terms at the top
417
+ // level, for a caller that knows nothing about codecs, and the measured
418
+ // families beside them.
419
+ const model = {
420
+ pixelTerm: 0.006, bitrateTerm: 0.012, constantTerm: 0,
421
+ families: {
422
+ h264: { pixelTerm: 0.006, bitrateTerm: 0.012, constantTerm: 0 },
423
+ hevc: { pixelTerm: 0.011, bitrateTerm: 0.020, constantTerm: 0 },
424
+ hevc10: { pixelTerm: 0.017, bitrateTerm: 0.026, constantTerm: 0 }
425
+ }
426
+ };
427
+ const rates = { megapixelsPerSecond: 50, megabitsPerSecond: 9 };
428
+ const asH264 = decodeCostOf(model, { ...rates, codec: "h264", bitDepth: 8 });
429
+ const asHevc = decodeCostOf(model, { ...rates, codec: "hevc", bitDepth: 8 });
430
+ const asHevc10 = decodeCostOf(model, { ...rates, codec: "hevc", bitDepth: 10 });
431
+ assert.equal(Number(asH264.toFixed(4)), 0.408);
432
+ assert.equal(Number(asHevc.toFixed(4)), 0.73);
433
+ assert.equal(Number(asHevc10.toFixed(4)), 1.084);
434
+ // The whole point: the same file costs more as HEVC than as H.264, and more
435
+ // again at ten bits. A single fit could not say that.
436
+ assert.ok(asHevc > asH264 && asHevc10 > asHevc);
437
+ });
438
+
439
+ test("a family with no clips is priced as H.264, and a model with no families still works", () => {
440
+ const withFamilies = {
441
+ pixelTerm: 0.006, bitrateTerm: 0.012, constantTerm: 0,
442
+ families: { h264: { pixelTerm: 0.006, bitrateTerm: 0.012, constantTerm: 0 } }
443
+ };
444
+ const rates = { megapixelsPerSecond: 50, megabitsPerSecond: 9 };
445
+ // AV1 has no set of its own yet.
446
+ assert.equal(
447
+ decodeCostOf(withFamilies, { ...rates, codec: "av1", bitDepth: 8 }),
448
+ decodeCostOf(withFamilies, { ...rates, codec: "h264", bitDepth: 8 })
449
+ );
450
+ // And a flat model — every model before this release — is unchanged.
451
+ const flat = { pixelTerm: 0.006, bitrateTerm: 0.012, constantTerm: 0 };
452
+ assert.equal(decodeCostOf(flat, { ...rates, codec: "hevc", bitDepth: 10 }), 0.408);
453
+ });
454
+
455
+ test("the family is chosen by codec and depth, and unknown names fall to H.264", () => {
456
+ assert.equal(decodeFamilyOf({ codec: "hevc", bitDepth: 8 }), "hevc");
457
+ assert.equal(decodeFamilyOf({ codec: "HEVC", bitDepth: 10 }), "hevc10");
458
+ assert.equal(decodeFamilyOf({ codec: "h265", bitDepth: 12 }), "hevc10");
459
+ assert.equal(decodeFamilyOf({ codec: "h264", bitDepth: 10 }), "h264");
460
+ assert.equal(decodeFamilyOf({ codec: "vc1", bitDepth: null }), "h264");
461
+ assert.equal(decodeFamilyOf({}), "h264");
462
+ });