@torrent-tv/proxy 2.87.10 → 2.87.12

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