@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,273 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Step 14 of roadmap item 97: which modes this machine may encode with,
|
|
3
|
-
* what each costs at every size, and what the proxy keeps between runs about
|
|
4
|
-
* its own admitted encodes — all on fakes. Nothing here runs ffmpeg, starts a
|
|
5
|
-
* torrent client or touches a swarm; the one file written is in a temporary
|
|
6
|
-
* directory.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import test from "node:test";
|
|
10
|
-
import assert from "node:assert/strict";
|
|
11
|
-
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
12
|
-
import os from "node:os";
|
|
13
|
-
import path from "node:path";
|
|
14
|
-
import {
|
|
15
|
-
configurationKeyOf,
|
|
16
|
-
kindOfEncoderName,
|
|
17
|
-
parseFfmpegVersion,
|
|
18
|
-
parseNvidiaSmi,
|
|
19
|
-
parseX264Version,
|
|
20
|
-
UNKNOWN
|
|
21
|
-
} from "../services/encode/fingerprint.js";
|
|
22
|
-
import { interpolationErrorOf, throughputAt } from "../services/encode/throughput.js";
|
|
23
|
-
import { calibrateEncoder, HostCalibration } from "../services/encode/calibration.js";
|
|
24
|
-
import { canSustainOutput, pickSoftwarePreset } from "../services/encode/hwaccel.js";
|
|
25
|
-
import { contentOf, LocalObservations } from "../services/encode/LocalObservations.js";
|
|
26
|
-
import { OutputSpec, VideoOutput, CutGrid } from "../services/encode/output/index.js";
|
|
27
|
-
import { PEAK_CLASS, videoLoadOfSpec } from "../services/encode/quality/link-budget.js";
|
|
28
|
-
import { SoftwareEncoder } from "../services/encode/SoftwareEncoder.js";
|
|
29
|
-
import { NvencEncoder } from "../services/encode/NvencEncoder.js";
|
|
30
|
-
|
|
31
|
-
const FINGERPRINT = Object.freeze({
|
|
32
|
-
ffmpeg: "8.1.2",
|
|
33
|
-
x264: "core 164 r3108 31e19f9",
|
|
34
|
-
cpu: "Cortex-A72",
|
|
35
|
-
threads: 4,
|
|
36
|
-
devices: { vaapi: { model: "0x8086:0x9a49", driver: "i915 (kernel 6.6.31)" } }
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
/** A re-encoded picture's spec, the way `OutputOpening` builds one. */
|
|
40
|
-
function specAt({ encoder = "libx264", width = 1280, height = 720, fps = 24, preset = "veryfast", rateControl = null } = {}) {
|
|
41
|
-
return new OutputSpec({
|
|
42
|
-
sourceKey: "src",
|
|
43
|
-
segmentFormatId: "fmp4",
|
|
44
|
-
grid: new CutGrid({ kind: "uniform", fileIndex: 0 }),
|
|
45
|
-
video: new VideoOutput({ fileIndex: 0, encode: { encoder, width, height, fps, preset, tonemap: false, rateControl } }),
|
|
46
|
-
audio: null
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// --- The configuration key -------------------------------------------------
|
|
51
|
-
|
|
52
|
-
test("the builds and devices are read out of what the tools print", () => {
|
|
53
|
-
assert.equal(parseFfmpegVersion("ffmpeg version 8.1.2 Copyright (c) 2000-2025\nbuilt with gcc"), "8.1.2");
|
|
54
|
-
assert.equal(parseFfmpegVersion("garbage"), UNKNOWN);
|
|
55
|
-
const stream = Buffer.from("\0\0\x01\x06\x05x264 - core 164 r3108 31e19f9 - H.264/MPEG-4 AVC codec", "latin1");
|
|
56
|
-
assert.equal(parseX264Version(stream), "core 164 r3108 31e19f9");
|
|
57
|
-
assert.equal(parseX264Version(Buffer.from("no version here")), UNKNOWN);
|
|
58
|
-
assert.deepEqual(parseNvidiaSmi("NVIDIA RTX 4000 SFF Ada Generation, 550.54.14\n"), {
|
|
59
|
-
model: "NVIDIA RTX 4000 SFF Ada Generation",
|
|
60
|
-
driver: "550.54.14"
|
|
61
|
-
});
|
|
62
|
-
assert.equal(kindOfEncoderName("h264_nvenc"), "nvenc");
|
|
63
|
-
assert.equal(kindOfEncoderName("libx264"), "software");
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test("a driver update invalidates what a device was seen doing, and nothing about software", () => {
|
|
67
|
-
const vaapi = { name: "h264_vaapi", kind: "vaapi" };
|
|
68
|
-
const software = { name: "libx264", kind: "software" };
|
|
69
|
-
const updated = { ...FINGERPRINT, devices: { vaapi: { model: "0x8086:0x9a49", driver: "i915 (kernel 6.8.0)" } } };
|
|
70
|
-
assert.notEqual(configurationKeyOf(FINGERPRINT, vaapi), configurationKeyOf(updated, vaapi));
|
|
71
|
-
assert.equal(configurationKeyOf(FINGERPRINT, software), configurationKeyOf(updated, software));
|
|
72
|
-
// Another x264 build or another thread count is another software configuration.
|
|
73
|
-
assert.notEqual(configurationKeyOf(FINGERPRINT, software), configurationKeyOf({ ...FINGERPRINT, threads: 14 }, software));
|
|
74
|
-
assert.notEqual(configurationKeyOf(FINGERPRINT, software), configurationKeyOf({ ...FINGERPRINT, x264: "core 165" }, software));
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
// --- Throughput by size ----------------------------------------------------
|
|
78
|
-
|
|
79
|
-
const READINGS = [
|
|
80
|
-
{ width: 256, height: 144, pixelsPerSec: 40e6 },
|
|
81
|
-
{ width: 640, height: 360, pixelsPerSec: 36e6 },
|
|
82
|
-
{ width: 1280, height: 720, pixelsPerSec: 30e6 }
|
|
83
|
-
];
|
|
84
|
-
|
|
85
|
-
test("a size is priced from the readings around it, lowered by the error measured for interpolating", () => {
|
|
86
|
-
const error = interpolationErrorOf(READINGS);
|
|
87
|
-
assert.ok(error !== null && error >= 0, "three readings give a measured bound");
|
|
88
|
-
const entry = { preset: "veryfast", pixelsPerSec: 36e6, bySize: READINGS, interpolationError: error };
|
|
89
|
-
assert.equal(throughputAt(entry, { width: 640, height: 360 }), 36e6, "a size read is its own reading");
|
|
90
|
-
const between = throughputAt(entry, { width: 854, height: 480 });
|
|
91
|
-
assert.ok(between < 36e6 && between > 30e6 * (1 - error), `between the two readings, got ${between}`);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
test("a size beyond what was read is not a size this machine has shown it can do", () => {
|
|
95
|
-
const entry = { preset: "veryfast", pixelsPerSec: 36e6, bySize: READINGS, interpolationError: 0.05 };
|
|
96
|
-
assert.equal(throughputAt(entry, { width: 1920, height: 1080 }), null);
|
|
97
|
-
assert.equal(throughputAt(entry, { width: 128, height: 72 }), null);
|
|
98
|
-
// Two readings measure no interpolation error: only the sizes read are known.
|
|
99
|
-
const two = { preset: "veryfast", pixelsPerSec: 36e6, bySize: READINGS.slice(0, 2), interpolationError: null };
|
|
100
|
-
assert.equal(throughputAt(two, { width: 426, height: 240 }), null);
|
|
101
|
-
assert.equal(throughputAt(two, { width: 640, height: 360 }), 36e6);
|
|
102
|
-
// An entry with no readings by size answers with its one figure, as before.
|
|
103
|
-
assert.equal(throughputAt({ preset: "fast", pixelsPerSec: 5e6 }, { width: 1920, height: 1080 }), 5e6);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
test("the offer does not keep a rung at a size no mode was measured at", () => {
|
|
107
|
-
const benchmark = [{ preset: "ultrafast", pixelsPerSec: 36e6, bySize: READINGS, interpolationError: 0.05 }];
|
|
108
|
-
const at1080 = canSustainOutput({ benchmark, outputPixelsPerSec: 1920 * 1080 * 24, frame: { width: 1920, height: 1080 } });
|
|
109
|
-
assert.deepEqual(at1080, { speed: null, sustainable: false });
|
|
110
|
-
// The same mode at a size it WAS read at, with decoding priced: 30 Mpx/s
|
|
111
|
-
// against 1280x720x24 is 1.36x, and a decode at 10x leaves 1.19x.
|
|
112
|
-
const at720 = canSustainOutput({
|
|
113
|
-
benchmark,
|
|
114
|
-
outputPixelsPerSec: 1280 * 720 * 24,
|
|
115
|
-
frame: { width: 1280, height: 720 },
|
|
116
|
-
observedDecodeCostSec: 0.1
|
|
117
|
-
});
|
|
118
|
-
assert.equal(at720.sustainable, true);
|
|
119
|
-
assert.ok(Math.abs(at720.speed - 1 / (0.1 + (1280 * 720 * 24) / 30e6)) < 1e-9);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
test("a preset not measured at a size is not chosen there", () => {
|
|
123
|
-
const benchmark = [
|
|
124
|
-
{ preset: "fast", pixelsPerSec: 20e6, bySize: READINGS.slice(0, 2), interpolationError: null },
|
|
125
|
-
{ preset: "ultrafast", pixelsPerSec: 60e6, bySize: READINGS, interpolationError: 0.05 }
|
|
126
|
-
];
|
|
127
|
-
assert.equal(pickSoftwarePreset(benchmark, 1280 * 720 * 24, {}, { width: 1280, height: 720 }), "ultrafast");
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
// --- The calibration walk --------------------------------------------------
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* A calibration with every reading replaced: `speeds` maps "rung@WxH" to how
|
|
134
|
-
* many times realtime that mode encodes there, `failing` the rungs whose
|
|
135
|
-
* segments do not decode.
|
|
136
|
-
*/
|
|
137
|
-
async function calibrateWith({ encoder, speeds, failing = [] }) {
|
|
138
|
-
const asked = [];
|
|
139
|
-
const result = await calibrateEncoder({
|
|
140
|
-
ffmpegBin: "never-run",
|
|
141
|
-
encoder,
|
|
142
|
-
measure: {
|
|
143
|
-
check: async ({ rung }) => (failing.includes(rung)
|
|
144
|
-
? { ok: false, reason: "a segment does not decode", segmentKbps: null }
|
|
145
|
-
: { ok: true, reason: "", segmentKbps: { averageKbps: 900, peakKbps: 1400 } }),
|
|
146
|
-
rawFrames: async (frame) => `raw-${frame.width}x${frame.height}`,
|
|
147
|
-
speed: async (rung, rawPath, frame) => {
|
|
148
|
-
asked.push(`${rung}@${frame.width}x${frame.height}`);
|
|
149
|
-
return speeds[`${rung}@${frame.width}x${frame.height}`] ?? null;
|
|
150
|
-
},
|
|
151
|
-
release: () => {}
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
return { ...result, asked };
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
test("a mode whose segments do not decode is not used at all", async () => {
|
|
158
|
-
const encoder = new SoftwareEncoder();
|
|
159
|
-
const speeds = {};
|
|
160
|
-
for (const rung of encoder.selectableRungs) {
|
|
161
|
-
for (const size of ["256x144", "640x360", "1280x720", "1920x1080", "3840x2160"]) {
|
|
162
|
-
speeds[`${rung}@${size}`] = 2;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
const { modes, refused } = await calibrateWith({ encoder, speeds, failing: ["faster"] });
|
|
166
|
-
assert.ok(!modes.some((mode) => mode.preset === "faster"));
|
|
167
|
-
assert.ok(refused.some((one) => one.preset === "faster" && /decode/.test(one.reason)));
|
|
168
|
-
assert.equal(modes.length, encoder.selectableRungs.length - 1);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
test("slower modes are not read where a faster one fell behind, nor larger sizes where the fastest did", async () => {
|
|
172
|
-
const encoder = new SoftwareEncoder();
|
|
173
|
-
const speeds = {
|
|
174
|
-
"ultrafast@256x144": 20, "superfast@256x144": 15, "veryfast@256x144": 10, "faster@256x144": 8, "fast@256x144": 6,
|
|
175
|
-
"ultrafast@640x360": 6, "superfast@640x360": 4, "veryfast@640x360": 0.8,
|
|
176
|
-
"ultrafast@1280x720": 0.9
|
|
177
|
-
};
|
|
178
|
-
const { modes, asked } = await calibrateWith({ encoder, speeds });
|
|
179
|
-
assert.ok(!asked.includes("faster@640x360"), "a mode slower than one that fell behind is not read");
|
|
180
|
-
assert.ok(!asked.some((one) => one.endsWith("@1920x1080")), "a size larger than one the fastest could not hold is not read");
|
|
181
|
-
const ultrafast = modes.find((mode) => mode.preset === "ultrafast");
|
|
182
|
-
assert.deepEqual(ultrafast.bySize.map((reading) => `${reading.width}x${reading.height}`), ["256x144", "640x360", "1280x720"]);
|
|
183
|
-
assert.equal(ultrafast.pixelsPerSec, 640 * 360 * 24 * 6, "the reference figure is the 640x360 reading");
|
|
184
|
-
const fast = modes.find((mode) => mode.preset === "fast");
|
|
185
|
-
assert.equal(throughputAt(fast, { width: 640, height: 360 }), null, "fast was never read at 640x360");
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
test("a device is calibrated only at the one setting its arguments pass", () => {
|
|
189
|
-
assert.deepEqual(new NvencEncoder().selectableRungs, ["p4"]);
|
|
190
|
-
assert.ok(new NvencEncoder().buildVideoArgs({ targetWidth: 1280, targetHeight: 720, segmentDurationSec: 4 }).includes("p4"));
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
test("the modes of the encoder in use are what prices, and software is kept for the fallback", () => {
|
|
194
|
-
const calibration = new HostCalibration({ byKind: { vaapi: [{ preset: "h264_vaapi", pixelsPerSec: 1 }], software: [] }, fingerprint: FINGERPRINT });
|
|
195
|
-
assert.equal(calibration.modesFor("vaapi").length, 1);
|
|
196
|
-
assert.deepEqual(calibration.modesFor("software"), [], "calibrated, nothing qualified");
|
|
197
|
-
assert.equal(calibration.modesFor("nvenc"), null, "not calibrated at all");
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
// --- Local observations ----------------------------------------------------
|
|
201
|
-
|
|
202
|
-
test("what is kept applies only on the configuration it was seen on, and survives a restart on it", () => {
|
|
203
|
-
const directory = mkdtempSync(path.join(os.tmpdir(), "ttv-observations-"));
|
|
204
|
-
const filePath = path.join(directory, "local-observations.json");
|
|
205
|
-
try {
|
|
206
|
-
const first = new LocalObservations({ fingerprint: FINGERPRINT, filePath });
|
|
207
|
-
const content = { codec: "hevc", bitDepth: 10, width: 1920, height: 1080, megabitsPerSecond: 8 };
|
|
208
|
-
first.noteEncode({ spec: specAt(), content, aloneSpeedX: 1.7, segmentKbps: { averageKbps: 2100, peakKbps: 3900 } });
|
|
209
|
-
first.notePreparation({ spec: specAt(), seconds: 6.5, bufferedSec: 41 });
|
|
210
|
-
|
|
211
|
-
const software = configurationKeyOf(FINGERPRINT, { name: "libx264", kind: "software" });
|
|
212
|
-
const again = new LocalObservations({ fingerprint: FINGERPRINT, filePath });
|
|
213
|
-
again.load([software]);
|
|
214
|
-
assert.equal(again.slowestAloneSpeed(specAt(), content), 1.7);
|
|
215
|
-
assert.equal(again.peakMbps(specAt()), 3.9);
|
|
216
|
-
assert.equal(again.longestPreparationSec(specAt()), 6.5);
|
|
217
|
-
|
|
218
|
-
// Another libx264 build: nothing kept matches, and the file is rewritten without it.
|
|
219
|
-
const rebuilt = { ...FINGERPRINT, x264: "core 165 r3200 abc" };
|
|
220
|
-
const afterUpdate = new LocalObservations({ fingerprint: rebuilt, filePath });
|
|
221
|
-
afterUpdate.load([configurationKeyOf(rebuilt, { name: "libx264", kind: "software" })]);
|
|
222
|
-
assert.equal(afterUpdate.slowestAloneSpeed(specAt(), content), null);
|
|
223
|
-
assert.equal(afterUpdate.peakMbps(specAt()), null);
|
|
224
|
-
assert.equal(JSON.parse(readFileSync(filePath, "utf8")).entries.speed && Object.keys(JSON.parse(readFileSync(filePath, "utf8")).entries.speed).length, 0);
|
|
225
|
-
} finally {
|
|
226
|
-
rmSync(directory, { recursive: true, force: true });
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
test("a speed seen on easier material is not applied to harder material, and the slowest comparable one is used", () => {
|
|
231
|
-
const observations = new LocalObservations({ fingerprint: FINGERPRINT, filePath: "" });
|
|
232
|
-
const light = { codec: "hevc", bitDepth: 10, width: 1920, height: 1080, megabitsPerSecond: 4 };
|
|
233
|
-
const heavy = { ...light, megabitsPerSecond: 12 };
|
|
234
|
-
observations.noteEncode({ spec: specAt(), content: light, aloneSpeedX: 2.4, segmentKbps: null });
|
|
235
|
-
assert.equal(observations.slowestAloneSpeed(specAt(), heavy), null, "a lighter source says nothing about a heavier one");
|
|
236
|
-
observations.noteEncode({ spec: specAt(), content: heavy, aloneSpeedX: 1.3, segmentKbps: null });
|
|
237
|
-
assert.equal(observations.slowestAloneSpeed(specAt(), light), 1.3, "both apply to the lighter source, and the slower wins");
|
|
238
|
-
assert.equal(observations.slowestAloneSpeed(specAt(), { ...light, codec: "h264" }), null, "another codec is not comparable");
|
|
239
|
-
assert.equal(observations.slowestAloneSpeed(specAt({ preset: "fast" }), light), null, "another mode is not this one");
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
test("an encoder with no bound of its own takes the peak seen, as an estimate and never as a bound", () => {
|
|
243
|
-
const hardware = specAt({ encoder: "h264_vaapi", preset: null });
|
|
244
|
-
assert.equal(videoLoadOfSpec(hardware, null).peakClass, PEAK_CLASS.UNKNOWN);
|
|
245
|
-
const seen = videoLoadOfSpec(hardware, null, 5.2);
|
|
246
|
-
assert.equal(seen.peakClass, PEAK_CLASS.ESTIMATED);
|
|
247
|
-
assert.equal(seen.mbps, 5.2);
|
|
248
|
-
// A bound the encoder is held to is never replaced by what was seen.
|
|
249
|
-
const bounded = specAt({ rateControl: { maxrateKbps: 3900, bufsizeKbps: 4500, level: "3.1" } });
|
|
250
|
-
assert.equal(videoLoadOfSpec(bounded, null, 9.9).peakClass, PEAK_CLASS.KNOWN);
|
|
251
|
-
assert.equal(videoLoadOfSpec(bounded, null, 9.9).mbps, 3.9);
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
test("a file of another shape is not read as observations", () => {
|
|
255
|
-
const directory = mkdtempSync(path.join(os.tmpdir(), "ttv-observations-"));
|
|
256
|
-
const filePath = path.join(directory, "local-observations.json");
|
|
257
|
-
try {
|
|
258
|
-
writeFileSync(filePath, JSON.stringify({ version: 0, entries: { speed: { x: [1] } } }));
|
|
259
|
-
const observations = new LocalObservations({ fingerprint: FINGERPRINT, filePath });
|
|
260
|
-
observations.load([configurationKeyOf(FINGERPRINT, { name: "libx264", kind: "software" })]);
|
|
261
|
-
assert.equal(observations.peakMbps(specAt()), null);
|
|
262
|
-
} finally {
|
|
263
|
-
rmSync(directory, { recursive: true, force: true });
|
|
264
|
-
}
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
test("what an encode was made from is described by the file's own facts", () => {
|
|
268
|
-
assert.deepEqual(
|
|
269
|
-
contentOf({ width: 1920, height: 1080, decode: { codec: "hevc", bitDepth: 10, megabitsPerSecond: 8, megapixelsPerSecond: 50 } }),
|
|
270
|
-
{ codec: "hevc", bitDepth: 10, width: 1920, height: 1080, megabitsPerSecond: 8 }
|
|
271
|
-
);
|
|
272
|
-
assert.equal(contentOf({ width: 0, height: 0, decode: null }), null);
|
|
273
|
-
});
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file An established fact is said once, then with decreasing frequency.
|
|
3
|
-
*
|
|
4
|
-
* Field 2026-09-12: one absent piece produced 235 000 lines in 92 minutes —
|
|
5
|
-
* about 55 a second, 68.8 % of them byte-identical repeats — and it turned the
|
|
6
|
-
* log file over twice, so the beginning of the failure was gone before anybody
|
|
7
|
-
* read it. A log is not spoilt by its size; it is spoilt by uniformity.
|
|
8
|
-
*
|
|
9
|
-
* Matched verbatim, the whole line: that catches nearly all of the flood and
|
|
10
|
-
* cannot merge two different statements. Normalising numbers would catch a
|
|
11
|
-
* little more and would also merge the memory series, which exists precisely to
|
|
12
|
-
* catch a runaway.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import test from "node:test";
|
|
16
|
-
import assert from "node:assert/strict";
|
|
17
|
-
import { logger, writeAlreadyDecided } from "../utils/logger.js";
|
|
18
|
-
|
|
19
|
-
/** What reached the console while `body` ran. */
|
|
20
|
-
function captured(body) {
|
|
21
|
-
const lines = [];
|
|
22
|
-
const real = { log: console.log, warn: console.warn, error: console.error };
|
|
23
|
-
console.log = (line) => lines.push(String(line));
|
|
24
|
-
console.warn = (line) => lines.push(String(line));
|
|
25
|
-
console.error = (line) => lines.push(String(line));
|
|
26
|
-
try {
|
|
27
|
-
body();
|
|
28
|
-
} finally {
|
|
29
|
-
console.log = real.log;
|
|
30
|
-
console.warn = real.warn;
|
|
31
|
-
console.error = real.error;
|
|
32
|
-
}
|
|
33
|
-
return lines;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** A line no other test in this process will have said. */
|
|
37
|
-
function unique(what) {
|
|
38
|
-
return `logger-repeats ${what} ${Math.random().toString(36).slice(2)}`;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
test("the first time is said, and the repeats right behind it are not", () => {
|
|
42
|
-
const message = unique("flood");
|
|
43
|
-
const lines = captured(() => {
|
|
44
|
-
for (let at = 0; at < 500; at += 1) {
|
|
45
|
-
logger.info(message);
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
assert.equal(lines.length, 1, "five hundred identical lines are one fact");
|
|
49
|
-
assert.ok(lines[0].includes(message));
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test("two different lines are both said — repeats are matched whole", () => {
|
|
53
|
-
const one = unique("one");
|
|
54
|
-
const other = unique("other");
|
|
55
|
-
const lines = captured(() => {
|
|
56
|
-
logger.info(one);
|
|
57
|
-
logger.info(other);
|
|
58
|
-
logger.info(one);
|
|
59
|
-
logger.info(other);
|
|
60
|
-
});
|
|
61
|
-
assert.equal(lines.length, 2, "different statements never merge");
|
|
62
|
-
assert.ok(lines[0].includes(one));
|
|
63
|
-
assert.ok(lines[1].includes(other));
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test("a line whose numbers differ is a different line", () => {
|
|
67
|
-
const stem = unique("rss");
|
|
68
|
-
const lines = captured(() => {
|
|
69
|
-
logger.info(`${stem} rss=327MB`);
|
|
70
|
-
logger.info(`${stem} rss=726MB`);
|
|
71
|
-
logger.info(`${stem} rss=4616MB`);
|
|
72
|
-
});
|
|
73
|
-
assert.equal(
|
|
74
|
-
lines.length,
|
|
75
|
-
3,
|
|
76
|
-
"the memory series exists to catch a runaway; suppressing it would be worse than the flood"
|
|
77
|
-
);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test("when it is said again, it says how many were held back", async () => {
|
|
81
|
-
const message = unique("counted");
|
|
82
|
-
const first = captured(() => {
|
|
83
|
-
logger.info(message);
|
|
84
|
-
for (let at = 0; at < 9; at += 1) {
|
|
85
|
-
logger.info(message);
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
assert.equal(first.length, 1);
|
|
89
|
-
|
|
90
|
-
// The first interval is a second. Waited for rather than assumed: this asks
|
|
91
|
-
// the logger itself when it is ready to speak again, with a deadline as a
|
|
92
|
-
// backstop.
|
|
93
|
-
const deadline = Date.now() + 10_000;
|
|
94
|
-
let later = [];
|
|
95
|
-
while (later.length === 0) {
|
|
96
|
-
if (Date.now() > deadline) {
|
|
97
|
-
assert.fail("the line should be said again once its interval has passed");
|
|
98
|
-
}
|
|
99
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
100
|
-
later = captured(() => logger.info(message));
|
|
101
|
-
}
|
|
102
|
-
const said = later[0].match(/\[said (\d+) more time\(s\) in the last ([0-9.]+)s\]/);
|
|
103
|
-
assert.ok(said, `the rate is the fact here, and must be stated: ${later[0]}`);
|
|
104
|
-
// At least the nine held back above. Each poll that found nothing said the
|
|
105
|
-
// line again and was itself held back, so the exact figure belongs to how
|
|
106
|
-
// often this test asked — the property being pinned is that the held-back
|
|
107
|
-
// count is reported at all, and that it counts every one of them.
|
|
108
|
-
assert.ok(Number(said[1]) >= 9, `held-back count too low: ${later[0]}`);
|
|
109
|
-
assert.ok(Number(said[2]) > 0, `the span it covers must be stated: ${later[0]}`);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test("a line already decided on another thread is written as it is", () => {
|
|
113
|
-
const message = unique("forwarded");
|
|
114
|
-
const lines = captured(() => {
|
|
115
|
-
writeAlreadyDecided("info", message);
|
|
116
|
-
writeAlreadyDecided("info", message);
|
|
117
|
-
writeAlreadyDecided("warn", message);
|
|
118
|
-
});
|
|
119
|
-
// The worker holds the same rule and applies it before forwarding. Deciding
|
|
120
|
-
// again here would be one decision taken twice on two different histories,
|
|
121
|
-
// and the file's own promise is that a line cannot reach the console and miss
|
|
122
|
-
// the file.
|
|
123
|
-
assert.equal(lines.length, 3, "a forwarded line is not judged a second time");
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
test("every level goes through the same rule", () => {
|
|
127
|
-
const message = unique("levels");
|
|
128
|
-
const lines = captured(() => {
|
|
129
|
-
logger.warn(message);
|
|
130
|
-
logger.warn(message);
|
|
131
|
-
logger.error(message);
|
|
132
|
-
});
|
|
133
|
-
assert.equal(lines.length, 1, "a flood of errors is still a flood");
|
|
134
|
-
});
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file What "the encoder is ahead of the viewer" actually means.
|
|
3
|
-
*
|
|
4
|
-
* It was measured as the highest segment number lying in the session's
|
|
5
|
-
* directory. That equals the look-ahead only while a viewer moves forward
|
|
6
|
-
* through one encoder run, and the difference destroyed a session on
|
|
7
|
-
* 2026-08-06: a seek forward left segments 662-665 on disk, the viewer then
|
|
8
|
-
* seeked BACK to 646, and the limiter compared 6950 s of output against a
|
|
9
|
-
* viewer at 6700 s, called it "250s ahead", and suspended a run 136 ms after it
|
|
10
|
-
* started — before it had produced anything. With the encoder stopped nothing
|
|
11
|
-
* read the input, so no pieces were requested (`0 selection(s)` with 33 peers
|
|
12
|
-
* connected) and segment 646 was never made. The viewer sat on a spinner while
|
|
13
|
-
* a stopped ffmpeg was held for being "too far ahead".
|
|
14
|
-
*
|
|
15
|
-
* The measure is the unbroken run of segments starting where the viewer is.
|
|
16
|
-
* Segments beyond a hole are not look-ahead: the viewer cannot reach them until
|
|
17
|
-
* the hole is filled.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import test from "node:test";
|
|
21
|
-
import assert from "node:assert/strict";
|
|
22
|
-
import { contiguousEnd } from "../services/encode/CushionReport.js";
|
|
23
|
-
|
|
24
|
-
test("an unbroken run reports its last segment", () => {
|
|
25
|
-
assert.equal(contiguousEnd(new Set([10, 11, 12, 13]), 10), 13);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
test("a hole stops the count, whatever lies beyond it", () => {
|
|
29
|
-
assert.equal(
|
|
30
|
-
contiguousEnd(new Set([10, 11, 662, 663, 664, 665]), 10),
|
|
31
|
-
11,
|
|
32
|
-
"segments past a gap are unreachable and must not count as ready"
|
|
33
|
-
);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test("the field case: a backward seek onto a segment nobody has made", () => {
|
|
37
|
-
// What the directory held after the forward seek, and where the viewer went.
|
|
38
|
-
const onDisk = new Set([661, 662, 663, 664, 665]);
|
|
39
|
-
assert.equal(
|
|
40
|
-
contiguousEnd(onDisk, 646),
|
|
41
|
-
null,
|
|
42
|
-
"the viewer's own segment is missing — that is a wait, not a look-ahead"
|
|
43
|
-
);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test("a single segment counts as itself", () => {
|
|
47
|
-
assert.equal(contiguousEnd(new Set([5]), 5), 5);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test("nothing on disk is not zero ahead — it is unknown", () => {
|
|
51
|
-
assert.equal(
|
|
52
|
-
contiguousEnd(new Set(), 0),
|
|
53
|
-
null,
|
|
54
|
-
"zero and null lead to opposite decisions: pause versus encode now"
|
|
55
|
-
);
|
|
56
|
-
});
|