@torrent-tv/proxy 2.86.0 → 2.87.1

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 (385) hide show
  1. package/CHANGELOG.md +2142 -1915
  2. package/CLAUDE.md +322 -197
  3. package/assets/calibration/NOTICE.md +2 -2
  4. package/bin/cli.js +684 -616
  5. package/biome.json +409 -233
  6. package/docs/browser-proxy-contract.md +94 -0
  7. package/docs/container-architecture.md +218 -213
  8. package/docs/download-architecture.md +7 -7
  9. package/docs/encode-architecture.md +287 -11
  10. package/docs/encode-run-state.md +2 -2
  11. package/docs/logs.md +97 -97
  12. package/docs/{disk-architecture.md → storage-architecture.md} +107 -1
  13. package/openspec/changes/adaptive-bitrate/design.md +2 -2
  14. package/openspec/changes/adaptive-bitrate/proposal.md +1 -1
  15. package/openspec/changes/adaptive-bitrate/tasks.md +1 -1
  16. package/openspec/changes/chunked-request-bodies/design.md +1 -1
  17. package/openspec/changes/chunked-request-bodies/proposal.md +1 -1
  18. package/openspec/changes/chunked-request-bodies/tasks.md +1 -1
  19. package/openspec/changes/cold-start/design.md +1 -1
  20. package/openspec/changes/disk-cap/proposal.md +1 -1
  21. package/openspec/changes/proxy-observability/proposal.md +2 -2
  22. package/openspec/changes/track-selection/proposal.md +1 -1
  23. package/openspec/changes/transcode-quality/proposal.md +1 -1
  24. package/package.json +2 -2
  25. package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
  26. package/research/double-spawn-2026-09-10.md +171 -171
  27. package/routes/api/playback-plan/post.js +2 -2
  28. package/routes/api/sources/files/get.js +1 -1
  29. package/routes/api/sources/stats/get.js +1 -1
  30. package/routes/api/sources/warm/post.js +10 -3
  31. package/routes/api/subtitles/get.js +230 -230
  32. package/routes/api/transcode-sessions/fragment-far/post.js +3 -3
  33. package/routes/api/transcode-sessions/net-report/post.js +129 -94
  34. package/routes/api/transcode-sessions/post.js +160 -139
  35. package/routes/api/transcode-sessions/progress/get.js +3 -3
  36. package/routes/api/transcode-sessions/release/post.js +3 -3
  37. package/routes/api/transcode-sessions/seek/post.js +12 -3
  38. package/routes/stream/get.js +2 -2
  39. package/routes/transcode/audio-file/get.js +15 -6
  40. package/routes/transcode/audio-warm/get.js +9 -6
  41. package/routes/transcode/session-file/get.js +52 -268
  42. package/routes/transcode/variant-file/get.js +46 -7
  43. package/routes/transcode/variant-warm/get.js +19 -6
  44. package/scripts/render-run-graph.js +1 -1
  45. package/server.js +720 -465
  46. package/services/encode/CoverageMap.js +428 -428
  47. package/services/encode/CushionReport.js +345 -0
  48. package/services/encode/EncodeAdmission.js +332 -0
  49. package/services/{orchestrators → encode}/EncodeOrchestrator.js +307 -29
  50. package/services/encode/EncodeRun.js +16 -9
  51. package/services/encode/EncodeRuns.js +1229 -0
  52. package/services/encode/Encoder.js +18 -2
  53. package/services/encode/EncoderSelection.js +38 -0
  54. package/services/encode/LocalObservations.js +359 -0
  55. package/services/encode/NvencEncoder.js +5 -0
  56. package/services/encode/OutputOpening.js +755 -0
  57. package/services/encode/OutputTimes.js +579 -0
  58. package/services/encode/Renditions.js +2162 -0
  59. package/services/encode/RunProgress.js +84 -0
  60. package/services/encode/SoftwareEncoder.js +12 -7
  61. package/services/encode/args.js +402 -186
  62. package/services/encode/calibration.js +361 -0
  63. package/services/encode/contention.js +128 -128
  64. package/services/encode/encode-run-state.js +16 -20
  65. package/services/encode/fingerprint.js +322 -0
  66. package/services/{hwaccel.js → encode/hwaccel.js} +82 -42
  67. package/services/{learned-median.js → encode/learned-median.js} +11 -0
  68. package/services/encode/output/EncodedOutput.js +57 -0
  69. package/services/encode/output/Output.js +131 -0
  70. package/services/{output/LiveOutputs.js → encode/output/OutputCatalog.js} +141 -40
  71. package/services/encode/output/OutputSpec.js +390 -0
  72. package/services/{output → encode/output}/Timeline.js +12 -37
  73. package/services/encode/output/index.js +20 -0
  74. package/services/encode/output/ladder.js +32 -0
  75. package/services/{output → encode/output}/playlists.js +3 -0
  76. package/services/encode/output-key-format.js +21 -0
  77. package/services/encode/quality/EncodeCost.js +1083 -0
  78. package/services/encode/quality/HostLoad.js +525 -0
  79. package/services/encode/quality/HostTimings.js +249 -0
  80. package/services/encode/quality/OutputQualityState.js +28 -0
  81. package/services/encode/quality/QualityController.js +935 -0
  82. package/services/encode/quality/QualityOffer.js +416 -0
  83. package/services/encode/quality/drain-threat.js +47 -0
  84. package/services/encode/quality/link-budget.js +254 -0
  85. package/services/encode/quality/output-format.js +266 -0
  86. package/services/encode/quality/serving-output.js +216 -0
  87. package/services/encode/quality/visible-rung.js +57 -0
  88. package/services/encode/run-command.js +23 -20
  89. package/services/encode/run-inputs.js +42 -0
  90. package/services/{segment-formats → encode/segment-formats}/fmp4.js +63 -0
  91. package/services/{segment-formats → encode/segment-formats}/index.js +3 -3
  92. package/services/encode/segment-formats/init-compat.js +324 -0
  93. package/services/{segment-formats → encode/segment-formats}/mp4-boxes.js +8 -3
  94. package/services/encode/throughput.js +97 -0
  95. package/services/{orchestrators → media}/ContainerOrchestrator.js +45 -7
  96. package/services/media/KeyframeTables.js +316 -0
  97. package/services/{source → media}/SourceFile.js +7 -55
  98. package/services/{torrent-worker/subtitle-cues.js → media/SubtitleCues.js} +103 -132
  99. package/services/media/SubtitleOrchestrator.js +167 -0
  100. package/services/{audio-inventory.js → media/audio-inventory.js} +7 -1
  101. package/services/{container → media/container}/AviContainer.js +266 -266
  102. package/services/{container → media/container}/Container.js +596 -601
  103. package/services/media/container/ContainerFactory.js +56 -0
  104. package/services/media/container/KeyframeTable.js +272 -0
  105. package/services/{container → media/container}/MatroskaContainer.js +1485 -1485
  106. package/services/{container → media/container}/Mp4Container.js +1244 -1244
  107. package/services/{container → media/container}/SubtitleFileContainer.js +322 -322
  108. package/services/media/keyframe-probe.js +157 -0
  109. package/services/media/media-info-probe.js +93 -0
  110. package/services/{playback-planner.js → media/playback-planner.js} +82 -39
  111. package/services/{tracks → media/tracks}/AudioTrack.js +131 -131
  112. package/services/{tracks → media/tracks}/language-detect.js +174 -174
  113. package/services/server/OutputLifecycle.js +418 -0
  114. package/services/server/SegmentServing.js +1146 -0
  115. package/services/server/ViewerRequests.js +522 -0
  116. package/services/server/controllers/PlaybackController.js +36 -0
  117. package/services/{controllers → server/controllers}/SubtitleController.js +17 -33
  118. package/services/server/http-responses.js +60 -0
  119. package/services/server/transcode-session-files.js +325 -0
  120. package/services/server/wire-outputs.js +983 -0
  121. package/services/storage/Diagnostics.js +202 -0
  122. package/services/storage/MachineBudget.js +235 -0
  123. package/services/{piece-store → storage}/allowance.js +107 -107
  124. package/services/{core-dumps.js → storage/core-dumps.js} +1 -1
  125. package/services/{files → storage/files}/CompletedFiles.js +116 -1
  126. package/services/{disk → storage}/free.js +33 -0
  127. package/services/storage/machine-memory.js +56 -0
  128. package/services/{memory-report.js → storage/memory-report.js} +575 -596
  129. package/services/{piece-store → storage/piece-store}/piece-disk-store.js +0 -7
  130. package/services/{piece-store → storage/piece-store}/shared-piece-store.js +28 -21
  131. package/services/{encode → storage/segment-store}/SegmentStore.js +395 -8
  132. package/services/storage/wire.js +175 -0
  133. package/services/{download → torrent/download}/SwarmSelection.js +2 -2
  134. package/services/{download → torrent/download}/registry.js +2 -2
  135. package/services/torrent/files.js +1 -1
  136. package/services/torrent/piece-store-of.js +29 -0
  137. package/services/{torrent-pool.js → torrent/torrent-pool.js} +26 -119
  138. package/services/{torrent-worker → torrent/worker}/background-fill.js +2 -2
  139. package/services/{torrent-worker → torrent/worker}/channel.js +271 -250
  140. package/services/{torrent-worker → torrent/worker}/client.js +77 -58
  141. package/services/torrent/worker/held-bytes.js +155 -0
  142. package/services/{torrent-worker → torrent/worker}/install-webrtc-shim.js +1 -1
  143. package/services/{torrent-worker → torrent/worker}/piece-reader.js +21 -30
  144. package/services/{torrent-worker → torrent/worker}/pool-adapter.js +101 -137
  145. package/services/{torrent-worker → torrent/worker}/protocol.js +26 -30
  146. package/services/torrent/worker/resume-warm.js +107 -0
  147. package/services/{torrent-worker → torrent/worker}/webrtc-shim.js +2 -2
  148. package/services/{torrent-worker → torrent/worker}/worker.js +68 -159
  149. package/services/{data-channel-handler.js → transport/data-channel-handler.js} +1356 -1356
  150. package/services/{delivery-probe.js → transport/delivery-probe.js} +842 -842
  151. package/services/{health-collector.js → transport/health-collector.js} +6 -35
  152. package/services/{packet-witness.js → transport/packet-witness.js} +808 -784
  153. package/services/{port-mapper.js → transport/port-mapper.js} +1 -1
  154. package/services/{usrsctp-state.js → transport/usrsctp-state.js} +1 -1
  155. package/services/{webrtc-manager.js → transport/webrtc-manager.js} +1 -1
  156. package/services/viewer/Assignments.js +340 -0
  157. package/services/{priority → viewer}/PriorityMap.js +390 -390
  158. package/services/{priority → viewer}/PriorityOrchestrator.js +325 -320
  159. package/services/viewer/Viewer.js +657 -483
  160. package/services/viewer/Viewers.js +368 -257
  161. package/services/viewer/active-output.js +37 -54
  162. package/services/viewer/audio-start.js +4 -3
  163. package/services/viewer/buffer-trend.js +81 -0
  164. package/services/viewer/choices.js +647 -0
  165. package/services/viewer/positions.js +126 -120
  166. package/services/viewer/report-intake.js +46 -45
  167. package/test/asset-paths.test.js +28 -0
  168. package/test/audio-inventory.test.js +3 -3
  169. package/test/audio-track-bitrate.test.js +40 -0
  170. package/test/auto-quality-step.test.js +707 -515
  171. package/test/available-share.test.js +1 -1
  172. package/test/background-fill.test.js +1 -1
  173. package/test/client-log-file.test.js +110 -110
  174. package/test/completed-files.test.js +1 -1
  175. package/test/concurrent-cost.test.js +13 -3
  176. package/test/container-media-info.test.js +3 -3
  177. package/test/core-dumps.test.js +1 -1
  178. package/test/coverage-follows-the-disk.test.js +46 -9
  179. package/test/cue-push-carries-what-the-walk-found.test.js +58 -0
  180. package/test/cut-times-timeline.test.js +2 -2
  181. package/test/cuts-follow-published-grid.test.js +9 -8
  182. package/test/data-channel-frame.test.js +1 -1
  183. package/test/dead-torrent-handle.test.js +1 -1
  184. package/test/declared-rates.test.js +2 -2
  185. package/test/declared-tracks.test.js +2 -2
  186. package/test/decode-cost-fit.test.js +1 -1
  187. package/test/decode-cost.test.js +34 -25
  188. package/test/delivery-probe.test.js +1 -1
  189. package/test/demand-register.test.js +4 -4
  190. package/test/dht-bootstrap.test.js +1 -1
  191. package/test/drain-threat.test.js +65 -0
  192. package/test/duplicate-add.test.js +1 -1
  193. package/test/encode-admission.test.js +146 -0
  194. package/test/encode-cost.test.js +199 -10
  195. package/test/encode-orchestrator.test.js +39 -1
  196. package/test/encode-plan-viewers.test.js +2 -2
  197. package/test/encode-run.test.js +2 -2
  198. package/test/encode-slope.test.js +1 -1
  199. package/test/encoded-output.test.js +39 -0
  200. package/test/encoder-readings.test.js +1 -1
  201. package/test/every-claimant-is-told-its-share.test.js +152 -0
  202. package/test/failed-encoder-still-serves-what-exists.test.js +354 -0
  203. package/test/failed-start-breaker.test.js +81 -30
  204. package/test/fastest-wires.test.js +1 -1
  205. package/test/ffmpeg-stream-counts.test.js +1 -1
  206. package/test/file-edges.test.js +3 -3
  207. package/test/fmp4-self-contained.test.js +1 -1
  208. package/test/fmp4-timestamps.test.js +1 -1
  209. package/test/fmp4-track-completeness.test.js +1 -1
  210. package/test/format-keyed-outputs.test.js +78 -0
  211. package/test/health-metrics.test.js +3 -2
  212. package/test/held-request-width.test.js +22 -23
  213. package/test/helpers/encode-run.js +136 -136
  214. package/test/helpers/held-file.js +29 -0
  215. package/test/helpers/manager.js +57 -0
  216. package/test/helpers/output-spec.js +63 -0
  217. package/test/host-load.test.js +1 -1
  218. package/test/index-accuracy-is-about-the-file.test.js +128 -0
  219. package/test/init-compat.test.js +219 -0
  220. package/test/input-lost-quiets-the-plan.test.js +1 -1
  221. package/test/input-unavailable.test.js +1 -1
  222. package/test/keeping-period.test.js +4 -4
  223. package/test/keyframe-index-accuracy.test.js +97 -45
  224. package/test/keyframe-table-budget.test.js +28 -33
  225. package/test/keyframes-belong-to-the-file.test.js +174 -57
  226. package/test/learned-median.test.js +1 -1
  227. package/test/letting-go-asks-who-is-watching.test.js +191 -0
  228. package/test/limit-row-by-area.test.js +60 -0
  229. package/test/link-budget.test.js +111 -0
  230. package/test/link-probe.test.js +56 -56
  231. package/test/local-encoding-data.test.js +273 -0
  232. package/test/look-ahead-coverage.test.js +1 -1
  233. package/test/manager-surface.test.js +294 -0
  234. package/test/matroska-blocks.test.js +0 -0
  235. package/test/matroska-cues-track.test.js +1 -1
  236. package/test/memory-budget.test.js +2 -2
  237. package/test/mp4-composition-times.test.js +0 -0
  238. package/test/mp4-subtitles.test.js +1 -1
  239. package/test/one-authority.test.js +45 -41
  240. package/test/one-budget.test.js +149 -0
  241. package/test/one-output-one-session.test.js +204 -0
  242. package/test/{disk-space.test.js → one-resource-divided.test.js} +30 -10
  243. package/test/one-serving-output-per-height.test.js +458 -0
  244. package/test/orchestrator-wired.test.js +46 -45
  245. package/test/output-catalog.test.js +39 -0
  246. package/test/output-shape.test.js +4 -51
  247. package/test/output-spec.test.js +251 -157
  248. package/test/output-speed.test.js +17 -15
  249. package/test/outputs-being-prepared.test.js +40 -0
  250. package/test/packet-witness-ring.test.js +2 -2
  251. package/test/packet-witness.test.js +1 -1
  252. package/test/piece-disk-store.test.js +2 -2
  253. package/test/piece-from-whole-file.test.js +1 -1
  254. package/test/piece-lru.test.js +1 -1
  255. package/test/piece-reader.test.js +2 -2
  256. package/test/piece-store-eviction.test.js +1 -1
  257. package/test/piece-store-never-refuses.test.js +1 -1
  258. package/test/piece-store-reservations.test.js +1 -1
  259. package/test/piece-store-slow-disk.test.js +1 -1
  260. package/test/piece-tail.test.js +1 -1
  261. package/test/plan-host-timings.test.js +1 -1
  262. package/test/plan-is-steady.test.js +225 -225
  263. package/test/priority-map-download.test.js +4 -4
  264. package/test/priority-map-emptied.test.js +204 -205
  265. package/test/priority-map-per-output.test.js +215 -209
  266. package/test/priority-map.test.js +1 -1
  267. package/test/probe-wedge-certainty.test.js +1 -1
  268. package/test/produced-copy-choice.test.js +15 -60
  269. package/test/quality-offer.test.js +162 -0
  270. package/test/quality-variants.test.js +1229 -1167
  271. package/test/rate-control-in-the-output.test.js +180 -0
  272. package/test/read-survives-withdrawal.test.js +2 -2
  273. package/test/read-window.test.js +10 -3
  274. package/test/resume-warm.test.js +1 -1
  275. package/test/run-inputs.test.js +80 -0
  276. package/test/run-position-follows-published-grid.test.js +13 -11
  277. package/test/run-progress.test.js +65 -0
  278. package/test/same-height-switch.test.js +664 -0
  279. package/test/seek-frees-the-viewers-requests.test.js +217 -0
  280. package/test/seek-landing.test.js +13 -13
  281. package/test/seek-target-not-superseded.test.js +28 -29
  282. package/test/segment-closed-on-both-branches.test.js +96 -0
  283. package/test/segment-cut-times.test.js +1 -1
  284. package/test/segment-hold-supersede.test.js +28 -35
  285. package/test/segment-judged-by-its-own-header.test.js +221 -0
  286. package/test/segment-serve-wiring.test.js +130 -39
  287. package/test/segment-store-eviction.test.js +22 -7
  288. package/test/segment-store.test.js +98 -14
  289. package/test/segment-true-start.test.js +1 -1
  290. package/test/segments-are-shared.test.js +17 -16
  291. package/test/send-chunk-bytes.test.js +1 -1
  292. package/test/serving-existing-output.test.js +102 -0
  293. package/test/serving-output.test.js +102 -0
  294. package/test/shared-piece-store.test.js +1 -1
  295. package/test/source-file.test.js +1 -1
  296. package/test/stale-request-after-seek.test.js +17 -21
  297. package/test/startup-readings.test.js +10 -5
  298. package/test/subtitle-cue-framing.test.js +5 -5
  299. package/test/subtitle-cue-source.test.js +36 -38
  300. package/test/subtitle-cue-walk.test.js +11 -10
  301. package/test/subtitle-defaults.test.js +1 -1
  302. package/test/subtitle-language.test.js +4 -4
  303. package/test/subtitle-track-numbering.test.js +5 -4
  304. package/test/supply-margin.test.js +1 -1
  305. package/test/swarm-follows-readers.test.js +3 -3
  306. package/test/swarm-reach.test.js +1 -1
  307. package/test/swarm-selection.test.js +3 -3
  308. package/test/tail-duplication.test.js +1 -1
  309. package/test/the-channel-has-both-ends.test.js +91 -0
  310. package/test/the-torrent-thread-serves-bytes.test.js +107 -0
  311. package/test/thread-pool.test.js +3 -3
  312. package/test/timeline.test.js +1 -12
  313. package/test/torrent-cost.test.js +1 -1
  314. package/test/tracks-begin-together.test.js +38 -24
  315. package/test/tunnel-renewal.test.js +1 -1
  316. package/test/two-viewers-one-picture.test.js +120 -87
  317. package/test/upload-hurry.test.js +3 -3
  318. package/test/usrsctp-state.test.js +1 -1
  319. package/test/video-facts.test.js +1 -1
  320. package/test/viewer-assignments.test.js +143 -0
  321. package/test/viewer-outputs.test.js +60 -52
  322. package/test/viewer-presence.test.js +22 -25
  323. package/test/viewer-requests-admission.test.js +89 -0
  324. package/test/viewer-states.test.js +189 -202
  325. package/test/viewer-subtitle-subscription.test.js +1 -1
  326. package/test/viewer.test.js +139 -136
  327. package/test/viewing-generations.test.js +390 -0
  328. package/test/visible-rung.test.js +37 -0
  329. package/test/wait-ledger.test.js +1 -1
  330. package/test/webrtc-shim.test.js +3 -3
  331. package/test/wedge-certainty.test.js +2 -2
  332. package/test/withdraw-piece-claim.test.js +2 -2
  333. package/test/worker-channel.test.js +1 -31
  334. package/utils/client-log-file.js +151 -151
  335. package/scripts/read-container-index.mjs +0 -108
  336. package/services/container/ContainerFactory.js +0 -122
  337. package/services/controllers/PlaybackController.js +0 -33
  338. package/services/disk/DiskSpace.js +0 -150
  339. package/services/disk/wire.js +0 -61
  340. package/services/hls-session-manager.js +0 -9608
  341. package/services/orchestrators/SubtitleOrchestrator.js +0 -160
  342. package/services/output/Output.js +0 -94
  343. package/services/output/OutputSpec.js +0 -202
  344. package/services/output/index.js +0 -1
  345. package/services/output/ladder.js +0 -26
  346. package/services/produced-index.js +0 -222
  347. package/services/quality/EncodeCost.js +0 -555
  348. package/services/torrent-worker/container-tracks.js +0 -414
  349. package/services/viewer/link-readings.js +0 -56
  350. package/test/produced-index.test.js +0 -142
  351. /package/{services → bin}/thread-pool.js +0 -0
  352. /package/services/{available-share.js → encode/available-share.js} +0 -0
  353. /package/services/{decode-cost-fit.js → encode/decode-cost-fit.js} +0 -0
  354. /package/services/{encoder-readings.js → encode/encoder-readings.js} +0 -0
  355. /package/services/{host-load.js → encode/host-load.js} +0 -0
  356. /package/services/{output → encode/output}/cut-grid.js +0 -0
  357. /package/services/{output → encode/output}/rates.js +0 -0
  358. /package/services/{segment-formats → encode/segment-formats}/mpegts.js +0 -0
  359. /package/services/{torrent-cost.js → encode/torrent-cost.js} +0 -0
  360. /package/services/{container → media/container}/ebml-reader.js +0 -0
  361. /package/services/{ffmpeg-banner.js → media/ffmpeg-banner.js} +0 -0
  362. /package/services/{tracks → media/tracks}/ContainerTrack.js +0 -0
  363. /package/services/{tracks → media/tracks}/ImageSubtitleTrack.js +0 -0
  364. /package/services/{tracks → media/tracks}/SubtitleTrack.js +0 -0
  365. /package/services/{tracks → media/tracks}/TextSubtitleTrack.js +0 -0
  366. /package/services/{tracks → media/tracks}/VideoTrack.js +0 -0
  367. /package/services/{tracks → media/tracks}/index.js +0 -0
  368. /package/services/{files → storage/files}/piece-from-whole-file.js +0 -0
  369. /package/services/{disk → storage}/keep.js +0 -0
  370. /package/services/{piece-store → storage/piece-store}/piece-lru.js +0 -0
  371. /package/services/{disk → storage}/returns.js +0 -0
  372. /package/services/{demand → torrent/demand}/DemandRegister.js +0 -0
  373. /package/services/{demand → torrent/demand}/Urgency.js +0 -0
  374. /package/services/{demand → torrent/demand}/Window.js +0 -0
  375. /package/services/{demand → torrent/demand}/index.js +0 -0
  376. /package/services/{demand → torrent/demand}/pieces.js +0 -0
  377. /package/services/{download → torrent/download}/withdraw-claim.js +0 -0
  378. /package/services/{supply-margin.js → torrent/supply-margin.js} +0 -0
  379. /package/services/{torrent-source-key.js → torrent/torrent-source-key.js} +0 -0
  380. /package/services/{torrent-worker → torrent/worker}/fastest-wires.js +0 -0
  381. /package/services/{torrent-worker → torrent/worker}/handle-state.js +0 -0
  382. /package/services/{nat-classifier.js → transport/nat-classifier.js} +0 -0
  383. /package/services/{registry-api.js → transport/registry-api.js} +0 -0
  384. /package/services/{tunnel-client.js → transport/tunnel-client.js} +0 -0
  385. /package/services/{priority → viewer}/WaitLedger.js +0 -0
