@torrent-tv/proxy 2.85.1 → 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 (381) hide show
  1. package/CHANGELOG.md +2138 -1909
  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 -489
  157. package/services/viewer/Viewers.js +368 -257
  158. package/services/viewer/active-output.js +37 -54
  159. package/services/viewer/audio-start.js +46 -0
  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 -44
  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 -518
  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 -172
  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 -9689
  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/test/produced-index.test.js +0 -142
  347. /package/{services → bin}/thread-pool.js +0 -0
  348. /package/services/{available-share.js → encode/available-share.js} +0 -0
  349. /package/services/{decode-cost-fit.js → encode/decode-cost-fit.js} +0 -0
  350. /package/services/{encoder-readings.js → encode/encoder-readings.js} +0 -0
  351. /package/services/{host-load.js → encode/host-load.js} +0 -0
  352. /package/services/{output → encode/output}/cut-grid.js +0 -0
  353. /package/services/{output → encode/output}/rates.js +0 -0
  354. /package/services/{segment-formats → encode/segment-formats}/mpegts.js +0 -0
  355. /package/services/{torrent-cost.js → encode/torrent-cost.js} +0 -0
  356. /package/services/{container → media/container}/ebml-reader.js +0 -0
  357. /package/services/{ffmpeg-banner.js → media/ffmpeg-banner.js} +0 -0
  358. /package/services/{tracks → media/tracks}/ContainerTrack.js +0 -0
  359. /package/services/{tracks → media/tracks}/ImageSubtitleTrack.js +0 -0
  360. /package/services/{tracks → media/tracks}/SubtitleTrack.js +0 -0
  361. /package/services/{tracks → media/tracks}/TextSubtitleTrack.js +0 -0
  362. /package/services/{tracks → media/tracks}/VideoTrack.js +0 -0
  363. /package/services/{tracks → media/tracks}/index.js +0 -0
  364. /package/services/{files → storage/files}/piece-from-whole-file.js +0 -0
  365. /package/services/{disk → storage}/keep.js +0 -0
  366. /package/services/{piece-store → storage/piece-store}/piece-lru.js +0 -0
  367. /package/services/{disk → storage}/returns.js +0 -0
  368. /package/services/{demand → torrent/demand}/DemandRegister.js +0 -0
  369. /package/services/{demand → torrent/demand}/Urgency.js +0 -0
  370. /package/services/{demand → torrent/demand}/Window.js +0 -0
  371. /package/services/{demand → torrent/demand}/index.js +0 -0
  372. /package/services/{demand → torrent/demand}/pieces.js +0 -0
  373. /package/services/{download → torrent/download}/withdraw-claim.js +0 -0
  374. /package/services/{supply-margin.js → torrent/supply-margin.js} +0 -0
  375. /package/services/{torrent-source-key.js → torrent/torrent-source-key.js} +0 -0
  376. /package/services/{torrent-worker → torrent/worker}/fastest-wires.js +0 -0
  377. /package/services/{torrent-worker → torrent/worker}/handle-state.js +0 -0
  378. /package/services/{nat-classifier.js → transport/nat-classifier.js} +0 -0
  379. /package/services/{registry-api.js → transport/registry-api.js} +0 -0
  380. /package/services/{tunnel-client.js → transport/tunnel-client.js} +0 -0
  381. /package/services/{priority → viewer}/WaitLedger.js +0 -0
