@torrent-tv/proxy 2.89.16 → 2.89.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -3
- package/package.json +10 -2
- package/routes/api/client-logs/post.js +9 -2
- package/server.js +0 -11
- package/.claude/commands/opsx/apply.md +0 -155
- package/.claude/commands/opsx/archive.md +0 -160
- package/.claude/commands/opsx/explore.md +0 -174
- package/.claude/commands/opsx/propose.md +0 -109
- package/.claude/commands/opsx/sync.md +0 -143
- package/.claude/skills/openspec-apply-change/SKILL.md +0 -159
- package/.claude/skills/openspec-archive-change/SKILL.md +0 -117
- package/.claude/skills/openspec-explore/SKILL.md +0 -289
- package/.claude/skills/openspec-propose/SKILL.md +0 -113
- package/.claude/skills/openspec-sync-specs/SKILL.md +0 -147
- package/.dockerignore +0 -5
- package/AGENTS.md +0 -363
- package/CHANGELOG.md +0 -2373
- package/Dockerfile +0 -30
- package/biome.json +0 -412
- package/corpus.tmp.json +0 -1
- package/docs/browser-proxy-contract.md +0 -155
- package/docs/container-architecture.md +0 -273
- package/docs/download-architecture.md +0 -261
- package/docs/encode-architecture.md +0 -765
- package/docs/encode-run-state.md +0 -96
- package/docs/logs.md +0 -138
- package/docs/playback-start-forecast.md +0 -113
- package/docs/socks5-egress-research.md +0 -114
- package/docs/storage-architecture.md +0 -273
- package/knip.json +0 -14
- package/main2.cpuprofile +0 -1
- package/openspec/changes/adaptive-bitrate/.openspec.yaml +0 -0
- package/openspec/changes/adaptive-bitrate/design.md +0 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +0 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +0 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +0 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +0 -2
- package/openspec/changes/binary-distribution/proposal.md +0 -53
- package/openspec/changes/chunked-request-bodies/design.md +0 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +0 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +0 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +0 -40
- package/openspec/changes/cold-start/design.md +0 -98
- package/openspec/changes/cold-start/proposal.md +0 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +0 -47
- package/openspec/changes/cold-start/tasks.md +0 -48
- package/openspec/changes/disk-cap/.openspec.yaml +0 -2
- package/openspec/changes/disk-cap/proposal.md +0 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +0 -25
- package/openspec/changes/disk-cap/tasks.md +0 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +0 -2
- package/openspec/changes/proxy-observability/design.md +0 -38
- package/openspec/changes/proxy-observability/proposal.md +0 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +0 -33
- package/openspec/changes/proxy-observability/tasks.md +0 -21
- package/openspec/changes/seek-debounce/.openspec.yaml +0 -0
- package/openspec/changes/seek-debounce/design.md +0 -118
- package/openspec/changes/seek-debounce/proposal.md +0 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +0 -41
- package/openspec/changes/seek-debounce/tasks.md +0 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +0 -2
- package/openspec/changes/subtitle-language/proposal.md +0 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +0 -34
- package/openspec/changes/subtitle-language/tasks.md +0 -26
- package/openspec/changes/track-selection/.openspec.yaml +0 -2
- package/openspec/changes/track-selection/design.md +0 -47
- package/openspec/changes/track-selection/proposal.md +0 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +0 -41
- package/openspec/changes/track-selection/tasks.md +0 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +0 -2
- package/openspec/changes/transcode-quality/proposal.md +0 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +0 -139
- package/openspec/changes/transcode-quality/tasks.md +0 -63
- package/openspec/config.yaml +0 -38
- package/public/.well-known/appspecific/com.chrome.devtools.json +0 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +0 -329
- package/research/double-spawn-2026-09-10.md +0 -171
- package/research/handover-2026-09-13.md +0 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +0 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +0 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +0 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +0 -225
- package/research/valgrind-replica-2026-08-27/logs.tar.gz +0 -0
- package/scripts/build-audio-samples.mjs +0 -31
- package/scripts/render-run-graph.js +0 -173
- package/test/asset-paths.test.js +0 -28
- package/test/audio-buffer-continuity.test.js +0 -77
- package/test/audio-calibration.test.js +0 -23
- package/test/audio-inventory.test.js +0 -212
- package/test/audio-track-bitrate.test.js +0 -40
- package/test/audio-warm-recorded.test.js +0 -63
- package/test/auto-quality-step.test.js +0 -762
- package/test/available-share.test.js +0 -76
- package/test/background-fill.test.js +0 -190
- package/test/client-log-file.test.js +0 -126
- package/test/coalesce.test.js +0 -39
- package/test/completed-files.test.js +0 -115
- package/test/concurrent-cost.test.js +0 -148
- package/test/container-media-info.test.js +0 -228
- package/test/contention.test.js +0 -76
- package/test/core-dumps.test.js +0 -57
- package/test/coverage-follows-the-disk.test.js +0 -233
- package/test/coverage-map.test.js +0 -195
- package/test/cue-push-carries-what-the-walk-found.test.js +0 -58
- package/test/cut-times-timeline.test.js +0 -65
- package/test/cuts-follow-published-grid.test.js +0 -109
- package/test/data-channel-frame.test.js +0 -63
- package/test/data-channel-viewer-lifecycle.test.js +0 -193
- package/test/dead-torrent-handle.test.js +0 -52
- package/test/declared-rates.test.js +0 -116
- package/test/declared-tracks.test.js +0 -43
- package/test/decode-cost-fit.test.js +0 -202
- package/test/decode-cost.test.js +0 -475
- package/test/delivery-probe-field.test.js +0 -173
- package/test/delivery-probe.test.js +0 -382
- package/test/demand-register.test.js +0 -206
- package/test/dht-bootstrap.test.js +0 -72
- package/test/drain-threat.test.js +0 -65
- package/test/duplicate-add.test.js +0 -48
- package/test/encode-admission.test.js +0 -170
- package/test/encode-cost.test.js +0 -275
- package/test/encode-exit.test.js +0 -111
- package/test/encode-orchestrator.test.js +0 -426
- package/test/encode-plan-viewers.test.js +0 -725
- package/test/encode-plan.test.js +0 -545
- package/test/encode-run-state.test.js +0 -314
- package/test/encode-run.test.js +0 -248
- package/test/encode-slope.test.js +0 -46
- package/test/encoded-output.test.js +0 -39
- package/test/encoder-kinds.test.js +0 -122
- package/test/encoder-readings.test.js +0 -68
- package/test/episode-naming.test.js +0 -131
- package/test/every-claimant-is-told-its-share.test.js +0 -152
- package/test/failed-encoder-still-serves-what-exists.test.js +0 -354
- package/test/failed-start-breaker.test.js +0 -202
- package/test/fastest-wires.test.js +0 -143
- package/test/ffmpeg-stream-counts.test.js +0 -101
- package/test/file-edges.test.js +0 -191
- package/test/files-in-use.test.js +0 -34
- package/test/fixtures/audio-frame-boundary.json +0 -1
- package/test/fixtures/refused-audio-20261002.json +0 -10
- package/test/fmp4-presentation-clock.test.js +0 -148
- package/test/fmp4-self-contained.test.js +0 -80
- package/test/fmp4-timestamps.test.js +0 -140
- package/test/fmp4-track-completeness.test.js +0 -41
- package/test/format-keyed-outputs.test.js +0 -78
- package/test/health-metrics.test.js +0 -36
- package/test/held-read-ownership.test.js +0 -77
- package/test/held-request-width.test.js +0 -157
- package/test/helpers/encode-run.js +0 -248
- package/test/helpers/held-file.js +0 -77
- package/test/helpers/manager.js +0 -62
- package/test/helpers/matroska-file.js +0 -296
- package/test/helpers/output-spec.js +0 -63
- package/test/host-load.test.js +0 -68
- package/test/index-accuracy-is-about-the-file.test.js +0 -128
- package/test/init-compat.test.js +0 -219
- package/test/input-lost-quiets-the-plan.test.js +0 -261
- package/test/input-unavailable.test.js +0 -48
- package/test/installation-id.test.js +0 -39
- package/test/keeping-period.test.js +0 -83
- package/test/keyframe-index-accuracy.test.js +0 -188
- package/test/keyframe-table-budget.test.js +0 -123
- package/test/keyframes-belong-to-the-file.test.js +0 -249
- package/test/learned-median.test.js +0 -37
- package/test/letting-go-asks-who-is-watching.test.js +0 -191
- package/test/limit-row-by-area.test.js +0 -60
- package/test/link-budget.test.js +0 -111
- package/test/link-probe.test.js +0 -56
- package/test/local-encoding-data.test.js +0 -273
- package/test/logger-repeats.test.js +0 -134
- package/test/look-ahead-coverage.test.js +0 -56
- package/test/manager-surface.test.js +0 -291
- package/test/matroska-blocks.test.js +0 -0
- package/test/matroska-cluster-discovery.test.js +0 -323
- package/test/matroska-cues-track.test.js +0 -192
- package/test/media-time.test.js +0 -62
- package/test/memory-budget.test.js +0 -390
- package/test/move-cost.test.js +0 -216
- package/test/mp4-composition-times.test.js +0 -0
- package/test/mp4-subtitles.test.js +0 -173
- package/test/one-authority.test.js +0 -285
- package/test/one-budget.test.js +0 -149
- package/test/one-output-one-session.test.js +0 -273
- package/test/one-resource-divided.test.js +0 -185
- package/test/one-serving-output-per-height.test.js +0 -458
- package/test/orchestrator-wired.test.js +0 -207
- package/test/output-catalog.test.js +0 -39
- package/test/output-shape.test.js +0 -21
- package/test/output-spec.test.js +0 -251
- package/test/output-speed.test.js +0 -92
- package/test/outputs-being-prepared.test.js +0 -40
- package/test/packet-witness-ring.test.js +0 -236
- package/test/packet-witness.test.js +0 -148
- package/test/piece-completeness.test.js +0 -78
- package/test/piece-disk-store.test.js +0 -401
- package/test/piece-from-whole-file.test.js +0 -129
- package/test/piece-lru.test.js +0 -319
- package/test/piece-reader.test.js +0 -162
- package/test/piece-selection-offset.test.js +0 -85
- package/test/piece-store-eviction.test.js +0 -595
- package/test/piece-store-never-refuses.test.js +0 -153
- package/test/piece-store-reservations.test.js +0 -403
- package/test/piece-store-slow-disk.test.js +0 -159
- package/test/piece-tail.test.js +0 -97
- package/test/plan-host-timings.test.js +0 -68
- package/test/plan-is-steady.test.js +0 -225
- package/test/playback-readiness.test.js +0 -518
- package/test/playback-start-regressions.test.js +0 -101
- package/test/priority-map-download.test.js +0 -174
- package/test/priority-map-emptied.test.js +0 -204
- package/test/priority-map-per-output.test.js +0 -215
- package/test/priority-map.test.js +0 -163
- package/test/probe-wedge-certainty.test.js +0 -60
- package/test/produced-copy-choice.test.js +0 -213
- package/test/published-timeline.test.js +0 -83
- package/test/quality-offer.test.js +0 -160
- package/test/quality-variants.test.js +0 -1274
- package/test/rate-control-in-the-output.test.js +0 -181
- package/test/read-survives-withdrawal.test.js +0 -164
- package/test/read-window.test.js +0 -365
- package/test/reads-not-yet-arrived.test.js +0 -218
- package/test/resume-warm.test.js +0 -39
- package/test/run-graph-drift.test.js +0 -37
- package/test/run-inputs.test.js +0 -80
- package/test/run-intervals.test.js +0 -100
- package/test/run-position-follows-published-grid.test.js +0 -89
- package/test/run-progress.test.js +0 -64
- package/test/same-height-switch.test.js +0 -663
- package/test/seek-frees-the-viewers-requests.test.js +0 -217
- package/test/seek-landing.test.js +0 -109
- package/test/seek-target-not-superseded.test.js +0 -195
- package/test/segment-closed-on-both-branches.test.js +0 -114
- package/test/segment-cut-times.test.js +0 -63
- package/test/segment-demand.test.js +0 -80
- package/test/segment-hold-supersede.test.js +0 -91
- package/test/segment-judged-by-its-own-header.test.js +0 -222
- package/test/segment-serve-wiring.test.js +0 -481
- package/test/segment-store-eviction.test.js +0 -247
- package/test/segment-store.test.js +0 -352
- package/test/segment-true-start.test.js +0 -86
- package/test/segments-are-shared.test.js +0 -176
- package/test/send-chunk-bytes.test.js +0 -86
- package/test/serving-existing-output.test.js +0 -111
- package/test/serving-output.test.js +0 -102
- package/test/shared-piece-store.test.js +0 -310
- package/test/sidecar-naming.test.js +0 -142
- package/test/soundtrack-rate-bound.test.js +0 -289
- package/test/source-file.test.js +0 -133
- package/test/source-files-route.test.js +0 -113
- package/test/source-registry.test.js +0 -18
- package/test/split-a-stretch.test.js +0 -139
- package/test/stale-request-after-seek.test.js +0 -158
- package/test/startup-readings.test.js +0 -124
- package/test/stream-route.test.js +0 -237
- package/test/subtitle-cue-framing.test.js +0 -200
- package/test/subtitle-cue-source.test.js +0 -102
- package/test/subtitle-cue-walk.test.js +0 -385
- package/test/subtitle-cursor.test.js +0 -63
- package/test/subtitle-defaults.test.js +0 -118
- package/test/subtitle-language.test.js +0 -252
- package/test/subtitle-metadata.test.js +0 -15
- package/test/subtitle-plan-waits.test.js +0 -125
- package/test/subtitle-track-numbering.test.js +0 -391
- package/test/supply-margin.test.js +0 -190
- package/test/swarm-follows-readers.test.js +0 -174
- package/test/swarm-reach.test.js +0 -130
- package/test/swarm-selection.test.js +0 -220
- package/test/tail-duplication.test.js +0 -167
- package/test/the-channel-has-both-ends.test.js +0 -91
- package/test/the-torrent-thread-serves-bytes.test.js +0 -107
- package/test/thread-pool.test.js +0 -53
- package/test/timeline.test.js +0 -84
- package/test/torrent-contents.test.js +0 -235
- package/test/torrent-cost.test.js +0 -71
- package/test/torrent-files.test.js +0 -221
- package/test/torrent-naming.test.js +0 -255
- package/test/tracks-begin-together.test.js +0 -209
- package/test/tunnel-renewal.test.js +0 -167
- package/test/two-viewers-one-picture.test.js +0 -508
- package/test/upload-hurry.test.js +0 -186
- package/test/usrsctp-state.test.js +0 -131
- package/test/video-facts.test.js +0 -102
- package/test/viewer-assignments.test.js +0 -143
- package/test/viewer-outputs.test.js +0 -285
- package/test/viewer-presence.test.js +0 -116
- package/test/viewer-requests-admission.test.js +0 -92
- package/test/viewer-starts-on-map.test.js +0 -27
- package/test/viewer-states.test.js +0 -192
- package/test/viewer-subtitle-subscription.test.js +0 -96
- package/test/viewer.test.js +0 -139
- package/test/viewing-generations.test.js +0 -390
- package/test/visible-rung.test.js +0 -37
- package/test/wait-ledger.test.js +0 -100
- package/test/webrtc-shim.test.js +0 -149
- package/test/wedge-certainty.test.js +0 -131
- package/test/whole-file-held-read.test.js +0 -30
- package/test/withdraw-piece-claim.test.js +0 -212
- package/test/worker-channel.test.js +0 -158
package/test/decode-cost.test.js
DELETED
|
@@ -1,475 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file What a re-encode costs on this host, decoding included.
|
|
3
|
-
*
|
|
4
|
-
* The budget used to price the encoder alone. Measured on the addon host
|
|
5
|
-
* 2026-08-14, that made it offer a 240p rung it then ran at 0.388-0.947x while
|
|
6
|
-
* claiming the host cleared the bar 2.5 times over — the error on that rung was
|
|
7
|
-
* 209 %. The decode term brings a controlled measurement to within 5 %.
|
|
8
|
-
*
|
|
9
|
-
* WHAT IS ASSERTED HERE IS ARITHMETIC, over constants that were measured once
|
|
10
|
-
* on the addon host and written down. The live benchmark used to run here too —
|
|
11
|
-
* on the reasoning that a fit which only ever meets invented numbers can be
|
|
12
|
-
* wrong in every way that matters — and it could not be asserted about: a clip
|
|
13
|
-
* that "said nothing" under load makes the whole benchmark answer with nothing,
|
|
14
|
-
* so the very first line of that test failed by luck. The orderings it was for
|
|
15
|
-
* are held over the fit itself, deterministically, in `decode-cost-fit.test.js`.
|
|
16
|
-
*
|
|
17
|
-
* What no test covers as a result is the live path — lifting a clip out of its
|
|
18
|
-
* container, feeding it through the pipe, reading the slope. That is integration
|
|
19
|
-
* over real ffmpeg, and it belongs to a stand run before a release rather than
|
|
20
|
-
* to a check that measures whatever else the machine was doing (roadmap item
|
|
21
|
-
* 52).
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
import test from "node:test";
|
|
25
|
-
import assert from "node:assert/strict";
|
|
26
|
-
import { Timeline } from "../services/encode/output/Timeline.js";
|
|
27
|
-
import { createRequire } from "node:module";
|
|
28
|
-
import { spawn } from "node:child_process";
|
|
29
|
-
import { mkdtemp, rm } from "node:fs/promises";
|
|
30
|
-
import { decodeCostOf, decodeFamilyOf } from "../services/encode/decode-cost-fit.js";
|
|
31
|
-
import { Output } from "../services/encode/output/Output.js";
|
|
32
|
-
import { outputSpec } from "./helpers/output-spec.js";
|
|
33
|
-
import os from "node:os";
|
|
34
|
-
import path from "node:path";
|
|
35
|
-
import {
|
|
36
|
-
canSustainOutput,
|
|
37
|
-
decodeSpeedFor,
|
|
38
|
-
predictedRealtimeSpeed,
|
|
39
|
-
speedBar
|
|
40
|
-
} from "../services/encode/hwaccel.js";
|
|
41
|
-
import { wireOutputs } from "../services/server/wire-outputs.js";
|
|
42
|
-
import { SourceFile, sourceDecodeCharacteristics } from "../services/media/SourceFile.js";
|
|
43
|
-
import { parseFfmpegBitrateKbps, parseFfmpegVideoDimensions, parseFfmpegVideoFps } from "../services/media/ffmpeg-banner.js";
|
|
44
|
-
import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
|
|
45
|
-
|
|
46
|
-
const require = createRequire(import.meta.url);
|
|
47
|
-
const ffmpegBin = require("ffmpeg-static");
|
|
48
|
-
const CALIBRATION_DIR = path.join(import.meta.dirname, "..", "assets", "calibration");
|
|
49
|
-
|
|
50
|
-
// The constants measured on the addon host (CM4) on 2026-08-14. Used here as
|
|
51
|
-
// a fixed host, so the arithmetic can be checked against figures that were
|
|
52
|
-
// measured rather than against figures this test invented.
|
|
53
|
-
const ADDON_HOST_MODEL = { pixelTerm: 0.005555, bitrateTerm: 0.00990, constantTerm: 0.0572 };
|
|
54
|
-
// The film measured that day: 1920x1080 at 24 fps, about 8 Mbit/s.
|
|
55
|
-
const MEASURED_FILM = { megapixelsPerSecond: (1920 * 1080 * 24) / 1e6, megabitsPerSecond: 8 };
|
|
56
|
-
|
|
57
|
-
test("decode cost prices the film it was checked against", () => {
|
|
58
|
-
const speed = decodeSpeedFor(ADDON_HOST_MODEL, MEASURED_FILM);
|
|
59
|
-
|
|
60
|
-
// 0.005555 x 49.77 + 0.00990 x 8 + 0.0572 = 0.4129 s per second of video.
|
|
61
|
-
assert.ok(Math.abs(1 / speed - 0.4129) < 0.001, `cost was ${(1 / speed).toFixed(4)} s/s`);
|
|
62
|
-
// Measured that day: 0.434 s/s. The claim is 5 %, not exactness.
|
|
63
|
-
assert.ok(Math.abs(1 / speed - 0.434) / 0.434 < 0.05);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test("decoding and encoding share the machine, so their speeds combine", () => {
|
|
67
|
-
// The rung that broke playback: 240p at 24 fps, encoded at 5.99x by the
|
|
68
|
-
// measurement, decoded at 2.31x. Measured combined speed was 1.48x.
|
|
69
|
-
const outputPixelsPerSec = 426 * 240 * 24;
|
|
70
|
-
const speed = predictedRealtimeSpeed({
|
|
71
|
-
decodeModel: { pixelTerm: 0, bitrateTerm: 0, constantTerm: 1 / 2.31 },
|
|
72
|
-
encodePixelsPerSec: outputPixelsPerSec * 5.99,
|
|
73
|
-
outputPixelsPerSec,
|
|
74
|
-
source: MEASURED_FILM
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
assert.ok(Math.abs(speed - 1.67) < 0.01, `predicted ${speed.toFixed(2)}x`);
|
|
78
|
-
// 1.67 against the 1.48x that rung measured under a controlled encode: 12.8 %
|
|
79
|
-
// out. The figure is pinned so a change to the combination has to state what
|
|
80
|
-
// it does to it.
|
|
81
|
-
assert.ok(Math.abs(speed - 1.48) / 1.48 < 0.13);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test("with no decode fit the prediction is the encoder alone — what it was before", () => {
|
|
85
|
-
const outputPixelsPerSec = 426 * 240 * 24;
|
|
86
|
-
const speed = predictedRealtimeSpeed({
|
|
87
|
-
decodeModel: null,
|
|
88
|
-
encodePixelsPerSec: outputPixelsPerSec * 5.99,
|
|
89
|
-
outputPixelsPerSec,
|
|
90
|
-
source: MEASURED_FILM
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
assert.equal(speed, 5.99, "the old figure, four times the truth on that rung");
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test("a rung is refused when the combined speed is under the bar", () => {
|
|
97
|
-
// The addon host's fastest preset, read from its own log: 11.2 Mpx/s.
|
|
98
|
-
const benchmark = [{ preset: "ultrafast", pixelsPerSec: 11.2e6 }];
|
|
99
|
-
const source = MEASURED_FILM;
|
|
100
|
-
|
|
101
|
-
const heavy = canSustainOutput({
|
|
102
|
-
benchmark,
|
|
103
|
-
decodeModel: ADDON_HOST_MODEL,
|
|
104
|
-
source,
|
|
105
|
-
outputPixelsPerSec: 1280 * 720 * 24
|
|
106
|
-
});
|
|
107
|
-
assert.equal(heavy.sustainable, false, "720p needs 22 Mpx/s from an 11.2 Mpx/s host");
|
|
108
|
-
assert.ok(heavy.speed < 1, `predicted ${heavy.speed.toFixed(2)}x`);
|
|
109
|
-
|
|
110
|
-
// The 240p rung of that film predicts 1.58x on an idle machine, and with
|
|
111
|
-
// nothing known about the swarm the bar is realtime, so it is offered.
|
|
112
|
-
const light = canSustainOutput({
|
|
113
|
-
benchmark,
|
|
114
|
-
decodeModel: ADDON_HOST_MODEL,
|
|
115
|
-
source,
|
|
116
|
-
outputPixelsPerSec: 426 * 240 * 24
|
|
117
|
-
});
|
|
118
|
-
assert.ok(Math.abs(light.speed - 1.58) < 0.01, `predicted ${light.speed.toFixed(2)}x`);
|
|
119
|
-
assert.equal(light.sustainable, true);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
test("the bar is what this file's own supply demands, when it has been measured", () => {
|
|
123
|
-
// The field torrent of 2026-08-17: waits of 1.49 s median arriving every
|
|
124
|
-
// 2.22 s demand 1.67x of any step that is to survive them. The same 240p rung
|
|
125
|
-
// predicted at 1.58x clears realtime and does not clear that.
|
|
126
|
-
const benchmark = [{ preset: "ultrafast", pixelsPerSec: 11.2e6 }];
|
|
127
|
-
const rung = {
|
|
128
|
-
benchmark,
|
|
129
|
-
decodeModel: ADDON_HOST_MODEL,
|
|
130
|
-
source: MEASURED_FILM,
|
|
131
|
-
outputPixelsPerSec: 426 * 240 * 24
|
|
132
|
-
};
|
|
133
|
-
assert.equal(canSustainOutput({ ...rung, requiredSpeed: 1.67 }).sustainable, false);
|
|
134
|
-
assert.equal(canSustainOutput({ ...rung, requiredSpeed: 1.2 }).sustainable, true);
|
|
135
|
-
// A swarm that has not been measured cannot raise the bar, and cannot lower
|
|
136
|
-
// it below realtime either.
|
|
137
|
-
assert.equal(speedBar(null), 1);
|
|
138
|
-
assert.equal(speedBar(0.4), 1);
|
|
139
|
-
assert.equal(speedBar(1.67), 1.67);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test("a reading from the running encoder outranks the model of the clips", () => {
|
|
143
|
-
// The field case, 2026-08-14: the 240p rung of that film ran at 0.95x at its
|
|
144
|
-
// best on a host whose fastest preset benchmarked at 11.2 Mpx/s. Subtracting
|
|
145
|
-
// the encode half of that reading leaves what the SOURCE costs to decode
|
|
146
|
-
// there — 0.83 s per second of video, i.e. 1.20x — which is four times what
|
|
147
|
-
// the H.264 clips predicted for it, and is the truth about this file.
|
|
148
|
-
const benchmark = [{ preset: "ultrafast", pixelsPerSec: 11.2e6 }];
|
|
149
|
-
const observedDecodeCostSec = 1 / 0.95 - (426 * 240 * 24) / 11.2e6;
|
|
150
|
-
assert.ok(Math.abs(observedDecodeCostSec - 0.8335) < 0.001);
|
|
151
|
-
|
|
152
|
-
const withObservation = canSustainOutput({
|
|
153
|
-
benchmark,
|
|
154
|
-
decodeModel: ADDON_HOST_MODEL,
|
|
155
|
-
source: MEASURED_FILM,
|
|
156
|
-
outputPixelsPerSec: 426 * 240 * 24,
|
|
157
|
-
observedDecodeCostSec
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
// The same rung the clip model called 1.58x and admitted is now priced at
|
|
161
|
-
// 0.95x — the speed it was actually seen to run at — and refused.
|
|
162
|
-
assert.ok(Math.abs(withObservation.speed - 0.95) < 0.01, `priced ${withObservation.speed.toFixed(2)}x`);
|
|
163
|
-
assert.equal(withObservation.sustainable, false);
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
test("an observation prices a host whose clips were never fitted", () => {
|
|
167
|
-
// No model at all — clips missing, or a fit rejected. Before an observation
|
|
168
|
-
// nothing can be refused; after one, the same rung is priced and refused.
|
|
169
|
-
const benchmark = [{ preset: "ultrafast", pixelsPerSec: 11.2e6 }];
|
|
170
|
-
const outputPixelsPerSec = 426 * 240 * 24;
|
|
171
|
-
|
|
172
|
-
const unpriced = canSustainOutput({
|
|
173
|
-
benchmark,
|
|
174
|
-
decodeModel: null,
|
|
175
|
-
source: MEASURED_FILM,
|
|
176
|
-
outputPixelsPerSec
|
|
177
|
-
});
|
|
178
|
-
assert.deepEqual(unpriced, { speed: null, sustainable: true });
|
|
179
|
-
|
|
180
|
-
const priced = canSustainOutput({
|
|
181
|
-
benchmark,
|
|
182
|
-
decodeModel: null,
|
|
183
|
-
source: MEASURED_FILM,
|
|
184
|
-
outputPixelsPerSec,
|
|
185
|
-
observedDecodeCostSec: 0.8335
|
|
186
|
-
});
|
|
187
|
-
assert.equal(priced.sustainable, false);
|
|
188
|
-
assert.ok(priced.speed < 1, `priced ${priced.speed?.toFixed(2)}x`);
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
test("not calibrated means nothing judged; calibrated with nothing qualified means refused", () => {
|
|
192
|
-
const uncalibrated = canSustainOutput({
|
|
193
|
-
benchmark: null,
|
|
194
|
-
decodeModel: null,
|
|
195
|
-
source: null,
|
|
196
|
-
outputPixelsPerSec: 1920 * 1080 * 24
|
|
197
|
-
});
|
|
198
|
-
assert.deepEqual(uncalibrated, { speed: null, sustainable: true });
|
|
199
|
-
|
|
200
|
-
const nothingQualified = canSustainOutput({
|
|
201
|
-
benchmark: [],
|
|
202
|
-
decodeModel: null,
|
|
203
|
-
source: null,
|
|
204
|
-
outputPixelsPerSec: 1920 * 1080 * 24
|
|
205
|
-
});
|
|
206
|
-
assert.deepEqual(nothingQualified, { speed: null, sustainable: false });
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
test("a real ffmpeg banner reads into the figures the budget prices", async () => {
|
|
210
|
-
// The chain a session actually runs: ffmpeg prints its banner, the four
|
|
211
|
-
// readers take the four facts out of it, and `sourceDecodeCharacteristics`
|
|
212
|
-
// turns them into the two the fit uses. Run against a real file, because the
|
|
213
|
-
// bitrate reader is new and a banner is the one input nobody can invent
|
|
214
|
-
// faithfully.
|
|
215
|
-
const stderr = await new Promise((resolve) => {
|
|
216
|
-
let text = "";
|
|
217
|
-
const child = spawn(
|
|
218
|
-
ffmpegBin,
|
|
219
|
-
["-hide_banner", "-loglevel", "info", "-i", path.join(CALIBRATION_DIR, "cal-h264-720-hi.mp4"), "-t", "0.1", "-f", "null", "-"],
|
|
220
|
-
{ stdio: ["ignore", "ignore", "pipe"], windowsHide: true }
|
|
221
|
-
);
|
|
222
|
-
child.stderr.on("data", (chunk) => {
|
|
223
|
-
text += String(chunk);
|
|
224
|
-
});
|
|
225
|
-
child.on("close", () => resolve(text));
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
// The VIDEO stream's rate (9940), not the container's (9946). On a clip that
|
|
229
|
-
// carries nothing else the two differ only by container overhead; on a film
|
|
230
|
-
// with two AC-3 tracks they differ by the whole of the audio, and the fit
|
|
231
|
-
// these figures feed was made from clips decoded with `-an`.
|
|
232
|
-
assert.equal(parseFfmpegBitrateKbps(stderr), 9940, "the video stream's own bitrate, in kb/s");
|
|
233
|
-
const dimensions = parseFfmpegVideoDimensions(stderr);
|
|
234
|
-
const figures = sourceDecodeCharacteristics({
|
|
235
|
-
width: dimensions.width,
|
|
236
|
-
height: dimensions.height,
|
|
237
|
-
fps: parseFfmpegVideoFps(stderr),
|
|
238
|
-
bitrateKbps: parseFfmpegBitrateKbps(stderr)
|
|
239
|
-
});
|
|
240
|
-
assert.ok(Math.abs(figures.megapixelsPerSecond - (1280 * 720 * 24) / 1e6) < 0.01);
|
|
241
|
-
assert.ok(Math.abs(figures.megabitsPerSecond - 9.94) < 0.001);
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
test("the video stream's bitrate is preferred, and the output's is never read", () => {
|
|
245
|
-
// A real banner states three rates: the container's, the input video
|
|
246
|
-
// stream's, and — below "Stream mapping:" — the one ffmpeg is about to
|
|
247
|
-
// produce. Only the middle one describes the source.
|
|
248
|
-
const banner = [
|
|
249
|
-
" Duration: 01:31:19.00, start: 0.000000, bitrate: 9500 kb/s",
|
|
250
|
-
" Stream #0:0: Video: h264 (High), yuv420p, 1920x1080, 7800 kb/s, 23.98 fps",
|
|
251
|
-
" Stream #0:1: Audio: ac3, 48000 Hz, 5.1, fltp, 640 kb/s",
|
|
252
|
-
"Stream mapping:",
|
|
253
|
-
" Stream #0:0(und): Video: wrapped_avframe, yuv420p, 1920x1080, q=2-31, 200 kb/s, 24 fps"
|
|
254
|
-
].join("\n");
|
|
255
|
-
assert.equal(parseFfmpegBitrateKbps(banner), 7800);
|
|
256
|
-
|
|
257
|
-
// No per-stream rate: the container's stands in, as it always did.
|
|
258
|
-
const noStreamRate = [
|
|
259
|
-
" Duration: 00:10:00.00, bitrate: 4200 kb/s",
|
|
260
|
-
" Stream #0:0: Video: hevc (Main 10), yuv420p10le, 1920x1080, 23.98 fps"
|
|
261
|
-
].join("\n");
|
|
262
|
-
assert.equal(parseFfmpegBitrateKbps(noStreamRate), 4200);
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
test("a banner with no bitrate reads as no bitrate, not as zero", () => {
|
|
266
|
-
assert.equal(parseFfmpegBitrateKbps("Duration: 00:01:00.00, start: 0.000000\n"), null);
|
|
267
|
-
assert.equal(parseFfmpegBitrateKbps(""), null);
|
|
268
|
-
assert.equal(parseFfmpegBitrateKbps("Duration: 00:01:00.00, start: 0.000, bitrate: 8000 kb/s"), 8000);
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
test("the source's decode figures come off the probe, or not at all", () => {
|
|
272
|
-
assert.deepEqual(sourceDecodeCharacteristics({ width: 1920, height: 1080, fps: 24, bitrateKbps: 8000 }), {
|
|
273
|
-
megapixelsPerSecond: (1920 * 1080 * 24) / 1e6,
|
|
274
|
-
megabitsPerSecond: 8,
|
|
275
|
-
// Which measurement of this host applies. Absent on the probe means absent
|
|
276
|
-
// here — the caller then prices the source as H.264 8-bit, which is what
|
|
277
|
-
// every source was priced as before the model was fitted per family.
|
|
278
|
-
codec: "",
|
|
279
|
-
bitDepth: null
|
|
280
|
-
});
|
|
281
|
-
assert.deepEqual(
|
|
282
|
-
sourceDecodeCharacteristics({ width: 1920, height: 1080, fps: 24, bitrateKbps: 8000, codec: "hevc", bitDepth: 10 }),
|
|
283
|
-
{
|
|
284
|
-
megapixelsPerSecond: (1920 * 1080 * 24) / 1e6,
|
|
285
|
-
megabitsPerSecond: 8,
|
|
286
|
-
codec: "hevc",
|
|
287
|
-
bitDepth: 10
|
|
288
|
-
}
|
|
289
|
-
);
|
|
290
|
-
assert.equal(sourceDecodeCharacteristics({ width: 1920, height: 1080, fps: 24, bitrateKbps: null }), null);
|
|
291
|
-
assert.equal(sourceDecodeCharacteristics(null), null);
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
test("the OFFER drops the rungs the host cannot hold, and the master keeps addressing them", async (t) => {
|
|
295
|
-
const dirPath = await mkdtemp(path.join(os.tmpdir(), "decode-cost-"));
|
|
296
|
-
const manager = wireOutputs({
|
|
297
|
-
enabled: true,
|
|
298
|
-
ffmpegBin: "ffmpeg",
|
|
299
|
-
localBindHost: "127.0.0.1",
|
|
300
|
-
localPort: 9090,
|
|
301
|
-
// A host that can encode a little: 3 Mpx/s, so only the smallest rungs of a
|
|
302
|
-
// 1080p source can be produced faster than they are watched.
|
|
303
|
-
softwarePresetBenchmark: [{ preset: "ultrafast", pixelsPerSec: 3e6 }],
|
|
304
|
-
decodeCostModel: ADDON_HOST_MODEL
|
|
305
|
-
});
|
|
306
|
-
t.after(async () => {
|
|
307
|
-
await manager.lifecycle.disposeAll();
|
|
308
|
-
await rm(dirPath, { recursive: true, force: true });
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
const session = {
|
|
312
|
-
id: "ccccccccddddeeee",
|
|
313
|
-
spec: outputSpec({ transcodeVideo: false, cutGrid: "keyframe" }),
|
|
314
|
-
dirPath,
|
|
315
|
-
// Where this file is cut, held by the file. A fixture that stated it
|
|
316
|
-
// on the session was describing what production no longer does.
|
|
317
|
-
timeline: new Timeline({
|
|
318
|
-
boundaries: Array.from({ length: 101 }, (_, index) => index * 4),
|
|
319
|
-
cutGrid: "keyframe"
|
|
320
|
-
}),
|
|
321
|
-
state: "ready",
|
|
322
|
-
// The file's own facts: the picture's size, and what decoding it costs,
|
|
323
|
-
// which is derived from them rather than stated beside them. These are the
|
|
324
|
-
// measured film of MEASURED_FILM above — 1080p24 at 8 Mbit/s.
|
|
325
|
-
file: new SourceFile({ sourceKey: "source-1", fileIndex: 0, name: "video.mkv" }).learn({
|
|
326
|
-
width: 1920,
|
|
327
|
-
height: 1080,
|
|
328
|
-
fps: 24,
|
|
329
|
-
bitrateKbps: 8000
|
|
330
|
-
}),
|
|
331
|
-
// An ordinary session reads its own file, and its sound is inside it. The
|
|
332
|
-
// three differ only for a soundtrack shipped as a file of its own.
|
|
333
|
-
get inputFile() { return this.file; },
|
|
334
|
-
get audioFile() { return this.file; },
|
|
335
|
-
startedAt: Date.now(),
|
|
336
|
-
lastAccessedAt: Date.now(),
|
|
337
|
-
ffmpeg: null,
|
|
338
|
-
lastError: "",
|
|
339
|
-
consumers: new Set(),
|
|
340
|
-
segmentFormat: fmp4Format,
|
|
341
|
-
transcodeVideo: false,
|
|
342
|
-
transcodeAudio: true,
|
|
343
|
-
audioTrackIndex: 0,
|
|
344
|
-
// A copy can only be cut where the source already has a keyframe, and a
|
|
345
|
-
// master is offered only when that grid is real.
|
|
346
|
-
// The shape this output is encoded AS, decided once for the output.
|
|
347
|
-
output: new Output({
|
|
348
|
-
encodeWidth: 0,
|
|
349
|
-
encodeHeight: 0,
|
|
350
|
-
outputFps: 24,
|
|
351
|
-
softwarePreset: null,
|
|
352
|
-
applyTonemap: false
|
|
353
|
-
}),
|
|
354
|
-
usesExplicitCuts: true,
|
|
355
|
-
useSyntheticPlaylist: true,
|
|
356
|
-
playlistText: "#EXTM3U\n",
|
|
357
|
-
segmentCount: 100,
|
|
358
|
-
progress: { state: "running", processedSeconds: 0, startPositionSeconds: 0 }
|
|
359
|
-
};
|
|
360
|
-
manager.outputs.set(session.id, session);
|
|
361
|
-
|
|
362
|
-
assert.deepEqual(
|
|
363
|
-
manager.quality.offeredHeights(session),
|
|
364
|
-
[1080],
|
|
365
|
-
"every rung under the copy runs below realtime here, so there is nothing to switch to"
|
|
366
|
-
);
|
|
367
|
-
// The master is NOT that answer. It says which rungs can be spliced onto this
|
|
368
|
-
// cut grid, which is a fact about the file, and it has to hold still for the
|
|
369
|
-
// session's life: the browser is handed its address at creation, and a live
|
|
370
|
-
// figure that withdrew it left a session answering 404 to itself (field
|
|
371
|
-
// 2026-08-18, "Moana (2016).mkv" — nothing played at all).
|
|
372
|
-
const weakMaster = manager.renditions.buildMasterPlaylist(session.id);
|
|
373
|
-
assert.ok(weakMaster, "published once, whatever the host is managing this second");
|
|
374
|
-
assert.deepEqual(
|
|
375
|
-
[...weakMaster.matchAll(/^v\/(\d+)\/index\.m3u8$/gm)].map((match) => Number(match[1])),
|
|
376
|
-
[1080, 720, 540, 480, 360, 240],
|
|
377
|
-
"the ladder of the source, addressable — the menu the viewer sees is offeredHeights"
|
|
378
|
-
);
|
|
379
|
-
|
|
380
|
-
// A host with a little more encoder keeps the rungs it can actually hold. A
|
|
381
|
-
// second host, because the encoder's measured modes are a fact the host is
|
|
382
|
-
// started with — read through the encoder in use, never assigned later — and
|
|
383
|
-
// a second session on it, because the answer is settled once per session.
|
|
384
|
-
const strongerHost = wireOutputs({
|
|
385
|
-
enabled: true,
|
|
386
|
-
ffmpegBin: "ffmpeg",
|
|
387
|
-
localBindHost: "127.0.0.1",
|
|
388
|
-
localPort: 9090,
|
|
389
|
-
softwarePresetBenchmark: [{ preset: "ultrafast", pixelsPerSec: 12e6 }],
|
|
390
|
-
decodeCostModel: ADDON_HOST_MODEL
|
|
391
|
-
});
|
|
392
|
-
t.after(() => strongerHost.lifecycle.disposeAll());
|
|
393
|
-
const stronger = { ...session, id: "ddddddddeeeeffff", offeredHeightsCache: undefined };
|
|
394
|
-
strongerHost.outputs.set(stronger.id, stronger);
|
|
395
|
-
assert.deepEqual(
|
|
396
|
-
strongerHost.quality.offeredHeights(stronger),
|
|
397
|
-
[1080, 360, 240],
|
|
398
|
-
"nothing is known about this swarm, so the bar is realtime"
|
|
399
|
-
);
|
|
400
|
-
const master = strongerHost.renditions.buildMasterPlaylist(stronger.id);
|
|
401
|
-
assert.ok(master, "1080p copied plus every rung that can be spliced beside it");
|
|
402
|
-
assert.deepEqual(
|
|
403
|
-
[...master.matchAll(/^v\/(\d+)\/index\.m3u8$/gm)].map((match) => Number(match[1])),
|
|
404
|
-
[1080, 720, 540, 480, 360, 240],
|
|
405
|
-
"the same published set as before: what the host manages is the offer's business, not the document's"
|
|
406
|
-
);
|
|
407
|
-
|
|
408
|
-
// The same host, once the reader has measured what this file's supply
|
|
409
|
-
// demands: waits arriving as they did on the field torrent of 2026-08-17 ask
|
|
410
|
-
// 1.67x of any step, and the rungs that only just cleared realtime go.
|
|
411
|
-
// What the supply demands is a fact of the FILE, measured by its reader
|
|
412
|
-
// (1 + worst wait 1.49 s / median interval 2.22 s, twelve waits), and it is
|
|
413
|
-
// asked of the host's load by the file — not read off a session.
|
|
414
|
-
strongerHost.hostLoad.requiredSpeedFor = () => 1.67;
|
|
415
|
-
const onAThinSwarm = {
|
|
416
|
-
...stronger,
|
|
417
|
-
id: "eeeeeeeeffff0000",
|
|
418
|
-
offeredHeightsCache: undefined
|
|
419
|
-
};
|
|
420
|
-
strongerHost.outputs.set(onAThinSwarm.id, onAThinSwarm);
|
|
421
|
-
assert.deepEqual(
|
|
422
|
-
strongerHost.quality.offeredHeights(onAThinSwarm),
|
|
423
|
-
[1080],
|
|
424
|
-
"the copied height costs no encoder and stays; nothing re-encoded survives that supply"
|
|
425
|
-
);
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
test("a source is priced by its own codec family when that family was measured", () => {
|
|
429
|
-
// A model as the startup benchmark now returns it: H.264 terms at the top
|
|
430
|
-
// level, for a caller that knows nothing about codecs, and the measured
|
|
431
|
-
// families beside them.
|
|
432
|
-
const model = {
|
|
433
|
-
pixelTerm: 0.006, bitrateTerm: 0.012, constantTerm: 0,
|
|
434
|
-
families: {
|
|
435
|
-
h264: { pixelTerm: 0.006, bitrateTerm: 0.012, constantTerm: 0 },
|
|
436
|
-
hevc: { pixelTerm: 0.011, bitrateTerm: 0.020, constantTerm: 0 },
|
|
437
|
-
hevc10: { pixelTerm: 0.017, bitrateTerm: 0.026, constantTerm: 0 }
|
|
438
|
-
}
|
|
439
|
-
};
|
|
440
|
-
const rates = { megapixelsPerSecond: 50, megabitsPerSecond: 9 };
|
|
441
|
-
const asH264 = decodeCostOf(model, { ...rates, codec: "h264", bitDepth: 8 });
|
|
442
|
-
const asHevc = decodeCostOf(model, { ...rates, codec: "hevc", bitDepth: 8 });
|
|
443
|
-
const asHevc10 = decodeCostOf(model, { ...rates, codec: "hevc", bitDepth: 10 });
|
|
444
|
-
assert.equal(Number(asH264.toFixed(4)), 0.408);
|
|
445
|
-
assert.equal(Number(asHevc.toFixed(4)), 0.73);
|
|
446
|
-
assert.equal(Number(asHevc10.toFixed(4)), 1.084);
|
|
447
|
-
// The whole point: the same file costs more as HEVC than as H.264, and more
|
|
448
|
-
// again at ten bits. A single fit could not say that.
|
|
449
|
-
assert.ok(asHevc > asH264 && asHevc10 > asHevc);
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
test("a family with no clips is priced as H.264, and a model with no families still works", () => {
|
|
453
|
-
const withFamilies = {
|
|
454
|
-
pixelTerm: 0.006, bitrateTerm: 0.012, constantTerm: 0,
|
|
455
|
-
families: { h264: { pixelTerm: 0.006, bitrateTerm: 0.012, constantTerm: 0 } }
|
|
456
|
-
};
|
|
457
|
-
const rates = { megapixelsPerSecond: 50, megabitsPerSecond: 9 };
|
|
458
|
-
// AV1 has no set of its own yet.
|
|
459
|
-
assert.equal(
|
|
460
|
-
decodeCostOf(withFamilies, { ...rates, codec: "av1", bitDepth: 8 }),
|
|
461
|
-
decodeCostOf(withFamilies, { ...rates, codec: "h264", bitDepth: 8 })
|
|
462
|
-
);
|
|
463
|
-
// And a flat model — every model before this release — is unchanged.
|
|
464
|
-
const flat = { pixelTerm: 0.006, bitrateTerm: 0.012, constantTerm: 0 };
|
|
465
|
-
assert.equal(decodeCostOf(flat, { ...rates, codec: "hevc", bitDepth: 10 }), 0.408);
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
test("the family is chosen by codec and depth, and unknown names fall to H.264", () => {
|
|
469
|
-
assert.equal(decodeFamilyOf({ codec: "hevc", bitDepth: 8 }), "hevc");
|
|
470
|
-
assert.equal(decodeFamilyOf({ codec: "HEVC", bitDepth: 10 }), "hevc10");
|
|
471
|
-
assert.equal(decodeFamilyOf({ codec: "h265", bitDepth: 12 }), "hevc10");
|
|
472
|
-
assert.equal(decodeFamilyOf({ codec: "h264", bitDepth: 10 }), "h264");
|
|
473
|
-
assert.equal(decodeFamilyOf({ codec: "vc1", bitDepth: null }), "h264");
|
|
474
|
-
assert.equal(decodeFamilyOf({}), "h264");
|
|
475
|
-
});
|
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
import test from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
|
|
4
|
-
import { createDeliveryProbe, PROBE_INTERVAL_MS } from "../services/transport/delivery-probe.js";
|
|
5
|
-
|
|
6
|
-
// The probe service run end to end, against channels that only record what is
|
|
7
|
-
// sent and a peer whose echoes this file writes. The shapes are the field's of
|
|
8
|
-
// 2026-09-28 (research/stalled-channel-recovery-2026-10-04.md in meta): two
|
|
9
|
-
// connections on one proxy, one healthy and idle, one wedged.
|
|
10
|
-
|
|
11
|
-
const LABELS = ["proxy", "proxy-control", "proxy-fast"];
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* A connection with three channels and a peer that echoes on its own cadence.
|
|
15
|
-
*
|
|
16
|
-
* `peer.delivering` decides whether the probes reach the far end; while it is
|
|
17
|
-
* false the far end goes on echoing — the reverse direction keeps working in a
|
|
18
|
-
* wedge — but the newest number it has seen stays where it stopped.
|
|
19
|
-
*
|
|
20
|
-
* @param {import("node:test").TestContext} t
|
|
21
|
-
* @param {{ rttMs?: number }} [options]
|
|
22
|
-
*/
|
|
23
|
-
function connection(t, { rttMs = 6 } = {}) {
|
|
24
|
-
t.mock.timers.enable({ apis: ["setInterval", "Date"], now: 0 });
|
|
25
|
-
const lines = [];
|
|
26
|
-
const captures = [];
|
|
27
|
-
const probe = createDeliveryProbe({
|
|
28
|
-
log: (line) => lines.push(line),
|
|
29
|
-
readDelivery: () => ({ bytesPerSecond: 3_000_000, rttMs }),
|
|
30
|
-
getTransportSnapshot: () => ({ remote: "peer" }),
|
|
31
|
-
witness: {
|
|
32
|
-
maybeCapture: (trigger) => {
|
|
33
|
-
captures.push({ at: Date.now(), trigger });
|
|
34
|
-
return true;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
// The far end's own clock and how long a probe takes to become a fact there:
|
|
39
|
-
// half the crossing, and the page handling the message. The page reports
|
|
40
|
-
// when it saw each probe and when it sent the report, on its own clock — the
|
|
41
|
-
// timestamps the verdict used to turn into a one-way delay.
|
|
42
|
-
const PEER_CLOCK_OFFSET_MS = 37;
|
|
43
|
-
const HANDLING_MS = 4;
|
|
44
|
-
const peer = { delivering: true, seen: 0, seenAt: 0, channelBytes: 0, loopLagMs: 0, visibility: "visible" };
|
|
45
|
-
const channels = LABELS.map((label) => ({
|
|
46
|
-
label,
|
|
47
|
-
sendMessage(message) {
|
|
48
|
-
const { seq, sentAt } = JSON.parse(message);
|
|
49
|
-
if (peer.delivering && seq > peer.seen) {
|
|
50
|
-
peer.seen = seq;
|
|
51
|
-
peer.seenAt = sentAt + rttMs / 2 + HANDLING_MS + PEER_CLOCK_OFFSET_MS;
|
|
52
|
-
}
|
|
53
|
-
},
|
|
54
|
-
bufferedAmount: () => 0
|
|
55
|
-
}));
|
|
56
|
-
for (const channel of channels) {
|
|
57
|
-
probe.attach("s1", "s1", channel.label, channel);
|
|
58
|
-
}
|
|
59
|
-
const echo = () =>
|
|
60
|
-
probe.noteEcho("s1", {
|
|
61
|
-
type: "probe-echo",
|
|
62
|
-
seen: Object.fromEntries(LABELS.map((label) => [label, peer.seen])),
|
|
63
|
-
seenAt: Object.fromEntries(LABELS.map((label) => [label, peer.seenAt])),
|
|
64
|
-
sentAt: Date.now() + PEER_CLOCK_OFFSET_MS,
|
|
65
|
-
report: {
|
|
66
|
-
visibility: peer.visibility,
|
|
67
|
-
loopLagMs: peer.loopLagMs,
|
|
68
|
-
transportBytesReceived: peer.channelBytes,
|
|
69
|
-
channels: Object.fromEntries(LABELS.map((label) => [label, { messages: 0, bytes: peer.channelBytes }])),
|
|
70
|
-
pending: 0
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
t.after(() => probe.dispose());
|
|
74
|
-
return {
|
|
75
|
-
peer,
|
|
76
|
-
lines,
|
|
77
|
-
captures,
|
|
78
|
-
/**
|
|
79
|
-
* Run the clock, echoing every `echoEveryMs` — composed just before the
|
|
80
|
-
* probe of that instant arrives, which is the latest a healthy report can
|
|
81
|
-
* be.
|
|
82
|
-
*/
|
|
83
|
-
run(ms, echoEveryMs = PROBE_INTERVAL_MS) {
|
|
84
|
-
for (let elapsed = 0; elapsed < ms; elapsed += echoEveryMs) {
|
|
85
|
-
t.mock.timers.tick(echoEveryMs - 1);
|
|
86
|
-
echo();
|
|
87
|
-
t.mock.timers.tick(1);
|
|
88
|
-
}
|
|
89
|
-
},
|
|
90
|
-
verdicts(fromMs = 0) {
|
|
91
|
-
return lines
|
|
92
|
-
.filter((line) => line.startsWith("[dc-probe]"))
|
|
93
|
-
.map((line) => ({ verdict: line.split(" ")[2], at: Date.parse(line.match(/at=(\S+)/)[1]) }))
|
|
94
|
-
.filter((entry) => entry.at >= fromMs);
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
test("an idle healthy connection is not called stopped (2026-09-28, 35cff221)", (t) => {
|
|
100
|
-
// rtt 6 ms, every queue empty, nothing but probes crossing. The verdict that
|
|
101
|
-
// compared a one-way delay of 6-54 ms with an allowance of 3 ms printed
|
|
102
|
-
// `association-stopped` on this connection every five seconds.
|
|
103
|
-
const link = connection(t);
|
|
104
|
-
link.run(60_000);
|
|
105
|
-
// From the first echo on: before it there is nothing to judge.
|
|
106
|
-
const named = new Set(link.verdicts(PROBE_INTERVAL_MS).map((entry) => entry.verdict));
|
|
107
|
-
assert.deepEqual([...named], ["flowing"], `every probe was delivered: ${link.lines.at(-1)}`);
|
|
108
|
-
assert.equal(link.captures.length, 0);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
test("a wedge is called stopped within seconds and reaches the witness (2026-09-28, 10b53a7e)", (t) => {
|
|
112
|
-
// Probe 1464 was the last the peer saw; echoes went on arriving for the
|
|
113
|
-
// whole minute. The old verdict read `flowing` on every line.
|
|
114
|
-
const link = connection(t, { rttMs: 26 });
|
|
115
|
-
link.run(30_000);
|
|
116
|
-
const wedgedAt = Date.now();
|
|
117
|
-
link.peer.delivering = false;
|
|
118
|
-
link.run(30_000);
|
|
119
|
-
const after = link.verdicts(wedgedAt);
|
|
120
|
-
const firstStop = after.find((entry) => entry.verdict === "association-stopped");
|
|
121
|
-
assert.ok(firstStop, `the wedge must be named: ${link.lines.at(-1)}`);
|
|
122
|
-
assert.ok(firstStop.at - wedgedAt <= 2_000, `named ${firstStop.at - wedgedAt} ms after the last delivery`);
|
|
123
|
-
assert.equal(after.at(-1).verdict, "association-stopped", "and it stays named while nothing arrives");
|
|
124
|
-
assert.equal(link.captures.length, 1, "one capture per wedge");
|
|
125
|
-
assert.ok(link.captures[0].at - wedgedAt <= 3_000, `captured ${link.captures[0].at - wedgedAt} ms in`);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
test("delivery resuming clears the verdict and arms the next capture", (t) => {
|
|
129
|
-
const link = connection(t);
|
|
130
|
-
link.run(10_000);
|
|
131
|
-
link.peer.delivering = false;
|
|
132
|
-
link.run(10_000);
|
|
133
|
-
assert.equal(link.captures.length, 1);
|
|
134
|
-
const resumedAt = Date.now();
|
|
135
|
-
link.peer.delivering = true;
|
|
136
|
-
link.run(10_000);
|
|
137
|
-
assert.equal(link.verdicts(resumedAt).at(-1).verdict, "flowing");
|
|
138
|
-
link.peer.delivering = false;
|
|
139
|
-
link.run(10_000);
|
|
140
|
-
assert.equal(link.captures.length, 2, "a second wedge is a second capture");
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
test("a hidden tab answering late is read as flowing (field 2026-09-03 loop delays)", (t) => {
|
|
144
|
-
// A hidden tab echoes about once a second and reports its own event loop as
|
|
145
|
-
// late — 681 → 1881 ms in the field. Every probe still reaches it.
|
|
146
|
-
const link = connection(t);
|
|
147
|
-
link.run(10_000);
|
|
148
|
-
link.peer.visibility = "hidden";
|
|
149
|
-
link.peer.loopLagMs = 681;
|
|
150
|
-
link.run(5_000, 1_000);
|
|
151
|
-
link.peer.loopLagMs = 1881;
|
|
152
|
-
const hiddenAt = Date.now();
|
|
153
|
-
link.run(30_000, 2_000);
|
|
154
|
-
const named = new Set(link.verdicts(hiddenAt).map((entry) => entry.verdict));
|
|
155
|
-
assert.deepEqual([...named], ["flowing"], `every probe is delivered: ${link.lines.at(-1)}`);
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
test("late reports while film is still arriving are read as flowing", (t) => {
|
|
159
|
-
// A hidden tab reports every three seconds and says beforehand that its loop
|
|
160
|
-
// is that late; the far end's channel byte counter says film is crossing.
|
|
161
|
-
const link = connection(t);
|
|
162
|
-
link.run(10_000);
|
|
163
|
-
link.peer.visibility = "hidden";
|
|
164
|
-
link.peer.loopLagMs = 3_000;
|
|
165
|
-
link.run(1_000);
|
|
166
|
-
const lateAt = Date.now();
|
|
167
|
-
for (let index = 0; index < 20; index += 1) {
|
|
168
|
-
link.peer.channelBytes += 4_000_000;
|
|
169
|
-
link.run(3_000, 3_000);
|
|
170
|
-
}
|
|
171
|
-
const named = new Set(link.verdicts(lateAt).map((entry) => entry.verdict));
|
|
172
|
-
assert.deepEqual([...named], ["flowing"], `film is arriving: ${link.lines.at(-1)}`);
|
|
173
|
-
});
|