@torrent-tv/proxy 2.87.4 → 2.87.6

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 (460) 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 +94 -94
  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 +1 -1
  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/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/audio-inventory.js +212 -212
  178. package/services/media/container/AviContainer.js +266 -266
  179. package/services/media/container/Container.js +603 -603
  180. package/services/media/container/ContainerFactory.js +56 -56
  181. package/services/media/container/KeyframeTable.js +272 -272
  182. package/services/media/container/MatroskaContainer.js +1494 -1494
  183. package/services/media/container/Mp4Container.js +1244 -1244
  184. package/services/media/container/SubtitleFileContainer.js +322 -322
  185. package/services/media/container/ebml-reader.js +159 -159
  186. package/services/media/ffmpeg-banner.js +260 -260
  187. package/services/media/keyframe-probe.js +157 -157
  188. package/services/media/media-info-probe.js +93 -93
  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/PlaybackController.js +36 -36
  202. package/services/server/controllers/SubtitleController.js +109 -109
  203. package/services/server/http-responses.js +60 -60
  204. package/services/server/transcode-session-files.js +325 -325
  205. package/services/server/wire-outputs.js +983 -983
  206. package/services/storage/Diagnostics.js +202 -202
  207. package/services/storage/MachineBudget.js +235 -235
  208. package/services/storage/allowance.js +107 -107
  209. package/services/storage/core-dumps.js +106 -106
  210. package/services/storage/files/CompletedFiles.js +391 -391
  211. package/services/storage/files/piece-from-whole-file.js +118 -118
  212. package/services/storage/free.js +71 -71
  213. package/services/storage/keep.js +48 -48
  214. package/services/storage/machine-memory.js +56 -56
  215. package/services/storage/memory-report.js +575 -575
  216. package/services/storage/piece-store/piece-disk-store.js +516 -516
  217. package/services/storage/piece-store/piece-lru.js +495 -495
  218. package/services/storage/piece-store/shared-piece-store.js +1880 -1880
  219. package/services/storage/returns.js +103 -103
  220. package/services/storage/segment-store/SegmentStore.js +1196 -1196
  221. package/services/storage/wire.js +175 -175
  222. package/services/torrent/Contents.js +324 -324
  223. package/services/torrent/demand/DemandRegister.js +202 -202
  224. package/services/torrent/demand/Urgency.js +137 -137
  225. package/services/torrent/demand/Window.js +118 -118
  226. package/services/torrent/demand/index.js +8 -8
  227. package/services/torrent/demand/pieces.js +140 -140
  228. package/services/torrent/download/SwarmSelection.js +386 -386
  229. package/services/torrent/download/registry.js +102 -102
  230. package/services/torrent/download/withdraw-claim.js +80 -80
  231. package/services/torrent/files.js +455 -455
  232. package/services/torrent/naming.js +619 -619
  233. package/services/torrent/piece-store-of.js +29 -29
  234. package/services/torrent/supply-margin.js +262 -262
  235. package/services/torrent/torrent-pool.js +2 -2
  236. package/services/torrent/worker/background-fill.js +213 -213
  237. package/services/torrent/worker/channel.js +271 -271
  238. package/services/torrent/worker/fastest-wires.js +342 -342
  239. package/services/torrent/worker/handle-state.js +23 -23
  240. package/services/torrent/worker/held-bytes.js +155 -155
  241. package/services/torrent/worker/install-webrtc-shim.js +32 -32
  242. package/services/torrent/worker/piece-reader.js +1078 -1078
  243. package/services/torrent/worker/pool-adapter.js +1 -1
  244. package/services/torrent/worker/resume-warm.js +107 -107
  245. package/services/torrent/worker/webrtc-shim.js +219 -219
  246. package/services/transport/data-channel-handler.js +1356 -1356
  247. package/services/transport/delivery-probe.js +842 -842
  248. package/services/transport/health-collector.js +53 -53
  249. package/services/transport/nat-classifier.js +231 -231
  250. package/services/transport/packet-witness.js +808 -808
  251. package/services/transport/port-mapper.js +235 -235
  252. package/services/transport/registry-api.js +81 -81
  253. package/services/transport/tunnel-client.js +447 -447
  254. package/services/transport/usrsctp-state.js +175 -175
  255. package/services/transport/webrtc-manager.js +484 -484
  256. package/services/viewer/Assignments.js +340 -340
  257. package/services/viewer/PriorityMap.js +390 -390
  258. package/services/viewer/PriorityOrchestrator.js +325 -325
  259. package/services/viewer/Viewer.js +657 -657
  260. package/services/viewer/Viewers.js +368 -368
  261. package/services/viewer/active-output.js +37 -37
  262. package/services/viewer/audio-start.js +46 -46
  263. package/services/viewer/buffer-trend.js +81 -81
  264. package/services/viewer/choices.js +647 -647
  265. package/services/viewer/positions.js +126 -126
  266. package/services/viewer/report-intake.js +46 -46
  267. package/store/source-registry.js +71 -73
  268. package/test/asset-paths.test.js +28 -28
  269. package/test/audio-inventory.test.js +176 -176
  270. package/test/audio-track-bitrate.test.js +40 -40
  271. package/test/auto-quality-step.test.js +707 -707
  272. package/test/available-share.test.js +76 -76
  273. package/test/background-fill.test.js +190 -190
  274. package/test/client-log-file.test.js +110 -110
  275. package/test/completed-files.test.js +115 -115
  276. package/test/concurrent-cost.test.js +148 -148
  277. package/test/container-media-info.test.js +228 -228
  278. package/test/contention.test.js +76 -76
  279. package/test/core-dumps.test.js +57 -57
  280. package/test/coverage-follows-the-disk.test.js +228 -228
  281. package/test/coverage-map.test.js +195 -195
  282. package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
  283. package/test/cut-times-timeline.test.js +65 -65
  284. package/test/cuts-follow-published-grid.test.js +109 -109
  285. package/test/data-channel-frame.test.js +63 -63
  286. package/test/dead-torrent-handle.test.js +52 -52
  287. package/test/declared-rates.test.js +116 -116
  288. package/test/declared-tracks.test.js +35 -35
  289. package/test/decode-cost-fit.test.js +202 -202
  290. package/test/decode-cost.test.js +462 -462
  291. package/test/delivery-probe.test.js +393 -393
  292. package/test/demand-register.test.js +206 -206
  293. package/test/dht-bootstrap.test.js +72 -72
  294. package/test/drain-threat.test.js +65 -65
  295. package/test/duplicate-add.test.js +48 -48
  296. package/test/encode-admission.test.js +146 -146
  297. package/test/encode-cost.test.js +283 -283
  298. package/test/encode-exit.test.js +111 -111
  299. package/test/encode-orchestrator.test.js +419 -419
  300. package/test/encode-plan-viewers.test.js +718 -718
  301. package/test/encode-plan.test.js +545 -545
  302. package/test/encode-run-state.test.js +314 -314
  303. package/test/encode-run.test.js +161 -161
  304. package/test/encode-slope.test.js +46 -46
  305. package/test/encoded-output.test.js +39 -39
  306. package/test/encoder-kinds.test.js +122 -122
  307. package/test/encoder-readings.test.js +46 -46
  308. package/test/every-claimant-is-told-its-share.test.js +152 -152
  309. package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
  310. package/test/failed-start-breaker.test.js +202 -202
  311. package/test/fastest-wires.test.js +143 -143
  312. package/test/ffmpeg-stream-counts.test.js +78 -78
  313. package/test/file-edges.test.js +191 -191
  314. package/test/fmp4-self-contained.test.js +80 -80
  315. package/test/fmp4-timestamps.test.js +140 -140
  316. package/test/fmp4-track-completeness.test.js +41 -41
  317. package/test/format-keyed-outputs.test.js +78 -78
  318. package/test/health-metrics.test.js +38 -38
  319. package/test/held-request-width.test.js +157 -157
  320. package/test/helpers/encode-run.js +136 -136
  321. package/test/helpers/held-file.js +29 -29
  322. package/test/helpers/manager.js +57 -57
  323. package/test/helpers/output-spec.js +63 -63
  324. package/test/host-load.test.js +68 -68
  325. package/test/index-accuracy-is-about-the-file.test.js +128 -128
  326. package/test/init-compat.test.js +219 -219
  327. package/test/input-lost-quiets-the-plan.test.js +261 -261
  328. package/test/input-unavailable.test.js +48 -48
  329. package/test/installation-id.test.js +39 -39
  330. package/test/keeping-period.test.js +83 -83
  331. package/test/keyframe-index-accuracy.test.js +188 -188
  332. package/test/keyframe-table-budget.test.js +102 -102
  333. package/test/keyframes-belong-to-the-file.test.js +249 -249
  334. package/test/learned-median.test.js +37 -37
  335. package/test/letting-go-asks-who-is-watching.test.js +191 -191
  336. package/test/limit-row-by-area.test.js +60 -60
  337. package/test/link-budget.test.js +111 -111
  338. package/test/link-probe.test.js +56 -56
  339. package/test/local-encoding-data.test.js +273 -273
  340. package/test/logger-repeats.test.js +134 -134
  341. package/test/look-ahead-coverage.test.js +56 -56
  342. package/test/manager-surface.test.js +292 -292
  343. package/test/matroska-cues-track.test.js +192 -192
  344. package/test/memory-budget.test.js +390 -390
  345. package/test/move-cost.test.js +216 -216
  346. package/test/mp4-subtitles.test.js +173 -173
  347. package/test/one-authority.test.js +285 -285
  348. package/test/one-budget.test.js +149 -149
  349. package/test/one-output-one-session.test.js +204 -204
  350. package/test/one-resource-divided.test.js +185 -185
  351. package/test/one-serving-output-per-height.test.js +458 -458
  352. package/test/orchestrator-wired.test.js +206 -206
  353. package/test/output-catalog.test.js +39 -39
  354. package/test/output-shape.test.js +21 -21
  355. package/test/output-spec.test.js +251 -251
  356. package/test/output-speed.test.js +88 -88
  357. package/test/outputs-being-prepared.test.js +40 -40
  358. package/test/packet-witness-ring.test.js +236 -236
  359. package/test/packet-witness.test.js +148 -148
  360. package/test/piece-disk-store.test.js +381 -381
  361. package/test/piece-from-whole-file.test.js +129 -129
  362. package/test/piece-lru.test.js +319 -319
  363. package/test/piece-reader.test.js +162 -162
  364. package/test/piece-selection-offset.test.js +85 -85
  365. package/test/piece-store-eviction.test.js +560 -560
  366. package/test/piece-store-never-refuses.test.js +153 -153
  367. package/test/piece-store-reservations.test.js +318 -318
  368. package/test/piece-store-slow-disk.test.js +159 -159
  369. package/test/piece-tail.test.js +97 -97
  370. package/test/plan-host-timings.test.js +68 -68
  371. package/test/plan-is-steady.test.js +225 -225
  372. package/test/priority-map-download.test.js +174 -174
  373. package/test/priority-map-emptied.test.js +204 -204
  374. package/test/priority-map-per-output.test.js +215 -215
  375. package/test/priority-map.test.js +163 -163
  376. package/test/probe-wedge-certainty.test.js +60 -60
  377. package/test/produced-copy-choice.test.js +213 -213
  378. package/test/published-timeline.test.js +83 -83
  379. package/test/quality-offer.test.js +162 -162
  380. package/test/quality-variants.test.js +1229 -1229
  381. package/test/rate-control-in-the-output.test.js +180 -180
  382. package/test/read-survives-withdrawal.test.js +164 -164
  383. package/test/read-window.test.js +365 -365
  384. package/test/resume-warm.test.js +39 -39
  385. package/test/run-graph-drift.test.js +37 -37
  386. package/test/run-inputs.test.js +80 -80
  387. package/test/run-intervals.test.js +100 -100
  388. package/test/run-position-follows-published-grid.test.js +89 -89
  389. package/test/run-progress.test.js +65 -65
  390. package/test/same-height-switch.test.js +664 -664
  391. package/test/seek-frees-the-viewers-requests.test.js +217 -217
  392. package/test/seek-landing.test.js +109 -109
  393. package/test/seek-target-not-superseded.test.js +195 -195
  394. package/test/segment-closed-on-both-branches.test.js +96 -96
  395. package/test/segment-cut-times.test.js +63 -63
  396. package/test/segment-demand.test.js +80 -80
  397. package/test/segment-hold-supersede.test.js +91 -91
  398. package/test/segment-judged-by-its-own-header.test.js +221 -221
  399. package/test/segment-serve-wiring.test.js +475 -475
  400. package/test/segment-store-eviction.test.js +247 -247
  401. package/test/segment-store.test.js +323 -323
  402. package/test/segment-true-start.test.js +86 -86
  403. package/test/segments-are-shared.test.js +176 -176
  404. package/test/send-chunk-bytes.test.js +86 -86
  405. package/test/serving-existing-output.test.js +102 -102
  406. package/test/serving-output.test.js +102 -102
  407. package/test/shared-piece-store.test.js +310 -310
  408. package/test/sidecar-naming.test.js +142 -142
  409. package/test/source-file.test.js +133 -133
  410. package/test/source-files-route.test.js +103 -103
  411. package/test/source-registry.test.js +18 -0
  412. package/test/split-a-stretch.test.js +139 -139
  413. package/test/stale-request-after-seek.test.js +158 -158
  414. package/test/startup-readings.test.js +124 -124
  415. package/test/stream-route.test.js +182 -182
  416. package/test/subtitle-cue-framing.test.js +200 -200
  417. package/test/subtitle-cue-source.test.js +102 -102
  418. package/test/subtitle-cue-walk.test.js +370 -370
  419. package/test/subtitle-cursor.test.js +63 -63
  420. package/test/subtitle-defaults.test.js +118 -118
  421. package/test/subtitle-language.test.js +252 -252
  422. package/test/subtitle-track-numbering.test.js +386 -386
  423. package/test/supply-margin.test.js +177 -177
  424. package/test/swarm-follows-readers.test.js +174 -174
  425. package/test/swarm-reach.test.js +130 -130
  426. package/test/swarm-selection.test.js +220 -220
  427. package/test/tail-duplication.test.js +167 -167
  428. package/test/the-channel-has-both-ends.test.js +91 -91
  429. package/test/the-torrent-thread-serves-bytes.test.js +107 -107
  430. package/test/thread-pool.test.js +53 -53
  431. package/test/timeline.test.js +84 -84
  432. package/test/torrent-contents.test.js +235 -235
  433. package/test/torrent-cost.test.js +71 -71
  434. package/test/torrent-files.test.js +221 -221
  435. package/test/torrent-naming.test.js +255 -255
  436. package/test/tracks-begin-together.test.js +209 -209
  437. package/test/tunnel-renewal.test.js +167 -167
  438. package/test/two-viewers-one-picture.test.js +407 -407
  439. package/test/upload-hurry.test.js +186 -186
  440. package/test/usrsctp-state.test.js +131 -131
  441. package/test/video-facts.test.js +102 -102
  442. package/test/viewer-assignments.test.js +143 -143
  443. package/test/viewer-outputs.test.js +285 -285
  444. package/test/viewer-presence.test.js +116 -116
  445. package/test/viewer-requests-admission.test.js +89 -89
  446. package/test/viewer-states.test.js +189 -189
  447. package/test/viewer-subtitle-subscription.test.js +96 -96
  448. package/test/viewer.test.js +139 -139
  449. package/test/viewing-generations.test.js +390 -390
  450. package/test/visible-rung.test.js +37 -37
  451. package/test/wait-ledger.test.js +100 -100
  452. package/test/webrtc-shim.test.js +149 -149
  453. package/test/wedge-certainty.test.js +131 -131
  454. package/test/withdraw-piece-claim.test.js +212 -212
  455. package/test/worker-channel.test.js +158 -158
  456. package/utils/client-log-file.js +151 -151
  457. package/utils/logger.js +312 -312
  458. package/utils/parse-range.js +25 -25
  459. package/utils/perf.js +59 -59
  460. package/{services/torrent → utils}/torrent-source-key.js +38 -38
