@torrent-tv/proxy 2.88.2 → 2.88.4

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 (475) hide show
  1. package/.claude/commands/opsx/apply.md +155 -155
  2. package/.claude/commands/opsx/archive.md +160 -160
  3. package/.claude/commands/opsx/explore.md +174 -174
  4. package/.claude/commands/opsx/propose.md +109 -109
  5. package/.claude/commands/opsx/sync.md +143 -143
  6. package/.claude/skills/openspec-apply-change/SKILL.md +159 -159
  7. package/.claude/skills/openspec-archive-change/SKILL.md +117 -117
  8. package/.claude/skills/openspec-explore/SKILL.md +289 -289
  9. package/.claude/skills/openspec-propose/SKILL.md +113 -113
  10. package/.claude/skills/openspec-sync-specs/SKILL.md +147 -147
  11. package/.dockerignore +5 -5
  12. package/AGENTS.md +322 -322
  13. package/CHANGELOG.md +2259 -2246
  14. package/Dockerfile +30 -30
  15. package/LICENSE +16 -16
  16. package/README.md +369 -369
  17. package/assets/calibration/NOTICE.md +100 -100
  18. package/assets/diagnostics/sctpstate.gdb +31 -31
  19. package/bin/cli.js +684 -684
  20. package/bin/thread-pool.js +30 -30
  21. package/biome.json +411 -411
  22. package/docs/browser-proxy-contract.md +23 -4
  23. package/docs/container-architecture.md +218 -218
  24. package/docs/download-architecture.md +261 -261
  25. package/docs/encode-architecture.md +729 -729
  26. package/docs/encode-run-state.md +96 -96
  27. package/docs/logs.md +97 -97
  28. package/docs/socks5-egress-research.md +114 -114
  29. package/docs/storage-architecture.md +267 -267
  30. package/knip.json +14 -14
  31. package/openspec/changes/adaptive-bitrate/design.md +92 -92
  32. package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
  33. package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
  34. package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
  35. package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
  36. package/openspec/changes/binary-distribution/proposal.md +53 -53
  37. package/openspec/changes/chunked-request-bodies/design.md +81 -81
  38. package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
  39. package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
  40. package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
  41. package/openspec/changes/cold-start/design.md +98 -98
  42. package/openspec/changes/cold-start/proposal.md +62 -62
  43. package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
  44. package/openspec/changes/cold-start/tasks.md +48 -48
  45. package/openspec/changes/disk-cap/.openspec.yaml +2 -2
  46. package/openspec/changes/disk-cap/proposal.md +37 -37
  47. package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
  48. package/openspec/changes/disk-cap/tasks.md +19 -19
  49. package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
  50. package/openspec/changes/proxy-observability/design.md +38 -38
  51. package/openspec/changes/proxy-observability/proposal.md +49 -49
  52. package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
  53. package/openspec/changes/proxy-observability/tasks.md +21 -21
  54. package/openspec/changes/seek-debounce/design.md +118 -118
  55. package/openspec/changes/seek-debounce/proposal.md +59 -59
  56. package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
  57. package/openspec/changes/seek-debounce/tasks.md +33 -33
  58. package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
  59. package/openspec/changes/subtitle-language/proposal.md +50 -50
  60. package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
  61. package/openspec/changes/subtitle-language/tasks.md +26 -26
  62. package/openspec/changes/track-selection/.openspec.yaml +2 -2
  63. package/openspec/changes/track-selection/design.md +47 -47
  64. package/openspec/changes/track-selection/proposal.md +45 -45
  65. package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
  66. package/openspec/changes/track-selection/tasks.md +20 -20
  67. package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
  68. package/openspec/changes/transcode-quality/proposal.md +51 -51
  69. package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
  70. package/openspec/changes/transcode-quality/tasks.md +63 -63
  71. package/openspec/config.yaml +38 -38
  72. package/package.json +1 -1
  73. package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
  74. package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
  75. package/research/double-spawn-2026-09-10.md +171 -171
  76. package/research/handover-2026-09-13.md +209 -209
  77. package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
  78. package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
  79. package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
  80. package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
  81. package/routes/api/client-logs/post.js +86 -86
  82. package/routes/api/delivery-sink/get.js +72 -72
  83. package/routes/api/link-probe/get.js +51 -51
  84. package/routes/api/playback-plan/audio-tracks/post.js +29 -29
  85. package/routes/api/playback-plan/post.js +53 -53
  86. package/routes/api/sources/post.js +36 -36
  87. package/routes/api/sources/stats/get.js +132 -132
  88. package/routes/api/sources/warm/post.js +217 -217
  89. package/routes/api/subtitles/get.js +230 -230
  90. package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
  91. package/routes/api/transcode-sessions/net-report/post.js +138 -138
  92. package/routes/api/transcode-sessions/post.js +178 -173
  93. package/routes/api/transcode-sessions/progress/get.js +27 -27
  94. package/routes/api/transcode-sessions/release/post.js +41 -41
  95. package/routes/api/transcode-sessions/seek/post.js +63 -63
  96. package/routes/health/get.js +13 -13
  97. package/routes/healthz/get.js +13 -13
  98. package/routes/stream/get.js +380 -380
  99. package/routes/transcode/audio-file/get.js +64 -64
  100. package/routes/transcode/audio-warm/get.js +95 -84
  101. package/routes/transcode/session-file/get.js +52 -52
  102. package/routes/transcode/variant-file/get.js +95 -95
  103. package/routes/transcode/variant-warm/get.js +84 -84
  104. package/scripts/render-run-graph.js +173 -173
  105. package/server.js +725 -725
  106. package/services/encode/CoverageMap.js +428 -428
  107. package/services/encode/CushionReport.js +345 -345
  108. package/services/encode/EncodeAdmission.js +376 -376
  109. package/services/encode/EncodeOrchestrator.js +1217 -1217
  110. package/services/encode/EncodePlan.js +1366 -1366
  111. package/services/encode/EncodeRun.js +813 -813
  112. package/services/encode/EncodeRuns.js +1235 -1235
  113. package/services/encode/Encoder.js +142 -142
  114. package/services/encode/EncoderSelection.js +38 -38
  115. package/services/encode/LocalObservations.js +359 -359
  116. package/services/encode/NvencEncoder.js +50 -50
  117. package/services/encode/OutputOpening.js +783 -758
  118. package/services/encode/OutputTimes.js +579 -579
  119. package/services/encode/QsvEncoder.js +58 -58
  120. package/services/encode/Renditions.js +2282 -2184
  121. package/services/encode/RunProgress.js +84 -84
  122. package/services/encode/SegmentDemand.js +95 -95
  123. package/services/encode/SoftwareEncoder.js +121 -121
  124. package/services/encode/V4l2m2mEncoder.js +53 -53
  125. package/services/encode/VaapiEncoder.js +66 -66
  126. package/services/encode/args.js +418 -416
  127. package/services/encode/available-share.js +89 -89
  128. package/services/encode/calibration.js +361 -361
  129. package/services/encode/contention.js +128 -128
  130. package/services/encode/decode-cost-fit.js +363 -363
  131. package/services/encode/encode-exit.js +114 -114
  132. package/services/encode/encode-run-state.js +551 -551
  133. package/services/encode/encoder-readings.js +45 -45
  134. package/services/encode/fingerprint.js +322 -322
  135. package/services/encode/host-load.js +174 -174
  136. package/services/encode/hwaccel.js +1894 -1894
  137. package/services/encode/index.js +9 -9
  138. package/services/encode/learned-median.js +90 -90
  139. package/services/encode/output/EncodedOutput.js +57 -57
  140. package/services/encode/output/Output.js +131 -131
  141. package/services/encode/output/OutputCatalog.js +414 -414
  142. package/services/encode/output/OutputSpec.js +390 -390
  143. package/services/encode/output/Timeline.js +308 -308
  144. package/services/encode/output/cut-grid.js +107 -107
  145. package/services/encode/output/index.js +20 -20
  146. package/services/encode/output/ladder.js +32 -32
  147. package/services/encode/output/playlists.js +284 -284
  148. package/services/encode/output/rates.js +125 -125
  149. package/services/encode/output-key-format.js +21 -21
  150. package/services/encode/quality/EncodeCost.js +1150 -1141
  151. package/services/encode/quality/HostLoad.js +525 -525
  152. package/services/encode/quality/HostTimings.js +249 -249
  153. package/services/encode/quality/OutputQualityState.js +28 -28
  154. package/services/encode/quality/QualityController.js +935 -935
  155. package/services/encode/quality/QualityOffer.js +416 -416
  156. package/services/encode/quality/drain-threat.js +47 -47
  157. package/services/encode/quality/link-budget.js +349 -254
  158. package/services/encode/quality/output-format.js +319 -315
  159. package/services/encode/quality/serving-output.js +216 -216
  160. package/services/encode/quality/visible-rung.js +57 -57
  161. package/services/encode/run-budget.js +97 -97
  162. package/services/encode/run-command.js +754 -754
  163. package/services/encode/run-costs.js +155 -155
  164. package/services/encode/run-inputs.js +42 -42
  165. package/services/encode/segment-formats/fmp4.js +456 -456
  166. package/services/encode/segment-formats/index.js +85 -85
  167. package/services/encode/segment-formats/init-compat.js +324 -324
  168. package/services/encode/segment-formats/mp4-boxes.js +21 -6
  169. package/services/encode/segment-formats/mpegts.js +134 -134
  170. package/services/encode/start-stop-cost.js +178 -178
  171. package/services/encode/throughput.js +97 -97
  172. package/services/encode/torrent-cost.js +88 -88
  173. package/services/identity/installation-id.js +38 -38
  174. package/services/media/ContainerOrchestrator.js +149 -149
  175. package/services/media/KeyframeTables.js +316 -316
  176. package/services/media/SourceFile.js +298 -298
  177. package/services/media/SubtitleCues.js +520 -520
  178. package/services/media/SubtitleOrchestrator.js +167 -167
  179. package/services/media/audio-inventory.js +298 -245
  180. package/services/media/container/AviContainer.js +266 -266
  181. package/services/media/container/Container.js +609 -603
  182. package/services/media/container/ContainerFactory.js +56 -56
  183. package/services/media/container/KeyframeTable.js +272 -272
  184. package/services/media/container/MatroskaContainer.js +1494 -1494
  185. package/services/media/container/Mp4Container.js +1344 -1244
  186. package/services/media/container/ebml-reader.js +159 -159
  187. package/services/media/ffmpeg-banner.js +260 -260
  188. package/services/media/keyframe-probe.js +157 -157
  189. package/services/media/media-info-probe.js +93 -93
  190. package/services/media/playback-planner.js +931 -931
  191. package/services/media/tracks/AudioTrack.js +143 -131
  192. package/services/media/tracks/ContainerTrack.js +75 -75
  193. package/services/media/tracks/ImageSubtitleTrack.js +19 -19
  194. package/services/media/tracks/SubtitleTrack.js +38 -38
  195. package/services/media/tracks/TextSubtitleTrack.js +287 -287
  196. package/services/media/tracks/VideoTrack.js +33 -33
  197. package/services/media/tracks/audio-bound.js +233 -0
  198. package/services/media/tracks/index.js +15 -15
  199. package/services/media/tracks/language-detect.js +174 -174
  200. package/services/server/OutputLifecycle.js +418 -418
  201. package/services/server/SegmentServing.js +1150 -1150
  202. package/services/server/ViewerRequests.js +760 -760
  203. package/services/server/WaitLedger.js +142 -142
  204. package/services/server/controllers/PlaybackController.js +40 -40
  205. package/services/server/controllers/SubtitleController.js +109 -109
  206. package/services/server/http-responses.js +60 -60
  207. package/services/server/playback-readiness.js +13 -8
  208. package/services/server/transcode-session-files.js +325 -325
  209. package/services/server/wire-outputs.js +995 -992
  210. package/services/storage/Diagnostics.js +202 -202
  211. package/services/storage/MachineBudget.js +235 -235
  212. package/services/storage/allowance.js +107 -107
  213. package/services/storage/core-dumps.js +106 -106
  214. package/services/storage/files/CompletedFiles.js +391 -391
  215. package/services/storage/files/piece-from-whole-file.js +118 -118
  216. package/services/storage/free.js +71 -71
  217. package/services/storage/keep.js +48 -48
  218. package/services/storage/machine-memory.js +56 -56
  219. package/services/storage/memory-report.js +575 -575
  220. package/services/storage/piece-store/piece-disk-store.js +539 -539
  221. package/services/storage/piece-store/piece-lru.js +495 -495
  222. package/services/storage/piece-store/shared-piece-store.js +1931 -1931
  223. package/services/storage/returns.js +103 -103
  224. package/services/storage/segment-store/SegmentStore.js +1240 -1240
  225. package/services/storage/wire.js +175 -175
  226. package/services/torrent/demand/DemandRegister.js +202 -202
  227. package/services/torrent/demand/Urgency.js +137 -137
  228. package/services/torrent/demand/Window.js +118 -118
  229. package/services/torrent/demand/index.js +8 -8
  230. package/services/torrent/demand/pieces.js +140 -140
  231. package/services/torrent/download/SwarmSelection.js +386 -386
  232. package/services/torrent/download/registry.js +102 -102
  233. package/services/torrent/download/withdraw-claim.js +80 -80
  234. package/services/torrent/episode-naming.js +182 -182
  235. package/services/torrent/files.js +455 -455
  236. package/services/torrent/naming.js +619 -619
  237. package/services/torrent/piece-store-of.js +29 -29
  238. package/services/torrent/supply-margin.js +262 -262
  239. package/services/torrent/torrent-pool.js +2675 -2675
  240. package/services/torrent/worker/background-fill.js +213 -213
  241. package/services/torrent/worker/channel.js +271 -271
  242. package/services/torrent/worker/client.js +765 -765
  243. package/services/torrent/worker/fastest-wires.js +342 -342
  244. package/services/torrent/worker/handle-state.js +23 -23
  245. package/services/torrent/worker/held-bytes.js +155 -155
  246. package/services/torrent/worker/install-webrtc-shim.js +32 -32
  247. package/services/torrent/worker/piece-reader.js +1078 -1078
  248. package/services/torrent/worker/pool-adapter.js +371 -371
  249. package/services/torrent/worker/protocol.js +184 -184
  250. package/services/torrent/worker/resume-warm.js +107 -107
  251. package/services/torrent/worker/webrtc-shim.js +219 -219
  252. package/services/torrent/worker/worker.js +1129 -1129
  253. package/services/transport/data-channel-handler.js +1369 -1369
  254. package/services/transport/delivery-probe.js +842 -842
  255. package/services/transport/health-collector.js +53 -53
  256. package/services/transport/nat-classifier.js +231 -231
  257. package/services/transport/packet-witness.js +808 -808
  258. package/services/transport/port-mapper.js +235 -235
  259. package/services/transport/registry-api.js +81 -81
  260. package/services/transport/tunnel-client.js +447 -447
  261. package/services/transport/usrsctp-state.js +175 -175
  262. package/services/transport/webrtc-manager.js +484 -484
  263. package/services/viewer/Assignments.js +340 -340
  264. package/services/viewer/PriorityMap.js +390 -390
  265. package/services/viewer/PriorityOrchestrator.js +325 -325
  266. package/services/viewer/Viewer.js +691 -691
  267. package/services/viewer/Viewers.js +368 -368
  268. package/services/viewer/active-output.js +37 -37
  269. package/services/viewer/audio-start.js +46 -46
  270. package/services/viewer/buffer-trend.js +81 -81
  271. package/services/viewer/choices.js +647 -647
  272. package/services/viewer/positions.js +126 -126
  273. package/services/viewer/report-intake.js +46 -46
  274. package/store/source-registry.js +71 -71
  275. package/test/asset-paths.test.js +28 -28
  276. package/test/audio-buffer-continuity.test.js +70 -0
  277. package/test/audio-inventory.test.js +212 -212
  278. package/test/audio-track-bitrate.test.js +40 -40
  279. package/test/auto-quality-step.test.js +707 -707
  280. package/test/available-share.test.js +76 -76
  281. package/test/background-fill.test.js +190 -190
  282. package/test/client-log-file.test.js +110 -110
  283. package/test/completed-files.test.js +115 -115
  284. package/test/concurrent-cost.test.js +148 -148
  285. package/test/container-media-info.test.js +228 -228
  286. package/test/contention.test.js +76 -76
  287. package/test/core-dumps.test.js +57 -57
  288. package/test/coverage-follows-the-disk.test.js +228 -228
  289. package/test/coverage-map.test.js +195 -195
  290. package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
  291. package/test/cut-times-timeline.test.js +65 -65
  292. package/test/cuts-follow-published-grid.test.js +109 -109
  293. package/test/data-channel-frame.test.js +63 -63
  294. package/test/data-channel-viewer-lifecycle.test.js +92 -92
  295. package/test/dead-torrent-handle.test.js +52 -52
  296. package/test/declared-rates.test.js +116 -116
  297. package/test/declared-tracks.test.js +43 -43
  298. package/test/decode-cost-fit.test.js +202 -202
  299. package/test/decode-cost.test.js +462 -462
  300. package/test/delivery-probe.test.js +393 -393
  301. package/test/demand-register.test.js +206 -206
  302. package/test/dht-bootstrap.test.js +72 -72
  303. package/test/drain-threat.test.js +65 -65
  304. package/test/duplicate-add.test.js +48 -48
  305. package/test/encode-admission.test.js +146 -146
  306. package/test/encode-cost.test.js +283 -283
  307. package/test/encode-exit.test.js +111 -111
  308. package/test/encode-orchestrator.test.js +419 -419
  309. package/test/encode-plan-viewers.test.js +718 -718
  310. package/test/encode-plan.test.js +545 -545
  311. package/test/encode-run-state.test.js +314 -314
  312. package/test/encode-run.test.js +162 -162
  313. package/test/encode-slope.test.js +46 -46
  314. package/test/encoded-output.test.js +39 -39
  315. package/test/encoder-kinds.test.js +122 -122
  316. package/test/encoder-readings.test.js +46 -46
  317. package/test/episode-naming.test.js +131 -131
  318. package/test/every-claimant-is-told-its-share.test.js +152 -152
  319. package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
  320. package/test/failed-start-breaker.test.js +202 -202
  321. package/test/fastest-wires.test.js +143 -143
  322. package/test/ffmpeg-stream-counts.test.js +78 -78
  323. package/test/file-edges.test.js +191 -191
  324. package/test/fixtures/audio-frame-boundary.json +1 -0
  325. package/test/fmp4-presentation-clock.test.js +147 -147
  326. package/test/fmp4-self-contained.test.js +80 -80
  327. package/test/fmp4-timestamps.test.js +140 -140
  328. package/test/fmp4-track-completeness.test.js +41 -41
  329. package/test/format-keyed-outputs.test.js +78 -78
  330. package/test/health-metrics.test.js +38 -38
  331. package/test/held-request-width.test.js +157 -157
  332. package/test/helpers/encode-run.js +136 -136
  333. package/test/helpers/held-file.js +29 -29
  334. package/test/helpers/manager.js +57 -57
  335. package/test/helpers/output-spec.js +63 -63
  336. package/test/host-load.test.js +68 -68
  337. package/test/index-accuracy-is-about-the-file.test.js +128 -128
  338. package/test/init-compat.test.js +219 -219
  339. package/test/input-lost-quiets-the-plan.test.js +261 -261
  340. package/test/input-unavailable.test.js +48 -48
  341. package/test/installation-id.test.js +39 -39
  342. package/test/keeping-period.test.js +83 -83
  343. package/test/keyframe-index-accuracy.test.js +188 -188
  344. package/test/keyframe-table-budget.test.js +102 -102
  345. package/test/keyframes-belong-to-the-file.test.js +249 -249
  346. package/test/learned-median.test.js +37 -37
  347. package/test/letting-go-asks-who-is-watching.test.js +191 -191
  348. package/test/limit-row-by-area.test.js +60 -60
  349. package/test/link-budget.test.js +112 -111
  350. package/test/link-probe.test.js +56 -56
  351. package/test/local-encoding-data.test.js +273 -273
  352. package/test/logger-repeats.test.js +134 -134
  353. package/test/look-ahead-coverage.test.js +56 -56
  354. package/test/manager-surface.test.js +292 -292
  355. package/test/matroska-cues-track.test.js +192 -192
  356. package/test/memory-budget.test.js +390 -390
  357. package/test/move-cost.test.js +216 -216
  358. package/test/mp4-subtitles.test.js +173 -173
  359. package/test/one-authority.test.js +285 -285
  360. package/test/one-budget.test.js +149 -149
  361. package/test/one-output-one-session.test.js +270 -204
  362. package/test/one-resource-divided.test.js +185 -185
  363. package/test/one-serving-output-per-height.test.js +458 -458
  364. package/test/orchestrator-wired.test.js +206 -206
  365. package/test/output-catalog.test.js +39 -39
  366. package/test/output-shape.test.js +21 -21
  367. package/test/output-spec.test.js +251 -251
  368. package/test/output-speed.test.js +88 -88
  369. package/test/outputs-being-prepared.test.js +40 -40
  370. package/test/packet-witness-ring.test.js +236 -236
  371. package/test/packet-witness.test.js +148 -148
  372. package/test/piece-disk-store.test.js +401 -401
  373. package/test/piece-from-whole-file.test.js +129 -129
  374. package/test/piece-lru.test.js +319 -319
  375. package/test/piece-reader.test.js +162 -162
  376. package/test/piece-selection-offset.test.js +85 -85
  377. package/test/piece-store-eviction.test.js +595 -595
  378. package/test/piece-store-never-refuses.test.js +153 -153
  379. package/test/piece-store-reservations.test.js +403 -403
  380. package/test/piece-store-slow-disk.test.js +159 -159
  381. package/test/piece-tail.test.js +97 -97
  382. package/test/plan-host-timings.test.js +68 -68
  383. package/test/plan-is-steady.test.js +225 -225
  384. package/test/playback-readiness.test.js +453 -453
  385. package/test/playback-start-regressions.test.js +99 -99
  386. package/test/priority-map-download.test.js +174 -174
  387. package/test/priority-map-emptied.test.js +204 -204
  388. package/test/priority-map-per-output.test.js +215 -215
  389. package/test/priority-map.test.js +163 -163
  390. package/test/probe-wedge-certainty.test.js +60 -60
  391. package/test/produced-copy-choice.test.js +213 -213
  392. package/test/published-timeline.test.js +83 -83
  393. package/test/quality-offer.test.js +162 -162
  394. package/test/quality-variants.test.js +1257 -1229
  395. package/test/rate-control-in-the-output.test.js +180 -180
  396. package/test/read-survives-withdrawal.test.js +164 -164
  397. package/test/read-window.test.js +365 -365
  398. package/test/resume-warm.test.js +39 -39
  399. package/test/run-graph-drift.test.js +37 -37
  400. package/test/run-inputs.test.js +80 -80
  401. package/test/run-intervals.test.js +100 -100
  402. package/test/run-position-follows-published-grid.test.js +89 -89
  403. package/test/run-progress.test.js +65 -65
  404. package/test/same-height-switch.test.js +664 -664
  405. package/test/seek-frees-the-viewers-requests.test.js +217 -217
  406. package/test/seek-landing.test.js +109 -109
  407. package/test/seek-target-not-superseded.test.js +195 -195
  408. package/test/segment-closed-on-both-branches.test.js +96 -96
  409. package/test/segment-cut-times.test.js +63 -63
  410. package/test/segment-demand.test.js +80 -80
  411. package/test/segment-hold-supersede.test.js +91 -91
  412. package/test/segment-judged-by-its-own-header.test.js +221 -221
  413. package/test/segment-serve-wiring.test.js +475 -475
  414. package/test/segment-store-eviction.test.js +247 -247
  415. package/test/segment-store.test.js +323 -323
  416. package/test/segment-true-start.test.js +86 -86
  417. package/test/segments-are-shared.test.js +176 -176
  418. package/test/send-chunk-bytes.test.js +86 -86
  419. package/test/serving-existing-output.test.js +105 -102
  420. package/test/serving-output.test.js +102 -102
  421. package/test/shared-piece-store.test.js +310 -310
  422. package/test/sidecar-naming.test.js +142 -142
  423. package/test/soundtrack-rate-bound.test.js +289 -0
  424. package/test/source-file.test.js +133 -133
  425. package/test/source-files-route.test.js +113 -113
  426. package/test/source-registry.test.js +18 -18
  427. package/test/split-a-stretch.test.js +139 -139
  428. package/test/stale-request-after-seek.test.js +158 -158
  429. package/test/startup-readings.test.js +124 -124
  430. package/test/stream-route.test.js +209 -209
  431. package/test/subtitle-cue-framing.test.js +200 -200
  432. package/test/subtitle-cue-source.test.js +102 -102
  433. package/test/subtitle-cue-walk.test.js +370 -370
  434. package/test/subtitle-cursor.test.js +63 -63
  435. package/test/subtitle-defaults.test.js +118 -118
  436. package/test/subtitle-language.test.js +252 -252
  437. package/test/subtitle-track-numbering.test.js +386 -386
  438. package/test/supply-margin.test.js +177 -177
  439. package/test/swarm-follows-readers.test.js +174 -174
  440. package/test/swarm-reach.test.js +130 -130
  441. package/test/swarm-selection.test.js +220 -220
  442. package/test/tail-duplication.test.js +167 -167
  443. package/test/the-channel-has-both-ends.test.js +91 -91
  444. package/test/the-torrent-thread-serves-bytes.test.js +107 -107
  445. package/test/thread-pool.test.js +53 -53
  446. package/test/timeline.test.js +84 -84
  447. package/test/torrent-contents.test.js +235 -235
  448. package/test/torrent-cost.test.js +71 -71
  449. package/test/torrent-files.test.js +221 -221
  450. package/test/torrent-naming.test.js +255 -255
  451. package/test/tracks-begin-together.test.js +209 -209
  452. package/test/tunnel-renewal.test.js +167 -167
  453. package/test/two-viewers-one-picture.test.js +504 -407
  454. package/test/upload-hurry.test.js +186 -186
  455. package/test/usrsctp-state.test.js +131 -131
  456. package/test/video-facts.test.js +102 -102
  457. package/test/viewer-assignments.test.js +143 -143
  458. package/test/viewer-outputs.test.js +285 -285
  459. package/test/viewer-presence.test.js +116 -116
  460. package/test/viewer-requests-admission.test.js +89 -89
  461. package/test/viewer-states.test.js +189 -189
  462. package/test/viewer-subtitle-subscription.test.js +96 -96
  463. package/test/viewer.test.js +139 -139
  464. package/test/viewing-generations.test.js +390 -390
  465. package/test/visible-rung.test.js +37 -37
  466. package/test/wait-ledger.test.js +100 -100
  467. package/test/webrtc-shim.test.js +149 -149
  468. package/test/wedge-certainty.test.js +131 -131
  469. package/test/withdraw-piece-claim.test.js +212 -212
  470. package/test/worker-channel.test.js +158 -158
  471. package/utils/client-log-file.js +151 -151
  472. package/utils/logger.js +312 -312
  473. package/utils/parse-range.js +25 -25
  474. package/utils/perf.js +59 -59
  475. package/utils/torrent-source-key.js +38 -38
