@torrent-tv/proxy 2.86.0 → 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 (382) hide show
  1. package/CHANGELOG.md +2138 -1915
  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 -483
  157. package/services/viewer/Viewers.js +368 -257
  158. package/services/viewer/active-output.js +37 -54
  159. package/services/viewer/audio-start.js +4 -3
  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 -45
  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 -515
  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 -202
  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 -9608
  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/services/viewer/link-readings.js +0 -56
  347. package/test/produced-index.test.js +0 -142
  348. /package/{services → bin}/thread-pool.js +0 -0
  349. /package/services/{available-share.js → encode/available-share.js} +0 -0
  350. /package/services/{decode-cost-fit.js → encode/decode-cost-fit.js} +0 -0
  351. /package/services/{encoder-readings.js → encode/encoder-readings.js} +0 -0
  352. /package/services/{host-load.js → encode/host-load.js} +0 -0
  353. /package/services/{output → encode/output}/cut-grid.js +0 -0
  354. /package/services/{output → encode/output}/rates.js +0 -0
  355. /package/services/{segment-formats → encode/segment-formats}/mpegts.js +0 -0
  356. /package/services/{torrent-cost.js → encode/torrent-cost.js} +0 -0
  357. /package/services/{container → media/container}/ebml-reader.js +0 -0
  358. /package/services/{ffmpeg-banner.js → media/ffmpeg-banner.js} +0 -0
  359. /package/services/{tracks → media/tracks}/ContainerTrack.js +0 -0
  360. /package/services/{tracks → media/tracks}/ImageSubtitleTrack.js +0 -0
  361. /package/services/{tracks → media/tracks}/SubtitleTrack.js +0 -0
  362. /package/services/{tracks → media/tracks}/TextSubtitleTrack.js +0 -0
  363. /package/services/{tracks → media/tracks}/VideoTrack.js +0 -0
  364. /package/services/{tracks → media/tracks}/index.js +0 -0
  365. /package/services/{files → storage/files}/piece-from-whole-file.js +0 -0
  366. /package/services/{disk → storage}/keep.js +0 -0
  367. /package/services/{piece-store → storage/piece-store}/piece-lru.js +0 -0
  368. /package/services/{disk → storage}/returns.js +0 -0
  369. /package/services/{demand → torrent/demand}/DemandRegister.js +0 -0
  370. /package/services/{demand → torrent/demand}/Urgency.js +0 -0
  371. /package/services/{demand → torrent/demand}/Window.js +0 -0
  372. /package/services/{demand → torrent/demand}/index.js +0 -0
  373. /package/services/{demand → torrent/demand}/pieces.js +0 -0
  374. /package/services/{download → torrent/download}/withdraw-claim.js +0 -0
  375. /package/services/{supply-margin.js → torrent/supply-margin.js} +0 -0
  376. /package/services/{torrent-source-key.js → torrent/torrent-source-key.js} +0 -0
  377. /package/services/{torrent-worker → torrent/worker}/fastest-wires.js +0 -0
  378. /package/services/{torrent-worker → torrent/worker}/handle-state.js +0 -0
  379. /package/services/{nat-classifier.js → transport/nat-classifier.js} +0 -0
  380. /package/services/{registry-api.js → transport/registry-api.js} +0 -0
  381. /package/services/{tunnel-client.js → transport/tunnel-client.js} +0 -0
  382. /package/services/{priority → viewer}/WaitLedger.js +0 -0
