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:
Miguel Ángel
2026-07-08 15:53:08 -04:00
committed by GitHub
parent 6f8bf5f364
commit f5f94a9495
3 changed files with 133 additions and 0 deletions
+3
View File
@@ -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();
});
});
+101
View File
@@ -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`);
}