@torrent-tv/proxy 2.9.120 → 2.9.122
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,12 @@
|
|
|
1
|
+
## 2.9.122
|
|
2
|
+
|
|
3
|
+
- **Fix**: A session created with a start position begins encoding there, instead of at the top of the file. The position was honoured everywhere except the one place that mattered: it went into the session key and into the log line, and then the first run started at index 0 regardless. Measured 2026-08-06 on a Retry after the proxy had restarted — the session was created with `start=1580s`, the encoder began at #0, the player asked for #152, and 45 s later the browser gave up with "no data arrived from the proxy" while the transcode ran happily at 9.9x through the opening credits.
|
|
4
|
+
|
|
5
|
+
## 2.9.121
|
|
6
|
+
|
|
7
|
+
- **Fix**: A segment that is short of a track is no longer served, which is what left a seek hanging with the proxy answering every request in 98 ms. A run that is terminated closes its current output file properly — trailing index and all — but the file holds only what had been muxed by then, and after a seek-restart that is routinely one track of two. Nothing about such a file looks unfinished: it exists, the next one exists, so the readiness rule called it done. Measured 2026-08-06 on a stuck session: segment #133 carried one `tfdt` where #131 and #134 carried two, and #132 was zero bytes. The fragments are already walked to stamp their timestamps, so counting the tracks in them costs nothing; a segment missing one is deleted and produced again.
|
|
8
|
+
- **New**: The session-start line says where the browser asked the encoder to begin. A resume that reaches hls.js but not this call makes the player request a segment nobody was told to produce — measured the same day, the session began at #0 while the player asked for #127 and gave up 45.6 s later — and neither side said what it meant.
|
|
9
|
+
|
|
1
10
|
## 2.9.120
|
|
2
11
|
|
|
3
12
|
- **Fix**: The rest of the file is fetched while the viewer needs nothing, instead of the link sitting idle. The background fill was re-evaluated only when a reader window MOVED, so during exactly the state it exists for — the encoder held back by the look-ahead cap, the viewer comfortably ahead, the link free — nothing was fetched at all. It is now owned by the pool's own timer rather than by the reader, because a parked reader cannot act, and parked is the whole point. Priority 0 against the window's 1, and withdrawn the moment any reader window wants something, so it can never take capacity from the picture.
|
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { createReadStream, readdirSync } from "node:fs";
|
|
11
|
-
import { access, mkdir, readdir, readFile, rm, stat } from "node:fs/promises";
|
|
11
|
+
import { access, mkdir, readdir, readFile, rm, stat, unlink } from "node:fs/promises";
|
|
12
12
|
import { Readable } from "node:stream";
|
|
13
13
|
import os from "node:os";
|
|
14
14
|
import path from "node:path";
|
|
@@ -1430,6 +1430,12 @@ export class HlsSessionManager {
|
|
|
1430
1430
|
// one of these, so "is the host actually running the build I published?"
|
|
1431
1431
|
// is answered by the log itself instead of a round trip to the machine.
|
|
1432
1432
|
`transcode ${sessionId} start (proxy ${PROXY_VERSION}) "${logName}" ` +
|
|
1433
|
+
// Where the browser asked the encoder to begin. A resume that reaches
|
|
1434
|
+
// hls.js but not this call makes the player request a segment nobody
|
|
1435
|
+
// was told to produce: measured 2026-08-06, the session began at #0
|
|
1436
|
+
// while the player asked for #127 and gave up 45.6 s later. Neither
|
|
1437
|
+
// side saying what it meant is why that took three attempts to place.
|
|
1438
|
+
`start=${Math.round(normalizedStartPosition)}s ` +
|
|
1433
1439
|
`video=${transcodeVideo ? `${this.videoEncoder.name}${softwarePreset ? `/${softwarePreset}` : ""}` : "copy"} ` +
|
|
1434
1440
|
`audio=${transcodeAudio ? "aac" : "copy"} ` +
|
|
1435
1441
|
// Branch tag for log correlation: A = video re-encode (fixed GOP, grid
|
|
@@ -1448,7 +1454,18 @@ export class HlsSessionManager {
|
|
|
1448
1454
|
`duration=${hasDuration ? formatSeconds(durationSeconds) : "unknown"} segments=${segmentCount}`
|
|
1449
1455
|
);
|
|
1450
1456
|
|
|
1451
|
-
|
|
1457
|
+
// Begin where the viewer asked, not at the top of the file. The position
|
|
1458
|
+
// was already honoured everywhere EXCEPT here: it went into the session key
|
|
1459
|
+
// and into the log line, and then the first run started at index 0 anyway.
|
|
1460
|
+
// Measured 2026-08-06 on a Retry after the proxy restarted — the session
|
|
1461
|
+
// was created with `start=1580s`, the encoder began at #0, the player
|
|
1462
|
+
// asked for #152, and 45 s later the browser gave up with "no data arrived
|
|
1463
|
+
// from the proxy" while the transcode ran happily at 9.9x through the
|
|
1464
|
+
// opening credits.
|
|
1465
|
+
const firstIndex = normalizedStartPosition > 0
|
|
1466
|
+
? this.#segmentIndexForTime(session, normalizedStartPosition)
|
|
1467
|
+
: 0;
|
|
1468
|
+
await this.#startEncodeRun(session, firstIndex);
|
|
1452
1469
|
|
|
1453
1470
|
try {
|
|
1454
1471
|
await this.waitUntilReady(session);
|
|
@@ -3388,6 +3405,30 @@ export class HlsSessionManager {
|
|
|
3388
3405
|
const bytes = session.usesExplicitCuts && session.segmentFormat.stripInit
|
|
3389
3406
|
? session.segmentFormat.stripInit(raw)
|
|
3390
3407
|
: raw;
|
|
3408
|
+
// A segment that is short of a track is not servable, whatever the
|
|
3409
|
+
// directory says about it. A terminated run closes its current output
|
|
3410
|
+
// file properly — trailing index and all — but with only what had been
|
|
3411
|
+
// muxed by then, and after a seek-restart that is routinely one track
|
|
3412
|
+
// of two. The file exists and so does the next one, so the readiness
|
|
3413
|
+
// rule calls it done. Measured 2026-08-06: segment #133 held one track
|
|
3414
|
+
// where its neighbours held two, the proxy answered every request for
|
|
3415
|
+
// it in 98 ms, and the viewer's seek never completed. Treated as not
|
|
3416
|
+
// ready, so the encoder makes it again.
|
|
3417
|
+
if (
|
|
3418
|
+
typeof session.segmentFormat.hasEveryTrack === "function" &&
|
|
3419
|
+
!session.segmentFormat.hasEveryTrack(bytes, session.initBytes ?? null)
|
|
3420
|
+
) {
|
|
3421
|
+
logger.warn(
|
|
3422
|
+
`transcode ${session.id} segment #${index} is short of a track — ` +
|
|
3423
|
+
"left behind by a run that was terminated; producing it again"
|
|
3424
|
+
);
|
|
3425
|
+
try {
|
|
3426
|
+
await unlink(filePath);
|
|
3427
|
+
} catch {
|
|
3428
|
+
// Already gone, or being rewritten: either way nothing to do.
|
|
3429
|
+
}
|
|
3430
|
+
return { kind: "warming-up" };
|
|
3431
|
+
}
|
|
3391
3432
|
const prepared = session.segmentFormat.prepareSegmentBytes(bytes, {
|
|
3392
3433
|
startSeconds: this.#segmentStartTime(session, index),
|
|
3393
3434
|
initBytes: session.initBytes ?? null
|
|
@@ -10,7 +10,23 @@
|
|
|
10
10
|
* See {@link SegmentFormat} in `./index.js` for the interface contract.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { readTrackTimescales, stampSegmentStartTime } from "./mp4-boxes.js";
|
|
13
|
+
import { readTrackTimescales, stampSegmentStartTime, walkBoxes } from "./mp4-boxes.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* How many distinct tracks have a fragment in this segment.
|
|
17
|
+
*
|
|
18
|
+
* @param {Buffer} bytes
|
|
19
|
+
* @returns {number}
|
|
20
|
+
*/
|
|
21
|
+
function countFragmentTracks(bytes) {
|
|
22
|
+
const tracks = new Set();
|
|
23
|
+
walkBoxes(bytes, (type, bodyStart) => {
|
|
24
|
+
if (type === "tfhd" && bodyStart + 8 <= bytes.length) {
|
|
25
|
+
tracks.add(bytes.readUInt32BE(bodyStart + 4));
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
return tracks.size;
|
|
29
|
+
}
|
|
14
30
|
|
|
15
31
|
/**
|
|
16
32
|
* Where the fragments begin and where the trailing index starts, as offsets of
|
|
@@ -185,6 +201,35 @@ export const fmp4Format = {
|
|
|
185
201
|
*/
|
|
186
202
|
needsSegmentRewrite: true,
|
|
187
203
|
|
|
204
|
+
/**
|
|
205
|
+
* Whether a segment carries every track the init promises.
|
|
206
|
+
*
|
|
207
|
+
* A run that is TERMINATED closes its current output file properly — trailing
|
|
208
|
+
* index and all — but the file holds only what had been muxed by then, which
|
|
209
|
+
* after a seek-restart is routinely one track of two. Nothing about it looks
|
|
210
|
+
* unfinished: the file exists, the next one exists, so the readiness rule
|
|
211
|
+
* calls it done and it is served. The player then cannot use it and the seek
|
|
212
|
+
* never completes. Measured 2026-08-06: segment #133 carried one `tfdt`
|
|
213
|
+
* where its neighbours carried two, and a viewer sat on a spinner while the
|
|
214
|
+
* proxy answered every request in 98 ms.
|
|
215
|
+
*
|
|
216
|
+
* Cheap to check: the fragments are already walked to stamp them.
|
|
217
|
+
*
|
|
218
|
+
* @param {Buffer} bytes - The media segment, init header already removed.
|
|
219
|
+
* @param {Buffer | null} initBytes
|
|
220
|
+
* @returns {boolean} False only when a track is provably missing.
|
|
221
|
+
*/
|
|
222
|
+
hasEveryTrack(bytes, initBytes) {
|
|
223
|
+
if (!initBytes || initBytes.length === 0) {
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
const expected = readTrackTimescales(initBytes);
|
|
227
|
+
if (expected.size === 0) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
return countFragmentTracks(bytes) >= expected.size;
|
|
231
|
+
},
|
|
232
|
+
|
|
188
233
|
prepareSegmentBytes(bytes, { startSeconds, initBytes }) {
|
|
189
234
|
if (!initBytes || initBytes.length === 0) {
|
|
190
235
|
// No init cached yet — nothing to read timescales from. The player always
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file A segment that is short of a track must not be served.
|
|
3
|
+
*
|
|
4
|
+
* A run that is terminated closes its current output file properly — trailing
|
|
5
|
+
* index and all — but the file holds only what had been muxed by then, and
|
|
6
|
+
* after a seek-restart that is routinely one track of two. Nothing about it
|
|
7
|
+
* looks unfinished, so the readiness rule called it done and served it, and the
|
|
8
|
+
* player could not complete the seek. Measured 2026-08-06: segment #133 carried
|
|
9
|
+
* one `tfdt` where its neighbours carried two, every request for it was
|
|
10
|
+
* answered in 98 ms, and the viewer sat on a spinner.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import test from "node:test";
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import { fmp4Format } from "../services/segment-formats/fmp4.js";
|
|
16
|
+
// Build a minimal init declaring two tracks, and fragments declaring one/two.
|
|
17
|
+
function box(type, body) {
|
|
18
|
+
const b = Buffer.alloc(8 + body.length);
|
|
19
|
+
b.writeUInt32BE(8 + body.length, 0); b.write(type, 4, "latin1"); body.copy(b, 8);
|
|
20
|
+
return b;
|
|
21
|
+
}
|
|
22
|
+
function mdhd(timescale) { const b = Buffer.alloc(20); b.writeUInt32BE(timescale, 12); return box("mdhd", b); }
|
|
23
|
+
function tkhd(id) { const b = Buffer.alloc(84); b.writeUInt32BE(id, 12); return box("tkhd", b); }
|
|
24
|
+
function trak(id, ts) { return box("trak", Buffer.concat([tkhd(id), box("mdia", mdhd(ts))])); }
|
|
25
|
+
const init = box("moov", Buffer.concat([trak(1, 16000), trak(2, 48000)]));
|
|
26
|
+
function tfhd(id) { const b = Buffer.alloc(8); b.writeUInt32BE(id, 4); return box("tfhd", b); }
|
|
27
|
+
function moof(ids) { return box("moof", Buffer.concat(ids.map((id) => box("traf", tfhd(id))))); }
|
|
28
|
+
|
|
29
|
+
test("a segment with every track is accepted", () => {
|
|
30
|
+
assert.equal(fmp4Format.hasEveryTrack(moof([1, 2]), init), true);
|
|
31
|
+
});
|
|
32
|
+
test("a segment left short of a track by a killed run is refused", () => {
|
|
33
|
+
assert.equal(fmp4Format.hasEveryTrack(moof([1]), init), false,
|
|
34
|
+
"one track of two is what a terminated run leaves behind, and it looks complete");
|
|
35
|
+
});
|
|
36
|
+
test("an empty file is refused", () => {
|
|
37
|
+
assert.equal(fmp4Format.hasEveryTrack(Buffer.alloc(0), init), false);
|
|
38
|
+
});
|
|
39
|
+
test("without an init there is nothing to compare against, so it is allowed", () => {
|
|
40
|
+
assert.equal(fmp4Format.hasEveryTrack(moof([1]), null), true);
|
|
41
|
+
});
|