@torrent-tv/proxy 2.87.7 → 2.87.8

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 (456) hide show
  1. package/.claude/commands/opsx/apply.md +155 -155
  2. package/.claude/commands/opsx/archive.md +160 -160
  3. package/.claude/commands/opsx/explore.md +174 -174
  4. package/.claude/commands/opsx/propose.md +109 -109
  5. package/.claude/commands/opsx/sync.md +143 -143
  6. package/.claude/skills/openspec-apply-change/SKILL.md +159 -159
  7. package/.claude/skills/openspec-archive-change/SKILL.md +117 -117
  8. package/.claude/skills/openspec-explore/SKILL.md +289 -289
  9. package/.claude/skills/openspec-propose/SKILL.md +113 -113
  10. package/.claude/skills/openspec-sync-specs/SKILL.md +147 -147
  11. package/.dockerignore +5 -5
  12. package/AGENTS.md +322 -322
  13. package/CHANGELOG.md +4 -0
  14. package/Dockerfile +30 -30
  15. package/LICENSE +16 -16
  16. package/README.md +369 -369
  17. package/assets/calibration/NOTICE.md +100 -100
  18. package/assets/diagnostics/sctpstate.gdb +31 -31
  19. package/bin/cli.js +684 -684
  20. package/bin/thread-pool.js +30 -30
  21. package/biome.json +411 -411
  22. package/docs/container-architecture.md +218 -218
  23. package/docs/download-architecture.md +261 -261
  24. package/docs/encode-architecture.md +729 -729
  25. package/docs/encode-run-state.md +96 -96
  26. package/docs/logs.md +97 -97
  27. package/docs/socks5-egress-research.md +114 -114
  28. package/docs/storage-architecture.md +267 -267
  29. package/knip.json +14 -14
  30. package/openspec/changes/adaptive-bitrate/design.md +92 -92
  31. package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
  32. package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
  33. package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
  34. package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
  35. package/openspec/changes/binary-distribution/proposal.md +53 -53
  36. package/openspec/changes/chunked-request-bodies/design.md +81 -81
  37. package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
  38. package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
  39. package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
  40. package/openspec/changes/cold-start/design.md +98 -98
  41. package/openspec/changes/cold-start/proposal.md +62 -62
  42. package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
  43. package/openspec/changes/cold-start/tasks.md +48 -48
  44. package/openspec/changes/disk-cap/.openspec.yaml +2 -2
  45. package/openspec/changes/disk-cap/proposal.md +37 -37
  46. package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
  47. package/openspec/changes/disk-cap/tasks.md +19 -19
  48. package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
  49. package/openspec/changes/proxy-observability/design.md +38 -38
  50. package/openspec/changes/proxy-observability/proposal.md +49 -49
  51. package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
  52. package/openspec/changes/proxy-observability/tasks.md +21 -21
  53. package/openspec/changes/seek-debounce/design.md +118 -118
  54. package/openspec/changes/seek-debounce/proposal.md +59 -59
  55. package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
  56. package/openspec/changes/seek-debounce/tasks.md +33 -33
  57. package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
  58. package/openspec/changes/subtitle-language/proposal.md +50 -50
  59. package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
  60. package/openspec/changes/subtitle-language/tasks.md +26 -26
  61. package/openspec/changes/track-selection/.openspec.yaml +2 -2
  62. package/openspec/changes/track-selection/design.md +47 -47
  63. package/openspec/changes/track-selection/proposal.md +45 -45
  64. package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
  65. package/openspec/changes/track-selection/tasks.md +20 -20
  66. package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
  67. package/openspec/changes/transcode-quality/proposal.md +51 -51
  68. package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
  69. package/openspec/changes/transcode-quality/tasks.md +63 -63
  70. package/openspec/config.yaml +38 -38
  71. package/package.json +1 -1
  72. package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
  73. package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
  74. package/research/double-spawn-2026-09-10.md +171 -171
  75. package/research/handover-2026-09-13.md +209 -209
  76. package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
  77. package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
  78. package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
  79. package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
  80. package/routes/api/client-logs/post.js +86 -86
  81. package/routes/api/delivery-sink/get.js +72 -72
  82. package/routes/api/link-probe/get.js +51 -51
  83. package/routes/api/playback-plan/audio-tracks/post.js +29 -29
  84. package/routes/api/playback-plan/post.js +53 -53
  85. package/routes/api/sources/files/get.js +100 -100
  86. package/routes/api/sources/post.js +36 -36
  87. package/routes/api/sources/stats/get.js +132 -132
  88. package/routes/api/subtitles/get.js +230 -230
  89. package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
  90. package/routes/api/transcode-sessions/net-report/post.js +129 -129
  91. package/routes/api/transcode-sessions/post.js +160 -160
  92. package/routes/api/transcode-sessions/progress/get.js +27 -27
  93. package/routes/api/transcode-sessions/release/post.js +41 -41
  94. package/routes/api/transcode-sessions/seek/post.js +63 -63
  95. package/routes/health/get.js +13 -13
  96. package/routes/healthz/get.js +13 -13
  97. package/routes/stream/get.js +361 -361
  98. package/routes/transcode/audio-file/get.js +64 -64
  99. package/routes/transcode/audio-warm/get.js +84 -84
  100. package/routes/transcode/session-file/get.js +52 -52
  101. package/routes/transcode/variant-file/get.js +95 -95
  102. package/routes/transcode/variant-warm/get.js +84 -84
  103. package/scripts/render-run-graph.js +173 -173
  104. package/services/encode/CoverageMap.js +428 -428
  105. package/services/encode/CushionReport.js +345 -345
  106. package/services/encode/EncodeAdmission.js +332 -332
  107. package/services/encode/EncodeOrchestrator.js +1217 -1217
  108. package/services/encode/EncodePlan.js +1366 -1366
  109. package/services/encode/EncodeRun.js +776 -776
  110. package/services/encode/EncodeRuns.js +1229 -1229
  111. package/services/encode/Encoder.js +142 -142
  112. package/services/encode/EncoderSelection.js +38 -38
  113. package/services/encode/LocalObservations.js +359 -359
  114. package/services/encode/NvencEncoder.js +50 -50
  115. package/services/encode/OutputOpening.js +755 -755
  116. package/services/encode/OutputTimes.js +579 -579
  117. package/services/encode/QsvEncoder.js +58 -58
  118. package/services/encode/Renditions.js +2162 -2162
  119. package/services/encode/RunProgress.js +84 -84
  120. package/services/encode/SegmentDemand.js +95 -95
  121. package/services/encode/SoftwareEncoder.js +121 -121
  122. package/services/encode/V4l2m2mEncoder.js +53 -53
  123. package/services/encode/VaapiEncoder.js +66 -66
  124. package/services/encode/args.js +416 -416
  125. package/services/encode/available-share.js +89 -89
  126. package/services/encode/calibration.js +361 -361
  127. package/services/encode/contention.js +128 -128
  128. package/services/encode/decode-cost-fit.js +363 -363
  129. package/services/encode/encode-exit.js +114 -114
  130. package/services/encode/encode-run-state.js +551 -551
  131. package/services/encode/encoder-readings.js +45 -45
  132. package/services/encode/fingerprint.js +322 -322
  133. package/services/encode/host-load.js +174 -174
  134. package/services/encode/hwaccel.js +1894 -1894
  135. package/services/encode/index.js +9 -9
  136. package/services/encode/learned-median.js +90 -90
  137. package/services/encode/output/EncodedOutput.js +57 -57
  138. package/services/encode/output/Output.js +131 -131
  139. package/services/encode/output/OutputCatalog.js +414 -414
  140. package/services/encode/output/OutputSpec.js +390 -390
  141. package/services/encode/output/Timeline.js +308 -308
  142. package/services/encode/output/cut-grid.js +107 -107
  143. package/services/encode/output/index.js +20 -20
  144. package/services/encode/output/ladder.js +32 -32
  145. package/services/encode/output/playlists.js +284 -284
  146. package/services/encode/output/rates.js +125 -125
  147. package/services/encode/output-key-format.js +21 -21
  148. package/services/encode/quality/EncodeCost.js +1083 -1083
  149. package/services/encode/quality/HostLoad.js +525 -525
  150. package/services/encode/quality/HostTimings.js +249 -249
  151. package/services/encode/quality/OutputQualityState.js +28 -28
  152. package/services/encode/quality/QualityController.js +935 -935
  153. package/services/encode/quality/QualityOffer.js +416 -416
  154. package/services/encode/quality/drain-threat.js +47 -47
  155. package/services/encode/quality/link-budget.js +254 -254
  156. package/services/encode/quality/output-format.js +266 -266
  157. package/services/encode/quality/serving-output.js +216 -216
  158. package/services/encode/quality/visible-rung.js +57 -57
  159. package/services/encode/run-budget.js +97 -97
  160. package/services/encode/run-command.js +747 -747
  161. package/services/encode/run-costs.js +155 -155
  162. package/services/encode/run-inputs.js +42 -42
  163. package/services/encode/segment-formats/fmp4.js +437 -437
  164. package/services/encode/segment-formats/index.js +77 -77
  165. package/services/encode/segment-formats/init-compat.js +324 -324
  166. package/services/encode/segment-formats/mp4-boxes.js +311 -311
  167. package/services/encode/segment-formats/mpegts.js +134 -134
  168. package/services/encode/start-stop-cost.js +178 -178
  169. package/services/encode/throughput.js +97 -97
  170. package/services/encode/torrent-cost.js +88 -88
  171. package/services/identity/installation-id.js +38 -38
  172. package/services/media/ContainerOrchestrator.js +149 -149
  173. package/services/media/KeyframeTables.js +316 -316
  174. package/services/media/SourceFile.js +298 -298
  175. package/services/media/SubtitleCues.js +520 -520
  176. package/services/media/SubtitleOrchestrator.js +167 -167
  177. package/services/media/container/AviContainer.js +266 -266
  178. package/services/media/container/Container.js +603 -603
  179. package/services/media/container/ContainerFactory.js +56 -56
  180. package/services/media/container/KeyframeTable.js +272 -272
  181. package/services/media/container/MatroskaContainer.js +1494 -1494
  182. package/services/media/container/Mp4Container.js +1244 -1244
  183. package/services/media/container/SubtitleFileContainer.js +322 -322
  184. package/services/media/container/ebml-reader.js +159 -159
  185. package/services/media/ffmpeg-banner.js +260 -260
  186. package/services/media/keyframe-probe.js +157 -157
  187. package/services/media/media-info-probe.js +93 -93
  188. package/services/media/tracks/AudioTrack.js +131 -131
  189. package/services/media/tracks/ContainerTrack.js +75 -75
  190. package/services/media/tracks/ImageSubtitleTrack.js +19 -19
  191. package/services/media/tracks/SubtitleTrack.js +38 -38
  192. package/services/media/tracks/TextSubtitleTrack.js +287 -287
  193. package/services/media/tracks/VideoTrack.js +33 -33
  194. package/services/media/tracks/index.js +15 -15
  195. package/services/media/tracks/language-detect.js +174 -174
  196. package/services/server/OutputLifecycle.js +418 -418
  197. package/services/server/SegmentServing.js +1146 -1146
  198. package/services/server/ViewerRequests.js +522 -522
  199. package/services/server/WaitLedger.js +142 -142
  200. package/services/server/controllers/SubtitleController.js +109 -109
  201. package/services/server/http-responses.js +60 -60
  202. package/services/server/transcode-session-files.js +325 -325
  203. package/services/server/wire-outputs.js +983 -983
  204. package/services/storage/Diagnostics.js +202 -202
  205. package/services/storage/MachineBudget.js +235 -235
  206. package/services/storage/allowance.js +107 -107
  207. package/services/storage/core-dumps.js +106 -106
  208. package/services/storage/files/CompletedFiles.js +391 -391
  209. package/services/storage/files/piece-from-whole-file.js +118 -118
  210. package/services/storage/free.js +71 -71
  211. package/services/storage/keep.js +48 -48
  212. package/services/storage/machine-memory.js +56 -56
  213. package/services/storage/memory-report.js +575 -575
  214. package/services/storage/piece-store/piece-disk-store.js +516 -516
  215. package/services/storage/piece-store/piece-lru.js +495 -495
  216. package/services/storage/piece-store/shared-piece-store.js +1880 -1880
  217. package/services/storage/returns.js +103 -103
  218. package/services/storage/segment-store/SegmentStore.js +1196 -1196
  219. package/services/storage/wire.js +175 -175
  220. package/services/torrent/Contents.js +324 -324
  221. package/services/torrent/demand/DemandRegister.js +202 -202
  222. package/services/torrent/demand/Urgency.js +137 -137
  223. package/services/torrent/demand/Window.js +118 -118
  224. package/services/torrent/demand/index.js +8 -8
  225. package/services/torrent/demand/pieces.js +140 -140
  226. package/services/torrent/download/SwarmSelection.js +386 -386
  227. package/services/torrent/download/registry.js +102 -102
  228. package/services/torrent/download/withdraw-claim.js +80 -80
  229. package/services/torrent/files.js +455 -455
  230. package/services/torrent/naming.js +619 -619
  231. package/services/torrent/piece-store-of.js +29 -29
  232. package/services/torrent/supply-margin.js +262 -262
  233. package/services/torrent/worker/background-fill.js +213 -213
  234. package/services/torrent/worker/channel.js +271 -271
  235. package/services/torrent/worker/fastest-wires.js +342 -342
  236. package/services/torrent/worker/handle-state.js +23 -23
  237. package/services/torrent/worker/held-bytes.js +155 -155
  238. package/services/torrent/worker/install-webrtc-shim.js +32 -32
  239. package/services/torrent/worker/piece-reader.js +1078 -1078
  240. package/services/torrent/worker/resume-warm.js +107 -107
  241. package/services/torrent/worker/webrtc-shim.js +219 -219
  242. package/services/transport/data-channel-handler.js +21 -8
  243. package/services/transport/delivery-probe.js +842 -842
  244. package/services/transport/health-collector.js +53 -53
  245. package/services/transport/nat-classifier.js +231 -231
  246. package/services/transport/packet-witness.js +808 -808
  247. package/services/transport/port-mapper.js +235 -235
  248. package/services/transport/registry-api.js +81 -81
  249. package/services/transport/tunnel-client.js +447 -447
  250. package/services/transport/usrsctp-state.js +175 -175
  251. package/services/transport/webrtc-manager.js +484 -484
  252. package/services/viewer/Assignments.js +340 -340
  253. package/services/viewer/PriorityMap.js +390 -390
  254. package/services/viewer/PriorityOrchestrator.js +325 -325
  255. package/services/viewer/Viewer.js +657 -657
  256. package/services/viewer/Viewers.js +368 -368
  257. package/services/viewer/active-output.js +37 -37
  258. package/services/viewer/audio-start.js +46 -46
  259. package/services/viewer/buffer-trend.js +81 -81
  260. package/services/viewer/choices.js +647 -647
  261. package/services/viewer/positions.js +126 -126
  262. package/services/viewer/report-intake.js +46 -46
  263. package/store/source-registry.js +71 -71
  264. package/test/asset-paths.test.js +28 -28
  265. package/test/audio-track-bitrate.test.js +40 -40
  266. package/test/auto-quality-step.test.js +707 -707
  267. package/test/available-share.test.js +76 -76
  268. package/test/background-fill.test.js +190 -190
  269. package/test/client-log-file.test.js +110 -110
  270. package/test/completed-files.test.js +115 -115
  271. package/test/concurrent-cost.test.js +148 -148
  272. package/test/container-media-info.test.js +228 -228
  273. package/test/contention.test.js +76 -76
  274. package/test/core-dumps.test.js +57 -57
  275. package/test/coverage-follows-the-disk.test.js +228 -228
  276. package/test/coverage-map.test.js +195 -195
  277. package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
  278. package/test/cut-times-timeline.test.js +65 -65
  279. package/test/cuts-follow-published-grid.test.js +109 -109
  280. package/test/data-channel-frame.test.js +63 -63
  281. package/test/data-channel-viewer-lifecycle.test.js +92 -0
  282. package/test/dead-torrent-handle.test.js +52 -52
  283. package/test/declared-rates.test.js +116 -116
  284. package/test/declared-tracks.test.js +35 -35
  285. package/test/decode-cost-fit.test.js +202 -202
  286. package/test/decode-cost.test.js +462 -462
  287. package/test/delivery-probe.test.js +393 -393
  288. package/test/demand-register.test.js +206 -206
  289. package/test/dht-bootstrap.test.js +72 -72
  290. package/test/drain-threat.test.js +65 -65
  291. package/test/duplicate-add.test.js +48 -48
  292. package/test/encode-admission.test.js +146 -146
  293. package/test/encode-cost.test.js +283 -283
  294. package/test/encode-exit.test.js +111 -111
  295. package/test/encode-orchestrator.test.js +419 -419
  296. package/test/encode-plan-viewers.test.js +718 -718
  297. package/test/encode-plan.test.js +545 -545
  298. package/test/encode-run-state.test.js +314 -314
  299. package/test/encode-run.test.js +161 -161
  300. package/test/encode-slope.test.js +46 -46
  301. package/test/encoded-output.test.js +39 -39
  302. package/test/encoder-kinds.test.js +122 -122
  303. package/test/encoder-readings.test.js +46 -46
  304. package/test/every-claimant-is-told-its-share.test.js +152 -152
  305. package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
  306. package/test/failed-start-breaker.test.js +202 -202
  307. package/test/fastest-wires.test.js +143 -143
  308. package/test/ffmpeg-stream-counts.test.js +78 -78
  309. package/test/file-edges.test.js +191 -191
  310. package/test/fmp4-self-contained.test.js +80 -80
  311. package/test/fmp4-timestamps.test.js +140 -140
  312. package/test/fmp4-track-completeness.test.js +41 -41
  313. package/test/format-keyed-outputs.test.js +78 -78
  314. package/test/health-metrics.test.js +38 -38
  315. package/test/held-request-width.test.js +157 -157
  316. package/test/helpers/encode-run.js +136 -136
  317. package/test/helpers/held-file.js +29 -29
  318. package/test/helpers/manager.js +57 -57
  319. package/test/helpers/output-spec.js +63 -63
  320. package/test/host-load.test.js +68 -68
  321. package/test/index-accuracy-is-about-the-file.test.js +128 -128
  322. package/test/init-compat.test.js +219 -219
  323. package/test/input-lost-quiets-the-plan.test.js +261 -261
  324. package/test/input-unavailable.test.js +48 -48
  325. package/test/installation-id.test.js +39 -39
  326. package/test/keeping-period.test.js +83 -83
  327. package/test/keyframe-index-accuracy.test.js +188 -188
  328. package/test/keyframe-table-budget.test.js +102 -102
  329. package/test/keyframes-belong-to-the-file.test.js +249 -249
  330. package/test/learned-median.test.js +37 -37
  331. package/test/letting-go-asks-who-is-watching.test.js +191 -191
  332. package/test/limit-row-by-area.test.js +60 -60
  333. package/test/link-budget.test.js +111 -111
  334. package/test/link-probe.test.js +56 -56
  335. package/test/local-encoding-data.test.js +273 -273
  336. package/test/logger-repeats.test.js +134 -134
  337. package/test/look-ahead-coverage.test.js +56 -56
  338. package/test/manager-surface.test.js +292 -292
  339. package/test/matroska-cues-track.test.js +192 -192
  340. package/test/memory-budget.test.js +390 -390
  341. package/test/move-cost.test.js +216 -216
  342. package/test/mp4-subtitles.test.js +173 -173
  343. package/test/one-authority.test.js +285 -285
  344. package/test/one-budget.test.js +149 -149
  345. package/test/one-output-one-session.test.js +204 -204
  346. package/test/one-resource-divided.test.js +185 -185
  347. package/test/one-serving-output-per-height.test.js +458 -458
  348. package/test/orchestrator-wired.test.js +206 -206
  349. package/test/output-catalog.test.js +39 -39
  350. package/test/output-shape.test.js +21 -21
  351. package/test/output-spec.test.js +251 -251
  352. package/test/output-speed.test.js +88 -88
  353. package/test/outputs-being-prepared.test.js +40 -40
  354. package/test/packet-witness-ring.test.js +236 -236
  355. package/test/packet-witness.test.js +148 -148
  356. package/test/piece-disk-store.test.js +381 -381
  357. package/test/piece-from-whole-file.test.js +129 -129
  358. package/test/piece-lru.test.js +319 -319
  359. package/test/piece-reader.test.js +162 -162
  360. package/test/piece-selection-offset.test.js +85 -85
  361. package/test/piece-store-eviction.test.js +560 -560
  362. package/test/piece-store-never-refuses.test.js +153 -153
  363. package/test/piece-store-reservations.test.js +318 -318
  364. package/test/piece-store-slow-disk.test.js +159 -159
  365. package/test/piece-tail.test.js +97 -97
  366. package/test/plan-host-timings.test.js +68 -68
  367. package/test/plan-is-steady.test.js +225 -225
  368. package/test/priority-map-download.test.js +174 -174
  369. package/test/priority-map-emptied.test.js +204 -204
  370. package/test/priority-map-per-output.test.js +215 -215
  371. package/test/priority-map.test.js +163 -163
  372. package/test/probe-wedge-certainty.test.js +60 -60
  373. package/test/produced-copy-choice.test.js +213 -213
  374. package/test/published-timeline.test.js +83 -83
  375. package/test/quality-offer.test.js +162 -162
  376. package/test/quality-variants.test.js +1229 -1229
  377. package/test/rate-control-in-the-output.test.js +180 -180
  378. package/test/read-survives-withdrawal.test.js +164 -164
  379. package/test/read-window.test.js +365 -365
  380. package/test/resume-warm.test.js +39 -39
  381. package/test/run-graph-drift.test.js +37 -37
  382. package/test/run-inputs.test.js +80 -80
  383. package/test/run-intervals.test.js +100 -100
  384. package/test/run-position-follows-published-grid.test.js +89 -89
  385. package/test/run-progress.test.js +65 -65
  386. package/test/same-height-switch.test.js +664 -664
  387. package/test/seek-frees-the-viewers-requests.test.js +217 -217
  388. package/test/seek-landing.test.js +109 -109
  389. package/test/seek-target-not-superseded.test.js +195 -195
  390. package/test/segment-closed-on-both-branches.test.js +96 -96
  391. package/test/segment-cut-times.test.js +63 -63
  392. package/test/segment-demand.test.js +80 -80
  393. package/test/segment-hold-supersede.test.js +91 -91
  394. package/test/segment-judged-by-its-own-header.test.js +221 -221
  395. package/test/segment-serve-wiring.test.js +475 -475
  396. package/test/segment-store-eviction.test.js +247 -247
  397. package/test/segment-store.test.js +323 -323
  398. package/test/segment-true-start.test.js +86 -86
  399. package/test/segments-are-shared.test.js +176 -176
  400. package/test/send-chunk-bytes.test.js +86 -86
  401. package/test/serving-existing-output.test.js +102 -102
  402. package/test/serving-output.test.js +102 -102
  403. package/test/shared-piece-store.test.js +310 -310
  404. package/test/sidecar-naming.test.js +142 -142
  405. package/test/source-file.test.js +133 -133
  406. package/test/source-files-route.test.js +103 -103
  407. package/test/source-registry.test.js +18 -18
  408. package/test/split-a-stretch.test.js +139 -139
  409. package/test/stale-request-after-seek.test.js +158 -158
  410. package/test/startup-readings.test.js +124 -124
  411. package/test/stream-route.test.js +182 -182
  412. package/test/subtitle-cue-framing.test.js +200 -200
  413. package/test/subtitle-cue-source.test.js +102 -102
  414. package/test/subtitle-cue-walk.test.js +370 -370
  415. package/test/subtitle-cursor.test.js +63 -63
  416. package/test/subtitle-defaults.test.js +118 -118
  417. package/test/subtitle-language.test.js +252 -252
  418. package/test/subtitle-track-numbering.test.js +386 -386
  419. package/test/supply-margin.test.js +177 -177
  420. package/test/swarm-follows-readers.test.js +174 -174
  421. package/test/swarm-reach.test.js +130 -130
  422. package/test/swarm-selection.test.js +220 -220
  423. package/test/tail-duplication.test.js +167 -167
  424. package/test/the-channel-has-both-ends.test.js +91 -91
  425. package/test/the-torrent-thread-serves-bytes.test.js +107 -107
  426. package/test/thread-pool.test.js +53 -53
  427. package/test/timeline.test.js +84 -84
  428. package/test/torrent-contents.test.js +235 -235
  429. package/test/torrent-cost.test.js +71 -71
  430. package/test/torrent-files.test.js +221 -221
  431. package/test/torrent-naming.test.js +255 -255
  432. package/test/tracks-begin-together.test.js +209 -209
  433. package/test/tunnel-renewal.test.js +167 -167
  434. package/test/two-viewers-one-picture.test.js +407 -407
  435. package/test/upload-hurry.test.js +186 -186
  436. package/test/usrsctp-state.test.js +131 -131
  437. package/test/video-facts.test.js +102 -102
  438. package/test/viewer-assignments.test.js +143 -143
  439. package/test/viewer-outputs.test.js +285 -285
  440. package/test/viewer-presence.test.js +116 -116
  441. package/test/viewer-requests-admission.test.js +89 -89
  442. package/test/viewer-states.test.js +189 -189
  443. package/test/viewer-subtitle-subscription.test.js +96 -96
  444. package/test/viewer.test.js +139 -139
  445. package/test/viewing-generations.test.js +390 -390
  446. package/test/visible-rung.test.js +37 -37
  447. package/test/wait-ledger.test.js +100 -100
  448. package/test/webrtc-shim.test.js +149 -149
  449. package/test/wedge-certainty.test.js +131 -131
  450. package/test/withdraw-piece-claim.test.js +212 -212
  451. package/test/worker-channel.test.js +158 -158
  452. package/utils/client-log-file.js +151 -151
  453. package/utils/logger.js +312 -312
  454. package/utils/parse-range.js +25 -25
  455. package/utils/perf.js +59 -59
  456. package/utils/torrent-source-key.js +38 -38
