fix(engine): h265 renders no longer fail on hosts with more than 16 cores (#6193)

* fix(engine): h265 renders no longer fail on hosts with more than 16 cores

libx265 rejects more than 16 frame threads, so the thread count taken from the
CPU quota is now capped at 16 for x265. x264 is unchanged.

* fix(engine): pass the encoder to the thread cap in the CFR re-encode
This commit is contained in:
Miguel Ángel
2026-10-24 05:33:56 +00:00
committed by GitHub
parent ebb057fb62
commit 53e1dfce9e
5 changed files with 44 additions and 7 deletions
+1 -1
View File
@@ -327,7 +327,7 @@ export function buildEncoderArgs(
} else {
const encoderName = codec === "h264" ? "libx264" : "libx265";
args.push("-c:v", encoderName, "-preset", preset);
appendSoftwareEncoderThreadsArg(args);
appendSoftwareEncoderThreadsArg(args, encoderName);
if (bitrate) args.push("-b:v", bitrate);
else args.push("-crf", String(quality));
@@ -14,7 +14,7 @@ vi.mock("node:os", async (importOriginal) => ({
import { buildEncoderArgs } from "./chunkEncoder.js";
import type { EncoderOptions } from "./chunkEncoder.types.js";
import { appendSoftwareEncoderThreadsArg } from "./softwareEncoderThreads.js";
import { appendSoftwareEncoderThreadsArg, X265_MAX_THREADS } from "./softwareEncoderThreads.js";
import { buildStreamingArgs } from "./streamingEncoder.js";
const FFMPEG = getFfmpegBinary();
@@ -42,11 +42,22 @@ const threadsOf = (args: string[]) => args[args.indexOf("-threads") + 1];
describe("software encoder thread count", () => {
it("takes the CPU quota and floors at one", () => {
const args: string[] = [];
appendSoftwareEncoderThreadsArg(args, 6.9);
appendSoftwareEncoderThreadsArg(args, 0);
appendSoftwareEncoderThreadsArg(args, "libx264", 6.9);
appendSoftwareEncoderThreadsArg(args, "libx264", 0);
expect(args).toEqual(["-threads", "6", "-threads", "1"]);
});
it("caps libx265 at 16 threads and leaves libx264 uncapped", () => {
const x265: string[] = [];
const x264: string[] = [];
appendSoftwareEncoderThreadsArg(x265, "libx265", 64);
appendSoftwareEncoderThreadsArg(x264, "libx264", 64);
expect([x265, x264]).toEqual([
["-threads", "16"],
["-threads", "64"],
]);
});
it.each(["h264", "h265"] as const)(
"pins %s threads in both encoders to the quota, not the host",
(codec) => {
@@ -65,6 +76,28 @@ describe("software encoder thread count", () => {
);
});
describe.skipIf(!HAS_FFMPEG)("libx265 thread limit", () => {
const encode = (threads: number) =>
spawnSync(FFMPEG, [
...QUIET,
...SOURCE,
"-frames:v",
"4",
"-c:v",
"libx265",
"-threads",
String(threads),
"-f",
"null",
"-",
]).status;
it("rejects 17 threads and accepts the cap", () => {
expect(encode(X265_MAX_THREADS + 1)).not.toBe(0);
expect(encode(X265_MAX_THREADS)).toBe(0);
}, 60_000);
});
describe.skipIf(!HAS_TASKSET || !HAS_FFMPEG)("encoded bytes across host sizes", () => {
const dirs: string[] = [];
afterEach(() => {
@@ -1,9 +1,13 @@
import { availableParallelism } from "node:os";
export const X265_MAX_THREADS = 16;
/** x264/x265 bitstreams depend on the thread count, so it comes from the CPU quota, never ffmpeg's host-core guess. */
export function appendSoftwareEncoderThreadsArg(
args: string[],
encoder: "libx264" | "libx265",
parallelism: number = availableParallelism(),
): void {
args.push("-threads", String(Math.max(1, Math.floor(parallelism))));
const cap = encoder === "libx265" ? X265_MAX_THREADS : Infinity;
args.push("-threads", String(Math.min(cap, Math.max(1, Math.floor(parallelism)))));
}
@@ -339,7 +339,7 @@ export function buildStreamingArgs(
} else {
const encoderName = codec === "h264" ? "libx264" : "libx265";
args.push("-c:v", encoderName, "-preset", preset);
appendSoftwareEncoderThreadsArg(args);
appendSoftwareEncoderThreadsArg(args, encoderName);
if (bitrate) args.push("-b:v", bitrate);
else args.push("-crf", String(quality));
@@ -285,7 +285,7 @@ export function cfrEncodeArgs(
"-r",
fpsArg,
];
appendSoftwareEncoderThreadsArg(args);
appendSoftwareEncoderThreadsArg(args, "libx264");
return args;
}