@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
|
@@ -141,11 +141,13 @@ export function requiredSpeedFrom(waits) {
|
|
|
141
141
|
const lostSec = interruptions
|
|
142
142
|
.slice(0, last)
|
|
143
143
|
.reduce((total, one) => total + (one.end - one.start), 0) / 1000;
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
|
|
144
|
+
// Strictly below one: the span holds at least one gap of positive length
|
|
145
|
+
// (`intervals` above is not empty), and the gaps are the time the supply
|
|
146
|
+
// delivered. So the speed this asks for is finite whatever it measured, and
|
|
147
|
+
// it is not cut down to a figure chosen here: a supply that delivered for a
|
|
148
|
+
// thousandth of the time asks a thousand times realtime, which no re-encode
|
|
149
|
+
// meets and which leaves the copy, as it should.
|
|
150
|
+
const lostShare = spanSec > 0 ? lostSec / spanSec : 0;
|
|
149
151
|
return {
|
|
150
152
|
requiredSpeed: 1 / (1 - lostShare),
|
|
151
153
|
lostShare,
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
import test from "node:test";
|
|
20
20
|
import { recordViewerReport } from "../services/viewer/report-intake.js";
|
|
21
|
-
import { fakeProcess as fakeEncoder, startRunOn } from "./helpers/encode-run.js";
|
|
21
|
+
import { fakeProcess as fakeEncoder, measureSpeed, startRunOn } from "./helpers/encode-run.js";
|
|
22
22
|
import assert from "node:assert/strict";
|
|
23
23
|
import { SourceFile } from "../services/media/SourceFile.js";
|
|
24
24
|
import { Timeline } from "../services/encode/output/Timeline.js";
|
|
@@ -92,7 +92,7 @@ function fakeSession({ dirPath, transcodeVideo = true, cutGrid = transcodeVideo
|
|
|
92
92
|
usesExplicitCuts: false,
|
|
93
93
|
useSyntheticPlaylist: true,
|
|
94
94
|
playlistText: "#EXTM3U\n",
|
|
95
|
-
progress: { state: "running", processedSeconds: 40, startPositionSeconds: 0
|
|
95
|
+
progress: { state: "running", processedSeconds: 40, startPositionSeconds: 0 }
|
|
96
96
|
};
|
|
97
97
|
}
|
|
98
98
|
|
|
@@ -162,7 +162,7 @@ function drainingReports(viewer, linkMbps) {
|
|
|
162
162
|
* This viewer's page reports a buffer that grows, on the link named.
|
|
163
163
|
*
|
|
164
164
|
* @param {object} viewer
|
|
165
|
-
* @param {number} linkMbps
|
|
165
|
+
* @param {number | null} linkMbps - Null when the page has not measured it.
|
|
166
166
|
* @returns {void}
|
|
167
167
|
*/
|
|
168
168
|
function fillingReports(viewer, linkMbps) {
|
|
@@ -195,12 +195,12 @@ test("a picture that cannot be kept up with is asked for as another VARIANT, and
|
|
|
195
195
|
});
|
|
196
196
|
|
|
197
197
|
const sizeBefore = `${session.encodeWidth}x${session.encodeHeight}`;
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
|
|
198
|
+
// Below realtime over its own working time, and the viewer's buffer runs
|
|
199
|
+
// dry on its trend although their link carries the stream.
|
|
200
|
+
measureSpeed([...session.runs][0], 0.7);
|
|
201
|
+
drainingReports(manager.viewers.get("viewer"), 80);
|
|
202
202
|
|
|
203
|
-
await manager.quality.
|
|
203
|
+
await manager.quality.noteViewerReported(session.id, "viewer");
|
|
204
204
|
|
|
205
205
|
assert.equal(
|
|
206
206
|
`${session.encodeWidth}x${session.encodeHeight}`,
|
|
@@ -224,10 +224,10 @@ test("a manual viewer is never sent an automatic quality request", async (t) =>
|
|
|
224
224
|
await rm(dirPath, { recursive: true, force: true });
|
|
225
225
|
});
|
|
226
226
|
manager.viewers.get("viewer").qualityMode = "manual";
|
|
227
|
-
|
|
228
|
-
|
|
227
|
+
measureSpeed([...session.runs][0], 0.7);
|
|
228
|
+
drainingReports(manager.viewers.get("viewer"), 80);
|
|
229
229
|
|
|
230
|
-
await manager.quality.
|
|
230
|
+
await manager.quality.noteViewerReported(session.id, "viewer");
|
|
231
231
|
|
|
232
232
|
assert.equal(manager.viewers.get("viewer").qualityAsk, null);
|
|
233
233
|
const progress = await manager.viewerRequests.getSessionProgress(BASE_ID, "viewer");
|
|
@@ -255,8 +255,8 @@ test("the request reaches the browser in the progress report, and stops once the
|
|
|
255
255
|
assert.equal(manager.viewers.get("viewer").qualityAsk, null, "and it is let go of, not merely hidden");
|
|
256
256
|
});
|
|
257
257
|
|
|
258
|
-
test("a request
|
|
259
|
-
const { manager, dirPath } = await managerWithSession();
|
|
258
|
+
test("a request stands while its conditions hold, and the next report that does not ask for it lets it go", async (t) => {
|
|
259
|
+
const { manager, session, dirPath } = await managerWithSession();
|
|
260
260
|
t.after(async () => {
|
|
261
261
|
await manager.lifecycle.disposeAll();
|
|
262
262
|
// The store closes its watch on the directory as it drops the output, and
|
|
@@ -265,14 +265,39 @@ test("a request the player never follows runs out instead of being repeated for
|
|
|
265
265
|
await rm(dirPath, { recursive: true, force: true });
|
|
266
266
|
});
|
|
267
267
|
|
|
268
|
-
//
|
|
269
|
-
// stream with no variants. Neither is an error; both look the same from here.
|
|
268
|
+
// Asked when their buffer was running dry; no chosen time ends it.
|
|
270
269
|
manager.viewers.get("viewer").qualityAsk = { height: 480, at: Date.now() - 120_000, reason: "measured" };
|
|
270
|
+
const standing = await manager.viewerRequests.getSessionProgress(BASE_ID, "viewer");
|
|
271
|
+
assert.equal(standing.requestedHeight, 480, "two minutes on, it still stands: nothing has said otherwise");
|
|
271
272
|
|
|
272
|
-
|
|
273
|
+
// Their next report shows a buffer that grows: what it was asked for is gone.
|
|
274
|
+
// The machine has no room to spare, so nothing else is asked in its place.
|
|
275
|
+
measureSpeed([...session.runs][0], 0.9);
|
|
276
|
+
fillingReports(manager.viewers.get("viewer"), null);
|
|
277
|
+
await manager.quality.noteViewerReported(session.id, "viewer");
|
|
273
278
|
|
|
279
|
+
const progress = await manager.viewerRequests.getSessionProgress(BASE_ID, "viewer");
|
|
274
280
|
assert.equal(progress.requestedHeight, 0);
|
|
275
|
-
assert.equal(manager.viewers.get("viewer").qualityAsk, null, "
|
|
281
|
+
assert.equal(manager.viewers.get("viewer").qualityAsk, null, "let go by the judgement that did not repeat it");
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test("a request the next report asks for again stands", async (t) => {
|
|
285
|
+
const { manager, session, dirPath } = await managerWithSession();
|
|
286
|
+
t.after(async () => {
|
|
287
|
+
await manager.lifecycle.disposeAll();
|
|
288
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
289
|
+
await rm(dirPath, { recursive: true, force: true });
|
|
290
|
+
});
|
|
291
|
+
measureSpeed([...session.runs][0], 0.7);
|
|
292
|
+
drainingReports(manager.viewers.get("viewer"), 80);
|
|
293
|
+
await manager.quality.noteViewerReported(session.id, "viewer");
|
|
294
|
+
const asked = manager.viewers.get("viewer").qualityAsk;
|
|
295
|
+
assert.ok(asked, "the machine cannot keep up and the buffer runs dry");
|
|
296
|
+
|
|
297
|
+
drainingReports(manager.viewers.get("viewer"), 80);
|
|
298
|
+
await manager.quality.noteViewerReported(session.id, "viewer");
|
|
299
|
+
|
|
300
|
+
assert.equal(manager.viewers.get("viewer").qualityAsk, asked, "the same request, not let go and not made again");
|
|
276
301
|
});
|
|
277
302
|
|
|
278
303
|
test("a COPIED picture is never asked to slow its encoder, because it has none", async (t) => {
|
|
@@ -287,10 +312,10 @@ test("a COPIED picture is never asked to slow its encoder, because it has none",
|
|
|
287
312
|
|
|
288
313
|
// Whatever this reading says, a copy has no encoder to make cheaper: moving
|
|
289
314
|
// the viewer to a RE-ENCODED rung costs the machine more, not less.
|
|
290
|
-
|
|
291
|
-
|
|
315
|
+
measureSpeed([...session.runs][0], 0.4);
|
|
316
|
+
drainingReports(manager.viewers.get("viewer"), 80);
|
|
292
317
|
|
|
293
|
-
await manager.quality.
|
|
318
|
+
await manager.quality.noteViewerReported(session.id, "viewer");
|
|
294
319
|
|
|
295
320
|
assert.equal(manager.viewers.get("viewer").qualityAsk, null, "the copy path's lever is the viewer's link, not the CPU");
|
|
296
321
|
});
|
|
@@ -467,15 +492,18 @@ test("the way BACK UP exists, one rung at a time", async (t) => {
|
|
|
467
492
|
await rm(dirPath, { recursive: true, force: true });
|
|
468
493
|
});
|
|
469
494
|
|
|
470
|
-
// The viewer is on 480p
|
|
471
|
-
// than
|
|
495
|
+
// The viewer is on 480p, the machine makes it ahead of realtime over its own
|
|
496
|
+
// working time, and their buffer grows and holds more than another output
|
|
497
|
+
// takes to be ready. No window has to pass: every term is measured.
|
|
472
498
|
session.variantHeight = 480;
|
|
473
499
|
session.encodeWidth = 854;
|
|
474
500
|
session.encodeHeight = 480;
|
|
475
|
-
|
|
476
|
-
|
|
501
|
+
measureSpeed([...session.runs][0], 2.4);
|
|
502
|
+
// Their link is not measured here: what is under test is the room, and a
|
|
503
|
+
// measured link would also weigh a soundtrack this fixture states no rate for.
|
|
504
|
+
fillingReports(manager.viewers.get("viewer"), null);
|
|
477
505
|
|
|
478
|
-
await manager.quality.
|
|
506
|
+
await manager.quality.noteViewerReported(session.id, "viewer");
|
|
479
507
|
|
|
480
508
|
assert.ok(manager.viewers.get("viewer").qualityAsk, "for most of this project's life there was no step up at all");
|
|
481
509
|
assert.equal(
|
|
@@ -642,8 +670,9 @@ test("a picture seen larger than the rung on screen is asked one rung up, when t
|
|
|
642
670
|
session.variantHeight = 480;
|
|
643
671
|
session.encodeWidth = 854;
|
|
644
672
|
session.encodeHeight = 480;
|
|
645
|
-
|
|
673
|
+
measureSpeed([...session.runs][0], 2.4);
|
|
646
674
|
manager.viewers.get("viewer").noteVisiblePicture({ width: 1920, height: 1080 });
|
|
675
|
+
fillingReports(manager.viewers.get("viewer"), null);
|
|
647
676
|
|
|
648
677
|
await manager.quality.noteViewerReported(session.id, "viewer");
|
|
649
678
|
|
|
@@ -696,12 +725,29 @@ test("a step down for the machine goes to the rung the picture seen bounds, wher
|
|
|
696
725
|
await rm(dirPath, { recursive: true, force: true });
|
|
697
726
|
});
|
|
698
727
|
manager.viewers.get("viewer").noteVisiblePicture({ width: 640, height: 360 });
|
|
699
|
-
|
|
700
|
-
|
|
728
|
+
measureSpeed([...session.runs][0], 0.7);
|
|
729
|
+
drainingReports(manager.viewers.get("viewer"), 80);
|
|
701
730
|
|
|
702
|
-
await manager.quality.
|
|
731
|
+
await manager.quality.noteViewerReported(session.id, "viewer");
|
|
703
732
|
|
|
704
733
|
const asked = manager.viewers.get("viewer").qualityAsk?.height;
|
|
705
734
|
assert.ok(asked, "the machine cannot keep up: a step down is asked");
|
|
706
735
|
assert.ok(asked <= 360, `not a rung between that the screen cannot show (asked for ${asked}p)`);
|
|
707
736
|
});
|
|
737
|
+
|
|
738
|
+
test("a machine below realtime whose viewer's buffer is filling moves nothing", async (t) => {
|
|
739
|
+
const { manager, session, dirPath } = await managerWithSession();
|
|
740
|
+
t.after(async () => {
|
|
741
|
+
await manager.lifecycle.disposeAll();
|
|
742
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
743
|
+
await rm(dirPath, { recursive: true, force: true });
|
|
744
|
+
});
|
|
745
|
+
// What the cushion already holds covers the shortfall: no chosen window of
|
|
746
|
+
// slowness decides, the viewer's own buffer trend does.
|
|
747
|
+
measureSpeed([...session.runs][0], 0.7);
|
|
748
|
+
fillingReports(manager.viewers.get("viewer"), 80);
|
|
749
|
+
|
|
750
|
+
await manager.quality.noteViewerReported(session.id, "viewer");
|
|
751
|
+
|
|
752
|
+
assert.ok(!(manager.viewers.get("viewer").qualityAsk?.height < 720), "no step down while the buffer grows");
|
|
753
|
+
});
|
package/test/decode-cost.test.js
CHANGED
|
@@ -355,7 +355,7 @@ test("the OFFER drops the rungs the host cannot hold, and the master keeps addre
|
|
|
355
355
|
useSyntheticPlaylist: true,
|
|
356
356
|
playlistText: "#EXTM3U\n",
|
|
357
357
|
segmentCount: 100,
|
|
358
|
-
progress: { state: "running", processedSeconds: 0, startPositionSeconds: 0
|
|
358
|
+
progress: { state: "running", processedSeconds: 0, startPositionSeconds: 0 }
|
|
359
359
|
};
|
|
360
360
|
manager.outputs.set(session.id, session);
|
|
361
361
|
|
package/test/encode-cost.test.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import test from "node:test";
|
|
11
11
|
import assert from "node:assert/strict";
|
|
12
12
|
import { EncodeCost } from "../services/encode/quality/EncodeCost.js";
|
|
13
|
-
import { startRunOn } from "./helpers/encode-run.js";
|
|
13
|
+
import { measureSpeed, startRunOn } from "./helpers/encode-run.js";
|
|
14
14
|
import { outputSpec } from "./helpers/output-spec.js";
|
|
15
15
|
import { runStateOf } from "../services/encode/encode-run-state.js";
|
|
16
16
|
import { qualityStateOf } from "../services/encode/quality/OutputQualityState.js";
|
|
@@ -39,10 +39,8 @@ function costOn(readings = {}) {
|
|
|
39
39
|
runningEncoders: () => readings.runningEncoders?.() ?? 0,
|
|
40
40
|
encodersRunningNow: () => 0,
|
|
41
41
|
torrentCostSecFor: () => 0,
|
|
42
|
-
boundBy: async () => readings.boundBy ?? "cpu",
|
|
43
42
|
runsFor: (session) => [...(session.runs ?? [])],
|
|
44
|
-
stateFor: (session) => runStateOf(session.runs)
|
|
45
|
-
progressFor: (session) => session.progress ?? null
|
|
43
|
+
stateFor: (session) => runStateOf(session.runs)
|
|
46
44
|
});
|
|
47
45
|
return { cost, asked: () => asked, host };
|
|
48
46
|
}
|
|
@@ -64,30 +62,28 @@ function sessionProducing(what = {}) {
|
|
|
64
62
|
}),
|
|
65
63
|
file: { key: what.key ?? "torrent:abc:0", name: "film.mkv" },
|
|
66
64
|
output: { encodeWidth: 0, encodeHeight: 0, outputFps: 25, softwarePreset: null },
|
|
67
|
-
|
|
65
|
+
clock: { at: 1000 },
|
|
68
66
|
runs: new Set()
|
|
69
67
|
};
|
|
70
|
-
startRunOn(session, { from: 0,
|
|
68
|
+
startRunOn(session, { from: 0, clock: session.clock });
|
|
71
69
|
return session;
|
|
72
70
|
}
|
|
73
71
|
|
|
74
72
|
/**
|
|
75
|
-
*
|
|
73
|
+
* The session's run measures its speed, and is learned from as the product
|
|
74
|
+
* learns: once the run has a reading.
|
|
76
75
|
*
|
|
77
76
|
* @param {EncodeCost} cost
|
|
78
77
|
* @param {object} session
|
|
79
|
-
* @param {number} speedX - Seconds of film produced per second of
|
|
78
|
+
* @param {number} speedX - Seconds of film produced per second of the run's own work.
|
|
79
|
+
* @param {number} [waitedMs] - How long the run's input waits for the swarm
|
|
80
|
+
* between two closed pieces.
|
|
80
81
|
* @returns {Promise<void>}
|
|
81
82
|
*/
|
|
82
|
-
async function watchItRun(cost, session, speedX) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
// Backdated rather than waited for: the window a speed may be read over is
|
|
87
|
-
// seconds, and a test that sleeps through it measures the clock.
|
|
88
|
-
qualityStateOf(session).learnSample.takenAt -= 10_000;
|
|
89
|
-
session.progress.processedSeconds = 10 * speedX;
|
|
90
|
-
await cost.learnFrom(session);
|
|
83
|
+
async function watchItRun(cost, session, speedX, waitedMs = 0) {
|
|
84
|
+
const run = [...session.runs][0];
|
|
85
|
+
measureSpeed(run, speedX, session.clock, waitedMs);
|
|
86
|
+
await cost.learnFrom(session, run);
|
|
91
87
|
}
|
|
92
88
|
|
|
93
89
|
test("the host is asked at the moment of the question, not when this was built", () => {
|
|
@@ -163,17 +159,16 @@ test("a copy is priced from what it was seen doing", async () => {
|
|
|
163
159
|
);
|
|
164
160
|
});
|
|
165
161
|
|
|
166
|
-
test("a
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
const { cost } = costOn({ boundBy: "download" });
|
|
162
|
+
test("the time a run's input waited for the swarm is not in the price of this host", async () => {
|
|
163
|
+
// Field 2026-10-01: a copy waited 32.93 s and 43.91 s for two pieces and read
|
|
164
|
+
// 0.21x over the clock. Over its own working time it is the machine's speed.
|
|
165
|
+
const { cost } = costOn();
|
|
171
166
|
const session = sessionProducing({ transcodeVideo: false });
|
|
172
167
|
|
|
173
|
-
await watchItRun(cost, session,
|
|
168
|
+
await watchItRun(cost, session, 8, 9_000);
|
|
174
169
|
|
|
175
|
-
assert.equal(
|
|
176
|
-
assert.equal(
|
|
170
|
+
assert.equal(qualityStateOf(session).lastAloneSpeed, 8, "the speed is the run's own, not the swarm's");
|
|
171
|
+
assert.equal(cost.copyVersionFor("torrent:abc:0"), 1, "and it is filed as what copying costs here");
|
|
177
172
|
});
|
|
178
173
|
|
|
179
174
|
test("a picture's reading taken beside another encoder is not filed", async () => {
|
|
@@ -189,30 +184,27 @@ test("a picture's reading taken beside another encoder is not filed", async () =
|
|
|
189
184
|
assert.equal(cost.copyVersionFor("torrent:abc:0"), 0);
|
|
190
185
|
});
|
|
191
186
|
|
|
192
|
-
test("
|
|
187
|
+
test("a run that has measured nothing teaches nothing", async () => {
|
|
193
188
|
const { cost } = costOn();
|
|
194
189
|
const session = sessionProducing({ transcodeVideo: false });
|
|
195
|
-
|
|
190
|
+
const run = [...session.runs][0];
|
|
196
191
|
|
|
197
|
-
await cost.learnFrom(session);
|
|
192
|
+
await cost.learnFrom(session, run);
|
|
198
193
|
|
|
199
|
-
assert.equal(
|
|
200
|
-
assert.
|
|
194
|
+
assert.equal(run.speedReading, null, "a speed needs two closed pieces of one run");
|
|
195
|
+
assert.equal(cost.copyVersionFor("torrent:abc:0"), 0);
|
|
201
196
|
});
|
|
202
197
|
|
|
203
|
-
test("a
|
|
198
|
+
test("a restarted run starts its own reckoning, so a seek is never read as speed", async () => {
|
|
199
|
+
// Twenty minutes of film against five seconds of work is a seek, and filed as
|
|
200
|
+
// a price it admits every step there is. The reading belongs to the run that
|
|
201
|
+
// made it, so a new run at another place has none until its own pieces close.
|
|
204
202
|
const { cost } = costOn();
|
|
205
203
|
const session = sessionProducing({ transcodeVideo: false });
|
|
206
|
-
await cost.learnFrom(session);
|
|
207
|
-
qualityStateOf(session).learnSample.takenAt -= 10_000;
|
|
208
|
-
// A new run, at another place in the film: twenty minutes of film against five
|
|
209
|
-
// seconds of clock is a seek, and filed as a price it admits every step there
|
|
210
|
-
// is.
|
|
211
204
|
session.runs = new Set();
|
|
212
|
-
startRunOn(session, { from: 300,
|
|
213
|
-
session.progress.processedSeconds = 1200;
|
|
205
|
+
const moved = startRunOn(session, { from: 300, clock: session.clock });
|
|
214
206
|
|
|
215
|
-
await cost.learnFrom(session);
|
|
207
|
+
await cost.learnFrom(session, moved);
|
|
216
208
|
|
|
217
209
|
assert.equal(cost.copyVersionFor("torrent:abc:0"), 0);
|
|
218
210
|
});
|
|
@@ -12,6 +12,7 @@ import test from "node:test";
|
|
|
12
12
|
import assert from "node:assert/strict";
|
|
13
13
|
import { EventEmitter } from "node:events";
|
|
14
14
|
import { EncodeRun } from "../services/encode/EncodeRun.js";
|
|
15
|
+
import { measureSpeed } from "./helpers/encode-run.js";
|
|
15
16
|
import { ENCODE_EXIT } from "../services/encode/encode-exit.js";
|
|
16
17
|
import { SoftwareEncoder } from "../services/encode/SoftwareEncoder.js";
|
|
17
18
|
import { EncodeOrchestrator } from "../services/encode/EncodeOrchestrator.js";
|
|
@@ -33,10 +34,16 @@ const RUN_COSTS = { killCostSec: 0, firstByteWaitSec: 0.12 };
|
|
|
33
34
|
|
|
34
35
|
const PICTURE = "torrent:abc:fmt=fmp4:grid=kf@0:video-only:v=0/copy";
|
|
35
36
|
|
|
37
|
+
/** What every run of these tests reads as now; a measurement advances it. */
|
|
38
|
+
const clock = { at: 1000 };
|
|
39
|
+
|
|
36
40
|
class FakeProcess extends EventEmitter {
|
|
37
41
|
constructor() {
|
|
38
42
|
super();
|
|
39
43
|
this.pid = 1;
|
|
44
|
+
// The progress channel and the channel closed pieces are named on.
|
|
45
|
+
this.stdout = new EventEmitter();
|
|
46
|
+
this.stdio = [null, this.stdout, null, new EventEmitter()];
|
|
40
47
|
this.signals = [];
|
|
41
48
|
}
|
|
42
49
|
|
|
@@ -123,7 +130,7 @@ function orchestrator({ maxRuns = 2 } = {}) {
|
|
|
123
130
|
buildArgs: () => ["-i", "in", "out"],
|
|
124
131
|
spawn: () => process_,
|
|
125
132
|
logger: { info: (line) => lines.push(line), warn: (line) => lines.push(line) },
|
|
126
|
-
now: () =>
|
|
133
|
+
now: () => clock.at,
|
|
127
134
|
onEnded: (ended) => made.noteEnded(ended)
|
|
128
135
|
});
|
|
129
136
|
// Filed under the run itself: a run has no name, so a test that has to
|
|
@@ -146,7 +153,7 @@ function orchestrator({ maxRuns = 2 } = {}) {
|
|
|
146
153
|
buildArgs: () => ["-i", "in", "out"],
|
|
147
154
|
spawn: () => process_,
|
|
148
155
|
logger: { info: (line) => lines.push(line), warn: (line) => lines.push(line) },
|
|
149
|
-
now: () =>
|
|
156
|
+
now: () => clock.at,
|
|
150
157
|
onEnded: (ended) => made.noteEnded(ended)
|
|
151
158
|
});
|
|
152
159
|
processes.set(run, process_);
|
|
@@ -210,7 +217,7 @@ test("an encoder already working covers what it will reach in time", () => {
|
|
|
210
217
|
// wanted at all.
|
|
211
218
|
const { made, buildRun } = orchestrator();
|
|
212
219
|
const first = buildRun({ from: 0, to: -1 });
|
|
213
|
-
first
|
|
220
|
+
measureSpeed(first, 8, clock);
|
|
214
221
|
made.adopt(PICTURE, first);
|
|
215
222
|
made.noteProduced(PICTURE, 0);
|
|
216
223
|
made.noteProduced(PICTURE, 1);
|
|
@@ -240,7 +247,7 @@ test("somebody stopped where no encoder can arrive in time is served, and the sc
|
|
|
240
247
|
// seconds anybody spends looking at a spinner — outranks it.
|
|
241
248
|
const { made, buildRun } = orchestrator();
|
|
242
249
|
const first = buildRun({ from: 0, to: -1 });
|
|
243
|
-
first
|
|
250
|
+
measureSpeed(first, 1, clock);
|
|
244
251
|
made.adopt(PICTURE, first);
|
|
245
252
|
made.noteProduced(PICTURE, 0);
|
|
246
253
|
made.noteProduced(PICTURE, 1);
|
|
@@ -260,7 +267,7 @@ test("two encoders on one output never share a segment number", () => {
|
|
|
260
267
|
// what that neighbour makes sooner.
|
|
261
268
|
const { made, buildRun } = orchestrator();
|
|
262
269
|
const first = buildRun({ from: 0, to: -1 });
|
|
263
|
-
first
|
|
270
|
+
measureSpeed(first, 1, clock);
|
|
264
271
|
made.adopt(PICTURE, first);
|
|
265
272
|
made.noteProduced(PICTURE, 0);
|
|
266
273
|
wants(made, [{ from: 200, to: 230, withinSeconds: 0 }]);
|
|
@@ -294,7 +301,7 @@ test("a second viewer far behind is served, at whatever the score says is soones
|
|
|
294
301
|
wants(made, [{ from: 500, to: 530, withinSeconds: 0 }]);
|
|
295
302
|
made.reconcile();
|
|
296
303
|
for (const run of made.runsOn(PICTURE)) {
|
|
297
|
-
run
|
|
304
|
+
measureSpeed(run, 2, clock);
|
|
298
305
|
}
|
|
299
306
|
wants(made, [
|
|
300
307
|
{ from: 500, to: 530, withinSeconds: 0 },
|
|
@@ -359,7 +366,7 @@ test("a run that meets material made elsewhere is moved past it", () => {
|
|
|
359
366
|
for (let index = 102; index <= 150; index += 1) {
|
|
360
367
|
made.coverageOf(PICTURE).markReady(index);
|
|
361
368
|
}
|
|
362
|
-
first
|
|
369
|
+
measureSpeed(first, 1, clock);
|
|
363
370
|
made.reconcile();
|
|
364
371
|
const runs = made.runsOn(PICTURE);
|
|
365
372
|
assert.equal(runs.length, 1, "one encoder, moved rather than joined by another");
|
|
@@ -407,7 +414,7 @@ test("the swarm limits the encoders, whatever the processor allows", () => {
|
|
|
407
414
|
const { made, lines } = orchestrator({ maxRuns: 2 });
|
|
408
415
|
wants(made, [{ from: 100, to: 130 }]);
|
|
409
416
|
made.reconcile();
|
|
410
|
-
made.runsOn(PICTURE)[0]
|
|
417
|
+
measureSpeed(made.runsOn(PICTURE)[0], 8, clock);
|
|
411
418
|
wants(made, [{ from: 500, to: 530 }]);
|
|
412
419
|
made.reconcile();
|
|
413
420
|
|