@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
@@ -23,15 +23,15 @@
23
23
  * result when it is ready.
24
24
  */
25
25
 
26
- import { CoverageMap } from "../encode/CoverageMap.js";
27
- import { firstUnmetWant, planEncoders } from "../encode/EncodePlan.js";
28
- import { endOfRun } from "../encode/EncodeRun.js";
29
- import { ENCODE_EXIT } from "../encode/encode-exit.js";
30
- import { affordableRuns } from "../encode/run-budget.js";
31
- import { RunCosts } from "../encode/run-costs.js";
32
- import { contentionPenalty } from "../encode/contention.js";
33
- import { waits } from "../priority/WaitLedger.js";
34
- import { SegmentDemand } from "../encode/SegmentDemand.js";
26
+ import { CoverageMap } from "./CoverageMap.js";
27
+ import { firstUnmetWant, planEncoders } from "./EncodePlan.js";
28
+ import { endOfRun } from "./EncodeRun.js";
29
+ import { ENCODE_EXIT } from "./encode-exit.js";
30
+ import { affordableRuns } from "./run-budget.js";
31
+ import { RunCosts } from "./run-costs.js";
32
+ import { contentionPenalty } from "./contention.js";
33
+ import { SegmentDemand } from "./SegmentDemand.js";
34
+ import { runStateOf } from "./encode-run-state.js";
35
35
 
36
36
  /**
37
37
  * How long nothing is placed on an output whose input has just gone, and the
@@ -46,14 +46,32 @@ import { SegmentDemand } from "../encode/SegmentDemand.js";
46
46
  */
47
47
  const INPUT_QUIET_BASE_MS = 2_000;
48
48
  const INPUT_QUIET_MAX_MS = 15_000;
49
+ const START_FAST_FAIL_MS = 2_000;
50
+ const MAX_FAILED_STARTS = 3;
49
51
 
