@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
@@ -9,7 +9,7 @@
9
9
  import assert from "node:assert/strict";
10
10
  import test from "node:test";
11
11
 
12
- import { availableShareFrom, correctForAvailability } from "../services/available-share.js";
12
+ import { availableShareFrom, correctForAvailability } from "../services/encode/available-share.js";
13
13
 
14
14
  test("only the work nobody has been charged for is subtracted", () => {
15
15
  // The field reading: the box is saturated, our encoder has more than half of
@@ -11,7 +11,7 @@
11
11
  import assert from "node:assert/strict";
12
12
  import test from "node:test";
13
13
  import { EventEmitter } from "node:events";
14
- import { fillFileInBackground, fillIsRunning } from "../services/torrent-worker/background-fill.js";
14
+ import { fillFileInBackground, fillIsRunning } from "../services/torrent/worker/background-fill.js";
15
15
 
16
16
  /**
17
17
  * A file whose reads resolve when the test says so, recording what was asked
@@ -1,110 +1,110 @@
1
- /**
2
- * @file The browser's log, written beside the proxy's.
3
- *
4
- * What these hold is the naming, because the name is the whole point: the two
5
- * halves of a session have to join without guessing, and a torrent name is
6
- * arbitrary bytes chosen by a stranger that is about to become a path.
7
- */
8
-
9
- import test from "node:test";
10
- import assert from "node:assert/strict";
11
- import { mkdtempSync, readdirSync, readFileSync } from "node:fs";
12
- import { tmpdir } from "node:os";
13
- import { join } from "node:path";
14
-
15
- import { createClientLogFiles } from "../utils/client-log-file.js";
16
-
17
- /** A directory with a proxy log in it, which is what the sink is given. */
18
- function aDirectory() {
19
- const dir = mkdtempSync(join(tmpdir(), "ttv-client-log-"));
20
- return { dir, proxyLog: join(dir, "proxy.log") };
21
- }
22
-
23
- const aSession = (over = {}) => ({
24
- sessionId: "abcd1234",
25
- startedAt: "2026-09-13T16:05:24.123Z",
26
- torrentName: "Reacher.S04E07.1080p.rus.LostFilm.TV.mkv",
27
- infoHash: "94dda59fdf252447d6ca5a8775dab2d11a832da5",
28
- ...over
29
- });
30
-
31
- test("the file lands beside the proxy log, named by when it started and what is watched", async () => {
32
- const { dir, proxyLog } = aDirectory();
33
- const logs = createClientLogFiles(proxyLog);
34
- logs.write(aSession(), ["first line"]);
35
- await logs.close();
36
-
37
- const [name] = readdirSync(dir).filter((f) => f.startsWith("client-"));
38
- assert.ok(name, "a client log file was written");
39
- // The stamp is UTC and sorts; the session id joins it to the proxy's lines;
40
- // the film says which one it is without opening the file.
41
- assert.match(name, /^client-20260913-160524-abcd1234-Reacher\.S04E07\.1080p\.rus\.LostFilm\.TV\.mkv-94dda59f\.log$/);
42
- assert.equal(readFileSync(join(dir, name), "utf8"), "first line\n");
43
- });
44
-
45
- test("lines from one session go on appending to the one file", async () => {
46
- const { dir, proxyLog } = aDirectory();
47
- const logs = createClientLogFiles(proxyLog);
48
- logs.write(aSession(), ["one", "two"]);
49
- logs.write(aSession(), ["three"]);
50
- await logs.close();
51
-
52
- const names = readdirSync(dir).filter((f) => f.startsWith("client-"));
53
- assert.equal(names.length, 1);
54
- assert.equal(readFileSync(join(dir, names[0]), "utf8"), "one\ntwo\nthree\n");
55
- });
56
-
57
- test("a session that has not chosen a film yet still gets its own file", async () => {
58
- // The page opening, a proxy being chosen, a connection failing — all before
59
- // any torrent exists, and all worth keeping.
60
- const { dir, proxyLog } = aDirectory();
61
- const logs = createClientLogFiles(proxyLog);
62
- logs.write(aSession({ torrentName: "", infoHash: "" }), ["before any film"]);
63
- await logs.close();
64
-
65
- const [name] = readdirSync(dir).filter((f) => f.startsWith("client-"));
66
- assert.match(name, /-no-torrent-yet\.log$/);
67
- });
68
-
69
- test("the file keeps its name when the film arrives mid-session", async () => {
70
- // Keyed by the session, not by the name: the earlier lines belong to the same
71
- // viewing and must not start a second file.
72
- const { dir, proxyLog } = aDirectory();
73
- const logs = createClientLogFiles(proxyLog);
74
- logs.write(aSession({ torrentName: "", infoHash: "" }), ["before"]);
75
- logs.write(aSession(), ["after"]);
76
- await logs.close();
77
-
78
- const names = readdirSync(dir).filter((f) => f.startsWith("client-"));
79
- assert.equal(names.length, 1);
80
- assert.match(names[0], /-no-torrent-yet\.log$/);
81
- assert.equal(readFileSync(join(dir, names[0]), "utf8"), "before\nafter\n");
82
- });
83
-
84
- test("a hostile torrent name cannot escape the directory", async () => {
85
- const { dir, proxyLog } = aDirectory();
86
- const logs = createClientLogFiles(proxyLog);
87
- logs.write(aSession({ torrentName: "../../etc/passwd" }), ["x"]);
88
- await logs.close();
89
-
90
- const names = readdirSync(dir).filter((f) => f.startsWith("client-"));
91
- assert.equal(names.length, 1);
92
- assert.ok(!names[0].includes(".."), `no traversal in ${names[0]}`);
93
- assert.ok(!names[0].includes("/"), `no separator in ${names[0]}`);
94
- });
95
-
96
- test("two sessions of one film are two files", async () => {
97
- const { dir, proxyLog } = aDirectory();
98
- const logs = createClientLogFiles(proxyLog);
99
- logs.write(aSession({ sessionId: "aaaa1111" }), ["one viewer"]);
100
- logs.write(aSession({ sessionId: "bbbb2222", startedAt: "2026-09-13T16:07:11.000Z" }), ["another"]);
101
- await logs.close();
102
-
103
- assert.equal(readdirSync(dir).filter((f) => f.startsWith("client-")).length, 2);
104
- });
105
-
106
- test("no proxy log path means nothing is written and nothing throws", async () => {
107
- const logs = createClientLogFiles("");
108
- logs.write(aSession(), ["dropped"]);
109
- await logs.close();
110
- });
1
+ /**
2
+ * @file The browser's log, written beside the proxy's.
3
+ *
4
+ * What these hold is the naming, because the name is the whole point: the two
5
+ * halves of a session have to join without guessing, and a torrent name is
6
+ * arbitrary bytes chosen by a stranger that is about to become a path.
7
+ */
8
+
9
+ import test from "node:test";
10
+ import assert from "node:assert/strict";
11
+ import { mkdtempSync, readdirSync, readFileSync } from "node:fs";
12
+ import { tmpdir } from "node:os";
13
+ import { join } from "node:path";
14
+
15
+ import { createClientLogFiles } from "../utils/client-log-file.js";
16
+
17
+ /** A directory with a proxy log in it, which is what the sink is given. */
18
+ function aDirectory() {
19
+ const dir = mkdtempSync(join(tmpdir(), "ttv-client-log-"));
20
+ return { dir, proxyLog: join(dir, "proxy.log") };
21
+ }
22
+
23
+ const aSession = (over = {}) => ({
24
+ sessionId: "abcd1234",
25
+ startedAt: "2026-09-13T16:05:24.123Z",
26
+ torrentName: "Reacher.S04E07.1080p.rus.LostFilm.TV.mkv",
27
+ infoHash: "94dda59fdf252447d6ca5a8775dab2d11a832da5",
28
+ ...over
29
+ });
30
+
31
+ test("the file lands beside the proxy log, named by when it started and what is watched", async () => {
32
+ const { dir, proxyLog } = aDirectory();
33
+ const logs = createClientLogFiles(proxyLog);
34
+ logs.write(aSession(), ["first line"]);
35
+ await logs.close();
36
+
37
+ const [name] = readdirSync(dir).filter((f) => f.startsWith("client-"));
38
+ assert.ok(name, "a client log file was written");
39
+ // The stamp is UTC and sorts; the session id joins it to the proxy's lines;
40
+ // the film says which one it is without opening the file.
41
+ assert.match(name, /^client-20260913-160524-abcd1234-Reacher\.S04E07\.1080p\.rus\.LostFilm\.TV\.mkv-94dda59f\.log$/);
42
+ assert.equal(readFileSync(join(dir, name), "utf8"), "first line\n");
43
+ });
44
+
45
+ test("lines from one session go on appending to the one file", async () => {
46
+ const { dir, proxyLog } = aDirectory();
47
+ const logs = createClientLogFiles(proxyLog);
48
+ logs.write(aSession(), ["one", "two"]);
49
+ logs.write(aSession(), ["three"]);
50
+ await logs.close();
51
+
52
+ const names = readdirSync(dir).filter((f) => f.startsWith("client-"));
53
+ assert.equal(names.length, 1);
54
+ assert.equal(readFileSync(join(dir, names[0]), "utf8"), "one\ntwo\nthree\n");
55
+ });
56
+
57
+ test("a session that has not chosen a film yet still gets its own file", async () => {
58
+ // The page opening, a proxy being chosen, a connection failing — all before
59
+ // any torrent exists, and all worth keeping.
60
+ const { dir, proxyLog } = aDirectory();
61
+ const logs = createClientLogFiles(proxyLog);
62
+ logs.write(aSession({ torrentName: "", infoHash: "" }), ["before any film"]);
63
+ await logs.close();
64
+
65
+ const [name] = readdirSync(dir).filter((f) => f.startsWith("client-"));
66
+ assert.match(name, /-no-torrent-yet\.log$/);
67
+ });
68
+
69
+ test("the file keeps its name when the film arrives mid-session", async () => {
70
+ // Keyed by the session, not by the name: the earlier lines belong to the same
71
+ // viewing and must not start a second file.
72
+ const { dir, proxyLog } = aDirectory();
73
+ const logs = createClientLogFiles(proxyLog);
74
+ logs.write(aSession({ torrentName: "", infoHash: "" }), ["before"]);
75
+ logs.write(aSession(), ["after"]);
76
+ await logs.close();
77
+
78
+ const names = readdirSync(dir).filter((f) => f.startsWith("client-"));
79
+ assert.equal(names.length, 1);
80
+ assert.match(names[0], /-no-torrent-yet\.log$/);
81
+ assert.equal(readFileSync(join(dir, names[0]), "utf8"), "before\nafter\n");
82
+ });
83
+
84
+ test("a hostile torrent name cannot escape the directory", async () => {
85
+ const { dir, proxyLog } = aDirectory();
86
+ const logs = createClientLogFiles(proxyLog);
87
+ logs.write(aSession({ torrentName: "../../etc/passwd" }), ["x"]);
88
+ await logs.close();
89
+
90
+ const names = readdirSync(dir).filter((f) => f.startsWith("client-"));
91
+ assert.equal(names.length, 1);
92
+ assert.ok(!names[0].includes(".."), `no traversal in ${names[0]}`);
93
+ assert.ok(!names[0].includes("/"), `no separator in ${names[0]}`);
94
+ });
95
+
96
+ test("two sessions of one film are two files", async () => {
97
+ const { dir, proxyLog } = aDirectory();
98
+ const logs = createClientLogFiles(proxyLog);
99
+ logs.write(aSession({ sessionId: "aaaa1111" }), ["one viewer"]);
100
+ logs.write(aSession({ sessionId: "bbbb2222", startedAt: "2026-09-13T16:07:11.000Z" }), ["another"]);
101
+ await logs.close();
102
+
103
+ assert.equal(readdirSync(dir).filter((f) => f.startsWith("client-")).length, 2);
104
+ });
105
+
106
+ test("no proxy log path means nothing is written and nothing throws", async () => {
107
+ const logs = createClientLogFiles("");
108
+ logs.write(aSession(), ["dropped"]);
109
+ await logs.close();
110
+ });
@@ -13,7 +13,7 @@ import fs from "node:fs/promises";
13
13
  import os from "node:os";
14
14
  import path from "node:path";
15
15
  import { Readable } from "node:stream";
16
- import { CompletedFiles } from "../services/files/CompletedFiles.js";
16
+ import { CompletedFiles } from "../services/storage/files/CompletedFiles.js";
17
17
 
18
18
  const INFO_HASH = "abcdef0123456789abcdef0123456789abcdef01";
19
19
 
@@ -11,7 +11,7 @@
11
11
  import assert from "node:assert/strict";
12
12
  import test from "node:test";
13
13
 
14
- import { canSustainOutput } from "../services/hwaccel.js";
14
+ import { canSustainOutput } from "../services/encode/hwaccel.js";
15
15
 
16
16
  // A host that encodes 640x360 at 24 fps about eleven times over, and decodes
17
17
  // 1080p24 at 2.6x — the addon host's own figures.
@@ -59,9 +59,9 @@ test("nothing else running leaves the answer exactly as it was", () => {
59
59
  assert.equal(withZero.speed, without.speed);
60
60
  });
61
61
 
62
- test("a host with nothing measured still refuses nothing", () => {
62
+ test("a wiring made without the startup calibration judges nothing", () => {
63
63
  const answer = canSustainOutput({
64
- benchmark: [],
64
+ benchmark: null,
65
65
  outputPixelsPerSec: RUNG_240P,
66
66
  concurrentCostSec: 0.125
67
67
  });
@@ -69,6 +69,16 @@ test("a host with nothing measured still refuses nothing", () => {
69
69
  assert.equal(answer.speed, null);
70
70
  });
71
71
 
72
+ test("a host calibrated with no mode qualified refuses a re-encode (roadmap item 97, step 14)", () => {
73
+ const answer = canSustainOutput({
74
+ benchmark: [],
75
+ outputPixelsPerSec: RUNG_240P,
76
+ concurrentCostSec: 0.125
77
+ });
78
+ assert.equal(answer.sustainable, false, "unknown capacity is not capacity");
79
+ assert.equal(answer.speed, null);
80
+ });
81
+
72
82
  test("a cost heavy enough to fill the machine puts a rung below realtime", () => {
73
83
  const answer = canSustainOutput({
74
84
  benchmark: BENCHMARK,
@@ -17,9 +17,9 @@
17
17
  import assert from "node:assert/strict";
18
18
  import test from "node:test";
19
19
 
20
- import { MatroskaContainer } from "../services/container/MatroskaContainer.js";
21
- import { Mp4Container } from "../services/container/Mp4Container.js";
22
- import { AviContainer } from "../services/container/AviContainer.js";
20
+ import { MatroskaContainer } from "../services/media/container/MatroskaContainer.js";
21
+ import { Mp4Container } from "../services/media/container/Mp4Container.js";
22
+ import { AviContainer } from "../services/media/container/AviContainer.js";
23
23
 
24
24
  /**
25
25
  * An EBML element: its id bytes, a four-byte size, then the payload.
@@ -9,7 +9,7 @@
9
9
  import assert from "node:assert/strict";
10
10
  import test from "node:test";
11
11
 
12
- import { dumpsToRemove, isCoreDump } from "../services/core-dumps.js";
12
+ import { dumpsToRemove, isCoreDump } from "../services/storage/core-dumps.js";
13
13
 
14
14
  const dumps = [
15
15
  { name: "core.WorkerThread.81.1787176646", writtenAt: 1787176646000 },
@@ -29,10 +29,10 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
29
29
  import os from "node:os";
30
30
  import path from "node:path";
31
31
  import { EncodeRun } from "../services/encode/EncodeRun.js";
32
- import { SegmentStore } from "../services/encode/SegmentStore.js";
32
+ import { SegmentStore } from "../services/storage/segment-store/SegmentStore.js";
33
33
  import { SoftwareEncoder } from "../services/encode/SoftwareEncoder.js";
34
- import { EncodeOrchestrator } from "../services/orchestrators/EncodeOrchestrator.js";
35
- import { fmp4Format } from "../services/segment-formats/fmp4.js";
34
+ import { EncodeOrchestrator } from "../services/encode/EncodeOrchestrator.js";
35
+ import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
36
36
 
37
37
  const PICTURE = "torrent:abc:fmt=fmp4:grid=kf@0:video-only:v=0/copy";
38
38
  const SEGMENTS = 482;
@@ -70,7 +70,12 @@ function orchestratorWithAStore() {
70
70
  firstByteWaitSec: 0.12,
71
71
  refetchSecPerFilmSecond: () => 0.25,
72
72
  startingSpeedFor: () => 2,
73
- segmentStore: store,
73
+ segmentCoverage: {
74
+ provenNumbers: (address) => store.provenNumbers(address),
75
+ announce: (address, index) => store.announce(address, index),
76
+ filesHeld: (address) => store.filesHeld(address),
77
+ clearUpAfter: (address, startedAt) => store.clearUpAfter(address, startedAt)
78
+ },
74
79
  now: () => 1000,
75
80
  logger,
76
81
  makeRun: ({ address, from, to }) => new EncodeRun({
@@ -114,7 +119,15 @@ function theWholeFilmIsOnDisk(store, dir) {
114
119
 
115
120
  test("a film that is entirely made needs no encoder", (t) => {
116
121
  const { made, store, dir, root } = orchestratorWithAStore();
117
- t.after(() => rmSync(root, { recursive: true, force: true }));
122
+ t.after(() => {
123
+ // THE STORE LETS ITS DIRECTORIES GO, and only then is the root removed. It
124
+ // holds a watch on each of them — that is how a piece closed by a muxer
125
+ // with no channel of its own becomes an event — and a directory with an
126
+ // open watch on it cannot be removed on Windows: `rmSync` retries until it
127
+ // gives up, and the check reads as a hang.
128
+ store.dropAll("the check is over");
129
+ rmSync(root, { recursive: true, force: true });
130
+ });
118
131
 
119
132
  theWholeFilmIsOnDisk(store, dir);
120
133
  aViewerAtTheStart(made);
@@ -128,7 +141,15 @@ test("a film that is entirely made needs no encoder", (t) => {
128
141
 
129
142
  test("segments the store has lost stop being ready, and an encoder is placed again", (t) => {
130
143
  const { made, store, dir, root } = orchestratorWithAStore();
131
- t.after(() => rmSync(root, { recursive: true, force: true }));
144
+ t.after(() => {
145
+ // THE STORE LETS ITS DIRECTORIES GO, and only then is the root removed. It
146
+ // holds a watch on each of them — that is how a piece closed by a muxer
147
+ // with no channel of its own becomes an event — and a directory with an
148
+ // open watch on it cannot be removed on Windows: `rmSync` retries until it
149
+ // gives up, and the check reads as a hang.
150
+ store.dropAll("the check is over");
151
+ rmSync(root, { recursive: true, force: true });
152
+ });
132
153
 
133
154
  theWholeFilmIsOnDisk(store, dir);
134
155
  aViewerAtTheStart(made);
@@ -151,7 +172,15 @@ test("segments the store has lost stop being ready, and an encoder is placed aga
151
172
 
152
173
  test("a piece discarded with the run that had it open is not ready either", (t) => {
153
174
  const { made, store, dir, root } = orchestratorWithAStore();
154
- t.after(() => rmSync(root, { recursive: true, force: true }));
175
+ t.after(() => {
176
+ // THE STORE LETS ITS DIRECTORIES GO, and only then is the root removed. It
177
+ // holds a watch on each of them — that is how a piece closed by a muxer
178
+ // with no channel of its own becomes an event — and a directory with an
179
+ // open watch on it cannot be removed on Windows: `rmSync` retries until it
180
+ // gives up, and the check reads as a hang.
181
+ store.dropAll("the check is over");
182
+ rmSync(root, { recursive: true, force: true });
183
+ });
155
184
 
156
185
  theWholeFilmIsOnDisk(store, dir);
157
186
  aViewerAtTheStart(made);
@@ -169,8 +198,16 @@ test("a piece discarded with the run that had it open is not ready either", (t)
169
198
  });
170
199
 
171
200
  test("nobody making what somebody waits for is said out loud", (t) => {
172
- const { made, root, lines } = orchestratorWithAStore();
173
- t.after(() => rmSync(root, { recursive: true, force: true }));
201
+ const { made, store, root, lines } = orchestratorWithAStore();
202
+ t.after(() => {
203
+ // THE STORE LETS ITS DIRECTORIES GO, and only then is the root removed. It
204
+ // holds a watch on each of them — that is how a piece closed by a muxer
205
+ // with no channel of its own becomes an event — and a directory with an
206
+ // open watch on it cannot be removed on Windows: `rmSync` retries until it
207
+ // gives up, and the check reads as a hang.
208
+ store.dropAll("the check is over");
209
+ rmSync(root, { recursive: true, force: true });
210
+ });
174
211
 
175
212
  // A HOST THAT CANNOT BUILD AN ENCODER. Whatever the reason — and there are
176
213
  // several — the state it leaves is the one that cost two sessions on
@@ -0,0 +1,58 @@
1
+ /**
2
+ * @file What the subtitle walk found reaches the viewer whole.
3
+ *
4
+ * It did not. The walk has produced `detectedLanguage` on every push since
5
+ * 2.69.0 — the language read from the CUES rather than from a container that
6
+ * often states none — and the whole of the "label moves when the answer
7
+ * arrives" behaviour rests on it. It never arrived: the fact was copied by hand
8
+ * twice on its way out, and each copy listed the fields it knew about. The
9
+ * worker posted `trackIndex, cues, language, cursor`; the main thread's client
10
+ * rebuilt the same four from the message. `detectedLanguage` was in neither
11
+ * list, so the browser was told nothing and the label it shows for an untagged
12
+ * track has never moved.
13
+ *
14
+ * It is not a typo to fix in two places — it is the shape. A fact copied field
15
+ * by field loses whatever the copier did not know about, and says nothing when
16
+ * it does. The walk's own result travels now, so a field added to it arrives
17
+ * without anybody being told to forward it.
18
+ *
19
+ * (Both copies are gone anyway: the walk runs on the thread that pushes.)
20
+ */
21
+
22
+ import test from "node:test";
23
+ import assert from "node:assert/strict";
24
+ import { readFileSync } from "node:fs";
25
+ import path from "node:path";
26
+ import { fileURLToPath } from "node:url";
27
+
28
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
29
+
30
+ /**
31
+ * @param {string} relative
32
+ * @returns {string}
33
+ */
34
+ function source(relative) {
35
+ return readFileSync(path.join(ROOT, relative), "utf8");
36
+ }
37
+
38
+ test("the walk states the language it read from the cues", () => {
39
+ // The producer. Without this the rest of the check is about nothing.
40
+ assert.match(source("services/media/SubtitleCues.js"), /detectedLanguage: detectLanguage\(/);
41
+ });
42
+
43
+ test("the consumer asks for it", () => {
44
+ // The other end has always destructured it, which is what made the loss
45
+ // silent: a field nobody forwards is `undefined` and reads as "not detected".
46
+ assert.match(
47
+ source("services/transport/data-channel-handler.js"),
48
+ /function publishSubtitleCues\(\{[^}]*detectedLanguage[^}]*\}\)/
49
+ );
50
+ });
51
+
52
+ test("the push forwards the whole of what the walk found, rather than a list of fields", () => {
53
+ // The one line that closes it. A list of names here is how the fact was lost
54
+ // twice, and a list cannot say that it is incomplete.
55
+ const wiring = source("server.js");
56
+
57
+ assert.match(wiring, /onSubtitleCues\?\.\(\{ sourceKey, fileIndex, \.\.\.entry \}\)/);
58
+ });
@@ -13,10 +13,10 @@
13
13
  */
14
14
 
15
15
  import assert from "node:assert/strict";
16
- import { Timeline } from "../services/output/Timeline.js";
16
+ import { Timeline } from "../services/encode/output/Timeline.js";
17
17
  import test from "node:test";
18
18
 
19
- import { onKeyframeGridFor } from "../services/hls-session-manager.js";
19
+ import { onKeyframeGridFor } from "../services/encode/run-command.js";
20
20
 
21
21
  test("a copied picture works on the source's timeline", () => {
22
22
  assert.equal(
@@ -15,11 +15,12 @@
15
15
  */
16
16
 
17
17
  import assert from "node:assert/strict";
18
- import { computeCutGrid } from "../services/output/cut-grid.js";
19
- import { Timeline } from "../services/output/Timeline.js";
18
+ import { computeCutGrid } from "../services/encode/output/cut-grid.js";
19
+ import { Timeline } from "../services/encode/output/Timeline.js";
20
20
  import test from "node:test";
21
21
 
22
- import { HlsSessionManager, segmentCutTimesFrom } from "../services/hls-session-manager.js";
22
+ import { wireOutputs } from "../services/server/wire-outputs.js";
23
+ import { segmentCutTimesFrom } from "../services/encode/run-command.js";
23
24
 
24
25
  /** What the playlist in the player's hands says. */
25
26
  const PUBLISHED = [0, 4.004, 8.008, 12.012, 16.016, 20.02];
@@ -29,10 +30,10 @@ const CORRECTED = [0, 4.004, 6.006, 12.012, 14.014, 20.02];
29
30
  /**
30
31
  * A session that has published one grid and since corrected another.
31
32
  *
32
- * @returns {{ manager: HlsSessionManager, session: object }}
33
+ * @returns {{ manager: object, session: object }}
33
34
  */
34
35
  function sessionWithDriftedGrid() {
35
- const manager = new HlsSessionManager({
36
+ const manager = wireOutputs({
36
37
  enabled: true,
37
38
  ffmpegBin: "ffmpeg",
38
39
  localBindHost: "127.0.0.1",
@@ -47,7 +48,7 @@ function sessionWithDriftedGrid() {
47
48
 
48
49
  test("the cut list is the one the playlist was written from", () => {
49
50
  const { manager, session } = sessionWithDriftedGrid();
50
- const grid = manager.publishedGridFor(session);
51
+ const grid = manager.outputTimes.publishedGridFor(session);
51
52
  assert.deepEqual(grid, PUBLISHED);
52
53
  // Interior cuts of a run starting at #1, rebased on the run's own start
53
54
  // (4.004 s), and stopping before the last entry, which is the file's end.
@@ -60,7 +61,7 @@ test("the cut list is the one the playlist was written from", () => {
60
61
  test("a corrected grid does not move the cuts of a session already being read", () => {
61
62
  const { manager, session } = sessionWithDriftedGrid();
62
63
  const fromCorrected = segmentCutTimesFrom(session.timeline.boundaries, 1);
63
- const fromPublished = segmentCutTimesFrom(manager.publishedGridFor(session), 1);
64
+ const fromPublished = segmentCutTimesFrom(manager.outputTimes.publishedGridFor(session), 1);
64
65
  assert.notDeepEqual(fromCorrected, fromPublished);
65
66
  // The gap the field measured: the corrected table would have cut #2 two
66
67
  // seconds early, which is four times what a player bridges.
@@ -70,7 +71,7 @@ test("a corrected grid does not move the cuts of a session already being read",
70
71
  test("a session that published no grid falls back to the live one", () => {
71
72
  const { manager } = sessionWithDriftedGrid();
72
73
  const session = { id: "no-playlist", timeline: new Timeline({ boundaries: [...CORRECTED], published: [], cutGrid: "uniform" }) };
73
- assert.deepEqual(manager.publishedGridFor(session), CORRECTED);
74
+ assert.deepEqual(manager.outputTimes.publishedGridFor(session), CORRECTED);
74
75
  });
75
76
 
76
77
  test("no segment is left shorter than a segment at the end of the film", () => {
@@ -10,7 +10,7 @@
10
10
 
11
11
  import test from "node:test";
12
12
  import assert from "node:assert/strict";
13
- import { encodeFrame } from "../services/data-channel-handler.js";
13
+ import { encodeFrame } from "../services/transport/data-channel-handler.js";
14
14
 
15
15
  const requestId = Buffer.from("abc123", "ascii");
16
16
 
@@ -19,7 +19,7 @@
19
19
 
20
20
  import test from "node:test";
21
21
  import assert from "node:assert/strict";
22
- import { isUsableTorrentHandle } from "../services/torrent-worker/handle-state.js";
22
+ import { isUsableTorrentHandle } from "../services/torrent/worker/handle-state.js";
23
23
 
24
24
  test("a live torrent is usable", () => {
25
25
  assert.equal(isUsableTorrentHandle({ destroyed: false, files: [{ name: "a.mkv" }] }), true);
@@ -10,8 +10,8 @@
10
10
 
11
11
  import test from "node:test";
12
12
  import assert from "node:assert/strict";
13
- import { declaredRates } from "../services/output/rates.js";
14
- import { bitrateFor } from "../services/output/playlists.js";
13
+ import { declaredRates } from "../services/encode/output/rates.js";
14
+ import { bitrateFor } from "../services/encode/output/playlists.js";
15
15
 
16
16
  // The field file: 2 806 246 976 bytes over 20:18, cut into 291 pieces.
17
17
  const FIELD = { fileLength: 2_806_246_976, durationSeconds: 1218.73 };
@@ -16,8 +16,8 @@ import test from "node:test";
16
16
  import assert from "node:assert/strict";
17
17
  import { readFileSync } from "node:fs";
18
18
 
19
- const planner = readFileSync(new URL("../services/playback-planner.js", import.meta.url), "utf8");
20
- const sessions = readFileSync(new URL("../services/hls-session-manager.js", import.meta.url), "utf8");
19
+ const planner = readFileSync(new URL("../services/media/playback-planner.js", import.meta.url), "utf8");
20
+ const sessions = readFileSync(new URL("../services/encode/Renditions.js", import.meta.url), "utf8");
21
21
 
22
22
  test("every field the declaration reads is a field the cache stores", () => {
23
23
  // What `declaredTracks` reads off the cached media info.
@@ -11,7 +11,7 @@
11
11
  import assert from "node:assert/strict";
12
12
  import test from "node:test";
13
13
 
14
- import { decodeCostOf, fitDecodeCost } from "../services/decode-cost-fit.js";
14
+ import { decodeCostOf, fitDecodeCost } from "../services/encode/decode-cost-fit.js";
15
15
 
16
16
  const FPS = 24;
17
17