@torrent-tv/proxy 2.87.8 → 2.87.9

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 (455) hide show
  1. package/.claude/commands/opsx/apply.md +155 -155
  2. package/.claude/commands/opsx/archive.md +160 -160
  3. package/.claude/commands/opsx/explore.md +174 -174
  4. package/.claude/commands/opsx/propose.md +109 -109
  5. package/.claude/commands/opsx/sync.md +143 -143
  6. package/.claude/skills/openspec-apply-change/SKILL.md +159 -159
  7. package/.claude/skills/openspec-archive-change/SKILL.md +117 -117
  8. package/.claude/skills/openspec-explore/SKILL.md +289 -289
  9. package/.claude/skills/openspec-propose/SKILL.md +113 -113
  10. package/.claude/skills/openspec-sync-specs/SKILL.md +147 -147
  11. package/.dockerignore +5 -5
  12. package/AGENTS.md +322 -322
  13. package/CHANGELOG.md +4 -0
  14. package/Dockerfile +30 -30
  15. package/LICENSE +16 -16
  16. package/README.md +369 -369
  17. package/assets/calibration/NOTICE.md +100 -100
  18. package/assets/diagnostics/sctpstate.gdb +31 -31
  19. package/bin/cli.js +684 -684
  20. package/bin/thread-pool.js +30 -30
  21. package/biome.json +411 -411
  22. package/docs/container-architecture.md +218 -218
  23. package/docs/download-architecture.md +261 -261
  24. package/docs/encode-architecture.md +729 -729
  25. package/docs/encode-run-state.md +96 -96
  26. package/docs/logs.md +97 -97
  27. package/docs/socks5-egress-research.md +114 -114
  28. package/docs/storage-architecture.md +267 -267
  29. package/knip.json +14 -14
  30. package/openspec/changes/adaptive-bitrate/design.md +92 -92
  31. package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
  32. package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
  33. package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
  34. package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
  35. package/openspec/changes/binary-distribution/proposal.md +53 -53
  36. package/openspec/changes/chunked-request-bodies/design.md +81 -81
  37. package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
  38. package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
  39. package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
  40. package/openspec/changes/cold-start/design.md +98 -98
  41. package/openspec/changes/cold-start/proposal.md +62 -62
  42. package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
  43. package/openspec/changes/cold-start/tasks.md +48 -48
  44. package/openspec/changes/disk-cap/.openspec.yaml +2 -2
  45. package/openspec/changes/disk-cap/proposal.md +37 -37
  46. package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
  47. package/openspec/changes/disk-cap/tasks.md +19 -19
  48. package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
  49. package/openspec/changes/proxy-observability/design.md +38 -38
  50. package/openspec/changes/proxy-observability/proposal.md +49 -49
  51. package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
  52. package/openspec/changes/proxy-observability/tasks.md +21 -21
  53. package/openspec/changes/seek-debounce/design.md +118 -118
  54. package/openspec/changes/seek-debounce/proposal.md +59 -59
  55. package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
  56. package/openspec/changes/seek-debounce/tasks.md +33 -33
  57. package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
  58. package/openspec/changes/subtitle-language/proposal.md +50 -50
  59. package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
  60. package/openspec/changes/subtitle-language/tasks.md +26 -26
  61. package/openspec/changes/track-selection/.openspec.yaml +2 -2
  62. package/openspec/changes/track-selection/design.md +47 -47
  63. package/openspec/changes/track-selection/proposal.md +45 -45
  64. package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
  65. package/openspec/changes/track-selection/tasks.md +20 -20
  66. package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
  67. package/openspec/changes/transcode-quality/proposal.md +51 -51
  68. package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
  69. package/openspec/changes/transcode-quality/tasks.md +63 -63
  70. package/openspec/config.yaml +38 -38
  71. package/package.json +1 -1
  72. package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
  73. package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
  74. package/research/double-spawn-2026-09-10.md +171 -171
  75. package/research/handover-2026-09-13.md +209 -209
  76. package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
  77. package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
  78. package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
  79. package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
  80. package/routes/api/client-logs/post.js +86 -86
  81. package/routes/api/delivery-sink/get.js +72 -72
  82. package/routes/api/link-probe/get.js +51 -51
  83. package/routes/api/playback-plan/audio-tracks/post.js +29 -29
  84. package/routes/api/playback-plan/post.js +53 -53
  85. package/routes/api/sources/files/get.js +100 -100
  86. package/routes/api/sources/post.js +36 -36
  87. package/routes/api/sources/stats/get.js +132 -132
  88. package/routes/api/subtitles/get.js +230 -230
  89. package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
  90. package/routes/api/transcode-sessions/net-report/post.js +129 -129
  91. package/routes/api/transcode-sessions/post.js +160 -160
  92. package/routes/api/transcode-sessions/progress/get.js +27 -27
  93. package/routes/api/transcode-sessions/release/post.js +41 -41
  94. package/routes/api/transcode-sessions/seek/post.js +63 -63
  95. package/routes/health/get.js +13 -13
  96. package/routes/healthz/get.js +13 -13
  97. package/routes/stream/get.js +361 -361
  98. package/routes/transcode/audio-file/get.js +64 -64
  99. package/routes/transcode/audio-warm/get.js +84 -84
  100. package/routes/transcode/session-file/get.js +52 -52
  101. package/routes/transcode/variant-file/get.js +95 -95
  102. package/routes/transcode/variant-warm/get.js +84 -84
  103. package/scripts/render-run-graph.js +173 -173
  104. package/services/encode/CoverageMap.js +428 -428
  105. package/services/encode/CushionReport.js +345 -345
  106. package/services/encode/EncodeAdmission.js +332 -332
  107. package/services/encode/EncodeOrchestrator.js +1217 -1217
  108. package/services/encode/EncodePlan.js +1366 -1366
  109. package/services/encode/EncodeRun.js +776 -776
  110. package/services/encode/EncodeRuns.js +1229 -1229
  111. package/services/encode/Encoder.js +142 -142
  112. package/services/encode/EncoderSelection.js +38 -38
  113. package/services/encode/LocalObservations.js +359 -359
  114. package/services/encode/NvencEncoder.js +50 -50
  115. package/services/encode/OutputOpening.js +755 -755
  116. package/services/encode/OutputTimes.js +579 -579
  117. package/services/encode/QsvEncoder.js +58 -58
  118. package/services/encode/Renditions.js +2162 -2162
  119. package/services/encode/RunProgress.js +84 -84
  120. package/services/encode/SegmentDemand.js +95 -95
  121. package/services/encode/SoftwareEncoder.js +121 -121
  122. package/services/encode/V4l2m2mEncoder.js +53 -53
  123. package/services/encode/VaapiEncoder.js +66 -66
  124. package/services/encode/args.js +416 -416
  125. package/services/encode/available-share.js +89 -89
  126. package/services/encode/calibration.js +361 -361
  127. package/services/encode/contention.js +128 -128
  128. package/services/encode/decode-cost-fit.js +363 -363
  129. package/services/encode/encode-exit.js +114 -114
  130. package/services/encode/encode-run-state.js +551 -551
  131. package/services/encode/encoder-readings.js +45 -45
  132. package/services/encode/fingerprint.js +322 -322
  133. package/services/encode/host-load.js +174 -174
  134. package/services/encode/hwaccel.js +1894 -1894
  135. package/services/encode/index.js +9 -9
  136. package/services/encode/learned-median.js +90 -90
  137. package/services/encode/output/EncodedOutput.js +57 -57
  138. package/services/encode/output/Output.js +131 -131
  139. package/services/encode/output/OutputCatalog.js +414 -414
  140. package/services/encode/output/OutputSpec.js +390 -390
  141. package/services/encode/output/Timeline.js +308 -308
  142. package/services/encode/output/cut-grid.js +107 -107
  143. package/services/encode/output/index.js +20 -20
  144. package/services/encode/output/ladder.js +32 -32
  145. package/services/encode/output/playlists.js +284 -284
  146. package/services/encode/output/rates.js +125 -125
  147. package/services/encode/output-key-format.js +21 -21
  148. package/services/encode/quality/EncodeCost.js +1083 -1083
  149. package/services/encode/quality/HostLoad.js +525 -525
  150. package/services/encode/quality/HostTimings.js +249 -249
  151. package/services/encode/quality/OutputQualityState.js +28 -28
  152. package/services/encode/quality/QualityController.js +935 -935
  153. package/services/encode/quality/QualityOffer.js +416 -416
  154. package/services/encode/quality/drain-threat.js +47 -47
  155. package/services/encode/quality/link-budget.js +254 -254
  156. package/services/encode/quality/output-format.js +266 -266
  157. package/services/encode/quality/serving-output.js +216 -216
  158. package/services/encode/quality/visible-rung.js +57 -57
  159. package/services/encode/run-budget.js +97 -97
  160. package/services/encode/run-command.js +747 -747
  161. package/services/encode/run-costs.js +155 -155
  162. package/services/encode/run-inputs.js +42 -42
  163. package/services/encode/segment-formats/fmp4.js +437 -437
  164. package/services/encode/segment-formats/index.js +77 -77
  165. package/services/encode/segment-formats/init-compat.js +324 -324
  166. package/services/encode/segment-formats/mp4-boxes.js +311 -311
  167. package/services/encode/segment-formats/mpegts.js +134 -134
  168. package/services/encode/start-stop-cost.js +178 -178
  169. package/services/encode/throughput.js +97 -97
  170. package/services/encode/torrent-cost.js +88 -88
  171. package/services/identity/installation-id.js +38 -38
  172. package/services/media/ContainerOrchestrator.js +149 -149
  173. package/services/media/KeyframeTables.js +316 -316
  174. package/services/media/SourceFile.js +298 -298
  175. package/services/media/SubtitleCues.js +520 -520
  176. package/services/media/SubtitleOrchestrator.js +167 -167
  177. package/services/media/container/AviContainer.js +266 -266
  178. package/services/media/container/Container.js +603 -603
  179. package/services/media/container/ContainerFactory.js +56 -56
  180. package/services/media/container/KeyframeTable.js +272 -272
  181. package/services/media/container/MatroskaContainer.js +1494 -1494
  182. package/services/media/container/Mp4Container.js +1244 -1244
  183. package/services/media/container/SubtitleFileContainer.js +322 -322
  184. package/services/media/container/ebml-reader.js +159 -159
  185. package/services/media/ffmpeg-banner.js +260 -260
  186. package/services/media/keyframe-probe.js +157 -157
  187. package/services/media/media-info-probe.js +93 -93
  188. package/services/media/playback-planner.js +1 -1
  189. package/services/media/tracks/AudioTrack.js +131 -131
  190. package/services/media/tracks/ContainerTrack.js +75 -75
  191. package/services/media/tracks/ImageSubtitleTrack.js +19 -19
  192. package/services/media/tracks/SubtitleTrack.js +38 -38
  193. package/services/media/tracks/TextSubtitleTrack.js +287 -287
  194. package/services/media/tracks/VideoTrack.js +33 -33
  195. package/services/media/tracks/index.js +15 -15
  196. package/services/media/tracks/language-detect.js +174 -174
  197. package/services/server/OutputLifecycle.js +418 -418
  198. package/services/server/SegmentServing.js +1146 -1146
  199. package/services/server/ViewerRequests.js +522 -522
  200. package/services/server/WaitLedger.js +142 -142
  201. package/services/server/controllers/SubtitleController.js +109 -109
  202. package/services/server/http-responses.js +60 -60
  203. package/services/server/transcode-session-files.js +325 -325
  204. package/services/server/wire-outputs.js +983 -983
  205. package/services/storage/Diagnostics.js +202 -202
  206. package/services/storage/MachineBudget.js +235 -235
  207. package/services/storage/allowance.js +107 -107
  208. package/services/storage/core-dumps.js +106 -106
  209. package/services/storage/files/CompletedFiles.js +391 -391
  210. package/services/storage/files/piece-from-whole-file.js +118 -118
  211. package/services/storage/free.js +71 -71
  212. package/services/storage/keep.js +48 -48
  213. package/services/storage/machine-memory.js +56 -56
  214. package/services/storage/memory-report.js +575 -575
  215. package/services/storage/piece-store/piece-disk-store.js +516 -516
  216. package/services/storage/piece-store/piece-lru.js +495 -495
  217. package/services/storage/piece-store/shared-piece-store.js +1880 -1880
  218. package/services/storage/returns.js +103 -103
  219. package/services/storage/segment-store/SegmentStore.js +1196 -1196
  220. package/services/storage/wire.js +175 -175
  221. package/services/torrent/Contents.js +324 -324
  222. package/services/torrent/demand/DemandRegister.js +202 -202
  223. package/services/torrent/demand/Urgency.js +137 -137
  224. package/services/torrent/demand/Window.js +118 -118
  225. package/services/torrent/demand/index.js +8 -8
  226. package/services/torrent/demand/pieces.js +140 -140
  227. package/services/torrent/download/SwarmSelection.js +386 -386
  228. package/services/torrent/download/registry.js +102 -102
  229. package/services/torrent/download/withdraw-claim.js +80 -80
  230. package/services/torrent/files.js +455 -455
  231. package/services/torrent/naming.js +619 -619
  232. package/services/torrent/piece-store-of.js +29 -29
  233. package/services/torrent/supply-margin.js +262 -262
  234. package/services/torrent/worker/background-fill.js +213 -213
  235. package/services/torrent/worker/channel.js +271 -271
  236. package/services/torrent/worker/fastest-wires.js +342 -342
  237. package/services/torrent/worker/handle-state.js +23 -23
  238. package/services/torrent/worker/held-bytes.js +155 -155
  239. package/services/torrent/worker/install-webrtc-shim.js +32 -32
  240. package/services/torrent/worker/piece-reader.js +1078 -1078
  241. package/services/torrent/worker/resume-warm.js +107 -107
  242. package/services/torrent/worker/webrtc-shim.js +219 -219
  243. package/services/transport/delivery-probe.js +842 -842
  244. package/services/transport/health-collector.js +53 -53
  245. package/services/transport/nat-classifier.js +231 -231
  246. package/services/transport/packet-witness.js +808 -808
  247. package/services/transport/port-mapper.js +235 -235
  248. package/services/transport/registry-api.js +81 -81
  249. package/services/transport/tunnel-client.js +447 -447
  250. package/services/transport/usrsctp-state.js +175 -175
  251. package/services/transport/webrtc-manager.js +484 -484
  252. package/services/viewer/Assignments.js +340 -340
  253. package/services/viewer/PriorityMap.js +390 -390
  254. package/services/viewer/PriorityOrchestrator.js +325 -325
  255. package/services/viewer/Viewer.js +657 -657
  256. package/services/viewer/Viewers.js +368 -368
  257. package/services/viewer/active-output.js +37 -37
  258. package/services/viewer/audio-start.js +46 -46
  259. package/services/viewer/buffer-trend.js +81 -81
  260. package/services/viewer/choices.js +647 -647
  261. package/services/viewer/positions.js +126 -126
  262. package/services/viewer/report-intake.js +46 -46
  263. package/store/source-registry.js +71 -71
  264. package/test/asset-paths.test.js +28 -28
  265. package/test/audio-track-bitrate.test.js +40 -40
  266. package/test/auto-quality-step.test.js +707 -707
  267. package/test/available-share.test.js +76 -76
  268. package/test/background-fill.test.js +190 -190
  269. package/test/client-log-file.test.js +110 -110
  270. package/test/completed-files.test.js +115 -115
  271. package/test/concurrent-cost.test.js +148 -148
  272. package/test/container-media-info.test.js +228 -228
  273. package/test/contention.test.js +76 -76
  274. package/test/core-dumps.test.js +57 -57
  275. package/test/coverage-follows-the-disk.test.js +228 -228
  276. package/test/coverage-map.test.js +195 -195
  277. package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
  278. package/test/cut-times-timeline.test.js +65 -65
  279. package/test/cuts-follow-published-grid.test.js +109 -109
  280. package/test/data-channel-frame.test.js +63 -63
  281. package/test/dead-torrent-handle.test.js +52 -52
  282. package/test/declared-rates.test.js +116 -116
  283. package/test/declared-tracks.test.js +35 -35
  284. package/test/decode-cost-fit.test.js +202 -202
  285. package/test/decode-cost.test.js +462 -462
  286. package/test/delivery-probe.test.js +393 -393
  287. package/test/demand-register.test.js +206 -206
  288. package/test/dht-bootstrap.test.js +72 -72
  289. package/test/drain-threat.test.js +65 -65
  290. package/test/duplicate-add.test.js +48 -48
  291. package/test/encode-admission.test.js +146 -146
  292. package/test/encode-cost.test.js +283 -283
  293. package/test/encode-exit.test.js +111 -111
  294. package/test/encode-orchestrator.test.js +419 -419
  295. package/test/encode-plan-viewers.test.js +718 -718
  296. package/test/encode-plan.test.js +545 -545
  297. package/test/encode-run-state.test.js +314 -314
  298. package/test/encode-run.test.js +161 -161
  299. package/test/encode-slope.test.js +46 -46
  300. package/test/encoded-output.test.js +39 -39
  301. package/test/encoder-kinds.test.js +122 -122
  302. package/test/encoder-readings.test.js +46 -46
  303. package/test/every-claimant-is-told-its-share.test.js +152 -152
  304. package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
  305. package/test/failed-start-breaker.test.js +202 -202
  306. package/test/fastest-wires.test.js +143 -143
  307. package/test/ffmpeg-stream-counts.test.js +78 -78
  308. package/test/file-edges.test.js +191 -191
  309. package/test/fmp4-self-contained.test.js +80 -80
  310. package/test/fmp4-timestamps.test.js +140 -140
  311. package/test/fmp4-track-completeness.test.js +41 -41
  312. package/test/format-keyed-outputs.test.js +78 -78
  313. package/test/health-metrics.test.js +38 -38
  314. package/test/held-request-width.test.js +157 -157
  315. package/test/helpers/encode-run.js +136 -136
  316. package/test/helpers/held-file.js +29 -29
  317. package/test/helpers/manager.js +57 -57
  318. package/test/helpers/output-spec.js +63 -63
  319. package/test/host-load.test.js +68 -68
  320. package/test/index-accuracy-is-about-the-file.test.js +128 -128
  321. package/test/init-compat.test.js +219 -219
  322. package/test/input-lost-quiets-the-plan.test.js +261 -261
  323. package/test/input-unavailable.test.js +48 -48
  324. package/test/installation-id.test.js +39 -39
  325. package/test/keeping-period.test.js +83 -83
  326. package/test/keyframe-index-accuracy.test.js +188 -188
  327. package/test/keyframe-table-budget.test.js +102 -102
  328. package/test/keyframes-belong-to-the-file.test.js +249 -249
  329. package/test/learned-median.test.js +37 -37
  330. package/test/letting-go-asks-who-is-watching.test.js +191 -191
  331. package/test/limit-row-by-area.test.js +60 -60
  332. package/test/link-budget.test.js +111 -111
  333. package/test/link-probe.test.js +56 -56
  334. package/test/local-encoding-data.test.js +273 -273
  335. package/test/logger-repeats.test.js +134 -134
  336. package/test/look-ahead-coverage.test.js +56 -56
  337. package/test/manager-surface.test.js +292 -292
  338. package/test/matroska-cues-track.test.js +192 -192
  339. package/test/memory-budget.test.js +390 -390
  340. package/test/move-cost.test.js +216 -216
  341. package/test/mp4-subtitles.test.js +173 -173
  342. package/test/one-authority.test.js +285 -285
  343. package/test/one-budget.test.js +149 -149
  344. package/test/one-output-one-session.test.js +204 -204
  345. package/test/one-resource-divided.test.js +185 -185
  346. package/test/one-serving-output-per-height.test.js +458 -458
  347. package/test/orchestrator-wired.test.js +206 -206
  348. package/test/output-catalog.test.js +39 -39
  349. package/test/output-shape.test.js +21 -21
  350. package/test/output-spec.test.js +251 -251
  351. package/test/output-speed.test.js +88 -88
  352. package/test/outputs-being-prepared.test.js +40 -40
  353. package/test/packet-witness-ring.test.js +236 -236
  354. package/test/packet-witness.test.js +148 -148
  355. package/test/piece-disk-store.test.js +381 -381
  356. package/test/piece-from-whole-file.test.js +129 -129
  357. package/test/piece-lru.test.js +319 -319
  358. package/test/piece-reader.test.js +162 -162
  359. package/test/piece-selection-offset.test.js +85 -85
  360. package/test/piece-store-eviction.test.js +560 -560
  361. package/test/piece-store-never-refuses.test.js +153 -153
  362. package/test/piece-store-reservations.test.js +318 -318
  363. package/test/piece-store-slow-disk.test.js +159 -159
  364. package/test/piece-tail.test.js +97 -97
  365. package/test/plan-host-timings.test.js +68 -68
  366. package/test/plan-is-steady.test.js +225 -225
  367. package/test/priority-map-download.test.js +174 -174
  368. package/test/priority-map-emptied.test.js +204 -204
  369. package/test/priority-map-per-output.test.js +215 -215
  370. package/test/priority-map.test.js +163 -163
  371. package/test/probe-wedge-certainty.test.js +60 -60
  372. package/test/produced-copy-choice.test.js +213 -213
  373. package/test/published-timeline.test.js +83 -83
  374. package/test/quality-offer.test.js +162 -162
  375. package/test/quality-variants.test.js +1229 -1229
  376. package/test/rate-control-in-the-output.test.js +180 -180
  377. package/test/read-survives-withdrawal.test.js +164 -164
  378. package/test/read-window.test.js +365 -365
  379. package/test/resume-warm.test.js +39 -39
  380. package/test/run-graph-drift.test.js +37 -37
  381. package/test/run-inputs.test.js +80 -80
  382. package/test/run-intervals.test.js +100 -100
  383. package/test/run-position-follows-published-grid.test.js +89 -89
  384. package/test/run-progress.test.js +65 -65
  385. package/test/same-height-switch.test.js +664 -664
  386. package/test/seek-frees-the-viewers-requests.test.js +217 -217
  387. package/test/seek-landing.test.js +109 -109
  388. package/test/seek-target-not-superseded.test.js +195 -195
  389. package/test/segment-closed-on-both-branches.test.js +96 -96
  390. package/test/segment-cut-times.test.js +63 -63
  391. package/test/segment-demand.test.js +80 -80
  392. package/test/segment-hold-supersede.test.js +91 -91
  393. package/test/segment-judged-by-its-own-header.test.js +221 -221
  394. package/test/segment-serve-wiring.test.js +475 -475
  395. package/test/segment-store-eviction.test.js +247 -247
  396. package/test/segment-store.test.js +323 -323
  397. package/test/segment-true-start.test.js +86 -86
  398. package/test/segments-are-shared.test.js +176 -176
  399. package/test/send-chunk-bytes.test.js +86 -86
  400. package/test/serving-existing-output.test.js +102 -102
  401. package/test/serving-output.test.js +102 -102
  402. package/test/shared-piece-store.test.js +310 -310
  403. package/test/sidecar-naming.test.js +142 -142
  404. package/test/source-file.test.js +133 -133
  405. package/test/source-files-route.test.js +103 -103
  406. package/test/source-registry.test.js +18 -18
  407. package/test/split-a-stretch.test.js +139 -139
  408. package/test/stale-request-after-seek.test.js +158 -158
  409. package/test/startup-readings.test.js +124 -124
  410. package/test/stream-route.test.js +182 -182
  411. package/test/subtitle-cue-framing.test.js +200 -200
  412. package/test/subtitle-cue-source.test.js +102 -102
  413. package/test/subtitle-cue-walk.test.js +370 -370
  414. package/test/subtitle-cursor.test.js +63 -63
  415. package/test/subtitle-defaults.test.js +118 -118
  416. package/test/subtitle-language.test.js +252 -252
  417. package/test/subtitle-track-numbering.test.js +386 -386
  418. package/test/supply-margin.test.js +177 -177
  419. package/test/swarm-follows-readers.test.js +174 -174
  420. package/test/swarm-reach.test.js +130 -130
  421. package/test/swarm-selection.test.js +220 -220
  422. package/test/tail-duplication.test.js +167 -167
  423. package/test/the-channel-has-both-ends.test.js +91 -91
  424. package/test/the-torrent-thread-serves-bytes.test.js +107 -107
  425. package/test/thread-pool.test.js +53 -53
  426. package/test/timeline.test.js +84 -84
  427. package/test/torrent-contents.test.js +235 -235
  428. package/test/torrent-cost.test.js +71 -71
  429. package/test/torrent-files.test.js +221 -221
  430. package/test/torrent-naming.test.js +255 -255
  431. package/test/tracks-begin-together.test.js +209 -209
  432. package/test/tunnel-renewal.test.js +167 -167
  433. package/test/two-viewers-one-picture.test.js +407 -407
  434. package/test/upload-hurry.test.js +186 -186
  435. package/test/usrsctp-state.test.js +131 -131
  436. package/test/video-facts.test.js +102 -102
  437. package/test/viewer-assignments.test.js +143 -143
  438. package/test/viewer-outputs.test.js +285 -285
  439. package/test/viewer-presence.test.js +116 -116
  440. package/test/viewer-requests-admission.test.js +89 -89
  441. package/test/viewer-states.test.js +189 -189
  442. package/test/viewer-subtitle-subscription.test.js +96 -96
  443. package/test/viewer.test.js +139 -139
  444. package/test/viewing-generations.test.js +390 -390
  445. package/test/visible-rung.test.js +37 -37
  446. package/test/wait-ledger.test.js +100 -100
  447. package/test/webrtc-shim.test.js +149 -149
  448. package/test/wedge-certainty.test.js +131 -131
  449. package/test/withdraw-piece-claim.test.js +212 -212
  450. package/test/worker-channel.test.js +158 -158
  451. package/utils/client-log-file.js +151 -151
  452. package/utils/logger.js +312 -312
  453. package/utils/parse-range.js +25 -25
  454. package/utils/perf.js +59 -59
  455. package/utils/torrent-source-key.js +38 -38
