@torrent-tv/proxy 2.87.11 → 2.87.12

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.
@@ -69,6 +69,7 @@ import { correctForAvailability } from "./available-share.js";
69
69
  * @property {() => Set<string>} [watchedAddresses] - Every output a present
70
70
  * viewer is watching that still has something left to make.
71
71
  * @property {(address: string) => OutputLoad | null} loadOf
72
+ * @property {(spec: object, file: object) => OutputLoad | null} [loadForCandidate]
72
73
  * @property {() => ({ share: number, known: boolean } | null)} availability
73
74
  */
74
75
 
@@ -189,7 +190,50 @@ export class EncodeAdmission {
189
190
  if (this.#heldWithoutRun().has(address)) {
190
191
  return { admitted: true, reason: "it already holds a place for another viewer", speedX: null };
191
192
  }
192
- const load = this.#host.loadOf(address);
193
+ return this.#assessCandidate(address, this.#host.loadOf(address));
194
+ }
195
+
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.
200
+ *
201
+ * @param {object} spec
202
+ * @param {object} file
203
+ * @returns {{ admitted: boolean, reason: string, speedX: number | null }}
204
+ */
205
+ previewCandidate(spec, file) {
206
+ const address = typeof spec?.toKey === "function" ? spec.toKey() : "";
207
+ if (!address) {
208
+ return { admitted: false, reason: "the output has no address", speedX: null };
209
+ }
210
+ if ((this.#host.liveRunsByAddress().get(address) ?? 0) > 0) {
211
+ return { admitted: true, reason: "an encoder already runs there", speedX: null };
212
+ }
213
+ if (this.#heldWithoutRun().has(address)) {
214
+ return { admitted: true, reason: "it already holds a place for another viewer", speedX: null };
215
+ }
216
+ const load = this.#host.loadForCandidate?.(spec, file) ?? null;
217
+ return this.#assessCandidate(address, load);
218
+ }
219
+
220
+ /**
221
+ * Projected production rate for outputs currently holding machine capacity.
222
+ * A live encoder reading takes precedence; this measured-cost projection
223
+ * supplies the playback trajectory before its first run sample arrives.
224
+ *
225
+ * @returns {number | null}
226
+ */
227
+ projectedSpeedX() {
228
+ const occupied = this.#occupied(null);
229
+ if (occupied.unpriced.length > 0) {
230
+ return null;
231
+ }
232
+ const speedX = this.#speedAt(occupied.costSec);
233
+ return Number.isFinite(speedX) && speedX > 0 ? speedX : null;
234
+ }
235
+
236
+ #assessCandidate(address, load) {
193
237
  if (!load || load.costSec === null) {
194
238
  return { admitted: false, reason: "the output has no measured encoding cost", speedX: null };
195
239
  }
@@ -45,7 +45,7 @@ export class OutputOpening {
45
45
  #host;
46
46
 
47
47
  /**
48
- * @param {object} host - `logger`, `probeMediaInfo`, `probeKeyframeTimes`, `enabled`, `segmentFormat`, `renditions`, `sourceFiles`, `getCachedMediaInfo`, `localBaseUrl`, `tonemapSupported`, `videoEncoder`, `hostLoad`, `keyframeTables`, `timelines`, `segmentDurationSec`, `softwarePresetBenchmark`, `decodeCostModel`, `encodeCost`, `outputs`, `segmentOutputFiles`, `hostTimings`, `returns`, `encodeRuns`
48
+ * @param {object} host - `logger`, `probeMediaInfo`, `probeKeyframeTimes`, `enabled`, `segmentFormat`, `renditions`, `sourceFiles`, `getCachedMediaInfo`, `localBaseUrl`, `tonemapSupported`, `videoEncoder`, `hostLoad`, `keyframeTables`, `timelines`, `segmentDurationSec`, `softwarePresetBenchmark`, `decodeCostModel`, `encodeCost`, `admission`, `outputs`, `segmentOutputFiles`, `hostTimings`, `returns`, `encodeRuns`
49
49
  * @param {SegmentOutputFiles} host.segmentOutputFiles - The storage
50
50
  * operations needed while selecting or opening an output.
51
51
  */
@@ -573,6 +573,9 @@ export class OutputOpening {
573
573
  requiredSpeed: this.#host.hostLoad.requiredSpeedFor(sourceKey, fileIndex)
574
574
  },
575
575
  chooseBudget: (params) => this.#host.encodeCost.chooseEncodeBudget(params),
576
+ machineAdmission: this.#host.admission
577
+ ? (candidate) => this.#host.admission.previewCandidate(candidate, file)
578
+ : null,
576
579
  tonemap: applyTonemap,
577
580
  capKbps,
578
581
  limitsFor: (frame) => this.#host.limitsFor(frame),
@@ -329,6 +329,37 @@ export class EncodeCost {
329
329
  if (!session) {
330
330
  return null;
331
331
  }
332
+ return this.#loadOfSession(session);
333
+ }
334
+
335
+ /**
336
+ * What a not-yet-opened output would cost, priced by the same readings and
337
+ * rules as an output already in the catalogue. This lets the machine-wide
338
+ * admission answer before the format is committed, so AUTO can select a
339
+ * lower rung that the whole host can sustain.
340
+ *
341
+ * @param {object} spec
342
+ * @param {object} file
343
+ * @returns {{ costSec: number | null, fileKey: string, fileCostSec: number } | null}
344
+ */
345
+ loadForCandidate(spec, file) {
346
+ if (!spec || !file) {
347
+ return null;
348
+ }
349
+ const encode = spec.video?.encode ?? null;
350
+ const candidate = {
351
+ spec,
352
+ file,
353
+ output: {
354
+ encodeWidth: Number(encode?.width) || 0,
355
+ encodeHeight: Number(encode?.height) || 0,
356
+ outputFps: Number(encode?.fps) || TRANSCODE_FPS
357
+ }
358
+ };
359
+ return this.#loadOfSession(candidate);
360
+ }
361
+
362
+ #loadOfSession(session) {
332
363
  const fileKey = session.file?.key ?? "";
333
364
  const fileCost = Number(this.#torrentCostSecFor(session));
334
365
  const fileCostSec = Number.isFinite(fileCost) && fileCost > 0 ? fileCost : 0;
@@ -51,6 +51,8 @@ import { linkAnswerFigures, linkCouldCarry, loadOf, videoLoadOfSpec } from "./li
51
51
  * @param {unknown} params.benchmark - The startup measurement of the software encoder, or null.
52
52
  * @param {{ decodeModel: object | null, observedDecodeCostSec: number | null, requiredSpeed: number | null }} params.cost
53
53
  * @param {(params: object) => object | null} params.chooseBudget - The realtime budget's ladder choice.
54
+ * @param {(spec: OutputSpec) => { admitted: boolean, reason: string, speedX: number | null }} [params.machineAdmission] -
55
+ * The proxy's machine-wide capacity decision for a candidate output.
54
56
  * @param {boolean} params.tonemap
55
57
  * @param {number | null} [params.capKbps] - A nominal limit asked for outright;
56
58
  * above the size's own it is refused (`softwareRateControlFor`).
@@ -86,7 +88,8 @@ export function decideOutputFormat({
86
88
  limitsFor = (frame) => [nominalKbpsFor(frame)],
87
89
  audioLoad = null,
88
90
  specWith,
89
- serving
91
+ serving,
92
+ machineAdmission = null
90
93
  }) {
91
94
  const judge = (spec) => linkCouldCarry(
92
95
  serving.linkMbps,
@@ -170,6 +173,22 @@ export function decideOutputFormat({
170
173
  : measured && w > 0
171
174
  ? pickSoftwarePreset(benchmark, w * h * fps, priced, { width: w, height: h })
172
175
  : null);
176
+ const budgetFor = (spec) => {
177
+ const encode = spec.video?.encode;
178
+ if (!budget || !encode) {
179
+ return budget;
180
+ }
181
+ const rungIndex = budget.ladder?.findIndex((rung) =>
182
+ rung.width === encode.width && rung.height === encode.height
183
+ ) ?? budget.rungIndex;
184
+ return {
185
+ ...budget,
186
+ width: encode.width,
187
+ height: encode.height,
188
+ preset: encode.preset,
189
+ rungIndex: rungIndex >= 0 ? rungIndex : budget.rungIndex
190
+ };
191
+ };
173
192
  // Every format at one size this viewer could be given, highest limit first.
174
193
  // A hardware encoder is given no limit, so it has exactly one.
175
194
  const formatsAt = (w, h) => {
@@ -220,12 +239,28 @@ export function decideOutputFormat({
220
239
  }
221
240
  }
222
241
 
223
- let anySizeHeld = machineCan(width, height);
224
- if (anySizeHeld) {
242
+ let anySizeHeld = false;
243
+ let machineRefusal = null;
244
+ const admittedByMachine = (spec) => {
245
+ if (!machineCan(spec.video?.encode?.width ?? 0, spec.video?.encode?.height ?? 0)) {
246
+ return false;
247
+ }
248
+ const answer = typeof machineAdmission === "function" ? machineAdmission(spec) : null;
249
+ if (answer?.admitted === false) {
250
+ machineRefusal ??= answer;
251
+ return false;
252
+ }
253
+ return true;
254
+ };
255
+ if (machineCan(width, height)) {
225
256
  for (const spec of wantedFormats) {
257
+ if (!admittedByMachine(spec)) {
258
+ continue;
259
+ }
260
+ anySizeHeld = true;
226
261
  const answer = judge(spec);
227
262
  if (answer.admitted) {
228
- return given(spec, answer, { budget, wantedKey });
263
+ return given(spec, answer, { budget: budgetFor(spec), wantedKey });
229
264
  }
230
265
  }
231
266
  }
@@ -234,11 +269,14 @@ export function decideOutputFormat({
234
269
  if (!machineCan(rung.width, rung.height)) {
235
270
  continue;
236
271
  }
237
- anySizeHeld = true;
238
272
  for (const spec of formatsAt(rung.width, rung.height)) {
273
+ if (!admittedByMachine(spec)) {
274
+ continue;
275
+ }
276
+ anySizeHeld = true;
239
277
  const answer = judge(spec);
240
278
  if (answer.admitted) {
241
- return given(spec, answer, { wantedKey });
279
+ return given(spec, answer, { budget: budgetFor(spec), wantedKey });
242
280
  }
243
281
  }
244
282
  }
@@ -247,10 +285,21 @@ export function decideOutputFormat({
247
285
  // Not the viewer's link: this machine. Said apart, because the answer to
248
286
  // it is another proxy, found before anything plays.
249
287
  return {
250
- ...refused(wantedSpec, null, `this machine has not shown it can re-encode this picture at ${width}x${height}@${fps} or any size below it`, { budget, wantedKey }),
288
+ ...refused(
289
+ wantedSpec,
290
+ null,
291
+ machineRefusal?.reason ?? `this machine has not shown it can re-encode this picture at ${width}x${height}@${fps} or any size below it`,
292
+ { budget, wantedKey }
293
+ ),
251
294
  unavailable: {
252
- reason: `this machine has not shown it can re-encode this picture at ${width}x${height}@${fps} or any size below it`,
253
- figures: { width, height, fps, encoder: encoder.name },
295
+ reason: machineRefusal?.reason ?? `this machine has not shown it can re-encode this picture at ${width}x${height}@${fps} or any size below it`,
296
+ figures: {
297
+ width,
298
+ height,
299
+ fps,
300
+ encoder: encoder.name,
301
+ ...(Number.isFinite(machineRefusal?.speedX) ? { speedX: Number(machineRefusal.speedX.toFixed(3)) } : {})
302
+ },
254
303
  bound: "machine"
255
304
  }
256
305
  };