@torrent-tv/proxy 2.9.49 → 2.9.51
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 +12 -0
- package/package.json +2 -2
- package/services/hls-session-manager.js +177 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## 2.9.52
|
|
2
|
+
|
|
3
|
+
- **Chore**: `npm audit` fixes. `@fastify/static` 9.1.3 → 10.1.2 (fixes GHSA-83w8-p2f5-377r route-guard path-traversal bypass and GHSA-8pvw-jcv7-9cmj non-canonical-path authorization bypass — no API change to our usage, verified with a live smoke test: healthz, tunnel connect, and static registration all still work). `brace-expansion`/`fast-uri`/`find-my-way` bumped via `npm audit fix` (transitive, no direct dependency change). Residual: `ip` (via `webtorrent@2.8.5` → `torrent-discovery` → `bittorrent-tracker`) stays flagged high (GHSA-2p57-rm9w-gvfp / CVE-2024-29415, SSRF via `isPublic()` misclassification) — investigated and left as an accepted risk, not an oversight: the advisory has no upstream fix (`first_patched_version: null`, every published version of `ip` is flagged) and `npm audit fix --force`'s only offered fix is downgrading `webtorrent` to 0.7.3, which would reintroduce the exact download-freeze regressions 2.9.44 rolled back from 3.x to avoid. The only actual call site in our dependency tree (`bittorrent-tracker/lib/server/parse-udp.js`) uses `ip.toString()` for UDP-integer→string formatting in the tracker-SERVER's request parser — code we never execute (WebTorrent only uses `bittorrent-tracker` as a tracker CLIENT) — and the vulnerable function itself, `isPublic()`, is not called anywhere in the chain. Revisit if/when a maintained `ip` replacement lands upstream in `bittorrent-tracker`.
|
|
4
|
+
|
|
5
|
+
## 2.9.51
|
|
6
|
+
|
|
7
|
+
- **Fix**: The 2.9.50 keyframe-snap seek fix did not reliably apply on the re-encode path for containers needing a full packet scan (observed: AVI). The probe ran with a 6 s cap shared with the video-copy path (there it is fast, moov-index based); on a container needing a full scan, 6 s was not enough, the probe returned null, and the seek fell back to the raw (unsnapped) target — the exact case the circuit breaker exists to catch, not prevent. Split the two paths: video-copy keeps the blocking 6 s probe (segment boundaries need it before the first segment can be produced); video re-encode now runs the probe in the BACKGROUND with a full 25 s budget, since segment boundaries there are the uniform grid and never depend on it — only a later seek benefits from the snap. `#startEncodeRun` already reads `session.keyframeTimes` fresh on every call, so a seek arriving after the background probe resolves picks up the snap automatically; one arriving before still falls back to the existing circuit breaker (no regression). Verified live on the field AVI: far seek to the previously-hanging segment now returns in ~12 s instead of the ~90 s stall.
|
|
8
|
+
|
|
9
|
+
## 2.9.50
|
|
10
|
+
|
|
11
|
+
- **Fix**: Seeking could get stuck in an infinite restart loop on some containers (observed: AVI with VBR MP3 audio), producing nothing for ~90 s until the whole WebRTC session died — the on-screen symptom of "seeking does nothing." Root cause, two parts: (1) `-accurate_seek -ss X` before `-i` trusts the container's own on-the-fly seek/index to land near X; for this AVI it pointed at a position with no valid frame boundary at all, so ffmpeg failed outright ("Seek failed" / "Header missing") — not just imprecisely — and every retry re-tried the SAME bad container-computed position. (2) `#ensureEncodingFor`/`#fireSettledSeek` never checked for a `"failed"` session state, and `#startEncodeRun` unconditionally resets state back to `"starting"` on every call — so a failed run's next client poll silently re-armed and re-ran the identical failing seek, forever. Fixed both: the video-keyframe probe (previously only used for the copy path's segment boundaries) now also feeds a two-step seek — jump to the nearest REAL keyframe (a position ffmpeg has already proven it can decode, read directly from the packet list, not the container's live index) before `-i`, then trim the short residual precisely after `-i` (always frame-accurate, no reliance on `-accurate_seek`'s trust in the container). A circuit breaker caps consecutive fast failures (exits within 2 s — never did real work) at the SAME target to 3 before the session is left in its terminal `failed` state instead of looping — a different seek target still gets a fresh attempt budget. Verified: the keyframe-snap helper against synthetic data, and the breaker's state machine (3 attempts at one target → blocked, a different target → fresh budget, only 4 real ffmpeg spawns instead of an unbounded loop).
|
|
12
|
+
|
|
1
13
|
## 2.9.49
|
|
2
14
|
|
|
3
15
|
- **New**: `getSessionProgress` (the transcode-session progress endpoint) now also reports `outputMbps` — the observed produced bitrate from recently completed segment files (already computed internally for the viewer-link budget check, `#checkLinkBudget`), so the browser can turn its OWN measured link throughput into a delivery-speed multiplier for the unified download/transcode/delivery playback-start ETA, the same way the transcode's own `speed` already is one.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@torrent-tv/proxy",
|
|
3
|
-
"version": "2.9.
|
|
3
|
+
"version": "2.9.51",
|
|
4
4
|
"description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"publishConfig": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@fastify/cors": "^11.2.0",
|
|
22
22
|
"@fastify/helmet": "^13.0.2",
|
|
23
|
-
"@fastify/static": "^
|
|
23
|
+
"@fastify/static": "^10.1.2",
|
|
24
24
|
"@silentbot1/nat-api": "^0.4.9",
|
|
25
25
|
"chalk": "^5.4.1",
|
|
26
26
|
"commander": "^12.1.0",
|
|
@@ -72,6 +72,19 @@ const SEEK_SETTLE_MAX_MS = 2_500;
|
|
|
72
72
|
// escalation step: SIGTERM, then SIGKILL) before spawning its replacement into
|
|
73
73
|
// the same session directory. See #startEncodeRun.
|
|
74
74
|
const ENCODE_RUN_TERMINATE_GRACE_MS = 2_000;
|
|
75
|
+
// A seek-restart run that exits this fast never did real work — it failed at
|
|
76
|
+
// the seek/open step itself (container demux error, bad audio frame boundary,
|
|
77
|
+
// etc.), not mid-stream. Used to tell a genuine seek failure apart from a
|
|
78
|
+
// later, unrelated crash so the circuit breaker below only counts the former.
|
|
79
|
+
const SEEK_FAST_FAIL_MS = 2_000;
|
|
80
|
+
// Circuit breaker: consecutive fast failures AT THE SAME target before we stop
|
|
81
|
+
// auto-retrying and leave the session in its terminal "failed" state (surfaced
|
|
82
|
+
// to the client as a clean, retryable error) instead of looping forever. The
|
|
83
|
+
// keyframe-snap seek (see #startEncodeRun) already fixes the dominant failure
|
|
84
|
+
// mode (an unreliable container-computed seek position); this is a safety net
|
|
85
|
+
// for whatever residual case still fails — not a second competing "fix" that
|
|
86
|
+
// blindly retries the identical command hoping for a different result.
|
|
87
|
+
const MAX_SEEK_FAILURES = 3;
|
|
75
88
|
// Idle TTL: a session is disposed this long after the last segment/playlist
|
|
76
89
|
// access. Long enough that a viewer who pauses, backgrounds the tab, or briefly
|
|
77
90
|
// turns the phone off can resume WITHOUT a cold ffmpeg restart (the warm session
|
|
@@ -575,6 +588,27 @@ function computeSegmentBoundaries({ transcodeVideo, durationSeconds, segDur, key
|
|
|
575
588
|
return boundaries.length >= 2 ? boundaries : uniform();
|
|
576
589
|
}
|
|
577
590
|
|
|
591
|
+
/**
|
|
592
|
+
* The largest keyframe time that does not exceed `target`, from a SORTED
|
|
593
|
+
* (ascending) array of keyframe times such as {@link probeVideoKeyframeTimes}
|
|
594
|
+
* returns. Null when `target` is before the first keyframe or the array is
|
|
595
|
+
* empty — the caller then falls back to its unsnapped target.
|
|
596
|
+
*
|
|
597
|
+
* @param {number[]} keyframeTimes - Sorted ascending.
|
|
598
|
+
* @param {number} target
|
|
599
|
+
* @returns {number | null}
|
|
600
|
+
*/
|
|
601
|
+
function nearestKeyframeAtOrBefore(keyframeTimes, target) {
|
|
602
|
+
let result = null;
|
|
603
|
+
for (const time of keyframeTimes) {
|
|
604
|
+
if (time > target) {
|
|
605
|
+
break;
|
|
606
|
+
}
|
|
607
|
+
result = time;
|
|
608
|
+
}
|
|
609
|
+
return result;
|
|
610
|
+
}
|
|
611
|
+
|
|
578
612
|
function isWarmupTimeoutError(error) {
|
|
579
613
|
if (!(error instanceof Error)) {
|
|
580
614
|
return false;
|
|
@@ -621,6 +655,12 @@ function normalizeLogFileName(fileName, fileIndex) {
|
|
|
621
655
|
* @property {number} encodeRunGeneration - Bumped on every #startEncodeRun call;
|
|
622
656
|
* lets a call that awaited the previous ffmpeg's exit detect it was superseded
|
|
623
657
|
* by a newer restart request and abort instead of spawning a second process.
|
|
658
|
+
* @property {number[] | null} keyframeTimes - Real source keyframe times
|
|
659
|
+
* (sorted seconds), or null when the probe failed/timed out. Used to snap a
|
|
660
|
+
* source seek onto a known-valid position (see #startEncodeRun).
|
|
661
|
+
* @property {number} seekFailureTarget - Segment index of the last fast seek
|
|
662
|
+
* failure, for the consecutive-failure circuit breaker (see MAX_SEEK_FAILURES).
|
|
663
|
+
* @property {number} seekFailureCount - Consecutive fast failures at seekFailureTarget.
|
|
624
664
|
*/
|
|
625
665
|
|
|
626
666
|
/**
|
|
@@ -828,28 +868,71 @@ export class HlsSessionManager {
|
|
|
828
868
|
|
|
829
869
|
// For the video-copy path we cannot insert keyframes, so the playlist's
|
|
830
870
|
// segment boundaries must match the source's real keyframes (otherwise the
|
|
831
|
-
// player sees gaps on seek).
|
|
832
|
-
//
|
|
833
|
-
//
|
|
871
|
+
// player sees gaps on seek). Re-encoded video uses a uniform grid for
|
|
872
|
+
// segment boundaries instead (its fixed GOP makes the cuts land there —
|
|
873
|
+
// computeSegmentBoundaries ignores keyframeTimes when transcodeVideo).
|
|
874
|
+
//
|
|
875
|
+
// But the probe is ALSO used for something both branches need: choosing a
|
|
876
|
+
// SOURCE seek position ffmpeg can actually land on. `-ss` before `-i` trusts
|
|
877
|
+
// the container's own on-the-fly seek/index, which for some containers
|
|
878
|
+
// (observed: AVI with VBR MP3 audio) can point at a position with no valid
|
|
879
|
+
// frame boundary at all — ffmpeg then fails outright ("Seek failed" /
|
|
880
|
+
// "Header missing"), not just imprecisely. Snapping the seek to the nearest
|
|
881
|
+
// KNOWN real keyframe (see #startEncodeRun) avoids that. So probe for both
|
|
882
|
+
// branches; on failure both fall back to their current behaviour (uniform
|
|
883
|
+
// grid for boundaries, raw target for seeking) — no regression.
|
|
834
884
|
let keyframeTimes = null;
|
|
835
|
-
let keyframeMs = -1; // -1 = not run (skipped)
|
|
885
|
+
let keyframeMs = -1; // -1 = not run (skipped), -2 = running in the background
|
|
836
886
|
if (hasDuration && !transcodeVideo) {
|
|
837
|
-
//
|
|
838
|
-
//
|
|
839
|
-
//
|
|
887
|
+
// Video-COPY path: keyframeTimes are REQUIRED to build correct segment
|
|
888
|
+
// boundaries (the playlist itself), so this MUST block session creation —
|
|
889
|
+
// an incorrect playlist is worse than a slower start. Short timeout: mp4
|
|
890
|
+
// keyframes come from the moov index (fast); containers that force a full
|
|
891
|
+
// packet scan time out and fall back to a uniform grid, so this never adds
|
|
892
|
+
// more than ~6 s to session start.
|
|
840
893
|
const keyframeStartMs = Date.now();
|
|
841
894
|
keyframeTimes = await probeVideoKeyframeTimes(this.ffmpegBin, inputUrl.toString(), 6_000);
|
|
842
895
|
keyframeMs = Date.now() - keyframeStartMs;
|
|
843
896
|
if (!keyframeTimes) {
|
|
844
897
|
logger.warn(
|
|
845
898
|
`transcode ${sessionId}: keyframe probe unavailable; using uniform grid ` +
|
|
846
|
-
`for
|
|
899
|
+
`for "${logName}" (seek precision may be reduced)`
|
|
847
900
|
);
|
|
848
901
|
}
|
|
902
|
+
} else if (hasDuration && transcodeVideo) {
|
|
903
|
+
// Re-encode path: keyframeTimes are ONLY used to snap a LATER seek (see
|
|
904
|
+
// #startEncodeRun) — segment boundaries stay on the uniform grid either
|
|
905
|
+
// way. So this does NOT need to block session creation / the first
|
|
906
|
+
// segment's start. Run it in the background with a FULL budget instead of
|
|
907
|
+
// the 6 s cap: AVI-class containers need a full packet scan, which 6 s can
|
|
908
|
+
// never afford without delaying playback start — that starved budget is
|
|
909
|
+
// exactly why the probe kept missing on the container where the seek bug
|
|
910
|
+
// was field-diagnosed. #startEncodeRun reads session.keyframeTimes fresh
|
|
911
|
+
// on every call, so a seek that happens AFTER this finishes picks it up
|
|
912
|
+
// automatically; one that happens before falls back to the existing
|
|
913
|
+
// circuit breaker as a safety net (no regression either way).
|
|
914
|
+
keyframeMs = -2;
|
|
915
|
+
const backgroundStartedAt = Date.now();
|
|
916
|
+
void probeVideoKeyframeTimes(this.ffmpegBin, inputUrl.toString(), 25_000).then((times) => {
|
|
917
|
+
const liveSession = this.sessionsById.get(sessionId);
|
|
918
|
+
if (!liveSession || liveSession.state === "disposed") {
|
|
919
|
+
return; // Session gone before the probe finished — nothing to update.
|
|
920
|
+
}
|
|
921
|
+
liveSession.keyframeTimes = times;
|
|
922
|
+
const elapsedMs = Date.now() - backgroundStartedAt;
|
|
923
|
+
logger.info(
|
|
924
|
+
times
|
|
925
|
+
? `transcode ${sessionId}: background keyframe probe found ${times.length} keyframes ` +
|
|
926
|
+
`(${elapsedMs}ms) for "${logName}" — later seeks will snap to them`
|
|
927
|
+
: `transcode ${sessionId}: background keyframe probe unavailable (${elapsedMs}ms) for "${logName}" ` +
|
|
928
|
+
`— seeks keep using the raw target (falls back to the circuit breaker on failure)`
|
|
929
|
+
);
|
|
930
|
+
});
|
|
849
931
|
}
|
|
850
932
|
logger.info(
|
|
851
933
|
`cold-start ${sessionId.slice(0, 8)}: media-info=${mediaInfoMs}ms (${mediaInfoSource}) ` +
|
|
852
|
-
`keyframes=${keyframeMs
|
|
934
|
+
`keyframes=${keyframeMs === -1 ? "skipped" : keyframeMs === -2 ? "background" : `${keyframeMs}ms`} ` +
|
|
935
|
+
`create-total=${Date.now() - createEntryMs}ms`
|
|
853
936
|
);
|
|
854
937
|
const segmentBoundaries = hasDuration
|
|
855
938
|
? computeSegmentBoundaries({
|
|
@@ -953,6 +1036,11 @@ export class HlsSessionManager {
|
|
|
953
1036
|
// keyframe positions for copied video. Drives the playlist and seeking.
|
|
954
1037
|
segmentBoundaries,
|
|
955
1038
|
segmentCount,
|
|
1039
|
+
// Real source keyframe times (sorted seconds), or null when the probe
|
|
1040
|
+
// failed/timed out. Used by #startEncodeRun to snap a source seek onto a
|
|
1041
|
+
// KNOWN valid position instead of trusting the container's own on-the-fly
|
|
1042
|
+
// seek at an arbitrary target — see the probe call above for why.
|
|
1043
|
+
keyframeTimes,
|
|
956
1044
|
playlistText: hasDuration ? this.#buildVodPlaylist(segmentBoundaries) : "",
|
|
957
1045
|
// Segment index the current ffmpeg run started producing from.
|
|
958
1046
|
encodeStartIndex: 0,
|
|
@@ -966,6 +1054,12 @@ export class HlsSessionManager {
|
|
|
966
1054
|
seekSettleTimer: null,
|
|
967
1055
|
seekTarget: null,
|
|
968
1056
|
seekFirstFarAt: 0,
|
|
1057
|
+
// Circuit breaker: consecutive FAST failures (see SEEK_FAST_FAIL_MS) at
|
|
1058
|
+
// seekFailureTarget. Reset whenever a run starts at a DIFFERENT target or
|
|
1059
|
+
// survives past the fast-fail window. See the exit handler in
|
|
1060
|
+
// #wireEncodeProcess and MAX_SEEK_FAILURES.
|
|
1061
|
+
seekFailureTarget: -1,
|
|
1062
|
+
seekFailureCount: 0,
|
|
969
1063
|
progress: {
|
|
970
1064
|
state: "starting",
|
|
971
1065
|
processedSeconds: 0,
|
|
@@ -1512,12 +1606,40 @@ export class HlsSessionManager {
|
|
|
1512
1606
|
// (startSeconds is already a real-keyframe offset from 0, so add back the
|
|
1513
1607
|
// container start time); for re-encode startSeconds is a plain grid offset.
|
|
1514
1608
|
const seekSeconds = session.transcodeVideo ? startSeconds : startSeconds + sourceStartTime;
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1609
|
+
// Two-step seek when we have a real keyframe map: jump to a KNOWN-valid
|
|
1610
|
+
// keyframe (coarse, before -i — safe because WE sourced it from ffprobe,
|
|
1611
|
+
// not the container's own on-the-fly seek/index) and trim the short
|
|
1612
|
+
// residual (bounded by the keyframe interval) precisely AFTER -i, which is
|
|
1613
|
+
// always frame-accurate regardless of -accurate_seek.
|
|
1614
|
+
//
|
|
1615
|
+
// Root cause this works around: `-accurate_seek -ss X` before -i trusts the
|
|
1616
|
+
// CONTAINER's own seek to land near X. For some containers (observed: AVI
|
|
1617
|
+
// with VBR MP3 audio) that on-the-fly seek can point at a position with no
|
|
1618
|
+
// valid frame boundary at all — ffmpeg fails outright ("Seek failed" /
|
|
1619
|
+
// "Header missing"), not just imprecisely, and repeatedly so since every
|
|
1620
|
+
// retry re-tries the SAME bad container-computed position. A keyframe we
|
|
1621
|
+
// read directly from the packet list is a position ffmpeg has already
|
|
1622
|
+
// proven it can decode.
|
|
1623
|
+
const snappedKeyframe = Array.isArray(session.keyframeTimes) && session.keyframeTimes.length > 0
|
|
1624
|
+
? nearestKeyframeAtOrBefore(session.keyframeTimes, seekSeconds)
|
|
1625
|
+
: null;
|
|
1626
|
+
if (snappedKeyframe !== null) {
|
|
1627
|
+
const residualSeconds = Math.max(0, seekSeconds - snappedKeyframe);
|
|
1628
|
+
if (snappedKeyframe > 0) {
|
|
1629
|
+
args.push("-ss", String(snappedKeyframe));
|
|
1630
|
+
}
|
|
1631
|
+
args.push("-i", session.inputUrl);
|
|
1632
|
+
if (residualSeconds > 0) {
|
|
1633
|
+
args.push("-ss", String(residualSeconds));
|
|
1634
|
+
}
|
|
1635
|
+
} else {
|
|
1636
|
+
if (seekSeconds > 0) {
|
|
1637
|
+
// No keyframe map (probe failed/timed out) — fall back to the previous
|
|
1638
|
+
// behaviour: trust the container's own accurate seek.
|
|
1639
|
+
args.push("-accurate_seek", "-ss", String(seekSeconds));
|
|
1640
|
+
}
|
|
1641
|
+
args.push("-i", session.inputUrl);
|
|
1519
1642
|
}
|
|
1520
|
-
args.push("-i", session.inputUrl);
|
|
1521
1643
|
if (session.transcodeVideo) {
|
|
1522
1644
|
// Branch A (re-encode): fixed GOP makes keyframes land exactly on the
|
|
1523
1645
|
// segment grid; relabel output onto the original timeline so segment N
|
|
@@ -1709,6 +1831,30 @@ export class HlsSessionManager {
|
|
|
1709
1831
|
void this.#startEncodeRun(session, session.encodeStartIndex);
|
|
1710
1832
|
return;
|
|
1711
1833
|
}
|
|
1834
|
+
// Circuit-breaker bookkeeping: a seek-restart run that exits THIS fast
|
|
1835
|
+
// never did real work — it failed at the seek/open step itself, not
|
|
1836
|
+
// mid-stream (see SEEK_FAST_FAIL_MS). Track consecutive fast failures at
|
|
1837
|
+
// the SAME target so #ensureEncodingFor/#fireSettledSeek (which check
|
|
1838
|
+
// this below) can stop retrying instead of looping forever on a position
|
|
1839
|
+
// that keeps failing even with the keyframe-snapped seek.
|
|
1840
|
+
const elapsedMs = Date.now() - session.lastRestartAt;
|
|
1841
|
+
if (elapsedMs < SEEK_FAST_FAIL_MS && session.encodeStartIndex > 0) {
|
|
1842
|
+
if (session.seekFailureTarget === session.encodeStartIndex) {
|
|
1843
|
+
session.seekFailureCount += 1;
|
|
1844
|
+
} else {
|
|
1845
|
+
session.seekFailureTarget = session.encodeStartIndex;
|
|
1846
|
+
session.seekFailureCount = 1;
|
|
1847
|
+
}
|
|
1848
|
+
logger.warn(
|
|
1849
|
+
`transcode ${session.id} fast failure at segment #${session.encodeStartIndex} ` +
|
|
1850
|
+
`(${elapsedMs}ms) — ${session.seekFailureCount}/${MAX_SEEK_FAILURES} consecutive`
|
|
1851
|
+
);
|
|
1852
|
+
} else {
|
|
1853
|
+
// Real progress was made (or this was the very first run) — not a
|
|
1854
|
+
// repeating seek failure. Reset the breaker.
|
|
1855
|
+
session.seekFailureTarget = -1;
|
|
1856
|
+
session.seekFailureCount = 0;
|
|
1857
|
+
}
|
|
1712
1858
|
session.state = "failed";
|
|
1713
1859
|
session.progress.state = "failed";
|
|
1714
1860
|
session.progress.updatedAt = Date.now();
|
|
@@ -1743,6 +1889,15 @@ export class HlsSessionManager {
|
|
|
1743
1889
|
if (withinWindow) {
|
|
1744
1890
|
return;
|
|
1745
1891
|
}
|
|
1892
|
+
// Circuit breaker: this exact target has already failed MAX_SEEK_FAILURES
|
|
1893
|
+
// times in a row (fast failures — see #wireEncodeProcess's exit handler).
|
|
1894
|
+
// Stop auto-retrying it; session.state stays "failed" so getFileStream
|
|
1895
|
+
// reports a clean, retryable error instead of looping forever. A DIFFERENT
|
|
1896
|
+
// target (the viewer seeking elsewhere) is unaffected — it gets its own
|
|
1897
|
+
// fresh attempt budget.
|
|
1898
|
+
if (index === session.seekFailureTarget && session.seekFailureCount >= MAX_SEEK_FAILURES) {
|
|
1899
|
+
return;
|
|
1900
|
+
}
|
|
1746
1901
|
// Far request = a server-side seek. Do NOT restart on the first one:
|
|
1747
1902
|
// debounce a burst of scattered requests into a single restart at the
|
|
1748
1903
|
// position the player ended on. Record the latest target and (re)arm the
|
|
@@ -1776,6 +1931,14 @@ export class HlsSessionManager {
|
|
|
1776
1931
|
session.seekFirstFarAt = 0;
|
|
1777
1932
|
return;
|
|
1778
1933
|
}
|
|
1934
|
+
// Circuit breaker (defense in depth): a timer armed before the cap was hit
|
|
1935
|
+
// could still be pending when it was reached — do not fire the restart it
|
|
1936
|
+
// was going to make. See the matching check in #ensureEncodingFor.
|
|
1937
|
+
if (target === session.seekFailureTarget && session.seekFailureCount >= MAX_SEEK_FAILURES) {
|
|
1938
|
+
session.seekTarget = null;
|
|
1939
|
+
session.seekFirstFarAt = 0;
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1779
1942
|
// Minimum gap between actual restarts (the settle already collapses bursts;
|
|
1780
1943
|
// this only guards back-to-back seeks). If still cooling down, re-arm once
|
|
1781
1944
|
// for the remaining cooldown instead of restarting now.
|