@torrent-tv/proxy 2.107.50 → 2.107.51

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.50",
3
+ "version": "2.107.51",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -154,7 +154,7 @@ export class ViewerRequests {
154
154
  #forecastPieces = new WeakMap();
155
155
 
156
156
  /**
157
- * @param {object} host - `opening`, `activeOutputFor`, `viewerSecondsOn`, `minimumBufferSecondsFor`, `getSourceStats`, `planEncodersSoon`, `encodeRuns`, `encodeSpeedReadingsOf`, `outputTimes`, `outputs`, `lookaheadSeconds`, `quality`, `qualityOffer`, `renditions`, `segmentDurationSec`, `segmentStore`, `sourceFiles`, `startupWaitMs`, `viewers`, `acceptsGeneration`, `generationOfRequest`, `noteGivenOutput`, `holdForResponse`, `noteServingVerdict`, `servingVerdictOf`
157
+ * @param {object} host - `opening`, `activeOutputFor`, `viewerSecondsOn`, `minimumBufferSecondsFor`, `getSourceStats`, `planEncodersSoon`, `encodeRuns`, `encodeSpeedReadingsOf`, `outputs`, `lookaheadSeconds`, `quality`, `qualityOffer`, `renditions`, `segmentDurationSec`, `segmentStore`, `sourceFiles`, `startupWaitMs`, `viewers`, `placeViewerOn`, `chooseAudioTrack`, `acceptsGeneration`, `generationOfRequest`, `noteGivenOutput`, `holdForResponse`, `noteServingVerdict`, `servingVerdictOf`
158
158
  */
159
159
  constructor(host) {
160
160
  this.#host = host;
@@ -351,6 +351,7 @@ export class ViewerRequests {
351
351
  // Whether they were already on the output this request lands on, read at
352
352
  // the moment they are put on it.
353
353
  let alreadyOn = false;
354
+ let placedHere = false;
354
355
  // THE PLACE ON THIS MACHINE, AND THE VIEWER IN IT, in one synchronous call
355
356
  // the opening makes the moment it has the output (roadmap item 97, step 14).
356
357
  // Asked and placed together, so the next viewer to open anything is asked
@@ -367,7 +368,7 @@ export class ViewerRequests {
367
368
  : this.#host.admitsWatching(candidate);
368
369
  if (answer.admitted) {
369
370
  known = this.#host.viewers.of(candidate, consumerId);
370
- this.placeViewer(candidate, known, startPositionSeconds);
371
+ placedHere = this.placeViewerOn(candidate, consumerId, startPositionSeconds) === true;
371
372
  }
372
373
  return answer;
373
374
  }
@@ -385,7 +386,7 @@ export class ViewerRequests {
385
386
  // browser may need a track re-encoded that the first viewer's could
386
387
  // decode as it stands.
387
388
  const viewer = this.#host.viewers.of(output, consumerId);
388
- viewer.audio = audio;
389
+ this.#host.chooseAudioTrack(output, consumerId, audio);
389
390
  // The picture as their page sees it, which bounds the height made for
390
391
  // them from here on (roadmap item 98).
391
392
  viewer.noteVisiblePicture(request.visiblePicture);
@@ -400,7 +401,7 @@ export class ViewerRequests {
400
401
  : null,
401
402
  bufferedAheadSec: request.bufferedAheadSec,
402
403
  bufferLimitSeconds: request.bufferLimitSeconds,
403
- positionSeconds: startPositionSeconds,
404
+ ...(placedHere ? { positionSeconds: startPositionSeconds } : {}),
404
405
  waiting: true,
405
406
  playing: false,
406
407
  visiblePicture: request.visiblePicture
@@ -438,65 +439,24 @@ export class ViewerRequests {
438
439
  }
439
440
 
440
441
  /**
441
- * Put a viewer where their own request says they are.
442
+ * Ask the viewer layer to place this viewer at their initial position.
442
443
  *
443
- * A viewer arrives by asking for a POSITION — zero, or the time an address
444
- * bar carried — so "we do not know where they are" is not a state a viewer
445
- * can be in. Before 2026-09-05 it was: position was written only by a segment
446
- * request, so a viewer counted as placeless until they had asked for a
447
- * segment, and an output whose viewers were all placeless had every encoder
448
- * on it stopped for having nobody. The soundtrack that failed that day could
449
- * not have asked: the segment it would have asked for needed an `init.mp4`
450
- * that the stopped encoder was going to make.
451
- *
452
- * Only ever places a viewer who has none. A viewer already placed is being
453
- * kept current by their own requests and seeks, and a fresh create request
454
- * carries a default of zero that must not drag them back to the beginning.
455
- *
456
- * @param {HlsSession} session
457
- * @param {import("../viewer/Viewer.js").Viewer} viewer
458
- * @param {number} positionSeconds
459
- * @returns {void}
460
- */
461
- /**
462
- * Put a viewer named by id onto this output, where they have no position yet.
463
- *
464
- * The same operation as {@link placeViewer}, addressed the way another
465
- * component may address it: by name. Encoding asks for this when a request
466
- * for any file of an output is the first sign of the person behind it, and it
467
- * must not be handed the viewer to do it — a viewer's record has one writer,
468
- * and that is the viewer layer.
444
+ * Encoding asks for this when a request for any file of an output is the
445
+ * first sign of the person behind it. The operation takes the viewer's name
446
+ * and position; the viewer layer owns the record and its idempotency rule.
469
447
  *
470
448
  * @param {object} output
471
449
  * @param {string} consumerId
472
450
  * @param {number} positionSeconds
473
- * @returns {void}
451
+ * @returns {boolean} Whether the viewer layer placed the viewer.
474
452
  */
475
453
  placeViewerOn(output, consumerId, positionSeconds) {
476
454
  if (!output || !consumerId) {
477
- return;
478
- }
479
- const viewer = this.#host.viewers.join(output, consumerId);
480
- if (viewer) this.placeViewer(output, viewer, positionSeconds);
481
- }
482
-
483
- placeViewer(session, viewer, positionSeconds) {
484
- if (viewer.position !== null) {
485
- return;
486
- }
487
- const seconds = Number.isFinite(positionSeconds) && positionSeconds > 0 ? positionSeconds : 0;
488
- let segment = 0;
489
- try {
490
- const index = this.#host.outputTimes.segmentIndexForTime(session, seconds);
491
- if (Number.isInteger(index) && index >= 0) {
492
- segment = index;
493
- }
494
- } catch {
495
- // A session whose cut table is not built yet places its viewer at the
496
- // beginning, which is where the run starts anyway.
455
+ return false;
497
456
  }
498
- viewer.position = { segment, seconds, at: Date.now(), seeked: null };
499
- this.#host.planEncodersSoon();
457
+ const placed = this.#host.placeViewerOn(output, consumerId, positionSeconds) === true;
458
+ if (placed) this.#host.planEncodersSoon();
459
+ return placed;
500
460
  }
501
461
 
502
462
  /**
@@ -51,6 +51,7 @@ import {
51
51
  noteStepBeingWarmed,
52
52
  noteStepOnScreen,
53
53
  outputsBeingPrepared,
54
+ placeInitially,
54
55
  placeOn,
55
56
  presentOn,
56
57
  qualityModeOf,
@@ -682,8 +683,11 @@ export function wireOutputs({
682
683
  return video + audio;
683
684
  },
684
685
  get opening() { return parts.opening; },
685
- get outputTimes() { return parts.outputTimes; },
686
686
  get outputs() { return parts.outputs; },
687
+ placeViewerOn: (output, consumerId, seconds, now) =>
688
+ placeInitially(parts.viewers, output, consumerId, seconds, now),
689
+ chooseAudioTrack: (output, consumerId, choice) =>
690
+ chooseAudioTrack(parts.viewers, output, consumerId, choice),
687
691
  get lookaheadSeconds() { return parts.lookaheadSeconds; },
688
692
  get quality() { return parts.quality; },
689
693
  get qualityOffer() { return parts.qualityOffer; },
@@ -427,6 +427,22 @@ export class Viewer {
427
427
  this.bufferedSeconds = 0;
428
428
  }
429
429
 
430
+ /**
431
+ * Place this viewer at the position supplied by their first output opening.
432
+ * An opening that reuses an output must not reset a position already stated
433
+ * by this viewer. Initial placement has none of the buffer or pause effects
434
+ * of an explicit seek.
435
+ *
436
+ * @param {number} seconds
437
+ * @param {number} [now]
438
+ * @returns {boolean} Whether this call placed the viewer.
439
+ */
440
+ placeInitially(seconds, now = Date.now()) {
441
+ if (this.position !== null) return false;
442
+ this.#place(Number.isFinite(seconds) && seconds > 0 ? seconds : 0, now);
443
+ return true;
444
+ }
445
+
430
446
  /**
431
447
  * Where they are, stated — without anything a move to another place means.
432
448
  *
@@ -96,10 +96,26 @@ export function audioChoiceOf(viewers, output, consumerId) {
96
96
  * @returns {void}
97
97
  */
98
98
  export function chooseAudioTrack(viewers, output, consumerId, choice) {
99
+ if (!choice) return;
99
100
  const viewer = viewers.join(output, consumerId);
100
101
  if (viewer) viewer.audio = { trackIndex: choice.trackIndex, transcode: choice.transcode === true };
101
102
  }
102
103
 
104
+ /**
105
+ * Place a viewer at the initial position named by an output opening.
106
+ *
107
+ * @param {object} viewers
108
+ * @param {object} output
109
+ * @param {string} consumerId
110
+ * @param {number} seconds
111
+ * @param {number} [now]
112
+ * @returns {boolean} Whether this call placed the viewer.
113
+ */
114
+ export function placeInitially(viewers, output, consumerId, seconds, now = Date.now()) {
115
+ const viewer = viewers.join(output, consumerId, now);
116
+ return viewer?.placeInitially(seconds, now) ?? false;
117
+ }
118
+
103
119
  /**
104
120
  * The step on this viewer's screen, as a session id, or null for the picture
105
121
  * itself.