@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,65 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file When a move to a smaller output is prepared for a viewer, and when it is
|
|
3
|
-
* not (roadmap item 98), and which way their buffer is going.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import test from "node:test";
|
|
7
|
-
import assert from "node:assert/strict";
|
|
8
|
-
import { drainThreat } from "../services/encode/quality/drain-threat.js";
|
|
9
|
-
import { bufferTrend } from "../services/viewer/buffer-trend.js";
|
|
10
|
-
|
|
11
|
-
test("a buffer that falls but lasts past the new piece's readiness moves nothing", () => {
|
|
12
|
-
// 60 s held, draining at 0.5 s/s: empty in 120 s. The new output would have
|
|
13
|
-
// the piece in 8 s, and the next report comes in 10 s.
|
|
14
|
-
const answer = drainThreat({ bufferedSec: 60, slope: -0.5, reportGapSec: 10, secondsToReady: 8 });
|
|
15
|
-
assert.equal(answer.threat, false);
|
|
16
|
-
assert.equal(answer.secondsToEmpty, 120);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
test("a buffer that would end before the new piece is ready starts the move", () => {
|
|
20
|
-
// 6 s held, draining at 0.5 s/s: empty in 12 s — sooner than 8 s of making
|
|
21
|
-
// plus the 10 s until this viewer is heard from again.
|
|
22
|
-
assert.equal(drainThreat({ bufferedSec: 6, slope: -0.5, reportGapSec: 10, secondsToReady: 8 }).threat, true);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
test("the wait for the next report is part of the reckoning", () => {
|
|
26
|
-
const without = drainThreat({ bufferedSec: 10, slope: -1, reportGapSec: 0, secondsToReady: 8 });
|
|
27
|
-
const withGap = drainThreat({ bufferedSec: 10, slope: -1, reportGapSec: 3, secondsToReady: 8 });
|
|
28
|
-
assert.equal(without.threat, false, "10 s left against 8 s of making");
|
|
29
|
-
assert.equal(withGap.threat, true, "but the next chance to act is 3 s away, too late");
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test("a buffer that holds or grows is no threat, and neither is one with no trend yet", () => {
|
|
33
|
-
assert.equal(drainThreat({ bufferedSec: 1, slope: 0, reportGapSec: 10, secondsToReady: 30 }).threat, false);
|
|
34
|
-
assert.equal(drainThreat({ bufferedSec: 1, slope: 0.4, reportGapSec: 10, secondsToReady: 30 }).threat, false);
|
|
35
|
-
assert.equal(drainThreat({ bufferedSec: 1, slope: null, reportGapSec: 10, secondsToReady: 30 }).threat, false);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test("a readiness nobody can state is not assumed to be quick", () => {
|
|
39
|
-
assert.equal(drainThreat({ bufferedSec: 600, slope: -0.1, reportGapSec: 10, secondsToReady: null }).threat, true);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test("the trend is read over one segment's period, not from two readings", () => {
|
|
43
|
-
// A sawtooth over a flat line: a 4 s segment arrives, then 4 s play out.
|
|
44
|
-
const at = (seconds) => seconds * 1000;
|
|
45
|
-
const flat = [
|
|
46
|
-
{ at: at(0), seconds: 20 },
|
|
47
|
-
{ at: at(2), seconds: 18 },
|
|
48
|
-
{ at: at(4), seconds: 20 },
|
|
49
|
-
{ at: at(6), seconds: 18 },
|
|
50
|
-
{ at: at(8), seconds: 20 }
|
|
51
|
-
];
|
|
52
|
-
const trend = bufferTrend(flat, 4);
|
|
53
|
-
assert.ok(Math.abs(trend.slope) < 0.5, `the last two readings alone say +1 s/s; the period says ${trend.slope}`);
|
|
54
|
-
assert.equal(trend.reportGapSec, 2);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test("readings that do not yet span one segment say nothing", () => {
|
|
58
|
-
assert.equal(bufferTrend([{ at: 0, seconds: 10 }, { at: 1000, seconds: 9 }], 4), null);
|
|
59
|
-
assert.equal(bufferTrend([{ at: 0, seconds: 10 }], 4), null);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test("a steady drain reads as the rate it drains at", () => {
|
|
63
|
-
const readings = [0, 5, 10].map((second) => ({ at: second * 1000, seconds: 30 - second }));
|
|
64
|
-
assert.equal(bufferTrend(readings, 4).slope, -1);
|
|
65
|
-
});
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file One infohash, two sources: what happens when the second add is refused.
|
|
3
|
-
*
|
|
4
|
-
* The same film arrives as a magnet and as a `.torrent`. WebTorrent refuses the
|
|
5
|
-
* second add — one infohash, one torrent — and the pool then has to decide what
|
|
6
|
-
* to hand back. Adopting the one already there is right when it is ready, and
|
|
7
|
-
* wrong when it is not: a magnet whose swarm has not answered has no file list,
|
|
8
|
-
* so everything bound to it is answered 404.
|
|
9
|
-
*
|
|
10
|
-
* Measured 2026-08-15 on the addon host: a magnet with no reachable trackers
|
|
11
|
-
* was added first and never became ready; the same film's own `.torrent`, which
|
|
12
|
-
* carries the file list, the piece hashes and the trackers, then joined that
|
|
13
|
-
* empty torrent instead of replacing it. `/stream` answered 404, the encoder
|
|
14
|
-
* died on its first read, and the film stayed unplayable until the proxy was
|
|
15
|
-
* restarted — one bad magnet poisoning every later attempt at that infohash.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import test from "node:test";
|
|
19
|
-
import assert from "node:assert/strict";
|
|
20
|
-
import { duplicateAddDecision } from "../services/torrent/torrent-pool.js";
|
|
21
|
-
|
|
22
|
-
test("a ready torrent is adopted, whatever the new source carries", () => {
|
|
23
|
-
assert.equal(
|
|
24
|
-
duplicateAddDecision({ existingIsReady: true, incomingHasMetadata: true }),
|
|
25
|
-
"adopt",
|
|
26
|
-
"one swarm per infohash: a second copy would download the same pieces twice"
|
|
27
|
-
);
|
|
28
|
-
assert.equal(
|
|
29
|
-
duplicateAddDecision({ existingIsReady: true, incomingHasMetadata: false }),
|
|
30
|
-
"adopt"
|
|
31
|
-
);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test("an unready torrent is replaced when this add brings the metadata", () => {
|
|
35
|
-
assert.equal(
|
|
36
|
-
duplicateAddDecision({ existingIsReady: false, incomingHasMetadata: true }),
|
|
37
|
-
"replace",
|
|
38
|
-
"the .torrent holds what the swarm was being asked for; waiting for it is waiting for nothing"
|
|
39
|
-
);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test("two magnets wait, because neither has anything the other lacks", () => {
|
|
43
|
-
assert.equal(
|
|
44
|
-
duplicateAddDecision({ existingIsReady: false, incomingHasMetadata: false }),
|
|
45
|
-
"wait",
|
|
46
|
-
"replacing here would only restart the same search, and the caller's own bound answers"
|
|
47
|
-
);
|
|
48
|
-
});
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Whether the whole machine has a place for one more encoder (roadmap
|
|
3
|
-
* item 97, step 13).
|
|
4
|
-
*
|
|
5
|
-
* What is pinned here:
|
|
6
|
-
*
|
|
7
|
-
* 1. a place is held by an OUTPUT: two preparations onto one output take one
|
|
8
|
-
* place, and the second is admitted without arithmetic;
|
|
9
|
-
* 2. two preparations onto two DIFFERENT outputs with room for one: the first
|
|
10
|
-
* is admitted, the second refused, because it is asked after the first is
|
|
11
|
-
* recorded;
|
|
12
|
-
* 3. one of two viewers giving up a preparation of one output does not free
|
|
13
|
-
* its place;
|
|
14
|
-
* 4. the cost is what an encoder costs, not a count of processes: a picture
|
|
15
|
-
* and its soundtrack fit where two pictures do not — the addon host's own
|
|
16
|
-
* figures, 1080p alone at 1.96x;
|
|
17
|
-
* 5. an admitted encoder is never taken away: the machine's answer is never
|
|
18
|
-
* below what already runs, nor below one for an output a preparation holds;
|
|
19
|
-
* 6. a new output on a full machine is given no encoder.
|
|
20
|
-
*
|
|
21
|
-
* The records of who is being prepared are stood in for by a set the check
|
|
22
|
-
* writes to, which is what the viewers' records are read as.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
import test from "node:test";
|
|
26
|
-
import assert from "node:assert/strict";
|
|
27
|
-
import { EncodeAdmission } from "../services/encode/EncodeAdmission.js";
|
|
28
|
-
|
|
29
|
-
// 1080p re-encoded on the addon host: 1.96x alone.
|
|
30
|
-
const PICTURE_1080 = 1 / 1.96;
|
|
31
|
-
// A re-encoded soundtrack: a small fraction of a picture.
|
|
32
|
-
const SOUNDTRACK = 0.02;
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* A machine with the outputs named, their costs, what runs and who is being
|
|
36
|
-
* prepared onto what.
|
|
37
|
-
*
|
|
38
|
-
* @param {{ costs: Record<string, number | null>, running?: Record<string, number>, share?: number | null }} how
|
|
39
|
-
*/
|
|
40
|
-
function machine(how) {
|
|
41
|
-
const running = new Map(Object.entries(how.running ?? {}));
|
|
42
|
-
/** Output address to the viewers being prepared onto it. @type {Map<string, Set<string>>} */
|
|
43
|
-
const preparing = new Map();
|
|
44
|
-
const admission = new EncodeAdmission({
|
|
45
|
-
liveRunsByAddress: () => new Map([...running].filter(([, count]) => count > 0)),
|
|
46
|
-
preparedAddresses: () => new Set([...preparing].filter(([, viewers]) => viewers.size > 0).map(([address]) => address)),
|
|
47
|
-
loadOf: (address) => (address in how.costs ? { costSec: how.costs[address], fileKey: "", fileCostSec: 0 } : null),
|
|
48
|
-
availability: () => (how.share === undefined || how.share === null ? null : { share: how.share, known: true })
|
|
49
|
-
});
|
|
50
|
-
return {
|
|
51
|
-
admission,
|
|
52
|
-
running,
|
|
53
|
-
/** A viewer's preparation recorded, as `Viewer` records it. */
|
|
54
|
-
prepare: (address, viewer) => {
|
|
55
|
-
const viewers = preparing.get(address) ?? new Set();
|
|
56
|
-
viewers.add(viewer);
|
|
57
|
-
preparing.set(address, viewers);
|
|
58
|
-
},
|
|
59
|
-
/** A viewer's preparation ended, on whichever path. */
|
|
60
|
-
giveUp: (address, viewer) => preparing.get(address)?.delete(viewer)
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
test("two preparations onto one output take one place", () => {
|
|
65
|
-
const { admission, prepare } = machine({ costs: { a: PICTURE_1080, b: PICTURE_1080 } });
|
|
66
|
-
assert.equal(admission.admitsPreparation("a").admitted, true);
|
|
67
|
-
prepare("a", "viewer-1");
|
|
68
|
-
const second = admission.admitsPreparation("a");
|
|
69
|
-
assert.equal(second.admitted, true, "the second viewer shares the place");
|
|
70
|
-
assert.match(second.reason, /already holds a place/);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test("two preparations onto two different outputs with room for one: the second is refused", () => {
|
|
74
|
-
const { admission, prepare } = machine({ costs: { a: PICTURE_1080, b: PICTURE_1080 } });
|
|
75
|
-
// The first is asked and recorded in one stretch, as the preparation does it.
|
|
76
|
-
assert.equal(admission.admitsPreparation("a").admitted, true);
|
|
77
|
-
prepare("a", "viewer-1");
|
|
78
|
-
const second = admission.admitsPreparation("b");
|
|
79
|
-
assert.equal(second.admitted, false, "both together would make 0.98x of each");
|
|
80
|
-
assert.ok(second.speedX < 1);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
test("one of two viewers giving up a preparation of one output does not free its place", () => {
|
|
84
|
-
const { admission, prepare, giveUp } = machine({ costs: { a: PICTURE_1080, b: PICTURE_1080 } });
|
|
85
|
-
prepare("a", "viewer-1");
|
|
86
|
-
prepare("a", "viewer-2");
|
|
87
|
-
giveUp("a", "viewer-1");
|
|
88
|
-
assert.equal(admission.admitsPreparation("b").admitted, false, "viewer-2 still holds the place on a");
|
|
89
|
-
assert.equal(admission.placesFor("a", 0).runs, 1, "and a keeps its encoder's place");
|
|
90
|
-
giveUp("a", "viewer-2");
|
|
91
|
-
assert.equal(admission.admitsPreparation("b").admitted, true, "the place ends with the last record");
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
test("a picture and its soundtrack fit where two pictures do not", () => {
|
|
95
|
-
const { admission } = machine({
|
|
96
|
-
costs: { picture: PICTURE_1080, sound: SOUNDTRACK, other: PICTURE_1080 },
|
|
97
|
-
running: { picture: 1 }
|
|
98
|
-
});
|
|
99
|
-
assert.equal(admission.admitsPreparation("sound").admitted, true, "0.53 s/s makes 1.89x");
|
|
100
|
-
assert.equal(admission.placesFor("sound", 0).runs >= 1, true);
|
|
101
|
-
assert.equal(admission.admitsPreparation("other").admitted, false, "1.02 s/s makes 0.98x");
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test("an admitted encoder is never taken away, and a held place is kept", () => {
|
|
105
|
-
const { admission } = machine({
|
|
106
|
-
costs: { a: PICTURE_1080, b: PICTURE_1080 },
|
|
107
|
-
running: { a: 1, b: 1 }
|
|
108
|
-
});
|
|
109
|
-
assert.equal(admission.placesFor("a", 1).runs, 1, "both running: each keeps its one");
|
|
110
|
-
assert.equal(admission.placesFor("b", 1).runs, 1);
|
|
111
|
-
const { admission: held, prepare: prepareHeld } = machine({
|
|
112
|
-
costs: { a: PICTURE_1080, b: PICTURE_1080 },
|
|
113
|
-
running: { b: 1 }
|
|
114
|
-
});
|
|
115
|
-
prepareHeld("a", "viewer-1");
|
|
116
|
-
assert.equal(held.placesFor("a", 0).runs, 1, "a place promised to a preparation is not withdrawn");
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
test("a new output on a full machine is given no encoder", () => {
|
|
120
|
-
const { admission } = machine({ costs: { a: PICTURE_1080, b: PICTURE_1080 }, running: { a: 1 } });
|
|
121
|
-
assert.equal(admission.placesFor("b", 0).runs, 0);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test("the share of the machine nobody has priced is taken off what there is", () => {
|
|
125
|
-
// 0.6 s/s alone makes 1.67x; with a third of the machine taken by work
|
|
126
|
-
// nobody priced it makes 1.11x, and a soundtrack beside it still fits.
|
|
127
|
-
const quiet = machine({ costs: { a: 0.6, b: 0.4 }, share: 1 });
|
|
128
|
-
assert.equal(quiet.admission.admitsPreparation("a").admitted, true);
|
|
129
|
-
const busy = machine({ costs: { a: 0.6, b: 0.4 }, running: { a: 1 }, share: 0.6 });
|
|
130
|
-
assert.equal(busy.admission.admitsPreparation("b").admitted, false, "1.0 s/s at 0.6 of the machine makes 0.6x");
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test("an output with no measured cost is not admitted", () => {
|
|
134
|
-
const { admission } = machine({ costs: { a: PICTURE_1080, b: null }, running: { a: 1 } });
|
|
135
|
-
const answer = admission.admitsPreparation("b");
|
|
136
|
-
assert.equal(answer.admitted, false);
|
|
137
|
-
assert.match(answer.reason, /no measured encoding cost/);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
test("unpriced occupied work is neither spare pool headroom nor capacity for another output", () => {
|
|
141
|
-
const { admission } = machine({ costs: { a: null, b: PICTURE_1080 }, running: { a: 1 } });
|
|
142
|
-
assert.equal(admission.headroom().encodeSpeedX, 0, "the pool sees no confirmed room");
|
|
143
|
-
const answer = admission.admitsPreparation("b");
|
|
144
|
-
assert.equal(answer.admitted, false);
|
|
145
|
-
assert.match(answer.reason, /capacity is unknown/);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
test("a near-free output is answered at once, whatever its price", () => {
|
|
149
|
-
// Field 2026-10-04: a soundtrack priced at 5.4e-14 s per film second kept
|
|
150
|
-
// the main thread counting encoders one at a time for minutes.
|
|
151
|
-
const { admission } = machine({ costs: { sound: 5.36680075607109e-14 }, share: 0.6 });
|
|
152
|
-
const started = Date.now();
|
|
153
|
-
const { runs } = admission.placesFor("sound", 0);
|
|
154
|
-
assert.ok(Date.now() - started < 1_000, "answered without counting one at a time");
|
|
155
|
-
assert.ok(runs > 1e12 && Number.isFinite(runs), `as many as fit: ${runs}`);
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
test("the count is the largest that keeps every output at realtime", () => {
|
|
159
|
-
for (const cost of [0.5, 0.1, 0.03, 0.007, 0.0011]) {
|
|
160
|
-
for (const share of [null, 1, 0.37]) {
|
|
161
|
-
const { admission } = machine({ costs: { a: cost, b: PICTURE_1080 / 4 }, running: { b: 1 }, share });
|
|
162
|
-
const usable = share ?? 1;
|
|
163
|
-
let expected = 0;
|
|
164
|
-
while (usable / (PICTURE_1080 / 4 + (expected + 1) * cost) >= 1) {
|
|
165
|
-
expected += 1;
|
|
166
|
-
}
|
|
167
|
-
assert.equal(admission.placesFor("a", 0).runs, Math.max(expected, 0), `cost ${cost}, share ${share}`);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
});
|
package/test/encode-cost.test.js
DELETED
|
@@ -1,275 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file What encoding costs this machine, asked of the object that owns it.
|
|
3
|
-
*
|
|
4
|
-
* The arithmetic itself is exercised end to end by `auto-quality-step` and
|
|
5
|
-
* `quality-variants`, which go through the session manager. What is pinned here
|
|
6
|
-
* is the seam the move created: this object is given the host's readings as a
|
|
7
|
-
* QUESTION rather than a copy, and it holds what an encoder taught it.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import test from "node:test";
|
|
11
|
-
import assert from "node:assert/strict";
|
|
12
|
-
import { EncodeCost } from "../services/encode/quality/EncodeCost.js";
|
|
13
|
-
import { measureSpeed, startRunOn } from "./helpers/encode-run.js";
|
|
14
|
-
import { outputSpec } from "./helpers/output-spec.js";
|
|
15
|
-
import { runStateOf } from "../services/encode/encode-run-state.js";
|
|
16
|
-
import { qualityStateOf } from "../services/encode/quality/OutputQualityState.js";
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* @param {object} [readings]
|
|
20
|
-
* @returns {{ cost: EncodeCost, asked: () => number, host: { share: number } }}
|
|
21
|
-
*/
|
|
22
|
-
function costOn(readings = {}) {
|
|
23
|
-
let asked = 0;
|
|
24
|
-
const host = { share: 1 };
|
|
25
|
-
const cost = new EncodeCost({
|
|
26
|
-
outputs: {
|
|
27
|
-
familyOf: (session) => readings.familyOf?.(session) ?? [],
|
|
28
|
-
variantHeightOf: () => 0
|
|
29
|
-
},
|
|
30
|
-
host: () => {
|
|
31
|
-
asked += 1;
|
|
32
|
-
return {
|
|
33
|
-
benchmark: readings.benchmark ?? null,
|
|
34
|
-
decodeModel: null,
|
|
35
|
-
contentionPenalties: null,
|
|
36
|
-
availability: { known: true, share: host.share }
|
|
37
|
-
};
|
|
38
|
-
},
|
|
39
|
-
runningEncoders: () => readings.runningEncoders?.() ?? 0,
|
|
40
|
-
encodersRunningNow: () => 0,
|
|
41
|
-
torrentCostSecFor: () => 0,
|
|
42
|
-
runsFor: (session) => [...(session.runs ?? [])],
|
|
43
|
-
stateFor: (session) => runStateOf(session.runs)
|
|
44
|
-
});
|
|
45
|
-
return { cost, asked: () => asked, host };
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* A session with one run going, of the kind this file measures.
|
|
50
|
-
*
|
|
51
|
-
* @param {{ audioOnly?: boolean, transcodeVideo?: boolean, audioSourceTrackIndex?: number, key?: string }} [what]
|
|
52
|
-
* @returns {object}
|
|
53
|
-
*/
|
|
54
|
-
function sessionProducing(what = {}) {
|
|
55
|
-
const session = {
|
|
56
|
-
id: "1111111122223333",
|
|
57
|
-
state: "ready",
|
|
58
|
-
spec: outputSpec({
|
|
59
|
-
audioOnly: what.audioOnly === true,
|
|
60
|
-
transcodeVideo: what.transcodeVideo === true,
|
|
61
|
-
audioSourceTrackIndex: what.audioSourceTrackIndex ?? 0
|
|
62
|
-
}),
|
|
63
|
-
file: { key: what.key ?? "torrent:abc:0", name: "film.mkv" },
|
|
64
|
-
output: { encodeWidth: 0, encodeHeight: 0, outputFps: 25, softwarePreset: null },
|
|
65
|
-
clock: { at: 1000 },
|
|
66
|
-
runs: new Set()
|
|
67
|
-
};
|
|
68
|
-
startRunOn(session, { from: 0, clock: session.clock });
|
|
69
|
-
return session;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* The session's run measures its speed, and is learned from as the product
|
|
74
|
-
* learns: once the run has a reading.
|
|
75
|
-
*
|
|
76
|
-
* @param {EncodeCost} cost
|
|
77
|
-
* @param {object} session
|
|
78
|
-
* @param {number} speedX - Seconds of film produced per second of the run's own work.
|
|
79
|
-
* @param {number} [waitedMs] - How long the run's input waits for the swarm
|
|
80
|
-
* between two closed pieces.
|
|
81
|
-
* @returns {Promise<void>}
|
|
82
|
-
*/
|
|
83
|
-
async function watchItRun(cost, session, speedX, waitedMs = 0) {
|
|
84
|
-
const run = [...session.runs][0];
|
|
85
|
-
measureSpeed(run, speedX, session.clock, waitedMs);
|
|
86
|
-
await cost.learnFrom(session, run);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
test("the host is asked at the moment of the question, not when this was built", () => {
|
|
90
|
-
// The share of the machine that is free is re-read every few seconds. Copied
|
|
91
|
-
// into this object when it was made, every later rung would be priced against
|
|
92
|
-
// a machine that has gone.
|
|
93
|
-
const { cost, asked, host } = costOn();
|
|
94
|
-
assert.equal(asked(), 0, "nothing is read until something is asked");
|
|
95
|
-
|
|
96
|
-
cost.sustainableHeights({ heights: [1080], sourceWidth: 1920, sourceHeight: 1080, fps: 24, source: null, transcodeVideo: true, ownHeight: 0 });
|
|
97
|
-
const first = asked();
|
|
98
|
-
assert.ok(first > 0);
|
|
99
|
-
|
|
100
|
-
host.share = 0.1;
|
|
101
|
-
cost.sustainableHeights({ heights: [1080], sourceWidth: 1920, sourceHeight: 1080, fps: 24, source: null, transcodeVideo: true, ownHeight: 0 });
|
|
102
|
-
assert.ok(asked() > first, "and read again on the next question");
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test("with no benchmark to judge by, every height offered is kept", () => {
|
|
106
|
-
// Nothing measured is not the same as nothing possible. Refusing here would
|
|
107
|
-
// hide the whole ladder on a host whose startup measurement failed.
|
|
108
|
-
const { cost } = costOn({ benchmark: null });
|
|
109
|
-
const kept = cost.sustainableHeights({
|
|
110
|
-
heights: [1080, 720, 480],
|
|
111
|
-
sourceWidth: 1920,
|
|
112
|
-
sourceHeight: 1080,
|
|
113
|
-
fps: 24,
|
|
114
|
-
source: null,
|
|
115
|
-
transcodeVideo: true,
|
|
116
|
-
ownHeight: 0
|
|
117
|
-
});
|
|
118
|
-
assert.deepEqual(kept, [1080, 720, 480]);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test("a rung measured below realtime is withdrawn, and a copied source height is not", () => {
|
|
122
|
-
const { cost } = costOn({ benchmark: null });
|
|
123
|
-
const kept = cost.sustainableHeights({
|
|
124
|
-
heights: [1080, 480],
|
|
125
|
-
sourceWidth: 1920,
|
|
126
|
-
sourceHeight: 1080,
|
|
127
|
-
fps: 24,
|
|
128
|
-
source: null,
|
|
129
|
-
// The base copies its picture, so the source height costs no encoder.
|
|
130
|
-
transcodeVideo: false,
|
|
131
|
-
ownHeight: 1080,
|
|
132
|
-
measuredHeights: new Map([[480, 0.4]])
|
|
133
|
-
});
|
|
134
|
-
assert.deepEqual(kept, [1080], "the copy stays; the rung seen failing does not");
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
test("what an encoder taught this host is learned here, and held here", () => {
|
|
138
|
-
// The three that learn were methods of the session manager, writing into maps
|
|
139
|
-
// this object owned — one keeper, a different author, and the maps public for
|
|
140
|
-
// it. Nothing outside can write a price now, and nothing outside needs to.
|
|
141
|
-
const { cost } = costOn();
|
|
142
|
-
|
|
143
|
-
assert.equal(typeof cost.learnFrom, "function");
|
|
144
|
-
assert.equal(cost.copyCost, undefined, "the store is not the surface");
|
|
145
|
-
assert.equal(cost.decodeCostFor("torrent:abc:0"), null, "and nothing is known until an encoder says so");
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
test("a copy is priced from what it was seen doing", async () => {
|
|
149
|
-
const { cost } = costOn();
|
|
150
|
-
const session = sessionProducing({ transcodeVideo: false });
|
|
151
|
-
|
|
152
|
-
await watchItRun(cost, session, 8);
|
|
153
|
-
|
|
154
|
-
assert.equal(cost.copyVersionFor("torrent:abc:0"), 1, "the price is published, so every offer recomputes");
|
|
155
|
-
assert.equal(
|
|
156
|
-
qualityStateOf(session).lastAloneSpeed.toFixed(2),
|
|
157
|
-
"8.00",
|
|
158
|
-
"and what it did with the machine to itself is on the session, which withdraws a step seen failing"
|
|
159
|
-
);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
test("the time a run's input waited for the swarm is not in the price of this host", async () => {
|
|
163
|
-
// Field 2026-10-01: a copy waited 32.93 s and 43.91 s for two pieces and read
|
|
164
|
-
// 0.21x over the clock. Over its own working time it is the machine's speed.
|
|
165
|
-
const { cost } = costOn();
|
|
166
|
-
const session = sessionProducing({ transcodeVideo: false });
|
|
167
|
-
|
|
168
|
-
await watchItRun(cost, session, 8, 9_000);
|
|
169
|
-
|
|
170
|
-
assert.equal(qualityStateOf(session).lastAloneSpeed, 8, "the speed is the run's own, not the swarm's");
|
|
171
|
-
assert.equal(cost.copyVersionFor("torrent:abc:0"), 1, "and it is filed as what copying costs here");
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
test("a picture's reading taken beside another encoder is not filed", async () => {
|
|
175
|
-
// It contains that other encoder's work, and the budget ADDS the same work
|
|
176
|
-
// again when it predicts — so the price is counted twice and grows with every
|
|
177
|
-
// reading. Measured in the field 2026-08-15: copying, whose truth is 7.9x, was
|
|
178
|
-
// learned as 2.03x, and the quality menu collapsed to one height.
|
|
179
|
-
const { cost } = costOn({ runningEncoders: () => 2 });
|
|
180
|
-
const session = sessionProducing({ transcodeVideo: false });
|
|
181
|
-
|
|
182
|
-
await watchItRun(cost, session, 8);
|
|
183
|
-
|
|
184
|
-
assert.equal(cost.copyVersionFor("torrent:abc:0"), 0);
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
test("a run that has measured nothing teaches nothing", async () => {
|
|
188
|
-
const { cost } = costOn();
|
|
189
|
-
const session = sessionProducing({ transcodeVideo: false });
|
|
190
|
-
const run = [...session.runs][0];
|
|
191
|
-
|
|
192
|
-
await cost.learnFrom(session, run);
|
|
193
|
-
|
|
194
|
-
assert.equal(run.speedReading, null, "a speed needs two closed pieces of one run");
|
|
195
|
-
assert.equal(cost.copyVersionFor("torrent:abc:0"), 0);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
test("a restarted run starts its own reckoning, so a seek is never read as speed", async () => {
|
|
199
|
-
// Twenty minutes of film against five seconds of work is a seek, and filed as
|
|
200
|
-
// a price it admits every step there is. The reading belongs to the run that
|
|
201
|
-
// made it, so a new run at another place has none until its own pieces close.
|
|
202
|
-
const { cost } = costOn();
|
|
203
|
-
const session = sessionProducing({ transcodeVideo: false });
|
|
204
|
-
session.runs = new Set();
|
|
205
|
-
const moved = startRunOn(session, { from: 300, clock: session.clock });
|
|
206
|
-
|
|
207
|
-
await cost.learnFrom(session, moved);
|
|
208
|
-
|
|
209
|
-
assert.equal(cost.copyVersionFor("torrent:abc:0"), 0);
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
test("two soundtracks of one file are priced apart", async () => {
|
|
213
|
-
// The key names the TRACK, not the file. A dub and the original run at
|
|
214
|
-
// different speeds — different codecs, different channel counts — and one key
|
|
215
|
-
// for both files each over the other's readings.
|
|
216
|
-
const { cost } = costOn();
|
|
217
|
-
const first = sessionProducing({ audioOnly: true });
|
|
218
|
-
const second = sessionProducing({ audioOnly: true, audioSourceTrackIndex: 1 });
|
|
219
|
-
|
|
220
|
-
await watchItRun(cost, first, 40);
|
|
221
|
-
|
|
222
|
-
assert.equal(cost.audioVersionFor(first), 1);
|
|
223
|
-
assert.equal(cost.audioVersionFor(second), 0, "the other track has been told nothing about itself");
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
test("a soundtrack's own share is what is left after everything else running is paid for", async () => {
|
|
227
|
-
// A rendition runs for exactly as long as the picture it accompanies, so
|
|
228
|
-
// "alone" is a state it is never in and the price stayed unmeasured for ever.
|
|
229
|
-
// Its share is recovered by subtracting what the machine is already known to
|
|
230
|
-
// be spending — which is only possible once that other work HAS a price.
|
|
231
|
-
const picture = sessionProducing({ transcodeVideo: false });
|
|
232
|
-
const sound = sessionProducing({ audioOnly: true });
|
|
233
|
-
let running = 1;
|
|
234
|
-
const { cost } = costOn({
|
|
235
|
-
runningEncoders: () => running,
|
|
236
|
-
familyOf: () => [picture, sound]
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
// The picture alone first, so copying has a price: 8x, i.e. 0.125 s/s.
|
|
240
|
-
await watchItRun(cost, picture, 8);
|
|
241
|
-
assert.equal(cost.copyVersionFor("torrent:abc:0"), 1);
|
|
242
|
-
|
|
243
|
-
// Now the pair. The reading off the soundtrack contains both, at 4x — 0.25 s/s
|
|
244
|
-
// together — so the sound's own share is 0.25 − 0.125 = 0.125 s/s.
|
|
245
|
-
running = 2;
|
|
246
|
-
await watchItRun(cost, sound, 4);
|
|
247
|
-
|
|
248
|
-
assert.equal(cost.audioVersionFor(sound), 1, "the soundtrack has a price at last");
|
|
249
|
-
// What was filed, read back the way the product reads it: the sum of what
|
|
250
|
-
// everything OTHER than the picture is costing. Charged the whole reading
|
|
251
|
-
// instead of its own share, a soundtrack is priced at twice the truth and
|
|
252
|
-
// refuses quality steps the host could hold.
|
|
253
|
-
running = 1;
|
|
254
|
-
assert.equal(
|
|
255
|
-
cost.pricedConcurrentCost(picture).toFixed(3),
|
|
256
|
-
"0.125",
|
|
257
|
-
"0.25 s/s together, less the 0.125 s/s the copy was already known to cost"
|
|
258
|
-
);
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
test("nothing is attributed to a soundtrack while something running has no price", async () => {
|
|
262
|
-
const picture = sessionProducing({ transcodeVideo: true });
|
|
263
|
-
const sound = sessionProducing({ audioOnly: true });
|
|
264
|
-
const { cost } = costOn({
|
|
265
|
-
runningEncoders: () => 2,
|
|
266
|
-
// A re-encoded picture with no benchmark to price it by and no speed of its
|
|
267
|
-
// own: unpriced work, which would otherwise land in the soundtrack's account
|
|
268
|
-
// and refuse quality steps on it.
|
|
269
|
-
familyOf: () => [picture, sound]
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
await watchItRun(cost, sound, 4);
|
|
273
|
-
|
|
274
|
-
assert.equal(cost.audioVersionFor(sound), 0);
|
|
275
|
-
});
|
package/test/encode-exit.test.js
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file The four things an ffmpeg exit can mean, by their field cases.
|
|
3
|
-
*
|
|
4
|
-
* Each test names the release the classification cost. None of them needs a
|
|
5
|
-
* process: what went wrong in every case was the reading of the exit, not the
|
|
6
|
-
* handling of it.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import assert from "node:assert/strict";
|
|
10
|
-
import test from "node:test";
|
|
11
|
-
|
|
12
|
-
import { ENCODE_EXIT, classifyEncodeExit } from "../services/encode/encode-exit.js";
|
|
13
|
-
|
|
14
|
-
test("the predecessor a seek kills says nothing about the session", () => {
|
|
15
|
-
// The handler decides "is this exit mine" by comparing against the session's
|
|
16
|
-
// current process — and during a restart that field still names the process
|
|
17
|
-
// being killed, because the replacement is spawned afterwards. So a SIGKILLed
|
|
18
|
-
// predecessor passed the check and was read as the session's own run dying:
|
|
19
|
-
// a spurious "failed" that a segment request landing in that window is
|
|
20
|
-
// answered 500 for, and on a hardware host a permanent downgrade to libx264.
|
|
21
|
-
assert.equal(
|
|
22
|
-
classifyEncodeExit({ superseded: true, code: null, inputUnavailable: false }),
|
|
23
|
-
ENCODE_EXIT.IGNORED
|
|
24
|
-
);
|
|
25
|
-
// Including when it looks like a clean finish.
|
|
26
|
-
assert.equal(classifyEncodeExit({ superseded: true, code: 0 }), ENCODE_EXIT.IGNORED);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test("exit 0 short of the last segment is a failure, not a finished file", () => {
|
|
30
|
-
// 2.9.104. A run that had made 188 segments of 624 reported itself complete,
|
|
31
|
-
// the player consumed what was on disk and froze on the first segment nobody
|
|
32
|
-
// was making.
|
|
33
|
-
assert.equal(
|
|
34
|
-
classifyEncodeExit({ code: 0, producedThrough: 188, lastSegmentIndex: 623 }),
|
|
35
|
-
ENCODE_EXIT.SHORT
|
|
36
|
-
);
|
|
37
|
-
assert.equal(
|
|
38
|
-
classifyEncodeExit({ code: 0, producedThrough: 623, lastSegmentIndex: 623 }),
|
|
39
|
-
ENCODE_EXIT.COMPLETE
|
|
40
|
-
);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test("exit 0 is complete when there is nothing to check it against", () => {
|
|
44
|
-
// No published last segment, or nothing readable on disk: the claim cannot be
|
|
45
|
-
// contradicted, so it stands. Guessing "short" here would fail every session
|
|
46
|
-
// whose directory could not be read.
|
|
47
|
-
assert.equal(classifyEncodeExit({ code: 0, producedThrough: null, lastSegmentIndex: 623 }), ENCODE_EXIT.COMPLETE);
|
|
48
|
-
assert.equal(classifyEncodeExit({ code: 0, producedThrough: 12, lastSegmentIndex: null }), ENCODE_EXIT.COMPLETE);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test("data that went away is recoverable, and is not an encoder fault", () => {
|
|
52
|
-
// Field 2026-08-06: a torrent evicted mid-seek took the film with it, the run
|
|
53
|
-
// died on `File 0 not found`, and the session answered 500 from then on while
|
|
54
|
-
// the swarm was there and the data would have come back in seconds.
|
|
55
|
-
assert.equal(
|
|
56
|
-
classifyEncodeExit({ code: 1, inputUnavailable: true }),
|
|
57
|
-
ENCODE_EXIT.INPUT_LOST
|
|
58
|
-
);
|
|
59
|
-
// And the distinction is what keeps a working hardware encoder: the caller
|
|
60
|
-
// downgrades to software on FAILED, so an input that vanished must not
|
|
61
|
-
// arrive there.
|
|
62
|
-
assert.notEqual(classifyEncodeExit({ code: 1, inputUnavailable: true }), ENCODE_EXIT.FAILED);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test("anything else is a failure of this target", () => {
|
|
66
|
-
assert.equal(classifyEncodeExit({ code: 1, inputUnavailable: false }), ENCODE_EXIT.FAILED);
|
|
67
|
-
assert.equal(classifyEncodeExit({ code: null, inputUnavailable: false }), ENCODE_EXIT.FAILED);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test("every combination answers, and no input produces undefined", () => {
|
|
71
|
-
for (const superseded of [true, false]) {
|
|
72
|
-
for (const code of [0, 1, null]) {
|
|
73
|
-
for (const producedThrough of [null, 0, 10]) {
|
|
74
|
-
for (const lastSegmentIndex of [null, 0, 10]) {
|
|
75
|
-
for (const inputUnavailable of [true, false]) {
|
|
76
|
-
const answer = classifyEncodeExit({
|
|
77
|
-
superseded,
|
|
78
|
-
code,
|
|
79
|
-
producedThrough,
|
|
80
|
-
lastSegmentIndex,
|
|
81
|
-
inputUnavailable
|
|
82
|
-
});
|
|
83
|
-
assert.ok(
|
|
84
|
-
Object.values(ENCODE_EXIT).includes(answer),
|
|
85
|
-
`answered ${String(answer)} for ${JSON.stringify({ superseded, code, producedThrough, lastSegmentIndex, inputUnavailable })}`
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
assert.ok(Object.values(ENCODE_EXIT).includes(classifyEncodeExit()));
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test("a run that made nothing has not finished, whatever its exit code", () => {
|
|
96
|
-
// Field 2026-09-11: a run given #541..#541 was handed a start later than its
|
|
97
|
-
// own end, wrote 190 bytes that are not a fragment, and exited zero. Making
|
|
98
|
-
// no segment and being unable to read the directory were the same `null`, and
|
|
99
|
-
// the second reading — "cannot be contradicted, so it stands" — was applied
|
|
100
|
-
// to the first.
|
|
101
|
-
assert.equal(
|
|
102
|
-
classifyEncodeExit({ code: 0, producedThrough: null, producedCount: 0, lastSegmentIndex: 541 }),
|
|
103
|
-
ENCODE_EXIT.SHORT
|
|
104
|
-
);
|
|
105
|
-
// And the case that reading was written for still stands: nothing readable on
|
|
106
|
-
// disk is not a claim that nothing was made.
|
|
107
|
-
assert.equal(
|
|
108
|
-
classifyEncodeExit({ code: 0, producedThrough: null, producedCount: null, lastSegmentIndex: 623 }),
|
|
109
|
-
ENCODE_EXIT.COMPLETE
|
|
110
|
-
);
|
|
111
|
-
});
|