@torrent-tv/proxy 2.9.62 → 2.9.64
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/services/hls-session-manager.js +64 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## 2.9.64
|
|
2
|
+
|
|
3
|
+
- **Fix**: The 2.9.63 pull-to-lowest-awaited-segment dragged the encoder to the start of the file. A seek to #1354 restarted at **#123** — the position of the *previous* watch — because requests outstanding from before the seek still counted toward `lowestAwaitedIndex`. Two fixes: the awaited floor is cleared the moment a new seek arrives (earlier requests describe where the player used to be, not where it is going), and the pull is bounded by `SEEK_PULL_LIMIT_SEGMENTS` (120) below the target — anything deeper is a leftover, not the preceding keyframe.
|
|
4
|
+
|
|
5
|
+
## 2.9.63
|
|
6
|
+
|
|
7
|
+
- **Fix**: A seek landed the encoder on exactly the requested segment, which is the one position the player never asks for — so it produced files nobody was waiting for and playback hung. Per Apple HLS authoring guidance, a player given a position locates the nearest keyframe **preceding** it, decodes from there, and only then presents from the requested point; it therefore always fetches segments **below** the target. Measured on iOS: a seek to #1082 fetched from #1074 (8 back), one to #1358 fetched from #1301 (57 back) and asked for **nothing at or above** the target. The encoder now starts `SEEK_BACKOFF_SEGMENTS` (12) before the requested segment, and — since the needed depth varies and no fixed number covers it — is pulled down further to the lowest segment the player is actually waiting on, which its own requests report exactly (`lowestAwaitedIndex`). Only ever moves the start earlier, never later. Costs a few seconds of extra encoding per seek.
|
|
8
|
+
|
|
1
9
|
## 2.9.62
|
|
2
10
|
|
|
3
11
|
- **Fix**: The seek target is now taken **explicitly from the browser** (`POST /api/transcode-sessions/:id/seek`) instead of being inferred from which segments the player requests. Measured 2026-08-02: one viewer seek leaves **~25 concurrent segment requests** outstanding spanning #904..#1101, all held for a full 60 s with none aborted — ordinary read-ahead, not probing. There is therefore no such thing as "the segment the player ended on", and any rule choosing among them chooses noise: the old debounce produced **nine encoder restarts in one minute** (#576→#885→#609→#591→#673→#833→#624→#1071→#1101), each killed 5-8 s in, turning a single seek into a ~70 s ordeal that still landed correctly only by luck. Segment requests are now purely data fetches — held until produced, served from disk when behind the encoder — and never reposition it. Same separation both production references use (Jellyfin `startTimeTicks`, webtor `?t=`); see research/hls-seek-prior-art-2026-08-02.md. A seek already covered by the running encode does not restart it at all.
|
package/package.json
CHANGED
|
@@ -69,6 +69,29 @@ const ENCODER_STALL_MS = 12_000;
|
|
|
69
69
|
// 367,732,369,368,370 seconds apart) collapses to ONE restart at the position
|
|
70
70
|
// the player ended on, instead of ping-ponging ffmpeg between positions and
|
|
71
71
|
// producing nothing.
|
|
72
|
+
// How many segments BEFORE the requested position the encoder starts.
|
|
73
|
+
//
|
|
74
|
+
// Required by how HLS players seek, per Apple's HLS authoring guidance: given a
|
|
75
|
+
// position, the player locates the nearest IDR (keyframe) *preceding* it,
|
|
76
|
+
// decodes from there, and only then presents from the requested point. So it
|
|
77
|
+
// always fetches segments BELOW the target — measured 2026-08-02 on iOS: a seek
|
|
78
|
+
// to #1082 fetched from #1074 (8 back), one to #1358 fetched from #1301 (57
|
|
79
|
+
// back), and in the latter case the player asked for NOTHING at or above the
|
|
80
|
+
// target, so an encoder starting exactly on it produced only files nobody was
|
|
81
|
+
// waiting for and playback hung indefinitely.
|
|
82
|
+
//
|
|
83
|
+
// The observed backoff is not constant, so this is a floor, not the whole
|
|
84
|
+
// answer: #fireSettledSeek also pulls the start down to the lowest segment the
|
|
85
|
+
// player is actually waiting on when that is lower still. Costs a few seconds
|
|
86
|
+
// of extra encoding per seek.
|
|
87
|
+
const SEEK_BACKOFF_SEGMENTS = 12;
|
|
88
|
+
// Hard limit on how far below the requested segment the start may be pulled by
|
|
89
|
+
// `lowestAwaitedIndex`. Without it a stale request from earlier playback drags
|
|
90
|
+
// the encoder across the whole file: field 2026-08-02, a seek to #1354 was
|
|
91
|
+
// pulled to #123 — the start of the previous watch — because requests from
|
|
92
|
+
// before the seek were still counted. Anything deeper than this is not the
|
|
93
|
+
// preceding keyframe, it is a leftover.
|
|
94
|
+
const SEEK_PULL_LIMIT_SEGMENTS = 120;
|
|
72
95
|
const SEEK_SETTLE_MS = 1_200;
|
|
73
96
|
// Hard cap on the total settle wait, measured from the first far request of a
|
|
74
97
|
// burst, so a still-moving scrubber cannot delay a genuine seek forever.
|
|
@@ -1065,6 +1088,9 @@ export class HlsSessionManager {
|
|
|
1065
1088
|
// #wireEncodeProcess and MAX_SEEK_FAILURES.
|
|
1066
1089
|
seekFailureTarget: -1,
|
|
1067
1090
|
seekFailureCount: 0,
|
|
1091
|
+
// Lowest segment index the player is currently waiting for; -1 when
|
|
1092
|
+
// nothing is pending. See getFileStream and #fireSettledSeek.
|
|
1093
|
+
lowestAwaitedIndex: -1,
|
|
1068
1094
|
progress: {
|
|
1069
1095
|
state: "starting",
|
|
1070
1096
|
processedSeconds: 0,
|
|
@@ -2040,10 +2066,19 @@ export class HlsSessionManager {
|
|
|
2040
2066
|
);
|
|
2041
2067
|
return true;
|
|
2042
2068
|
}
|
|
2069
|
+
// Start BEFORE the requested position (see SEEK_BACKOFF_SEGMENTS): the
|
|
2070
|
+
// player needs a segment containing the preceding keyframe, so one that
|
|
2071
|
+
// begins exactly at the target is useless to it.
|
|
2072
|
+
const startIndex = Math.max(0, index - SEEK_BACKOFF_SEGMENTS);
|
|
2073
|
+
// A new seek invalidates everything the player was waiting for before it:
|
|
2074
|
+
// those requests describe where it USED to be. Clearing here is what keeps
|
|
2075
|
+
// the pull below anchored to this seek.
|
|
2076
|
+
session.lowestAwaitedIndex = -1;
|
|
2043
2077
|
logger.info(
|
|
2044
|
-
`transcode ${session.id} viewer seek to ${positionSeconds.toFixed(1)}s → segment #${index}`
|
|
2078
|
+
`transcode ${session.id} viewer seek to ${positionSeconds.toFixed(1)}s → segment #${index}, ` +
|
|
2079
|
+
`starting at #${startIndex} (${SEEK_BACKOFF_SEGMENTS} back for the preceding keyframe)`
|
|
2045
2080
|
);
|
|
2046
|
-
session.seekTarget =
|
|
2081
|
+
session.seekTarget = startIndex;
|
|
2047
2082
|
if (session.seekSettleTimer) {
|
|
2048
2083
|
clearTimeout(session.seekSettleTimer);
|
|
2049
2084
|
} else {
|
|
@@ -2140,10 +2175,23 @@ export class HlsSessionManager {
|
|
|
2140
2175
|
: producedThisRun >= this.segmentDurationSec
|
|
2141
2176
|
? `run produced ${producedThisRun.toFixed(1)}s (first segment done)`
|
|
2142
2177
|
: `grace of ${RUN_FIRST_SEGMENT_GRACE_MS / 1000}s expired`;
|
|
2178
|
+
// The player may be waiting on something below our fixed backoff — its own
|
|
2179
|
+
// requests say exactly how far back it needs the keyframe, so honour that
|
|
2180
|
+
// rather than a guess. Only ever pulls the start EARLIER, never later.
|
|
2181
|
+
const awaited = session.lowestAwaitedIndex;
|
|
2182
|
+
const pullFloor = Math.max(0, target - SEEK_PULL_LIMIT_SEGMENTS);
|
|
2183
|
+
const effectiveTarget = awaited >= pullFloor && awaited < target ? awaited : target;
|
|
2184
|
+
if (effectiveTarget !== target) {
|
|
2185
|
+
logger.info(
|
|
2186
|
+
`transcode ${session.id} pulling encode start #${target} → #${effectiveTarget} ` +
|
|
2187
|
+
`(lowest segment the player is waiting on)`
|
|
2188
|
+
);
|
|
2189
|
+
}
|
|
2143
2190
|
session.seekTarget = null;
|
|
2144
2191
|
session.seekFirstFarAt = 0;
|
|
2145
|
-
|
|
2146
|
-
|
|
2192
|
+
session.lowestAwaitedIndex = -1;
|
|
2193
|
+
logger.info(`transcode ${session.id} seek settle → restart at segment #${effectiveTarget} (${allowedBecause})`);
|
|
2194
|
+
void this.#startEncodeRun(session, effectiveTarget);
|
|
2147
2195
|
}
|
|
2148
2196
|
|
|
2149
2197
|
/**
|
|
@@ -2346,9 +2394,20 @@ export class HlsSessionManager {
|
|
|
2346
2394
|
// to wait for the current encode run to reach it or to restart the encoder
|
|
2347
2395
|
// at this position (server-side seeking). The caller long-polls.
|
|
2348
2396
|
if (!isPlaylist) {
|
|
2397
|
+
const requestedIndex = this.segmentFormat.segmentIndexFromName(fileName);
|
|
2398
|
+
// Remember the LOWEST segment currently being waited on. The player
|
|
2399
|
+
// always fetches below the seek target (it needs the preceding keyframe),
|
|
2400
|
+
// and by how much varies — 8 segments in one measured seek, 57 in
|
|
2401
|
+
// another. This is that figure straight from the player, and
|
|
2402
|
+
// #fireSettledSeek uses it to pull the encode start down when the fixed
|
|
2403
|
+
// SEEK_BACKOFF_SEGMENTS floor is not deep enough. Reset whenever a run
|
|
2404
|
+
// starts, so it only ever describes the pending seek.
|
|
2405
|
+
if (requestedIndex >= 0 && (session.lowestAwaitedIndex < 0 || requestedIndex < session.lowestAwaitedIndex)) {
|
|
2406
|
+
session.lowestAwaitedIndex = requestedIndex;
|
|
2407
|
+
}
|
|
2349
2408
|
this.#ensureEncodingFor(
|
|
2350
2409
|
session,
|
|
2351
|
-
|
|
2410
|
+
requestedIndex,
|
|
2352
2411
|
Number.isFinite(options?.requestSeq) ? options.requestSeq : Number.MAX_SAFE_INTEGER
|
|
2353
2412
|
);
|
|
2354
2413
|
}
|