@torrent-tv/proxy 2.107.25 → 2.107.27
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 +1 -1
- package/services/encode/EncodeAdmission.js +14 -4
- package/services/encode/quality/output-format.js +15 -12
- package/services/encode/quality/serving-output.js +4 -2
- package/services/server/ViewerRequests.js +5 -4
- package/services/server/wire-outputs.js +3 -2
- package/services/viewer/playback-readiness.js +28 -14
package/package.json
CHANGED
|
@@ -194,9 +194,10 @@ export class EncodeAdmission {
|
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
/**
|
|
197
|
-
* Whether
|
|
198
|
-
*
|
|
199
|
-
*
|
|
197
|
+
* Whether an output candidate can take a place on the whole machine. A
|
|
198
|
+
* partial output already in the catalogue uses its measured cost; a
|
|
199
|
+
* not-yet-opened output uses the same candidate estimate as before. The final
|
|
200
|
+
* claim uses `admitsWatching` after registering the output.
|
|
200
201
|
*
|
|
201
202
|
* @param {object} spec
|
|
202
203
|
* @param {object} file
|
|
@@ -207,13 +208,22 @@ export class EncodeAdmission {
|
|
|
207
208
|
if (!address) {
|
|
208
209
|
return { admitted: false, reason: "the output has no address", speedX: null };
|
|
209
210
|
}
|
|
211
|
+
if (this.#host.finished?.(address) === true) {
|
|
212
|
+
return { admitted: true, reason: "everything it serves is already made", speedX: null };
|
|
213
|
+
}
|
|
210
214
|
if ((this.#host.liveRunsByAddress().get(address) ?? 0) > 0) {
|
|
211
215
|
return { admitted: true, reason: "an encoder already runs there", speedX: null };
|
|
212
216
|
}
|
|
213
217
|
if (this.#heldWithoutRun().has(address)) {
|
|
214
218
|
return { admitted: true, reason: "it already holds a place for another viewer", speedX: null };
|
|
215
219
|
}
|
|
216
|
-
|
|
220
|
+
// A partial output already in the catalogue has its measured rate. Use it
|
|
221
|
+
// before the candidate estimate; completed outputs took the return above.
|
|
222
|
+
const existing = this.#host.loadOf(address);
|
|
223
|
+
const hasMeasuredExistingCost = Number.isFinite(existing?.costSec) && existing.costSec > 0;
|
|
224
|
+
const load = hasMeasuredExistingCost
|
|
225
|
+
? existing
|
|
226
|
+
: this.#host.loadForCandidate?.(spec, file) ?? existing;
|
|
217
227
|
return this.#assessCandidate(address, load);
|
|
218
228
|
}
|
|
219
229
|
|
|
@@ -228,6 +228,20 @@ export function decideOutputFormat({
|
|
|
228
228
|
rateControl: limit === null ? null : softwareRateControlFor({ width: w, height: h, fps, source, capKbps: limit })
|
|
229
229
|
}));
|
|
230
230
|
};
|
|
231
|
+
let machineRefusal = null;
|
|
232
|
+
const machineAdmissionFor = (spec) => typeof machineAdmission === "function" ? machineAdmission(spec) : null;
|
|
233
|
+
const machineCanContinue = (spec) => machineAdmissionFor(spec)?.admitted !== false;
|
|
234
|
+
const admittedByMachine = (spec) => {
|
|
235
|
+
if (!machineCan(spec.video?.encode?.width ?? 0, spec.video?.encode?.height ?? 0)) {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
const answer = machineAdmissionFor(spec);
|
|
239
|
+
if (answer?.admitted === false) {
|
|
240
|
+
machineRefusal ??= answer;
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
return true;
|
|
244
|
+
};
|
|
231
245
|
const wantedFormats = formatsAt(width, height);
|
|
232
246
|
const wantedSpec = wantedFormats[0];
|
|
233
247
|
const wantedKey = wantedSpec.toKey();
|
|
@@ -247,6 +261,7 @@ export function decideOutputFormat({
|
|
|
247
261
|
wanted: { width, height },
|
|
248
262
|
nextLowerArea: nextRungAreaBelow(buildResolutionLadder(ceiling.w, ceiling.h), height),
|
|
249
263
|
atRisk: measured && !canSustainOutput({ benchmark, ...priced, outputPixelsPerSec: width * height * fps, frame: { width, height } }).sustainable,
|
|
264
|
+
canProduce: (candidate) => machineCanContinue(candidate.spec),
|
|
250
265
|
judge: (candidate) => judge(candidate.spec),
|
|
251
266
|
candidates: servingCandidates({
|
|
252
267
|
wanted: wantedSpec,
|
|
@@ -262,18 +277,6 @@ export function decideOutputFormat({
|
|
|
262
277
|
}
|
|
263
278
|
|
|
264
279
|
let anySizeHeld = false;
|
|
265
|
-
let machineRefusal = null;
|
|
266
|
-
const admittedByMachine = (spec) => {
|
|
267
|
-
if (!machineCan(spec.video?.encode?.width ?? 0, spec.video?.encode?.height ?? 0)) {
|
|
268
|
-
return false;
|
|
269
|
-
}
|
|
270
|
-
const answer = typeof machineAdmission === "function" ? machineAdmission(spec) : null;
|
|
271
|
-
if (answer?.admitted === false) {
|
|
272
|
-
machineRefusal ??= answer;
|
|
273
|
-
return false;
|
|
274
|
-
}
|
|
275
|
-
return true;
|
|
276
|
-
};
|
|
277
280
|
if (machineCan(width, height)) {
|
|
278
281
|
for (const spec of wantedFormats) {
|
|
279
282
|
if (!admittedByMachine(spec)) {
|
|
@@ -48,15 +48,17 @@ import { admissionRank } from "./link-budget.js";
|
|
|
48
48
|
* on this viewer's own ladder; zero when there is none.
|
|
49
49
|
* @param {boolean} params.atRisk - Producing `wanted` here is measured not to
|
|
50
50
|
* keep up with what the file needs. False where nothing measured it.
|
|
51
|
+
* @param {(candidate: Candidate) => boolean} [params.canProduce] - Whether
|
|
52
|
+
* this machine can keep producing an output that is not finished.
|
|
51
53
|
* @param {(candidate: Candidate) => { admitted: boolean, load: object }} params.judge -
|
|
52
54
|
* What this viewer's link does with the candidate's whole load.
|
|
53
55
|
* @param {Candidate[]} params.candidates
|
|
54
56
|
* @returns {{ key: string, answer: object } | null} The output to serve, or
|
|
55
57
|
* null to produce `wanted`.
|
|
56
58
|
*/
|
|
57
|
-
export function chooseServingOutput({ mode, wanted, nextLowerArea, atRisk, judge, candidates }) {
|
|
59
|
+
export function chooseServingOutput({ mode, wanted, nextLowerArea, atRisk, canProduce = () => true, judge, candidates }) {
|
|
58
60
|
const admitted = (candidates ?? [])
|
|
59
|
-
.filter((candidate) => candidate.readyHere === true)
|
|
61
|
+
.filter((candidate) => candidate.readyHere === true && canProduce(candidate))
|
|
60
62
|
.map((candidate) => ({ candidate, answer: judge(candidate) }))
|
|
61
63
|
.filter((one) => one.answer.admitted);
|
|
62
64
|
const areaOf = (one) => one.candidate.width * one.candidate.height;
|
|
@@ -1282,11 +1282,12 @@ export class ViewerRequests {
|
|
|
1282
1282
|
const ranges = inventory.has(index) ? this.#host.encodeRuns.finishedMediaRangesOf(output, index) : undefined;
|
|
1283
1283
|
const mediaRanges = ranges && output.segmentFormat?.servedMediaRanges ?
|
|
1284
1284
|
output.segmentFormat.servedMediaRanges(ranges, { initBytes: sessionInit }) : undefined;
|
|
1285
|
-
//
|
|
1286
|
-
//
|
|
1287
|
-
// even though it adds no media after the viewer's position.
|
|
1285
|
+
// The player can request a fragment whose media timestamps extend a few
|
|
1286
|
+
// frames past its nominal playlist end while seeking to the next piece.
|
|
1288
1287
|
if (index !== 0 && mediaRanges === undefined && !inventory.has(index) &&
|
|
1289
|
-
Number.isFinite(endSeconds) && Number.isFinite(positionSeconds) &&
|
|
1288
|
+
Number.isFinite(endSeconds) && Number.isFinite(positionSeconds) &&
|
|
1289
|
+
!this.#host.playbackReadiness.endsWithinPlayerSeekTolerance(endSeconds, positionSeconds) &&
|
|
1290
|
+
endSeconds < positionSeconds) {
|
|
1290
1291
|
continue;
|
|
1291
1292
|
}
|
|
1292
1293
|
const startSeconds = timeline.publishedStartOf(index);
|
|
@@ -72,7 +72,7 @@ import { ViewerRequests } from "./ViewerRequests.js";
|
|
|
72
72
|
import { OutputLifecycle } from "./OutputLifecycle.js";
|
|
73
73
|
import { SegmentServing } from "./SegmentServing.js";
|
|
74
74
|
import { audioStartSecondsFor } from "../viewer/audio-start.js";
|
|
75
|
-
import { forecastRate, rateSummaryOf, predictPlaybackReadiness, RateTrend } from "../viewer/playback-readiness.js";
|
|
75
|
+
import { endsWithinPlayerSeekTolerance, forecastRate, rateSummaryOf, predictPlaybackReadiness, RateTrend } from "../viewer/playback-readiness.js";
|
|
76
76
|
import { audioRenditionName } from "../media/audio-inventory.js";
|
|
77
77
|
import { Renditions } from "../encode/Renditions.js";
|
|
78
78
|
import { CushionReport, LOOKAHEAD_PAUSE_SECONDS } from "../encode/CushionReport.js";
|
|
@@ -640,7 +640,8 @@ export function wireOutputs({
|
|
|
640
640
|
get encodeRuns() { return parts.encodeRuns; },
|
|
641
641
|
// When this viewer's playback can start and run to the end: the viewer
|
|
642
642
|
// component's forecast, which models the viewer's browser.
|
|
643
|
-
playbackReadiness: { predict: predictPlaybackReadiness, forecastRate, rateSummaryOf, RateTrend
|
|
643
|
+
playbackReadiness: { predict: predictPlaybackReadiness, forecastRate, rateSummaryOf, RateTrend,
|
|
644
|
+
endsWithinPlayerSeekTolerance },
|
|
644
645
|
// Whether the subtitle a viewer chose has been read where they stand — the
|
|
645
646
|
// source preparation's answer, set on these parts by the assembly.
|
|
646
647
|
subtitleReadyFor: (viewer) => parts.subtitleReadyFor?.(viewer) ?? true,
|
|
@@ -38,9 +38,20 @@ import {
|
|
|
38
38
|
ZERO
|
|
39
39
|
} from "./media-time.js";
|
|
40
40
|
|
|
41
|
-
// Match the media element's allowance
|
|
42
|
-
//
|
|
43
|
-
const
|
|
41
|
+
// Match the media element's allowance around a seek target. A fragment's
|
|
42
|
+
// timestamps can extend slightly beyond its nominal playlist boundary.
|
|
43
|
+
export const PLAYER_SEEK_TOLERANCE_SECONDS = 0.25;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {number} segmentEndSeconds
|
|
47
|
+
* @param {number} positionSeconds
|
|
48
|
+
* @returns {boolean}
|
|
49
|
+
*/
|
|
50
|
+
export function endsWithinPlayerSeekTolerance(segmentEndSeconds, positionSeconds) {
|
|
51
|
+
return Number.isFinite(segmentEndSeconds) && Number.isFinite(positionSeconds) &&
|
|
52
|
+
segmentEndSeconds <= positionSeconds &&
|
|
53
|
+
positionSeconds - segmentEndSeconds <= PLAYER_SEEK_TOLERANCE_SECONDS;
|
|
54
|
+
}
|
|
44
55
|
|
|
45
56
|
/**
|
|
46
57
|
* The mean rate predicted over a horizon by the integral of the measured rate.
|
|
@@ -223,7 +234,8 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
223
234
|
bitsPerMediaSecond: averageBitsPerMediaSecond(track, track.segments,
|
|
224
235
|
new Set(Array.isArray(track.readySegmentIndices) ? track.readySegmentIndices : [])),
|
|
225
236
|
segments: segmentsOf[trackIndex]
|
|
226
|
-
.filter((segment) => isPiece(segment) &&
|
|
237
|
+
.filter((segment) => isPiece(segment) &&
|
|
238
|
+
segment.endSeconds >= position - PLAYER_SEEK_TOLERANCE_SECONDS && segment.startSeconds < horizonEnd)
|
|
227
239
|
.sort((left, right) => left.startSeconds - right.startSeconds || left.index - right.index),
|
|
228
240
|
// The pieces after the near window, as the playlist cuts them, and the
|
|
229
241
|
// offset the page's player placed them by.
|
|
@@ -290,11 +302,11 @@ export function predictPlaybackReadiness(input = {}) {
|
|
|
290
302
|
for (const segment of segments) {
|
|
291
303
|
// Media the page already holds is not transferred again: every range of
|
|
292
304
|
// the piece still ahead of the viewer lies inside what the page holds.
|
|
293
|
-
const
|
|
305
|
+
const boundaryPrerequisite = endsWithinPlayerSeekTolerance(segment.endSeconds, position);
|
|
294
306
|
let held = segment.ranges.length > 0;
|
|
295
307
|
for (const range of segment.ranges) {
|
|
296
|
-
const requiredStart =
|
|
297
|
-
if ((
|
|
308
|
+
const requiredStart = boundaryPrerequisite ? range.start : latest(range.start, at);
|
|
309
|
+
if ((boundaryPrerequisite || compare(range.end, at) > 0) &&
|
|
298
310
|
!contains(state.clientRanges, requiredStart, range.end)) {
|
|
299
311
|
held = false;
|
|
300
312
|
break;
|
|
@@ -843,7 +855,7 @@ function heldRanges(ranges, segments, unit, position) {
|
|
|
843
855
|
const pieces = segments.flatMap((segment) => segment.ranges ?? []);
|
|
844
856
|
const zero = mediaTime(0n, unit);
|
|
845
857
|
const error = rescale(REPORTED_TIME_ERROR, unit);
|
|
846
|
-
const startTolerance = add(position, rescale(fromSeconds(
|
|
858
|
+
const startTolerance = add(position, rescale(fromSeconds(PLAYER_SEEK_TOLERANCE_SECONDS), unit));
|
|
847
859
|
const held = [];
|
|
848
860
|
for (const range of ranges) {
|
|
849
861
|
const reportedStart = rescale(range.start, unit);
|
|
@@ -870,7 +882,7 @@ function heldRanges(ranges, segments, unit, position) {
|
|
|
870
882
|
function withSeekStartTolerance(ranges, position, unit) {
|
|
871
883
|
const first = ranges.reduce((earliestRange, range) =>
|
|
872
884
|
!earliestRange || compare(range.start, earliestRange.start) < 0 ? range : earliestRange, null);
|
|
873
|
-
const toleranceEnd = add(position, rescale(fromSeconds(
|
|
885
|
+
const toleranceEnd = add(position, rescale(fromSeconds(PLAYER_SEEK_TOLERANCE_SECONDS), unit));
|
|
874
886
|
if (!first || compare(first.start, position) <= 0 || compare(first.start, toleranceEnd) > 0) {
|
|
875
887
|
return ranges;
|
|
876
888
|
}
|
|
@@ -879,10 +891,11 @@ function withSeekStartTolerance(ranges, position, unit) {
|
|
|
879
891
|
|
|
880
892
|
/**
|
|
881
893
|
* The pieces of a track this forecast places in exact time: those still to
|
|
882
|
-
* play (ending
|
|
883
|
-
* piece, every piece that has been read and
|
|
884
|
-
*
|
|
885
|
-
*
|
|
894
|
+
* play (ending within the player's seek tolerance of `position`) that start
|
|
895
|
+
* before `horizonEnd`, the first piece, every piece that has been read and
|
|
896
|
+
* every piece that is made. A piece ending just before a seek target is kept
|
|
897
|
+
* as a prerequisite because its media timestamps can extend past its nominal
|
|
898
|
+
* playlist boundary. A piece
|
|
886
899
|
* that is none of these changes no exact answer: after the near window it is
|
|
887
900
|
* scheduled in seconds, it is not where the track starts, no unread piece is
|
|
888
901
|
* placed by it (only read pieces are), it lends no frame to what the page
|
|
@@ -896,7 +909,8 @@ function withSeekStartTolerance(ranges, position, unit) {
|
|
|
896
909
|
function piecesThatMatter(track, position, horizonEnd = Number.POSITIVE_INFINITY) {
|
|
897
910
|
const made = new Set(Array.isArray(track.readySegmentIndices) ? track.readySegmentIndices : []);
|
|
898
911
|
const kept = track.segments.filter((segment) => segment?.index === 0 || segment?.mediaRanges !== undefined ||
|
|
899
|
-
made.has(segment?.index) || (!(Number.isFinite(segment?.endSeconds) &&
|
|
912
|
+
made.has(segment?.index) || (!(Number.isFinite(segment?.endSeconds) &&
|
|
913
|
+
segment.endSeconds < position - PLAYER_SEEK_TOLERANCE_SECONDS) &&
|
|
900
914
|
!(Number.isFinite(segment?.startSeconds) && segment.startSeconds >= horizonEnd)));
|
|
901
915
|
return kept.length === track.segments.length ? track.segments : kept;
|
|
902
916
|
}
|