@torrent-tv/proxy 2.100.9 → 2.100.11

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.100.9",
3
+ "version": "2.100.11",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -36,7 +36,15 @@ export class Renditions {
36
36
  #host;
37
37
 
38
38
  /**
39
- * @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`, `segmentClosed`, `bufferedSecondsOf`, `minimumBufferSecondsFor`
39
+ * The segment each viewer's move was last said to wait for on the output
40
+ * they are leaving, so that a wait is said once per segment.
41
+ *
42
+ * @type {Map<string, number>}
43
+ */
44
+ #moveWaitsSaid = new Map();
45
+
46
+ /**
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`, `bufferedSecondsOf`, `minimumBufferSecondsFor`
40
48
  */
41
49
  constructor(host) {
42
50
  this.#host = host;
@@ -836,6 +844,14 @@ export class Renditions {
836
844
  for (const consumerId of this.#host.switchingOnto(output)) {
837
845
  this.#reconsiderSameHeightSwitch(base, consumerId, index);
838
846
  }
847
+ // A segment closed on the output a viewer is LEAVING may be the one their
848
+ // move waits for (`#givenAndOpenOn`). It is behind the segment the move
849
+ // begins at by construction, so it is asked about with no index.
850
+ for (const consumerId of this.#host.switchingFrom?.(output) ?? []) {
851
+ if (this.#host.activeOutputFor({ base, consumerId, outputs: this.#host.outputs }) === output) {
852
+ this.#reconsiderSameHeightSwitch(base, consumerId, null);
853
+ }
854
+ }
839
855
  }
840
856
  }
841
857
 
@@ -894,6 +910,28 @@ export class Renditions {
894
910
  }
895
911
  }
896
912
 
913
+ /**
914
+ * The segment just before the one a move would begin at, when it was given
915
+ * to this viewer from the output on their screen and is not closed there:
916
+ * the segment their held request waits for. Null otherwise.
917
+ *
918
+ * @param {HlsSession | null} current - The output on their screen.
919
+ * @param {string} consumerId
920
+ * @param {number[]} askedHeights
921
+ * @param {number} next - The segment the move would begin at.
922
+ * @returns {number | null}
923
+ */
924
+ #givenAndOpenOn(current, consumerId, askedHeights, next) {
925
+ const segment = next - 1;
926
+ if (!current?.outputKey || segment < 0) {
927
+ return null;
928
+ }
929
+ const generation = this.#host.generationOfRequest(consumerId, Number.NaN);
930
+ const given = askedHeights.some((height) =>
931
+ this.#host.givenOutputOf(consumerId, generation, height, segment) === current.outputKey);
932
+ return given && !this.#host.segmentClosed(current.outputKey, segment) ? segment : null;
933
+ }
934
+
897
935
  /**
898
936
  * The segment this viewer will ask for next at these heights.
899
937
  *
@@ -967,6 +1005,24 @@ export class Renditions {
967
1005
  if (!this.#host.segmentClosed(prepared.outputKey ?? "", next)) {
968
1006
  return;
969
1007
  }
1008
+ // THE SEGMENT ALREADY GIVEN FROM THE OUTPUT ON SCREEN. A request is held by
1009
+ // the output it was given from, and leaving that output stops its encoder:
1010
+ // a segment given and not yet closed there would then be made by nobody.
1011
+ // On Home Assistant a viewer waiting for #2176 was moved from #2177 and
1012
+ // the picture stood for good (r71, torrent-tv/meta#178). The move waits for
1013
+ // that segment to be closed; an output whose encoding has failed for good
1014
+ // closes nothing, and its held request is the serving path's to answer.
1015
+ const open = this.#givenAndOpenOn(current, consumerId, move.askedHeights, next);
1016
+ if (open !== null && !this.#host.encodeRuns.hasFailed(current)) {
1017
+ if (this.#moveWaitsSaid.get(consumerId) !== open) {
1018
+ this.#moveWaitsSaid.set(consumerId, open);
1019
+ this.#host.logger.info(
1020
+ `transcode ${base.id} ${consumerId}'s move to ${move.outputKey} waits for #${open} on ` +
1021
+ `${current.outputKey}, given and not yet closed there`
1022
+ );
1023
+ }
1024
+ return;
1025
+ }
970
1026
  // THE CUSHION, for a move that is not urgent (roadmap item 98): a move up
971
1027
  // is made once the viewer holds the cushion this file needs, and their
972
1028
  // next report asks again until they do. A move DOWN is made at once — it
@@ -1026,6 +1082,7 @@ export class Renditions {
1026
1082
  * @returns {void}
1027
1083
  */
1028
1084
  #completeSameHeightSwitch(base, consumerId, move, prepared, current, next) {
1085
+ this.#moveWaitsSaid.delete(consumerId);
1029
1086
  for (const height of move.askedHeights) {
1030
1087
  this.#choose(consumerId, height, prepared, base);
1031
1088
  }
