@torrent-tv/proxy 2.89.7 → 2.89.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 (498) 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 +363 -338
  13. package/CHANGELOG.md +2345 -2335
  14. package/Dockerfile +30 -30
  15. package/LICENSE +16 -16
  16. package/README.md +369 -369
  17. package/assets/calibration/NOTICE.md +100 -100
  18. package/assets/calibration/audio/NOTICE.md +19 -19
  19. package/assets/calibration/audio/manifest.json +98 -98
  20. package/assets/diagnostics/sctpstate.gdb +31 -31
  21. package/bin/cli.js +684 -684
  22. package/bin/thread-pool.js +30 -30
  23. package/biome.json +412 -412
  24. package/docs/browser-proxy-contract.md +155 -155
  25. package/docs/container-architecture.md +273 -273
  26. package/docs/download-architecture.md +261 -261
  27. package/docs/encode-architecture.md +765 -765
  28. package/docs/encode-run-state.md +96 -96
  29. package/docs/logs.md +97 -97
  30. package/docs/playback-start-forecast.md +113 -113
  31. package/docs/socks5-egress-research.md +114 -114
  32. package/docs/storage-architecture.md +273 -273
  33. package/knip.json +14 -14
  34. package/openspec/changes/adaptive-bitrate/design.md +92 -92
  35. package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
  36. package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
  37. package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
  38. package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
  39. package/openspec/changes/binary-distribution/proposal.md +53 -53
  40. package/openspec/changes/chunked-request-bodies/design.md +81 -81
  41. package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
  42. package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
  43. package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
  44. package/openspec/changes/cold-start/design.md +98 -98
  45. package/openspec/changes/cold-start/proposal.md +62 -62
  46. package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
  47. package/openspec/changes/cold-start/tasks.md +48 -48
  48. package/openspec/changes/disk-cap/.openspec.yaml +2 -2
  49. package/openspec/changes/disk-cap/proposal.md +37 -37
  50. package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
  51. package/openspec/changes/disk-cap/tasks.md +19 -19
  52. package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
  53. package/openspec/changes/proxy-observability/design.md +38 -38
  54. package/openspec/changes/proxy-observability/proposal.md +49 -49
  55. package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
  56. package/openspec/changes/proxy-observability/tasks.md +21 -21
  57. package/openspec/changes/seek-debounce/design.md +118 -118
  58. package/openspec/changes/seek-debounce/proposal.md +59 -59
  59. package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
  60. package/openspec/changes/seek-debounce/tasks.md +33 -33
  61. package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
  62. package/openspec/changes/subtitle-language/proposal.md +50 -50
  63. package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
  64. package/openspec/changes/subtitle-language/tasks.md +26 -26
  65. package/openspec/changes/track-selection/.openspec.yaml +2 -2
  66. package/openspec/changes/track-selection/design.md +47 -47
  67. package/openspec/changes/track-selection/proposal.md +45 -45
  68. package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
  69. package/openspec/changes/track-selection/tasks.md +20 -20
  70. package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
  71. package/openspec/changes/transcode-quality/proposal.md +51 -51
  72. package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
  73. package/openspec/changes/transcode-quality/tasks.md +63 -63
  74. package/openspec/config.yaml +38 -38
  75. package/package.json +51 -50
  76. package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
  77. package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
  78. package/research/double-spawn-2026-09-10.md +171 -171
  79. package/research/handover-2026-09-13.md +209 -209
  80. package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
  81. package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
  82. package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
  83. package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
  84. package/routes/api/client-logs/post.js +86 -86
  85. package/routes/api/delivery-sink/get.js +72 -72
  86. package/routes/api/link-probe/get.js +51 -51
  87. package/routes/api/playback-plan/audio-tracks/post.js +29 -29
  88. package/routes/api/playback-plan/post.js +53 -53
  89. package/routes/api/sources/files/get.js +106 -106
  90. package/routes/api/sources/post.js +36 -36
  91. package/routes/api/sources/stats/get.js +132 -132
  92. package/routes/api/sources/warm/post.js +217 -217
  93. package/routes/api/subtitles/get.js +230 -230
  94. package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
  95. package/routes/api/transcode-sessions/net-report/post.js +138 -138
  96. package/routes/api/transcode-sessions/post.js +178 -178
  97. package/routes/api/transcode-sessions/progress/get.js +27 -27
  98. package/routes/api/transcode-sessions/release/post.js +41 -41
  99. package/routes/api/transcode-sessions/seek/post.js +63 -63
  100. package/routes/health/get.js +13 -13
  101. package/routes/healthz/get.js +13 -13
  102. package/routes/stream/get.js +416 -416
  103. package/routes/transcode/audio-file/get.js +64 -64
  104. package/routes/transcode/audio-warm/get.js +95 -95
  105. package/routes/transcode/session-file/get.js +52 -52
  106. package/routes/transcode/variant-file/get.js +95 -95
  107. package/routes/transcode/variant-warm/get.js +84 -84
  108. package/scripts/build-audio-samples.mjs +31 -31
  109. package/scripts/render-run-graph.js +173 -173
  110. package/server.js +748 -748
  111. package/services/encode/CoverageMap.js +428 -428
  112. package/services/encode/CushionReport.js +345 -345
  113. package/services/encode/EncodeAdmission.js +376 -376
  114. package/services/encode/EncodeOrchestrator.js +1192 -1192
  115. package/services/encode/EncodePlan.js +1368 -1368
  116. package/services/encode/EncodeRun.js +36 -12
  117. package/services/encode/Encoder.js +142 -142
  118. package/services/encode/EncoderSelection.js +38 -38
  119. package/services/encode/LocalObservations.js +359 -359
  120. package/services/encode/NvencEncoder.js +50 -50
  121. package/services/encode/OutputOpening.js +784 -784
  122. package/services/encode/OutputTimes.js +579 -579
  123. package/services/encode/QsvEncoder.js +58 -58
  124. package/services/encode/Renditions.js +2283 -2283
  125. package/services/encode/RunClock.js +87 -87
  126. package/services/encode/RunProgress.js +79 -79
  127. package/services/encode/SegmentDemand.js +95 -95
  128. package/services/encode/SoftwareEncoder.js +121 -121
  129. package/services/encode/V4l2m2mEncoder.js +53 -53
  130. package/services/encode/VaapiEncoder.js +66 -66
  131. package/services/encode/args.js +418 -418
  132. package/services/encode/audio-calibration.js +67 -67
  133. package/services/encode/audio-work-rate.js +15 -15
  134. package/services/encode/available-share.js +89 -89
  135. package/services/encode/calibration.js +361 -361
  136. package/services/encode/contention.js +128 -128
  137. package/services/encode/decode-cost-fit.js +363 -363
  138. package/services/encode/encode-exit.js +116 -116
  139. package/services/encode/encode-run-state.js +551 -551
  140. package/services/encode/encoder-readings.js +43 -43
  141. package/services/encode/fingerprint.js +322 -322
  142. package/services/encode/host-load.js +174 -174
  143. package/services/encode/hwaccel.js +1894 -1894
  144. package/services/encode/index.js +9 -9
  145. package/services/encode/learned-median.js +90 -90
  146. package/services/encode/output/EncodedOutput.js +57 -57
  147. package/services/encode/output/Output.js +131 -131
  148. package/services/encode/output/OutputCatalog.js +414 -414
  149. package/services/encode/output/OutputSpec.js +390 -390
  150. package/services/encode/output/Timeline.js +308 -308
  151. package/services/encode/output/cut-grid.js +107 -107
  152. package/services/encode/output/index.js +24 -24
  153. package/services/encode/output/ladder.js +32 -32
  154. package/services/encode/output/playlists.js +284 -284
  155. package/services/encode/output/rates.js +125 -125
  156. package/services/encode/output-key-format.js +21 -21
  157. package/services/encode/quality/EncodeCost.js +1043 -1043
  158. package/services/encode/quality/HostLoad.js +525 -525
  159. package/services/encode/quality/HostTimings.js +249 -249
  160. package/services/encode/quality/OutputQualityState.js +24 -24
  161. package/services/encode/quality/QualityController.js +758 -758
  162. package/services/encode/quality/QualityOffer.js +416 -416
  163. package/services/encode/quality/drain-threat.js +47 -47
  164. package/services/encode/quality/link-budget.js +337 -337
  165. package/services/encode/quality/output-format.js +319 -319
  166. package/services/encode/quality/serving-output.js +216 -216
  167. package/services/encode/quality/visible-rung.js +57 -57
  168. package/services/encode/run-budget.js +97 -97
  169. package/services/encode/run-command.js +757 -757
  170. package/services/encode/run-costs.js +155 -155
  171. package/services/encode/run-inputs.js +53 -53
  172. package/services/encode/segment-formats/fmp4.js +476 -476
  173. package/services/encode/segment-formats/init-compat.js +324 -324
  174. package/services/encode/segment-formats/mpegts.js +134 -134
  175. package/services/encode/start-stop-cost.js +178 -178
  176. package/services/encode/throughput.js +97 -97
  177. package/services/encode/torrent-cost.js +88 -88
  178. package/services/identity/installation-id.js +38 -38
  179. package/services/media/ContainerOrchestrator.js +202 -202
  180. package/services/media/KeyframeTables.js +356 -356
  181. package/services/media/SourceFile.js +298 -298
  182. package/services/media/SubtitleCues.js +592 -592
  183. package/services/media/SubtitleOrchestrator.js +167 -167
  184. package/services/media/audio-inventory.js +299 -299
  185. package/services/media/container/AviContainer.js +268 -268
  186. package/services/media/container/Container.js +632 -632
  187. package/services/media/container/ContainerFactory.js +38 -38
  188. package/services/media/container/KeyframeTable.js +272 -272
  189. package/services/media/container/MatroskaContainer.js +1031 -1031
  190. package/services/media/container/Mp4Container.js +1354 -1354
  191. package/services/media/container/SubtitleFileContainer.js +339 -339
  192. package/services/media/container/ebml-reader.js +159 -159
  193. package/services/media/container/ebml-stream.js +174 -174
  194. package/services/media/container/matroska-clusters.js +1165 -1165
  195. package/services/media/container/unavailable.js +77 -77
  196. package/services/media/ffmpeg-banner.js +260 -260
  197. package/services/media/keyframe-probe.js +157 -157
  198. package/services/media/media-info-probe.js +93 -93
  199. package/services/media/playback-planner.js +931 -931
  200. package/services/media/tracks/AudioTrack.js +143 -143
  201. package/services/media/tracks/ContainerTrack.js +84 -84
  202. package/services/media/tracks/ImageSubtitleTrack.js +19 -19
  203. package/services/media/tracks/SubtitleTrack.js +38 -38
  204. package/services/media/tracks/TextSubtitleTrack.js +295 -295
  205. package/services/media/tracks/VideoTrack.js +33 -33
  206. package/services/media/tracks/audio-bound.js +233 -233
  207. package/services/media/tracks/index.js +15 -15
  208. package/services/media/tracks/language-detect.js +174 -174
  209. package/services/server/OutputLifecycle.js +418 -418
  210. package/services/server/SegmentServing.js +1152 -1152
  211. package/services/server/ViewerRequests.js +785 -785
  212. package/services/server/WaitLedger.js +142 -142
  213. package/services/server/controllers/PlaybackController.js +40 -40
  214. package/services/server/controllers/SubtitleController.js +109 -109
  215. package/services/server/http-responses.js +60 -60
  216. package/services/server/transcode-session-files.js +325 -325
  217. package/services/server/wire-outputs.js +1029 -1029
  218. package/services/storage/Diagnostics.js +202 -202
  219. package/services/storage/MachineBudget.js +235 -235
  220. package/services/storage/allowance.js +107 -107
  221. package/services/storage/core-dumps.js +106 -106
  222. package/services/storage/files/CompletedFiles.js +391 -391
  223. package/services/storage/files/piece-from-whole-file.js +118 -118
  224. package/services/storage/free.js +71 -71
  225. package/services/storage/keep.js +48 -48
  226. package/services/storage/machine-memory.js +56 -56
  227. package/services/storage/memory-report.js +575 -575
  228. package/services/storage/piece-store/piece-disk-store.js +539 -539
  229. package/services/storage/piece-store/piece-lru.js +495 -495
  230. package/services/storage/piece-store/shared-piece-store.js +1937 -1937
  231. package/services/storage/returns.js +103 -103
  232. package/services/storage/segment-store/SegmentStore.js +1294 -1294
  233. package/services/storage/wire.js +175 -175
  234. package/services/torrent/Contents.js +382 -382
  235. package/services/torrent/demand/DemandRegister.js +202 -202
  236. package/services/torrent/demand/Urgency.js +137 -137
  237. package/services/torrent/demand/Window.js +118 -118
  238. package/services/torrent/demand/index.js +8 -8
  239. package/services/torrent/demand/pieces.js +140 -140
  240. package/services/torrent/download/SwarmSelection.js +386 -386
  241. package/services/torrent/download/registry.js +102 -102
  242. package/services/torrent/download/withdraw-claim.js +80 -80
  243. package/services/torrent/episode-naming.js +182 -182
  244. package/services/torrent/files.js +455 -455
  245. package/services/torrent/naming.js +619 -619
  246. package/services/torrent/piece-store-of.js +29 -29
  247. package/services/torrent/supply-margin.js +264 -264
  248. package/services/torrent/torrent-pool.js +2688 -2688
  249. package/services/torrent/worker/background-fill.js +213 -213
  250. package/services/torrent/worker/channel.js +271 -271
  251. package/services/torrent/worker/client.js +836 -836
  252. package/services/torrent/worker/fastest-wires.js +342 -342
  253. package/services/torrent/worker/handle-state.js +23 -23
  254. package/services/torrent/worker/held-bytes.js +231 -231
  255. package/services/torrent/worker/install-webrtc-shim.js +32 -32
  256. package/services/torrent/worker/piece-reader.js +1078 -1078
  257. package/services/torrent/worker/pool-adapter.js +371 -371
  258. package/services/torrent/worker/protocol.js +184 -184
  259. package/services/torrent/worker/resume-warm.js +107 -107
  260. package/services/torrent/worker/webrtc-shim.js +219 -219
  261. package/services/torrent/worker/worker.js +1137 -1137
  262. package/services/transport/data-channel-handler.js +1406 -1373
  263. package/services/transport/delivery-probe.js +798 -842
  264. package/services/transport/health-collector.js +53 -53
  265. package/services/transport/nat-classifier.js +231 -231
  266. package/services/transport/packet-witness.js +808 -808
  267. package/services/transport/port-mapper.js +235 -235
  268. package/services/transport/registry-api.js +81 -81
  269. package/services/transport/tunnel-client.js +447 -447
  270. package/services/transport/usrsctp-state.js +175 -175
  271. package/services/transport/webrtc-manager.js +484 -484
  272. package/services/viewer/Assignments.js +340 -340
  273. package/services/viewer/PriorityMap.js +421 -421
  274. package/services/viewer/PriorityOrchestrator.js +325 -325
  275. package/services/viewer/Viewer.js +691 -691
  276. package/services/viewer/Viewers.js +368 -368
  277. package/services/viewer/active-output.js +37 -37
  278. package/services/viewer/audio-start.js +46 -46
  279. package/services/viewer/buffer-trend.js +81 -81
  280. package/services/viewer/choices.js +647 -647
  281. package/services/viewer/media-time.js +355 -355
  282. package/services/viewer/playback-readiness.js +635 -635
  283. package/services/viewer/positions.js +126 -126
  284. package/services/viewer/report-intake.js +46 -46
  285. package/store/source-registry.js +71 -71
  286. package/test/asset-paths.test.js +28 -28
  287. package/test/audio-buffer-continuity.test.js +77 -77
  288. package/test/audio-calibration.test.js +23 -23
  289. package/test/audio-inventory.test.js +212 -212
  290. package/test/audio-track-bitrate.test.js +40 -40
  291. package/test/auto-quality-step.test.js +762 -762
  292. package/test/available-share.test.js +76 -76
  293. package/test/background-fill.test.js +190 -190
  294. package/test/client-log-file.test.js +110 -110
  295. package/test/coalesce.test.js +39 -39
  296. package/test/completed-files.test.js +115 -115
  297. package/test/concurrent-cost.test.js +148 -148
  298. package/test/container-media-info.test.js +228 -228
  299. package/test/contention.test.js +76 -76
  300. package/test/core-dumps.test.js +57 -57
  301. package/test/coverage-follows-the-disk.test.js +233 -228
  302. package/test/coverage-map.test.js +195 -195
  303. package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
  304. package/test/cut-times-timeline.test.js +65 -65
  305. package/test/cuts-follow-published-grid.test.js +109 -109
  306. package/test/data-channel-frame.test.js +63 -63
  307. package/test/data-channel-viewer-lifecycle.test.js +162 -92
  308. package/test/dead-torrent-handle.test.js +52 -52
  309. package/test/declared-rates.test.js +116 -116
  310. package/test/declared-tracks.test.js +43 -43
  311. package/test/decode-cost-fit.test.js +202 -202
  312. package/test/decode-cost.test.js +475 -475
  313. package/test/delivery-probe-field.test.js +173 -0
  314. package/test/delivery-probe.test.js +382 -393
  315. package/test/demand-register.test.js +206 -206
  316. package/test/dht-bootstrap.test.js +72 -72
  317. package/test/drain-threat.test.js +65 -65
  318. package/test/duplicate-add.test.js +48 -48
  319. package/test/encode-admission.test.js +146 -146
  320. package/test/encode-cost.test.js +275 -275
  321. package/test/encode-exit.test.js +111 -111
  322. package/test/encode-orchestrator.test.js +426 -426
  323. package/test/encode-plan-viewers.test.js +725 -725
  324. package/test/encode-plan.test.js +545 -545
  325. package/test/encode-run-state.test.js +314 -314
  326. package/test/encode-run.test.js +20 -0
  327. package/test/encode-slope.test.js +46 -46
  328. package/test/encoded-output.test.js +39 -39
  329. package/test/encoder-kinds.test.js +122 -122
  330. package/test/encoder-readings.test.js +68 -68
  331. package/test/episode-naming.test.js +131 -131
  332. package/test/every-claimant-is-told-its-share.test.js +152 -152
  333. package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
  334. package/test/failed-start-breaker.test.js +202 -202
  335. package/test/fastest-wires.test.js +143 -143
  336. package/test/ffmpeg-stream-counts.test.js +78 -78
  337. package/test/file-edges.test.js +191 -191
  338. package/test/fixtures/audio-frame-boundary.json +1 -1
  339. package/test/fmp4-self-contained.test.js +80 -80
  340. package/test/fmp4-timestamps.test.js +140 -140
  341. package/test/fmp4-track-completeness.test.js +41 -41
  342. package/test/format-keyed-outputs.test.js +78 -78
  343. package/test/health-metrics.test.js +36 -38
  344. package/test/held-read-ownership.test.js +77 -77
  345. package/test/held-request-width.test.js +157 -157
  346. package/test/helpers/encode-run.js +248 -240
  347. package/test/helpers/held-file.js +77 -77
  348. package/test/helpers/manager.js +62 -62
  349. package/test/helpers/matroska-file.js +296 -296
  350. package/test/helpers/output-spec.js +63 -63
  351. package/test/host-load.test.js +68 -68
  352. package/test/index-accuracy-is-about-the-file.test.js +128 -128
  353. package/test/init-compat.test.js +219 -219
  354. package/test/input-lost-quiets-the-plan.test.js +261 -261
  355. package/test/input-unavailable.test.js +48 -48
  356. package/test/installation-id.test.js +39 -39
  357. package/test/keeping-period.test.js +83 -83
  358. package/test/keyframe-index-accuracy.test.js +188 -188
  359. package/test/keyframe-table-budget.test.js +123 -123
  360. package/test/keyframes-belong-to-the-file.test.js +249 -249
  361. package/test/learned-median.test.js +37 -37
  362. package/test/letting-go-asks-who-is-watching.test.js +191 -191
  363. package/test/limit-row-by-area.test.js +60 -60
  364. package/test/link-budget.test.js +111 -111
  365. package/test/link-probe.test.js +56 -56
  366. package/test/local-encoding-data.test.js +273 -273
  367. package/test/logger-repeats.test.js +134 -134
  368. package/test/look-ahead-coverage.test.js +56 -56
  369. package/test/manager-surface.test.js +291 -291
  370. package/test/matroska-cluster-discovery.test.js +323 -323
  371. package/test/matroska-cues-track.test.js +192 -192
  372. package/test/media-time.test.js +62 -62
  373. package/test/memory-budget.test.js +390 -390
  374. package/test/move-cost.test.js +216 -216
  375. package/test/mp4-subtitles.test.js +173 -173
  376. package/test/one-authority.test.js +285 -285
  377. package/test/one-budget.test.js +149 -149
  378. package/test/one-output-one-session.test.js +273 -273
  379. package/test/one-resource-divided.test.js +185 -185
  380. package/test/one-serving-output-per-height.test.js +458 -458
  381. package/test/orchestrator-wired.test.js +207 -207
  382. package/test/output-catalog.test.js +39 -39
  383. package/test/output-shape.test.js +21 -21
  384. package/test/output-spec.test.js +251 -251
  385. package/test/output-speed.test.js +92 -87
  386. package/test/outputs-being-prepared.test.js +40 -40
  387. package/test/packet-witness-ring.test.js +236 -236
  388. package/test/packet-witness.test.js +148 -148
  389. package/test/piece-disk-store.test.js +401 -401
  390. package/test/piece-from-whole-file.test.js +129 -129
  391. package/test/piece-lru.test.js +319 -319
  392. package/test/piece-reader.test.js +162 -162
  393. package/test/piece-selection-offset.test.js +85 -85
  394. package/test/piece-store-eviction.test.js +595 -595
  395. package/test/piece-store-never-refuses.test.js +153 -153
  396. package/test/piece-store-reservations.test.js +403 -403
  397. package/test/piece-store-slow-disk.test.js +159 -159
  398. package/test/piece-tail.test.js +97 -97
  399. package/test/plan-host-timings.test.js +68 -68
  400. package/test/plan-is-steady.test.js +225 -225
  401. package/test/playback-readiness.test.js +518 -518
  402. package/test/playback-start-regressions.test.js +101 -101
  403. package/test/priority-map-download.test.js +174 -174
  404. package/test/priority-map-emptied.test.js +204 -204
  405. package/test/priority-map-per-output.test.js +215 -215
  406. package/test/priority-map.test.js +163 -163
  407. package/test/probe-wedge-certainty.test.js +60 -60
  408. package/test/produced-copy-choice.test.js +213 -213
  409. package/test/published-timeline.test.js +83 -83
  410. package/test/quality-offer.test.js +160 -160
  411. package/test/quality-variants.test.js +1274 -1270
  412. package/test/rate-control-in-the-output.test.js +181 -180
  413. package/test/read-survives-withdrawal.test.js +164 -164
  414. package/test/read-window.test.js +365 -365
  415. package/test/reads-not-yet-arrived.test.js +218 -218
  416. package/test/resume-warm.test.js +39 -39
  417. package/test/run-graph-drift.test.js +37 -37
  418. package/test/run-inputs.test.js +80 -80
  419. package/test/run-intervals.test.js +100 -100
  420. package/test/run-position-follows-published-grid.test.js +89 -89
  421. package/test/run-progress.test.js +64 -64
  422. package/test/same-height-switch.test.js +663 -663
  423. package/test/seek-frees-the-viewers-requests.test.js +217 -217
  424. package/test/seek-landing.test.js +109 -109
  425. package/test/seek-target-not-superseded.test.js +195 -195
  426. package/test/segment-closed-on-both-branches.test.js +114 -114
  427. package/test/segment-cut-times.test.js +63 -63
  428. package/test/segment-demand.test.js +80 -80
  429. package/test/segment-hold-supersede.test.js +91 -91
  430. package/test/segment-judged-by-its-own-header.test.js +222 -222
  431. package/test/segment-serve-wiring.test.js +481 -481
  432. package/test/segment-store-eviction.test.js +247 -247
  433. package/test/segment-store.test.js +352 -352
  434. package/test/segment-true-start.test.js +86 -86
  435. package/test/segments-are-shared.test.js +176 -176
  436. package/test/send-chunk-bytes.test.js +86 -86
  437. package/test/serving-existing-output.test.js +111 -111
  438. package/test/serving-output.test.js +102 -102
  439. package/test/shared-piece-store.test.js +310 -310
  440. package/test/sidecar-naming.test.js +142 -142
  441. package/test/soundtrack-rate-bound.test.js +289 -289
  442. package/test/source-file.test.js +133 -133
  443. package/test/source-files-route.test.js +113 -113
  444. package/test/source-registry.test.js +18 -18
  445. package/test/split-a-stretch.test.js +139 -139
  446. package/test/stale-request-after-seek.test.js +158 -158
  447. package/test/startup-readings.test.js +124 -124
  448. package/test/stream-route.test.js +237 -237
  449. package/test/subtitle-cue-framing.test.js +200 -200
  450. package/test/subtitle-cue-source.test.js +102 -102
  451. package/test/subtitle-cue-walk.test.js +385 -385
  452. package/test/subtitle-cursor.test.js +63 -63
  453. package/test/subtitle-defaults.test.js +118 -118
  454. package/test/subtitle-language.test.js +252 -252
  455. package/test/subtitle-metadata.test.js +15 -15
  456. package/test/subtitle-plan-waits.test.js +125 -125
  457. package/test/subtitle-track-numbering.test.js +391 -391
  458. package/test/supply-margin.test.js +190 -190
  459. package/test/swarm-follows-readers.test.js +174 -174
  460. package/test/swarm-reach.test.js +130 -130
  461. package/test/swarm-selection.test.js +220 -220
  462. package/test/tail-duplication.test.js +167 -167
  463. package/test/the-channel-has-both-ends.test.js +91 -91
  464. package/test/the-torrent-thread-serves-bytes.test.js +107 -107
  465. package/test/thread-pool.test.js +53 -53
  466. package/test/timeline.test.js +84 -84
  467. package/test/torrent-contents.test.js +235 -235
  468. package/test/torrent-cost.test.js +71 -71
  469. package/test/torrent-files.test.js +221 -221
  470. package/test/torrent-naming.test.js +255 -255
  471. package/test/tracks-begin-together.test.js +209 -209
  472. package/test/tunnel-renewal.test.js +167 -167
  473. package/test/two-viewers-one-picture.test.js +508 -508
  474. package/test/upload-hurry.test.js +186 -186
  475. package/test/usrsctp-state.test.js +131 -131
  476. package/test/video-facts.test.js +102 -102
  477. package/test/viewer-assignments.test.js +143 -143
  478. package/test/viewer-outputs.test.js +285 -285
  479. package/test/viewer-presence.test.js +116 -116
  480. package/test/viewer-requests-admission.test.js +92 -92
  481. package/test/viewer-starts-on-map.test.js +27 -27
  482. package/test/viewer-states.test.js +192 -189
  483. package/test/viewer-subtitle-subscription.test.js +96 -96
  484. package/test/viewer.test.js +139 -139
  485. package/test/viewing-generations.test.js +390 -390
  486. package/test/visible-rung.test.js +37 -37
  487. package/test/wait-ledger.test.js +100 -100
  488. package/test/webrtc-shim.test.js +149 -149
  489. package/test/wedge-certainty.test.js +131 -131
  490. package/test/whole-file-held-read.test.js +30 -30
  491. package/test/withdraw-piece-claim.test.js +212 -212
  492. package/test/worker-channel.test.js +158 -158
  493. package/utils/client-log-file.js +151 -151
  494. package/utils/coalesce.js +41 -41
  495. package/utils/logger.js +312 -312
  496. package/utils/parse-range.js +25 -25
  497. package/utils/perf.js +59 -59
  498. package/utils/torrent-source-key.js +38 -38
