@torrent-tv/proxy 2.89.16 → 2.89.18

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 (299) hide show
  1. package/README.md +14 -3
  2. package/package.json +10 -2
  3. package/routes/api/client-logs/post.js +9 -2
  4. package/server.js +0 -11
  5. package/.claude/commands/opsx/apply.md +0 -155
  6. package/.claude/commands/opsx/archive.md +0 -160
  7. package/.claude/commands/opsx/explore.md +0 -174
  8. package/.claude/commands/opsx/propose.md +0 -109
  9. package/.claude/commands/opsx/sync.md +0 -143
  10. package/.claude/skills/openspec-apply-change/SKILL.md +0 -159
  11. package/.claude/skills/openspec-archive-change/SKILL.md +0 -117
  12. package/.claude/skills/openspec-explore/SKILL.md +0 -289
  13. package/.claude/skills/openspec-propose/SKILL.md +0 -113
  14. package/.claude/skills/openspec-sync-specs/SKILL.md +0 -147
  15. package/.dockerignore +0 -5
  16. package/AGENTS.md +0 -363
  17. package/CHANGELOG.md +0 -2373
  18. package/Dockerfile +0 -30
  19. package/biome.json +0 -412
  20. package/corpus.tmp.json +0 -1
  21. package/docs/browser-proxy-contract.md +0 -155
  22. package/docs/container-architecture.md +0 -273
  23. package/docs/download-architecture.md +0 -261
  24. package/docs/encode-architecture.md +0 -765
  25. package/docs/encode-run-state.md +0 -96
  26. package/docs/logs.md +0 -138
  27. package/docs/playback-start-forecast.md +0 -113
  28. package/docs/socks5-egress-research.md +0 -114
  29. package/docs/storage-architecture.md +0 -273
  30. package/knip.json +0 -14
  31. package/main2.cpuprofile +0 -1
  32. package/openspec/changes/adaptive-bitrate/.openspec.yaml +0 -0
  33. package/openspec/changes/adaptive-bitrate/design.md +0 -92
  34. package/openspec/changes/adaptive-bitrate/proposal.md +0 -56
  35. package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +0 -51
  36. package/openspec/changes/adaptive-bitrate/tasks.md +0 -40
  37. package/openspec/changes/binary-distribution/.openspec.yaml +0 -2
  38. package/openspec/changes/binary-distribution/proposal.md +0 -53
  39. package/openspec/changes/chunked-request-bodies/design.md +0 -81
  40. package/openspec/changes/chunked-request-bodies/proposal.md +0 -53
  41. package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +0 -30
  42. package/openspec/changes/chunked-request-bodies/tasks.md +0 -40
  43. package/openspec/changes/cold-start/design.md +0 -98
  44. package/openspec/changes/cold-start/proposal.md +0 -62
  45. package/openspec/changes/cold-start/specs/cold-start/spec.md +0 -47
  46. package/openspec/changes/cold-start/tasks.md +0 -48
  47. package/openspec/changes/disk-cap/.openspec.yaml +0 -2
  48. package/openspec/changes/disk-cap/proposal.md +0 -37
  49. package/openspec/changes/disk-cap/specs/disk-cap/spec.md +0 -25
  50. package/openspec/changes/disk-cap/tasks.md +0 -19
  51. package/openspec/changes/proxy-observability/.openspec.yaml +0 -2
  52. package/openspec/changes/proxy-observability/design.md +0 -38
  53. package/openspec/changes/proxy-observability/proposal.md +0 -49
  54. package/openspec/changes/proxy-observability/specs/observability/spec.md +0 -33
  55. package/openspec/changes/proxy-observability/tasks.md +0 -21
  56. package/openspec/changes/seek-debounce/.openspec.yaml +0 -0
  57. package/openspec/changes/seek-debounce/design.md +0 -118
  58. package/openspec/changes/seek-debounce/proposal.md +0 -59
  59. package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +0 -41
  60. package/openspec/changes/seek-debounce/tasks.md +0 -33
  61. package/openspec/changes/subtitle-language/.openspec.yaml +0 -2
  62. package/openspec/changes/subtitle-language/proposal.md +0 -50
  63. package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +0 -34
  64. package/openspec/changes/subtitle-language/tasks.md +0 -26
  65. package/openspec/changes/track-selection/.openspec.yaml +0 -2
  66. package/openspec/changes/track-selection/design.md +0 -47
  67. package/openspec/changes/track-selection/proposal.md +0 -45
  68. package/openspec/changes/track-selection/specs/track-selection/spec.md +0 -41
  69. package/openspec/changes/track-selection/tasks.md +0 -20
  70. package/openspec/changes/transcode-quality/.openspec.yaml +0 -2
  71. package/openspec/changes/transcode-quality/proposal.md +0 -51
  72. package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +0 -139
  73. package/openspec/changes/transcode-quality/tasks.md +0 -63
  74. package/openspec/config.yaml +0 -38
  75. package/public/.well-known/appspecific/com.chrome.devtools.json +0 -1
  76. package/research/delivery-wedge-stand-ladder-2026-09-13.md +0 -329
  77. package/research/double-spawn-2026-09-10.md +0 -171
  78. package/research/handover-2026-09-13.md +0 -209
  79. package/research/handover-reader-claims-removal-2026-09-12.md +0 -166
  80. package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +0 -175
  81. package/research/preset-benchmark-size-scaling-2026-08-22.md +0 -55
  82. package/research/priority-map-is-the-truth-2026-09-12.md +0 -225
  83. package/research/valgrind-replica-2026-08-27/logs.tar.gz +0 -0
  84. package/scripts/build-audio-samples.mjs +0 -31
  85. package/scripts/render-run-graph.js +0 -173
  86. package/test/asset-paths.test.js +0 -28
  87. package/test/audio-buffer-continuity.test.js +0 -77
  88. package/test/audio-calibration.test.js +0 -23
  89. package/test/audio-inventory.test.js +0 -212
  90. package/test/audio-track-bitrate.test.js +0 -40
  91. package/test/audio-warm-recorded.test.js +0 -63
  92. package/test/auto-quality-step.test.js +0 -762
  93. package/test/available-share.test.js +0 -76
  94. package/test/background-fill.test.js +0 -190
  95. package/test/client-log-file.test.js +0 -126
  96. package/test/coalesce.test.js +0 -39
  97. package/test/completed-files.test.js +0 -115
  98. package/test/concurrent-cost.test.js +0 -148
  99. package/test/container-media-info.test.js +0 -228
  100. package/test/contention.test.js +0 -76
  101. package/test/core-dumps.test.js +0 -57
  102. package/test/coverage-follows-the-disk.test.js +0 -233
  103. package/test/coverage-map.test.js +0 -195
  104. package/test/cue-push-carries-what-the-walk-found.test.js +0 -58
  105. package/test/cut-times-timeline.test.js +0 -65
  106. package/test/cuts-follow-published-grid.test.js +0 -109
  107. package/test/data-channel-frame.test.js +0 -63
  108. package/test/data-channel-viewer-lifecycle.test.js +0 -193
  109. package/test/dead-torrent-handle.test.js +0 -52
  110. package/test/declared-rates.test.js +0 -116
  111. package/test/declared-tracks.test.js +0 -43
  112. package/test/decode-cost-fit.test.js +0 -202
  113. package/test/decode-cost.test.js +0 -475
  114. package/test/delivery-probe-field.test.js +0 -173
  115. package/test/delivery-probe.test.js +0 -382
  116. package/test/demand-register.test.js +0 -206
  117. package/test/dht-bootstrap.test.js +0 -72
  118. package/test/drain-threat.test.js +0 -65
  119. package/test/duplicate-add.test.js +0 -48
  120. package/test/encode-admission.test.js +0 -170
  121. package/test/encode-cost.test.js +0 -275
  122. package/test/encode-exit.test.js +0 -111
  123. package/test/encode-orchestrator.test.js +0 -426
  124. package/test/encode-plan-viewers.test.js +0 -725
  125. package/test/encode-plan.test.js +0 -545
  126. package/test/encode-run-state.test.js +0 -314
  127. package/test/encode-run.test.js +0 -248
  128. package/test/encode-slope.test.js +0 -46
  129. package/test/encoded-output.test.js +0 -39
  130. package/test/encoder-kinds.test.js +0 -122
  131. package/test/encoder-readings.test.js +0 -68
  132. package/test/episode-naming.test.js +0 -131
  133. package/test/every-claimant-is-told-its-share.test.js +0 -152
  134. package/test/failed-encoder-still-serves-what-exists.test.js +0 -354
  135. package/test/failed-start-breaker.test.js +0 -202
  136. package/test/fastest-wires.test.js +0 -143
  137. package/test/ffmpeg-stream-counts.test.js +0 -101
  138. package/test/file-edges.test.js +0 -191
  139. package/test/files-in-use.test.js +0 -34
  140. package/test/fixtures/audio-frame-boundary.json +0 -1
  141. package/test/fixtures/refused-audio-20261002.json +0 -10
  142. package/test/fmp4-presentation-clock.test.js +0 -148
  143. package/test/fmp4-self-contained.test.js +0 -80
  144. package/test/fmp4-timestamps.test.js +0 -140
  145. package/test/fmp4-track-completeness.test.js +0 -41
  146. package/test/format-keyed-outputs.test.js +0 -78
  147. package/test/health-metrics.test.js +0 -36
  148. package/test/held-read-ownership.test.js +0 -77
  149. package/test/held-request-width.test.js +0 -157
  150. package/test/helpers/encode-run.js +0 -248
  151. package/test/helpers/held-file.js +0 -77
  152. package/test/helpers/manager.js +0 -62
  153. package/test/helpers/matroska-file.js +0 -296
  154. package/test/helpers/output-spec.js +0 -63
  155. package/test/host-load.test.js +0 -68
  156. package/test/index-accuracy-is-about-the-file.test.js +0 -128
  157. package/test/init-compat.test.js +0 -219
  158. package/test/input-lost-quiets-the-plan.test.js +0 -261
  159. package/test/input-unavailable.test.js +0 -48
  160. package/test/installation-id.test.js +0 -39
  161. package/test/keeping-period.test.js +0 -83
  162. package/test/keyframe-index-accuracy.test.js +0 -188
  163. package/test/keyframe-table-budget.test.js +0 -123
  164. package/test/keyframes-belong-to-the-file.test.js +0 -249
  165. package/test/learned-median.test.js +0 -37
  166. package/test/letting-go-asks-who-is-watching.test.js +0 -191
  167. package/test/limit-row-by-area.test.js +0 -60
  168. package/test/link-budget.test.js +0 -111
  169. package/test/link-probe.test.js +0 -56
  170. package/test/local-encoding-data.test.js +0 -273
  171. package/test/logger-repeats.test.js +0 -134
  172. package/test/look-ahead-coverage.test.js +0 -56
  173. package/test/manager-surface.test.js +0 -291
  174. package/test/matroska-blocks.test.js +0 -0
  175. package/test/matroska-cluster-discovery.test.js +0 -323
  176. package/test/matroska-cues-track.test.js +0 -192
  177. package/test/media-time.test.js +0 -62
  178. package/test/memory-budget.test.js +0 -390
  179. package/test/move-cost.test.js +0 -216
  180. package/test/mp4-composition-times.test.js +0 -0
  181. package/test/mp4-subtitles.test.js +0 -173
  182. package/test/one-authority.test.js +0 -285
  183. package/test/one-budget.test.js +0 -149
  184. package/test/one-output-one-session.test.js +0 -273
  185. package/test/one-resource-divided.test.js +0 -185
  186. package/test/one-serving-output-per-height.test.js +0 -458
  187. package/test/orchestrator-wired.test.js +0 -207
  188. package/test/output-catalog.test.js +0 -39
  189. package/test/output-shape.test.js +0 -21
  190. package/test/output-spec.test.js +0 -251
  191. package/test/output-speed.test.js +0 -92
  192. package/test/outputs-being-prepared.test.js +0 -40
  193. package/test/packet-witness-ring.test.js +0 -236
  194. package/test/packet-witness.test.js +0 -148
  195. package/test/piece-completeness.test.js +0 -78
  196. package/test/piece-disk-store.test.js +0 -401
  197. package/test/piece-from-whole-file.test.js +0 -129
  198. package/test/piece-lru.test.js +0 -319
  199. package/test/piece-reader.test.js +0 -162
  200. package/test/piece-selection-offset.test.js +0 -85
  201. package/test/piece-store-eviction.test.js +0 -595
  202. package/test/piece-store-never-refuses.test.js +0 -153
  203. package/test/piece-store-reservations.test.js +0 -403
  204. package/test/piece-store-slow-disk.test.js +0 -159
  205. package/test/piece-tail.test.js +0 -97
  206. package/test/plan-host-timings.test.js +0 -68
  207. package/test/plan-is-steady.test.js +0 -225
  208. package/test/playback-readiness.test.js +0 -518
  209. package/test/playback-start-regressions.test.js +0 -101
  210. package/test/priority-map-download.test.js +0 -174
  211. package/test/priority-map-emptied.test.js +0 -204
  212. package/test/priority-map-per-output.test.js +0 -215
  213. package/test/priority-map.test.js +0 -163
  214. package/test/probe-wedge-certainty.test.js +0 -60
  215. package/test/produced-copy-choice.test.js +0 -213
  216. package/test/published-timeline.test.js +0 -83
  217. package/test/quality-offer.test.js +0 -160
  218. package/test/quality-variants.test.js +0 -1274
  219. package/test/rate-control-in-the-output.test.js +0 -181
  220. package/test/read-survives-withdrawal.test.js +0 -164
  221. package/test/read-window.test.js +0 -365
  222. package/test/reads-not-yet-arrived.test.js +0 -218
  223. package/test/resume-warm.test.js +0 -39
  224. package/test/run-graph-drift.test.js +0 -37
  225. package/test/run-inputs.test.js +0 -80
  226. package/test/run-intervals.test.js +0 -100
  227. package/test/run-position-follows-published-grid.test.js +0 -89
  228. package/test/run-progress.test.js +0 -64
  229. package/test/same-height-switch.test.js +0 -663
  230. package/test/seek-frees-the-viewers-requests.test.js +0 -217
  231. package/test/seek-landing.test.js +0 -109
  232. package/test/seek-target-not-superseded.test.js +0 -195
  233. package/test/segment-closed-on-both-branches.test.js +0 -114
  234. package/test/segment-cut-times.test.js +0 -63
  235. package/test/segment-demand.test.js +0 -80
  236. package/test/segment-hold-supersede.test.js +0 -91
  237. package/test/segment-judged-by-its-own-header.test.js +0 -222
  238. package/test/segment-serve-wiring.test.js +0 -481
  239. package/test/segment-store-eviction.test.js +0 -247
  240. package/test/segment-store.test.js +0 -352
  241. package/test/segment-true-start.test.js +0 -86
  242. package/test/segments-are-shared.test.js +0 -176
  243. package/test/send-chunk-bytes.test.js +0 -86
  244. package/test/serving-existing-output.test.js +0 -111
  245. package/test/serving-output.test.js +0 -102
  246. package/test/shared-piece-store.test.js +0 -310
  247. package/test/sidecar-naming.test.js +0 -142
  248. package/test/soundtrack-rate-bound.test.js +0 -289
  249. package/test/source-file.test.js +0 -133
  250. package/test/source-files-route.test.js +0 -113
  251. package/test/source-registry.test.js +0 -18
  252. package/test/split-a-stretch.test.js +0 -139
  253. package/test/stale-request-after-seek.test.js +0 -158
  254. package/test/startup-readings.test.js +0 -124
  255. package/test/stream-route.test.js +0 -237
  256. package/test/subtitle-cue-framing.test.js +0 -200
  257. package/test/subtitle-cue-source.test.js +0 -102
  258. package/test/subtitle-cue-walk.test.js +0 -385
  259. package/test/subtitle-cursor.test.js +0 -63
  260. package/test/subtitle-defaults.test.js +0 -118
  261. package/test/subtitle-language.test.js +0 -252
  262. package/test/subtitle-metadata.test.js +0 -15
  263. package/test/subtitle-plan-waits.test.js +0 -125
  264. package/test/subtitle-track-numbering.test.js +0 -391
  265. package/test/supply-margin.test.js +0 -190
  266. package/test/swarm-follows-readers.test.js +0 -174
  267. package/test/swarm-reach.test.js +0 -130
  268. package/test/swarm-selection.test.js +0 -220
  269. package/test/tail-duplication.test.js +0 -167
  270. package/test/the-channel-has-both-ends.test.js +0 -91
  271. package/test/the-torrent-thread-serves-bytes.test.js +0 -107
  272. package/test/thread-pool.test.js +0 -53
  273. package/test/timeline.test.js +0 -84
  274. package/test/torrent-contents.test.js +0 -235
  275. package/test/torrent-cost.test.js +0 -71
  276. package/test/torrent-files.test.js +0 -221
  277. package/test/torrent-naming.test.js +0 -255
  278. package/test/tracks-begin-together.test.js +0 -209
  279. package/test/tunnel-renewal.test.js +0 -167
  280. package/test/two-viewers-one-picture.test.js +0 -508
  281. package/test/upload-hurry.test.js +0 -186
  282. package/test/usrsctp-state.test.js +0 -131
  283. package/test/video-facts.test.js +0 -102
  284. package/test/viewer-assignments.test.js +0 -143
  285. package/test/viewer-outputs.test.js +0 -285
  286. package/test/viewer-presence.test.js +0 -116
  287. package/test/viewer-requests-admission.test.js +0 -92
  288. package/test/viewer-starts-on-map.test.js +0 -27
  289. package/test/viewer-states.test.js +0 -192
  290. package/test/viewer-subtitle-subscription.test.js +0 -96
  291. package/test/viewer.test.js +0 -139
  292. package/test/viewing-generations.test.js +0 -390
  293. package/test/visible-rung.test.js +0 -37
  294. package/test/wait-ledger.test.js +0 -100
  295. package/test/webrtc-shim.test.js +0 -149
  296. package/test/wedge-certainty.test.js +0 -131
  297. package/test/whole-file-held-read.test.js +0 -30
  298. package/test/withdraw-piece-claim.test.js +0 -212
  299. package/test/worker-channel.test.js +0 -158
