@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.
- package/README.md +14 -3
- package/package.json +10 -2
- package/routes/api/client-logs/post.js +9 -2
- package/server.js +0 -11
- package/.claude/commands/opsx/apply.md +0 -155
- package/.claude/commands/opsx/archive.md +0 -160
- package/.claude/commands/opsx/explore.md +0 -174
- package/.claude/commands/opsx/propose.md +0 -109
- package/.claude/commands/opsx/sync.md +0 -143
- package/.claude/skills/openspec-apply-change/SKILL.md +0 -159
- package/.claude/skills/openspec-archive-change/SKILL.md +0 -117
- package/.claude/skills/openspec-explore/SKILL.md +0 -289
- package/.claude/skills/openspec-propose/SKILL.md +0 -113
- package/.claude/skills/openspec-sync-specs/SKILL.md +0 -147
- package/.dockerignore +0 -5
- package/AGENTS.md +0 -363
- package/CHANGELOG.md +0 -2373
- package/Dockerfile +0 -30
- package/biome.json +0 -412
- package/corpus.tmp.json +0 -1
- package/docs/browser-proxy-contract.md +0 -155
- package/docs/container-architecture.md +0 -273
- package/docs/download-architecture.md +0 -261
- package/docs/encode-architecture.md +0 -765
- package/docs/encode-run-state.md +0 -96
- package/docs/logs.md +0 -138
- package/docs/playback-start-forecast.md +0 -113
- package/docs/socks5-egress-research.md +0 -114
- package/docs/storage-architecture.md +0 -273
- package/knip.json +0 -14
- package/main2.cpuprofile +0 -1
- package/openspec/changes/adaptive-bitrate/.openspec.yaml +0 -0
- package/openspec/changes/adaptive-bitrate/design.md +0 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +0 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +0 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +0 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +0 -2
- package/openspec/changes/binary-distribution/proposal.md +0 -53
- package/openspec/changes/chunked-request-bodies/design.md +0 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +0 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +0 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +0 -40
- package/openspec/changes/cold-start/design.md +0 -98
- package/openspec/changes/cold-start/proposal.md +0 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +0 -47
- package/openspec/changes/cold-start/tasks.md +0 -48
- package/openspec/changes/disk-cap/.openspec.yaml +0 -2
- package/openspec/changes/disk-cap/proposal.md +0 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +0 -25
- package/openspec/changes/disk-cap/tasks.md +0 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +0 -2
- package/openspec/changes/proxy-observability/design.md +0 -38
- package/openspec/changes/proxy-observability/proposal.md +0 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +0 -33
- package/openspec/changes/proxy-observability/tasks.md +0 -21
- package/openspec/changes/seek-debounce/.openspec.yaml +0 -0
- package/openspec/changes/seek-debounce/design.md +0 -118
- package/openspec/changes/seek-debounce/proposal.md +0 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +0 -41
- package/openspec/changes/seek-debounce/tasks.md +0 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +0 -2
- package/openspec/changes/subtitle-language/proposal.md +0 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +0 -34
- package/openspec/changes/subtitle-language/tasks.md +0 -26
- package/openspec/changes/track-selection/.openspec.yaml +0 -2
- package/openspec/changes/track-selection/design.md +0 -47
- package/openspec/changes/track-selection/proposal.md +0 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +0 -41
- package/openspec/changes/track-selection/tasks.md +0 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +0 -2
- package/openspec/changes/transcode-quality/proposal.md +0 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +0 -139
- package/openspec/changes/transcode-quality/tasks.md +0 -63
- package/openspec/config.yaml +0 -38
- package/public/.well-known/appspecific/com.chrome.devtools.json +0 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +0 -329
- package/research/double-spawn-2026-09-10.md +0 -171
- package/research/handover-2026-09-13.md +0 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +0 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +0 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +0 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +0 -225
- package/research/valgrind-replica-2026-08-27/logs.tar.gz +0 -0
- package/scripts/build-audio-samples.mjs +0 -31
- package/scripts/render-run-graph.js +0 -173
- package/test/asset-paths.test.js +0 -28
- package/test/audio-buffer-continuity.test.js +0 -77
- package/test/audio-calibration.test.js +0 -23
- package/test/audio-inventory.test.js +0 -212
- package/test/audio-track-bitrate.test.js +0 -40
- package/test/audio-warm-recorded.test.js +0 -63
- package/test/auto-quality-step.test.js +0 -762
- package/test/available-share.test.js +0 -76
- package/test/background-fill.test.js +0 -190
- package/test/client-log-file.test.js +0 -126
- package/test/coalesce.test.js +0 -39
- package/test/completed-files.test.js +0 -115
- package/test/concurrent-cost.test.js +0 -148
- package/test/container-media-info.test.js +0 -228
- package/test/contention.test.js +0 -76
- package/test/core-dumps.test.js +0 -57
- package/test/coverage-follows-the-disk.test.js +0 -233
- package/test/coverage-map.test.js +0 -195
- package/test/cue-push-carries-what-the-walk-found.test.js +0 -58
- package/test/cut-times-timeline.test.js +0 -65
- package/test/cuts-follow-published-grid.test.js +0 -109
- package/test/data-channel-frame.test.js +0 -63
- package/test/data-channel-viewer-lifecycle.test.js +0 -193
- package/test/dead-torrent-handle.test.js +0 -52
- package/test/declared-rates.test.js +0 -116
- package/test/declared-tracks.test.js +0 -43
- package/test/decode-cost-fit.test.js +0 -202
- package/test/decode-cost.test.js +0 -475
- package/test/delivery-probe-field.test.js +0 -173
- package/test/delivery-probe.test.js +0 -382
- package/test/demand-register.test.js +0 -206
- package/test/dht-bootstrap.test.js +0 -72
- package/test/drain-threat.test.js +0 -65
- package/test/duplicate-add.test.js +0 -48
- package/test/encode-admission.test.js +0 -170
- package/test/encode-cost.test.js +0 -275
- package/test/encode-exit.test.js +0 -111
- package/test/encode-orchestrator.test.js +0 -426
- package/test/encode-plan-viewers.test.js +0 -725
- package/test/encode-plan.test.js +0 -545
- package/test/encode-run-state.test.js +0 -314
- package/test/encode-run.test.js +0 -248
- package/test/encode-slope.test.js +0 -46
- package/test/encoded-output.test.js +0 -39
- package/test/encoder-kinds.test.js +0 -122
- package/test/encoder-readings.test.js +0 -68
- package/test/episode-naming.test.js +0 -131
- package/test/every-claimant-is-told-its-share.test.js +0 -152
- package/test/failed-encoder-still-serves-what-exists.test.js +0 -354
- package/test/failed-start-breaker.test.js +0 -202
- package/test/fastest-wires.test.js +0 -143
- package/test/ffmpeg-stream-counts.test.js +0 -101
- package/test/file-edges.test.js +0 -191
- package/test/files-in-use.test.js +0 -34
- package/test/fixtures/audio-frame-boundary.json +0 -1
- package/test/fixtures/refused-audio-20261002.json +0 -10
- package/test/fmp4-presentation-clock.test.js +0 -148
- package/test/fmp4-self-contained.test.js +0 -80
- package/test/fmp4-timestamps.test.js +0 -140
- package/test/fmp4-track-completeness.test.js +0 -41
- package/test/format-keyed-outputs.test.js +0 -78
- package/test/health-metrics.test.js +0 -36
- package/test/held-read-ownership.test.js +0 -77
- package/test/held-request-width.test.js +0 -157
- package/test/helpers/encode-run.js +0 -248
- package/test/helpers/held-file.js +0 -77
- package/test/helpers/manager.js +0 -62
- package/test/helpers/matroska-file.js +0 -296
- package/test/helpers/output-spec.js +0 -63
- package/test/host-load.test.js +0 -68
- package/test/index-accuracy-is-about-the-file.test.js +0 -128
- package/test/init-compat.test.js +0 -219
- package/test/input-lost-quiets-the-plan.test.js +0 -261
- package/test/input-unavailable.test.js +0 -48
- package/test/installation-id.test.js +0 -39
- package/test/keeping-period.test.js +0 -83
- package/test/keyframe-index-accuracy.test.js +0 -188
- package/test/keyframe-table-budget.test.js +0 -123
- package/test/keyframes-belong-to-the-file.test.js +0 -249
- package/test/learned-median.test.js +0 -37
- package/test/letting-go-asks-who-is-watching.test.js +0 -191
- package/test/limit-row-by-area.test.js +0 -60
- package/test/link-budget.test.js +0 -111
- package/test/link-probe.test.js +0 -56
- package/test/local-encoding-data.test.js +0 -273
- package/test/logger-repeats.test.js +0 -134
- package/test/look-ahead-coverage.test.js +0 -56
- package/test/manager-surface.test.js +0 -291
- package/test/matroska-blocks.test.js +0 -0
- package/test/matroska-cluster-discovery.test.js +0 -323
- package/test/matroska-cues-track.test.js +0 -192
- package/test/media-time.test.js +0 -62
- package/test/memory-budget.test.js +0 -390
- package/test/move-cost.test.js +0 -216
- package/test/mp4-composition-times.test.js +0 -0
- package/test/mp4-subtitles.test.js +0 -173
- package/test/one-authority.test.js +0 -285
- package/test/one-budget.test.js +0 -149
- package/test/one-output-one-session.test.js +0 -273
- package/test/one-resource-divided.test.js +0 -185
- package/test/one-serving-output-per-height.test.js +0 -458
- package/test/orchestrator-wired.test.js +0 -207
- package/test/output-catalog.test.js +0 -39
- package/test/output-shape.test.js +0 -21
- package/test/output-spec.test.js +0 -251
- package/test/output-speed.test.js +0 -92
- package/test/outputs-being-prepared.test.js +0 -40
- package/test/packet-witness-ring.test.js +0 -236
- package/test/packet-witness.test.js +0 -148
- package/test/piece-completeness.test.js +0 -78
- package/test/piece-disk-store.test.js +0 -401
- package/test/piece-from-whole-file.test.js +0 -129
- package/test/piece-lru.test.js +0 -319
- package/test/piece-reader.test.js +0 -162
- package/test/piece-selection-offset.test.js +0 -85
- package/test/piece-store-eviction.test.js +0 -595
- package/test/piece-store-never-refuses.test.js +0 -153
- package/test/piece-store-reservations.test.js +0 -403
- package/test/piece-store-slow-disk.test.js +0 -159
- package/test/piece-tail.test.js +0 -97
- package/test/plan-host-timings.test.js +0 -68
- package/test/plan-is-steady.test.js +0 -225
- package/test/playback-readiness.test.js +0 -518
- package/test/playback-start-regressions.test.js +0 -101
- package/test/priority-map-download.test.js +0 -174
- package/test/priority-map-emptied.test.js +0 -204
- package/test/priority-map-per-output.test.js +0 -215
- package/test/priority-map.test.js +0 -163
- package/test/probe-wedge-certainty.test.js +0 -60
- package/test/produced-copy-choice.test.js +0 -213
- package/test/published-timeline.test.js +0 -83
- package/test/quality-offer.test.js +0 -160
- package/test/quality-variants.test.js +0 -1274
- package/test/rate-control-in-the-output.test.js +0 -181
- package/test/read-survives-withdrawal.test.js +0 -164
- package/test/read-window.test.js +0 -365
- package/test/reads-not-yet-arrived.test.js +0 -218
- package/test/resume-warm.test.js +0 -39
- package/test/run-graph-drift.test.js +0 -37
- package/test/run-inputs.test.js +0 -80
- package/test/run-intervals.test.js +0 -100
- package/test/run-position-follows-published-grid.test.js +0 -89
- package/test/run-progress.test.js +0 -64
- package/test/same-height-switch.test.js +0 -663
- package/test/seek-frees-the-viewers-requests.test.js +0 -217
- package/test/seek-landing.test.js +0 -109
- package/test/seek-target-not-superseded.test.js +0 -195
- package/test/segment-closed-on-both-branches.test.js +0 -114
- package/test/segment-cut-times.test.js +0 -63
- package/test/segment-demand.test.js +0 -80
- package/test/segment-hold-supersede.test.js +0 -91
- package/test/segment-judged-by-its-own-header.test.js +0 -222
- package/test/segment-serve-wiring.test.js +0 -481
- package/test/segment-store-eviction.test.js +0 -247
- package/test/segment-store.test.js +0 -352
- package/test/segment-true-start.test.js +0 -86
- package/test/segments-are-shared.test.js +0 -176
- package/test/send-chunk-bytes.test.js +0 -86
- package/test/serving-existing-output.test.js +0 -111
- package/test/serving-output.test.js +0 -102
- package/test/shared-piece-store.test.js +0 -310
- package/test/sidecar-naming.test.js +0 -142
- package/test/soundtrack-rate-bound.test.js +0 -289
- package/test/source-file.test.js +0 -133
- package/test/source-files-route.test.js +0 -113
- package/test/source-registry.test.js +0 -18
- package/test/split-a-stretch.test.js +0 -139
- package/test/stale-request-after-seek.test.js +0 -158
- package/test/startup-readings.test.js +0 -124
- package/test/stream-route.test.js +0 -237
- package/test/subtitle-cue-framing.test.js +0 -200
- package/test/subtitle-cue-source.test.js +0 -102
- package/test/subtitle-cue-walk.test.js +0 -385
- package/test/subtitle-cursor.test.js +0 -63
- package/test/subtitle-defaults.test.js +0 -118
- package/test/subtitle-language.test.js +0 -252
- package/test/subtitle-metadata.test.js +0 -15
- package/test/subtitle-plan-waits.test.js +0 -125
- package/test/subtitle-track-numbering.test.js +0 -391
- package/test/supply-margin.test.js +0 -190
- package/test/swarm-follows-readers.test.js +0 -174
- package/test/swarm-reach.test.js +0 -130
- package/test/swarm-selection.test.js +0 -220
- package/test/tail-duplication.test.js +0 -167
- package/test/the-channel-has-both-ends.test.js +0 -91
- package/test/the-torrent-thread-serves-bytes.test.js +0 -107
- package/test/thread-pool.test.js +0 -53
- package/test/timeline.test.js +0 -84
- package/test/torrent-contents.test.js +0 -235
- package/test/torrent-cost.test.js +0 -71
- package/test/torrent-files.test.js +0 -221
- package/test/torrent-naming.test.js +0 -255
- package/test/tracks-begin-together.test.js +0 -209
- package/test/tunnel-renewal.test.js +0 -167
- package/test/two-viewers-one-picture.test.js +0 -508
- package/test/upload-hurry.test.js +0 -186
- package/test/usrsctp-state.test.js +0 -131
- package/test/video-facts.test.js +0 -102
- package/test/viewer-assignments.test.js +0 -143
- package/test/viewer-outputs.test.js +0 -285
- package/test/viewer-presence.test.js +0 -116
- package/test/viewer-requests-admission.test.js +0 -92
- package/test/viewer-starts-on-map.test.js +0 -27
- package/test/viewer-states.test.js +0 -192
- package/test/viewer-subtitle-subscription.test.js +0 -96
- package/test/viewer.test.js +0 -139
- package/test/viewing-generations.test.js +0 -390
- package/test/visible-rung.test.js +0 -37
- package/test/wait-ledger.test.js +0 -100
- package/test/webrtc-shim.test.js +0 -149
- package/test/wedge-certainty.test.js +0 -131
- package/test/whole-file-held-read.test.js +0 -30
- package/test/withdraw-piece-claim.test.js +0 -212
- package/test/worker-channel.test.js +0 -158
package/test/move-cost.test.js
DELETED
|
@@ -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
|
-
});
|