@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 +8 -0
- package/package.json +1 -1
- package/server.js +3 -1
- package/services/hls-session-manager.js +83 -0
- package/services/playback-planner.js +14 -1
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
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
|