@torrent-tv/proxy 2.87.20 → 2.88.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 (474) 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 +8 -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/browser-proxy-contract.md +133 -133
  23. package/docs/container-architecture.md +218 -218
  24. package/docs/download-architecture.md +261 -261
  25. package/docs/encode-architecture.md +729 -729
  26. package/docs/encode-run-state.md +96 -96
  27. package/docs/logs.md +97 -97
  28. package/docs/socks5-egress-research.md +114 -114
  29. package/docs/storage-architecture.md +267 -267
  30. package/knip.json +14 -14
  31. package/openspec/changes/adaptive-bitrate/design.md +92 -92
  32. package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
  33. package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
  34. package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
  35. package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
  36. package/openspec/changes/binary-distribution/proposal.md +53 -53
  37. package/openspec/changes/chunked-request-bodies/design.md +81 -81
  38. package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
  39. package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
  40. package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
  41. package/openspec/changes/cold-start/design.md +98 -98
  42. package/openspec/changes/cold-start/proposal.md +62 -62
  43. package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
  44. package/openspec/changes/cold-start/tasks.md +48 -48
  45. package/openspec/changes/disk-cap/.openspec.yaml +2 -2
  46. package/openspec/changes/disk-cap/proposal.md +37 -37
  47. package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
  48. package/openspec/changes/disk-cap/tasks.md +19 -19
  49. package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
  50. package/openspec/changes/proxy-observability/design.md +38 -38
  51. package/openspec/changes/proxy-observability/proposal.md +49 -49
  52. package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
  53. package/openspec/changes/proxy-observability/tasks.md +21 -21
  54. package/openspec/changes/seek-debounce/design.md +118 -118
  55. package/openspec/changes/seek-debounce/proposal.md +59 -59
  56. package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
  57. package/openspec/changes/seek-debounce/tasks.md +33 -33
  58. package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
  59. package/openspec/changes/subtitle-language/proposal.md +50 -50
  60. package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
  61. package/openspec/changes/subtitle-language/tasks.md +26 -26
  62. package/openspec/changes/track-selection/.openspec.yaml +2 -2
  63. package/openspec/changes/track-selection/design.md +47 -47
  64. package/openspec/changes/track-selection/proposal.md +45 -45
  65. package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
  66. package/openspec/changes/track-selection/tasks.md +20 -20
  67. package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
  68. package/openspec/changes/transcode-quality/proposal.md +51 -51
  69. package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
  70. package/openspec/changes/transcode-quality/tasks.md +63 -63
  71. package/openspec/config.yaml +38 -38
  72. package/package.json +50 -50
  73. package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
  74. package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
  75. package/research/double-spawn-2026-09-10.md +171 -171
  76. package/research/handover-2026-09-13.md +209 -209
  77. package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
  78. package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
  79. package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
  80. package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
  81. package/routes/api/client-logs/post.js +86 -86
  82. package/routes/api/delivery-sink/get.js +72 -72
  83. package/routes/api/link-probe/get.js +51 -51
  84. package/routes/api/playback-plan/audio-tracks/post.js +29 -29
  85. package/routes/api/playback-plan/post.js +53 -53
  86. package/routes/api/sources/files/get.js +106 -100
  87. package/routes/api/sources/post.js +36 -36
  88. package/routes/api/sources/stats/get.js +132 -132
  89. package/routes/api/sources/warm/post.js +217 -217
  90. package/routes/api/subtitles/get.js +230 -230
  91. package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
  92. package/routes/api/transcode-sessions/net-report/post.js +138 -138
  93. package/routes/api/transcode-sessions/post.js +173 -173
  94. package/routes/api/transcode-sessions/progress/get.js +27 -27
  95. package/routes/api/transcode-sessions/release/post.js +41 -41
  96. package/routes/api/transcode-sessions/seek/post.js +63 -63
  97. package/routes/health/get.js +13 -13
  98. package/routes/healthz/get.js +13 -13
  99. package/routes/stream/get.js +380 -380
  100. package/routes/transcode/audio-file/get.js +64 -64
  101. package/routes/transcode/audio-warm/get.js +84 -84
  102. package/routes/transcode/session-file/get.js +52 -52
  103. package/routes/transcode/variant-file/get.js +95 -95
  104. package/routes/transcode/variant-warm/get.js +84 -84
  105. package/scripts/render-run-graph.js +173 -173
  106. package/server.js +725 -725
  107. package/services/encode/CoverageMap.js +428 -428
  108. package/services/encode/CushionReport.js +345 -345
  109. package/services/encode/EncodeAdmission.js +376 -376
  110. package/services/encode/EncodeOrchestrator.js +1217 -1217
  111. package/services/encode/EncodePlan.js +1366 -1366
  112. package/services/encode/EncodeRun.js +813 -813
  113. package/services/encode/EncodeRuns.js +1235 -1235
  114. package/services/encode/Encoder.js +142 -142
  115. package/services/encode/EncoderSelection.js +38 -38
  116. package/services/encode/LocalObservations.js +359 -359
  117. package/services/encode/NvencEncoder.js +50 -50
  118. package/services/encode/OutputOpening.js +758 -758
  119. package/services/encode/OutputTimes.js +579 -579
  120. package/services/encode/QsvEncoder.js +58 -58
  121. package/services/encode/Renditions.js +2184 -2184
  122. package/services/encode/RunProgress.js +84 -84
  123. package/services/encode/SegmentDemand.js +95 -95
  124. package/services/encode/SoftwareEncoder.js +121 -121
  125. package/services/encode/V4l2m2mEncoder.js +53 -53
  126. package/services/encode/VaapiEncoder.js +66 -66
  127. package/services/encode/args.js +416 -416
  128. package/services/encode/available-share.js +89 -89
  129. package/services/encode/calibration.js +361 -361
  130. package/services/encode/contention.js +128 -128
  131. package/services/encode/decode-cost-fit.js +363 -363
  132. package/services/encode/encode-exit.js +114 -114
  133. package/services/encode/encode-run-state.js +551 -551
  134. package/services/encode/encoder-readings.js +45 -45
  135. package/services/encode/fingerprint.js +322 -322
  136. package/services/encode/host-load.js +174 -174
  137. package/services/encode/hwaccel.js +1894 -1894
  138. package/services/encode/index.js +9 -9
  139. package/services/encode/learned-median.js +90 -90
  140. package/services/encode/output/EncodedOutput.js +57 -57
  141. package/services/encode/output/Output.js +131 -131
  142. package/services/encode/output/OutputCatalog.js +414 -414
  143. package/services/encode/output/OutputSpec.js +390 -390
  144. package/services/encode/output/Timeline.js +308 -308
  145. package/services/encode/output/cut-grid.js +107 -107
  146. package/services/encode/output/index.js +20 -20
  147. package/services/encode/output/ladder.js +32 -32
  148. package/services/encode/output/playlists.js +284 -284
  149. package/services/encode/output/rates.js +125 -125
  150. package/services/encode/output-key-format.js +21 -21
  151. package/services/encode/quality/EncodeCost.js +1141 -1141
  152. package/services/encode/quality/HostLoad.js +525 -525
  153. package/services/encode/quality/HostTimings.js +249 -249
  154. package/services/encode/quality/OutputQualityState.js +28 -28
  155. package/services/encode/quality/QualityController.js +935 -935
  156. package/services/encode/quality/QualityOffer.js +416 -416
  157. package/services/encode/quality/drain-threat.js +47 -47
  158. package/services/encode/quality/link-budget.js +254 -254
  159. package/services/encode/quality/output-format.js +315 -315
  160. package/services/encode/quality/serving-output.js +216 -216
  161. package/services/encode/quality/visible-rung.js +57 -57
  162. package/services/encode/run-budget.js +97 -97
  163. package/services/encode/run-command.js +754 -754
  164. package/services/encode/run-costs.js +155 -155
  165. package/services/encode/run-inputs.js +42 -42
  166. package/services/encode/segment-formats/fmp4.js +456 -456
  167. package/services/encode/segment-formats/index.js +85 -85
  168. package/services/encode/segment-formats/init-compat.js +324 -324
  169. package/services/encode/segment-formats/mp4-boxes.js +515 -515
  170. package/services/encode/segment-formats/mpegts.js +134 -134
  171. package/services/encode/start-stop-cost.js +178 -178
  172. package/services/encode/throughput.js +97 -97
  173. package/services/encode/torrent-cost.js +88 -88
  174. package/services/identity/installation-id.js +38 -38
  175. package/services/media/ContainerOrchestrator.js +149 -149
  176. package/services/media/KeyframeTables.js +316 -316
  177. package/services/media/SourceFile.js +298 -298
  178. package/services/media/SubtitleCues.js +520 -520
  179. package/services/media/SubtitleOrchestrator.js +167 -167
  180. package/services/media/audio-inventory.js +245 -245
  181. package/services/media/container/AviContainer.js +266 -266
  182. package/services/media/container/Container.js +603 -603
  183. package/services/media/container/ContainerFactory.js +56 -56
  184. package/services/media/container/KeyframeTable.js +272 -272
  185. package/services/media/container/MatroskaContainer.js +1494 -1494
  186. package/services/media/container/Mp4Container.js +1244 -1244
  187. package/services/media/container/SubtitleFileContainer.js +322 -322
  188. package/services/media/container/ebml-reader.js +159 -159
  189. package/services/media/ffmpeg-banner.js +260 -260
  190. package/services/media/keyframe-probe.js +157 -157
  191. package/services/media/media-info-probe.js +93 -93
  192. package/services/media/playback-planner.js +931 -931
  193. package/services/media/tracks/AudioTrack.js +131 -131
  194. package/services/media/tracks/ContainerTrack.js +75 -75
  195. package/services/media/tracks/ImageSubtitleTrack.js +19 -19
  196. package/services/media/tracks/SubtitleTrack.js +38 -38
  197. package/services/media/tracks/TextSubtitleTrack.js +287 -287
  198. package/services/media/tracks/VideoTrack.js +33 -33
  199. package/services/media/tracks/index.js +15 -15
  200. package/services/media/tracks/language-detect.js +174 -174
  201. package/services/server/OutputLifecycle.js +418 -418
  202. package/services/server/SegmentServing.js +1150 -1150
  203. package/services/server/ViewerRequests.js +760 -760
  204. package/services/server/WaitLedger.js +142 -142
  205. package/services/server/controllers/PlaybackController.js +40 -40
  206. package/services/server/controllers/SubtitleController.js +109 -109
  207. package/services/server/http-responses.js +60 -60
  208. package/services/server/playback-readiness.js +696 -696
  209. package/services/server/transcode-session-files.js +325 -325
  210. package/services/server/wire-outputs.js +992 -992
  211. package/services/storage/Diagnostics.js +202 -202
  212. package/services/storage/MachineBudget.js +235 -235
  213. package/services/storage/allowance.js +107 -107
  214. package/services/storage/core-dumps.js +106 -106
  215. package/services/storage/files/CompletedFiles.js +391 -391
  216. package/services/storage/files/piece-from-whole-file.js +118 -118
  217. package/services/storage/free.js +71 -71
  218. package/services/storage/keep.js +48 -48
  219. package/services/storage/machine-memory.js +56 -56
  220. package/services/storage/memory-report.js +575 -575
  221. package/services/storage/piece-store/piece-disk-store.js +539 -539
  222. package/services/storage/piece-store/piece-lru.js +495 -495
  223. package/services/storage/piece-store/shared-piece-store.js +1931 -1908
  224. package/services/storage/returns.js +103 -103
  225. package/services/storage/segment-store/SegmentStore.js +1240 -1240
  226. package/services/storage/wire.js +175 -175
  227. package/services/torrent/Contents.js +382 -324
  228. package/services/torrent/demand/DemandRegister.js +202 -202
  229. package/services/torrent/demand/Urgency.js +137 -137
  230. package/services/torrent/demand/Window.js +118 -118
  231. package/services/torrent/demand/index.js +8 -8
  232. package/services/torrent/demand/pieces.js +140 -140
  233. package/services/torrent/download/SwarmSelection.js +386 -386
  234. package/services/torrent/download/registry.js +102 -102
  235. package/services/torrent/download/withdraw-claim.js +80 -80
  236. package/services/torrent/episode-naming.js +182 -0
  237. package/services/torrent/files.js +455 -455
  238. package/services/torrent/naming.js +619 -619
  239. package/services/torrent/piece-store-of.js +29 -29
  240. package/services/torrent/supply-margin.js +262 -262
  241. package/services/torrent/torrent-pool.js +2675 -2675
  242. package/services/torrent/worker/background-fill.js +213 -213
  243. package/services/torrent/worker/channel.js +271 -271
  244. package/services/torrent/worker/client.js +765 -765
  245. package/services/torrent/worker/fastest-wires.js +342 -342
  246. package/services/torrent/worker/handle-state.js +23 -23
  247. package/services/torrent/worker/held-bytes.js +155 -155
  248. package/services/torrent/worker/install-webrtc-shim.js +32 -32
  249. package/services/torrent/worker/piece-reader.js +1078 -1078
  250. package/services/torrent/worker/pool-adapter.js +371 -371
  251. package/services/torrent/worker/protocol.js +184 -184
  252. package/services/torrent/worker/resume-warm.js +107 -107
  253. package/services/torrent/worker/webrtc-shim.js +219 -219
  254. package/services/torrent/worker/worker.js +1129 -1129
  255. package/services/transport/data-channel-handler.js +1369 -1369
  256. package/services/transport/delivery-probe.js +842 -842
  257. package/services/transport/health-collector.js +53 -53
  258. package/services/transport/nat-classifier.js +231 -231
  259. package/services/transport/packet-witness.js +808 -808
  260. package/services/transport/port-mapper.js +235 -235
  261. package/services/transport/registry-api.js +81 -81
  262. package/services/transport/tunnel-client.js +447 -447
  263. package/services/transport/usrsctp-state.js +175 -175
  264. package/services/transport/webrtc-manager.js +484 -484
  265. package/services/viewer/Assignments.js +340 -340
  266. package/services/viewer/PriorityMap.js +390 -390
  267. package/services/viewer/PriorityOrchestrator.js +325 -325
  268. package/services/viewer/Viewer.js +691 -691
  269. package/services/viewer/Viewers.js +368 -368
  270. package/services/viewer/active-output.js +37 -37
  271. package/services/viewer/audio-start.js +46 -46
  272. package/services/viewer/buffer-trend.js +81 -81
  273. package/services/viewer/choices.js +647 -647
  274. package/services/viewer/positions.js +126 -126
  275. package/services/viewer/report-intake.js +46 -46
  276. package/store/source-registry.js +71 -71
  277. package/test/asset-paths.test.js +28 -28
  278. package/test/audio-inventory.test.js +212 -212
  279. package/test/audio-track-bitrate.test.js +40 -40
  280. package/test/auto-quality-step.test.js +707 -707
  281. package/test/available-share.test.js +76 -76
  282. package/test/background-fill.test.js +190 -190
  283. package/test/client-log-file.test.js +110 -110
  284. package/test/completed-files.test.js +115 -115
  285. package/test/concurrent-cost.test.js +148 -148
  286. package/test/container-media-info.test.js +228 -228
  287. package/test/contention.test.js +76 -76
  288. package/test/core-dumps.test.js +57 -57
  289. package/test/coverage-follows-the-disk.test.js +228 -228
  290. package/test/coverage-map.test.js +195 -195
  291. package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
  292. package/test/cut-times-timeline.test.js +65 -65
  293. package/test/cuts-follow-published-grid.test.js +109 -109
  294. package/test/data-channel-frame.test.js +63 -63
  295. package/test/data-channel-viewer-lifecycle.test.js +92 -92
  296. package/test/dead-torrent-handle.test.js +52 -52
  297. package/test/declared-rates.test.js +116 -116
  298. package/test/declared-tracks.test.js +43 -43
  299. package/test/decode-cost-fit.test.js +202 -202
  300. package/test/decode-cost.test.js +462 -462
  301. package/test/delivery-probe.test.js +393 -393
  302. package/test/demand-register.test.js +206 -206
  303. package/test/dht-bootstrap.test.js +72 -72
  304. package/test/drain-threat.test.js +65 -65
  305. package/test/duplicate-add.test.js +48 -48
  306. package/test/encode-admission.test.js +146 -146
  307. package/test/encode-cost.test.js +283 -283
  308. package/test/encode-exit.test.js +111 -111
  309. package/test/encode-orchestrator.test.js +419 -419
  310. package/test/encode-plan-viewers.test.js +718 -718
  311. package/test/encode-plan.test.js +545 -545
  312. package/test/encode-run-state.test.js +314 -314
  313. package/test/encode-run.test.js +162 -162
  314. package/test/encode-slope.test.js +46 -46
  315. package/test/encoded-output.test.js +39 -39
  316. package/test/encoder-kinds.test.js +122 -122
  317. package/test/encoder-readings.test.js +46 -46
  318. package/test/episode-naming.test.js +131 -0
  319. package/test/every-claimant-is-told-its-share.test.js +152 -152
  320. package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
  321. package/test/failed-start-breaker.test.js +202 -202
  322. package/test/fastest-wires.test.js +143 -143
  323. package/test/ffmpeg-stream-counts.test.js +78 -78
  324. package/test/file-edges.test.js +191 -191
  325. package/test/fmp4-presentation-clock.test.js +147 -147
  326. package/test/fmp4-self-contained.test.js +80 -80
  327. package/test/fmp4-timestamps.test.js +140 -140
  328. package/test/fmp4-track-completeness.test.js +41 -41
  329. package/test/format-keyed-outputs.test.js +78 -78
  330. package/test/health-metrics.test.js +38 -38
  331. package/test/held-request-width.test.js +157 -157
  332. package/test/helpers/encode-run.js +136 -136
  333. package/test/helpers/held-file.js +29 -29
  334. package/test/helpers/manager.js +57 -57
  335. package/test/helpers/output-spec.js +63 -63
  336. package/test/host-load.test.js +68 -68
  337. package/test/index-accuracy-is-about-the-file.test.js +128 -128
  338. package/test/init-compat.test.js +219 -219
  339. package/test/input-lost-quiets-the-plan.test.js +261 -261
  340. package/test/input-unavailable.test.js +48 -48
  341. package/test/installation-id.test.js +39 -39
  342. package/test/keeping-period.test.js +83 -83
  343. package/test/keyframe-index-accuracy.test.js +188 -188
  344. package/test/keyframe-table-budget.test.js +102 -102
  345. package/test/keyframes-belong-to-the-file.test.js +249 -249
  346. package/test/learned-median.test.js +37 -37
  347. package/test/letting-go-asks-who-is-watching.test.js +191 -191
  348. package/test/limit-row-by-area.test.js +60 -60
  349. package/test/link-budget.test.js +111 -111
  350. package/test/link-probe.test.js +56 -56
  351. package/test/local-encoding-data.test.js +273 -273
  352. package/test/logger-repeats.test.js +134 -134
  353. package/test/look-ahead-coverage.test.js +56 -56
  354. package/test/manager-surface.test.js +292 -292
  355. package/test/matroska-cues-track.test.js +192 -192
  356. package/test/memory-budget.test.js +390 -390
  357. package/test/move-cost.test.js +216 -216
  358. package/test/mp4-subtitles.test.js +173 -173
  359. package/test/one-authority.test.js +285 -285
  360. package/test/one-budget.test.js +149 -149
  361. package/test/one-output-one-session.test.js +204 -204
  362. package/test/one-resource-divided.test.js +185 -185
  363. package/test/one-serving-output-per-height.test.js +458 -458
  364. package/test/orchestrator-wired.test.js +206 -206
  365. package/test/output-catalog.test.js +39 -39
  366. package/test/output-shape.test.js +21 -21
  367. package/test/output-spec.test.js +251 -251
  368. package/test/output-speed.test.js +88 -88
  369. package/test/outputs-being-prepared.test.js +40 -40
  370. package/test/packet-witness-ring.test.js +236 -236
  371. package/test/packet-witness.test.js +148 -148
  372. package/test/piece-disk-store.test.js +401 -401
  373. package/test/piece-from-whole-file.test.js +129 -129
  374. package/test/piece-lru.test.js +319 -319
  375. package/test/piece-reader.test.js +162 -162
  376. package/test/piece-selection-offset.test.js +85 -85
  377. package/test/piece-store-eviction.test.js +595 -595
  378. package/test/piece-store-never-refuses.test.js +153 -153
  379. package/test/piece-store-reservations.test.js +403 -365
  380. package/test/piece-store-slow-disk.test.js +159 -159
  381. package/test/piece-tail.test.js +97 -97
  382. package/test/plan-host-timings.test.js +68 -68
  383. package/test/plan-is-steady.test.js +225 -225
  384. package/test/playback-readiness.test.js +453 -453
  385. package/test/playback-start-regressions.test.js +99 -99
  386. package/test/priority-map-download.test.js +174 -174
  387. package/test/priority-map-emptied.test.js +204 -204
  388. package/test/priority-map-per-output.test.js +215 -215
  389. package/test/priority-map.test.js +163 -163
  390. package/test/probe-wedge-certainty.test.js +60 -60
  391. package/test/produced-copy-choice.test.js +213 -213
  392. package/test/published-timeline.test.js +83 -83
  393. package/test/quality-offer.test.js +162 -162
  394. package/test/quality-variants.test.js +1229 -1229
  395. package/test/rate-control-in-the-output.test.js +180 -180
  396. package/test/read-survives-withdrawal.test.js +164 -164
  397. package/test/read-window.test.js +365 -365
  398. package/test/resume-warm.test.js +39 -39
  399. package/test/run-graph-drift.test.js +37 -37
  400. package/test/run-inputs.test.js +80 -80
  401. package/test/run-intervals.test.js +100 -100
  402. package/test/run-position-follows-published-grid.test.js +89 -89
  403. package/test/run-progress.test.js +65 -65
  404. package/test/same-height-switch.test.js +664 -664
  405. package/test/seek-frees-the-viewers-requests.test.js +217 -217
  406. package/test/seek-landing.test.js +109 -109
  407. package/test/seek-target-not-superseded.test.js +195 -195
  408. package/test/segment-closed-on-both-branches.test.js +96 -96
  409. package/test/segment-cut-times.test.js +63 -63
  410. package/test/segment-demand.test.js +80 -80
  411. package/test/segment-hold-supersede.test.js +91 -91
  412. package/test/segment-judged-by-its-own-header.test.js +221 -221
  413. package/test/segment-serve-wiring.test.js +475 -475
  414. package/test/segment-store-eviction.test.js +247 -247
  415. package/test/segment-store.test.js +323 -323
  416. package/test/segment-true-start.test.js +86 -86
  417. package/test/segments-are-shared.test.js +176 -176
  418. package/test/send-chunk-bytes.test.js +86 -86
  419. package/test/serving-existing-output.test.js +102 -102
  420. package/test/serving-output.test.js +102 -102
  421. package/test/shared-piece-store.test.js +310 -310
  422. package/test/sidecar-naming.test.js +142 -142
  423. package/test/source-file.test.js +133 -133
  424. package/test/source-files-route.test.js +12 -2
  425. package/test/source-registry.test.js +18 -18
  426. package/test/split-a-stretch.test.js +139 -139
  427. package/test/stale-request-after-seek.test.js +158 -158
  428. package/test/startup-readings.test.js +124 -124
  429. package/test/stream-route.test.js +209 -209
  430. package/test/subtitle-cue-framing.test.js +200 -200
  431. package/test/subtitle-cue-source.test.js +102 -102
  432. package/test/subtitle-cue-walk.test.js +370 -370
  433. package/test/subtitle-cursor.test.js +63 -63
  434. package/test/subtitle-defaults.test.js +118 -118
  435. package/test/subtitle-language.test.js +252 -252
  436. package/test/subtitle-track-numbering.test.js +386 -386
  437. package/test/supply-margin.test.js +177 -177
  438. package/test/swarm-follows-readers.test.js +174 -174
  439. package/test/swarm-reach.test.js +130 -130
  440. package/test/swarm-selection.test.js +220 -220
  441. package/test/tail-duplication.test.js +167 -167
  442. package/test/the-channel-has-both-ends.test.js +91 -91
  443. package/test/the-torrent-thread-serves-bytes.test.js +107 -107
  444. package/test/thread-pool.test.js +53 -53
  445. package/test/timeline.test.js +84 -84
  446. package/test/torrent-contents.test.js +235 -235
  447. package/test/torrent-cost.test.js +71 -71
  448. package/test/torrent-files.test.js +221 -221
  449. package/test/torrent-naming.test.js +255 -255
  450. package/test/tracks-begin-together.test.js +209 -209
  451. package/test/tunnel-renewal.test.js +167 -167
  452. package/test/two-viewers-one-picture.test.js +407 -407
  453. package/test/upload-hurry.test.js +186 -186
  454. package/test/usrsctp-state.test.js +131 -131
  455. package/test/video-facts.test.js +102 -102
  456. package/test/viewer-assignments.test.js +143 -143
  457. package/test/viewer-outputs.test.js +285 -285
  458. package/test/viewer-presence.test.js +116 -116
  459. package/test/viewer-requests-admission.test.js +89 -89
  460. package/test/viewer-states.test.js +189 -189
  461. package/test/viewer-subtitle-subscription.test.js +96 -96
  462. package/test/viewer.test.js +139 -139
  463. package/test/viewing-generations.test.js +390 -390
  464. package/test/visible-rung.test.js +37 -37
  465. package/test/wait-ledger.test.js +100 -100
  466. package/test/webrtc-shim.test.js +149 -149
  467. package/test/wedge-certainty.test.js +131 -131
  468. package/test/withdraw-piece-claim.test.js +212 -212
  469. package/test/worker-channel.test.js +158 -158
  470. package/utils/client-log-file.js +151 -151
  471. package/utils/logger.js +312 -312
  472. package/utils/parse-range.js +25 -25
  473. package/utils/perf.js +59 -59
  474. package/utils/torrent-source-key.js +38 -38
