@torrent-tv/proxy 2.9.0 → 2.9.2
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/package.json +1 -1
- package/services/hls-session-manager.js +1 -1
- package/services/hwaccel.js +108 -35
package/package.json
CHANGED
|
@@ -512,7 +512,7 @@ export class HlsSessionManager {
|
|
|
512
512
|
|
|
513
513
|
logger.info(
|
|
514
514
|
`transcode ${sessionId} start "${logName}" ` +
|
|
515
|
-
`video=${transcodeVideo ?
|
|
515
|
+
`video=${transcodeVideo ? this.videoEncoder.name : "copy"} audio=${transcodeAudio ? "aac" : "copy"} ` +
|
|
516
516
|
`duration=${hasDuration ? formatSeconds(durationSeconds) : "unknown"} segments=${segmentCount}`
|
|
517
517
|
);
|
|
518
518
|
|
package/services/hwaccel.js
CHANGED
|
@@ -21,7 +21,9 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { spawn } from "node:child_process";
|
|
24
|
-
import { readdirSync } from "node:fs";
|
|
24
|
+
import { mkdtempSync, readdirSync, rmSync } from "node:fs";
|
|
25
|
+
import os from "node:os";
|
|
26
|
+
import path from "node:path";
|
|
25
27
|
|
|
26
28
|
const SOFTWARE_PRESET = "superfast";
|
|
27
29
|
const SOFTWARE_CRF = "24";
|
|
@@ -141,8 +143,11 @@ function nvencDescriptor() {
|
|
|
141
143
|
|
|
142
144
|
/** @returns {import("./hwaccel.js").VideoEncoderDescriptor} */
|
|
143
145
|
function v4l2m2mDescriptor() {
|
|
144
|
-
// ARM SoC (e.g. Raspberry Pi) stateful M2M encoder. No GPU
|
|
145
|
-
// software,
|
|
146
|
+
// ARM SoC (e.g. Raspberry Pi / HA Yellow) stateful M2M encoder. No GPU
|
|
147
|
+
// scaler — scale in software, hand YUV420 frames to the hardware encoder.
|
|
148
|
+
// `-g` aligns the GOP to the segment length so an IDR lands on every segment
|
|
149
|
+
// boundary; this is verified by the keyframe-alignment test before use,
|
|
150
|
+
// because v4l2m2m does not always honour these hints.
|
|
146
151
|
return {
|
|
147
152
|
name: "h264_v4l2m2m",
|
|
148
153
|
kind: "v4l2m2m",
|
|
@@ -155,12 +160,14 @@ function v4l2m2mDescriptor() {
|
|
|
155
160
|
`scale=${w}:${h}:force_original_aspect_ratio=decrease:force_divisible_by=2,fps=${TRANSCODE_FPS},format=yuv420p`,
|
|
156
161
|
"-c:v", "h264_v4l2m2m",
|
|
157
162
|
"-b:v", "3M",
|
|
163
|
+
"-g", String(TRANSCODE_FPS * segmentDurationSec),
|
|
158
164
|
...keyFrameArgs(segmentDurationSec)
|
|
159
165
|
];
|
|
160
166
|
}
|
|
161
167
|
};
|
|
162
168
|
}
|
|
163
169
|
|
|
170
|
+
|
|
164
171
|
/**
|
|
165
172
|
* @typedef {Object} VideoEncoderDescriptor
|
|
166
173
|
* @property {string} name
|
|
@@ -253,44 +260,89 @@ function hasV4l2Device() {
|
|
|
253
260
|
}
|
|
254
261
|
|
|
255
262
|
/**
|
|
256
|
-
*
|
|
257
|
-
*
|
|
263
|
+
* Build a full ffmpeg command that encodes a short, *moving* synthetic clip
|
|
264
|
+
* (testsrc2 — far more representative than a static black frame) through the
|
|
265
|
+
* candidate encoder into real HLS segments in `outDir`, with keyframes forced
|
|
266
|
+
* on segment boundaries. Verifying the resulting segments (see
|
|
267
|
+
* {@link verifySegmentsDecodeCleanly}) catches encoders that silently produce
|
|
268
|
+
* a corrupted or non-IDR-aligned stream (e.g. some V4L2 M2M builds).
|
|
258
269
|
*
|
|
259
270
|
* @param {VideoEncoderDescriptor} descriptor
|
|
271
|
+
* @param {number} segmentDurationSec
|
|
272
|
+
* @param {string} outDir
|
|
260
273
|
* @returns {string[]}
|
|
261
274
|
*/
|
|
262
|
-
function
|
|
263
|
-
const
|
|
275
|
+
function buildEncoderTestArgs(descriptor, segmentDurationSec, outDir) {
|
|
276
|
+
const durationSec = Math.max(8, segmentDurationSec * 3);
|
|
277
|
+
const source = ["-f", "lavfi", "-i", `testsrc2=s=640x360:r=${TRANSCODE_FPS}:d=${durationSec}`];
|
|
278
|
+
const kf = keyFrameArgs(segmentDurationSec);
|
|
279
|
+
|
|
280
|
+
/** @type {string[]} */
|
|
281
|
+
let pre = ["-hide_banner", "-loglevel", "error"];
|
|
282
|
+
/** @type {string[]} */
|
|
283
|
+
let encode;
|
|
264
284
|
switch (descriptor.kind) {
|
|
265
285
|
case "vaapi":
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
...src,
|
|
270
|
-
"-vf", "format=nv12,hwupload", "-c:v", "h264_vaapi",
|
|
271
|
-
"-f", "null", "-"
|
|
272
|
-
];
|
|
286
|
+
pre = [...pre, "-vaapi_device", String(descriptor.device)];
|
|
287
|
+
encode = ["-vf", "format=nv12,hwupload", "-c:v", "h264_vaapi", "-qp", "24", ...kf];
|
|
288
|
+
break;
|
|
273
289
|
case "qsv":
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
...src,
|
|
278
|
-
"-vf", "hwupload=extra_hw_frames=16,format=qsv", "-c:v", "h264_qsv",
|
|
279
|
-
"-f", "null", "-"
|
|
280
|
-
];
|
|
290
|
+
pre = [...pre, "-qsv_device", String(descriptor.device)];
|
|
291
|
+
encode = ["-vf", "hwupload=extra_hw_frames=16,format=qsv", "-c:v", "h264_qsv", "-global_quality", "24", ...kf];
|
|
292
|
+
break;
|
|
281
293
|
case "nvenc":
|
|
282
|
-
|
|
294
|
+
encode = ["-c:v", "h264_nvenc", "-preset", "p4", "-cq", "24", "-pix_fmt", "yuv420p", ...kf];
|
|
295
|
+
break;
|
|
283
296
|
case "v4l2m2m":
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
...src, "-pix_fmt", "yuv420p", "-c:v", "h264_v4l2m2m", "-f", "null", "-"
|
|
287
|
-
];
|
|
297
|
+
encode = ["-pix_fmt", "yuv420p", "-c:v", "h264_v4l2m2m", "-b:v", "3M", "-g", String(TRANSCODE_FPS * segmentDurationSec), ...kf];
|
|
298
|
+
break;
|
|
288
299
|
default:
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
...src, "-c:v", "libx264", "-preset", "ultrafast", "-f", "null", "-"
|
|
292
|
-
];
|
|
300
|
+
encode = ["-c:v", "libx264", "-preset", "ultrafast", "-pix_fmt", "yuv420p", ...kf];
|
|
301
|
+
break;
|
|
293
302
|
}
|
|
303
|
+
|
|
304
|
+
const hlsOut = [
|
|
305
|
+
"-f", "hls",
|
|
306
|
+
"-hls_time", String(segmentDurationSec),
|
|
307
|
+
"-hls_list_size", "0",
|
|
308
|
+
"-hls_flags", "independent_segments",
|
|
309
|
+
"-hls_segment_filename", path.join(outDir, "seg-%03d.ts"),
|
|
310
|
+
path.join(outDir, "index.m3u8")
|
|
311
|
+
];
|
|
312
|
+
return [...pre, ...source, ...encode, ...hlsOut];
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Verify the HLS segments produced by the test encode are valid: at least two
|
|
317
|
+
* segments exist, and each decodes standalone without errors. A segment that
|
|
318
|
+
* does not begin with a keyframe (broken/corrupted output) emits decode errors
|
|
319
|
+
* when read on its own, which fails this check.
|
|
320
|
+
*
|
|
321
|
+
* @param {string} ffmpegBin
|
|
322
|
+
* @param {string} outDir
|
|
323
|
+
* @returns {Promise<boolean>}
|
|
324
|
+
*/
|
|
325
|
+
async function verifySegmentsDecodeCleanly(ffmpegBin, outDir) {
|
|
326
|
+
let files;
|
|
327
|
+
try {
|
|
328
|
+
files = readdirSync(outDir).filter((n) => /^seg-\d+\.ts$/.test(n)).sort();
|
|
329
|
+
} catch {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
if (files.length < 2) {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
for (const file of files) {
|
|
336
|
+
const result = await runFfmpeg(
|
|
337
|
+
ffmpegBin,
|
|
338
|
+
["-hide_banner", "-loglevel", "error", "-i", path.join(outDir, file), "-f", "null", "-"],
|
|
339
|
+
8000
|
|
340
|
+
);
|
|
341
|
+
if (result.code !== 0 || result.stderr.trim().length > 0) {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return true;
|
|
294
346
|
}
|
|
295
347
|
|
|
296
348
|
/**
|
|
@@ -298,10 +350,10 @@ function testEncodeArgs(descriptor) {
|
|
|
298
350
|
* software libx264). Each hardware candidate is verified with a real
|
|
299
351
|
* test-encode before being selected.
|
|
300
352
|
*
|
|
301
|
-
* @param {{ ffmpegBin: string, logger?: { info: (m: string) => void, warn: (m: string) => void } }} options
|
|
353
|
+
* @param {{ ffmpegBin: string, logger?: { info: (m: string) => void, warn: (m: string) => void }, segmentDurationSec?: number }} options
|
|
302
354
|
* @returns {Promise<VideoEncoderDescriptor>}
|
|
303
355
|
*/
|
|
304
|
-
export async function detectVideoEncoder({ ffmpegBin, logger }) {
|
|
356
|
+
export async function detectVideoEncoder({ ffmpegBin, logger, segmentDurationSec = 4 }) {
|
|
305
357
|
const log = logger ?? { info: () => {}, warn: () => {} };
|
|
306
358
|
const software = softwareDescriptor();
|
|
307
359
|
|
|
@@ -324,20 +376,41 @@ export async function detectVideoEncoder({ ffmpegBin, logger }) {
|
|
|
324
376
|
if (has("h264_vaapi") && renderNodes.length > 0) {
|
|
325
377
|
candidates.push(vaapiDescriptor(renderNodes[0]));
|
|
326
378
|
}
|
|
379
|
+
// h264_v4l2m2m (ARM SoC / Raspberry Pi / HA Yellow). It is gated behind the
|
|
380
|
+
// strict keyframe-alignment test below, because some V4L2 M2M builds silently
|
|
381
|
+
// emit a corrupted / non-IDR-aligned stream; the test rejects those and the
|
|
382
|
+
// host falls back to software libx264.
|
|
327
383
|
if (has("h264_v4l2m2m") && hasV4l2Device()) {
|
|
328
384
|
candidates.push(v4l2m2mDescriptor());
|
|
329
385
|
}
|
|
330
386
|
|
|
331
387
|
for (const candidate of candidates) {
|
|
332
|
-
const
|
|
333
|
-
|
|
388
|
+
const dir = mkdtempSync(path.join(os.tmpdir(), "tt-hwtest-"));
|
|
389
|
+
let ok = false;
|
|
390
|
+
try {
|
|
391
|
+
const encoded = await runFfmpeg(
|
|
392
|
+
ffmpegBin,
|
|
393
|
+
buildEncoderTestArgs(candidate, segmentDurationSec, dir),
|
|
394
|
+
25000
|
|
395
|
+
);
|
|
396
|
+
if (encoded.code === 0) {
|
|
397
|
+
ok = await verifySegmentsDecodeCleanly(ffmpegBin, dir);
|
|
398
|
+
}
|
|
399
|
+
} finally {
|
|
400
|
+
try {
|
|
401
|
+
rmSync(dir, { recursive: true, force: true });
|
|
402
|
+
} catch {
|
|
403
|
+
// best effort
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (ok) {
|
|
334
407
|
log.info(
|
|
335
408
|
`hwaccel: using hardware encoder ${candidate.name}` +
|
|
336
409
|
`${candidate.device ? ` (${candidate.device})` : ""}`
|
|
337
410
|
);
|
|
338
411
|
return candidate;
|
|
339
412
|
}
|
|
340
|
-
log.warn(`hwaccel: ${candidate.name}
|
|
413
|
+
log.warn(`hwaccel: ${candidate.name} failed the HLS keyframe-alignment test; skipping`);
|
|
341
414
|
}
|
|
342
415
|
|
|
343
416
|
log.info("hwaccel: no working hardware encoder; using software libx264");
|