@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
package/test/read-window.test.js
DELETED
|
@@ -1,365 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file What a read asks the torrent to download, and what it gives back.
|
|
3
|
-
*
|
|
4
|
-
* A read used to select its whole requested range and never deselect it.
|
|
5
|
-
* ffmpeg opens its input as `bytes <position>-<EOF>`, so the first read of a
|
|
6
|
-
* session claimed the entire file and marked every piece of it critical — and
|
|
7
|
-
* the claim outlived the read, which is abandoned a second later when ffmpeg
|
|
8
|
-
* seeks. Nothing after that could outrank it: measured on a 4.7 GB film, a seek
|
|
9
|
-
* to 89.1% waited 93 s while the swarm fetched 2.47 GB in file order.
|
|
10
|
-
*
|
|
11
|
-
* Now a read claims only the piece it is STOPPED on, and gives it back when it
|
|
12
|
-
* ends. What should be downloaded ahead of a viewer is the priority map's
|
|
13
|
-
* answer, stated once for the whole file by the side that knows where the
|
|
14
|
-
* viewers are; a read is consumption, not a forecast. Fifteen reads on one file
|
|
15
|
-
* used to declare fifteen windows on a piece store holding sixteen pieces, and
|
|
16
|
-
* half of all evictions then took a piece a reader had declared (field
|
|
17
|
-
* 2026-09-05).
|
|
18
|
-
*
|
|
19
|
-
* These tests pin what is left: a read that is not stopped claims nothing, a
|
|
20
|
-
* read that is stopped claims the pieces it is waiting for and nothing beyond
|
|
21
|
-
* them, and every claim is given back.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
import test from "node:test";
|
|
25
|
-
import assert from "node:assert/strict";
|
|
26
|
-
import { EventEmitter } from "node:events";
|
|
27
|
-
import os from "node:os";
|
|
28
|
-
import path from "node:path";
|
|
29
|
-
import fs from "node:fs/promises";
|
|
30
|
-
import {
|
|
31
|
-
nextWindowPieces,
|
|
32
|
-
readFragments,
|
|
33
|
-
readWindowFor
|
|
34
|
-
} from "../services/torrent/worker/piece-reader.js";
|
|
35
|
-
import { SharedPieceStore } from "../services/storage/piece-store/shared-piece-store.js";
|
|
36
|
-
|
|
37
|
-
const PIECE = 1024;
|
|
38
|
-
// The production window is 32 MB against 8 MiB pieces — four of them. Sized
|
|
39
|
-
// here in pieces so the test does not depend on either constant.
|
|
40
|
-
const WINDOW_PIECES = 4;
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* A torrent that records every selection call instead of downloading anything.
|
|
44
|
-
*
|
|
45
|
-
* @param {{ pieceCount: number, present?: (index: number) => boolean }} shape
|
|
46
|
-
*/
|
|
47
|
-
async function recordingTorrent({ pieceCount, present = () => true }) {
|
|
48
|
-
const directory = await fs.mkdtemp(path.join(os.tmpdir(), "read-window-test-"));
|
|
49
|
-
const totalLength = pieceCount * PIECE;
|
|
50
|
-
// ROOM FOR THE WHOLE FIXTURE, because none of these tests is about eviction.
|
|
51
|
-
// At 64 pieces of memory an 8000-piece fixture spilled 7936 of them to disk —
|
|
52
|
-
// a file each — and read most of them back, so four checks about which piece
|
|
53
|
-
// RANGES a reader claims cost 25-28 seconds apiece and this one file took
|
|
54
|
-
// 132 s of a 140 s suite. The store's own behaviour under pressure is
|
|
55
|
-
// measured where that is the subject (`piece-store-eviction`,
|
|
56
|
-
// `piece-lru`, `piece-disk-store`).
|
|
57
|
-
const store = new SharedPieceStore(PIECE, {
|
|
58
|
-
length: totalLength,
|
|
59
|
-
memoryBytes: totalLength,
|
|
60
|
-
path: directory,
|
|
61
|
-
name: "test"
|
|
62
|
-
});
|
|
63
|
-
for (let index = 0; index < pieceCount; index += 1) {
|
|
64
|
-
await new Promise((resolve, reject) => {
|
|
65
|
-
store.put(index, Buffer.alloc(PIECE, index % 251), (error) => (error ? reject(error) : resolve()));
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** @type {Array<{ call: string, from: number, to: number, stream?: boolean }>} */
|
|
70
|
-
const calls = [];
|
|
71
|
-
/** Live stream selections, as WebTorrent counts them: exact bounds, duplicates allowed. */
|
|
72
|
-
const held = [];
|
|
73
|
-
|
|
74
|
-
const torrent = Object.assign(new EventEmitter(), {
|
|
75
|
-
pieceLength: PIECE,
|
|
76
|
-
store,
|
|
77
|
-
bitfield: { get: (index) => present(index) },
|
|
78
|
-
files: [{ offset: 0, length: totalLength, name: "file.bin" }],
|
|
79
|
-
_critical: [],
|
|
80
|
-
_selections: { _items: [] },
|
|
81
|
-
calls,
|
|
82
|
-
held,
|
|
83
|
-
_select(from, to, _priority, _notify, isStreamSelection) {
|
|
84
|
-
calls.push({ call: "select", from, to, stream: isStreamSelection === true });
|
|
85
|
-
held.push(`${from}-${to}`);
|
|
86
|
-
this._selections._items.push({ from, to });
|
|
87
|
-
},
|
|
88
|
-
_deselect(from, to, isStreamSelection) {
|
|
89
|
-
calls.push({ call: "deselect", from, to, stream: isStreamSelection === true });
|
|
90
|
-
const at = held.indexOf(`${from}-${to}`);
|
|
91
|
-
if (at >= 0) {
|
|
92
|
-
held.splice(at, 1);
|
|
93
|
-
}
|
|
94
|
-
const item = this._selections._items.findIndex((one) => one.from === from && one.to === to);
|
|
95
|
-
if (item >= 0) {
|
|
96
|
-
this._selections._items.splice(item, 1);
|
|
97
|
-
}
|
|
98
|
-
},
|
|
99
|
-
critical(from, to) {
|
|
100
|
-
calls.push({ call: "critical", from, to });
|
|
101
|
-
for (let index = from; index <= to; index += 1) {
|
|
102
|
-
this._critical[index] = true;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
return { torrent, store, directory };
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/** Read a range to the end, releasing every fragment. */
|
|
111
|
-
async function drain(torrent, start, end) {
|
|
112
|
-
for await (const fragment of readFragments({
|
|
113
|
-
torrent,
|
|
114
|
-
fileIndex: 0,
|
|
115
|
-
start,
|
|
116
|
-
end,
|
|
117
|
-
cancellation: { isCancelled: () => false }
|
|
118
|
-
})) {
|
|
119
|
-
fragment.release();
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
test("the window is bounded and clamped to the end of the read", () => {
|
|
124
|
-
assert.deepEqual(readWindowFor({ pieceIndex: 10, lastPiece: 999, windowPieces: 4 }), { from: 10, to: 13 });
|
|
125
|
-
assert.deepEqual(
|
|
126
|
-
readWindowFor({ pieceIndex: 997, lastPiece: 999, windowPieces: 4 }),
|
|
127
|
-
{ from: 997, to: 999 },
|
|
128
|
-
"the window never reaches past the range the reader was given"
|
|
129
|
-
);
|
|
130
|
-
assert.deepEqual(
|
|
131
|
-
readWindowFor({ pieceIndex: 5, lastPiece: 999, windowPieces: 0 }),
|
|
132
|
-
{ from: 5, to: 5 },
|
|
133
|
-
"a degenerate size still asks for the piece under the head"
|
|
134
|
-
);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
test("a read that is not stopped claims nothing", async () => {
|
|
138
|
-
// 8000 pieces of 1 KB, every one of them present, and the read asks as ffmpeg
|
|
139
|
-
// does: to the last byte of the file. Nothing is missing, so this read is
|
|
140
|
-
// waiting for nothing, so it wants nothing of the swarm — what lies ahead of
|
|
141
|
-
// it belongs to the priority map.
|
|
142
|
-
const { torrent, store, directory } = await recordingTorrent({ pieceCount: 8000 });
|
|
143
|
-
try {
|
|
144
|
-
const iterator = readFragments({
|
|
145
|
-
torrent,
|
|
146
|
-
fileIndex: 0,
|
|
147
|
-
start: 0,
|
|
148
|
-
end: 8000 * PIECE - 1,
|
|
149
|
-
cancellation: { isCancelled: () => false },
|
|
150
|
-
windowBytes: WINDOW_PIECES * PIECE
|
|
151
|
-
});
|
|
152
|
-
const first = await iterator.next();
|
|
153
|
-
first.value.release();
|
|
154
|
-
|
|
155
|
-
assert.deepEqual(
|
|
156
|
-
torrent.held,
|
|
157
|
-
[],
|
|
158
|
-
"the read declared a window of its own, which is the forecast that has to come from the map"
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
await iterator.return();
|
|
162
|
-
} finally {
|
|
163
|
-
store.destroy(() => undefined);
|
|
164
|
-
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
165
|
-
}
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
test("a finished read leaves nothing selected", async () => {
|
|
169
|
-
const { torrent, store, directory } = await recordingTorrent({ pieceCount: 40 });
|
|
170
|
-
try {
|
|
171
|
-
await drain(torrent, 0, 40 * PIECE - 1);
|
|
172
|
-
assert.deepEqual(torrent.held, [], "the read kept its claim after finishing");
|
|
173
|
-
} finally {
|
|
174
|
-
store.destroy(() => undefined);
|
|
175
|
-
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
test("an abandoned read leaves nothing selected", async () => {
|
|
180
|
-
const { torrent, store, directory } = await recordingTorrent({ pieceCount: 8000 });
|
|
181
|
-
try {
|
|
182
|
-
const iterator = readFragments({
|
|
183
|
-
torrent,
|
|
184
|
-
fileIndex: 0,
|
|
185
|
-
start: 0,
|
|
186
|
-
end: 8000 * PIECE - 1,
|
|
187
|
-
cancellation: { isCancelled: () => false },
|
|
188
|
-
windowBytes: WINDOW_PIECES * PIECE
|
|
189
|
-
});
|
|
190
|
-
const first = await iterator.next();
|
|
191
|
-
first.value.release();
|
|
192
|
-
// What ffmpeg does to its opening read the moment it seeks.
|
|
193
|
-
await iterator.return();
|
|
194
|
-
|
|
195
|
-
assert.deepEqual(torrent.held, [], "an abandoned read kept its claim forever");
|
|
196
|
-
} finally {
|
|
197
|
-
store.destroy(() => undefined);
|
|
198
|
-
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
test("two stopped readers add up, and one leaving takes only its own", async () => {
|
|
203
|
-
// Nothing has arrived yet, so both readers are stopped on their first piece
|
|
204
|
-
// and each states it. Two claims, each withdrawn on its own.
|
|
205
|
-
let arrived = false;
|
|
206
|
-
const { torrent, store, directory } = await recordingTorrent({
|
|
207
|
-
pieceCount: 8000,
|
|
208
|
-
present: () => arrived
|
|
209
|
-
});
|
|
210
|
-
try {
|
|
211
|
-
const head = readFragments({
|
|
212
|
-
torrent, fileIndex: 0, start: 0, end: 8000 * PIECE - 1,
|
|
213
|
-
cancellation: { isCancelled: () => false },
|
|
214
|
-
windowBytes: WINDOW_PIECES * PIECE
|
|
215
|
-
});
|
|
216
|
-
const tail = readFragments({
|
|
217
|
-
torrent, fileIndex: 0, start: 4000 * PIECE, end: 8000 * PIECE - 1,
|
|
218
|
-
cancellation: { isCancelled: () => false },
|
|
219
|
-
windowBytes: WINDOW_PIECES * PIECE
|
|
220
|
-
});
|
|
221
|
-
const headPending = head.next();
|
|
222
|
-
const tailPending = tail.next();
|
|
223
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
224
|
-
|
|
225
|
-
const pieceOf = (range) => Number(range.split("-")[0]);
|
|
226
|
-
const held = [...torrent.held];
|
|
227
|
-
assert.ok(held.some((range) => pieceOf(range) < 4000), "the head reader claimed nothing");
|
|
228
|
-
assert.ok(held.some((range) => pieceOf(range) >= 4000), "the tail reader claimed nothing");
|
|
229
|
-
|
|
230
|
-
// Let both through to a yield, so ending them runs their `finally` at once.
|
|
231
|
-
arrived = true;
|
|
232
|
-
torrent.emit("verified", 0);
|
|
233
|
-
torrent.emit("verified", 4000);
|
|
234
|
-
(await headPending).value.release();
|
|
235
|
-
(await tailPending).value.release();
|
|
236
|
-
|
|
237
|
-
await tail.return();
|
|
238
|
-
const after = [...torrent.held];
|
|
239
|
-
assert.ok(
|
|
240
|
-
after.every((range) => pieceOf(range) < 4000),
|
|
241
|
-
"the tail reader left its claim behind"
|
|
242
|
-
);
|
|
243
|
-
|
|
244
|
-
await head.return();
|
|
245
|
-
assert.deepEqual(torrent.held, [], "the last reader left something behind");
|
|
246
|
-
} finally {
|
|
247
|
-
store.destroy(() => undefined);
|
|
248
|
-
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
249
|
-
}
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
test("criticality marks the window being waited for, not the whole range", async () => {
|
|
253
|
-
// Nothing is present, so the reader blocks on its first piece and marks it.
|
|
254
|
-
let arrived = false;
|
|
255
|
-
const { torrent, store, directory } = await recordingTorrent({
|
|
256
|
-
pieceCount: 8000,
|
|
257
|
-
present: () => arrived
|
|
258
|
-
});
|
|
259
|
-
try {
|
|
260
|
-
const iterator = readFragments({
|
|
261
|
-
torrent, fileIndex: 0, start: 0, end: 8000 * PIECE - 1,
|
|
262
|
-
cancellation: { isCancelled: () => false },
|
|
263
|
-
windowBytes: WINDOW_PIECES * PIECE
|
|
264
|
-
});
|
|
265
|
-
const pending = iterator.next();
|
|
266
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
267
|
-
|
|
268
|
-
const criticals = torrent.calls.filter((entry) => entry.call === "critical");
|
|
269
|
-
assert.equal(criticals.length, 1);
|
|
270
|
-
const marked = criticals[0].to - criticals[0].from + 1;
|
|
271
|
-
// The window, and nothing beyond it. Marking only the blocked piece made
|
|
272
|
-
// the pieces after it arrive strictly one at a time — measured, the first
|
|
273
|
-
// segment after a seek took 7.2 s for four pieces. Marking the whole
|
|
274
|
-
// requested range would be the old mistake: for ffmpeg's input that is
|
|
275
|
-
// every piece to the end of the file, and the flag stops meaning anything.
|
|
276
|
-
assert.equal(marked, WINDOW_PIECES, `marked ${marked} pieces critical`);
|
|
277
|
-
assert.ok(criticals[0].to < 8000 - 1, "criticality must not reach the end of the file");
|
|
278
|
-
|
|
279
|
-
arrived = true;
|
|
280
|
-
torrent.emit("verified", 0);
|
|
281
|
-
(await pending).value.release();
|
|
282
|
-
await iterator.return();
|
|
283
|
-
} finally {
|
|
284
|
-
store.destroy(() => undefined);
|
|
285
|
-
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
286
|
-
}
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
test("a reader that is abandoned mid-fragment does not keep the piece pinned", async () => {
|
|
290
|
-
// The pin is taken before the fragment is handed out and released by the
|
|
291
|
-
// consumer — but a seek abandons the iterator between two fragments, and the
|
|
292
|
-
// consumer never gets the chance. Field 2026-08-06: after one seek every slot
|
|
293
|
-
// in the store was pinned, the store answered `Every resident piece is
|
|
294
|
-
// pinned; no slot can be freed` to the WebTorrent client, which closed the
|
|
295
|
-
// store and destroyed the torrent.
|
|
296
|
-
const { torrent, store, directory } = await recordingTorrent({ pieceCount: 40 });
|
|
297
|
-
try {
|
|
298
|
-
const iterator = readFragments({
|
|
299
|
-
torrent,
|
|
300
|
-
fileIndex: 0,
|
|
301
|
-
start: 0,
|
|
302
|
-
end: 40 * PIECE - 1,
|
|
303
|
-
cancellation: { isCancelled: () => false },
|
|
304
|
-
windowBytes: WINDOW_PIECES * PIECE
|
|
305
|
-
});
|
|
306
|
-
await iterator.next(); // held, deliberately NOT released
|
|
307
|
-
await iterator.return(); // what a seek does
|
|
308
|
-
|
|
309
|
-
assert.equal(
|
|
310
|
-
store.stats().pinned,
|
|
311
|
-
0,
|
|
312
|
-
"the abandoned fragment's piece is still pinned; slots leak one per seek"
|
|
313
|
-
);
|
|
314
|
-
} finally {
|
|
315
|
-
store.destroy(() => undefined);
|
|
316
|
-
await fs.rm(directory, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
317
|
-
}
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
// ------------------------------------------ the window that grows into a lead
|
|
321
|
-
|
|
322
|
-
test("a wait that mattered widens the window by a piece", () => {
|
|
323
|
-
// Field 2026-08-17: 5.1-5.9 MB/s delivered against ~1 MB/s consumed, and the
|
|
324
|
-
// reader still blocked 47 times in two minutes. The surplus never became
|
|
325
|
-
// distance ahead of the head.
|
|
326
|
-
assert.equal(
|
|
327
|
-
nextWindowPieces({ current: 4, base: 4, ceiling: 12, waitedMs: 1457, waitThresholdMs: 1000 }),
|
|
328
|
-
5
|
|
329
|
-
);
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
test("a piece that was already there gives a piece back", () => {
|
|
333
|
-
assert.equal(
|
|
334
|
-
nextWindowPieces({ current: 7, base: 4, ceiling: 12, waitedMs: 0, waitThresholdMs: 1000 }),
|
|
335
|
-
6
|
|
336
|
-
);
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
test("it never shrinks below what the caller asked for", () => {
|
|
340
|
-
assert.equal(
|
|
341
|
-
nextWindowPieces({ current: 4, base: 4, ceiling: 12, waitedMs: 0, waitThresholdMs: 1000 }),
|
|
342
|
-
4
|
|
343
|
-
);
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
test("it never grows past this reader's share of the store", () => {
|
|
347
|
-
assert.equal(
|
|
348
|
-
nextWindowPieces({ current: 12, base: 4, ceiling: 12, waitedMs: 4453, waitThresholdMs: 1000 }),
|
|
349
|
-
12
|
|
350
|
-
);
|
|
351
|
-
// A ceiling below the base cannot pull the window under it: the caller sized
|
|
352
|
-
// the base from the file's own byte rate, and a store too small to hold it is
|
|
353
|
-
// an argument about memory, not about what the reader needs next.
|
|
354
|
-
assert.equal(
|
|
355
|
-
nextWindowPieces({ current: 4, base: 4, ceiling: 1, waitedMs: 2000, waitThresholdMs: 1000 }),
|
|
356
|
-
4
|
|
357
|
-
);
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
test("a wait exactly at the threshold counts as a wait", () => {
|
|
361
|
-
assert.equal(
|
|
362
|
-
nextWindowPieces({ current: 4, base: 4, ceiling: 9, waitedMs: 1000, waitThresholdMs: 1000 }),
|
|
363
|
-
5
|
|
364
|
-
);
|
|
365
|
-
});
|
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file A read whose bytes have not arrived is not an answer about the file.
|
|
3
|
-
*
|
|
4
|
-
* Field 2026-10-01: the subtitle plan of an episode was read while its Cues
|
|
5
|
-
* table was still downloading, the read was given up, and the plan kept "no
|
|
6
|
-
* clusters" for the life of the process — the track showed nothing for a whole
|
|
7
|
-
* session while the same table, read eleven seconds later for the keyframes,
|
|
8
|
-
* was there. The same shape was in the container choice, the track table, the
|
|
9
|
-
* keyframe table, MP4's `moov` and AVI's `idx1`. Every check here makes the
|
|
10
|
-
* bytes missing first and present afterwards, and asks the same object again.
|
|
11
|
-
*
|
|
12
|
-
* Fakes only: a buffer and a reader over it. No torrent, no thread.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import test from "node:test";
|
|
16
|
-
import assert from "node:assert/strict";
|
|
17
|
-
|
|
18
|
-
import { MatroskaContainer } from "../services/media/container/MatroskaContainer.js";
|
|
19
|
-
import { Mp4Container } from "../services/media/container/Mp4Container.js";
|
|
20
|
-
import { AviContainer } from "../services/media/container/AviContainer.js";
|
|
21
|
-
import { ContainerOrchestrator } from "../services/media/ContainerOrchestrator.js";
|
|
22
|
-
import { KeyframeTables } from "../services/media/KeyframeTables.js";
|
|
23
|
-
import { isUnavailable } from "../services/media/container/unavailable.js";
|
|
24
|
-
import {
|
|
25
|
-
ID,
|
|
26
|
-
buildMatroska,
|
|
27
|
-
clusterData,
|
|
28
|
-
cueBlock,
|
|
29
|
-
element,
|
|
30
|
-
readerOver,
|
|
31
|
-
stringElement,
|
|
32
|
-
trackEntry,
|
|
33
|
-
uintElement
|
|
34
|
-
} from "./helpers/matroska-file.js";
|
|
35
|
-
|
|
36
|
-
const quiet = { info() {}, warn() {} };
|
|
37
|
-
|
|
38
|
-
function episode({ subtitleLanguage = null, cuesBeforeClusters = false, cues = [1], extraTrack = null } = {}) {
|
|
39
|
-
const tracks = [
|
|
40
|
-
trackEntry({ number: 1, type: 1, codecId: "V_MPEG4/ISO/AVC", language: "jpn" }),
|
|
41
|
-
trackEntry({ number: 3, type: 17, codecId: "S_TEXT/UTF8", language: subtitleLanguage })
|
|
42
|
-
];
|
|
43
|
-
if (extraTrack) {
|
|
44
|
-
tracks.push(extraTrack);
|
|
45
|
-
}
|
|
46
|
-
return buildMatroska({
|
|
47
|
-
tracks,
|
|
48
|
-
cues,
|
|
49
|
-
cuesBeforeClusters,
|
|
50
|
-
clusters: [
|
|
51
|
-
{ ticks: 0, data: clusterData({ ticks: 0, blocks: [cueBlock({ track: 3, relativeTicks: 100, durationTicks: 900, text: "one" })] }) },
|
|
52
|
-
{ ticks: 5000, data: clusterData({ ticks: 5000, blocks: [cueBlock({ track: 3, relativeTicks: 0, durationTicks: 900, text: "two" })] }) }
|
|
53
|
-
]
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
test("a subtitle plan read before the Cues table arrived is not kept; read again, it names the clusters", async () => {
|
|
58
|
-
const { file, cuesAt, clusterAt } = episode({ cues: [1, 3] });
|
|
59
|
-
let tailHere = false;
|
|
60
|
-
const { read } = readerOver(file, { missing: (start) => !tailHere && start >= cuesAt });
|
|
61
|
-
const container = new MatroskaContainer({ readRange: read, fileSize: file.length });
|
|
62
|
-
|
|
63
|
-
await assert.rejects(container.readSubtitlePlan(), (error) => isUnavailable(error));
|
|
64
|
-
|
|
65
|
-
tailHere = true;
|
|
66
|
-
const plan = await container.readSubtitlePlan();
|
|
67
|
-
assert.equal(plan.cuesState, "complete");
|
|
68
|
-
assert.deepEqual(plan.tracks[0].clusterPositions, clusterAt, "both clusters the table names for the track");
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test("a read that returns fewer bytes than asked is not taken for the element", async () => {
|
|
72
|
-
const { file } = episode();
|
|
73
|
-
const { read } = readerOver(file, { short: true });
|
|
74
|
-
await assert.rejects(new MatroskaContainer({ readRange: read, fileSize: file.length }).readTracks(), (error) =>
|
|
75
|
-
isUnavailable(error)
|
|
76
|
-
);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test("a head that has not arrived leaves no container behind, and the next ask builds one", async () => {
|
|
80
|
-
const { file } = episode();
|
|
81
|
-
let headHere = false;
|
|
82
|
-
const { read } = readerOver(file, { missing: (start) => !headHere && start === 0 });
|
|
83
|
-
const containers = new ContainerOrchestrator();
|
|
84
|
-
const params = { sourceKey: "s", fileIndex: 0, readRange: read, fileSize: file.length, label: "x.mkv" };
|
|
85
|
-
|
|
86
|
-
await assert.rejects(containers.containerFor(params), (error) => isUnavailable(error));
|
|
87
|
-
assert.equal(containers.known("s", 0), undefined, "nothing was kept for a head that is not here");
|
|
88
|
-
assert.deepEqual(await containers.getTracks(params), [], "the track table answers nothing yet");
|
|
89
|
-
|
|
90
|
-
headHere = true;
|
|
91
|
-
const container = await containers.containerFor(params);
|
|
92
|
-
assert.equal(container.formatName, "matroska");
|
|
93
|
-
assert.equal((await containers.getTracks(params)).length, 2);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test("a keyframe table asked for while its Cues are missing stays unanswered, and is filled when they arrive", async () => {
|
|
97
|
-
const { file, cuesAt } = episode();
|
|
98
|
-
let tailHere = false;
|
|
99
|
-
const { read } = readerOver(file, { missing: (start) => !tailHere && start >= cuesAt });
|
|
100
|
-
const containers = new ContainerOrchestrator();
|
|
101
|
-
const params = { sourceKey: "k", fileIndex: 0, readRange: read, fileSize: file.length, label: "x.mkv" };
|
|
102
|
-
const tables = new KeyframeTables({
|
|
103
|
-
readTable: async () => {
|
|
104
|
-
const index = await containers.getKeyframeIndex(params);
|
|
105
|
-
return { times: index?.times ?? null, tolerance: 0, format: "matroska" };
|
|
106
|
-
},
|
|
107
|
-
logger: quiet
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
await assert.rejects(tables.read({ sourceKey: "k", fileIndex: 0 }), (error) => isUnavailable(error));
|
|
111
|
-
const table = tables.of({ sourceKey: "k", fileIndex: 0 });
|
|
112
|
-
assert.equal(table.answered, false, "a read that did not arrive is not an answer");
|
|
113
|
-
|
|
114
|
-
tailHere = true;
|
|
115
|
-
tables.readAgainIfUnanswered({ sourceKey: "k", fileIndex: 0 });
|
|
116
|
-
await tables.read({ sourceKey: "k", fileIndex: 0 });
|
|
117
|
-
assert.equal(table.answered, true);
|
|
118
|
-
assert.deepEqual(table.times, [0, 5], "the same object the sessions hold now carries the times");
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test("a Cues element stored before the first cluster, with no SeekHead entry, is found", async () => {
|
|
122
|
-
const { file } = episode({ cuesBeforeClusters: true });
|
|
123
|
-
const { read } = readerOver(file);
|
|
124
|
-
const index = await new MatroskaContainer({ readRange: read, fileSize: file.length }).parseKeyframeIndex();
|
|
125
|
-
assert.deepEqual(index.times, [0, 5]);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
test("a Tracks element larger than the head window is read by its own size", async () => {
|
|
129
|
-
// A CodecPrivate of 80 KB pushes the end of Tracks past the 64 KB head.
|
|
130
|
-
const big = element(
|
|
131
|
-
ID.TRACK_ENTRY,
|
|
132
|
-
Buffer.concat([
|
|
133
|
-
uintElement(ID.TRACK_NUMBER, 4),
|
|
134
|
-
uintElement(ID.TRACK_TYPE, 17),
|
|
135
|
-
stringElement(ID.CODEC_ID, "S_TEXT/ASS"),
|
|
136
|
-
element(0x63a2, Buffer.alloc(80 * 1024, 0x20))
|
|
137
|
-
])
|
|
138
|
-
);
|
|
139
|
-
const { file } = episode({ extraTrack: big });
|
|
140
|
-
const { read } = readerOver(file);
|
|
141
|
-
const tracks = await new MatroskaContainer({ readRange: read, fileSize: file.length }).readTracks();
|
|
142
|
-
assert.deepEqual(tracks.map((track) => track.trackNumber), [1, 3, 4]);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
test("the language: LanguageBCP47 wins, then Language, then the format's default `eng`; `und` stays unknown", async () => {
|
|
146
|
-
const cases = [
|
|
147
|
-
{ entry: { language: null }, language: "eng", source: "default" },
|
|
148
|
-
{ entry: { language: "" }, language: "eng", source: "language" },
|
|
149
|
-
{ entry: { language: "rus" }, language: "rus", source: "language" },
|
|
150
|
-
{ entry: { language: null, languageBcp47: "pt-BR" }, language: "pt-BR", source: "bcp47" },
|
|
151
|
-
{ entry: { language: "por", languageBcp47: "pt-BR" }, language: "pt-BR", source: "bcp47" },
|
|
152
|
-
{ entry: { language: "und" }, language: "und", source: "language" }
|
|
153
|
-
];
|
|
154
|
-
for (const { entry, language, source } of cases) {
|
|
155
|
-
const { file } = buildMatroska({
|
|
156
|
-
tracks: [trackEntry({ number: 3, type: 17, codecId: "S_TEXT/UTF8", ...entry })],
|
|
157
|
-
cues: null,
|
|
158
|
-
clusters: [{ ticks: 0, data: clusterData({ ticks: 0, blocks: [] }) }]
|
|
159
|
-
});
|
|
160
|
-
const { read } = readerOver(file);
|
|
161
|
-
const [track] = await new MatroskaContainer({ readRange: read, fileSize: file.length }).readTracks();
|
|
162
|
-
assert.equal(track.language, language, JSON.stringify(entry));
|
|
163
|
-
assert.equal(track.languageSource, source, JSON.stringify(entry));
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
/** @param {string} type @param {Buffer} payload */
|
|
168
|
-
function box(type, payload) {
|
|
169
|
-
const header = Buffer.alloc(8);
|
|
170
|
-
header.writeUInt32BE(8 + payload.length, 0);
|
|
171
|
-
header.write(type, 4, "latin1");
|
|
172
|
-
return Buffer.concat([header, payload]);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
test("an MP4 whose `moov` has not arrived keeps nothing; read again, it answers", async () => {
|
|
176
|
-
const mvhd = Buffer.alloc(100);
|
|
177
|
-
mvhd.writeUInt32BE(1000, 12); // timescale
|
|
178
|
-
mvhd.writeUInt32BE(5000, 16); // duration
|
|
179
|
-
const ftyp = box("ftyp", Buffer.from("isom\0\0\0\0isom", "latin1"));
|
|
180
|
-
const moov = box("moov", box("mvhd", mvhd));
|
|
181
|
-
const file = Buffer.concat([ftyp, moov]);
|
|
182
|
-
let moovHere = false;
|
|
183
|
-
const { read } = readerOver(file, { missing: (start) => !moovHere && start >= ftyp.length });
|
|
184
|
-
const container = new Mp4Container({ readRange: read, fileSize: file.length });
|
|
185
|
-
|
|
186
|
-
await assert.rejects(container.readMediaInfo(), (error) => isUnavailable(error));
|
|
187
|
-
moovHere = true;
|
|
188
|
-
assert.equal((await container.readMediaInfo()).durationSeconds, 5);
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
test("an MP4 `moov` larger than this reads whole is refused by name, not reported absent", async () => {
|
|
192
|
-
const ftyp = box("ftyp", Buffer.from("isom\0\0\0\0isom", "latin1"));
|
|
193
|
-
const header = Buffer.alloc(8);
|
|
194
|
-
header.writeUInt32BE(40 * 1024 * 1024, 0);
|
|
195
|
-
header.write("moov", 4, "latin1");
|
|
196
|
-
const file = Buffer.concat([ftyp, header, Buffer.alloc(64)]);
|
|
197
|
-
const { read } = readerOver(file);
|
|
198
|
-
const container = new Mp4Container({ readRange: read, fileSize: file.length });
|
|
199
|
-
assert.equal(await container.readSubtitlePlan(), null);
|
|
200
|
-
assert.deepEqual(container.moovRefused, { size: 40 * 1024 * 1024, limit: 32 * 1024 * 1024 });
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
test("an AVI head that has not arrived leaves no media info behind", async () => {
|
|
204
|
-
const avih = Buffer.alloc(56);
|
|
205
|
-
avih.writeUInt32LE(40_000, 0); // 25 frames a second
|
|
206
|
-
avih.writeUInt32LE(250, 16); // ten seconds
|
|
207
|
-
const chunk = Buffer.concat([Buffer.from("avih", "latin1"), Buffer.from([56, 0, 0, 0]), avih]);
|
|
208
|
-
const list = Buffer.concat([Buffer.from("LIST", "latin1"), Buffer.alloc(4), Buffer.from("hdrl", "latin1"), chunk]);
|
|
209
|
-
list.writeUInt32LE(list.length - 8, 4);
|
|
210
|
-
const riff = Buffer.concat([Buffer.from("RIFF", "latin1"), Buffer.alloc(4), Buffer.from("AVI ", "latin1"), list]);
|
|
211
|
-
riff.writeUInt32LE(riff.length - 8, 4);
|
|
212
|
-
let here = false;
|
|
213
|
-
const { read } = readerOver(riff, { missing: () => !here });
|
|
214
|
-
const container = new AviContainer({ readRange: read, fileSize: riff.length });
|
|
215
|
-
await assert.rejects(container.readMediaInfo(), (error) => isUnavailable(error));
|
|
216
|
-
here = true;
|
|
217
|
-
assert.equal((await container.readMediaInfo()).durationSeconds, 10);
|
|
218
|
-
});
|
package/test/resume-warm.test.js
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Where a viewer's resume position falls in the file, in bytes.
|
|
3
|
-
*
|
|
4
|
-
* The warm-up fetches a file's two edges, because that is what the codec probe
|
|
5
|
-
* reads. The region the VIEWER will resume at was asked for by nobody until the
|
|
6
|
-
* encoder opened its input — field 2026-09-03, 53 s after the Retry button on a
|
|
7
|
-
* cold torrent, and the piece it then needed took another 46.3 s to arrive.
|
|
8
|
-
* Turning the position into an offset is the only arithmetic in that path, so it
|
|
9
|
-
* is the only part with anything to get wrong.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import assert from "node:assert/strict";
|
|
13
|
-
import test from "node:test";
|
|
14
|
-
|
|
15
|
-
import { resumeByteOffset } from "../services/torrent/worker/resume-warm.js";
|
|
16
|
-
|
|
17
|
-
test("a position halfway through a film is halfway through its file", () => {
|
|
18
|
-
assert.equal(resumeByteOffset(1000, 100, 50), 500);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
test("the field case lands where the encoder went looking", () => {
|
|
22
|
-
// The measured session: 171.338 s into a 23:41 episode of 541 MB, and the
|
|
23
|
-
// read that blocked was at 63 MB.
|
|
24
|
-
const at = resumeByteOffset(541 * 1024 * 1024, 23 * 60 + 41, 171.338);
|
|
25
|
-
const megabytes = at / (1024 * 1024);
|
|
26
|
-
assert.ok(megabytes > 60 && megabytes < 68, `landed at ${megabytes.toFixed(1)}MB`);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test("a position past the end reads the end, rather than past it", () => {
|
|
30
|
-
const length = 1000;
|
|
31
|
-
const at = resumeByteOffset(length, 100, 10_000);
|
|
32
|
-
assert.equal(at, length - 1);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test("nothing is known, nothing is guessed", () => {
|
|
36
|
-
assert.equal(resumeByteOffset(0, 100, 50), 0, "no file length");
|
|
37
|
-
assert.equal(resumeByteOffset(1000, 0, 50), 0, "no duration — the container did not declare one");
|
|
38
|
-
assert.equal(resumeByteOffset(1000, 100, 0), 0, "the viewer is at the beginning, where the edges already are");
|
|
39
|
-
});
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file The drawing cannot disagree with the table.
|
|
3
|
-
*
|
|
4
|
-
* `docs/encode-run-state.md` is generated from `services/encode/encode-run-state.js`.
|
|
5
|
-
* A picture kept beside the code drifts — this repository has the receipts —
|
|
6
|
-
* so the check is mechanical: regenerate it and compare. A behavioural change
|
|
7
|
-
* to the table that forgets `npm run graph` fails here, in the same commit.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import assert from "node:assert/strict";
|
|
11
|
-
import { readFileSync } from "node:fs";
|
|
12
|
-
import test from "node:test";
|
|
13
|
-
|
|
14
|
-
import { GRAPH_DOC_PATH, renderRunGraphMarkdown } from "../scripts/render-run-graph.js";
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Line endings are not part of the comparison. Git is configured with
|
|
18
|
-
* `autocrlf=true` on the machine this is developed on, so the same file is LF
|
|
19
|
-
* in the index and CRLF in the working tree — and a test failing on that would
|
|
20
|
-
* point at the generator, whose output is always LF, and be unfixable by the
|
|
21
|
-
* remedy it suggests.
|
|
22
|
-
*
|
|
23
|
-
* @param {string} text
|
|
24
|
-
* @returns {string}
|
|
25
|
-
*/
|
|
26
|
-
function withoutLineEndings(text) {
|
|
27
|
-
return text.replace(/\r\n/g, "\n");
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
test("the committed picture is the one the table produces", () => {
|
|
31
|
-
const committed = readFileSync(GRAPH_DOC_PATH, "utf8");
|
|
32
|
-
assert.equal(
|
|
33
|
-
withoutLineEndings(committed),
|
|
34
|
-
withoutLineEndings(renderRunGraphMarkdown()),
|
|
35
|
-
"docs/encode-run-state.md is out of date — run `npm run graph` and commit the result"
|
|
36
|
-
);
|
|
37
|
-
});
|