@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
|
@@ -24,21 +24,6 @@
|
|
|
24
24
|
|
|
25
25
|
import { AUDIO_TRANSCODE_KBPS, maxrateKbpsFor, nominalKbpsFor } from "../args.js";
|
|
26
26
|
|
|
27
|
-
/**
|
|
28
|
-
* How much of a measured link may be spent on the stream.
|
|
29
|
-
*
|
|
30
|
-
* The rest is what the link does when it is not being perfect: transport
|
|
31
|
-
* framing, retransmission, the other tabs, the moment somebody else in the
|
|
32
|
-
* house starts something. A load sized to the whole reading stalls on the
|
|
33
|
-
* first of those.
|
|
34
|
-
*
|
|
35
|
-
* Chosen, not measured, and written as a constant rather than dressed up as a
|
|
36
|
-
* measurement. What would replace it is a reading of the transport's own
|
|
37
|
-
* overhead and of how far a link's throughput varies over a session, which
|
|
38
|
-
* nothing takes.
|
|
39
|
-
*/
|
|
40
|
-
export const LINK_SAFETY = 0.8;
|
|
41
|
-
|
|
42
27
|
/** How far a figure in a load can be trusted. */
|
|
43
28
|
export const PEAK_CLASS = Object.freeze({
|
|
44
29
|
KNOWN: "known",
|
|
@@ -279,7 +264,11 @@ export function linkCouldCarry(linkMbps, load) {
|
|
|
279
264
|
if (load.peakClass === PEAK_CLASS.UNKNOWN) {
|
|
280
265
|
return answer(LINK_VERDICT.NO_SAFE_BOUND, false, false);
|
|
281
266
|
}
|
|
282
|
-
|
|
267
|
+
// The measured link against the whole load, with no share held back. The
|
|
268
|
+
// reading is what the link carried while it carried a piece, and the stream
|
|
269
|
+
// needs that on average; what a link does when it is not perfect — a pause,
|
|
270
|
+
// another tab — is what the viewer's buffer and `drain-threat.js` measure.
|
|
271
|
+
if (linkMbps < load.totalMbps) {
|
|
283
272
|
return answer(LINK_VERDICT.DOES_NOT_FIT, false, false);
|
|
284
273
|
}
|
|
285
274
|
return load.peakClass === PEAK_CLASS.KNOWN
|
|
@@ -338,7 +327,6 @@ export function linkAnswerFigures(answer) {
|
|
|
338
327
|
return {
|
|
339
328
|
verdict: answer.verdict,
|
|
340
329
|
linkMbps: answer.linkMbps,
|
|
341
|
-
linkSafety: LINK_SAFETY,
|
|
342
330
|
videoMbps: answer.load.video?.mbps ?? null,
|
|
343
331
|
videoClass: answer.load.video?.peakClass ?? null,
|
|
344
332
|
audioMbps: answer.load.audio?.mbps ?? null,
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
* sized in seconds of playback by whoever knows the file's byte rate.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
/** The query parameter naming which run of an output a read feeds. */
|
|
15
|
+
const RUN_PARAMETER = "run";
|
|
16
|
+
|
|
14
17
|
/**
|
|
15
18
|
* @param {object} params
|
|
16
19
|
* @param {{ fileIndex: number, streamUrl: (baseUrl: string, options?: { sessionId?: string }) => URL }} params.picture
|
|
@@ -20,15 +23,23 @@
|
|
|
20
23
|
* @param {"video-only" | "audio-only" | "muxed" | "empty"} params.carries
|
|
21
24
|
* @param {boolean} params.audioSeparate - The picture's sound travels as its own output.
|
|
22
25
|
* @param {string} params.sessionId - Which output this read feeds.
|
|
26
|
+
* @param {number} [params.runToken] - Which run of that output the read feeds,
|
|
27
|
+
* so the time its input waits is charged to that run and not to another one
|
|
28
|
+
* working on the same output.
|
|
23
29
|
* @param {number} params.readWindowBytes - Zero when no window was measured.
|
|
24
30
|
* @param {string} params.baseUrl
|
|
25
31
|
* @returns {{ inputFile: object, audioFile: object, inputUrl: string, audioInputUrl: string }}
|
|
26
32
|
*/
|
|
27
|
-
export function encoderInputs({ picture, soundtrack, carries, audioSeparate, sessionId, readWindowBytes, baseUrl }) {
|
|
33
|
+
export function encoderInputs({ picture, soundtrack, carries, audioSeparate, sessionId, runToken, readWindowBytes, baseUrl }) {
|
|
28
34
|
// A soundtrack shipped as its own file is encoded FROM that file, so a
|
|
29
35
|
// rendition of it reads nothing else.
|
|
30
36
|
const inputFile = carries === "audio-only" && soundtrack !== picture ? soundtrack : picture;
|
|
31
37
|
const input = inputFile.streamUrl(baseUrl, { sessionId });
|
|
38
|
+
const forRun = (url) => {
|
|
39
|
+
if (Number.isInteger(runToken)) url.searchParams.set(RUN_PARAMETER, String(runToken));
|
|
40
|
+
return url;
|
|
41
|
+
};
|
|
42
|
+
forRun(input);
|
|
32
43
|
if (Number.isFinite(readWindowBytes) && readWindowBytes > 0) {
|
|
33
44
|
input.searchParams.set("windowBytes", String(readWindowBytes));
|
|
34
45
|
input.searchParams.set("reader", "playback");
|
|
@@ -36,7 +47,7 @@ export function encoderInputs({ picture, soundtrack, carries, audioSeparate, ses
|
|
|
36
47
|
// The second input exists only for a picture that carries its sound inside
|
|
37
48
|
// itself when that sound is in another file.
|
|
38
49
|
const audioInputUrl = carries !== "audio-only" && !audioSeparate && soundtrack !== inputFile
|
|
39
|
-
? soundtrack.streamUrl(baseUrl, { sessionId }).toString()
|
|
50
|
+
? forRun(soundtrack.streamUrl(baseUrl, { sessionId })).toString()
|
|
40
51
|
: "";
|
|
41
52
|
return { inputFile, audioFile: soundtrack, inputUrl: input.toString(), audioInputUrl };
|
|
42
53
|
}
|
|
@@ -607,7 +607,9 @@ export class SegmentServing {
|
|
|
607
607
|
: this.#host.runStartTimeFor(session, this.#host.encodeRuns.earliestStartOf(session) ?? 0);
|
|
608
608
|
const position = Number(progress?.processedSeconds);
|
|
609
609
|
const produced = Number.isFinite(position) ? position - runStartSeconds : null;
|
|
610
|
-
const
|
|
610
|
+
const measured = this.#host.encodeRuns.liveRunsOf(session)
|
|
611
|
+
.reduce((best, run) => Math.max(best, run.speedX || 0), 0);
|
|
612
|
+
const speed = measured > 0 ? `${measured.toFixed(2)}x` : "n/a";
|
|
611
613
|
logger.warn(
|
|
612
614
|
`transcode ${session.id} holding ${fileName}: ${reason} ` +
|
|
613
615
|
`(runs from #${this.#host.encodeRuns.earliestStartOf(session) ?? "?"}, viewer at #${this.#host.outputTimes.segmentIndexForTime(session, this.#host.viewerSecondsOn(session))}, ` +
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { logger } from "../../utils/logger.js";
|
|
11
|
-
import { isOutputName
|
|
11
|
+
import { isOutputName } from "../encode/output/index.js";
|
|
12
12
|
|
|
13
13
|
function isWarmupTimeoutError(error) {
|
|
14
14
|
if (!(error instanceof Error)) {
|
|
@@ -484,7 +484,6 @@ export class ViewerRequests {
|
|
|
484
484
|
// segment (the only thing it waits for before playback starts) instead
|
|
485
485
|
// of a percentage of the whole-file transcode.
|
|
486
486
|
segmentDurationSec: this.#host.segmentDurationSec,
|
|
487
|
-
speed: progress.speed,
|
|
488
487
|
outputMbps,
|
|
489
488
|
playbackReadiness,
|
|
490
489
|
// The height the viewer is WATCHING right now, which is what the menu
|
|
@@ -566,11 +565,7 @@ export class ViewerRequests {
|
|
|
566
565
|
const timeline = output.timeline;
|
|
567
566
|
const trackName = output === session ? "video" : "audio";
|
|
568
567
|
const nativeRanges = Array.from({ length: timeline?.segmentCount ?? 0 }, (_, index) =>
|
|
569
|
-
this.#host.
|
|
570
|
-
startSeconds: timeline.publishedStartOf(index),
|
|
571
|
-
endSeconds: index < timeline.segmentCount - 1 ?
|
|
572
|
-
timeline.publishedStartOf(index + 1) - SEGMENT_CUT_TIME_DELTA_SECONDS : undefined
|
|
573
|
-
}));
|
|
568
|
+
this.#host.encodeRuns.finishedMediaRangesOf(output, index));
|
|
574
569
|
// The page states the offset its player applied to this track, and the
|
|
575
570
|
// init it applied it to is the session's. Until both are known, the
|
|
576
571
|
// pieces stay on their own presentation timeline: there is nothing yet
|
|
@@ -247,7 +247,10 @@ export function wireOutputs({
|
|
|
247
247
|
pathFor: (address) => parts.segmentStore.pathFor(address),
|
|
248
248
|
initOf: (address) => parts.segmentStore.initOf(address),
|
|
249
249
|
directoryFor: (address) => parts.segmentStore.directoryFor(address),
|
|
250
|
-
publish: (address, makingName, format,
|
|
250
|
+
publish: (address, makingName, format, read) => parts.segmentStore.publish(address, makingName, format, read),
|
|
251
|
+
closedBytesOf: (address, makingName) => parts.segmentStore.closedBytesOf(address, makingName),
|
|
252
|
+
mediaRangesOf: (address, index, where) => parts.segmentStore.mediaRangesOf(address, index, where),
|
|
253
|
+
remove: (address, index, because) => parts.segmentStore.remove(address, index, because)
|
|
251
254
|
};
|
|
252
255
|
const segmentOutputFiles = {
|
|
253
256
|
addresses: () => parts.segmentStore.addresses(),
|
|
@@ -257,7 +260,8 @@ export function wireOutputs({
|
|
|
257
260
|
useFormat: (address, format) => parts.segmentStore.useFormat(address, format)
|
|
258
261
|
};
|
|
259
262
|
const segmentPaths = {
|
|
260
|
-
pathOf: (address, index) => parts.segmentStore.pathOf(address, index)
|
|
263
|
+
pathOf: (address, index) => parts.segmentStore.pathOf(address, index),
|
|
264
|
+
sizesOf: (address) => parts.segmentStore.sizesOf(address)
|
|
261
265
|
};
|
|
262
266
|
/**
|
|
263
267
|
* Whether everything this output will ever serve is made: every segment of
|
|
@@ -303,6 +307,11 @@ export function wireOutputs({
|
|
|
303
307
|
// The encoders of this proxy: built where the plan places them, followed while they run, accounted when they end.
|
|
304
308
|
parts.encodeRuns = new EncodeRuns({
|
|
305
309
|
logger,
|
|
310
|
+
// What this host is doing with an encode is learned each time a run has
|
|
311
|
+
// measured its processing speed, on every host.
|
|
312
|
+
noteRunSpeedMeasured: (session, run) => {
|
|
313
|
+
void parts.encodeCost.learnFrom(session, run);
|
|
314
|
+
},
|
|
306
315
|
// What an admitted encode was seen to do, filed under its configuration.
|
|
307
316
|
observeEncodeEnded: (session) => {
|
|
308
317
|
if (!parts.localObservations || !session?.spec?.video?.encode) {
|
|
@@ -317,7 +326,6 @@ export function wireOutputs({
|
|
|
317
326
|
});
|
|
318
327
|
},
|
|
319
328
|
viewerSecondsOn: (output, consumerId, now) => viewerSecondsOn(parts.viewers, output, consumerId, now),
|
|
320
|
-
noteRunStarting: (output) => parts.quality.noteRunStarting(output),
|
|
321
329
|
inputOf: (...args) => parts.renditions.inputOf(...args),
|
|
322
330
|
producedNumbers: (...args) => parts.serving.producedNumbers(...args),
|
|
323
331
|
servesAudioSeparately: (...args) => parts.renditions.servesAudioSeparately(...args),
|
|
@@ -809,11 +817,8 @@ export function wireOutputs({
|
|
|
809
817
|
runningEncoders: () => parts.encodeOrchestrator.runningCount(),
|
|
810
818
|
encodersRunningNow: () => parts.encodeOrchestrator.runningCount(),
|
|
811
819
|
torrentCostSecFor: (session) => parts.hostLoad.torrentCostSecFor(session),
|
|
812
|
-
boundBy: (session) => parts.quality.classifyTranscodeBound(session),
|
|
813
820
|
runsFor: (session) => parts.encodeRuns.runsOf(session),
|
|
814
|
-
stateFor: (session) => parts.encodeRuns.runStateOf(session)
|
|
815
|
-
// The run's OWN progress: a speed is a pair of readings of one process.
|
|
816
|
-
progressFor: (_session, run) => run?.progress?.snapshot?.() ?? null
|
|
821
|
+
stateFor: (session) => parts.encodeRuns.runStateOf(session)
|
|
817
822
|
});
|
|
818
823
|
// WHICH HEIGHTS ARE ON THE MENU, which is the arithmetic above plus three
|
|
819
824
|
// things that are nothing to do with it: whose answer it is, what may never
|