@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
@@ -0,0 +1,219 @@
1
+ /**
2
+ * @file The rule that decides whether one output's pieces may be played under
3
+ * another's header — and it is written BEFORE the experiment that will use it.
4
+ *
5
+ * The order matters and was a review finding: a criterion derived from the
6
+ * differences an experiment happened to produce proves only that those
7
+ * differences were handled. This one compares the whole header, names every
8
+ * difference by where it sits, and refuses anything no listed exception covers.
9
+ */
10
+
11
+ import test from "node:test";
12
+ import assert from "node:assert/strict";
13
+ import { INIT_DIFFERENCE_EXCEPTIONS, compareInits, initLeaves } from "../services/encode/segment-formats/init-compat.js";
14
+
15
+ /**
16
+ * One ISO base media box.
17
+ *
18
+ * @param {string} type
19
+ * @param {...(Buffer | number[])} parts
20
+ * @returns {Buffer}
21
+ */
22
+ function box(type, ...parts) {
23
+ const body = Buffer.concat(parts.map((part) => (Buffer.isBuffer(part) ? part : Buffer.from(part))));
24
+ const header = Buffer.alloc(8);
25
+ header.writeUInt32BE(body.length + 8, 0);
26
+ header.write(type, 4, "latin1");
27
+ return Buffer.concat([header, body]);
28
+ }
29
+
30
+ /**
31
+ * A version-0 `mdhd`: version+flags, creation, modification, timescale,
32
+ * duration, language, quality.
33
+ *
34
+ * @param {number} timescale
35
+ * @returns {Buffer}
36
+ */
37
+ function mdhd(timescale) {
38
+ const body = Buffer.alloc(24);
39
+ body.writeUInt32BE(timescale, 12);
40
+ return box("mdhd", body);
41
+ }
42
+
43
+ /**
44
+ * A `btrt`: decoding buffer size, maximum rate, average rate.
45
+ *
46
+ * @param {number} maxBitrate
47
+ * @returns {Buffer}
48
+ */
49
+ function btrt(maxBitrate) {
50
+ const body = Buffer.alloc(12);
51
+ body.writeUInt32BE(1875072, 0);
52
+ body.writeUInt32BE(maxBitrate, 4);
53
+ body.writeUInt32BE(maxBitrate, 8);
54
+ return box("btrt", body);
55
+ }
56
+
57
+ /**
58
+ * A `stsd` carrying one visual sample entry: its 78-byte preamble, then the
59
+ * codec configuration, then optionally a bit rate declaration.
60
+ *
61
+ * @param {{ entryType?: string, config?: number[], maxBitrate?: number | null, preamble?: number }} [how]
62
+ * @returns {Buffer}
63
+ */
64
+ function stsd({ entryType = "avc1", config = [1, 100, 0, 31], maxBitrate = null, preamble = 78 } = {}) {
65
+ const versionAndCount = Buffer.alloc(8);
66
+ versionAndCount.writeUInt32BE(1, 4);
67
+ const children = [box("avcC", config)];
68
+ if (maxBitrate !== null) {
69
+ children.push(btrt(maxBitrate));
70
+ }
71
+ const entry = box(entryType, Buffer.alloc(preamble), ...children);
72
+ return box("stsd", versionAndCount, entry);
73
+ }
74
+
75
+ /**
76
+ * A whole init segment, shaped as ffmpeg writes one.
77
+ *
78
+ * @param {{ timescale?: number, tracks?: number } & Parameters<typeof stsd>[0]} [how]
79
+ * @returns {Buffer}
80
+ */
81
+ function init({ timescale = 90000, tracks = 1, ...entry } = {}) {
82
+ const trak = (index) =>
83
+ box(
84
+ "trak",
85
+ box("tkhd", Buffer.alloc(84, index)),
86
+ box("mdia", mdhd(timescale), box("hdlr", Buffer.alloc(32)), box("minf", box("stbl", stsd(entry))))
87
+ );
88
+ const traks = [];
89
+ for (let index = 0; index < tracks; index += 1) {
90
+ traks.push(trak(index));
91
+ }
92
+ return Buffer.concat([
93
+ box("ftyp", Buffer.from("isom")),
94
+ box("moov", box("mvhd", Buffer.alloc(100)), ...traks, box("mvex", box("trex", Buffer.alloc(24))))
95
+ ]);
96
+ }
97
+
98
+ const ENTRY = "moov[0]/trak[0]/mdia[0]/minf[0]/stbl[0]/stsd[0]/avc1[0]";
99
+
100
+ test("every exception carries the reason it is one", () => {
101
+ for (const exception of INIT_DIFFERENCE_EXCEPTIONS) {
102
+ assert.ok(exception.why && exception.why.length > 40, `no reason given for ${JSON.stringify(exception)}`);
103
+ assert.ok(
104
+ exception.path !== undefined || exception.sampleEntryBox !== undefined,
105
+ "an exception says WHAT it covers"
106
+ );
107
+ }
108
+ });
109
+
110
+ test("every leaf of the header is named by whose child it is, inside a sample entry too", () => {
111
+ const paths = [...initLeaves(init({ tracks: 2, maxBitrate: 3_000_000 })).keys()];
112
+
113
+ assert.ok(paths.includes("moov[0]/trak[0]/mdia[0]/mdhd[0]"), paths.join(" "));
114
+ assert.ok(paths.includes("moov[0]/trak[1]/mdia[0]/mdhd[0]"), "the second track is its own path");
115
+ assert.ok(paths.includes("moov[0]/mvex[0]/trex[0]"));
116
+ assert.ok(
117
+ paths.includes(`${ENTRY}/avcC[0]`),
118
+ "the codec configuration is a leaf of its own, so it can refuse on its own"
119
+ );
120
+ assert.ok(paths.includes(`${ENTRY}/btrt[0]`), "and so is the bit rate declaration");
121
+ });
122
+
123
+ test("one header serves its own pieces", () => {
124
+ const answer = compareInits(init(), init());
125
+
126
+ assert.equal(answer.compatible, true);
127
+ assert.deepEqual(answer.differences, []);
128
+ });
129
+
130
+ test("a bit rate declaration is the one difference that does not refuse", () => {
131
+ const answer = compareInits(init({ maxBitrate: 3_000_000 }), init({ maxBitrate: 1_200_000 }));
132
+
133
+ assert.equal(answer.compatible, true, answer.differences.join(" | "));
134
+ assert.deepEqual(answer.refusedPaths, []);
135
+ assert.ok(
136
+ answer.differences.some((line) => line.includes("max 3000000") && line.includes("max 1200000")),
137
+ `the difference is still reported, with its numbers: ${answer.differences.join(" | ")}`
138
+ );
139
+ });
140
+
141
+ test("allowing the bit rate does not allow the parameter sets beside it", () => {
142
+ const answer = compareInits(
143
+ init({ maxBitrate: 3_000_000, config: [1, 100, 0, 31] }),
144
+ init({ maxBitrate: 1_200_000, config: [1, 100, 0, 30] })
145
+ );
146
+
147
+ assert.equal(answer.compatible, false);
148
+ assert.deepEqual(answer.refusedPaths, [`${ENTRY}/avcC[0]`]);
149
+ });
150
+
151
+ test("the bit rate exception is scoped to a sample entry", () => {
152
+ const strayLeft = box("moov", box("trak", box("mdia", box("minf", box("stbl", btrt(3_000_000))))));
153
+ const strayRight = box("moov", box("trak", box("mdia", box("minf", box("stbl", btrt(1_200_000))))));
154
+
155
+ const answer = compareInits(strayLeft, strayRight);
156
+
157
+ assert.equal(answer.compatible, false, "a btrt that is nobody's sample entry is not the box the reason is about");
158
+ assert.deepEqual(answer.refusedPaths, ["moov[0]/trak[0]/mdia[0]/minf[0]/stbl[0]/btrt[0]"]);
159
+ });
160
+
161
+ test("a sample entry whose children do not tile is compared whole", () => {
162
+ const answer = compareInits(init({ preamble: 8, config: [1, 100, 0, 31] }), init({ preamble: 8, config: [1, 100, 0, 30] }));
163
+
164
+ assert.equal(answer.compatible, false);
165
+ assert.deepEqual(
166
+ answer.refusedPaths,
167
+ [ENTRY],
168
+ "the descent is checked, and where it does not hold the whole entry refuses"
169
+ );
170
+ });
171
+
172
+ test("a different media timescale is refused, and said in those words", () => {
173
+ const answer = compareInits(init({ timescale: 90000 }), init({ timescale: 48000 }));
174
+
175
+ assert.equal(answer.compatible, false);
176
+ assert.ok(
177
+ answer.differences.some((line) => line.includes("timescale 90000 against 48000")),
178
+ answer.differences.join(" | ")
179
+ );
180
+ assert.deepEqual(answer.refusedPaths, ["moov[0]/trak[0]/mdia[0]/mdhd[0]"]);
181
+ });
182
+
183
+ test("a track present in one header and not the other is refused", () => {
184
+ const answer = compareInits(init({ tracks: 1 }), init({ tracks: 2 }));
185
+
186
+ assert.equal(answer.compatible, false);
187
+ assert.ok(
188
+ answer.differences.some((line) => line.includes("trak[1]") && line.includes("absent from the first")),
189
+ answer.differences.join(" | ")
190
+ );
191
+ });
192
+
193
+ test("an exact-path exception decides by path", () => {
194
+ const served = init({ timescale: 90000 });
195
+ const made = init({ timescale: 48000 });
196
+
197
+ const refused = compareInits(served, made);
198
+ const permitted = compareInits(served, made, {
199
+ exceptions: [{ path: "moov[0]/trak[0]/mdia[0]/mdhd[0]", why: "for this check only" }]
200
+ });
201
+
202
+ assert.equal(refused.compatible, false);
203
+ assert.equal(permitted.compatible, true, "the exception list is what decides, and it is asked by path");
204
+ assert.equal(permitted.differences.length, 1, "a permitted difference is still reported");
205
+ });
206
+
207
+ test("allowing one path does not allow the same box of another track", () => {
208
+ const answer = compareInits(init({ tracks: 2, timescale: 90000 }), init({ tracks: 2, timescale: 48000 }), {
209
+ exceptions: [{ path: "moov[0]/trak[0]/mdia[0]/mdhd[0]", why: "for this check only" }]
210
+ });
211
+
212
+ assert.equal(answer.compatible, false);
213
+ assert.deepEqual(answer.refusedPaths, ["moov[0]/trak[1]/mdia[0]/mdhd[0]"]);
214
+ });
215
+
216
+ test("a missing header is never compatible", () => {
217
+ assert.equal(compareInits(init(), Buffer.alloc(0)).compatible, false);
218
+ assert.equal(compareInits(Buffer.alloc(0), init()).compatible, false);
219
+ });
@@ -20,7 +20,7 @@ import { EventEmitter } from "node:events";
20
20
  import { EncodeRun } from "../services/encode/EncodeRun.js";
