@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,163 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file The priority map: one number per second of film.
|
|
3
|
-
*
|
|
4
|
-
* The map is literally a map — an array as long as the film, saying how urgently
|
|
5
|
-
* each second is wanted, and beside it how long until somebody plays it. What is
|
|
6
|
-
* checked here is what the numbers MEAN, since only their order is meaningful:
|
|
7
|
-
* in front of a viewer beats behind them, nearer beats further, and two viewers
|
|
8
|
-
* merge to the more urgent of them.
|
|
9
|
-
*
|
|
10
|
-
* Nothing here spawns anything, reads a disk or looks at a clock: this layer is
|
|
11
|
-
* exercised with plain values alone, which is the layer check made executable.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import test from "node:test";
|
|
15
|
-
import assert from "node:assert/strict";
|
|
16
|
-
import {
|
|
17
|
-
emptyMap,
|
|
18
|
-
inWorkingOrder,
|
|
19
|
-
isBehindEverybody,
|
|
20
|
-
mapForViewer,
|
|
21
|
-
mergeMaps,
|
|
22
|
-
runsOf
|
|
23
|
-
} from "../services/viewer/PriorityMap.js";
|
|
24
|
-
|
|
25
|
-
const FILM = 3600;
|
|
26
|
-
const ALLOWANCE = 8;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* @param {number} atSeconds
|
|
30
|
-
* @param {boolean} [playing]
|
|
31
|
-
*/
|
|
32
|
-
function viewer(atSeconds, playing = true) {
|
|
33
|
-
return mapForViewer({
|
|
34
|
-
atSeconds,
|
|
35
|
-
durationSeconds: FILM,
|
|
36
|
-
allowanceSeconds: ALLOWANCE,
|
|
37
|
-
playing
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
test("the map is one entry per second of film", () => {
|
|
42
|
-
const map = viewer(600);
|
|
43
|
-
assert.equal(map.durationSeconds, FILM);
|
|
44
|
-
assert.equal(map.priority.length, FILM);
|
|
45
|
-
assert.equal(map.secondsUntilPlayed.length, FILM);
|
|
46
|
-
assert.equal(map.behind.length, FILM);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test("the nearer a viewer is to a second, the higher its number", () => {
|
|
50
|
-
// The whole of what a priority means: which of two seconds is wanted first.
|
|
51
|
-
const map = viewer(600);
|
|
52
|
-
assert.ok(map.priority[600] > map.priority[700], "their own second beats one a hundred on");
|
|
53
|
-
assert.ok(map.priority[700] > map.priority[3000], "and that beats one forty minutes on");
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test("the time is the distance, because a viewer covers a second of film in a second", () => {
|
|
57
|
-
const map = viewer(600);
|
|
58
|
-
assert.equal(map.secondsUntilPlayed[600], 0, "they are there now");
|
|
59
|
-
assert.equal(map.secondsUntilPlayed[660], 60, "a minute of film away is a minute away");
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test("what is behind a viewer is wanted, and wanted last", () => {
|
|
63
|
-
// Still wanted — a seek back must be cheap — but it yields to everything
|
|
64
|
-
// anybody is walking towards, however far off that is.
|
|
65
|
-
const map = viewer(600);
|
|
66
|
-
assert.equal(map.behind[599], 1);
|
|
67
|
-
assert.equal(map.behind[600], 0);
|
|
68
|
-
assert.ok(isBehindEverybody(map.priority[599]));
|
|
69
|
-
assert.ok(map.priority[599] < map.priority[3599],
|
|
70
|
-
"the last second of the film outranks the second they just watched");
|
|
71
|
-
assert.equal(map.secondsUntilPlayed[599], Number.POSITIVE_INFINITY,
|
|
72
|
-
"nobody is on their way there, so there is no time by which it must exist");
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test("a viewer who has stopped the picture keeps their position, and loses their times", () => {
|
|
76
|
-
// A pause removes the time, not the direction. Collapsed to one flat value
|
|
77
|
-
// over the whole film, as it was, their position disappeared entirely — and
|
|
78
|
-
// with it the rule that what is in front of them is made first.
|
|
79
|
-
const map = viewer(600, false);
|
|
80
|
-
assert.equal(map.behind[599], 1, "what they have watched is still behind them");
|
|
81
|
-
assert.equal(map.behind[600], 0, "and what they have not is still in front");
|
|
82
|
-
assert.ok(map.priority[600] > map.priority[599], "in front still outranks behind");
|
|
83
|
-
assert.ok(map.priority[600] > map.priority[3000], "and nearer still outranks further");
|
|
84
|
-
for (const second of [599, 600, 3000]) {
|
|
85
|
-
assert.equal(map.secondsUntilPlayed[second], Number.POSITIVE_INFINITY,
|
|
86
|
-
"but nothing has a time, because they are on their way nowhere");
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test("anybody who is watching outranks anybody who has stopped", () => {
|
|
91
|
-
// An ordering fact rather than a chosen number: somebody watching needs their
|
|
92
|
-
// next second almost at once, while somebody stopped needs theirs at a time
|
|
93
|
-
// nothing here knows.
|
|
94
|
-
const watching = viewer(600);
|
|
95
|
-
const stopped = viewer(3000, false);
|
|
96
|
-
assert.ok(watching.priority[3599] > stopped.priority[3000],
|
|
97
|
-
"the far tail of a watching viewer beats the very next second of a stopped one");
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
test("two viewers merge to the more urgent of them, second by second", () => {
|
|
101
|
-
const merged = mergeMaps([viewer(600), viewer(1800)]);
|
|
102
|
-
assert.equal(merged.priority[1800], merged.priority[600],
|
|
103
|
-
"each of them is at the top of the scale where they stand");
|
|
104
|
-
assert.equal(merged.secondsUntilPlayed[1800], 0, "the nearer time wins");
|
|
105
|
-
assert.equal(merged.behind[599], 1, "behind both of them is behind");
|
|
106
|
-
assert.equal(merged.behind[700], 0, "and in front of ANYBODY is in front");
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test("the second viewer's position is not buried under the first viewer's distance", () => {
|
|
110
|
-
// The failure this guards: one viewer's far film outranking another viewer's
|
|
111
|
-
// own second, so the second of them is served by nobody.
|
|
112
|
-
const merged = mergeMaps([viewer(600), viewer(1800)]);
|
|
113
|
-
assert.ok(merged.priority[1800] > merged.priority[1799],
|
|
114
|
-
"where the second viewer stands beats the film just before them");
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
test("a film nobody is watching wants nothing", () => {
|
|
118
|
-
const merged = mergeMaps([]);
|
|
119
|
-
assert.equal(merged.durationSeconds, 0);
|
|
120
|
-
assert.deepEqual(runsOf(merged), []);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
test("the map as stretches says the same thing in fewer numbers", () => {
|
|
124
|
-
const runs = runsOf(viewer(600));
|
|
125
|
-
assert.ok(runs.length > 1 && runs.length < 40, "a handful of stretches, not thousands");
|
|
126
|
-
assert.equal(runs[0].from, 0, "starting at the beginning of the film");
|
|
127
|
-
assert.equal(runs[0].behind, true, "which is behind them");
|
|
128
|
-
assert.equal(runs[runs.length - 1].to, FILM, "and ending at its end");
|
|
129
|
-
for (let index = 0; index < runs.length - 1; index += 1) {
|
|
130
|
-
assert.equal(runs[index].to, runs[index + 1].from, "with no gaps");
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
test("the stretches widen with distance, so any film is a handful of them", () => {
|
|
135
|
-
// Near the viewer the difference between now and ten seconds away decides what
|
|
136
|
-
// is made first; twenty minutes out, one more division changes no decision.
|
|
137
|
-
const ahead = runsOf(viewer(0)).filter((run) => run.behind === false);
|
|
138
|
-
for (let index = 1; index < ahead.length; index += 1) {
|
|
139
|
-
assert.ok(ahead[index].to - ahead[index].from >= ahead[index - 1].to - ahead[index - 1].from,
|
|
140
|
-
"each stretch is at least as wide as the one before it");
|
|
141
|
-
}
|
|
142
|
-
assert.ok(ahead[0].to - ahead[0].from <= ALLOWANCE + 1,
|
|
143
|
-
"and the first is as wide as the measured allowance");
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
test("the working order is most urgent first, earliest film within one priority", () => {
|
|
147
|
-
const ordered = inWorkingOrder(runsOf(viewer(600)));
|
|
148
|
-
for (let index = 0; index < ordered.length - 1; index += 1) {
|
|
149
|
-
const left = ordered[index];
|
|
150
|
-
const right = ordered[index + 1];
|
|
151
|
-
assert.ok(
|
|
152
|
-
left.priority > right.priority || (left.priority === right.priority && left.from < right.from),
|
|
153
|
-
"sorted by priority, then by position"
|
|
154
|
-
);
|
|
155
|
-
}
|
|
156
|
-
assert.equal(ordered[0].from, 600, "and the first of all is where the viewer is stopped");
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
test("a map of no length is a statement, and it says nothing is wanted", () => {
|
|
160
|
-
const map = emptyMap(0);
|
|
161
|
-
assert.equal(map.durationSeconds, 0);
|
|
162
|
-
assert.deepEqual(runsOf(map), []);
|
|
163
|
-
});
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import test from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
|
|
4
|
-
import { probeWedgeIsCertain, readProbeState, PROBE_INTERVAL_MS } from "../services/transport/delivery-probe.js";
|
|
5
|
-
|
|
6
|
-
test("a seen-counter bounded lag is not a wedge, however long it lasts", () => {
|
|
7
|
-
// Session 4dcac61b, field log 2026-08-28: gap held at 6-7 probes for 95+
|
|
8
|
-
// seconds on a backgrounded tab, but `seen` kept climbing right along with
|
|
9
|
-
// `sent` — this connection's own history says gaps up to ~3.5 s (7 probes
|
|
10
|
-
// at 500 ms) are ordinary, so the same 3.5 s stuck must not read as certain.
|
|
11
|
-
const verdict = probeWedgeIsCertain({
|
|
12
|
-
stuckForMs: 3400,
|
|
13
|
-
longestHealthySeenGapMs: 3500
|
|
14
|
-
});
|
|
15
|
-
assert.equal(verdict.isCertain, false);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
test("a seen-counter frozen past this connection's own worst legitimate gap is a wedge", () => {
|
|
19
|
-
// Session d85ae4f5, the same field log: `seen` frozen at one value for over
|
|
20
|
-
// a minute while `sent` climbed unbounded — this is the shape the detector
|
|
21
|
-
// exists to catch.
|
|
22
|
-
const verdict = probeWedgeIsCertain({
|
|
23
|
-
stuckForMs: 90_000,
|
|
24
|
-
longestHealthySeenGapMs: 3500
|
|
25
|
-
});
|
|
26
|
-
assert.equal(verdict.isCertain, true);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test("with no healthy history yet, one probe interval is still required", () => {
|
|
30
|
-
const verdict = probeWedgeIsCertain({
|
|
31
|
-
stuckForMs: PROBE_INTERVAL_MS - 1,
|
|
32
|
-
longestHealthySeenGapMs: 0
|
|
33
|
-
});
|
|
34
|
-
assert.equal(verdict.isCertain, false);
|
|
35
|
-
assert.equal(verdict.needMs, PROBE_INTERVAL_MS);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test("readProbeState calls association-stopped only when the unreliable channel agrees", () => {
|
|
39
|
-
// Ordered channels behind, but the unordered/no-retransmit one current:
|
|
40
|
-
// head-of-line blocking in one stream, not the association.
|
|
41
|
-
const streamStuck = readProbeState({
|
|
42
|
-
seq: 100,
|
|
43
|
-
seen: { proxy: 90, "proxy-control": 90, "proxy-fast": 99 },
|
|
44
|
-
labels: ["proxy", "proxy-control", "proxy-fast"],
|
|
45
|
-
echoes: 5,
|
|
46
|
-
echoAgeMs: 100,
|
|
47
|
-
allowed: { proxy: 2, "proxy-control": 2, "proxy-fast": 2 }
|
|
48
|
-
});
|
|
49
|
-
assert.equal(streamStuck.verdict, "stream-stuck");
|
|
50
|
-
|
|
51
|
-
const associationStopped = readProbeState({
|
|
52
|
-
seq: 100,
|
|
53
|
-
seen: { proxy: 90, "proxy-control": 90, "proxy-fast": 90 },
|
|
54
|
-
labels: ["proxy", "proxy-control", "proxy-fast"],
|
|
55
|
-
echoes: 5,
|
|
56
|
-
echoAgeMs: 100,
|
|
57
|
-
allowed: { proxy: 2, "proxy-control": 2, "proxy-fast": 2 }
|
|
58
|
-
});
|
|
59
|
-
assert.equal(associationStopped.verdict, "association-stopped");
|
|
60
|
-
});
|
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file A piece that cannot be played: waited for, or removed and made again.
|
|
3
|
-
*
|
|
4
|
-
* Field 2026-09-03. A run was suspended 548 ms after it started with
|
|
5
|
-
* `segment-00025.mp4` newly opened, so the file stayed at zero bytes. From then
|
|
6
|
-
* on the two halves of the proxy disagreed about it and neither could see the
|
|
7
|
-
* other's reason: the look-ahead counted the NAME, found the numbering unbroken
|
|
8
|
-
* through #83, called it `420s ahead of the viewer` and kept the encoder
|
|
9
|
-
* stopped; the serving path read the FILE, found it short of a track, and
|
|
10
|
-
* waited for a run that had produced nothing to finish it.
|
|
11
|
-
*
|
|
12
|
-
* The viewer's picture stood still for ten minutes on a transport measuring
|
|
13
|
-
* 3-9 ms round trip.
|
|
14
|
-
*
|
|
15
|
-
* The question of WHICH copy answers is gone with the directory-per-run scheme:
|
|
16
|
-
* there is one directory per output, runs are kept apart by their stretches,
|
|
17
|
-
* and one name is one file. What is left is the question that remains real —
|
|
18
|
-
* whether a file that cannot be played is being written now or was left by a
|
|
19
|
-
* run that has ended.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import test from "node:test";
|
|
23
|
-
import assert from "node:assert/strict";
|
|
24
|
-
import { SourceFile } from "../services/media/SourceFile.js";
|
|
25
|
-
import { Timeline } from "../services/encode/output/Timeline.js";
|
|
26
|
-
import { mkdtemp, readdir, rm, writeFile } from "node:fs/promises";
|
|
27
|
-
import os from "node:os";
|
|
28
|
-
import path from "node:path";
|
|
29
|
-
import { managerWithOwnStore } from "./helpers/manager.js";
|
|
30
|
-
import { startRunOn } from "./helpers/encode-run.js";
|
|
31
|
-
import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
|
|
32
|
-
import { outputSpec } from "./helpers/output-spec.js";
|
|
33
|
-
|
|
34
|
-
const MOVIE_TIMESCALE = 1000;
|
|
35
|
-
const VIDEO_TIMESCALE = 90_000;
|
|
36
|
-
const AUDIO_TIMESCALE = 48_000;
|
|
37
|
-
const SEGMENT_SECONDS = 12.5;
|
|
38
|
-
const SESSION_ID = "aef21c88a8d64a9a";
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* @param {string} type
|
|
42
|
-
* @param {Buffer} body
|
|
43
|
-
* @returns {Buffer}
|
|
44
|
-
*/
|
|
45
|
-
function box(type, body) {
|
|
46
|
-
const head = Buffer.alloc(8);
|
|
47
|
-
head.writeUInt32BE(8 + body.length, 0);
|
|
48
|
-
head.write(type, 4, "latin1");
|
|
49
|
-
return Buffer.concat([head, body]);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* @param {number} offsetSeconds
|
|
54
|
-
* @returns {Buffer}
|
|
55
|
-
*/
|
|
56
|
-
function emptyEdit(offsetSeconds) {
|
|
57
|
-
const body = Buffer.alloc(16);
|
|
58
|
-
body.writeUInt32BE(1, 4);
|
|
59
|
-
body.writeUInt32BE(Math.round(offsetSeconds * MOVIE_TIMESCALE), 8);
|
|
60
|
-
body.writeInt32BE(-1, 12);
|
|
61
|
-
return box("elst", body);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* @param {number} trackId
|
|
66
|
-
* @param {number} timescale
|
|
67
|
-
* @param {number} offsetSeconds
|
|
68
|
-
* @returns {Buffer}
|
|
69
|
-
*/
|
|
70
|
-
function trak(trackId, timescale, offsetSeconds) {
|
|
71
|
-
const tkhdBody = Buffer.alloc(84);
|
|
72
|
-
tkhdBody.writeUInt32BE(trackId, 12);
|
|
73
|
-
const mdhdBody = Buffer.alloc(20);
|
|
74
|
-
mdhdBody.writeUInt32BE(timescale, 12);
|
|
75
|
-
return box("trak", Buffer.concat([
|
|
76
|
-
box("tkhd", tkhdBody),
|
|
77
|
-
box("edts", emptyEdit(offsetSeconds)),
|
|
78
|
-
box("mdia", box("mdhd", mdhdBody))
|
|
79
|
-
]));
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* @param {number} trackId
|
|
84
|
-
* @returns {Buffer}
|
|
85
|
-
*/
|
|
86
|
-
function traf(trackId) {
|
|
87
|
-
const tfhdBody = Buffer.alloc(8);
|
|
88
|
-
tfhdBody.writeUInt32BE(trackId, 4);
|
|
89
|
-
const tfdtBody = Buffer.alloc(12);
|
|
90
|
-
tfdtBody.writeUInt8(1, 0);
|
|
91
|
-
tfdtBody.writeBigUInt64BE(0n, 4);
|
|
92
|
-
return box("traf", Buffer.concat([box("tfhd", tfhdBody), box("tfdt", tfdtBody)]));
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* A piece shaped like the `segment` muxer's output, carrying both tracks.
|
|
97
|
-
*
|
|
98
|
-
* @param {number} offsetSeconds
|
|
99
|
-
* @returns {Buffer}
|
|
100
|
-
*/
|
|
101
|
-
function wholePiece(offsetSeconds) {
|
|
102
|
-
const mvhdBody = Buffer.alloc(100);
|
|
103
|
-
mvhdBody.writeUInt32BE(MOVIE_TIMESCALE, 12);
|
|
104
|
-
const moov = box("moov", Buffer.concat([
|
|
105
|
-
box("mvhd", mvhdBody),
|
|
106
|
-
trak(1, VIDEO_TIMESCALE, offsetSeconds),
|
|
107
|
-
trak(2, AUDIO_TIMESCALE, offsetSeconds)
|
|
108
|
-
]));
|
|
109
|
-
const moof = box("moof", Buffer.concat([traf(1), traf(2)]));
|
|
110
|
-
return Buffer.concat([
|
|
111
|
-
box("ftyp", Buffer.alloc(16, 0)),
|
|
112
|
-
moov,
|
|
113
|
-
moof,
|
|
114
|
-
box("mdat", Buffer.alloc(64, 0x5a)),
|
|
115
|
-
box("mfra", Buffer.alloc(24, 0))
|
|
116
|
-
]);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* A session with the output directory every run writes into.
|
|
122
|
-
*
|
|
123
|
-
* @returns {Promise<{ manager: object, session: object, dirPath: string }>}
|
|
124
|
-
*/
|
|
125
|
-
async function sessionOnOneDirectory() {
|
|
126
|
-
const dirPath = await mkdtemp(path.join(os.tmpdir(), "produced-copy-"));
|
|
127
|
-
// Its own store root — see `helpers/manager.js` for what sharing one cost.
|
|
128
|
-
const { manager } = managerWithOwnStore();
|
|
129
|
-
const session = {
|
|
130
|
-
id: SESSION_ID,
|
|
131
|
-
spec: outputSpec({ transcodeVideo: false }),
|
|
132
|
-
dirPath,
|
|
133
|
-
// Where this file is cut, held by the file. A fixture that stated it
|
|
134
|
-
// on the session was describing what production no longer does.
|
|
135
|
-
timeline: new Timeline({
|
|
136
|
-
boundaries: [0, SEGMENT_SECONDS, 25, 37.5, 50],
|
|
137
|
-
cutGrid: "uniform"
|
|
138
|
-
}),
|
|
139
|
-
state: "ready",
|
|
140
|
-
file: new SourceFile({ sourceKey: "source-1", fileIndex: 0, name: "Drifters - 04.mkv" }),
|
|
141
|
-
// An ordinary session reads its own file, and its sound is inside it. The
|
|
142
|
-
// three differ only for a soundtrack shipped as a file of its own.
|
|
143
|
-
get inputFile() { return this.file; },
|
|
144
|
-
get audioFile() { return this.file; },
|
|
145
|
-
startedAt: Date.now(),
|
|
146
|
-
lastAccessedAt: Date.now(),
|
|
147
|
-
runs: new Set(),
|
|
148
|
-
lastError: "",
|
|
149
|
-
segmentFormat: fmp4Format,
|
|
150
|
-
useSyntheticPlaylist: true,
|
|
151
|
-
playlistText: "#EXTM3U\n",
|
|
152
|
-
initBytes: fmp4Format.extractInit(wholePiece(0)),
|
|
153
|
-
waitEpoch: 0
|
|
154
|
-
};
|
|
155
|
-
manager.outputs.set(SESSION_ID, session);
|
|
156
|
-
// A run exists and is alive — the state the field case was in, and the one in
|
|
157
|
-
// which the old test called every leftover "still being written".
|
|
158
|
-
startRunOn(session, { from: 0, usesExplicitCuts: true });
|
|
159
|
-
return { manager, session, dirPath };
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
test("a leftover of a run that has ended is not served, and answering does not delete it", async (t) => {
|
|
165
|
-
const { manager, session, dirPath } = await sessionOnOneDirectory();
|
|
166
|
-
t.after(async () => {
|
|
167
|
-
await manager.lifecycle.disposeAll();
|
|
168
|
-
await rm(dirPath, { recursive: true, force: true });
|
|
169
|
-
});
|
|
170
|
-
// Opened by a run that is gone. It has a name and no bytes, so there is
|
|
171
|
-
// nothing to serve.
|
|
172
|
-
session.runs = new Set();
|
|
173
|
-
await writeFile(path.join(dirPath, "segment-00001.mp4"), Buffer.alloc(0));
|
|
174
|
-
|
|
175
|
-
const result = await manager.serving.getFileStream(SESSION_ID, "segment-00001.mp4");
|
|
176
|
-
|
|
177
|
-
assert.equal(result.kind, "warming-up", "nothing servable exists yet, so the viewer waits");
|
|
178
|
-
// AND ANSWERING A REQUEST DELETES NOTHING. It used to: this test was written
|
|
179
|
-
// when the serving path cleared up after ended runs, and several runs of an
|
|
180
|
-
// output share one directory — so what a request removed could be the piece a
|
|
181
|
-
// LIVE run had just finished. Clearing up belongs to the ending of a run,
|
|
182
|
-
// which is the only thing that knows what it left open and what it named
|
|
183
|
-
// and it knows because the piece is under that run's own working name
|
|
184
|
-
// (`SegmentStore.clearUpAfter`, checked in `segment-store.test.js`). A file
|
|
185
|
-
// with no bytes is
|
|
186
|
-
// ignored by everything that reads the directory, so leaving it costs nothing
|
|
187
|
-
// and deleting it from here costs a segment.
|
|
188
|
-
const left = (await readdir(dirPath)).filter((name) => name === "segment-00001.mp4");
|
|
189
|
-
assert.deepEqual(left, ["segment-00001.mp4"]);
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
test("the current run's own unfinished piece is waited for, never deleted", async (t) => {
|
|
193
|
-
const { manager, session, dirPath } = await sessionOnOneDirectory();
|
|
194
|
-
t.after(async () => {
|
|
195
|
-
await manager.lifecycle.disposeAll();
|
|
196
|
-
await rm(dirPath, { recursive: true, force: true });
|
|
197
|
-
});
|
|
198
|
-
// The same file, in the directory of the run that is alive. It is being
|
|
199
|
-
// written right now. Deleting it is the 2026-08-06 incident: #225 was removed
|
|
200
|
-
// 14 s into the run producing it, which then wrote on into a file nobody
|
|
201
|
-
// could open, and the segment never appeared.
|
|
202
|
-
//
|
|
203
|
-
// The live run begins at #1, so #1 is its own first piece.
|
|
204
|
-
startRunOn(session, { from: 1, producing: false, usesExplicitCuts: true });
|
|
205
|
-
await writeFile(path.join(dirPath, "segment-00001.mp4"), Buffer.alloc(0));
|
|
206
|
-
|
|
207
|
-
const result = await manager.serving.getFileStream(SESSION_ID, "segment-00001.mp4");
|
|
208
|
-
|
|
209
|
-
assert.equal(result.kind, "warming-up");
|
|
210
|
-
const left = (await readdir(dirPath)).filter((name) => name === "segment-00001.mp4");
|
|
211
|
-
assert.deepEqual(left, ["segment-00001.mp4"], "the live run's own output must be left alone");
|
|
212
|
-
});
|
|
213
|
-
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file A segment must be stamped where the playlist the player holds says.
|
|
3
|
-
*
|
|
4
|
-
* The field case this pins, 2026-08-17: a seek to 1590.4 s restarted the audio
|
|
5
|
-
* rendition at #291; the produced segments #292 and #293 carried their own
|
|
6
|
-
* positions, 1587.892 s and 1592.692 s, while the playlist the browser was
|
|
7
|
-
* holding said 1585.376 s and 1590.585 s. The browser fetched those two
|
|
8
|
-
* segments **1908 times each** over ten minutes, every one served in 4 ms, and
|
|
9
|
-
* the film never moved: a fragment appended more than `maxBufferHole` away from
|
|
10
|
-
* where the playlist put it is not recognised as buffered, so the player asks
|
|
11
|
-
* for it again.
|
|
12
|
-
*
|
|
13
|
-
* The rule, and the reason it is not simply "always use the file's own time":
|
|
14
|
-
* the boundary table keeps being corrected from produced segments, and the
|
|
15
|
-
* playlist does not. Whichever is right about the FILE, only one of them was
|
|
16
|
-
* sent to the player.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import assert from "node:assert/strict";
|
|
20
|
-
import test from "node:test";
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* hls.js's own default. A gap smaller than this is skipped; a larger one is a
|
|
24
|
-
* hole, and the fragment across it counts as not loaded.
|
|
25
|
-
*/
|
|
26
|
-
const PLAYER_BUFFER_HOLE_SEC = 0.5;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* The decision under test, in the shape the session manager applies it.
|
|
30
|
-
*
|
|
31
|
-
* @param {{ trueStart: number | null, publishedStart: number }} reading
|
|
32
|
-
* @returns {{ stamp: number, followedPlaylist: boolean }}
|
|
33
|
-
*/
|
|
34
|
-
function stampFor({ trueStart, publishedStart }) {
|
|
35
|
-
if (trueStart === null) {
|
|
36
|
-
return { stamp: publishedStart, followedPlaylist: true };
|
|
37
|
-
}
|
|
38
|
-
if (Math.abs(trueStart - publishedStart) > PLAYER_BUFFER_HOLE_SEC) {
|
|
39
|
-
return { stamp: publishedStart, followedPlaylist: true };
|
|
40
|
-
}
|
|
41
|
-
return { stamp: trueStart, followedPlaylist: false };
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
test("the field case: a segment far from its playlist position is stamped where the playlist says", () => {
|
|
45
|
-
const segment292 = stampFor({ trueStart: 1587.892, publishedStart: 1585.376 });
|
|
46
|
-
assert.equal(segment292.stamp, 1585.376);
|
|
47
|
-
assert.equal(segment292.followedPlaylist, true);
|
|
48
|
-
|
|
49
|
-
const segment293 = stampFor({ trueStart: 1592.692, publishedStart: 1590.585 });
|
|
50
|
-
assert.equal(segment293.stamp, 1590.585);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test("a file whose index is honest keeps its own position", () => {
|
|
54
|
-
// Within what a player bridges, the file's own figure is the truthful one and
|
|
55
|
-
// it is what keeps speech and subtitles together (2026-08-06, 4.17 s of drift
|
|
56
|
-
// when the picture was stamped from a lying index).
|
|
57
|
-
const reading = stampFor({ trueStart: 120.13, publishedStart: 120.0 });
|
|
58
|
-
assert.equal(reading.stamp, 120.13);
|
|
59
|
-
assert.equal(reading.followedPlaylist, false);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test("the boundary is exactly the player's own tolerance", () => {
|
|
63
|
-
assert.equal(stampFor({ trueStart: 10.5, publishedStart: 10.0 }).stamp, 10.5);
|
|
64
|
-
assert.equal(stampFor({ trueStart: 10.51, publishedStart: 10.0 }).stamp, 10.0);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test("a segment that does not say where it begins is stamped from the playlist", () => {
|
|
68
|
-
assert.equal(stampFor({ trueStart: null, publishedStart: 42.0 }).stamp, 42.0);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test("a corrected boundary does not move the stamp of a session already playing", () => {
|
|
72
|
-
// The correction writes into the live table; the published one is frozen. If
|
|
73
|
-
// the stamp followed the live table, every correction would move segments
|
|
74
|
-
// under a player holding the original playlist — which is the same failure
|
|
75
|
-
// seen from the other side.
|
|
76
|
-
const published = [0, 4, 8, 12];
|
|
77
|
-
const live = [...published];
|
|
78
|
-
live[2] = 9.7; // corrected from a produced segment
|
|
79
|
-
const publishedStart = published[2];
|
|
80
|
-
const reading = stampFor({ trueStart: live[2], publishedStart });
|
|
81
|
-
assert.equal(publishedStart, 8);
|
|
82
|
-
assert.equal(reading.stamp, 8, "the player was told 8 s and must be given 8 s");
|
|
83
|
-
});
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Which heights are on the menu, and when that answer may be reused.
|
|
3
|
-
*
|
|
4
|
-
* The arithmetic is `EncodeCost` and is checked next door. What is checked here
|
|
5
|
-
* is the part that is not arithmetic: whose answer it is, what may never be
|
|
6
|
-
* withdrawn, and the cache — which is the whole reason this is an object, and
|
|
7
|
-
* which had no check of any kind while it lived in the session manager.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import test from "node:test";
|
|
11
|
-
import assert from "node:assert/strict";
|
|
12
|
-
import { EncodeCost } from "../services/encode/quality/EncodeCost.js";
|
|
13
|
-
import { QualityOffer } from "../services/encode/quality/QualityOffer.js";
|
|
14
|
-
import { startRunOn } from "./helpers/encode-run.js";
|
|
15
|
-
import { outputSpec } from "./helpers/output-spec.js";
|
|
16
|
-
import { runStateOf } from "../services/encode/encode-run-state.js";
|
|
17
|
-
import { qualityStateOf } from "../services/encode/quality/OutputQualityState.js";
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* A session with one run going.
|
|
21
|
-
*
|
|
22
|
-
* @returns {object}
|
|
23
|
-
*/
|
|
24
|
-
function sessionProducing() {
|
|
25
|
-
const session = {
|
|
26
|
-
id: "1111111122223333",
|
|
27
|
-
state: "ready",
|
|
28
|
-
spec: outputSpec(),
|
|
29
|
-
file: { key: "torrent:abc:0", name: "film.mkv", width: 1920, height: 1080 },
|
|
30
|
-
output: { encodeWidth: 0, encodeHeight: 0, outputFps: 25, softwarePreset: null },
|
|
31
|
-
progress: { processedSeconds: 0 },
|
|
32
|
-
runs: new Set()
|
|
33
|
-
};
|
|
34
|
-
startRunOn(session, { from: 0, speedX: 1 });
|
|
35
|
-
return session;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* A picture, and the offer's cache in play.
|
|
40
|
-
*
|
|
41
|
-
* @param {object} [supply]
|
|
42
|
-
* @returns {{ offer: QualityOffer, picture: object, computed: () => number, supply: object }}
|
|
43
|
-
*/
|
|
44
|
-
function offerOver(
|
|
45
|
-
supply = { requiredSpeed: null, megabytesPerSecond: null, costPerMegabyte: null },
|
|
46
|
-
occupancyKnownFor = () => true
|
|
47
|
-
) {
|
|
48
|
-
const picture = sessionProducing();
|
|
49
|
-
const outputs = {
|
|
50
|
-
familyOf: () => [picture],
|
|
51
|
-
pictureOf: () => picture,
|
|
52
|
-
variantHeightOf: () => 0,
|
|
53
|
-
sessionsOn: () => []
|
|
54
|
-
};
|
|
55
|
-
const cost = new EncodeCost({
|
|
56
|
-
outputs,
|
|
57
|
-
host: () => ({ benchmark: null, decodeModel: null, contentionPenalties: null, availability: null }),
|
|
58
|
-
runningEncoders: () => 0,
|
|
59
|
-
encodersRunningNow: () => 0,
|
|
60
|
-
torrentCostSecFor: () => 0,
|
|
61
|
-
runsFor: (session) => [...(session.runs ?? [])],
|
|
62
|
-
stateFor: (session) => runStateOf(session.runs)
|
|
63
|
-
});
|
|
64
|
-
let computed = 0;
|
|
65
|
-
const real = cost.sustainableHeights.bind(cost);
|
|
66
|
-
cost.sustainableHeights = (params) => {
|
|
67
|
-
computed += 1;
|
|
68
|
-
return real(params);
|
|
69
|
-
};
|
|
70
|
-
const offer = new QualityOffer({
|
|
71
|
-
encodeCost: cost,
|
|
72
|
-
outputs,
|
|
73
|
-
stateFor: (session) => runStateOf(session.runs),
|
|
74
|
-
heightsOnScreen: () => [],
|
|
75
|
-
supplyFor: () => supply,
|
|
76
|
-
occupancyKnownFor
|
|
77
|
-
});
|
|
78
|
-
return { offer, picture, computed: () => computed, supply };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
test("the pool gets no positive answer while another occupied output has no measured cost", () => {
|
|
82
|
-
const { offer } = offerOver(undefined, () => false);
|
|
83
|
-
|
|
84
|
-
assert.equal(
|
|
85
|
-
offer.predictOfferedHeights({ width: 1920, height: 1080, fps: 25, bitrateKbps: 5000 }),
|
|
86
|
-
null
|
|
87
|
-
);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test("the offer is computed once and then answered from the cache", () => {
|
|
91
|
-
// It is asked on the path that serves every playlist, every init and every
|
|
92
|
-
// segment. Recomputing it each time is the arithmetic of the whole ladder per
|
|
93
|
-
// request, and its refusal line written for the life of the film.
|
|
94
|
-
const { offer, picture, computed } = offerOver();
|
|
95
|
-
|
|
96
|
-
offer.offeredHeightsFor(picture);
|
|
97
|
-
offer.offeredHeightsFor(picture);
|
|
98
|
-
|
|
99
|
-
assert.equal(computed(), 1);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
test("what the swarm is doing is part of what identifies that cached answer", () => {
|
|
103
|
-
// The bar rises when the reader meets interruptions and the torrent's price
|
|
104
|
-
// moves every few seconds. Left out of the key, a menu computed while nothing
|
|
105
|
-
// was known about the swarm would stand for the whole film — offering steps
|
|
106
|
-
// supply cannot support, and passing every route guard on the way.
|
|
107
|
-
const supply = { requiredSpeed: null, megabytesPerSecond: null, costPerMegabyte: null };
|
|
108
|
-
const { offer, picture, computed } = offerOver(supply);
|
|
109
|
-
offer.offeredHeightsFor(picture);
|
|
110
|
-
|
|
111
|
-
supply.requiredSpeed = 6.12;
|
|
112
|
-
offer.offeredHeightsFor(picture);
|
|
113
|
-
assert.equal(computed(), 2, "the bar this file's own interruptions demand moved");
|
|
114
|
-
|
|
115
|
-
supply.megabytesPerSecond = 1.4;
|
|
116
|
-
offer.offeredHeightsFor(picture);
|
|
117
|
-
assert.equal(computed(), 3, "and so did what a viewer draws through it");
|
|
118
|
-
|
|
119
|
-
supply.costPerMegabyte = 0.05;
|
|
120
|
-
offer.offeredHeightsFor(picture);
|
|
121
|
-
assert.equal(computed(), 4, "and what a megabyte costs this process");
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test("and so is what each running encode was last seen doing", () => {
|
|
125
|
-
// It is an input twice over — it withdraws a step measured below realtime,
|
|
126
|
-
// and it prices every running picture in the committed total. On a COPIED
|
|
127
|
-
// picture nothing else in the key ever moves, so without this the menu would
|
|
128
|
-
// be pinned to what was computed before anything had been measured.
|
|
129
|
-
const { offer, picture, computed } = offerOver();
|
|
130
|
-
offer.offeredHeightsFor(picture);
|
|
131
|
-
|
|
132
|
-
qualityStateOf(picture).lastAloneSpeed = 0.4;
|
|
133
|
-
offer.offeredHeightsFor(picture);
|
|
134
|
-
|
|
135
|
-
assert.equal(computed(), 2);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
test("a step asking for the family's answer does not keep it as its own", () => {
|
|
139
|
-
// A step is a session of its own and knows only its own encode: asked while
|
|
140
|
-
// the viewer watches 240p, the 240p session priced the 1080p step as a
|
|
141
|
-
// re-encode — because ITS video is re-encoded — and refused it on a host that
|
|
142
|
-
// had been serving that very height by COPY minutes earlier. Only the picture
|
|
143
|
-
// knows what the family can do with the source, so the picture answers; and
|
|
144
|
-
// the answer must not then be filed on the asker, whose own flags are what
|
|
145
|
-
// made the wrong answer possible in the first place.
|
|
146
|
-
const { offer, picture } = offerOver();
|
|
147
|
-
const step = sessionProducing();
|
|
148
|
-
step.id = "9999999988887777";
|
|
149
|
-
step.spec = outputSpec({ transcodeVideo: true });
|
|
150
|
-
|
|
151
|
-
const answer = offer.offeredHeightsFor(step);
|
|
152
|
-
|
|
153
|
-
assert.ok(Array.isArray(answer), "the step is still answered — its viewer is watching it");
|
|
154
|
-
assert.equal(qualityStateOf(step).offeredHeightsCache, undefined, "and nothing is filed on the asker");
|
|
155
|
-
assert.equal(
|
|
156
|
-
qualityStateOf(picture).offeredHeightsCache,
|
|
157
|
-
undefined,
|
|
158
|
-
"nor on the picture, whose own key was never the one this answer was computed against"
|
|
159
|
-
);
|
|
160
|
-
});
|