@@ -1063,6 +1120,7 @@ export class Renditions {
1063
1120
  * @returns {void}
1064
1121
  */
1065
1122
  #cancelSameHeightSwitch(base, consumerId, reason) {
1123
+ this.#moveWaitsSaid.delete(consumerId);
1066
1124
  const move = this.#host.sameHeightSwitchOf(consumerId);
1067
1125
  if (!move) {
1068
1126
  return;
@@ -502,21 +502,13 @@ export class QualityController {
502
502
  remainingSec: Number.isFinite(buffer?.positionSec) && session.file?.durationSeconds > 0
503
503
  ? Math.max(0, session.file.durationSeconds - buffer.positionSec) : null
504
504
  });
505
- // A viewer whose picture has not moved yet — at the start or after a seek —
506
- // is held for the material a draining buffer would run out of, while their
507
- // buffer grows: the trend says nothing about them. The same two reasons
508
- // then decide. Held this way on Home Assistant, a picture made at 0.92-0.96x
509
- // was moved to a faster setting 11 s after the start, which had waited 30 s
510
- // for it (r70, torrent-tv/meta#178).
511
- const held = !threat && buffer?.waiting === true;
512
- if (!threat && !held) {
505
+ if (!threat) {
513
506
  return false;
514
507
  }
515
- const why = held ? "their picture is held" : "their buffer is draining";
516
508
  // Whatever else happens, a step UP prepared for them is not what they need.
517
- this.#dropUpAsk(session, consumerId, why);
509
+ this.#dropUpAsk(session, consumerId, "their buffer is draining");
518
510
  if (this.#host.sameHeightSwitchDirection(consumerId) === "up") {
519
- this.#host.cancelSameHeightSwitch(consumerId, why);
511
+ this.#host.cancelSameHeightSwitch(consumerId, "their buffer is draining");
520
512
  }
521
513
  const report = this.#host.linkReportOf(session, consumerId);
522
514
  const observed = report ? await this.observedStreamMbps(session) : null;
@@ -532,8 +524,7 @@ export class QualityController {
532
524
  const reasonText =
533
525
  (linkShort ? `link=${report.linkMbps.toFixed(2)}Mbps stream=${observed.toFixed(2)}Mbps ` : "") +
534
526
  (machineShort ? `CPU-bound speed=${speed.toFixed(2)}x ` : "") +
535
- `buffer=${(buffer?.bufferedSec ?? 0).toFixed(1)}s ` +
536
- (held ? "held before it moves " : `empty in ${secondsToEmpty === null ? "?" : secondsToEmpty.toFixed(1)}s `) +
527
+ `buffer=${(buffer?.bufferedSec ?? 0).toFixed(1)}s empty in ${secondsToEmpty === null ? "?" : secondsToEmpty.toFixed(1)}s ` +
537
528
  `for ${consumerId}`;
538
529
  if (linkShort) {
539
530
  const move = await this.#host.prepareSameHeightSwitch(session, consumerId, "down", `viewer-link-bound ${reasonText}`);
@@ -60,6 +60,7 @@ import {
60
60
  stepBeingWarmedOf,
61
61
  stepBeingWarmedSinceOf,
62
62
  stepOnScreenOf,
63
+ switchingFrom,
63
64
  switchingOnto,
64
65
  watches
65
66
  } from "../viewer/choices.js";
@@ -450,6 +451,7 @@ export function wireOutputs({
450
451
  sameHeightSwitchOf: (consumerId) => sameHeightSwitchOf(parts.viewers, consumerId),
451
452
  noteSameHeightSwitch: (consumerId, value) => noteSameHeightSwitch(parts.viewers, consumerId, value),
452
453
  switchingOnto: (output) => switchingOnto(parts.viewers, output),
454
+ switchingFrom: (output) => switchingFrom(parts.viewers, output),
453
455
  // What a viewer holds, and the cushion this file needs before a move that
454
456
  // is not urgent is made (roadmap item 98).
455
457
  bufferedSecondsOf: (consumerId) => bufferedSecondsOf(parts.viewers, consumerId),
@@ -327,7 +327,7 @@ export function visiblePictureOf(viewers, output, consumerId) {
327
327
  * @param {object} output
328
328
  * @param {string} consumerId
329
329
  * @param {number} spanSec - The segment duration of the output they watch.
330
- * @returns {{ bufferedSec: number, slope: number | null, reportGapSec: number, positionSec: number | null, waiting: boolean } | null}
330
+ * @returns {{ bufferedSec: number, slope: number | null, reportGapSec: number } | null}
331
331
  * Null for nobody of that name; `slope` null while their reports do not yet
332
332
  * span one segment.
333
333
  */
@@ -339,9 +339,7 @@ export function bufferOf(viewers, output, consumerId, spanSec) {
339
339
  const trend = viewer.bufferTrend(spanSec);
340
340
  const held = Number.isFinite(viewer.bufferedSeconds) ? Math.max(0, viewer.bufferedSeconds) : 0;
341
341
  return { bufferedSec: held, slope: trend?.slope ?? null, reportGapSec: trend?.reportGapSec ?? 0,
342
- positionSec: viewer.positionSeconds() ?? null,
343
- // Blocked on material owed to them, with their picture not moving.
344
- waiting: viewer.waiting === true && viewer.playing !== true };
342
+ positionSec: viewer.positionSeconds() ?? null };
345
343
  }
346
344
 
347
345
  /**
@@ -567,6 +565,25 @@ export function switchingOnto(viewers, output) {
567
565
  return found;
568
566
  }
569
567
 
568
+ /**
569
+ * The present viewers on this output for whom a move onto ANOTHER output is
570
+ * being prepared: the ones a segment closed here may let go.
571
+ *
572
+ * @param {object} viewers
573
+ * @param {object} output
574
+ * @returns {string[]}
575
+ */
576
+ export function switchingFrom(viewers, output) {
577
+ const found = [];
578
+ for (const [consumerId, viewer] of viewers.forOutput(output)) {
579
+ const move = viewer.sameHeightSwitch;
580
+ if (viewer.isPresent() && move && move.outputKey !== output.outputKey) {
581
+ found.push(consumerId);
582
+ }
583
+ }
584
+ return found;
585
+ }
586
+
570
587
  /**
571
588
  * Every output a present viewer is being prepared onto, as ids.
572
589
  *