@torrent-tv/proxy 2.76.4 → 2.76.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/bin/cli.js +14 -2
- package/biome.json +32 -0
- package/package.json +1 -1
- package/server.js +5 -1
- package/services/encode/encode-run-state.js +67 -0
- package/services/hls-session-manager.js +77 -539
- package/services/quality/EncodeCost.js +500 -0
- package/services/torrent-worker/client.js +13 -0
- package/services/torrent-worker/pool-adapter.js +384 -370
- package/services/torrent-worker/protocol.js +2 -0
- package/services/torrent-worker/worker.js +23 -0
- package/services/tunnel-client.js +18 -2
- package/test/encode-cost.test.js +94 -0
|
@@ -57,6 +57,8 @@ export const Command = {
|
|
|
57
57
|
FILE_STATS: "file-stats",
|
|
58
58
|
/** Bytes every torrent here has moved, for pricing the torrent's own cost. */
|
|
59
59
|
TORRENT_TOTALS: "torrent-totals",
|
|
60
|
+
/** Which films this proxy holds right now, for content affinity. */
|
|
61
|
+
HELD_TORRENTS: "held-torrents",
|
|
60
62
|
/** Reorder piece selection around a read position (seek prioritisation). */
|
|
61
63
|
PRIORITIZE: "prioritize",
|
|
62
64
|
/** Read a byte range; the body arrives as CHUNK messages. */
|
|
@@ -375,6 +375,29 @@ async function runCommand(command, params, id) {
|
|
|
375
375
|
return released;
|
|
376
376
|
}
|
|
377
377
|
|
|
378
|
+
case Command.HELD_TORRENTS: {
|
|
379
|
+
// Which films this proxy has right now, and how much of each. Answered
|
|
380
|
+
// from the live client rather than from the main thread's map of
|
|
381
|
+
// stand-ins, which is only cleared on shutdown and would name films this
|
|
382
|
+
// proxy let go of hours ago.
|
|
383
|
+
const held = [];
|
|
384
|
+
for (const torrent of pool.client?.torrents ?? []) {
|
|
385
|
+
const infoHash = String(torrent?.infoHash ?? "");
|
|
386
|
+
if (!infoHash) {
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
held.push({
|
|
390
|
+
infoHash,
|
|
391
|
+
// A viewer sent here for a film nobody has downloaded any of gains
|
|
392
|
+
// nothing, so the share is reported and the decision is made where
|
|
393
|
+
// the viewer is.
|
|
394
|
+
progress: Number.isFinite(torrent?.progress) ? torrent.progress : 0,
|
|
395
|
+
bytes: Number.isFinite(torrent?.downloaded) ? torrent.downloaded : 0
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
return { held };
|
|
399
|
+
}
|
|
400
|
+
|
|
378
401
|
case Command.TORRENT_TOTALS: {
|
|
379
402
|
// Downloaded and uploaded are counted apart: hashing every downloaded
|
|
380
403
|
// byte is work of a different order from sending one back to the swarm,
|
|
@@ -250,8 +250,24 @@ export function createTunnelClient({
|
|
|
250
250
|
|
|
251
251
|
// Health check: server requests current metrics for proxy scoring.
|
|
252
252
|
if (message.type === "health-request") {
|
|
253
|
-
|
|
254
|
-
|
|
253
|
+
// Awaited: the answer now carries which films this proxy holds, and the
|
|
254
|
+
// truthful list of those lives on the torrent thread.
|
|
255
|
+
void (async () => {
|
|
256
|
+
let answer = {};
|
|
257
|
+
try {
|
|
258
|
+
answer = typeof onHealthRequest === "function" ? await onHealthRequest() : {};
|
|
259
|
+
} catch {
|
|
260
|
+
// silent-ok: a proxy that cannot describe itself is scored on
|
|
261
|
+
// nothing rather than not answered at all, which would drop it out
|
|
262
|
+
// of every selection until the next poll.
|
|
263
|
+
}
|
|
264
|
+
send({
|
|
265
|
+
type: "health-response",
|
|
266
|
+
requestId: message.requestId,
|
|
267
|
+
metrics: answer?.metrics ?? answer ?? {},
|
|
268
|
+
holds: Array.isArray(answer?.holds) ? answer.holds : []
|
|
269
|
+
});
|
|
270
|
+
})();
|
|
255
271
|
return;
|
|
256
272
|
}
|
|
257
273
|
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file What encoding costs this machine, asked of the object that owns it.
|
|
3
|
+
*
|
|
4
|
+
* The arithmetic itself is exercised end to end by `auto-quality-step` and
|
|
5
|
+
* `quality-variants`, which go through the session manager. What is pinned here
|
|
6
|
+
* is the seam the move created: this object is given the host's readings as a
|
|
7
|
+
* QUESTION rather than a copy, and it holds what an encoder taught it.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import test from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import { EncodeCost } from "../services/quality/EncodeCost.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {object} [readings]
|
|
16
|
+
* @returns {{ cost: EncodeCost, asked: () => number, host: { share: number } }}
|
|
17
|
+
*/
|
|
18
|
+
function costOn(readings = {}) {
|
|
19
|
+
let asked = 0;
|
|
20
|
+
const host = { share: 1 };
|
|
21
|
+
const cost = new EncodeCost({
|
|
22
|
+
liveOutputs: { familyOf: () => [], variantHeightOf: () => 0 },
|
|
23
|
+
host: () => {
|
|
24
|
+
asked += 1;
|
|
25
|
+
return {
|
|
26
|
+
benchmark: readings.benchmark ?? null,
|
|
27
|
+
decodeModel: null,
|
|
28
|
+
contentionPenalties: null,
|
|
29
|
+
availability: { known: true, share: host.share }
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
audioCostKey: () => "audio",
|
|
33
|
+
runningEncoders: () => 0,
|
|
34
|
+
encodersRunningNow: () => 0,
|
|
35
|
+
torrentCostSecFor: () => 0
|
|
36
|
+
});
|
|
37
|
+
return { cost, asked: () => asked, host };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
test("the host is asked at the moment of the question, not when this was built", () => {
|
|
41
|
+
// The share of the machine that is free is re-read every few seconds. Copied
|
|
42
|
+
// into this object when it was made, every later rung would be priced against
|
|
43
|
+
// a machine that has gone.
|
|
44
|
+
const { cost, asked, host } = costOn();
|
|
45
|
+
assert.equal(asked(), 0, "nothing is read until something is asked");
|
|
46
|
+
|
|
47
|
+
cost.sustainableHeights({ heights: [1080], sourceWidth: 1920, sourceHeight: 1080, fps: 24, source: null, transcodeVideo: true, ownHeight: 0 });
|
|
48
|
+
const first = asked();
|
|
49
|
+
assert.ok(first > 0);
|
|
50
|
+
|
|
51
|
+
host.share = 0.1;
|
|
52
|
+
cost.sustainableHeights({ heights: [1080], sourceWidth: 1920, sourceHeight: 1080, fps: 24, source: null, transcodeVideo: true, ownHeight: 0 });
|
|
53
|
+
assert.ok(asked() > first, "and read again on the next question");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("with no benchmark to judge by, every height offered is kept", () => {
|
|
57
|
+
// Nothing measured is not the same as nothing possible. Refusing here would
|
|
58
|
+
// hide the whole ladder on a host whose startup measurement failed.
|
|
59
|
+
const { cost } = costOn({ benchmark: null });
|
|
60
|
+
const kept = cost.sustainableHeights({
|
|
61
|
+
heights: [1080, 720, 480],
|
|
62
|
+
sourceWidth: 1920,
|
|
63
|
+
sourceHeight: 1080,
|
|
64
|
+
fps: 24,
|
|
65
|
+
source: null,
|
|
66
|
+
transcodeVideo: true,
|
|
67
|
+
ownHeight: 0
|
|
68
|
+
});
|
|
69
|
+
assert.deepEqual(kept, [1080, 720, 480]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("a rung measured below realtime is withdrawn, and a copied source height is not", () => {
|
|
73
|
+
const { cost } = costOn({ benchmark: null });
|
|
74
|
+
const kept = cost.sustainableHeights({
|
|
75
|
+
heights: [1080, 480],
|
|
76
|
+
sourceWidth: 1920,
|
|
77
|
+
sourceHeight: 1080,
|
|
78
|
+
fps: 24,
|
|
79
|
+
source: null,
|
|
80
|
+
// The base copies its picture, so the source height costs no encoder.
|
|
81
|
+
transcodeVideo: false,
|
|
82
|
+
ownHeight: 1080,
|
|
83
|
+
measuredHeights: new Map([[480, 0.4]])
|
|
84
|
+
});
|
|
85
|
+
assert.deepEqual(kept, [1080], "the copy stays; the rung seen failing does not");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("what an encoder taught this host is held here, and nowhere else", () => {
|
|
89
|
+
const { cost } = costOn();
|
|
90
|
+
cost.copyCost.set("torrent:abc:0", { costSec: 0.125, readings: [8], version: 1 });
|
|
91
|
+
assert.equal(cost.copyCost.get("torrent:abc:0").costSec, 0.125);
|
|
92
|
+
assert.equal(cost.audioCost.size, 0);
|
|
93
|
+
assert.equal(cost.decodeCost.size, 0);
|
|
94
|
+
});
|