21
21
  import { ENCODE_EXIT } from "../services/encode/encode-exit.js";
22
22
  import { SoftwareEncoder } from "../services/encode/SoftwareEncoder.js";
23
- import { EncodeOrchestrator } from "../services/orchestrators/EncodeOrchestrator.js";
23
+ import { EncodeOrchestrator } from "../services/encode/EncodeOrchestrator.js";
24
24
 
25
25
  const PICTURE = "torrent:abc:fmt=fmp4:grid=kf@0:video-only:v=0/copy";
26
26
 
@@ -14,7 +14,7 @@
14
14
 
15
15
  import test from "node:test";
16
16
  import assert from "node:assert/strict";
17
- import { isInputUnavailable } from "../services/hls-session-manager.js";
17
+ import { isInputUnavailable } from "../services/encode/EncodeRuns.js";
18
18
 
19
19
  test("the messages the field produced when data went away are all temporary", () => {
20
20
  for (const message of [
@@ -11,8 +11,8 @@
11
11
 
12
12
  import test from "node:test";
13
13
  import assert from "node:assert/strict";
14
- import { IDLE_KEEP_MS } from "../services/disk/keep.js";
15
- import { Returns } from "../services/disk/returns.js";
14
+ import { IDLE_KEEP_MS } from "../services/storage/keep.js";
15
+ import { Returns } from "../services/storage/returns.js";
16
16
 
17
17
  const MINUTE = 60 * 1000;
18
18
 
@@ -38,8 +38,8 @@ test("one number, and both kinds of material read it", async () => {
38
38
  const here = path.dirname(fileURLToPath(import.meta.url));
39
39
  const read = (relative) => readFileSync(path.join(here, "..", relative), "utf8");
40
40
 
41
- assert.match(read("services/torrent-pool.js"), /TORRENT_IDLE_TTL_MS = IDLE_KEEP_MS/);
42
- assert.match(read("services/hls-session-manager.js"), /SEGMENT_STORE_IDLE_MS = IDLE_KEEP_MS/);
41
+ assert.match(read("services/torrent/torrent-pool.js"), /TORRENT_IDLE_TTL_MS = IDLE_KEEP_MS/);
42
+ assert.match(read("services/server/OutputLifecycle.js"), /SEGMENT_STORE_IDLE_MS = IDLE_KEEP_MS/);
43
43
  });
44
44
 
45
45
  test("a session opened on material still held is a return, and its age is kept", () => {
@@ -11,93 +11,156 @@
11
11
  * states where it truly begins, and it is already read whole in order to be
12
12
  * stamped. Only boundaries that were actually produced are counted — the parts
13
13
  * somebody watched.
14
+ *
15
+ * Counted ON THE FILE'S OWN TABLE, because that is whose fact it is: the same
16
+ * finding for every quality step of the film and for every later session of it.
14
17
  */
15
18
 
16
19
  import test from "node:test";
17
20
  import assert from "node:assert/strict";
18
- import { SourceFile } from "../services/source/SourceFile.js";
19
- import { Timeline } from "../services/output/Timeline.js";
20
- import { newIndexCheck, noteIndexDeviation } from "../services/hls-session-manager.js";
21
+ import { SourceFile } from "../services/media/SourceFile.js";
22
+ import { Timeline } from "../services/encode/output/Timeline.js";
23
+ import { KeyframeTable } from "../services/media/container/KeyframeTable.js";
24
+ import { outputSpec } from "./helpers/output-spec.js";
25
+
26
+ const TOLERANCE = 0.25;
27
+
28
+ /**
29
+ * A table naming keyframes every four seconds, which is what the grid of a
30
+ * copied picture is built from.
31
+ *
32
+ * @returns {KeyframeTable}
33
+ */
34
+ function fourSecondKeyframes() {
35
+ return new KeyframeTable().learn({
36
+ times: [0, 4, 8, 12, 16, 20],
37
+ format: "matroska"
38
+ });
39
+ }
21
40
 
22
41
  test("an index that describes its file exactly is reported as such", () => {
23
- const check = newIndexCheck();
42
+ const table = fourSecondKeyframes();
24
43
 
25
44
  for (let index = 0; index < 4; index += 1) {
26
- noteIndexDeviation(check, index, 0);
45
+ table.witness({ index, trueStart: index * 4, deviationSec: 0, toleranceSec: TOLERANCE });
27
46
  }
28
47
 
29
- assert.equal(check.checked, 4);
30
- assert.equal(check.disagreed, 0, "nothing disagreed — which is a finding, not silence");
31
- assert.equal(check.maxDeviationSec, 0);
48
+ assert.equal(table.evidence.checked, 4);
49
+ assert.equal(table.evidence.disagreed, 0, "nothing disagreed — which is a finding, not silence");
50
+ assert.equal(table.evidence.maxDeviationSec, 0);
32
51
  });
33
52
 
34
53
  test("a boundary the index placed wrongly is counted, with how far out it was", () => {
35
- const check = newIndexCheck();
54
+ const table = fourSecondKeyframes();
36
55
 
37
- noteIndexDeviation(check, 0, 0);
56
+ table.witness({ index: 0, trueStart: 0, deviationSec: 0, toleranceSec: TOLERANCE });
38
57
  // The measured shape: the playlist said 157.99 s, the file cut at 153.82 s.
39
- noteIndexDeviation(check, 2, 4.17);
40
- noteIndexDeviation(check, 3, 0.01);
58
+ table.witness({ index: 2, trueStart: 3.83, deviationSec: 4.17, toleranceSec: TOLERANCE });
59
+ table.witness({ index: 3, trueStart: 12.01, deviationSec: 0.01, toleranceSec: TOLERANCE });
41
60
 
42
- assert.equal(check.checked, 3);
43
- assert.equal(check.disagreed, 1);
44
- assert.equal(check.firstDisagreementIndex, 2);
61
+ assert.equal(table.evidence.checked, 3);
62
+ assert.equal(table.evidence.disagreed, 1);
63
+ assert.equal(table.evidence.firstDisagreementIndex, 2);
45
64
  assert.equal(
46
- check.maxDeviationSec,
65
+ table.evidence.maxDeviationSec,
47
66
  4.17,
48
67
  "the size of the error is what decides whether a rung can be cut on this grid"
49
68
  );
50
69
  });
51
70
 
52
71
  test("a deviation within tolerance is not a disagreement, but still shows in the worst case", () => {
53
- const check = newIndexCheck();
72
+ const table = fourSecondKeyframes();
73
+
74
+ table.witness({ index: 0, trueStart: 0.2, deviationSec: 0.2, toleranceSec: TOLERANCE });
75
+
76
+ assert.equal(
77
+ table.evidence.disagreed,
78
+ 0,
79
+ "rounding in a container's timestamps is not the index being wrong"
80
+ );
81
+ assert.equal(
82
+ table.evidence.maxDeviationSec,
83
+ 0.2,
84
+ "and it is still worth knowing how close to the line it ran"
85
+ );
86
+ });
87
+
88
+ test("a piece that began at ANOTHER time the table names is told apart from one that began nowhere", () => {
89
+ // The discriminator, and it decides which of two opposite faults this is. A
90
+ // piece that began at another keyframe of the same list was not mis-described
91
+ // by the table: the grid was built over a gap in it. One that began where the
92
+ // table names nothing is the table describing times the file does not have.
93
+ // The table answers it itself, because it is the only thing that holds the
94
+ // list — asked of the caller, it was one more fact travelling by hand.
95
+ const table = fourSecondKeyframes();
54
96
 
55
- noteIndexDeviation(check, 0, 0.2);
97
+ table.witness({ index: 1, trueStart: 8, deviationSec: 4, toleranceSec: TOLERANCE });
98
+ table.witness({ index: 2, trueStart: 9.7, deviationSec: 1.7, toleranceSec: TOLERANCE });
56
99
 
57
- assert.equal(check.disagreed, 0, "rounding in a container's timestamps is not the index being wrong");
58
- assert.equal(check.maxDeviationSec, 0.2, "and it is still worth knowing how close to the line it ran");
100
+ assert.equal(table.evidence.disagreed, 2);
101
+ assert.equal(table.evidence.landedOnAnotherKeyframe, 1);
102
+ });
103
+
104
+ test("a table that has read nothing claims no evidence about itself", () => {
105
+ // It cannot be wrong about a file it has not described. `readable` is false,
106
+ // every picture of the file is re-encoded, and no copy can witness anything.
107
+ const table = new KeyframeTable().learn({ times: null, format: "mpegts" });
108
+
109
+ assert.equal(table.evidence, null);
110
+ assert.equal(table.names(4), false);
111
+ });
112
+
113
+ test("a segment requested again is not new evidence", () => {
114
+ const table = fourSecondKeyframes();
115
+
116
+ for (let repeat = 0; repeat < 3; repeat += 1) {
117
+ table.witness({ index: 1, trueStart: 4.9, deviationSec: 0.9, toleranceSec: TOLERANCE });
118
+ }
119
+
120
+ assert.equal(table.evidence.checked, 1, "a repeat request is the same boundary, counted once");
121
+ assert.equal(table.evidence.disagreed, 1);
59
122
  });
60
123
 
61
124
  test("a boundary the index got wrong is replaced by the time the file really has", async (t) => {
62
- const { HlsSessionManager } = await import("../services/hls-session-manager.js");
63
- const manager = new HlsSessionManager({
125
+ const { wireOutputs } = await import("../services/server/wire-outputs.js");
126
+ const manager = wireOutputs({
64
127
  enabled: true,
65
128
  ffmpegBin: "ffmpeg",
66
129
  localBindHost: "127.0.0.1",
67
130
  localPort: 9090
68
131
  });
69
- t.after(() => manager.disposeAll());
132
+ t.after(() => manager.lifecycle.disposeAll());
70
133
  // ONE table for the film, held by both. It used to be a copy each, kept in
71
134
  // step by writing the correction into every member — which is what the shared
72
135
  // table replaces, and what drifted in the field.
73
136
  const boundaries = [0, 10, 20, 30, 40];
74
137
  const base = {
75
- id: "aaaaaaaa-1111-2222-3333-444444444444",
138
+ id: "aaaaaaaa11112222",
139
+ spec: outputSpec({ transcodeVideo: false }),
76
140
  fileName: "film.mkv",
77
141
  state: "ready",
78
- transcodeVideo: false,
79
142
  timeline: new Timeline({ boundaries: boundaries, cutGrid: "uniform" }),
80
143
  file: new SourceFile({ sourceKey: "source-1", fileIndex: 0, name: "film.mkv" }),
81
144
  segmentFormat: { segmentFileName: (index) => `segment-${index}.mp4` }
82
145
  };
83
146
  const rung = {
84
- id: "bbbbbbbb-1111-2222-3333-444444444444",
147
+ id: "bbbbbbbb11112222",
148
+ spec: outputSpec({ transcodeVideo: true, height: 540 }),
85
149
  fileName: "film.mkv",
86
150
  state: "ready",
87
- transcodeVideo: true,
88
151
  timeline: new Timeline({ boundaries: boundaries, cutGrid: "uniform" }),
89
152
  // A step of the picture: the same file, and made as a step.
90
153
  file: base.file,
91
154
  variantHeight: 540,
92
155
  isStep: true
93
156
  };
94
- base.file.stepHeights.set(540, 540);
95
- manager.sessionsById.set(base.id, base);
96
- manager.sessionsById.set(rung.id, rung);
157
+ manager.outputs.set(base.id, base);
158
+ manager.outputs.set(rung.id, rung);
159
+ manager.outputs.markStep(rung);
97
160
 
98
161
  // The copy produced segment #2, and it really begins at 17.4 s — the index
99
162
  // said 20. This is the shape reproduced from the field on 2026-08-12.
100
- manager.correctBoundaryFromSegment(base, 2, 17.4);
163
+ manager.outputTimes.correctBoundaryFromSegment(base, 2, 17.4);
101
164
 
102
165
  assert.equal(
103
166
  base.timeline.boundaries[2],
@@ -118,19 +181,8 @@ test("a boundary the index got wrong is replaced by the time the file really has
118
181
  // A reading that cannot be a boundary is not evidence about one. It comes
119
182
  // from a run that started somewhere else, and applying it would leave the
120
183
  // table describing nothing.
121
- manager.correctBoundaryFromSegment(base, 2, 35);
122
- manager.correctBoundaryFromSegment(base, 2, 5);
123
- manager.correctBoundaryFromSegment(base, 0, 3);
184
+ manager.outputTimes.correctBoundaryFromSegment(base, 2, 35);
185
+ manager.outputTimes.correctBoundaryFromSegment(base, 2, 5);
186
+ manager.outputTimes.correctBoundaryFromSegment(base, 0, 3);
124
187
  assert.deepEqual(base.timeline.boundaries, [0, 10, 17.4, 30, 40], "out-of-order readings are refused");
125
188
  });
126
-
127
- test("a segment requested again is not new evidence", () => {
128
- const check = newIndexCheck();
129
-
130
- noteIndexDeviation(check, 1, 0.9);
131
- noteIndexDeviation(check, 1, 0.9);
132
- noteIndexDeviation(check, 1, 0.9);
133
-
134
- assert.equal(check.checked, 1, "a repeat request is the same boundary, counted once");
135
- assert.equal(check.disagreed, 1);
136
- });
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @file How long a session waits for the file's keyframe table.
2
+ * @file How long anybody waits for the file's keyframe table.
3
3
  *
4
4
  * With the table a picture is copied; without it the whole picture is
5
5
  * re-encoded, which on a weak host is the difference between almost free and
@@ -16,66 +16,62 @@
16
16
 
17
17
  import test from "node:test";
18
18
  import assert from "node:assert/strict";
19
- import { HlsSessionManager } from "../services/hls-session-manager.js";
19
+ import { KeyframeTables } from "../services/media/KeyframeTables.js";
20
+
21
+ const QUIET = { info: () => {}, warn: () => {} };
20
22
 
21
23
  /**
22
24
  * @param {number} budgetMs
23
- * @returns {HlsSessionManager}
25
+ * @param {(params: object) => Promise<object | null>} readTable
26
+ * @returns {KeyframeTables}
24
27
  */
25
- function manager(budgetMs) {
26
- return new HlsSessionManager({
27
- enabled: true,
28
- ffmpegBin: "ffmpeg",
29
- localBindHost: "127.0.0.1",
30
- localPort: 9090,
31
- keyframeTableBudgetMs: budgetMs
32
- });
28
+ function tables(budgetMs, readTable) {
29
+ return new KeyframeTables({ readTable, budgetMs, logger: QUIET });
33
30
  }
34
31
 
35
32
  const FILE = { sourceKey: "torrent:abc", fileIndex: 0, logName: "a.mkv" };
36
33
 
37
34
  test("a table that arrives inside the budget is the answer", async () => {
38
- const sessions = manager(1_000);
39
- sessions.getContainerKeyframes = async () => ({ times: [0, 4, 8], tolerance: 0, format: "matroska" });
35
+ const keyframes = tables(1_000, async () => ({ times: [0, 4, 8], tolerance: 0, format: "matroska" }));
40
36
 
41
- const answer = await sessions.readKeyframeTableWithin(FILE);
37
+ const { table, arrived } = await keyframes.within(FILE);
42
38
 
43
- assert.deepEqual(answer.times, [0, 4, 8]);
44
- assert.equal(answer.arrived, true, "the file has answered, so what it said may be written onto it");
39
+ assert.deepEqual(table.times, [0, 4, 8]);
40
+ assert.equal(arrived, true, "the file has answered, so the picture may be copied");
41
+ assert.equal(table.readable, true);
45
42
  });
46
43
 
47
44
  test("a table that has not arrived gives up on copying rather than on the session", async () => {
48
- const sessions = manager(60);
49
45
  // The bytes it needs are still coming off the swarm. On the field host this
50
46
  // is a torrent with one peer: the read was still waiting after two minutes.
51
- sessions.getContainerKeyframes = () => new Promise(() => {});
47
+ const keyframes = tables(60, () => new Promise(() => {}));
52
48
 
53
49
  const startedAt = Date.now();
54
- const answer = await sessions.readKeyframeTableWithin(FILE);
50
+ const { table, arrived } = await keyframes.within(FILE);
55
51
  const waited = Date.now() - startedAt;
56
52
 
57
- assert.equal(answer.times, null, "no table, so this session cannot copy the picture");
53
+ assert.equal(table.readable, false, "no table, so this session cannot copy the picture");
58
54
  assert.equal(
59
- answer.arrived,
55
+ table.answered,
60
56
  false,
61
- "and it says the file has NOT answered — an absence written onto the file would make " +
62
- "a passing shortage of bytes look like a property of the bytes"
57
+ "and the file has NOT answered — an absence recorded as an answer would make a passing " +
58
+ "shortage of bytes look like a property of the bytes"
63
59
  );
60
+ assert.equal(arrived, false);
64
61
  assert.ok(waited < 2_000, `the wait ended at the bound, not at the read (${waited}ms)`);
65
62
  });
66
63
 
67
64
  test("the read goes on after the budget, so the next session of the file gets the copy", async () => {
68
- const sessions = manager(40);
69
65
  let reads = 0;
70
66
  let answerLate = null;
71
- sessions.getContainerKeyframes = () => {
67
+ const keyframes = tables(40, () => {
72
68
  reads += 1;
73
69
  return new Promise((resolve) => {
74
70
  answerLate = resolve;
75
71
  });
76
- };
72
+ });
77
73
 
78
- const first = await sessions.readKeyframeTableWithin(FILE);
74
+ const first = await keyframes.within(FILE);
79
75
  assert.equal(first.arrived, false);
80
76
 
81
77
  answerLate({ times: [0, 5, 10], tolerance: 0, format: "matroska" });
@@ -87,20 +83,19 @@ test("the read goes on after the budget, so the next session of the file gets th
87
83
  // is the very thing the last assertion here is about.
88
84
  await new Promise((resolve) => { setImmediate(resolve); });
89
85
 
90
- const second = await sessions.readKeyframeTableWithin(FILE);
91
- assert.deepEqual(second.times, [0, 5, 10], "the late answer was kept, not thrown away");
86
+ const second = await keyframes.within(FILE);
87
+ assert.deepEqual(second.table.times, [0, 5, 10], "the late answer was kept, not thrown away");
92
88
  assert.equal(second.arrived, true);
93
89
  assert.equal(reads, 1, "and it was not read a second time");
94
90
  });
95
91
 
96
92
  test("a read that fails is not turned into a bounded wait's silence", async () => {
97
- const sessions = manager(1_000);
98
- sessions.getContainerKeyframes = async () => {
93
+ const keyframes = tables(1_000, async () => {
99
94
  throw new Error("the head is not downloaded");
100
- };
95
+ });
101
96
 
102
97
  await assert.rejects(
103
- () => sessions.readKeyframeTableWithin(FILE),
98
+ () => keyframes.read(FILE),
104
99
  /the head is not downloaded/,
105
100
  "a read that threw is a different thing from a read that is still running"
106
101
  );