@@ -1,254 +1,349 @@
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
- }
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 stream is held to: a re-encode's `maxrate`, or the
14
+ * most a copied soundtrack's codec allows it to carry;
15
+ * 2. `estimated` — an average: a copied picture's or soundtrack's rate as the
16
+ * FILE states it, or the target a re-encoded soundtrack is asked for. An
17
+ * average is not a bound, so a load built on it is admitted only by
18
+ * estimate and is never confirmed;
19
+ * 3. `unknown` — no figure at all: a hardware encoder given no limit, a copied
20
+ * soundtrack whose rate nothing states.
21
+ *
22
+ * A load is as trustworthy as its least trustworthy part.
23
+ */
24
+
25
+ import { AUDIO_TRANSCODE_KBPS, maxrateKbpsFor, nominalKbpsFor } from "../args.js";
26
+
27
+ /**
28
+ * How much of a measured link may be spent on the stream.
29
+ *
30
+ * The rest is what the link does when it is not being perfect: transport
31
+ * framing, retransmission, the other tabs, the moment somebody else in the
32
+ * house starts something. A load sized to the whole reading stalls on the
33
+ * first of those.
34
+ *
35
+ * Chosen, not measured, and written as a constant rather than dressed up as a
36
+ * measurement. What would replace it is a reading of the transport's own
37
+ * overhead and of how far a link's throughput varies over a session, which
38
+ * nothing takes.
39
+ */
40
+ export const LINK_SAFETY = 0.8;
41
+
42
+ /** How far a figure in a load can be trusted. */
43
+ export const PEAK_CLASS = Object.freeze({
44
+ KNOWN: "known",
45
+ ESTIMATED: "estimated",
46
+ UNKNOWN: "unknown"
47
+ });
48
+
49
+ /** What a link was found to do with a load. */
50
+ export const LINK_VERDICT = Object.freeze({
51
+ FITS: "fits",
52
+ ESTIMATED_TO_FIT: "estimated to fit",
53
+ DOES_NOT_FIT: "does not fit",
54
+ NO_SAFE_BOUND: "no safe bound",
55
+ NO_MEASUREMENT: "no measurement"
56
+ });
57
+
58
+ const CLASS_ORDER = [PEAK_CLASS.KNOWN, PEAK_CLASS.ESTIMATED, PEAK_CLASS.UNKNOWN];
59
+
60
+ /**
61
+ * @typedef {{ mbps: number | null, peakClass: string }} LoadPart
62
+ * @typedef {{ video: LoadPart | null, audio: LoadPart | null, totalMbps: number | null, peakClass: string }} Load
63
+ */
64
+
65
+ /**
66
+ * @param {number | null} mbps
67
+ * @param {string} peakClass
68
+ * @returns {LoadPart}
69
+ */
70
+ function part(mbps, peakClass) {
71
+ const figure = Number.isFinite(mbps) && mbps > 0 ? mbps : null;
72
+ // A figure that is missing is no figure, whatever the part claimed to be.
73
+ return { mbps: figure, peakClass: figure === null ? PEAK_CLASS.UNKNOWN : peakClass };
74
+ }
75
+
76
+ /**
77
+ * The picture part of a load, read from an output's identity.
78
+ *
79
+ * An encoder given no limit states no bound, and its part is `unknown` — unless
80
+ * this host has seen outputs of exactly this mode carry a peak (roadmap item
81
+ * 97, step 14), and then that peak is its figure, as an ESTIMATE: it is what
82
+ * was seen, not a bound anything holds the encoder to.
83
+ *
84
+ * @param {import("../output/OutputSpec.js").OutputSpec} spec
85
+ * @param {number | null} sourceMbps - The average rate the source file states.
86
+ * @param {number | null} [observedPeakMbps] - The largest peak outputs of this
87
+ * mode were seen carrying on this configuration, or null.
88
+ * @returns {LoadPart | null} Null for an output that carries no picture.
89
+ */
90
+ export function videoLoadOfSpec(spec, sourceMbps, observedPeakMbps = null) {
91
+ if (!spec?.video) {
92
+ return null;
93
+ }
94
+ const encode = spec.video.encode;
95
+ if (!encode) {
96
+ return part(sourceMbps, PEAK_CLASS.ESTIMATED);
97
+ }
98
+ const rateControl = encode.rateControl;
99
+ if (rateControl) {
100
+ return part(rateControl.maxrateKbps / 1000, PEAK_CLASS.KNOWN);
101
+ }
102
+ return Number.isFinite(observedPeakMbps) && observedPeakMbps > 0
103
+ ? part(observedPeakMbps, PEAK_CLASS.ESTIMATED)
104
+ : part(null, PEAK_CLASS.UNKNOWN);
105
+ }
106
+
107
+ /**
108
+ * The picture part of a load for a height nothing has produced yet, the way
109
+ * it WOULD be produced here: at the nominal limit on the software encoder,
110
+ * with no limit on a hardware one, and as the source's own stream when the
111
+ * picture is copied at the source's height.
112
+ *
113
+ * The nominal limit is the one of the FRAME the height would be encoded at,
114
+ * chosen by its area (`nominalKbpsFor`) — the same row the output is then
115
+ * opened in, so the offer and the output cannot price one step differently.
116
+ *
117
+ * @param {{ sourceHeight: number, copiesAtSource: boolean, sourceMbps: number | null, encoderKind: string }} picture
118
+ * @param {{ width: number, height: number }} frame - The frame this height is
119
+ * encoded at for this source.
120
+ * @returns {LoadPart}
121
+ */
122
+ export function videoLoadForFrame(picture, frame) {
123
+ const sourceHeight = Math.round(Number(picture?.sourceHeight) || 0);
124
+ if (picture?.copiesAtSource === true && frame.height === sourceHeight) {
125
+ return part(picture?.sourceMbps ?? null, PEAK_CLASS.ESTIMATED);
126
+ }
127
+ if (picture?.encoderKind !== "software") {
128
+ return part(null, PEAK_CLASS.UNKNOWN);
129
+ }
130
+ return part(maxrateKbpsFor(nominalKbpsFor(frame)) / 1000, PEAK_CLASS.KNOWN);
131
+ }
132
+
133
+ /**
134
+ * The picture part of a load for a software encode at a nominal limit nothing
135
+ * has produced yet. The bound is the one `softwareRateControlFor` would give
136
+ * it, so an output opened at that limit carries exactly this load.
137
+ *
138
+ * @param {number} nominalKbps
139
+ * @returns {LoadPart}
140
+ */
141
+ export function videoLoadOfLimit(nominalKbps) {
142
+ return part(maxrateKbpsFor(nominalKbps) / 1000, PEAK_CLASS.KNOWN);
143
+ }
144
+
145
+ /**
146
+ * The soundtrack part of a load: the track THIS viewer receives, in the mode
147
+ * it is actually produced in.
148
+ *
149
+ * Copied, the track weighs what its codec allows it to — the bound its
150
+ * configuration is held to, `known` — and where no such bound is confirmed, the
151
+ * average the file states for it, `estimated`. Re-encoded, it weighs the rate
152
+ * the encoder is asked for, and that is `estimated` too: ffmpeg's AAC encoder
153
+ * treats `-b:a` as a target its rate control drifts towards, not a limit
154
+ * (`libavcodec/aacenc.c`, 8.1.2), and the 6144-bit frame limit it does hold
155
+ * cannot be turned into a rate without the output's sampling frequency, which
156
+ * the command does not fix. `research/soundtrack-rate-bound-2026-10-01.md`.
157
+ *
158
+ * @param {import("../../media/audio-inventory.js").AudioInventoryEntry | null | undefined} entry -
159
+ * The inventory entry of the track; null when nothing describes it.
160
+ * @param {boolean | null} transcode - How it reaches the viewer; null when the
161
+ * viewer receives no sound from this output or beside it.
162
+ * @returns {LoadPart | null}
163
+ */
164
+ export function soundtrackLoadOf(entry, transcode) {
165
+ if (transcode === null || transcode === undefined) {
166
+ return null;
167
+ }
168
+ if (transcode === true) {
169
+ return part(AUDIO_TRANSCODE_KBPS / 1000, PEAK_CLASS.ESTIMATED);
170
+ }
171
+ const peakKbps = Number(entry?.peakKbps);
172
+ if (Number.isFinite(peakKbps) && peakKbps > 0) {
173
+ return part(peakKbps / 1000, PEAK_CLASS.KNOWN);
174
+ }
175
+ return part((Number(entry?.bitrateKbps) || 0) / 1000, PEAK_CLASS.ESTIMATED);
176
+ }
177
+
178
+ /**
179
+ * Whether a copied track has a figure for the link to be asked about: a
180
+ * confirmed bound, or a rate the file states.
181
+ *
182
+ * @param {import("../../media/audio-inventory.js").AudioInventoryEntry | null | undefined} entry
183
+ * @returns {boolean}
184
+ */
185
+ export function soundtrackHasFigure(entry) {
186
+ const peakKbps = Number(entry?.peakKbps);
187
+ const bitrateKbps = Number(entry?.bitrateKbps);
188
+ return (Number.isFinite(peakKbps) && peakKbps > 0) || (Number.isFinite(bitrateKbps) && bitrateKbps > 0);
189
+ }
190
+
191
+ /**
192
+ * How a soundtrack is produced for a viewer, decided once, when they choose it.
193
+ *
194
+ * 1. the browser plays the track as it is and the track has a figure — copied;
195
+ * 2. the browser cannot play it, or it has no figure at all — re-encoded to AAC,
196
+ * where re-encoding is allowed. Without a figure the link cannot be asked
197
+ * whether a copy fits it, and the re-encode is the soundtrack that does have
198
+ * one;
199
+ * 3. re-encoding not allowed: a track the browser plays is copied, figure or
200
+ * not, and the link's own answer about it stands; a track it cannot play
201
+ * cannot be served at all.
202
+ *
203
+ * @param {object} params
204
+ * @param {import("../../media/audio-inventory.js").AudioInventoryEntry | null | undefined} params.entry
205
+ * @param {boolean} params.browserPlays - Whether the viewer's browser can play
206
+ * this track's codec as it stands.
207
+ * @param {boolean} params.transcodeAllowed
208
+ * @returns {{ transcode: boolean | null, cause: string }} `transcode: null`
209
+ * when the track cannot be served. `cause` is one of {@link SOUNDTRACK_MODE_CAUSE}.
210
+ */
211
+ export function chooseSoundtrackMode({ entry, browserPlays, transcodeAllowed }) {
212
+ const hasFigure = soundtrackHasFigure(entry);
213
+ if (browserPlays && hasFigure) {
214
+ return { transcode: false, cause: SOUNDTRACK_MODE_CAUSE.COPY };
215
+ }
216
+ if (transcodeAllowed) {
217
+ return { transcode: true, cause: browserPlays ? SOUNDTRACK_MODE_CAUSE.NO_FIGURE : SOUNDTRACK_MODE_CAUSE.UNPLAYABLE };
218
+ }
219
+ return browserPlays
220
+ ? { transcode: false, cause: SOUNDTRACK_MODE_CAUSE.COPY_WITHOUT_FIGURE }
221
+ : { transcode: null, cause: SOUNDTRACK_MODE_CAUSE.CANNOT_SERVE };
222
+ }
223
+
224
+ /** Why a soundtrack is produced the way {@link chooseSoundtrackMode} decided. */
225
+ export const SOUNDTRACK_MODE_CAUSE = Object.freeze({
226
+ COPY: "copied: the browser plays it and its rate is stated",
227
+ NO_FIGURE: "re-encoded: nothing states how much a copy of it would carry",
228
+ UNPLAYABLE: "re-encoded: the browser cannot play it as it is",
229
+ COPY_WITHOUT_FIGURE: "copied without a stated rate: re-encoding is not allowed here",
230
+ CANNOT_SERVE: "the browser cannot play it and re-encoding is not allowed here"
231
+ });
232
+
233
+ /**
234
+ * The whole load: the parts added, and the class of the least trustworthy.
235
+ *
236
+ * @param {LoadPart | null} video
237
+ * @param {LoadPart | null} audio
238
+ * @returns {Load}
239
+ */
240
+ export function loadOf(video, audio) {
241
+ const parts = [video, audio].filter(Boolean);
242
+ const peakClass = parts.reduce(
243
+ (worst, one) => (CLASS_ORDER.indexOf(one.peakClass) > CLASS_ORDER.indexOf(worst) ? one.peakClass : worst),
244
+ PEAK_CLASS.KNOWN
245
+ );
246
+ const totalMbps = peakClass === PEAK_CLASS.UNKNOWN
247
+ ? null
248
+ : parts.reduce((sum, one) => sum + one.mbps, 0);
249
+ return { video, audio, totalMbps, peakClass };
250
+ }
251
+
252
+ /**
253
+ * What a link that measured `linkMbps` does with a load.
254
+ *
255
+ * 1. nothing measured the link — every class is admitted, since the link gave
256
+ * no ground to refuse, and nothing is confirmed;
257
+ * 2. an `unknown` load against a measured link — `no safe bound`, refused:
258
+ * there is no number to compare;
259
+ * 3. otherwise the load is compared with the usable share of the link: a
260
+ * `known` load that passes FITS and is confirmed, an `estimated` one that
261
+ * passes is admitted by estimate and is NOT confirmed.
262
+ *
263
+ * @param {number | null} linkMbps
264
+ * @param {Load} load
265
+ * @returns {{ verdict: string, admitted: boolean, confirmed: boolean, linkMbps: number | null, load: Load }}
266
+ */
267
+ export function linkCouldCarry(linkMbps, load) {
268
+ const measured = Number.isFinite(linkMbps) && linkMbps > 0;
269
+ const answer = (verdict, admitted, confirmed) => ({
270
+ verdict,
271
+ admitted,
272
+ confirmed,
273
+ linkMbps: measured ? linkMbps : null,
274
+ load
275
+ });
276
+ if (!measured) {
277
+ return answer(LINK_VERDICT.NO_MEASUREMENT, true, false);
278
+ }
279
+ if (load.peakClass === PEAK_CLASS.UNKNOWN) {
280
+ return answer(LINK_VERDICT.NO_SAFE_BOUND, false, false);
281
+ }
282
+ if (linkMbps * LINK_SAFETY < load.totalMbps) {
283
+ return answer(LINK_VERDICT.DOES_NOT_FIT, false, false);
284
+ }
285
+ return load.peakClass === PEAK_CLASS.KNOWN
286
+ ? answer(LINK_VERDICT.FITS, true, true)
287
+ : answer(LINK_VERDICT.ESTIMATED_TO_FIT, true, false);
288
+ }
289
+
290
+ /**
291
+ * Why a link refused a load, in words naming what was missing or too much.
292
+ *
293
+ * `no safe bound` names every part that has no figure — the picture, the
294
+ * soundtrack or both — because that, and not the link, is what the refusal is
295
+ * about. Any other refusal is about the size of the load and is described by
296
+ * the caller, who knows which outputs were weighed.
297
+ *
298
+ * @param {{ verdict: string, load: Load }} answer
299
+ * @param {string} otherwise - The words for a load that is too large.
300
+ * @returns {string}
301
+ */
302
+ export function linkRefusalReason(answer, otherwise) {
303
+ if (answer?.verdict !== LINK_VERDICT.NO_SAFE_BOUND) {
304
+ return otherwise;
305
+ }
306
+ const missing = [
307
+ answer.load?.video?.peakClass === PEAK_CLASS.UNKNOWN ? "the picture" : "",
308
+ answer.load?.audio?.peakClass === PEAK_CLASS.UNKNOWN ? "the soundtrack" : ""
309
+ ].filter(Boolean);
310
+ return `nothing states how much ${missing.length > 0 ? missing.join(" or ") : "this output"} would send, ` +
311
+ "so this viewer's link cannot be asked whether it fits";
312
+ }
313
+
314
+ /**
315
+ * How strongly an answer is admitted, for ordering: a load whose figure is a
316
+ * bound before one whose figure is an average, and both before one with no
317
+ * figure (which is only ever admitted with nothing measured). Zero for a
318
+ * refusal.
319
+ *
320
+ * @param {{ admitted: boolean, load: Load }} answer
321
+ * @returns {number}
322
+ */
323
+ export function admissionRank(answer) {
324
+ if (!answer.admitted) {
325
+ return 0;
326
+ }
327
+ return 3 - CLASS_ORDER.indexOf(answer.load.peakClass);
328
+ }
329
+
330
+ /**
331
+ * The figures behind an answer, as plain values a log line or a report can
332
+ * carry.
333
+ *
334
+ * @param {{ verdict: string, linkMbps: number | null, load: Load }} answer
335
+ * @returns {object}
336
+ */
337
+ export function linkAnswerFigures(answer) {
338
+ return {
339
+ verdict: answer.verdict,
340
+ linkMbps: answer.linkMbps,
341
+ linkSafety: LINK_SAFETY,
342
+ videoMbps: answer.load.video?.mbps ?? null,
343
+ videoClass: answer.load.video?.peakClass ?? null,
344
+ audioMbps: answer.load.audio?.mbps ?? null,
345
+ audioClass: answer.load.audio?.peakClass ?? null,
346
+ totalMbps: answer.load.totalMbps,
347
+ peakClass: answer.load.peakClass
348
+ };
349
+ }