From f4ecf96918dbcc95bbe5ac20bf10c7d05b1e13a2 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 4 May 2026 20:16:10 -0700 Subject: [PATCH] fix(engine,cli,producer): address PR #627 review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - engine/chunkEncoder, engine/streamingEncoder: extend `-bf 0` to GPU h264 paths (nvenc, qsv, vaapi) and `-b_strategy 0` for qsv so GPU-encoded outputs avoid negative-DTS freezes too — not just SW libx264. - engine/videoFrameExtractor: detect mid-path traversal (e.g. `assets/../../foo.mp4`) by normalizing first and re-anchoring at the project root. Adds a regression test. - engine/videoFrameExtractor: dedupe stderr "src not resolvable" warnings by `video.src` so a comp with N broken sources logs once, not N times. - engine/videoFrameExtractor.test: drop dynamic `require("node:fs")`, use ES `import { writeFileSync } from "node:fs"`. - engine/ffprobe: extract `readTagCI` helper for case-insensitive ffprobe tag reads (will recur for other libavformat-versioned sidecar tags). - cli/background-removal/pipeline: collapse Quality / QUALITIES / QUALITY_CRF / DEFAULT_QUALITY / isQuality surface using `Quality = keyof typeof QUALITY_CRF`. - producer/renderOrchestrator: replace `v.src.startsWith("/")` with `isAbsolute(v.src)` in the HDR probe path so Windows absolute paths (`C:\...`) aren't treated as relative — matches the audioMixer guard. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../cli/src/background-removal/pipeline.ts | 12 ++-- packages/engine/src/services/chunkEncoder.ts | 19 ++++++ .../engine/src/services/streamingEncoder.ts | 13 ++++ .../src/services/videoFrameExtractor.test.ts | 53 ++++++++++----- .../src/services/videoFrameExtractor.ts | 68 +++++++++++++------ packages/engine/src/utils/ffprobe.ts | 23 +++++-- .../src/services/renderOrchestrator.ts | 7 +- 7 files changed, 138 insertions(+), 57 deletions(-) diff --git a/packages/cli/src/background-removal/pipeline.ts b/packages/cli/src/background-removal/pipeline.ts index 03ce3cb63..99b95386e 100644 --- a/packages/cli/src/background-removal/pipeline.ts +++ b/packages/cli/src/background-removal/pipeline.ts @@ -20,15 +20,15 @@ import { type Device, type ModelId } from "./manager.js"; export type OutputFormat = "webm" | "mov" | "png"; -export type Quality = "fast" | "balanced" | "best"; - -export const QUALITIES: readonly Quality[] = ["fast", "balanced", "best"] as const; - -export const QUALITY_CRF: Record = { +export const QUALITY_CRF = { fast: 30, balanced: 18, best: 12, -}; +} as const; + +export type Quality = keyof typeof QUALITY_CRF; + +export const QUALITIES = Object.keys(QUALITY_CRF) as readonly Quality[]; export const DEFAULT_QUALITY: Quality = "balanced"; diff --git a/packages/engine/src/services/chunkEncoder.ts b/packages/engine/src/services/chunkEncoder.ts index a5c7ffe3e..6e9ea1b2d 100644 --- a/packages/engine/src/services/chunkEncoder.ts +++ b/packages/engine/src/services/chunkEncoder.ts @@ -139,6 +139,25 @@ export function buildEncoderArgs( else args.push("-global_quality", String(quality)); break; } + + // Same B-frame story as the SW branch below — nvenc emits B-frames + // by default (qsv via b_strategy, vaapi too), and the negative-DTS + // freeze hits the same downstream players. The unconditional + // `-avoid_negative_ts make_zero` near the bottom of this function + // covers the mux level, but we belt-and-suspenders the encoder too + // so even tools that consume the chunk file directly (without going + // through our mux step) play correctly. videotoolbox doesn't accept + // `-bf` so it's skipped — videotoolbox h264 also doesn't emit + // negative DTS in practice on macOS Sonoma+. + if ( + codec === "h264" && + (gpuEncoder === "nvenc" || gpuEncoder === "qsv" || gpuEncoder === "vaapi") + ) { + args.push("-bf", "0"); + if (gpuEncoder === "qsv") { + args.push("-b_strategy", "0"); + } + } } else { const encoderName = codec === "h264" ? "libx264" : "libx265"; args.push("-c:v", encoderName, "-preset", preset); diff --git a/packages/engine/src/services/streamingEncoder.ts b/packages/engine/src/services/streamingEncoder.ts index a527df830..03c0d0735 100644 --- a/packages/engine/src/services/streamingEncoder.ts +++ b/packages/engine/src/services/streamingEncoder.ts @@ -221,6 +221,19 @@ export function buildStreamingArgs( else args.push("-global_quality", String(quality)); break; } + + // Mirror SW branch: GPU h264 paths emit B-frames by default (nvenc, qsv, + // vaapi) and produce the same negative-DTS freeze for downstream players. + // See chunkEncoder.buildEncoderArgs for the full explanation. + if ( + codec === "h264" && + (gpuEncoder === "nvenc" || gpuEncoder === "qsv" || gpuEncoder === "vaapi") + ) { + args.push("-bf", "0"); + if (gpuEncoder === "qsv") { + args.push("-b_strategy", "0"); + } + } } else { const encoderName = codec === "h264" ? "libx264" : "libx265"; args.push("-c:v", encoderName, "-preset", preset); diff --git a/packages/engine/src/services/videoFrameExtractor.test.ts b/packages/engine/src/services/videoFrameExtractor.test.ts index e83ca1254..29fd98eed 100644 --- a/packages/engine/src/services/videoFrameExtractor.test.ts +++ b/packages/engine/src/services/videoFrameExtractor.test.ts @@ -1,5 +1,13 @@ import { afterAll, beforeAll, describe, expect, it } from "vitest"; -import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs"; +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + readdirSync, + rmSync, + writeFileSync, +} from "node:fs"; import { createHash } from "node:crypto"; import { join } from "node:path"; import { tmpdir } from "node:os"; @@ -26,19 +34,11 @@ import { runFfmpeg } from "../utils/runFfmpeg.js"; // synthesized VFR fixture. const HAS_FFMPEG = spawnSync("ffmpeg", ["-version"]).status === 0; -// Regression: a long-standing footgun where `