@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,131 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file The usrsctp state reader's gating and command construction.
|
|
3
|
-
*
|
|
4
|
-
* Roadmap item 11: reads usrsctp's live association state via gdb the moment
|
|
5
|
-
* a wedge is declared. Everything here is the part that decides WHEN and WITH
|
|
6
|
-
* WHAT ARGUMENTS — the spawning itself is thin glue around these, the same
|
|
7
|
-
* shape as the packet witness (test/packet-witness.test.js).
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import test from "node:test";
|
|
11
|
-
import assert from "node:assert/strict";
|
|
12
|
-
import { EventEmitter } from "node:events";
|
|
13
|
-
|
|
14
|
-
import { createUsrsctpStateReader, SCTPSTATE_SCRIPT_PATH } from "../services/transport/usrsctp-state.js";
|
|
15
|
-
|
|
16
|
-
class FakeChild extends EventEmitter {
|
|
17
|
-
constructor(command, args) {
|
|
18
|
-
super();
|
|
19
|
-
this.command = command;
|
|
20
|
-
this.args = args;
|
|
21
|
-
this.stdout = new EventEmitter();
|
|
22
|
-
this.stderr = new EventEmitter();
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
kill() {
|
|
26
|
-
// Not exercised by these tests — every fake run finishes on its own.
|
|
27
|
-
return true;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* @param {string} output - What the fake gdb writes to stdout before closing.
|
|
33
|
-
* @returns {{ spawnProcess: Function, calls: Array<{ command: string, args: string[] }> }}
|
|
34
|
-
*/
|
|
35
|
-
function makeSpawn(output) {
|
|
36
|
-
const calls = [];
|
|
37
|
-
const spawnProcess = (command, args) => {
|
|
38
|
-
calls.push({ command, args });
|
|
39
|
-
const child = new FakeChild(command, args);
|
|
40
|
-
setImmediate(() => {
|
|
41
|
-
if (output) {
|
|
42
|
-
child.stdout.emit("data", Buffer.from(output));
|
|
43
|
-
}
|
|
44
|
-
child.emit("close", 0);
|
|
45
|
-
});
|
|
46
|
-
return child;
|
|
47
|
-
};
|
|
48
|
-
return { spawnProcess, calls };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Wait until `check` holds, rather than for a chosen interval. */
|
|
52
|
-
async function waitFor(check) {
|
|
53
|
-
const deadline = Date.now() + 5_000;
|
|
54
|
-
for (;;) {
|
|
55
|
-
if (check()) {
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
if (Date.now() > deadline) {
|
|
59
|
-
throw new Error("timed out waiting for the reading to be logged");
|
|
60
|
-
}
|
|
61
|
-
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
test("the deadline is counted outside the process gdb stops", async () => {
|
|
66
|
-
const { spawnProcess, calls } = makeSpawn("state=8 rwnd=95890\n");
|
|
67
|
-
const lines = [];
|
|
68
|
-
const reader = createUsrsctpStateReader({
|
|
69
|
-
log: (message) => lines.push(message),
|
|
70
|
-
spawnProcess,
|
|
71
|
-
pid: 4242
|
|
72
|
-
});
|
|
73
|
-
const started = reader.maybeRead("test wedge");
|
|
74
|
-
assert.equal(started, true);
|
|
75
|
-
await waitFor(() => lines.length > 0);
|
|
76
|
-
assert.equal(calls.length, 1);
|
|
77
|
-
// `timeout` is a separate process, so it keeps counting while gdb holds every
|
|
78
|
-
// thread of this one. A `setTimeout` here cannot fire — measured in the field
|
|
79
|
-
// on 2026-09-05, where gdb held the proxy for four minutes and the guard set
|
|
80
|
-
// for fifteen seconds never ran.
|
|
81
|
-
assert.equal(calls[0].command, "timeout");
|
|
82
|
-
assert.deepEqual(calls[0].args, [
|
|
83
|
-
"-s",
|
|
84
|
-
"KILL",
|
|
85
|
-
"15",
|
|
86
|
-
"gdb",
|
|
87
|
-
"-q",
|
|
88
|
-
"-batch",
|
|
89
|
-
"-p",
|
|
90
|
-
"4242",
|
|
91
|
-
"-x",
|
|
92
|
-
SCTPSTATE_SCRIPT_PATH
|
|
93
|
-
]);
|
|
94
|
-
assert.match(lines[0], /state=8 rwnd=95890/);
|
|
95
|
-
assert.match(lines[0], /test wedge/);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test("no output is reported as no reading, not silence", async () => {
|
|
99
|
-
const { spawnProcess } = makeSpawn("");
|
|
100
|
-
const lines = [];
|
|
101
|
-
const reader = createUsrsctpStateReader({ log: (message) => lines.push(message), spawnProcess, pid: 1 });
|
|
102
|
-
reader.maybeRead("empty case");
|
|
103
|
-
await waitFor(() => lines.length > 0);
|
|
104
|
-
assert.match(lines[0], /no reading/);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test("a second read is refused within the cooldown, like the packet witness", async () => {
|
|
108
|
-
const { spawnProcess, calls } = makeSpawn("state=8\n");
|
|
109
|
-
const lines = [];
|
|
110
|
-
const reader = createUsrsctpStateReader({
|
|
111
|
-
log: (message) => lines.push(message),
|
|
112
|
-
spawnProcess,
|
|
113
|
-
pid: 1,
|
|
114
|
-
cooldownMs: 60_000
|
|
115
|
-
});
|
|
116
|
-
assert.equal(reader.maybeRead("first"), true);
|
|
117
|
-
await waitFor(() => lines.length > 0);
|
|
118
|
-
assert.equal(reader.maybeRead("second, too soon"), false);
|
|
119
|
-
assert.equal(calls.length, 1);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
test("a missing gdb is reported, not thrown", async () => {
|
|
123
|
-
const lines = [];
|
|
124
|
-
const spawnProcess = () => {
|
|
125
|
-
throw new Error("spawn gdb ENOENT");
|
|
126
|
-
};
|
|
127
|
-
const reader = createUsrsctpStateReader({ log: (message) => lines.push(message), spawnProcess, pid: 1 });
|
|
128
|
-
reader.maybeRead("no gdb on this host");
|
|
129
|
-
await waitFor(() => lines.length > 0);
|
|
130
|
-
assert.match(lines[0], /could not start gdb/);
|
|
131
|
-
});
|
package/test/video-facts.test.js
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file The picture's facts, from the two readings that state them.
|
|
3
|
-
*
|
|
4
|
-
* Audio and subtitles have had this reconciliation since their flags were first
|
|
5
|
-
* read from the file. Video never did: every figure the encode is planned from
|
|
6
|
-
* came from ffmpeg's `-i` banner alone, and the `VideoTrack` the container
|
|
7
|
-
* declares was read and then used for nothing but a line in the log — so a file
|
|
8
|
-
* that states its bit depth or its HDR signalling, against a probe that does
|
|
9
|
-
* not print them, disagreed with nobody watching.
|
|
10
|
-
*
|
|
11
|
-
* What each check pins is WHICH reading answers, and why: the size and the
|
|
12
|
-
* frame rate are what the decoder will produce, so the probe answers; the bit
|
|
13
|
-
* depth and the HDR signalling are the file saying how its samples are to be
|
|
14
|
-
* read, so the container answers.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import test from "node:test";
|
|
18
|
-
import assert from "node:assert/strict";
|
|
19
|
-
|
|
20
|
-
import { Container } from "../services/media/container/Container.js";
|
|
21
|
-
|
|
22
|
-
const banner = (fields) => ({ width: null, height: null, fps: null, isHdr: false, bitDepth: null, ...fields });
|
|
23
|
-
|
|
24
|
-
test("with no container reading the probe answers for everything", () => {
|
|
25
|
-
const facts = Container.mergeVideoFacts(
|
|
26
|
-
banner({ width: 1920, height: 1080, fps: 23.976, isHdr: true, bitDepth: 10 }),
|
|
27
|
-
null
|
|
28
|
-
);
|
|
29
|
-
assert.equal(facts.width, 1920);
|
|
30
|
-
assert.equal(facts.height, 1080);
|
|
31
|
-
assert.equal(facts.fps, 23.976);
|
|
32
|
-
assert.equal(facts.isHdr, true);
|
|
33
|
-
assert.equal(facts.bitDepth, 10);
|
|
34
|
-
assert.deepEqual(facts.disagreements, []);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test("the size and the frame rate are the probe's, because that is what will be decoded", () => {
|
|
38
|
-
const facts = Container.mergeVideoFacts(
|
|
39
|
-
banner({ width: 1920, height: 1080, fps: 24 }),
|
|
40
|
-
{ width: 1440, height: 1080, fps: 25, displayWidth: 1920, displayHeight: 1080 }
|
|
41
|
-
);
|
|
42
|
-
assert.equal(facts.width, 1920, "the ladder and the scale filter are sized to the decoded frame");
|
|
43
|
-
assert.equal(facts.height, 1080);
|
|
44
|
-
assert.equal(facts.fps, 24);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test("the bit depth and the HDR signalling are the file's, because the file states them", () => {
|
|
48
|
-
// A ten-bit HEVC whose probe printed neither: this is the case that decides
|
|
49
|
-
// whether tone mapping runs and how the decode cost is priced.
|
|
50
|
-
const facts = Container.mergeVideoFacts(
|
|
51
|
-
banner({ width: 3840, height: 2160, fps: 24 }),
|
|
52
|
-
{ width: 3840, height: 2160, bitDepth: 10, isHdr: true }
|
|
53
|
-
);
|
|
54
|
-
assert.equal(facts.bitDepth, 10);
|
|
55
|
-
assert.equal(facts.isHdr, true);
|
|
56
|
-
assert.deepEqual(facts.disagreements, [], "one side saying nothing is not a disagreement");
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test("a field only the probe states is still answered", () => {
|
|
60
|
-
const facts = Container.mergeVideoFacts(
|
|
61
|
-
banner({ width: 1280, height: 720, fps: 30, bitDepth: 8 }),
|
|
62
|
-
{ width: null, height: null, fps: null, bitDepth: null }
|
|
63
|
-
);
|
|
64
|
-
assert.equal(facts.width, 1280);
|
|
65
|
-
assert.equal(facts.bitDepth, 8);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test("a real disagreement is reported, with both values and which is which", () => {
|
|
69
|
-
const facts = Container.mergeVideoFacts(
|
|
70
|
-
banner({ width: 1920, height: 1080, bitDepth: 8, isHdr: false }),
|
|
71
|
-
{ width: 1280, height: 720, bitDepth: 10, isHdr: true }
|
|
72
|
-
);
|
|
73
|
-
assert.equal(facts.disagreements.length, 3);
|
|
74
|
-
assert.ok(facts.disagreements.some((line) => /width 1280 in the container against 1920 in the probe/.test(line)));
|
|
75
|
-
assert.ok(facts.disagreements.some((line) => /bit depth 10 in the container against 8 in the probe/.test(line)));
|
|
76
|
-
// HDR is not among them, and cannot be: see `mergeVideoFacts`.
|
|
77
|
-
assert.ok(!facts.disagreements.some((line) => /HDR/.test(line)));
|
|
78
|
-
// And the rule still decides: the probe for the size, the file for the rest.
|
|
79
|
-
assert.equal(facts.width, 1920);
|
|
80
|
-
assert.equal(facts.bitDepth, 10);
|
|
81
|
-
assert.equal(facts.isHdr, true);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test("the display size is the container's alone — the probe has no such field", () => {
|
|
85
|
-
const facts = Container.mergeVideoFacts(
|
|
86
|
-
banner({ width: 1440, height: 1080 }),
|
|
87
|
-
{ width: 1440, height: 1080, displayWidth: 1920, displayHeight: 1080 }
|
|
88
|
-
);
|
|
89
|
-
assert.equal(facts.displayWidth, 1920);
|
|
90
|
-
assert.equal(facts.displayHeight, 1080);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test("zero and nonsense are not values", () => {
|
|
94
|
-
const facts = Container.mergeVideoFacts(
|
|
95
|
-
banner({ width: 0, height: 0, fps: 0, bitDepth: 0 }),
|
|
96
|
-
{ width: 1920, height: 1080, fps: 24, bitDepth: 8 }
|
|
97
|
-
);
|
|
98
|
-
assert.equal(facts.width, 1920, "a probe that printed nothing does not outrank a file that speaks");
|
|
99
|
-
assert.equal(facts.fps, 24);
|
|
100
|
-
assert.equal(facts.bitDepth, 8);
|
|
101
|
-
assert.deepEqual(facts.disagreements, []);
|
|
102
|
-
});
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file What a viewer's own assignment to an output must do.
|
|
3
|
-
*
|
|
4
|
-
* The property under all of it: one viewer's link, seek or choice never moves
|
|
5
|
-
* another viewer's output. That is roadmap item 97, and the record it replaces
|
|
6
|
-
* — one entry per (file, height) for everybody — could not express it.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import test from "node:test";
|
|
10
|
-
import assert from "node:assert/strict";
|
|
11
|
-
import { Assignments } from "../services/viewer/Assignments.js";
|
|
12
|
-
|
|
13
|
-
const WINDOW_MS = 30_000;
|
|
14
|
-
const make = () => new Assignments({ acceptWindowMs: WINDOW_MS });
|
|
15
|
-
|
|
16
|
-
test("the acceptance window is given, not invented", () => {
|
|
17
|
-
assert.throws(() => new Assignments({}), TypeError);
|
|
18
|
-
assert.throws(() => new Assignments({ acceptWindowMs: -1 }), TypeError);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
test("a choice stands until the rule moves it, and a request never moves it", () => {
|
|
22
|
-
const mine = make();
|
|
23
|
-
|
|
24
|
-
mine.choose(720, "out-a");
|
|
25
|
-
mine.give(mine.generation, 720, 4, "out-a");
|
|
26
|
-
|
|
27
|
-
assert.equal(mine.chosenFor(720), "out-a");
|
|
28
|
-
mine.give(mine.generation, 720, 5, "out-b");
|
|
29
|
-
assert.equal(mine.chosenFor(720), "out-a", "being given something else does not re-choose");
|
|
30
|
-
mine.choose(720, "out-b");
|
|
31
|
-
assert.equal(mine.chosenFor(720), "out-b");
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test("two viewers do not share an answer", () => {
|
|
35
|
-
const thin = make();
|
|
36
|
-
const thick = make();
|
|
37
|
-
|
|
38
|
-
thin.choose(720, "out-540-slow");
|
|
39
|
-
thick.choose(720, "out-720-full");
|
|
40
|
-
|
|
41
|
-
assert.equal(thin.chosenFor(720), "out-540-slow");
|
|
42
|
-
assert.equal(thick.chosenFor(720), "out-720-full", "the other viewer's link decided nothing here");
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test("a repeat of a request is answered by whatever answered it, not decided again", () => {
|
|
46
|
-
const mine = make();
|
|
47
|
-
const first = mine.generation;
|
|
48
|
-
|
|
49
|
-
mine.give(first, 720, 9, "out-a");
|
|
50
|
-
|
|
51
|
-
assert.equal(mine.givenFor(first, 720, 9), "out-a");
|
|
52
|
-
assert.equal(mine.givenFor(first, 720, 10), "", "a segment nothing answered says so");
|
|
53
|
-
assert.equal(mine.givenFor(first + 1, 720, 9), "", "and a later generation is not the same question");
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test("a request carries its own generation, so a late one is not read against the current", () => {
|
|
57
|
-
const mine = make();
|
|
58
|
-
const made = mine.generation;
|
|
59
|
-
|
|
60
|
-
mine.statedGeneration(1, 1_000);
|
|
61
|
-
|
|
62
|
-
assert.equal(mine.generation, 1);
|
|
63
|
-
assert.notEqual(mine.generation, made);
|
|
64
|
-
assert.equal(mine.accepts(made, 1_000 + WINDOW_MS), true, "inside the window it is still answered");
|
|
65
|
-
assert.equal(mine.accepts(made, 1_000 + WINDOW_MS + 1), false, "past it, a new request is not");
|
|
66
|
-
assert.equal(mine.accepts(mine.generation, 1_000 + 10 * WINDOW_MS), true, "the current one never runs out");
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test("an accepted response keeps its output past its own generation's window", () => {
|
|
70
|
-
const mine = make();
|
|
71
|
-
const made = mine.generation;
|
|
72
|
-
mine.give(made, 720, 3, "out-a");
|
|
73
|
-
const token = mine.accept("out-a");
|
|
74
|
-
|
|
75
|
-
mine.statedGeneration(1, 1_000);
|
|
76
|
-
mine.choose(720, "out-b");
|
|
77
|
-
|
|
78
|
-
const longAfter = 1_000 + 10 * WINDOW_MS;
|
|
79
|
-
assert.equal(mine.accepts(made, longAfter), false, "no NEW request of that generation is taken");
|
|
80
|
-
assert.ok(
|
|
81
|
-
mine.heldKeys(longAfter).has("out-a"),
|
|
82
|
-
"but the output whose bytes are still going out is held — dropping it is what the second deadline exists to prevent"
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
mine.release(token);
|
|
86
|
-
assert.equal(mine.heldKeys(longAfter).has("out-a"), false, "and released once the response is done");
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
test("what was given inside a live generation holds its output, and is forgotten past the window", () => {
|
|
90
|
-
const mine = make();
|
|
91
|
-
const made = mine.generation;
|
|
92
|
-
mine.give(made, 720, 3, "out-a");
|
|
93
|
-
mine.statedGeneration(1, 1_000);
|
|
94
|
-
|
|
95
|
-
assert.ok(mine.heldKeys(1_000 + WINDOW_MS).has("out-a"));
|
|
96
|
-
assert.equal(
|
|
97
|
-
mine.heldKeys(1_000 + WINDOW_MS + 1).has("out-a"),
|
|
98
|
-
false,
|
|
99
|
-
"past the window nothing stands on it, so it may be let go"
|
|
100
|
-
);
|
|
101
|
-
assert.equal(
|
|
102
|
-
mine.givenFor(made, 720, 3),
|
|
103
|
-
"",
|
|
104
|
-
"and the generation is pruned rather than kept for the life of the viewing"
|
|
105
|
-
);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
test("a viewer leaving holds nothing, response in flight included", () => {
|
|
109
|
-
const mine = make();
|
|
110
|
-
mine.choose(720, "out-a");
|
|
111
|
-
mine.accept("out-b");
|
|
112
|
-
mine.give(mine.generation, 720, 1, "out-c");
|
|
113
|
-
|
|
114
|
-
mine.clear();
|
|
115
|
-
|
|
116
|
-
assert.equal(mine.heldKeys().size, 0);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
test("the page owns the number, so a lower one does not take the viewer back", () => {
|
|
120
|
-
const mine = make();
|
|
121
|
-
mine.statedGeneration(4, 1_000);
|
|
122
|
-
|
|
123
|
-
assert.equal(mine.statedGeneration(2, 2_000), 4, "a reordered or repeated message changes nothing");
|
|
124
|
-
assert.equal(mine.statedGeneration(4, 2_000), 4, "and neither does the one already current");
|
|
125
|
-
assert.equal(mine.generation, 4);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
test("a request ahead of what the page has announced is still answered", () => {
|
|
129
|
-
const mine = make();
|
|
130
|
-
|
|
131
|
-
// The page raises its own number before it sends anything, so a segment
|
|
132
|
-
// request can overtake the seek that announces the new viewing.
|
|
133
|
-
assert.equal(mine.accepts(1), true, "being behind the page means this side has not heard yet");
|
|
134
|
-
assert.equal(mine.accepts(7), true);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
test("a request that states no generation is answered", () => {
|
|
138
|
-
const mine = make();
|
|
139
|
-
mine.statedGeneration(3, 1_000);
|
|
140
|
-
|
|
141
|
-
assert.equal(mine.accepts(Number.NaN, 1_000 + 10 * WINDOW_MS), true);
|
|
142
|
-
assert.equal(mine.accepts(undefined, 1_000 + 10 * WINDOW_MS), true, "a transport with no loader of ours builds no such parameter");
|
|
143
|
-
});
|
|
@@ -1,285 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file What a viewer is watching, and what happens to it when they leave.
|
|
3
|
-
*
|
|
4
|
-
* The relation "this person watches this output" is stored on the viewer, and
|
|
5
|
-
* these checks hold the operations that derive the opposite direction.
|
|
6
|
-
*
|
|
7
|
-
* 1. The viewer must never be dropped from the PICTURE. Their chosen
|
|
8
|
-
* soundtrack, their position and their step are recorded there, and the
|
|
9
|
-
* picture is the only id the browser knows. Dropping them was reachable by
|
|
10
|
-
* an ordinary sequence — down a step, back to the picture's own height, down
|
|
11
|
-
* again — and cost them the soundtrack they had picked.
|
|
12
|
-
* 2. A viewer who leaves must be subtracted from EVERY output they were
|
|
13
|
-
* watching, and an output with nobody left must be let go. Nothing outside
|
|
14
|
-
* the session manager knows the id of a quality step or of a separately
|
|
15
|
-
* published soundtrack, so nothing else can ever release one.
|
|
16
|
-
* 3. Whatever removes a viewer from an output must release what their watching
|
|
17
|
-
* claimed of production. The only place a claim is released is the plan's
|
|
18
|
-
* pass over the output's viewers, so a viewer deleted by any other route
|
|
19
|
-
* leaves a claim that nothing can reach.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import { chooseOutput } from "../services/viewer/choices.js";
|
|
23
|
-
import test from "node:test";
|
|
24
|
-
import assert from "node:assert/strict";
|
|
25
|
-
import { mkdtemp, rm } from "node:fs/promises";
|
|
26
|
-
import os from "node:os";
|
|
27
|
-
import path from "node:path";
|
|
28
|
-
import { wireOutputs } from "../services/server/wire-outputs.js";
|
|
29
|
-
import { Viewers } from "../services/viewer/Viewers.js";
|
|
30
|
-
import { SourceFile } from "../services/media/SourceFile.js";
|
|
31
|
-
import { Timeline } from "../services/encode/output/Timeline.js";
|
|
32
|
-
import { Output } from "../services/encode/output/Output.js";
|
|
33
|
-
import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
|
|
34
|
-
import { fakeProcess as fakeEncoder, startRunOn } from "./helpers/encode-run.js";
|
|
35
|
-
import { outputSpec } from "./helpers/output-spec.js";
|
|
36
|
-
|
|
37
|
-
const BASE_ID = "aaaaaaaabbbbcccc";
|
|
38
|
-
const STEP_ID = "1111111122223333";
|
|
39
|
-
const AUDIO_ID = "9999999988887777";
|
|
40
|
-
const VIEWER = "viewer-one";
|
|
41
|
-
const SEGMENT_SECONDS = 4;
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* A session shaped like a live one, without the ffmpeg run behind it.
|
|
45
|
-
*
|
|
46
|
-
* @param {{ id: string, dirPath: string, file: SourceFile, encodeHeight?: number,
|
|
47
|
-
* audioOnly?: boolean, isStep?: boolean }} params
|
|
48
|
-
* @returns {object}
|
|
49
|
-
*/
|
|
50
|
-
function fakeSession({ id, dirPath, file, encodeHeight = 0, audioOnly = false, isStep = false }) {
|
|
51
|
-
return {
|
|
52
|
-
id,
|
|
53
|
-
spec: outputSpec({
|
|
54
|
-
sourceKey: file.sourceKey,
|
|
55
|
-
fileIndex: file.fileIndex,
|
|
56
|
-
transcodeVideo: !audioOnly && encodeHeight > 0,
|
|
57
|
-
audioOnly,
|
|
58
|
-
height: encodeHeight
|
|
59
|
-
}),
|
|
60
|
-
dirPath,
|
|
61
|
-
timeline: new Timeline({
|
|
62
|
-
boundaries: Array.from({ length: 101 }, (_, index) => index * SEGMENT_SECONDS),
|
|
63
|
-
cutGrid: "uniform"
|
|
64
|
-
}),
|
|
65
|
-
state: "ready",
|
|
66
|
-
file,
|
|
67
|
-
get inputFile() { return this.file; },
|
|
68
|
-
get audioFile() { return this.file; },
|
|
69
|
-
startedAt: Date.now(),
|
|
70
|
-
lastAccessedAt: Date.now(),
|
|
71
|
-
ffmpeg: null,
|
|
72
|
-
lastError: "",
|
|
73
|
-
segmentFormat: fmp4Format,
|
|
74
|
-
transcodeVideo: !audioOnly,
|
|
75
|
-
transcodeAudio: true,
|
|
76
|
-
audioTrackIndex: 0,
|
|
77
|
-
audioOnly,
|
|
78
|
-
isStep,
|
|
79
|
-
variantHeight: isStep ? encodeHeight : undefined,
|
|
80
|
-
// Left set so that disposal does not remove a directory the other sessions
|
|
81
|
-
// of this output are still named by.
|
|
82
|
-
outputKey: `output-${id}`,
|
|
83
|
-
output: new Output({
|
|
84
|
-
encodeWidth: 0,
|
|
85
|
-
encodeHeight,
|
|
86
|
-
outputFps: 24,
|
|
87
|
-
softwarePreset: null,
|
|
88
|
-
applyTonemap: false
|
|
89
|
-
}),
|
|
90
|
-
encodeRunGeneration: 0,
|
|
91
|
-
failedStartAt: -1,
|
|
92
|
-
failedStartCount: 0,
|
|
93
|
-
seekSettleTimer: null,
|
|
94
|
-
seekTarget: null,
|
|
95
|
-
waitEpoch: 0,
|
|
96
|
-
runs: new Set(),
|
|
97
|
-
usesExplicitCuts: false,
|
|
98
|
-
useSyntheticPlaylist: true,
|
|
99
|
-
playlistText: "#EXTM3U\n",
|
|
100
|
-
segmentCount: 100,
|
|
101
|
-
progress: { state: "running", processedSeconds: 0, startPositionSeconds: 0 }
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* A picture with one quality step and one separately published soundtrack, all
|
|
107
|
-
* of one file, and one viewer watching all three — which is what an ordinary
|
|
108
|
-
* session looks like once the viewer has picked a language and a quality.
|
|
109
|
-
*
|
|
110
|
-
* @returns {Promise<{ manager: object, base: object, step: object,
|
|
111
|
-
* audio: object, dirPath: string, released: string[] }>}
|
|
112
|
-
*/
|
|
113
|
-
async function pictureWithStepAndSoundtrack() {
|
|
114
|
-
const dirPath = await mkdtemp(path.join(os.tmpdir(), "viewer-outputs-"));
|
|
115
|
-
const manager = wireOutputs({
|
|
116
|
-
enabled: true,
|
|
117
|
-
ffmpegBin: "ffmpeg",
|
|
118
|
-
localBindHost: "127.0.0.1",
|
|
119
|
-
localPort: 9090
|
|
120
|
-
});
|
|
121
|
-
const file = new SourceFile({ sourceKey: "source-1", fileIndex: 0, name: "video.mkv" })
|
|
122
|
-
.learn({ width: 1920, height: 1080, fps: 24, bitrateKbps: 8000 });
|
|
123
|
-
const base = fakeSession({ id: BASE_ID, dirPath, file, encodeHeight: 812 });
|
|
124
|
-
const step = fakeSession({ id: STEP_ID, dirPath, file, encodeHeight: 540, isStep: true });
|
|
125
|
-
const audio = fakeSession({ id: AUDIO_ID, dirPath, file, audioOnly: true });
|
|
126
|
-
// The browser never learns these two ids: what keeps them is who watches
|
|
127
|
-
// them, and nothing else.
|
|
128
|
-
for (const session of [base, step, audio]) {
|
|
129
|
-
manager.outputs.set(session.id, session);
|
|
130
|
-
}
|
|
131
|
-
manager.outputs.markStep(step);
|
|
132
|
-
return { manager, base, step, audio, dirPath };
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
test("a viewer who steps down, back to the picture's own height and down again keeps their record", async (t) => {
|
|
136
|
-
const { manager, base, dirPath } = await pictureWithStepAndSoundtrack();
|
|
137
|
-
t.after(() => rm(dirPath, { recursive: true, force: true }));
|
|
138
|
-
startRunOn(base, { process: fakeEncoder() });
|
|
139
|
-
const viewer = manager.viewers.of(base, VIEWER);
|
|
140
|
-
viewer.audio = { trackIndex: 1, transcode: true };
|
|
141
|
-
viewer.position = { segment: 25, seconds: 100, at: Date.now() };
|
|
142
|
-
// The rule chose the step for them at 540p; the record is theirs.
|
|
143
|
-
chooseOutput(manager.viewers, VIEWER, 540, manager.outputs.get(STEP_ID).outputKey);
|
|
144
|
-
|
|
145
|
-
await manager.renditions.resolveVariantFile(BASE_ID, 540, "segment-00025.mp4", VIEWER);
|
|
146
|
-
manager.renditions.viewerPlays(BASE_ID, VIEWER, 540, 100);
|
|
147
|
-
await manager.renditions.resolveVariantFile(BASE_ID, 812, "segment-00026.mp4", VIEWER);
|
|
148
|
-
manager.renditions.viewerPlays(BASE_ID, VIEWER, 812, 104);
|
|
149
|
-
await manager.renditions.resolveVariantFile(BASE_ID, 540, "segment-00027.mp4", VIEWER);
|
|
150
|
-
manager.renditions.viewerPlays(BASE_ID, VIEWER, 540, 108);
|
|
151
|
-
|
|
152
|
-
const known = manager.viewers.forOutput(base).get(VIEWER);
|
|
153
|
-
assert.ok(known, "the picture is the one id the browser holds — a viewer is never dropped from it");
|
|
154
|
-
assert.deepEqual(
|
|
155
|
-
known.audio,
|
|
156
|
-
{ trackIndex: 1, transcode: true },
|
|
157
|
-
"the soundtrack they chose is recorded on the picture, and a quality switch does not touch it"
|
|
158
|
-
);
|
|
159
|
-
assert.equal(known.activeVariantId, STEP_ID, "and the step they moved to is where they are");
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
test("a viewer leaving is subtracted from every output, and one nobody is left watching is let go", async (t) => {
|
|
163
|
-
const { manager, base, step, audio, dirPath } = await pictureWithStepAndSoundtrack();
|
|
164
|
-
t.after(() => rm(dirPath, { recursive: true, force: true }));
|
|
165
|
-
// Watching all three, which is what a viewer who picked a language and a
|
|
166
|
-
// quality is doing.
|
|
167
|
-
manager.viewers.of(base, VIEWER).position = { segment: 25, seconds: 100, at: Date.now() };
|
|
168
|
-
manager.viewers.of(step, VIEWER);
|
|
169
|
-
manager.viewers.of(audio, VIEWER);
|
|
170
|
-
assert.deepEqual(
|
|
171
|
-
[...manager.viewers.of(base, VIEWER).outputs].sort(),
|
|
172
|
-
[BASE_ID, STEP_ID, AUDIO_ID].sort(),
|
|
173
|
-
"one viewer, one object, and it knows all three outputs it is watching"
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
await manager.lifecycle.releaseSessionConsumer(BASE_ID, VIEWER, "the viewer left");
|
|
177
|
-
|
|
178
|
-
assert.equal(manager.outputs.has(BASE_ID), false, "the picture goes with its last consumer");
|
|
179
|
-
assert.equal(
|
|
180
|
-
manager.outputs.has(STEP_ID),
|
|
181
|
-
false,
|
|
182
|
-
"and so does the quality step: nobody is watching it, and no one outside this class knows its id"
|
|
183
|
-
);
|
|
184
|
-
assert.equal(
|
|
185
|
-
manager.outputs.has(AUDIO_ID),
|
|
186
|
-
false,
|
|
187
|
-
"and the soundtrack, for the same reason — it used to sit for half an hour holding an encoder, a directory and a claim on the torrent"
|
|
188
|
-
);
|
|
189
|
-
assert.equal(manager.viewers.size, 0, "and nobody is left in the registry, which must not be a map that only grows");
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
test("an output somebody else is still watching is kept when one viewer leaves", async (t) => {
|
|
193
|
-
const { manager, base, step, audio, dirPath } = await pictureWithStepAndSoundtrack();
|
|
194
|
-
t.after(() => rm(dirPath, { recursive: true, force: true }));
|
|
195
|
-
const second = "viewer-two";
|
|
196
|
-
manager.viewers.of(base, VIEWER).position = { segment: 25, seconds: 100, at: Date.now() };
|
|
197
|
-
manager.viewers.of(step, VIEWER);
|
|
198
|
-
manager.viewers.of(audio, VIEWER);
|
|
199
|
-
// The second viewer is listening to the same soundtrack and watching the
|
|
200
|
-
// picture at its own height.
|
|
201
|
-
manager.viewers.of(base, second).position = { segment: 25, seconds: 100, at: Date.now() };
|
|
202
|
-
manager.viewers.of(audio, second);
|
|
203
|
-
|
|
204
|
-
await manager.lifecycle.releaseSessionConsumer(BASE_ID, VIEWER, "the first viewer left");
|
|
205
|
-
|
|
206
|
-
assert.equal(manager.outputs.has(BASE_ID), true, "the picture stays: it still has a consumer");
|
|
207
|
-
assert.equal(
|
|
208
|
-
manager.outputs.has(AUDIO_ID),
|
|
209
|
-
true,
|
|
210
|
-
"and the soundtrack stays, because having no listeners is what kills it and it has one"
|
|
211
|
-
);
|
|
212
|
-
assert.equal(
|
|
213
|
-
manager.viewers.forOutput(base).has(VIEWER),
|
|
214
|
-
false,
|
|
215
|
-
"the viewer who left is gone from the picture"
|
|
216
|
-
);
|
|
217
|
-
assert.equal(
|
|
218
|
-
manager.viewers.forOutput(audio).has(VIEWER),
|
|
219
|
-
false,
|
|
220
|
-
"and from the soundtrack, which is the half of the relation the viewer holds"
|
|
221
|
-
);
|
|
222
|
-
assert.equal(manager.viewers.forOutput(step).size, 0, "the step they had is watched by nobody");
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
test("nothing is left wanting production once the last viewer has left", async (t) => {
|
|
226
|
-
const { manager, base, step, audio, dirPath } = await pictureWithStepAndSoundtrack();
|
|
227
|
-
t.after(() => rm(dirPath, { recursive: true, force: true }));
|
|
228
|
-
manager.viewers.of(base, VIEWER).position = { segment: 25, seconds: 100, at: Date.now() };
|
|
229
|
-
manager.viewers.of(step, VIEWER);
|
|
230
|
-
manager.viewers.of(audio, VIEWER);
|
|
231
|
-
|
|
232
|
-
await manager.lifecycle.releaseSessionConsumer(BASE_ID, VIEWER, "the viewer left");
|
|
233
|
-
|
|
234
|
-
// NOTHING HAS TO BE RELEASED, and that is the point of the shape. A claim per
|
|
235
|
-
// viewer per output used to be held in the encoding layer and taken back one
|
|
236
|
-
// by one when they left — the viewer's own name as the key of a claim, which
|
|
237
|
-
// is the coupling the layer rule forbids. What the plan is given now is a map
|
|
238
|
-
// with nobody's name in it, rebuilt from whoever is watching; a viewer who has
|
|
239
|
-
// left is simply not in the next one.
|
|
240
|
-
for (const id of [BASE_ID, STEP_ID, AUDIO_ID]) {
|
|
241
|
-
const session = manager.outputs.get(id);
|
|
242
|
-
assert.equal(session, undefined, `${id.slice(0, 8)} is let go with its last viewer`);
|
|
243
|
-
}
|
|
244
|
-
manager.encodeRuns.planEncodersNow();
|
|
245
|
-
for (const address of [base.outputKey, step.outputKey, audio.outputKey]) {
|
|
246
|
-
assert.deepEqual(
|
|
247
|
-
manager.encodeOrchestrator.wantedSegmentsOn(address),
|
|
248
|
-
[],
|
|
249
|
-
"and no output is left asking for an encoder"
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
test("the reverse view is derived from the relation stored on the viewer", () => {
|
|
255
|
-
const viewers = new Viewers();
|
|
256
|
-
const picture = { id: "picture" };
|
|
257
|
-
const soundtrack = { id: "soundtrack" };
|
|
258
|
-
|
|
259
|
-
const viewer = viewers.of(picture, VIEWER);
|
|
260
|
-
viewers.of(soundtrack, VIEWER);
|
|
261
|
-
assert.equal(viewers.of(soundtrack, VIEWER), viewer, "one person is one object, whatever they are watching");
|
|
262
|
-
assert.deepEqual([...viewer.outputs].sort(), ["picture", "soundtrack"]);
|
|
263
|
-
assert.equal(viewers.forOutput(picture).get(VIEWER), viewer);
|
|
264
|
-
|
|
265
|
-
viewers.leaves(soundtrack, VIEWER);
|
|
266
|
-
assert.deepEqual([...viewer.outputs], ["picture"], "the stored relation changes once");
|
|
267
|
-
assert.equal(viewers.forOutput(soundtrack).has(VIEWER), false);
|
|
268
|
-
assert.equal(viewers.size, 1, "and the person is still known, because they are still watching something");
|
|
269
|
-
|
|
270
|
-
viewers.leaves(picture, VIEWER);
|
|
271
|
-
assert.equal(viewers.size, 0, "watching nothing, they are forgotten");
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
test("a request that names nobody makes no viewer", () => {
|
|
275
|
-
// A nameless viewer used to belong to whichever output met it: the same
|
|
276
|
-
// person was a different object on each output, and two nameless people on
|
|
277
|
-
// one output were one object. Every viewer has a name now.
|
|
278
|
-
const viewers = new Viewers();
|
|
279
|
-
const oneFilm = { id: "one" };
|
|
280
|
-
|
|
281
|
-
assert.throws(() => viewers.of(oneFilm, ""), TypeError);
|
|
282
|
-
assert.equal(viewers.size, 0, "nothing was registered");
|
|
283
|
-
assert.equal(viewers.forOutput(oneFilm).size, 0, "and nobody watches the output");
|
|
284
|
-
assert.equal(viewers.getForOutput(oneFilm, ""), null);
|
|
285
|
-
});
|