@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
@@ -1,209 +1,215 @@
1
- /**
2
- * @file One fact asked at two scopes: what the swarm is asked for, and what the
3
- * encoders are placed by.
4
- *
5
- * Both are right for what asks them. The swarm is asked for bytes of a FILE,
6
- * and the picture, a quality step and a soundtrack of one film read the same
7
- * bytes — so every viewer of any of them wants that file's bytes. Encoders are
8
- * placed per OUTPUT, and a person watching 480p wants nothing of the 1080p
9
- * output at all.
10
- *
11
- * One map for both was the second authority over encoders. Every output of a
12
- * film was handed the whole film's map, so the plan wanted an encoder on every
13
- * one of them; what actually stopped the ones nobody was watching was the
14
- * session manager killing them by its own judgement — and since a viewer moving
15
- * between steps also announces itself, the plan started them again on the next
16
- * pass. Field 2026-09-08.
17
- *
18
- * Built out of the real classes throughout: the real viewer registry holds real
19
- * viewers, the real `LiveOutputs` answers what a film's shape is, and the real
20
- * `PriorityOrchestrator` builds the maps. A session is a plain object in this
21
- * proxy — there is no class for one — so the literals below are the thing
22
- * itself and not a stand-in for it.
23
- */
24
-
25
- import test from "node:test";
26
- import assert from "node:assert/strict";
27
- import { PriorityOrchestrator } from "../services/priority/PriorityOrchestrator.js";
28
- import { LiveOutputs } from "../services/output/LiveOutputs.js";
29
- import { Viewers } from "../services/viewer/Viewers.js";
30
- import { viewersOf } from "../services/viewer/Viewer.js";
31
- import { runsOf } from "../services/priority/PriorityMap.js";
32
-
33
- const FILM = { sourceKey: "source-1", fileIndex: 0, durationSeconds: 600 };
34
- const STALE_AFTER_MS = 60_000;
35
-
36
- /**
37
- * A session, as much of one as these classes read.
38
- *
39
- * @param {{ id: string, outputKey: string, isStep?: boolean, audioOnly?: boolean }} params
40
- * @returns {object}
41
- */
42
- function outputOf({ id, outputKey, isStep = false, audioOnly = false }) {
43
- return {
44
- id,
45
- outputKey,
46
- isStep,
47
- audioOnly,
48
- state: "ready",
49
- sourceKey: FILM.sourceKey,
50
- fileIndex: FILM.fileIndex,
51
- file: { key: "film-1", durationSeconds: FILM.durationSeconds }
52
- };
53
- }
54
-
55
- /**
56
- * The real orchestrator over the real registry, wired the way the session
57
- * manager wires it.
58
- *
59
- * @param {object[]} sessions
60
- * @returns {{ priority: PriorityOrchestrator, viewers: Viewers, publish: () => void }}
61
- */
62
- function over(sessions) {
63
- const live = new LiveOutputs({ sessionsById: new Map(sessions.map((one) => [one.id, one])) });
64
- const viewers = new Viewers();
65
- const priority = new PriorityOrchestrator({
66
- publish: () => {},
67
- viewersOf: (session) => viewersOf(session),
68
- allowanceFor: () => 10,
69
- watchedBy: (session, viewer) => !live.supersededBy(session, viewer.activeVariantId ?? null)
70
- });
71
- return {
72
- priority,
73
- viewers,
74
- live,
75
- publish: () => priority.publishFor({ sessionGroups: [sessions], staleAfterMs: STALE_AFTER_MS })
76
- };
77
- }
78
-
79
- test("a person on a step wants nothing of the picture they stepped off", () => {
80
- const picture = outputOf({ id: "pic", outputKey: "out:1080" });
81
- const step = outputOf({ id: "step", outputKey: "out:480", isStep: true });
82
- const { priority, viewers, publish } = over([picture, step]);
83
- // ONE viewer object per person, referenced from both outputs — which is why
84
- // the step they are on is known when the picture is asked about.
85
- viewers.of(picture, "p");
86
- const person = viewers.of(step, "p");
87
- person.moveTo(300);
88
- person.activeVariantId = "step";
89
- publish();
90
-
91
- assert.ok(
92
- runsOf(priority.mapForOutput("out:480")).length > 0,
93
- "the step they are watching is wanted"
94
- );
95
- assert.deepEqual(
96
- runsOf(priority.mapForOutput("out:1080")),
97
- [],
98
- "and the picture is producing for nobody, which is what makes its encoder unwanted"
99
- );
100
- // The bytes are another matter: both outputs read the same file, and the
101
- // swarm is asked for the file.
102
- assert.ok(
103
- runsOf(priority.mapFor(FILM.sourceKey, FILM.fileIndex)).length > 0,
104
- "the download still wants the film, because that is what is being watched"
105
- );
106
- });
107
-
108
- test("a soundtrack is wanted by whoever is registered on it", () => {
109
- const picture = outputOf({ id: "pic", outputKey: "out:1080" });
110
- const sound = outputOf({ id: "rus", outputKey: "out:a1", audioOnly: true });
111
- const { priority, viewers, publish } = over([picture, sound]);
112
- // Standing on a STEP of the picture says nothing about the sound: the tracks
113
- // nobody chose are let go of where a track is chosen, so being known to a
114
- // soundtrack is listening to it.
115
- const person = viewers.of(sound, "p");
116
- person.moveTo(120);
117
- person.activeVariantId = "step";
118
- publish();
119
-
120
- assert.ok(runsOf(priority.mapForOutput("out:a1")).length > 0);
121
- });
122
-
123
- test("a step being warmed is wanted, and so is the picture still on screen", () => {
124
- // Both are genuinely being produced through a warm-up, which is the price of
125
- // the switch not being visible. Whether the machine can afford two encoders
126
- // is the budget's question and not this one's.
127
- const picture = outputOf({ id: "pic", outputKey: "out:1080" });
128
- const step = outputOf({ id: "step", outputKey: "out:480", isStep: true });
129
- const { priority, viewers, publish } = over([picture, step]);
130
- viewers.of(picture, "p");
131
- const person = viewers.of(step, "p");
132
- person.moveTo(300);
133
- person.warmingVariantId = "step";
134
- publish();
135
-
136
- assert.ok(runsOf(priority.mapForOutput("out:1080")).length > 0, "still on screen");
137
- assert.ok(runsOf(priority.mapForOutput("out:480")).length > 0, "being made ready");
138
- });
139
-
140
- test("an output everybody has left is stated as empty, not left as it was", () => {
141
- // The difference matters: the plan stops the encoders of an output whose map
142
- // is empty, and would keep the map it had when somebody was watching.
143
- const picture = outputOf({ id: "pic", outputKey: "out:1080" });
144
- const { priority, viewers, publish } = over([picture]);
145
- viewers.of(picture, "p").moveTo(300);
146
-
147
- publish();
148
- assert.ok(runsOf(priority.mapForOutput("out:1080")).length > 0);
149
-
150
- viewers.leaves(picture, "p");
151
- publish();
152
- assert.deepEqual(runsOf(priority.mapForOutput("out:1080")), []);
153
- });
154
-
155
- test("a viewer who has gone is not watching anything", () => {
156
- // Presence is the connection and nothing else. Silence — however long — is a
157
- // paused viewer, a throttled hidden tab, or a full cushion, and none of those
158
- // is leaving; what ends it is a statement.
159
- const picture = outputOf({ id: "pic", outputKey: "out:1080" });
160
- const live = new LiveOutputs({ sessionsById: new Map([["pic", picture]]) });
161
- const viewers = new Viewers();
162
- const priority = new PriorityOrchestrator({
163
- publish: () => {},
164
- viewersOf: (session) => viewersOf(session),
165
- allowanceFor: () => 10,
166
- watchedBy: (session, viewer) => !live.supersededBy(session, viewer.activeVariantId ?? null)
167
- });
168
- const person = viewers.of(picture, "p");
169
- person.moveTo(300);
170
- viewers.leaves(picture, "p");
171
-
172
- priority.publishFor({ sessionGroups: [[picture]], now: Date.now() });
173
-
174
- assert.deepEqual(runsOf(priority.mapForOutput("out:1080")), []);
175
- });
176
-
177
- test("a file and an output that are gone are forgotten", () => {
178
- // These are the projection of the live sessions, never a memory of them. Left
179
- // to accumulate they were three maps that only grew, and `forget` was written
180
- // for that and called from nowhere.
181
- const picture = outputOf({ id: "pic", outputKey: "out:1080" });
182
- const { priority, viewers, publish } = over([picture]);
183
- viewers.of(picture, "p").moveTo(300);
184
-
185
- publish();
186
- assert.ok(runsOf(priority.mapForOutput("out:1080")).length > 0);
187
-
188
- priority.publishFor({ sessionGroups: [[]], staleAfterMs: STALE_AFTER_MS });
189
- assert.deepEqual(runsOf(priority.mapForOutput("out:1080")), [], "the output is gone");
190
- assert.deepEqual(
191
- runsOf(priority.mapFor(FILM.sourceKey, FILM.fileIndex)),
192
- [],
193
- "and so is the file"
194
- );
195
- });
196
-
197
- test("the picture is watched by a person who never moved off it", () => {
198
- const picture = outputOf({ id: "pic", outputKey: "out:1080" });
199
- const { viewers, live } = over([picture]);
200
- const person = viewers.of(picture, "p");
201
-
202
- assert.equal(live.supersededBy(picture, null), false, "no step is active");
203
- person.activeVariantId = "pic";
204
- assert.equal(
205
- live.supersededBy(picture, person.activeVariantId),
206
- false,
207
- "and naming the picture itself as the step is the same statement"
208
- );
209
- });
1
+ /**
2
+ * @file One fact asked at two scopes: what the swarm is asked for, and what the
3
+ * encoders are placed by.
4
+ *
5
+ * Both are right for what asks them. The swarm is asked for bytes of a FILE,
6
+ * and the picture, a quality step and a soundtrack of one film read the same
7
+ * bytes — so every viewer of any of them wants that file's bytes. Encoders are
8
+ * placed per OUTPUT, and a person watching 480p wants nothing of the 1080p
9
+ * output at all.
10
+ *
11
+ * One map for both was the second authority over encoders. Every output of a
12
+ * film was handed the whole film's map, so the plan wanted an encoder on every
13
+ * one of them; what actually stopped the ones nobody was watching was the
14
+ * session manager killing them by its own judgement — and since a viewer moving
15
+ * between steps also announces itself, the plan started them again on the next
16
+ * pass. Field 2026-09-08.
17
+ *
18
+ * Built out of the real classes throughout: the real viewer registry holds real
19
+ * viewers, the real `OutputCatalog` answers what a film's shape is, and the real
20
+ * `PriorityOrchestrator` builds the maps. A session is a plain object in this
21
+ * proxy — there is no class for one — so the literals below are the thing
22
+ * itself and not a stand-in for it.
23
+ */
24
+
25
+ import test from "node:test";
26
+ import assert from "node:assert/strict";
27
+ import { PriorityOrchestrator } from "../services/viewer/PriorityOrchestrator.js";
28
+ import { OutputCatalog } from "../services/encode/output/OutputCatalog.js";
29
+ import { Viewers } from "../services/viewer/Viewers.js";
30
+ import { runsOf } from "../services/viewer/PriorityMap.js";
31
+ import { outputSpec } from "./helpers/output-spec.js";
32
+
33
+ const FILM = { sourceKey: "source-1", fileIndex: 0, durationSeconds: 600 };
34
+ const STALE_AFTER_MS = 60_000;
35
+
36
+ /**
37
+ * A session, as much of one as these classes read.
38
+ *
39
+ * @param {{ id: string, outputKey: string, isStep?: boolean, audioOnly?: boolean }} params
40
+ * @returns {object}
41
+ */
42
+ function outputOf({ id, outputKey, isStep = false, audioOnly = false }) {
43
+ return {
44
+ id,
45
+ spec: outputSpec({ sourceKey: FILM.sourceKey, audioOnly }),
46
+ outputKey,
47
+ isStep,
48
+ audioOnly,
49
+ state: "ready",
50
+ sourceKey: FILM.sourceKey,
51
+ fileIndex: FILM.fileIndex,
52
+ file: { key: "film-1", durationSeconds: FILM.durationSeconds }
53
+ };
54
+ }
55
+
56
+ /**
57
+ * The real orchestrator over the real registry, wired the way the session
58
+ * manager wires it.
59
+ *
60
+ * @param {object[]} sessions
61
+ * @returns {{ priority: PriorityOrchestrator, viewers: Viewers, publish: () => void }}
62
+ */
63
+ function over(sessions) {
64
+ const live = new OutputCatalog();
65
+ for (const one of sessions) {
66
+ live.set(one.id, one);
67
+ if (one.isStep) live.markStep(one);
68
+ }
69
+ const viewers = new Viewers();
70
+ const priority = new PriorityOrchestrator({
71
+ publish: () => {},
72
+ viewers,
73
+ allowanceFor: () => 10,
74
+ watchedBy: (session, viewer) => !live.supersededBy(session, viewer.activeVariantId ?? null)
75
+ });
76
+ return {
77
+ priority,
78
+ viewers,
79
+ live,
80
+ publish: () => priority.publishFor({ sessionGroups: [sessions], staleAfterMs: STALE_AFTER_MS })
81
+ };
82
+ }
83
+
84
+ test("a person on a step wants nothing of the picture they stepped off", () => {
85
+ const picture = outputOf({ id: "pic", outputKey: "out:1080" });
86
+ const step = outputOf({ id: "step", outputKey: "out:480", isStep: true });
87
+ const { priority, viewers, publish } = over([picture, step]);
88
+ // ONE viewer object per person, referenced from both outputs — which is why
89
+ // the step they are on is known when the picture is asked about.
90
+ viewers.of(picture, "p");
91
+ const person = viewers.of(step, "p");
92
+ person.moveTo(300);
93
+ person.activeVariantId = "step";
94
+ publish();
95
+
96
+ assert.ok(
97
+ runsOf(priority.mapForOutput("out:480")).length > 0,
98
+ "the step they are watching is wanted"
99
+ );
100
+ assert.deepEqual(
101
+ runsOf(priority.mapForOutput("out:1080")),
102
+ [],
103
+ "and the picture is producing for nobody, which is what makes its encoder unwanted"
104
+ );
105
+ // The bytes are another matter: both outputs read the same file, and the
106
+ // swarm is asked for the file.
107
+ assert.ok(
108
+ runsOf(priority.mapFor(FILM.sourceKey, FILM.fileIndex)).length > 0,
109
+ "the download still wants the film, because that is what is being watched"
110
+ );
111
+ });
112
+
113
+ test("a soundtrack is wanted by whoever is registered on it", () => {
114
+ const picture = outputOf({ id: "pic", outputKey: "out:1080" });
115
+ const sound = outputOf({ id: "rus", outputKey: "out:a1", audioOnly: true });
116
+ const { priority, viewers, publish } = over([picture, sound]);
117
+ // Standing on a STEP of the picture says nothing about the sound: the tracks
118
+ // nobody chose are let go of where a track is chosen, so being known to a
119
+ // soundtrack is listening to it.
120
+ const person = viewers.of(sound, "p");
121
+ person.moveTo(120);
122
+ person.activeVariantId = "step";
123
+ publish();
124
+
125
+ assert.ok(runsOf(priority.mapForOutput("out:a1")).length > 0);
126
+ });
127
+
128
+ test("a step being warmed is wanted, and so is the picture still on screen", () => {
129
+ // Both are genuinely being produced through a warm-up, which is the price of
130
+ // the switch not being visible. Whether the machine can afford two encoders
131
+ // is the budget's question and not this one's.
132
+ const picture = outputOf({ id: "pic", outputKey: "out:1080" });
133
+ const step = outputOf({ id: "step", outputKey: "out:480", isStep: true });
134
+ const { priority, viewers, publish } = over([picture, step]);
135
+ viewers.of(picture, "p");
136
+ const person = viewers.of(step, "p");
137
+ person.moveTo(300);
138
+ person.warmingVariantId = "step";
139
+ publish();
140
+
141
+ assert.ok(runsOf(priority.mapForOutput("out:1080")).length > 0, "still on screen");
142
+ assert.ok(runsOf(priority.mapForOutput("out:480")).length > 0, "being made ready");
143
+ });
144
+
145
+ test("an output everybody has left is stated as empty, not left as it was", () => {
146
+ // The difference matters: the plan stops the encoders of an output whose map
147
+ // is empty, and would keep the map it had when somebody was watching.
148
+ const picture = outputOf({ id: "pic", outputKey: "out:1080" });
149
+ const { priority, viewers, publish } = over([picture]);
150
+ viewers.of(picture, "p").moveTo(300);
151
+
152
+ publish();
153
+ assert.ok(runsOf(priority.mapForOutput("out:1080")).length > 0);
154
+
155
+ viewers.leaves(picture, "p");
156
+ publish();
157
+ assert.deepEqual(runsOf(priority.mapForOutput("out:1080")), []);
158
+ });
159
+
160
+ test("a viewer who has gone is not watching anything", () => {
161
+ // Presence is the connection and nothing else. Silence — however long — is a
162
+ // paused viewer, a throttled hidden tab, or a full cushion, and none of those
163
+ // is leaving; what ends it is a statement.
164
+ const picture = outputOf({ id: "pic", outputKey: "out:1080" });
165
+ const live = new OutputCatalog();
166
+ live.set("pic", picture);
167
+ const viewers = new Viewers();
168
+ const priority = new PriorityOrchestrator({
169
+ publish: () => {},
170
+ viewers,
171
+ allowanceFor: () => 10,
172
+ watchedBy: (session, viewer) => !live.supersededBy(session, viewer.activeVariantId ?? null)
173
+ });
174
+ const person = viewers.of(picture, "p");
175
+ person.moveTo(300);
176
+ viewers.leaves(picture, "p");
177
+
178
+ priority.publishFor({ sessionGroups: [[picture]], now: Date.now() });
179
+
180
+ assert.deepEqual(runsOf(priority.mapForOutput("out:1080")), []);
181
+ });
182
+
183
+ test("a file and an output that are gone are forgotten", () => {
184
+ // These are the projection of the live sessions, never a memory of them. Left
185
+ // to accumulate they were three maps that only grew, and `forget` was written
186
+ // for that and called from nowhere.
187
+ const picture = outputOf({ id: "pic", outputKey: "out:1080" });
188
+ const { priority, viewers, publish } = over([picture]);
189
+ viewers.of(picture, "p").moveTo(300);
190
+
191
+ publish();
192
+ assert.ok(runsOf(priority.mapForOutput("out:1080")).length > 0);
193
+
194
+ priority.publishFor({ sessionGroups: [[]], staleAfterMs: STALE_AFTER_MS });
195
+ assert.deepEqual(runsOf(priority.mapForOutput("out:1080")), [], "the output is gone");
196
+ assert.deepEqual(
197
+ runsOf(priority.mapFor(FILM.sourceKey, FILM.fileIndex)),
198
+ [],
199
+ "and so is the file"
200
+ );
201
+ });
202
+
203
+ test("the picture is watched by a person who never moved off it", () => {
204
+ const picture = outputOf({ id: "pic", outputKey: "out:1080" });
205
+ const { viewers, live } = over([picture]);
206
+ const person = viewers.of(picture, "p");
207
+
208
+ assert.equal(live.supersededBy(picture, null), false, "no step is active");
209
+ person.activeVariantId = "pic";
210
+ assert.equal(
211
+ live.supersededBy(picture, person.activeVariantId),
212
+ false,
213
+ "and naming the picture itself as the step is the same statement"
214
+ );
215
+ });
@@ -20,7 +20,7 @@ import {
20
20
  mapForViewer,
21
21
  mergeMaps,
22
22
  runsOf
23
- } from "../services/priority/PriorityMap.js";
23
+ } from "../services/viewer/PriorityMap.js";
24
24
 
