@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,216 +0,0 @@
1
- /**
2
- * @file What moving a running encoder costs, and what the plan does while
3
- * nobody has measured it.
4
- *
5
- * Field 2026-09-08: 39 moves in one session, 24 of them between three adjacent
6
- * numbers — #58 to #59, #59 to #58, #58 to #60, #60 to #58, six times each,
7
- * about 0.8 s apart — while the viewer's picture stood still for 116.7 s in
8
- * three interruptions, the worst of them 91.8 s. The zone the viewer's own
9
- * position defines slides forward one number at a time, and every slide made
10
- * standing one number behind it score worse than standing in it.
11
- *
12
- * THREE FAULTS IN THE ARITHMETIC, and no threshold anywhere. There was one for
13
- * a while — "a move must beat staying by at least what moving costs" — and it
14
- * was a prop under a comparison that was wrong rather than indifferent. It is
15
- * gone.
16
- *
17
- * 1. **A body was charged a whole piece for the one it was already making.**
18
- * `arrival = delay + (index - at + 1) / rate` is right for a body that does
19
- * not exist yet and wrong for a run 0.8 s into a 0.9 s piece. `delaySec` is
20
- * now when the body finishes the piece it STANDS ON, so from #58 reaching #59
21
- * costs 0.14 + 0.94 = 1.08 s against 1.88 s for a kill and a cold start — a
22
- * decision by eight hundred milliseconds, where the double charge had made it
23
- * a coin flip lost by ten.
24
- * 2. **The piece was priced at the unpenalised rate** while arrivals used the
25
- * penalised one, so every extra body looked cheaper than it is and the plan
26
- * bought a second encoder where one served. One rate, the one in force.
27
- * 3. **`withinSeconds: null` was read through `Number()`**, where it is 0, so
28
- * the film BEHIND the viewers was due immediately and was the most urgent
29
- * material in the file. It bought encoders and it took the run standing in
30
- * front of the viewer, because that run was the nearest body to it.
31
- *
32
- * And what a move costs is `Infinity` until something has been measured, because
33
- * a move is irreversible while leaving the encoder alone is always available. A
34
- * run killed before producing anything is a measurement too — a lower bound on
35
- * the first output — which is the only reading a thrash can supply.
36
- */
37
-
38
- import test from "node:test";
39
- import assert from "node:assert/strict";
40
- import { RunCosts } from "../services/encode/run-costs.js";
41
- import { planEncoders } from "../services/encode/EncodePlan.js";
42
- import { CoverageMap } from "../services/encode/CoverageMap.js";
43
-
44
- test("nothing measured is a plain zero, and the floor is derived where the arithmetic is", () => {
45
- const costs = new RunCosts();
46
-
47
- const { firstByteWaitSec, killCostSec } = costs.seconds();
48
- assert.equal(firstByteWaitSec, 0, "no reading is said as none, not as a guess");
49
- assert.equal(killCostSec, 0);
50
- // There was an `Infinity` here — the cost of a move, made unaffordable until
51
- // something had been measured, on the reasoning that an unmeasured price must
52
- // not license an irreversible act. It was an exception in a model that needs
53
- // none: a first piece cannot appear faster than it takes to ENCODE one, and
54
- // how fast this host encodes is measured before any viewer exists, so the
55
- // floor belongs where the arithmetic is.
56
- assert.equal("moveCostSec" in costs.seconds(), false, "no such figure any more");
57
- });
58
-
59
- test("a run killed before producing anything is a lower bound on the first output", () => {
60
- const costs = new RunCosts();
61
-
62
- // Exactly what a thrash supplies: a run that lived 800 ms and finished
63
- // nothing. It says the first output takes AT LEAST that long, which is a fact
64
- // and the only reading a thrash can give — every run in one is killed before
65
- // it produces.
66
- costs.note({ livedMs: 800, dyingMs: 40 });
67
-
68
- const { firstByteWaitSec, killCostSec } = costs.seconds();
69
- assert.ok(Math.abs(firstByteWaitSec - 0.8) < 0.001, `got ${firstByteWaitSec}`);
70
- assert.ok(Math.abs(killCostSec - 0.04) < 0.001, `got ${killCostSec}`);
71
- });
72
-
73
- test("a run that produced something is measured by its first output, not its life", () => {
74
- const costs = new RunCosts();
75
-
76
- costs.note({ livedMs: 60_000, firstOutputMs: 1260, dyingMs: 100 });
77
-
78
- const { firstByteWaitSec } = costs.seconds();
79
- assert.ok(Math.abs(firstByteWaitSec - 1.26) < 0.001, `got ${firstByteWaitSec}`);
80
- });
81
-
82
- /**
83
- * The map's real shape: one segment at the viewer, doubling zones ahead down to
84
- * p91, and everything behind them at p1 with no deadline. Written out because a
85
- * fixture of two zones is not this, and the difference decides the answer: with
86
- * nothing stated past the viewer's own zone, a run one segment behind it is
87
- * compared on that zone alone and loses by a tenth of a second.
88
- *
89
- * @param {number} head - The segment the viewer is on.
90
- * @param {number} count
91
- * @returns {object[]}
92
- */
93
- function mapAt(head, count) {
94
- const zones = [];
95
- if (head > 0) {
96
- zones.push({ from: 0, to: head - 1, priority: 1, withinSeconds: null, behind: true });
97
- }
98
- let from = head;
99
- let width = 1;
100
- let rank = 100;
101
- while (from < count && rank > 90) {
102
- const to = Math.min(count - 1, from + width - 1);
103
- zones.push({ from, to, priority: rank, withinSeconds: (from - head) * 4.2, behind: false });
104
- from = to + 1;
105
- width *= 2;
106
- rank -= 1;
107
- }
108
- if (from < count) {
109
- zones.push({ from, to: count - 1, priority: 90, withinSeconds: (from - head) * 4.2, behind: false });
110
- }
111
- return zones;
112
- }
113
-
114
- test("an encoder is left alone while the viewer is still at or before it", () => {
115
- // Every slide of the viewer's zone used to make standing one number behind it
116
- // score worse than standing in it — by ten milliseconds, which is nothing but
117
- // the double charge for a piece already being made. What holds now is the
118
- // narrower and true statement: while the viewer's own zone still contains the
119
- // encoder's position, it is left alone. A viewer BEFORE it is a different
120
- // case entirely and correctly moves it back — encoders only go forward, so
121
- // one standing past a viewer never reaches them.
122
- //
123
- // Once the viewer has PASSED it, moving forward is correct and happens once: a
124
- // run that has produced nothing in 0.8 s of a 1.26 s warm-up owes 0.46 s
125
- // before its piece exists, while a fresh one at the viewer's own number owes
126
- // 0.32 s of spawn and then the piece — so the viewer is served sooner, and the
127
- // number left behind is in nobody's zone.
128
- const coverage = new CoverageMap();
129
- coverage.setSegmentCount(482);
130
- const run = { from: 58, to: -1, head: 58, speedX: 4.45, isAlive: true, startedAt: 1_000_000 };
131
- coverage.claim(run, 58, -1);
132
-
133
- for (const viewerAt of [58]) {
134
- const actions = planEncoders({
135
- coverage,
136
- windows: mapAt(viewerAt, 482),
137
- runs: [run],
138
- maxRuns: 3,
139
- segmentSeconds: 4.2,
140
- speedX: 4.45,
141
- killCostSec: 0.04,
142
- // Measured on the addon host: a run started at 15:50:15.521 and its first
143
- // piece existed at 15:50:16.785.
144
- firstByteWaitSec: 1.26,
145
- refetchSecPerFilmSecond: 0,
146
- // 1.98 at 1920x1080, measured: a second encoder takes very nearly all of
147
- // the first's speed.
148
- contentionPenaltyFor: (others) => (others <= 0 ? 1 : 1.98 ** others),
149
- now: 1_000_000 + 800
150
- });
151
-
152
- assert.deepEqual(
153
- actions.filter((one) => one.type === "move"),
154
- [],
155
- `the viewer at #${viewerAt} does not cost the encoder its place`
156
- );
157
- }
158
-
159
- // And once they are past it, exactly one move — not one per slide.
160
- const past = [59, 60, 61].map((viewerAt) => planEncoders({
161
- coverage,
162
- windows: mapAt(viewerAt, 482),
163
- runs: [run],
164
- maxRuns: 3,
165
- segmentSeconds: 4.2,
166
- speedX: 4.45,
167
- killCostSec: 0.04,
168
- firstByteWaitSec: 1.26,
169
- refetchSecPerFilmSecond: 0,
170
- contentionPenaltyFor: (others) => (others <= 0 ? 1 : 1.98 ** others),
171
- now: 1_000_000 + 800
172
- }).filter((one) => one.type === "move"));
173
-
174
- assert.deepEqual(
175
- past.map((moves) => moves.length),
176
- [1, 1, 1],
177
- "one move to where the viewer now is, whichever number that is"
178
- );
179
- assert.deepEqual(
180
- past.map((moves) => moves[0].from),
181
- [59, 60, 61],
182
- "and it goes to the viewer's own number, not one past it"
183
- );
184
- });
185
-
186
- test("a move that genuinely saves the viewer time still happens", () => {
187
- // The other half: the viewer jumped fourteen segments ahead, and driving there
188
- // at 4.45x would take 13 s while a cold start takes 0.94 s. Refusing this
189
- // would be the opposite fault.
190
- const coverage = new CoverageMap();
191
- coverage.setSegmentCount(482);
192
- const run = { from: 0, to: 481, head: 44, speedX: 4.45, isAlive: true };
193
- coverage.claim(run, 0, 481);
194
-
195
- const actions = planEncoders({
196
- coverage,
197
- windows: [{ from: 58, to: 59, priority: 100, withinSeconds: 0, behind: false }],
198
- runs: [run],
199
- maxRuns: 3,
200
- segmentSeconds: 4.2,
201
- speedX: 4.45,
202
- killCostSec: 0.04,
203
- firstByteWaitSec: 1.26,
204
- refetchSecPerFilmSecond: 0,
205
- contentionPenaltyFor: () => 1
206
- });
207
-
208
- // WHERE the encoder ends up, not how it got there: the run at #44 reaches
209
- // nothing anybody waits for, so the plan may either take it to #58 or stop it
210
- // and start one there. Both are one process at #58, and which is cheaper is
211
- // the measured difference between a kill and a cold start.
212
- const placed = actions
213
- .filter((one) => one.type === "move" || one.type === "start")
214
- .map((one) => one.from);
215
- assert.deepEqual(placed, [58], "fourteen segments of driving is worth a cold start");
216
- });
Binary file
@@ -1,173 +0,0 @@
1
- /**
2
- * @file Reading an MP4's text subtitle track out of its sample table.
3
- *
4
- * The file is built here, so every answer is known in advance: two cues at
5
- * stated times, in stated places, with an empty sample between them — the way
6
- * the format says "nothing on screen just now".
7
- */
8
-
9
- import test from "node:test";
10
- import assert from "node:assert/strict";
11
- import { Mp4Container } from "../services/media/container/Mp4Container.js";
12
-
13
- function box(type, payload) {
14
- const header = Buffer.alloc(8);
15
- header.writeUInt32BE(payload.length + 8, 0);
16
- header.write(type, 4, "latin1");
17
- return Buffer.concat([header, payload]);
18
- }
19
-
20
- function fullBox(type, payload) {
21
- return box(type, Buffer.concat([Buffer.from([0, 0, 0, 0]), payload]));
22
- }
23
-
24
- function u32(...values) {
25
- const buffer = Buffer.alloc(values.length * 4);
26
- values.forEach((value, index) => buffer.writeUInt32BE(value, index * 4));
27
- return buffer;
28
- }
29
-
30
- /** A `tx3g` sample: a two-byte length, then the text. */
31
- function textSample(text) {
32
- const bytes = Buffer.from(text, "utf8");
33
- const length = Buffer.alloc(2);
34
- length.writeUInt16BE(bytes.length, 0);
35
- return Buffer.concat([length, bytes]);
36
- }
37
-
38
- /**
39
- * A file with one text track: three samples, the middle one empty.
40
- *
41
- * @param {number} [displayFlags] - The `tx3g` sample entry's display flags.
42
- * @param {boolean} [withFtyp] - Open the file with an `ftyp` box, which is what
43
- * the container layer sniffs for before it will read anything.
44
- * @returns {{ file: Buffer, samples: Buffer[] }}
45
- */
46
- function buildFile(displayFlags = 0, withFtyp = false) {
47
- const samples = [textSample("First line"), Buffer.alloc(2), textSample("Second line")];
48
- const timescale = 1000;
49
-
50
- const mdhd = fullBox("mdhd", Buffer.concat([
51
- u32(0, 0, timescale, 60_000),
52
- // Language "eng", five bits a letter offset from 0x60, then a spare word.
53
- Buffer.from([0x15, 0xc7, 0, 0])
54
- ]));
55
- const hdlr = fullBox("hdlr", Buffer.concat([u32(0), Buffer.from("sbtl", "latin1"), u32(0, 0, 0)]));
56
- const tkhd = fullBox("tkhd", u32(0, 0, 7, 0, 60_000));
57
-
58
- // A sample entry opens with 6 reserved bytes and a 2-byte data reference
59
- // index, and `displayFlags` is the 32 bits after them.
60
- const tx3g = Buffer.alloc(24);
61
- tx3g.writeUInt32BE(displayFlags >>> 0, 8);
62
- const stsd = fullBox("stsd", Buffer.concat([u32(1), box("tx3g", tx3g)]));
63
- // Two seconds a sample, so the cues sit at 0-2, 2-4 and 4-6 seconds.
64
- const stts = fullBox("stts", Buffer.concat([u32(1), u32(3, 2000)]));
65
- const stsz = fullBox("stsz", Buffer.concat([u32(0, 3), u32(...samples.map((s) => s.length))]));
66
- const stsc = fullBox("stsc", Buffer.concat([u32(1), u32(1, 3, 1)]));
67
-
68
- const stbl = box("stbl", Buffer.concat([stsd, stts, stsz, stsc, fullBox("stco", Buffer.concat([u32(1), u32(0)]))]));
69
- const minf = box("minf", stbl);
70
- const mdia = box("mdia", Buffer.concat([mdhd, hdlr, minf]));
71
- const trak = box("trak", Buffer.concat([tkhd, mdia]));
72
- const moovDraft = box("moov", trak);
73
-
74
- // The samples sit after moov, so the chunk offset is known only now. Its
75
- // length does not change when the placeholder becomes the real value.
76
- const ftyp = withFtyp
77
- ? box("ftyp", Buffer.concat([Buffer.from("isom", "latin1"), u32(512), Buffer.from("isom", "latin1")]))
78
- : Buffer.alloc(0);
79
- const mdatStart = ftyp.length + moovDraft.length + 8;
80
- const stcoReal = fullBox("stco", Buffer.concat([u32(1), u32(mdatStart)]));
81
- const stblReal = box("stbl", Buffer.concat([stsd, stts, stsz, stsc, stcoReal]));
82
- const moov = box("moov", box("trak", Buffer.concat([
83
- tkhd,
84
- box("mdia", Buffer.concat([mdhd, hdlr, box("minf", stblReal)]))
85
- ])));
86
- const mdat = box("mdat", Buffer.concat(samples));
87
- return { file: Buffer.concat([ftyp, moov, mdat]), samples };
88
- }
89
-
90
- function readerOver(file) {
91
- return async (start, end) => {
92
- const last = Math.min(end, file.length - 1);
93
- return start > last ? null : file.subarray(start, last + 1);
94
- };
95
- }
96
-
97
- test("a text track's cues are found with their times and their byte ranges", async () => {
98
- const { file, samples } = buildFile();
99
-
100
- const plan = await Mp4Container.readSubtitlePlan(readerOver(file), file.length);
101
-
102
- assert.equal(plan.tracks.length, 1);
103
- const track = plan.tracks[0];
104
- assert.equal(track.format, "tx3g");
105
- assert.equal(track.language, "eng");
106
- assert.equal(track.samples.length, 2, "the empty sample is a gap, not a cue");
107
- assert.deepEqual(
108
- track.samples.map((sample) => [sample.startSeconds, sample.endSeconds]),
109
- [[0, 2], [4, 6]],
110
- "times come from the duration table, and the gap keeps its place in it"
111
- );
112
- // The bytes the plan points at are the ones that hold the text.
113
- const first = file.subarray(track.samples[0].offset, track.samples[0].offset + track.samples[0].size);
114
- assert.equal(Mp4Container.cueTextOf(first, "tx3g"), "First line");
115
- const second = file.subarray(track.samples[1].offset, track.samples[1].offset + track.samples[1].size);
116
- assert.equal(Mp4Container.cueTextOf(second, "tx3g"), "Second line");
117
- assert.equal(second.length, samples[2].length);
118
- });
119
-
120
- test("a file with no text track offers nothing", async () => {
121
- const moov = box("moov", box("trak", box("mdia", Buffer.concat([
122
- fullBox("mdhd", Buffer.concat([u32(0, 0, 1000, 100), Buffer.from([0, 0, 0, 0])])),
123
- fullBox("hdlr", Buffer.concat([u32(0), Buffer.from("vide", "latin1"), u32(0, 0, 0)]))
124
- ]))));
125
- const file = Buffer.concat([moov, box("mdat", Buffer.alloc(4))]);
126
-
127
- const plan = await Mp4Container.readSubtitlePlan(readerOver(file), file.length);
128
-
129
- assert.deepEqual(plan.tracks, []);
130
- });
131
-
132
- test("a WebVTT sample gives up the text inside its payload box", () => {
133
- const payl = box("payl", Buffer.from("Hello there", "utf8"));
134
- const sample = box("vttc", payl);
135
-
136
- assert.equal(Mp4Container.cueTextOf(sample, "wvtt"), "Hello there");
137
- });
138
-
139
- test("the sample entry's display flags say whether the track is forced", async () => {
140
- // Apple's QuickTime File Format, "Display flags" under Subtitle sample
141
- // description: `0x40000000` "Some samples are forced", `0x80000000` "All
142
- // samples are forced", and setting the second requires the first — together
143
- // `0xC0000000`. `0x20000000` is vertical placement and says nothing about
144
- // forcing.
145
- const readFlags = async (displayFlags) => {
146
- const { file } = buildFile(displayFlags);
147
- const plan = await Mp4Container.readSubtitlePlan(readerOver(file), file.length);
148
- const track = plan.tracks[0];
149
- return [track.someSamplesForced, track.allSamplesForced];
150
- };
151
-
152
- assert.deepEqual(await readFlags(0), [false, false]);
153
- assert.deepEqual(await readFlags(0x40000000), [true, false]);
154
- assert.deepEqual(await readFlags(0xc0000000), [true, true]);
155
- // A writer that set only the second bit still means the track is forced.
156
- assert.deepEqual(await readFlags(0x80000000), [false, true]);
157
- // Vertical placement is a different field's business.
158
- assert.deepEqual(await readFlags(0x20000000), [false, false]);
159
- });
160
-
161
- test("a forced sample entry reaches the track the viewer is offered", async () => {
162
- const forced = buildFile(0xc0000000, true).file;
163
- const plain = buildFile(0, true).file;
164
-
165
- const trackOf = async (file) => {
166
- const container = new Mp4Container({ readRange: readerOver(file), fileSize: file.length });
167
- const tracks = await container.readTracks();
168
- return tracks.find((track) => track.type === "subtitle");
169
- };
170
-
171
- assert.equal((await trackOf(forced))?.isForced, true);
172
- assert.equal((await trackOf(plain))?.isForced, false);
173
- });
@@ -1,285 +0,0 @@
1
- /**
2
- * @file One place decides which encoders exist.
3
- *
4
- * Not a check of behaviour but of shape, and it is here because the shape is
5
- * what failed. Three separate places used to decide where an encoder should
6
- * work and whether it should go on living, and they disagreed on every pass:
7
- * measured in the field on 2026-09-05, 684 starts and 660 stops in 482 seconds,
8
- * of which 294 were one place killing what another had just decided to keep,
9
- * while the viewer's own segment went unmade for 32.3 seconds.
10
- *
11
- * Every rule below is one that was broken then. A reader who needs to add a
12
- * fourth place should read this file first and then not.
13
- */
14
-
15
- import test from "node:test";
16
- import assert from "node:assert/strict";
17
- import { readFileSync, readdirSync } from "node:fs";
18
- import path from "node:path";
19
- import { fileURLToPath } from "node:url";
20
- import { EventEmitter } from "node:events";
21
- import { EncodeRun } from "../services/encode/EncodeRun.js";
22
-
23
- const HERE = path.dirname(fileURLToPath(import.meta.url));
24
-
25
- /**
26
- * @param {string} relative
27
- * @returns {string}
28
- */
29
- function source(relative) {
30
- return readFileSync(path.join(HERE, "..", relative), "utf8");
31
- }
32
-
33
- /**
34
- * Every source file of the proxy's services, read together.
35
- *
36
- * A rule that something must not come back is asked of all of them: asked of
37
- * one file, it passes as soon as the code moves to another.
38
- *
39
- * @returns {string}
40
- */
41
- function everything() {
42
- return readdirSync(path.join(HERE, "..", "services"), { recursive: true })
43
- .filter((name) => String(name).endsWith(".js"))
44
- .map((name) => source(path.join("services", String(name))))
45
- .join("\n");
46
- }
47
-
48
- /**
49
- * Lines of code, without comments or blanks: a rule about what the code does
50
- * must not be answered by what a comment says about it.
51
- *
52
- * @param {string} text
53
- * @returns {string[]}
54
- */
55
- function statements(text) {
56
- return text
57
- .split("\n")
58
- .map((line) => line.trim())
59
- .filter((line) => line.length > 0 && !line.startsWith("//") && !line.startsWith("*") && !line.startsWith("/*"));
60
- }
61
-
62
- test("an encoder is stopped for scheduling reasons in exactly one place", () => {
63
- // The orchestrator decides; nobody else may. What is left in the session
64
- // manager is teardown — the session is going away and its encoders with it —
65
- // which is not a decision about which encoders should exist.
66
- const orchestrator = statements(source("services/encode/EncodeOrchestrator.js"));
67
- const stopsInOrchestrator = orchestrator.filter((line) => line.includes("run.stop("));
68
- assert.equal(stopsInOrchestrator.length, 1, "the orchestrator stops runs in one place");
69
-
70
- const wiring = statements(source("services/server/wire-outputs.js"));
71
- assert.deepEqual(wiring.filter((line) => line.includes(".stop(")), [], "the wiring stops nothing");
72
- const lifecycle = statements(source("services/server/OutputLifecycle.js"));
73
- const stopsAtTeardown = lifecycle.filter((line) => line.includes(".stop("));
74
- assert.equal(
75
- stopsAtTeardown.length,
76
- 1,
77
- "an output's end stops its runs, and nothing else there does: " + stopsAtTeardown.join(" / ")
78
- );
79
- // And the runs themselves stop one for a stated reason in one place, which is
80
- // what a corrected cut table asks through.
81
- const runs = statements(source("services/encode/EncodeRuns.js"));
82
- assert.equal(runs.filter((line) => line.includes(".stop(")).length, 1);
83
- });
84
-
85
- test("nothing outside the encoding layer starts an encoder", () => {
86
- // A run is built in one place. Two places building them is how a start came
87
- // to kill what the plan had decided to keep — the killing lived in the
88
- // building.
89
- const builds = statements(everything()).filter((line) => line.includes("new EncodeRun("));
90
- assert.equal(builds.length, 1, "one place builds an encoder");
91
- });
92
-
93
- test("a run exists means its process is running, so nothing can start one twice", () => {
94
- // WHAT THIS IS FOR, and why the test above did not catch it. That one counts
95
- // where a run is BUILT and asks nothing about where it is STARTED, and until
96
- // 2026-09-10 those were two acts: the session manager built a run and started
97
- // it, handed it back, and the orchestrator started it again. Every run of
98
- // every session therefore had two ffmpeg processes on one output writing one
99
- // set of names — 207 runs against 414 spawns in the field logs of 08-10
100
- // September, without a single exception — and only the second was reachable,
101
- // so a stop killed one and the other ran on, measured 105 seconds past its
102
- // own run's death.
103
- //
104
- // Asserted over the source because the guarantee IS a property of the source:
105
- // there is no second act for a second owner to perform.
106
- const files = [
107
- "services/encode/EncodeRun.js",
108
- "services/encode/EncodeOrchestrator.js",
109
- "services/encode/EncodeRuns.js",
110
- "services/server/wire-outputs.js"
111
- ];
112
- for (const file of files) {
113
- const starts = statements(source(file)).filter(
114
- (line) =>
115
- /(?:^|[^a-zA-Z#])start\(/.test(line) &&
116
- !/startsWith|startPosition|startIndex|startSeconds|startAt|startNumber/.test(line)
117
- );
118
- assert.deepEqual(starts, [], "an encoder is started by hand in " + file + ": " + starts.join(" / "));
119
- }
120
- });
121
-
122
- test("starting an encoder stops nothing", () => {
123
- // The rule that broke it: the start path looked for a live run whose own
124
- // start was not below the new one's and killed it. It is not enough that the
125
- // line is gone — the words it was written with must not come back.
126
- const manager = everything();
127
- assert.equal(
128
- manager.includes("previousRun"),
129
- false,
130
- "there is no such thing as the previous run: a session holds several"
131
- );
132
- assert.equal(manager.includes("a new run is taking its place"), false);
133
- });
134
-
135
- test("a seek moves the viewer and nothing else", () => {
136
- // It used to do eleven things and write the position into five places. What
137
- // follows from a viewer moving is the map's business, and the orchestrators
138
- // read the map.
139
- const manager = source("services/server/ViewerRequests.js");
140
- const at = manager.indexOf("requestSeek(sessionId, positionSeconds");
141
- assert.notEqual(at, -1, "the seek is where this reads it");
142
- const seek = manager.slice(at, at + 2000);
143
- const body = seek.slice(0, seek.indexOf("\n }\n"));
144
- assert.equal(body.includes("#startEncodeRun"), false, "a seek starts no encoder");
145
- assert.equal(body.includes("setTimeout"), false, "and waits for nothing");
146
- });
147
-
148
- test("how far an encoder may work is answered once", () => {
149
- // The plan computes the stretch and it reaches ffmpeg. A second computation
150
- // somewhere else is what made the first one pointless: it was passed and then
151
- // dropped by a parameter list that did not name it.
152
- const orchestrator = source("services/encode/EncodeOrchestrator.js");
153
- assert.match(
154
- orchestrator,
155
- /makeRun\(\{ address, from, to, because \}\)/,
156
- "the stretch is handed over, and so are the plan's own words for why"
157
- );
158
- });
159
-
160
- test("only the plan places an encoder", () => {
161
- // The count is the whole of this item. There were eight other places: the
162
- // first run of a session, a viewer joining it further in, a rung or a
163
- // soundtrack being warmed, one being switched to, a hardware encoder falling
164
- // back to software, an input coming back, a cut table correcting itself, and
165
- // a settled seek. Each of them chose a position by a rule of its own, and the
166
- // plan — which is arithmetic over what is made, what is being made and what is
167
- // wanted — was left to compare its answer against theirs.
168
- const manager = source("services/encode/EncodeRuns.js");
169
- const starts = [...manager.matchAll(/this\.#startEncodeRun\(/g)].length;
170
- assert.equal(starts, 1, "one caller, and it is the one the plan asks through");
171
- assert.match(
172
- manager,
173
- /makeRunAt\(address, from, to, because\)[\s\S]{0,3000}this\.#startEncodeRun\(base, from, because, \{ to \}\)/,
174
- "and that caller is what the plan is given to build runs with"
175
- );
176
- });
177
-
178
- test("nobody stops an encoder for being unwatched", () => {
179
- // Whether an encoder is still wanted is the same question as where one should
180
- // be, and the plan answers it: an output with nobody on it has a priority map
181
- // with nothing in it. Answered here as well, it was answered twice by two
182
- // rules — and since a viewer moving between steps announces itself, the plan
183
- // started again what this class had just killed, several times a second.
184
- const manager = everything();
185
- assert.equal(
186
- manager.includes("no viewer is watching"),
187
- false,
188
- "a rung nobody is on is a fact, not an act"
189
- );
190
- assert.equal(manager.includes("no viewer is listening to audio track"), false);
191
- assert.equal(
192
- manager.includes("warmed for a switch the viewer did not make"),
193
- false,
194
- "an abandoned warm-up is a viewer leaving an output"
195
- );
196
- assert.equal(manager.includes("prepared for a track change the viewer did not make"), false);
197
- });
198
-
199
- test("the seek settle machinery is gone, whole", () => {
200
- // A viewer's position is applied the moment they state it. The settle was a
201
- // second debounce on a signal the browser had already debounced, and every
202
- // millisecond of it was dead time in front of the viewer; the cooldown behind
203
- // it existed because segment REQUESTS once steered the encoder.
204
- const manager = everything();
205
- for (const gone of [
206
- "seekSettleTimer",
207
- "seekTarget",
208
- "seekFirstFarAt",
209
- "SEEK_SETTLE_MS",
210
- "SEEK_SETTLE_MAX_MS",
211
- "RESTART_COOLDOWN_MS",
212
- "SEEK_BACKOFF_SEGMENTS",
213
- "#fireSettledSeek",
214
- "#seekSession"
215
- ]) {
216
- assert.equal(manager.includes(gone), false, `${gone} is gone`);
217
- }
218
- });
219
-
220
- test("where a soundtrack begins is read off the table, not handed in", () => {
221
- // The instant a number really begins is a fact of the FILE's cutting, held in
222
- // the live table every session of the file shares. Passed as an argument by
223
- // the one caller that had measured it, only a run started by that caller ever
224
- // had it, and a run the plan placed at the same number landed apart again.
225
- const manager = source("services/encode/EncodeRuns.js");
226
- assert.match(
227
- manager,
228
- /const positionSecondsOverride = session\.spec\.carries === "audio-only"\s*\n?\s*\? trueStartOf\(session\.timeline, startIndex\)/,
229
- "derived where the run is built"
230
- );
231
- assert.equal(
232
- manager.includes("this.#startEncodeRun(member, index, trueStart)"),
233
- false,
234
- "and not carried in from the correction that measured it"
235
- );
236
- });
237
-
238
- test("each output is handed its own priority map, and the plan is what reads it", () => {
239
- // The map is one fact asked at two scopes, and both are right for what asks
240
- // them: the swarm is asked for bytes of a FILE, which every output of it
241
- // reads, and encoders are placed per OUTPUT, which a person watching 480p
242
- // wants nothing of at 1080p.
243
- const manager = source("services/encode/EncodeRuns.js");
244
- assert.match(
245
- manager,
246
- /notePriorityMap\([\s\S]{0,200}mapForOutput\(address\)/,
247
- "the encoding reads the output's own map"
248
- );
249
- assert.equal(
250
- manager.includes("this.priority.mapFor("),
251
- false,
252
- "and never the whole film's, which wanted an encoder on every output of it"
253
- );
254
- });
255
-
256
- test("one process per run, and no way to ask for a second", () => {
257
- const spawned = [];
258
- const run = new EncodeRun({
259
- address: "torrent:abc:video-only:v=0/copy",
260
- encoder: { name: "libx264", kind: "software" },
261
- from: 0,
262
- to: 4,
263
- buildArgs: () => [],
264
- spawn: () => {
265
- const child = new EventEmitter();
266
- child.stdout = new EventEmitter();
267
- child.stderr = new EventEmitter();
268
- child.stdio = [null, child.stdout, child.stderr, new EventEmitter()];
269
- child.kill = () => undefined;
270
- child.pid = 1 + spawned.length;
271
- spawned.push(child);
272
- return child;
273
- },
274
- logger: { info: () => undefined, warn: () => undefined },
275
- now: () => 1000,
276
- because: "a test asked for it"
277
- });
278
-
279
- assert.equal(spawned.length, 1, "building a run did not put a process on the machine");
280
- assert.equal(
281
- Reflect.get(run, "start"),
282
- undefined,
283
- "a run still offers a way to start it a second time"
284
- );
285
- });