@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,176 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Two sessions of one output write into one place, and each serves what
|
|
3
|
-
* the other has made.
|
|
4
|
-
*
|
|
5
|
-
* This is what the whole address change is for. Measured 2026-09-03: two
|
|
6
|
-
* viewers of one copied file got two sessions with byte-identical output —
|
|
7
|
-
* `segment-00000.mp4` of 4141899 bytes, twice — and neither could see the
|
|
8
|
-
* other's work, because the directory was named after the session's own random
|
|
9
|
-
* id (`research/two-viewers-one-file-2026-09-03.md`).
|
|
10
|
-
*
|
|
11
|
-
* The start position is deliberately NOT part of the address: segment 42 covers
|
|
12
|
-
* the same span whoever began where, so two viewers who opened the same film at
|
|
13
|
-
* different places produce interchangeable segments and must share them.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import test from "node:test";
|
|
17
|
-
import assert from "node:assert/strict";
|
|
18
|
-
import { SourceFile } from "../services/media/SourceFile.js";
|
|
19
|
-
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
20
|
-
import os from "node:os";
|
|
21
|
-
import path from "node:path";
|
|
22
|
-
import { wireOutputs } from "../services/server/wire-outputs.js";
|
|
23
|
-
import { SegmentStore } from "../services/storage/segment-store/SegmentStore.js";
|
|
24
|
-
import { Timeline } from "../services/encode/output/Timeline.js";
|
|
25
|
-
import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
|
|
26
|
-
import { outputSpec } from "./helpers/output-spec.js";
|
|
27
|
-
|
|
28
|
-
const OUTPUT_KEY = "torrent:abc:fmt=fmp4:grid=kf@0:video-only:v=0/copy";
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* @returns {{ manager: object, store: SegmentStore, root: string }}
|
|
32
|
-
*/
|
|
33
|
-
function managerOverATempStore() {
|
|
34
|
-
const root = mkdtempSync(path.join(os.tmpdir(), "shared-segments-"));
|
|
35
|
-
const store = new SegmentStore({ root });
|
|
36
|
-
const manager = wireOutputs({
|
|
37
|
-
enabled: true,
|
|
38
|
-
ffmpegBin: "ffmpeg",
|
|
39
|
-
localBindHost: "127.0.0.1",
|
|
40
|
-
localPort: 9090,
|
|
41
|
-
segmentStore: store
|
|
42
|
-
});
|
|
43
|
-
return { manager, store, root };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* A session shaped like a live one, sharing an output with any other built
|
|
48
|
-
* from the same key.
|
|
49
|
-
*
|
|
50
|
-
* @param {{ id: string, dirPath: string, outputKey: string }} params
|
|
51
|
-
* @returns {object}
|
|
52
|
-
*/
|
|
53
|
-
function sessionOn({ id, dirPath, outputKey }) {
|
|
54
|
-
return {
|
|
55
|
-
id,
|
|
56
|
-
spec: outputSpec({ transcodeVideo: false }),
|
|
57
|
-
outputKey,
|
|
58
|
-
dirPath,
|
|
59
|
-
state: "ready",
|
|
60
|
-
file: new SourceFile({ sourceKey: "source-1", fileIndex: 0, name: "video.mkv" }),
|
|
61
|
-
// An ordinary session reads its own file, and its sound is inside it. The
|
|
62
|
-
// three differ only for a soundtrack shipped as a file of its own.
|
|
63
|
-
get inputFile() { return this.file; },
|
|
64
|
-
get audioFile() { return this.file; },
|
|
65
|
-
segmentFormat: fmp4Format,
|
|
66
|
-
// Where the file is cut, which every live session holds.
|
|
67
|
-
timeline: new Timeline({ boundaries: [0, 4, 8], cutGrid: "keyframe" }),
|
|
68
|
-
lastAccessedAt: Date.now()
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
test("a second session of the same output is given the same directory", (t) => {
|
|
73
|
-
const { store, root } = managerOverATempStore();
|
|
74
|
-
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
75
|
-
|
|
76
|
-
// Two viewers who opened one film ten minutes apart. Their sessions differ —
|
|
77
|
-
// the start position is still what keeps their encoders apart — but what they
|
|
78
|
-
// produce is the same material.
|
|
79
|
-
assert.equal(store.pathFor(OUTPUT_KEY), store.pathFor(OUTPUT_KEY));
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test("each session serves the segments the other one's encoder made", (t) => {
|
|
83
|
-
const { manager, store, root } = managerOverATempStore();
|
|
84
|
-
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
85
|
-
|
|
86
|
-
const dirPath = store.directoryFor(OUTPUT_KEY);
|
|
87
|
-
store.useFormat(OUTPUT_KEY, fmp4Format);
|
|
88
|
-
|
|
89
|
-
// Two runs writing into the ONE directory this output has. They are kept
|
|
90
|
-
// apart by the stretches they were given, not by a directory each, so neither
|
|
91
|
-
// can reach the other's numbers and there is nothing to separate.
|
|
92
|
-
writeFileSync(path.join(dirPath, "segment-00000.mp4"), Buffer.alloc(32, 1));
|
|
93
|
-
writeFileSync(path.join(dirPath, "segment-00100.mp4"), Buffer.alloc(32, 2));
|
|
94
|
-
|
|
95
|
-
const viewerOne = sessionOn({ id: "s-one", dirPath, outputKey: OUTPUT_KEY });
|
|
96
|
-
const viewerTwo = sessionOn({ id: "s-two", dirPath, outputKey: OUTPUT_KEY });
|
|
97
|
-
manager.outputs.set(viewerOne.id, viewerOne);
|
|
98
|
-
manager.outputs.set(viewerTwo.id, viewerTwo);
|
|
99
|
-
|
|
100
|
-
const sorted = (numbers) => [...numbers].sort((left, right) => left - right);
|
|
101
|
-
const heldByOne = sorted(manager.serving.producedSegmentNumbers(viewerOne));
|
|
102
|
-
const heldByTwo = sorted(manager.serving.producedSegmentNumbers(viewerTwo));
|
|
103
|
-
|
|
104
|
-
assert.deepEqual(heldByOne, [0, 100]);
|
|
105
|
-
assert.deepEqual(heldByTwo, [0, 100], "neither viewer's session is the owner of either run");
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
test("a session leaving does not take the segments with it", async (t) => {
|
|
109
|
-
const { manager, store, root } = managerOverATempStore();
|
|
110
|
-
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
111
|
-
|
|
112
|
-
const dirPath = store.directoryFor(OUTPUT_KEY);
|
|
113
|
-
store.useFormat(OUTPUT_KEY, fmp4Format);
|
|
114
|
-
writeFileSync(path.join(dirPath, "segment-00000.mp4"), Buffer.alloc(32, 1));
|
|
115
|
-
|
|
116
|
-
const viewerOne = sessionOn({ id: "s-one", dirPath, outputKey: OUTPUT_KEY });
|
|
117
|
-
const viewerTwo = sessionOn({ id: "s-two", dirPath, outputKey: OUTPUT_KEY });
|
|
118
|
-
manager.outputs.set(viewerOne.id, viewerOne);
|
|
119
|
-
manager.outputs.set(viewerTwo.id, viewerTwo);
|
|
120
|
-
|
|
121
|
-
await manager.lifecycle.disposeSession(viewerOne.id);
|
|
122
|
-
await manager.lifecycle.disposeSession(viewerTwo.id);
|
|
123
|
-
|
|
124
|
-
// Both sessions are gone and the material is still here. That is the whole
|
|
125
|
-
// decoupling: a session ending says nothing about whether anybody will ask
|
|
126
|
-
// for these segments again — the viewer who left may come back, and a viewer
|
|
127
|
-
// who never had a session here may open the same film in a minute.
|
|
128
|
-
assert.notEqual(store.pathOf(OUTPUT_KEY, 0), null);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
test("what nobody has read for long enough goes, and time is the only judge", (t) => {
|
|
132
|
-
const root = mkdtempSync(path.join(os.tmpdir(), "shared-segments-"));
|
|
133
|
-
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
134
|
-
let clock = 1_000_000;
|
|
135
|
-
const store = new SegmentStore({ root, now: () => clock });
|
|
136
|
-
|
|
137
|
-
const dirPath = store.directoryFor(OUTPUT_KEY);
|
|
138
|
-
store.useFormat(OUTPUT_KEY, fmp4Format);
|
|
139
|
-
writeFileSync(path.join(dirPath, "segment-00000.mp4"), Buffer.alloc(32, 1));
|
|
140
|
-
|
|
141
|
-
clock += 60_000;
|
|
142
|
-
assert.deepEqual(
|
|
143
|
-
store.enforce({ idleMs: 3_600_000, maxBytes: 1e9 }),
|
|
144
|
-
{ droppedIdle: 0, droppedForRoom: 0, segmentsRemoved: 0, bytes: 32 }
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
clock += 7_200_000;
|
|
148
|
-
const swept = store.enforce({ idleMs: 3_600_000, maxBytes: 1e9 });
|
|
149
|
-
assert.equal(swept.droppedIdle, 1);
|
|
150
|
-
assert.equal(store.pathOf(OUTPUT_KEY, 0), null);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
test("over the allowance, what was read longest ago goes first", (t) => {
|
|
154
|
-
const root = mkdtempSync(path.join(os.tmpdir(), "shared-segments-"));
|
|
155
|
-
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
156
|
-
let clock = 1_000_000;
|
|
157
|
-
const store = new SegmentStore({ root, now: () => clock });
|
|
158
|
-
|
|
159
|
-
const older = OUTPUT_KEY;
|
|
160
|
-
const newer = `${OUTPUT_KEY}:second`;
|
|
161
|
-
for (const key of [older, newer]) {
|
|
162
|
-
const dir = store.directoryFor(key);
|
|
163
|
-
store.useFormat(key, fmp4Format);
|
|
164
|
-
writeFileSync(path.join(dir, "segment-00000.mp4"), Buffer.alloc(1000, 1));
|
|
165
|
-
clock += 1000;
|
|
166
|
-
}
|
|
167
|
-
// Reading the newer one again is what puts the older one at the front of the
|
|
168
|
-
// queue: the judge is the last REQUEST, not when the file was written.
|
|
169
|
-
clock += 1000;
|
|
170
|
-
store.pathOf(newer, 0);
|
|
171
|
-
|
|
172
|
-
const swept = store.enforce({ idleMs: 3_600_000, maxBytes: 1500 });
|
|
173
|
-
assert.equal(swept.droppedForRoom, 1);
|
|
174
|
-
assert.equal(store.pathOf(older, 0), null);
|
|
175
|
-
assert.notEqual(store.pathOf(newer, 0), null);
|
|
176
|
-
});
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import test from "node:test";
|
|
3
|
-
import { bodySender } from "../services/transport/data-channel-handler.js";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Drives the sender over a body arriving in the given read sizes, and answers
|
|
7
|
-
* both what reached the far end and what was written — so "the body is
|
|
8
|
-
* unchanged" is asserted against the body rather than against a length.
|
|
9
|
-
*
|
|
10
|
-
* @param {number} sizeBytes
|
|
11
|
-
* @param {number[]} readSizes - The sizes the body read hands over, in order.
|
|
12
|
-
* @returns {{ sent: Buffer[], counted: number, written: Buffer }}
|
|
13
|
-
*/
|
|
14
|
-
function sendBody(sizeBytes, readSizes) {
|
|
15
|
-
/** @type {Buffer[]} */
|
|
16
|
-
const sent = [];
|
|
17
|
-
const sender = bodySender((bytes) => sent.push(Buffer.from(bytes)), sizeBytes);
|
|
18
|
-
/** @type {Buffer[]} */
|
|
19
|
-
const written = [];
|
|
20
|
-
let counted = 0;
|
|
21
|
-
let next = 0;
|
|
22
|
-
for (const size of readSizes) {
|
|
23
|
-
const block = Buffer.alloc(size);
|
|
24
|
-
for (let index = 0; index < size; index += 1) {
|
|
25
|
-
block[index] = next & 0xff;
|
|
26
|
-
next += 1;
|
|
27
|
-
}
|
|
28
|
-
written.push(block);
|
|
29
|
-
counted += sender.push(block);
|
|
30
|
-
}
|
|
31
|
-
counted += sender.flush();
|
|
32
|
-
return { sent, counted, written: Buffer.concat(written) };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
test("with no size chosen, one body read is one message", () => {
|
|
36
|
-
const { sent, counted } = sendBody(0, [1000, 2000, 3]);
|
|
37
|
-
assert.deepEqual(
|
|
38
|
-
sent.map((message) => message.length),
|
|
39
|
-
[1000, 2000, 3]
|
|
40
|
-
);
|
|
41
|
-
assert.equal(counted, 3);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test("a smaller size cuts a body read into messages of exactly that size", () => {
|
|
45
|
-
const { sent, counted } = sendBody(400, [1000]);
|
|
46
|
-
assert.deepEqual(
|
|
47
|
-
sent.map((message) => message.length),
|
|
48
|
-
[400, 400, 200]
|
|
49
|
-
);
|
|
50
|
-
assert.equal(counted, 3);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test("a larger size joins body reads until it is filled", () => {
|
|
54
|
-
const { sent } = sendBody(2500, [1000, 1000, 1000, 1000]);
|
|
55
|
-
assert.deepEqual(
|
|
56
|
-
sent.map((message) => message.length),
|
|
57
|
-
[2500, 1500]
|
|
58
|
-
);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test("every byte arrives once, in order, whatever the size", () => {
|
|
62
|
-
for (const size of [0, 1, 7, 400, 2500, 999_999]) {
|
|
63
|
-
const { sent, written } = sendBody(size, [1000, 3, 5000, 17]);
|
|
64
|
-
assert.deepEqual(Buffer.concat(sent), written, `size ${size} changed the body`);
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test("a size larger than the whole body still sends it, once, on flush", () => {
|
|
69
|
-
const { sent, written, counted } = sendBody(10_000, [100, 200]);
|
|
70
|
-
assert.equal(sent.length, 1);
|
|
71
|
-
assert.equal(counted, 1);
|
|
72
|
-
assert.deepEqual(sent[0], written);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test("the count is of messages, which is what a reading is attributed to", () => {
|
|
76
|
-
// 163 messages for 10 657 210 bytes is the field reading of 2026-09-12; at
|
|
77
|
-
// 16 KB the same body is 651 of them, and that difference is the measurement.
|
|
78
|
-
const { counted } = sendBody(16 * 1024, [10_657_210]);
|
|
79
|
-
assert.equal(counted, Math.ceil(10_657_210 / (16 * 1024)));
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test("nothing is sent for an empty body, and flush has nothing to release", () => {
|
|
83
|
-
const { sent, counted } = sendBody(400, []);
|
|
84
|
-
assert.deepEqual(sent, []);
|
|
85
|
-
assert.equal(counted, 0);
|
|
86
|
-
});
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file An output already made serves a viewer when the rules decided with the
|
|
3
|
-
* user 2026-09-16 allow it, through the paths a real request takes.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import test from "node:test";
|
|
7
|
-
import assert from "node:assert/strict";
|
|
8
|
-
import { writeFileSync } from "node:fs";
|
|
9
|
-
import path from "node:path";
|
|
10
|
-
import { managerWithOwnStore } from "./helpers/manager.js";
|
|
11
|
-
import { outputSpec } from "./helpers/output-spec.js";
|
|
12
|
-
import { SourceFile } from "../services/media/SourceFile.js";
|
|
13
|
-
import { Timeline } from "../services/encode/output/Timeline.js";
|
|
14
|
-
import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
|
|
15
|
-
import { Output } from "../services/encode/output/Output.js";
|
|
16
|
-
import { qualityStateOf } from "../services/encode/quality/OutputQualityState.js";
|
|
17
|
-
|
|
18
|
-
const TORRENT = "torrent:11f0929918e2b5aa2e5b71ecdbe5c0f1a4bbf7d1";
|
|
19
|
-
|
|
20
|
-
function hostWithA1080pOutput(t) {
|
|
21
|
-
const { manager, store, cleanup } = managerWithOwnStore({
|
|
22
|
-
startupWaitMs: 0,
|
|
23
|
-
videoEncoder: { kind: "vaapi", name: "h264_vaapi", inputArgs: [] }
|
|
24
|
-
});
|
|
25
|
-
t.after(async () => {
|
|
26
|
-
await manager.lifecycle.disposeAll();
|
|
27
|
-
cleanup();
|
|
28
|
-
});
|
|
29
|
-
manager.encodeRuns.planEncodersNow = () => {};
|
|
30
|
-
manager.encodeRuns.planEncodersSoon = () => {};
|
|
31
|
-
manager.getCachedMediaInfo = () => ({ durationSeconds: 400, width: 1920, height: 1080, fps: 24 });
|
|
32
|
-
// The soundtrack the plan listed, at the rate the file states: a copy of it
|
|
33
|
-
// has a figure, so it stays a copy.
|
|
34
|
-
manager.getCachedAudioTracks = () => [{ index: 0, fileIndex: 0, sourceTrackIndex: 0, bitrateKbps: 128 }];
|
|
35
|
-
const spec = outputSpec({
|
|
36
|
-
sourceKey: TORRENT,
|
|
37
|
-
transcodeVideo: true,
|
|
38
|
-
transcodeAudio: false,
|
|
39
|
-
encoder: "h264_vaapi",
|
|
40
|
-
width: 1920,
|
|
41
|
-
height: 1080,
|
|
42
|
-
fps: 24,
|
|
43
|
-
cutGrid: "uniform"
|
|
44
|
-
});
|
|
45
|
-
const key = spec.toKey();
|
|
46
|
-
store.useFormat(key, fmp4Format);
|
|
47
|
-
// The piece at the start of the film is made.
|
|
48
|
-
writeFileSync(path.join(store.directoryFor(key), fmp4Format.segmentFileName(0)), Buffer.alloc(100));
|
|
49
|
-
const file = new SourceFile({ sourceKey: TORRENT, fileIndex: 0, name: "film.mkv" }).learn({ width: 1920, height: 1080, durationSeconds: 400 });
|
|
50
|
-
const made = {
|
|
51
|
-
id: spec.toName(),
|
|
52
|
-
spec,
|
|
53
|
-
get outputKey() { return this.spec.toKey(); },
|
|
54
|
-
file,
|
|
55
|
-
timeline: new Timeline({ boundaries: Array.from({ length: 101 }, (_, index) => index * 4), cutGrid: "uniform" }),
|
|
56
|
-
output: new Output({ encodeWidth: 1920, encodeHeight: 1080, outputFps: 24 }),
|
|
57
|
-
segmentFormat: fmp4Format,
|
|
58
|
-
useSyntheticPlaylist: true
|
|
59
|
-
};
|
|
60
|
-
manager.outputs.set(made.id, made);
|
|
61
|
-
// WHAT ITS ENCODER WAS SEEN DOING. Its first piece is made, so an encoder ran
|
|
62
|
-
// on it here; a viewer put on it now needs one again, and a place for one is
|
|
63
|
-
// asked by what one costs — with no measured cost the proxy refuses (roadmap
|
|
64
|
-
// item 97, step 14).
|
|
65
|
-
qualityStateOf(made).lastAloneSpeed = 2;
|
|
66
|
-
return { manager, made };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function request(over) {
|
|
70
|
-
return {
|
|
71
|
-
sourceKey: TORRENT,
|
|
72
|
-
fileIndex: 0,
|
|
73
|
-
fileName: "film.mkv",
|
|
74
|
-
transcodeVideo: true,
|
|
75
|
-
transcodeAudio: false,
|
|
76
|
-
segmentFormatId: "fmp4",
|
|
77
|
-
...over
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
test("the automatic choice is served by a better output whose piece at the viewer's position is made", async (t) => {
|
|
82
|
-
const { manager, made } = hostWithA1080pOutput(t);
|
|
83
|
-
const answered = await manager.viewerRequests.createOrGetSession(
|
|
84
|
-
request({ consumerId: "auto-viewer", targetWidth: 1280, targetHeight: 720 })
|
|
85
|
-
);
|
|
86
|
-
assert.equal(answered, made);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
test("a size picked by hand is not served by another size", async (t) => {
|
|
90
|
-
const { manager, made } = hostWithA1080pOutput(t);
|
|
91
|
-
let answered = null;
|
|
92
|
-
try {
|
|
93
|
-
answered = await manager.viewerRequests.createOrGetSession(
|
|
94
|
-
request({ consumerId: "picker", targetWidth: 1280, targetHeight: 720, exactSize: true })
|
|
95
|
-
);
|
|
96
|
-
} catch {
|
|
97
|
-
answered = null;
|
|
98
|
-
}
|
|
99
|
-
assert.notEqual(answered, made);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
test("a step asked for by an automatic viewer and served by the picture leaves the picture a picture", async (t) => {
|
|
103
|
-
const { manager, made } = hostWithA1080pOutput(t);
|
|
104
|
-
manager.viewers.of(made, "auto-viewer").qualityMode = "auto";
|
|
105
|
-
|
|
106
|
-
const variant = await manager.renditions.resolveVariantSession(made.id, 720, 0, "auto-viewer");
|
|
107
|
-
|
|
108
|
-
assert.equal(variant, made, "the picture already made at 1080p serves the 720p request");
|
|
109
|
-
assert.notEqual(made.isStep, true, "and is not turned into a step of itself");
|
|
110
|
-
assert.ok(manager.viewers.forOutput(made).has("auto-viewer"));
|
|
111
|
-
});
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Which existing output serves a viewer: the rules decided with the user
|
|
3
|
-
* 2026-09-16, with the viewer's own link asked in every mode and on every
|
|
4
|
-
* branch (roadmap item 97, step 11).
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import test from "node:test";
|
|
8
|
-
import assert from "node:assert/strict";
|
|
9
|
-
import { chooseServingOutput } from "../services/encode/quality/serving-output.js";
|
|
10
|
-
import { linkCouldCarry, loadOf, PEAK_CLASS } from "../services/encode/quality/link-budget.js";
|
|
11
|
-
|
|
12
|
-
const WANTED = { width: 1824, height: 1026 };
|
|
13
|
-
const NEXT_LOWER = 1280 * 720;
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* @param {string} key
|
|
17
|
-
* @param {number} width
|
|
18
|
-
* @param {number} height
|
|
19
|
-
* @param {{ mbps?: number | null, peakClass?: string, readyHere?: boolean }} [over]
|
|
20
|
-
*/
|
|
21
|
-
function candidate(key, width, height, { mbps = 5, peakClass = PEAK_CLASS.KNOWN, readyHere = true } = {}) {
|
|
22
|
-
return { key, width, height, readyHere, load: loadOf({ mbps, peakClass: mbps === null ? PEAK_CLASS.UNKNOWN : peakClass }, null) };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* @param {object} over
|
|
27
|
-
* @param {number | null} [linkMbps] - What this viewer's link measured.
|
|
28
|
-
*/
|
|
29
|
-
function choose(over, linkMbps = null) {
|
|
30
|
-
return chooseServingOutput({
|
|
31
|
-
mode: "auto",
|
|
32
|
-
wanted: WANTED,
|
|
33
|
-
nextLowerArea: NEXT_LOWER,
|
|
34
|
-
atRisk: false,
|
|
35
|
-
judge: (one) => linkCouldCarry(linkMbps, one.load),
|
|
36
|
-
candidates: [],
|
|
37
|
-
...over
|
|
38
|
-
})?.key ?? null;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
test("the same quality or higher is served, the nearest of them", () => {
|
|
42
|
-
assert.equal(choose({ candidates: [candidate("2160", 3840, 2160), candidate("1080", 1920, 1080)] }), "1080");
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test("higher quality is not served over a link it does not fit", () => {
|
|
46
|
-
assert.equal(choose({ candidates: [candidate("1080", 1920, 1080, { mbps: 12 })] }, 10), null);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test("a little lower — above the next rung down — is served, and only if the link admits it", () => {
|
|
50
|
-
assert.equal(choose({ candidates: [candidate("1000", 1776, 1000)] }), "1000");
|
|
51
|
-
assert.equal(
|
|
52
|
-
choose({ candidates: [candidate("1000", 1776, 1000, { mbps: 12 })] }, 10),
|
|
53
|
-
null,
|
|
54
|
-
"a smaller picture proves nothing by itself: its own load is asked"
|
|
55
|
-
);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test("the next rung down or lower is served only when making the wanted format risks a wait, and only if it fits", () => {
|
|
59
|
-
const lower = [candidate("720", 1280, 720), candidate("480", 854, 480)];
|
|
60
|
-
assert.equal(choose({ candidates: lower }), null);
|
|
61
|
-
assert.equal(choose({ candidates: lower, atRisk: true }), "720");
|
|
62
|
-
assert.equal(
|
|
63
|
-
choose({ candidates: [candidate("720", 1280, 720, { mbps: 12 }), candidate("480", 854, 480, { mbps: 3 })], atRisk: true }, 10),
|
|
64
|
-
"480",
|
|
65
|
-
"the larger one does not fit this link, so it is not taken"
|
|
66
|
-
);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test("an output without the piece at the viewer's position spares them nothing", () => {
|
|
70
|
-
assert.equal(choose({ candidates: [candidate("1080", 1920, 1080, { readyHere: false })] }), null);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test("a size chosen by hand is served only exactly, and only if the link admits it", () => {
|
|
74
|
-
const candidates = [candidate("1080", 1920, 1080), candidate("exact", 1824, 1026)];
|
|
75
|
-
assert.equal(choose({ mode: "manual", candidates }), "exact");
|
|
76
|
-
assert.equal(choose({ mode: "manual", candidates: [candidate("1080", 1920, 1080)], atRisk: true }), null);
|
|
77
|
-
assert.equal(
|
|
78
|
-
choose({ mode: "manual", candidates: [candidate("exact", 1824, 1026, { mbps: 12 })] }, 10),
|
|
79
|
-
null,
|
|
80
|
-
"the size they picked, but more than their link carries: not handed over"
|
|
81
|
-
);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test("of two outputs of the size picked by hand, the higher limit the link admits is served", () => {
|
|
85
|
-
const candidates = [candidate("2800", 1824, 1026, { mbps: 3.64 }), candidate("1400", 1824, 1026, { mbps: 1.82 })];
|
|
86
|
-
assert.equal(choose({ mode: "manual", candidates }, 10), "2800");
|
|
87
|
-
assert.equal(choose({ mode: "manual", candidates }, 3), "1400", "the link admits only the lower limit");
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test("a bound is taken before an average, even when the average's limit is higher", () => {
|
|
91
|
-
const candidates = [
|
|
92
|
-
candidate("estimated", 1824, 1026, { mbps: 6, peakClass: PEAK_CLASS.ESTIMATED }),
|
|
93
|
-
candidate("known", 1824, 1026, { mbps: 4, peakClass: PEAK_CLASS.KNOWN })
|
|
94
|
-
];
|
|
95
|
-
assert.equal(choose({ mode: "manual", candidates }, 10), "known");
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test("an output with no bound is not served against a measured link, and is while nothing measured it", () => {
|
|
99
|
-
const candidates = [candidate("hardware", 1824, 1026, { mbps: null })];
|
|
100
|
-
assert.equal(choose({ mode: "manual", candidates }, 50), null);
|
|
101
|
-
assert.equal(choose({ mode: "manual", candidates }, null), "hardware");
|
|
102
|
-
});
|