@@ -1,359 +1,359 @@
1
- /**
2
- * @file What this proxy has seen its own admitted encodes do, kept between
3
- * runs and used only on the configuration it was seen on (roadmap item 97,
4
- * step 14).
5
- *
6
- * NOT A TABLE OF QUALITY, and not a condition for anything. The proxy selects,
7
- * admits and serves with nothing kept here at all: the startup calibration is
8
- * what qualifies a mode and prices it, and the admission is what decides
9
- * whether there is room. What is kept here makes three of those figures more
10
- * exact on THIS machine once it has done the work for real:
11
- *
12
- * 1. how fast an encode of this mode, at this size, actually ran alone on this
13
- * kind of source — used in place of the startup prediction when an output
14
- * of that mode is priced;
15
- * 2. what the segments of an output of this mode and rate control actually
16
- * carried, average and peak — used as the peak of an encoder that states no
17
- * bound of its own (a hardware encoder has no `maxrate`), which otherwise
18
- * has no figure at all;
19
- * 3. how long an output of this mode took to have the piece a viewer needed
20
- * after it was asked for, and what that viewer held when they moved — used
21
- * as the time to ready when deciding whether a draining buffer is a threat.
22
- *
23
- * WHAT IS NEVER KEPT: a picture, a clip, a segment, anything about a viewer
24
- * beyond the seconds they held, and anything that leaves this machine.
25
- *
26
- * WHAT DECIDES WHETHER A FIGURE APPLIES is the configuration it was seen on:
27
- * every key starts with the configuration key of the encoder that made it
28
- * (`fingerprint.js`). A figure seen on another ffmpeg, another libx264, another
29
- * processor or thread count, another device or driver matches nothing and is
30
- * dropped when the file is read — not converted, not scaled.
31
- *
32
- * AND WHETHER IT IS COMPARABLE. A speed is used only for a source of the same
33
- * codec, bit depth and frame, whose bitrate is no higher than the one it was
34
- * seen on — a slower reading on harder material is safe to apply to easier
35
- * material, the reverse is not — and it is the SLOWEST such reading that is
36
- * used. The largest peak and the longest preparation are used, for the same
37
- * reason: each errs toward the viewer.
38
- *
39
- * Written when something is noted — a run ending, a move completing — which
40
- * is rare, so a process that is killed loses at most what it was doing.
41
- */
42
-
43
- import { readFileSync, writeFileSync } from "node:fs";
44
- import { configurationKeyOf, kindOfEncoderName } from "./fingerprint.js";
45
-
46
- /**
47
- * How many readings are kept per key. A history window, not a figure about the
48
- * machine: long enough that one run cannot decide, short enough that the
49
- * figure follows the host as it changes. The same length the host timings keep
50
- * (`quality/HostTimings.js`).
51
- */
52
- const READINGS_KEPT = 20;
53
-
54
- /** The version of the file's shape; a file of another shape is not read. */
55
- const FILE_VERSION = 1;
56
-
57
- /**
58
- * @typedef {object} ContentDescriptor
59
- * @property {string} codec
60
- * @property {number | null} bitDepth
61
- * @property {number} width
62
- * @property {number} height
63
- * @property {number} megabitsPerSecond
64
- */
65
-
66
- export class LocalObservations {
67
- /** @type {import("./fingerprint.js").HostFingerprint} */
68
- #fingerprint;
69
-
70
- /** @type {string} */
71
- #filePath;
72
-
73
- /** @type {{ info: (m: string) => void, warn: (m: string) => void }} */
74
- #logger;
75
-
76
- /** @type {() => number} */
77
- #now;
78
-
79
- /** @type {{ speed: Record<string, object[]>, rates: Record<string, object[]>, preparation: Record<string, object[]> }} */
80
- #entries = { speed: {}, rates: {}, preparation: {} };
81
-
82
- /**
83
- * @param {object} params
84
- * @param {import("./fingerprint.js").HostFingerprint} params.fingerprint
85
- * @param {string} params.filePath - Where the readings are kept; empty keeps
86
- * them for this process only.
87
- * @param {{ info: (m: string) => void, warn: (m: string) => void }} [params.logger]
88
- * @param {() => number} [params.now]
89
- */
90
- constructor({ fingerprint, filePath, logger = null, now = Date.now }) {
91
- if (!fingerprint || typeof fingerprint !== "object") {
92
- throw new TypeError("LocalObservations requires the host's fingerprint");
93
- }
94
- this.#fingerprint = fingerprint;
95
- this.#filePath = typeof filePath === "string" ? filePath : "";
96
- this.#logger = logger ?? { info: () => {}, warn: () => {} };
97
- this.#now = now;
98
- }
99
-
100
- /**
101
- * The key of the MODE an output is encoded in: the configuration, the
102
- * encoder, the frame and rate, the speed setting and whether it tone-maps.
103
- * Null for an output that re-encodes no picture.
104
- *
105
- * @param {import("./output/OutputSpec.js").OutputSpec | null | undefined} spec
106
- * @returns {string | null}
107
- */
108
- modeKeyOf(spec) {
109
- const encode = spec?.video?.encode;
110
- if (!encode) {
111
- return null;
112
- }
113
- const configuration = configurationKeyOf(this.#fingerprint, {
114
- name: encode.encoder,
115
- kind: kindOfEncoderName(encode.encoder)
116
- });
117
- return `${configuration}|${encode.encoder}/${encode.width}x${encode.height}@${encode.fps}/` +
118
- `${encode.preset ?? "-"}/${encode.tonemap ? "tonemap" : "none"}`;
119
- }
120
-
121
- /**
122
- * The mode's key with its rate control, which is what bounds what its
123
- * segments carry.
124
- *
125
- * @param {import("./output/OutputSpec.js").OutputSpec | null | undefined} spec
126
- * @returns {string | null}
127
- */
128
- rateKeyOf(spec) {
129
- const mode = this.modeKeyOf(spec);
130
- if (mode === null) {
131
- return null;
132
- }
133
- const rate = spec.video.encode.rateControl;
134
- return `${mode}/vbv=${rate ? `${rate.maxrateKbps}k-${rate.bufsizeKbps}k@${rate.level ?? "-"}` : "-"}`;
135
- }
136
-
137
- /**
138
- * Read what an earlier run kept, dropping whatever no current configuration
139
- * matches.
140
- *
141
- * @param {string[]} currentConfigurations - The configuration keys of every
142
- * encoder this process may use.
143
- * @returns {void}
144
- */
145
- load(currentConfigurations) {
146
- if (!this.#filePath) {
147
- return;
148
- }
149
- let raw;
150
- try {
151
- raw = JSON.parse(readFileSync(this.#filePath, "utf8"));
152
- } catch {
153
- return; // no file yet, or unreadable: nothing is known, which is the base case
154
- }
155
- if (raw?.version !== FILE_VERSION || !raw.entries || typeof raw.entries !== "object") {
156
- this.#logger.info(`local observations: ${this.#filePath} is of another shape; starting afresh`);
157
- return;
158
- }
159
- const allowed = new Set(currentConfigurations);
160
- let kept = 0;
161
- let dropped = 0;
162
- for (const kind of ["speed", "rates", "preparation"]) {
163
- const section = raw.entries[kind] && typeof raw.entries[kind] === "object" ? raw.entries[kind] : {};
164
- for (const [key, readings] of Object.entries(section)) {
165
- const configuration = key.split("|")[0];
166
- if (!allowed.has(configuration) || !Array.isArray(readings)) {
167
- dropped += 1;
168
- continue;
169
- }
170
- this.#entries[kind][key] = readings.slice(-READINGS_KEPT);
171
- kept += 1;
172
- }
173
- }
174
- this.#logger.info(
175
- `local observations: ${kept} key(s) kept from ${this.#filePath}` +
176
- (dropped > 0 ? `, ${dropped} dropped because they were seen on a configuration this host no longer has` : "")
177
- );
178
- if (dropped > 0) {
179
- this.#save();
180
- }
181
- }
182
-
183
- /**
184
- * An encode of this output has ended: keep how fast it ran alone and what
185
- * its segments carried.
186
- *
187
- * @param {object} params
188
- * @param {import("./output/OutputSpec.js").OutputSpec} params.spec
189
- * @param {ContentDescriptor | null} params.content - What it was encoded from.
190
- * @param {number | null} params.aloneSpeedX - Seconds of film per second,
191
- * measured with nothing else encoding.
192
- * @param {{ averageKbps: number, peakKbps: number } | null} params.segmentKbps
193
- * @returns {void}
194
- */
195
- noteEncode({ spec, content, aloneSpeedX, segmentKbps }) {
196
- const modeKey = this.modeKeyOf(spec);
197
- if (modeKey === null) {
198
- return;
199
- }
200
- let changed = false;
201
- if (Number.isFinite(aloneSpeedX) && aloneSpeedX > 0 && LocalObservations.#usableContent(content)) {
202
- this.#push("speed", modeKey, { content, speedX: aloneSpeedX, at: this.#now() });
203
- changed = true;
204
- }
205
- const rateKey = this.rateKeyOf(spec);
206
- if (
207
- segmentKbps &&
208
- Number.isFinite(segmentKbps.averageKbps) && segmentKbps.averageKbps > 0 &&
209
- Number.isFinite(segmentKbps.peakKbps) && segmentKbps.peakKbps > 0
210
- ) {
211
- this.#push("rates", rateKey, {
212
- averageKbps: segmentKbps.averageKbps,
213
- peakKbps: segmentKbps.peakKbps,
214
- at: this.#now()
215
- });
216
- changed = true;
217
- }
218
- if (changed) {
219
- this.#save();
220
- }
221
- }
222
-
223
- /**
224
- * A viewer has been moved onto an output that was being prepared for them:
225
- * keep how long it took and what they held when they moved.
226
- *
227
- * @param {object} params
228
- * @param {import("./output/OutputSpec.js").OutputSpec} params.spec - The output moved onto.
229
- * @param {number} params.seconds - From the preparation starting to the move.
230
- * @param {number | null} params.bufferedSec - What the viewer held at the move.
231
- * @returns {void}
232
- */
233
- notePreparation({ spec, seconds, bufferedSec }) {
234
- const modeKey = this.modeKeyOf(spec);
235
- if (modeKey === null || !(Number.isFinite(seconds) && seconds >= 0)) {
236
- return;
237
- }
238
- this.#push("preparation", modeKey, {
239
- seconds,
240
- bufferedSec: Number.isFinite(bufferedSec) ? bufferedSec : null,
241
- at: this.#now()
242
- });
243
- this.#save();
244
- }
245
-
246
- /**
247
- * The slowest this mode has been seen running alone on comparable material,
248
- * or null where nothing comparable has been seen.
249
- *
250
- * @param {import("./output/OutputSpec.js").OutputSpec} spec
251
- * @param {ContentDescriptor | null} content
252
- * @returns {number | null}
253
- */
254
- slowestAloneSpeed(spec, content) {
255
- const modeKey = this.modeKeyOf(spec);
256
- if (modeKey === null || !LocalObservations.#usableContent(content)) {
257
- return null;
258
- }
259
- let slowest = null;
260
- for (const reading of this.#entries.speed[modeKey] ?? []) {
261
- const seen = reading.content;
262
- if (
263
- seen?.codec === content.codec &&
264
- (seen?.bitDepth ?? null) === (content.bitDepth ?? null) &&
265
- seen?.width === content.width &&
266
- seen?.height === content.height &&
267
- seen?.megabitsPerSecond >= content.megabitsPerSecond
268
- ) {
269
- slowest = slowest === null ? reading.speedX : Math.min(slowest, reading.speedX);
270
- }
271
- }
272
- return slowest;
273
- }
274
-
275
- /**
276
- * The largest peak an output of this mode and rate control has been seen
277
- * carrying, in megabits per second, or null.
278
- *
279
- * @param {import("./output/OutputSpec.js").OutputSpec} spec
280
- * @returns {number | null}
281
- */
282
- peakMbps(spec) {
283
- const rateKey = this.rateKeyOf(spec);
284
- const readings = rateKey === null ? [] : (this.#entries.rates[rateKey] ?? []);
285
- return readings.length === 0 ? null : Math.max(...readings.map((reading) => reading.peakKbps)) / 1000;
286
- }
287
-
288
- /**
289
- * The longest an output of this mode has been seen taking to be ready for a
290
- * viewer moving onto it, in seconds, or null.
291
- *
292
- * @param {import("./output/OutputSpec.js").OutputSpec} spec
293
- * @returns {number | null}
294
- */
295
- longestPreparationSec(spec) {
296
- const modeKey = this.modeKeyOf(spec);
297
- const readings = modeKey === null ? [] : (this.#entries.preparation[modeKey] ?? []);
298
- return readings.length === 0 ? null : Math.max(...readings.map((reading) => reading.seconds));
299
- }
300
-
301
- /**
302
- * @param {"speed" | "rates" | "preparation"} kind
303
- * @param {string} key
304
- * @param {object} reading
305
- * @returns {void}
306
- */
307
- #push(kind, key, reading) {
308
- const readings = [...(this.#entries[kind][key] ?? []), reading].slice(-READINGS_KEPT);
309
- this.#entries[kind][key] = readings;
310
- }
311
-
312
- /** @returns {void} */
313
- #save() {
314
- if (!this.#filePath) {
315
- return;
316
- }
317
- try {
318
- writeFileSync(this.#filePath, JSON.stringify({ version: FILE_VERSION, entries: this.#entries }));
319
- } catch (error) {
320
- // A read-only install keeps what it learned for this process only.
321
- this.#logger.warn(
322
- `local observations: could not write ${this.#filePath} (${error instanceof Error ? error.message : String(error)})`
323
- );
324
- }
325
- }
326
-
327
- /**
328
- * @param {ContentDescriptor | null} content
329
- * @returns {boolean}
330
- */
331
- static #usableContent(content) {
332
- return Boolean(content) &&
333
- typeof content.codec === "string" && content.codec.length > 0 &&
334
- content.width > 0 && content.height > 0 &&
335
- Number.isFinite(content.megabitsPerSecond) && content.megabitsPerSecond > 0;
336
- }
337
- }
338
-
339
- /**
340
- * What an observation of an encode is filed as having been made FROM.
341
- *
342
- * @param {{ width: number | null, height: number | null, decode: { codec: string, bitDepth: number | null, megabitsPerSecond: number } | null } | null} file
343
- * @returns {ContentDescriptor | null}
344
- */
345
- export function contentOf(file) {
346
- const decode = file?.decode ?? null;
347
- const width = Number(file?.width) || 0;
348
- const height = Number(file?.height) || 0;
349
- if (!decode || width <= 0 || height <= 0) {
350
- return null;
351
- }
352
- return {
353
- codec: decode.codec,
354
- bitDepth: decode.bitDepth ?? null,
355
- width,
356
- height,
357
- megabitsPerSecond: decode.megabitsPerSecond
358
- };
359
- }
1
+ /**
2
+ * @file What this proxy has seen its own admitted encodes do, kept between
3
+ * runs and used only on the configuration it was seen on (roadmap item 97,
4
+ * step 14).
5
+ *
6
+ * NOT A TABLE OF QUALITY, and not a condition for anything. The proxy selects,
7
+ * admits and serves with nothing kept here at all: the startup calibration is
8
+ * what qualifies a mode and prices it, and the admission is what decides
9
+ * whether there is room. What is kept here makes three of those figures more
10
+ * exact on THIS machine once it has done the work for real:
11
+ *
12
+ * 1. how fast an encode of this mode, at this size, actually ran alone on this
13
+ * kind of source — used in place of the startup prediction when an output
14
+ * of that mode is priced;
15
+ * 2. what the segments of an output of this mode and rate control actually
16
+ * carried, average and peak — used as the peak of an encoder that states no
17
+ * bound of its own (a hardware encoder has no `maxrate`), which otherwise
18
+ * has no figure at all;
19
+ * 3. how long an output of this mode took to have the piece a viewer needed
20
+ * after it was asked for, and what that viewer held when they moved — used
21
+ * as the time to ready when deciding whether a draining buffer is a threat.
22
+ *
23
+ * WHAT IS NEVER KEPT: a picture, a clip, a segment, anything about a viewer
24
+ * beyond the seconds they held, and anything that leaves this machine.
25
+ *
26
+ * WHAT DECIDES WHETHER A FIGURE APPLIES is the configuration it was seen on:
27
+ * every key starts with the configuration key of the encoder that made it
28
+ * (`fingerprint.js`). A figure seen on another ffmpeg, another libx264, another
29
+ * processor or thread count, another device or driver matches nothing and is
30
+ * dropped when the file is read — not converted, not scaled.
31
+ *
32
+ * AND WHETHER IT IS COMPARABLE. A speed is used only for a source of the same
33
+ * codec, bit depth and frame, whose bitrate is no higher than the one it was
34
+ * seen on — a slower reading on harder material is safe to apply to easier
35
+ * material, the reverse is not — and it is the SLOWEST such reading that is
36
+ * used. The largest peak and the longest preparation are used, for the same
37
+ * reason: each errs toward the viewer.
38
+ *
39
+ * Written when something is noted — a run ending, a move completing — which
40
+ * is rare, so a process that is killed loses at most what it was doing.
41
+ */
42
+
43
+ import { readFileSync, writeFileSync } from "node:fs";
44
+ import { configurationKeyOf, kindOfEncoderName } from "./fingerprint.js";
45
+
46
+ /**
47
+ * How many readings are kept per key. A history window, not a figure about the
48
+ * machine: long enough that one run cannot decide, short enough that the
49
+ * figure follows the host as it changes. The same length the host timings keep
50
+ * (`quality/HostTimings.js`).
51
+ */
52
+ const READINGS_KEPT = 20;
53
+
54
+ /** The version of the file's shape; a file of another shape is not read. */
55
+ const FILE_VERSION = 1;
56
+
57
+ /**
58
+ * @typedef {object} ContentDescriptor
59
+ * @property {string} codec
60
+ * @property {number | null} bitDepth
61
+ * @property {number} width
62
+ * @property {number} height
63
+ * @property {number} megabitsPerSecond
64
+ */
65
+
66
+ export class LocalObservations {
67
+ /** @type {import("./fingerprint.js").HostFingerprint} */
68
+ #fingerprint;
69
+
70
+ /** @type {string} */
71
+ #filePath;
72
+
73
+ /** @type {{ info: (m: string) => void, warn: (m: string) => void }} */
74
+ #logger;
75
+
76
+ /** @type {() => number} */
77
+ #now;
78
+
79
+ /** @type {{ speed: Record<string, object[]>, rates: Record<string, object[]>, preparation: Record<string, object[]> }} */
80
+ #entries = { speed: {}, rates: {}, preparation: {} };
81
+
82
+ /**
83
+ * @param {object} params
84
+ * @param {import("./fingerprint.js").HostFingerprint} params.fingerprint
85
+ * @param {string} params.filePath - Where the readings are kept; empty keeps
86
+ * them for this process only.
87
+ * @param {{ info: (m: string) => void, warn: (m: string) => void }} [params.logger]
88
+ * @param {() => number} [params.now]
89
+ */
90
+ constructor({ fingerprint, filePath, logger = null, now = Date.now }) {
91
+ if (!fingerprint || typeof fingerprint !== "object") {
92
+ throw new TypeError("LocalObservations requires the host's fingerprint");
93
+ }
94
+ this.#fingerprint = fingerprint;
95
+ this.#filePath = typeof filePath === "string" ? filePath : "";
96
+ this.#logger = logger ?? { info: () => {}, warn: () => {} };
97
+ this.#now = now;
98
+ }
99
+
100
+ /**
101
+ * The key of the MODE an output is encoded in: the configuration, the
102
+ * encoder, the frame and rate, the speed setting and whether it tone-maps.
103
+ * Null for an output that re-encodes no picture.
104
+ *
105
+ * @param {import("./output/OutputSpec.js").OutputSpec | null | undefined} spec
106
+ * @returns {string | null}
107
+ */
108
+ modeKeyOf(spec) {
109
+ const encode = spec?.video?.encode;
110
+ if (!encode) {
111
+ return null;
112
+ }
113
+ const configuration = configurationKeyOf(this.#fingerprint, {
114
+ name: encode.encoder,
115
+ kind: kindOfEncoderName(encode.encoder)
116
+ });
117
+ return `${configuration}|${encode.encoder}/${encode.width}x${encode.height}@${encode.fps}/` +
118
+ `${encode.preset ?? "-"}/${encode.tonemap ? "tonemap" : "none"}`;
119
+ }
120
+
121
+ /**
122
+ * The mode's key with its rate control, which is what bounds what its
123
+ * segments carry.
124
+ *
125
+ * @param {import("./output/OutputSpec.js").OutputSpec | null | undefined} spec
126
+ * @returns {string | null}
127
+ */
128
+ rateKeyOf(spec) {
129
+ const mode = this.modeKeyOf(spec);
130
+ if (mode === null) {
131
+ return null;
132
+ }
133
+ const rate = spec.video.encode.rateControl;
134
+ return `${mode}/vbv=${rate ? `${rate.maxrateKbps}k-${rate.bufsizeKbps}k@${rate.level ?? "-"}` : "-"}`;
135
+ }
136
+
137
+ /**
138
+ * Read what an earlier run kept, dropping whatever no current configuration
139
+ * matches.
140
+ *
141
+ * @param {string[]} currentConfigurations - The configuration keys of every
142
+ * encoder this process may use.
143
+ * @returns {void}
144
+ */
145
+ load(currentConfigurations) {
146
+ if (!this.#filePath) {
147
+ return;
148
+ }
149
+ let raw;
150
+ try {
151
+ raw = JSON.parse(readFileSync(this.#filePath, "utf8"));
152
+ } catch {
153
+ return; // no file yet, or unreadable: nothing is known, which is the base case
154
+ }
155
+ if (raw?.version !== FILE_VERSION || !raw.entries || typeof raw.entries !== "object") {
156
+ this.#logger.info(`local observations: ${this.#filePath} is of another shape; starting afresh`);
157
+ return;
158
+ }
159
+ const allowed = new Set(currentConfigurations);
160
+ let kept = 0;
161
+ let dropped = 0;
162
+ for (const kind of ["speed", "rates", "preparation"]) {
163
+ const section = raw.entries[kind] && typeof raw.entries[kind] === "object" ? raw.entries[kind] : {};
164
+ for (const [key, readings] of Object.entries(section)) {
165
+ const configuration = key.split("|")[0];
166
+ if (!allowed.has(configuration) || !Array.isArray(readings)) {
167
+ dropped += 1;
168
+ continue;
169
+ }
170
+ this.#entries[kind][key] = readings.slice(-READINGS_KEPT);
171
+ kept += 1;
172
+ }
173
+ }
174
+ this.#logger.info(
175
+ `local observations: ${kept} key(s) kept from ${this.#filePath}` +
176
+ (dropped > 0 ? `, ${dropped} dropped because they were seen on a configuration this host no longer has` : "")
177
+ );
178
+ if (dropped > 0) {
179
+ this.#save();
180
+ }
181
+ }
182
+
183
+ /**
184
+ * An encode of this output has ended: keep how fast it ran alone and what
185
+ * its segments carried.
186
+ *
187
+ * @param {object} params
188
+ * @param {import("./output/OutputSpec.js").OutputSpec} params.spec
189
+ * @param {ContentDescriptor | null} params.content - What it was encoded from.
190
+ * @param {number | null} params.aloneSpeedX - Seconds of film per second,
191
+ * measured with nothing else encoding.
192
+ * @param {{ averageKbps: number, peakKbps: number } | null} params.segmentKbps
193
+ * @returns {void}
194
+ */
195
+ noteEncode({ spec, content, aloneSpeedX, segmentKbps }) {
196
+ const modeKey = this.modeKeyOf(spec);
197
+ if (modeKey === null) {
198
+ return;
199
+ }
200
+ let changed = false;
201
+ if (Number.isFinite(aloneSpeedX) && aloneSpeedX > 0 && LocalObservations.#usableContent(content)) {
202
+ this.#push("speed", modeKey, { content, speedX: aloneSpeedX, at: this.#now() });
203
+ changed = true;
204
+ }
205
+ const rateKey = this.rateKeyOf(spec);
206
+ if (
207
+ segmentKbps &&
208
+ Number.isFinite(segmentKbps.averageKbps) && segmentKbps.averageKbps > 0 &&
209
+ Number.isFinite(segmentKbps.peakKbps) && segmentKbps.peakKbps > 0
210
+ ) {
211
+ this.#push("rates", rateKey, {
212
+ averageKbps: segmentKbps.averageKbps,
213
+ peakKbps: segmentKbps.peakKbps,
214
+ at: this.#now()
215
+ });
216
+ changed = true;
217
+ }
218
+ if (changed) {
219
+ this.#save();
220
+ }
221
+ }
222
+
223
+ /**
224
+ * A viewer has been moved onto an output that was being prepared for them:
225
+ * keep how long it took and what they held when they moved.
226
+ *
227
+ * @param {object} params
228
+ * @param {import("./output/OutputSpec.js").OutputSpec} params.spec - The output moved onto.
229
+ * @param {number} params.seconds - From the preparation starting to the move.
230
+ * @param {number | null} params.bufferedSec - What the viewer held at the move.
231
+ * @returns {void}
232
+ */
233
+ notePreparation({ spec, seconds, bufferedSec }) {
234
+ const modeKey = this.modeKeyOf(spec);
235
+ if (modeKey === null || !(Number.isFinite(seconds) && seconds >= 0)) {
236
+ return;
237
+ }
238
+ this.#push("preparation", modeKey, {
239
+ seconds,
240
+ bufferedSec: Number.isFinite(bufferedSec) ? bufferedSec : null,
241
+ at: this.#now()
242
+ });
243
+ this.#save();
244
+ }
245
+
246
+ /**
247
+ * The slowest this mode has been seen running alone on comparable material,
248
+ * or null where nothing comparable has been seen.
249
+ *
250
+ * @param {import("./output/OutputSpec.js").OutputSpec} spec
251
+ * @param {ContentDescriptor | null} content
252
+ * @returns {number | null}
253
+ */
254
+ slowestAloneSpeed(spec, content) {
255
+ const modeKey = this.modeKeyOf(spec);
256
+ if (modeKey === null || !LocalObservations.#usableContent(content)) {
257
+ return null;
258
+ }
259
+ let slowest = null;
260
+ for (const reading of this.#entries.speed[modeKey] ?? []) {
261
+ const seen = reading.content;
262
+ if (
263
+ seen?.codec === content.codec &&
264
+ (seen?.bitDepth ?? null) === (content.bitDepth ?? null) &&
265
+ seen?.width === content.width &&
266
+ seen?.height === content.height &&
267
+ seen?.megabitsPerSecond >= content.megabitsPerSecond
268
+ ) {
269
+ slowest = slowest === null ? reading.speedX : Math.min(slowest, reading.speedX);
270
+ }
271
+ }
272
+ return slowest;
273
+ }
274
+
275
+ /**
276
+ * The largest peak an output of this mode and rate control has been seen
277
+ * carrying, in megabits per second, or null.
278
+ *
279
+ * @param {import("./output/OutputSpec.js").OutputSpec} spec
280
+ * @returns {number | null}
281
+ */
282
+ peakMbps(spec) {
283
+ const rateKey = this.rateKeyOf(spec);
284
+ const readings = rateKey === null ? [] : (this.#entries.rates[rateKey] ?? []);
285
+ return readings.length === 0 ? null : Math.max(...readings.map((reading) => reading.peakKbps)) / 1000;
286
+ }
287
+
288
+ /**
289
+ * The longest an output of this mode has been seen taking to be ready for a
290
+ * viewer moving onto it, in seconds, or null.
291
+ *
292
+ * @param {import("./output/OutputSpec.js").OutputSpec} spec
293
+ * @returns {number | null}
294
+ */
295
+ longestPreparationSec(spec) {
296
+ const modeKey = this.modeKeyOf(spec);
297
+ const readings = modeKey === null ? [] : (this.#entries.preparation[modeKey] ?? []);
298
+ return readings.length === 0 ? null : Math.max(...readings.map((reading) => reading.seconds));
299
+ }
300
+
301
+ /**
302
+ * @param {"speed" | "rates" | "preparation"} kind
303
+ * @param {string} key
304
+ * @param {object} reading
305
+ * @returns {void}
306
+ */
307
+ #push(kind, key, reading) {
308
+ const readings = [...(this.#entries[kind][key] ?? []), reading].slice(-READINGS_KEPT);
309
+ this.#entries[kind][key] = readings;
310
+ }
311
+
312
+ /** @returns {void} */
313
+ #save() {
314
+ if (!this.#filePath) {
315
+ return;
316
+ }
317
+ try {
318
+ writeFileSync(this.#filePath, JSON.stringify({ version: FILE_VERSION, entries: this.#entries }));
319
+ } catch (error) {
320
+ // A read-only install keeps what it learned for this process only.
321
+ this.#logger.warn(
322
+ `local observations: could not write ${this.#filePath} (${error instanceof Error ? error.message : String(error)})`
323
+ );
324
+ }
325
+ }
326
+
327
+ /**
328
+ * @param {ContentDescriptor | null} content
329
+ * @returns {boolean}
330
+ */
331
+ static #usableContent(content) {
332
+ return Boolean(content) &&
333
+ typeof content.codec === "string" && content.codec.length > 0 &&
334
+ content.width > 0 && content.height > 0 &&
335
+ Number.isFinite(content.megabitsPerSecond) && content.megabitsPerSecond > 0;
336
+ }
337
+ }
338
+
339
+ /**
340
+ * What an observation of an encode is filed as having been made FROM.
341
+ *
342
+ * @param {{ width: number | null, height: number | null, decode: { codec: string, bitDepth: number | null, megabitsPerSecond: number } | null } | null} file
343
+ * @returns {ContentDescriptor | null}
344
+ */
345
+ export function contentOf(file) {
346
+ const decode = file?.decode ?? null;
347
+ const width = Number(file?.width) || 0;
348
+ const height = Number(file?.height) || 0;
349
+ if (!decode || width <= 0 || height <= 0) {
350
+ return null;
351
+ }
352
+ return {
353
+ codec: decode.codec,
354
+ bitDepth: decode.bitDepth ?? null,
355
+ width,
356
+ height,
357
+ megabitsPerSecond: decode.megabitsPerSecond
358
+ };
359
+ }