@@ -0,0 +1,254 @@
1
+ /**
2
+ * @file What a viewer's link can carry, and what an output asks of it.
3
+ *
4
+ * PURE FUNCTIONS: the viewer's connection enters as a NUMBER — how many megabits
5
+ * the last report showed — and never as a reference to a viewer. Nothing here
6
+ * holds anybody, reads anything or can be asked at the wrong moment.
7
+ *
8
+ * WHAT A LINK CARRIES IS A LOAD, NOT A PICTURE (roadmap item 97, step 11). The
9
+ * viewer receives the picture AND the soundtrack they chose — inside the
10
+ * picture's own stream, or as a separate one — so both are added before the
11
+ * link is asked. And each part says how much its figure can be trusted:
12
+ *
13
+ * 1. `known` — a bound the encoder is held to: a re-encode's `maxrate`, or the
14
+ * constant rate a re-encoded soundtrack is produced at;
15
+ * 2. `estimated` — a figure the FILE states: a copied picture's average rate,
16
+ * a copied soundtrack's stated rate. An average is not a bound, so a load
17
+ * built on it is admitted only by estimate and is never confirmed;
18
+ * 3. `unknown` — no figure at all: a hardware encoder given no limit, a copied
19
+ * soundtrack whose rate nothing states.
20
+ *
21
+ * A load is as trustworthy as its least trustworthy part.
22
+ */
23
+
24
+ import { maxrateKbpsFor, nominalKbpsFor } from "../args.js";
25
+
26
+ /**
27
+ * How much of a measured link may be spent on the stream.
28
+ *
29
+ * The rest is what the link does when it is not being perfect: transport
30
+ * framing, retransmission, the other tabs, the moment somebody else in the
31
+ * house starts something. A load sized to the whole reading stalls on the
32
+ * first of those.
33
+ *
34
+ * Chosen, not measured, and written as a constant rather than dressed up as a
35
+ * measurement. What would replace it is a reading of the transport's own
36
+ * overhead and of how far a link's throughput varies over a session, which
37
+ * nothing takes.
38
+ */
39
+ export const LINK_SAFETY = 0.8;
40
+
41
+ /** How far a figure in a load can be trusted. */
42
+ export const PEAK_CLASS = Object.freeze({
43
+ KNOWN: "known",
44
+ ESTIMATED: "estimated",
45
+ UNKNOWN: "unknown"
46
+ });
47
+
48
+ /** What a link was found to do with a load. */
49
+ export const LINK_VERDICT = Object.freeze({
50
+ FITS: "fits",
51
+ ESTIMATED_TO_FIT: "estimated to fit",
52
+ DOES_NOT_FIT: "does not fit",
53
+ NO_SAFE_BOUND: "no safe bound",
54
+ NO_MEASUREMENT: "no measurement"
55
+ });
56
+
57
+ const CLASS_ORDER = [PEAK_CLASS.KNOWN, PEAK_CLASS.ESTIMATED, PEAK_CLASS.UNKNOWN];
58
+
59
+ /**
60
+ * @typedef {{ mbps: number | null, peakClass: string }} LoadPart
61
+ * @typedef {{ video: LoadPart | null, audio: LoadPart | null, totalMbps: number | null, peakClass: string }} Load
62
+ */
63
+
64
+ /**
65
+ * @param {number | null} mbps
66
+ * @param {string} peakClass
67
+ * @returns {LoadPart}
68
+ */
69
+ function part(mbps, peakClass) {
70
+ const figure = Number.isFinite(mbps) && mbps > 0 ? mbps : null;
71
+ // A figure that is missing is no figure, whatever the part claimed to be.
72
+ return { mbps: figure, peakClass: figure === null ? PEAK_CLASS.UNKNOWN : peakClass };
73
+ }
74
+
75
+ /**
76
+ * The picture part of a load, read from an output's identity.
77
+ *
78
+ * An encoder given no limit states no bound, and its part is `unknown` — unless
79
+ * this host has seen outputs of exactly this mode carry a peak (roadmap item
80
+ * 97, step 14), and then that peak is its figure, as an ESTIMATE: it is what
81
+ * was seen, not a bound anything holds the encoder to.
82
+ *
83
+ * @param {import("../output/OutputSpec.js").OutputSpec} spec
84
+ * @param {number | null} sourceMbps - The average rate the source file states.
85
+ * @param {number | null} [observedPeakMbps] - The largest peak outputs of this
86
+ * mode were seen carrying on this configuration, or null.
87
+ * @returns {LoadPart | null} Null for an output that carries no picture.
88
+ */
89
+ export function videoLoadOfSpec(spec, sourceMbps, observedPeakMbps = null) {
90
+ if (!spec?.video) {
91
+ return null;
92
+ }
93
+ const encode = spec.video.encode;
94
+ if (!encode) {
95
+ return part(sourceMbps, PEAK_CLASS.ESTIMATED);
96
+ }
97
+ const rateControl = encode.rateControl;
98
+ if (rateControl) {
99
+ return part(rateControl.maxrateKbps / 1000, PEAK_CLASS.KNOWN);
100
+ }
101
+ return Number.isFinite(observedPeakMbps) && observedPeakMbps > 0
102
+ ? part(observedPeakMbps, PEAK_CLASS.ESTIMATED)
103
+ : part(null, PEAK_CLASS.UNKNOWN);
104
+ }
105
+
106
+ /**
107
+ * The picture part of a load for a height nothing has produced yet, the way
108
+ * it WOULD be produced here: at the nominal limit on the software encoder,
109
+ * with no limit on a hardware one, and as the source's own stream when the
110
+ * picture is copied at the source's height.
111
+ *
112
+ * The nominal limit is the one of the FRAME the height would be encoded at,
113
+ * chosen by its area (`nominalKbpsFor`) — the same row the output is then
114
+ * opened in, so the offer and the output cannot price one step differently.
115
+ *
116
+ * @param {{ sourceHeight: number, copiesAtSource: boolean, sourceMbps: number | null, encoderKind: string }} picture
117
+ * @param {{ width: number, height: number }} frame - The frame this height is
118
+ * encoded at for this source.
119
+ * @returns {LoadPart}
120
+ */
121
+ export function videoLoadForFrame(picture, frame) {
122
+ const sourceHeight = Math.round(Number(picture?.sourceHeight) || 0);
123
+ if (picture?.copiesAtSource === true && frame.height === sourceHeight) {
124
+ return part(picture?.sourceMbps ?? null, PEAK_CLASS.ESTIMATED);
125
+ }
126
+ if (picture?.encoderKind !== "software") {
127
+ return part(null, PEAK_CLASS.UNKNOWN);
128
+ }
129
+ return part(maxrateKbpsFor(nominalKbpsFor(frame)) / 1000, PEAK_CLASS.KNOWN);
130
+ }
131
+
132
+ /**
133
+ * The picture part of a load for a software encode at a nominal limit nothing
134
+ * has produced yet. The bound is the one `softwareRateControlFor` would give
135
+ * it, so an output opened at that limit carries exactly this load.
136
+ *
137
+ * @param {number} nominalKbps
138
+ * @returns {LoadPart}
139
+ */
140
+ export function videoLoadOfLimit(nominalKbps) {
141
+ return part(maxrateKbpsFor(nominalKbps) / 1000, PEAK_CLASS.KNOWN);
142
+ }
143
+
144
+ /**
145
+ * The soundtrack part of a load: the track THIS viewer chose.
146
+ *
147
+ * @param {{ transcode: boolean, bitrateKbps: number | null } | null} track -
148
+ * Null when the viewer receives no sound from this output or beside it.
149
+ * @param {number} transcodeKbps - What a re-encoded track is produced at.
150
+ * @returns {LoadPart | null}
151
+ */
152
+ export function audioLoadOf(track, transcodeKbps) {
153
+ if (!track) {
154
+ return null;
155
+ }
156
+ if (track.transcode === true) {
157
+ return part(transcodeKbps / 1000, PEAK_CLASS.KNOWN);
158
+ }
159
+ return part((Number(track.bitrateKbps) || 0) / 1000, PEAK_CLASS.ESTIMATED);
160
+ }
161
+
162
+ /**
163
+ * The whole load: the parts added, and the class of the least trustworthy.
164
+ *
165
+ * @param {LoadPart | null} video
166
+ * @param {LoadPart | null} audio
167
+ * @returns {Load}
168
+ */
169
+ export function loadOf(video, audio) {
170
+ const parts = [video, audio].filter(Boolean);
171
+ const peakClass = parts.reduce(
172
+ (worst, one) => (CLASS_ORDER.indexOf(one.peakClass) > CLASS_ORDER.indexOf(worst) ? one.peakClass : worst),
173
+ PEAK_CLASS.KNOWN
174
+ );
175
+ const totalMbps = peakClass === PEAK_CLASS.UNKNOWN
176
+ ? null
177
+ : parts.reduce((sum, one) => sum + one.mbps, 0);
178
+ return { video, audio, totalMbps, peakClass };
179
+ }
180
+
181
+ /**
182
+ * What a link that measured `linkMbps` does with a load.
183
+ *
184
+ * 1. nothing measured the link — every class is admitted, since the link gave
185
+ * no ground to refuse, and nothing is confirmed;
186
+ * 2. an `unknown` load against a measured link — `no safe bound`, refused:
187
+ * there is no number to compare;
188
+ * 3. otherwise the load is compared with the usable share of the link: a
189
+ * `known` load that passes FITS and is confirmed, an `estimated` one that
190
+ * passes is admitted by estimate and is NOT confirmed.
191
+ *
192
+ * @param {number | null} linkMbps
193
+ * @param {Load} load
194
+ * @returns {{ verdict: string, admitted: boolean, confirmed: boolean, linkMbps: number | null, load: Load }}
195
+ */
196
+ export function linkCouldCarry(linkMbps, load) {
197
+ const measured = Number.isFinite(linkMbps) && linkMbps > 0;
198
+ const answer = (verdict, admitted, confirmed) => ({
199
+ verdict,
200
+ admitted,
201
+ confirmed,
202
+ linkMbps: measured ? linkMbps : null,
203
+ load
204
+ });
205
+ if (!measured) {
206
+ return answer(LINK_VERDICT.NO_MEASUREMENT, true, false);
207
+ }
208
+ if (load.peakClass === PEAK_CLASS.UNKNOWN) {
209
+ return answer(LINK_VERDICT.NO_SAFE_BOUND, false, false);
210
+ }
211
+ if (linkMbps * LINK_SAFETY < load.totalMbps) {
212
+ return answer(LINK_VERDICT.DOES_NOT_FIT, false, false);
213
+ }
214
+ return load.peakClass === PEAK_CLASS.KNOWN
215
+ ? answer(LINK_VERDICT.FITS, true, true)
216
+ : answer(LINK_VERDICT.ESTIMATED_TO_FIT, true, false);
217
+ }
218
+
219
+ /**
220
+ * How strongly an answer is admitted, for ordering: a load whose figure is a
221
+ * bound before one whose figure is an average, and both before one with no
222
+ * figure (which is only ever admitted with nothing measured). Zero for a
223
+ * refusal.
224
+ *
225
+ * @param {{ admitted: boolean, load: Load }} answer
226
+ * @returns {number}
227
+ */
228
+ export function admissionRank(answer) {
229
+ if (!answer.admitted) {
230
+ return 0;
231
+ }
232
+ return 3 - CLASS_ORDER.indexOf(answer.load.peakClass);
233
+ }
234
+
235
+ /**
236
+ * The figures behind an answer, as plain values a log line or a report can
237
+ * carry.
238
+ *
239
+ * @param {{ verdict: string, linkMbps: number | null, load: Load }} answer
240
+ * @returns {object}
241
+ */
242
+ export function linkAnswerFigures(answer) {
243
+ return {
244
+ verdict: answer.verdict,
245
+ linkMbps: answer.linkMbps,
246
+ linkSafety: LINK_SAFETY,
247
+ videoMbps: answer.load.video?.mbps ?? null,
248
+ videoClass: answer.load.video?.peakClass ?? null,
249
+ audioMbps: answer.load.audio?.mbps ?? null,
250
+ audioClass: answer.load.audio?.peakClass ?? null,
251
+ totalMbps: answer.load.totalMbps,
252
+ peakClass: answer.load.peakClass
253
+ };
254
+ }
@@ -0,0 +1,266 @@
1
+ /**
2
+ * @file The format an output produces, decided before it is named — because
3
+ * the name IS the format (decided with the user 2026-09-16).
4
+ *
5
+ * A copy is the source's own picture. A re-encode is which encoder, the size,
6
+ * the frame rate, the speed setting, whether HDR is tone mapped, and the rate
7
+ * control:
8
+ *
9
+ * 1. a size produced exactly — every rung of a master — is the box asked for,
10
+ * fitted to the source, and only its speed setting is chosen;
11
+ * 2. a size left to the budget is the highest rung of the viewer's own ladder
12
+ * this machine holds, with the speed setting for that rung;
13
+ * 3. on a hardware encoder, or with no startup measurement, there is no ladder
14
+ * to choose from and the box asked for is produced.
15
+ *
16
+ * The rate control is part of the format: a software encode is bounded at a
17
+ * limit from `limitsFor(frame)` — the nominal one until a set of lower limits
18
+ * is decided — or at the limit asked for (`capKbps`), and declared at the
19
+ * level of that size's nominal output. A hardware encoder is given no bound.
20
+ *
21
+ * AND THE VIEWER'S LINK DECIDES WHETHER IT MAY BE GIVEN AT ALL (roadmap item
22
+ * 97, step 11). Every format considered is judged by what this viewer's link
23
+ * does with its whole load — the picture and the soundtrack THEY chose
24
+ * (`link-budget.js`). In order:
25
+ *
26
+ * 1. an output already here that suits them (`serving-output.js`);
27
+ * 2. the size wanted, at the highest limit their link admits;
28
+ * 3. in AUTO only, a smaller rung of their ladder at the highest limit their
29
+ * link admits — a smaller size proves nothing by itself;
30
+ * 4. otherwise NOTHING: the answer is `unavailable`, with the figures, and no
31
+ * output is named. A format known not to fit is never handed over.
32
+ *
33
+ * Nothing here reads the machine or the disk: what it needs arrives as values
34
+ * and as functions over them.
35
+ */
36
+
37
+ import { computeOutputDimensions, nominalKbpsFor, softwareRateControlFor } from "../args.js";
38
+ import { buildResolutionLadder, canSustainOutput, pickSoftwarePreset } from "../hwaccel.js";
39
+ import { OutputSpec } from "../output/OutputSpec.js";
40
+ import { chooseServingOutput, nextRungAreaBelow, servingCandidates } from "./serving-output.js";
41
+ import { linkAnswerFigures, linkCouldCarry, loadOf, videoLoadOfSpec } from "./link-budget.js";
42
+
43
+ /**
44
+ * @param {object} params
45
+ * @param {boolean} params.encodesPicture - The output carries a picture and re-encodes it.
46
+ * @param {boolean} params.exact - The size is produced exactly as asked.
47
+ * @param {{ width: number, height: number }} params.target - The box asked for; zeroes mean the source's.
48
+ * @param {{ width: number, height: number, megabitsPerSecond: number | null, decode: object | null }} params.source
49
+ * @param {number} params.fps
50
+ * @param {{ kind: string, name: string }} params.encoder
51
+ * @param {unknown} params.benchmark - The startup measurement of the software encoder, or null.
52
+ * @param {{ decodeModel: object | null, observedDecodeCostSec: number | null, requiredSpeed: number | null }} params.cost
53
+ * @param {(params: object) => object | null} params.chooseBudget - The realtime budget's ladder choice.
54
+ * @param {boolean} params.tonemap
55
+ * @param {number | null} [params.capKbps] - A nominal limit asked for outright;
56
+ * above the size's own it is refused (`softwareRateControlFor`).
57
+ * @param {(frame: { width: number, height: number }) => number[]} [params.limitsFor] -
58
+ * The nominal limits a frame may be produced at, highest first; the row is
59
+ * chosen by the frame's area (`limitRowFor`).
60
+ * @param {import("./link-budget.js").LoadPart | null} [params.audioLoad] - The
61
+ * soundtrack this viewer receives with the picture, from it or beside it.
62
+ * @param {(encode: object | null) => OutputSpec} params.specWith - The output
63
+ * with this picture format and every other part as the request made it.
64
+ * @param {{ mode: "auto" | "manual", linkMbps: number | null, keys: Iterable<string>, readyAt: (key: string) => boolean, observedPeakMbps?: (spec: OutputSpec) => number | null }} params.serving
65
+ * @returns {{
66
+ * spec: OutputSpec | null,
67
+ * budget: object | null,
68
+ * wantedKey: string,
69
+ * reusedKey: string | null,
70
+ * answer: object | null,
71
+ * unavailable: { reason: string, figures: object | null } | null
72
+ * }}
73
+ */
74
+ export function decideOutputFormat({
75
+ encodesPicture,
76
+ exact,
77
+ target,
78
+ source,
79
+ fps,
80
+ encoder,
81
+ benchmark,
82
+ cost,
83
+ chooseBudget,
84
+ tonemap,
85
+ capKbps = null,
86
+ limitsFor = (frame) => [nominalKbpsFor(frame)],
87
+ audioLoad = null,
88
+ specWith,
89
+ serving
90
+ }) {
91
+ const judge = (spec) => linkCouldCarry(
92
+ serving.linkMbps,
93
+ loadOf(videoLoadOfSpec(spec, source.megabitsPerSecond, serving.observedPeakMbps?.(spec) ?? null), audioLoad)
94
+ );
95
+ const given = (spec, answer, extra = {}) => ({
96
+ spec,
97
+ budget: null,
98
+ wantedKey: spec.toKey(),
99
+ reusedKey: null,
100
+ answer,
101
+ unavailable: null,
102
+ ...extra
103
+ });
104
+ const refused = (spec, answer, reason, extra = {}) => ({
105
+ spec: null,
106
+ budget: null,
107
+ wantedKey: spec.toKey(),
108
+ reusedKey: null,
109
+ answer,
110
+ unavailable: { reason, figures: answer ? linkAnswerFigures(answer) : null, bound: "link" },
111
+ ...extra
112
+ });
113
+
114
+ if (!encodesPicture) {
115
+ const spec = specWith(null);
116
+ if (!spec.video) {
117
+ // A soundtrack on its own: the viewer's load was judged with the picture
118
+ // it plays beside, which counted this track.
119
+ return given(spec, null);
120
+ }
121
+ const answer = judge(spec);
122
+ return answer.admitted
123
+ ? given(spec, answer)
124
+ : refused(spec, answer, "the source's own picture is more than this viewer's link carries");
125
+ }
126
+
127
+ const ceiling = computeOutputDimensions(target.width, target.height, source.width, source.height);
128
+ const budget = exact
129
+ ? null
130
+ : chooseBudget({
131
+ transcodeVideo: true,
132
+ targetWidth: target.width,
133
+ targetHeight: target.height,
134
+ sourceWidth: source.width,
135
+ sourceHeight: source.height,
136
+ outputFps: fps,
137
+ source: source.decode,
138
+ requiredSpeed: cost.requiredSpeed
139
+ });
140
+ const width = budget?.width ?? ceiling?.w ?? 0;
141
+ const height = budget?.height ?? ceiling?.h ?? 0;
142
+ const priced = {
143
+ decodeModel: cost.decodeModel,
144
+ source: source.decode,
145
+ observedDecodeCostSec: cost.observedDecodeCostSec,
146
+ requiredSpeed: cost.requiredSpeed
147
+ };
148
+ const software = encoder.kind === "software";
149
+ const measured = software && Boolean(benchmark);
150
+ // WHETHER THIS MACHINE HAS SHOWN IT CAN ENCODE THIS SIZE (roadmap item 97,
151
+ // step 14): a mode of this encoder qualified at startup, measured at this
152
+ // frame, and fast enough on its own for this source. A size that fails is not
153
+ // a format anybody is given here — not the one asked for, and not a lower
154
+ // one in its place. Whether the machine has room for it BESIDE what already
155
+ // runs is the admission's, asked when the output is opened.
156
+ const machineCan = (w, h) => {
157
+ if (!(w > 0 && h > 0)) {
158
+ return true; // no size to judge: the source's own, which the probe could not read
159
+ }
160
+ if (!Array.isArray(benchmark)) {
161
+ return true; // not calibrated: a wiring made without the startup calibration
162
+ }
163
+ if (benchmark.length === 0) {
164
+ return false; // calibrated, and no mode qualified
165
+ }
166
+ return canSustainOutput({ benchmark, ...priced, outputPixelsPerSec: w * h * fps, frame: { width: w, height: h } }).sustainable;
167
+ };
168
+ const presetFor = (w, h) => (budget && w === width && h === height
169
+ ? budget.preset
170
+ : measured && w > 0
171
+ ? pickSoftwarePreset(benchmark, w * h * fps, priced, { width: w, height: h })
172
+ : null);
173
+ // Every format at one size this viewer could be given, highest limit first.
174
+ // A hardware encoder is given no limit, so it has exactly one.
175
+ const formatsAt = (w, h) => {
176
+ const limits = !software || !(w > 0 && h > 0)
177
+ ? [null]
178
+ : capKbps !== null && capKbps !== undefined && w === width && h === height
179
+ ? [capKbps]
180
+ : limitsFor({ width: w, height: h });
181
+ return limits.map((limit) => specWith({
182
+ encoder: encoder.name,
183
+ width: w,
184
+ height: h,
185
+ fps,
186
+ preset: presetFor(w, h),
187
+ tonemap,
188
+ rateControl: limit === null ? null : softwareRateControlFor({ width: w, height: h, fps, capKbps: limit })
189
+ }));
190
+ };
191
+ const wantedFormats = formatsAt(width, height);
192
+ const wantedSpec = wantedFormats[0];
193
+ const wantedKey = wantedSpec.toKey();
194
+
195
+ // A LIMIT NAMED IN THE REQUEST IS WHAT WAS ASKED FOR, and no other output
196
+ // stands in for it. It is named when a viewer is being moved to another
197
+ // limit of the height they watch (roadmap item 97, step 12), and the output
198
+ // they are being moved OFF always suits them by the rule below — so letting
199
+ // the rule answer would hand them back the very output the move is for
200
+ // leaving. An output with exactly this limit is still reused: it has the same
201
+ // key, and the key is what `OutputOpening` looks up.
202
+ const limitNamed = capKbps !== null && capKbps !== undefined;
203
+ if (ceiling && !limitNamed) {
204
+ const served = chooseServingOutput({
205
+ mode: serving.mode,
206
+ wanted: { width, height },
207
+ nextLowerArea: nextRungAreaBelow(buildResolutionLadder(ceiling.w, ceiling.h), height),
208
+ atRisk: measured && !canSustainOutput({ benchmark, ...priced, outputPixelsPerSec: width * height * fps, frame: { width, height } }).sustainable,
209
+ judge: (candidate) => judge(candidate.spec),
210
+ candidates: servingCandidates({
211
+ wanted: wantedSpec,
212
+ keys: serving.keys,
213
+ source,
214
+ encoderName: encoder.name,
215
+ readyAt: serving.readyAt
216
+ })
217
+ });
218
+ if (served && served.key !== wantedKey) {
219
+ return given(OutputSpec.fromKey(served.key), served.answer, { budget, wantedKey, reusedKey: served.key });
220
+ }
221
+ }
222
+
223
+ let anySizeHeld = machineCan(width, height);
224
+ if (anySizeHeld) {
225
+ for (const spec of wantedFormats) {
226
+ const answer = judge(spec);
227
+ if (answer.admitted) {
228
+ return given(spec, answer, { budget, wantedKey });
229
+ }
230
+ }
231
+ }
232
+ if (serving.mode !== "manual" && ceiling) {
233
+ for (const rung of buildResolutionLadder(ceiling.w, ceiling.h).filter((one) => one.height < height)) {
234
+ if (!machineCan(rung.width, rung.height)) {
235
+ continue;
236
+ }
237
+ anySizeHeld = true;
238
+ for (const spec of formatsAt(rung.width, rung.height)) {
239
+ const answer = judge(spec);
240
+ if (answer.admitted) {
241
+ return given(spec, answer, { wantedKey });
242
+ }
243
+ }
244
+ }
245
+ }
246
+ if (!anySizeHeld) {
247
+ // Not the viewer's link: this machine. Said apart, because the answer to
248
+ // it is another proxy, found before anything plays.
249
+ return {
250
+ ...refused(wantedSpec, null, `this machine has not shown it can re-encode this picture at ${width}x${height}@${fps} or any size below it`, { budget, wantedKey }),
251
+ unavailable: {
252
+ reason: `this machine has not shown it can re-encode this picture at ${width}x${height}@${fps} or any size below it`,
253
+ figures: { width, height, fps, encoder: encoder.name },
254
+ bound: "machine"
255
+ }
256
+ };
257
+ }
258
+ return refused(
259
+ wantedSpec,
260
+ judge(wantedSpec),
261
+ serving.mode === "manual"
262
+ ? "no limit of the size picked is admitted by this viewer's link"
263
+ : "no size and limit this viewer's ladder offers is admitted by their link",
264
+ { budget, wantedKey }
265
+ );
266
+ }