@torrent-tv/proxy 2.87.8 → 2.87.9

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