@@ -1,475 +1,475 @@
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 }
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 host, because the encoder's measured modes are a fact the host is
382
- // started with — read through the encoder in use, never assigned later — and
383
- // a second session on it, because the answer is settled once per session.
384
- const strongerHost = wireOutputs({
385
- enabled: true,
386
- ffmpegBin: "ffmpeg",
387
- localBindHost: "127.0.0.1",
388
- localPort: 9090,
389
- softwarePresetBenchmark: [{ preset: "ultrafast", pixelsPerSec: 12e6 }],
390
- decodeCostModel: ADDON_HOST_MODEL
391
- });
392
- t.after(() => strongerHost.lifecycle.disposeAll());
393
- const stronger = { ...session, id: "ddddddddeeeeffff", offeredHeightsCache: undefined };
394
- strongerHost.outputs.set(stronger.id, stronger);
395
- assert.deepEqual(
396
- strongerHost.quality.offeredHeights(stronger),
397
- [1080, 360, 240],
398
- "nothing is known about this swarm, so the bar is realtime"
399
- );
400
- const master = strongerHost.renditions.buildMasterPlaylist(stronger.id);
401
- assert.ok(master, "1080p copied plus every rung that can be spliced beside it");
402
- assert.deepEqual(
403
- [...master.matchAll(/^v\/(\d+)\/index\.m3u8$/gm)].map((match) => Number(match[1])),
404
- [1080, 720, 540, 480, 360, 240],
405
- "the same published set as before: what the host manages is the offer's business, not the document's"
406
- );
407
-
408
- // The same host, once the reader has measured what this file's supply
409
- // demands: waits arriving as they did on the field torrent of 2026-08-17 ask
410
- // 1.67x of any step, and the rungs that only just cleared realtime go.
411
- // What the supply demands is a fact of the FILE, measured by its reader
412
- // (1 + worst wait 1.49 s / median interval 2.22 s, twelve waits), and it is
413
- // asked of the host's load by the file — not read off a session.
414
- strongerHost.hostLoad.requiredSpeedFor = () => 1.67;
415
- const onAThinSwarm = {
416
- ...stronger,
417
- id: "eeeeeeeeffff0000",
418
- offeredHeightsCache: undefined
419
- };
420
- strongerHost.outputs.set(onAThinSwarm.id, onAThinSwarm);
421
- assert.deepEqual(
422
- strongerHost.quality.offeredHeights(onAThinSwarm),
423
- [1080],
424
- "the copied height costs no encoder and stays; nothing re-encoded survives that supply"
425
- );
426
- });
427
-
428
- test("a source is priced by its own codec family when that family was measured", () => {
429
- // A model as the startup benchmark now returns it: H.264 terms at the top
430
- // level, for a caller that knows nothing about codecs, and the measured
431
- // families beside them.
432
- const model = {
433
- pixelTerm: 0.006, bitrateTerm: 0.012, constantTerm: 0,
434
- families: {
435
- h264: { pixelTerm: 0.006, bitrateTerm: 0.012, constantTerm: 0 },
436
- hevc: { pixelTerm: 0.011, bitrateTerm: 0.020, constantTerm: 0 },
437
- hevc10: { pixelTerm: 0.017, bitrateTerm: 0.026, constantTerm: 0 }
438
- }
439
- };
440
- const rates = { megapixelsPerSecond: 50, megabitsPerSecond: 9 };
441
- const asH264 = decodeCostOf(model, { ...rates, codec: "h264", bitDepth: 8 });
442
- const asHevc = decodeCostOf(model, { ...rates, codec: "hevc", bitDepth: 8 });
443
- const asHevc10 = decodeCostOf(model, { ...rates, codec: "hevc", bitDepth: 10 });
444
- assert.equal(Number(asH264.toFixed(4)), 0.408);
445
- assert.equal(Number(asHevc.toFixed(4)), 0.73);
446
- assert.equal(Number(asHevc10.toFixed(4)), 1.084);
447
- // The whole point: the same file costs more as HEVC than as H.264, and more
448
- // again at ten bits. A single fit could not say that.
449
- assert.ok(asHevc > asH264 && asHevc10 > asHevc);
450
- });
451
-
452
- test("a family with no clips is priced as H.264, and a model with no families still works", () => {
453
- const withFamilies = {
454
- pixelTerm: 0.006, bitrateTerm: 0.012, constantTerm: 0,
455
- families: { h264: { pixelTerm: 0.006, bitrateTerm: 0.012, constantTerm: 0 } }
456
- };
457
- const rates = { megapixelsPerSecond: 50, megabitsPerSecond: 9 };
458
- // AV1 has no set of its own yet.
459
- assert.equal(
460
- decodeCostOf(withFamilies, { ...rates, codec: "av1", bitDepth: 8 }),
461
- decodeCostOf(withFamilies, { ...rates, codec: "h264", bitDepth: 8 })
462
- );
463
- // And a flat model — every model before this release — is unchanged.
464
- const flat = { pixelTerm: 0.006, bitrateTerm: 0.012, constantTerm: 0 };
465
- assert.equal(decodeCostOf(flat, { ...rates, codec: "hevc", bitDepth: 10 }), 0.408);
466
- });
467
-
468
- test("the family is chosen by codec and depth, and unknown names fall to H.264", () => {
469
- assert.equal(decodeFamilyOf({ codec: "hevc", bitDepth: 8 }), "hevc");
470
- assert.equal(decodeFamilyOf({ codec: "HEVC", bitDepth: 10 }), "hevc10");
471
- assert.equal(decodeFamilyOf({ codec: "h265", bitDepth: 12 }), "hevc10");
472
- assert.equal(decodeFamilyOf({ codec: "h264", bitDepth: 10 }), "h264");
473
- assert.equal(decodeFamilyOf({ codec: "vc1", bitDepth: null }), "h264");
474
- assert.equal(decodeFamilyOf({}), "h264");
475
- });
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 }
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 host, because the encoder's measured modes are a fact the host is
382
+ // started with — read through the encoder in use, never assigned later — and
383
+ // a second session on it, because the answer is settled once per session.
384
+ const strongerHost = wireOutputs({
385
+ enabled: true,
386
+ ffmpegBin: "ffmpeg",
387
+ localBindHost: "127.0.0.1",
388
+ localPort: 9090,
389
+ softwarePresetBenchmark: [{ preset: "ultrafast", pixelsPerSec: 12e6 }],
390
+ decodeCostModel: ADDON_HOST_MODEL
391
+ });
392
+ t.after(() => strongerHost.lifecycle.disposeAll());
393
+ const stronger = { ...session, id: "ddddddddeeeeffff", offeredHeightsCache: undefined };
394
+ strongerHost.outputs.set(stronger.id, stronger);
395
+ assert.deepEqual(
396
+ strongerHost.quality.offeredHeights(stronger),
397
+ [1080, 360, 240],
398
+ "nothing is known about this swarm, so the bar is realtime"
399
+ );
400
+ const master = strongerHost.renditions.buildMasterPlaylist(stronger.id);
401
+ assert.ok(master, "1080p copied plus every rung that can be spliced beside it");
402
+ assert.deepEqual(
403
+ [...master.matchAll(/^v\/(\d+)\/index\.m3u8$/gm)].map((match) => Number(match[1])),
404
+ [1080, 720, 540, 480, 360, 240],
405
+ "the same published set as before: what the host manages is the offer's business, not the document's"
406
+ );
407
+
408
+ // The same host, once the reader has measured what this file's supply
409
+ // demands: waits arriving as they did on the field torrent of 2026-08-17 ask
410
+ // 1.67x of any step, and the rungs that only just cleared realtime go.
411
+ // What the supply demands is a fact of the FILE, measured by its reader
412
+ // (1 + worst wait 1.49 s / median interval 2.22 s, twelve waits), and it is
413
+ // asked of the host's load by the file — not read off a session.
414
+ strongerHost.hostLoad.requiredSpeedFor = () => 1.67;
415
+ const onAThinSwarm = {
416
+ ...stronger,
417
+ id: "eeeeeeeeffff0000",
418
+ offeredHeightsCache: undefined
419
+ };
420
+ strongerHost.outputs.set(onAThinSwarm.id, onAThinSwarm);
421
+ assert.deepEqual(
422
+ strongerHost.quality.offeredHeights(onAThinSwarm),
423
+ [1080],
424
+ "the copied height costs no encoder and stays; nothing re-encoded survives that supply"
425
+ );
426
+ });
427
+
428
+ test("a source is priced by its own codec family when that family was measured", () => {
429
+ // A model as the startup benchmark now returns it: H.264 terms at the top
430
+ // level, for a caller that knows nothing about codecs, and the measured
431
+ // families beside them.
432
+ const model = {
433
+ pixelTerm: 0.006, bitrateTerm: 0.012, constantTerm: 0,
434
+ families: {
435
+ h264: { pixelTerm: 0.006, bitrateTerm: 0.012, constantTerm: 0 },
436
+ hevc: { pixelTerm: 0.011, bitrateTerm: 0.020, constantTerm: 0 },
437
+ hevc10: { pixelTerm: 0.017, bitrateTerm: 0.026, constantTerm: 0 }
438
+ }
439
+ };
440
+ const rates = { megapixelsPerSecond: 50, megabitsPerSecond: 9 };
441
+ const asH264 = decodeCostOf(model, { ...rates, codec: "h264", bitDepth: 8 });
442
+ const asHevc = decodeCostOf(model, { ...rates, codec: "hevc", bitDepth: 8 });
443
+ const asHevc10 = decodeCostOf(model, { ...rates, codec: "hevc", bitDepth: 10 });
444
+ assert.equal(Number(asH264.toFixed(4)), 0.408);
445
+ assert.equal(Number(asHevc.toFixed(4)), 0.73);
446
+ assert.equal(Number(asHevc10.toFixed(4)), 1.084);
447
+ // The whole point: the same file costs more as HEVC than as H.264, and more
448
+ // again at ten bits. A single fit could not say that.
449
+ assert.ok(asHevc > asH264 && asHevc10 > asHevc);
450
+ });
451
+
452
+ test("a family with no clips is priced as H.264, and a model with no families still works", () => {
453
+ const withFamilies = {
454
+ pixelTerm: 0.006, bitrateTerm: 0.012, constantTerm: 0,
455
+ families: { h264: { pixelTerm: 0.006, bitrateTerm: 0.012, constantTerm: 0 } }
456
+ };
457
+ const rates = { megapixelsPerSecond: 50, megabitsPerSecond: 9 };
458
+ // AV1 has no set of its own yet.
459
+ assert.equal(
460
+ decodeCostOf(withFamilies, { ...rates, codec: "av1", bitDepth: 8 }),
461
+ decodeCostOf(withFamilies, { ...rates, codec: "h264", bitDepth: 8 })
462
+ );
463
+ // And a flat model — every model before this release — is unchanged.
464
+ const flat = { pixelTerm: 0.006, bitrateTerm: 0.012, constantTerm: 0 };
465
+ assert.equal(decodeCostOf(flat, { ...rates, codec: "hevc", bitDepth: 10 }), 0.408);
466
+ });
467
+
468
+ test("the family is chosen by codec and depth, and unknown names fall to H.264", () => {
469
+ assert.equal(decodeFamilyOf({ codec: "hevc", bitDepth: 8 }), "hevc");
470
+ assert.equal(decodeFamilyOf({ codec: "HEVC", bitDepth: 10 }), "hevc10");
471
+ assert.equal(decodeFamilyOf({ codec: "h265", bitDepth: 12 }), "hevc10");
472
+ assert.equal(decodeFamilyOf({ codec: "h264", bitDepth: 10 }), "h264");
473
+ assert.equal(decodeFamilyOf({ codec: "vc1", bitDepth: null }), "h264");
474
+ assert.equal(decodeFamilyOf({}), "h264");
475
+ });