@torrent-tv/proxy 2.89.17 → 2.89.19
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/server.js +0 -11
- package/utils/client-log-file.js +147 -42
- 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 -2377
- 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 -151
- 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/client-logs-route.test.js +0 -34
- 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
|
@@ -1,314 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file The encoder-run table, exercised as a graph.
|
|
3
|
-
*
|
|
4
|
-
* These are properties of the specification, not of any run: no ffmpeg, no
|
|
5
|
-
* torrent, no filesystem, no clock. What they buy is the thing five field
|
|
6
|
-
* failures in a row were — an empty cell — being visible before a release
|
|
7
|
-
* rather than after one.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import assert from "node:assert/strict";
|
|
11
|
-
import test from "node:test";
|
|
12
|
-
|
|
13
|
-
import {
|
|
14
|
-
ABSENT_EDGE_INVARIANTS,
|
|
15
|
-
ENCODE_RUN_EVENT,
|
|
16
|
-
ENCODE_RUN_STATE,
|
|
17
|
-
ENCODE_RUN_SUPERSTATE,
|
|
18
|
-
INITIAL_RUN_STATE,
|
|
19
|
-
answerForMissingSegment,
|
|
20
|
-
declaredContainment,
|
|
21
|
-
declaredEdges,
|
|
22
|
-
isInputBeingRead,
|
|
23
|
-
isWithin,
|
|
24
|
-
mayRestart,
|
|
25
|
-
nextState,
|
|
26
|
-
processCanBeSignalled,
|
|
27
|
-
wireState
|
|
28
|
-
} from "../services/encode/encode-run-state.js";
|
|
29
|
-
|
|
30
|
-
const ALL_STATES = Object.values(ENCODE_RUN_STATE);
|
|
31
|
-
const ALL_EVENTS = Object.values(ENCODE_RUN_EVENT);
|
|
32
|
-
|
|
33
|
-
// ------------------------------------------------------------ graph discipline
|
|
34
|
-
|
|
35
|
-
test("the relation is deterministic", () => {
|
|
36
|
-
for (const state of ALL_STATES) {
|
|
37
|
-
for (const event of ALL_EVENTS) {
|
|
38
|
-
assert.equal(nextState(state, event), nextState(state, event));
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test("every pair is answered, and nothing throws", () => {
|
|
44
|
-
for (const state of ALL_STATES) {
|
|
45
|
-
for (const event of ALL_EVENTS) {
|
|
46
|
-
const target = nextState(state, event);
|
|
47
|
-
assert.ok(
|
|
48
|
-
target === null || ALL_STATES.includes(target),
|
|
49
|
-
`${state} + ${event} answered ${String(target)}, which is neither a state nor "ignored"`
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
// Nonsense in, "ignored" out — never an exception. The machine this pattern
|
|
54
|
-
// replaces threw from inside an event handler, which left the caller's work
|
|
55
|
-
// half applied.
|
|
56
|
-
assert.equal(nextState("NOT_A_STATE", ENCODE_RUN_EVENT.SPAWNED), null);
|
|
57
|
-
assert.equal(nextState(ENCODE_RUN_STATE.IDLE, "NOT_AN_EVENT"), null);
|
|
58
|
-
assert.equal(nextState(undefined, undefined), null);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test("one target per state and event — no pair is declared twice", () => {
|
|
62
|
-
const seen = new Set();
|
|
63
|
-
for (const edge of declaredEdges()) {
|
|
64
|
-
const pair = `${edge.from}+${edge.event}`;
|
|
65
|
-
assert.ok(!seen.has(pair), `${pair} is declared more than once`);
|
|
66
|
-
seen.add(pair);
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test("the table cannot be edited through what it hands out", () => {
|
|
71
|
-
const [first] = declaredEdges();
|
|
72
|
-
const mutated = declaredEdges();
|
|
73
|
-
mutated[0].to = "TAMPERED";
|
|
74
|
-
assert.equal(declaredEdges()[0].to, first.to);
|
|
75
|
-
const containment = declaredContainment();
|
|
76
|
-
containment[0].parent = "TAMPERED";
|
|
77
|
-
assert.notEqual(declaredContainment()[0].parent, "TAMPERED");
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test("every state is reachable from where a run begins", () => {
|
|
81
|
-
const seen = new Set([INITIAL_RUN_STATE]);
|
|
82
|
-
const queue = [INITIAL_RUN_STATE];
|
|
83
|
-
while (queue.length > 0) {
|
|
84
|
-
const state = queue.shift();
|
|
85
|
-
for (const event of ALL_EVENTS) {
|
|
86
|
-
const target = nextState(state, event);
|
|
87
|
-
if (target && !seen.has(target)) {
|
|
88
|
-
seen.add(target);
|
|
89
|
-
queue.push(target);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
assert.deepEqual(
|
|
94
|
-
ALL_STATES.filter((state) => !seen.has(state)),
|
|
95
|
-
[],
|
|
96
|
-
"a state no sequence of events can enter is either dead code or a missing edge"
|
|
97
|
-
);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
test("no state is a dead end — every one can start a run again", () => {
|
|
101
|
-
for (const state of ALL_STATES) {
|
|
102
|
-
assert.equal(
|
|
103
|
-
nextState(state, ENCODE_RUN_EVENT.SPAWNED),
|
|
104
|
-
ENCODE_RUN_STATE.STARTING,
|
|
105
|
-
`${state} must be able to spawn a run: a session that cannot restart answers 500 for ever, ` +
|
|
106
|
-
"which is what the roadmap calls the empty cell"
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
test("the two edges that lead back to their own state are deliberate", () => {
|
|
112
|
-
const selfEdges = [];
|
|
113
|
-
for (const state of ALL_STATES) {
|
|
114
|
-
for (const event of ALL_EVENTS) {
|
|
115
|
-
if (nextState(state, event) === state) {
|
|
116
|
-
selfEdges.push(`${state} + ${event}`);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
assert.deepEqual(
|
|
121
|
-
selfEdges.sort(),
|
|
122
|
-
[
|
|
123
|
-
// A new run spawned while one was starting: the state is right already,
|
|
124
|
-
// and nothing in the caller re-runs on entry — the spawn has happened.
|
|
125
|
-
`${ENCODE_RUN_STATE.STARTING} + ${ENCODE_RUN_EVENT.SPAWNED}`,
|
|
126
|
-
// Suspending what is suspended. The caller returns early before signalling,
|
|
127
|
-
// so this is the answer to an event that cannot actually be raised twice.
|
|
128
|
-
`${ENCODE_RUN_STATE.SUSPENDED} + ${ENCODE_RUN_EVENT.SUSPEND_ORDERED}`
|
|
129
|
-
].sort(),
|
|
130
|
-
"an edge that leads back to its own state must be argued for, not acquired"
|
|
131
|
-
);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
test("the table stays small enough to hold in the head", () => {
|
|
135
|
-
assert.ok(
|
|
136
|
-
declaredEdges().length <= 12,
|
|
137
|
-
`${declaredEdges().length} declared edges — a near-complete digraph asserts nothing`
|
|
138
|
-
);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
// ------------------------------------------------------- what must not happen
|
|
142
|
-
|
|
143
|
-
test("the absent edges are absent", () => {
|
|
144
|
-
for (const invariant of ABSENT_EDGE_INVARIANTS) {
|
|
145
|
-
assert.notEqual(
|
|
146
|
-
nextState(invariant.from, invariant.event),
|
|
147
|
-
invariant.mustNotReach,
|
|
148
|
-
`${invariant.from} + ${invariant.event} reached ${invariant.mustNotReach}: ${invariant.because}`
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
test("an event that means nothing here is ignored, not obeyed", () => {
|
|
154
|
-
assert.equal(nextState(ENCODE_RUN_STATE.IDLE, ENCODE_RUN_EVENT.FIRST_SEGMENT), null);
|
|
155
|
-
assert.equal(nextState(ENCODE_RUN_STATE.IDLE, ENCODE_RUN_EVENT.EXITED_COMPLETE), null);
|
|
156
|
-
assert.equal(nextState(ENCODE_RUN_STATE.STOPPED, ENCODE_RUN_EVENT.RESUME_ORDERED), null);
|
|
157
|
-
assert.equal(nextState(ENCODE_RUN_STATE.ENDED_COMPLETE, ENCODE_RUN_EVENT.SUSPEND_ORDERED), null);
|
|
158
|
-
assert.equal(nextState(ENCODE_RUN_STATE.PRODUCING, ENCODE_RUN_EVENT.RETRY_DUE), null);
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
// ------------------------------------------------------------------ hierarchy
|
|
162
|
-
|
|
163
|
-
test("an edge on a superstate reaches every state inside it", () => {
|
|
164
|
-
const alive = ALL_STATES.filter((state) => isWithin(state, ENCODE_RUN_SUPERSTATE.ALIVE));
|
|
165
|
-
assert.deepEqual(
|
|
166
|
-
alive.sort(),
|
|
167
|
-
[ENCODE_RUN_STATE.PRODUCING, ENCODE_RUN_STATE.STARTING, ENCODE_RUN_STATE.SUSPENDED].sort()
|
|
168
|
-
);
|
|
169
|
-
for (const state of alive) {
|
|
170
|
-
assert.equal(nextState(state, ENCODE_RUN_EVENT.EXITED_FAILED), ENCODE_RUN_STATE.ENDED_FAILED);
|
|
171
|
-
assert.equal(nextState(state, ENCODE_RUN_EVENT.EXITED_INPUT_LOST), ENCODE_RUN_STATE.RETRY_WAIT);
|
|
172
|
-
assert.equal(nextState(state, ENCODE_RUN_EVENT.STOP_ORDERED), ENCODE_RUN_STATE.STOPPED);
|
|
173
|
-
}
|
|
174
|
-
const working = ALL_STATES.filter((state) => isWithin(state, ENCODE_RUN_SUPERSTATE.WORKING));
|
|
175
|
-
assert.deepEqual(working.sort(), [...alive, ENCODE_RUN_STATE.RETRY_WAIT].sort());
|
|
176
|
-
for (const state of ALL_STATES) {
|
|
177
|
-
assert.ok(isWithin(state, ENCODE_RUN_SUPERSTATE.RUN), `${state} must sit inside RUN`);
|
|
178
|
-
}
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
test("a state's own edge wins over the one it inherits", () => {
|
|
182
|
-
// STARTING inherits SPAWNED from RUN and declares FIRST_SEGMENT itself; the
|
|
183
|
-
// walk must stop at the first hit rather than continue up the chain.
|
|
184
|
-
assert.equal(
|
|
185
|
-
nextState(ENCODE_RUN_STATE.STARTING, ENCODE_RUN_EVENT.FIRST_SEGMENT),
|
|
186
|
-
ENCODE_RUN_STATE.PRODUCING
|
|
187
|
-
);
|
|
188
|
-
assert.equal(
|
|
189
|
-
nextState(ENCODE_RUN_STATE.SUSPENDED, ENCODE_RUN_EVENT.RESUME_ORDERED),
|
|
190
|
-
ENCODE_RUN_STATE.PRODUCING
|
|
191
|
-
);
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
// -------------------------------------------------------------------- outputs
|
|
195
|
-
|
|
196
|
-
test("only a live run reads its input", () => {
|
|
197
|
-
assert.equal(isInputBeingRead(ENCODE_RUN_STATE.STARTING), true);
|
|
198
|
-
assert.equal(isInputBeingRead(ENCODE_RUN_STATE.PRODUCING), true);
|
|
199
|
-
assert.equal(
|
|
200
|
-
isInputBeingRead(ENCODE_RUN_STATE.SUSPENDED),
|
|
201
|
-
false,
|
|
202
|
-
"a suspended encoder reads nothing — the reader's window is then satisfied, WebTorrent drops " +
|
|
203
|
-
"the selection, and the download dies with the swarm open (eleven minutes, 2026-08-05)"
|
|
204
|
-
);
|
|
205
|
-
for (const state of [
|
|
206
|
-
ENCODE_RUN_STATE.IDLE,
|
|
207
|
-
ENCODE_RUN_STATE.RETRY_WAIT,
|
|
208
|
-
ENCODE_RUN_STATE.STOPPED,
|
|
209
|
-
ENCODE_RUN_STATE.ENDED_COMPLETE,
|
|
210
|
-
ENCODE_RUN_STATE.ENDED_FAILED
|
|
211
|
-
]) {
|
|
212
|
-
assert.equal(isInputBeingRead(state), false, `${state} has no process to read anything`);
|
|
213
|
-
}
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
test("a signal may be sent only where a process exists", () => {
|
|
217
|
-
for (const state of ALL_STATES) {
|
|
218
|
-
assert.equal(
|
|
219
|
-
processCanBeSignalled(state),
|
|
220
|
-
isWithin(state, ENCODE_RUN_SUPERSTATE.ALIVE),
|
|
221
|
-
`${state} answered the wrong thing about whether it holds a process`
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
test("a missing segment is held everywhere except a terminal failure", () => {
|
|
227
|
-
for (const state of ALL_STATES) {
|
|
228
|
-
assert.equal(
|
|
229
|
-
answerForMissingSegment(state),
|
|
230
|
-
state === ENCODE_RUN_STATE.ENDED_FAILED ? "fail" : "hold",
|
|
231
|
-
`${state} must give one answer for a missing segment, not one per call site`
|
|
232
|
-
);
|
|
233
|
-
}
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
test("the wire state is a function of the run state alone", () => {
|
|
237
|
-
assert.equal(wireState(ENCODE_RUN_STATE.IDLE), "starting");
|
|
238
|
-
assert.equal(wireState(ENCODE_RUN_STATE.RETRY_WAIT), "starting");
|
|
239
|
-
assert.equal(wireState(ENCODE_RUN_STATE.STARTING), "running");
|
|
240
|
-
assert.equal(wireState(ENCODE_RUN_STATE.PRODUCING), "running");
|
|
241
|
-
assert.equal(wireState(ENCODE_RUN_STATE.SUSPENDED), "running");
|
|
242
|
-
assert.equal(wireState(ENCODE_RUN_STATE.STOPPED), "running");
|
|
243
|
-
assert.equal(wireState(ENCODE_RUN_STATE.ENDED_COMPLETE), "ready");
|
|
244
|
-
assert.equal(wireState(ENCODE_RUN_STATE.ENDED_FAILED), "failed");
|
|
245
|
-
for (const state of ALL_STATES) {
|
|
246
|
-
assert.ok(
|
|
247
|
-
["starting", "running", "ready", "failed"].includes(wireState(state)),
|
|
248
|
-
`${state} produced a wire state the browser has never been told about`
|
|
249
|
-
);
|
|
250
|
-
}
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
test("only a finished file refuses to be repositioned", () => {
|
|
254
|
-
for (const state of ALL_STATES) {
|
|
255
|
-
assert.equal(mayRestart(state), state !== ENCODE_RUN_STATE.ENDED_COMPLETE);
|
|
256
|
-
}
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
// ------------------------------------------------- the failures, by their name
|
|
260
|
-
|
|
261
|
-
test("the sawtooth of 2.9.93 is not expressible", () => {
|
|
262
|
-
// Any segment request released a suspended encoder, and the run drifted from
|
|
263
|
-
// 155 s to 922 s ahead of the viewer in three minutes. Resuming is an output
|
|
264
|
-
// of how far ahead the run is, computed by the caller — never an edge hung on
|
|
265
|
-
// a request arriving.
|
|
266
|
-
assert.equal(nextState(ENCODE_RUN_STATE.SUSPENDED, ENCODE_RUN_EVENT.FIRST_SEGMENT), null);
|
|
267
|
-
assert.equal(
|
|
268
|
-
nextState(ENCODE_RUN_STATE.SUSPENDED, ENCODE_RUN_EVENT.RESUME_ORDERED),
|
|
269
|
-
ENCODE_RUN_STATE.PRODUCING
|
|
270
|
-
);
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
test("a dead run cannot be mistaken for one that is covering the seek", () => {
|
|
274
|
-
// the process handle pointed at a corpse, so every later seek was waved through
|
|
275
|
-
// as "already covered by the running encode" and the session answered 500 for
|
|
276
|
-
// as long as the viewer kept trying.
|
|
277
|
-
assert.equal(processCanBeSignalled(ENCODE_RUN_STATE.ENDED_FAILED), false);
|
|
278
|
-
assert.equal(isInputBeingRead(ENCODE_RUN_STATE.ENDED_FAILED), false);
|
|
279
|
-
assert.equal(
|
|
280
|
-
nextState(ENCODE_RUN_STATE.ENDED_FAILED, ENCODE_RUN_EVENT.SPAWNED),
|
|
281
|
-
ENCODE_RUN_STATE.STARTING,
|
|
282
|
-
"and the way out of it is a spawn — the edge whose absence is the empty cell"
|
|
283
|
-
);
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
test("a rung the viewer left is not the same as a run that has not started", () => {
|
|
287
|
-
// Both are "no process" in the field set this replaces, and they call for
|
|
288
|
-
// opposite answers: one must not be revived by its own held requests, the
|
|
289
|
-
// other is waiting to be spawned.
|
|
290
|
-
assert.notEqual(ENCODE_RUN_STATE.STOPPED, ENCODE_RUN_STATE.IDLE);
|
|
291
|
-
assert.equal(nextState(ENCODE_RUN_STATE.STOPPED, ENCODE_RUN_EVENT.RESUME_ORDERED), null);
|
|
292
|
-
assert.equal(nextState(ENCODE_RUN_STATE.IDLE, ENCODE_RUN_EVENT.SPAWNED), ENCODE_RUN_STATE.STARTING);
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
test("an input that dried up is not a finished file", () => {
|
|
296
|
-
// ffmpeg exits 0 for both, and the difference had to be recovered by
|
|
297
|
-
// comparing what was produced against the published playlist (2.9.104).
|
|
298
|
-
assert.equal(
|
|
299
|
-
nextState(ENCODE_RUN_STATE.PRODUCING, ENCODE_RUN_EVENT.EXITED_SHORT),
|
|
300
|
-
ENCODE_RUN_STATE.ENDED_FAILED
|
|
301
|
-
);
|
|
302
|
-
assert.equal(
|
|
303
|
-
nextState(ENCODE_RUN_STATE.PRODUCING, ENCODE_RUN_EVENT.EXITED_COMPLETE),
|
|
304
|
-
ENCODE_RUN_STATE.ENDED_COMPLETE
|
|
305
|
-
);
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
test("losing the input holds the viewer's requests instead of failing them", () => {
|
|
309
|
-
const afterLoss = nextState(ENCODE_RUN_STATE.PRODUCING, ENCODE_RUN_EVENT.EXITED_INPUT_LOST);
|
|
310
|
-
assert.equal(afterLoss, ENCODE_RUN_STATE.RETRY_WAIT);
|
|
311
|
-
assert.equal(answerForMissingSegment(afterLoss), "hold");
|
|
312
|
-
assert.equal(wireState(afterLoss), "starting");
|
|
313
|
-
assert.equal(nextState(afterLoss, ENCODE_RUN_EVENT.RETRY_DUE), ENCODE_RUN_STATE.IDLE);
|
|
314
|
-
});
|
package/test/encode-run.test.js
DELETED
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file One encoder's lifetime: the stretch it was given, where it got to, and
|
|
3
|
-
* whether its ending was the normal one.
|
|
4
|
-
*
|
|
5
|
-
* The process is injected, so none of this needs ffmpeg. What is checked is the
|
|
6
|
-
* accounting the user asked for on 2026-09-04: exactly one ending is normal,
|
|
7
|
-
* every other one says why, and none of them goes unrecorded.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import test from "node:test";
|
|
11
|
-
import assert from "node:assert/strict";
|
|
12
|
-
import { EventEmitter } from "node:events";
|
|
13
|
-
import { EncodeRun } from "../services/encode/EncodeRun.js";
|
|
14
|
-
import { ENCODE_EXIT } from "../services/encode/encode-exit.js";
|
|
15
|
-
import { SoftwareEncoder } from "../services/encode/SoftwareEncoder.js";
|
|
16
|
-
import { fakeProcess, silentLogger } from "./helpers/encode-run.js";
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* A process that does nothing until the test says what became of it.
|
|
20
|
-
*/
|
|
21
|
-
class FakeProcess extends EventEmitter {
|
|
22
|
-
constructor() {
|
|
23
|
-
super();
|
|
24
|
-
this.pid = 4242;
|
|
25
|
-
this.signals = [];
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
kill(signal) {
|
|
29
|
-
this.signals.push(signal);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
exitWith(code, signal = null) {
|
|
33
|
-
this.emit("exit", code, signal);
|
|
34
|
-
this.emit("close", code, signal);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* @param {Partial<{from:number,to:number}>} [span]
|
|
40
|
-
*/
|
|
41
|
-
function makeRun(span = {}) {
|
|
42
|
-
const lines = [];
|
|
43
|
-
const ends = [];
|
|
44
|
-
const process_ = new FakeProcess();
|
|
45
|
-
const run = new EncodeRun({
|
|
46
|
-
address: "torrent:abc:fmt=fmp4:grid=kf@0:video-only:v=0/enc/libx264/854x480@24/veryfast/none/vbv=-",
|
|
47
|
-
encoder: new SoftwareEncoder(),
|
|
48
|
-
from: span.from ?? 10,
|
|
49
|
-
to: span.to ?? 14,
|
|
50
|
-
buildArgs: () => ["-i", "in", "out"],
|
|
51
|
-
spawn: () => process_,
|
|
52
|
-
logger: {
|
|
53
|
-
info: (line) => lines.push(["info", line]),
|
|
54
|
-
warn: (line) => lines.push(["warn", line])
|
|
55
|
-
},
|
|
56
|
-
now: () => 1000,
|
|
57
|
-
onEnded: (ended) => ends.push(ended),
|
|
58
|
-
because: "nobody is making #10 and a viewer is waiting for it"
|
|
59
|
-
});
|
|
60
|
-
return { run, process: process_, lines, ends };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
test("a start says why it is starting and with what", () => {
|
|
64
|
-
// The argument list alone cannot say whether this was a first open, a seek, a
|
|
65
|
-
// quality step or a move off covered material — and with several runs at once
|
|
66
|
-
// that is the only way to tell one start from another.
|
|
67
|
-
const { lines } = makeRun();
|
|
68
|
-
const [level, line] = lines[0];
|
|
69
|
-
assert.equal(level, "info");
|
|
70
|
-
// A run is named by what it produces: the stretch it was given, on the output
|
|
71
|
-
// it makes segments of. No other live run of that output can hold it.
|
|
72
|
-
assert.match(line, /encode-run #10\.\.#14 of torrent:abc:/);
|
|
73
|
-
assert.match(line, /nobody is making #10/);
|
|
74
|
-
assert.match(line, /ffmpeg -i in out/);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test("its head is the next number it will make, and moves as it makes them", () => {
|
|
78
|
-
const { run } = makeRun();
|
|
79
|
-
assert.equal(run.head, 10);
|
|
80
|
-
run.noteProduced(10);
|
|
81
|
-
run.noteProduced(11);
|
|
82
|
-
assert.equal(run.head, 12);
|
|
83
|
-
assert.equal(run.reached, 11);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test("reaching the end of its stretch and exiting is the one normal ending", () => {
|
|
87
|
-
const { run, process, ends, lines } = makeRun({ from: 10, to: 12 });
|
|
88
|
-
for (const index of [10, 11, 12]) {
|
|
89
|
-
run.noteProduced(index);
|
|
90
|
-
}
|
|
91
|
-
process.exitWith(0, null);
|
|
92
|
-
assert.equal(ends.length, 1);
|
|
93
|
-
assert.equal(ends[0].ending, ENCODE_EXIT.COMPLETE);
|
|
94
|
-
assert.equal(ends[0].normal, true);
|
|
95
|
-
assert.equal(lines.at(-1)[0], "info", "a normal ending is not a warning");
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test("exiting cleanly short of its stretch is abnormal and says where it stopped", () => {
|
|
99
|
-
// ffmpeg exits zero both at the end of a file and when its input stops
|
|
100
|
-
// producing bytes; over a torrent the two look identical to it.
|
|
101
|
-
const { run, process, ends } = makeRun({ from: 10, to: 14 });
|
|
102
|
-
run.noteProduced(10);
|
|
103
|
-
run.noteProduced(11);
|
|
104
|
-
process.exitWith(0, null);
|
|
105
|
-
assert.equal(ends[0].ending, ENCODE_EXIT.SHORT);
|
|
106
|
-
assert.equal(ends[0].normal, false);
|
|
107
|
-
assert.equal(ends[0].reached, 11);
|
|
108
|
-
assert.match(ends[0].because, /#11 of #14/);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
test("a non-zero exit is a failure carrying its code", () => {
|
|
112
|
-
const { process, ends, lines } = makeRun();
|
|
113
|
-
process.exitWith(255, null);
|
|
114
|
-
assert.equal(ends[0].ending, ENCODE_EXIT.FAILED);
|
|
115
|
-
assert.equal(ends[0].code, 255);
|
|
116
|
-
assert.equal(lines.at(-1)[0], "warn");
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
test("our own kill is abnormal too, and carries the reason we gave", () => {
|
|
120
|
-
// Hiding it among the normal endings would make a count of abnormal endings
|
|
121
|
-
// useless, which is the whole point of counting them.
|
|
122
|
-
const { run, process, ends } = makeRun();
|
|
123
|
-
run.stop("moved past 20 segments that are already made");
|
|
124
|
-
assert.deepEqual(process.signals, ["SIGTERM"]);
|
|
125
|
-
process.exitWith(null, "SIGTERM");
|
|
126
|
-
assert.equal(ends[0].ending, ENCODE_EXIT.STOPPED);
|
|
127
|
-
assert.equal(ends[0].normal, false);
|
|
128
|
-
assert.match(ends[0].because, /already made/);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
test("an ending reports the stretch, how far it got and how long it lived", () => {
|
|
132
|
-
const { run, process, ends } = makeRun({ from: 10, to: 14 });
|
|
133
|
-
run.noteProduced(10);
|
|
134
|
-
process.exitWith(1, null);
|
|
135
|
-
const ended = ends[0];
|
|
136
|
-
assert.equal(ended.from, 10);
|
|
137
|
-
assert.equal(ended.to, 14);
|
|
138
|
-
assert.equal(ended.reached, 10);
|
|
139
|
-
assert.equal(ended.livedMs, 0, "the clock is injected, so this is exact");
|
|
140
|
-
assert.equal(ended.address.includes("854x480"), true);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
test("a process that could not be started ends like any other failure", () => {
|
|
144
|
-
const { process, ends } = makeRun();
|
|
145
|
-
process.emit("error", new Error("ENOENT"));
|
|
146
|
-
assert.equal(ends[0].ending, ENCODE_EXIT.FAILED);
|
|
147
|
-
assert.match(ends[0].because, /ENOENT/);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
test("its speed is film made between closed pieces over its own working time", () => {
|
|
151
|
-
const clock = { at: 1000 };
|
|
152
|
-
const process_ = new FakeProcess();
|
|
153
|
-
process_.stdout = new EventEmitter();
|
|
154
|
-
process_.stdio = [null, process_.stdout, null, new EventEmitter()];
|
|
155
|
-
const readings = [];
|
|
156
|
-
const run = new EncodeRun({
|
|
157
|
-
address: "torrent:abc:fmt=fmp4:grid=kf@0:video-only:v=0/copy",
|
|
158
|
-
encoder: new SoftwareEncoder(),
|
|
159
|
-
from: 0,
|
|
160
|
-
to: 9,
|
|
161
|
-
buildArgs: () => [],
|
|
162
|
-
spawn: () => process_,
|
|
163
|
-
logger: { info() {}, warn() {} },
|
|
164
|
-
now: () => clock.at,
|
|
165
|
-
onSpeedMeasured: (reading) => readings.push(reading)
|
|
166
|
-
});
|
|
167
|
-
assert.equal(run.speedX, 0, "nothing measured yet");
|
|
168
|
-
const report = (seconds) => process_.stdout.emit("data", `out_time_ms=${seconds * 1_000_000}\n`);
|
|
169
|
-
const closed = (name) => process_.stdio[3].emit("data", `${name}\n`);
|
|
170
|
-
report(0);
|
|
171
|
-
closed("a");
|
|
172
|
-
clock.at += 1000;
|
|
173
|
-
report(4);
|
|
174
|
-
closed("b"); // "a" is published: the reckoning starts here
|
|
175
|
-
assert.equal(run.speedX, 0, "one closed piece measures nothing");
|
|
176
|
-
// The input waits for the swarm for 30 s; that is not the run's work.
|
|
177
|
-
run.inputWaitBegins();
|
|
178
|
-
clock.at += 30_000;
|
|
179
|
-
run.inputWaitEnds();
|
|
180
|
-
clock.at += 2000;
|
|
181
|
-
report(12);
|
|
182
|
-
closed("c"); // "b" is published
|
|
183
|
-
assert.equal(run.speedX, 4, "8 s of film over 2 s of work");
|
|
184
|
-
assert.deepEqual(readings, [{ speed: 4, at: 34_000 }], "told once, with when");
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
test("a segment number behind its own start is not its to claim", () => {
|
|
188
|
-
const { run } = makeRun({ from: 10, to: 14 });
|
|
189
|
-
run.noteProduced(4);
|
|
190
|
-
assert.deepEqual(run.produced, []);
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
test("CSV closure times survive split channel chunks and a zero first-entry start", () => {
|
|
194
|
-
const process_ = new FakeProcess();
|
|
195
|
-
process_.stdio = [null, null, null, new EventEmitter()];
|
|
196
|
-
const closed = [];
|
|
197
|
-
const run = new EncodeRun({
|
|
198
|
-
address: "audio", encoder: new SoftwareEncoder(), from: 13, to: 14,
|
|
199
|
-
buildArgs: () => [], spawn: () => process_, logger: { info() {}, warn() {} },
|
|
200
|
-
indexOfName: name => Number(name.match(/(\d+)\.mp4$/)[1]),
|
|
201
|
-
onClosed: (name, timing) => { closed.push({ name, timing }); return name; }
|
|
202
|
-
});
|
|
203
|
-
process_.stdio[3].emit("data", "making-0-00013.mp4,0.000000,81.364");
|
|
204
|
-
process_.stdio[3].emit("data", "000\nmaking-0-00014.mp4,81.364000,91.800000\n");
|
|
205
|
-
process_.exitWith(0);
|
|
206
|
-
assert.equal(run.reached, 14);
|
|
207
|
-
assert.deepEqual(closed.map(({ timing }) => timing), [
|
|
208
|
-
{ endMicros: 81_364_000n },
|
|
209
|
-
{ endMicros: 91_800_000n }
|
|
210
|
-
]);
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
test("a refused publication stops its claim and ends as a publication failure", () => {
|
|
214
|
-
const process_ = new FakeProcess();
|
|
215
|
-
process_.stdio = [null, null, null, new EventEmitter()];
|
|
216
|
-
const ends = [];
|
|
217
|
-
const run = new EncodeRun({
|
|
218
|
-
address: "audio", encoder: new SoftwareEncoder(), from: 13, to: 14,
|
|
219
|
-
buildArgs: () => [], spawn: () => process_, logger: { info() {}, warn() {} },
|
|
220
|
-
onClosed: () => { throw new Error("piece 13 is incomplete"); },
|
|
221
|
-
onEnded: ended => ends.push(ended)
|
|
222
|
-
});
|
|
223
|
-
process_.stdio[3].emit("data", "making-0-00013.mp4,71.55,71.56\nmaking-0-00014.mp4,71.56,91.8\n");
|
|
224
|
-
assert.deepEqual(process_.signals, ["SIGTERM"]);
|
|
225
|
-
process_.exitWith(0);
|
|
226
|
-
assert.equal(run.isAlive, false);
|
|
227
|
-
assert.equal(ends[0].ending, ENCODE_EXIT.PUBLICATION_FAILED);
|
|
228
|
-
assert.equal(ends[0].because, "piece 13 is incomplete");
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
test("a run whose process never started is not signalled, because pid 0 is our own process group", () => {
|
|
232
|
-
// Node reports a failed start (ENOENT) on a later turn; until then the child's
|
|
233
|
-
// process id is 0, and a signal sent to it is `kill(0, …)` — every process in
|
|
234
|
-
// our group, the proxy included. Measured 2026-10-03 under `node --test`.
|
|
235
|
-
const child = fakeProcess({ pid: null });
|
|
236
|
-
const run = new EncodeRun({
|
|
237
|
-
address: "torrent:abc:video-only:v=0/copy",
|
|
238
|
-
encoder: { name: "libx264", kind: "software" },
|
|
239
|
-
from: 0,
|
|
240
|
-
to: 4,
|
|
241
|
-
buildArgs: () => [],
|
|
242
|
-
spawn: () => child,
|
|
243
|
-
logger: silentLogger,
|
|
244
|
-
because: "a test asked for it"
|
|
245
|
-
});
|
|
246
|
-
run.stop("the viewer left");
|
|
247
|
-
assert.deepEqual(child.signals, [], "a process that did not start was sent a signal");
|
|
248
|
-
});
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file The slope that turns ffmpeg's progress reports into a preset's speed.
|
|
3
|
-
*
|
|
4
|
-
* Written against the faults a review found in it on 2026-08-15, each of which
|
|
5
|
-
* would have opened the quality ladder rather than closing it: a window of no
|
|
6
|
-
* width at all, and a position ffmpeg reports as the smallest signed 64-bit
|
|
7
|
-
* integer when it has none yet.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import assert from "node:assert/strict";
|
|
11
|
-
import test from "node:test";
|
|
12
|
-
|
|
13
|
-
import { slopeOf } from "../services/encode/hwaccel.js";
|
|
14
|
-
|
|
15
|
-
const at = (wallSec, outSec) => ({ wallSec, outSec });
|
|
16
|
-
|
|
17
|
-
test("a steady run measures its own speed", () => {
|
|
18
|
-
assert.equal(slopeOf([at(1, 10), at(2, 14)]), 4);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
test("a window of no width is refused, however tempting the numbers", () => {
|
|
22
|
-
// Two reports a millisecond apart: 0.042 s of video over 0.001 s of clock is
|
|
23
|
-
// 42x, on a host that may be doing 3x.
|
|
24
|
-
assert.equal(slopeOf([at(1.0, 10), at(1.001, 10.042)], 0.2), null);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
test("a run that produced nothing between two reports says nothing", () => {
|
|
28
|
-
assert.equal(slopeOf([at(1, 10), at(3, 10)]), null);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test("one report is not a measurement", () => {
|
|
32
|
-
assert.equal(slopeOf([at(1, 10)]), null);
|
|
33
|
-
assert.equal(slopeOf([]), null);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test("an impossible speed is a fault, not a fast machine", () => {
|
|
37
|
-
// Nothing encodes 640x360 five thousand times realtime; a reading that says
|
|
38
|
-
// so is a measurement fault, and letting it through raises the bar every
|
|
39
|
-
// ladder decision is taken from.
|
|
40
|
-
assert.equal(slopeOf([at(1, 0), at(2, 5000)]), null);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test("the narrowest allowed window is honoured", () => {
|
|
44
|
-
assert.equal(slopeOf([at(1.0, 10), at(1.3, 11)], 0.5), null);
|
|
45
|
-
assert.ok(slopeOf([at(1.0, 10), at(1.6, 12)], 0.5) > 0);
|
|
46
|
-
});
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file An output holds what is born with it, and not what is produced.
|
|
3
|
-
*
|
|
4
|
-
* Decided with the user 2026-09-16: which pieces are made is the segment
|
|
5
|
-
* store's, and which encoders run is the encoding orchestrator's. A second
|
|
6
|
-
* holder of either is how the stale second owner stopped playback outright.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import test from "node:test";
|
|
10
|
-
import assert from "node:assert/strict";
|
|
11
|
-
import { EncodedOutput } from "../services/encode/output/EncodedOutput.js";
|
|
12
|
-
import { outputSpec } from "./helpers/output-spec.js";
|
|
13
|
-
|
|
14
|
-
function made() {
|
|
15
|
-
return new EncodedOutput({
|
|
16
|
-
id: "aaaaaaaabbbbcccc",
|
|
17
|
-
spec: outputSpec({ transcodeVideo: true, width: 1280, height: 720 }),
|
|
18
|
-
file: { key: "source-1:0" },
|
|
19
|
-
keyframes: null,
|
|
20
|
-
timeline: null,
|
|
21
|
-
segmentFormat: null,
|
|
22
|
-
output: null,
|
|
23
|
-
useSyntheticPlaylist: true,
|
|
24
|
-
playlistText: "#EXTM3U\n",
|
|
25
|
-
variantHeight: 720
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
test("its address is its format, read from the spec and kept nowhere else", () => {
|
|
30
|
-
const output = made();
|
|
31
|
-
assert.equal(output.outputKey, output.spec.toKey());
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test("it keeps no statement of what has been produced or what is running", () => {
|
|
35
|
-
const output = made();
|
|
36
|
-
for (const field of ["runs", "runState", "progress", "produced", "ready", "state", "lastError", "failedStartCount"]) {
|
|
37
|
-
assert.equal(field in output, false, `${field} belongs to the store or the encoding orchestrator`);
|
|
38
|
-
}
|
|
39
|
-
});
|