@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.107.25",
3
+ "version": "2.107.27",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -194,9 +194,10 @@ export class EncodeAdmission {
194
194
  }
195
195
 
196
196
  /**
197
- * Whether a not-yet-opened format can take a place on the whole machine.
198
- * The output choice calls this for each candidate rung; the final claim uses
199
- * `admitsWatching` with the same arithmetic after registering the output.
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
- const load = this.#host.loadForCandidate?.(spec, file) ?? null;
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
- // At a playlist boundary the player can request the fragment ending
1286
- // exactly at its seek target. It is work needed to reach the target,
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) && endSeconds < 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 for a range that starts a few frames
42
- // after its current position (see server/public/domain/buffer-metrics.js).
43
- const CLIENT_RANGE_START_TOLERANCE_SECONDS = 0.25;
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) && segment.endSeconds >= position && segment.startSeconds < horizonEnd)
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 exactBoundaryPrerequisite = segment.endSeconds === position;
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 = exactBoundaryPrerequisite ? range.start : latest(range.start, at);
297
- if ((exactBoundaryPrerequisite || compare(range.end, at) > 0) &&
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(CLIENT_RANGE_START_TOLERANCE_SECONDS), unit));
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(CLIENT_RANGE_START_TOLERANCE_SECONDS), unit));
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 at or after `position`) that start before `horizonEnd`, the first
883
- * piece, every piece that has been read and every piece that is made. The piece
884
- * ending exactly at a seek position is kept as a prerequisite: the playlist
885
- * player can request it before the piece starting at that position. A piece
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) && segment.endSeconds < position) &&
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
  }