@torrent-tv/proxy 2.49.0 → 2.50.0
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
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
## 2.50.0
|
|
2
|
+
|
|
3
|
+
- **Fix**: An AVI seek is asked for late enough to survive the container's own arithmetic. AVI names a keyframe by its frame NUMBER, and `services/container-index/avi.js` turned that into a time by multiplying by the frame duration the header declares — which lands 10-44 ms from the presentation time the demuxer computes, always under one frame (measured 2026-08-21 against the files themselves: 1196 index entries against 1196 real keyframes and 901 against 901, the frames exactly right and only their names off). A name sitting just BELOW its real keyframe seeks to before it and lands on the one before that, which is the same fault the landing offset already exists for. The reader now declares how far its times may be, and the request carries that on top. Matroska and MP4 declare nothing, because they state instants outright — nine files and 11 665 keyframes with not one disagreement.
|
|
4
|
+
- **Fix**: A container with no keyframe index is re-encoded rather than copied against a grid nobody knows. MPEG-TS carries no index of any kind — measured the same day, 669 real keyframes and nothing to read them from without walking the file — and a copied picture can only be cut at the source's own keyframes, so declaring an even grid is a falsehood the player punishes: it walks the whole file to rebuild the timeline, or presents audio with no picture because a segment begins with nothing decodable, both field-observed 2026-08-02. Re-encoding PLACES keyframes on our own cuts, so the grid is right by construction whatever the container. A container whose index could not be read inside the budget lands here too, for the same reason. It costs an encoder, and the alternative was a broken playlist.
|
|
5
|
+
|
|
1
6
|
## 2.49.0
|
|
2
7
|
|
|
3
8
|
- **Fix**: The torrent worker is allowed to END rather than being torn down under itself. A core dump read on 2026-08-21 named the fault the proxy has been dying of: `SIGSEGV` in `v8::Value::IsArrayBufferView` reached through `napi_get_buffer_info` from utp-native's `on_utp_accept`, called from its UDP read — all of it inside `node::Environment::CleanupHandles`, under `FreeEnvironment`, on `Worker::Run`. That is a teardown race, not a data fault, which is why neither patch our forked library already carries touched it: a datagram arriving while the environment is being freed walks into an isolate that no longer exists. `destroyAll` called `Worker.terminate()` immediately after destroying the client inside, and `terminate()` frees the environment with libuv's handle callbacks still queued. It now waits for the thread to exit by itself — once the client is destroyed nothing holds its loop open — with `terminate()` kept as a five-second fallback, because a shutdown that hangs is worse than one that is forced.
|
package/package.json
CHANGED
|
@@ -152,5 +152,16 @@ export async function readAviKeyframeTimes(readRange, fileSize) {
|
|
|
152
152
|
}
|
|
153
153
|
videoFrame += 1;
|
|
154
154
|
}
|
|
155
|
-
|
|
155
|
+
if (times.length === 0) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
// AVI names a keyframe by its FRAME NUMBER, and the time above is that number
|
|
159
|
+
// multiplied by the frame duration the header declares. The frames are the
|
|
160
|
+
// right ones — measured 2026-08-21 against the files themselves, 1196 index
|
|
161
|
+
// entries against 1196 real keyframes and 901 against 901, exactly — but the
|
|
162
|
+
// names are 10-44 ms away from the presentation times the demuxer computes,
|
|
163
|
+
// always under one frame. So the caller is told how far a time here may be
|
|
164
|
+
// from the instant it refers to, and can ask for a seek late enough that it
|
|
165
|
+
// still lands on the frame rather than on the one before it.
|
|
166
|
+
return { times, tolerance: secondsPerFrame };
|
|
156
167
|
}
|
|
@@ -62,7 +62,7 @@ const READERS = [
|
|
|
62
62
|
* @param {ReadRange} params.readRange
|
|
63
63
|
* @param {number} params.fileSize
|
|
64
64
|
* @param {string} [params.label] - For logging only.
|
|
65
|
-
* @returns {Promise<{ times: number[] | null, format: string }>} Ascending
|
|
65
|
+
* @returns {Promise<{ times: number[] | null, format: string, tolerance: number }>} Ascending
|
|
66
66
|
* seconds, or null times when this file has no readable index — the caller
|
|
67
67
|
* must then not claim to know the grid. The format is which reader matched,
|
|
68
68
|
* reported whether or not it produced anything: how often an index disagrees
|
|
@@ -71,27 +71,39 @@ const READERS = [
|
|
|
71
71
|
*/
|
|
72
72
|
export async function readKeyframeIndex({ readRange, fileSize, label = "" }) {
|
|
73
73
|
if (typeof readRange !== "function" || !Number.isFinite(fileSize) || fileSize <= 0) {
|
|
74
|
-
return { times: null, format: "unknown" };
|
|
74
|
+
return { times: null, format: "unknown", tolerance: 0 };
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
const startedAt = Date.now();
|
|
78
78
|
let times = null;
|
|
79
79
|
let format = "unrecognised";
|
|
80
|
+
// How far a time in `times` may be from the instant it names. Zero wherever
|
|
81
|
+
// the container states instants outright, which Matroska and MP4 both do.
|
|
82
|
+
let tolerance = 0;
|
|
80
83
|
try {
|
|
81
84
|
const sniff = await readRange(0, Math.min(SNIFF_BYTES - 1, fileSize - 1));
|
|
82
85
|
if (!sniff) {
|
|
83
|
-
return { times: null, format: "unread" };
|
|
86
|
+
return { times: null, format: "unread", tolerance: 0 };
|
|
84
87
|
}
|
|
85
88
|
const reader = READERS.find((candidate) => candidate.matches(sniff));
|
|
86
89
|
if (reader) {
|
|
87
90
|
format = reader.name;
|
|
88
|
-
|
|
91
|
+
const read = await reader.read(readRange, fileSize);
|
|
92
|
+
// A reader may answer with the times alone, or with how far those times
|
|
93
|
+
// may sit from the instants they name — which only AVI has to say,
|
|
94
|
+
// because only AVI computes them from frame numbers.
|
|
95
|
+
if (Array.isArray(read)) {
|
|
96
|
+
times = read;
|
|
97
|
+
} else if (read && Array.isArray(read.times)) {
|
|
98
|
+
times = read.times;
|
|
99
|
+
tolerance = Number.isFinite(read.tolerance) ? read.tolerance : 0;
|
|
100
|
+
}
|
|
89
101
|
}
|
|
90
102
|
} catch (error) {
|
|
91
103
|
// A malformed or partially-downloaded index must never take playback down —
|
|
92
104
|
// it only means the grid is unknown, which the caller already handles.
|
|
93
105
|
logger.warn(`container-index: failed to read index for "${label}": ${error?.message ?? error}`);
|
|
94
|
-
return { times: null, format };
|
|
106
|
+
return { times: null, format, tolerance: 0 };
|
|
95
107
|
}
|
|
96
108
|
|
|
97
109
|
const elapsedMs = Date.now() - startedAt;
|
|
@@ -102,5 +114,5 @@ export async function readKeyframeIndex({ readRange, fileSize, label = "" }) {
|
|
|
102
114
|
} else {
|
|
103
115
|
logger.info(`container-index: no usable index for "${label}" (${format}, ${elapsedMs}ms)`);
|
|
104
116
|
}
|
|
105
|
-
return { times, format };
|
|
117
|
+
return { times, format, tolerance };
|
|
106
118
|
}
|
|
@@ -1196,12 +1196,17 @@ export function seekLandingOffsetFor(session, keyframe) {
|
|
|
1196
1196
|
if (session?.transcodeVideo === true) {
|
|
1197
1197
|
return 0;
|
|
1198
1198
|
}
|
|
1199
|
+
// A grid whose times are approximate needs that error added on top, or a name
|
|
1200
|
+
// sitting just below its real keyframe seeks to before it and lands on the
|
|
1201
|
+
// one before that. Only AVI declares one.
|
|
1202
|
+
const tolerance = Number.isFinite(session?.keyframeTolerance) ? Math.max(0, session.keyframeTolerance) : 0;
|
|
1203
|
+
const wanted = SEEK_LANDING_OFFSET_SEC + tolerance;
|
|
1199
1204
|
const times = Array.isArray(session?.keyframeTimes) ? session.keyframeTimes : [];
|
|
1200
1205
|
const next = times.find((time) => time > keyframe + 0.001);
|
|
1201
1206
|
if (next === undefined) {
|
|
1202
|
-
return
|
|
1207
|
+
return wanted;
|
|
1203
1208
|
}
|
|
1204
|
-
return Math.min(
|
|
1209
|
+
return Math.min(wanted, (next - keyframe) / 2);
|
|
1205
1210
|
}
|
|
1206
1211
|
|
|
1207
1212
|
/**
|
|
@@ -1746,6 +1751,13 @@ export class HlsSessionManager {
|
|
|
1746
1751
|
// branches; on failure both fall back to their current behaviour (uniform
|
|
1747
1752
|
// grid for boundaries, raw target for seeking) — no regression.
|
|
1748
1753
|
let keyframeTimes = null;
|
|
1754
|
+
// How far a time in `keyframeTimes` may sit from the instant it names. Only
|
|
1755
|
+
// AVI has anything to declare here: it stores frame NUMBERS and the time is
|
|
1756
|
+
// that number times the frame duration, which lands 10-44 ms from the
|
|
1757
|
+
// presentation time the demuxer computes (measured 2026-08-21). A seek made
|
|
1758
|
+
// at such a name can fall just BELOW the real keyframe and land on the one
|
|
1759
|
+
// before it, which is the same fault the landing offset exists for.
|
|
1760
|
+
let keyframeTolerance = 0;
|
|
1749
1761
|
let keyframeMs = -1; // -1 = not run (skipped), -2 = running in the background
|
|
1750
1762
|
// Which container supplied the index, carried so the accuracy summary can
|
|
1751
1763
|
// say what it is a summary OF.
|
|
@@ -1758,6 +1770,9 @@ export class HlsSessionManager {
|
|
|
1758
1770
|
if (inheritedGrid) {
|
|
1759
1771
|
keyframeTimes = inheritedGrid.keyframeTimes;
|
|
1760
1772
|
containerFormat = inheritedGrid.containerFormat ?? "";
|
|
1773
|
+
keyframeTolerance = Number.isFinite(inheritedGrid.keyframeTolerance)
|
|
1774
|
+
? inheritedGrid.keyframeTolerance
|
|
1775
|
+
: 0;
|
|
1761
1776
|
} else if (hasDuration && !transcodeVideo && !audioOnly) {
|
|
1762
1777
|
// Video-COPY path: keyframeTimes are REQUIRED to build correct segment
|
|
1763
1778
|
// boundaries (the playlist itself), so this MUST block session creation —
|
|
@@ -1779,11 +1794,28 @@ export class HlsSessionManager {
|
|
|
1779
1794
|
const index = await this.#readContainerKeyframes({ sourceKey, fileIndex, inputUrl, logName });
|
|
1780
1795
|
keyframeTimes = index.times;
|
|
1781
1796
|
containerFormat = index.format;
|
|
1797
|
+
keyframeTolerance = Number.isFinite(index.tolerance) ? index.tolerance : 0;
|
|
1782
1798
|
keyframeMs = Date.now() - keyframeStartMs;
|
|
1783
1799
|
if (!keyframeTimes) {
|
|
1800
|
+
// No index, so there is no honest grid for a COPY: a copied picture can
|
|
1801
|
+
// only be cut at the source's own keyframes, and we do not know where
|
|
1802
|
+
// they are. Declaring an even grid instead is a falsehood the player
|
|
1803
|
+
// punishes — it walks the whole file to rebuild the timeline, or shows
|
|
1804
|
+
// audio with no picture because a segment begins with nothing
|
|
1805
|
+
// decodable (both field-observed 2026-08-02).
|
|
1806
|
+
//
|
|
1807
|
+
// Re-encoding is the honest answer and costs an encoder: keyframes are
|
|
1808
|
+
// then PLACED at our own cut times rather than found, so the grid is
|
|
1809
|
+
// correct by construction whatever the container. MPEG-TS is the case
|
|
1810
|
+
// this exists for — measured 2026-08-21, 669 real keyframes and no
|
|
1811
|
+
// index of any kind to read them from — and a container whose index
|
|
1812
|
+
// could not be read in the budget lands here too, which is right for
|
|
1813
|
+
// the same reason.
|
|
1814
|
+
transcodeVideo = true;
|
|
1784
1815
|
logger.warn(
|
|
1785
|
-
`transcode ${sessionId}: no
|
|
1786
|
-
`
|
|
1816
|
+
`transcode ${sessionId}: no keyframe index in the ${containerFormat} container for ` +
|
|
1817
|
+
`"${logName}" — a copied picture has no honest grid without one, so the video is ` +
|
|
1818
|
+
"re-encoded instead and its keyframes are placed on our own cuts"
|
|
1787
1819
|
);
|
|
1788
1820
|
}
|
|
1789
1821
|
} else if (hasDuration && transcodeVideo) {
|
|
@@ -2035,6 +2067,9 @@ export class HlsSessionManager {
|
|
|
2035
2067
|
// KNOWN valid position instead of trusting the container's own on-the-fly
|
|
2036
2068
|
// seek at an arbitrary target — see the probe call above for why.
|
|
2037
2069
|
keyframeTimes,
|
|
2070
|
+
// How far those times may sit from the instants they name — nonzero only
|
|
2071
|
+
// for AVI, which computes them from frame numbers.
|
|
2072
|
+
keyframeTolerance,
|
|
2038
2073
|
// Which container the index came from, and how well it has held up. The
|
|
2039
2074
|
// cut times of a copied video ARE its index, and an index can be wrong —
|
|
2040
2075
|
// measured 2026-08-06, one claimed a keyframe four seconds from where the
|
|
@@ -6848,6 +6883,7 @@ export class HlsSessionManager {
|
|
|
6848
6883
|
// creations (field 2026-08-17, corrections of 0.6-2.9 s).
|
|
6849
6884
|
published: base.publishedBoundaries,
|
|
6850
6885
|
keyframeTimes: base.keyframeTimes,
|
|
6886
|
+
keyframeTolerance: base.keyframeTolerance,
|
|
6851
6887
|
containerFormat: base.containerFormat
|
|
6852
6888
|
}
|
|
6853
6889
|
: null,
|
|
@@ -48,3 +48,29 @@ test("no keyframe list is still answered", () => {
|
|
|
48
48
|
assert.equal(seekLandingOffsetFor({ transcodeVideo: false }, 5), OFFSET);
|
|
49
49
|
assert.equal(seekLandingOffsetFor(null, 5), OFFSET);
|
|
50
50
|
});
|
|
51
|
+
|
|
52
|
+
test("a grid whose times are approximate is asked for that much later again", () => {
|
|
53
|
+
// AVI names a keyframe by its frame NUMBER and the time is that number times
|
|
54
|
+
// the frame duration, so a name can sit just BELOW the keyframe it refers to
|
|
55
|
+
// — measured 2026-08-21, 10-44 ms out on two files, always under one frame.
|
|
56
|
+
// Asking at the name alone would seek to before the real keyframe and land on
|
|
57
|
+
// the one before that, which is the fault this offset exists for.
|
|
58
|
+
const session = {
|
|
59
|
+
transcodeVideo: false,
|
|
60
|
+
keyframeTolerance: 0.04,
|
|
61
|
+
keyframeTimes: [0, 4.004, 8.008, 12.012]
|
|
62
|
+
};
|
|
63
|
+
assert.equal(seekLandingOffsetFor(session, 4.004), OFFSET + 0.04);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("an exact grid claims no tolerance", () => {
|
|
67
|
+
// Matroska and MP4 state instants outright — measured the same day, nine
|
|
68
|
+
// files and 11 665 keyframes with not one disagreement.
|
|
69
|
+
const session = { transcodeVideo: false, keyframeTolerance: 0, keyframeTimes: [0, 4.004, 8.008] };
|
|
70
|
+
assert.equal(seekLandingOffsetFor(session, 4.004), OFFSET);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("the bound still holds once a tolerance is added", () => {
|
|
74
|
+
const session = { transcodeVideo: false, keyframeTolerance: 1, keyframeTimes: [0, 0.1, 0.2] };
|
|
75
|
+
assert.equal(seekLandingOffsetFor(session, 0.1), 0.05);
|
|
76
|
+
});
|