@@ -0,0 +1,180 @@
1
+ /**
2
+ * @file The bitrate limit and the level are decided with the rest of an
3
+ * output's format, before it is named — roadmap item 97, step 10.
4
+ *
5
+ * Nothing here starts ffmpeg, a torrent or a worker: the decision is a pure
6
+ * function of values.
7
+ */
8
+
9
+ import test from "node:test";
10
+ import assert from "node:assert/strict";
11
+ import { decideOutputFormat } from "../services/encode/quality/output-format.js";
12
+ import { nominalKbpsFor, softwareRateControlFor } from "../services/encode/args.js";
13
+ import { AudioOutput, CutGrid, OutputSpec, VideoOutput } from "../services/encode/output/OutputSpec.js";
14
+
15
+ /**
16
+ * A 720p output asked for exactly, from a 1080p source, on the encoder given.
17
+ *
18
+ * @param {{ kind: string, name: string }} encoder
19
+ * @param {number | null} [capKbps]
20
+ * @returns {ReturnType<typeof decideOutputFormat>}
21
+ */
22
+ function decide(encoder, capKbps = null) {
23
+ return decideOutputFormat({
24
+ encodesPicture: true,
25
+ exact: true,
26
+ target: { width: 1280, height: 720 },
27
+ source: { width: 1920, height: 1080, megabitsPerSecond: 8, decode: null },
28
+ fps: 24,
29
+ encoder,
30
+ benchmark: null,
31
+ cost: { decodeModel: null, observedDecodeCostSec: null, requiredSpeed: null },
32
+ chooseBudget: () => null,
33
+ tonemap: false,
34
+ capKbps,
35
+ specWith: (encode) => new OutputSpec({
36
+ sourceKey: "torrent:abc",
37
+ segmentFormatId: "fmp4",
38
+ grid: new CutGrid({ kind: "uniform", fileIndex: 0 }),
39
+ video: new VideoOutput({ fileIndex: 0, encode }),
40
+ audio: new AudioOutput({ fileIndex: 0, trackIndex: 0, transcode: true })
41
+ }),
42
+ serving: { mode: "manual", linkMbps: null, keys: [], readyAt: () => false }
43
+ });
44
+ }
45
+
46
+ const SOFTWARE = { kind: "software", name: "libx264" };
47
+ const HARDWARE = { kind: "nvenc", name: "h264_nvenc" };
48
+
49
+ test("a software output is bounded at its size's nominal rate unless a lower limit is asked for", () => {
50
+ const plain = decide(SOFTWARE).spec.video.encode.rateControl;
51
+ const lower = decide(SOFTWARE, 1400).spec.video.encode.rateControl;
52
+
53
+ assert.deepEqual(plain, softwareRateControlFor({ width: 1280, height: 720, fps: 24 }));
54
+ assert.deepEqual(lower, softwareRateControlFor({ width: 1280, height: 720, fps: 24, capKbps: 1400 }));
55
+ assert.equal(lower.level, plain.level, "both are declared at the nominal output's level");
56
+ assert.notEqual(decide(SOFTWARE).spec.toKey(), decide(SOFTWARE, 1400).spec.toKey(), "and are two outputs");
57
+ });
58
+
59
+ test("a limit named in the request is not answered by an output of another limit already here", () => {
60
+ // The nominal 720p output exists and is ready, and it suits this viewer —
61
+ // which is exactly what a move to a lower limit is for leaving.
62
+ const nominal = decide(SOFTWARE).spec.toKey();
63
+ const asked = decideOutputFormat({
64
+ encodesPicture: true,
65
+ exact: true,
66
+ target: { width: 1280, height: 720 },
67
+ source: { width: 1920, height: 1080, megabitsPerSecond: 8, decode: null },
68
+ fps: 24,
69
+ encoder: SOFTWARE,
70
+ benchmark: null,
71
+ cost: { decodeModel: null, observedDecodeCostSec: null, requiredSpeed: null },
72
+ chooseBudget: () => null,
73
+ tonemap: false,
74
+ capKbps: 1400,
75
+ specWith: (encode) => new OutputSpec({
76
+ sourceKey: "torrent:abc",
77
+ segmentFormatId: "fmp4",
78
+ grid: new CutGrid({ kind: "uniform", fileIndex: 0 }),
79
+ video: new VideoOutput({ fileIndex: 0, encode }),
80
+ audio: new AudioOutput({ fileIndex: 0, trackIndex: 0, transcode: true })
81
+ }),
82
+ serving: { mode: "manual", linkMbps: null, keys: [nominal], readyAt: () => true }
83
+ });
84
+ assert.equal(asked.reusedKey, null);
85
+ assert.deepEqual(asked.spec.video.encode.rateControl, softwareRateControlFor({ width: 1280, height: 720, fps: 24, capKbps: 1400 }));
86
+ });
87
+
88
+ test("a hardware output states no limit, because it is given none", () => {
89
+ assert.equal(decide(HARDWARE).spec.video.encode.rateControl, null);
90
+ assert.ok(decide(HARDWARE).spec.toKey().endsWith("/vbv=-:a=0/0/aac"));
91
+ });
92
+
93
+ test("a limit above the size's own is refused rather than lowered", () => {
94
+ assert.throws(() => decide(SOFTWARE, nominalKbpsFor({ width: 1280, height: 720 }) + 1), RangeError);
95
+ assert.throws(() => decide(SOFTWARE, 0), RangeError, "and a limit that is not a positive rate is not a limit");
96
+ });
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // Step 11: the viewer's own link decides whether a format may be given at all.
100
+ // ---------------------------------------------------------------------------
101
+
102
+ /**
103
+ * A 720p output of a 1080p source for a viewer whose link measured `linkMbps`.
104
+ *
105
+ * @param {{ mode?: "auto" | "manual", linkMbps?: number | null, limits?: number[], encoder?: object, audioLoad?: object | null }} how
106
+ */
107
+ function decideFor({ mode = "manual", linkMbps = null, limits = null, encoder = SOFTWARE, audioLoad = null } = {}) {
108
+ return decideOutputFormat({
109
+ encodesPicture: true,
110
+ exact: true,
111
+ target: { width: 1280, height: 720 },
112
+ source: { width: 1920, height: 1080, megabitsPerSecond: 8, decode: null },
113
+ fps: 24,
114
+ encoder,
115
+ benchmark: null,
116
+ cost: { decodeModel: null, observedDecodeCostSec: null, requiredSpeed: null },
117
+ chooseBudget: () => null,
118
+ tonemap: false,
119
+ ...(limits ? { limitsFor: (frame) => (frame.height === 720 ? limits : [nominalKbpsFor(frame)]) } : {}),
120
+ audioLoad,
121
+ specWith: (encode) => new OutputSpec({
122
+ sourceKey: "torrent:abc",
123
+ segmentFormatId: "fmp4",
124
+ grid: new CutGrid({ kind: "uniform", fileIndex: 0 }),
125
+ video: new VideoOutput({ fileIndex: 0, encode }),
126
+ audio: null
127
+ }),
128
+ serving: { mode, linkMbps, keys: [], readyAt: () => false }
129
+ });
130
+ }
131
+
132
+ test("a size picked by hand keeps its height and takes the highest limit the link admits", () => {
133
+ // 720p nominal peaks at 3.64 Mbit/s; a 3 Mbit/s link admits 2.4 of it.
134
+ const decided = decideFor({ mode: "manual", linkMbps: 3, limits: [2800, 1400] });
135
+ assert.equal(decided.unavailable, null);
136
+ assert.equal(decided.spec.video.encode.height, 720, "the height picked by hand is kept");
137
+ assert.deepEqual(
138
+ decided.spec.video.encode.rateControl,
139
+ softwareRateControlFor({ width: 1280, height: 720, fps: 24, capKbps: 1400 })
140
+ );
141
+ assert.equal(decided.answer.verdict, "fits");
142
+ });
143
+
144
+ test("AUTO moves to a smaller rung only when that rung's own load is admitted", () => {
145
+ // Nothing at 720p fits 3 Mbit/s with only the nominal limit, and 480p's
146
+ // nominal peak (1.82 Mbit/s) does.
147
+ const decided = decideFor({ mode: "auto", linkMbps: 3 });
148
+ assert.equal(decided.unavailable, null);
149
+ assert.ok(decided.spec.video.encode.height < 720, "a smaller rung");
150
+ assert.ok(decided.answer.admitted, "whose own load the link admits");
151
+ assert.equal(
152
+ decideFor({ mode: "auto", linkMbps: 0.3 }).unavailable?.reason?.length > 0,
153
+ true,
154
+ "and with nothing admitted at any rung, nothing is handed over"
155
+ );
156
+ });
157
+
158
+ test("nothing admitted is answered as unavailable, with the figures, and names no output", () => {
159
+ const decided = decideFor({ mode: "manual", linkMbps: 3 });
160
+ assert.equal(decided.spec, null, "a format known not to fit is never handed over");
161
+ assert.ok(decided.unavailable.reason.length > 0);
162
+ assert.equal(decided.unavailable.figures.verdict, "does not fit");
163
+ assert.equal(decided.unavailable.figures.linkMbps, 3);
164
+ });
165
+
166
+ test("a hardware output is unavailable against a measured link, and given while nothing measured it", () => {
167
+ assert.equal(decideFor({ encoder: HARDWARE, linkMbps: 50 }).unavailable.figures.verdict, "no safe bound");
168
+ assert.equal(decideFor({ encoder: HARDWARE, linkMbps: null }).answer.verdict, "no measurement");
169
+ });
170
+
171
+ test("the soundtrack the viewer hears counts toward what their link carries", () => {
172
+ // 720p nominal peaks at 3.64 Mbit/s. A 4.6 Mbit/s link admits 3.68 of it:
173
+ // enough for the picture, not for the picture and 128 kbit/s of sound.
174
+ const linkMbps = 4.6;
175
+ assert.equal(decideFor({ mode: "manual", linkMbps }).answer.verdict, "fits");
176
+ assert.ok(
177
+ decideFor({ mode: "manual", linkMbps, audioLoad: { mbps: 0.128, peakClass: "known" } }).unavailable,
178
+ "with the sound it no longer fits"
179
+ );
180
+ });
@@ -15,8 +15,8 @@ import { EventEmitter } from "node:events";
15
15
  import os from "node:os";
