@torrent-tv/proxy 2.85.1 → 2.87.0

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 (381) hide show
  1. package/CHANGELOG.md +2138 -1909
  2. package/CLAUDE.md +318 -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/container-architecture.md +218 -213
  7. package/docs/download-architecture.md +7 -7
  8. package/docs/encode-architecture.md +287 -11
  9. package/docs/encode-run-state.md +2 -2
  10. package/docs/logs.md +97 -97
  11. package/docs/{disk-architecture.md → storage-architecture.md} +107 -1
  12. package/openspec/changes/adaptive-bitrate/design.md +2 -2
  13. package/openspec/changes/adaptive-bitrate/proposal.md +1 -1
  14. package/openspec/changes/adaptive-bitrate/tasks.md +1 -1
  15. package/openspec/changes/chunked-request-bodies/design.md +1 -1
  16. package/openspec/changes/chunked-request-bodies/proposal.md +1 -1
  17. package/openspec/changes/chunked-request-bodies/tasks.md +1 -1
  18. package/openspec/changes/cold-start/design.md +1 -1
  19. package/openspec/changes/disk-cap/proposal.md +1 -1
  20. package/openspec/changes/proxy-observability/proposal.md +2 -2
  21. package/openspec/changes/track-selection/proposal.md +1 -1
  22. package/openspec/changes/transcode-quality/proposal.md +1 -1
  23. package/package.json +2 -2
  24. package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
  25. package/research/double-spawn-2026-09-10.md +171 -171
  26. package/routes/api/playback-plan/post.js +2 -2
  27. package/routes/api/sources/files/get.js +1 -1
  28. package/routes/api/sources/stats/get.js +1 -1
  29. package/routes/api/sources/warm/post.js +10 -3
  30. package/routes/api/subtitles/get.js +230 -230
  31. package/routes/api/transcode-sessions/fragment-far/post.js +3 -3
  32. package/routes/api/transcode-sessions/net-report/post.js +129 -94
  33. package/routes/api/transcode-sessions/post.js +160 -139
  34. package/routes/api/transcode-sessions/progress/get.js +3 -3
  35. package/routes/api/transcode-sessions/release/post.js +3 -3
  36. package/routes/api/transcode-sessions/seek/post.js +12 -3
  37. package/routes/stream/get.js +2 -2
  38. package/routes/transcode/audio-file/get.js +15 -6
  39. package/routes/transcode/audio-warm/get.js +8 -5
  40. package/routes/transcode/session-file/get.js +429 -268
  41. package/routes/transcode/variant-file/get.js +47 -7
  42. package/routes/transcode/variant-warm/get.js +18 -6
  43. package/scripts/render-run-graph.js +1 -1
  44. package/server.js +720 -465
  45. package/services/encode/CoverageMap.js +428 -428
  46. package/services/encode/CushionReport.js +345 -0
  47. package/services/encode/EncodeAdmission.js +332 -0
  48. package/services/{orchestrators → encode}/EncodeOrchestrator.js +285 -17
  49. package/services/encode/EncodeRun.js +16 -9
  50. package/services/encode/EncodeRuns.js +1219 -0
  51. package/services/encode/Encoder.js +18 -2
  52. package/services/encode/EncoderSelection.js +38 -0
  53. package/services/encode/LocalObservations.js +359 -0
  54. package/services/encode/NvencEncoder.js +5 -0
  55. package/services/encode/OutputOpening.js +745 -0
  56. package/services/encode/OutputTimes.js +579 -0
  57. package/services/encode/Renditions.js +2162 -0
  58. package/services/encode/RunProgress.js +84 -0
  59. package/services/encode/SoftwareEncoder.js +12 -7
  60. package/services/encode/args.js +402 -186
  61. package/services/encode/calibration.js +361 -0
  62. package/services/encode/contention.js +128 -128
  63. package/services/encode/encode-run-state.js +16 -20
  64. package/services/encode/fingerprint.js +322 -0
  65. package/services/{hwaccel.js → encode/hwaccel.js} +82 -42
  66. package/services/{learned-median.js → encode/learned-median.js} +11 -0
  67. package/services/encode/output/EncodedOutput.js +57 -0
  68. package/services/encode/output/Output.js +131 -0
  69. package/services/{output/LiveOutputs.js → encode/output/OutputCatalog.js} +141 -40
  70. package/services/encode/output/OutputSpec.js +390 -0
  71. package/services/{output → encode/output}/Timeline.js +12 -37
  72. package/services/encode/output/index.js +20 -0
  73. package/services/encode/output/ladder.js +32 -0
  74. package/services/{output → encode/output}/playlists.js +3 -0
  75. package/services/encode/output-key-format.js +21 -0
  76. package/services/encode/quality/EncodeCost.js +1083 -0
  77. package/services/encode/quality/HostLoad.js +525 -0
  78. package/services/encode/quality/HostTimings.js +249 -0
  79. package/services/encode/quality/OutputQualityState.js +28 -0
  80. package/services/encode/quality/QualityController.js +931 -0
  81. package/services/encode/quality/QualityOffer.js +416 -0
  82. package/services/encode/quality/drain-threat.js +47 -0
  83. package/services/encode/quality/link-budget.js +254 -0
  84. package/services/encode/quality/output-format.js +266 -0
  85. package/services/encode/quality/serving-output.js +216 -0
  86. package/services/encode/quality/visible-rung.js +57 -0
  87. package/services/encode/run-command.js +23 -20
  88. package/services/encode/run-inputs.js +42 -0
  89. package/services/{segment-formats → encode/segment-formats}/fmp4.js +63 -0
  90. package/services/{segment-formats → encode/segment-formats}/index.js +3 -3
  91. package/services/encode/segment-formats/init-compat.js +324 -0
  92. package/services/{segment-formats → encode/segment-formats}/mp4-boxes.js +8 -3
  93. package/services/encode/throughput.js +97 -0
  94. package/services/{orchestrators → media}/ContainerOrchestrator.js +45 -7
  95. package/services/media/KeyframeTables.js +316 -0
  96. package/services/{source → media}/SourceFile.js +7 -55
  97. package/services/{torrent-worker/subtitle-cues.js → media/SubtitleCues.js} +103 -132
  98. package/services/media/SubtitleOrchestrator.js +167 -0
  99. package/services/{audio-inventory.js → media/audio-inventory.js} +7 -1
  100. package/services/{container → media/container}/AviContainer.js +266 -266
  101. package/services/{container → media/container}/Container.js +596 -601
  102. package/services/media/container/ContainerFactory.js +56 -0
  103. package/services/media/container/KeyframeTable.js +272 -0
  104. package/services/{container → media/container}/MatroskaContainer.js +1485 -1485
  105. package/services/{container → media/container}/Mp4Container.js +1244 -1244
  106. package/services/{container → media/container}/SubtitleFileContainer.js +322 -322
  107. package/services/media/keyframe-probe.js +157 -0
  108. package/services/media/media-info-probe.js +93 -0
  109. package/services/{playback-planner.js → media/playback-planner.js} +82 -39
  110. package/services/{tracks → media/tracks}/AudioTrack.js +131 -131
  111. package/services/{tracks → media/tracks}/language-detect.js +174 -174
  112. package/services/server/OutputLifecycle.js +418 -0
  113. package/services/server/SegmentServing.js +1146 -0
  114. package/services/server/ViewerRequests.js +522 -0
  115. package/services/server/controllers/PlaybackController.js +36 -0
  116. package/services/{controllers → server/controllers}/SubtitleController.js +17 -33
  117. package/services/server/wire-outputs.js +958 -0
  118. package/services/storage/Diagnostics.js +202 -0
  119. package/services/storage/MachineBudget.js +235 -0
  120. package/services/{piece-store → storage}/allowance.js +107 -107
  121. package/services/{core-dumps.js → storage/core-dumps.js} +1 -1
  122. package/services/{files → storage/files}/CompletedFiles.js +116 -1
  123. package/services/{disk → storage}/free.js +33 -0
  124. package/services/storage/machine-memory.js +56 -0
  125. package/services/{memory-report.js → storage/memory-report.js} +575 -596
  126. package/services/{piece-store → storage/piece-store}/piece-disk-store.js +0 -7
  127. package/services/{piece-store → storage/piece-store}/shared-piece-store.js +28 -21
  128. package/services/{encode → storage/segment-store}/SegmentStore.js +395 -8
  129. package/services/storage/wire.js +175 -0
  130. package/services/{download → torrent/download}/SwarmSelection.js +2 -2
  131. package/services/{download → torrent/download}/registry.js +2 -2
  132. package/services/torrent/files.js +1 -1
  133. package/services/torrent/piece-store-of.js +29 -0
  134. package/services/{torrent-pool.js → torrent/torrent-pool.js} +26 -119
  135. package/services/{torrent-worker → torrent/worker}/background-fill.js +2 -2
  136. package/services/{torrent-worker → torrent/worker}/channel.js +271 -250
  137. package/services/{torrent-worker → torrent/worker}/client.js +77 -58
  138. package/services/torrent/worker/held-bytes.js +155 -0
  139. package/services/{torrent-worker → torrent/worker}/install-webrtc-shim.js +1 -1
  140. package/services/{torrent-worker → torrent/worker}/piece-reader.js +21 -30
  141. package/services/{torrent-worker → torrent/worker}/pool-adapter.js +101 -137
  142. package/services/{torrent-worker → torrent/worker}/protocol.js +26 -30
  143. package/services/torrent/worker/resume-warm.js +107 -0
  144. package/services/{torrent-worker → torrent/worker}/webrtc-shim.js +2 -2
  145. package/services/{torrent-worker → torrent/worker}/worker.js +68 -159
  146. package/services/{data-channel-handler.js → transport/data-channel-handler.js} +1356 -1356
  147. package/services/{delivery-probe.js → transport/delivery-probe.js} +842 -842
  148. package/services/{health-collector.js → transport/health-collector.js} +6 -35
  149. package/services/{packet-witness.js → transport/packet-witness.js} +808 -784
  150. package/services/{port-mapper.js → transport/port-mapper.js} +1 -1
  151. package/services/{usrsctp-state.js → transport/usrsctp-state.js} +1 -1
  152. package/services/{webrtc-manager.js → transport/webrtc-manager.js} +1 -1
  153. package/services/viewer/Assignments.js +340 -0
  154. package/services/{priority → viewer}/PriorityMap.js +390 -390
  155. package/services/{priority → viewer}/PriorityOrchestrator.js +325 -320
  156. package/services/viewer/Viewer.js +657 -489
  157. package/services/viewer/Viewers.js +368 -257
  158. package/services/viewer/active-output.js +37 -54
  159. package/services/viewer/audio-start.js +46 -0
  160. package/services/viewer/buffer-trend.js +81 -0
  161. package/services/viewer/choices.js +647 -0
  162. package/services/viewer/positions.js +126 -120
  163. package/services/viewer/report-intake.js +46 -44
  164. package/test/asset-paths.test.js +28 -0
  165. package/test/audio-inventory.test.js +3 -3
  166. package/test/audio-track-bitrate.test.js +40 -0
  167. package/test/auto-quality-step.test.js +707 -518
  168. package/test/available-share.test.js +1 -1
  169. package/test/background-fill.test.js +1 -1
  170. package/test/client-log-file.test.js +110 -110
  171. package/test/completed-files.test.js +1 -1
  172. package/test/concurrent-cost.test.js +13 -3
  173. package/test/container-media-info.test.js +3 -3
  174. package/test/core-dumps.test.js +1 -1
  175. package/test/coverage-follows-the-disk.test.js +40 -8
  176. package/test/cue-push-carries-what-the-walk-found.test.js +58 -0
  177. package/test/cut-times-timeline.test.js +2 -2
  178. package/test/cuts-follow-published-grid.test.js +9 -8
  179. package/test/data-channel-frame.test.js +1 -1
  180. package/test/dead-torrent-handle.test.js +1 -1
  181. package/test/declared-rates.test.js +2 -2
  182. package/test/declared-tracks.test.js +2 -2
  183. package/test/decode-cost-fit.test.js +1 -1
  184. package/test/decode-cost.test.js +34 -25
  185. package/test/delivery-probe.test.js +1 -1
  186. package/test/demand-register.test.js +4 -4
  187. package/test/dht-bootstrap.test.js +1 -1
  188. package/test/drain-threat.test.js +65 -0
  189. package/test/duplicate-add.test.js +1 -1
  190. package/test/encode-admission.test.js +146 -0
  191. package/test/encode-cost.test.js +199 -10
  192. package/test/encode-orchestrator.test.js +39 -1
  193. package/test/encode-plan-viewers.test.js +2 -2
  194. package/test/encode-run.test.js +2 -2
  195. package/test/encode-slope.test.js +1 -1
  196. package/test/encoded-output.test.js +39 -0
  197. package/test/encoder-readings.test.js +1 -1
  198. package/test/every-claimant-is-told-its-share.test.js +152 -0
  199. package/test/failed-encoder-still-serves-what-exists.test.js +354 -0
  200. package/test/failed-start-breaker.test.js +81 -30
  201. package/test/fastest-wires.test.js +1 -1
  202. package/test/ffmpeg-stream-counts.test.js +1 -1
  203. package/test/file-edges.test.js +3 -3
  204. package/test/fmp4-self-contained.test.js +1 -1
  205. package/test/fmp4-timestamps.test.js +1 -1
  206. package/test/fmp4-track-completeness.test.js +1 -1
  207. package/test/format-keyed-outputs.test.js +78 -0
  208. package/test/health-metrics.test.js +3 -2
  209. package/test/held-request-width.test.js +22 -23
  210. package/test/helpers/encode-run.js +136 -136
  211. package/test/helpers/held-file.js +29 -0
  212. package/test/helpers/manager.js +57 -0
  213. package/test/helpers/output-spec.js +63 -0
  214. package/test/host-load.test.js +1 -1
  215. package/test/index-accuracy-is-about-the-file.test.js +128 -0
  216. package/test/init-compat.test.js +219 -0
  217. package/test/input-lost-quiets-the-plan.test.js +1 -1
  218. package/test/input-unavailable.test.js +1 -1
  219. package/test/keeping-period.test.js +4 -4
  220. package/test/keyframe-index-accuracy.test.js +97 -45
  221. package/test/keyframe-table-budget.test.js +28 -33
  222. package/test/keyframes-belong-to-the-file.test.js +174 -57
  223. package/test/learned-median.test.js +1 -1
  224. package/test/letting-go-asks-who-is-watching.test.js +191 -0
  225. package/test/limit-row-by-area.test.js +60 -0
  226. package/test/link-budget.test.js +111 -0
  227. package/test/link-probe.test.js +56 -56
  228. package/test/local-encoding-data.test.js +273 -0
  229. package/test/look-ahead-coverage.test.js +1 -1
  230. package/test/manager-surface.test.js +294 -0
  231. package/test/matroska-blocks.test.js +0 -0
  232. package/test/matroska-cues-track.test.js +1 -1
  233. package/test/memory-budget.test.js +2 -2
  234. package/test/mp4-composition-times.test.js +0 -0
  235. package/test/mp4-subtitles.test.js +1 -1
  236. package/test/one-authority.test.js +45 -41
  237. package/test/one-budget.test.js +149 -0
  238. package/test/one-output-one-session.test.js +204 -0
  239. package/test/{disk-space.test.js → one-resource-divided.test.js} +30 -10
  240. package/test/one-serving-output-per-height.test.js +458 -0
  241. package/test/orchestrator-wired.test.js +46 -45
  242. package/test/output-catalog.test.js +39 -0
  243. package/test/output-shape.test.js +4 -51
  244. package/test/output-spec.test.js +251 -157
  245. package/test/output-speed.test.js +17 -15
  246. package/test/outputs-being-prepared.test.js +40 -0
  247. package/test/packet-witness-ring.test.js +2 -2
  248. package/test/packet-witness.test.js +1 -1
  249. package/test/piece-disk-store.test.js +2 -2
  250. package/test/piece-from-whole-file.test.js +1 -1
  251. package/test/piece-lru.test.js +1 -1
  252. package/test/piece-reader.test.js +2 -2
  253. package/test/piece-store-eviction.test.js +1 -1
  254. package/test/piece-store-never-refuses.test.js +1 -1
  255. package/test/piece-store-reservations.test.js +1 -1
  256. package/test/piece-store-slow-disk.test.js +1 -1
  257. package/test/piece-tail.test.js +1 -1
  258. package/test/plan-host-timings.test.js +1 -1
  259. package/test/plan-is-steady.test.js +225 -225
  260. package/test/priority-map-download.test.js +4 -4
  261. package/test/priority-map-emptied.test.js +204 -205
  262. package/test/priority-map-per-output.test.js +215 -209
  263. package/test/priority-map.test.js +1 -1
  264. package/test/probe-wedge-certainty.test.js +1 -1
  265. package/test/produced-copy-choice.test.js +15 -60
  266. package/test/quality-offer.test.js +162 -0
  267. package/test/quality-variants.test.js +1229 -1167
  268. package/test/rate-control-in-the-output.test.js +180 -0
  269. package/test/read-survives-withdrawal.test.js +2 -2
  270. package/test/read-window.test.js +10 -3
  271. package/test/resume-warm.test.js +1 -1
  272. package/test/run-inputs.test.js +80 -0
  273. package/test/run-position-follows-published-grid.test.js +13 -11
  274. package/test/run-progress.test.js +65 -0
  275. package/test/same-height-switch.test.js +664 -0
  276. package/test/seek-frees-the-viewers-requests.test.js +217 -0
  277. package/test/seek-landing.test.js +13 -13
  278. package/test/seek-target-not-superseded.test.js +28 -29
  279. package/test/segment-closed-on-both-branches.test.js +96 -0
  280. package/test/segment-cut-times.test.js +1 -1
  281. package/test/segment-hold-supersede.test.js +28 -35
  282. package/test/segment-judged-by-its-own-header.test.js +221 -0
  283. package/test/segment-serve-wiring.test.js +130 -39
  284. package/test/segment-store-eviction.test.js +22 -7
  285. package/test/segment-store.test.js +98 -14
  286. package/test/segment-true-start.test.js +1 -1
  287. package/test/segments-are-shared.test.js +17 -16
  288. package/test/send-chunk-bytes.test.js +1 -1
  289. package/test/serving-existing-output.test.js +102 -0
  290. package/test/serving-output.test.js +102 -0
  291. package/test/shared-piece-store.test.js +1 -1
  292. package/test/source-file.test.js +1 -1
  293. package/test/stale-request-after-seek.test.js +17 -21
  294. package/test/startup-readings.test.js +10 -5
  295. package/test/subtitle-cue-framing.test.js +5 -5
  296. package/test/subtitle-cue-source.test.js +36 -38
  297. package/test/subtitle-cue-walk.test.js +11 -10
  298. package/test/subtitle-defaults.test.js +1 -1
  299. package/test/subtitle-language.test.js +4 -4
  300. package/test/subtitle-track-numbering.test.js +5 -4
  301. package/test/supply-margin.test.js +1 -1
  302. package/test/swarm-follows-readers.test.js +3 -3
  303. package/test/swarm-reach.test.js +1 -1
  304. package/test/swarm-selection.test.js +3 -3
  305. package/test/tail-duplication.test.js +1 -1
  306. package/test/the-channel-has-both-ends.test.js +91 -0
  307. package/test/the-torrent-thread-serves-bytes.test.js +107 -0
  308. package/test/thread-pool.test.js +3 -3
  309. package/test/timeline.test.js +1 -12
  310. package/test/torrent-cost.test.js +1 -1
  311. package/test/tracks-begin-together.test.js +38 -24
  312. package/test/tunnel-renewal.test.js +1 -1
  313. package/test/two-viewers-one-picture.test.js +120 -87
  314. package/test/upload-hurry.test.js +3 -3
  315. package/test/usrsctp-state.test.js +1 -1
  316. package/test/video-facts.test.js +1 -1
  317. package/test/viewer-assignments.test.js +143 -0
  318. package/test/viewer-outputs.test.js +60 -52
  319. package/test/viewer-presence.test.js +22 -25
  320. package/test/viewer-requests-admission.test.js +89 -0
  321. package/test/viewer-states.test.js +189 -172
  322. package/test/viewer-subtitle-subscription.test.js +1 -1
  323. package/test/viewer.test.js +139 -136
  324. package/test/viewing-generations.test.js +390 -0
  325. package/test/visible-rung.test.js +37 -0
  326. package/test/wait-ledger.test.js +1 -1
  327. package/test/webrtc-shim.test.js +3 -3
  328. package/test/wedge-certainty.test.js +2 -2
  329. package/test/withdraw-piece-claim.test.js +2 -2
  330. package/test/worker-channel.test.js +1 -31
  331. package/utils/client-log-file.js +151 -151
  332. package/scripts/read-container-index.mjs +0 -108
  333. package/services/container/ContainerFactory.js +0 -122
  334. package/services/controllers/PlaybackController.js +0 -33
  335. package/services/disk/DiskSpace.js +0 -150
  336. package/services/disk/wire.js +0 -61
  337. package/services/hls-session-manager.js +0 -9689
  338. package/services/orchestrators/SubtitleOrchestrator.js +0 -160
  339. package/services/output/Output.js +0 -94
  340. package/services/output/OutputSpec.js +0 -202
  341. package/services/output/index.js +0 -1
  342. package/services/output/ladder.js +0 -26
  343. package/services/produced-index.js +0 -222
  344. package/services/quality/EncodeCost.js +0 -555
  345. package/services/torrent-worker/container-tracks.js +0 -414
  346. package/test/produced-index.test.js +0 -142
  347. /package/{services → bin}/thread-pool.js +0 -0
  348. /package/services/{available-share.js → encode/available-share.js} +0 -0
  349. /package/services/{decode-cost-fit.js → encode/decode-cost-fit.js} +0 -0
  350. /package/services/{encoder-readings.js → encode/encoder-readings.js} +0 -0
  351. /package/services/{host-load.js → encode/host-load.js} +0 -0
  352. /package/services/{output → encode/output}/cut-grid.js +0 -0
  353. /package/services/{output → encode/output}/rates.js +0 -0
  354. /package/services/{segment-formats → encode/segment-formats}/mpegts.js +0 -0
  355. /package/services/{torrent-cost.js → encode/torrent-cost.js} +0 -0
  356. /package/services/{container → media/container}/ebml-reader.js +0 -0
  357. /package/services/{ffmpeg-banner.js → media/ffmpeg-banner.js} +0 -0
  358. /package/services/{tracks → media/tracks}/ContainerTrack.js +0 -0
  359. /package/services/{tracks → media/tracks}/ImageSubtitleTrack.js +0 -0
  360. /package/services/{tracks → media/tracks}/SubtitleTrack.js +0 -0
  361. /package/services/{tracks → media/tracks}/TextSubtitleTrack.js +0 -0
  362. /package/services/{tracks → media/tracks}/VideoTrack.js +0 -0
  363. /package/services/{tracks → media/tracks}/index.js +0 -0
  364. /package/services/{files → storage/files}/piece-from-whole-file.js +0 -0
  365. /package/services/{disk → storage}/keep.js +0 -0
  366. /package/services/{piece-store → storage/piece-store}/piece-lru.js +0 -0
  367. /package/services/{disk → storage}/returns.js +0 -0
  368. /package/services/{demand → torrent/demand}/DemandRegister.js +0 -0
  369. /package/services/{demand → torrent/demand}/Urgency.js +0 -0
  370. /package/services/{demand → torrent/demand}/Window.js +0 -0
  371. /package/services/{demand → torrent/demand}/index.js +0 -0
  372. /package/services/{demand → torrent/demand}/pieces.js +0 -0
  373. /package/services/{download → torrent/download}/withdraw-claim.js +0 -0
  374. /package/services/{supply-margin.js → torrent/supply-margin.js} +0 -0
  375. /package/services/{torrent-source-key.js → torrent/torrent-source-key.js} +0 -0
  376. /package/services/{torrent-worker → torrent/worker}/fastest-wires.js +0 -0
  377. /package/services/{torrent-worker → torrent/worker}/handle-state.js +0 -0
  378. /package/services/{nat-classifier.js → transport/nat-classifier.js} +0 -0
  379. /package/services/{registry-api.js → transport/registry-api.js} +0 -0
  380. /package/services/{tunnel-client.js → transport/tunnel-client.js} +0 -0
  381. /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
+ }