mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-12 15:20:13 +00:00
fix(studio): preserve alpha proxy playback (#2625)
* fix(studio): preserve alpha proxy playback * test(cli): pin VP8 alpha proxy pre-resolution * fix(studio): emit textarea field-sizing CSS * test(studio): verify textarea CSS generation
This commit is contained in:
@@ -215,7 +215,7 @@ describe("decideMediaProxyEligibility", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("does not recycle an alpha VP9 source into the same proxy codec", () => {
|
||||
it("proxies an alpha VP9 source to the Chromium-compatible VP8 variant", () => {
|
||||
expect(
|
||||
decideMediaProxyEligibility({
|
||||
codecName: "vp9",
|
||||
@@ -223,19 +223,19 @@ describe("decideMediaProxyEligibility", () => {
|
||||
representativeMime: 'video/webm; codecs="vp09.00.10.08"',
|
||||
hasAlpha: true,
|
||||
}),
|
||||
).toEqual({ eligible: false, reason: "proxy_target_codec" });
|
||||
).toEqual({ eligible: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe("proxyVariantFor", () => {
|
||||
it("uses VP9 for alpha and H.264 otherwise", () => {
|
||||
it("uses VP8 for alpha and H.264 otherwise", () => {
|
||||
const facts = {
|
||||
codecName: "prores",
|
||||
browserHostile: true,
|
||||
representativeMime: null,
|
||||
hasAlpha: true,
|
||||
};
|
||||
expect(proxyVariantFor(facts)).toBe("vp9");
|
||||
expect(proxyVariantFor(facts)).toBe("vp8");
|
||||
expect(proxyVariantFor({ ...facts, hasAlpha: false })).toBe("h264");
|
||||
});
|
||||
});
|
||||
@@ -248,7 +248,7 @@ describe("resolveProxyVariantRequest", () => {
|
||||
representativeMime: null,
|
||||
hasAlpha: true,
|
||||
};
|
||||
expect(resolveProxyVariantRequest("auto", facts)).toBe("vp9");
|
||||
expect(resolveProxyVariantRequest("auto", facts)).toBe("vp8");
|
||||
expect(resolveProxyVariantRequest("h264", facts)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,7 +23,7 @@ export interface AssetCodecFacts {
|
||||
* the runtime always proxies rather than probing `canPlayType`). */
|
||||
representativeMime: string | null;
|
||||
/** Source carries an alpha channel (ffprobe pix_fmt). Alpha sources use a
|
||||
* VP9/WebM proxy so their transparency is preserved. */
|
||||
* VP8/WebM proxy so their transparency is preserved across Chromium builds. */
|
||||
hasAlpha: boolean;
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ export const BROWSER_HOSTILE_CODECS: Record<string, string | null> = {
|
||||
vp9: 'video/webm; codecs="vp09.00.10.08"',
|
||||
};
|
||||
|
||||
export type ProxyVariant = "h264" | "vp9";
|
||||
export type ProxyVariant = "h264" | "vp8";
|
||||
export type ProxyVariantRequest = ProxyVariant | "auto";
|
||||
|
||||
export const PROXY_VARIANT_CONFIG: Record<
|
||||
@@ -55,7 +55,7 @@ export const PROXY_VARIANT_CONFIG: Record<
|
||||
{ extension: ".mp4" | ".webm"; contentType: "video/mp4" | "video/webm" }
|
||||
> = {
|
||||
h264: { extension: ".mp4", contentType: "video/mp4" },
|
||||
vp9: { extension: ".webm", contentType: "video/webm" },
|
||||
vp8: { extension: ".webm", contentType: "video/webm" },
|
||||
};
|
||||
|
||||
export function isProxyVariant(value: string): value is ProxyVariant {
|
||||
@@ -67,7 +67,7 @@ export function isProxyVariantRequest(value: string): value is ProxyVariantReque
|
||||
}
|
||||
|
||||
export function proxyVariantFor(facts: AssetCodecFacts): ProxyVariant {
|
||||
return facts.hasAlpha ? "vp9" : "h264";
|
||||
return facts.hasAlpha ? "vp8" : "h264";
|
||||
}
|
||||
|
||||
export function resolveProxyVariantRequest(
|
||||
@@ -78,10 +78,7 @@ export function resolveProxyVariantRequest(
|
||||
return request === "auto" || request === expected ? expected : null;
|
||||
}
|
||||
|
||||
export type MediaProxyIneligibilityReason =
|
||||
| "browser_safe_codec"
|
||||
| "proxy_target_codec"
|
||||
| "unknown_codec";
|
||||
export type MediaProxyIneligibilityReason = "browser_safe_codec" | "unknown_codec";
|
||||
|
||||
export type MediaProxyEligibility =
|
||||
| { eligible: true }
|
||||
@@ -91,9 +88,6 @@ export type MediaProxyEligibility =
|
||||
export function decideMediaProxyEligibility(facts: AssetCodecFacts | null): MediaProxyEligibility {
|
||||
if (!facts) return { eligible: false, reason: "unknown_codec" };
|
||||
if (!facts.browserHostile) return { eligible: false, reason: "browser_safe_codec" };
|
||||
if (facts.hasAlpha && facts.codecName === "vp9") {
|
||||
return { eligible: false, reason: "proxy_target_codec" };
|
||||
}
|
||||
return { eligible: true };
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ function injectScriptTagIntoHead(html: string, scriptTag: string): string {
|
||||
* responses). No second concurrency limiter here — the transcoder's own
|
||||
* global bound throttles both pre-warm and element-triggered calls.
|
||||
* Pre-warm failures are swallowed; an actual `?hf-proxy=` request surfaces
|
||||
* them as a 502. Alpha-bearing entries pre-warm their VP9/WebM variant.
|
||||
* them as a 502. Alpha-bearing entries pre-warm their VP8/WebM variant.
|
||||
*
|
||||
* The single shared implementation for every auto-proxy surface — the studio
|
||||
* preview route (via `injectMediaCodecMap` below) and the CLI's composition /
|
||||
|
||||
@@ -157,7 +157,7 @@ describe("resolveProxy", () => {
|
||||
expect(cacheDirEntries).toEqual([expectedCachePath.split("/").at(-1)]);
|
||||
});
|
||||
|
||||
it("uses VP9 alpha-safe args and a distinct WebM cache path", async () => {
|
||||
it("uses Chromium-compatible VP8 alpha args and a distinct WebM cache path", async () => {
|
||||
const { spawn, calls } = createSpawnSpy();
|
||||
const { resolveProxy, getProxyCachePath } = await loadModule(spawn, FFMPEG_PATH);
|
||||
const projectDir = tmpProject();
|
||||
@@ -165,15 +165,16 @@ describe("resolveProxy", () => {
|
||||
writeFileSync(sourcePath, "source-bytes");
|
||||
|
||||
const h264Path = getProxyCachePath(projectDir, sourcePath, "h264");
|
||||
const vp9Path = getProxyCachePath(projectDir, sourcePath, "vp9");
|
||||
const resultPromise = resolveProxy(projectDir, sourcePath, "vp9");
|
||||
const vp8Path = getProxyCachePath(projectDir, sourcePath, "vp8");
|
||||
const resultPromise = resolveProxy(projectDir, sourcePath, "vp8");
|
||||
await flush();
|
||||
|
||||
expect(vp9Path).not.toBe(h264Path);
|
||||
expect(vp9Path).toMatch(/\.webm$/);
|
||||
expect(vp8Path).not.toBe(h264Path);
|
||||
expect(vp8Path).toMatch(/\.webm$/);
|
||||
expect(h264Path).toMatch(/\.mp4$/);
|
||||
const args = calls[0]!.args;
|
||||
expect(args).toContain("libvpx-vp9");
|
||||
expect(args).toContain("libvpx");
|
||||
expect(args).not.toContain("libvpx-vp9");
|
||||
expect(args).toContain("yuva420p");
|
||||
expect(args).toContain("libopus");
|
||||
expect(args[args.indexOf("-b:v") + 1]).toBe("0");
|
||||
@@ -182,14 +183,14 @@ describe("resolveProxy", () => {
|
||||
expect(args[args.indexOf("-auto-alt-ref") + 1]).toBe("0");
|
||||
expect(args[args.indexOf("-metadata:s:v:0") + 1]).toBe("alpha_mode=1");
|
||||
expect(args[args.indexOf("-ac") + 1]).toBe("2");
|
||||
expect(args).toContain("-row-mt");
|
||||
expect(args).not.toContain("-row-mt");
|
||||
expect(args).toContain("-cpu-used");
|
||||
expect(args).not.toContain("-movflags");
|
||||
expect(args).not.toContain("+faststart");
|
||||
expect(args[args.indexOf("-vf") + 1]).toContain("format=yuva420p");
|
||||
|
||||
succeed(calls[0]!, "fake-vp9-bytes");
|
||||
await expect(resultPromise).resolves.toBe(vp9Path);
|
||||
succeed(calls[0]!, "fake-vp8-bytes");
|
||||
await expect(resultPromise).resolves.toBe(vp8Path);
|
||||
});
|
||||
|
||||
it("returns without spawning on a cache hit", async () => {
|
||||
@@ -236,21 +237,21 @@ describe("resolveProxy", () => {
|
||||
await result;
|
||||
});
|
||||
|
||||
it("preserves alpha on HDR-tagged VP9 proxies by bypassing the opaque tonemap chain", async () => {
|
||||
it("preserves alpha on HDR-tagged VP8 proxies by bypassing the opaque tonemap chain", async () => {
|
||||
const { spawn, calls } = createSpawnSpy();
|
||||
const { resolveProxy } = await loadModule(spawn, FFMPEG_PATH, true);
|
||||
const projectDir = tmpProject();
|
||||
const sourcePath = join(projectDir, "hdr-alpha.mov");
|
||||
writeFileSync(sourcePath, "source-bytes");
|
||||
|
||||
const result = resolveProxy(projectDir, sourcePath, "vp9");
|
||||
const result = resolveProxy(projectDir, sourcePath, "vp8");
|
||||
await flush();
|
||||
|
||||
expect(calls).toHaveLength(1);
|
||||
const filter = calls[0]!.args[calls[0]!.args.indexOf("-vf") + 1];
|
||||
expect(filter).toContain("format=yuva420p");
|
||||
expect(filter).not.toContain("tonemap=");
|
||||
succeed(calls[0]!, "fake-vp9-alpha-bytes");
|
||||
succeed(calls[0]!, "fake-vp8-alpha-bytes");
|
||||
await result;
|
||||
});
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ import { PROXY_VARIANT_CONFIG, type ProxyVariant } from "./mediaCodecMap.js";
|
||||
* entry still lands for the next request.
|
||||
*/
|
||||
|
||||
export const PROXY_PARAMS_VERSION = "v3";
|
||||
export const PROXY_PARAMS_VERSION = "v4";
|
||||
|
||||
const CACHE_DIR_NAME = ".transcode-cache";
|
||||
|
||||
@@ -314,13 +314,13 @@ async function runFfmpeg(
|
||||
if (!ffmpegPath) {
|
||||
throw new FfmpegUnavailableError();
|
||||
}
|
||||
// The HDR tonemap filters discard alpha. VP9 is the alpha-preserving proxy
|
||||
// The HDR tonemap filters discard alpha. VP8 is the alpha-preserving proxy
|
||||
// variant, so retain its source color values instead of making it opaque.
|
||||
if (metadata.color.isHdr && variant !== "vp9") await ensureHdrFilters(ffmpegPath);
|
||||
if (metadata.color.isHdr && variant !== "vp8") await ensureHdrFilters(ffmpegPath);
|
||||
const evenScale = "scale=trunc(iw/2)*2:trunc(ih/2)*2";
|
||||
const pixelFormat = variant === "vp9" ? "yuva420p" : "yuv420p";
|
||||
const pixelFormat = variant === "vp8" ? "yuva420p" : "yuv420p";
|
||||
const videoFilter =
|
||||
metadata.color.isHdr && variant !== "vp9"
|
||||
metadata.color.isHdr && variant !== "vp8"
|
||||
? [
|
||||
"zscale=t=linear:npl=100",
|
||||
"tonemap=hable:desat=0",
|
||||
@@ -354,9 +354,9 @@ async function runFfmpeg(
|
||||
"-movflags",
|
||||
"+faststart",
|
||||
];
|
||||
const vp9Args = [
|
||||
const vp8Args = [
|
||||
"-c:v",
|
||||
"libvpx-vp9",
|
||||
"libvpx",
|
||||
"-b:v",
|
||||
"0",
|
||||
"-crf",
|
||||
@@ -371,8 +371,6 @@ async function runFfmpeg(
|
||||
"bt709",
|
||||
"-color_trc",
|
||||
"bt709",
|
||||
"-row-mt",
|
||||
"1",
|
||||
"-cpu-used",
|
||||
"4",
|
||||
"-auto-alt-ref",
|
||||
@@ -384,7 +382,7 @@ async function runFfmpeg(
|
||||
"-c:a",
|
||||
"libopus",
|
||||
];
|
||||
const args = [...commonArgs, ...(variant === "vp9" ? vp9Args : h264Args), outputPath];
|
||||
const args = [...commonArgs, ...(variant === "vp8" ? vp8Args : h264Args), outputPath];
|
||||
|
||||
// Hard ceiling so a hung ffmpeg can never permanently occupy one of the
|
||||
// global transcode slots: the child is killed and the slot released via
|
||||
|
||||
Reference in New Issue
Block a user