50
52
  export class EncodeOrchestrator {
53
+ /** Derived priority maps. Discardable and rebuilt from viewer state. */
54
+ #demand = new SegmentDemand();
55
+ /** @type {(address: string) => string} */
56
+ #describeWaits = () => "";
57
+
51
58
  /** Output address to what has been made of it. @type {Map<string, CoverageMap>} */
52
59
  #coverage = new Map();
53
60
 
54
61
  /** Output address to the runs on it. @type {Map<string, import("../encode/EncodeRun.js").EncodeRun[]>} */
55
62
  #runs = new Map();
56
63
 
64
+ /** Last state after an output has no registered run. @type {Map<string, string>} */
65
+ #lastState = new Map();
66
+
67
+ /** Last encoder error per output. @type {Map<string, string>} */
68
+ #lastError = new Map();
69
+
70
+ /** Last progress snapshot per output after its run ends. @type {Map<string, object>} */
71
+ #lastProgress = new Map();
72
+
73
+ /** Consecutive fast failures at one position, per output. @type {Map<string, { at: number, count: number }>} */
74
+ #failedStarts = new Map();
57
75
 
58
76
  /** The fastest speed measured on one output, kept across restarts. @type {Map<string, number>} */
59
77
  #lastSpeed = new Map();
@@ -93,12 +111,20 @@ export class EncodeOrchestrator {
93
111
  this.#costs.noteStartup(measured);
94
112
  }
95
113
 
114
+ /** Derived segment demand for one output, copied from the discardable cache. */
115
+ wantedSegmentsOn(address) {
116
+ return [...this.#demand.mapOn(address)];
117
+ }
118
+
96
119
  /** The last reason a budget was cut, so the same one is not said twice. */
97
120
  #lastBudgetReason = new Map();
98
121
 
99
122
  /** The last unmet want said out loud, so a stuck one is said once. */
100
123
  #lastUnmet = new Map();
101
124
 
125
+ /** The last time the machine bound an output's encoders, so it is said once. */
126
+ #lastMachineBound = new Map();
127
+
102
128
  /**
103
129
  * @param {object} params
104
130
  * @param {(address: string) => number} params.maxRunsFor - How many encoders
@@ -117,6 +143,11 @@ export class EncodeOrchestrator {
117
143
  * encoding speed.
118
144
  * @param {{ info: (line: string) => void, warn: (line: string) => void }} params.logger
119
145
  * @param {() => number} [params.now]
146
+ * @param {(address: string) => string} [params.describeWaits] - What the
147
+ * viewers actually waited for on this output, by band, for the state line.
148
+ * HANDED IN: what anybody waited for belongs to the priority layer, and
149
+ * this one may be asked about encoders with no ledger, no clock and no
150
+ * viewer anywhere. Absent, the line simply does not carry it.
120
151
  */
121
152
  constructor({
122
153
  maxRunsFor,
@@ -127,15 +158,22 @@ export class EncodeOrchestrator {
127
158
  startingSpeedFor = () => 0,
128
159
  segmentStore = null,
129
160
  planSoon = null,
161
+ describeWaits = null,
162
+ admission = null,
130
163
  logger,
131
164
  now
132
165
  }) {
166
+ // HOW MANY ENCODERS THE WHOLE MACHINE HOLDS, across outputs. `maxRunsFor`
167
+ // answers for one output as though it were alone; this is what refuses the
168
+ // encoder that would slow everybody below realtime. Absent in a check that
169
+ // is about one output, and then only the per-output limit binds.
170
+ this.admission = admission && typeof admission.placesFor === "function" ? admission : null;
171
+ this.#describeWaits = typeof describeWaits === "function" ? describeWaits : () => "";
133
172
  // The store of produced segments — the layer below this one. It is asked to
134
173
  // clean up after a run that ended other than by reaching the end of its
135
174
  // stretch, which is the one thing an ending must not leave behind: a file
136
175
  // under a name that promises a whole segment.
137
176
  this.segmentStore = segmentStore;
138
- this.demand = new SegmentDemand();
139
177
  this.maxRunsFor = maxRunsFor;
140
178
  // Seconds of swarm time per second of film: what re-encoding material that
141
179
  // already exists costs the download, over and above the encoder's own time.
@@ -249,6 +287,189 @@ export class EncodeOrchestrator {
249
287
  return this.#runs.get(address) ?? [];
250
288
  }
251
289
 
290
+ /**
291
+ * The aggregate state of this output's current runs, or its last ending when
292
+ * none remain.
293
+ *
294
+ * @param {string} address
295
+ * @returns {string}
296
+ */
297
+ stateOf(address) {
298
+ const runs = this.runsOn(address);
299
+ return runs.length > 0 ? runStateOf(runs) : (this.#lastState.get(address) ?? runStateOf([]));
300
+ }
301
+
302
+ /** @param {string} address @returns {string} */
303
+ errorOf(address) {
304
+ return this.#lastError.get(address) ?? "";
305
+ }
306
+
307
+ /**
308
+ * Progress of the live run that covers a position, or the most recently
309
+ * updated run when no position is supplied.
310
+ *
311
+ * @param {string} address
312
+ * @param {number | null} [index]
313
+ * @returns {object | null}
314
+ */
315
+ progressOf(address, index = null) {
316
+ const live = this.runsOn(address).filter((run) => run.isAlive);
317
+ const covering = Number.isInteger(index)
318
+ ? live.filter((run) => run.from <= index && endOfRun(run) >= index)
319
+ : live;
320
+ const candidates = covering.length > 0 ? covering : live;
321
+ const run = [...candidates].sort(
322
+ (left, right) => (right.progress?.updatedAt ?? 0) - (left.progress?.updatedAt ?? 0)
323
+ )[0];
324
+ return run?.progress?.snapshot?.() ?? this.#lastProgress.get(address) ?? null;
325
+ }
326
+
327
+ /**
328
+ * Whether another start at this position is allowed.
329
+ *
330
+ * @param {string} address
331
+ * @param {number} from
332
+ * @returns {boolean}
333
+ */
334
+ mayStartAt(address, from) {
335
+ const failed = this.#failedStarts.get(address);
336
+ return failed?.at !== from || failed.count < MAX_FAILED_STARTS;
337
+ }
338
+
339
+ /**
340
+ * Record whether a failed run died before it performed sustained work.
341
+ *
342
+ * @param {string} address
343
+ * @param {number} from
344
+ * @param {number} livedMs
345
+ * @returns {{ at: number, count: number, blocked: boolean, limit: number, fastMs: number }}
346
+ */
347
+ noteStartFailure(address, from, livedMs) {
348
+ if (!(livedMs < START_FAST_FAIL_MS)) {
349
+ this.#failedStarts.delete(address);
350
+ return { at: -1, count: 0, blocked: false, limit: MAX_FAILED_STARTS, fastMs: START_FAST_FAIL_MS };
351
+ }
352
+ const previous = this.#failedStarts.get(address);
353
+ const next = {
354
+ at: from,
355
+ count: previous?.at === from ? previous.count + 1 : 1
356
+ };
357
+ this.#failedStarts.set(address, next);
358
+ return {
359
+ ...next,
360
+ blocked: next.count >= MAX_FAILED_STARTS,
361
+ limit: MAX_FAILED_STARTS,
362
+ fastMs: START_FAST_FAIL_MS
363
+ };
364
+ }
365
+
366
+ /**
367
+ * An output that no longer exists and has no run left on it.
368
+ *
369
+ * How its last run ended, what went wrong and which position kept failing are
370
+ * facts about THAT output. The name follows from the key, so the next output
371
+ * with the same parameters has the same address; kept, those facts would
372
+ * greet it as its own — a failed ending makes its first wait throw before any
373
+ * encoder is placed, and nothing would ever clear it.
374
+ *
375
+ * @param {string} address
376
+ */
377
+ forgetOutput(address) {
378
+ if (this.runsOn(address).length > 0) {
379
+ return;
380
+ }
381
+ this.#lastState.delete(address);
382
+ this.#lastError.delete(address);
383
+ this.#lastProgress.delete(address);
384
+ this.#failedStarts.delete(address);
385
+ }
386
+
387
+ /** @param {string} address @returns {{ at: number, count: number, blocked: boolean, limit: number }} */
388
+ startFailureOf(address) {
389
+ const failure = this.#failedStarts.get(address) ?? { at: -1, count: 0 };
390
+ return { ...failure, blocked: failure.count >= MAX_FAILED_STARTS, limit: MAX_FAILED_STARTS };
391
+ }
392
+
393
+ /**
394
+ * What the map says about one segment of one output, and about the map it
395
+ * sits in.
396
+ *
397
+ * Asked by whoever is holding a request and measuring the wait: a wait means
398
+ * one thing at the top rank and another at the bottom, and telling them apart
399
+ * needs both the segment's own rank and the highest rank stated, since a rank
400
+ * is a position in this map and not an absolute number.
401
+ *
402
+ * AND AN EMPTY MAP IS NOT AN ANSWER. A top rank of zero says the map has not
403
+ * been built yet — a session created a moment ago, before the first pass —
404
+ * and that is a different thing from "nobody is coming". Conflated, every
405
+ * request behind the run reads as absent for as long as a fresh session has
406
+ * no map.
407
+ *
408
+ * It exists because the one caller reached through this object into the store
409
+ * behind it. A reach like that is how a store becomes the surface: the day it
410
+ * changes, a caller two layers away breaks for a reason it cannot see.
411
+ *
412
+ * @param {string} address
413
+ * @param {number} index
414
+ * @returns {{ rank: number, topRank: number }}
415
+ */
416
+ rankAt(address, index) {
417
+ return this.#demand.rankOf(address, index);
418
+ }
419
+
420
+ /**
421
+ * How many encoder processes are running on this machine right now, over
422
+ * every output.
423
+ *
424
+ * ONE COUNT, and it is this class's because this class is what makes and
425
+ * unmakes runs: adopted the moment one exists and forgotten the moment one
426
+ * ends, both synchronously. The session manager answered the same question
427
+ * twice by walking its own registry — once per SESSION and once per RUN —
428
+ * which agreed only while a session held at most one run. It has held several
429
+ * since the plan was allowed to place them, and the per-session count is the
430
+ * wrong one of the two wherever they differ: what a second encoder costs was
431
+ * measured between ffmpeg PROCESSES, not between outputs.
432
+ *
433
+ * A SUSPENDED run is not counted. It is stopped where it stands and is using
434
+ * nothing; a cost measured beside it is a cost measured alone.
435
+ *
436
+ * @returns {number}
437
+ */
438
+ /**
439
+ * How many encoders are alive on each output, suspended ones included.
440
+ *
441
+ * What the machine-wide admission counts as holding a place. A suspended
442
+ * encoder is counted here, unlike in {@link runningCount}: that one answers
443
+ * what is competing for the processor this instant, this one what the
444
+ * machine is committed to — and a parked encoder resumes the moment the
445
+ * viewer catches up.
446
+ *
447
+ * @returns {Map<string, number>}
448
+ */
449
+ liveRunsByAddress() {
450
+ /** @type {Map<string, number>} */
451
+ const byAddress = new Map();
452
+ for (const [address, runs] of this.#runs) {
453
+ const alive = runs.filter((run) => run.isAlive).length;
454
+ if (alive > 0) {
455
+ byAddress.set(address, alive);
456
+ }
457
+ }
458
+ return byAddress;
459
+ }
460
+
461
+ runningCount() {
462
+ let running = 0;
463
+ for (const runs of this.#runs.values()) {
464
+ for (const run of runs) {
465
+ if (run.isAlive && !run.isSuspended) {
466
+ running += 1;
467
+ }
468
+ }
469
+ }
470
+ return running;
471
+ }
472
+
252
473
  /**
253
474
  * How long an output is, once its playlist is known.
254
475
  *
@@ -307,7 +528,7 @@ export class EncodeOrchestrator {
307
528
  * @param {{ from: number, to: number, priority: number, withinSeconds: number }[]} zones
308
529
  */
309
530
  notePriorityMap(address, zones) {
310
- this.demand.state(address, zones);
531
+ this.#demand.state(address, zones);
311
532
  // NOBODY IS COMING HERE, so what was remembered about this output's input
312
533
  // is about nobody. Kept, those three entries would stay for the life of the
313
534
  // process, and the wait they describe would greet whoever opens this output
@@ -330,6 +551,7 @@ export class EncodeOrchestrator {
330
551
  // means. A statement kept beside the disk would be a second owner of one
331
552
  // fact, which is what item 87 removed from the coverage map.
332
553
  this.coverageOf(address).markReady(index);
554
+ this.segmentStore?.announce?.(address, index);
333
555
  for (const run of this.runsOn(address)) {
334
556
  run.noteProduced(index);
335
557
  }
@@ -364,7 +586,7 @@ export class EncodeOrchestrator {
364
586
  * state, so a pass that finds nothing to change does nothing.
365
587
  */
366
588
  reconcile() {
367
- const addresses = new Set([...this.demand.addresses(), ...this.#runs.keys()]);
589
+ const addresses = new Set([...this.#demand.addresses(), ...this.#runs.keys()]);
368
590
  for (const address of addresses) {
369
591
  this.#reconcileOne(address);
370
592
  }
@@ -433,12 +655,12 @@ export class EncodeOrchestrator {
433
655
  // used to reach into the layer that STATES them for the same arithmetic,
434
656
  // which is the coupling the layer rule forbids; the arithmetic itself now
435
657
  // lives where it belongs to nobody.
436
- const windows = this.demand.mapOn(address);
658
+ const windows = this.#demand.mapOn(address);
437
659
  const live = this.runsOn(address).filter((run) => run.isAlive);
438
660
  // Asked ONCE. It is arithmetic over measurements, but it also says out loud
439
661
  // when the reason it cuts the budget changes, so asking it three times in
440
662
  // one pass is three chances to say a thing that happened once.
441
- const maxRuns = this.#affordableOn(address, live);
663
+ const maxRuns = this.#withinMachine(address, live, this.#affordableOn(address, live));
442
664
  // THE TERMS EVERY ARRIVAL IS COMPUTED FROM, named here so the line below can
443
665
  // print them. A decision of this plan is `delay + (index - at) / rate +
444
666
  // madeBetween * refetch` against a deadline, so without the rate and the two
@@ -691,6 +913,42 @@ export class EncodeOrchestrator {
691
913
  return budget.runs;
692
914
  }
693
915
 
916
+ /**
917
+ * The per-output limit, bounded by what the whole machine can still hold.
918
+ *
919
+ * The per-output answer is at least one whatever else is running; the
920
+ * machine's may be none, and that is the refusal: the output gets no encoder
921
+ * until one elsewhere ends. It is never below what already runs here, so a
922
+ * refusal stops a start and never takes an encoder away.
923
+ *
924
+ * Said out loud when the machine is what bound it, once per change.
925
+ *
926
+ * @param {string} address
927
+ * @param {object[]} live
928
+ * @param {number} perOutput
929
+ * @returns {number}
930
+ */
931
+ #withinMachine(address, live, perOutput) {
932
+ if (!this.admission) {
933
+ return perOutput;
934
+ }
935
+ const machine = this.admission.placesFor(address, live.length);
936
+ const runs = Math.min(perOutput, machine.runs);
937
+ const said = runs < perOutput ? `${runs}:${machine.because}` : "";
938
+ if (said !== (this.#lastMachineBound.get(address) ?? "")) {
939
+ if (said) {
940
+ this.logger.info(
941
+ `encode: ${runs} encoder(s) on ${address} — the whole machine holds no more ` +
942
+ `(${machine.because}; this output alone would be allowed ${perOutput})`
943
+ );
944
+ this.#lastMachineBound.set(address, said);
945
+ } else {
946
+ this.#lastMachineBound.delete(address);
947
+ }
948
+ }
949
+ return runs;
950
+ }
951
+
694
952
  /**
695
953
  * Take charge of a run this class did not start.
696
954
  *
@@ -734,6 +992,16 @@ export class EncodeOrchestrator {
734
992
  * @param {import("../encode/EncodeRun.js").RunEnded} ended
735
993
  */
736
994
  noteEnded(ended) {
995
+ this.#lastState.set(ended.address, ended.run?.state ?? "IDLE");
996
+ if (ended.lastError) {
997
+ this.#lastError.set(ended.address, ended.lastError);
998
+ } else if (ended.because && ended.ending !== ENCODE_EXIT.COMPLETE) {
999
+ this.#lastError.set(ended.address, ended.because);
1000
+ }
1001
+ const progress = ended.run?.progress?.snapshot?.();
1002
+ if (progress) {
1003
+ this.#lastProgress.set(ended.address, progress);
1004
+ }
737
1005
  this.#costs.note(ended);
738
1006
  // Exactly one ending is normal — the run reached the end of the stretch it
739
1007
  // was given and closed its last file. Every other leaves a piece open, and
@@ -889,9 +1157,9 @@ export class EncodeOrchestrator {
889
1157
  */
890
1158
  describe() {
891
1159
  const parts = [];
892
- for (const address of new Set([...this.demand.addresses(), ...this.#runs.keys()])) {
1160
+ for (const address of new Set([...this.#demand.addresses(), ...this.#runs.keys()])) {
893
1161
  const coverage = this.coverageOf(address);
894
- const stated = this.demand.mapOn(address);
1162
+ const stated = this.#demand.mapOn(address);
895
1163
  const windows = stated.map((zone) => ({ from: zone.from, to: zone.to }));
896
1164
  const waiting = firstUnmetWant(coverage, windows);
897
1165
  const runs = this.runsOn(address)
@@ -921,7 +1189,7 @@ export class EncodeOrchestrator {
921
1189
  // existed there was no figure anywhere saying whether the urgent zone was
922
1190
  // served first — the map could have been read backwards and every line
923
1191
  // above would have looked the same.
924
- const served = waits.describe(address);
1192
+ const served = this.#describeWaits(address);
925
1193
  parts.push(
926
1194
  `${address} ready=${coverage.stats().ready}` +
927
1195
  `${held < 0 ? "" : ` of ${held} file(s) on disk`} ` +
@@ -39,6 +39,7 @@
39
39
 
40
40
  import { ENCODE_RUN_EVENT, ENCODE_RUN_STATE, INITIAL_RUN_STATE, nextState } from "./encode-run-state.js";
41
41
  import { classifyEncodeExit, ENCODE_EXIT } from "./encode-exit.js";
42
+ import { RunProgress } from "./RunProgress.js";
42
43
 
43
44
  /** Microseconds in a second, as ffmpeg's `out_time_ms` counts them. */
44
45
  const MICROSECONDS_PER_SECOND = 1_000_000;
@@ -186,12 +187,8 @@ export class EncodeRun {
186
187
  * encoder itself names it on its own channel, and answers with the name that
187
188
  * piece is served under — because making it servable is a rename, and only
188
189
  * whoever owns the disk can perform one.
189
- * @param {(progress: { processedSeconds: number | null, speed: string | null }) => void} [params.onProgress]
190
- * Called for every `-progress` report. Seconds count from the START OF THIS
191
- * RUN on both branches — neither `-output_ts_offset` nor `-copyts` changes
192
- * what `-progress` reports, both measured — so rebasing them onto the
193
- * source's timeline is the caller's, which is the only side that knows
194
- * where this run began.
190
+ * @param {(progress: object) => void} [params.onProgress] - Called with this
191
+ * run's progress on the source timeline after every `-progress` report.
195
192
  * @param {() => number | null} [params.lastSegmentIndex] - The film's last
196
193
  * segment number, for telling "reached the end" from "the input dried up".
197
194
  * ffmpeg exits zero for both, and over a torrent it cannot tell them apart.
@@ -208,6 +205,8 @@ export class EncodeRun {
208
205
  * a closed piece's name carries. How a piece is named belongs to the format
209
206
  * that writes it, so it arrives as a plain function rather than this class
210
207
  * knowing any naming.
208
+ * @param {number} [params.startSeconds] - Where this run begins on the source timeline.
209
+ * @param {number | null} [params.totalSeconds] - Source duration when known.
211
210
  */
212
211
  constructor({
213
212
  address,
@@ -226,6 +225,8 @@ export class EncodeRun {
226
225
  argsDescribed = "",
227
226
  usesExplicitCuts = false,
228
227
  indexOfName,
228
+ startSeconds = 0,
229
+ totalSeconds = null,
229
230
  because = "no reason was given"
230
231
  }) {
231
232
  this.address = address;
@@ -236,6 +237,7 @@ export class EncodeRun {
236
237
  this.spawnProcess = spawn;
237
238
  this.logger = logger;
238
239
  this.now = typeof now === "function" ? now : Date.now;
240
+ this.progress = new RunProgress({ startSeconds, totalSeconds, now: this.now });
239
241
  this.onEnded = typeof onEnded === "function" ? onEnded : () => {};
240
242
  this.onProgress = typeof onProgress === "function" ? onProgress : () => {};
241
243
  // Told the NAME of every piece the encoder has closed. The name is the
@@ -449,17 +451,22 @@ export class EncodeRun {
449
451
  if (key === "out_time_ms") {
450
452
  const numeric = Number(value);
451
453
  if (Number.isFinite(numeric) && numeric >= 0) {
452
- this.onProgress({ processedSeconds: numeric / MICROSECONDS_PER_SECOND, speed: null });
454
+ this.#reportProgress({ processedSeconds: numeric / MICROSECONDS_PER_SECOND, speed: null });
453
455
  }
454
456
  } else if (key === "out_time") {
455
- this.onProgress({ processedSeconds: null, speed: null, outTime: value });
457
+ this.#reportProgress({ processedSeconds: null, speed: null, outTime: value });
456
458
  } else if (key === "speed") {
457
459
  this.noteSpeed(Number.parseFloat(value));
458
- this.onProgress({ processedSeconds: null, speed: value });
460
+ this.#reportProgress({ processedSeconds: null, speed: value });
459
461
  }
460
462
  }
461
463
  }
462
464
 
465
+ #reportProgress(report) {
466
+ this.progress.note(report);
467
+ this.onProgress(this.progress.snapshot());
468
+ }
469
+
463
470
  /**
464
471
  * A segment this run has finished.
465
472
  *