@torrent-tv/proxy 2.89.3 → 2.89.5
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/AGENTS.md +338 -336
- package/CHANGELOG.md +2319 -2303
- package/docs/encode-architecture.md +55 -19
- package/docs/playback-start-forecast.md +13 -5
- package/docs/storage-architecture.md +6 -0
- package/package.json +1 -1
- package/routes/stream/get.js +38 -2
- package/server.js +748 -745
- package/services/encode/EncodeOrchestrator.js +24 -49
- package/services/encode/EncodePlan.js +5 -3
- package/services/encode/EncodeRun.js +80 -17
- package/services/encode/EncodeRuns.js +1339 -1239
- package/services/encode/Renditions.js +2 -1
- package/services/encode/RunClock.js +87 -0
- package/services/encode/RunProgress.js +1 -6
- package/services/encode/encoder-readings.js +26 -28
- package/services/encode/piece-completeness.js +46 -0
- package/services/encode/quality/EncodeCost.js +1043 -1167
- package/services/encode/quality/OutputQualityState.js +1 -5
- package/services/encode/quality/QualityController.js +213 -390
- package/services/encode/quality/link-budget.js +5 -17
- package/services/encode/run-inputs.js +13 -2
- package/services/server/SegmentServing.js +3 -1
- package/services/server/ViewerRequests.js +2 -7
- package/services/server/wire-outputs.js +12 -7
- package/services/storage/segment-store/SegmentStore.js +1294 -1255
- package/services/torrent/supply-margin.js +7 -5
- package/test/auto-quality-step.test.js +75 -29
- package/test/decode-cost.test.js +1 -1
- package/test/encode-cost.test.js +31 -39
- package/test/encode-orchestrator.test.js +15 -8
- package/test/encode-plan-viewers.test.js +34 -27
- package/test/encode-run.test.js +34 -6
- package/test/encoder-readings.test.js +48 -26
- package/test/helpers/encode-run.js +113 -9
- package/test/link-budget.test.js +4 -5
- package/test/manager-surface.test.js +1 -2
- package/test/orchestrator-wired.test.js +5 -4
- package/test/output-speed.test.js +0 -1
- package/test/piece-completeness.test.js +42 -0
- package/test/quality-offer.test.js +1 -3
- package/test/quality-variants.test.js +1 -1
- package/test/run-progress.test.js +2 -3
- package/test/same-height-switch.test.js +1 -2
- package/test/segment-closed-on-both-branches.test.js +114 -96
- package/test/segment-store.test.js +15 -15
- package/test/stream-route.test.js +28 -0
- package/test/supply-margin.test.js +13 -0
- package/test/two-viewers-one-picture.test.js +1 -1
- package/test/viewer-outputs.test.js +1 -1
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Whether a closed piece holds the whole of its stretch of film.
|
|
3
|
+
*
|
|
4
|
+
* Decided by the encoding from the cut table and the piece's own media; the
|
|
5
|
+
* segment store only keeps what it is given.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import test from "node:test";
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import { cutOf, judgePiece } from "../services/encode/piece-completeness.js";
|
|
11
|
+
import { SEGMENT_CUT_TIME_DELTA_SECONDS } from "../services/encode/output/index.js";
|
|
12
|
+
|
|
13
|
+
/** Media ending where stated, in the shape a format's reading has. */
|
|
14
|
+
const endingAt = (seconds) => ({ end: seconds });
|
|
15
|
+
const format = { producedThroughSeconds: (ranges) => ranges?.end ?? null };
|
|
16
|
+
|
|
17
|
+
/** Cuts every ten seconds, ten pieces. */
|
|
18
|
+
const timeline = { segmentCount: 10, publishedStartOf: (index) => index * 10 };
|
|
19
|
+
|
|
20
|
+
test("a piece must reach its next cut less the delta the muxer is given", () => {
|
|
21
|
+
assert.equal(cutOf(timeline, 3), 40 - SEGMENT_CUT_TIME_DELTA_SECONDS);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("the final piece has no next cut", () => {
|
|
25
|
+
assert.equal(cutOf(timeline, 9), undefined);
|
|
26
|
+
assert.deepEqual(judgePiece(format, endingAt(95), cutOf(timeline, 9)), { whole: true, throughSeconds: 95 });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("an audio piece ending within the delta of its cut is whole", () => {
|
|
30
|
+
// Field 2026-10-01: AAC frames end 4.7389 s into a piece cut at 4.755 s.
|
|
31
|
+
const cut = 4.755 - SEGMENT_CUT_TIME_DELTA_SECONDS;
|
|
32
|
+
assert.equal(judgePiece(format, endingAt(4.738913832199547), cut).whole, true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("a piece closed short of its cut is not whole", () => {
|
|
36
|
+
// A run killed mid-piece: 84 ms of film where ten seconds were cut.
|
|
37
|
+
assert.deepEqual(judgePiece(format, endingAt(456.084), 466), { whole: false, throughSeconds: 456.084 });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("a piece with no playable media is not whole, whatever its cut", () => {
|
|
41
|
+
assert.deepEqual(judgePiece(format, null, undefined), { whole: false, throughSeconds: null });
|
|
42
|
+
});
|
|
@@ -58,10 +58,8 @@ function offerOver(
|
|
|
58
58
|
runningEncoders: () => 0,
|
|
59
59
|
encodersRunningNow: () => 0,
|
|
60
60
|
torrentCostSecFor: () => 0,
|
|
61
|
-
boundBy: async () => "cpu",
|
|
62
61
|
runsFor: (session) => [...(session.runs ?? [])],
|
|
63
|
-
stateFor: (session) => runStateOf(session.runs)
|
|
64
|
-
progressFor: (session) => session.progress ?? null
|
|
62
|
+
stateFor: (session) => runStateOf(session.runs)
|
|
65
63
|
});
|
|
66
64
|
let computed = 0;
|
|
67
65
|
const real = cost.sustainableHeights.bind(cost);
|
|
@@ -131,7 +131,7 @@ function fakeSession({
|
|
|
131
131
|
useSyntheticPlaylist: true,
|
|
132
132
|
playlistText: "#EXTM3U\n",
|
|
133
133
|
segmentCount: 100,
|
|
134
|
-
progress: { state: "running", processedSeconds: 0, startPositionSeconds: 0
|
|
134
|
+
progress: { state: "running", processedSeconds: 0, startPositionSeconds: 0 }
|
|
135
135
|
};
|
|
136
136
|
}
|
|
137
137
|
|
|
@@ -16,7 +16,7 @@ test("a run rebases ffmpeg's relative clock onto its own source position", () =>
|
|
|
16
16
|
let now = 10;
|
|
17
17
|
const progress = new RunProgress({ startSeconds: 600, totalSeconds: 1000, now: () => now });
|
|
18
18
|
now = 20;
|
|
19
|
-
progress.note({ processedSeconds: 12.5
|
|
19
|
+
progress.note({ processedSeconds: 12.5 });
|
|
20
20
|
|
|
21
21
|
assert.deepEqual(progress.snapshot(), {
|
|
22
22
|
processedSeconds: 612.5,
|
|
@@ -24,7 +24,6 @@ test("a run rebases ffmpeg's relative clock onto its own source position", () =>
|
|
|
24
24
|
totalSeconds: 1000,
|
|
25
25
|
percent: 3.125,
|
|
26
26
|
remainingSeconds: 387.5,
|
|
27
|
-
speed: "2.0x",
|
|
28
27
|
updatedAt: 20
|
|
29
28
|
});
|
|
30
29
|
});
|
|
@@ -33,7 +32,7 @@ test("two runs of one output retain separate clocks", () => {
|
|
|
33
32
|
const made = orchestrator();
|
|
34
33
|
const first = { from: 0, to: 49, isAlive: true, progress: new RunProgress({ startSeconds: 0 }) };
|
|
35
34
|
const second = { from: 50, to: 99, isAlive: true, progress: new RunProgress({ startSeconds: 200 }) };
|
|
36
|
-
first.progress.note({ processedSeconds: 40
|
|
35
|
+
first.progress.note({ processedSeconds: 40 });
|
|
37
36
|
second.progress.note({ processedSeconds: 8, speed: "2.0x" });
|
|
38
37
|
made.adopt("picture", first);
|
|
39
38
|
made.adopt("picture", second);
|
|
@@ -65,7 +65,6 @@ import { SourceFile } from "../services/media/SourceFile.js";
|
|
|
65
65
|
import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
|
|
66
66
|
import { outputSpec } from "./helpers/output-spec.js";
|
|
67
67
|
import { maxrateKbpsFor, nominalKbpsFor, softwareRateControlFor } from "../services/encode/args.js";
|
|
68
|
-
import { LINK_SAFETY } from "../services/encode/quality/link-budget.js";
|
|
69
68
|
|
|
70
69
|
const BASE_ID = "1111111122223333";
|
|
71
70
|
const SEGMENT_SECONDS = 4;
|
|
@@ -86,7 +85,7 @@ const AUDIO_MBPS = 0.128;
|
|
|
86
85
|
* @returns {number}
|
|
87
86
|
*/
|
|
88
87
|
function linkJustFor(limit) {
|
|
89
|
-
return
|
|
88
|
+
return maxrateKbpsFor(limit) / 1000 + AUDIO_MBPS + 0.01;
|
|
90
89
|
}
|
|
91
90
|
|
|
92
91
|
/**
|
|
@@ -1,96 +1,114 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file A piece is closed by one of two writers, and both must say so.
|
|
3
|
-
*
|
|
4
|
-
* The `segment` muxer writes under a working name and reports the closure on a
|
|
5
|
-
* channel of its own, which reaches `SegmentStore.publish`. The `hls` muxer
|
|
6
|
-
* writes through a temporary name of its own (`+temp_file`) and reports
|
|
7
|
-
* nothing: the file simply appears under the name it is served as. That branch
|
|
8
|
-
* is taken by every re-encoded output cut on the even grid — an ordinary
|
|
9
|
-
* quality step — and for it a wait could only end on its deadline, so a segment
|
|
10
|
-
* lying finished on disk was answered 503 after the whole hold.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import test from "node:test";
|
|
14
|
-
import assert from "node:assert/strict";
|
|
15
|
-
import { mkdtempSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
16
|
-
import os from "node:os";
|
|
17
|
-
import path from "node:path";
|
|
18
|
-
import { SegmentStore } from "../services/storage/segment-store/SegmentStore.js";
|
|
19
|
-
import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
|
|
20
|
-
|
|
21
|
-
const KEY = "torrent:abc:fmt=fmp4:grid=uniform@0:video-only:v=libx264/854x480";
|
|
22
|
-
|
|
23
|
-
/** A deadline is the backstop of these waits, never what they measure. */
|
|
24
|
-
const BACKSTOP_MS = 10_000;
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* @returns {{ store: SegmentStore, root: string }}
|
|
28
|
-
*/
|
|
29
|
-
function storeInATempRoot() {
|
|
30
|
-
const root = mkdtempSync(path.join(os.tmpdir(), "segment-closed-"));
|
|
31
|
-
const store = new SegmentStore({ root, logger: { info: () => {}, warn: () => {} } });
|
|
32
|
-
store.useFormat(KEY, fmp4Format);
|
|
33
|
-
return { store, root };
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
test("a piece that simply appears ends the wait that began before it", async (t) => {
|
|
37
|
-
const { store, root } = storeInATempRoot();
|
|
38
|
-
t.after(() => {
|
|
39
|
-
store.dropAll("the test is over");
|
|
40
|
-
rmSync(root, { recursive: true, force: true });
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
const dir = store.directoryFor(KEY);
|
|
44
|
-
// The wait begins on a segment nobody has written yet — the cold case, and
|
|
45
|
-
// the only one the deadline used to decide.
|
|
46
|
-
const waited = store.waitFor(KEY, 7, BACKSTOP_MS);
|
|
47
|
-
|
|
48
|
-
// What the `hls` muxer does: a temporary name of its own, renamed into place
|
|
49
|
-
// when the piece is whole. Nothing is told; the directory simply moves.
|
|
50
|
-
const temp = path.join(dir, "segment-00007.mp4.tmp");
|
|
51
|
-
writeFileSync(temp, Buffer.alloc(64));
|
|
52
|
-
renameSync(temp, path.join(dir, fmp4Format.segmentFileName(7)));
|
|
53
|
-
|
|
54
|
-
assert.equal(await waited, true, "the piece was on disk and the wait did not end");
|
|
55
|
-
assert.ok(store.pathOf(KEY, 7), "and the store must be able to hand it over");
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test("clearing up after a run is not its pieces arriving", async (t) => {
|
|
59
|
-
const { store, root } = storeInATempRoot();
|
|
60
|
-
t.after(() => {
|
|
61
|
-
store.dropAll("the test is over");
|
|
62
|
-
rmSync(root, { recursive: true, force: true });
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
const dir = store.directoryFor(KEY);
|
|
66
|
-
const unfinished = path.join(dir, fmp4Format.makingFileNameTemplate("3").replace("%05d", "00009"));
|
|
67
|
-
writeFileSync(unfinished, Buffer.alloc(64));
|
|
68
|
-
|
|
69
|
-
let ended = false;
|
|
70
|
-
// 200 ms is a grace before asserting that something did NOT happen, which can
|
|
71
|
-
// only ever pass too easily — never a measurement.
|
|
72
|
-
const waited = store.waitFor(KEY, 9, 200).then((value) => {
|
|
73
|
-
ended = value;
|
|
74
|
-
return value;
|
|
75
|
-
});
|
|
76
|
-
rmSync(unfinished, { force: true });
|
|
77
|
-
|
|
78
|
-
assert.equal(await waited, false, "a directory that moved because a file went holds nothing new");
|
|
79
|
-
assert.equal(ended, false);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test("the writer that does report a closure is unaffected", async (t) => {
|
|
83
|
-
const { store, root } = storeInATempRoot();
|
|
84
|
-
t.after(() => {
|
|
85
|
-
store.dropAll("the test is over");
|
|
86
|
-
rmSync(root, { recursive: true, force: true });
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
const dir = store.directoryFor(KEY);
|
|
90
|
-
const making = fmp4Format.makingFileNameTemplate("4").replace("%05d", "00004");
|
|
91
|
-
writeFileSync(path.join(dir, making), Buffer.alloc(64));
|
|
92
|
-
|
|
93
|
-
const waited = store.waitFor(KEY, 4, BACKSTOP_MS);
|
|
94
|
-
assert.equal(store.publish(KEY, making, fmp4Format), fmp4Format.segmentFileName(4));
|
|
95
|
-
assert.equal(await waited, true);
|
|
96
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file A piece is closed by one of two writers, and both must say so.
|
|
3
|
+
*
|
|
4
|
+
* The `segment` muxer writes under a working name and reports the closure on a
|
|
5
|
+
* channel of its own, which reaches `SegmentStore.publish`. The `hls` muxer
|
|
6
|
+
* writes through a temporary name of its own (`+temp_file`) and reports
|
|
7
|
+
* nothing: the file simply appears under the name it is served as. That branch
|
|
8
|
+
* is taken by every re-encoded output cut on the even grid — an ordinary
|
|
9
|
+
* quality step — and for it a wait could only end on its deadline, so a segment
|
|
10
|
+
* lying finished on disk was answered 503 after the whole hold.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import test from "node:test";
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import { mkdtempSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
16
|
+
import os from "node:os";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { SegmentStore } from "../services/storage/segment-store/SegmentStore.js";
|
|
19
|
+
import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
|
|
20
|
+
|
|
21
|
+
const KEY = "torrent:abc:fmt=fmp4:grid=uniform@0:video-only:v=libx264/854x480";
|
|
22
|
+
|
|
23
|
+
/** A deadline is the backstop of these waits, never what they measure. */
|
|
24
|
+
const BACKSTOP_MS = 10_000;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @returns {{ store: SegmentStore, root: string }}
|
|
28
|
+
*/
|
|
29
|
+
function storeInATempRoot() {
|
|
30
|
+
const root = mkdtempSync(path.join(os.tmpdir(), "segment-closed-"));
|
|
31
|
+
const store = new SegmentStore({ root, logger: { info: () => {}, warn: () => {} } });
|
|
32
|
+
store.useFormat(KEY, fmp4Format);
|
|
33
|
+
return { store, root };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
test("a piece that simply appears ends the wait that began before it", async (t) => {
|
|
37
|
+
const { store, root } = storeInATempRoot();
|
|
38
|
+
t.after(() => {
|
|
39
|
+
store.dropAll("the test is over");
|
|
40
|
+
rmSync(root, { recursive: true, force: true });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const dir = store.directoryFor(KEY);
|
|
44
|
+
// The wait begins on a segment nobody has written yet — the cold case, and
|
|
45
|
+
// the only one the deadline used to decide.
|
|
46
|
+
const waited = store.waitFor(KEY, 7, BACKSTOP_MS);
|
|
47
|
+
|
|
48
|
+
// What the `hls` muxer does: a temporary name of its own, renamed into place
|
|
49
|
+
// when the piece is whole. Nothing is told; the directory simply moves.
|
|
50
|
+
const temp = path.join(dir, "segment-00007.mp4.tmp");
|
|
51
|
+
writeFileSync(temp, Buffer.alloc(64));
|
|
52
|
+
renameSync(temp, path.join(dir, fmp4Format.segmentFileName(7)));
|
|
53
|
+
|
|
54
|
+
assert.equal(await waited, true, "the piece was on disk and the wait did not end");
|
|
55
|
+
assert.ok(store.pathOf(KEY, 7), "and the store must be able to hand it over");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("clearing up after a run is not its pieces arriving", async (t) => {
|
|
59
|
+
const { store, root } = storeInATempRoot();
|
|
60
|
+
t.after(() => {
|
|
61
|
+
store.dropAll("the test is over");
|
|
62
|
+
rmSync(root, { recursive: true, force: true });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const dir = store.directoryFor(KEY);
|
|
66
|
+
const unfinished = path.join(dir, fmp4Format.makingFileNameTemplate("3").replace("%05d", "00009"));
|
|
67
|
+
writeFileSync(unfinished, Buffer.alloc(64));
|
|
68
|
+
|
|
69
|
+
let ended = false;
|
|
70
|
+
// 200 ms is a grace before asserting that something did NOT happen, which can
|
|
71
|
+
// only ever pass too easily — never a measurement.
|
|
72
|
+
const waited = store.waitFor(KEY, 9, 200).then((value) => {
|
|
73
|
+
ended = value;
|
|
74
|
+
return value;
|
|
75
|
+
});
|
|
76
|
+
rmSync(unfinished, { force: true });
|
|
77
|
+
|
|
78
|
+
assert.equal(await waited, false, "a directory that moved because a file went holds nothing new");
|
|
79
|
+
assert.equal(ended, false);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("the writer that does report a closure is unaffected", async (t) => {
|
|
83
|
+
const { store, root } = storeInATempRoot();
|
|
84
|
+
t.after(() => {
|
|
85
|
+
store.dropAll("the test is over");
|
|
86
|
+
rmSync(root, { recursive: true, force: true });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const dir = store.directoryFor(KEY);
|
|
90
|
+
const making = fmp4Format.makingFileNameTemplate("4").replace("%05d", "00004");
|
|
91
|
+
writeFileSync(path.join(dir, making), Buffer.alloc(64));
|
|
92
|
+
|
|
93
|
+
const waited = store.waitFor(KEY, 4, BACKSTOP_MS);
|
|
94
|
+
assert.equal(store.publish(KEY, making, fmp4Format), fmp4Format.segmentFileName(4));
|
|
95
|
+
assert.equal(await waited, true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("a directory removed from outside and made again is watched again", async (t) => {
|
|
99
|
+
const { store, root } = storeInATempRoot();
|
|
100
|
+
t.after(() => {
|
|
101
|
+
store.dropAll("the test is over");
|
|
102
|
+
rmSync(root, { recursive: true, force: true });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Removed by something other than the store, so its watch was not stopped
|
|
106
|
+
// first. The watch held on that path watches a directory that is gone.
|
|
107
|
+
rmSync(store.directoryFor(KEY), { recursive: true, force: true });
|
|
108
|
+
const dir = store.directoryFor(KEY);
|
|
109
|
+
const waited = store.waitFor(KEY, 3, BACKSTOP_MS);
|
|
110
|
+
|
|
111
|
+
writeFileSync(path.join(dir, fmp4Format.segmentFileName(3)), Buffer.alloc(64));
|
|
112
|
+
|
|
113
|
+
assert.equal(await waited, true, "the piece arrived in the new directory and the wait did not end");
|
|
114
|
+
});
|
|
@@ -10,7 +10,6 @@ import os from "node:os";
|
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
import { directoryNameFor, SegmentStore } from "../services/storage/segment-store/SegmentStore.js";
|
|
12
12
|
import { fmp4Format } from "../services/encode/segment-formats/fmp4.js";
|
|
13
|
-
import { SEGMENT_CUT_TIME_DELTA_SECONDS } from "../services/encode/output/index.js";
|
|
14
13
|
|
|
15
14
|
/**
|
|
16
15
|
* @returns {{ store: SegmentStore, root: string, lines: string[] }}
|
|
@@ -36,32 +35,33 @@ function writeSegment(dir, index, bytes = 16) {
|
|
|
36
35
|
|
|
37
36
|
const KEY = "torrent:abc:fmt=fmp4:grid=kf@0:video-only:v=0/copy";
|
|
38
37
|
|
|
39
|
-
test("a
|
|
38
|
+
test("a published piece keeps the media intervals the encoding read from it", (t) => {
|
|
39
|
+
// The store does not judge a piece; whether it is whole is the encoding's
|
|
40
|
+
// decision (`encode/piece-completeness.js`), taken before it is published.
|
|
40
41
|
const { store, root } = storeInATempRoot();
|
|
41
42
|
t.after(() => { store.dropAll("the check is over"); rmSync(root, { recursive: true, force: true }); });
|
|
42
|
-
const format = { ...fmp4Format, readMediaRanges: () =>
|
|
43
|
-
producedThroughSeconds: (ranges) => ranges.at(-1).end };
|
|
43
|
+
const format = { ...fmp4Format, readMediaRanges: () => assert.fail("the store does not read a piece it was handed") };
|
|
44
44
|
const dir = store.directoryFor(KEY);
|
|
45
45
|
store.useFormat(KEY, format);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
assert.
|
|
46
|
+
writeFileSync(path.join(dir, "making-0-00000.mp4"), Buffer.from("closed"));
|
|
47
|
+
assert.deepEqual(store.closedBytesOf(KEY, "making-0-00000.mp4"), Buffer.from("closed"));
|
|
48
|
+
const mediaRanges = { tracks: [] };
|
|
49
|
+
assert.equal(store.publish(KEY, "making-0-00000.mp4", format, { mediaRanges }), "segment-00000.mp4");
|
|
50
|
+
assert.equal(store.mediaRangesOf(KEY, 0), mediaRanges);
|
|
50
51
|
});
|
|
51
52
|
|
|
52
|
-
test("a
|
|
53
|
-
const { store, root } = storeInATempRoot();
|
|
53
|
+
test("a stored piece is taken off the disk at its owner's word", (t) => {
|
|
54
|
+
const { store, root, lines } = storeInATempRoot();
|
|
54
55
|
t.after(() => { store.dropAll("the check is over"); rmSync(root, { recursive: true, force: true }); });
|
|
55
|
-
const format = { ...fmp4Format, readMediaRanges: () =>
|
|
56
|
-
producedThroughSeconds: (ranges) => ranges.at(-1).end };
|
|
56
|
+
const format = { ...fmp4Format, readMediaRanges: () => ({ tracks: [] }) };
|
|
57
57
|
const dir = store.directoryFor(KEY);
|
|
58
58
|
store.useFormat(KEY, format);
|
|
59
|
-
writeFileSync(path.join(dir, "making-0-00071.mp4"), Buffer.alloc(16));
|
|
60
|
-
assert.equal(store.publish(KEY, "making-0-00071.mp4", format, { endSeconds: 466 }), null);
|
|
61
59
|
writeSegment(dir, 71);
|
|
62
60
|
assert.ok(store.mediaRangesOf(KEY, 71));
|
|
63
|
-
|
|
61
|
+
store.remove(KEY, 71, "short of its cut");
|
|
64
62
|
assert.equal(store.pathOf(KEY, 71), null);
|
|
63
|
+
assert.equal(store.mediaRangesOf(KEY, 71), undefined);
|
|
64
|
+
assert.ok(lines.some((line) => line.includes("segment 71") && line.includes("short of its cut")));
|
|
65
65
|
});
|
|
66
66
|
|
|
67
67
|
test("two viewers of one output are given the same directory", (t) => {
|
|
@@ -207,3 +207,31 @@ test("a file this proxy does not have whole still goes to the torrent", async ()
|
|
|
207
207
|
await handleStreamGet(req, reply, deps);
|
|
208
208
|
assert.equal(state.prioritized.length, 1, "the ordinary path was not taken");
|
|
209
209
|
});
|
|
210
|
+
|
|
211
|
+
test("every wait of an encoder's input read is marked for the run its URL names", async () => {
|
|
212
|
+
// The run's processing speed is read over its own working time; the time its
|
|
213
|
+
// input waits for the swarm is what this marks (RunClock).
|
|
214
|
+
const { req, reply, deps } = harness({ method: "GET", range: "bytes=0-5" });
|
|
215
|
+
const { files: [file] } = await deps.torrentPool.getTorrent();
|
|
216
|
+
const pieces = [Buffer.from("abc"), Buffer.from("def")];
|
|
217
|
+
file.createFragmentReader = () => ({
|
|
218
|
+
cancel() {},
|
|
219
|
+
async *[Symbol.asyncIterator]() {
|
|
220
|
+
for (const bytes of pieces) {
|
|
221
|
+
yield { bytes, release() {} };
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
reply.raw.write = (_bytes, done) => done();
|
|
226
|
+
reply.raw.writableEnded = false;
|
|
227
|
+
reply.raw.destroyed = false;
|
|
228
|
+
req.query.session = "output-1";
|
|
229
|
+
req.query.run = "7";
|
|
230
|
+
const marks = [];
|
|
231
|
+
deps.noteInputWaiting = (runToken, waiting) => marks.push([runToken, waiting]);
|
|
232
|
+
|
|
233
|
+
await handleStreamGet(req, reply, deps);
|
|
234
|
+
|
|
235
|
+
assert.deepEqual(marks, [[7, true], [7, false], [7, true], [7, false], [7, true], [7, false]],
|
|
236
|
+
"a wait before each fragment and one before the end, each closed");
|
|
237
|
+
});
|
|
@@ -175,3 +175,16 @@ test("a term with nothing observed contributes nothing", () => {
|
|
|
175
175
|
assert.equal(minimumBufferFrom({}), null, "without a segment duration nothing can be said");
|
|
176
176
|
assert.equal(minimumBufferFrom({ segmentSeconds: 0 }), null);
|
|
177
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
|
+
});
|
|
@@ -132,7 +132,7 @@ function fakeSession({
|
|
|
132
132
|
useSyntheticPlaylist: true,
|
|
133
133
|
playlistText: "#EXTM3U\n",
|
|
134
134
|
segmentCount: 100,
|
|
135
|
-
progress: { state: "running", processedSeconds: 0, startPositionSeconds: 0
|
|
135
|
+
progress: { state: "running", processedSeconds: 0, startPositionSeconds: 0 }
|
|
136
136
|
};
|
|
137
137
|
}
|
|
138
138
|
|
|
@@ -98,7 +98,7 @@ function fakeSession({ id, dirPath, file, encodeHeight = 0, audioOnly = false, i
|
|
|
98
98
|
useSyntheticPlaylist: true,
|
|
99
99
|
playlistText: "#EXTM3U\n",
|
|
100
100
|
segmentCount: 100,
|
|
101
|
-
progress: { state: "running", processedSeconds: 0, startPositionSeconds: 0
|
|
101
|
+
progress: { state: "running", processedSeconds: 0, startPositionSeconds: 0 }
|
|
102
102
|
};
|
|
103
103
|
}
|
|
104
104
|
|