@torrent-tv/proxy 2.107.41 → 2.107.43
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/package.json
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
|
|
40
40
|
import { ENCODE_RUN_EVENT, ENCODE_RUN_STATE, INITIAL_RUN_STATE, nextState } from "./encode-run-state.js";
|
|
41
41
|
import { classifyEncodeExit, ENCODE_EXIT } from "./encode-exit.js";
|
|
42
|
-
import {
|
|
42
|
+
import { workDeltaBetween } from "./encoder-readings.js";
|
|
43
43
|
import { RunClock } from "./RunClock.js";
|
|
44
44
|
import { RunProgress } from "./RunProgress.js";
|
|
45
45
|
|
|
@@ -229,7 +229,7 @@ export class EncodeRun {
|
|
|
229
229
|
* @param {{ info: (line: string) => void, warn: (line: string) => void, error?: (line: string) => void }} params.logger
|
|
230
230
|
* @param {() => number} [params.now]
|
|
231
231
|
* @param {(ended: RunEnded) => void} [params.onEnded]
|
|
232
|
-
* @param {(reading: { speed: number, at: number }) => void} [params.onSpeedMeasured] -
|
|
232
|
+
* @param {(reading: { speed: number, at: number, workSeconds: number, workingSeconds: number }) => void} [params.onSpeedMeasured] -
|
|
233
233
|
* Told each time this run has measured its processing speed, once per
|
|
234
234
|
* reading.
|
|
235
235
|
* @param {(name: string, following: string | null) => string | null} [params.onClosed] - Called with the
|
|
@@ -353,10 +353,10 @@ export class EncodeRun {
|
|
|
353
353
|
}
|
|
354
354
|
|
|
355
355
|
/**
|
|
356
|
-
* The
|
|
357
|
-
* piece
|
|
356
|
+
* The processing speed, its produced media and active working time, with
|
|
357
|
+
* the measurement time; null before a piece supplies a measured interval.
|
|
358
358
|
*
|
|
359
|
-
* @returns {{ speed: number, at: number } | null}
|
|
359
|
+
* @returns {{ speed: number, at: number, workSeconds: number, workingSeconds: number } | null}
|
|
360
360
|
*/
|
|
361
361
|
get speedReading() {
|
|
362
362
|
return this.#speedReading === null ? null : { ...this.#speedReading };
|
|
@@ -764,9 +764,9 @@ export class EncodeRun {
|
|
|
764
764
|
if (sample === null) {
|
|
765
765
|
return;
|
|
766
766
|
}
|
|
767
|
-
const
|
|
768
|
-
if (
|
|
769
|
-
this.#speedReading = { speed, at: sample.at };
|
|
767
|
+
const work = workDeltaBetween(this.#pieceSample, sample);
|
|
768
|
+
if (work !== null) {
|
|
769
|
+
this.#speedReading = { speed: work.workSeconds / work.workingSeconds, at: sample.at, ...work };
|
|
770
770
|
this.#pieceSample = sample;
|
|
771
771
|
this.#speedMeasurementPending = false;
|
|
772
772
|
this.onSpeedMeasured(this.speedReading);
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
export class SpeedReadings {
|
|
17
|
-
/** @type {WeakMap<object, Array<{ speed: number, at: number }>>} */
|
|
17
|
+
/** @type {WeakMap<object, Array<{ speed: number, at: number, workSeconds?: number, workingSeconds?: number }>>} */
|
|
18
18
|
#byOutput = new WeakMap();
|
|
19
19
|
#keepMs;
|
|
20
20
|
|
|
@@ -25,7 +25,7 @@ export class SpeedReadings {
|
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* @param {object} output
|
|
28
|
-
* @param {{ speed: number, at: number } | null} reading
|
|
28
|
+
* @param {{ speed: number, at: number, workSeconds?: number, workingSeconds?: number } | null} reading
|
|
29
29
|
*/
|
|
30
30
|
note(output, reading) {
|
|
31
31
|
if (!output || !(reading?.speed > 0) || !Number.isFinite(reading.at)) return;
|
|
@@ -33,7 +33,12 @@ export class SpeedReadings {
|
|
|
33
33
|
if (!readings) this.#byOutput.set(output, readings = []);
|
|
34
34
|
let place = readings.length;
|
|
35
35
|
while (place > 0 && readings[place - 1].at > reading.at) place -= 1;
|
|
36
|
-
|
|
36
|
+
const sample = { speed: reading.speed, at: reading.at };
|
|
37
|
+
if (Number.isFinite(reading.workSeconds) && reading.workSeconds > 0 &&
|
|
38
|
+
Number.isFinite(reading.workingSeconds) && reading.workingSeconds > 0) {
|
|
39
|
+
Object.assign(sample, { workSeconds: reading.workSeconds, workingSeconds: reading.workingSeconds });
|
|
40
|
+
}
|
|
41
|
+
readings.splice(place, 0, sample);
|
|
37
42
|
const cutoff = readings.at(-1).at - this.#keepMs;
|
|
38
43
|
// As the forecast's own trend keeps them: the newest one before the horizon stays.
|
|
39
44
|
while (readings.length > 1 && readings[1].at <= cutoff) readings.shift();
|
|
@@ -41,7 +46,7 @@ export class SpeedReadings {
|
|
|
41
46
|
|
|
42
47
|
/**
|
|
43
48
|
* @param {object} output
|
|
44
|
-
* @returns {Array<{ speed: number, at: number }>} The
|
|
49
|
+
* @returns {Array<{ speed: number, at: number, workSeconds?: number, workingSeconds?: number }>} The readings in measurement order.
|
|
45
50
|
*/
|
|
46
51
|
windowOf(output) {
|
|
47
52
|
return [...(this.#byOutput.get(output) ?? [])];
|
|
@@ -31,13 +31,21 @@
|
|
|
31
31
|
* @returns {number | null} Film seconds made per second of the run's own work.
|
|
32
32
|
*/
|
|
33
33
|
export function speedFromWork(previous, current) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
const work = workDeltaBetween(previous, current);
|
|
35
|
+
return work === null ? null : work.workSeconds / work.workingSeconds;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Produced media and active processing time between samples of one run.
|
|
40
|
+
* @param {WorkSample | null} previous
|
|
41
|
+
* @param {WorkSample | null} current
|
|
42
|
+
* @returns {{ workSeconds: number, workingSeconds: number } | null}
|
|
43
|
+
*/
|
|
44
|
+
export function workDeltaBetween(previous, current) {
|
|
45
|
+
if (!previous || !current) return null;
|
|
46
|
+
const workSeconds = current.producedSeconds - previous.producedSeconds;
|
|
38
47
|
const workingSeconds = (current.workingMs - previous.workingMs) / 1000;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return producedSeconds / workingSeconds;
|
|
48
|
+
return Number.isFinite(workSeconds) && workSeconds > 0 &&
|
|
49
|
+
Number.isFinite(workingSeconds) && workingSeconds > 0
|
|
50
|
+
? { workSeconds, workingSeconds } : null;
|
|
43
51
|
}
|
|
@@ -76,12 +76,13 @@ export function forecastRate(readings, now = Date.now(), horizonSeconds = 0, wei
|
|
|
76
76
|
* ordered by when each was taken, readings taken in the same millisecond kept
|
|
77
77
|
* apart. Null for no reading.
|
|
78
78
|
*
|
|
79
|
-
* @param {Array<{ at: number, speed?: number, value?: number }>} readings
|
|
79
|
+
* @param {Array<{ at: number, speed?: number, value?: number, workSeconds?: number, workingSeconds?: number }>} readings
|
|
80
80
|
* @returns {object | null}
|
|
81
81
|
*/
|
|
82
82
|
export function rateSummaryOf(readings) {
|
|
83
83
|
const ordered = (Array.isArray(readings) ? readings : [])
|
|
84
|
-
.map(reading => ({ at: reading?.at, value: reading?.value ?? reading?.speed
|
|
84
|
+
.map(reading => ({ at: reading?.at, value: reading?.value ?? reading?.speed,
|
|
85
|
+
workSeconds: reading?.workSeconds, workingSeconds: reading?.workingSeconds }))
|
|
85
86
|
.filter(reading => Number.isFinite(reading.at) && Number.isFinite(reading.value) && reading.value >= 0)
|
|
86
87
|
.sort((left, right) => left.at - right.at);
|
|
87
88
|
return ordered.length > 0 ? summarizeRate(ordered) : null;
|
|
@@ -278,9 +279,9 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
278
279
|
[{ start: at, end: add(at, measured ? nearest(fromSeconds(buffered), unit) :
|
|
279
280
|
rescale(fromSeconds(buffered), unit)), frame: zero }],
|
|
280
281
|
ready: new Set(Array.isArray(track.readySegmentIndices) ? track.readySegmentIndices : []),
|
|
281
|
-
// Film made over
|
|
282
|
-
//
|
|
283
|
-
//
|
|
282
|
+
// Film made over active processing time, summed over the readings retained.
|
|
283
|
+
// Point-only legacy readings retain their equal-work harmonic summary.
|
|
284
|
+
// The last reading alone is one piece's content and machine — a
|
|
284
285
|
// first piece of credits read 6.49x and started a cold AVI on 2.4 s of
|
|
285
286
|
// buffer that the next piece, at 0.57x, did not hold (torrent-tv/meta#178).
|
|
286
287
|
curve: rateCurve(track.readings, now, "piece"),
|
|
@@ -687,16 +688,8 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
687
688
|
}
|
|
688
689
|
const tailStock = [];
|
|
689
690
|
if (trackState.some(({ tailEndSeconds }) => Number.isFinite(tailEndSeconds))) {
|
|
690
|
-
// Where each source's bytes stop being the near window's: the last byte
|
|
691
|
-
// its pieces read, in the file's own offsets.
|
|
692
|
-
const windowEnds = new Map();
|
|
693
|
-
for (const { sourceByteIntervals } of jobs) {
|
|
694
|
-
for (const [sourceId, ranges] of sourceByteIntervals) {
|
|
695
|
-
for (const range of ranges) windowEnds.set(sourceId, Math.max(windowEnds.get(sourceId) ?? 0, range.end));
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
691
|
const stop = scheduleTail({ trackState, sourceState, runStarts, trackFinish,
|
|
699
|
-
link, position, buffered, reserve, transferFinish, constrain, duration, stock: tailStock
|
|
692
|
+
link, position, buffered, reserve, transferFinish, constrain, duration, stock: tailStock });
|
|
700
693
|
if (stop) {
|
|
701
694
|
return { ...result(false, null, buffered, reserve, null, stop.reason, preparedSegments),
|
|
702
695
|
...(stop.unavailableSource ? { unavailableSource: stop.unavailableSource } : {}) };
|
|
@@ -799,7 +792,7 @@ function isPiece(segment) {
|
|
|
799
792
|
* @returns {{ reason: string, unavailableSource?: object } | null} Why there is no answer, or null.
|
|
800
793
|
*/
|
|
801
794
|
function scheduleTail({ trackState, sourceState, runStarts, trackFinish, link,
|
|
802
|
-
position, buffered, reserve, transferFinish, constrain, duration, stock = null
|
|
795
|
+
position, buffered, reserve, transferFinish, constrain, duration, stock = null }) {
|
|
803
796
|
const tailStart = Math.max(position, ...trackState.map(state => state.tailStartSeconds));
|
|
804
797
|
if (!Number.isFinite(tailStart)) return null;
|
|
805
798
|
const sourceDurations = new Map();
|
|
@@ -823,11 +816,17 @@ function scheduleTail({ trackState, sourceState, runStarts, trackFinish, link,
|
|
|
823
816
|
stock?.push({ sourceId, skipped: "complete" });
|
|
824
817
|
continue;
|
|
825
818
|
}
|
|
826
|
-
// Held means on this proxy now,
|
|
827
|
-
//
|
|
828
|
-
// counted again as held took 117 s of film off a 90-minute tail.
|
|
829
|
-
//
|
|
830
|
-
|
|
819
|
+
// Held means on this proxy now, in the bytes of the film after the near
|
|
820
|
+
// window: the window's own bytes are counted above, and its planned
|
|
821
|
+
// arrivals counted again as held took 117 s of film off a 90-minute tail.
|
|
822
|
+
// Where those bytes begin is placed by the density the tail is priced at.
|
|
823
|
+
// Neither the torrent thread's plan nor the bytes the window reads are
|
|
824
|
+
// that place: both can reach the end of the file — a copied Matroska file
|
|
825
|
+
// reads its index there — and the whole of Despicable Me 3 then counted
|
|
826
|
+
// as held (Home Assistant 2026-10-26, torrent-tv/meta#239).
|
|
827
|
+
const fileLength = Number(source.source.fileLength);
|
|
828
|
+
const windowEnd = fileLength > 0 && duration > 0
|
|
829
|
+
? Math.min(fileLength, Math.max(0, fileLength * tailStart / duration)) : 0;
|
|
831
830
|
const bytes = estimateUnmappedSourceBytes(source.source, seconds, duration,
|
|
832
831
|
heldBeyond(source.source.residence, windowEnd), windowEnd);
|
|
833
832
|
if (bytes === null) return { reason: "source-measurement-unavailable",
|
|
@@ -1419,6 +1418,10 @@ function rateCurve(readings, now, weighting = "last") {
|
|
|
1419
1418
|
|
|
1420
1419
|
function summarizeRate(readings) {
|
|
1421
1420
|
const latest = readings.at(-1);
|
|
1421
|
+
const workKnown = readings.every(reading => Number.isFinite(reading.workSeconds) && reading.workSeconds > 0 &&
|
|
1422
|
+
Number.isFinite(reading.workingSeconds) && reading.workingSeconds > 0);
|
|
1423
|
+
const workSeconds = workKnown ? readings.reduce((sum, reading) => sum + reading.workSeconds, 0) : null;
|
|
1424
|
+
const workingSeconds = workKnown ? readings.reduce((sum, reading) => sum + reading.workingSeconds, 0) : null;
|
|
1422
1425
|
let weighted = 0;
|
|
1423
1426
|
for (let index = 0; index + 1 < readings.length; index += 1) {
|
|
1424
1427
|
weighted += readings[index].value * (readings[index + 1].at - readings[index].at);
|
|
@@ -1429,12 +1432,13 @@ function summarizeRate(readings) {
|
|
|
1429
1432
|
lastAt: latest.at,
|
|
1430
1433
|
lastValue: latest.value,
|
|
1431
1434
|
meanValue: readings.reduce((sum, reading) => sum + reading.value, 0) / readings.length,
|
|
1432
|
-
//
|
|
1435
|
+
// Equal-work summary for legacy readings without measured work quantities.
|
|
1433
1436
|
harmonicValue: readings.some(reading => !(reading.value > 0)) ? 0
|
|
1434
1437
|
: readings.length / readings.reduce((sum, reading) => sum + 1 / reading.value, 0),
|
|
1435
1438
|
// Each reading holding until the next: quantity over the span.
|
|
1436
1439
|
timeWeightedValue: spanMs > 0 ? weighted / spanMs : latest.value,
|
|
1437
|
-
spanSeconds: spanMs / 1000
|
|
1440
|
+
spanSeconds: spanMs / 1000,
|
|
1441
|
+
...(workKnown ? { workSeconds, workingSeconds, workRateValue: workSeconds / workingSeconds } : {})
|
|
1438
1442
|
};
|
|
1439
1443
|
}
|
|
1440
1444
|
|
|
@@ -1442,7 +1446,8 @@ function rateCurveFrom(summary, weighting = "last") {
|
|
|
1442
1446
|
// Conditional projection of the measured service. No exponential
|
|
1443
1447
|
// relaxation, smoothing time, slope extrapolation or guessed recovery.
|
|
1444
1448
|
// A new measurement changes the integral; polling does not change its rate.
|
|
1445
|
-
const value = weighting === "piece" && Number.isFinite(summary.
|
|
1449
|
+
const value = weighting === "piece" && Number.isFinite(summary.workRateValue) ? summary.workRateValue
|
|
1450
|
+
: weighting === "piece" && Number.isFinite(summary.harmonicValue) ? summary.harmonicValue
|
|
1446
1451
|
: weighting === "time" && Number.isFinite(summary.timeWeightedValue) ? summary.timeWeightedValue
|
|
1447
1452
|
: summary.lastValue;
|
|
1448
1453
|
const rate = Math.max(0, value);
|