mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix(cli): warn when a WebM render silently drops its alpha channel [P2] (#2044)
* fix(cli): warn when a WebM render loses its requested alpha channel HyperFrames always encodes WebM with an alpha-capable pixel format (yuva420p), but some ffmpeg/libvpx builds silently emit opaque yuv420p even when handed alpha input and -pix_fmt yuva420p. The render succeeds and plays back fine, so the lost transparency is only discovered after compositing (users report shipping a solid-black clip and colorkeying it out by hand). After a WebM render, best-effort ffprobe the output's pix_fmt; if it lacks alpha, print a non-blocking warning that names the concrete remedy (--format mov / ProRes 4444). Only WebM is checked (mp4 is intentionally opaque; mov/png carry alpha through paths that don't hit libvpx-vp9), and a failed probe stays silent rather than warning speculatively. Pure decision (pixelFormatHasAlpha / webmAlphaAdvisory) unit-tested; verified end-to-end that a transparent WebM render now surfaces the warning while an MP4 render stays silent. * fix(cli): key WebM alpha check on ALPHA_MODE tag, not pix_fmt (R1 blocker) R1 (Rames/Via) correctly flagged the detection as ~100% false-positive on working builds. libvpx-vp9 stores the alpha plane in a Matroska BlockAdditional sidecar, so ffprobe ALWAYS reports pix_fmt=yuv420p for a correct transparent WebM (per docs/guides/rendering.mdx #1823 and the webm-concat-copy smoke test). The real signal is the stream-level ALPHA_MODE=1 tag: a working encode writes it; a build that can't emit the sidecar omits it and produces genuinely opaque output. Re-cut the probe to read stream_tags=alpha_mode (JSON, case-insensitive) and warn only when a probed WebM lacks ALPHA_MODE=1. Tests inverted accordingly (alphaMode:true → silent; alphaMode:false → warn). Verified end-to-end: a transparent webm render on an alpha-preserving build (ALPHA_MODE=1) now emits 0 warnings; previously it warned on every webm.
This commit is contained in:
@@ -57,6 +57,7 @@ import { formatLintFindings } from "../utils/lintFormat.js";
|
||||
import { loadProducer } from "../utils/producer.js";
|
||||
import { c } from "../ui/colors.js";
|
||||
import { formatBytes, formatRenderSummaryDetail, errorBox } from "../ui/format.js";
|
||||
import { warnIfWebmAlphaDropped } from "../utils/webmAlphaCheck.js";
|
||||
import { renderProgress } from "../ui/progress.js";
|
||||
import {
|
||||
trackRenderComplete,
|
||||
@@ -1373,6 +1374,7 @@ async function renderDocker(
|
||||
// threaded back here; the summary shows render time only (never a wrong video
|
||||
// length). Probe the output with ffprobe if a duration figure is wanted here.
|
||||
printRenderComplete(outputPath, elapsed, options.quiet);
|
||||
warnIfWebmAlphaDropped(outputPath, options.format, options.quiet);
|
||||
if (options.exitAfterComplete) scheduleRenderProcessExit();
|
||||
return { renderTimeMs: elapsed };
|
||||
}
|
||||
@@ -1475,6 +1477,7 @@ export async function renderLocal(
|
||||
job.perfSummary?.compositionDurationSeconds,
|
||||
job.perfSummary?.totalFrames,
|
||||
);
|
||||
warnIfWebmAlphaDropped(outputPath, options.format, options.quiet);
|
||||
if (!options.skipFeedback) {
|
||||
await maybePromptRenderFeedback({
|
||||
renderDurationMs: elapsed,
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { webmAlphaAdvisory } from "./webmAlphaCheck.js";
|
||||
|
||||
describe("webmAlphaAdvisory", () => {
|
||||
it("warns when a probed webm lacks the ALPHA_MODE sidecar tag", () => {
|
||||
// A build that dropped the alpha sidecar: ffprobe reported a stream but no
|
||||
// ALPHA_MODE=1 tag. (pix_fmt is irrelevant — libvpx-vp9 always reports
|
||||
// yuv420p; the sidecar tag is the real signal.)
|
||||
const msg = webmAlphaAdvisory("webm", { probed: true, alphaMode: false });
|
||||
expect(msg).toBeDefined();
|
||||
expect(msg).toContain("ALPHA_MODE");
|
||||
expect(msg).toContain("--format mov");
|
||||
});
|
||||
|
||||
it("stays SILENT when the webm carries ALPHA_MODE=1 (working transparent WebM)", () => {
|
||||
// Regression guard for the #2044 R1 blocker: a correct transparent WebM
|
||||
// reports pix_fmt=yuv420p BUT ALPHA_MODE=1 — it must NOT warn.
|
||||
expect(webmAlphaAdvisory("webm", { probed: true, alphaMode: true })).toBeUndefined();
|
||||
});
|
||||
|
||||
it("stays silent when the output could not be probed", () => {
|
||||
expect(webmAlphaAdvisory("webm", { probed: false, alphaMode: false })).toBeUndefined();
|
||||
});
|
||||
|
||||
it("stays silent for non-webm formats (mp4 opaque; mov carries alpha natively)", () => {
|
||||
expect(webmAlphaAdvisory("mp4", { probed: true, alphaMode: false })).toBeUndefined();
|
||||
expect(webmAlphaAdvisory("mov", { probed: true, alphaMode: false })).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { findFFprobe } from "../browser/ffmpeg.js";
|
||||
import { c } from "../ui/colors.js";
|
||||
|
||||
/**
|
||||
* Result of probing a WebM's first video stream for its alpha sidecar.
|
||||
* `probed` distinguishes "ffprobe ran and reported a video stream" from a
|
||||
* failed/absent probe (so a probe failure stays silent, not a false warning).
|
||||
*/
|
||||
export interface WebmAlphaProbe {
|
||||
probed: boolean;
|
||||
/** True when the VP9 stream declares the alpha sidecar (ALPHA_MODE=1 tag). */
|
||||
alphaMode: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide whether to warn that a WebM render lost its transparency, or
|
||||
* `undefined` when nothing is wrong / can't be determined.
|
||||
*
|
||||
* IMPORTANT — the signal is the `ALPHA_MODE=1` stream tag, NOT `pix_fmt`.
|
||||
* libvpx-vp9 stores the alpha plane in a Matroska BlockAdditional sidecar and
|
||||
* ALWAYS reports `pix_fmt=yuv420p` even for a correct transparent WebM (see
|
||||
* docs/guides/rendering.mdx and the webm-concat-copy smoke test). A working
|
||||
* encode writes `ALPHA_MODE=1`; an ffmpeg/libvpx build that can't emit the
|
||||
* sidecar omits the tag and produces genuinely opaque output. Keying on the
|
||||
* tag means builds that preserve alpha stay silent (no false positive) and
|
||||
* only builds that actually drop it get the warning.
|
||||
*
|
||||
* Pure over (format, probe) so the decision is unit-testable without spawning
|
||||
* ffprobe. Only WebM is checked; MP4 is intentionally opaque and MOV/PNG-seq
|
||||
* carry alpha through non-libvpx paths.
|
||||
*/
|
||||
export function webmAlphaAdvisory(format: string, probe: WebmAlphaProbe): string | undefined {
|
||||
if (format !== "webm") return undefined;
|
||||
if (!probe.probed || probe.alphaMode) return undefined;
|
||||
return (
|
||||
"The WebM output has no VP9 alpha sidecar (the ALPHA_MODE stream tag is absent), " +
|
||||
"so transparency was flattened to opaque. Your ffmpeg/libvpx-vp9 build cannot emit " +
|
||||
"the alpha plane on this platform. For guaranteed transparency, re-render with " +
|
||||
"--format mov (ProRes 4444)."
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort ffprobe of a file's first video stream for the ALPHA_MODE tag.
|
||||
* Returns `{ probed: false }` on any failure (no ffprobe, spawn error,
|
||||
* unreadable file, no video stream) — this is a diagnostic, never a reason to
|
||||
* fail a completed render. The tag key is matched case-insensitively (ffprobe
|
||||
* surfaces it as `ALPHA_MODE`; some builds lower-case it).
|
||||
*/
|
||||
function probeWebmAlpha(filePath: string): WebmAlphaProbe {
|
||||
try {
|
||||
const ffprobePath = findFFprobe();
|
||||
if (!ffprobePath) return { probed: false, alphaMode: false };
|
||||
const raw = execFileSync(
|
||||
ffprobePath,
|
||||
[
|
||||
"-v",
|
||||
"error",
|
||||
"-select_streams",
|
||||
"v:0",
|
||||
"-show_entries",
|
||||
"stream=codec_name:stream_tags=alpha_mode",
|
||||
"-of",
|
||||
"json",
|
||||
filePath,
|
||||
],
|
||||
{ encoding: "utf-8", timeout: 15_000 },
|
||||
);
|
||||
const parsed = JSON.parse(raw) as {
|
||||
streams?: Array<{ codec_name?: string; tags?: Record<string, string> }>;
|
||||
};
|
||||
const stream = parsed.streams?.[0];
|
||||
if (!stream || typeof stream.codec_name !== "string") {
|
||||
return { probed: false, alphaMode: false };
|
||||
}
|
||||
const tags = stream.tags ?? {};
|
||||
const alphaMode = Object.entries(tags).some(
|
||||
([k, v]) => k.toLowerCase() === "alpha_mode" && String(v) === "1",
|
||||
);
|
||||
return { probed: true, alphaMode };
|
||||
} catch {
|
||||
return { probed: false, alphaMode: false };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* After a completed WebM render, verify the output actually carries the alpha
|
||||
* sidecar. Some ffmpeg/libvpx-vp9 builds silently produce opaque output — the
|
||||
* render succeeds and looks fine in a player, but transparency is gone, which
|
||||
* the user only discovers after compositing. Surface it loudly here with the
|
||||
* concrete `--format mov` remedy. Best-effort and non-blocking; a build that
|
||||
* DOES preserve alpha (ALPHA_MODE=1) stays silent.
|
||||
*/
|
||||
export function warnIfWebmAlphaDropped(outputPath: string, format: string, quiet: boolean): void {
|
||||
if (quiet || format !== "webm") return;
|
||||
const advisory = webmAlphaAdvisory(format, probeWebmAlpha(outputPath));
|
||||
if (!advisory) return;
|
||||
console.warn(`\n${c.warn("⚠")} ${c.bold("Transparency not preserved")}`);
|
||||
console.warn(` ${c.dim(advisory)}\n`);
|
||||
}
|
||||
Reference in New Issue
Block a user