@@ -1,57 +1,57 @@
1
- /**
2
- * @file A session manager for a test, with a store of its own.
3
- *
4
- * ONE MANAGER, ONE ROOT. In production that is true by construction: one
5
- * process holds one manager, and the store's root is a fixed path so that a
6
- * restart can find what the process before it finished (`SegmentStore.sweep`).
7
- * Under `node --test` it is false — the runner starts one PROCESS PER FILE and
8
- * runs them in parallel, so every manager built without a store of its own
9
- * shared the one default root with every other file running at that moment.
10
- *
11
- * What that cost, measured 2026-09-14: `disposeAll` removes the store's whole
12
- * root, and four files call it between them 40 times. Running the twenty-one
13
- * files that build a manager gave 2, 3 and 4 failures on consecutive runs and
14
- * zero with `--test-concurrency=1`; the victim was always
15
- * `segment-serve-wiring.test.js`, reporting a segment missing that it had
16
- * written itself a moment earlier, because another process had deleted the
17
- * directory under it. `npm test` is `node --test`, so this was the project's
18
- * own test command, and it is why a red result had become something to read
19
- * past rather than to act on.
20
- *
21
- * A test that fails by luck is worse than one that does not exist: it costs a
22
- * run every time, and it trains everyone to ignore red.
23
- */
24
-
25
- import { mkdtempSync, rmSync } from "node:fs";
26
- import os from "node:os";
27
- import path from "node:path";
28
- import { wireOutputs } from "../../services/server/wire-outputs.js";
29
- import { SegmentStore } from "../../services/storage/segment-store/SegmentStore.js";
30
-
31
- /**
32
- * A manager whose produced segments live where nothing else can reach them.
33
- *
34
- * @param {object} [options] - Passed to the manager, over the defaults below.
35
- * A `segmentStore` given here wins, for a test that wants to watch the store
36
- * itself.
37
- * @returns {{ manager: object, store: SegmentStore, root: string,
38
- * cleanup: () => void }} `cleanup` removes the root; call it from `t.after`.
39
- */
40
- export function managerWithOwnStore(options = {}) {
41
- const root = mkdtempSync(path.join(os.tmpdir(), "ttv-store-"));
42
- const store = options.segmentStore ?? new SegmentStore({ root });
43
- const manager = wireOutputs({
44
- enabled: true,
45
- ffmpegBin: "ffmpeg",
46
- localBindHost: "127.0.0.1",
47
- localPort: 9090,
48
- ...options,
49
- segmentStore: store
50
- });
51
- return {
52
- manager,
53
- store,
54
- root,
55
- cleanup: () => rmSync(root, { recursive: true, force: true })
56
- };
57
- }
1
+ /**
2
+ * @file A session manager for a test, with a store of its own.
3
+ *
4
+ * ONE MANAGER, ONE ROOT. In production that is true by construction: one
5
+ * process holds one manager, and the store's root is a fixed path so that a
6
+ * restart can find what the process before it finished (`SegmentStore.sweep`).
7
+ * Under `node --test` it is false — the runner starts one PROCESS PER FILE and
8
+ * runs them in parallel, so every manager built without a store of its own
9
+ * shared the one default root with every other file running at that moment.
10
+ *
11
+ * What that cost, measured 2026-09-14: `disposeAll` removes the store's whole
12
+ * root, and four files call it between them 40 times. Running the twenty-one
13
+ * files that build a manager gave 2, 3 and 4 failures on consecutive runs and
14
+ * zero with `--test-concurrency=1`; the victim was always
15
+ * `segment-serve-wiring.test.js`, reporting a segment missing that it had
16
+ * written itself a moment earlier, because another process had deleted the
17
+ * directory under it. `npm test` is `node --test`, so this was the project's
18
+ * own test command, and it is why a red result had become something to read
19
+ * past rather than to act on.
20
+ *
21
+ * A test that fails by luck is worse than one that does not exist: it costs a
22
+ * run every time, and it trains everyone to ignore red.
23
+ */
24
+
25
+ import { mkdtempSync, rmSync } from "node:fs";
26
+ import os from "node:os";
27
+ import path from "node:path";
28
+ import { wireOutputs } from "../../services/server/wire-outputs.js";
29
+ import { SegmentStore } from "../../services/storage/segment-store/SegmentStore.js";
30
+
31
+ /**
32
+ * A manager whose produced segments live where nothing else can reach them.
33
+ *
34
+ * @param {object} [options] - Passed to the manager, over the defaults below.
35
+ * A `segmentStore` given here wins, for a test that wants to watch the store
36
+ * itself.
37
+ * @returns {{ manager: object, store: SegmentStore, root: string,
38
+ * cleanup: () => void }} `cleanup` removes the root; call it from `t.after`.
39
+ */
40
+ export function managerWithOwnStore(options = {}) {
41
+ const root = mkdtempSync(path.join(os.tmpdir(), "ttv-store-"));
42
+ const store = options.segmentStore ?? new SegmentStore({ root });
43
+ const manager = wireOutputs({
44
+ enabled: true,
45
+ ffmpegBin: "ffmpeg",
46
+ localBindHost: "127.0.0.1",
47
+ localPort: 9090,
48
+ ...options,
49
+ segmentStore: store
50
+ });
51
+ return {
52
+ manager,
53
+ store,
54
+ root,
55
+ cleanup: () => rmSync(root, { recursive: true, force: true })
56
+ };
57
+ }
@@ -1,63 +1,63 @@
1
- import { AudioOutput, CutGrid, OutputSpec, VideoOutput } from "../../services/encode/output/OutputSpec.js";
2
-
3
- /**
4
- * Build the output identity used by production objects in focused tests.
5
- *
6
- * @param {object} [params]
7
- * @param {string} [params.sourceKey]
8
- * @param {number} [params.fileIndex]
9
- * @param {boolean} [params.transcodeVideo]
10
- * @param {boolean} [params.transcodeAudio]
11
- * @param {boolean} [params.audioOnly]
12
- * @param {boolean} [params.audioSeparate]
13
- * @param {number} [params.audioFileIndex]
14
- * @param {number} [params.audioSourceTrackIndex]
15
- * @param {number} [params.width]
16
- * @param {number} [params.height]
17
- * @param {string} [params.encoder]
18
- * @param {number} [params.fps]
19
- * @param {string | null} [params.preset]
20
- * @param {boolean} [params.tonemap]
21
- * @param {{ maxrateKbps: number, bufsizeKbps: number, level: string | null } | null} [params.rateControl]
22
- * @param {"keyframe" | "uniform"} [params.cutGrid]
23
- * @param {string} [params.segmentFormatId]
24
- * @returns {OutputSpec}
25
- */
26
- export function outputSpec({
27
- sourceKey = "source-1",
28
- fileIndex = 0,
29
- transcodeVideo = false,
30
- transcodeAudio = true,
31
- audioOnly = false,
32
- audioSeparate = false,
33
- audioFileIndex = fileIndex,
34
- audioSourceTrackIndex = 0,
35
- width = 0,
36
- height = 0,
37
- encoder = "libx264",
38
- fps = 24,
39
- preset = null,
40
- tonemap = false,
41
- rateControl = null,
42
- cutGrid = "uniform",
43
- segmentFormatId = "fmp4"
44
- } = {}) {
45
- return new OutputSpec({
46
- sourceKey,
47
- segmentFormatId,
48
- grid: new CutGrid({ kind: cutGrid, fileIndex }),
49
- video: audioOnly
50
- ? null
51
- : new VideoOutput({
52
- fileIndex,
53
- encode: transcodeVideo ? { encoder, width, height, fps, preset, tonemap, rateControl } : null
54
- }),
55
- audio: audioSeparate
56
- ? null
57
- : new AudioOutput({
58
- fileIndex: audioFileIndex,
59
- trackIndex: audioSourceTrackIndex,
60
- transcode: transcodeAudio
61
- })
62
- });
63
- }
1
+ import { AudioOutput, CutGrid, OutputSpec, VideoOutput } from "../../services/encode/output/OutputSpec.js";
2
+
3
+ /**
4
+ * Build the output identity used by production objects in focused tests.
5
+ *
6
+ * @param {object} [params]
7
+ * @param {string} [params.sourceKey]
8
+ * @param {number} [params.fileIndex]
9
+ * @param {boolean} [params.transcodeVideo]
10
+ * @param {boolean} [params.transcodeAudio]
11
+ * @param {boolean} [params.audioOnly]
12
+ * @param {boolean} [params.audioSeparate]
13
+ * @param {number} [params.audioFileIndex]
14
+ * @param {number} [params.audioSourceTrackIndex]
15
+ * @param {number} [params.width]
16
+ * @param {number} [params.height]
17
+ * @param {string} [params.encoder]
18
+ * @param {number} [params.fps]
19
+ * @param {string | null} [params.preset]
20
+ * @param {boolean} [params.tonemap]
21
+ * @param {{ maxrateKbps: number, bufsizeKbps: number, level: string | null } | null} [params.rateControl]
22
+ * @param {"keyframe" | "uniform"} [params.cutGrid]
23
+ * @param {string} [params.segmentFormatId]
24
+ * @returns {OutputSpec}
25
+ */
26
+ export function outputSpec({
27
+ sourceKey = "source-1",
28
+ fileIndex = 0,
29
+ transcodeVideo = false,
30
+ transcodeAudio = true,
31
+ audioOnly = false,
32
+ audioSeparate = false,
33
+ audioFileIndex = fileIndex,
34
+ audioSourceTrackIndex = 0,
35
+ width = 0,
36
+ height = 0,
37
+ encoder = "libx264",
38
+ fps = 24,
39
+ preset = null,
40
+ tonemap = false,
41
+ rateControl = null,
42
+ cutGrid = "uniform",
43
+ segmentFormatId = "fmp4"
44
+ } = {}) {
45
+ return new OutputSpec({
46
+ sourceKey,
47
+ segmentFormatId,
48
+ grid: new CutGrid({ kind: cutGrid, fileIndex }),
49
+ video: audioOnly
50
+ ? null
51
+ : new VideoOutput({
52
+ fileIndex,
53
+ encode: transcodeVideo ? { encoder, width, height, fps, preset, tonemap, rateControl } : null
54
+ }),
55
+ audio: audioSeparate
56
+ ? null
57
+ : new AudioOutput({
58
+ fileIndex: audioFileIndex,
59
+ trackIndex: audioSourceTrackIndex,
60
+ transcode: transcodeAudio
61
+ })
62
+ });
63
+ }
@@ -1,68 +1,68 @@
1
- /**
2
- * @file The arithmetic that turns two readings of the machine into shares.
3
- *
4
- * The readings themselves are files this host may or may not have; what is
5
- * tested here is what is DERIVED from them, because that is where a wrong
6
- * number would quietly become a wrong conclusion about why an encoder is slow.
7
- */
8
-
9
- import assert from "node:assert/strict";
10
- import test from "node:test";
11
-
12
- import { readProcessCpuSeconds, readSystemCpu, sampleHost, shareOfMachine } from "../services/encode/host-load.js";
13
-
14
- test("a process using one core of four for a second reports a quarter of the machine", () => {
15
- const before = { takenAt: 1_000, processCpuSeconds: 10, system: null };
16
- const after = { takenAt: 2_000, processCpuSeconds: 11, system: null };
17
- const share = shareOfMachine(before, after, 4);
18
- assert.equal(share?.elapsedSec, 1);
19
- assert.equal(share?.processShare, 0.25);
20
- });
21
-
22
- test("a process using every core reports the whole machine", () => {
23
- const share = shareOfMachine(
24
- { takenAt: 0, processCpuSeconds: 0, system: null },
25
- { takenAt: 2_000, processCpuSeconds: 8, system: null },
26
- 4
27
- );
28
- assert.equal(share?.processShare, 1);
29
- });
30
-
31
- test("waiting for a disk is counted apart from working", () => {
32
- const before = { takenAt: 0, processCpuSeconds: null, system: { busySeconds: 100, idleSeconds: 900, iowaitSeconds: 10 } };
33
- const after = { takenAt: 1_000, processCpuSeconds: null, system: { busySeconds: 101, idleSeconds: 902, iowaitSeconds: 11 } };
34
- const share = shareOfMachine(before, after, 4);
35
- assert.equal(share?.systemShare, 0.25);
36
- assert.equal(share?.iowaitShare, 0.25);
37
- });
38
-
39
- test("two readings taken at the same instant say nothing rather than dividing by zero", () => {
40
- assert.equal(shareOfMachine({ takenAt: 5, processCpuSeconds: 1, system: null }, { takenAt: 5, processCpuSeconds: 2, system: null }, 4), null);
41
- });
42
-
43
- test("a missing reading leaves that share unknown, not zero", () => {
44
- const share = shareOfMachine(
45
- { takenAt: 0, processCpuSeconds: null, system: null },
46
- { takenAt: 1_000, processCpuSeconds: 1, system: null },
47
- 4
48
- );
49
- assert.equal(share?.processShare, null);
50
- assert.equal(share?.systemShare, null);
51
- });
52
-
53
- test("a process that does not exist reports nothing at all", async () => {
54
- assert.equal(await readProcessCpuSeconds(0), null);
55
- assert.equal(await readProcessCpuSeconds(-1), null);
56
- // A pid far above any real one on this machine.
57
- assert.equal(await readProcessCpuSeconds(4_000_000), null);
58
- });
59
-
60
- test("on a host without /proc the readings are null and the sampler still answers", async () => {
61
- // This runs on Linux in CI and on Windows here; both must be safe. What is
62
- // asserted is the SHAPE — a reading is either a number or null, never a throw.
63
- const system = await readSystemCpu();
64
- assert.ok(system === null || typeof system.busySeconds === "number");
65
- const sample = await sampleHost(null);
66
- assert.equal(typeof sample.takenAt, "number");
67
- assert.equal(sample.processCpuSeconds, null);
68
- });
1
+ /**
2
+ * @file The arithmetic that turns two readings of the machine into shares.
3
+ *
4
+ * The readings themselves are files this host may or may not have; what is
5
+ * tested here is what is DERIVED from them, because that is where a wrong
6
+ * number would quietly become a wrong conclusion about why an encoder is slow.
7
+ */
8
+
9
+ import assert from "node:assert/strict";
10
+ import test from "node:test";
11
+
12
+ import { readProcessCpuSeconds, readSystemCpu, sampleHost, shareOfMachine } from "../services/encode/host-load.js";
13
+
14
+ test("a process using one core of four for a second reports a quarter of the machine", () => {
15
+ const before = { takenAt: 1_000, processCpuSeconds: 10, system: null };
16
+ const after = { takenAt: 2_000, processCpuSeconds: 11, system: null };
17
+ const share = shareOfMachine(before, after, 4);
18
+ assert.equal(share?.elapsedSec, 1);
19
+ assert.equal(share?.processShare, 0.25);
20
+ });
21
+
22
+ test("a process using every core reports the whole machine", () => {
23
+ const share = shareOfMachine(
24
+ { takenAt: 0, processCpuSeconds: 0, system: null },
25
+ { takenAt: 2_000, processCpuSeconds: 8, system: null },
26
+ 4
27
+ );
28
+ assert.equal(share?.processShare, 1);
29
+ });
30
+
31
+ test("waiting for a disk is counted apart from working", () => {
32
+ const before = { takenAt: 0, processCpuSeconds: null, system: { busySeconds: 100, idleSeconds: 900, iowaitSeconds: 10 } };
33
+ const after = { takenAt: 1_000, processCpuSeconds: null, system: { busySeconds: 101, idleSeconds: 902, iowaitSeconds: 11 } };
34
+ const share = shareOfMachine(before, after, 4);
35
+ assert.equal(share?.systemShare, 0.25);
36
+ assert.equal(share?.iowaitShare, 0.25);
37
+ });
38
+
39
+ test("two readings taken at the same instant say nothing rather than dividing by zero", () => {
40
+ assert.equal(shareOfMachine({ takenAt: 5, processCpuSeconds: 1, system: null }, { takenAt: 5, processCpuSeconds: 2, system: null }, 4), null);
41
+ });
42
+
43
+ test("a missing reading leaves that share unknown, not zero", () => {
44
+ const share = shareOfMachine(
45
+ { takenAt: 0, processCpuSeconds: null, system: null },
46
+ { takenAt: 1_000, processCpuSeconds: 1, system: null },
47
+ 4
48
+ );
49
+ assert.equal(share?.processShare, null);
50
+ assert.equal(share?.systemShare, null);
51
+ });
52
+
53
+ test("a process that does not exist reports nothing at all", async () => {
54
+ assert.equal(await readProcessCpuSeconds(0), null);
55
+ assert.equal(await readProcessCpuSeconds(-1), null);
56
+ // A pid far above any real one on this machine.
57
+ assert.equal(await readProcessCpuSeconds(4_000_000), null);
58
+ });
59
+
60
+ test("on a host without /proc the readings are null and the sampler still answers", async () => {
61
+ // This runs on Linux in CI and on Windows here; both must be safe. What is
62
+ // asserted is the SHAPE — a reading is either a number or null, never a throw.
63
+ const system = await readSystemCpu();
64
+ assert.ok(system === null || typeof system.busySeconds === "number");
65
+ const sample = await sampleHost(null);
66
+ assert.equal(typeof sample.takenAt, "number");
67
+ assert.equal(sample.processCpuSeconds, null);
68
+ });
@@ -1,128 +1,128 @@
1
- /**
2
- * @file What a produced piece reveals, and WHOSE fact it is.
3
- *
4
- * One reading is taken — how far a produced piece began from where its grid
5
- * said it would — and three different facts are drawn from it, which the code
6
- * that draws them already stated in its own three branches:
7
- *
8
- * 1. a COPIED picture can only be cut where a keyframe already is, so a
9
- * disagreement says the container's keyframe TABLE describes times the file
10
- * does not have. A fact of the FILE;
11
- * 2. a RE-ENCODED rung was told to put a keyframe at that instant and did not,
12
- * so its pieces no longer stand where the stream it accompanies would have
13
- * put them. A fact of that OUTPUT;
14
- * 3. a SOUNDTRACK is cut exactly where it is asked to be, to within an audio
15
- * frame, so it says nothing about any keyframe at all — only that the
16
- * picture's grid has moved under a run already going. A fact of that OUTPUT.
17
- *
18
- * Until 2026-09-15 all three went into ONE tally, and the tally was on the cut
19
- * table — which is held per (file, grid), so a picture and the soundtrack muxed
20
- * into the same file shared it. Three consequences, and these checks are about
21
- * all of them: the file's finding died with a grid it does not belong to, one
22
- * output's reading of a piece number silenced another's reading of its own, and
23
- * the summary could not say which of the three facts it was summarising.
24
- */
25
-
26
- import test from "node:test";
27
- import assert from "node:assert/strict";
28
- import { Timeline, Timelines } from "../services/encode/output/Timeline.js";
29
- import { Output } from "../services/encode/output/Output.js";
30
- import { KeyframeTable } from "../services/media/container/KeyframeTable.js";
31
-
32
- const KEYFRAMES = [0, 4.004, 8.008, 12.012, 16.016];
33
- const TOLERANCE = 0.25;
34
-
35
- /**
36
- * @param {number[]} boundaries
37
- * @returns {Timeline}
38
- */
39
- function timelineOver(boundaries) {
40
- return new Timeline({ boundaries: [...boundaries], cutGrid: "keyframe" });
41
- }
42
-
43
- /**
44
- * @returns {Output}
45
- */
46
- function anOutput() {
47
- return new Output({
48
- encodeWidth: 0,
49
- encodeHeight: 0,
50
- outputFps: 25,
51
- softwarePreset: null,
52
- applyTonemap: false
53
- });
54
- }
55
-
56
- test("a picture and the soundtrack inside the same file share one cut table, and no longer one tally", () => {
57
- // Sharing the table is right — it is what makes them cut at the same instants
58
- // and is the whole reason it is held per file and grid. What must not be
59
- // shared is what each of them LEARNS, since the two learn different things.
60
- const tables = new Timelines();
61
- const key = Timelines.keyFor("torrent:abc", 3, "keyframe");
62
-
63
- const picture = tables.get(key, () => timelineOver(KEYFRAMES));
64
- const sound = tables.get(key, () => timelineOver(KEYFRAMES));
65
-
66
- assert.equal(sound, picture, "one table, so the cuts cannot drift apart");
67
- assert.equal(
68
- Object.keys(picture).some((field) => /check|tally|deviation|landing/i.test(field)),
69
- false,
70
- "and nothing on it counts what anybody found, which two outputs would have shared"
71
- );
72
- });
73
-
74
- test("two outputs each keep their own reading of the same piece number", () => {
75
- // `seen` exists so that a piece asked for twice is not counted twice — right
76
- // for one producer, and wrong across two. Shared, it dropped whichever of the
77
- // two finished second, so which reading survived was decided by a race.
78
- const picture = anOutput();
79
- const sound = anOutput();
80
-
81
- picture.noteLanding({ index: 7, deviationSec: 0.9, toleranceSec: TOLERANCE });
82
- sound.noteLanding({ index: 7, deviationSec: 0, toleranceSec: TOLERANCE });
83
-
84
- assert.equal(picture.landing.checked, 1);
85
- assert.equal(picture.landing.disagreed, 1, "the picture's piece did begin away from its grid");
86
- assert.equal(sound.landing.checked, 1, "and the sound's reading of its own #7 is not a repeat");
87
- assert.equal(sound.landing.disagreed, 0, "the sound was cut exactly where it was asked");
88
-
89
- // What `seen` is FOR, which the split must not lose: within one output a
90
- // piece is produced and served over and over while a viewer is refused it,
91
- // and the same piece is one reading however many times it is read.
92
- picture.noteLanding({ index: 7, deviationSec: 0.9, toleranceSec: TOLERANCE });
93
- picture.noteLanding({ index: 7, deviationSec: 0.9, toleranceSec: TOLERANCE });
94
-
95
- assert.equal(picture.landing.checked, 1, "its own repeat is still the same piece, counted once");
96
- assert.equal(picture.landing.disagreed, 1);
97
- });
98
-
99
- test("only a copy's reading reaches the file's table, and it outlives any one grid", () => {
100
- // The whole point of moving it: the table is one per FILE, so what a copy
101
- // showed about it is there for every step and every later session of those
102
- // bytes — while a tally on a cut table died with the grid, and a grid is not
103
- // what the reading is about.
104
- const table = new KeyframeTable().learn({ times: KEYFRAMES, format: "matroska" });
105
-
106
- table.witness({ index: 2, trueStart: 12.012, deviationSec: 4.004, toleranceSec: TOLERANCE });
107
-
108
- assert.equal(table.evidence.checked, 1);
109
- assert.equal(table.evidence.disagreed, 1);
110
- assert.equal(
111
- table.evidence.landedOnAnotherKeyframe,
112
- 1,
113
- "and the table itself says the piece began at another time it names — the grid was built over a gap"
114
- );
115
- });
116
-
117
- test("each tally answers one question, so a summary of it cannot be a mixture", () => {
118
- // The distinction the three branches make when they WARN is now kept in what
119
- // they count, so the summary can restate it — and the summary is what is read
120
- // when nobody was watching the log live.
121
- const sound = anOutput();
122
- const table = new KeyframeTable().learn({ times: KEYFRAMES, format: "matroska" });
123
-
124
- sound.noteLanding({ index: 1, deviationSec: 0.8, toleranceSec: TOLERANCE });
125
-
126
- assert.equal(sound.landing.disagreed, 1, "the sound's grid moved under its run, and that is all it says");
127
- assert.equal(table.evidence, null, "nothing a soundtrack produced is evidence about the file's keyframes");
128
- });
1
+ /**
2
+ * @file What a produced piece reveals, and WHOSE fact it is.
3
+ *
4
+ * One reading is taken — how far a produced piece began from where its grid
5
+ * said it would — and three different facts are drawn from it, which the code
6
+ * that draws them already stated in its own three branches:
7
+ *
8
+ * 1. a COPIED picture can only be cut where a keyframe already is, so a
9
+ * disagreement says the container's keyframe TABLE describes times the file
10
+ * does not have. A fact of the FILE;
11
+ * 2. a RE-ENCODED rung was told to put a keyframe at that instant and did not,
12
+ * so its pieces no longer stand where the stream it accompanies would have
13
+ * put them. A fact of that OUTPUT;
14
+ * 3. a SOUNDTRACK is cut exactly where it is asked to be, to within an audio
15
+ * frame, so it says nothing about any keyframe at all — only that the
16
+ * picture's grid has moved under a run already going. A fact of that OUTPUT.
17
+ *
18
+ * Until 2026-09-15 all three went into ONE tally, and the tally was on the cut
19
+ * table — which is held per (file, grid), so a picture and the soundtrack muxed
20
+ * into the same file shared it. Three consequences, and these checks are about
21
+ * all of them: the file's finding died with a grid it does not belong to, one
22
+ * output's reading of a piece number silenced another's reading of its own, and
23
+ * the summary could not say which of the three facts it was summarising.
24
+ */
25
+
26
+ import test from "node:test";
27
+ import assert from "node:assert/strict";
28
+ import { Timeline, Timelines } from "../services/encode/output/Timeline.js";
29
+ import { Output } from "../services/encode/output/Output.js";
30
+ import { KeyframeTable } from "../services/media/container/KeyframeTable.js";
31
+
32
+ const KEYFRAMES = [0, 4.004, 8.008, 12.012, 16.016];
33
+ const TOLERANCE = 0.25;
34
+
35
+ /**
36
+ * @param {number[]} boundaries
37
+ * @returns {Timeline}
38
+ */
39
+ function timelineOver(boundaries) {
40
+ return new Timeline({ boundaries: [...boundaries], cutGrid: "keyframe" });
41
+ }
42
+
43
+ /**
44
+ * @returns {Output}
45
+ */
46
+ function anOutput() {
47
+ return new Output({
48
+ encodeWidth: 0,
49
+ encodeHeight: 0,
50
+ outputFps: 25,
51
+ softwarePreset: null,
52
+ applyTonemap: false
53
+ });
54
+ }
55
+
56
+ test("a picture and the soundtrack inside the same file share one cut table, and no longer one tally", () => {
57
+ // Sharing the table is right — it is what makes them cut at the same instants
58
+ // and is the whole reason it is held per file and grid. What must not be
59
+ // shared is what each of them LEARNS, since the two learn different things.
60
+ const tables = new Timelines();
61
+ const key = Timelines.keyFor("torrent:abc", 3, "keyframe");
62
+
63
+ const picture = tables.get(key, () => timelineOver(KEYFRAMES));
64
+ const sound = tables.get(key, () => timelineOver(KEYFRAMES));
65
+
66
+ assert.equal(sound, picture, "one table, so the cuts cannot drift apart");
67
+ assert.equal(
68
+ Object.keys(picture).some((field) => /check|tally|deviation|landing/i.test(field)),
69
+ false,
70
+ "and nothing on it counts what anybody found, which two outputs would have shared"
71
+ );
72
+ });
73
+
74
+ test("two outputs each keep their own reading of the same piece number", () => {
75
+ // `seen` exists so that a piece asked for twice is not counted twice — right
76
+ // for one producer, and wrong across two. Shared, it dropped whichever of the
77
+ // two finished second, so which reading survived was decided by a race.
78
+ const picture = anOutput();
79
+ const sound = anOutput();
80
+
81
+ picture.noteLanding({ index: 7, deviationSec: 0.9, toleranceSec: TOLERANCE });
82
+ sound.noteLanding({ index: 7, deviationSec: 0, toleranceSec: TOLERANCE });
83
+
84
+ assert.equal(picture.landing.checked, 1);
85
+ assert.equal(picture.landing.disagreed, 1, "the picture's piece did begin away from its grid");
86
+ assert.equal(sound.landing.checked, 1, "and the sound's reading of its own #7 is not a repeat");
87
+ assert.equal(sound.landing.disagreed, 0, "the sound was cut exactly where it was asked");
88
+
89
+ // What `seen` is FOR, which the split must not lose: within one output a
90
+ // piece is produced and served over and over while a viewer is refused it,
91
+ // and the same piece is one reading however many times it is read.
92
+ picture.noteLanding({ index: 7, deviationSec: 0.9, toleranceSec: TOLERANCE });
93
+ picture.noteLanding({ index: 7, deviationSec: 0.9, toleranceSec: TOLERANCE });
94
+
95
+ assert.equal(picture.landing.checked, 1, "its own repeat is still the same piece, counted once");
96
+ assert.equal(picture.landing.disagreed, 1);
97
+ });
98
+
99
+ test("only a copy's reading reaches the file's table, and it outlives any one grid", () => {
100
+ // The whole point of moving it: the table is one per FILE, so what a copy
101
+ // showed about it is there for every step and every later session of those
102
+ // bytes — while a tally on a cut table died with the grid, and a grid is not
103
+ // what the reading is about.
104
+ const table = new KeyframeTable().learn({ times: KEYFRAMES, format: "matroska" });
105
+
106
+ table.witness({ index: 2, trueStart: 12.012, deviationSec: 4.004, toleranceSec: TOLERANCE });
107
+
108
+ assert.equal(table.evidence.checked, 1);
109
+ assert.equal(table.evidence.disagreed, 1);
110
+ assert.equal(
111
+ table.evidence.landedOnAnotherKeyframe,
112
+ 1,
113
+ "and the table itself says the piece began at another time it names — the grid was built over a gap"
114
+ );
115
+ });
116
+
117
+ test("each tally answers one question, so a summary of it cannot be a mixture", () => {
118
+ // The distinction the three branches make when they WARN is now kept in what
119
+ // they count, so the summary can restate it — and the summary is what is read
120
+ // when nobody was watching the log live.
121
+ const sound = anOutput();
122
+ const table = new KeyframeTable().learn({ times: KEYFRAMES, format: "matroska" });
123
+
124
+ sound.noteLanding({ index: 1, deviationSec: 0.8, toleranceSec: TOLERANCE });
125
+
126
+ assert.equal(sound.landing.disagreed, 1, "the sound's grid moved under its run, and that is all it says");
127
+ assert.equal(table.evidence, null, "nothing a soundtrack produced is evidence about the file's keyframes");
128
+ });