@torrent-tv/proxy 2.9.92 → 2.9.93
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 +6 -0
- package/package.json +1 -1
- package/services/hls-session-manager.js +74 -27
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## 2.9.93
|
|
2
|
+
|
|
3
|
+
- **Fix**: A seek could kill playback outright. Restarting at a position that lands exactly on a keyframe leaves a floating-point residue — `seekSeconds - snappedKeyframe` came out as `3.3333333249174757e-7` — and `String()` renders anything below 1e-6 in exponential notation, which ffmpeg's duration parser rejects: `Invalid duration for option ss`. The run died on startup, and from then on every segment request answered 500. Time arguments are now formatted in fixed notation, and a residue under a millisecond is dropped rather than passed on, because it is not a real offset.
|
|
4
|
+
- **Fix**: A session could never recover from a dead encoder. The "already covered by the running encode, not restarting" shortcut did not check that the run was alive, so once one had died `session.ffmpeg` still pointed at the corpse and every later seek was waved through as already covered. One ffmpeg failure therefore became a session that answered 500 for as long as the viewer kept trying.
|
|
5
|
+
- **Fix**: The look-ahead bound held the encoder back but did not keep it there. Any segment request released it, including a request for something produced ten minutes earlier, so it sawtoothed between suspended and running and drifted from 155 s to 922 s ahead of the viewer over three minutes. A request now re-evaluates the same condition the monitor uses instead of resuming outright.
|
|
6
|
+
|
|
1
7
|
## 2.9.92
|
|
2
8
|
|
|
3
9
|
- **Fix**: A seek acts on what the viewer asked for, instead of waiting out guards built for a signal that no longer exists. Three delays sat in front of every seek, all of them there because a far segment REQUEST used to steer the encoder and the player's playlist scan produced dozens of them. Requests stopped steering anything when the position became explicit, so what arrives now is only ever a position the viewer stated. The settle window drops from 1.2 s to 300 ms (the browser already collapses a drag into one report at 300 ms — this was a second debounce on an already-debounced signal, and it cost 1.2 s of every measured seek). The floor between restarts drops from 4 s to 500 ms, now a guard against a client spamming the endpoint rather than a policy about noise. And a run in progress is no longer protected for up to 30 s while it reaches its first segment: finishing a segment for a position the viewer has left is work nobody wants, and the hold could delay a genuine second seek by the whole grace. Measured cost of the old behaviour, 2026-08-04: two seeks 1.3 s apart produced two restarts 4.4 s apart, the first encoding 119.5 s of content before the second killed it.
|
package/package.json
CHANGED
|
@@ -573,6 +573,28 @@ async function probeVideoKeyframeTimes(ffmpegBin, inputUrl, timeoutMs = 25_000)
|
|
|
573
573
|
});
|
|
574
574
|
}
|
|
575
575
|
|
|
576
|
+
/**
|
|
577
|
+
* A number of seconds as ffmpeg will accept it.
|
|
578
|
+
*
|
|
579
|
+
* `String(n)` switches to exponential notation below 1e-6, and ffmpeg's
|
|
580
|
+
* duration parser rejects that outright: a field session died on
|
|
581
|
+
* `Invalid duration for option ss: 3.3333333249174757e-7`, after which the
|
|
582
|
+
* transcode was in state `failed` and every segment request answered 500 for
|
|
583
|
+
* as long as the viewer kept trying. Anything under a millisecond is also not a
|
|
584
|
+
* real offset — it is the residue of subtracting two nearly equal floats — so
|
|
585
|
+
* it is dropped rather than passed on.
|
|
586
|
+
*
|
|
587
|
+
* @param {number} value
|
|
588
|
+
* @returns {string}
|
|
589
|
+
*/
|
|
590
|
+
export function ffmpegSeconds(value) {
|
|
591
|
+
if (!Number.isFinite(value) || Math.abs(value) < 0.001) {
|
|
592
|
+
return "0";
|
|
593
|
+
}
|
|
594
|
+
// Microsecond resolution, fixed notation, no trailing zero noise.
|
|
595
|
+
return value.toFixed(6).replace(/\.?0+$/, "");
|
|
596
|
+
}
|
|
597
|
+
|
|
576
598
|
/**
|
|
577
599
|
* Compute segment START times (a 0-based timeline) for a session.
|
|
578
600
|
*
|
|
@@ -1588,24 +1610,41 @@ export class HlsSessionManager {
|
|
|
1588
1610
|
*/
|
|
1589
1611
|
#enforceLookAhead() {
|
|
1590
1612
|
for (const session of this.sessionsById.values()) {
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1613
|
+
this.#enforceLookAheadFor(session);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
/**
|
|
1618
|
+
* Decide whether one session's encoder should be running right now.
|
|
1619
|
+
*
|
|
1620
|
+
* Called both on the monitor's interval and the moment a segment is
|
|
1621
|
+
* requested. It must be the SAME decision in both places: an earlier version
|
|
1622
|
+
* simply resumed on any request, which meant a request for a segment produced
|
|
1623
|
+
* ten minutes ago released an encoder that had nothing left to do — measured
|
|
1624
|
+
* 2026-08-04, the encoder sawtoothed between suspended and running and drifted
|
|
1625
|
+
* from 135 s to 702 s ahead of the viewer while doing it.
|
|
1626
|
+
*
|
|
1627
|
+
* @param {HlsSession} session
|
|
1628
|
+
* @returns {void}
|
|
1629
|
+
*/
|
|
1630
|
+
#enforceLookAheadFor(session) {
|
|
1631
|
+
if (!session || session.state === "disposed" || !session.ffmpeg) {
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
const encodedTo = Number(session.progress?.processedSeconds);
|
|
1635
|
+
if (!Number.isFinite(encodedTo)) {
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
// Where the viewer is. Before the first segment request, the position the
|
|
1639
|
+
// run started at — so a session nobody has read from yet is bounded too.
|
|
1640
|
+
const viewerAt = Number.isInteger(session.lastRequestedSegment)
|
|
1641
|
+
? this.#segmentStartTime(session, session.lastRequestedSegment)
|
|
1642
|
+
: this.#segmentStartTime(session, session.encodeStartIndex ?? 0);
|
|
1643
|
+
const ahead = encodedTo - viewerAt;
|
|
1644
|
+
if (!session.encoderPaused && ahead > LOOKAHEAD_PAUSE_SECONDS) {
|
|
1645
|
+
this.#pauseEncoder(session, `${Math.round(ahead)}s ahead of the viewer`);
|
|
1646
|
+
} else if (session.encoderPaused && ahead <= LOOKAHEAD_RESUME_SECONDS) {
|
|
1647
|
+
this.#resumeEncoder(session, `${Math.round(ahead)}s ahead of the viewer`);
|
|
1609
1648
|
}
|
|
1610
1649
|
}
|
|
1611
1650
|
|
|
@@ -1933,17 +1972,17 @@ export class HlsSessionManager {
|
|
|
1933
1972
|
if (snappedKeyframe !== null) {
|
|
1934
1973
|
const residualSeconds = Math.max(0, seekSeconds - snappedKeyframe);
|
|
1935
1974
|
if (snappedKeyframe > 0) {
|
|
1936
|
-
args.push("-ss",
|
|
1975
|
+
args.push("-ss", ffmpegSeconds(snappedKeyframe));
|
|
1937
1976
|
}
|
|
1938
1977
|
args.push("-i", session.inputUrl);
|
|
1939
1978
|
if (residualSeconds > 0) {
|
|
1940
|
-
args.push("-ss",
|
|
1979
|
+
args.push("-ss", ffmpegSeconds(residualSeconds));
|
|
1941
1980
|
}
|
|
1942
1981
|
} else {
|
|
1943
1982
|
if (seekSeconds > 0) {
|
|
1944
1983
|
// No keyframe map (probe failed/timed out) — fall back to the previous
|
|
1945
1984
|
// behaviour: trust the container's own accurate seek.
|
|
1946
|
-
args.push("-accurate_seek", "-ss",
|
|
1985
|
+
args.push("-accurate_seek", "-ss", ffmpegSeconds(seekSeconds));
|
|
1947
1986
|
}
|
|
1948
1987
|
args.push("-i", session.inputUrl);
|
|
1949
1988
|
}
|
|
@@ -1952,7 +1991,7 @@ export class HlsSessionManager {
|
|
|
1952
1991
|
// segment grid; relabel output onto the original timeline so segment N
|
|
1953
1992
|
// carries PTS = N × segmentDuration.
|
|
1954
1993
|
if (startSeconds > 0) {
|
|
1955
|
-
args.push("-output_ts_offset",
|
|
1994
|
+
args.push("-output_ts_offset", ffmpegSeconds(startSeconds));
|
|
1956
1995
|
}
|
|
1957
1996
|
} else {
|
|
1958
1997
|
// Branch B (video copied — only audio is transcoded): we cannot insert
|
|
@@ -1964,7 +2003,7 @@ export class HlsSessionManager {
|
|
|
1964
2003
|
// beginning and desyncs audio/video). Audio is transcoded on this timeline.
|
|
1965
2004
|
args.push("-copyts");
|
|
1966
2005
|
if (sourceStartTime !== 0) {
|
|
1967
|
-
args.push("-output_ts_offset",
|
|
2006
|
+
args.push("-output_ts_offset", ffmpegSeconds(-sourceStartTime));
|
|
1968
2007
|
}
|
|
1969
2008
|
}
|
|
1970
2009
|
args.push(
|
|
@@ -2389,8 +2428,14 @@ export class HlsSessionManager {
|
|
|
2389
2428
|
: this.#segmentStartTime(session, head);
|
|
2390
2429
|
const currentSeg = Math.max(head, this.#segmentIndexForTime(session, processed));
|
|
2391
2430
|
// Already covered by the running encode — the data is on its way, so
|
|
2392
|
-
// restarting would only destroy work the viewer is waiting for.
|
|
2393
|
-
|
|
2431
|
+
// restarting would only destroy work the viewer is waiting for. The run has
|
|
2432
|
+
// to be ALIVE for that to hold: after a run died, `session.ffmpeg` still
|
|
2433
|
+
// pointed at the dead process and every later seek was waved through as
|
|
2434
|
+
// "already covered", so nothing could ever restart it. Measured 2026-08-04:
|
|
2435
|
+
// one ffmpeg failure turned into a session that answered 500 to every
|
|
2436
|
+
// segment for as long as the viewer kept trying.
|
|
2437
|
+
const runIsAlive = session.ffmpeg != null && !hasChildExited(session.ffmpeg);
|
|
2438
|
+
if (runIsAlive && index >= head && index <= currentSeg + MAX_LOOKAHEAD_SEGMENTS) {
|
|
2394
2439
|
logger.info(
|
|
2395
2440
|
`transcode ${session.id} seek to ${positionSeconds.toFixed(1)}s (#${index}) ` +
|
|
2396
2441
|
`already within the running encode (#${head}..#${currentSeg}) — not restarting`
|
|
@@ -2678,8 +2723,10 @@ export class HlsSessionManager {
|
|
|
2678
2723
|
const requested = session.segmentFormat.segmentIndexFromName(fileName);
|
|
2679
2724
|
if (requested >= 0) {
|
|
2680
2725
|
session.lastRequestedSegment = requested;
|
|
2681
|
-
// A viewer who has caught up must not wait out the monitor's interval
|
|
2682
|
-
|
|
2726
|
+
// A viewer who has caught up must not wait out the monitor's interval —
|
|
2727
|
+
// but only if they HAVE caught up, which is why this re-evaluates the
|
|
2728
|
+
// same condition instead of resuming outright.
|
|
2729
|
+
this.#enforceLookAheadFor(session);
|
|
2683
2730
|
}
|
|
2684
2731
|
}
|
|
2685
2732
|
try {
|