25
25
  const FILM = 3600;
26
26
  const ALLOWANCE = 8;
@@ -1,7 +1,7 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
3
 
4
- import { probeWedgeIsCertain, readProbeState, PROBE_INTERVAL_MS } from "../services/delivery-probe.js";
4
+ import { probeWedgeIsCertain, readProbeState, PROBE_INTERVAL_MS } from "../services/transport/delivery-probe.js";
5
5
 
6
6
  test("a seen-counter bounded lag is not a wedge, however long it lasts", () => {
7
7
  // Session 4dcac61b, field log 2026-08-28: gap held at 6-7 probes for 95+
@@ -21,20 +21,21 @@
21
21
 
22
22
  import test from "node:test";
23
23
  import assert from "node:assert/strict";
24
- import { SourceFile } from "../services/source/SourceFile.js";
25
- import { Timeline } from "../services/output/Timeline.js";
24
+ import { SourceFile } from "../services/media/SourceFile.js";
25
+ import { Timeline } from "../services/encode/output/Timeline.js";
26
26
  import { mkdtemp, readdir, rm, writeFile } from "node:fs/promises";
27
27
  import os from "node:os";
28
28
  import path from "node:path";
29
- import { HlsSessionManager, usableSegmentIndices } from "../services/hls-session-manager.js";
29
+ import { managerWithOwnStore } from "./helpers/manager.js";
30
30
  import { startRunOn } from "./helpers/encode-run.js";
31
- import { fmp4Format } from "../services/segment-formats/fmp4.js";
31
+ import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
32
+ import { outputSpec } from "./helpers/output-spec.js";
32
33
 
33
34
  const MOVIE_TIMESCALE = 1000;
34
35
  const VIDEO_TIMESCALE = 90_000;
35
36
  const AUDIO_TIMESCALE = 48_000;
36
37
  const SEGMENT_SECONDS = 12.5;
37
- const SESSION_ID = "aef21c88-a8d6-4a9a-8e7a-d0a9536351cf";
38
+ const SESSION_ID = "aef21c88a8d64a9a";
38
39
 
39
40
  /**
40
41
  * @param {string} type
@@ -119,18 +120,15 @@ function wholePiece(offsetSeconds) {
119
120
  /**
120
121
  * A session with the output directory every run writes into.
121
122
  *
122
- * @returns {Promise<{ manager: HlsSessionManager, session: object, dirPath: string }>}
123
+ * @returns {Promise<{ manager: object, session: object, dirPath: string }>}
123
124
  */
124
125
  async function sessionOnOneDirectory() {
125
126
  const dirPath = await mkdtemp(path.join(os.tmpdir(), "produced-copy-"));
126
- const manager = new HlsSessionManager({
127
- enabled: true,
128
- ffmpegBin: "ffmpeg",
129
- localBindHost: "127.0.0.1",
130
- localPort: 9090
131
- });
127
+ // Its own store root — see `helpers/manager.js` for what sharing one cost.
128
+ const { manager } = managerWithOwnStore();
132
129
  const session = {
133
130
  id: SESSION_ID,
131
+ spec: outputSpec({ transcodeVideo: false }),
134
132
  dirPath,
135
133
  // Where this file is cut, held by the file. A fixture that stated it
136
134
  // on the session was describing what production no longer does.
@@ -145,20 +143,16 @@ async function sessionOnOneDirectory() {
145
143
  get inputFile() { return this.file; },
146
144
  get audioFile() { return this.file; },
147
145
  startedAt: Date.now(),
148
- createEntryMs: Date.now(),
149
146
  lastAccessedAt: Date.now(),
150
147
  runs: new Set(),
151
148
  lastError: "",
152
- consumers: new Set(),
153
- viewers: new Map(),
154
149
  segmentFormat: fmp4Format,
155
150
  useSyntheticPlaylist: true,
156
151
  playlistText: "#EXTM3U\n",
157
152
  initBytes: fmp4Format.extractInit(wholePiece(0)),
158
- firstSegmentLogged: false,
159
153
  waitEpoch: 0
160
154
  };
161
- manager.sessionsById.set(SESSION_ID, session);
155
+ manager.outputs.set(SESSION_ID, session);
162
156
  // A run exists and is alive — the state the field case was in, and the one in
163
157
  // which the old test called every leftover "still being written".
164
158
  startRunOn(session, { from: 0, usesExplicitCuts: true });
@@ -170,7 +164,7 @@ async function sessionOnOneDirectory() {
170
164
  test("a leftover of a run that has ended is not served, and answering does not delete it", async (t) => {
171
165
  const { manager, session, dirPath } = await sessionOnOneDirectory();
172
166
  t.after(async () => {
173
- await manager.disposeAll();
167
+ await manager.lifecycle.disposeAll();
174
168
  await rm(dirPath, { recursive: true, force: true });
175
169
  });
176
170
  // Opened by a run that is gone. It has a name and no bytes, so there is
@@ -178,7 +172,7 @@ test("a leftover of a run that has ended is not served, and answering does not d
178
172
  session.runs = new Set();
179
173
  await writeFile(path.join(dirPath, "segment-00001.mp4"), Buffer.alloc(0));
180
174
 
181
- const result = await manager.getFileStream(SESSION_ID, "segment-00001.mp4", { requestSeq: 1 });
175
+ const result = await manager.serving.getFileStream(SESSION_ID, "segment-00001.mp4");
182
176
 
183
177
  assert.equal(result.kind, "warming-up", "nothing servable exists yet, so the viewer waits");
184
178
  // AND ANSWERING A REQUEST DELETES NOTHING. It used to: this test was written
@@ -198,7 +192,7 @@ test("a leftover of a run that has ended is not served, and answering does not d
198
192
  test("the current run's own unfinished piece is waited for, never deleted", async (t) => {
199
193
  const { manager, session, dirPath } = await sessionOnOneDirectory();
200
194
  t.after(async () => {
201
- await manager.disposeAll();
195
+ await manager.lifecycle.disposeAll();
202
196
  await rm(dirPath, { recursive: true, force: true });
203
197
  });
204
198
  // The same file, in the directory of the run that is alive. It is being
@@ -210,49 +204,10 @@ test("the current run's own unfinished piece is waited for, never deleted", asyn
210
204
  startRunOn(session, { from: 1, producing: false, usesExplicitCuts: true });
211
205
  await writeFile(path.join(dirPath, "segment-00001.mp4"), Buffer.alloc(0));
212
206
 
213
- const result = await manager.getFileStream(SESSION_ID, "segment-00001.mp4", { requestSeq: 1 });
207
+ const result = await manager.serving.getFileStream(SESSION_ID, "segment-00001.mp4");
214
208
 
215
209
  assert.equal(result.kind, "warming-up");
216
210
  const left = (await readdir(dirPath)).filter((name) => name === "segment-00001.mp4");
217
211
  assert.deepEqual(left, ["segment-00001.mp4"], "the live run's own output must be left alone");
218
212
  });
219
213
 
220
- test("the look-ahead does not count a file with nothing in it", async (t) => {
221
- const dirPath = await mkdtemp(path.join(os.tmpdir(), "usable-indices-"));
222
- t.after(async () => {
223
- await rm(dirPath, { recursive: true, force: true });
224
- });
225
- await writeFile(path.join(dirPath, "segment-00000.mp4"), wholePiece(0));
226
- await writeFile(path.join(dirPath, "segment-00001.mp4"), Buffer.alloc(0));
227
-
228
- assert.deepEqual(
229
- [...usableSegmentIndices([dirPath], fmp4Format, new Set())].sort((a, b) => a - b),
230
- [0],
231
- "an empty file bridged the hole and bought the encoder a suspension it had not earned"
232
- );
233
-
234
- // The same number, made properly: now it genuinely is ready.
235
- await writeFile(path.join(dirPath, "segment-00001.mp4"), wholePiece(SEGMENT_SECONDS));
236
- assert.deepEqual(
237
- [...usableSegmentIndices([dirPath], fmp4Format, new Set())].sort((a, b) => a - b),
238
- [0, 1],
239
- "a copy with bytes in it, which is what the serving path will find"
240
- );
241
- });
242
-
243
- test("what the output holds is asked of the filesystem once per file", async (t) => {
244
- const dirPath = await mkdtemp(path.join(os.tmpdir(), "usable-memo-"));
245
- t.after(async () => {
246
- await rm(dirPath, { recursive: true, force: true });
247
- });
248
- await writeFile(path.join(dirPath, "segment-00000.mp4"), wholePiece(0));
249
- const known = new Set();
250
-
251
- usableSegmentIndices([dirPath], fmp4Format, known);
252
-
253
- assert.deepEqual(
254
- [...known],
255
- [path.join(dirPath, "segment-00000.mp4")],
256
- "a piece that has bytes never loses them, and this runs on the thread carrying the data channel"
257
- );
258
- });