@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
|
@@ -1661,7 +1661,7 @@ export class Renditions {
|
|
|
1661
1661
|
) ?? null;
|
|
1662
1662
|
}
|
|
1663
1663
|
|
|
1664
|
-
inputOf(session) {
|
|
1664
|
+
inputOf(session, runToken) {
|
|
1665
1665
|
return encoderInputs({
|
|
1666
1666
|
picture: session.file,
|
|
1667
1667
|
soundtrack: session.spec.audio
|
|
@@ -1670,6 +1670,7 @@ export class Renditions {
|
|
|
1670
1670
|
carries: session.spec.carries,
|
|
1671
1671
|
audioSeparate: this.servesAudioSeparately(session),
|
|
1672
1672
|
sessionId: session.id,
|
|
1673
|
+
runToken,
|
|
1673
1674
|
readWindowBytes: this.#host.encodeRuns.readWindowFor(session),
|
|
1674
1675
|
baseUrl: this.#host.localBaseUrl
|
|
1675
1676
|
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Where one encoder run's time went: working, waiting for its input, or
|
|
3
|
+
* stopped.
|
|
4
|
+
*
|
|
5
|
+
* A speed read as "film made per second of clock" measures the swarm as well
|
|
6
|
+
* as the machine whenever the input runs dry: field 2026-10-01, a copy waited
|
|
7
|
+
* 32.93 s and 43.91 s for two pieces and its speed read 0.21x, which the
|
|
8
|
+
* forecast then carried over the whole remaining film while it charged the
|
|
9
|
+
* same download separately. So the clock is split, from two facts that are
|
|
10
|
+
* observed and not inferred:
|
|
11
|
+
*
|
|
12
|
+
* 1. the input read waited — the stream route says when it starts and stops
|
|
13
|
+
* waiting for bytes of this run's input;
|
|
14
|
+
* 2. the process was stopped — the run says when it sends `SIGSTOP` and
|
|
15
|
+
* `SIGCONT`.
|
|
16
|
+
*
|
|
17
|
+
* Time during which either holds is idle; the rest is the run's own work. Two
|
|
18
|
+
* inputs that wait at once, or a wait while the process is stopped, are idle
|
|
19
|
+
* once. What is not separated is what the encoder does with bytes it already
|
|
20
|
+
* holds while its read waits; that is bounded by its input buffer, which is
|
|
21
|
+
* the one error this split has.
|
|
22
|
+
*/
|
|
23
|
+
export class RunClock {
|
|
24
|
+
/** @type {() => number} */
|
|
25
|
+
#now;
|
|
26
|
+
#startedAt;
|
|
27
|
+
/** Idle causes in force now: input reads waiting, plus one while stopped. */
|
|
28
|
+
#idleCauses = 0;
|
|
29
|
+
#idleSince = 0;
|
|
30
|
+
#idleMs = 0;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {{ now?: () => number }} [options]
|
|
34
|
+
*/
|
|
35
|
+
constructor({ now = Date.now } = {}) {
|
|
36
|
+
this.#now = now;
|
|
37
|
+
this.#startedAt = now();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#idleBegins() {
|
|
41
|
+
if (this.#idleCauses === 0) {
|
|
42
|
+
this.#idleSince = this.#now();
|
|
43
|
+
}
|
|
44
|
+
this.#idleCauses += 1;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#idleEnds() {
|
|
48
|
+
if (this.#idleCauses === 0) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
this.#idleCauses -= 1;
|
|
52
|
+
if (this.#idleCauses === 0) {
|
|
53
|
+
this.#idleMs += this.#now() - this.#idleSince;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** An input read of this run starts waiting for bytes. */
|
|
58
|
+
inputWaitBegins() {
|
|
59
|
+
this.#idleBegins();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** An input read of this run has its bytes. */
|
|
63
|
+
inputWaitEnds() {
|
|
64
|
+
this.#idleEnds();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The process is stopped. */
|
|
68
|
+
stopped() {
|
|
69
|
+
this.#idleBegins();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The process runs again. */
|
|
73
|
+
continued() {
|
|
74
|
+
this.#idleEnds();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Milliseconds of the run's own work so far.
|
|
79
|
+
*
|
|
80
|
+
* @param {number} [at]
|
|
81
|
+
* @returns {number}
|
|
82
|
+
*/
|
|
83
|
+
workingMs(at = this.#now()) {
|
|
84
|
+
const idleNow = this.#idleCauses > 0 ? at - this.#idleSince : 0;
|
|
85
|
+
return Math.max(0, at - this.#startedAt - this.#idleMs - idleNow);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -28,23 +28,19 @@ export class RunProgress {
|
|
|
28
28
|
this.totalSeconds = Number.isFinite(totalSeconds) && totalSeconds > 0 ? totalSeconds : null;
|
|
29
29
|
this.percent = null;
|
|
30
30
|
this.remainingSeconds = this.totalSeconds;
|
|
31
|
-
this.speed = "";
|
|
32
31
|
this.updatedAt = now();
|
|
33
32
|
this.lastLoggedAt = 0;
|
|
34
33
|
this.now = now;
|
|
35
34
|
this.#derive();
|
|
36
35
|
}
|
|
37
36
|
|
|
38
|
-
note({ processedSeconds = null, outTime
|
|
37
|
+
note({ processedSeconds = null, outTime }) {
|
|
39
38
|
const relative = Number.isFinite(processedSeconds)
|
|
40
39
|
? processedSeconds
|
|
41
40
|
: parseTimestamp(outTime);
|
|
42
41
|
if (relative !== null) {
|
|
43
42
|
this.processedSeconds = this.startPositionSeconds + Math.max(0, relative);
|
|
44
43
|
}
|
|
45
|
-
if (typeof speed === "string") {
|
|
46
|
-
this.speed = speed;
|
|
47
|
-
}
|
|
48
44
|
this.updatedAt = this.now();
|
|
49
45
|
this.#derive();
|
|
50
46
|
return this.snapshot();
|
|
@@ -65,7 +61,6 @@ export class RunProgress {
|
|
|
65
61
|
totalSeconds: this.totalSeconds,
|
|
66
62
|
percent: this.percent,
|
|
67
63
|
remainingSeconds: this.remainingSeconds,
|
|
68
|
-
speed: this.speed,
|
|
69
64
|
updatedAt: this.updatedAt
|
|
70
65
|
};
|
|
71
66
|
}
|
|
@@ -1,45 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file Turning two
|
|
2
|
+
* @file Turning two work samples of a running encoder into a speed.
|
|
3
3
|
*
|
|
4
|
-
* ffmpeg reports `speed=` cumulatively, over the whole run
|
|
5
|
-
* every second
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* ffmpeg reports `speed=` cumulatively, over the whole run: every second the
|
|
5
|
+
* encoder was stopped and every second its input waited for the swarm is in
|
|
6
|
+
* the denominator. A copy at eight times realtime that waits a minute reads
|
|
7
|
+
* 1.6x; a copy that waited 32.93 s and 43.91 s for two pieces read 0.21x in the
|
|
8
|
+
* field on 2026-10-01, and the forecast carried that over the whole film while
|
|
9
|
+
* charging the same download separately.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* A run's work sample states the film made and the milliseconds of the run's
|
|
12
|
+
* OWN work, with input waits and stops taken out (`RunClock`). Two samples of
|
|
13
|
+
* one run give its processing speed over the stretch between them; the run
|
|
14
|
+
* takes them when it closes pieces (`EncodeRun.speedReading`), so there is no
|
|
15
|
+
* window to choose.
|
|
13
16
|
*/
|
|
14
17
|
|
|
15
18
|
/**
|
|
16
|
-
* @typedef {object}
|
|
17
|
-
* @property {number}
|
|
18
|
-
* @property {number}
|
|
19
|
+
* @typedef {object} WorkSample
|
|
20
|
+
* @property {number} at - Wall clock, in milliseconds.
|
|
21
|
+
* @property {number} producedSeconds - Film this run has made.
|
|
22
|
+
* @property {number} workingMs - The run's own working time so far.
|
|
19
23
|
*/
|
|
20
24
|
|
|
21
25
|
/**
|
|
22
|
-
*
|
|
26
|
+
* Processing speed between two samples of one run, or null when the pair
|
|
27
|
+
* cannot answer: nothing made, or no working time between them.
|
|
23
28
|
*
|
|
24
|
-
* @param {
|
|
25
|
-
* @param {
|
|
26
|
-
* @
|
|
27
|
-
* @returns {number | null} Video seconds produced per second of clock.
|
|
29
|
+
* @param {WorkSample | null} previous
|
|
30
|
+
* @param {WorkSample | null} current
|
|
31
|
+
* @returns {number | null} Film seconds made per second of the run's own work.
|
|
28
32
|
*/
|
|
29
|
-
export function
|
|
33
|
+
export function speedFromWork(previous, current) {
|
|
30
34
|
if (!previous || !current) {
|
|
31
35
|
return null;
|
|
32
36
|
}
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
if (!
|
|
37
|
+
const producedSeconds = current.producedSeconds - previous.producedSeconds;
|
|
38
|
+
const workingSeconds = (current.workingMs - previous.workingMs) / 1000;
|
|
39
|
+
if (!(producedSeconds > 0) || !(workingSeconds > 0)) {
|
|
36
40
|
return null;
|
|
37
41
|
}
|
|
38
|
-
|
|
39
|
-
// the second happens when a run has just been repositioned and has not yet
|
|
40
|
-
// reached the position it is restarting from.
|
|
41
|
-
if (!(wallSeconds >= minimumWindowSec) || !(producedSeconds > 0)) {
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
return producedSeconds / wallSeconds;
|
|
42
|
+
return producedSeconds / workingSeconds;
|
|
45
43
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Whether a closed piece holds the whole of its stretch of film.
|
|
3
|
+
*
|
|
4
|
+
* A fact of production, decided where production is known: the cut a piece
|
|
5
|
+
* must reach comes from the output's cut table, and what it holds from the
|
|
6
|
+
* piece's own media intervals as its format reads them. The segment store keeps
|
|
7
|
+
* the bytes and whatever is derived from them; it does not judge them.
|
|
8
|
+
*
|
|
9
|
+
* A non-final piece is whole when the end of its last frame, on every track,
|
|
10
|
+
* reaches its next cut less `SEGMENT_CUT_TIME_DELTA_SECONDS`, the delta the
|
|
11
|
+
* muxer is configured with. The final piece has no next cut and is whole when
|
|
12
|
+
* it holds playable media at all. A piece that holds no playable media is never
|
|
13
|
+
* whole: an interrupted run can close a file with nothing in it.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { SEGMENT_CUT_TIME_DELTA_SECONDS } from "./output/index.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The time a piece must reach, or undefined for the final piece.
|
|
20
|
+
*
|
|
21
|
+
* @param {{ segmentCount: number, publishedStartOf: (index: number) => number }} timeline
|
|
22
|
+
* @param {number} index
|
|
23
|
+
* @returns {number | undefined}
|
|
24
|
+
*/
|
|
25
|
+
export function cutOf(timeline, index) {
|
|
26
|
+
return index < timeline.segmentCount - 1
|
|
27
|
+
? timeline.publishedStartOf(index + 1) - SEGMENT_CUT_TIME_DELTA_SECONDS
|
|
28
|
+
: undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {{ producedThroughSeconds?: (ranges: object) => number | null }} format
|
|
33
|
+
* @param {object} ranges - The piece's media intervals, as the format read them.
|
|
34
|
+
* @param {number | undefined} cutSeconds - From {@link cutOf}.
|
|
35
|
+
* @returns {{ whole: boolean, throughSeconds: number | null }}
|
|
36
|
+
*/
|
|
37
|
+
export function judgePiece(format, ranges, cutSeconds) {
|
|
38
|
+
const throughSeconds = format.producedThroughSeconds?.(ranges) ?? null;
|
|
39
|
+
if (throughSeconds === null) {
|
|
40
|
+
return { whole: false, throughSeconds };
|
|
41
|
+
}
|
|
42
|
+
if (Number.isFinite(cutSeconds) && throughSeconds < cutSeconds) {
|
|
43
|
+
return { whole: false, throughSeconds };
|
|
44
|
+
}
|
|
45
|
+
return { whole: true, throughSeconds };
|
|
46
|
+
}
|