@torrent-tv/proxy 2.86.0 → 2.87.1

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