@@ -1,29 +1,29 @@
1
- /**
2
- * @file The piece store behind a torrent, reached through what it does.
3
- *
4
- * WebTorrent wraps the store it was given in stores of its own, each holding
5
- * the next one as `store`, so the store the torrent component talks to is found
6
- * by walking that chain. It is recognised by the interface this component uses
7
- * — which pieces the readers have asked to keep (`protectedRanges`) — and not by
8
- * its class: the torrent component does not import the storage component's
9
- * implementation, it is handed it where the thread is assembled
10
- * (`worker/worker.js`).
11
- */
12
-
13
- /** How deep WebTorrent's own wrappers are allowed to go before the walk gives up. */
14
- const WRAPPER_DEPTH = 8;
15
-
16
- /**
17
- * @param {{ store?: object } | null | undefined} torrent
18
- * @returns {object | null}
19
- */
20
- export function pieceStoreOf(torrent) {
21
- let candidate = torrent?.store;
22
- for (let depth = 0; candidate && depth < WRAPPER_DEPTH; depth += 1) {
23
- if (typeof candidate.protectedRanges === "function") {
24
- return candidate;
25
- }
26
- candidate = candidate.store;
27
- }
28
- return null;
29
- }
1
+ /**
2
+ * @file The piece store behind a torrent, reached through what it does.
3
+ *
4
+ * WebTorrent wraps the store it was given in stores of its own, each holding
5
+ * the next one as `store`, so the store the torrent component talks to is found
6
+ * by walking that chain. It is recognised by the interface this component uses
7
+ * — which pieces the readers have asked to keep (`protectedRanges`) — and not by
8
+ * its class: the torrent component does not import the storage component's
9
+ * implementation, it is handed it where the thread is assembled
10
+ * (`worker/worker.js`).
11
+ */
12
+
13
+ /** How deep WebTorrent's own wrappers are allowed to go before the walk gives up. */
14
+ const WRAPPER_DEPTH = 8;
15
+
16
+ /**
17
+ * @param {{ store?: object } | null | undefined} torrent
18
+ * @returns {object | null}
19
+ */
20
+ export function pieceStoreOf(torrent) {
21
+ let candidate = torrent?.store;
22
+ for (let depth = 0; candidate && depth < WRAPPER_DEPTH; depth += 1) {
23
+ if (typeof candidate.protectedRanges === "function") {
24
+ return candidate;
25
+ }
26
+ candidate = candidate.store;
27
+ }
28
+ return null;
29
+ }
@@ -1,262 +1,262 @@
1
- /**
2
- * @file What speed a step must run at, and how much buffer a viewer needs —
3
- * both derived from the supply's own interruptions rather than chosen by hand.
4
- *
5
- * The two numbers this replaces were guesses. The speed margin was 1.5, and on
6
- * the field session of 2026-08-17 a step admitted by it ran at 1.05x and
7
- * stalled; the pre-buffer target was 25 s, which is sixteen seconds of waiting
8
- * before the picture starts that nobody had shown to be necessary.
9
- *
10
- * Both follow from the same two measured quantities, and from nothing else:
11
- *
12
- * W — how long the supply is INTERRUPTED (the worst recent stall);
13
- * T — how long the encoder RUNS between two stalls (the median recent gap).
14
- *
15
- * Both words are load-bearing, and getting either wrong is what produced a
16
- * demanded speed of 4422x on 2026-08-31.
17
- *
18
- * A stall is not a wait. Several readers walk one file — the picture and each
19
- * audio rendition — so a piece that has not arrived blocks all of them, and
20
- * their waits end within milliseconds of each other. Counted as separate
21
- * interruptions they gave an interval of 0.00 s. Waits are therefore merged into
22
- * the stretches during which the supply was not delivering, however many readers
23
- * noticed.
24
- *
25
- * And T is the RUNNING time, from the end of one stall to the start of the next
26
- * — not the spacing between their ends, which includes a stall's own duration
27
- * and so credits the encoder with cushion it was not building.
28
- *
29
- * **The margin.** A step producing at speed `v` gains `v - 1` seconds of
30
- * cushion for every second it runs, and an interruption of `W` seconds costs
31
- * `W`. It therefore survives its own supply only if what it gains between
32
- * interruptions exceeds what one costs:
33
- *
34
- * (v - 1) × T > W i.e. v > 1 + W / T
35
- *
36
- * On the field torrent: stalls of 1.49 s with 0.73 s of running between them,
37
- * the worst 3.16 s, so the honest bar is 5.33 — against the 1.5 that was
38
- * assumed, and the 1.05 that was measured. (An earlier reading of the same data
39
- * gave 2.42 by using the end-to-end spacing; it was too low in the same
40
- * direction as the guess it replaced.) The same arithmetic explains why a copied
41
- * stream never stalls: at 8x it gains 10.7 s between stalls and loses at most
42
- * 4.8 s.
43
- *
44
- * **The buffer.** It must cover the worst interruption that can arrive before
45
- * it can be refilled, whichever source that interruption comes from, plus the
46
- * segment being played — which must be whole:
47
- *
48
- * B = segment duration + max(W_supply, D_production, T_transfer)
49
- *
50
- * On the same session that is 7-9 s rather than 25. It rises by itself when a
51
- * session's interruptions grow, which is what makes it safe to lower: the
52
- * figure is continuously measured, not chosen once.
53
- *
54
- * Every quantity here is measured. Nothing in this file is a coefficient, and
55
- * nothing smooths, weights or decays anything.
56
- */
57
-
58
- /**
59
- * One measured interruption: how long a read waited, and when the wait ended.
60
- *
61
- * @typedef {object} SupplyWait
62
- * @property {number} waitedMs - How long the read was blocked.
63
- * @property {number} at - Wall-clock ms when the wait ended.
64
- */
65
-
66
- /**
67
- * The speed a step must sustain to survive this file's supply on this swarm.
68
- *
69
- * Returns null when the evidence does not exist yet — fewer than two waits
70
- * means no interval has been observed, and an interval invented from one point
71
- * would be exactly the kind of number this file exists to remove. A caller with
72
- * null must say it does not know, never substitute a default.
73
- *
74
- * @param {SupplyWait[]} waits - Recent waits, in any order.
75
- * @returns {{ requiredSpeed: number, worstWaitSec: number, medianIntervalSec: number, samples: number } | null}
76
- */
77
- export function requiredSpeedFrom(waits) {
78
- const ordered = usableWaits(waits);
79
- if (ordered.length < 2) {
80
- return null;
81
- }
82
- // One INTERRUPTION, not one wait. Several readers walk the same file — the
83
- // picture and each audio rendition — and a piece that has not arrived blocks
84
- // all of them at once. Counted as separate interruptions, those simultaneous
85
- // waits gave a near-zero interval and therefore a required speed of thousands:
86
- // measured 2026-08-31, `worst wait 13.26s, one every 0.00s, 2 measured` became
87
- // 4422.00x, and every quality step was refused against it.
88
- const interruptions = mergeOverlapping(ordered);
89
- if (interruptions.length < 2) {
90
- // One interruption shows no interval, and an interval invented from one
91
- // point is exactly what this file exists to remove.
92
- return null;
93
- }
94
- const worstWaitSec = Math.max(...interruptions.map((one) => one.end - one.start)) / 1000;
95
- const intervals = [];
96
- for (let index = 1; index < interruptions.length; index += 1) {
97
- // From the END of one interruption to the START of the next: that is the
98
- // stretch the encoder actually runs for and builds cushion in. Measuring
99
- // end-to-end instead counted each interruption's own duration as part of
100
- // the recovery it is supposed to be recovered from.
101
- const gapMs = interruptions[index].start - interruptions[index - 1].end;
102
- if (gapMs > 0) {
103
- intervals.push(gapMs / 1000);
104
- }
105
- }
106
- if (intervals.length === 0) {
107
- return null;
108
- }
109
- const medianIntervalSec = median(intervals);
110
- if (!(medianIntervalSec > 0)) {
111
- return null;
112
- }
113
- // THE SHARE OF ITS TIME THE READING LOST, over the stretch actually observed.
114
- //
115
- // The model is unchanged and it was always right: if a fraction `f` of the
116
- // time is spent not delivering, then producing one second of film takes
117
- // `1/(1 - f)` seconds, so a step must run that much faster than realtime.
118
- // What was wrong was the two quantities fed into it — the WORST single
119
- // interruption divided by the MEDIAN gap between interruptions, a maximum
120
- // over a median, from two populations that need not be the same events at
121
- // all. It asks what would happen if the worst interruption recurred at the
122
- // typical rate, which is a compound case that never occurs, and it divides by
123
- // a gap that goes to zero whenever interruptions arrive in a burst.
124
- //
125
- // Field 2026-09-08: 0.79 s (one jump, on a file already downloaded whole)
126
- // over 0.01 s (the gaps inside a burst of microsecond waits) gave 158.60x,
127
- // and the quality budget refused every step against it forty times in one
128
- // session. The same measurements as a share of time lost give 1.00x, because
129
- // that is what a reading of a complete file loses.
130
- //
131
- // The denominator here cannot vanish: it is the span the interruptions are
132
- // spread over, which contains them.
133
- // OVER WHOLE CYCLES, from the first interruption's start to the last one's
134
- // start. That window holds exactly one running stretch per interruption in
135
- // it, so the share does not depend on where the window happens to be cut —
136
- // measured to the last interruption's END it counts one interruption more
137
- // than it counts running stretches, and the same supply then reads 1.54x or
138
- // 1.45x according to nothing but the moment the log line was printed.
139
- const last = interruptions.length - 1;
140
- const spanSec = (interruptions[last].start - interruptions[0].start) / 1000;
141
- const lostSec = interruptions
142
- .slice(0, last)
143
- .reduce((total, one) => total + (one.end - one.start), 0) / 1000;
144
- // A span that is all interruption says the supply delivered nothing at all
145
- // while it was watched. There is no speed that survives that, and saying a
146
- // huge number is less honest than saying it is not a speed question — so the
147
- // largest figure any step is ever compared against is stated and named.
148
- const lostShare = spanSec > 0 ? Math.min(0.99, lostSec / spanSec) : 0;
149
- return {
150
- requiredSpeed: 1 / (1 - lostShare),
151
- lostShare,
152
- spanSec,
153
- lostSec,
154
- worstWaitSec,
155
- medianIntervalSec,
156
- // Interruptions, not waits: what the figure is derived from. The two differ
157
- // whenever more than one reader walks the file, and reporting the raw count
158
- // is what made the 4422x line look better evidenced than it was — "24
159
- // measured" was 24 waits over far fewer actual stalls.
160
- samples: interruptions.length,
161
- waits: ordered.length
162
- };
163
- }
164
-
165
- /**
166
- * Waits joined into the interruptions they actually were.
167
- *
168
- * A wait spans `[at - waitedMs, at]`. Two that overlap or touch are one stretch
169
- * during which the supply was not delivering, however many readers noticed it.
170
- *
171
- * @param {SupplyWait[]} ordered - Usable waits, oldest END first.
172
- * @returns {Array<{ start: number, end: number }>} Disjoint, in time order.
173
- */
174
- function mergeOverlapping(ordered) {
175
- const spans = ordered
176
- .map((wait) => ({ start: wait.at - wait.waitedMs, end: wait.at }))
177
- .sort((left, right) => left.start - right.start);
178
- /** @type {Array<{ start: number, end: number }>} */
179
- const merged = [];
180
- for (const span of spans) {
181
- const last = merged[merged.length - 1];
182
- if (last && span.start <= last.end) {
183
- last.end = Math.max(last.end, span.end);
184
- continue;
185
- }
186
- merged.push({ ...span });
187
- }
188
- return merged;
189
- }
190
-
191
- /**
192
- * The smallest buffer at which no interruption reaches the viewer.
193
- *
194
- * Each term is the worst OBSERVED value over a recent window, and a term with
195
- * nothing observed contributes nothing rather than a guess. The segment is
196
- * always included: the one being played has to be whole.
197
- *
198
- * @param {object} observed
199
- * @param {number} observed.segmentSeconds - The session's segment duration.
200
- * @param {number} [observed.worstSupplyWaitSec] - Longest wait for a piece.
201
- * @param {number} [observed.worstProductionGapSec] - Longest gap between
202
- * consecutive segments beyond their own length: what a step at 1.0x costs.
203
- * @param {number} [observed.worstTransferSec] - Longest time to move one
204
- * segment over the channel to the viewer.
205
- * @returns {{ seconds: number, from: string } | null} Null when the segment
206
- * duration is unknown, since then nothing here can be stated.
207
- */
208
- export function minimumBufferFrom(observed = {}) {
209
- const segmentSeconds = Number(observed.segmentSeconds);
210
- if (!Number.isFinite(segmentSeconds) || segmentSeconds <= 0) {
211
- return null;
212
- }
213
- const terms = [
214
- { name: "supply", seconds: positiveOrZero(observed.worstSupplyWaitSec) },
215
- { name: "production", seconds: positiveOrZero(observed.worstProductionGapSec) },
216
- { name: "transfer", seconds: positiveOrZero(observed.worstTransferSec) }
217
- ];
218
- const worst = terms.reduce(
219
- (largest, term) => (term.seconds > largest.seconds ? term : largest),
220
- { name: "none", seconds: 0 }
221
- );
222
- return {
223
- seconds: segmentSeconds + worst.seconds,
224
- // Which interruption sets the figure, so a session's log says what the
225
- // viewer is actually waiting for rather than only how long.
226
- from: worst.name
227
- };
228
- }
229
-
230
- /**
231
- * Waits that can be reasoned about, oldest first.
232
- *
233
- * @param {SupplyWait[]} waits
234
- * @returns {SupplyWait[]}
235
- */
236
- function usableWaits(waits) {
237
- if (!Array.isArray(waits)) {
238
- return [];
239
- }
240
- return waits
241
- .filter((wait) => Number.isFinite(wait?.waitedMs) && wait.waitedMs > 0 && Number.isFinite(wait?.at))
242
- .sort((left, right) => left.at - right.at);
243
- }
244
-
245
- /**
246
- * @param {number[]} values - Non-empty.
247
- * @returns {number}
248
- */
249
- function median(values) {
250
- const sorted = [...values].sort((left, right) => left - right);
251
- const middle = Math.floor(sorted.length / 2);
252
- return sorted.length % 2 === 0 ? (sorted[middle - 1] + sorted[middle]) / 2 : sorted[middle];
253
- }
254
-
255
- /**
256
- * @param {unknown} value
257
- * @returns {number}
258
- */
259
- function positiveOrZero(value) {
260
- const numeric = Number(value);
261
- return Number.isFinite(numeric) && numeric > 0 ? numeric : 0;
262
- }
1
+ /**
2
+ * @file What speed a step must run at, and how much buffer a viewer needs —
3
+ * both derived from the supply's own interruptions rather than chosen by hand.
4
+ *
5
+ * The two numbers this replaces were guesses. The speed margin was 1.5, and on
6
+ * the field session of 2026-08-17 a step admitted by it ran at 1.05x and
7
+ * stalled; the pre-buffer target was 25 s, which is sixteen seconds of waiting
8
+ * before the picture starts that nobody had shown to be necessary.
9
+ *
10
+ * Both follow from the same two measured quantities, and from nothing else:
11
+ *
12
+ * W — how long the supply is INTERRUPTED (the worst recent stall);
13
+ * T — how long the encoder RUNS between two stalls (the median recent gap).
14
+ *
15
+ * Both words are load-bearing, and getting either wrong is what produced a
16
+ * demanded speed of 4422x on 2026-08-31.
17
+ *
18
+ * A stall is not a wait. Several readers walk one file — the picture and each
19
+ * audio rendition — so a piece that has not arrived blocks all of them, and
20
+ * their waits end within milliseconds of each other. Counted as separate
21
+ * interruptions they gave an interval of 0.00 s. Waits are therefore merged into
22
+ * the stretches during which the supply was not delivering, however many readers
23
+ * noticed.
24
+ *
25
+ * And T is the RUNNING time, from the end of one stall to the start of the next
26
+ * — not the spacing between their ends, which includes a stall's own duration
27
+ * and so credits the encoder with cushion it was not building.
28
+ *
29
+ * **The margin.** A step producing at speed `v` gains `v - 1` seconds of
30
+ * cushion for every second it runs, and an interruption of `W` seconds costs
31
+ * `W`. It therefore survives its own supply only if what it gains between
32
+ * interruptions exceeds what one costs:
33
+ *
34
+ * (v - 1) × T > W i.e. v > 1 + W / T
35
+ *
36
+ * On the field torrent: stalls of 1.49 s with 0.73 s of running between them,
37
+ * the worst 3.16 s, so the honest bar is 5.33 — against the 1.5 that was
38
+ * assumed, and the 1.05 that was measured. (An earlier reading of the same data
39
+ * gave 2.42 by using the end-to-end spacing; it was too low in the same
40
+ * direction as the guess it replaced.) The same arithmetic explains why a copied
41
+ * stream never stalls: at 8x it gains 10.7 s between stalls and loses at most
42
+ * 4.8 s.
43
+ *
44
+ * **The buffer.** It must cover the worst interruption that can arrive before
45
+ * it can be refilled, whichever source that interruption comes from, plus the
46
+ * segment being played — which must be whole:
47
+ *
48
+ * B = segment duration + max(W_supply, D_production, T_transfer)
49
+ *
50
+ * On the same session that is 7-9 s rather than 25. It rises by itself when a
51
+ * session's interruptions grow, which is what makes it safe to lower: the
52
+ * figure is continuously measured, not chosen once.
53
+ *
54
+ * Every quantity here is measured. Nothing in this file is a coefficient, and
55
+ * nothing smooths, weights or decays anything.
56
+ */
57
+
58
+ /**
59
+ * One measured interruption: how long a read waited, and when the wait ended.
60
+ *
61
+ * @typedef {object} SupplyWait
62
+ * @property {number} waitedMs - How long the read was blocked.
63
+ * @property {number} at - Wall-clock ms when the wait ended.
64
+ */
65
+
66
+ /**
67
+ * The speed a step must sustain to survive this file's supply on this swarm.
68
+ *
69
+ * Returns null when the evidence does not exist yet — fewer than two waits
70
+ * means no interval has been observed, and an interval invented from one point
71
+ * would be exactly the kind of number this file exists to remove. A caller with
72
+ * null must say it does not know, never substitute a default.
73
+ *
74
+ * @param {SupplyWait[]} waits - Recent waits, in any order.
75
+ * @returns {{ requiredSpeed: number, worstWaitSec: number, medianIntervalSec: number, samples: number } | null}
76
+ */
77
+ export function requiredSpeedFrom(waits) {
78
+ const ordered = usableWaits(waits);
79
+ if (ordered.length < 2) {
80
+ return null;
81
+ }
82
+ // One INTERRUPTION, not one wait. Several readers walk the same file — the
83
+ // picture and each audio rendition — and a piece that has not arrived blocks
84
+ // all of them at once. Counted as separate interruptions, those simultaneous
85
+ // waits gave a near-zero interval and therefore a required speed of thousands:
86
+ // measured 2026-08-31, `worst wait 13.26s, one every 0.00s, 2 measured` became
87
+ // 4422.00x, and every quality step was refused against it.
88
+ const interruptions = mergeOverlapping(ordered);
89
+ if (interruptions.length < 2) {
90
+ // One interruption shows no interval, and an interval invented from one
91
+ // point is exactly what this file exists to remove.
92
+ return null;
93
+ }
94
+ const worstWaitSec = Math.max(...interruptions.map((one) => one.end - one.start)) / 1000;
95
+ const intervals = [];
96
+ for (let index = 1; index < interruptions.length; index += 1) {
97
+ // From the END of one interruption to the START of the next: that is the
98
+ // stretch the encoder actually runs for and builds cushion in. Measuring
99
+ // end-to-end instead counted each interruption's own duration as part of
100
+ // the recovery it is supposed to be recovered from.
101
+ const gapMs = interruptions[index].start - interruptions[index - 1].end;
102
+ if (gapMs > 0) {
103
+ intervals.push(gapMs / 1000);
104
+ }
105
+ }
106
+ if (intervals.length === 0) {
107
+ return null;
108
+ }
109
+ const medianIntervalSec = median(intervals);
110
+ if (!(medianIntervalSec > 0)) {
111
+ return null;
112
+ }
113
+ // THE SHARE OF ITS TIME THE READING LOST, over the stretch actually observed.
114
+ //
115
+ // The model is unchanged and it was always right: if a fraction `f` of the
116
+ // time is spent not delivering, then producing one second of film takes
117
+ // `1/(1 - f)` seconds, so a step must run that much faster than realtime.
118
+ // What was wrong was the two quantities fed into it — the WORST single
119
+ // interruption divided by the MEDIAN gap between interruptions, a maximum
120
+ // over a median, from two populations that need not be the same events at
121
+ // all. It asks what would happen if the worst interruption recurred at the
122
+ // typical rate, which is a compound case that never occurs, and it divides by
123
+ // a gap that goes to zero whenever interruptions arrive in a burst.
124
+ //
125
+ // Field 2026-09-08: 0.79 s (one jump, on a file already downloaded whole)
126
+ // over 0.01 s (the gaps inside a burst of microsecond waits) gave 158.60x,
127
+ // and the quality budget refused every step against it forty times in one
128
+ // session. The same measurements as a share of time lost give 1.00x, because
129
+ // that is what a reading of a complete file loses.
130
+ //
131
+ // The denominator here cannot vanish: it is the span the interruptions are
132
+ // spread over, which contains them.
133
+ // OVER WHOLE CYCLES, from the first interruption's start to the last one's
134
+ // start. That window holds exactly one running stretch per interruption in
135
+ // it, so the share does not depend on where the window happens to be cut —
136
+ // measured to the last interruption's END it counts one interruption more
137
+ // than it counts running stretches, and the same supply then reads 1.54x or
138
+ // 1.45x according to nothing but the moment the log line was printed.
139
+ const last = interruptions.length - 1;
140
+ const spanSec = (interruptions[last].start - interruptions[0].start) / 1000;
141
+ const lostSec = interruptions
142
+ .slice(0, last)
143
+ .reduce((total, one) => total + (one.end - one.start), 0) / 1000;
144
+ // A span that is all interruption says the supply delivered nothing at all
145
+ // while it was watched. There is no speed that survives that, and saying a
146
+ // huge number is less honest than saying it is not a speed question — so the
147
+ // largest figure any step is ever compared against is stated and named.
148
+ const lostShare = spanSec > 0 ? Math.min(0.99, lostSec / spanSec) : 0;
149
+ return {
150
+ requiredSpeed: 1 / (1 - lostShare),
151
+ lostShare,
152
+ spanSec,
153
+ lostSec,
154
+ worstWaitSec,
155
+ medianIntervalSec,
156
+ // Interruptions, not waits: what the figure is derived from. The two differ
157
+ // whenever more than one reader walks the file, and reporting the raw count
158
+ // is what made the 4422x line look better evidenced than it was — "24
159
+ // measured" was 24 waits over far fewer actual stalls.
160
+ samples: interruptions.length,
161
+ waits: ordered.length
162
+ };
163
+ }
164
+
165
+ /**
166
+ * Waits joined into the interruptions they actually were.
167
+ *
168
+ * A wait spans `[at - waitedMs, at]`. Two that overlap or touch are one stretch
169
+ * during which the supply was not delivering, however many readers noticed it.
170
+ *
171
+ * @param {SupplyWait[]} ordered - Usable waits, oldest END first.
172
+ * @returns {Array<{ start: number, end: number }>} Disjoint, in time order.
173
+ */
174
+ function mergeOverlapping(ordered) {
175
+ const spans = ordered
176
+ .map((wait) => ({ start: wait.at - wait.waitedMs, end: wait.at }))
177
+ .sort((left, right) => left.start - right.start);
178
+ /** @type {Array<{ start: number, end: number }>} */
179
+ const merged = [];
180
+ for (const span of spans) {
181
+ const last = merged[merged.length - 1];
182
+ if (last && span.start <= last.end) {
183
+ last.end = Math.max(last.end, span.end);
184
+ continue;
185
+ }
186
+ merged.push({ ...span });
187
+ }
188
+ return merged;
189
+ }
190
+
191
+ /**
192
+ * The smallest buffer at which no interruption reaches the viewer.
193
+ *
194
+ * Each term is the worst OBSERVED value over a recent window, and a term with
195
+ * nothing observed contributes nothing rather than a guess. The segment is
196
+ * always included: the one being played has to be whole.
197
+ *
198
+ * @param {object} observed
199
+ * @param {number} observed.segmentSeconds - The session's segment duration.
200
+ * @param {number} [observed.worstSupplyWaitSec] - Longest wait for a piece.
201
+ * @param {number} [observed.worstProductionGapSec] - Longest gap between
202
+ * consecutive segments beyond their own length: what a step at 1.0x costs.
203
+ * @param {number} [observed.worstTransferSec] - Longest time to move one
204
+ * segment over the channel to the viewer.
205
+ * @returns {{ seconds: number, from: string } | null} Null when the segment
206
+ * duration is unknown, since then nothing here can be stated.
207
+ */
208
+ export function minimumBufferFrom(observed = {}) {
209
+ const segmentSeconds = Number(observed.segmentSeconds);
210
+ if (!Number.isFinite(segmentSeconds) || segmentSeconds <= 0) {
211
+ return null;
212
+ }
213
+ const terms = [
214
+ { name: "supply", seconds: positiveOrZero(observed.worstSupplyWaitSec) },
215
+ { name: "production", seconds: positiveOrZero(observed.worstProductionGapSec) },
216
+ { name: "transfer", seconds: positiveOrZero(observed.worstTransferSec) }
217
+ ];
218
+ const worst = terms.reduce(
219
+ (largest, term) => (term.seconds > largest.seconds ? term : largest),
220
+ { name: "none", seconds: 0 }
221
+ );
222
+ return {
223
+ seconds: segmentSeconds + worst.seconds,
224
+ // Which interruption sets the figure, so a session's log says what the
225
+ // viewer is actually waiting for rather than only how long.
226
+ from: worst.name
227
+ };
228
+ }
229
+
230
+ /**
231
+ * Waits that can be reasoned about, oldest first.
232
+ *
233
+ * @param {SupplyWait[]} waits
234
+ * @returns {SupplyWait[]}
235
+ */
236
+ function usableWaits(waits) {
237
+ if (!Array.isArray(waits)) {
238
+ return [];
239
+ }
240
+ return waits
241
+ .filter((wait) => Number.isFinite(wait?.waitedMs) && wait.waitedMs > 0 && Number.isFinite(wait?.at))
242
+ .sort((left, right) => left.at - right.at);
243
+ }
244
+
245
+ /**
246
+ * @param {number[]} values - Non-empty.
247
+ * @returns {number}
248
+ */
249
+ function median(values) {
250
+ const sorted = [...values].sort((left, right) => left - right);
251
+ const middle = Math.floor(sorted.length / 2);
252
+ return sorted.length % 2 === 0 ? (sorted[middle - 1] + sorted[middle]) / 2 : sorted[middle];
253
+ }
254
+
255
+ /**
256
+ * @param {unknown} value
257
+ * @returns {number}
258
+ */
259
+ function positiveOrZero(value) {
260
+ const numeric = Number(value);
261
+ return Number.isFinite(numeric) && numeric > 0 ? numeric : 0;
262
+ }