@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 +4 -0
- package/package.json +1 -1
- package/server.js +2 -1
- package/services/hls-session-manager.js +48 -0
- package/services/playback-planner.js +10 -1
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
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
|