@torrent-tv/proxy 2.9.43 → 2.9.45
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 +3 -2
- package/services/hls-session-manager.js +51 -11
- package/services/hwaccel.js +21 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## 2.9.45
|
|
2
|
+
|
|
3
|
+
- **New**: HLS transcode output switched from MPEG-TS (`.ts`) to **fMP4/CMAF** (`.m4s` segments + a shared `init.mp4`). Codec parameter sets (SPS/PPS) now live once in the init segment (referenced by `#EXT-X-MAP`) instead of being repeated in every segment. Benefits: (1) hardware encoders that do not repeat parameter sets — notably the CM4 / HA-Yellow `h264_v4l2m2m` — produce independently-usable segments (on `.ts` the segments after the first lacked SPS/PPS → "non-existing PPS", which is why v4l2m2m was rejected); (2) lower container overhead. The synthetic VOD playlist now emits `#EXT-X-VERSION:7` + `#EXT-X-MAP`; each seek-restart run rewrites `init.mp4`, so `getFileStream` caches and serves the FIRST init for the whole session — it is codec-config-only and position-independent (verified: a single init cleanly decodes segments produced by a later seek-restart run). Raised v4l2m2m `-num_capture_buffers` to 32 (the default 4 deadlocks / drops frames on the CM4). **Verified**: server-side clean decode of the synthetic playlist across seek-restart runs; end-to-end playback **and seek** in hls.js 1.6.16. **Still needs**: verification on native iOS HLS (Safari fMP4) before relying on it. NOTE: v4l2m2m still emits a residual no-picture access unit that the strict startup test rejects, so it continues to fall back to software for now (no regression); fMP4 removes the SPS/PPS blocker — the remaining quirk is separate.
|
|
4
|
+
|
|
5
|
+
## 2.9.44
|
|
6
|
+
|
|
7
|
+
- **Fix**: Roll back to WebTorrent **2.8.5** (pinned) — 3.x introduced two regressions that broke downloading. (1) `torrent.downloaded`/`file.downloaded`/`file.progress` throw on a `deselect`-ed null piece (worked around in 2.9.43). (2) Worse: the internal piece picker itself throws `Cannot read properties of null (reading 'reserve'/'missing')` when it tries to request a block from a piece our seek prioritization (`prioritizeByteRange` `deselect`) removed — download freezes dead after a seek (field-observed: file stuck at ~51%, `down=0`, picker crashing every second). 2.8.5 is the known-good version: `select`/`deselect`/`critical` and the byte getters all work (verified — add, multi-file download, and the full deselect+critical seek pattern run with zero crashes on 2.8.5). Also pinned **`uint8-util` 2.2.6**: 2.8.5's own range is `^2.2.5`, which *allows* the incompatible 2.3.x that a fresh global install pulled (the original `arr2hex` crash), so the transitive version must be forced back — webtorrent dedupes to 2.2.6 while sub-deps that need 2.3.x keep their own nested copy. The 2.9.43 null-safe getter helpers are now redundant (2.8.5 getters never throw) but left in as harmless defensive code.
|
|
8
|
+
|
|
1
9
|
## 2.9.43
|
|
2
10
|
|
|
3
11
|
- **Fix**: Torrents stalled at the metadata/download stage on WebTorrent 3.x — the adaptive upload throttle dropped the client-wide limit to `0` whenever no file had an active reader (e.g. the window before the first read is acquired). In WebTorrent 3.x `throttleUpload(0)` blocks the ENTIRE swarm exchange client-wide — even peer connections and DOWNLOAD — not just seeding (verified: `throttleUpload(0)` → 0 peers, 0 download; `throttleUpload(8KB/s)` → peers connect, multi-MB/s download). The idle branch now returns a minimal keep-alive floor (`UPLOAD_IDLE_FLOOR_BYTES` = 8 KB/s) instead of 0; still effectively no seeding, but the swarm stays alive.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@torrent-tv/proxy",
|
|
3
|
-
"version": "2.9.
|
|
3
|
+
"version": "2.9.45",
|
|
4
4
|
"description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"publishConfig": {
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
"franc": "^6.2.0",
|
|
30
30
|
"get-port": "^7.1.0",
|
|
31
31
|
"node-datachannel": "^0.32.0",
|
|
32
|
-
"
|
|
32
|
+
"uint8-util": "2.2.6",
|
|
33
|
+
"webtorrent": "2.8.5",
|
|
33
34
|
"ws": "^8.18.2"
|
|
34
35
|
}
|
|
35
36
|
}
|
|
@@ -31,7 +31,12 @@ import {
|
|
|
31
31
|
} from "./ffmpeg-banner.js";
|
|
32
32
|
|
|
33
33
|
const PLAYLIST_FILE_NAME = "index.m3u8";
|
|
34
|
-
|
|
34
|
+
// fMP4 (CMAF) segments: SPS/PPS live once in the init segment, so every media
|
|
35
|
+
// segment is small and hardware encoders that do not repeat parameter sets
|
|
36
|
+
// (e.g. v4l2m2m) still produce independently-usable segments. The init segment
|
|
37
|
+
// is referenced by `#EXT-X-MAP` and fetched once by the player.
|
|
38
|
+
const SEGMENT_INIT_FILE_NAME = "init.mp4";
|
|
39
|
+
const SEGMENT_FILE_NAME_PATTERN = /^segment-\d{5}\.m4s$/;
|
|
35
40
|
const CLEANUP_INTERVAL_MS = 30_000;
|
|
36
41
|
const DEFAULT_SEGMENT_DURATION_SEC = 4;
|
|
37
42
|
// How many segments ahead of the current encode head a missing-segment request
|
|
@@ -203,18 +208,22 @@ function isSafeSessionId(value) {
|
|
|
203
208
|
* @returns {boolean}
|
|
204
209
|
*/
|
|
205
210
|
function isSafeFileName(fileName) {
|
|
206
|
-
return
|
|
211
|
+
return (
|
|
212
|
+
fileName === PLAYLIST_FILE_NAME ||
|
|
213
|
+
fileName === SEGMENT_INIT_FILE_NAME ||
|
|
214
|
+
SEGMENT_FILE_NAME_PATTERN.test(fileName)
|
|
215
|
+
);
|
|
207
216
|
}
|
|
208
217
|
|
|
209
218
|
/**
|
|
210
219
|
* Extract the zero-based segment index from a segment file name.
|
|
211
220
|
* Returns -1 when the name is not a valid segment file.
|
|
212
221
|
*
|
|
213
|
-
* @param {string} fileName - e.g. "segment-00012.
|
|
222
|
+
* @param {string} fileName - e.g. "segment-00012.m4s"
|
|
214
223
|
* @returns {number}
|
|
215
224
|
*/
|
|
216
225
|
function segmentIndexFromName(fileName) {
|
|
217
|
-
const match = /^segment-(\d{5})\.
|
|
226
|
+
const match = /^segment-(\d{5})\.m4s$/.exec(fileName);
|
|
218
227
|
if (!match) {
|
|
219
228
|
return -1;
|
|
220
229
|
}
|
|
@@ -1000,16 +1009,20 @@ export class HlsSessionManager {
|
|
|
1000
1009
|
}
|
|
1001
1010
|
const lines = [
|
|
1002
1011
|
"#EXTM3U",
|
|
1003
|
-
|
|
1012
|
+
// Version 7: required for fMP4 media segments + `#EXT-X-MAP`.
|
|
1013
|
+
"#EXT-X-VERSION:7",
|
|
1004
1014
|
`#EXT-X-TARGETDURATION:${Math.ceil(maxDuration)}`,
|
|
1005
1015
|
"#EXT-X-MEDIA-SEQUENCE:0",
|
|
1006
1016
|
"#EXT-X-PLAYLIST-TYPE:VOD",
|
|
1007
|
-
"#EXT-X-INDEPENDENT-SEGMENTS"
|
|
1017
|
+
"#EXT-X-INDEPENDENT-SEGMENTS",
|
|
1018
|
+
// The fMP4 init segment (codec config / SPS/PPS). Fetched once; applies to
|
|
1019
|
+
// every media segment below.
|
|
1020
|
+
`#EXT-X-MAP:URI="${SEGMENT_INIT_FILE_NAME}"`
|
|
1008
1021
|
];
|
|
1009
1022
|
for (let index = 0; index < count; index += 1) {
|
|
1010
1023
|
const duration = Math.max(0.1, boundaries[index + 1] - boundaries[index]);
|
|
1011
1024
|
lines.push(`#EXTINF:${duration.toFixed(6)},`);
|
|
1012
|
-
lines.push(`segment-${String(index).padStart(5, "0")}.
|
|
1025
|
+
lines.push(`segment-${String(index).padStart(5, "0")}.m4s`);
|
|
1013
1026
|
}
|
|
1014
1027
|
lines.push("#EXT-X-ENDLIST");
|
|
1015
1028
|
return `${lines.join("\n")}\n`;
|
|
@@ -1148,7 +1161,7 @@ export class HlsSessionManager {
|
|
|
1148
1161
|
}
|
|
1149
1162
|
const indices = [];
|
|
1150
1163
|
for (const name of names) {
|
|
1151
|
-
const match = /^segment-(\d{5})\.
|
|
1164
|
+
const match = /^segment-(\d{5})\.m4s$/.exec(name);
|
|
1152
1165
|
if (match) {
|
|
1153
1166
|
indices.push(parseInt(match[1], 10));
|
|
1154
1167
|
}
|
|
@@ -1161,7 +1174,7 @@ export class HlsSessionManager {
|
|
|
1161
1174
|
let bytes = 0;
|
|
1162
1175
|
try {
|
|
1163
1176
|
for (const index of completed) {
|
|
1164
|
-
const st = await stat(path.join(session.dirPath, `segment-${String(index).padStart(5, "0")}.
|
|
1177
|
+
const st = await stat(path.join(session.dirPath, `segment-${String(index).padStart(5, "0")}.m4s`));
|
|
1165
1178
|
bytes += st.size;
|
|
1166
1179
|
}
|
|
1167
1180
|
} catch {
|
|
@@ -1466,10 +1479,18 @@ export class HlsSessionManager {
|
|
|
1466
1479
|
"0",
|
|
1467
1480
|
"-hls_flags",
|
|
1468
1481
|
"independent_segments+temp_file",
|
|
1482
|
+
// fMP4 (CMAF) segments: codec config goes once into the init segment,
|
|
1483
|
+
// referenced by `#EXT-X-MAP`. Each seek-restart run rewrites init.mp4, but
|
|
1484
|
+
// it is codec-config only (position-independent), so getFileStream caches
|
|
1485
|
+
// and serves the first one for the whole session.
|
|
1486
|
+
"-hls_segment_type",
|
|
1487
|
+
"fmp4",
|
|
1488
|
+
"-hls_fmp4_init_filename",
|
|
1489
|
+
SEGMENT_INIT_FILE_NAME,
|
|
1469
1490
|
"-start_number",
|
|
1470
1491
|
String(safeIndex),
|
|
1471
1492
|
"-hls_segment_filename",
|
|
1472
|
-
"segment-%05d.
|
|
1493
|
+
"segment-%05d.m4s",
|
|
1473
1494
|
// ffmpeg writes its own playlist here; we ignore it and serve the
|
|
1474
1495
|
// synthetic VOD playlist instead (see getFileStream).
|
|
1475
1496
|
PLAYLIST_FILE_NAME
|
|
@@ -1779,6 +1800,25 @@ export class HlsSessionManager {
|
|
|
1779
1800
|
};
|
|
1780
1801
|
}
|
|
1781
1802
|
|
|
1803
|
+
// The fMP4 init segment (referenced by #EXT-X-MAP). It is codec-config only
|
|
1804
|
+
// and position-independent, but each seek-restart run REWRITES init.mp4, so
|
|
1805
|
+
// cache the FIRST one and always serve that — otherwise the init the player
|
|
1806
|
+
// fetched could differ from a later run's, breaking playback after a seek.
|
|
1807
|
+
if (fileName === SEGMENT_INIT_FILE_NAME) {
|
|
1808
|
+
if (session.initBytes) {
|
|
1809
|
+
return { kind: "file", stream: Readable.from([session.initBytes]), contentType: "video/mp4", isPlaylist: false };
|
|
1810
|
+
}
|
|
1811
|
+
try {
|
|
1812
|
+
const bytes = await readFile(path.join(session.dirPath, SEGMENT_INIT_FILE_NAME));
|
|
1813
|
+
session.initBytes = bytes;
|
|
1814
|
+
return { kind: "file", stream: Readable.from([bytes]), contentType: "video/mp4", isPlaylist: false };
|
|
1815
|
+
} catch {
|
|
1816
|
+
// Not produced yet — the encode run started at session creation writes
|
|
1817
|
+
// it early; the caller long-polls until it appears.
|
|
1818
|
+
return { kind: "warming-up" };
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1782
1822
|
const filePath = path.join(session.dirPath, fileName);
|
|
1783
1823
|
try {
|
|
1784
1824
|
await access(filePath);
|
|
@@ -1799,7 +1839,7 @@ export class HlsSessionManager {
|
|
|
1799
1839
|
contentType:
|
|
1800
1840
|
fileName === PLAYLIST_FILE_NAME
|
|
1801
1841
|
? "application/vnd.apple.mpegurl"
|
|
1802
|
-
: "video/
|
|
1842
|
+
: "video/mp4",
|
|
1803
1843
|
isPlaylist: fileName === PLAYLIST_FILE_NAME
|
|
1804
1844
|
};
|
|
1805
1845
|
} catch (_error) {
|
package/services/hwaccel.js
CHANGED
|
@@ -303,6 +303,10 @@ function v4l2m2mDescriptor() {
|
|
|
303
303
|
"-vf",
|
|
304
304
|
`scale=${w}:${h}:force_original_aspect_ratio=decrease:force_divisible_by=2,fps=${outFps},format=yuv420p`,
|
|
305
305
|
"-c:v", "h264_v4l2m2m",
|
|
306
|
+
// More capture buffers than the default 4 — the default deadlocks /
|
|
307
|
+
// drops frames on the CM4 encoder ("All capture buffers returned to
|
|
308
|
+
// userspace").
|
|
309
|
+
"-num_capture_buffers", "32",
|
|
306
310
|
"-b:v", "3M",
|
|
307
311
|
"-g", String(outFps * segmentDurationSec),
|
|
308
312
|
...keyFrameArgs(segmentDurationSec)
|
|
@@ -438,7 +442,7 @@ function buildEncoderTestArgs(descriptor, segmentDurationSec, outDir) {
|
|
|
438
442
|
encode = ["-c:v", "h264_nvenc", "-preset", "p4", "-cq", "24", "-pix_fmt", "yuv420p", ...kf];
|
|
439
443
|
break;
|
|
440
444
|
case "v4l2m2m":
|
|
441
|
-
encode = ["-pix_fmt", "yuv420p", "-c:v", "h264_v4l2m2m", "-b:v", "3M", "-g", String(TRANSCODE_FPS * segmentDurationSec), ...kf];
|
|
445
|
+
encode = ["-pix_fmt", "yuv420p", "-c:v", "h264_v4l2m2m", "-num_capture_buffers", "32", "-b:v", "3M", "-g", String(TRANSCODE_FPS * segmentDurationSec), ...kf];
|
|
442
446
|
break;
|
|
443
447
|
default:
|
|
444
448
|
encode = ["-c:v", "libx264", "-preset", "ultrafast", "-pix_fmt", "yuv420p", ...kf];
|
|
@@ -450,7 +454,10 @@ function buildEncoderTestArgs(descriptor, segmentDurationSec, outDir) {
|
|
|
450
454
|
"-hls_time", String(segmentDurationSec),
|
|
451
455
|
"-hls_list_size", "0",
|
|
452
456
|
"-hls_flags", "independent_segments",
|
|
453
|
-
|
|
457
|
+
// fMP4 (CMAF) — matches the runtime pipeline (hls-session-manager).
|
|
458
|
+
"-hls_segment_type", "fmp4",
|
|
459
|
+
"-hls_fmp4_init_filename", "init.mp4",
|
|
460
|
+
"-hls_segment_filename", path.join(outDir, "seg-%03d.m4s"),
|
|
454
461
|
path.join(outDir, "index.m3u8")
|
|
455
462
|
];
|
|
456
463
|
return [...pre, ...source, ...encode, ...hlsOut];
|
|
@@ -469,24 +476,24 @@ function buildEncoderTestArgs(descriptor, segmentDurationSec, outDir) {
|
|
|
469
476
|
async function verifySegmentsDecodeCleanly(ffmpegBin, outDir) {
|
|
470
477
|
let files;
|
|
471
478
|
try {
|
|
472
|
-
files = readdirSync(outDir).filter((n) => /^seg-\d+\.
|
|
479
|
+
files = readdirSync(outDir).filter((n) => /^seg-\d+\.m4s$/.test(n));
|
|
473
480
|
} catch {
|
|
474
481
|
return false;
|
|
475
482
|
}
|
|
476
483
|
if (files.length < 2) {
|
|
477
484
|
return false;
|
|
478
485
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
return
|
|
486
|
+
// fMP4: parameter sets (SPS/PPS) live in init.mp4, not in each segment.
|
|
487
|
+
// Decode the whole playlist (ffmpeg's own, which references init.mp4 via
|
|
488
|
+
// #EXT-X-MAP), so every segment is exercised together with the init. Any
|
|
489
|
+
// corrupt / non-conformant segment (e.g. some V4L2 M2M builds emit a stray
|
|
490
|
+
// no-picture access unit) surfaces as a decode error here.
|
|
491
|
+
const result = await runFfmpeg(
|
|
492
|
+
ffmpegBin,
|
|
493
|
+
["-hide_banner", "-loglevel", "error", "-i", path.join(outDir, "index.m3u8"), "-f", "null", "-"],
|
|
494
|
+
12000
|
|
495
|
+
);
|
|
496
|
+
return result.code === 0 && result.stderr.trim().length === 0;
|
|
490
497
|
}
|
|
491
498
|
|
|
492
499
|
/**
|