@torrent-tv/proxy 2.80.8 → 2.80.10
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 +1675 -1658
- package/bin/cli.js +606 -596
- package/package.json +51 -50
- package/services/data-channel-handler.js +2 -4
- package/services/encode/CoverageMap.js +428 -401
- package/services/encode/EncodePlan.js +0 -3
- package/services/encode/EncodeRun.js +14 -0
- package/services/encode/SegmentStore.js +718 -669
- package/services/hls-session-manager.js +44 -162
- package/services/hwaccel.js +2 -2
- package/services/memory-report.js +596 -592
- package/services/orchestrators/EncodeOrchestrator.js +143 -11
- package/services/quality/EncodeCost.js +555 -555
- package/test/auto-quality-step.test.js +514 -507
- package/test/contention.test.js +1 -1
- package/test/coverage-follows-the-disk.test.js +187 -0
- package/test/coverage-map.test.js +195 -178
- package/test/encode-orchestrator.test.js +1 -2
- package/test/encode-plan-viewers.test.js +18 -19
- package/test/encode-plan.test.js +3 -4
- package/test/held-request-width.test.js +155 -154
- package/test/helpers/encode-run.js +136 -128
- package/test/orchestrator-wired.test.js +16 -11
- package/test/produced-copy-choice.test.js +358 -327
- package/test/run-intervals.test.js +100 -100
- package/test/segment-serve-wiring.test.js +76 -20
- package/test/segment-store.test.js +216 -187
- package/test/stale-request-after-seek.test.js +51 -77
- package/test/tracks-begin-together.test.js +176 -153
- package/test/viewer-outputs.test.js +278 -273
- package/test/behind-head-repair.test.js +0 -261
- /package/services/{contention.js → encode/contention.js} +0 -0
- /package/{test/decode-cost.test.js → test-measured/decode-cost.measured.js} +0 -0
- /package/{test/decode-measurement.test.js → test-measured/decode-measurement.measured.js} +0 -0
|
@@ -1,187 +1,216 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file One store of segments, addressed by what they are, and what it makes of
|
|
3
|
-
* what a killed process left behind.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import test from "node:test";
|
|
7
|
-
import assert from "node:assert/strict";
|
|
8
|
-
import { mkdtempSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
9
|
-
import os from "node:os";
|
|
10
|
-
import path from "node:path";
|
|
11
|
-
import { directoryNameFor, SegmentStore } from "../services/encode/SegmentStore.js";
|
|
12
|
-
import { fmp4Format } from "../services/segment-formats/fmp4.js";
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* @returns {{ store: SegmentStore, root: string, lines: string[] }}
|
|
16
|
-
*/
|
|
17
|
-
function storeInATempRoot() {
|
|
18
|
-
const root = mkdtempSync(path.join(os.tmpdir(), "segment-store-"));
|
|
19
|
-
const lines = [];
|
|
20
|
-
const store = new SegmentStore({
|
|
21
|
-
root,
|
|
22
|
-
logger: { info: (line) => lines.push(line), warn: (line) => lines.push(line) }
|
|
23
|
-
});
|
|
24
|
-
return { store, root, lines };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* @param {string} dir
|
|
29
|
-
* @param {number} index
|
|
30
|
-
* @param {number} bytes
|
|
31
|
-
*/
|
|
32
|
-
function writeSegment(dir, index, bytes = 16) {
|
|
33
|
-
writeFileSync(path.join(dir, fmp4Format.segmentFileName(index)), Buffer.alloc(bytes));
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const KEY = "torrent:abc:fmt=fmp4:grid=kf@0:video-only:v=0/copy";
|
|
37
|
-
|
|
38
|
-
test("two viewers of one output are given the same directory", (t) => {
|
|
39
|
-
const { store, root } = storeInATempRoot();
|
|
40
|
-
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
41
|
-
|
|
42
|
-
// Which viewer asked never enters it — the address is what the segments are.
|
|
43
|
-
assert.equal(store.directoryFor(KEY), store.directoryFor(KEY));
|
|
44
|
-
assert.notEqual(store.directoryFor(KEY), store.directoryFor(`${KEY}:other`));
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test("the directory says what it holds, so a later process can tell", (t) => {
|
|
48
|
-
const { store, root } = storeInATempRoot();
|
|
49
|
-
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
50
|
-
|
|
51
|
-
const dir = store.directoryFor(KEY);
|
|
52
|
-
const names = readdirSync(dir);
|
|
53
|
-
assert.deepEqual(names, ["key.txt"]);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test("a segment is proven closed by the existence of the next one", (t) => {
|
|
57
|
-
const { store, root } = storeInATempRoot();
|
|
58
|
-
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
59
|
-
|
|
60
|
-
const dir = store.directoryFor(KEY);
|
|
61
|
-
store.useFormat(KEY, fmp4Format);
|
|
62
|
-
writeSegment(dir, 0);
|
|
63
|
-
writeSegment(dir, 1);
|
|
64
|
-
writeSegment(dir, 2);
|
|
65
|
-
|
|
66
|
-
// The `
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
assert.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
store.
|
|
78
|
-
|
|
79
|
-
writeSegment(dir,
|
|
80
|
-
writeSegment(dir,
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
assert.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
store.useFormat(KEY, fmp4Format);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
assert.equal(
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
assert.equal(
|
|
185
|
-
|
|
186
|
-
assert.equal(
|
|
187
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file One store of segments, addressed by what they are, and what it makes of
|
|
3
|
+
* what a killed process left behind.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { mkdtempSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
9
|
+
import os from "node:os";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { directoryNameFor, SegmentStore } from "../services/encode/SegmentStore.js";
|
|
12
|
+
import { fmp4Format } from "../services/segment-formats/fmp4.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @returns {{ store: SegmentStore, root: string, lines: string[] }}
|
|
16
|
+
*/
|
|
17
|
+
function storeInATempRoot() {
|
|
18
|
+
const root = mkdtempSync(path.join(os.tmpdir(), "segment-store-"));
|
|
19
|
+
const lines = [];
|
|
20
|
+
const store = new SegmentStore({
|
|
21
|
+
root,
|
|
22
|
+
logger: { info: (line) => lines.push(line), warn: (line) => lines.push(line) }
|
|
23
|
+
});
|
|
24
|
+
return { store, root, lines };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {string} dir
|
|
29
|
+
* @param {number} index
|
|
30
|
+
* @param {number} bytes
|
|
31
|
+
*/
|
|
32
|
+
function writeSegment(dir, index, bytes = 16) {
|
|
33
|
+
writeFileSync(path.join(dir, fmp4Format.segmentFileName(index)), Buffer.alloc(bytes));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const KEY = "torrent:abc:fmt=fmp4:grid=kf@0:video-only:v=0/copy";
|
|
37
|
+
|
|
38
|
+
test("two viewers of one output are given the same directory", (t) => {
|
|
39
|
+
const { store, root } = storeInATempRoot();
|
|
40
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
41
|
+
|
|
42
|
+
// Which viewer asked never enters it — the address is what the segments are.
|
|
43
|
+
assert.equal(store.directoryFor(KEY), store.directoryFor(KEY));
|
|
44
|
+
assert.notEqual(store.directoryFor(KEY), store.directoryFor(`${KEY}:other`));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("the directory says what it holds, so a later process can tell", (t) => {
|
|
48
|
+
const { store, root } = storeInATempRoot();
|
|
49
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
50
|
+
|
|
51
|
+
const dir = store.directoryFor(KEY);
|
|
52
|
+
const names = readdirSync(dir);
|
|
53
|
+
assert.deepEqual(names, ["key.txt"]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("a segment is proven closed by the existence of the next one", (t) => {
|
|
57
|
+
const { store, root } = storeInATempRoot();
|
|
58
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
59
|
+
|
|
60
|
+
const dir = store.directoryFor(KEY);
|
|
61
|
+
store.useFormat(KEY, fmp4Format);
|
|
62
|
+
writeSegment(dir, 0);
|
|
63
|
+
writeSegment(dir, 1);
|
|
64
|
+
writeSegment(dir, 2);
|
|
65
|
+
|
|
66
|
+
// The `hls` muxer carries no channel to report on and renames into place when
|
|
67
|
+
// it closes a piece, so on that branch existence IS the proof; the same
|
|
68
|
+
// answers for the pieces a previous life of this process left behind.
|
|
69
|
+
assert.deepEqual(store.provenNumbers(KEY), [0, 1]);
|
|
70
|
+
assert.equal(store.unprovenNumber(KEY), 2);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("a file of no bytes is not a segment, whatever it is called", (t) => {
|
|
74
|
+
const { store, root } = storeInATempRoot();
|
|
75
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
76
|
+
|
|
77
|
+
const dir = store.directoryFor(KEY);
|
|
78
|
+
store.useFormat(KEY, fmp4Format);
|
|
79
|
+
writeSegment(dir, 0);
|
|
80
|
+
writeSegment(dir, 1);
|
|
81
|
+
writeSegment(dir, 2, 0);
|
|
82
|
+
|
|
83
|
+
// What a run killed the moment after opening its next piece leaves behind.
|
|
84
|
+
// Counted as a segment once, it closed the only hole in the numbering and
|
|
85
|
+
// convinced the look-ahead the encoder had produced it.
|
|
86
|
+
assert.equal(store.pathOf(KEY, 2), null);
|
|
87
|
+
assert.deepEqual(store.provenNumbers(KEY), [0]);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("a run's own stretch bounds what it unproves", (t) => {
|
|
91
|
+
const { store, root } = storeInATempRoot();
|
|
92
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
93
|
+
|
|
94
|
+
store.useFormat(KEY, fmp4Format);
|
|
95
|
+
store.directoryFor(KEY);
|
|
96
|
+
for (let index = 0; index <= 5; index += 1) {
|
|
97
|
+
store.markClosed(KEY, index);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// A run of #0..#0 rewrites #0 and nothing else. Unbounded, this forgot the
|
|
101
|
+
// whole film beyond it — and with readiness a projection of what is proven,
|
|
102
|
+
// that is an output declaring itself unmade whenever an encoder starts near
|
|
103
|
+
// the beginning, which is a fresh encoder for every segment of it.
|
|
104
|
+
//
|
|
105
|
+
// Asked with no files on the disk, so the successor rule cannot answer for
|
|
106
|
+
// the statements and only the statements are under test.
|
|
107
|
+
store.forgetClosed(KEY, 0, 0);
|
|
108
|
+
assert.equal(store.isClosed(KEY, 0), false, "the one number it will rewrite");
|
|
109
|
+
assert.equal(store.isClosed(KEY, 1), true, "and nothing beyond its stretch");
|
|
110
|
+
assert.equal(store.isClosed(KEY, 5), true);
|
|
111
|
+
|
|
112
|
+
store.forgetClosed(KEY, 3);
|
|
113
|
+
assert.equal(store.isClosed(KEY, 2), true);
|
|
114
|
+
assert.equal(store.isClosed(KEY, 3), false, "no end given means to the end of the film");
|
|
115
|
+
assert.equal(store.isClosed(KEY, 5), false);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("a directory that has not moved is not read again", (t) => {
|
|
119
|
+
const { store, root } = storeInATempRoot();
|
|
120
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
121
|
+
|
|
122
|
+
const dir = store.directoryFor(KEY);
|
|
123
|
+
store.useFormat(KEY, fmp4Format);
|
|
124
|
+
writeSegment(dir, 0);
|
|
125
|
+
writeSegment(dir, 1);
|
|
126
|
+
|
|
127
|
+
const first = store.refresh(KEY);
|
|
128
|
+
const second = store.refresh(KEY);
|
|
129
|
+
assert.equal(first, second, "the same reading is handed back, not a fresh listing");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("what a killed process left is found, named and counted", (t) => {
|
|
133
|
+
const { store, root, lines } = storeInATempRoot();
|
|
134
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
135
|
+
|
|
136
|
+
const dir = path.join(root, directoryNameFor(KEY));
|
|
137
|
+
mkdirSync(dir, { recursive: true });
|
|
138
|
+
writeFileSync(path.join(dir, "key.txt"), `${KEY}\n`);
|
|
139
|
+
writeSegment(dir, 0);
|
|
140
|
+
writeSegment(dir, 1);
|
|
141
|
+
const orphan = path.join(root, "0123456789abcdef");
|
|
142
|
+
mkdirSync(orphan, { recursive: true });
|
|
143
|
+
writeSegment(orphan, 0);
|
|
144
|
+
|
|
145
|
+
const swept = store.sweep();
|
|
146
|
+
assert.equal(swept.directories, 2);
|
|
147
|
+
assert.equal(swept.unidentified, 1, "the one with no key file cannot be matched to a request");
|
|
148
|
+
assert.equal(swept.segments, 3);
|
|
149
|
+
assert.match(lines.join("\n"), /startup sweep/);
|
|
150
|
+
assert.match(lines.join("\n"), /ended\s+without anything recording why/);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("what survived a kill is taken back, minus the piece nothing proves", (t) => {
|
|
154
|
+
const { store, root } = storeInATempRoot();
|
|
155
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
156
|
+
|
|
157
|
+
const dir = path.join(root, directoryNameFor(KEY));
|
|
158
|
+
mkdirSync(dir, { recursive: true });
|
|
159
|
+
writeFileSync(path.join(dir, "key.txt"), `${KEY}\n`);
|
|
160
|
+
for (let index = 0; index <= 5; index += 1) {
|
|
161
|
+
writeSegment(dir, index);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const taken = store.adoptWhatSurvived(() => fmp4Format);
|
|
165
|
+
|
|
166
|
+
assert.equal(taken.adopted, 1);
|
|
167
|
+
assert.equal(taken.unprovenRemoved, 1, "the highest number was being written when the run died");
|
|
168
|
+
// Five kept rather than six thrown away: a copied segment's bytes depend only
|
|
169
|
+
// on the source, and re-encoding them costs a machine already short of it.
|
|
170
|
+
assert.deepEqual(store.provenNumbers(KEY), [0, 1, 2, 3, 4]);
|
|
171
|
+
assert.equal(store.pathOf(KEY, 5), null);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("a directory that cannot name itself is thrown away rather than served", (t) => {
|
|
175
|
+
const { store, root } = storeInATempRoot();
|
|
176
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
177
|
+
|
|
178
|
+
const orphan = path.join(root, "0123456789abcdef");
|
|
179
|
+
mkdirSync(orphan, { recursive: true });
|
|
180
|
+
writeSegment(orphan, 0);
|
|
181
|
+
|
|
182
|
+
const taken = store.adoptWhatSurvived(() => fmp4Format);
|
|
183
|
+
|
|
184
|
+
assert.equal(taken.adopted, 0);
|
|
185
|
+
assert.equal(taken.dropped, 1);
|
|
186
|
+
assert.equal(readdirSync(root).length, 0);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("an output this proxy can no longer serve is thrown away too", (t) => {
|
|
190
|
+
const { store, root } = storeInATempRoot();
|
|
191
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
192
|
+
|
|
193
|
+
const dir = path.join(root, directoryNameFor(KEY));
|
|
194
|
+
mkdirSync(dir, { recursive: true });
|
|
195
|
+
writeFileSync(path.join(dir, "key.txt"), `${KEY}\n`);
|
|
196
|
+
writeSegment(dir, 0);
|
|
197
|
+
|
|
198
|
+
const taken = store.adoptWhatSurvived(() => null);
|
|
199
|
+
|
|
200
|
+
assert.equal(taken.dropped, 1);
|
|
201
|
+
assert.equal(readdirSync(root).length, 0);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("what the store weighs is reported, and dropping one output frees it", (t) => {
|
|
205
|
+
const { store, root } = storeInATempRoot();
|
|
206
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
207
|
+
|
|
208
|
+
const dir = store.directoryFor(KEY);
|
|
209
|
+
store.useFormat(KEY, fmp4Format);
|
|
210
|
+
writeSegment(dir, 0, 1000);
|
|
211
|
+
writeSegment(dir, 1, 1000);
|
|
212
|
+
|
|
213
|
+
assert.equal(store.stats().bytes, 2000);
|
|
214
|
+
store.drop(KEY, "nobody is watching it");
|
|
215
|
+
assert.equal(store.stats().outputs, 0);
|
|
216
|
+
});
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file A request
|
|
2
|
+
* @file A segment request steers nothing.
|
|
3
3
|
*
|
|
4
4
|
* Field 2026-08-17: the viewer seeked to 2083.4 s, both runs restarted at
|
|
5
5
|
* segment #373, and a request for #371 — issued before the seek and reissued by
|
|
6
6
|
* the player a second later — moved the encoder to #370. The viewer was at
|
|
7
7
|
* #374 and waited for the encoder to come back to them.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* That was answered first by a guard: a request behind what the viewer had
|
|
10
|
+
* REPORTED was refused, while the same traffic still moved the encoder when
|
|
11
|
+
* nothing had been reported. The guard is gone with the thing it guarded. A
|
|
12
|
+
* request for a file is not a statement about where anybody is, and nothing
|
|
13
|
+
* about it belongs in the decision of where encoders go — that is read from the
|
|
14
|
+
* priority map, which is built from where the viewers are and from nothing else.
|
|
12
15
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* the guard rather than of the weather.
|
|
16
|
+
* So the rule pinned here is now the whole rule, and it has no exception: a
|
|
17
|
+
* request moves neither the encoder nor the viewer, whether or not the viewer
|
|
18
|
+
* has said anything.
|
|
17
19
|
*/
|
|
18
20
|
|
|
19
21
|
import assert from "node:assert/strict";
|
|
@@ -40,79 +42,48 @@ const SESSION_ID = "22222222-3333-4444-5555-666666666666";
|
|
|
40
42
|
* @returns {Promise<{ manager: HlsSessionManager, session: object, dirPath: string }>}
|
|
41
43
|
*/
|
|
42
44
|
async function sessionWithRunAt373() {
|
|
43
|
-
const dirPath = await mkdtemp(path.join(os.tmpdir(), "stale-
|
|
45
|
+
const dirPath = await mkdtemp(path.join(os.tmpdir(), "stale-request-"));
|
|
44
46
|
const manager = new HlsSessionManager({
|
|
45
47
|
enabled: true,
|
|
46
48
|
ffmpegBin: "ffmpeg",
|
|
47
49
|
localBindHost: "127.0.0.1",
|
|
48
50
|
localPort: 9090
|
|
49
51
|
});
|
|
50
|
-
const boundaries = [];
|
|
51
|
-
for (let index = 0; index <= 600; index += 1) {
|
|
52
|
-
boundaries.push(index * SEGMENT_SECONDS);
|
|
53
|
-
}
|
|
54
52
|
const session = {
|
|
55
53
|
id: SESSION_ID,
|
|
56
54
|
dirPath,
|
|
57
|
-
// Where this file is cut, held by the file. A fixture that stated it
|
|
58
|
-
// on the session was describing what production no longer does.
|
|
59
55
|
timeline: new Timeline({
|
|
60
|
-
boundaries:
|
|
56
|
+
boundaries: Array.from({ length: 901 }, (_, index) => index * SEGMENT_SECONDS),
|
|
61
57
|
cutGrid: "uniform"
|
|
62
58
|
}),
|
|
63
59
|
state: "ready",
|
|
64
60
|
file: new SourceFile({ sourceKey: "source-1", fileIndex: 0, name: "film.mkv" }),
|
|
65
|
-
// An ordinary session reads its own file, and its sound is inside it. The
|
|
66
|
-
// three differ only for a soundtrack shipped as a file of its own.
|
|
67
61
|
get inputFile() { return this.file; },
|
|
68
62
|
get audioFile() { return this.file; },
|
|
63
|
+
segmentFormat: fmp4Format,
|
|
64
|
+
segmentCount: 900,
|
|
65
|
+
startedAt: Date.now(),
|
|
69
66
|
createEntryMs: Date.now(),
|
|
70
67
|
lastAccessedAt: Date.now(),
|
|
68
|
+
runs: new Set(),
|
|
71
69
|
consumers: new Set(),
|
|
72
|
-
|
|
70
|
+
viewers: new Map(),
|
|
71
|
+
progress: { processedSeconds: RUN_STARTS_AT * SEGMENT_SECONDS },
|
|
73
72
|
useSyntheticPlaylist: true,
|
|
74
73
|
playlistText: "#EXTM3U\n",
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
firstWantedAt: new Map(),
|
|
79
|
-
holdExplainedAt: new Map(),
|
|
80
|
-
seekSettleTimer: null,
|
|
81
|
-
seekTarget: null,
|
|
82
|
-
failedStartAt: -1,
|
|
83
|
-
failedStartCount: 0,
|
|
84
|
-
waitEpoch: 0,
|
|
85
|
-
firstSegmentLogged: true,
|
|
86
|
-
progress: { processedSeconds: RUN_STARTS_AT * SEGMENT_SECONDS, speed: "1.0x", startPositionSeconds: RUN_STARTS_AT * SEGMENT_SECONDS }
|
|
74
|
+
lastError: "",
|
|
75
|
+
firstSegmentLogged: false,
|
|
76
|
+
waitEpoch: 0
|
|
87
77
|
};
|
|
88
78
|
manager.sessionsById.set(SESSION_ID, session);
|
|
89
|
-
|
|
90
|
-
startRunOn(session, { from: RUN_STARTS_AT, usesExplicitCuts: true });
|
|
79
|
+
startRunOn(session, { from: RUN_STARTS_AT, usesExplicitCuts: true, speedX: 2 });
|
|
91
80
|
return { manager, session, dirPath };
|
|
92
81
|
}
|
|
93
82
|
|
|
94
83
|
/**
|
|
95
|
-
* The traffic that dragged the encoder back: the same index asked for twice,
|
|
96
|
-
* first wanted long enough ago to pass the repair's patience guard.
|
|
97
|
-
*
|
|
98
|
-
* @param {object} session
|
|
99
|
-
*/
|
|
100
|
-
function askedTwiceLongEnough(session) {
|
|
101
|
-
session.firstWantedAt.set(BEHIND_INDEX, Date.now() - 5_000);
|
|
102
|
-
session.behindHeadAsks.set(BEHIND_INDEX, { count: 3, at: Date.now() });
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Put the session down without going through disposal.
|
|
107
|
-
*
|
|
108
|
-
* Disposal signals the encoder and waits for it to die, which a stub cannot do
|
|
109
|
-
* — and none of that is what these tests are about. Clearing the map and any
|
|
110
|
-
* armed timer leaves nothing running.
|
|
111
|
-
*
|
|
112
84
|
* @param {HlsSessionManager} manager
|
|
113
85
|
* @param {object} session
|
|
114
86
|
* @param {string} dirPath
|
|
115
|
-
* @returns {Promise<void>}
|
|
116
87
|
*/
|
|
117
88
|
async function tidy(manager, session, dirPath) {
|
|
118
89
|
if (session.seekSettleTimer) {
|
|
@@ -131,45 +102,48 @@ test("a request behind where the viewer said they are does not move the encoder"
|
|
|
131
102
|
});
|
|
132
103
|
|
|
133
104
|
// The viewer stated their position: 2083.4 s, which is segment #520 here.
|
|
134
|
-
manager.requestSeek(SESSION_ID, 2083.4);
|
|
135
|
-
askedTwiceLongEnough(session);
|
|
105
|
+
manager.requestSeek(SESSION_ID, 2083.4, "viewer-1");
|
|
136
106
|
|
|
137
|
-
|
|
107
|
+
await manager.getFileStream(
|
|
138
108
|
SESSION_ID,
|
|
139
109
|
fmp4Format.segmentFileName(BEHIND_INDEX),
|
|
140
110
|
{ requestSeq: 1 }
|
|
141
111
|
);
|
|
142
112
|
|
|
143
|
-
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
BEHIND_INDEX - 1,
|
|
113
|
+
// The field log's line was `seek settle → restart at segment #370`. Nothing
|
|
114
|
+
// aims an encoder from here at all now, so there is no destination for a
|
|
115
|
+
// stale request to become.
|
|
116
|
+
assert.equal(
|
|
117
|
+
session.seekTarget ?? null,
|
|
118
|
+
null,
|
|
150
119
|
"a request behind the viewer must not become the encoder's destination"
|
|
151
120
|
);
|
|
152
|
-
assert.equal(
|
|
121
|
+
assert.equal(
|
|
122
|
+
session.seekSettleTimer ?? null,
|
|
123
|
+
null,
|
|
124
|
+
"and it must not arm a restart"
|
|
125
|
+
);
|
|
153
126
|
});
|
|
154
127
|
|
|
155
|
-
test("the same traffic
|
|
128
|
+
test("the same traffic moves nothing when the viewer has said nothing either", async (t) => {
|
|
156
129
|
const { manager, session, dirPath } = await sessionWithRunAt373();
|
|
157
130
|
t.after(async () => {
|
|
158
131
|
await tidy(manager, session, dirPath);
|
|
159
132
|
});
|
|
160
133
|
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
"with nothing said by the viewer, a request stuck behind the head still repairs the run"
|
|
134
|
+
// THE CONTROL, INVERTED. It used to be that without a reported seek this very
|
|
135
|
+
// traffic DID move the encoder — the guard applied only to a viewer who had
|
|
136
|
+
// spoken. That exception is the thing that was removed: a request is evidence
|
|
137
|
+
// about what a player is reading, never about where a person is, and an
|
|
138
|
+
// encoder placed from it is placed from a number the player picked.
|
|
139
|
+
await manager.getFileStream(
|
|
140
|
+
SESSION_ID,
|
|
141
|
+
fmp4Format.segmentFileName(BEHIND_INDEX),
|
|
142
|
+
{ requestSeq: 1 }
|
|
171
143
|
);
|
|
172
|
-
|
|
144
|
+
|
|
145
|
+
assert.equal(session.seekTarget ?? null, null);
|
|
146
|
+
assert.equal(session.seekSettleTimer ?? null, null);
|
|
173
147
|
});
|
|
174
148
|
|
|
175
149
|
test("a request cannot move the viewer's position backwards", async (t) => {
|
|
@@ -178,12 +152,12 @@ test("a request cannot move the viewer's position backwards", async (t) => {
|
|
|
178
152
|
await tidy(manager, session, dirPath);
|
|
179
153
|
});
|
|
180
154
|
|
|
181
|
-
manager.requestSeek(SESSION_ID, 2083.4);
|
|
155
|
+
manager.requestSeek(SESSION_ID, 2083.4, "viewer-1");
|
|
182
156
|
await manager.getFileStream(SESSION_ID, fmp4Format.segmentFileName(BEHIND_INDEX), { requestSeq: 1 });
|
|
183
157
|
|
|
184
158
|
assert.equal(
|
|
185
|
-
|
|
159
|
+
manager.viewers.get("viewer-1")?.positionSeconds(),
|
|
186
160
|
2083.4,
|
|
187
|
-
"a stale request must not rewrite what the viewer
|
|
161
|
+
"a stale request must not rewrite what the viewer said about themselves"
|
|
188
162
|
);
|
|
189
163
|
});
|