@torrent-tv/proxy 2.89.5 → 2.89.6
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 +11 -0
- package/package.json +1 -1
- package/test/auto-quality-step.test.js +762 -753
- package/test/decode-cost.test.js +475 -462
- package/test/format-keyed-outputs.test.js +78 -78
- package/test/helpers/manager.js +62 -57
- package/test/quality-variants.test.js +1270 -1257
- package/test/segment-judged-by-its-own-header.test.js +222 -221
- package/test/segment-serve-wiring.test.js +9 -3
- package/test/serving-existing-output.test.js +111 -105
- package/test/two-viewers-one-picture.test.js +508 -504
- package/test/viewer-requests-admission.test.js +92 -89
- package/test/viewing-generations.test.js +390 -390
|
@@ -1,78 +1,78 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file An output is named by the format it produces, and nothing may put
|
|
3
|
-
* pieces of another format under that name.
|
|
4
|
-
*
|
|
5
|
-
* Two ways that could still happen once the name became the format: a
|
|
6
|
-
* directory left by an earlier version, whose key named the box a viewer asked
|
|
7
|
-
* for and so cannot say what format is inside; and a hardware encoder failing,
|
|
8
|
-
* after which the proxy encodes with software while outputs named by the failed
|
|
9
|
-
* encoder are still being watched.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import test from "node:test";
|
|
13
|
-
import assert from "node:assert/strict";
|
|
14
|
-
import { existsSync, writeFileSync } from "node:fs";
|
|
15
|
-
import path from "node:path";
|
|
16
|
-
import { managerWithOwnStore } from "./helpers/manager.js";
|
|
17
|
-
import { outputSpec } from "./helpers/output-spec.js";
|
|
18
|
-
import { ENCODE_EXIT } from "../services/encode/encode-exit.js";
|
|
19
|
-
import { SourceFile } from "../services/media/SourceFile.js";
|
|
20
|
-
|
|
21
|
-
test("a directory whose key names an asked-for box is not adopted, and one naming a format is", (t) => {
|
|
22
|
-
const { store, root, manager, cleanup } = managerWithOwnStore();
|
|
23
|
-
t.after(cleanup);
|
|
24
|
-
const oldKey = "torrent:abc:fmt=fmp4:grid=even@0:video-only:v=0/enc:1280x720:budget";
|
|
25
|
-
const newKey = outputSpec({ sourceKey: "torrent:abc", transcodeVideo: true, width: 1280, height: 720, audioSeparate: true }).toKey();
|
|
26
|
-
const oldDir = store.directoryFor(oldKey);
|
|
27
|
-
const newDir = store.directoryFor(newKey);
|
|
28
|
-
writeFileSync(path.join(oldDir, "segment-00000.mp4"), Buffer.alloc(10));
|
|
29
|
-
writeFileSync(path.join(newDir, "segment-00000.mp4"), Buffer.alloc(10));
|
|
30
|
-
|
|
31
|
-
const result = manager.lifecycle.adoptSegmentsLeftBehind();
|
|
32
|
-
|
|
33
|
-
assert.equal(result.adopted, 1);
|
|
34
|
-
assert.equal(result.dropped, 1);
|
|
35
|
-
assert.equal(existsSync(oldDir), false, "the old directory goes");
|
|
36
|
-
assert.equal(existsSync(newDir), true, "the named format stays");
|
|
37
|
-
assert.ok(root);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test("when a hardware encoder fails, the outputs it named are closed rather than continued by software", async (t) => {
|
|
41
|
-
const { manager, cleanup } = managerWithOwnStore({
|
|
42
|
-
videoEncoder: { kind: "vaapi", name: "h264_vaapi", inputArgs: [] }
|
|
43
|
-
});
|
|
44
|
-
t.after(async () => {
|
|
45
|
-
await manager.lifecycle.disposeAll();
|
|
46
|
-
cleanup();
|
|
47
|
-
});
|
|
48
|
-
manager.encodeRuns.planEncodersNow = () => {};
|
|
49
|
-
manager.encodeRuns.planEncodersSoon = () => {};
|
|
50
|
-
const file = new SourceFile({ sourceKey: "torrent:abc", fileIndex: 0, name: "film.mkv" });
|
|
51
|
-
const record = (id, encoder) => {
|
|
52
|
-
const spec = outputSpec({ sourceKey: "torrent:abc", transcodeVideo: true, width: 1280, height: 720, encoder });
|
|
53
|
-
const output = { id, spec, get outputKey() { return this.spec.toKey(); }, file };
|
|
54
|
-
manager.outputs.set(id, output);
|
|
55
|
-
return output;
|
|
56
|
-
};
|
|
57
|
-
const failing = record("aaaaaaaa00000001", "h264_vaapi");
|
|
58
|
-
const software = record("aaaaaaaa00000002", "libx264");
|
|
59
|
-
const run = { from: 0, to: -1, argsDescribed: "ffmpeg …" };
|
|
60
|
-
manager.encodeOrchestrator.adopt(failing.outputKey, run);
|
|
61
|
-
|
|
62
|
-
manager.encodeRuns.noteRunEnded(failing, run, {
|
|
63
|
-
address: failing.outputKey,
|
|
64
|
-
run,
|
|
65
|
-
ending: ENCODE_EXIT.FAILED,
|
|
66
|
-
from: 0,
|
|
67
|
-
to: -1,
|
|
68
|
-
livedMs: 30_000,
|
|
69
|
-
because: "vaapi failed",
|
|
70
|
-
lastError: "vaapi failed",
|
|
71
|
-
producedCount: 3
|
|
72
|
-
});
|
|
73
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
74
|
-
|
|
75
|
-
assert.equal(manager.
|
|
76
|
-
assert.equal(manager.outputs.has(failing.id), false, "an output of the failed encoder is closed");
|
|
77
|
-
assert.equal(manager.outputs.has(software.id), true, "an output of another encoder is untouched");
|
|
78
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file An output is named by the format it produces, and nothing may put
|
|
3
|
+
* pieces of another format under that name.
|
|
4
|
+
*
|
|
5
|
+
* Two ways that could still happen once the name became the format: a
|
|
6
|
+
* directory left by an earlier version, whose key named the box a viewer asked
|
|
7
|
+
* for and so cannot say what format is inside; and a hardware encoder failing,
|
|
8
|
+
* after which the proxy encodes with software while outputs named by the failed
|
|
9
|
+
* encoder are still being watched.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import test from "node:test";
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
import { existsSync, writeFileSync } from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { managerWithOwnStore } from "./helpers/manager.js";
|
|
17
|
+
import { outputSpec } from "./helpers/output-spec.js";
|
|
18
|
+
import { ENCODE_EXIT } from "../services/encode/encode-exit.js";
|
|
19
|
+
import { SourceFile } from "../services/media/SourceFile.js";
|
|
20
|
+
|
|
21
|
+
test("a directory whose key names an asked-for box is not adopted, and one naming a format is", (t) => {
|
|
22
|
+
const { store, root, manager, cleanup } = managerWithOwnStore();
|
|
23
|
+
t.after(cleanup);
|
|
24
|
+
const oldKey = "torrent:abc:fmt=fmp4:grid=even@0:video-only:v=0/enc:1280x720:budget";
|
|
25
|
+
const newKey = outputSpec({ sourceKey: "torrent:abc", transcodeVideo: true, width: 1280, height: 720, audioSeparate: true }).toKey();
|
|
26
|
+
const oldDir = store.directoryFor(oldKey);
|
|
27
|
+
const newDir = store.directoryFor(newKey);
|
|
28
|
+
writeFileSync(path.join(oldDir, "segment-00000.mp4"), Buffer.alloc(10));
|
|
29
|
+
writeFileSync(path.join(newDir, "segment-00000.mp4"), Buffer.alloc(10));
|
|
30
|
+
|
|
31
|
+
const result = manager.lifecycle.adoptSegmentsLeftBehind();
|
|
32
|
+
|
|
33
|
+
assert.equal(result.adopted, 1);
|
|
34
|
+
assert.equal(result.dropped, 1);
|
|
35
|
+
assert.equal(existsSync(oldDir), false, "the old directory goes");
|
|
36
|
+
assert.equal(existsSync(newDir), true, "the named format stays");
|
|
37
|
+
assert.ok(root);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("when a hardware encoder fails, the outputs it named are closed rather than continued by software", async (t) => {
|
|
41
|
+
const { manager, cleanup } = managerWithOwnStore({
|
|
42
|
+
videoEncoder: { kind: "vaapi", name: "h264_vaapi", inputArgs: [] }
|
|
43
|
+
});
|
|
44
|
+
t.after(async () => {
|
|
45
|
+
await manager.lifecycle.disposeAll();
|
|
46
|
+
cleanup();
|
|
47
|
+
});
|
|
48
|
+
manager.encodeRuns.planEncodersNow = () => {};
|
|
49
|
+
manager.encodeRuns.planEncodersSoon = () => {};
|
|
50
|
+
const file = new SourceFile({ sourceKey: "torrent:abc", fileIndex: 0, name: "film.mkv" });
|
|
51
|
+
const record = (id, encoder) => {
|
|
52
|
+
const spec = outputSpec({ sourceKey: "torrent:abc", transcodeVideo: true, width: 1280, height: 720, encoder });
|
|
53
|
+
const output = { id, spec, get outputKey() { return this.spec.toKey(); }, file };
|
|
54
|
+
manager.outputs.set(id, output);
|
|
55
|
+
return output;
|
|
56
|
+
};
|
|
57
|
+
const failing = record("aaaaaaaa00000001", "h264_vaapi");
|
|
58
|
+
const software = record("aaaaaaaa00000002", "libx264");
|
|
59
|
+
const run = { from: 0, to: -1, argsDescribed: "ffmpeg …" };
|
|
60
|
+
manager.encodeOrchestrator.adopt(failing.outputKey, run);
|
|
61
|
+
|
|
62
|
+
manager.encodeRuns.noteRunEnded(failing, run, {
|
|
63
|
+
address: failing.outputKey,
|
|
64
|
+
run,
|
|
65
|
+
ending: ENCODE_EXIT.FAILED,
|
|
66
|
+
from: 0,
|
|
67
|
+
to: -1,
|
|
68
|
+
livedMs: 30_000,
|
|
69
|
+
because: "vaapi failed",
|
|
70
|
+
lastError: "vaapi failed",
|
|
71
|
+
producedCount: 3
|
|
72
|
+
});
|
|
73
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
74
|
+
|
|
75
|
+
assert.equal(manager.encoders.current.kind, "software");
|
|
76
|
+
assert.equal(manager.outputs.has(failing.id), false, "an output of the failed encoder is closed");
|
|
77
|
+
assert.equal(manager.outputs.has(software.id), true, "an output of another encoder is untouched");
|
|
78
|
+
});
|
package/test/helpers/manager.js
CHANGED
|
@@ -1,57 +1,62 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file A session manager for a test, with a store of its own.
|
|
3
|
-
*
|
|
4
|
-
* ONE MANAGER, ONE ROOT. In production that is true by construction: one
|
|
5
|
-
* process holds one manager, and the store's root is a fixed path so that a
|
|
6
|
-
* restart can find what the process before it finished (`SegmentStore.sweep`).
|
|
7
|
-
* Under `node --test` it is false — the runner starts one PROCESS PER FILE and
|
|
8
|
-
* runs them in parallel, so every manager built without a store of its own
|
|
9
|
-
* shared the one default root with every other file running at that moment.
|
|
10
|
-
*
|
|
11
|
-
* What that cost, measured 2026-09-14: `disposeAll` removes the store's whole
|
|
12
|
-
* root, and four files call it between them 40 times. Running the twenty-one
|
|
13
|
-
* files that build a manager gave 2, 3 and 4 failures on consecutive runs and
|
|
14
|
-
* zero with `--test-concurrency=1`; the victim was always
|
|
15
|
-
* `segment-serve-wiring.test.js`, reporting a segment missing that it had
|
|
16
|
-
* written itself a moment earlier, because another process had deleted the
|
|
17
|
-
* directory under it. `npm test` is `node --test`, so this was the project's
|
|
18
|
-
* own test command, and it is why a red result had become something to read
|
|
19
|
-
* past rather than to act on.
|
|
20
|
-
*
|
|
21
|
-
* A test that fails by luck is worse than one that does not exist: it costs a
|
|
22
|
-
* run every time, and it trains everyone to ignore red.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
26
|
-
import os from "node:os";
|
|
27
|
-
import path from "node:path";
|
|
28
|
-
import { wireOutputs } from "../../services/server/wire-outputs.js";
|
|
29
|
-
import { SegmentStore } from "../../services/storage/segment-store/SegmentStore.js";
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* A manager whose produced segments live where nothing else can reach them.
|
|
33
|
-
*
|
|
34
|
-
* @param {object} [options] - Passed to the manager, over the defaults below.
|
|
35
|
-
* A `segmentStore` given here wins, for a test that wants to watch the store
|
|
36
|
-
* itself.
|
|
37
|
-
* @returns {{ manager: object, store: SegmentStore, root: string,
|
|
38
|
-
* cleanup: () => void }} `cleanup` removes the root; call it from `t.after`.
|
|
39
|
-
*/
|
|
40
|
-
export function managerWithOwnStore(options = {}) {
|
|
41
|
-
const root = mkdtempSync(path.join(os.tmpdir(), "ttv-store-"));
|
|
42
|
-
const store = options.segmentStore ?? new SegmentStore({ root });
|
|
43
|
-
const manager = wireOutputs({
|
|
44
|
-
enabled: true,
|
|
45
|
-
ffmpegBin: "ffmpeg",
|
|
46
|
-
localBindHost: "127.0.0.1",
|
|
47
|
-
localPort: 9090,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @file A session manager for a test, with a store of its own.
|
|
3
|
+
*
|
|
4
|
+
* ONE MANAGER, ONE ROOT. In production that is true by construction: one
|
|
5
|
+
* process holds one manager, and the store's root is a fixed path so that a
|
|
6
|
+
* restart can find what the process before it finished (`SegmentStore.sweep`).
|
|
7
|
+
* Under `node --test` it is false — the runner starts one PROCESS PER FILE and
|
|
8
|
+
* runs them in parallel, so every manager built without a store of its own
|
|
9
|
+
* shared the one default root with every other file running at that moment.
|
|
10
|
+
*
|
|
11
|
+
* What that cost, measured 2026-09-14: `disposeAll` removes the store's whole
|
|
12
|
+
* root, and four files call it between them 40 times. Running the twenty-one
|
|
13
|
+
* files that build a manager gave 2, 3 and 4 failures on consecutive runs and
|
|
14
|
+
* zero with `--test-concurrency=1`; the victim was always
|
|
15
|
+
* `segment-serve-wiring.test.js`, reporting a segment missing that it had
|
|
16
|
+
* written itself a moment earlier, because another process had deleted the
|
|
17
|
+
* directory under it. `npm test` is `node --test`, so this was the project's
|
|
18
|
+
* own test command, and it is why a red result had become something to read
|
|
19
|
+
* past rather than to act on.
|
|
20
|
+
*
|
|
21
|
+
* A test that fails by luck is worse than one that does not exist: it costs a
|
|
22
|
+
* run every time, and it trains everyone to ignore red.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
26
|
+
import os from "node:os";
|
|
27
|
+
import path from "node:path";
|
|
28
|
+
import { wireOutputs } from "../../services/server/wire-outputs.js";
|
|
29
|
+
import { SegmentStore } from "../../services/storage/segment-store/SegmentStore.js";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A manager whose produced segments live where nothing else can reach them.
|
|
33
|
+
*
|
|
34
|
+
* @param {object} [options] - Passed to the manager, over the defaults below.
|
|
35
|
+
* A `segmentStore` given here wins, for a test that wants to watch the store
|
|
36
|
+
* itself.
|
|
37
|
+
* @returns {{ manager: object, store: SegmentStore, root: string,
|
|
38
|
+
* cleanup: () => void }} `cleanup` removes the root; call it from `t.after`.
|
|
39
|
+
*/
|
|
40
|
+
export function managerWithOwnStore(options = {}) {
|
|
41
|
+
const root = mkdtempSync(path.join(os.tmpdir(), "ttv-store-"));
|
|
42
|
+
const store = options.segmentStore ?? new SegmentStore({ root });
|
|
43
|
+
const manager = wireOutputs({
|
|
44
|
+
enabled: true,
|
|
45
|
+
ffmpegBin: "ffmpeg",
|
|
46
|
+
localBindHost: "127.0.0.1",
|
|
47
|
+
localPort: 9090,
|
|
48
|
+
// What this host has measured about itself is kept beside the store, not
|
|
49
|
+
// beside the installed proxy: read from there, one run's figures became
|
|
50
|
+
// the next run's premises, and a check passed only on a machine that had
|
|
51
|
+
// run the suite before.
|
|
52
|
+
stateDir: root,
|
|
53
|
+
...options,
|
|
54
|
+
segmentStore: store
|
|
55
|
+
});
|
|
56
|
+
return {
|
|
57
|
+
manager,
|
|
58
|
+
store,
|
|
59
|
+
root,
|
|
60
|
+
cleanup: () => rmSync(root, { recursive: true, force: true })
|
|
61
|
+
};
|
|
62
|
+
}
|