@torrent-tv/proxy 2.9.100 → 2.9.102

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,11 @@
1
+ ## 2.9.102
2
+
3
+ - **New**: The playback plan also reports what this host takes to CREATE a session — median of the last eight, 116-843 ms depending on whether the keyframe index is already in hand. It is the second term of the browser's end-to-end estimate, which is being rebuilt as a sum over the stages that have not happened yet rather than a choice between figures that each describe only one of them (`research/playback-eta-2026-08-05.md`).
4
+
5
+ ## 2.9.101
6
+
7
+ - **New**: The playback plan reports what this host takes to produce a session's first segment — the median of its last eight, measured from session-create to a servable segment (782-1518 ms on the field host). The browser needs it for the gap between "the file is downloaded" and "a segment exists", where until now it assumed the pipeline merely keeps up with realtime and therefore showed 15 s where 3.8 s were left. It is per-host, so a weak box and a fast one each answer for themselves.
8
+
1
9
  ## 2.9.100
2
10
 
3
11
  - **New**: A long wait for a piece now says who was working on it. The open question about a seek is that a single 8 MiB piece takes 3.0-4.6 s while the swarm as a whole moves 4-6 MB/s, so only about 2 MB/s reaches the piece being waited for — and whether that is because few peers hold it, few are being asked, or each is slow could not be told apart from outside. The line now carries the rate achieved on that piece and, sampled at its peak while waiting, how many connected peers had it, how many were asked, and how many blocks were in flight.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.9.100",
3
+ "version": "2.9.102",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "publishConfig": {
package/server.js CHANGED
@@ -164,7 +164,9 @@ export async function startProxyServer({ host, port, transcodeAudio, ffmpegBin,
164
164
  localBaseUrl: hlsSessionManager.localBaseUrl,
165
165
  sourceRegistry,
166
166
  torrentPool,
167
- warmKeyframeIndex: (params) => hlsSessionManager.warmKeyframeIndex(params)
167
+ warmKeyframeIndex: (params) => hlsSessionManager.warmKeyframeIndex(params),
168
+ expectedFirstSegmentMs: () => hlsSessionManager.expectedFirstSegmentMs(),
169
+ expectedSessionCreateMs: () => hlsSessionManager.expectedSessionCreateMs()
168
170
  });
169
171
 
170
172
  app.get("/health", async (req, reply) => handleHealthGet(req, reply, { version }));
@@ -800,6 +800,24 @@ function normalizeLogFileName(fileName, fileIndex) {
800
800
  * expired after {@link HlsSessionManagerOptions.sessionTtlMs} of idle time.
801
801
  */
802
802
  export class HlsSessionManager {
803
+ /**
804
+ * Recent times from session-create to a servable first segment, in ms.
805
+ * See #rememberFirstSegmentLatency.
806
+ *
807
+ * @type {number[]}
808
+ */
809
+ #firstSegmentLatencies = [];
810
+
811
+ /**
812
+ * Recent times to create a session, in ms — the second term of the browser's
813
+ * estimate. Measured for the same reason as the first: it is 116-843 ms
814
+ * depending on whether the keyframe index is already in hand, and guessing it
815
+ * was one of the ways the shown figure stopped describing the whole wait.
816
+ *
817
+ * @type {number[]}
818
+ */
819
+ #sessionCreateLatencies = [];
820
+
803
821
  /**
804
822
  * @param {HlsSessionManagerOptions} options
805
823
  */
@@ -1113,6 +1131,7 @@ export class HlsSessionManager {
1113
1131
  );
1114
1132
  });
1115
1133
  }
