@torrent-tv/proxy 2.74.1 → 2.76.0
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/CHANGELOG.md +1504 -1453
- package/CLAUDE.md +19 -6
- package/biome.json +182 -1
- package/docs/container-architecture.md +27 -6
- package/docs/encode-run-state.md +1 -1
- package/knip.json +14 -0
- package/package.json +1 -1
- package/routes/api/sources/warm/post.js +1 -1
- package/routes/api/transcode-sessions/post.js +185 -185
- package/routes/api/transcode-sessions/progress/get.js +5 -1
- package/routes/transcode/audio-file/get.js +11 -1
- package/routes/transcode/audio-warm/get.js +11 -1
- package/routes/transcode/session-file/get.js +1 -1
- package/routes/transcode/variant-file/get.js +10 -1
- package/scripts/render-run-graph.js +2 -2
- package/server.js +25 -0
- package/services/audio-inventory.js +9 -214
- package/services/container/AviContainer.js +266 -81
- package/services/container/Container.js +281 -80
- package/services/container/ContainerFactory.js +67 -0
- package/services/container/MatroskaContainer.js +327 -8
- package/services/container/Mp4Container.js +373 -27
- package/services/container/SubtitleFileContainer.js +0 -1
- package/services/controllers/SubtitleController.js +128 -128
- package/services/demand/index.js +7 -10
- package/services/download/registry.js +0 -14
- package/services/encode/CoverageMap.js +281 -0
- package/services/encode/EncodePlan.js +255 -0
- package/services/encode/EncodeRun.js +587 -0
- package/services/encode/Encoder.js +84 -0
- package/services/encode/NvencEncoder.js +45 -0
- package/services/encode/QsvEncoder.js +47 -0
- package/services/encode/SegmentDemand.js +0 -0
- package/services/encode/SegmentStore.js +529 -0
- package/services/encode/SoftwareEncoder.js +111 -0
- package/services/encode/V4l2m2mEncoder.js +53 -0
- package/services/encode/VaapiEncoder.js +53 -0
- package/services/encode/args.js +200 -0
- package/services/{encode-exit.js → encode/encode-exit.js} +17 -0
- package/services/encode/index.js +9 -0
- package/services/encode/run-command.js +647 -0
- package/services/hls-session-manager.js +11073 -10711
- package/services/hwaccel.js +1688 -1992
- package/services/orchestrators/EncodeOrchestrator.js +359 -0
- package/services/output/LiveOutputs.js +213 -0
- package/services/output/Output.js +94 -0
- package/services/output/OutputSpec.js +195 -0
- package/services/output/Timeline.js +220 -0
- package/services/output/index.js +1 -0
- package/services/output/ladder.js +26 -0
- package/services/playback-planner.js +806 -747
- package/services/produced-index.js +222 -300
- package/services/source/SourceFile.js +346 -0
- package/services/{sidecar-files.js → torrent/files.js} +107 -11
- package/services/torrent/naming.js +619 -0
- package/services/torrent-worker/client.js +10 -0
- package/services/torrent-worker/container-tracks.js +71 -43
- package/services/torrent-worker/pool-adapter.js +370 -333
- package/services/torrent-worker/protocol.js +7 -0
- package/services/torrent-worker/subtitle-cues.js +549 -549
- package/services/torrent-worker/worker.js +18 -0
- package/services/tracks/AudioTrack.js +131 -40
- package/services/tracks/TextSubtitleTrack.js +287 -287
- package/services/tracks/index.js +15 -14
- package/services/viewer/Viewer.js +145 -0
- package/services/viewer/Viewers.js +124 -0
- package/test/audio-inventory.test.js +176 -177
- package/test/auto-quality-step.test.js +508 -506
- package/test/behind-head-repair.test.js +17 -7
- package/test/coverage-map.test.js +153 -0
- package/test/cut-times-timeline.test.js +6 -5
- package/test/cuts-follow-published-grid.test.js +4 -4
- package/test/decode-cost.test.js +31 -12
- package/test/encode-exit.test.js +1 -1
- package/test/encode-orchestrator.test.js +196 -0
- package/test/encode-plan.test.js +245 -0
- package/test/encode-run-state.test.js +2 -2
- package/test/encode-run.test.js +168 -0
- package/test/encoder-kinds.test.js +122 -0
- package/test/held-request-width.test.js +9 -3
- package/test/helpers/encode-run.js +128 -0
- package/test/keyframe-index-accuracy.test.js +19 -12
- package/test/keyframes-belong-to-the-file.test.js +132 -0
- package/test/matroska-cues-track.test.js +192 -192
- package/test/mp4-composition-times.test.js +0 -0
- package/test/orchestrator-wired.test.js +164 -0
- package/test/output-shape.test.js +68 -0
- package/test/output-spec.test.js +157 -0
- package/test/produced-copy-choice.test.js +58 -92
- package/test/produced-index.test.js +142 -188
- package/test/quality-variants.test.js +1079 -1075
- package/test/run-graph-drift.test.js +1 -1
- package/test/run-intervals.test.js +329 -0
- package/test/run-position-follows-published-grid.test.js +4 -4
- package/test/seek-landing.test.js +8 -8
- package/test/seek-target-not-superseded.test.js +21 -9
- package/test/segment-demand.test.js +82 -0
- package/test/segment-serve-wiring.test.js +47 -52
- package/test/segment-store.test.js +187 -0
- package/test/segments-are-shared.test.js +175 -0
- package/test/sidecar-naming.test.js +142 -0
- package/test/source-file.test.js +133 -0
- package/test/stale-request-after-seek.test.js +18 -12
- package/test/subtitle-language.test.js +252 -252
- package/test/timeline.test.js +95 -0
- package/test/{sidecar-files.test.js → torrent-files.test.js} +44 -1
- package/test/torrent-naming.test.js +255 -0
- package/test/tracks-begin-together.test.js +44 -32
- package/test/two-viewers-one-picture.test.js +347 -0
- package/test/video-facts.test.js +102 -0
- package/test/viewer-outputs.test.js +273 -0
- package/test/viewer.test.js +91 -0
- package/utils/perf.js +1 -63
- package/services/container/index.js +0 -6
- package/services/container-index/avi.js +0 -167
- package/services/container-index/index.js +0 -118
- package/services/container-index/matroska.js +0 -336
- package/services/container-index/mp4.js +0 -358
- package/services/controllers/index.js +0 -2
- package/services/download/index.js +0 -8
- package/services/orchestrators/index.js +0 -2
- /package/services/{container-index → container}/ebml-reader.js +0 -0
- /package/services/{encode-run-state.js → encode/encode-run-state.js} +0 -0
- /package/services/{language-detect.js → tracks/language-detect.js} +0 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file A real `EncodeRun` over a process that is not real.
|
|
3
|
+
*
|
|
4
|
+
* Tests used to set a session's process fields by hand — `session.ffmpeg`,
|
|
5
|
+
* `session.runState`, `session.encodeStartIndex` — which is how a test can pass
|
|
6
|
+
* over code that no longer works: the fields were the thing under test as much
|
|
7
|
+
* as the behaviour was. A run is an object now, so a test builds the object the
|
|
8
|
+
* product builds and injects only the one thing a test may not have, which is a
|
|
9
|
+
* child process.
|
|
10
|
+
*
|
|
11
|
+
* The pid is deliberately absent unless a test asks for one. `pause` and
|
|
12
|
+
* `resume` send signals by pid, and a made-up number is somebody else's process
|
|
13
|
+
* on the machine running the tests.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { EncodeRun } from "../../services/encode/EncodeRun.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A child process that records what was done to it.
|
|
20
|
+
*
|
|
21
|
+
* @param {object} [options]
|
|
22
|
+
* @param {number | null} [options.pid] - Only where a test genuinely exercises
|
|
23
|
+
* suspend or resume, and then it must be this process's own.
|
|
24
|
+
* @returns {object}
|
|
25
|
+
*/
|
|
26
|
+
export function fakeProcess({ pid = null, exitsWhenKilled = true } = {}) {
|
|
27
|
+
/** @type {Map<string, (...args: unknown[]) => void>} */
|
|
28
|
+
const listeners = new Map();
|
|
29
|
+
return {
|
|
30
|
+
pid,
|
|
31
|
+
killed: false,
|
|
32
|
+
exitCode: null,
|
|
33
|
+
signalCode: null,
|
|
34
|
+
/** Every signal it was sent, in order. */
|
|
35
|
+
signals: [],
|
|
36
|
+
stdout: { on() {} },
|
|
37
|
+
stderr: { on() {} },
|
|
38
|
+
on(event, handler) {
|
|
39
|
+
const kept = listeners.get(event) ?? [];
|
|
40
|
+
kept.push(handler);
|
|
41
|
+
listeners.set(event, kept);
|
|
42
|
+
return this;
|
|
43
|
+
},
|
|
44
|
+
once(event, handler) {
|
|
45
|
+
return this.on(event, handler);
|
|
46
|
+
},
|
|
47
|
+
kill(signal = "SIGTERM") {
|
|
48
|
+
this.signals.push(signal);
|
|
49
|
+
this.killed = true;
|
|
50
|
+
// A real process answers a signal by exiting, and it does so on a later
|
|
51
|
+
// turn. A fake that never exits makes every disposal wait out the grace
|
|
52
|
+
// period, which is two seconds a test spends proving nothing.
|
|
53
|
+
if (exitsWhenKilled && this.exitCode === null && this.signalCode === null) {
|
|
54
|
+
queueMicrotask(() => this.exit(null, signal));
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
},
|
|
58
|
+
/**
|
|
59
|
+
* Report an exit, as the real thing would.
|
|
60
|
+
*
|
|
61
|
+
* @param {number | null} code
|
|
62
|
+
* @param {string | null} [signal]
|
|
63
|
+
*/
|
|
64
|
+
exit(code, signal = null) {
|
|
65
|
+
this.exitCode = code;
|
|
66
|
+
this.signalCode = signal;
|
|
67
|
+
for (const handler of listeners.get("exit") ?? []) {
|
|
68
|
+
handler(code, signal);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** A logger that says nothing, for tests that are not about the log. */
|
|
75
|
+
export const silentLogger = { info() {}, warn() {}, error() {} };
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Add a run to a session, started, over a fake process.
|
|
79
|
+
*
|
|
80
|
+
* A session holds a SET of runs — as many as the machine affords — so a test
|
|
81
|
+
* that wants two heads on one output calls this twice.
|
|
82
|
+
*
|
|
83
|
+
* @param {object} session - The session under test.
|
|
84
|
+
* @param {object} [options]
|
|
85
|
+
* @param {number} [options.from] - First segment number it is making.
|
|
86
|
+
* @param {number} [options.to] - Last, inclusive; below `from` means no end.
|
|
87
|
+
* @param {object} [options.process] - The process it should own.
|
|
88
|
+
* @param {boolean} [options.producing] - Whether it has already made its first
|
|
89
|
+
* segment, which is what moves it out of starting.
|
|
90
|
+
* @param {number | null} [options.lastSegmentIndex] - The film's last number,
|
|
91
|
+
* for telling a finished file from an input that dried up.
|
|
92
|
+
* @param {boolean} [options.usesExplicitCuts]
|
|
93
|
+
* @returns {import("../../services/encode/EncodeRun.js").EncodeRun}
|
|
94
|
+
*/
|
|
95
|
+
export function startRunOn(session, options = {}) {
|
|
96
|
+
const {
|
|
97
|
+
from = 0,
|
|
98
|
+
to = -1,
|
|
99
|
+
process: child = fakeProcess(),
|
|
100
|
+
producing = true,
|
|
101
|
+
lastSegmentIndex = null,
|
|
102
|
+
usesExplicitCuts = false
|
|
103
|
+
} = options;
|
|
104
|
+
const run = new EncodeRun({
|
|
105
|
+
address: session.outputKey ?? session.id ?? "output",
|
|
106
|
+
encoder: { name: "libx264", kind: "software" },
|
|
107
|
+
from,
|
|
108
|
+
to,
|
|
109
|
+
buildArgs: () => [],
|
|
110
|
+
spawn: () => child,
|
|
111
|
+
logger: silentLogger,
|
|
112
|
+
lastSegmentIndex: () => lastSegmentIndex,
|
|
113
|
+
usesExplicitCuts
|
|
114
|
+
});
|
|
115
|
+
run.start("a test asked for it");
|
|
116
|
+
if (producing) {
|
|
117
|
+
// What moves a run out of starting is its first segment, in the product as
|
|
118
|
+
// here — so a run that is producing has made one, and its head stands one
|
|
119
|
+
// past where it began. `from` is still where it started, which is what a
|
|
120
|
+
// test asserting a run's position asks for.
|
|
121
|
+
run.noteProduced(from);
|
|
122
|
+
}
|
|
123
|
+
if (!(session.runs instanceof Set)) {
|
|
124
|
+
session.runs = new Set();
|
|
125
|
+
}
|
|
126
|
+
session.runs.add(run);
|
|
127
|
+
return run;
|
|
128
|
+
}
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
|
|
16
16
|
import test from "node:test";
|
|
17
17
|
import assert from "node:assert/strict";
|
|
18
|
+
import { SourceFile } from "../services/source/SourceFile.js";
|
|
19
|
+
import { Timeline } from "../services/output/Timeline.js";
|
|
18
20
|
import { newIndexCheck, noteIndexDeviation } from "../services/hls-session-manager.js";
|
|
19
21
|
|
|
20
22
|
test("an index that describes its file exactly is reported as such", () => {
|
|
@@ -65,14 +67,17 @@ test("a boundary the index got wrong is replaced by the time the file really has
|
|
|
65
67
|
localPort: 9090
|
|
66
68
|
});
|
|
67
69
|
t.after(() => manager.disposeAll());
|
|
70
|
+
// ONE table for the film, held by both. It used to be a copy each, kept in
|
|
71
|
+
// step by writing the correction into every member — which is what the shared
|
|
72
|
+
// table replaces, and what drifted in the field.
|
|
73
|
+
const boundaries = [0, 10, 20, 30, 40];
|
|
68
74
|
const base = {
|
|
69
75
|
id: "aaaaaaaa-1111-2222-3333-444444444444",
|
|
70
76
|
fileName: "film.mkv",
|
|
71
77
|
state: "ready",
|
|
72
78
|
transcodeVideo: false,
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
variants: new Map(),
|
|
79
|
+
timeline: new Timeline({ boundaries: boundaries, cutGrid: "uniform" }),
|
|
80
|
+
file: new SourceFile({ sourceKey: "source-1", fileIndex: 0, name: "film.mkv" }),
|
|
76
81
|
segmentFormat: { segmentFileName: (index) => `segment-${index}.mp4` }
|
|
77
82
|
};
|
|
78
83
|
const rung = {
|
|
@@ -80,11 +85,13 @@ test("a boundary the index got wrong is replaced by the time the file really has
|
|
|
80
85
|
fileName: "film.mkv",
|
|
81
86
|
state: "ready",
|
|
82
87
|
transcodeVideo: true,
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
88
|
+
timeline: new Timeline({ boundaries: boundaries, cutGrid: "uniform" }),
|
|
89
|
+
// A step of the picture: the same file, and made as a step.
|
|
90
|
+
file: base.file,
|
|
91
|
+
variantHeight: 540,
|
|
92
|
+
isStep: true
|
|
86
93
|
};
|
|
87
|
-
base.
|
|
94
|
+
base.file.stepHeights.set(540, 540);
|
|
88
95
|
manager.sessionsById.set(base.id, base);
|
|
89
96
|
manager.sessionsById.set(rung.id, rung);
|
|
90
97
|
|
|
@@ -93,17 +100,17 @@ test("a boundary the index got wrong is replaced by the time the file really has
|
|
|
93
100
|
manager.correctBoundaryFromSegment(base, 2, 17.4);
|
|
94
101
|
|
|
95
102
|
assert.equal(
|
|
96
|
-
base.
|
|
103
|
+
base.timeline.boundaries[2],
|
|
97
104
|
17.4,
|
|
98
105
|
"the grid must describe the file, not the index — a rung forced onto 20 s would not join the copy"
|
|
99
106
|
);
|
|
100
107
|
assert.equal(
|
|
101
|
-
rung.
|
|
108
|
+
rung.timeline.boundaries[2],
|
|
102
109
|
17.4,
|
|
103
|
-
"the family shares one grid, so
|
|
110
|
+
"the family shares one grid — the same array, so there is nothing to keep in step"
|
|
104
111
|
);
|
|
105
112
|
assert.deepEqual(
|
|
106
|
-
base.
|
|
113
|
+
base.timeline.boundaries,
|
|
107
114
|
[0, 10, 17.4, 30, 40],
|
|
108
115
|
"only the boundary that was shown to be wrong moves"
|
|
109
116
|
);
|
|
@@ -114,7 +121,7 @@ test("a boundary the index got wrong is replaced by the time the file really has
|
|
|
114
121
|
manager.correctBoundaryFromSegment(base, 2, 35);
|
|
115
122
|
manager.correctBoundaryFromSegment(base, 2, 5);
|
|
116
123
|
manager.correctBoundaryFromSegment(base, 0, 3);
|
|
117
|
-
assert.deepEqual(base.
|
|
124
|
+
assert.deepEqual(base.timeline.boundaries, [0, 10, 17.4, 30, 40], "out-of-order readings are refused");
|
|
118
125
|
});
|
|
119
126
|
|
|
120
127
|
test("a segment requested again is not new evidence", () => {
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Where a file's keyframes are is a fact of the file, not of a session.
|
|
3
|
+
*
|
|
4
|
+
* It is a property of immutable bytes, like the duration and the track list, so
|
|
5
|
+
* a second reading could only agree. Two sessions created in the same moment
|
|
6
|
+
* used to read it twice — which is what two viewers opening one film do,
|
|
7
|
+
* measured 13 ms apart on 2026-09-03 — and the answer decides whether the
|
|
8
|
+
* picture can be copied at all, so it has to be one answer.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import test from "node:test";
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
import { HlsSessionManager } from "../services/hls-session-manager.js";
|
|
14
|
+
import { Container } from "../services/container/Container.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @returns {HlsSessionManager}
|
|
18
|
+
*/
|
|
19
|
+
function manager() {
|
|
20
|
+
return new HlsSessionManager({
|
|
21
|
+
enabled: true,
|
|
22
|
+
ffmpegBin: "ffmpeg",
|
|
23
|
+
localBindHost: "127.0.0.1",
|
|
24
|
+
localPort: 9090
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
test("two sessions asking at once make one read and join one wait", async () => {
|
|
29
|
+
const session = manager();
|
|
30
|
+
let reads = 0;
|
|
31
|
+
let answer = null;
|
|
32
|
+
session.getContainerKeyframes = () => {
|
|
33
|
+
reads += 1;
|
|
34
|
+
return new Promise((resolve) => {
|
|
35
|
+
answer = resolve;
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const first = session.warmKeyframeIndex({ sourceKey: "torrent:abc", fileIndex: 0, logName: "a.mkv" });
|
|
40
|
+
const second = session.warmKeyframeIndex({ sourceKey: "torrent:abc", fileIndex: 0, logName: "a.mkv" });
|
|
41
|
+
assert.equal(reads, 1, "the second asker joined the read already running");
|
|
42
|
+
|
|
43
|
+
answer({ times: [0, 4, 8], tolerance: 0 });
|
|
44
|
+
await Promise.all([first, second]);
|
|
45
|
+
assert.equal(reads, 1);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("the answer is remembered for the file, so a later session reads nothing", async () => {
|
|
49
|
+
const session = manager();
|
|
50
|
+
let reads = 0;
|
|
51
|
+
session.getContainerKeyframes = async () => {
|
|
52
|
+
reads += 1;
|
|
53
|
+
return { times: [0, 4, 8], tolerance: 0 };
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
await session.warmKeyframeIndex({ sourceKey: "torrent:abc", fileIndex: 0, logName: "a.mkv" });
|
|
57
|
+
await session.warmKeyframeIndex({ sourceKey: "torrent:abc", fileIndex: 0, logName: "a.mkv" });
|
|
58
|
+
|
|
59
|
+
assert.equal(reads, 1);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("two files of one torrent are two answers", async () => {
|
|
63
|
+
const session = manager();
|
|
64
|
+
const asked = [];
|
|
65
|
+
session.getContainerKeyframes = async ({ fileIndex }) => {
|
|
66
|
+
asked.push(fileIndex);
|
|
67
|
+
return { times: [0, 4], tolerance: 0 };
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
await session.warmKeyframeIndex({ sourceKey: "torrent:abc", fileIndex: 0, logName: "a.mkv" });
|
|
71
|
+
await session.warmKeyframeIndex({ sourceKey: "torrent:abc", fileIndex: 1, logName: "b.mkv" });
|
|
72
|
+
|
|
73
|
+
assert.deepEqual(asked, [0, 1]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("a file with no readable index says so once, and keeps saying it", async () => {
|
|
77
|
+
const session = manager();
|
|
78
|
+
let reads = 0;
|
|
79
|
+
session.getContainerKeyframes = async () => {
|
|
80
|
+
reads += 1;
|
|
81
|
+
return null;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
await session.warmKeyframeIndex({ sourceKey: "torrent:abc", fileIndex: 0, logName: "a.ts" });
|
|
85
|
+
await session.warmKeyframeIndex({ sourceKey: "torrent:abc", fileIndex: 0, logName: "a.ts" });
|
|
86
|
+
|
|
87
|
+
// "No index" is an answer about the file — it is what makes a copy of it
|
|
88
|
+
// re-encode instead — and it must be the same answer for every viewer.
|
|
89
|
+
assert.equal(reads, 1);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("a container reads its own table once", async () => {
|
|
93
|
+
let parses = 0;
|
|
94
|
+
class OneTable extends Container {
|
|
95
|
+
async parseKeyframeIndex() {
|
|
96
|
+
parses += 1;
|
|
97
|
+
return { times: [0, 2, 4], tolerance: 0 };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const container = new OneTable({ readRange: async () => null, fileSize: 10 });
|
|
101
|
+
|
|
102
|
+
const [first, second] = await Promise.all([
|
|
103
|
+
container.readKeyframeIndex(),
|
|
104
|
+
container.readKeyframeIndex()
|
|
105
|
+
]);
|
|
106
|
+
await container.readKeyframeIndex();
|
|
107
|
+
|
|
108
|
+
assert.equal(parses, 1);
|
|
109
|
+
assert.deepEqual(first, second);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("a read that threw is not remembered as an answer", async () => {
|
|
113
|
+
let parses = 0;
|
|
114
|
+
class Late extends Container {
|
|
115
|
+
async parseKeyframeIndex() {
|
|
116
|
+
parses += 1;
|
|
117
|
+
if (parses === 1) {
|
|
118
|
+
// The bytes it needed had not arrived yet, which is not a statement
|
|
119
|
+
// about the file.
|
|
120
|
+
throw new Error("nothing to read there yet");
|
|
121
|
+
}
|
|
122
|
+
return { times: [0, 2], tolerance: 0 };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const container = new Late({ readRange: async () => null, fileSize: 10 });
|
|
126
|
+
|
|
127
|
+
await assert.rejects(() => container.readKeyframeIndex());
|
|
128
|
+
const second = await container.readKeyframeIndex();
|
|
129
|
+
|
|
130
|
+
assert.equal(parses, 2);
|
|
131
|
+
assert.deepEqual(second.times, [0, 2]);
|
|
132
|
+
});
|