@@ -35,7 +35,7 @@ export class WorkerTorrentPool {
35
35
  #torrents = new Map();
36
36
 
37
37
  /**
38
- * @param {{ maxDiskBytes?: number, memoryBytes?: number, stateDir?: string }} [options]
38
+ * @param {{ memoryBytes?: number, stateDir?: string }} [options]
39
39
  */
40
40
  constructor(options = {}) {
41
41
  this.#client = new TorrentWorkerClient(options);
@@ -83,6 +83,22 @@ export class WorkerTorrentPool {
83
83
  return this.#client.allowSpillBytes(bytes);
84
84
  }
85
85
 
86
+ get memoryClaim() {
87
+ return this.#client.memoryClaim ?? { held: 0, wanted: 0 };
88
+ }
89
+
90
+ allowMemoryBytes(bytes) {
91
+ return this.#client.allowMemoryBytes(bytes);
92
+ }
93
+
94
+ get wholeFileBytes() {
95
+ return this.#client.wholeFileBytes ?? 0;
96
+ }
97
+
98
+ allowWholeFileBytes(bytes) {
99
+ return this.#client.allowWholeFileBytes(bytes);
100
+ }
101
+
86
102
  /**
87
103
  * Bytes every torrent here has moved.
88
104
  *
@@ -132,62 +148,8 @@ export class WorkerTorrentPool {
132
148
  return answer?.started === true;
133
149
  }
134
150
 
135
- /**
136
- * The text subtitle tracks a file carries, read from its own header.
137
- *
138
- * @param {object} torrent
139
- * @param {number} fileIndex
140
- * @returns {Promise<object[]>}
141
- */
142
- async getSubtitleTracks(torrent, fileIndex) {
143
- const sourceKey = torrent?.sourceKey;
144
- if (!sourceKey) {
145
- return [];
146
- }
147
- const answer = await this.#client.getSubtitleTracks({ sourceKey, fileIndex });
148
- return Array.isArray(answer?.tracks) ? answer.tracks : [];
149
- }
150
151
 
151
- /**
152
- * What the container itself declares about its subtitle tracks, in its own
153
- * order and including the picture-based ones — for lining up against
154
- * ffmpeg's own numbering.
155
- *
156
- * @param {object} torrent
157
- * @param {number} fileIndex
158
- * @returns {Promise<object[]>}
159
- */
160
- async getDeclaredSubtitleTracks(torrent, fileIndex) {
161
- const sourceKey = torrent?.sourceKey;
162
- if (!sourceKey) {
163
- return [];
164
- }
165
- const answer = await this.#client.getSubtitleTracks({ sourceKey, fileIndex });
166
- return Array.isArray(answer?.declared) ? answer.declared : [];
167
- }
168
152
 
169
- /**
170
- * Every track one file declares, read from its own header by the container
171
- * layer.
172
- *
173
- * The audio menu is built from ffmpeg's `-i` banner, which carries neither
174
- * `FlagOriginal`, `FlagCommentary`, `FlagVisualImpaired`, `FlagEnabled` nor
175
- * `LanguageBCP47` — so without this a director's commentary and the film
176
- * itself are indistinguishable in it. Also how a soundtrack shipped as its own
177
- * file is read: a `.mka` is Matroska and the same reader serves it.
178
- *
179
- * @param {object} torrent
180
- * @param {number} fileIndex
181
- * @returns {Promise<object[]>}
182
- */
183
- async getContainerTracks(torrent, fileIndex) {
184
- const sourceKey = torrent?.sourceKey;
185
- if (!sourceKey) {
186
- return [];
187
- }
188
- const answer = await this.#client.getContainerTracks({ sourceKey, fileIndex });
189
- return Array.isArray(answer?.tracks) ? answer.tracks : [];
190
- }
191
153
 
192
154
  /**
193
155
  * Start fetching the region a viewer is about to resume at. Named in seconds
@@ -198,99 +160,19 @@ export class WorkerTorrentPool {
198
160
  * @param {number} positionSeconds
199
161
  * @returns {Promise<boolean>}
200
162
  */
201
- async warmResumePosition(torrent, fileIndex, positionSeconds) {
163
+ async warmResumePosition(torrent, fileIndex, positionSeconds, durationSeconds) {
202
164
  const sourceKey = torrent?.sourceKey;
203
165
  if (!sourceKey) {
204
166
  return false;
205
167
  }
206
- const answer = await this.#client.warmResumePosition({ sourceKey, fileIndex, positionSeconds });
168
+ const answer = await this.#client.warmResumePosition({ sourceKey, fileIndex, positionSeconds, durationSeconds });
207
169
  return answer?.started === true;
208
170
  }
209
171
 
210
- /**
211
- * What one file declares about itself: format, duration, and where its own
212
- * timeline begins.
213
- *
214
- * @param {object} torrent
215
- * @param {number} fileIndex
216
- * @returns {Promise<import("../container/Container.js").ContainerMediaInfo | null>}
217
- */
218
- async getContainerMediaInfo(torrent, fileIndex) {
219
- const sourceKey = torrent?.sourceKey;
220
- if (!sourceKey) {
221
- return null;
222
- }
223
- const answer = await this.#client.getContainerMediaInfo({ sourceKey, fileIndex });
224
- return answer?.info ?? null;
225
- }
226
172
 
227
- /**
228
- * Where one file's keyframes are, from the container's own table. Null when
229
- * this file has no readable index — which is a final answer about the file,
230
- * and the reason a copy of it has to be re-encoded instead.
231
- *
232
- * @param {object} torrent
233
- * @param {number} fileIndex
234
- * @returns {Promise<{ times: number[], tolerance: number } | null>}
235
- */
236
- async getContainerKeyframes(torrent, fileIndex) {
237
- const sourceKey = torrent?.sourceKey;
238
- if (!sourceKey) {
239
- return null;
240
- }
241
- const answer = await this.#client.getContainerKeyframes({ sourceKey, fileIndex });
242
- return answer?.index ?? null;
243
- }
244
173
 
245
- /**
246
- * The audio tracks one file declares, in the order ffmpeg numbers them
247
- * `0:a:N`.
248
- *
249
- * @param {object} torrent
250
- * @param {number} fileIndex
251
- * @returns {Promise<object[]>}
252
- */
253
- async getDeclaredAudioTracks(torrent, fileIndex) {
254
- const tracks = await this.getContainerTracks(torrent, fileIndex);
255
- return tracks
256
- .filter((track) => track?.type === "audio")
257
- .sort((left, right) => (left.declaredIndex ?? 0) - (right.declaredIndex ?? 0));
258
- }
259
174
 
260
- /**
261
- * The video track one file declares, or null where it declares none.
262
- *
263
- * One, because ffmpeg's `0:v:0` is what everything downstream is built on and
264
- * a second video stream is a cover image far more often than a second film.
265
- *
266
- * @param {object} torrent
267
- * @param {number} fileIndex
268
- * @returns {Promise<object | null>}
269
- */
270
- async getDeclaredVideoTrack(torrent, fileIndex) {
271
- const tracks = await this.getContainerTracks(torrent, fileIndex);
272
- return (
273
- tracks
274
- .filter((track) => track?.type === "video")
275
- .sort((left, right) => (left.declaredIndex ?? 0) - (right.declaredIndex ?? 0))[0] ?? null
276
- );
277
- }
278
175
 
279
- /**
280
- * The cues of one subtitle track that the downloaded clusters already carry.
281
- *
282
- * @param {object} torrent
283
- * @param {number} fileIndex
284
- * @param {number} trackNumber
285
- * @returns {Promise<object | null>}
286
- */
287
- async getSubtitleCues(torrent, fileIndex, trackNumber) {
288
- const sourceKey = torrent?.sourceKey;
289
- if (!sourceKey) {
290
- return null;
291
- }
292
- return this.#client.getSubtitleCues({ sourceKey, fileIndex, trackNumber });
293
- }
294
176
 
295
177
  /**
296
178
  * Reorder piece selection around a read position.
@@ -352,6 +234,88 @@ export class WorkerTorrentPool {
352
234
  * @param {{ headBytes?: number, tailBytes?: number, timeoutMs?: number }} [options]
353
235
  * @returns {Promise<unknown>}
354
236
  */
237
+ /**
238
+ * One byte range of one file, as bytes, on THIS thread.
239
+ *
240
+ * What the media layer is built from: a container takes `readRange(start,
241
+ * end)` and nothing else, so with this it can be parsed here instead of in
242
+ * the torrent thread. It used to be parsed there for one stated reason —
243
+ * "the main thread cannot open a read stream on one of its files", which is
244
+ * true of WebTorrent's own API and not of the bytes: the pieces live in
245
+ * shared memory and this read is the same one that serves every segment,
246
+ * so it waits for what has not arrived and steers the swarm toward it.
247
+ *
248
+ * **It COPIES, deliberately.** The zero-copy path — positions across the
249
+ * channel, the piece pinned until the far side says it is done — exists for
250
+ * 10 MB segments on the critical path, where the copy was measured at
251
+ * 18.84 ms. A container header is 64-256 KB and read once per file, about
252
+ * 0.12 ms, against a parse of 0.8 s and a swarm wait of up to a minute. The
253
+ * pinning protocol would buy nothing and is not on this path.
254
+ *
255
+ * @param {object} torrent
256
+ * @param {number} fileIndex
257
+ * @param {number} start - First byte, inclusive.
258
+ * @param {number} end - Last byte, inclusive.
259
+ * @returns {Promise<Buffer | null>} Null when the read failed or was cut
260
+ * short, which a container reads as "this file does not say".
261
+ */
262
+ /**
263
+ * The byte ranges of one file the torrent holds WHOLE.
264
+ *
265
+ * What the subtitle walk decides from: it may read only what is already
266
+ * downloaded, and a list taken once per pass replaces a question per cluster.
267
+ *
268
+ * @param {object} torrent
269
+ * @param {number} fileIndex
270
+ * @returns {Promise<Array<[number, number]>>}
271
+ */
272
+ async heldRangesOf(torrent, fileIndex) {
273
+ const sourceKey = torrent?.sourceKey;
274
+ return sourceKey ? this.#client.heldRanges({ sourceKey, fileIndex }) : [];
275
+ }
276
+
277
+ /**
278
+ * Bytes of a range the torrent already holds, never fetched.
279
+ *
280
+ * Not `readRangeOf`: that one declares demand and steers the swarm, which is
281
+ * right for a viewer waiting on a segment and wrong for a walk that must pull
282
+ * nothing the viewer is not waiting for.
283
+ *
284
+ * @param {object} torrent
285
+ * @param {number} fileIndex
286
+ * @param {number} start
287
+ * @param {number} end - Inclusive.
288
+ * @returns {Promise<Buffer | null>}
289
+ */
290
+ async readHeldOf(torrent, fileIndex, start, end) {
291
+ const sourceKey = torrent?.sourceKey;
292
+ return sourceKey ? this.#client.readHeld({ sourceKey, fileIndex, start, end }) : null;
293
+ }
294
+
295
+ async readRangeOf(torrent, fileIndex, start, end) {
296
+ const sourceKey = torrent?.sourceKey;
297
+ if (!sourceKey || !(end >= start) || !(start >= 0)) {
298
+ return null;
299
+ }
300
+ const stream = this.#client.createReadStream({ sourceKey, fileIndex, start, end });
301
+ const chunks = [];
302
+ let total = 0;
303
+ try {
304
+ const reader = stream.getReader();
305
+ for (;;) {
306
+ const { done, value } = await reader.read();
307
+ if (done) {
308
+ break;
309
+ }
310
+ chunks.push(Buffer.from(value));
311
+ total += value.length;
312
+ }
313
+ } catch {
314
+ return null;
315
+ }
316
+ return total > 0 ? Buffer.concat(chunks, total) : null;
317
+ }
318
+
355
319
  async prefetchFileEdges(torrent, fileIndex, options = {}) {
356
320
  const sourceKey = torrent?.sourceKey;
357
321
  if (!sourceKey) {
@@ -79,37 +79,28 @@ export const Command = {
79
79
  * the work is under way.
80
80
  */
81
81
  FILL_FILE: "fill-file",
82
- /** The text subtitle tracks a file carries, for the viewer's menu. */
83
- SUBTITLE_TRACKS: "subtitle-tracks",
84
82
  /**
85
- * Every track a file declares, read from its own header by the container
86
- * layer. Answers what ffmpeg's `-i` banner cannot: FlagOriginal,
87
- * FlagCommentary, FlagVisualImpaired, FlagEnabled and LanguageBCP47 appear
88
- * nowhere in it. Asked of the picture AND of a soundtrack shipped as its own
89
- * file beside it, which is the same question about a different file.
90
- */
91
- CONTAINER_TRACKS: "container-tracks",
92
- /**
93
- * What a file declares about itself as a whole — format, duration, and where
94
- * its own timeline begins. Read from the same header, by the same reader, as
95
- * the track table above; the alternative was a second ffmpeg over the proxy's
96
- * own HTTP reading the same bytes again.
83
+ * Start fetching the region a viewer is about to resume at, named in seconds
84
+ * and turned into bytes here, where the file's own duration can be read.
97
85
  */
98
- CONTAINER_MEDIA_INFO: "container-media-info",
86
+ WARM_POSITION: "warm-position",
99
87
  /**
100
- * Where a file's keyframes are, from the container's own table. A property of
101
- * immutable bytes, read once per file by the same container that answers the
102
- * two above — and the answer decides whether a picture can be copied at all,
103
- * which must be the same answer for every viewer of that file.
88
+ * The byte ranges of one file the torrent holds WHOLE, so the subtitle walk
89
+ * can decide what it may read without asking the swarm. A list rather than a
90
+ * question per cluster: a pass asks about every cluster of the file, and
91
+ * hundreds of round trips for a walk meant to be free when there is nothing
92
+ * new would be the cost of the split, not of the work.
104
93
  */
105
- CONTAINER_KEYFRAMES: "container-keyframes",
94
+ HELD_RANGES: "held-ranges",
95
+
106
96
  /**
107
- * Start fetching the region a viewer is about to resume at, named in seconds
108
- * and turned into bytes here, where the file's own duration can be read.
97
+ * Bytes of a range the torrent already holds, read from the store and never
98
+ * fetched. The ordinary range read declares demand and steers the swarm,
99
+ * which is right for a viewer waiting on a segment and wrong for a walk that
100
+ * must pull nothing.
109
101
  */
110
- WARM_POSITION: "warm-position",
111
- /** Cues of one subtitle track, from the clusters already downloaded. */
112
- SUBTITLE_CUES: "subtitle-cues",
102
+ READ_HELD: "read-held",
103
+
113
104
  /** Shut the client down, optionally deleting downloaded data. */
114
105
  /**
115
106
  * How much disk the spilled pieces may take between them.
@@ -120,6 +111,8 @@ export const Command = {
120
111
  * divides its share between the stores it holds.
121
112
  */
122
113
  SPILL_ALLOWANCE: "spill-allowance",
114
+ WHOLE_FILES_ALLOWANCE: "whole-files-allowance",
115
+ MEMORY_ALLOWANCE: "memory-allowance",
123
116
  DESTROY_ALL: "destroy-all"
124
117
  };
125
118
 
@@ -155,12 +148,15 @@ export const Event = {
155
148
  /** A log line, so worker output reaches the same place as everything else. */
156
149
  LOG: "log",
157
150
  /**
158
- * New subtitle cues were read for one track, unprompted — the worker found
159
- * them off its own `verified`-piece walk, not in answer to a
160
- * {@link Command.SUBTITLE_CUES} call. Lets the main thread PUSH them to
161
- * whichever browser is watching instead of waiting to be asked.
151
+ * Pieces of these files have arrived.
152
+ *
153
+ * ANNOUNCED, not asked: the torrent is the only thing that knows a piece just
154
+ * verified, and what anybody does about it — walk a subtitle track's new
155
+ * clusters, say — is no business of this thread. It used to walk them itself,
156
+ * which put the cue reading in the thread that owns the swarm.
162
157
  */
163
- SUBTITLE_CUES_READY: "subtitle-cues-ready",
158
+ PIECES_ARRIVED: "pieces-arrived",
159
+
164
160
  /**
165
161
  * A file has been downloaded whole and written out as a file.
166
162
  *
@@ -0,0 +1,107 @@
1
+ /**
2
+ * @file Fetching the region under a viewer's resume position.
3
+ *
4
+ * All that is left of a file that used to read containers here as well. The
5
+ * parsing moved to the main thread on 2026-09-15: a container is built from one
6
+ * function, `readRange(start, end)`, the pieces behind it live in shared memory,
7
+ * and the reason it had been put here — "the main thread cannot open a read
8
+ * stream on one of its files" — was true of WebTorrent's own API and not of the
9
+ * bytes. Three commands, a second `ContainerOrchestrator` in this thread, and
10
+ * every answer carried back over the channel went with it.
11
+ *
12
+ * What stays is the part that really is the torrent's: turning a position in
13
+ * seconds into a byte offset and pulling that region off the swarm. How long
14
+ * the file runs is TOLD to it, because that is what the file states about
15
+ * itself and this thread no longer reads such things.
16
+ */
17
+
18
+ import { logger } from "../../../utils/logger.js";
19
+
20
+
21
+
22
+
23
+
24
+
25
+
26
+
27
+
28
+
29
+ /**
30
+ * How much of the file to pull in under the viewer's resume position.
31
+ *
32
+ * One piece of a video torrent is 4-16 MB and a resume lands anywhere inside
33
+ * one, so anything smaller would still leave the encoder waiting for the piece
34
+ * it starts in. Eight megabytes covers that piece and usually the next.
35
+ */
36
+ const RESUME_REGION_BYTES = 8 * 1024 * 1024;
37
+
38
+ /**
39
+ * Where a position in seconds falls in a file, in bytes.
40
+ *
41
+ * Proportional, and therefore approximate on a variable bitrate — which is what
42
+ * it is for: a prefetch that puts the swarm to work on roughly the right place
43
+ * while the plan and the session are still being built. The encoder's own read
44
+ * asks for the exact bytes a moment later and corrects it.
45
+ *
46
+ * A position past the end is clamped to the end rather than refused: a resume
47
+ * position can outlive the file it was recorded against, and reading the last
48
+ * bytes is harmless where reading past them is an error.
49
+ *
50
+ * @param {number} fileLength
51
+ * @param {number} durationSeconds
52
+ * @param {number} positionSeconds
53
+ * @returns {number}
54
+ */
55
+ export function resumeByteOffset(fileLength, durationSeconds, positionSeconds) {
56
+ if (!(fileLength > 0) || !(durationSeconds > 0) || !(positionSeconds > 0)) {
57
+ return 0;
58
+ }
59
+ const within = Math.min(positionSeconds, durationSeconds);
60
+ return Math.min(fileLength - 1, Math.floor((fileLength * within) / durationSeconds));
61
+ }
62
+
63
+ /**
64
+ * Start fetching the region a viewer is about to resume at.
65
+ *
66
+ * Where that region IS can only be worked out from two numbers the file itself
67
+ * holds — its length and its duration — so this belongs beside the container
68
+ * read rather than in the route: the route knows a position in seconds and
69
+ * nothing else. The conversion is proportional and therefore approximate on a
70
+ * variable bitrate; it is a prefetch, and the encoder's own read corrects it.
71
+ *
72
+ * @param {object} torrent
73
+ * @param {number} fileIndex
74
+ * @param {string} sourceKey
75
+ * @param {number} positionSeconds
76
+ * @param {{ prefetchEdges?: () => Promise<unknown>, fetchRegion?: (start: number, bytes: number) => Promise<unknown> }} options
77
+ * @returns {Promise<boolean>} Whether a region was asked for.
78
+ */
79
+ export async function warmResumePosition(torrent, fileIndex, sourceKey, positionSeconds, options = {}) {
80
+ const file = torrent?.files?.[fileIndex];
81
+ if (!file || !(positionSeconds > 0) || typeof options.fetchRegion !== "function") {
82
+ return false;
83
+ }
84
+ // TOLD, not worked out here. How long a file runs is what the file states
85
+ // about itself, and that is read on the main thread now; this one turns a
86
+ // position into a byte offset and fetches the region under it, which is all
87
+ // of the job that is actually the torrent's.
88
+ const duration = Number(options.durationSeconds);
89
+ if (!Number.isFinite(duration) || duration <= 0) {
90
+ logger.info(
91
+ `warm ${sourceKey.slice(0, 8)}: "${String(file.name).slice(0, 40)}" does not declare its ` +
92
+ "duration, so where the viewer's position falls in it cannot be worked out — " +
93
+ "the region under it is left to the encoder's own read"
94
+ );
95
+ return false;
96
+ }
97
+ const at = resumeByteOffset(file.length, duration, positionSeconds);
98
+ logger.info(
99
+ `warm ${sourceKey.slice(0, 8)}: fetching ${(RESUME_REGION_BYTES / (1024 * 1024)).toFixed(0)}MB under the ` +
100
+ `viewer's position ${positionSeconds.toFixed(1)}s of ${duration.toFixed(1)}s, which is ` +
101
+ `${(at / (1024 * 1024)).toFixed(1)}MB into "${String(file.name).slice(0, 40)}"`
102
+ );
103
+ await options.fetchRegion(at, RESUME_REGION_BYTES);
104
+ return true;
105
+ }
106
+
107
+
@@ -9,7 +9,7 @@
9
9
  * does not help. The upstream issue about workers (#129) was closed in 0.4.0
10
10
  * but only covers use from a worker ALONE, which does work.
11
11
  *
12
- * We need it twice: `webrtc-manager.js` runs the video channel to the browser on
12
+ * We need it twice: `transport/webrtc-manager.js` runs the video channel to the browser on
13
13
  * the main thread, and the torrent client — which is now on its own thread —
14
14
  * creates peer connections of its own to announce on `wss://` trackers. Before
15
15
  * the thread split both lived in one isolate and nothing was wrong; afterwards
@@ -17,7 +17,7 @@
17
17
  *
18
18
  * So the native stack stays where it earns its keep — the main thread, carrying
19
19
  * video — and the torrent's trackers get a JavaScript implementation, where the
20
- * traffic is a handful of signalling messages. `services/torrent-worker/worker.js`
20
+ * traffic is a handful of signalling messages. `services/torrent/worker/worker.js`
21
21
  * installs a module resolution hook that points `webrtc-polyfill` here; nothing
22
22
  * outside the worker thread is affected, and no dependency is patched (the addon
23
23
  * installs with `--ignore-scripts`, so a postinstall patch would never run).