@torrent-tv/proxy 2.80.9 → 2.80.11
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 +1685 -1666
- package/package.json +1 -1
- package/services/data-channel-handler.js +2 -4
- package/services/encode/EncodePlan.js +0 -3
- package/services/encode/EncodeRun.js +14 -0
- package/services/encode/run-command.js +729 -707
- package/services/hls-session-manager.js +10168 -10494
- package/services/hwaccel.js +2 -2
- package/services/orchestrators/EncodeOrchestrator.js +4 -4
- package/services/output/LiveOutputs.js +49 -0
- package/services/priority/PriorityOrchestrator.js +278 -174
- 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/decode-cost-fit.test.js +57 -0
- package/test/decode-cost.test.js +13 -36
- package/test/encode-orchestrator.test.js +1 -2
- package/test/encode-plan-viewers.test.js +18 -19
- package/test/encode-plan.test.js +539 -539
- package/test/held-request-width.test.js +155 -154
- package/test/helpers/encode-run.js +136 -128
- package/test/keyframe-table-budget.test.js +7 -1
- package/test/one-authority.test.js +220 -105
- package/test/orchestrator-wired.test.js +16 -11
- package/test/piece-store-slow-disk.test.js +32 -2
- package/test/priority-map-per-output.test.js +211 -0
- package/test/produced-copy-choice.test.js +358 -327
- package/test/quality-variants.test.js +1222 -1209
- package/test/segment-serve-wiring.test.js +76 -20
- package/test/stale-request-after-seek.test.js +51 -77
- package/test/tracks-begin-together.test.js +195 -153
- package/test/tunnel-renewal.test.js +34 -5
- package/test/two-viewers-one-picture.test.js +374 -347
- package/test/viewer-outputs.test.js +18 -14
- package/test/behind-head-repair.test.js +0 -261
- package/test/decode-measurement.test.js +0 -83
- /package/services/{contention.js → encode/contention.js} +0 -0
|
@@ -82,7 +82,6 @@ function fakeSession({ id, dirPath, file, encodeHeight = 0, audioOnly = false, i
|
|
|
82
82
|
applyTonemap: false
|
|
83
83
|
}),
|
|
84
84
|
encodeRunGeneration: 0,
|
|
85
|
-
lastRestartAt: 0,
|
|
86
85
|
failedStartAt: -1,
|
|
87
86
|
failedStartCount: 0,
|
|
88
87
|
seekSettleTimer: null,
|
|
@@ -128,14 +127,7 @@ async function pictureWithStepAndSoundtrack() {
|
|
|
128
127
|
manager.sessionsById.set(session.id, session);
|
|
129
128
|
}
|
|
130
129
|
file.stepHeights.set(540, 540);
|
|
131
|
-
|
|
132
|
-
const released = [];
|
|
133
|
-
const realRelease = manager.encodeOrchestrator.release.bind(manager.encodeOrchestrator);
|
|
134
|
-
manager.encodeOrchestrator.release = (claimant) => {
|
|
135
|
-
released.push(claimant);
|
|
136
|
-
return realRelease(claimant);
|
|
137
|
-
};
|
|
138
|
-
return { manager, base, step, audio, dirPath, released };
|
|
130
|
+
return { manager, base, step, audio, dirPath };
|
|
139
131
|
}
|
|
140
132
|
|
|
141
133
|
test("a viewer who steps down, back to the picture's own height and down again keeps their record", async (t) => {
|
|
@@ -224,8 +216,8 @@ test("an output somebody else is still watching is kept when one viewer leaves",
|
|
|
224
216
|
assert.equal(step.viewers.size, 0, "the step they had is watched by nobody");
|
|
225
217
|
});
|
|
226
218
|
|
|
227
|
-
test("
|
|
228
|
-
const { manager, base, step, audio, dirPath
|
|
219
|
+
test("nothing is left wanting production once the last viewer has left", async (t) => {
|
|
220
|
+
const { manager, base, step, audio, dirPath } = await pictureWithStepAndSoundtrack();
|
|
229
221
|
t.after(() => rm(dirPath, { recursive: true, force: true }));
|
|
230
222
|
manager.viewers.of(base, VIEWER).position = { segment: 25, seconds: 100, at: Date.now() };
|
|
231
223
|
manager.viewers.of(step, VIEWER);
|
|
@@ -233,10 +225,22 @@ test("leaving an output releases what the watching claimed of production", async
|
|
|
233
225
|
|
|
234
226
|
await manager.releaseSessionConsumer(BASE_ID, VIEWER, "the viewer left");
|
|
235
227
|
|
|
228
|
+
// NOTHING HAS TO BE RELEASED, and that is the point of the shape. A claim per
|
|
229
|
+
// viewer per output used to be held in the encoding layer and taken back one
|
|
230
|
+
// by one when they left — the viewer's own name as the key of a claim, which
|
|
231
|
+
// is the coupling the layer rule forbids. What the plan is given now is a map
|
|
232
|
+
// with nobody's name in it, rebuilt from whoever is watching; a viewer who has
|
|
233
|
+
// left is simply not in the next one.
|
|
236
234
|
for (const id of [BASE_ID, STEP_ID, AUDIO_ID]) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
235
|
+
const session = manager.sessionsById.get(id);
|
|
236
|
+
assert.equal(session, undefined, `${id.slice(0, 8)} is let go with its last viewer`);
|
|
237
|
+
}
|
|
238
|
+
manager.planEncodersNow();
|
|
239
|
+
for (const address of [base.outputKey, step.outputKey, audio.outputKey]) {
|
|
240
|
+
assert.deepEqual(
|
|
241
|
+
manager.encodeOrchestrator.demand.mapOn(address),
|
|
242
|
+
[],
|
|
243
|
+
"and no output is left asking for an encoder"
|
|
240
244
|
);
|
|
241
245
|
}
|
|
242
246
|
});
|
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file A segment request below the running encode must not be held for ever.
|
|
3
|
-
*
|
|
4
|
-
* The encoder only moves forward from where its run began, so a request BELOW
|
|
5
|
-
* that point cannot be answered by anything the run does. Every other far
|
|
6
|
-
* request is a claim the running encode may yet reach; this one is a hole.
|
|
7
|
-
*
|
|
8
|
-
* Measured 2026-08-11: a quality switch placed a run at segment #770 while the
|
|
9
|
-
* player needed #757, and the request was held for two minutes forty-one while
|
|
10
|
-
* the encoder produced 409 s of video nobody had asked for at 2.48x. The
|
|
11
|
-
* placement that caused it is fixed; this is the guard that stops the SHAPE
|
|
12
|
-
* from hanging a session again, whatever puts it there.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import test from "node:test";
|
|
16
|
-
import assert from "node:assert/strict";
|
|
17
|
-
import { SourceFile } from "../services/source/SourceFile.js";
|
|
18
|
-
import { Timeline } from "../services/output/Timeline.js";
|
|
19
|
-
import { mkdtemp, rm } from "node:fs/promises";
|
|
20
|
-
import os from "node:os";
|
|
21
|
-
import path from "node:path";
|
|
22
|
-
import { HlsSessionManager } from "../services/hls-session-manager.js";
|
|
23
|
-
import { startRunOn } from "./helpers/encode-run.js";
|
|
24
|
-
import { fmp4Format } from "../services/segment-formats/fmp4.js";
|
|
25
|
-
|
|
26
|
-
const SESSION_ID = "bbbbbbbb-cccc-dddd-eeee-ffffffffffff";
|
|
27
|
-
const SEGMENT_SECONDS = 4;
|
|
28
|
-
const RUN_STARTS_AT = 770;
|
|
29
|
-
const WANTED = 757;
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* A session whose run began well past the segment being asked for.
|
|
33
|
-
*
|
|
34
|
-
* @returns {Promise<{ manager: HlsSessionManager, session: object, dirPath: string, restarts: number[] }>}
|
|
35
|
-
*/
|
|
36
|
-
async function managerWithRunAhead() {
|
|
37
|
-
const dirPath = await mkdtemp(path.join(os.tmpdir(), "behind-head-"));
|
|
38
|
-
const manager = new HlsSessionManager({
|
|
39
|
-
enabled: true,
|
|
40
|
-
ffmpegBin: "ffmpeg",
|
|
41
|
-
localBindHost: "127.0.0.1",
|
|
42
|
-
localPort: 9090
|
|
43
|
-
});
|
|
44
|
-
const session = {
|
|
45
|
-
id: SESSION_ID,
|
|
46
|
-
dirPath,
|
|
47
|
-
// Where this file is cut, held by the file. A fixture that stated it
|
|
48
|
-
// on the session was describing what production no longer does.
|
|
49
|
-
timeline: new Timeline({
|
|
50
|
-
boundaries: Array.from({ length: 1937 }, (_, index) => index * SEGMENT_SECONDS),
|
|
51
|
-
cutGrid: "uniform"
|
|
52
|
-
}),
|
|
53
|
-
state: "ready",
|
|
54
|
-
file: new SourceFile({ sourceKey: "source-1", fileIndex: 0, name: "film.avi" }),
|
|
55
|
-
// An ordinary session reads its own file, and its sound is inside it. The
|
|
56
|
-
// three differ only for a soundtrack shipped as a file of its own.
|
|
57
|
-
get inputFile() { return this.file; },
|
|
58
|
-
get audioFile() { return this.file; },
|
|
59
|
-
startedAt: Date.now(),
|
|
60
|
-
createEntryMs: Date.now(),
|
|
61
|
-
lastAccessedAt: Date.now(),
|
|
62
|
-
lastError: "",
|
|
63
|
-
consumers: new Set(),
|
|
64
|
-
segmentFormat: fmp4Format,
|
|
65
|
-
transcodeVideo: true,
|
|
66
|
-
useSyntheticPlaylist: true,
|
|
67
|
-
playlistText: "#EXTM3U\n",
|
|
68
|
-
lastRestartAt: 0,
|
|
69
|
-
failedStartAt: -1,
|
|
70
|
-
failedStartCount: 0,
|
|
71
|
-
seekSettleTimer: null,
|
|
72
|
-
seekTarget: null,
|
|
73
|
-
waitEpoch: 0,
|
|
74
|
-
firstWantedAt: new Map(),
|
|
75
|
-
runs: new Set(),
|
|
76
|
-
progress: { state: "running", processedSeconds: RUN_STARTS_AT * SEGMENT_SECONDS + 400, startPositionSeconds: RUN_STARTS_AT * SEGMENT_SECONDS }
|
|
77
|
-
};
|
|
78
|
-
manager.sessionsById.set(SESSION_ID, session);
|
|
79
|
-
startRunOn(session, { from: RUN_STARTS_AT });
|
|
80
|
-
const restarts = [];
|
|
81
|
-
return { manager, session, dirPath, restarts };
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
test("a request behind the run is repaired once the player asks again", async (t) => {
|
|
85
|
-
const { manager, session, dirPath } = await managerWithRunAhead();
|
|
86
|
-
t.after(async () => {
|
|
87
|
-
await manager.disposeAll();
|
|
88
|
-
await rm(dirPath, { recursive: true, force: true });
|
|
89
|
-
});
|
|
90
|
-
// Asked for a moment ago and still unanswerable. What decides is that the
|
|
91
|
-
// player comes BACK for the same segment: it is the only one it wants, and no
|
|
92
|
-
// amount of waiting could say that as clearly.
|
|
93
|
-
session.firstWantedAt.set(WANTED, Date.now() - 1000);
|
|
94
|
-
const name = fmp4Format.segmentFileName(WANTED);
|
|
95
|
-
|
|
96
|
-
await manager.getFileStream(SESSION_ID, name, { requestSeq: 1 });
|
|
97
|
-
|
|
98
|
-
assert.equal(session.seekTarget, null, "one poll is not yet evidence");
|
|
99
|
-
|
|
100
|
-
await manager.getFileStream(SESSION_ID, name, { requestSeq: 2 });
|
|
101
|
-
|
|
102
|
-
assert.equal(
|
|
103
|
-
session.seekTarget,
|
|
104
|
-
WANTED - 1,
|
|
105
|
-
"the encoder must be moved back to it — one segment early, for the preceding keyframe"
|
|
106
|
-
);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test("a burst of different segments behind the run is a scan, and moves nothing", async (t) => {
|
|
110
|
-
const { manager, session, dirPath } = await managerWithRunAhead();
|
|
111
|
-
t.after(async () => {
|
|
112
|
-
await manager.disposeAll();
|
|
113
|
-
await rm(dirPath, { recursive: true, force: true });
|
|
114
|
-
});
|
|
115
|
-
// What hls.js does on a seek: dozens of indices within half a second, each
|
|
116
|
-
// abandoned. Field log 2026-08-02: #178, #681, #725, #807, #74, #245, #387.
|
|
117
|
-
const scanned = [WANTED, WANTED - 40, WANTED - 120, WANTED - 200, WANTED - 300];
|
|
118
|
-
for (const index of scanned) {
|
|
119
|
-
session.firstWantedAt.set(index, Date.now() - 1000);
|
|
120
|
-
}
|
|
121
|
-
// Asked ONCE each, which is what a scan is: the player opens them together
|
|
122
|
-
// and abandons them together. Field log 2026-08-02 — #178, #681, #725, #807,
|
|
123
|
-
// #74, #245, #387 within half a second, none of them repeated.
|
|
124
|
-
for (const index of scanned) {
|
|
125
|
-
await manager.getFileStream(SESSION_ID, fmp4Format.segmentFileName(index), { requestSeq: 1 });
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
assert.equal(
|
|
129
|
-
session.seekTarget,
|
|
130
|
-
null,
|
|
131
|
-
"moving the encoder to one of these would be moving it to a number the player picked at random"
|
|
132
|
-
);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
test("a request behind the run is left alone at first", async (t) => {
|
|
136
|
-
const { manager, session, dirPath } = await managerWithRunAhead();
|
|
137
|
-
t.after(async () => {
|
|
138
|
-
await manager.disposeAll();
|
|
139
|
-
await rm(dirPath, { recursive: true, force: true });
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
await manager.getFileStream(SESSION_ID, fmp4Format.segmentFileName(WANTED), { requestSeq: 1 });
|
|
143
|
-
|
|
144
|
-
assert.equal(
|
|
145
|
-
session.seekTarget,
|
|
146
|
-
null,
|
|
147
|
-
"a burst around a reported seek settles by itself, and the seek is what should move the encoder"
|
|
148
|
-
);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
test("a seek already settling is not overridden", async (t) => {
|
|
152
|
-
const { manager, session, dirPath } = await managerWithRunAhead();
|
|
153
|
-
t.after(async () => {
|
|
154
|
-
await manager.disposeAll();
|
|
155
|
-
await rm(dirPath, { recursive: true, force: true });
|
|
156
|
-
});
|
|
157
|
-
session.firstWantedAt.set(WANTED, Date.now() - 4000);
|
|
158
|
-
// The viewer has stated where they are and it is about to be acted on.
|
|
159
|
-
session.seekTarget = 1200;
|
|
160
|
-
session.seekSettleTimer = setTimeout(() => {}, 60_000);
|
|
161
|
-
t.after(() => clearTimeout(session.seekSettleTimer));
|
|
162
|
-
|
|
163
|
-
await manager.getFileStream(SESSION_ID, fmp4Format.segmentFileName(WANTED), { requestSeq: 1 });
|
|
164
|
-
|
|
165
|
-
assert.equal(
|
|
166
|
-
session.seekTarget,
|
|
167
|
-
1200,
|
|
168
|
-
"a statement from the viewer outranks anything inferred from what the player is fetching"
|
|
169
|
-
);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
test("a playlist scan far below the run is left where it belongs", async (t) => {
|
|
173
|
-
const { manager, session, dirPath } = await managerWithRunAhead();
|
|
174
|
-
t.after(async () => {
|
|
175
|
-
await manager.disposeAll();
|
|
176
|
-
await rm(dirPath, { recursive: true, force: true });
|
|
177
|
-
});
|
|
178
|
-
// A player that cannot get what it wants scans the playlist: field log
|
|
179
|
-
// 2026-08-02, probes at #178, #681, #725, #807, #74, #245, #387 within half a
|
|
180
|
-
// second. Steering on the lowest of those put the encoder at the start of the
|
|
181
|
-
// film and left the viewer's own requests unreachable ahead of it — the exact
|
|
182
|
-
// reason request-steering was removed from this proxy.
|
|
183
|
-
const probe = 74;
|
|
184
|
-
session.firstWantedAt.set(probe, Date.now() - 30_000);
|
|
185
|
-
|
|
186
|
-
await manager.getFileStream(SESSION_ID, fmp4Format.segmentFileName(probe), { requestSeq: 1 });
|
|
187
|
-
|
|
188
|
-
assert.equal(
|
|
189
|
-
session.seekTarget,
|
|
190
|
-
null,
|
|
191
|
-
"a misplaced run is out by a buffer; a scan probe is out by anything, and the two must not be confused"
|
|
192
|
-
);
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
test("a rung whose encoder was stopped is not brought back by a held request", async (t) => {
|
|
196
|
-
const { manager, session, dirPath } = await managerWithRunAhead();
|
|
197
|
-
t.after(async () => {
|
|
198
|
-
await manager.disposeAll();
|
|
199
|
-
await rm(dirPath, { recursive: true, force: true });
|
|
200
|
-
});
|
|
201
|
-
// What a quality switch leaves behind: the rung nobody is watching, parked.
|
|
202
|
-
session.runs = new Set();
|
|
203
|
-
session.firstWantedAt.set(WANTED, Date.now() - 4000);
|
|
204
|
-
|
|
205
|
-
await manager.getFileStream(SESSION_ID, fmp4Format.segmentFileName(WANTED), { requestSeq: 1 });
|
|
206
|
-
|
|
207
|
-
assert.equal(
|
|
208
|
-
session.seekTarget,
|
|
209
|
-
null,
|
|
210
|
-
"restarting it would put a second encoder on a host sized for one, for a rung nobody is watching"
|
|
211
|
-
);
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
test("a request ahead of the run is not touched", async (t) => {
|
|
215
|
-
const { manager, session, dirPath } = await managerWithRunAhead();
|
|
216
|
-
t.after(async () => {
|
|
217
|
-
await manager.disposeAll();
|
|
218
|
-
await rm(dirPath, { recursive: true, force: true });
|
|
219
|
-
});
|
|
220
|
-
const ahead = RUN_STARTS_AT + 400;
|
|
221
|
-
session.firstWantedAt.set(ahead, Date.now() - 30_000);
|
|
222
|
-
|
|
223
|
-
await manager.getFileStream(SESSION_ID, fmp4Format.segmentFileName(ahead), { requestSeq: 1 });
|
|
224
|
-
|
|
225
|
-
assert.equal(
|
|
226
|
-
session.seekTarget,
|
|
227
|
-
null,
|
|
228
|
-
"the running encode may yet reach it; restarting on a far request is what produced nine restarts in a minute"
|
|
229
|
-
);
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
test("a request far beyond the repair's reach is answered at once, not held", async (t) => {
|
|
233
|
-
const { manager, session, dirPath } = await managerWithRunAhead();
|
|
234
|
-
t.after(async () => {
|
|
235
|
-
await manager.disposeAll();
|
|
236
|
-
await rm(dirPath, { recursive: true, force: true });
|
|
237
|
-
});
|
|
238
|
-
// What a track change does: hls.js asks the new stream for segment #0 while
|
|
239
|
-
// the run is hundreds of segments in. The repair cannot reach it, no seek is
|
|
240
|
-
// coming, and the run only moves forward — so it can never be produced.
|
|
241
|
-
// Measured 2026-08-15: held for the full minute, and the viewer watched a
|
|
242
|
-
// spinner for 63 s after a track that had been made ready in 7.
|
|
243
|
-
const answer = await manager.getFileStream(SESSION_ID, fmp4Format.segmentFileName(0), { requestSeq: 1 });
|
|
244
|
-
|
|
245
|
-
assert.equal(answer.kind, "not-found", "answered, so the player can move on to what it can have");
|
|
246
|
-
assert.equal(session.seekTarget, null, "and the encoder was not sent to the start of the film for it");
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
test("a request just behind the run is still held, because the repair will fetch it", async (t) => {
|
|
250
|
-
const { manager, session, dirPath } = await managerWithRunAhead();
|
|
251
|
-
t.after(async () => {
|
|
252
|
-
await manager.disposeAll();
|
|
253
|
-
await rm(dirPath, { recursive: true, force: true });
|
|
254
|
-
});
|
|
255
|
-
const name = fmp4Format.segmentFileName(WANTED);
|
|
256
|
-
session.firstWantedAt.set(WANTED, Date.now() - 1000);
|
|
257
|
-
|
|
258
|
-
const answer = await manager.getFileStream(SESSION_ID, name, { requestSeq: 1 });
|
|
259
|
-
|
|
260
|
-
assert.equal(answer.kind, "warming-up", "within reach: the encoder is about to be moved there");
|
|
261
|
-
});
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file What the decode measurement must never get wrong.
|
|
3
|
-
*
|
|
4
|
-
* The quality menu is predicted from these readings, so a reading that is
|
|
5
|
-
* merely PRECISE is worth nothing — it has to be ordered the way decoding is.
|
|
6
|
-
* Under `-stream_loop -1` it was not: the loop restart costs 0.03 s on a 480p
|
|
7
|
-
* clip and 0.12 s on a 1080p one (the decoder tearing down and re-allocating
|
|
8
|
-
* its frame buffers), a five-second clip at 55x restarts eleven times a second,
|
|
9
|
-
* and the resulting bias depends on the clip's own resolution and on how fast
|
|
10
|
-
* the host is. On a fast desktop, 2026-08-20, that made 1080p read cheaper than
|
|
11
|
-
* 720p — which is not a thing a decoder does, and the fit refused to solve.
|
|
12
|
-
*
|
|
13
|
-
* These tests state the orderings instead of the numbers: the numbers are
|
|
14
|
-
* properties of whatever machine runs them.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import test from "node:test";
|
|
18
|
-
import assert from "node:assert/strict";
|
|
19
|
-
import path from "node:path";
|
|
20
|
-
import { fileURLToPath } from "node:url";
|
|
21
|
-
import ffmpegStatic from "ffmpeg-static";
|
|
22
|
-
import { measureDecodeSlope } from "../services/hwaccel.js";
|
|
23
|
-
|
|
24
|
-
const CLIPS = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "assets", "calibration");
|
|
25
|
-
const clip = (name) => path.join(CLIPS, name);
|
|
26
|
-
|
|
27
|
-
test("a bigger picture costs more than a smaller one of the same bitrate", async () => {
|
|
28
|
-
const big = await measureDecodeSlope(ffmpegStatic, clip("cal-h264-1080-hi.mp4"));
|
|
29
|
-
const small = await measureDecodeSlope(ffmpegStatic, clip("cal-h264-480-hi.mp4"));
|
|
30
|
-
|
|
31
|
-
assert.ok(big, "the 1080p clip was measured");
|
|
32
|
-
assert.ok(small, "the 480p clip was measured");
|
|
33
|
-
assert.ok(
|
|
34
|
-
small.speed > big.speed,
|
|
35
|
-
`480p decoded at ${small.speed.toFixed(1)}x and 1080p at ${big.speed.toFixed(1)}x — ` +
|
|
36
|
-
`an ordering no decoder produces, which is what the loop used to invert`
|
|
37
|
-
);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test("a thicker stream costs more than a thin one of the same size", async () => {
|
|
41
|
-
const thick = await measureDecodeSlope(ffmpegStatic, clip("cal-h264-480-hi.mp4"));
|
|
42
|
-
const thin = await measureDecodeSlope(ffmpegStatic, clip("cal-h264-480-lo.mp4"));
|
|
43
|
-
|
|
44
|
-
assert.ok(thick && thin);
|
|
45
|
-
assert.ok(
|
|
46
|
-
thin.speed > thick.speed,
|
|
47
|
-
`the same picture at ${thin.megabitsPerSecond.toFixed(2)} Mbit/s decoded at ${thin.speed.toFixed(1)}x ` +
|
|
48
|
-
`and at ${thick.megabitsPerSecond.toFixed(2)} Mbit/s at ${thick.speed.toFixed(1)}x`
|
|
49
|
-
);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test("HEVC costs more than H.264 for the same picture on the same machine", async () => {
|
|
53
|
-
const h264 = await measureDecodeSlope(ffmpegStatic, clip("cal-h264-480-lo.mp4"), "h264");
|
|
54
|
-
const hevc = await measureDecodeSlope(ffmpegStatic, clip("cal-hevc-480-lo.mp4"), "hevc");
|
|
55
|
-
|
|
56
|
-
assert.ok(h264 && hevc, "both families are lifted out of their containers correctly");
|
|
57
|
-
assert.ok(
|
|
58
|
-
h264.speed > hevc.speed,
|
|
59
|
-
`H.264 ${h264.speed.toFixed(1)}x against HEVC ${hevc.speed.toFixed(1)}x — the reason the model ` +
|
|
60
|
-
`is fitted per codec family at all`
|
|
61
|
-
);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test("the clip's own characteristics come back with the reading", async () => {
|
|
65
|
-
const measured = await measureDecodeSlope(ffmpegStatic, clip("cal-h264-1080-hi.mp4"));
|
|
66
|
-
|
|
67
|
-
assert.ok(measured);
|
|
68
|
-
// Read from the container rather than declared anywhere, so replacing a clip
|
|
69
|
-
// cannot silently invalidate the fit that rests on it.
|
|
70
|
-
assert.ok(measured.megapixelsPerSecond > 40 && measured.megapixelsPerSecond < 60);
|
|
71
|
-
assert.ok(measured.megabitsPerSecond > 5 && measured.megabitsPerSecond < 15);
|
|
72
|
-
assert.ok(measured.windowSec >= 0.5, "the slope is taken over a window wide enough to divide by");
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test("a reading carries what the pipe was actually fed, for item 4(d2)'s sanity check", async () => {
|
|
76
|
-
const measured = await measureDecodeSlope(ffmpegStatic, clip("cal-h264-480-lo.mp4"));
|
|
77
|
-
|
|
78
|
-
assert.ok(measured);
|
|
79
|
-
assert.ok(
|
|
80
|
-
measured.pipeThroughputMBps > 0,
|
|
81
|
-
"the write path pushed a nonzero amount of the clip through, or nothing was measured at all"
|
|
82
|
-
);
|
|
83
|
-
});
|
|
File without changes
|