16
16
  import path from "node:path";
17
17
  import fs from "node:fs/promises";
18
- import { readFragments } from "../services/torrent-worker/piece-reader.js";
19
- import { SharedPieceStore } from "../services/piece-store/shared-piece-store.js";
18
+ import { readFragments } from "../services/torrent/worker/piece-reader.js";
19
+ import { SharedPieceStore } from "../services/storage/piece-store/shared-piece-store.js";
20
20
 
21
21
  const PIECE = 1024;
22
22
  const TOTAL = 4 * PIECE;
@@ -31,8 +31,8 @@ import {
31
31
  nextWindowPieces,
32
32
  readFragments,
33
33
  readWindowFor
34
- } from "../services/torrent-worker/piece-reader.js";
35
- import { SharedPieceStore } from "../services/piece-store/shared-piece-store.js";
34
+ } from "../services/torrent/worker/piece-reader.js";
35
+ import { SharedPieceStore } from "../services/storage/piece-store/shared-piece-store.js";
36
36
 
37
37
  const PIECE = 1024;
38
38
  // The production window is 32 MB against 8 MiB pieces — four of them. Sized
@@ -47,9 +47,16 @@ const WINDOW_PIECES = 4;
47
47
  async function recordingTorrent({ pieceCount, present = () => true }) {
48
48
  const directory = await fs.mkdtemp(path.join(os.tmpdir(), "read-window-test-"));
49
49
  const totalLength = pieceCount * PIECE;
50
+ // ROOM FOR THE WHOLE FIXTURE, because none of these tests is about eviction.
51
+ // At 64 pieces of memory an 8000-piece fixture spilled 7936 of them to disk —
52
+ // a file each — and read most of them back, so four checks about which piece
53
+ // RANGES a reader claims cost 25-28 seconds apiece and this one file took
54
+ // 132 s of a 140 s suite. The store's own behaviour under pressure is
55
+ // measured where that is the subject (`piece-store-eviction`,
56
+ // `piece-lru`, `piece-disk-store`).
50
57
  const store = new SharedPieceStore(PIECE, {
51
58
  length: totalLength,
52
- memoryBytes: 64 * PIECE,
59
+ memoryBytes: totalLength,
53
60
  path: directory,
54
61
  name: "test"
55
62
  });
@@ -12,7 +12,7 @@
12
12
  import assert from "node:assert/strict";
13
13
  import test from "node:test";
14
14
 
15
- import { resumeByteOffset } from "../services/torrent-worker/container-tracks.js";
15
+ import { resumeByteOffset } from "../services/torrent/worker/resume-warm.js";
16
16
 
17
17
  test("a position halfway through a film is halfway through its file", () => {
18
18
  assert.equal(resumeByteOffset(1000, 100, 50), 500);
@@ -0,0 +1,80 @@
1
+ /**
2
+ * @file What an encoder is told to read.
3
+ *
4
+ * The encoder's own read of its source is the one read that tracks where the
5
+ * viewer is (`reader=playback`), and the only one sized in seconds of playback
6
+ * (`windowBytes`). Both travel on the input address. When the address stopped
7
+ * being kept on the session and started being built per run, both were left
8
+ * off it: every encoder read looked to the stream route like a probe, and the
9
+ * read window fell back to the reader's default.
10
+ */
11
+
12
+ import test from "node:test";
13
+ import assert from "node:assert/strict";
14
+ import { encoderInputs } from "../services/encode/run-inputs.js";
15
+
16
+ const BASE = "http://127.0.0.1:9090";
17
+
18
+ function file(fileIndex) {
19
+ return {
20
+ fileIndex,
21
+ streamUrl(baseUrl, { sessionId = "" } = {}) {
22
+ const url = new URL("/stream", `${baseUrl}/`);
23
+ url.searchParams.set("fileIndex", String(fileIndex));
24
+ if (sessionId) {
25
+ url.searchParams.set("session", sessionId);
26
+ }
27
+ return url;
28
+ }
29
+ };
30
+ }
31
+
32
+ test("the picture's read says it is the playback read, with its window", () => {
33
+ const picture = file(0);
34
+ const { inputUrl } = encoderInputs({
35
+ picture,
36
+ soundtrack: picture,
37
+ carries: "video-only",
38
+ audioSeparate: true,
39
+ sessionId: "aaaaaaaabbbbcccc",
40
+ readWindowBytes: 32_000_000,
41
+ baseUrl: BASE
42
+ });
43
+ const url = new URL(inputUrl);
44
+ assert.equal(url.searchParams.get("reader"), "playback");
45
+ assert.equal(url.searchParams.get("windowBytes"), "32000000");
46
+ assert.equal(url.searchParams.get("session"), "aaaaaaaabbbbcccc");
47
+ });
48
+
49
+ test("with no window measured the read states neither parameter", () => {
50
+ const picture = file(0);
51
+ const { inputUrl } = encoderInputs({
52
+ picture,
53
+ soundtrack: picture,
54
+ carries: "muxed",
55
+ audioSeparate: false,
56
+ sessionId: "aaaaaaaabbbbcccc",
57
+ readWindowBytes: 0,
58
+ baseUrl: BASE
59
+ });
60
+ const url = new URL(inputUrl);
61
+ assert.equal(url.searchParams.get("windowBytes"), null);
62
+ assert.equal(url.searchParams.get("reader"), null, "exactly as before: both are set together or neither");
63
+ });
64
+
65
+ test("a soundtrack in its own file is read directly by its rendition, and is a second input only when muxed", () => {
66
+ const picture = file(0);
67
+ const dub = file(3);
68
+ const rendition = encoderInputs({
69
+ picture, soundtrack: dub, carries: "audio-only", audioSeparate: true,
70
+ sessionId: "s", readWindowBytes: 1000, baseUrl: BASE
71
+ });
72
+ assert.equal(new URL(rendition.inputUrl).searchParams.get("fileIndex"), "3");
73
+ assert.equal(rendition.audioInputUrl, "");
74
+ const muxed = encoderInputs({
75
+ picture, soundtrack: dub, carries: "muxed", audioSeparate: false,
76
+ sessionId: "s", readWindowBytes: 1000, baseUrl: BASE
77
+ });
78
+ assert.equal(new URL(muxed.inputUrl).searchParams.get("fileIndex"), "0");
79
+ assert.equal(new URL(muxed.audioInputUrl).searchParams.get("fileIndex"), "3");
80
+ });
@@ -19,10 +19,12 @@
19
19
  */
20
20
 
21
21
  import assert from "node:assert/strict";
22
- import { Timeline } from "../services/output/Timeline.js";
22
+ import { Timeline } from "../services/encode/output/Timeline.js";
23
23
  import test from "node:test";
24
24
 
25
- import { HlsSessionManager, describeGridDrift, segmentCutTimesFrom } from "../services/hls-session-manager.js";
25
+ import { wireOutputs } from "../services/server/wire-outputs.js";
26
+ import { segmentCutTimesFrom } from "../services/encode/run-command.js";
27
+ import { describeGridDrift } from "../services/encode/OutputTimes.js";
26
28
 
27
29
  /** What the playlist in the player's hands says. */
28
30
  const PUBLISHED = [0, 8.342, 16.684, 25.026, 33.368, 41.71];
@@ -30,10 +32,10 @@ const PUBLISHED = [0, 8.342, 16.684, 25.026, 33.368, 41.71];
30
32
  const CORRECTED = [0, 8.342, 14.682, 25.026, 31.366, 41.71];
31
33
 
32
34
  /**
33
- * @returns {{ manager: HlsSessionManager, session: object }}
35
+ * @returns {{ manager: object, session: object }}
34
36
  */
35
37
  function sessionWithDriftedGrid() {
36
- const manager = new HlsSessionManager({
38
+ const manager = wireOutputs({
37
39
  enabled: true,
38
40
  ffmpegBin: "ffmpeg",
39
41
  localBindHost: "127.0.0.1",
@@ -48,14 +50,14 @@ function sessionWithDriftedGrid() {
48
50
 
49
51
  test("a run starts at the time the player was told, not at the corrected one", () => {
50
52
  const { manager, session } = sessionWithDriftedGrid();
51
- assert.equal(manager.runStartTimeFor(session, 2), 16.684);
52
- assert.notEqual(manager.runStartTimeFor(session, 2), session.timeline.boundaries[2]);
53
+ assert.equal(manager.outputTimes.runStartTimeFor(session, 2), 16.684);
54
+ assert.notEqual(manager.outputTimes.runStartTimeFor(session, 2), session.timeline.boundaries[2]);
53
55
  });
54
56
 
55
57
  test("position and cut list come from the same table", () => {
56
58
  const { manager, session } = sessionWithDriftedGrid();
57
- const grid = manager.publishedGridFor(session);
58
- const start = manager.runStartTimeFor(session, 2);
59
+ const grid = manager.outputTimes.publishedGridFor(session);
60
+ const start = manager.outputTimes.runStartTimeFor(session, 2);
59
61
  // The cut list is stated as offsets from where the run begins. Adding the
60
62
  // position back must land on the published boundaries exactly — which is the
61
63
  // property that was false while the two came from different tables.
@@ -66,13 +68,13 @@ test("position and cut list come from the same table", () => {
66
68
  test("a session that published no grid positions on the live one", () => {
67
69
  const { manager } = sessionWithDriftedGrid();
68
70
  const session = { id: "no-playlist", timeline: new Timeline({ boundaries: [...CORRECTED], published: [], cutGrid: "uniform" }) };
69
- assert.equal(manager.runStartTimeFor(session, 2), CORRECTED[2]);
71
+ assert.equal(manager.outputTimes.runStartTimeFor(session, 2), CORRECTED[2]);
70
72
  });
71
73
 
72
74
  test("an index beyond the table is clamped rather than returning nothing", () => {
73
75
  const { manager, session } = sessionWithDriftedGrid();
74
- assert.equal(manager.runStartTimeFor(session, 9999), PUBLISHED[PUBLISHED.length - 1]);
75
- assert.equal(manager.runStartTimeFor(session, -3), PUBLISHED[0]);
76
+ assert.equal(manager.outputTimes.runStartTimeFor(session, 9999), PUBLISHED[PUBLISHED.length - 1]);
77
+ assert.equal(manager.outputTimes.runStartTimeFor(session, -3), PUBLISHED[0]);
76
78
  });
77
79
 
78
80
  test("the drift between the two tables is stated in full", () => {
@@ -0,0 +1,65 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { RunProgress } from "../services/encode/RunProgress.js";
4
+ import { EncodeOrchestrator } from "../services/encode/EncodeOrchestrator.js";
5
+
6
+ function orchestrator() {
7
+ return new EncodeOrchestrator({
8
+ maxRunsFor: () => 2,
9
+ makeRun: () => null,
10
+ segmentSeconds: 4,
11
+ logger: { info() {}, warn() {} }
12
+ });
13
+ }
14
+
15
+ test("a run rebases ffmpeg's relative clock onto its own source position", () => {
16
+ let now = 10;
17
+ const progress = new RunProgress({ startSeconds: 600, totalSeconds: 1000, now: () => now });
18
+ now = 20;
19
+ progress.note({ processedSeconds: 12.5, speed: "2.0x" });
20
+
21
+ assert.deepEqual(progress.snapshot(), {
22
+ processedSeconds: 612.5,
23
+ startPositionSeconds: 600,
24
+ totalSeconds: 1000,
25
+ percent: 3.125,
26
+ remainingSeconds: 387.5,
27
+ speed: "2.0x",
28
+ updatedAt: 20
29
+ });
30
+ });
31
+
32
+ test("two runs of one output retain separate clocks", () => {
33
+ const made = orchestrator();
34
+ const first = { from: 0, to: 49, isAlive: true, progress: new RunProgress({ startSeconds: 0 }) };
35
+ const second = { from: 50, to: 99, isAlive: true, progress: new RunProgress({ startSeconds: 200 }) };
36
+ first.progress.note({ processedSeconds: 40, speed: "1.0x" });
37
+ second.progress.note({ processedSeconds: 8, speed: "2.0x" });
38
+ made.adopt("picture", first);
39
+ made.adopt("picture", second);
40
+
41
+ assert.equal(made.progressOf("picture", 20).processedSeconds, 40);
42
+ assert.equal(made.progressOf("picture", 60).processedSeconds, 208);
43
+ });
44
+
45
+ test("the last run's progress remains available after it ends", () => {
46
+ const made = orchestrator();
47
+ const progress = new RunProgress({ startSeconds: 100 });
48
+ progress.note({ processedSeconds: 5, speed: "1.5x" });
49
+ const run = { from: 25, to: 30, isAlive: false, state: "ENDED_COMPLETE", progress };
50
+ made.adopt("picture", run);
51
+ made.noteEnded({
52
+ address: "picture",
53
+ run,
54
+ ending: "complete",
55
+ because: "the assigned stretch was completed",
56
+ from: 25,
57
+ to: 30,
58
+ reached: 30,
59
+ livedMs: 1000,
60
+ normal: true,
61
+ lastError: ""
62
+ });
63
+
64
+ assert.equal(made.progressOf("picture").processedSeconds, 105);
65
+ });