@torrent-tv/proxy 2.87.10 → 2.87.11

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 (458) 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 +2186 -2181
  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 +124 -101
  23. package/docs/container-architecture.md +218 -218
  24. package/docs/download-architecture.md +261 -261
  25. package/docs/encode-architecture.md +729 -729
  26. package/docs/encode-run-state.md +96 -96
  27. package/docs/logs.md +97 -97
  28. package/docs/socks5-egress-research.md +114 -114
  29. package/docs/storage-architecture.md +267 -267
  30. package/knip.json +14 -14
  31. package/openspec/changes/adaptive-bitrate/design.md +92 -92
  32. package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
  33. package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
  34. package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
  35. package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
  36. package/openspec/changes/binary-distribution/proposal.md +53 -53
  37. package/openspec/changes/chunked-request-bodies/design.md +81 -81
  38. package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
  39. package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
  40. package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
  41. package/openspec/changes/cold-start/design.md +98 -98
  42. package/openspec/changes/cold-start/proposal.md +62 -62
  43. package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
  44. package/openspec/changes/cold-start/tasks.md +48 -48
  45. package/openspec/changes/disk-cap/.openspec.yaml +2 -2
  46. package/openspec/changes/disk-cap/proposal.md +37 -37
  47. package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
  48. package/openspec/changes/disk-cap/tasks.md +19 -19
  49. package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
  50. package/openspec/changes/proxy-observability/design.md +38 -38
  51. package/openspec/changes/proxy-observability/proposal.md +49 -49
  52. package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
  53. package/openspec/changes/proxy-observability/tasks.md +21 -21
  54. package/openspec/changes/seek-debounce/design.md +118 -118
  55. package/openspec/changes/seek-debounce/proposal.md +59 -59
  56. package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
  57. package/openspec/changes/seek-debounce/tasks.md +33 -33
  58. package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
  59. package/openspec/changes/subtitle-language/proposal.md +50 -50
  60. package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
  61. package/openspec/changes/subtitle-language/tasks.md +26 -26
  62. package/openspec/changes/track-selection/.openspec.yaml +2 -2
  63. package/openspec/changes/track-selection/design.md +47 -47
  64. package/openspec/changes/track-selection/proposal.md +45 -45
  65. package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
  66. package/openspec/changes/track-selection/tasks.md +20 -20
  67. package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
  68. package/openspec/changes/transcode-quality/proposal.md +51 -51
  69. package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
  70. package/openspec/changes/transcode-quality/tasks.md +63 -63
  71. package/openspec/config.yaml +38 -38
  72. package/package.json +50 -50
  73. package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
  74. package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
  75. package/research/double-spawn-2026-09-10.md +171 -171
  76. package/research/handover-2026-09-13.md +209 -209
  77. package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
  78. package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
  79. package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
  80. package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
  81. package/routes/api/client-logs/post.js +86 -86
  82. package/routes/api/delivery-sink/get.js +72 -72
  83. package/routes/api/link-probe/get.js +51 -51
  84. package/routes/api/playback-plan/audio-tracks/post.js +29 -29
  85. package/routes/api/playback-plan/post.js +53 -53
  86. package/routes/api/sources/files/get.js +100 -100
  87. package/routes/api/sources/post.js +36 -36
  88. package/routes/api/sources/stats/get.js +132 -132
  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 +136 -129
  92. package/routes/api/transcode-sessions/post.js +160 -160
  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 +361 -361
  99. package/routes/transcode/audio-file/get.js +64 -64
  100. package/routes/transcode/audio-warm/get.js +84 -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/services/encode/CoverageMap.js +428 -428
  106. package/services/encode/CushionReport.js +345 -345
  107. package/services/encode/EncodeAdmission.js +332 -332
  108. package/services/encode/EncodeOrchestrator.js +1217 -1217
  109. package/services/encode/EncodePlan.js +1366 -1366
  110. package/services/encode/EncodeRun.js +776 -776
  111. package/services/encode/EncodeRuns.js +1229 -1229
  112. package/services/encode/Encoder.js +142 -142
  113. package/services/encode/EncoderSelection.js +38 -38
  114. package/services/encode/LocalObservations.js +359 -359
  115. package/services/encode/NvencEncoder.js +50 -50
  116. package/services/encode/OutputOpening.js +755 -755
  117. package/services/encode/OutputTimes.js +579 -579
  118. package/services/encode/QsvEncoder.js +58 -58
  119. package/services/encode/Renditions.js +2184 -2162
  120. package/services/encode/RunProgress.js +84 -84
  121. package/services/encode/SegmentDemand.js +95 -95
  122. package/services/encode/SoftwareEncoder.js +121 -121
  123. package/services/encode/V4l2m2mEncoder.js +53 -53
  124. package/services/encode/VaapiEncoder.js +66 -66
  125. package/services/encode/args.js +416 -416
  126. package/services/encode/available-share.js +89 -89
  127. package/services/encode/calibration.js +361 -361
  128. package/services/encode/contention.js +128 -128
  129. package/services/encode/decode-cost-fit.js +363 -363
  130. package/services/encode/encode-exit.js +114 -114
  131. package/services/encode/encode-run-state.js +551 -551
  132. package/services/encode/encoder-readings.js +45 -45
  133. package/services/encode/fingerprint.js +322 -322
  134. package/services/encode/host-load.js +174 -174
  135. package/services/encode/hwaccel.js +1894 -1894
  136. package/services/encode/index.js +9 -9
  137. package/services/encode/learned-median.js +90 -90
  138. package/services/encode/output/EncodedOutput.js +57 -57
  139. package/services/encode/output/Output.js +131 -131
  140. package/services/encode/output/OutputCatalog.js +414 -414
  141. package/services/encode/output/OutputSpec.js +390 -390
  142. package/services/encode/output/Timeline.js +308 -308
  143. package/services/encode/output/cut-grid.js +107 -107
  144. package/services/encode/output/index.js +20 -20
  145. package/services/encode/output/ladder.js +32 -32
  146. package/services/encode/output/playlists.js +284 -284
  147. package/services/encode/output/rates.js +125 -125
  148. package/services/encode/output-key-format.js +21 -21
  149. package/services/encode/quality/EncodeCost.js +1110 -1083
  150. package/services/encode/quality/HostLoad.js +525 -525
  151. package/services/encode/quality/HostTimings.js +249 -249
  152. package/services/encode/quality/OutputQualityState.js +28 -28
  153. package/services/encode/quality/QualityController.js +935 -935
  154. package/services/encode/quality/QualityOffer.js +416 -416
  155. package/services/encode/quality/drain-threat.js +47 -47
  156. package/services/encode/quality/link-budget.js +254 -254
  157. package/services/encode/quality/output-format.js +266 -266
  158. package/services/encode/quality/serving-output.js +216 -216
  159. package/services/encode/quality/visible-rung.js +57 -57
  160. package/services/encode/run-budget.js +97 -97
  161. package/services/encode/run-command.js +747 -747
  162. package/services/encode/run-costs.js +155 -155
  163. package/services/encode/run-inputs.js +42 -42
  164. package/services/encode/segment-formats/fmp4.js +437 -437
  165. package/services/encode/segment-formats/index.js +77 -77
  166. package/services/encode/segment-formats/init-compat.js +324 -324
  167. package/services/encode/segment-formats/mp4-boxes.js +311 -311
  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/container/AviContainer.js +266 -266
  179. package/services/media/container/Container.js +603 -603
  180. package/services/media/container/ContainerFactory.js +56 -56
  181. package/services/media/container/KeyframeTable.js +272 -272
  182. package/services/media/container/MatroskaContainer.js +1494 -1494
  183. package/services/media/container/Mp4Container.js +1244 -1244
  184. package/services/media/container/SubtitleFileContainer.js +322 -322
  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 +131 -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/index.js +15 -15
  197. package/services/media/tracks/language-detect.js +174 -174
  198. package/services/server/OutputLifecycle.js +418 -418
  199. package/services/server/SegmentServing.js +1146 -1146
  200. package/services/server/ViewerRequests.js +703 -522
  201. package/services/server/WaitLedger.js +142 -142
  202. package/services/server/controllers/SubtitleController.js +109 -109
  203. package/services/server/http-responses.js +60 -60
  204. package/services/server/playback-readiness.js +620 -0
  205. package/services/server/transcode-session-files.js +325 -325
  206. package/services/server/wire-outputs.js +989 -983
  207. package/services/storage/Diagnostics.js +202 -202
  208. package/services/storage/MachineBudget.js +235 -235
  209. package/services/storage/allowance.js +107 -107
  210. package/services/storage/core-dumps.js +106 -106
  211. package/services/storage/files/CompletedFiles.js +391 -391
  212. package/services/storage/files/piece-from-whole-file.js +118 -118
  213. package/services/storage/free.js +71 -71
  214. package/services/storage/keep.js +48 -48
  215. package/services/storage/machine-memory.js +56 -56
  216. package/services/storage/memory-report.js +575 -575
  217. package/services/storage/piece-store/piece-disk-store.js +516 -516
  218. package/services/storage/piece-store/piece-lru.js +495 -495
  219. package/services/storage/piece-store/shared-piece-store.js +1880 -1880
  220. package/services/storage/returns.js +103 -103
  221. package/services/storage/segment-store/SegmentStore.js +1196 -1196
  222. package/services/storage/wire.js +175 -175
  223. package/services/torrent/Contents.js +324 -324
  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/files.js +455 -455
  233. package/services/torrent/naming.js +619 -619
  234. package/services/torrent/piece-store-of.js +29 -29
  235. package/services/torrent/supply-margin.js +262 -262
  236. package/services/torrent/worker/background-fill.js +213 -213
  237. package/services/torrent/worker/channel.js +271 -271
  238. package/services/torrent/worker/fastest-wires.js +342 -342
  239. package/services/torrent/worker/handle-state.js +23 -23
  240. package/services/torrent/worker/held-bytes.js +155 -155
  241. package/services/torrent/worker/install-webrtc-shim.js +32 -32
  242. package/services/torrent/worker/piece-reader.js +1078 -1078
  243. package/services/torrent/worker/resume-warm.js +107 -107
  244. package/services/torrent/worker/webrtc-shim.js +219 -219
  245. package/services/transport/delivery-probe.js +842 -842
  246. package/services/transport/health-collector.js +53 -53
  247. package/services/transport/nat-classifier.js +231 -231
  248. package/services/transport/packet-witness.js +808 -808
  249. package/services/transport/port-mapper.js +235 -235
  250. package/services/transport/registry-api.js +81 -81
  251. package/services/transport/tunnel-client.js +447 -447
  252. package/services/transport/usrsctp-state.js +175 -175
  253. package/services/transport/webrtc-manager.js +484 -484
  254. package/services/viewer/Assignments.js +340 -340
  255. package/services/viewer/PriorityMap.js +390 -390
  256. package/services/viewer/PriorityOrchestrator.js +325 -325
  257. package/services/viewer/Viewer.js +675 -657
  258. package/services/viewer/Viewers.js +368 -368
  259. package/services/viewer/active-output.js +37 -37
  260. package/services/viewer/audio-start.js +46 -46
  261. package/services/viewer/buffer-trend.js +81 -81
  262. package/services/viewer/choices.js +647 -647
  263. package/services/viewer/positions.js +126 -126
  264. package/services/viewer/report-intake.js +46 -46
  265. package/store/source-registry.js +71 -71
  266. package/test/asset-paths.test.js +28 -28
  267. package/test/audio-track-bitrate.test.js +40 -40
  268. package/test/auto-quality-step.test.js +707 -707
  269. package/test/available-share.test.js +76 -76
  270. package/test/background-fill.test.js +190 -190
  271. package/test/client-log-file.test.js +110 -110
  272. package/test/completed-files.test.js +115 -115
  273. package/test/concurrent-cost.test.js +148 -148
  274. package/test/container-media-info.test.js +228 -228
  275. package/test/contention.test.js +76 -76
  276. package/test/core-dumps.test.js +57 -57
  277. package/test/coverage-follows-the-disk.test.js +228 -228
  278. package/test/coverage-map.test.js +195 -195
  279. package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
  280. package/test/cut-times-timeline.test.js +65 -65
  281. package/test/cuts-follow-published-grid.test.js +109 -109
  282. package/test/data-channel-frame.test.js +63 -63
  283. package/test/dead-torrent-handle.test.js +52 -52
  284. package/test/declared-rates.test.js +116 -116
  285. package/test/declared-tracks.test.js +43 -43
  286. package/test/decode-cost-fit.test.js +202 -202
  287. package/test/decode-cost.test.js +462 -462
  288. package/test/delivery-probe.test.js +393 -393
  289. package/test/demand-register.test.js +206 -206
  290. package/test/dht-bootstrap.test.js +72 -72
  291. package/test/drain-threat.test.js +65 -65
  292. package/test/duplicate-add.test.js +48 -48
  293. package/test/encode-admission.test.js +146 -146
  294. package/test/encode-cost.test.js +283 -283
  295. package/test/encode-exit.test.js +111 -111
  296. package/test/encode-orchestrator.test.js +419 -419
  297. package/test/encode-plan-viewers.test.js +718 -718
  298. package/test/encode-plan.test.js +545 -545
  299. package/test/encode-run-state.test.js +314 -314
  300. package/test/encode-run.test.js +161 -161
  301. package/test/encode-slope.test.js +46 -46
  302. package/test/encoded-output.test.js +39 -39
  303. package/test/encoder-kinds.test.js +122 -122
  304. package/test/encoder-readings.test.js +46 -46
  305. package/test/every-claimant-is-told-its-share.test.js +152 -152
  306. package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
  307. package/test/failed-start-breaker.test.js +202 -202
  308. package/test/fastest-wires.test.js +143 -143
  309. package/test/ffmpeg-stream-counts.test.js +78 -78
  310. package/test/file-edges.test.js +191 -191
  311. package/test/fmp4-self-contained.test.js +80 -80
  312. package/test/fmp4-timestamps.test.js +140 -140
  313. package/test/fmp4-track-completeness.test.js +41 -41
  314. package/test/format-keyed-outputs.test.js +78 -78
  315. package/test/health-metrics.test.js +38 -38
  316. package/test/held-request-width.test.js +157 -157
  317. package/test/helpers/encode-run.js +136 -136
  318. package/test/helpers/held-file.js +29 -29
  319. package/test/helpers/manager.js +57 -57
  320. package/test/helpers/output-spec.js +63 -63
  321. package/test/host-load.test.js +68 -68
  322. package/test/index-accuracy-is-about-the-file.test.js +128 -128
  323. package/test/init-compat.test.js +219 -219
  324. package/test/input-lost-quiets-the-plan.test.js +261 -261
  325. package/test/input-unavailable.test.js +48 -48
  326. package/test/installation-id.test.js +39 -39
  327. package/test/keeping-period.test.js +83 -83
  328. package/test/keyframe-index-accuracy.test.js +188 -188
  329. package/test/keyframe-table-budget.test.js +102 -102
  330. package/test/keyframes-belong-to-the-file.test.js +249 -249
  331. package/test/learned-median.test.js +37 -37
  332. package/test/letting-go-asks-who-is-watching.test.js +191 -191
  333. package/test/limit-row-by-area.test.js +60 -60
  334. package/test/link-budget.test.js +111 -111
  335. package/test/link-probe.test.js +56 -56
  336. package/test/local-encoding-data.test.js +273 -273
  337. package/test/logger-repeats.test.js +134 -134
  338. package/test/look-ahead-coverage.test.js +56 -56
  339. package/test/manager-surface.test.js +292 -292
  340. package/test/matroska-cues-track.test.js +192 -192
  341. package/test/memory-budget.test.js +390 -390
  342. package/test/move-cost.test.js +216 -216
  343. package/test/mp4-subtitles.test.js +173 -173
  344. package/test/one-authority.test.js +285 -285
  345. package/test/one-budget.test.js +149 -149
  346. package/test/one-output-one-session.test.js +204 -204
  347. package/test/one-resource-divided.test.js +185 -185
  348. package/test/one-serving-output-per-height.test.js +458 -458
  349. package/test/orchestrator-wired.test.js +206 -206
  350. package/test/output-catalog.test.js +39 -39
  351. package/test/output-shape.test.js +21 -21
  352. package/test/output-spec.test.js +251 -251
  353. package/test/output-speed.test.js +88 -88
  354. package/test/outputs-being-prepared.test.js +40 -40
  355. package/test/packet-witness-ring.test.js +236 -236
  356. package/test/packet-witness.test.js +148 -148
  357. package/test/piece-disk-store.test.js +381 -381
  358. package/test/piece-from-whole-file.test.js +129 -129
  359. package/test/piece-lru.test.js +319 -319
  360. package/test/piece-reader.test.js +162 -162
  361. package/test/piece-selection-offset.test.js +85 -85
  362. package/test/piece-store-eviction.test.js +560 -560
  363. package/test/piece-store-never-refuses.test.js +153 -153
  364. package/test/piece-store-reservations.test.js +318 -318
  365. package/test/piece-store-slow-disk.test.js +159 -159
  366. package/test/piece-tail.test.js +97 -97
  367. package/test/plan-host-timings.test.js +68 -68
  368. package/test/plan-is-steady.test.js +225 -225
  369. package/test/playback-readiness.test.js +323 -0
  370. package/test/priority-map-download.test.js +174 -174
  371. package/test/priority-map-emptied.test.js +204 -204
  372. package/test/priority-map-per-output.test.js +215 -215
  373. package/test/priority-map.test.js +163 -163
  374. package/test/probe-wedge-certainty.test.js +60 -60
  375. package/test/produced-copy-choice.test.js +213 -213
  376. package/test/published-timeline.test.js +83 -83
  377. package/test/quality-offer.test.js +162 -162
  378. package/test/quality-variants.test.js +1229 -1229
  379. package/test/rate-control-in-the-output.test.js +180 -180
  380. package/test/read-survives-withdrawal.test.js +164 -164
  381. package/test/read-window.test.js +365 -365
  382. package/test/resume-warm.test.js +39 -39
  383. package/test/run-graph-drift.test.js +37 -37
  384. package/test/run-inputs.test.js +80 -80
  385. package/test/run-intervals.test.js +100 -100
  386. package/test/run-position-follows-published-grid.test.js +89 -89
  387. package/test/run-progress.test.js +65 -65
  388. package/test/same-height-switch.test.js +664 -664
  389. package/test/seek-frees-the-viewers-requests.test.js +217 -217
  390. package/test/seek-landing.test.js +109 -109
  391. package/test/seek-target-not-superseded.test.js +195 -195
  392. package/test/segment-closed-on-both-branches.test.js +96 -96
  393. package/test/segment-cut-times.test.js +63 -63
  394. package/test/segment-demand.test.js +80 -80
  395. package/test/segment-hold-supersede.test.js +91 -91
  396. package/test/segment-judged-by-its-own-header.test.js +221 -221
  397. package/test/segment-serve-wiring.test.js +475 -475
  398. package/test/segment-store-eviction.test.js +247 -247
  399. package/test/segment-store.test.js +323 -323
  400. package/test/segment-true-start.test.js +86 -86
  401. package/test/segments-are-shared.test.js +176 -176
  402. package/test/send-chunk-bytes.test.js +86 -86
  403. package/test/serving-existing-output.test.js +102 -102
  404. package/test/serving-output.test.js +102 -102
  405. package/test/shared-piece-store.test.js +310 -310
  406. package/test/sidecar-naming.test.js +142 -142
  407. package/test/source-file.test.js +133 -133
  408. package/test/source-files-route.test.js +103 -103
  409. package/test/source-registry.test.js +18 -18
  410. package/test/split-a-stretch.test.js +139 -139
  411. package/test/stale-request-after-seek.test.js +158 -158
  412. package/test/startup-readings.test.js +124 -124
  413. package/test/stream-route.test.js +182 -182
  414. package/test/subtitle-cue-framing.test.js +200 -200
  415. package/test/subtitle-cue-source.test.js +102 -102
  416. package/test/subtitle-cue-walk.test.js +370 -370
  417. package/test/subtitle-cursor.test.js +63 -63
  418. package/test/subtitle-defaults.test.js +118 -118
  419. package/test/subtitle-language.test.js +252 -252
  420. package/test/subtitle-track-numbering.test.js +386 -386
  421. package/test/supply-margin.test.js +177 -177
  422. package/test/swarm-follows-readers.test.js +174 -174
  423. package/test/swarm-reach.test.js +130 -130
  424. package/test/swarm-selection.test.js +220 -220
  425. package/test/tail-duplication.test.js +167 -167
  426. package/test/the-channel-has-both-ends.test.js +91 -91
  427. package/test/the-torrent-thread-serves-bytes.test.js +107 -107
  428. package/test/thread-pool.test.js +53 -53
  429. package/test/timeline.test.js +84 -84
  430. package/test/torrent-contents.test.js +235 -235
  431. package/test/torrent-cost.test.js +71 -71
  432. package/test/torrent-files.test.js +221 -221
  433. package/test/torrent-naming.test.js +255 -255
  434. package/test/tracks-begin-together.test.js +209 -209
  435. package/test/tunnel-renewal.test.js +167 -167
  436. package/test/two-viewers-one-picture.test.js +407 -407
  437. package/test/upload-hurry.test.js +186 -186
  438. package/test/usrsctp-state.test.js +131 -131
  439. package/test/video-facts.test.js +102 -102
  440. package/test/viewer-assignments.test.js +143 -143
  441. package/test/viewer-outputs.test.js +285 -285
  442. package/test/viewer-presence.test.js +116 -116
  443. package/test/viewer-requests-admission.test.js +89 -89
  444. package/test/viewer-states.test.js +189 -189
  445. package/test/viewer-subtitle-subscription.test.js +96 -96
  446. package/test/viewer.test.js +139 -139
  447. package/test/viewing-generations.test.js +390 -390
  448. package/test/visible-rung.test.js +37 -37
  449. package/test/wait-ledger.test.js +100 -100
  450. package/test/webrtc-shim.test.js +149 -149
  451. package/test/wedge-certainty.test.js +131 -131
  452. package/test/withdraw-piece-claim.test.js +212 -212
  453. package/test/worker-channel.test.js +158 -158
  454. package/utils/client-log-file.js +151 -151
  455. package/utils/logger.js +312 -312
  456. package/utils/parse-range.js +25 -25
  457. package/utils/perf.js +59 -59
  458. 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
+ });