@torrent-tv/proxy 2.101.18 → 2.101.20

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.101.18",
3
+ "version": "2.101.20",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -43,8 +43,11 @@ export class Renditions {
43
43
  */
44
44
  #moveWaitsSaid = new Map();
45
45
 
46
+ /** Viewers waiting for a preset's measured speed before a quality step up. @type {Set<string>} */
47
+ #presetSpeedWaitSaid = new Set();
48
+
46
49
  /**
47
- * @param {object} host - `viewerSecondsOn`, `audioStartSecondsFor`, `activeOutputFor`, `viewersOf`, `audioRenditionName`, `logger`, `placeViewer`, `viewerLeaves`, `createOrGetSession`, `planEncodersSoon`, `disposeSession`, `viewerPositionOf`, `encodeRuns`, `fileStartTimeReads`, `getCachedAudioTracks`, `getCachedMediaInfo`, `getContainerMediaInfo`, `localBaseUrl`, `outputTimes`, `outputs`, `quality`, `qualityOffer`, `segmentDurationSec`, `sourceFiles`, `viewers`, `generationOfRequest`, `givenOutputOf`, `noteGivenOutput`, `chosenOutputOf`, `chooseOutput`, `storedPieceReady`, `headersCompatible`, `audioChoiceOf`, `linkMbpsOf`, `qualityModeOf`, `noteServingVerdict`, `limitsFor`, `heightsChosenAs`, `highestGivenSegmentOf`, `sameHeightSwitchOf`, `noteSameHeightSwitch`, `switchingOnto`, `switchingFrom`, `segmentClosed`, `secondsUntilClosed`, `invalidateWaits`, `bufferedSecondsOf`, `minimumBufferSecondsFor`
50
+ * @param {object} host - `viewerSecondsOn`, `audioStartSecondsFor`, `activeOutputFor`, `viewersOf`, `audioRenditionName`, `logger`, `placeViewer`, `viewerLeaves`, `createOrGetSession`, `planEncodersSoon`, `disposeSession`, `viewerPositionOf`, `encodeRuns`, `fileStartTimeReads`, `getCachedAudioTracks`, `getCachedMediaInfo`, `getContainerMediaInfo`, `localBaseUrl`, `outputTimes`, `outputs`, `quality`, `qualityOffer`, `segmentDurationSec`, `sourceFiles`, `viewers`, `generationOfRequest`, `givenOutputOf`, `noteGivenOutput`, `chosenOutputOf`, `chooseOutput`, `storedPieceReady`, `headersCompatible`, `audioChoiceOf`, `linkMbpsOf`, `qualityModeOf`, `noteServingVerdict`, `limitsFor`, `heightsChosenAs`, `highestGivenSegmentOf`, `sameHeightSwitchOf`, `noteSameHeightSwitch`, `switchingOnto`, `switchingFrom`, `segmentClosed`, `secondsUntilClosed`, `invalidateWaits`, `bufferedSecondsOf`, `minimumBufferSecondsFor`, `processingSpeedOf`, `requiredSpeedOf`
48
51
  */
49
52
  constructor(host) {
50
53
  this.#host = host;
@@ -1052,6 +1055,25 @@ export class Renditions {
1052
1055
  return;
1053
1056
  }
1054
1057
  }
1058
+ // A quality step up may choose a slower encoding preset. The benchmark is
1059
+ // only a forecast; keep the current output until this candidate has closed
1060
+ // pieces at the source's required speed. A step down is urgent and must
1061
+ // not wait on a measurement from the output meant to rescue playback.
1062
+ if (move.direction === "up" && move.changes === "preset" && prepared.spec?.transcodesVideo) {
1063
+ const speed = this.#host.processingSpeedOf?.(prepared) ?? null;
1064
+ const required = this.#host.requiredSpeedOf?.(prepared) ?? 1;
1065
+ if (!Number.isFinite(speed) || speed < required) {
1066
+ if (!this.#presetSpeedWaitSaid.has(consumerId)) {
1067
+ this.#presetSpeedWaitSaid.add(consumerId);
1068
+ this.#host.logger.info(
1069
+ `transcode ${base.id} keeps ${consumerId} on ${current.outputKey} while ${prepared.outputKey} ` +
1070
+ `proves its preset at ${Number.isFinite(speed) ? `${speed.toFixed(2)}x` : "an unmeasured speed"}; ` +
1071
+ `requires ${required.toFixed(2)}x`
1072
+ );
1073
+ }
1074
+ return;
1075
+ }
1076
+ }
1055
1077
  // The player fetches the fMP4 init once and decodes every later fragment
1056
1078
  // against it. Changing bitrate outputs is safe only when the prepared
1057
1079
  // output's init says the same thing to that decoder. The new segment is
@@ -1151,6 +1173,7 @@ export class Renditions {
1151
1173
  */
1152
1174
  #completeSameHeightSwitch(base, consumerId, move, prepared, current, next) {
1153
1175
  this.#moveWaitsSaid.delete(consumerId);
1176
+ this.#presetSpeedWaitSaid.delete(consumerId);
1154
1177
  for (const height of move.askedHeights) {
1155
1178
  this.#choose(consumerId, height, prepared, base);
1156
1179
  }
@@ -1189,6 +1212,7 @@ export class Renditions {
1189
1212
  */
1190
1213
  #cancelSameHeightSwitch(base, consumerId, reason) {
1191
1214
  this.#moveWaitsSaid.delete(consumerId);
1215
+ this.#presetSpeedWaitSaid.delete(consumerId);
1192
1216
  const move = this.#host.sameHeightSwitchOf(consumerId);
1193
1217
  if (!move) {
1194
1218
  return;
@@ -430,7 +430,10 @@ export class QualityController {
430
430
  // it is read as a copy.
431
431
  const unchanged = standing && standingBefore &&
432
432
  standing.height === standingBefore.height && standing.at === standingBefore.at;
433
- if (unchanged && !this.#affirmed.has(consumerId)) {
433
+ const urgentMachineDown = unchanged && standing.urgent === true &&
434
+ standing.reason.startsWith("machine-bound ");
435
+ const machineRecovered = !urgentMachineDown || this.#roomToSpare(session, consumerId);
436
+ if (unchanged && !this.#affirmed.has(consumerId) && machineRecovered) {
434
437
  this.#host.dropAskOf(base, consumerId);
435
438
  logger.info(
436
439
  `[budget] transcode ${base.id} no longer asks ${consumerId} for ${standing.height}p: ` +
@@ -457,6 +457,7 @@ export function wireOutputs({
457
457
  presetLadder: () => (parts.encoders.current?.kind === "software" && Array.isArray(parts.softwarePresetBenchmark)
458
458
  ? parts.softwarePresetBenchmark.map((entry) => entry.preset) : []),
459
459
  presetSpeedOf: (output, preset) => parts.encodeCost.presetSpeedOf(output, preset, { segmentSeconds: parts.segmentDurationSec }),
460
+ processingSpeedOf: (output) => parts.encodeCost.latestSpeedReadingOf(output)?.speed ?? null,
460
461
  // The price a move was decided at, given to an output with no measured
461
462
  // piece: one already measured stands on its readings, and an output that
462
463
  // runs was priced with its own cost counted beside the one on screen.