@torrent-tv/proxy 2.61.0 → 2.62.0

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/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 2.62.0
2
+
3
+ - **New**: Each session says what its cushion actually is, every half minute: how many seconds of film are ready ahead of the EARLIEST viewer's picture, roughly how many megabytes that is off the swarm, and how deep a buffer the browsers say they are holding. The three were never printed together and could not be reconstructed afterwards from anything that was — so whether deepening the browser's buffer (2.61.0) protected anybody, and what it cost the person hosting the proxy, had no answer. Every term is measured: the produced range from the segments on disk, the picture from the viewers' own reports, the byte rate from the file's length over its duration. The read window sits on top of the megabytes figure, so it is a floor.
4
+
1
5
  ## 2.61.0
2
6
 
3
7
  - **New**: The proxy states how far ahead of the viewer it lets an encoder run — `lookaheadSeconds` on the session-create response. The browser sizes its forward buffer from it, so the two sides agree by construction instead of each carrying a figure of its own: the browser held 30 s, with a ceiling of 60, while this proxy keeps 120 s produced and suspends the encoder there. Three quarters of the protection against every interruption was left on the disk.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.61.0",
3
+ "version": "2.62.0",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "publishConfig": {
@@ -400,6 +400,10 @@ const READ_WINDOW_SECONDS = 30;
400
400
  const READ_WINDOW_MIN_BYTES = 16 * 1024 * 1024;
401
401
  const READ_WINDOW_MAX_BYTES = 96 * 1024 * 1024;
402
402
  const LOOKAHEAD_PAUSE_SECONDS = 120;
403
+ // How often each session says what its cushion is. Half a minute: the link
404
+ // reports that feed it arrive every ten seconds, and a line per session per
405
+ // ten seconds would drown the log on a host serving several.
406
+ const CUSHION_REPORT_MS = 30_000;
403
407
  // How old a viewer's link report may be and still describe where they are. It
404
408
  // is sent every 10 s, and a seek in between moves them somewhere this cannot
405
409
  // predict — so anything older is treated as no report at all.
@@ -2214,6 +2218,8 @@ export class HlsSessionManager {
2214
2218
  // buffer from another viewer's read head.
2215
2219
  /** @type {Map<string, { linkMbps: number, bufferedAheadSec: number, positionSeconds: number | null, at: number }>} */
2216
2220
  netReports: new Map(),
2221
+ // When this session last said what its cushion is (see #sayCushion).
2222
+ cushionSaidAt: 0,
2217
2223
  linkSlowSince: 0,
2218
2224
  sourceWidth,
2219
2225
  sourceHeight,
@@ -3339,6 +3345,7 @@ export class HlsSessionManager {
3339
3345
  // reached. Reported on its EDGES, because it is a state and not a stream.
3340
3346
  const claimed = Number(session.progress?.processedSeconds);
3341
3347
  const encodedTo = this.#segmentStartTime(session, viewerSegment) + aheadSeconds;
3348
+ this.#sayCushion(session, encodedTo);
3342
3349
  const disagrees =
3343
3350
  Number.isFinite(claimed) && Math.abs(claimed - encodedTo) > LOOKAHEAD_PAUSE_SECONDS;
3344
3351
  if (disagrees && !session.lookAheadDisagreementSince) {
@@ -3377,6 +3384,57 @@ export class HlsSessionManager {
3377
3384
  }
3378
3385
  }
3379
3386
 
3387
+ /**
3388
+ * What the cushion actually is, said once every half minute per session.
3389
+ *
3390
+ * Three quantities that were never printed together, and could not be
3391
+ * reconstructed afterwards from anything that was:
3392
+ *
3393
+ * - how far the produced range runs ahead of the EARLIEST viewer's picture,
3394
+ * which is the protection an interruption would have to exhaust before
3395
+ * anybody saw it;
3396
+ * - what that costs the person hosting this proxy, in megabytes of film
3397
+ * pulled off the swarm ahead of the picture — the read window sits on top
3398
+ * of it, so this is a floor;
3399
+ * - what the browsers say they are holding, so the depth asked for on that
3400
+ * side can be checked against the depth that arrived.
3401
+ *
3402
+ * Every term is measured: the produced range comes from the segments on disk,
3403
+ * the picture from the viewers' own reports, and the byte rate from the
3404
+ * file's length over its duration. Roadmap item 4.
3405
+ *
3406
+ * @param {HlsSession} session
3407
+ * @param {number} encodedTo - Seconds of film produced, contiguously, from
3408
+ * where the leading viewer is.
3409
+ * @returns {void}
3410
+ */
3411
+ #sayCushion(session, encodedTo) {
3412
+ const now = Date.now();
3413
+ if (now - (session.cushionSaidAt ?? 0) < CUSHION_REPORT_MS) {
3414
+ return;
3415
+ }
3416
+ const { earliestPosition, deepestBuffer, viewers } = this.#reportedPictureOf(session, now);
3417
+ // Nobody has said where they are, so there is no picture to measure
3418
+ // against and the line would be about nothing.
3419
+ if (earliestPosition === null) {
3420
+ return;
3421
+ }
3422
+ session.cushionSaidAt = now;
3423
+ const aheadOfPicture = Math.max(0, encodedTo - earliestPosition);
3424
+ const fileLength = this.#fileLengthByKey.get(`${session.sourceKey}:${session.fileIndex}`);
3425
+ const duration = Number(session.totalDurationSeconds) || Number(session.durationSeconds) || 0;
3426
+ const megabytes =
3427
+ Number.isFinite(fileLength) && fileLength > 0 && duration > 0
3428
+ ? ((aheadOfPicture * fileLength) / duration / 1e6).toFixed(0)
3429
+ : "?";
3430
+ logger.info(
3431
+ `transcode ${session.id.slice(0, 8)} cushion: ${Math.round(aheadOfPicture)}s of film ready ` +
3432
+ `ahead of the picture at ${Math.round(earliestPosition)}s (~${megabytes}MB pulled ahead), ` +
3433
+ `${viewers} viewer(s) holding up to ` +
3434
+ `${deepestBuffer === null ? "?" : deepestBuffer.toFixed(1)}s`
3435
+ );
3436
+ }
3437
+
3380
3438
  /**
3381
3439
  * Seconds of playback ready without a gap, starting at the segment the viewer
3382
3440
  * is on.
@@ -7714,21 +7772,29 @@ export class HlsSessionManager {
7714
7772
  * @param {HlsSession} base
7715
7773
  * @returns {number}
7716
7774
  */
7717
- #audioStartSecondsFor(base) {
7718
- const watching = this.#activeVariant(base);
7719
- const readHead = this.#viewerPositionOf(watching);
7720
- const now = Date.now();
7721
- let earliestStated = null;
7775
+ /**
7776
+ * Where the earliest viewer's picture is, and the deepest cushion any of them
7777
+ * reports holding — both read from the link reports, both null when nobody
7778
+ * has said recently.
7779
+ *
7780
+ * @param {HlsSession} session
7781
+ * @param {number} now
7782
+ * @returns {{ earliestPosition: number | null, deepestBuffer: number | null, viewers: number }}
7783
+ */
7784
+ #reportedPictureOf(session, now) {
7785
+ let earliestPosition = null;
7722
7786
  let deepestBuffer = null;
7723
- for (const report of watching.netReports.values()) {
7787
+ let viewers = 0;
7788
+ for (const report of session.netReports.values()) {
7724
7789
  if (now - report.at > NET_REPORT_FRESH_MS) {
7725
7790
  continue;
7726
7791
  }
7792
+ viewers += 1;
7727
7793
  if (Number.isFinite(report.positionSeconds)) {
7728
- earliestStated =
7729
- earliestStated === null
7794
+ earliestPosition =
7795
+ earliestPosition === null
7730
7796
  ? report.positionSeconds
7731
- : Math.min(earliestStated, report.positionSeconds);
7797
+ : Math.min(earliestPosition, report.positionSeconds);
7732
7798
  }
7733
7799
  if (Number.isFinite(report.bufferedAheadSec)) {
7734
7800
  deepestBuffer =
@@ -7737,6 +7803,14 @@ export class HlsSessionManager {
7737
7803
  : Math.max(deepestBuffer, report.bufferedAheadSec);
7738
7804
  }
7739
7805
  }
7806
+ return { earliestPosition, deepestBuffer, viewers };
7807
+ }
7808
+
7809
+ #audioStartSecondsFor(base) {
7810
+ const watching = this.#activeVariant(base);
7811
+ const readHead = this.#viewerPositionOf(watching);
7812
+ const now = Date.now();
7813
+ const { earliestPosition: earliestStated, deepestBuffer } = this.#reportedPictureOf(watching, now);
7740
7814
  if (earliestStated !== null) {
7741
7815
  // Never ahead of the read head: a position claiming to be past what has
7742
7816
  // been asked for is a report that arrived out of order, and acting on it