@@ -1,103 +1,103 @@
1
- /**
2
- * @file WHAT THE BROWSER IS TOLD IS IN A TORRENT.
3
- *
4
- * This is the one place the product answers it. The browser used to answer it
5
- * as well — a list of video extensions in its parser and a second, shorter pair
6
- * inside its picker — and the three had already diverged: measured 2026-09-12,
7
- * `.dat` was offered there as video and not counted here, which also decides
8
- * whether a sidecar whose name matches nothing can belong to the only video
9
- * present.
10
- *
11
- * So the shape of this answer is a contract with the page, and the page shows
12
- * what it is given without looking at a name.
13
- */
14
-
15
- import test from "node:test";
16
- import assert from "node:assert/strict";
17
- import { handleApiSourceFilesGet } from "../routes/api/sources/files/get.js";
18
-
19
- /**
20
- * A request, a reply and the torrent behind them.
21
- *
22
- * @param {Array<{ path: string, length: number }>} files
23
- * @returns {{ req: object, reply: object, sent: { body: object | null, code: number }, deps: object }}
24
- */
25
- function harness(files) {
26
- const sent = { body: null, code: 200 };
27
- const reply = {
28
- code(value) {
29
- sent.code = value;
30
- return reply;
31
- },
32
- send(body) {
33
- sent.body = body;
34
- return reply;
35
- }
36
- };
37
- const deps = {
38
- sourceRegistry: { get: () => ({ sourceType: "magnet", source: "magnet:?xt=urn:btih:abc" }) },
39
- torrentPool: {
40
- async getTorrent() {
41
- return { name: "Drifters", infoHash: "abc", files };
42
- }
43
- }
44
- };
45
- return { req: { params: { sourceKey: "key" }, query: {} }, reply, sent, deps };
46
- }
47
-
48
- test("the files come back in reading order, each saying what it is", () => {
49
- const { req, reply, sent, deps } = harness([
50
- { path: "Drifters/ep 10.mkv", length: 9 },
51
- { path: "Drifters/ep 2.mkv", length: 9 },
52
- { path: "Drifters/Rus Sound/ep 2.mka", length: 2 },
53
- { path: "Drifters/notes.nfo", length: 1 }
54
- ]);
55
-
56
- return handleApiSourceFilesGet(req, reply, deps).then(() => {
57
- assert.equal(sent.code, 200);
58
- assert.deepEqual(
59
- sent.body.files.map((file) => [file.relativePath, file.kind]),
60
- [
61
- // Runs of digits compare as numbers, so 2 comes before 10; the torrent's
62
- // own order is whatever the tool that made it chose.
63
- ["ep 2.mkv", "video"],
64
- ["ep 10.mkv", "video"],
65
- ["notes.nfo", "other"],
66
- ["Rus Sound/ep 2.mka", "audio"]
67
- ]
68
- );
69
- // Relative to the torrent root: WebTorrent prefixes its own name to every
70
- // path, and stripping it here means one rule in the product rather than two
71
- // that can disagree.
72
- assert.ok(sent.body.files.every((file) => !file.relativePath.startsWith("Drifters/")));
73
- });
74
- });
75
-
76
- test("each picture is named with what belongs to it, by index", () => {
77
- const { req, reply, sent, deps } = harness([
78
- { path: "Drifters/ep 1.mkv", length: 9 },
79
- { path: "Drifters/Rus Sound/ep 1.mka", length: 2 },
80
- { path: "Drifters/Sub/ep 1.ass", length: 1 },
81
- { path: "Drifters/ep 2.mkv", length: 9 }
82
- ]);
83
-
84
- return handleApiSourceFilesGet(req, reply, deps).then(() => {
85
- assert.deepEqual(sent.body.items, [
86
- { fileIndex: 0, audio: [1], subtitles: [2], images: [] },
87
- { fileIndex: 3, audio: [], subtitles: [], images: [] }
88
- ]);
89
- // The files themselves are in the list above; saying them twice is how two
90
- // copies of one fact start.
91
- assert.ok(sent.body.items.every((item) => item.audio.every((one) => Number.isInteger(one))));
92
- });
93
- });
94
-
95
- test("a torrent whose metadata has not arrived says so and lists nothing", () => {
96
- const { req, reply, sent, deps } = harness([]);
97
-
98
- return handleApiSourceFilesGet(req, reply, deps).then(() => {
99
- assert.deepEqual(sent.body.files, []);
100
- assert.deepEqual(sent.body.items, []);
101
- assert.equal(sent.body.name, "Drifters");
102
- });
103
- });
1
+ /**
2
+ * @file WHAT THE BROWSER IS TOLD IS IN A TORRENT.
3
+ *
4
+ * This is the one place the product answers it. The browser used to answer it
5
+ * as well — a list of video extensions in its parser and a second, shorter pair
6
+ * inside its picker — and the three had already diverged: measured 2026-09-12,
7
+ * `.dat` was offered there as video and not counted here, which also decides
8
+ * whether a sidecar whose name matches nothing can belong to the only video
9
+ * present.
10
+ *
11
+ * So the shape of this answer is a contract with the page, and the page shows
12
+ * what it is given without looking at a name.
13
+ */
14
+
15
+ import test from "node:test";
16
+ import assert from "node:assert/strict";
17
+ import { handleApiSourceFilesGet } from "../routes/api/sources/files/get.js";
18
+
19
+ /**
20
+ * A request, a reply and the torrent behind them.
21
+ *
22
+ * @param {Array<{ path: string, length: number }>} files
23
+ * @returns {{ req: object, reply: object, sent: { body: object | null, code: number }, deps: object }}
24
+ */
25
+ function harness(files) {
26
+ const sent = { body: null, code: 200 };
27
+ const reply = {
28
+ code(value) {
29
+ sent.code = value;
30
+ return reply;
31
+ },
32
+ send(body) {
33
+ sent.body = body;
34
+ return reply;
35
+ }
36
+ };
37
+ const deps = {
38
+ sourceRegistry: { get: () => ({ sourceType: "magnet", source: "magnet:?xt=urn:btih:abc" }) },
39
+ torrentPool: {
40
+ async getTorrent() {
41
+ return { name: "Drifters", infoHash: "abc", files };
42
+ }
43
+ }
44
+ };
45
+ return { req: { params: { sourceKey: "key" }, query: {} }, reply, sent, deps };
46
+ }
47
+
48
+ test("the files come back in reading order, each saying what it is", () => {
49
+ const { req, reply, sent, deps } = harness([
50
+ { path: "Drifters/ep 10.mkv", length: 9 },
51
+ { path: "Drifters/ep 2.mkv", length: 9 },
52
+ { path: "Drifters/Rus Sound/ep 2.mka", length: 2 },
53
+ { path: "Drifters/notes.nfo", length: 1 }
54
+ ]);
55
+
56
+ return handleApiSourceFilesGet(req, reply, deps).then(() => {
57
+ assert.equal(sent.code, 200);
58
+ assert.deepEqual(
59
+ sent.body.files.map((file) => [file.relativePath, file.kind]),
60
+ [
61
+ // Runs of digits compare as numbers, so 2 comes before 10; the torrent's
62
+ // own order is whatever the tool that made it chose.
63
+ ["ep 2.mkv", "video"],
64
+ ["ep 10.mkv", "video"],
65
+ ["notes.nfo", "other"],
66
+ ["Rus Sound/ep 2.mka", "audio"]
67
+ ]
68
+ );
69
+ // Relative to the torrent root: WebTorrent prefixes its own name to every
70
+ // path, and stripping it here means one rule in the product rather than two
71
+ // that can disagree.
72
+ assert.ok(sent.body.files.every((file) => !file.relativePath.startsWith("Drifters/")));
73
+ });
74
+ });
75
+
76
+ test("each picture is named with what belongs to it, by index", () => {
77
+ const { req, reply, sent, deps } = harness([
78
+ { path: "Drifters/ep 1.mkv", length: 9 },
79
+ { path: "Drifters/Rus Sound/ep 1.mka", length: 2 },
80
+ { path: "Drifters/Sub/ep 1.ass", length: 1 },
81
+ { path: "Drifters/ep 2.mkv", length: 9 }
82
+ ]);
83
+
84
+ return handleApiSourceFilesGet(req, reply, deps).then(() => {
85
+ assert.deepEqual(sent.body.items, [
86
+ { fileIndex: 0, audio: [1], subtitles: [2], images: [] },
87
+ { fileIndex: 3, audio: [], subtitles: [], images: [] }
88
+ ]);
89
+ // The files themselves are in the list above; saying them twice is how two
90
+ // copies of one fact start.
91
+ assert.ok(sent.body.items.every((item) => item.audio.every((one) => Number.isInteger(one))));
92
+ });
93
+ });
94
+
95
+ test("a torrent whose metadata has not arrived says so and lists nothing", () => {
96
+ const { req, reply, sent, deps } = harness([]);
97
+
98
+ return handleApiSourceFilesGet(req, reply, deps).then(() => {
99
+ assert.deepEqual(sent.body.files, []);
100
+ assert.deepEqual(sent.body.items, []);
101
+ assert.equal(sent.body.name, "Drifters");
102
+ });
103
+ });
@@ -0,0 +1,18 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createSourceRegistry } from "../store/source-registry.js";
4
+
5
+ const infoHash = "0123456789abcdef0123456789abcdef01234567";
6
+
7
+ test("magnet links for one torrent share a source key", async () => {
8
+ const registry = createSourceRegistry();
9
+ const first = `magnet:?xt=urn:btih:${infoHash}&dn=First+name&tr=udp%3A%2F%2Ftracker.one%3A80`;
10
+ const second = `magnet:?xt=urn:btih:${infoHash}&dn=Different+name&tr=udp%3A%2F%2Ftracker.two%3A80`;
11
+
12
+ const firstKey = await registry.upsert("magnet", first);
13
+ const secondKey = await registry.upsert("magnet", second);
14
+
15
+ assert.equal(firstKey, `torrent:${infoHash}`);
16
+ assert.equal(secondKey, firstKey);
17
+ assert.equal(registry.get(firstKey)?.source, second);
18
+ });
@@ -1,139 +1,139 @@
1
- /**
2
- * @file Where a second encoder joins a stretch, and whether it is worth having.
3
- *
4
- * Both halves are arithmetic and neither was asked before. The stretch used to
5
- * be halved, unconditionally:
6
- *
7
- * widestFrom = from + Math.floor(Math.min(room, to - from + 1) / 2)
8
- *
9
- * Halving IS the answer when both encoders are fresh and owe the same. It is not
10
- * when one of them is already partway through a piece, and it never asked the
11
- * other question at all — whether two encoders under this host's measured
12
- * contention beat one at full speed.
13
- *
14
- * The derivation. A stretch of unmade film runs from `from` to `to`; whoever is
15
- * already on it stands at `from` and owes `w` before the piece under it exists;
16
- * a fresh one placed at `x` owes `d` — its start and then a whole piece — and
17
- * both then work at the rate two encoders leave each other:
18
- *
19
- * the one there closes [from, x-1]: w + (x - 1 - from) / r rises with x
20
- * the fresh one closes [x, to]: d + (to - x) / r falls with x
21
- *
22
- * x* = (from + to + 1) / 2 + (d - w) * r / 2
23
- *
24
- * The shift is `firstByteWait / (2 * perPiece)` pieces, so it depends on how
25
- * long a piece takes here: 0.24 of a piece at 4.45x on a 4.2 s grid, but 0.96 at
26
- * 8.9x and 2.14 at 20x — a whole segment and more on the copy branch, where a
27
- * piece is short and a start is not. Measuring only the re-encode rate
28
- * understated it.
29
- */
30
-
31
- import test from "node:test";
32
- import assert from "node:assert/strict";
33
- import { planEncoders } from "../services/encode/EncodePlan.js";
34
- import { CoverageMap } from "../services/encode/CoverageMap.js";
35
-
36
- const SEGMENT_SECONDS = 4.2;
37
- const LAST = 199;
38
-
39
- /**
40
- * One encoder on a long stretch nobody has made, and room for more.
41
- *
42
- * @param {{ speedX: number, penalty: number, head?: number, ageMs?: number,
43
- * maxRuns?: number }} params
44
- * @returns {{ actions: object[], run: object }}
45
- */
46
- function planFor({ speedX, penalty, head = 0, ageMs = 60_000, maxRuns = 3, runs = null }) {
47
- const coverage = new CoverageMap({ segmentCount: LAST + 1 });
48
- const run = { from: 0, to: -1, head, speedX, isAlive: true, startedAt: 1_000_000 };
49
- const live = runs ?? [run];
50
- for (const one of live) {
51
- coverage.claim(one, one.from, one.to);
52
- }
53
- return {
54
- run,
55
- actions: planEncoders({
56
- coverage,
57
- // One zone over the whole film, due now, so nothing but the arithmetic of
58
- // sharing it decides anything.
59
- windows: [{ from: 0, to: LAST, priority: 100, withinSeconds: 0, behind: false }],
60
- runs: live,
61
- maxRuns,
62
- segmentSeconds: SEGMENT_SECONDS,
63
- speedX,
64
- killCostSec: 0.04,
65
- firstByteWaitSec: 0.9,
66
- moveCostSec: 0.94,
67
- refetchSecPerFilmSecond: 0,
68
- // Measured on the addon host: 1.70 at 854x480, 1.98 at 1920x1080. Raised
69
- // to the power of how many others there are, so a third costs again.
70
- contentionPenaltyFor: (others) => (others <= 0 ? 1 : penalty ** others),
71
- now: 1_000_000 + ageMs
72
- })
73
- };
74
- }
75
-
76
- test("one encoder that reaches everything in time is left to do it alone", () => {
77
- // A zone whose deadline GROWS with distance — which is what the map states,
78
- // one segment at a time — is comfortably served by one encoder at 4.45x: #199
79
- // is due in 835 s and arrives in 189 s. So there is no position to fill and
80
- // nothing to split, whatever the budget says.
81
- //
82
- // Measured rather than assumed: this is what the plan answers, and the test
83
- // was written expecting two encoders before it was run.
84
- const { actions } = planFor({ speedX: 4.45, penalty: 1, maxRuns: 3 });
85
-
86
- assert.deepEqual(
87
- actions.map((one) => `${one.type} #${one.from}..#${one.to}`),
88
- ["keep #0..#-1"],
89
- "one encoder, its road to the end of the film"
90
- );
91
- });
92
-
93
- test("a stretch nobody is on is split at the middle, and the halves bound each other", () => {
94
- // Both encoders are fresh, so both owe the same and `d - w` is zero: the
95
- // meeting point is the midpoint, which is what halving always said. What is
96
- // new is that each one's road ENDS where the next begins, so neither writes
97
- // into the other's names.
98
- const actions = planFor({ speedX: 4.45, penalty: 1, maxRuns: 2, runs: [] }).actions;
99
-
100
- assert.deepEqual(
101
- actions.map((one) => `${one.type} #${one.from}..#${one.to}`),
102
- ["start #0..#100", "start #101..#199"],
103
- "0..199 halved at 100"
104
- );
105
- });
106
-
107
- test("a third encoder halves the widest half that is left", () => {
108
- const actions = planFor({ speedX: 4.45, penalty: 1, maxRuns: 3, runs: [] }).actions;
109
-
110
- assert.deepEqual(
111
- actions.map((one) => `${one.type} #${one.from}..#${one.to}`),
112
- ["start #0..#50", "start #51..#100", "start #101..#199"],
113
- "the first half is halved again, greedily, widest first"
114
- );
115
- });
116
-
117
- test("a stretch of one piece is not split at all", () => {
118
- const coverage = new CoverageMap({ segmentCount: 2 });
119
- const actions = planEncoders({
120
- coverage,
121
- windows: [{ from: 0, to: 0, priority: 100, withinSeconds: 0, behind: false }],
122
- runs: [],
123
- maxRuns: 3,
124
- segmentSeconds: SEGMENT_SECONDS,
125
- speedX: 8.9,
126
- killCostSec: 0.04,
127
- firstByteWaitSec: 0.9,
128
- moveCostSec: 0.94,
129
- refetchSecPerFilmSecond: 0,
130
- contentionPenaltyFor: () => 1,
131
- now: 1_000_000
132
- });
133
-
134
- assert.equal(
135
- actions.filter((one) => one.type === "start").length,
136
- 1,
137
- "there is nothing to share"
138
- );
139
- });
1
+ /**
2
+ * @file Where a second encoder joins a stretch, and whether it is worth having.
3
+ *
4
+ * Both halves are arithmetic and neither was asked before. The stretch used to
5
+ * be halved, unconditionally:
6
+ *
7
+ * widestFrom = from + Math.floor(Math.min(room, to - from + 1) / 2)
8
+ *
9
+ * Halving IS the answer when both encoders are fresh and owe the same. It is not
10
+ * when one of them is already partway through a piece, and it never asked the
11
+ * other question at all — whether two encoders under this host's measured
12
+ * contention beat one at full speed.
13
+ *
14
+ * The derivation. A stretch of unmade film runs from `from` to `to`; whoever is
15
+ * already on it stands at `from` and owes `w` before the piece under it exists;
16
+ * a fresh one placed at `x` owes `d` — its start and then a whole piece — and
17
+ * both then work at the rate two encoders leave each other:
18
+ *
19
+ * the one there closes [from, x-1]: w + (x - 1 - from) / r rises with x
20
+ * the fresh one closes [x, to]: d + (to - x) / r falls with x
21
+ *
22
+ * x* = (from + to + 1) / 2 + (d - w) * r / 2
23
+ *
24
+ * The shift is `firstByteWait / (2 * perPiece)` pieces, so it depends on how
25
+ * long a piece takes here: 0.24 of a piece at 4.45x on a 4.2 s grid, but 0.96 at
26
+ * 8.9x and 2.14 at 20x — a whole segment and more on the copy branch, where a
27
+ * piece is short and a start is not. Measuring only the re-encode rate
28
+ * understated it.
29
+ */
30
+
31
+ import test from "node:test";
32
+ import assert from "node:assert/strict";
33
+ import { planEncoders } from "../services/encode/EncodePlan.js";
34
+ import { CoverageMap } from "../services/encode/CoverageMap.js";
35
+
36
+ const SEGMENT_SECONDS = 4.2;
37
+ const LAST = 199;
38
+
39
+ /**
40
+ * One encoder on a long stretch nobody has made, and room for more.
41
+ *
42
+ * @param {{ speedX: number, penalty: number, head?: number, ageMs?: number,
43
+ * maxRuns?: number }} params
44
+ * @returns {{ actions: object[], run: object }}
45
+ */
46
+ function planFor({ speedX, penalty, head = 0, ageMs = 60_000, maxRuns = 3, runs = null }) {
47
+ const coverage = new CoverageMap({ segmentCount: LAST + 1 });
48
+ const run = { from: 0, to: -1, head, speedX, isAlive: true, startedAt: 1_000_000 };
49
+ const live = runs ?? [run];
50
+ for (const one of live) {
51
+ coverage.claim(one, one.from, one.to);
52
+ }
53
+ return {
54
+ run,
55
+ actions: planEncoders({
56
+ coverage,
57
+ // One zone over the whole film, due now, so nothing but the arithmetic of
58
+ // sharing it decides anything.
59
+ windows: [{ from: 0, to: LAST, priority: 100, withinSeconds: 0, behind: false }],
60
+ runs: live,
61
+ maxRuns,
62
+ segmentSeconds: SEGMENT_SECONDS,
63
+ speedX,
64
+ killCostSec: 0.04,
65
+ firstByteWaitSec: 0.9,
66
+ moveCostSec: 0.94,
67
+ refetchSecPerFilmSecond: 0,
68
+ // Measured on the addon host: 1.70 at 854x480, 1.98 at 1920x1080. Raised
69
+ // to the power of how many others there are, so a third costs again.
70
+ contentionPenaltyFor: (others) => (others <= 0 ? 1 : penalty ** others),
71
+ now: 1_000_000 + ageMs
72
+ })
73
+ };
74
+ }
75
+
76
+ test("one encoder that reaches everything in time is left to do it alone", () => {
77
+ // A zone whose deadline GROWS with distance — which is what the map states,
78
+ // one segment at a time — is comfortably served by one encoder at 4.45x: #199
79
+ // is due in 835 s and arrives in 189 s. So there is no position to fill and
80
+ // nothing to split, whatever the budget says.
81
+ //
82
+ // Measured rather than assumed: this is what the plan answers, and the test
83
+ // was written expecting two encoders before it was run.
84
+ const { actions } = planFor({ speedX: 4.45, penalty: 1, maxRuns: 3 });
85
+
86
+ assert.deepEqual(
87
+ actions.map((one) => `${one.type} #${one.from}..#${one.to}`),
88
+ ["keep #0..#-1"],
89
+ "one encoder, its road to the end of the film"
90
+ );
91
+ });
92
+
93
+ test("a stretch nobody is on is split at the middle, and the halves bound each other", () => {
94
+ // Both encoders are fresh, so both owe the same and `d - w` is zero: the
95
+ // meeting point is the midpoint, which is what halving always said. What is
96
+ // new is that each one's road ENDS where the next begins, so neither writes
97
+ // into the other's names.
98
+ const actions = planFor({ speedX: 4.45, penalty: 1, maxRuns: 2, runs: [] }).actions;
99
+
100
+ assert.deepEqual(
101
+ actions.map((one) => `${one.type} #${one.from}..#${one.to}`),
102
+ ["start #0..#100", "start #101..#199"],
103
+ "0..199 halved at 100"
104
+ );
105
+ });
106
+
107
+ test("a third encoder halves the widest half that is left", () => {
108
+ const actions = planFor({ speedX: 4.45, penalty: 1, maxRuns: 3, runs: [] }).actions;
109
+
110
+ assert.deepEqual(
111
+ actions.map((one) => `${one.type} #${one.from}..#${one.to}`),
112
+ ["start #0..#50", "start #51..#100", "start #101..#199"],
113
+ "the first half is halved again, greedily, widest first"
114
+ );
115
+ });
116
+
117
+ test("a stretch of one piece is not split at all", () => {
118
+ const coverage = new CoverageMap({ segmentCount: 2 });
119
+ const actions = planEncoders({
120
+ coverage,
121
+ windows: [{ from: 0, to: 0, priority: 100, withinSeconds: 0, behind: false }],
122
+ runs: [],
123
+ maxRuns: 3,
124
+ segmentSeconds: SEGMENT_SECONDS,
125
+ speedX: 8.9,
126
+ killCostSec: 0.04,
127
+ firstByteWaitSec: 0.9,
128
+ moveCostSec: 0.94,
129
+ refetchSecPerFilmSecond: 0,
130
+ contentionPenaltyFor: () => 1,
131
+ now: 1_000_000
132
+ });
133
+
134
+ assert.equal(
135
+ actions.filter((one) => one.type === "start").length,
136
+ 1,
137
+ "there is nothing to share"
138
+ );
139
+ });