@torrent-tv/proxy 2.9.100 → 2.9.101

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.101
2
+
3
+ - **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.
4
+
1
5
  ## 2.9.100
2
6
 
3
7
  - **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.101",
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,8 @@ 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()
168
169
  });
169
170
 
170
171
  app.get("/health", async (req, reply) => handleHealthGet(req, reply, { version }));
@@ -800,6 +800,14 @@ 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
+
803
811
  /**
804
812
  * @param {HlsSessionManagerOptions} options
805
813
  */
@@ -2727,6 +2735,45 @@ export class HlsSessionManager {
2727
2735
  return session.requestSeqCounter;
2728
2736
  }
2729
2737
 
2738
+ /**
2739
+ * Remember how long this host took to make a session's first segment.
2740
+ *
2741
+ * The browser has to answer "how long until playback" during the gap between
2742
+ * the file being downloaded and the first segment existing, and until now it
2743
+ * assumed the pipeline merely keeps up with realtime — which on the measured
2744
+ * session meant showing 15 s where 3.8 s were left, and showing it as a jump
2745
+ * UP from 5.5 s. This host knows the real figure because it has just done it
2746
+ * several times: 782 ms, 1052 ms, 1387 ms, 1518 ms on the sessions measured
2747
+ * 2026-08-04/05. A median of recent runs is a measurement, not an assumption,
2748
+ * and it is per-host, so a weak box and a fast one each get their own.
2749
+ *
2750
+ * @param {number} latencyMs
2751
+ * @returns {void}
2752
+ */
2753
+ #rememberFirstSegmentLatency(latencyMs) {
2754
+ if (!Number.isFinite(latencyMs) || latencyMs <= 0) {
2755
+ return;
2756
+ }
2757
+ this.#firstSegmentLatencies.push(latencyMs);
2758
+ if (this.#firstSegmentLatencies.length > FIRST_SEGMENT_SAMPLES) {
2759
+ this.#firstSegmentLatencies.shift();
2760
+ }
2761
+ }
2762
+
2763
+ /**
2764
+ * What this host typically takes to produce a session's first segment, in
2765
+ * milliseconds — the median of recent runs, or null before any has finished.
2766
+ *
2767
+ * @returns {number | null}
2768
+ */
2769
+ expectedFirstSegmentMs() {
2770
+ if (this.#firstSegmentLatencies.length === 0) {
2771
+ return null;
2772
+ }
2773
+ const sorted = [...this.#firstSegmentLatencies].sort((left, right) => left - right);
2774
+ return sorted[Math.floor(sorted.length / 2)];
2775
+ }
2776
+
2730
2777
  /**
2731
2778
  * How many times the viewer has moved since this session started.
2732
2779
  *
@@ -2881,6 +2928,7 @@ export class HlsSessionManager {
2881
2928
  // — the time from session-create entry to a playable first segment.
2882
2929
  if (!isPlaylist && !session.firstSegmentLogged) {
2883
2930
  session.firstSegmentLogged = true;
2931
+ this.#rememberFirstSegmentLatency(Date.now() - session.createEntryMs);
2884
2932
  logger.info(
2885
2933
  `cold-start ${sessionId.slice(0, 8)}: first-segment ready +${Date.now() - session.createEntryMs}ms`
2886
2934
  );
@@ -264,6 +264,11 @@ 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,
267
272
  // Optional. Called once the file's edges are downloaded, so the keyframe
268
273
  // index — which reads the same tail of the file — is fetched alongside the
269
274
  // codec probe instead of after it. Late-bound to the HLS session manager,
@@ -396,6 +401,7 @@ export function createPlaybackPlanner({
396
401
  }
397
402
  const { audioCodec, videoCodec, container, durationSeconds, videoWidth, videoHeight, audioTracks, subtitleTracks } = probe;
398
403
  const codecsDetected = audioCodec.length > 0 || videoCodec.length > 0;
404
+ const firstSegmentMs = expectedFirstSegmentMs?.() ?? null;
399
405
  logger.info(
400
406
  `plan ${sourceKey.slice(0, 8)}:${fileIndex} torrent-ready=${torrentReadyMs}ms ` +
401
407
  `file-edges=${edgesReadyMs - torrentReadyMs}ms probe=${Date.now() - planEntryMs - edgesReadyMs}ms ` +
@@ -421,7 +427,10 @@ export function createPlaybackPlanner({
421
427
  videoHeight,
422
428
  // Full track inventory for the browser's audio/subtitle menus.
423
429
  audioTracks: audioTracks ?? [],
424
- subtitleTracks: subtitleTracks ?? []
430
+ subtitleTracks: subtitleTracks ?? [],
431
+ // What this host has recently taken to make a session's first segment.
432
+ // Null until one has finished since startup.
433
+ expectedFirstSegmentMs: firstSegmentMs
425
434
  };
426
435
  // Only cache a plan whose codecs were actually detected. An empty probe is
427
436
  // a "header not downloaded yet" signal, not a valid result — caching it