@torrent-tv/proxy 2.100.7 → 2.100.9

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.7",
3
+ "version": "2.100.9",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -502,13 +502,21 @@ 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
- if (!threat) {
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) {
506
513
  return false;
507
514
  }
515
+ const why = held ? "their picture is held" : "their buffer is draining";
508
516
  // Whatever else happens, a step UP prepared for them is not what they need.
509
- this.#dropUpAsk(session, consumerId, "their buffer is draining");
517
+ this.#dropUpAsk(session, consumerId, why);
510
518
  if (this.#host.sameHeightSwitchDirection(consumerId) === "up") {
511
- this.#host.cancelSameHeightSwitch(consumerId, "their buffer is draining");
519
+ this.#host.cancelSameHeightSwitch(consumerId, why);
512
520
  }
513
521
  const report = this.#host.linkReportOf(session, consumerId);
514
522
  const observed = report ? await this.observedStreamMbps(session) : null;
@@ -524,7 +532,8 @@ export class QualityController {
524
532
  const reasonText =
525
533
  (linkShort ? `link=${report.linkMbps.toFixed(2)}Mbps stream=${observed.toFixed(2)}Mbps ` : "") +
526
534
  (machineShort ? `CPU-bound speed=${speed.toFixed(2)}x ` : "") +
527
- `buffer=${(buffer?.bufferedSec ?? 0).toFixed(1)}s empty in ${secondsToEmpty === null ? "?" : secondsToEmpty.toFixed(1)}s ` +
535
+ `buffer=${(buffer?.bufferedSec ?? 0).toFixed(1)}s ` +
536
+ (held ? "held before it moves " : `empty in ${secondsToEmpty === null ? "?" : secondsToEmpty.toFixed(1)}s `) +
528
537
  `for ${consumerId}`;
529
538
  if (linkShort) {
530
539
  const move = await this.#host.prepareSameHeightSwitch(session, consumerId, "down", `viewer-link-bound ${reasonText}`);
@@ -104,6 +104,17 @@ export class SegmentStore {
104
104
 
105
105
  /** Output key → how to read its file names. @type {Map<string, object>} */
106
106
  #formats = new Map();
107
+ /**
108
+ * The outputs whose served pieces this store names itself, in `publish`.
109
+ * Their record is complete by construction: a piece arrives through
110
+ * `publish` and leaves through this store's removals. Only an output whose
111
+ * muxer renames its own pieces into place (the `hls` branch, taken where
112
+ * there is no grid of cuts) can hold a piece the record has not been told
113
+ * of yet, and only there does `pathOf` look at the disk.
114
+ *
115
+ * @type {Set<string>}
116
+ */
117
+ #publishing = new Set();
107
118
  #reads = new Map();
108
119
 
109
120
  /** Hold physical material while a read is prepared or its stream is open. */
@@ -668,6 +679,7 @@ export class SegmentStore {
668
679
  }
669
680
  // Publication registers the material even after the previous directory was dropped.
670
681
  this.useFormat(key, format);
682
+ this.#publishing.add(key);
671
683
  // What the directory holds has changed by this one piece.
672
684
  if (index >= 0) this.#heldArrived(key, index);
673
685
  else this.#held.delete(key);
@@ -1135,10 +1147,15 @@ export class SegmentStore {
1135
1147
  this.#touched.set(key, this.#now());
1136
1148
  const contents = this.refresh(key);
1137
1149
  const found = contents.byNumber.get(index);
1138
- if (found || !contents.followed) return found ?? null;
1150
+ // An output this store publishes answers from its record alone. Looking
1151
+ // at the disk for every piece not made, as it did for every output, cost
1152
+ // the main thread 12.6 s of 300 s at a cold start on Home Assistant, the
1153
+ // forecast asking about each piece of the film on every poll (r61,
1154
+ // torrent-tv/meta#178).
1155
+ if (found || !contents.followed || this.#publishing.has(key)) return found ?? null;
1139
1156
  // A reading kept up piece by piece answers from what it was told, and a
1140
- // change the watch failed to deliver would leave a closed piece absent for
1141
- // good. The one file asked about is looked at — a `stat`, not a listing.
1157
+ // piece a muxer renamed into place may not have been told of yet. The one
1158
+ // file asked about is looked at — a `stat`, not a listing.
1142
1159
  this.#heldArrived(key, index);
1143
1160
  return contents.byNumber.get(index) ?? null;
1144
1161
  }
@@ -1178,6 +1195,7 @@ export class SegmentStore {
1178
1195
  }
1179
1196
  this.#held.delete(key);
1180
1197
  this.#formats.delete(key);
1198
+ this.#publishing.delete(key);
1181
1199
  this.#mediaRanges.delete(key);
1182
1200
  this.#touched.delete(key);
1183
1201
  this.#inits.delete(key);
@@ -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 } | null}
330
+ * @returns {{ bufferedSec: number, slope: number | null, reportGapSec: number, positionSec: number | null, waiting: boolean } | 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,7 +339,9 @@ 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 };
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 };
343
345
  }
344
346
 
345
347
  /**