1134
+ this.#rememberSessionCreateLatency(Date.now() - createEntryMs);
1116
1135
  logger.info(
1117
1136
  `cold-start ${sessionId.slice(0, 8)}: media-info=${mediaInfoMs}ms (${mediaInfoSource}) ` +
1118
1137
  `keyframes=${keyframeMs === -1 ? "skipped" : keyframeMs === -2 ? "background" : `${keyframeMs}ms`} ` +
@@ -2727,6 +2746,69 @@ export class HlsSessionManager {
2727
2746
  return session.requestSeqCounter;
2728
2747
  }
2729
2748
 
2749
+ /**
2750
+ * Remember how long this host took to make a session's first segment.
2751
+ *
2752
+ * The browser has to answer "how long until playback" during the gap between
2753
+ * the file being downloaded and the first segment existing, and until now it
2754
+ * assumed the pipeline merely keeps up with realtime — which on the measured
2755
+ * session meant showing 15 s where 3.8 s were left, and showing it as a jump
2756
+ * UP from 5.5 s. This host knows the real figure because it has just done it
2757
+ * several times: 782 ms, 1052 ms, 1387 ms, 1518 ms on the sessions measured
2758
+ * 2026-08-04/05. A median of recent runs is a measurement, not an assumption,
2759
+ * and it is per-host, so a weak box and a fast one each get their own.
2760
+ *
2761
+ * @param {number} latencyMs
2762
+ * @returns {void}
2763
+ */
2764
+ #rememberSessionCreateLatency(latencyMs) {
2765
+ if (!Number.isFinite(latencyMs) || latencyMs <= 0) {
2766
+ return;
2767
+ }
2768
+ this.#sessionCreateLatencies.push(latencyMs);
2769
+ if (this.#sessionCreateLatencies.length > FIRST_SEGMENT_SAMPLES) {
2770
+ this.#sessionCreateLatencies.shift();
2771
+ }
2772
+ }
2773
+
2774
+ /**
2775
+ * What this host typically takes to create a session, in ms — the median of
2776
+ * recent ones, or null before any has finished.
2777
+ *
2778
+ * @returns {number | null}
2779
+ */
2780
+ expectedSessionCreateMs() {
2781
+ if (this.#sessionCreateLatencies.length === 0) {
2782
+ return null;
2783
+ }
2784
+ const sorted = [...this.#sessionCreateLatencies].sort((left, right) => left - right);
2785
+ return sorted[Math.floor(sorted.length / 2)];
2786
+ }
2787
+
2788
+ #rememberFirstSegmentLatency(latencyMs) {
2789
+ if (!Number.isFinite(latencyMs) || latencyMs <= 0) {
2790
+ return;
2791
+ }
2792
+ this.#firstSegmentLatencies.push(latencyMs);
2793
+ if (this.#firstSegmentLatencies.length > FIRST_SEGMENT_SAMPLES) {
2794
+ this.#firstSegmentLatencies.shift();
2795
+ }
2796
+ }
2797
+
2798
+ /**
2799
+ * What this host typically takes to produce a session's first segment, in
2800
+ * milliseconds — the median of recent runs, or null before any has finished.
2801
+ *
2802
+ * @returns {number | null}
2803
+ */
2804
+ expectedFirstSegmentMs() {
2805
+ if (this.#firstSegmentLatencies.length === 0) {
2806
+ return null;
2807
+ }
2808
+ const sorted = [...this.#firstSegmentLatencies].sort((left, right) => left - right);
2809
+ return sorted[Math.floor(sorted.length / 2)];
2810
+ }
2811
+
2730
2812
  /**
2731
2813
  * How many times the viewer has moved since this session started.
2732
2814
  *
@@ -2881,6 +2963,7 @@ export class HlsSessionManager {
2881
2963
  // — the time from session-create entry to a playable first segment.
2882
2964
  if (!isPlaylist && !session.firstSegmentLogged) {
2883
2965
  session.firstSegmentLogged = true;
2966
+ this.#rememberFirstSegmentLatency(Date.now() - session.createEntryMs);
2884
2967
  logger.info(
2885
2968
  `cold-start ${sessionId.slice(0, 8)}: first-segment ready +${Date.now() - session.createEntryMs}ms`
2886
2969
  );
@@ -264,6 +264,12 @@ export function createPlaybackPlanner({
264
264
  localBaseUrl,
265
265
  sourceRegistry,
266
266
  torrentPool,
267
+ // Optional. Reports what this host typically takes to produce a session's
268
+ // first segment. The browser needs it for the gap between "the file is
269
+ // downloaded" and "a segment exists": until now it assumed the pipeline
270
+ // merely keeps up with realtime, and showed 15 s where 3.8 s were left.
271
+ expectedFirstSegmentMs,
272
+ expectedSessionCreateMs,
267
273
  // Optional. Called once the file's edges are downloaded, so the keyframe
268
274
  // index — which reads the same tail of the file — is fetched alongside the
269
275
  // codec probe instead of after it. Late-bound to the HLS session manager,
@@ -396,6 +402,8 @@ export function createPlaybackPlanner({
396
402
  }
397
403
  const { audioCodec, videoCodec, container, durationSeconds, videoWidth, videoHeight, audioTracks, subtitleTracks } = probe;
398
404
  const codecsDetected = audioCodec.length > 0 || videoCodec.length > 0;
405
+ const firstSegmentMs = expectedFirstSegmentMs?.() ?? null;
406
+ const sessionCreateMs = expectedSessionCreateMs?.() ?? null;
399
407
  logger.info(
400
408
  `plan ${sourceKey.slice(0, 8)}:${fileIndex} torrent-ready=${torrentReadyMs}ms ` +
401
409
  `file-edges=${edgesReadyMs - torrentReadyMs}ms probe=${Date.now() - planEntryMs - edgesReadyMs}ms ` +
@@ -421,7 +429,12 @@ export function createPlaybackPlanner({
421
429
  videoHeight,
422
430
  // Full track inventory for the browser's audio/subtitle menus.
423
431
  audioTracks: audioTracks ?? [],
424
- subtitleTracks: subtitleTracks ?? []
432
+ subtitleTracks: subtitleTracks ?? [],
433
+ // What this host has recently taken to make a session's first segment.
434
+ // Null until one has finished since startup.
435
+ expectedFirstSegmentMs: firstSegmentMs,
436
+ // Second term of the browser's estimate; see research/playback-eta-2026-08-05.md.
437
+ expectedSessionCreateMs: sessionCreateMs
425
438
  };
426
439
  // Only cache a plan whose codecs were actually detected. An empty probe is
427
440
  // a "header not downloaded yet" signal, not a valid result — caching it