@torrent-tv/proxy 2.9.101 → 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,7 @@
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
+
1
5
  ## 2.9.101
2
6
 
3
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.9.101",
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
@@ -165,7 +165,8 @@ export async function startProxyServer({ host, port, transcodeAudio, ffmpegBin,
165
165
  sourceRegistry,
166
166
  torrentPool,
167
167
  warmKeyframeIndex: (params) => hlsSessionManager.warmKeyframeIndex(params),
168
- expectedFirstSegmentMs: () => hlsSessionManager.expectedFirstSegmentMs()
168
+ expectedFirstSegmentMs: () => hlsSessionManager.expectedFirstSegmentMs(),
169
+ expectedSessionCreateMs: () => hlsSessionManager.expectedSessionCreateMs()
169
170
  });
170
171
 
171
172
  app.get("/health", async (req, reply) => handleHealthGet(req, reply, { version }));
@@ -808,6 +808,16 @@ export class HlsSessionManager {
808
808
  */
809
809
  #firstSegmentLatencies = [];
810
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
+
811
821
  /**
812
822
  * @param {HlsSessionManagerOptions} options
813
823
  */
@@ -1121,6 +1131,7 @@ export class HlsSessionManager {
1121
1131
  );
1122
1132
  });
1123
1133
  }
1134
+ this.#rememberSessionCreateLatency(Date.now() - createEntryMs);
1124
1135
  logger.info(
1125
1136
  `cold-start ${sessionId.slice(0, 8)}: media-info=${mediaInfoMs}ms (${mediaInfoSource}) ` +
1126
1137
  `keyframes=${keyframeMs === -1 ? "skipped" : keyframeMs === -2 ? "background" : `${keyframeMs}ms`} ` +
@@ -2750,6 +2761,30 @@ export class HlsSessionManager {
2750
2761
  * @param {number} latencyMs
2751
2762
  * @returns {void}
2752
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
+
2753
2788
  #rememberFirstSegmentLatency(latencyMs) {
2754
2789
  if (!Number.isFinite(latencyMs) || latencyMs <= 0) {
2755
2790
  return;
@@ -269,6 +269,7 @@ export function createPlaybackPlanner({
269
269
  // downloaded" and "a segment exists": until now it assumed the pipeline
270
270
  // merely keeps up with realtime, and showed 15 s where 3.8 s were left.
271
271
  expectedFirstSegmentMs,
272
+ expectedSessionCreateMs,
272
273
  // Optional. Called once the file's edges are downloaded, so the keyframe
273
274
  // index — which reads the same tail of the file — is fetched alongside the
274
275
  // codec probe instead of after it. Late-bound to the HLS session manager,
@@ -402,6 +403,7 @@ export function createPlaybackPlanner({
402
403
  const { audioCodec, videoCodec, container, durationSeconds, videoWidth, videoHeight, audioTracks, subtitleTracks } = probe;
403
404
  const codecsDetected = audioCodec.length > 0 || videoCodec.length > 0;
404
405
  const firstSegmentMs = expectedFirstSegmentMs?.() ?? null;
406
+ const sessionCreateMs = expectedSessionCreateMs?.() ?? null;
405
407
  logger.info(
406
408
  `plan ${sourceKey.slice(0, 8)}:${fileIndex} torrent-ready=${torrentReadyMs}ms ` +
407
409
  `file-edges=${edgesReadyMs - torrentReadyMs}ms probe=${Date.now() - planEntryMs - edgesReadyMs}ms ` +
@@ -430,7 +432,9 @@ export function createPlaybackPlanner({
430
432
  subtitleTracks: subtitleTracks ?? [],
431
433
  // What this host has recently taken to make a session's first segment.
432
434
  // Null until one has finished since startup.
433
- expectedFirstSegmentMs: firstSegmentMs
435
+ expectedFirstSegmentMs: firstSegmentMs,
436
+ // Second term of the browser's estimate; see research/playback-eta-2026-08-05.md.
437
+ expectedSessionCreateMs: sessionCreateMs
434
438
  };
435
439
  // Only cache a plan whose codecs were actually detected. An empty probe is
436
440
  // a "header not downloaded yet" signal, not a valid result — caching it