@@ -1,273 +0,0 @@
1
- /**
2
- * @file Step 14 of roadmap item 97: which modes this machine may encode with,
3
- * what each costs at every size, and what the proxy keeps between runs about
4
- * its own admitted encodes — all on fakes. Nothing here runs ffmpeg, starts a
5
- * torrent client or touches a swarm; the one file written is in a temporary
6
- * directory.
7
- */
8
-
9
- import test from "node:test";
10
- import assert from "node:assert/strict";
11
- import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
12
- import os from "node:os";
13
- import path from "node:path";
14
- import {
15
- configurationKeyOf,
16
- kindOfEncoderName,
17
- parseFfmpegVersion,
18
- parseNvidiaSmi,
19
- parseX264Version,
20
- UNKNOWN
21
- } from "../services/encode/fingerprint.js";
22
- import { interpolationErrorOf, throughputAt } from "../services/encode/throughput.js";
23
- import { calibrateEncoder, HostCalibration } from "../services/encode/calibration.js";
24
- import { canSustainOutput, pickSoftwarePreset } from "../services/encode/hwaccel.js";
25
- import { contentOf, LocalObservations } from "../services/encode/LocalObservations.js";
26
- import { OutputSpec, VideoOutput, CutGrid } from "../services/encode/output/index.js";
27
- import { PEAK_CLASS, videoLoadOfSpec } from "../services/encode/quality/link-budget.js";
28
- import { SoftwareEncoder } from "../services/encode/SoftwareEncoder.js";
29
- import { NvencEncoder } from "../services/encode/NvencEncoder.js";
30
-
31
- const FINGERPRINT = Object.freeze({
32
- ffmpeg: "8.1.2",
33
- x264: "core 164 r3108 31e19f9",
34
- cpu: "Cortex-A72",
35
- threads: 4,
36
- devices: { vaapi: { model: "0x8086:0x9a49", driver: "i915 (kernel 6.6.31)" } }
37
- });
38
-
39
- /** A re-encoded picture's spec, the way `OutputOpening` builds one. */
40
- function specAt({ encoder = "libx264", width = 1280, height = 720, fps = 24, preset = "veryfast", rateControl = null } = {}) {
41
- return new OutputSpec({
42
- sourceKey: "src",
43
- segmentFormatId: "fmp4",
44
- grid: new CutGrid({ kind: "uniform", fileIndex: 0 }),
45
- video: new VideoOutput({ fileIndex: 0, encode: { encoder, width, height, fps, preset, tonemap: false, rateControl } }),
46
- audio: null
47
- });
48
- }
49
-
50
- // --- The configuration key -------------------------------------------------
51
-
52
- test("the builds and devices are read out of what the tools print", () => {
53
- assert.equal(parseFfmpegVersion("ffmpeg version 8.1.2 Copyright (c) 2000-2025\nbuilt with gcc"), "8.1.2");
54
- assert.equal(parseFfmpegVersion("garbage"), UNKNOWN);
55
- const stream = Buffer.from("\0\0\x01\x06\x05x264 - core 164 r3108 31e19f9 - H.264/MPEG-4 AVC codec", "latin1");
56
- assert.equal(parseX264Version(stream), "core 164 r3108 31e19f9");
57
- assert.equal(parseX264Version(Buffer.from("no version here")), UNKNOWN);
58
- assert.deepEqual(parseNvidiaSmi("NVIDIA RTX 4000 SFF Ada Generation, 550.54.14\n"), {
59
- model: "NVIDIA RTX 4000 SFF Ada Generation",
60
- driver: "550.54.14"
61
- });
62
- assert.equal(kindOfEncoderName("h264_nvenc"), "nvenc");
63
- assert.equal(kindOfEncoderName("libx264"), "software");
64
- });
65
-
66
- test("a driver update invalidates what a device was seen doing, and nothing about software", () => {
67
- const vaapi = { name: "h264_vaapi", kind: "vaapi" };
68
- const software = { name: "libx264", kind: "software" };
69
- const updated = { ...FINGERPRINT, devices: { vaapi: { model: "0x8086:0x9a49", driver: "i915 (kernel 6.8.0)" } } };
70
- assert.notEqual(configurationKeyOf(FINGERPRINT, vaapi), configurationKeyOf(updated, vaapi));
71
- assert.equal(configurationKeyOf(FINGERPRINT, software), configurationKeyOf(updated, software));
72
- // Another x264 build or another thread count is another software configuration.
73
- assert.notEqual(configurationKeyOf(FINGERPRINT, software), configurationKeyOf({ ...FINGERPRINT, threads: 14 }, software));
74
- assert.notEqual(configurationKeyOf(FINGERPRINT, software), configurationKeyOf({ ...FINGERPRINT, x264: "core 165" }, software));
75
- });
76
-
77
- // --- Throughput by size ----------------------------------------------------
78
-
79
- const READINGS = [
80
- { width: 256, height: 144, pixelsPerSec: 40e6 },
81
- { width: 640, height: 360, pixelsPerSec: 36e6 },
82
- { width: 1280, height: 720, pixelsPerSec: 30e6 }
83
- ];
84
-
85
- test("a size is priced from the readings around it, lowered by the error measured for interpolating", () => {
86
- const error = interpolationErrorOf(READINGS);
87
- assert.ok(error !== null && error >= 0, "three readings give a measured bound");
88
- const entry = { preset: "veryfast", pixelsPerSec: 36e6, bySize: READINGS, interpolationError: error };
89
- assert.equal(throughputAt(entry, { width: 640, height: 360 }), 36e6, "a size read is its own reading");
90
- const between = throughputAt(entry, { width: 854, height: 480 });
91
- assert.ok(between < 36e6 && between > 30e6 * (1 - error), `between the two readings, got ${between}`);
92
- });
93
-
94
- test("a size beyond what was read is not a size this machine has shown it can do", () => {
95
- const entry = { preset: "veryfast", pixelsPerSec: 36e6, bySize: READINGS, interpolationError: 0.05 };
96
- assert.equal(throughputAt(entry, { width: 1920, height: 1080 }), null);
97
- assert.equal(throughputAt(entry, { width: 128, height: 72 }), null);
98
- // Two readings measure no interpolation error: only the sizes read are known.
99
- const two = { preset: "veryfast", pixelsPerSec: 36e6, bySize: READINGS.slice(0, 2), interpolationError: null };
100
- assert.equal(throughputAt(two, { width: 426, height: 240 }), null);
101
- assert.equal(throughputAt(two, { width: 640, height: 360 }), 36e6);
102
- // An entry with no readings by size answers with its one figure, as before.
103
- assert.equal(throughputAt({ preset: "fast", pixelsPerSec: 5e6 }, { width: 1920, height: 1080 }), 5e6);
104
- });
105
-
106
- test("the offer does not keep a rung at a size no mode was measured at", () => {
107
- const benchmark = [{ preset: "ultrafast", pixelsPerSec: 36e6, bySize: READINGS, interpolationError: 0.05 }];
108
- const at1080 = canSustainOutput({ benchmark, outputPixelsPerSec: 1920 * 1080 * 24, frame: { width: 1920, height: 1080 } });
109
- assert.deepEqual(at1080, { speed: null, sustainable: false });
110
- // The same mode at a size it WAS read at, with decoding priced: 30 Mpx/s
111
- // against 1280x720x24 is 1.36x, and a decode at 10x leaves 1.19x.
112
- const at720 = canSustainOutput({
113
- benchmark,
114
- outputPixelsPerSec: 1280 * 720 * 24,
115
- frame: { width: 1280, height: 720 },
116
- observedDecodeCostSec: 0.1
117
- });
118
- assert.equal(at720.sustainable, true);
119
- assert.ok(Math.abs(at720.speed - 1 / (0.1 + (1280 * 720 * 24) / 30e6)) < 1e-9);
120
- });
121
-
122
- test("a preset not measured at a size is not chosen there", () => {
123
- const benchmark = [
124
- { preset: "fast", pixelsPerSec: 20e6, bySize: READINGS.slice(0, 2), interpolationError: null },
125
- { preset: "ultrafast", pixelsPerSec: 60e6, bySize: READINGS, interpolationError: 0.05 }
126
- ];
127
- assert.equal(pickSoftwarePreset(benchmark, 1280 * 720 * 24, {}, { width: 1280, height: 720 }), "ultrafast");
128
- });
129
-
130
- // --- The calibration walk --------------------------------------------------
131
-
132
- /**
133
- * A calibration with every reading replaced: `speeds` maps "rung@WxH" to how
134
- * many times realtime that mode encodes there, `failing` the rungs whose
135
- * segments do not decode.
136
- */
137
- async function calibrateWith({ encoder, speeds, failing = [] }) {
138
- const asked = [];
139
- const result = await calibrateEncoder({
140
- ffmpegBin: "never-run",
141
- encoder,
142
- measure: {
143
- check: async ({ rung }) => (failing.includes(rung)
144
- ? { ok: false, reason: "a segment does not decode", segmentKbps: null }
145
- : { ok: true, reason: "", segmentKbps: { averageKbps: 900, peakKbps: 1400 } }),
146
- rawFrames: async (frame) => `raw-${frame.width}x${frame.height}`,
147
- speed: async (rung, rawPath, frame) => {
148
- asked.push(`${rung}@${frame.width}x${frame.height}`);
149
- return speeds[`${rung}@${frame.width}x${frame.height}`] ?? null;
150
- },
151
- release: () => {}
152
- }
153
- });
154
- return { ...result, asked };
155
- }
156
-
157
- test("a mode whose segments do not decode is not used at all", async () => {
158
- const encoder = new SoftwareEncoder();
159
- const speeds = {};
160
- for (const rung of encoder.selectableRungs) {
161
- for (const size of ["256x144", "640x360", "1280x720", "1920x1080", "3840x2160"]) {
162
- speeds[`${rung}@${size}`] = 2;
163
- }
164
- }
165
- const { modes, refused } = await calibrateWith({ encoder, speeds, failing: ["faster"] });
166
- assert.ok(!modes.some((mode) => mode.preset === "faster"));
167
- assert.ok(refused.some((one) => one.preset === "faster" && /decode/.test(one.reason)));
168
- assert.equal(modes.length, encoder.selectableRungs.length - 1);
169
- });
170
-
171
- test("slower modes are not read where a faster one fell behind, nor larger sizes where the fastest did", async () => {
172
- const encoder = new SoftwareEncoder();
173
- const speeds = {
174
- "ultrafast@256x144": 20, "superfast@256x144": 15, "veryfast@256x144": 10, "faster@256x144": 8, "fast@256x144": 6,
175
- "ultrafast@640x360": 6, "superfast@640x360": 4, "veryfast@640x360": 0.8,
176
- "ultrafast@1280x720": 0.9
177
- };
178
- const { modes, asked } = await calibrateWith({ encoder, speeds });
179
- assert.ok(!asked.includes("faster@640x360"), "a mode slower than one that fell behind is not read");
180
- assert.ok(!asked.some((one) => one.endsWith("@1920x1080")), "a size larger than one the fastest could not hold is not read");
181
- const ultrafast = modes.find((mode) => mode.preset === "ultrafast");
182
- assert.deepEqual(ultrafast.bySize.map((reading) => `${reading.width}x${reading.height}`), ["256x144", "640x360", "1280x720"]);
183
- assert.equal(ultrafast.pixelsPerSec, 640 * 360 * 24 * 6, "the reference figure is the 640x360 reading");
184
- const fast = modes.find((mode) => mode.preset === "fast");
185
- assert.equal(throughputAt(fast, { width: 640, height: 360 }), null, "fast was never read at 640x360");
186
- });
187
-
188
- test("a device is calibrated only at the one setting its arguments pass", () => {
189
- assert.deepEqual(new NvencEncoder().selectableRungs, ["p4"]);
190
- assert.ok(new NvencEncoder().buildVideoArgs({ targetWidth: 1280, targetHeight: 720, segmentDurationSec: 4 }).includes("p4"));
191
- });
192
-
193
- test("the modes of the encoder in use are what prices, and software is kept for the fallback", () => {
194
- const calibration = new HostCalibration({ byKind: { vaapi: [{ preset: "h264_vaapi", pixelsPerSec: 1 }], software: [] }, fingerprint: FINGERPRINT });
195
- assert.equal(calibration.modesFor("vaapi").length, 1);
196
- assert.deepEqual(calibration.modesFor("software"), [], "calibrated, nothing qualified");
197
- assert.equal(calibration.modesFor("nvenc"), null, "not calibrated at all");
198
- });
199
-
200
- // --- Local observations ----------------------------------------------------
201
-
202
- test("what is kept applies only on the configuration it was seen on, and survives a restart on it", () => {
203
- const directory = mkdtempSync(path.join(os.tmpdir(), "ttv-observations-"));
204
- const filePath = path.join(directory, "local-observations.json");
205
- try {
206
- const first = new LocalObservations({ fingerprint: FINGERPRINT, filePath });
207
- const content = { codec: "hevc", bitDepth: 10, width: 1920, height: 1080, megabitsPerSecond: 8 };
208
- first.noteEncode({ spec: specAt(), content, aloneSpeedX: 1.7, segmentKbps: { averageKbps: 2100, peakKbps: 3900 } });
209
- first.notePreparation({ spec: specAt(), seconds: 6.5, bufferedSec: 41 });
210
-
211
- const software = configurationKeyOf(FINGERPRINT, { name: "libx264", kind: "software" });
212
- const again = new LocalObservations({ fingerprint: FINGERPRINT, filePath });
213
- again.load([software]);
214
- assert.equal(again.slowestAloneSpeed(specAt(), content), 1.7);
215
- assert.equal(again.peakMbps(specAt()), 3.9);
216
- assert.equal(again.longestPreparationSec(specAt()), 6.5);
217
-
218
- // Another libx264 build: nothing kept matches, and the file is rewritten without it.
219
- const rebuilt = { ...FINGERPRINT, x264: "core 165 r3200 abc" };
220
- const afterUpdate = new LocalObservations({ fingerprint: rebuilt, filePath });
221
- afterUpdate.load([configurationKeyOf(rebuilt, { name: "libx264", kind: "software" })]);
222
- assert.equal(afterUpdate.slowestAloneSpeed(specAt(), content), null);
223
- assert.equal(afterUpdate.peakMbps(specAt()), null);
224
- assert.equal(JSON.parse(readFileSync(filePath, "utf8")).entries.speed && Object.keys(JSON.parse(readFileSync(filePath, "utf8")).entries.speed).length, 0);
225
- } finally {
226
- rmSync(directory, { recursive: true, force: true });
227
- }
228
- });
229
-
230
- test("a speed seen on easier material is not applied to harder material, and the slowest comparable one is used", () => {
231
- const observations = new LocalObservations({ fingerprint: FINGERPRINT, filePath: "" });
232
- const light = { codec: "hevc", bitDepth: 10, width: 1920, height: 1080, megabitsPerSecond: 4 };
233
- const heavy = { ...light, megabitsPerSecond: 12 };
234
- observations.noteEncode({ spec: specAt(), content: light, aloneSpeedX: 2.4, segmentKbps: null });
235
- assert.equal(observations.slowestAloneSpeed(specAt(), heavy), null, "a lighter source says nothing about a heavier one");
236
- observations.noteEncode({ spec: specAt(), content: heavy, aloneSpeedX: 1.3, segmentKbps: null });
237
- assert.equal(observations.slowestAloneSpeed(specAt(), light), 1.3, "both apply to the lighter source, and the slower wins");
238
- assert.equal(observations.slowestAloneSpeed(specAt(), { ...light, codec: "h264" }), null, "another codec is not comparable");
239
- assert.equal(observations.slowestAloneSpeed(specAt({ preset: "fast" }), light), null, "another mode is not this one");
240
- });
241
-
242
- test("an encoder with no bound of its own takes the peak seen, as an estimate and never as a bound", () => {
243
- const hardware = specAt({ encoder: "h264_vaapi", preset: null });
244
- assert.equal(videoLoadOfSpec(hardware, null).peakClass, PEAK_CLASS.UNKNOWN);
245
- const seen = videoLoadOfSpec(hardware, null, 5.2);
246
- assert.equal(seen.peakClass, PEAK_CLASS.ESTIMATED);
247
- assert.equal(seen.mbps, 5.2);
248
- // A bound the encoder is held to is never replaced by what was seen.
249
- const bounded = specAt({ rateControl: { maxrateKbps: 3900, bufsizeKbps: 4500, level: "3.1" } });
250
- assert.equal(videoLoadOfSpec(bounded, null, 9.9).peakClass, PEAK_CLASS.KNOWN);
251
- assert.equal(videoLoadOfSpec(bounded, null, 9.9).mbps, 3.9);
252
- });
253
-
254
- test("a file of another shape is not read as observations", () => {
255
- const directory = mkdtempSync(path.join(os.tmpdir(), "ttv-observations-"));
256
- const filePath = path.join(directory, "local-observations.json");
257
- try {
258
- writeFileSync(filePath, JSON.stringify({ version: 0, entries: { speed: { x: [1] } } }));
259
- const observations = new LocalObservations({ fingerprint: FINGERPRINT, filePath });
260
- observations.load([configurationKeyOf(FINGERPRINT, { name: "libx264", kind: "software" })]);
261
- assert.equal(observations.peakMbps(specAt()), null);
262
- } finally {
263
- rmSync(directory, { recursive: true, force: true });
264
- }
265
- });
266
-
267
- test("what an encode was made from is described by the file's own facts", () => {
268
- assert.deepEqual(
269
- contentOf({ width: 1920, height: 1080, decode: { codec: "hevc", bitDepth: 10, megabitsPerSecond: 8, megapixelsPerSecond: 50 } }),
270
- { codec: "hevc", bitDepth: 10, width: 1920, height: 1080, megabitsPerSecond: 8 }
271
- );
272
- assert.equal(contentOf({ width: 0, height: 0, decode: null }), null);
273
- });
@@ -1,134 +0,0 @@
1
- /**
2
- * @file An established fact is said once, then with decreasing frequency.
3
- *
4
- * Field 2026-09-12: one absent piece produced 235 000 lines in 92 minutes —
5
- * about 55 a second, 68.8 % of them byte-identical repeats — and it turned the
6
- * log file over twice, so the beginning of the failure was gone before anybody
7
- * read it. A log is not spoilt by its size; it is spoilt by uniformity.
8
- *
9
- * Matched verbatim, the whole line: that catches nearly all of the flood and
10
- * cannot merge two different statements. Normalising numbers would catch a
11
- * little more and would also merge the memory series, which exists precisely to
12
- * catch a runaway.
13
- */
14
-
15
- import test from "node:test";
16
- import assert from "node:assert/strict";
17
- import { logger, writeAlreadyDecided } from "../utils/logger.js";
18
-
19
- /** What reached the console while `body` ran. */
20
- function captured(body) {
21
- const lines = [];
22
- const real = { log: console.log, warn: console.warn, error: console.error };
23
- console.log = (line) => lines.push(String(line));
24
- console.warn = (line) => lines.push(String(line));
25
- console.error = (line) => lines.push(String(line));
26
- try {
27
- body();
28
- } finally {
29
- console.log = real.log;
30
- console.warn = real.warn;
31
- console.error = real.error;
32
- }
33
- return lines;
34
- }
35
-
36
- /** A line no other test in this process will have said. */
37
- function unique(what) {
38
- return `logger-repeats ${what} ${Math.random().toString(36).slice(2)}`;
39
- }
40
-
41
- test("the first time is said, and the repeats right behind it are not", () => {
42
- const message = unique("flood");
43
- const lines = captured(() => {
44
- for (let at = 0; at < 500; at += 1) {
45
- logger.info(message);
46
- }
47
- });
48
- assert.equal(lines.length, 1, "five hundred identical lines are one fact");
49
- assert.ok(lines[0].includes(message));
50
- });
51
-
52
- test("two different lines are both said — repeats are matched whole", () => {
53
- const one = unique("one");
54
- const other = unique("other");
55
- const lines = captured(() => {
56
- logger.info(one);
57
- logger.info(other);
58
- logger.info(one);
59
- logger.info(other);
60
- });
61
- assert.equal(lines.length, 2, "different statements never merge");
62
- assert.ok(lines[0].includes(one));
63
- assert.ok(lines[1].includes(other));
64
- });
65
-
66
- test("a line whose numbers differ is a different line", () => {
67
- const stem = unique("rss");
68
- const lines = captured(() => {
69
- logger.info(`${stem} rss=327MB`);
70
- logger.info(`${stem} rss=726MB`);
71
- logger.info(`${stem} rss=4616MB`);
72
- });
73
- assert.equal(
74
- lines.length,
75
- 3,
76
- "the memory series exists to catch a runaway; suppressing it would be worse than the flood"
77
- );
78
- });
79
-
80
- test("when it is said again, it says how many were held back", async () => {
81
- const message = unique("counted");
82
- const first = captured(() => {
83
- logger.info(message);
84
- for (let at = 0; at < 9; at += 1) {
85
- logger.info(message);
86
- }
87
- });
88
- assert.equal(first.length, 1);
89
-
90
- // The first interval is a second. Waited for rather than assumed: this asks
91
- // the logger itself when it is ready to speak again, with a deadline as a
92
- // backstop.
93
- const deadline = Date.now() + 10_000;
94
- let later = [];
95
- while (later.length === 0) {
96
- if (Date.now() > deadline) {
97
- assert.fail("the line should be said again once its interval has passed");
98
- }
99
- await new Promise((resolve) => setTimeout(resolve, 100));
100
- later = captured(() => logger.info(message));
101
- }
102
- const said = later[0].match(/\[said (\d+) more time\(s\) in the last ([0-9.]+)s\]/);
103
- assert.ok(said, `the rate is the fact here, and must be stated: ${later[0]}`);
104
- // At least the nine held back above. Each poll that found nothing said the
105
- // line again and was itself held back, so the exact figure belongs to how
106
- // often this test asked — the property being pinned is that the held-back
107
- // count is reported at all, and that it counts every one of them.
108
- assert.ok(Number(said[1]) >= 9, `held-back count too low: ${later[0]}`);
109
- assert.ok(Number(said[2]) > 0, `the span it covers must be stated: ${later[0]}`);
110
- });
111
-
112
- test("a line already decided on another thread is written as it is", () => {
113
- const message = unique("forwarded");
114
- const lines = captured(() => {
115
- writeAlreadyDecided("info", message);
116
- writeAlreadyDecided("info", message);
117
- writeAlreadyDecided("warn", message);
118
- });
119
- // The worker holds the same rule and applies it before forwarding. Deciding
120
- // again here would be one decision taken twice on two different histories,
121
- // and the file's own promise is that a line cannot reach the console and miss
122
- // the file.
123
- assert.equal(lines.length, 3, "a forwarded line is not judged a second time");
124
- });
125
-
126
- test("every level goes through the same rule", () => {
127
- const message = unique("levels");
128
- const lines = captured(() => {
129
- logger.warn(message);
130
- logger.warn(message);
131
- logger.error(message);
132
- });
133
- assert.equal(lines.length, 1, "a flood of errors is still a flood");
134
- });
@@ -1,56 +0,0 @@
1
- /**
2
- * @file What "the encoder is ahead of the viewer" actually means.
3
- *
4
- * It was measured as the highest segment number lying in the session's
5
- * directory. That equals the look-ahead only while a viewer moves forward
6
- * through one encoder run, and the difference destroyed a session on
7
- * 2026-08-06: a seek forward left segments 662-665 on disk, the viewer then
8
- * seeked BACK to 646, and the limiter compared 6950 s of output against a
9
- * viewer at 6700 s, called it "250s ahead", and suspended a run 136 ms after it
10
- * started — before it had produced anything. With the encoder stopped nothing
11
- * read the input, so no pieces were requested (`0 selection(s)` with 33 peers
12
- * connected) and segment 646 was never made. The viewer sat on a spinner while
13
- * a stopped ffmpeg was held for being "too far ahead".
14
- *
15
- * The measure is the unbroken run of segments starting where the viewer is.
16
- * Segments beyond a hole are not look-ahead: the viewer cannot reach them until
17
- * the hole is filled.
18
- */
19
-
20
- import test from "node:test";
21
- import assert from "node:assert/strict";
22
- import { contiguousEnd } from "../services/encode/CushionReport.js";
23
-
24
- test("an unbroken run reports its last segment", () => {
25
- assert.equal(contiguousEnd(new Set([10, 11, 12, 13]), 10), 13);
26
- });
27
-
28
- test("a hole stops the count, whatever lies beyond it", () => {
29
- assert.equal(
30
- contiguousEnd(new Set([10, 11, 662, 663, 664, 665]), 10),
31
- 11,
32
- "segments past a gap are unreachable and must not count as ready"
33
- );
34
- });
35
-
36
- test("the field case: a backward seek onto a segment nobody has made", () => {
37
- // What the directory held after the forward seek, and where the viewer went.
38
- const onDisk = new Set([661, 662, 663, 664, 665]);
39
- assert.equal(
40
- contiguousEnd(onDisk, 646),
41
- null,
42
- "the viewer's own segment is missing — that is a wait, not a look-ahead"
43
- );
44
- });
45
-
46
- test("a single segment counts as itself", () => {
47
- assert.equal(contiguousEnd(new Set([5]), 5), 5);
48
- });
49
-
50
- test("nothing on disk is not zero ahead — it is unknown", () => {
51
- assert.equal(
52
- contiguousEnd(new Set(), 0),
53
- null,
54
- "zero and null lead to opposite decisions: pause versus encode now"
55
- );
56
- });