@torrent-tv/proxy 2.80.11 → 2.80.13
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 +14 -0
- package/CLAUDE.md +6 -0
- package/docs/download-architecture.md +45 -1
- package/docs/encode-architecture.md +180 -0
- package/package.json +1 -1
- package/routes/stream/get.js +324 -300
- package/routes/transcode/session-file/get.js +22 -2
- package/services/encode/SegmentDemand.js +34 -0
- package/services/hls-session-manager.js +19 -17
- package/services/orchestrators/EncodeOrchestrator.js +735 -726
- package/services/output/LiveOutputs.js +267 -282
- package/services/priority/WaitLedger.js +142 -0
- package/services/torrent-pool.js +38 -1
- package/services/torrent-worker/client.js +27 -12
- package/test/priority-map-per-output.test.js +5 -5
- package/test/quality-variants.test.js +4 -4
- package/test/wait-ledger.test.js +100 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { logger } from "../../../utils/logger.js";
|
|
2
|
+
import { bandOf, waits } from "../../../services/priority/WaitLedger.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* How long a request for a not-yet-produced file is held before answering with
|
|
@@ -99,7 +100,20 @@ export async function serveSessionFile(req, reply, { hlsSessionManager, sessionI
|
|
|
99
100
|
const outcome = clientAborted
|
|
100
101
|
? "client-aborted"
|
|
101
102
|
: result.kind === "ok" ? "served" : result.kind;
|
|
102
|
-
|
|
103
|
+
// AGAINST THE RANK THE MAP GAVE IT. A wait is the only thing that says
|
|
104
|
+
// whether the prioritisation is being used well, and this is the one place
|
|
105
|
+
// in the proxy where a viewer is measurably waiting for a named segment.
|
|
106
|
+
// Recorded even when the segment was served at once: a run of short waits
|
|
107
|
+
// at the top rank is what "the urgent zone is being served first" looks
|
|
108
|
+
// like, and without them the table would hold only the failures.
|
|
109
|
+
const ranked = result.ranked ?? null;
|
|
110
|
+
if (ranked) {
|
|
111
|
+
waits.note(ranked.address, heldMs, ranked.rank, ranked.topRank);
|
|
112
|
+
}
|
|
113
|
+
logger.info(
|
|
114
|
+
`[hold] ${fileName} ${outcome} after ${heldMs}ms` +
|
|
115
|
+
(ranked ? ` (the map wants it ${bandOf(ranked.rank, ranked.topRank)}, rank ${ranked.rank} of ${ranked.topRank})` : "")
|
|
116
|
+
);
|
|
103
117
|
}
|
|
104
118
|
|
|
105
119
|
if (result.kind === "not-found") {
|
|
@@ -180,6 +194,8 @@ export async function waitForSessionFile(hlsSessionManager, sessionId, fileName,
|
|
|
180
194
|
// this wait, and the segment the viewer wanted took 15 ms once it was asked
|
|
181
195
|
// for.
|
|
182
196
|
let seekEpoch = hlsSessionManager.seekEpoch(sessionId);
|
|
197
|
+
/** @type {{ address: string, rank: number, topRank: number } | null} */
|
|
198
|
+
let lastRanked = null;
|
|
183
199
|
while (Date.now() - startedAt < timeoutMs) {
|
|
184
200
|
const result = await hlsSessionManager.getFileStream(sessionId, fileName, {
|
|
185
201
|
requestSeq,
|
|
@@ -202,9 +218,13 @@ export async function waitForSessionFile(hlsSessionManager, sessionId, fileName,
|
|
|
202
218
|
);
|
|
203
219
|
seekEpoch = hlsSessionManager.seekEpoch(sessionId);
|
|
204
220
|
}
|
|
221
|
+
// The rank the map last gave this segment, so a request that runs out of
|
|
222
|
+
// patience is still counted against what it was promised. Kept across the
|
|
223
|
+
// polls because the timeout path has no result of its own.
|
|
224
|
+
lastRanked = result.ranked ?? lastRanked;
|
|
205
225
|
await delay(300);
|
|
206
226
|
}
|
|
207
|
-
return { kind: "warming-up" };
|
|
227
|
+
return { kind: "warming-up", ranked: lastRanked };
|
|
208
228
|
}
|
|
209
229
|
|
|
210
230
|
/**
|
|
@@ -58,4 +58,38 @@ export class SegmentDemand {
|
|
|
58
58
|
mapOn(address) {
|
|
59
59
|
return this.#maps.get(address) ?? [];
|
|
60
60
|
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* What the map says about one segment, and about the map it sits in.
|
|
64
|
+
*
|
|
65
|
+
* Asked by whoever MEASURES how well the map is being served: a wait means
|
|
66
|
+
* one thing at the top rank and another at the bottom, and telling them apart
|
|
67
|
+
* needs both the segment's own rank and the highest rank stated — a rank is a
|
|
68
|
+
* position in this map, not an absolute number.
|
|
69
|
+
*
|
|
70
|
+
* AND AN EMPTY MAP IS NOT AN ANSWER, which is why the top rank comes back even
|
|
71
|
+
* when the segment's own is zero. A top rank of zero says the map has not been
|
|
72
|
+
* built yet — a session created a moment ago, before the first pass — and that
|
|
73
|
+
* is a different thing from "nobody is coming". Conflated, every request
|
|
74
|
+
* behind the run reads as absent for as long as a fresh session has no map.
|
|
75
|
+
*
|
|
76
|
+
* @param {string} address
|
|
77
|
+
* @param {number} index
|
|
78
|
+
* @returns {{ rank: number, topRank: number }} Rank zero for a number in
|
|
79
|
+
* nobody's zone, which is a statement: nothing is coming for it.
|
|
80
|
+
*/
|
|
81
|
+
rankOf(address, index) {
|
|
82
|
+
const zones = this.mapOn(address);
|
|
83
|
+
let rank = 0;
|
|
84
|
+
let topRank = 0;
|
|
85
|
+
for (const zone of zones) {
|
|
86
|
+
if (zone.priority > topRank) {
|
|
87
|
+
topRank = zone.priority;
|
|
88
|
+
}
|
|
89
|
+
if (index >= zone.from && index <= zone.to && zone.priority > rank) {
|
|
90
|
+
rank = zone.priority;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { rank, topRank };
|
|
94
|
+
}
|
|
61
95
|
}
|
|
@@ -1563,7 +1563,11 @@ export class HlsSessionManager {
|
|
|
1563
1563
|
).catch(() => {});
|
|
1564
1564
|
},
|
|
1565
1565
|
viewersOf: (session) => viewersOf(session),
|
|
1566
|
-
|
|
1566
|
+
// WHERE THE TWO FACTS MEET, and this is the only place that holds both.
|
|
1567
|
+
// Which step is on somebody's screen belongs to the person; which output
|
|
1568
|
+
// a step supersedes belongs to the film's shape. Neither layer is handed
|
|
1569
|
+
// the other — one gets a plain id, the other is read for one field.
|
|
1570
|
+
watchedBy: (session, viewer) => !this.liveOutputs.supersededBy(session, viewer.activeVariantId ?? null),
|
|
1567
1571
|
allowanceFor: (session) => minimumBufferFrom({
|
|
1568
1572
|
segmentSeconds: this.segmentDurationSec,
|
|
1569
1573
|
worstSupplyWaitSec: session.supplyFigures?.worstWaitSec
|
|
@@ -9644,6 +9648,8 @@ export class HlsSessionManager {
|
|
|
9644
9648
|
|
|
9645
9649
|
|
|
9646
9650
|
#holdForProduction(session, fileName, isPlaylist, options) {
|
|
9651
|
+
/** @type {{ address: string, rank: number, topRank: number } | null} */
|
|
9652
|
+
let ranked = null;
|
|
9647
9653
|
if (!isPlaylist) {
|
|
9648
9654
|
this.#explainHold(session, fileName, "the file is not on disk");
|
|
9649
9655
|
}
|
|
@@ -9663,20 +9669,16 @@ export class HlsSessionManager {
|
|
|
9663
9669
|
// failed did the player move to the segment it actually needed — 63 s of
|
|
9664
9670
|
// spinner after a track that had been made ready in 7.
|
|
9665
9671
|
//
|
|
9666
|
-
// WHETHER ANYBODY IS COMING FOR IT, asked of the
|
|
9667
|
-
//
|
|
9668
|
-
//
|
|
9669
|
-
//
|
|
9670
|
-
//
|
|
9671
|
-
//
|
|
9672
|
-
|
|
9673
|
-
|
|
9674
|
-
|
|
9675
|
-
|
|
9676
|
-
// request as absent for as long as a fresh session has no map.
|
|
9677
|
-
const zones = this.encodeOrchestrator.demand.mapOn(session.outputKey ?? "");
|
|
9678
|
-
const nobodyIsComing = zones.length > 0 &&
|
|
9679
|
-
!zones.some((zone) => requestedIndex >= zone.from && requestedIndex <= zone.to);
|
|
9672
|
+
// WHETHER ANYBODY IS COMING FOR IT, asked of the map (`SegmentDemand.rankOf`,
|
|
9673
|
+
// which also tells "in nobody's zone" from "no map yet"). The same walk
|
|
9674
|
+
// was written out here and could answer only yes or no, so the RANK was
|
|
9675
|
+
// discarded at the one point where a viewer measurably waits for a named
|
|
9676
|
+
// segment; it goes back out with the answer, because the wait is measured
|
|
9677
|
+
// by whoever holds the request.
|
|
9678
|
+
const address = session.outputKey ?? "";
|
|
9679
|
+
const { rank, topRank } = this.encodeOrchestrator.demand.rankOf(address, requestedIndex);
|
|
9680
|
+
ranked = { address, rank, topRank };
|
|
9681
|
+
const nobodyIsComing = topRank > 0 && rank === 0;
|
|
9680
9682
|
if (
|
|
9681
9683
|
Number.isFinite(requestedIndex) &&
|
|
9682
9684
|
requestedIndex < (earliestRunStart(session) ?? 0) &&
|
|
@@ -9687,7 +9689,7 @@ export class HlsSessionManager {
|
|
|
9687
9689
|
`transcode ${session.id} segment #${requestedIndex} is ${(earliestRunStart(session) ?? 0) - requestedIndex} ` +
|
|
9688
9690
|
"segments behind the run and in nobody's zone; answered as absent rather than held"
|
|
9689
9691
|
);
|
|
9690
|
-
return { kind: "not-found" };
|
|
9692
|
+
return { kind: "not-found", ranked };
|
|
9691
9693
|
}
|
|
9692
9694
|
this.#ensureEncodingFor(
|
|
9693
9695
|
session,
|
|
@@ -9695,7 +9697,7 @@ export class HlsSessionManager {
|
|
|
9695
9697
|
Number.isFinite(options?.requestSeq) ? options.requestSeq : Number.MAX_SAFE_INTEGER
|
|
9696
9698
|
);
|
|
9697
9699
|
}
|
|
9698
|
-
return { kind: "warming-up" };
|
|
9700
|
+
return { kind: "warming-up", ranked };
|
|
9699
9701
|
}
|
|
9700
9702
|
|
|
9701
9703
|
/**
|