@torrent-tv/proxy 2.89.16 → 2.89.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -3
- package/package.json +10 -2
- package/routes/api/client-logs/post.js +9 -2
- package/server.js +0 -11
- package/.claude/commands/opsx/apply.md +0 -155
- package/.claude/commands/opsx/archive.md +0 -160
- package/.claude/commands/opsx/explore.md +0 -174
- package/.claude/commands/opsx/propose.md +0 -109
- package/.claude/commands/opsx/sync.md +0 -143
- package/.claude/skills/openspec-apply-change/SKILL.md +0 -159
- package/.claude/skills/openspec-archive-change/SKILL.md +0 -117
- package/.claude/skills/openspec-explore/SKILL.md +0 -289
- package/.claude/skills/openspec-propose/SKILL.md +0 -113
- package/.claude/skills/openspec-sync-specs/SKILL.md +0 -147
- package/.dockerignore +0 -5
- package/AGENTS.md +0 -363
- package/CHANGELOG.md +0 -2373
- package/Dockerfile +0 -30
- package/biome.json +0 -412
- package/corpus.tmp.json +0 -1
- package/docs/browser-proxy-contract.md +0 -155
- package/docs/container-architecture.md +0 -273
- package/docs/download-architecture.md +0 -261
- package/docs/encode-architecture.md +0 -765
- package/docs/encode-run-state.md +0 -96
- package/docs/logs.md +0 -138
- package/docs/playback-start-forecast.md +0 -113
- package/docs/socks5-egress-research.md +0 -114
- package/docs/storage-architecture.md +0 -273
- package/knip.json +0 -14
- package/main2.cpuprofile +0 -1
- package/openspec/changes/adaptive-bitrate/.openspec.yaml +0 -0
- package/openspec/changes/adaptive-bitrate/design.md +0 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +0 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +0 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +0 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +0 -2
- package/openspec/changes/binary-distribution/proposal.md +0 -53
- package/openspec/changes/chunked-request-bodies/design.md +0 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +0 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +0 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +0 -40
- package/openspec/changes/cold-start/design.md +0 -98
- package/openspec/changes/cold-start/proposal.md +0 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +0 -47
- package/openspec/changes/cold-start/tasks.md +0 -48
- package/openspec/changes/disk-cap/.openspec.yaml +0 -2
- package/openspec/changes/disk-cap/proposal.md +0 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +0 -25
- package/openspec/changes/disk-cap/tasks.md +0 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +0 -2
- package/openspec/changes/proxy-observability/design.md +0 -38
- package/openspec/changes/proxy-observability/proposal.md +0 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +0 -33
- package/openspec/changes/proxy-observability/tasks.md +0 -21
- package/openspec/changes/seek-debounce/.openspec.yaml +0 -0
- package/openspec/changes/seek-debounce/design.md +0 -118
- package/openspec/changes/seek-debounce/proposal.md +0 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +0 -41
- package/openspec/changes/seek-debounce/tasks.md +0 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +0 -2
- package/openspec/changes/subtitle-language/proposal.md +0 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +0 -34
- package/openspec/changes/subtitle-language/tasks.md +0 -26
- package/openspec/changes/track-selection/.openspec.yaml +0 -2
- package/openspec/changes/track-selection/design.md +0 -47
- package/openspec/changes/track-selection/proposal.md +0 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +0 -41
- package/openspec/changes/track-selection/tasks.md +0 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +0 -2
- package/openspec/changes/transcode-quality/proposal.md +0 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +0 -139
- package/openspec/changes/transcode-quality/tasks.md +0 -63
- package/openspec/config.yaml +0 -38
- package/public/.well-known/appspecific/com.chrome.devtools.json +0 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +0 -329
- package/research/double-spawn-2026-09-10.md +0 -171
- package/research/handover-2026-09-13.md +0 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +0 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +0 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +0 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +0 -225
- package/research/valgrind-replica-2026-08-27/logs.tar.gz +0 -0
- package/scripts/build-audio-samples.mjs +0 -31
- package/scripts/render-run-graph.js +0 -173
- package/test/asset-paths.test.js +0 -28
- package/test/audio-buffer-continuity.test.js +0 -77
- package/test/audio-calibration.test.js +0 -23
- package/test/audio-inventory.test.js +0 -212
- package/test/audio-track-bitrate.test.js +0 -40
- package/test/audio-warm-recorded.test.js +0 -63
- package/test/auto-quality-step.test.js +0 -762
- package/test/available-share.test.js +0 -76
- package/test/background-fill.test.js +0 -190
- package/test/client-log-file.test.js +0 -126
- package/test/coalesce.test.js +0 -39
- package/test/completed-files.test.js +0 -115
- package/test/concurrent-cost.test.js +0 -148
- package/test/container-media-info.test.js +0 -228
- package/test/contention.test.js +0 -76
- package/test/core-dumps.test.js +0 -57
- package/test/coverage-follows-the-disk.test.js +0 -233
- package/test/coverage-map.test.js +0 -195
- package/test/cue-push-carries-what-the-walk-found.test.js +0 -58
- package/test/cut-times-timeline.test.js +0 -65
- package/test/cuts-follow-published-grid.test.js +0 -109
- package/test/data-channel-frame.test.js +0 -63
- package/test/data-channel-viewer-lifecycle.test.js +0 -193
- package/test/dead-torrent-handle.test.js +0 -52
- package/test/declared-rates.test.js +0 -116
- package/test/declared-tracks.test.js +0 -43
- package/test/decode-cost-fit.test.js +0 -202
- package/test/decode-cost.test.js +0 -475
- package/test/delivery-probe-field.test.js +0 -173
- package/test/delivery-probe.test.js +0 -382
- package/test/demand-register.test.js +0 -206
- package/test/dht-bootstrap.test.js +0 -72
- package/test/drain-threat.test.js +0 -65
- package/test/duplicate-add.test.js +0 -48
- package/test/encode-admission.test.js +0 -170
- package/test/encode-cost.test.js +0 -275
- package/test/encode-exit.test.js +0 -111
- package/test/encode-orchestrator.test.js +0 -426
- package/test/encode-plan-viewers.test.js +0 -725
- package/test/encode-plan.test.js +0 -545
- package/test/encode-run-state.test.js +0 -314
- package/test/encode-run.test.js +0 -248
- package/test/encode-slope.test.js +0 -46
- package/test/encoded-output.test.js +0 -39
- package/test/encoder-kinds.test.js +0 -122
- package/test/encoder-readings.test.js +0 -68
- package/test/episode-naming.test.js +0 -131
- package/test/every-claimant-is-told-its-share.test.js +0 -152
- package/test/failed-encoder-still-serves-what-exists.test.js +0 -354
- package/test/failed-start-breaker.test.js +0 -202
- package/test/fastest-wires.test.js +0 -143
- package/test/ffmpeg-stream-counts.test.js +0 -101
- package/test/file-edges.test.js +0 -191
- package/test/files-in-use.test.js +0 -34
- package/test/fixtures/audio-frame-boundary.json +0 -1
- package/test/fixtures/refused-audio-20261002.json +0 -10
- package/test/fmp4-presentation-clock.test.js +0 -148
- package/test/fmp4-self-contained.test.js +0 -80
- package/test/fmp4-timestamps.test.js +0 -140
- package/test/fmp4-track-completeness.test.js +0 -41
- package/test/format-keyed-outputs.test.js +0 -78
- package/test/health-metrics.test.js +0 -36
- package/test/held-read-ownership.test.js +0 -77
- package/test/held-request-width.test.js +0 -157
- package/test/helpers/encode-run.js +0 -248
- package/test/helpers/held-file.js +0 -77
- package/test/helpers/manager.js +0 -62
- package/test/helpers/matroska-file.js +0 -296
- package/test/helpers/output-spec.js +0 -63
- package/test/host-load.test.js +0 -68
- package/test/index-accuracy-is-about-the-file.test.js +0 -128
- package/test/init-compat.test.js +0 -219
- package/test/input-lost-quiets-the-plan.test.js +0 -261
- package/test/input-unavailable.test.js +0 -48
- package/test/installation-id.test.js +0 -39
- package/test/keeping-period.test.js +0 -83
- package/test/keyframe-index-accuracy.test.js +0 -188
- package/test/keyframe-table-budget.test.js +0 -123
- package/test/keyframes-belong-to-the-file.test.js +0 -249
- package/test/learned-median.test.js +0 -37
- package/test/letting-go-asks-who-is-watching.test.js +0 -191
- package/test/limit-row-by-area.test.js +0 -60
- package/test/link-budget.test.js +0 -111
- package/test/link-probe.test.js +0 -56
- package/test/local-encoding-data.test.js +0 -273
- package/test/logger-repeats.test.js +0 -134
- package/test/look-ahead-coverage.test.js +0 -56
- package/test/manager-surface.test.js +0 -291
- package/test/matroska-blocks.test.js +0 -0
- package/test/matroska-cluster-discovery.test.js +0 -323
- package/test/matroska-cues-track.test.js +0 -192
- package/test/media-time.test.js +0 -62
- package/test/memory-budget.test.js +0 -390
- package/test/move-cost.test.js +0 -216
- package/test/mp4-composition-times.test.js +0 -0
- package/test/mp4-subtitles.test.js +0 -173
- package/test/one-authority.test.js +0 -285
- package/test/one-budget.test.js +0 -149
- package/test/one-output-one-session.test.js +0 -273
- package/test/one-resource-divided.test.js +0 -185
- package/test/one-serving-output-per-height.test.js +0 -458
- package/test/orchestrator-wired.test.js +0 -207
- package/test/output-catalog.test.js +0 -39
- package/test/output-shape.test.js +0 -21
- package/test/output-spec.test.js +0 -251
- package/test/output-speed.test.js +0 -92
- package/test/outputs-being-prepared.test.js +0 -40
- package/test/packet-witness-ring.test.js +0 -236
- package/test/packet-witness.test.js +0 -148
- package/test/piece-completeness.test.js +0 -78
- package/test/piece-disk-store.test.js +0 -401
- package/test/piece-from-whole-file.test.js +0 -129
- package/test/piece-lru.test.js +0 -319
- package/test/piece-reader.test.js +0 -162
- package/test/piece-selection-offset.test.js +0 -85
- package/test/piece-store-eviction.test.js +0 -595
- package/test/piece-store-never-refuses.test.js +0 -153
- package/test/piece-store-reservations.test.js +0 -403
- package/test/piece-store-slow-disk.test.js +0 -159
- package/test/piece-tail.test.js +0 -97
- package/test/plan-host-timings.test.js +0 -68
- package/test/plan-is-steady.test.js +0 -225
- package/test/playback-readiness.test.js +0 -518
- package/test/playback-start-regressions.test.js +0 -101
- package/test/priority-map-download.test.js +0 -174
- package/test/priority-map-emptied.test.js +0 -204
- package/test/priority-map-per-output.test.js +0 -215
- package/test/priority-map.test.js +0 -163
- package/test/probe-wedge-certainty.test.js +0 -60
- package/test/produced-copy-choice.test.js +0 -213
- package/test/published-timeline.test.js +0 -83
- package/test/quality-offer.test.js +0 -160
- package/test/quality-variants.test.js +0 -1274
- package/test/rate-control-in-the-output.test.js +0 -181
- package/test/read-survives-withdrawal.test.js +0 -164
- package/test/read-window.test.js +0 -365
- package/test/reads-not-yet-arrived.test.js +0 -218
- package/test/resume-warm.test.js +0 -39
- package/test/run-graph-drift.test.js +0 -37
- package/test/run-inputs.test.js +0 -80
- package/test/run-intervals.test.js +0 -100
- package/test/run-position-follows-published-grid.test.js +0 -89
- package/test/run-progress.test.js +0 -64
- package/test/same-height-switch.test.js +0 -663
- package/test/seek-frees-the-viewers-requests.test.js +0 -217
- package/test/seek-landing.test.js +0 -109
- package/test/seek-target-not-superseded.test.js +0 -195
- package/test/segment-closed-on-both-branches.test.js +0 -114
- package/test/segment-cut-times.test.js +0 -63
- package/test/segment-demand.test.js +0 -80
- package/test/segment-hold-supersede.test.js +0 -91
- package/test/segment-judged-by-its-own-header.test.js +0 -222
- package/test/segment-serve-wiring.test.js +0 -481
- package/test/segment-store-eviction.test.js +0 -247
- package/test/segment-store.test.js +0 -352
- package/test/segment-true-start.test.js +0 -86
- package/test/segments-are-shared.test.js +0 -176
- package/test/send-chunk-bytes.test.js +0 -86
- package/test/serving-existing-output.test.js +0 -111
- package/test/serving-output.test.js +0 -102
- package/test/shared-piece-store.test.js +0 -310
- package/test/sidecar-naming.test.js +0 -142
- package/test/soundtrack-rate-bound.test.js +0 -289
- package/test/source-file.test.js +0 -133
- package/test/source-files-route.test.js +0 -113
- package/test/source-registry.test.js +0 -18
- package/test/split-a-stretch.test.js +0 -139
- package/test/stale-request-after-seek.test.js +0 -158
- package/test/startup-readings.test.js +0 -124
- package/test/stream-route.test.js +0 -237
- package/test/subtitle-cue-framing.test.js +0 -200
- package/test/subtitle-cue-source.test.js +0 -102
- package/test/subtitle-cue-walk.test.js +0 -385
- package/test/subtitle-cursor.test.js +0 -63
- package/test/subtitle-defaults.test.js +0 -118
- package/test/subtitle-language.test.js +0 -252
- package/test/subtitle-metadata.test.js +0 -15
- package/test/subtitle-plan-waits.test.js +0 -125
- package/test/subtitle-track-numbering.test.js +0 -391
- package/test/supply-margin.test.js +0 -190
- package/test/swarm-follows-readers.test.js +0 -174
- package/test/swarm-reach.test.js +0 -130
- package/test/swarm-selection.test.js +0 -220
- package/test/tail-duplication.test.js +0 -167
- package/test/the-channel-has-both-ends.test.js +0 -91
- package/test/the-torrent-thread-serves-bytes.test.js +0 -107
- package/test/thread-pool.test.js +0 -53
- package/test/timeline.test.js +0 -84
- package/test/torrent-contents.test.js +0 -235
- package/test/torrent-cost.test.js +0 -71
- package/test/torrent-files.test.js +0 -221
- package/test/torrent-naming.test.js +0 -255
- package/test/tracks-begin-together.test.js +0 -209
- package/test/tunnel-renewal.test.js +0 -167
- package/test/two-viewers-one-picture.test.js +0 -508
- package/test/upload-hurry.test.js +0 -186
- package/test/usrsctp-state.test.js +0 -131
- package/test/video-facts.test.js +0 -102
- package/test/viewer-assignments.test.js +0 -143
- package/test/viewer-outputs.test.js +0 -285
- package/test/viewer-presence.test.js +0 -116
- package/test/viewer-requests-admission.test.js +0 -92
- package/test/viewer-starts-on-map.test.js +0 -27
- package/test/viewer-states.test.js +0 -192
- package/test/viewer-subtitle-subscription.test.js +0 -96
- package/test/viewer.test.js +0 -139
- package/test/viewing-generations.test.js +0 -390
- package/test/visible-rung.test.js +0 -37
- package/test/wait-ledger.test.js +0 -100
- package/test/webrtc-shim.test.js +0 -149
- package/test/wedge-certainty.test.js +0 -131
- package/test/whole-file-held-read.test.js +0 -30
- package/test/withdraw-piece-claim.test.js +0 -212
- package/test/worker-channel.test.js +0 -158
|
@@ -1,391 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file The number a subtitle track is asked for by, and one walk per file.
|
|
3
|
-
*
|
|
4
|
-
* Two rules, both found by reading on 2026-08-26 after the report that embedded
|
|
5
|
-
* subtitles appear late (`research/subtitle-delay-2026-08-26.md`):
|
|
6
|
-
*
|
|
7
|
-
* 1. The browser names a track by ffmpeg's `0:s:N`, which counts EVERY subtitle
|
|
8
|
-
* stream. The container plan drops the picture-based ones — PGS, VobSub —
|
|
9
|
-
* because they cannot become WebVTT, so counting the kept ones is a
|
|
10
|
-
* different numbering as soon as a file carries one of each. What that cost:
|
|
11
|
-
* the push landed on a track the browser does not know, and the browser's
|
|
12
|
-
* own request found no track at all and fell through to the ffmpeg
|
|
13
|
-
* extraction, which reads the whole film (752 s measured, 2026-08-19).
|
|
14
|
-
* 2. A file is walked once at a time. The walk marks a cluster as read only
|
|
15
|
-
* after two suspension points, and it is started both on every verified
|
|
16
|
-
* piece and on a 3 s timer, so two passes could read and parse the same
|
|
17
|
-
* cluster and push the same line twice.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import test from "node:test";
|
|
21
|
-
import assert from "node:assert/strict";
|
|
22
|
-
import { Readable } from "node:stream";
|
|
23
|
-
import { MatroskaContainer } from "../services/media/container/MatroskaContainer.js";
|
|
24
|
-
import { cuesHeldFor, forgetSubtitles } from "../services/media/SubtitleCues.js";
|
|
25
|
-
import { heldFileOver } from "./helpers/held-file.js";
|
|
26
|
-
|
|
27
|
-
const ID_EBML = 0x1a45dfa3;
|
|
28
|
-
const ID_SEGMENT = 0x18538067;
|
|
29
|
-
const ID_SEEK_HEAD = 0x114d9b74;
|
|
30
|
-
const ID_SEEK = 0x4dbb;
|
|
31
|
-
const ID_SEEK_ID = 0x53ab;
|
|
32
|
-
const ID_SEEK_POSITION = 0x53ac;
|
|
33
|
-
const ID_INFO = 0x1549a966;
|
|
34
|
-
const ID_TIMESTAMP_SCALE = 0x2ad7b1;
|
|
35
|
-
const ID_TRACKS = 0x1654ae6b;
|
|
36
|
-
const ID_TRACK_ENTRY = 0xae;
|
|
37
|
-
const ID_TRACK_NUMBER = 0xd7;
|
|
38
|
-
const ID_TRACK_TYPE = 0x83;
|
|
39
|
-
const ID_CODEC_ID = 0x86;
|
|
40
|
-
const ID_LANGUAGE = 0x22b59c;
|
|
41
|
-
const ID_LANGUAGE_BCP47 = 0x22b59d;
|
|
42
|
-
const ID_FLAG_ENABLED = 0xb9;
|
|
43
|
-
const ID_FLAG_FORCED = 0x55aa;
|
|
44
|
-
const ID_FLAG_HEARING_IMPAIRED = 0x55ab;
|
|
45
|
-
const ID_CUES = 0x1c53bb6b;
|
|
46
|
-
const ID_CUE_POINT = 0xbb;
|
|
47
|
-
const ID_CUE_TIME = 0xb3;
|
|
48
|
-
const ID_CUE_TRACK_POSITIONS = 0xb7;
|
|
49
|
-
const ID_CUE_TRACK = 0xf7;
|
|
50
|
-
const ID_CUE_CLUSTER_POSITION = 0xf1;
|
|
51
|
-
const ID_CLUSTER = 0x1f43b675;
|
|
52
|
-
const ID_TIMESTAMP = 0xe7;
|
|
53
|
-
|
|
54
|
-
/** An element id, as the bytes the specification gives it. */
|
|
55
|
-
function idBytes(id) {
|
|
56
|
-
const bytes = [];
|
|
57
|
-
let rest = id;
|
|
58
|
-
while (rest > 0) {
|
|
59
|
-
bytes.unshift(rest & 0xff);
|
|
60
|
-
rest = Math.floor(rest / 256);
|
|
61
|
-
}
|
|
62
|
-
return Buffer.from(bytes);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** A size, as a four-byte EBML variable-length integer. */
|
|
66
|
-
function sizeBytes(size) {
|
|
67
|
-
const buffer = Buffer.alloc(4);
|
|
68
|
-
buffer.writeUInt32BE(size, 0);
|
|
69
|
-
buffer[0] |= 0x10;
|
|
70
|
-
return buffer;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function element(id, payload) {
|
|
74
|
-
return Buffer.concat([idBytes(id), sizeBytes(payload.length), payload]);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function uintElement(id, value) {
|
|
78
|
-
const bytes = [];
|
|
79
|
-
let rest = value;
|
|
80
|
-
do {
|
|
81
|
-
bytes.unshift(rest & 0xff);
|
|
82
|
-
rest = Math.floor(rest / 256);
|
|
83
|
-
} while (rest > 0);
|
|
84
|
-
return element(id, Buffer.from(bytes));
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function stringElement(id, value) {
|
|
88
|
-
return element(id, Buffer.from(value, "utf8"));
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* An unsigned value at a FIXED four bytes. A cue's cluster position has to be
|
|
93
|
-
* written twice — once to measure the table, once with the position that
|
|
94
|
-
* measurement produced — and a value-sized element would make the second table
|
|
95
|
-
* a different length from the first, moving the very cluster it names.
|
|
96
|
-
*/
|
|
97
|
-
function uint32Element(id, value) {
|
|
98
|
-
const payload = Buffer.alloc(4);
|
|
99
|
-
payload.writeUInt32BE(value, 0);
|
|
100
|
-
return element(id, payload);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function trackEntry({ number, type, codecId, language, flags = {}, languageBcp47 = null }) {
|
|
104
|
-
const parts = [
|
|
105
|
-
uintElement(ID_TRACK_NUMBER, number),
|
|
106
|
-
uintElement(ID_TRACK_TYPE, type),
|
|
107
|
-
stringElement(ID_CODEC_ID, codecId)
|
|
108
|
-
];
|
|
109
|
-
if (language !== undefined && language !== null) {
|
|
110
|
-
parts.push(stringElement(ID_LANGUAGE, language));
|
|
111
|
-
}
|
|
112
|
-
if (languageBcp47 !== null) {
|
|
113
|
-
parts.push(stringElement(ID_LANGUAGE_BCP47, languageBcp47));
|
|
114
|
-
}
|
|
115
|
-
for (const [id, value] of [
|
|
116
|
-
[ID_FLAG_ENABLED, flags.enabled],
|
|
117
|
-
[ID_FLAG_FORCED, flags.forced],
|
|
118
|
-
[ID_FLAG_HEARING_IMPAIRED, flags.hearingImpaired]
|
|
119
|
-
]) {
|
|
120
|
-
if (value !== undefined) {
|
|
121
|
-
parts.push(uintElement(id, value ? 1 : 0));
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
return element(ID_TRACK_ENTRY, Buffer.concat(parts));
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* A file whose subtitle tracks are, in the container's own order: a picture
|
|
129
|
-
* one, then two text ones. ffmpeg numbers those `0:s:0`, `0:s:1`, `0:s:2`;
|
|
130
|
-
* the plan can only read the last two.
|
|
131
|
-
*
|
|
132
|
-
* The Cues table points both text tracks at one cluster, which is written after
|
|
133
|
-
* the table so its position can be stated.
|
|
134
|
-
*
|
|
135
|
-
* @returns {{ file: Buffer, clusterAt: number }}
|
|
136
|
-
*/
|
|
137
|
-
function buildFile() {
|
|
138
|
-
const info = element(ID_INFO, uintElement(ID_TIMESTAMP_SCALE, 1_000_000));
|
|
139
|
-
const tracks = element(ID_TRACKS, Buffer.concat([
|
|
140
|
-
trackEntry({ number: 1, type: 1, codecId: "V_MPEG4/ISO/AVC", language: "und" }),
|
|
141
|
-
trackEntry({ number: 2, type: 17, codecId: "S_HDMV/PGS", language: "eng" }),
|
|
142
|
-
trackEntry({ number: 3, type: 17, codecId: "S_TEXT/UTF8", language: "rus" }),
|
|
143
|
-
trackEntry({ number: 4, type: 17, codecId: "S_TEXT/ASS", language: "eng" })
|
|
144
|
-
]));
|
|
145
|
-
|
|
146
|
-
// Built twice: the cue points state where the cluster is, and that position
|
|
147
|
-
// is only known once everything before it has its final length. Every size
|
|
148
|
-
// and position here is written at a fixed width, so the draft and the final
|
|
149
|
-
// table are the same length.
|
|
150
|
-
const cuesWith = (clusterAt) => element(ID_CUES, Buffer.concat([
|
|
151
|
-
element(ID_CUE_POINT, Buffer.concat([
|
|
152
|
-
uintElement(ID_CUE_TIME, 1000),
|
|
153
|
-
element(ID_CUE_TRACK_POSITIONS, Buffer.concat([
|
|
154
|
-
uintElement(ID_CUE_TRACK, 3),
|
|
155
|
-
uint32Element(ID_CUE_CLUSTER_POSITION, clusterAt)
|
|
156
|
-
])),
|
|
157
|
-
element(ID_CUE_TRACK_POSITIONS, Buffer.concat([
|
|
158
|
-
uintElement(ID_CUE_TRACK, 4),
|
|
159
|
-
uint32Element(ID_CUE_CLUSTER_POSITION, clusterAt)
|
|
160
|
-
]))
|
|
161
|
-
]))
|
|
162
|
-
]));
|
|
163
|
-
|
|
164
|
-
const seekEntry = (targetId, position) => element(ID_SEEK, Buffer.concat([
|
|
165
|
-
element(ID_SEEK_ID, idBytes(targetId)),
|
|
166
|
-
element(ID_SEEK_POSITION, (() => {
|
|
167
|
-
const buffer = Buffer.alloc(4);
|
|
168
|
-
buffer.writeUInt32BE(position, 0);
|
|
169
|
-
return buffer;
|
|
170
|
-
})())
|
|
171
|
-
]));
|
|
172
|
-
const seekHeadWith = (infoAt, tracksAt, cuesAt) => element(ID_SEEK_HEAD, Buffer.concat([
|
|
173
|
-
seekEntry(ID_INFO, infoAt),
|
|
174
|
-
seekEntry(ID_TRACKS, tracksAt),
|
|
175
|
-
seekEntry(ID_CUES, cuesAt)
|
|
176
|
-
]));
|
|
177
|
-
|
|
178
|
-
const headLength = seekHeadWith(0, 0, 0).length;
|
|
179
|
-
const infoAt = headLength;
|
|
180
|
-
const tracksAt = infoAt + info.length;
|
|
181
|
-
const cuesAt = tracksAt + tracks.length;
|
|
182
|
-
// A position in the Cues table is measured from the Segment's payload, and so
|
|
183
|
-
// is the one the reader turns it into.
|
|
184
|
-
const clusterRelative = cuesAt + cuesWith(0).length;
|
|
185
|
-
|
|
186
|
-
// Enough of a cluster to be read and recognised: its own header and a
|
|
187
|
-
// timestamp. No blocks, so it yields no cues — what the walk test counts is
|
|
188
|
-
// that its bytes are fetched once, and that does not depend on their content.
|
|
189
|
-
const cluster = element(ID_CLUSTER, uintElement(ID_TIMESTAMP, 1000));
|
|
190
|
-
|
|
191
|
-
const segmentPayload = Buffer.concat([
|
|
192
|
-
seekHeadWith(infoAt, tracksAt, cuesAt),
|
|
193
|
-
info,
|
|
194
|
-
tracks,
|
|
195
|
-
cuesWith(clusterRelative),
|
|
196
|
-
cluster
|
|
197
|
-
]);
|
|
198
|
-
const ebml = element(ID_EBML, Buffer.from([0x42, 0x86, 0x81, 0x01]));
|
|
199
|
-
const segment = element(ID_SEGMENT, segmentPayload);
|
|
200
|
-
const segmentDataOffset = ebml.length + segment.length - segmentPayload.length;
|
|
201
|
-
return {
|
|
202
|
-
file: Buffer.concat([ebml, segment]),
|
|
203
|
-
clusterAt: segmentDataOffset + clusterRelative
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function readerOver(file) {
|
|
208
|
-
return async (start, end) => {
|
|
209
|
-
const last = Math.min(end, file.length - 1);
|
|
210
|
-
return start > last ? null : file.subarray(start, last + 1);
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
test("a text track is numbered as ffmpeg numbers it, past the picture ones", async () => {
|
|
215
|
-
const { file } = buildFile();
|
|
216
|
-
|
|
217
|
-
const plan = await MatroskaContainer.readSubtitlePlan(readerOver(file), file.length);
|
|
218
|
-
|
|
219
|
-
assert.equal(plan.declared.length, 3, "all three subtitle tracks are declared");
|
|
220
|
-
assert.deepEqual(plan.tracks.map((track) => track.trackNumber), [3, 4], "only the text ones are readable");
|
|
221
|
-
assert.deepEqual(
|
|
222
|
-
plan.tracks.map((track) => track.declaredIndex),
|
|
223
|
-
[1, 2],
|
|
224
|
-
"the PGS track is 0:s:0, so the text tracks are 0:s:1 and 0:s:2 — not 0 and 1"
|
|
225
|
-
);
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* A torrent holding one file entirely, counting the byte ranges read from it.
|
|
230
|
-
*
|
|
231
|
-
* @param {Buffer} bytes
|
|
232
|
-
* @returns {{ torrent: object, reads: Array<{ start: number, end: number }> }}
|
|
233
|
-
*/
|
|
234
|
-
function torrentOver(bytes) {
|
|
235
|
-
const reads = [];
|
|
236
|
-
const file = {
|
|
237
|
-
name: "film.mkv",
|
|
238
|
-
length: bytes.length,
|
|
239
|
-
offset: 0,
|
|
240
|
-
createReadStream({ start = 0, end = bytes.length - 1 } = {}) {
|
|
241
|
-
reads.push({ start, end });
|
|
242
|
-
// Asynchronous on purpose: a read that resolves in the same tick would
|
|
243
|
-
// hide exactly the interleaving this test is about.
|
|
244
|
-
return Readable.from((async function* chunks() {
|
|
245
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
246
|
-
yield bytes.subarray(start, end + 1);
|
|
247
|
-
})());
|
|
248
|
-
}
|
|
249
|
-
};
|
|
250
|
-
return {
|
|
251
|
-
reads,
|
|
252
|
-
torrent: {
|
|
253
|
-
pieceLength: 1024,
|
|
254
|
-
bitfield: { get: () => true },
|
|
255
|
-
files: [file]
|
|
256
|
-
}
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
test("two walks of one file at the same time read each cluster once", async () => {
|
|
261
|
-
const { file, clusterAt } = buildFile();
|
|
262
|
-
const { torrent, reads } = torrentOver(file);
|
|
263
|
-
const sourceKey = "torrent:numbering-test";
|
|
264
|
-
forgetSubtitles(sourceKey);
|
|
265
|
-
|
|
266
|
-
// Both text tracks at once, which is what the warmup does on every verified
|
|
267
|
-
// piece and every three seconds.
|
|
268
|
-
const [first, second] = await Promise.all([
|
|
269
|
-
cuesHeldFor(heldFileOver(torrent, 0, sourceKey), 3),
|
|
270
|
-
cuesHeldFor(heldFileOver(torrent, 0, sourceKey), 4)
|
|
271
|
-
]);
|
|
272
|
-
|
|
273
|
-
assert.equal(first.coveredClusters, 1, "the cluster the table names was walked");
|
|
274
|
-
assert.equal(second.coveredClusters, 1, "and the second track sees the same walk, not its own");
|
|
275
|
-
const clusterReads = reads.filter((range) => range.start === clusterAt);
|
|
276
|
-
assert.equal(
|
|
277
|
-
clusterReads.length,
|
|
278
|
-
2,
|
|
279
|
-
"one probe of the cluster's header and one read of its body — not two of each"
|
|
280
|
-
);
|
|
281
|
-
forgetSubtitles(sourceKey);
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* A file whose subtitle tracks carry the flags RFC 9559 defines for them: one
|
|
286
|
-
* forced, one for viewers who cannot hear, one the file marks unusable, one
|
|
287
|
-
* writing its language as RFC 5646 alongside the three-letter code, and one
|
|
288
|
-
* relying on Matroska's default language.
|
|
289
|
-
*
|
|
290
|
-
* @returns {Buffer}
|
|
291
|
-
*/
|
|
292
|
-
function fileWithFlags() {
|
|
293
|
-
const info = element(ID_INFO, uintElement(ID_TIMESTAMP_SCALE, 1_000_000));
|
|
294
|
-
const tracks = element(ID_TRACKS, Buffer.concat([
|
|
295
|
-
trackEntry({ number: 1, type: 1, codecId: "V_MPEG4/ISO/AVC", language: "und" }),
|
|
296
|
-
trackEntry({ number: 2, type: 17, codecId: "S_TEXT/UTF8", language: "rus", flags: { forced: true } }),
|
|
297
|
-
trackEntry({ number: 3, type: 17, codecId: "S_TEXT/UTF8", language: "eng", flags: { hearingImpaired: true } }),
|
|
298
|
-
trackEntry({ number: 4, type: 17, codecId: "S_TEXT/UTF8", language: "fre", flags: { enabled: false } }),
|
|
299
|
-
trackEntry({ number: 5, type: 17, codecId: "S_TEXT/ASS", language: "por", languageBcp47: "pt-BR" }),
|
|
300
|
-
trackEntry({ number: 6, type: 17, codecId: "S_TEXT/UTF8", language: null })
|
|
301
|
-
]));
|
|
302
|
-
const seekEntry = (targetId, position) => element(ID_SEEK, Buffer.concat([
|
|
303
|
-
element(ID_SEEK_ID, idBytes(targetId)),
|
|
304
|
-
uint32Element(ID_SEEK_POSITION, position)
|
|
305
|
-
]));
|
|
306
|
-
const seekHeadWith = (infoAt, tracksAt) => element(ID_SEEK_HEAD, Buffer.concat([
|
|
307
|
-
seekEntry(ID_INFO, infoAt),
|
|
308
|
-
seekEntry(ID_TRACKS, tracksAt)
|
|
309
|
-
]));
|
|
310
|
-
const headLength = seekHeadWith(0, 0).length;
|
|
311
|
-
const segmentPayload = Buffer.concat([
|
|
312
|
-
seekHeadWith(headLength, headLength + info.length),
|
|
313
|
-
info,
|
|
314
|
-
tracks
|
|
315
|
-
]);
|
|
316
|
-
const ebml = element(ID_EBML, Buffer.from([0x42, 0x86, 0x81, 0x01]));
|
|
317
|
-
return Buffer.concat([ebml, element(ID_SEGMENT, segmentPayload)]);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
test("the flags the file states about a track are read, not guessed from its name", async () => {
|
|
321
|
-
const plan = await MatroskaContainer.readSubtitlePlan(readerOver(fileWithFlags()), fileWithFlags().length);
|
|
322
|
-
|
|
323
|
-
const forced = plan.tracks.find((track) => track.trackNumber === 2);
|
|
324
|
-
assert.equal(forced.isForced, true, "FlagForced 0x55AA");
|
|
325
|
-
assert.equal(forced.isHearingImpaired, false);
|
|
326
|
-
|
|
327
|
-
const sdh = plan.tracks.find((track) => track.trackNumber === 3);
|
|
328
|
-
assert.equal(sdh.isHearingImpaired, true, "FlagHearingImpaired 0x55AB");
|
|
329
|
-
assert.equal(sdh.isForced, false);
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
test("a track the file marks unusable is not offered, but is still counted", async () => {
|
|
333
|
-
// FlagEnabled (0xB9): "Set to 1 if the track is usable." Track 4 says 0, so
|
|
334
|
-
// it is not offered — but it KEEPS its place in the numbering, because ffmpeg
|
|
335
|
-
// keeps it: `matroskadec.c` parses MATROSKA_ID_TRACKFLAGENABLED as EBML_NONE,
|
|
336
|
-
// reading the element and storing nothing, so the stream is created and gets
|
|
337
|
-
// its own `0:s:N`. Dropping it here would shift every track after it.
|
|
338
|
-
const plan = await MatroskaContainer.readSubtitlePlan(readerOver(fileWithFlags()), fileWithFlags().length);
|
|
339
|
-
|
|
340
|
-
assert.equal(plan.tracks.some((track) => track.trackNumber === 4), false, "not offered for extraction");
|
|
341
|
-
const counted = plan.declared.find((track) => track.trackNumber === 4);
|
|
342
|
-
assert.ok(counted, "still declared, so the numbering does not move");
|
|
343
|
-
assert.equal(counted.isEnabled, false);
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
test("an unusable track keeps its place, so the tracks after it keep theirs", async () => {
|
|
347
|
-
const plan = await MatroskaContainer.readSubtitlePlan(readerOver(fileWithFlags()), fileWithFlags().length);
|
|
348
|
-
|
|
349
|
-
// s:0 forced, s:1 SDH, s:2 the unusable one, s:3 the Brazilian track.
|
|
350
|
-
assert.deepEqual(
|
|
351
|
-
plan.tracks.map((track) => [track.trackNumber, track.declaredIndex]),
|
|
352
|
-
[[2, 0], [3, 1], [5, 3], [6, 4]]
|
|
353
|
-
);
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
test("the list ffmpeg is lined up against still speaks ffmpeg's language codes", async () => {
|
|
357
|
-
// `declared` exists to be paired with the `-i` banner, which prints the
|
|
358
|
-
// three-letter code; reporting "pt-BR" there would break the pairing and cost
|
|
359
|
-
// the FlagDefault reading with it.
|
|
360
|
-
const plan = await MatroskaContainer.readSubtitlePlan(readerOver(fileWithFlags()), fileWithFlags().length);
|
|
361
|
-
|
|
362
|
-
const declared = plan.declared.find((track) => track.trackNumber === 5);
|
|
363
|
-
assert.equal(declared.language, "por");
|
|
364
|
-
assert.equal(declared.languageBcp47, "pt-BR");
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
test("where the file writes RFC 5646, that is the language", async () => {
|
|
368
|
-
// "If this element is used, then any Language elements used in the same
|
|
369
|
-
// TrackEntry MUST be ignored."
|
|
370
|
-
const plan = await MatroskaContainer.readSubtitlePlan(readerOver(fileWithFlags()), fileWithFlags().length);
|
|
371
|
-
|
|
372
|
-
const track = plan.tracks.find((entry) => entry.trackNumber === 5);
|
|
373
|
-
assert.equal(track.language, "pt-BR", "not the three-letter por");
|
|
374
|
-
assert.equal(track.languageBcp47, "pt-BR");
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
test("an omitted Matroska Language element is the format's default, `eng`, and says it was not written", async () => {
|
|
378
|
-
// RFC 9559 §5.1.4.1.19 gives Language the default `eng`; RFC 8794 §11.1.19
|
|
379
|
-
// says an element equal to its default need not be written, and a reader MUST
|
|
380
|
-
// read the default. Until 2026-10-01 such a track was offered as Unknown.
|
|
381
|
-
const file = fileWithFlags();
|
|
382
|
-
const plan = await MatroskaContainer.readSubtitlePlan(readerOver(file), file.length);
|
|
383
|
-
|
|
384
|
-
const track = plan.tracks.find((entry) => entry.trackNumber === 6);
|
|
385
|
-
const declared = plan.declared.find((entry) => entry.trackNumber === 6);
|
|
386
|
-
assert.equal(track.language, "eng");
|
|
387
|
-
assert.equal(track.languageSource, "default");
|
|
388
|
-
assert.equal(track.declaresLanguage, false, "the element itself was not written");
|
|
389
|
-
assert.equal(declared.language, "eng");
|
|
390
|
-
assert.equal(declared.declaresLanguage, false);
|
|
391
|
-
});
|
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file The margin and the buffer, checked against the session they were
|
|
3
|
-
* derived from.
|
|
4
|
-
*
|
|
5
|
-
* The figures in these tests are the field measurements of 2026-08-17, so a
|
|
6
|
-
* change that breaks the arithmetic fails against reality rather than against
|
|
7
|
-
* an example someone invented.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import assert from "node:assert/strict";
|
|
11
|
-
import test from "node:test";
|
|
12
|
-
|
|
13
|
-
import { minimumBufferFrom, requiredSpeedFrom } from "../services/torrent/supply-margin.js";
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Waits spaced by `intervalSec`, each lasting `waitSec`.
|
|
17
|
-
*
|
|
18
|
-
* @param {number} count
|
|
19
|
-
* @param {number} intervalSec
|
|
20
|
-
* @param {number} waitSec
|
|
21
|
-
* @returns {Array<{ waitedMs: number, at: number }>}
|
|
22
|
-
*/
|
|
23
|
-
function evenlySpaced(count, intervalSec, waitSec) {
|
|
24
|
-
const waits = [];
|
|
25
|
-
for (let index = 0; index < count; index += 1) {
|
|
26
|
-
waits.push({ waitedMs: waitSec * 1000, at: 1_000_000 + index * intervalSec * 1000 });
|
|
27
|
-
}
|
|
28
|
-
return waits;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
test("the margin is what the supply's own interruptions demand", () => {
|
|
32
|
-
// The field torrent: waits of 1.49 s with 0.73 s of running between them, and
|
|
33
|
-
// the worst of them 3.16 s. Written out rather than generated, because the
|
|
34
|
-
// point of the test is that these exact spans give that exact answer.
|
|
35
|
-
const waits = [
|
|
36
|
-
{ waitedMs: 1490, at: 1490 }, // 0.00..1.49
|
|
37
|
-
{ waitedMs: 1490, at: 3710 }, // 2.22..3.71
|
|
38
|
-
{ waitedMs: 1490, at: 5930 }, // 4.44..5.93
|
|
39
|
-
{ waitedMs: 3160, at: 9820 }, // 6.66..9.82 ← the worst
|
|
40
|
-
{ waitedMs: 1490, at: 12_040 }, // 10.55..12.04
|
|
41
|
-
{ waitedMs: 1490, at: 14_260 } // 12.77..14.26
|
|
42
|
-
];
|
|
43
|
-
|
|
44
|
-
const answer = requiredSpeedFrom(waits);
|
|
45
|
-
|
|
46
|
-
assert.ok(answer);
|
|
47
|
-
assert.equal(answer.worstWaitSec, 3.16);
|
|
48
|
-
// 2.22 s from one wait's end to the next one's END, of which 1.49 s was spent
|
|
49
|
-
// waiting — so the encoder ran for 0.73 s. That running stretch is what the
|
|
50
|
-
// derivation calls T: `(v - 1) x T > W` prices what is GAINED between
|
|
51
|
-
// interruptions, and nothing is gained during one.
|
|
52
|
-
assert.ok(Math.abs(answer.medianIntervalSec - 0.73) < 0.001, `got ${answer.medianIntervalSec}`);
|
|
53
|
-
// THE SHARE OF ITS TIME THE SUPPLY LOST, over whole cycles: from the first
|
|
54
|
-
// interruption's start to the last one's, 12.77 s, of which 9.12 s was
|
|
55
|
-
// interruption. So the reading delivered for 3.65 s of every 12.77, and
|
|
56
|
-
// producing film at that share costs 1/(1 - 0.714) = 3.50x.
|
|
57
|
-
//
|
|
58
|
-
// The formula it replaced divided the WORST interruption by the TYPICAL gap —
|
|
59
|
-
// 1 + 3.16/0.73 = 5.33 — which asks what would happen if the worst recurred
|
|
60
|
-
// at the typical rate, a case that never occurred in this data, and which
|
|
61
|
-
// divides by a gap that goes to zero whenever interruptions arrive in a burst.
|
|
62
|
-
// Field 2026-09-08: 0.79 s over 0.01 s gave 158.60x on a file already
|
|
63
|
-
// downloaded whole, and every quality step was refused against it.
|
|
64
|
-
//
|
|
65
|
-
// The failure this file was written for is still caught: a step admitted at
|
|
66
|
-
// 1.5 ran at 1.05x and stalled, and 3.91x refuses 1.5 exactly as 5.33x did.
|
|
67
|
-
assert.ok(Math.abs(answer.lostShare - 0.7142) < 0.001, `got ${answer.lostShare}`);
|
|
68
|
-
assert.ok(Math.abs(answer.requiredSpeed - 3.4986) < 0.001, `got ${answer.requiredSpeed}`);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test("one stall seen by three readers is one interruption, not three", () => {
|
|
72
|
-
// The picture and two audio renditions walk the same file, so a piece that
|
|
73
|
-
// has not arrived blocks all three, and their waits end within milliseconds
|
|
74
|
-
// of each other. Field 2026-08-31: `worst wait 13.26s, one every 0.00s,
|
|
75
|
-
// 2 measured` produced a required speed of 4422.00x, and every quality step
|
|
76
|
-
// was refused against it.
|
|
77
|
-
const answer = requiredSpeedFrom([
|
|
78
|
-
{ waitedMs: 13_260, at: 1_000_000 },
|
|
79
|
-
{ waitedMs: 13_257, at: 1_000_003 }
|
|
80
|
-
]);
|
|
81
|
-
|
|
82
|
-
// Two overlapping waits are one interruption, and one interruption shows no
|
|
83
|
-
// interval — so the honest answer is that it is not known yet.
|
|
84
|
-
assert.equal(answer, null);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
test("overlapping waits merge, and the gap between stalls is what is left", () => {
|
|
88
|
-
const answer = requiredSpeedFrom([
|
|
89
|
-
// First stall: 10s..20s, noticed by two readers a moment apart.
|
|
90
|
-
{ waitedMs: 10_000, at: 20_000 },
|
|
91
|
-
{ waitedMs: 9_000, at: 19_500 },
|
|
92
|
-
// Second stall: 30s..34s, again seen twice.
|
|
93
|
-
{ waitedMs: 4_000, at: 34_000 },
|
|
94
|
-
{ waitedMs: 3_500, at: 33_800 }
|
|
95
|
-
]);
|
|
96
|
-
|
|
97
|
-
assert.ok(answer);
|
|
98
|
-
assert.equal(answer.samples, 2, "two interruptions");
|
|
99
|
-
assert.equal(answer.waits, 4, "from four waits");
|
|
100
|
-
assert.equal(answer.worstWaitSec, 10, "the merged stall, not one reader's view of it");
|
|
101
|
-
assert.equal(answer.medianIntervalSec, 10, "20s to 30s is when the encoder ran");
|
|
102
|
-
// One whole cycle: 10 s to 30 s, of which the 10 s interruption is half. The
|
|
103
|
-
// supply delivered for the other half, so a step must run at twice realtime.
|
|
104
|
-
assert.equal(answer.spanSec, 20);
|
|
105
|
-
assert.equal(answer.lostSec, 10);
|
|
106
|
-
assert.equal(answer.requiredSpeed, 2);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test("a copy at 8x clears its own supply with room to spare", () => {
|
|
110
|
-
// The same file's copied stream: waits up to 4.82 s, and it never stalled.
|
|
111
|
-
const waits = evenlySpaced(6, 15.5, 4.82);
|
|
112
|
-
const answer = requiredSpeedFrom(waits);
|
|
113
|
-
assert.ok(answer);
|
|
114
|
-
// Waits end 15.5 s apart and last 4.82 s, so the supply delivers for 10.68 s
|
|
115
|
-
// of every 15.5: 0.311 of its time lost, and 1/(1 - 0.311) = 1.45 against the
|
|
116
|
-
// 8x measured. Which is why a copy is the step a stranded viewer is always
|
|
117
|
-
// able to return to.
|
|
118
|
-
assert.ok(answer.requiredSpeed < 1.5, `got ${answer.requiredSpeed}`);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test("with too little evidence it says so instead of inventing a number", () => {
|
|
122
|
-
assert.equal(requiredSpeedFrom([]), null);
|
|
123
|
-
assert.equal(requiredSpeedFrom([{ waitedMs: 1500, at: 1 }]), null, "one wait shows no interval");
|
|
124
|
-
assert.equal(requiredSpeedFrom(null), null);
|
|
125
|
-
// Every wait at the same instant: no interval was observed, so no interval
|
|
126
|
-
// may be stated.
|
|
127
|
-
assert.equal(
|
|
128
|
-
requiredSpeedFrom([
|
|
129
|
-
{ waitedMs: 1000, at: 5 },
|
|
130
|
-
{ waitedMs: 1000, at: 5 }
|
|
131
|
-
]),
|
|
132
|
-
null
|
|
133
|
-
);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
test("readings that are not measurements are ignored, not averaged in", () => {
|
|
137
|
-
const answer = requiredSpeedFrom([
|
|
138
|
-
{ waitedMs: 0, at: 1000 },
|
|
139
|
-
{ waitedMs: Number.NaN, at: 2000 },
|
|
140
|
-
{ waitedMs: 1000, at: 3000 },
|
|
141
|
-
{ waitedMs: 2000, at: 5000 },
|
|
142
|
-
{ waitedMs: 1500, at: 7000 }
|
|
143
|
-
]);
|
|
144
|
-
assert.ok(answer);
|
|
145
|
-
assert.equal(answer.waits, 3, "only the three real waits count");
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
test("the buffer is one segment plus the worst interruption, and names which", () => {
|
|
149
|
-
const answer = minimumBufferFrom({
|
|
150
|
-
segmentSeconds: 4,
|
|
151
|
-
worstSupplyWaitSec: 3.16,
|
|
152
|
-
worstProductionGapSec: 1.2,
|
|
153
|
-
worstTransferSec: 0.066
|
|
154
|
-
});
|
|
155
|
-
assert.ok(answer);
|
|
156
|
-
assert.equal(answer.seconds, 7.16, "7.16 s against the 25 s that was chosen by hand");
|
|
157
|
-
assert.equal(answer.from, "supply");
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
test("a step that cannot keep up sets the buffer itself", () => {
|
|
161
|
-
const answer = minimumBufferFrom({
|
|
162
|
-
segmentSeconds: 4,
|
|
163
|
-
worstSupplyWaitSec: 1.4,
|
|
164
|
-
worstProductionGapSec: 6.5,
|
|
165
|
-
worstTransferSec: 0.05
|
|
166
|
-
});
|
|
167
|
-
assert.equal(answer.seconds, 10.5);
|
|
168
|
-
assert.equal(answer.from, "production", "the encoder, not the swarm, is what the viewer is waiting for");
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
test("a term with nothing observed contributes nothing", () => {
|
|
172
|
-
const answer = minimumBufferFrom({ segmentSeconds: 4 });
|
|
173
|
-
assert.equal(answer.seconds, 4, "one whole segment is the floor: the one being played");
|
|
174
|
-
assert.equal(answer.from, "none");
|
|
175
|
-
assert.equal(minimumBufferFrom({}), null, "without a segment duration nothing can be said");
|
|
176
|
-
assert.equal(minimumBufferFrom({ segmentSeconds: 0 }), null);
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
test("a supply that delivered for a sliver of the time asks for what it measured, uncut", () => {
|
|
180
|
-
// Two stalls of 99.9 s with 0.1 s of delivery between them: the share lost is
|
|
181
|
-
// 0.999, and the speed asked for is a thousand times realtime. A cap at a
|
|
182
|
-
// chosen 0.99 used to report a hundred.
|
|
183
|
-
const answer = requiredSpeedFrom([
|
|
184
|
-
{ waitedMs: 99_900, at: 99_900 },
|
|
185
|
-
{ waitedMs: 99_900, at: 199_900 }
|
|
186
|
-
]);
|
|
187
|
-
assert.ok(answer);
|
|
188
|
-
assert.ok(Math.abs(answer.lostShare - 0.999) < 1e-9, `got ${answer.lostShare}`);
|
|
189
|
-
assert.ok(Math.abs(answer.requiredSpeed - 1000) < 1e-6, `got ${answer.requiredSpeed}`);
|
|
190
|
-
});
|