@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
@@ -1,160 +0,0 @@
1
- /**
2
- * @file Subtitle orchestrator — application layer over subtitle domain.
3
- *
4
- * Wraps torrent-worker/subtitle-cues.js domain (planFor, cuesHeldFor,
5
- * warmSubtitleCues, forgetSubtitles) behind Container/Track abstraction.
6
- * Provides per-file track list and cue streaming, with the same "only already
7
- * downloaded clusters" rule as before. Controllers (HTTP or data-channel)
8
- * depend on this, not on the worker module directly.
9
- *
10
- * Delegates to ContainerOrchestrator for track enumeration so subtitle tracks
11
- * and their flags come from the unified ContainerTrack hierarchy.
12
- */
13
-
14
- import { containerOrchestrator } from "./ContainerOrchestrator.js";
15
- import {
16
- warmSubtitleCues as domainWarm,
17
- subtitleTracksOf,
18
- declaredSubtitleTracksOf,
19
- forgetSubtitles as domainForget
20
- } from "../torrent-worker/subtitle-cues.js";
21
- import { logger } from "../../utils/logger.js";
22
-
23
- export class SubtitleOrchestrator {
24
- /**
25
- * @param {import("./ContainerOrchestrator.js").ContainerOrchestrator} containerOrchestrator
26
- */
27
- constructor(containerOrchestrator) {
28
- this.containers = containerOrchestrator;
29
- }
30
-
31
- /**
32
- * Tracks for menu — text tracks via domain, enriched with ContainerTrack flags.
33
- * Falls back to container tracks when domain has no plan yet.
34
- * @param {object} torrent
35
- * @param {number} fileIndex
36
- * @param {string} sourceKey
37
- * @param {(start:number,end:number)=>Promise<Buffer|null>} [readRange]
38
- * @param {number} [fileSize]
39
- * @returns {Promise<import("../tracks/index.js").ContainerTrack[]>}
40
- */
41
- async getTracks(torrent, fileIndex, sourceKey, readRange, fileSize) {
42
- try {
43
- const domain = await subtitleTracksOf(torrent, fileIndex, sourceKey);
44
- if (Array.isArray(domain) && domain.length > 0) return domain;
45
- } catch {}
46
- if (readRange && Number.isFinite(fileSize)) {
47
- try {
48
- const tracks = await this.containers.getTracks({ sourceKey, fileIndex, readRange, fileSize, label: torrent?.files?.[fileIndex]?.name ?? "" });
49
- return tracks.filter((t) => t.type === "subtitle");
50
- } catch {}
51
- }
52
- return [];
53
- }
54
-
55
- /**
56
- * Declared subtitle tracks in container order (including image tracks) — for declaredIndex alignment.
57
- */
58
- async getDeclaredTracks(torrent, fileIndex, sourceKey) {
59
- try {
60
- return await declaredSubtitleTracksOf(torrent, fileIndex, sourceKey);
61
- } catch {
62
- return [];
63
- }
64
- }
65
-
66
- /**
67
- * Cues already downloaded for one track — ASKED OF THE TORRENT WORKER, never
68
- * walked here.
69
- *
70
- * The walk decides what it may read from `torrent.bitfield` and
71
- * `torrent.pieceLength`, and a torrent stand-in on the main thread has
72
- * neither: it carries `infoHash`, `name` and a `files` list whose reads go
73
- * back across the boundary (`torrent-worker/client.js`). So the same code
74
- * called here answers that nothing is downloaded, walks no clusters, and
75
- * returns an empty document — which is not a failure anything reports,
76
- * because "no cues held" is a legitimate answer.
77
- *
78
- * Field 2026-09-03, and it is the whole reason this method changed. An
79
- * episode already downloaded from an earlier sitting had its cues found
80
- * within a second and a half of the file being opened, and the first four
81
- * pushes — everything before 81.7 s — went out before the browser had
82
- * subscribed. The catch-up pull that exists for exactly that case answered
83
- * `WEBVTT` and nothing else, with `x-subtitle-covered-clusters: 0` against
84
- * 283 indexed, so the viewer watched the opening of the episode with no
85
- * subtitles and the rest of it with them.
86
- *
87
- * There is a second reason, independent of the bitfield. The register of what
88
- * has been walked, what has been found and in what ORDER lives in the module
89
- * that does the walking, and the worker already keeps one — the push path
90
- * fills it. Walking again on the main thread would build a SECOND register
91
- * with its own `seq` counter, and the browser mixes the cursors from both
92
- * paths (`#rememberCursor`): two counters would make a cursor from a pull and
93
- * a cursor from a push incomparable. One register, one walk, one cursor.
94
- *
95
- * @param {{ getSubtitleCues?: Function }} pool - The torrent pool, which is
96
- * what holds the channel to the worker.
97
- * @param {object} torrent
98
- * @param {number} fileIndex
99
- * @param {string} sourceKey
100
- * @param {number} trackNumber - Container trackNumber
101
- * @returns {Promise<{ cues: object[], coveredClusters: number, indexedClusters: number, track: object | null }>}
102
- */
103
- async getCues(pool, torrent, fileIndex, sourceKey, trackNumber) {
104
- // Built fresh on each of the three paths that need it. One shared literal
105
- // returned by reference would hand every caller the same array, and a
106
- // single one of them appending to it would change what the next caller
107
- // reads — which in a method about cue registers not leaking into each
108
- // other would be a poor thing to introduce.
109
- const empty = () => ({ cues: [], coveredClusters: 0, indexedClusters: 0, track: null });
110
- if (typeof pool?.getSubtitleCues !== "function") {
111
- // Nothing here can read pieces, and answering an empty document would be
112
- // indistinguishable from a file that genuinely holds no cues.
113
- logger.warn(
114
- "subtitle-orchestrator: the torrent pool cannot be asked for cues, " +
115
- "so none can be served — the walk needs the thread that owns the torrent"
116
- );
117
- return empty();
118
- }
119
- try {
120
- const answer = await pool.getSubtitleCues(torrent, fileIndex, trackNumber);
121
- if (!answer) {
122
- return empty();
123
- }
124
- return {
125
- cues: Array.isArray(answer.cues) ? answer.cues : [],
126
- coveredClusters: answer.coveredClusters ?? 0,
127
- indexedClusters: answer.indexedClusters ?? 0,
128
- // The worker answers with the track's own fields flat, because a
129
- // `ContainerTrack` is a class and only plain objects cross the boundary.
130
- track: {
131
- codecId: answer.codecId ?? "",
132
- codecPrivate: answer.codecPrivate ?? "",
133
- language: answer.language ?? ""
134
- }
135
- };
136
- } catch (e) {
137
- logger.warn(`subtitle-orchestrator: getCues failed: ${e?.message ?? e}`);
138
- return empty();
139
- }
140
- }
141
-
142
- /**
143
- * Warm all subtitle tracks of a file — called periodically and on verified pieces.
144
- * Returns per-track fresh cues for push.
145
- */
146
- async warm(torrent, fileIndex, sourceKey) {
147
- try {
148
- return await domainWarm(torrent, fileIndex, sourceKey);
149
- } catch {
150
- return [];
151
- }
152
- }
153
-
154
- forget(sourceKey, fileIndex) {
155
- domainForget(sourceKey, fileIndex);
156
- this.containers.forget(sourceKey, fileIndex);
157
- }
158
- }
159
-
160
- export const subtitleOrchestrator = new SubtitleOrchestrator(containerOrchestrator);
@@ -1,94 +0,0 @@
1
- /**
2
- * @file What one output is encoded AS, decided once.
3
- *
4
- * `OutputSpec` says what an output is — which tracks, in what form, cut how.
5
- * This is the other half: the shape the encoder is actually given for it. The
6
- * box in pixels, the frame rate, the speed setting, whether the picture is tone
7
- * mapped down from HDR.
8
- *
9
- * **Why it is not a fact of a session.** It is decided by the realtime budget
10
- * at the moment a session is created — what this machine could hold just then —
11
- * so two sessions of one output, made minutes apart, could be given different
12
- * shapes while claiming the same identity. Everything downstream assumes
13
- * otherwise: a segment of one is supposed to be interchangeable with a segment
14
- * of the other, and the master playlist names one `RESOLUTION` for both.
15
- *
16
- * Decided once per output and held here, that cannot happen. What the budget
17
- * learns afterwards moves the RATE cap, which is deliberately not here: rate
18
- * control appears in neither the SPS nor the PPS, so it can move under a player
19
- * that has already cached the init. The size cannot, which is exactly why the
20
- * size belongs to the output and the cap belongs to the run.
21
- */
22
-
23
- export class Output {
24
- /**
25
- * @param {object} params
26
- * @param {number} params.encodeWidth - 0 means the source's own width.
27
- * @param {number} params.encodeHeight - 0 means the source's own height.
28
- * @param {number} params.outputFps
29
- * @param {string | null} params.softwarePreset - The speed setting, where the
30
- * encoder has a ladder and one was chosen from it.
31
- * @param {boolean} params.applyTonemap
32
- */
33
- constructor({ encodeWidth, encodeHeight, outputFps, softwarePreset, applyTonemap }) {
34
- this.encodeWidth = Number.isFinite(encodeWidth) ? encodeWidth : 0;
35
- this.encodeHeight = Number.isFinite(encodeHeight) ? encodeHeight : 0;
36
- this.outputFps = Number.isFinite(outputFps) && outputFps > 0 ? outputFps : 0;
37
- this.softwarePreset = typeof softwarePreset === "string" ? softwarePreset : null;
38
- this.applyTonemap = applyTonemap === true;
39
- }
40
- }
41
-
42
- /**
43
- * The shapes this proxy has decided, one per output.
44
- *
45
- * Keyed by `OutputSpec.toKey()` and by nothing else: the shape is a property of
46
- * what is being produced, and two requests that produce the same thing must be
47
- * given the same one however far apart they arrive.
48
- */
49
- export class Outputs {
50
- /** @type {Map<string, Output>} */
51
- #byKey = new Map();
52
-
53
- /**
54
- * The shape for this output, decided by `decide` the first time it is asked
55
- * for and never again.
56
- *
57
- * @param {string} key
58
- * @param {() => Output} decide
59
- * @returns {Output}
60
- */
61
- get(key, decide) {
62
- let output = this.#byKey.get(key);
63
- if (!output) {
64
- output = decide();
65
- this.#byKey.set(key, output);
66
- }
67
- return output;
68
- }
69
-
70
- /**
71
- * Drop every shape nobody is holding.
72
- *
73
- * Same reason the timelines are swept: a map that only grows is the shape of
74
- * half the memory faults recorded in this project.
75
- *
76
- * @param {Set<Output>} inUse
77
- * @returns {number}
78
- */
79
- forgetUnused(inUse) {
80
- let dropped = 0;
81
- for (const [key, output] of [...this.#byKey]) {
82
- if (!inUse.has(output)) {
83
- this.#byKey.delete(key);
84
- dropped += 1;
85
- }
86
- }
87
- return dropped;
88
- }
89
-
90
- /** @returns {number} */
91
- get size() {
92
- return this.#byKey.size;
93
- }
94
- }
@@ -1,202 +0,0 @@
1
- /**
2
- * @file What a session PRODUCES, stated once and used as its identity.
3
- *
4
- * Three domain axes already have a home in this package: what a file states
5
- * about itself (`container/`), what a track states about itself (`tracks/`),
6
- * and what anybody wants off the swarm (`demand/`). This is the fourth — what
7
- * we make out of them — and it had no home at all: the identity of a session
8
- * was a `[...].join(":")` inside a 300-line function, with no name, no test and
9
- * nothing printing it.
10
- *
11
- * The rule it exists to express, stated by the user 2026-09-03:
12
- *
13
- * > A session belongs to the tracks its output actually carries. Its key holds
14
- * > those tracks' parameters and nothing else. Two sessions whose parameters
15
- * > agree ARE the same session, and the encoded result is reused by definition.
16
- *
17
- * So reuse between viewers is not a feature built on top of this — it is what a
18
- * correctly built key already means. A field of the REQUEST that does not
19
- * change one byte of the output must not appear here; measured 2026-09-03, two
20
- * viewers of one copied picture got two sessions whose output was identical
21
- * byte for byte (`research/two-viewers-one-file-2026-09-03.md`).
22
- *
23
- * What deliberately does NOT appear:
24
- *
25
- * 1. the viewer, in any form. Not the consumer id, not where they started, not
26
- * their viewport. A viewer is not a property of the material;
27
- * 2. the bitrate ceiling the viewer's measured link puts on a re-encode. It is
28
- * a runtime parameter of a SHARED session — the worst link among the live
29
- * consumers decides — so two viewers on one encode is the design, not a
30
- * collision. Rate control appears in neither the SPS nor the PPS, which is
31
- * why it can move under a player that has already cached the init;
32
- * 3. the video track number. Only `0:v:0` is ever mapped, so the file names the
33
- * picture. Add it here the day a second video track can be chosen.
34
- *
35
- * A limit worth stating: two specs that agree name interchangeable output
36
- * within ONE proxy. A copied picture's bytes depend only on the source, but a
37
- * re-encoded one's depend on this host's encoder, its preset and its rate cap —
38
- * so this is not enough to reuse segments BETWEEN proxies (roadmap item 41).
39
- */
40
-
41
- /**
42
- * The picture an output carries.
43
- */
44
- export class VideoOutput {
45
- /**
46
- * @param {object} params
47
- * @param {number} params.fileIndex - The file the picture is read from.
48
- * @param {{ width: number, height: number, exactSize: boolean } | null} params.encode
49
- * Null when the picture is copied — then the output is the source's own
50
- * size and nothing about a target box can change it. The box when it is
51
- * re-encoded, with `exactSize` saying that box is produced exactly as asked
52
- * and the realtime budget must not move it.
53
- *
54
- * `exactSize` says NOTHING ABOUT WHO ASKED. Every rung of a master playlist
55
- * sets it, whether the viewer picked that rung or the player moved itself,
56
- * because two rungs allowed to drift would land on one height and the
57
- * choice between them would mean nothing. It was called `manual` until
58
- * 2026-09-13, and that name was read as "the viewer did this" four times
59
- * over a few weeks — in field logs where the viewer had touched nothing.
60
- */
61
- constructor({ fileIndex, encode = null }) {
62
- this.fileIndex = Number.isInteger(fileIndex) && fileIndex >= 0 ? fileIndex : 0;
63
- this.encode = encode
64
- ? {
65
- width: Number.isInteger(encode.width) && encode.width > 0 ? encode.width : 0,
66
- height: Number.isInteger(encode.height) && encode.height > 0 ? encode.height : 0,
67
- exactSize: encode.exactSize === true
68
- }
69
- : null;
70
- }
71
-
72
- /**
73
- * @returns {string}
74
- */
75
- toKey() {
76
- if (!this.encode) {
77
- return `v=${this.fileIndex}/copy`;
78
- }
79
- const box = `${this.encode.width}x${this.encode.height}`;
80
- return `v=${this.fileIndex}/enc:${box}:${this.encode.exactSize ? "exact" : "budget"}`;
81
- }
82
- }
83
-
84
- /**
85
- * The soundtrack an output carries.
86
- */
87
- export class AudioOutput {
88
- /**
89
- * @param {object} params
90
- * @param {number} params.fileIndex - The file the TRACK lives in, which for a
91
- * dub shipped beside the picture is not the picture's file.
92
- * @param {number} params.trackIndex - `0:a:N` inside that file. The flat
93
- * number the browser sends spans the picture's own tracks and the files
94
- * beside it, and two flat numbers of two different pictures can name one
95
- * track; this is the number that cannot.
96
- * @param {boolean} params.transcode - Re-encoded to AAC, or copied.
97
- */
98
- constructor({ fileIndex, trackIndex, transcode }) {
99
- this.fileIndex = Number.isInteger(fileIndex) && fileIndex >= 0 ? fileIndex : 0;
100
- this.trackIndex = Number.isInteger(trackIndex) && trackIndex >= 0 ? trackIndex : 0;
101
- this.transcode = transcode === true;
102
- }
103
-
104
- /**
105
- * @returns {string}
106
- */
107
- toKey() {
108
- return `a=${this.fileIndex}/${this.trackIndex}/${this.transcode ? "aac" : "copy"}`;
109
- }
110
- }
111
-
112
- /**
113
- * Where an output is cut.
114
- *
115
- * Both forms belong to a FILE and not to the session: the keyframe grid is that
116
- * file's own keyframe times, and the uniform grid is derived from that file's
117
- * duration. A soundtrack takes the grid of the picture it accompanies, so its
118
- * grid names the picture's file — which is what makes two soundtrack sessions
119
- * cut for two different pictures tell themselves apart.
120
- */
121
- export class CutGrid {
122
- /**
123
- * @param {object} params
124
- * @param {"keyframe" | "uniform"} params.kind
125
- * @param {number} params.fileIndex - Whose keyframes, or whose duration.
126
- */
127
- constructor({ kind, fileIndex }) {
128
- this.kind = kind === "keyframe" ? "keyframe" : "uniform";
129
- this.fileIndex = Number.isInteger(fileIndex) && fileIndex >= 0 ? fileIndex : 0;
130
- }
131
-
132
- /**
133
- * @returns {string}
134
- */
135
- toKey() {
136
- return `grid=${this.kind === "keyframe" ? "kf" : "even"}@${this.fileIndex}`;
137
- }
138
- }
139
-
140
- /**
141
- * One encode of one torrent's material: which tracks, in what form, cut how,
142
- * packaged how.
143
- */
144
- export class OutputSpec {
145
- /**
146
- * @param {object} params
147
- * @param {string} params.sourceKey - `torrent:<infohash>`, the canonical
148
- * identity of the torrent itself: a magnet and a `.torrent` file for the
149
- * same content produce the same one (`torrent-source-key.js`). Nothing
150
- * further is needed to say WHICH film this is.
151
- * @param {string} params.segmentFormatId - fMP4 or MPEG-TS. Two viewers
152
- * asking for different containers cannot share one ffmpeg.
153
- * @param {CutGrid} params.grid
154
- * @param {VideoOutput | null} params.video
155
- * @param {AudioOutput | null} params.audio
156
- */
157
- constructor({ sourceKey, segmentFormatId, grid, video = null, audio = null }) {
158
- this.sourceKey = String(sourceKey ?? "");
159
- this.segmentFormatId = String(segmentFormatId ?? "");
160
- this.grid = grid;
161
- this.video = video;
162
- this.audio = audio;
163
- }
164
-
165
- /**
166
- * What this output carries, in the vocabulary the rest of the class uses.
167
- *
168
- * `muxed` is the one case where a session legitimately holds the parameters
169
- * of two tracks: a browser that does not understand rendition groups must be
170
- * sent its sound inside the picture's own stream.
171
- *
172
- * @returns {"video-only" | "audio-only" | "muxed" | "empty"}
173
- */
174
- get carries() {
175
- if (this.video && this.audio) {
176
- return "muxed";
177
- }
178
- if (this.video) {
179
- return "video-only";
180
- }
181
- if (this.audio) {
182
- return "audio-only";
183
- }
184
- return "empty";
185
- }
186
-
187
- /**
188
- * The identity. Two outputs with the same one are the same output.
189
- *
190
- * @returns {string}
191
- */
192
- toKey() {
193
- const parts = [this.sourceKey, `fmt=${this.segmentFormatId}`, this.grid.toKey(), this.carries];
194
- if (this.video) {
195
- parts.push(this.video.toKey());
196
- }
197
- if (this.audio) {
198
- parts.push(this.audio.toKey());
199
- }
200
- return parts.join(":");
201
- }
202
- }
@@ -1 +0,0 @@
1
- export { OutputSpec, VideoOutput, AudioOutput, CutGrid } from "./OutputSpec.js";
@@ -1,26 +0,0 @@
1
- /**
2
- * @file The heights a picture may be offered at.
3
- *
4
- * A fact about an output and about nothing else: it takes a source height and
5
- * answers with heights. What the machine can afford of them is the quality
6
- * budget's question, and what a player may splice between is `LiveOutputs`.
7
- */
8
-
9
- // The resolutions a viewer may choose between. Only rungs at or below the
10
- // source are offered: upscaling invents detail and costs the encoder more than
11
- // the source itself.
12
- const VARIANT_LADDER = [2160, 1440, 1080, 720, 540, 480, 360, 240];
13
-
14
- /**
15
- * The heights offered for a source of this height, largest first.
16
- *
17
- * @param {number} sourceHeight
18
- * @returns {number[]}
19
- */
20
- export function variantHeightsFor(sourceHeight) {
21
- if (!Number.isFinite(sourceHeight) || sourceHeight <= 0) {
22
- return [];
23
- }
24
- const rungs = VARIANT_LADDER.filter((height) => height < sourceHeight);
25
- return [Math.round(sourceHeight), ...rungs];
26
- }