fix(media): gate proxying on source eligibility

This commit is contained in:
Miguel Ángel
2026-07-16 16:10:04 +00:00
parent 53fecc1ac4
commit e3c0c1fa59
7 changed files with 242 additions and 25 deletions
+39 -1
View File
@@ -89,7 +89,11 @@ describe("maybeProxyProactively", () => {
it("swaps when hostile and representativeMime is null (no canPlayType check possible)", () => {
window.__HF_MEDIA_CODEC_MAP__ = {
"/video.mp4": { codecName: "prores", browserHostile: true, representativeMime: null },
"/video.mp4": {
codecName: "prores",
browserHostile: true,
representativeMime: null,
},
};
const el = createVideo("/video.mp4");
stubCanPlayType(el, "probably"); // should not even be consulted
@@ -235,6 +239,23 @@ describe("handleMetadataForProxy (reactive trigger)", () => {
);
});
it("does not proxy a mapped browser-safe codec on the metadata path", () => {
window.__HF_MEDIA_CODEC_MAP__ = { "/video.mp4": H264_ENTRY };
const el = createVideo("/video.mp4");
markZeroWidth(el);
handleMetadataForProxy(el);
expect(isProxied(el)).toBe(false);
expect(el.load).not.toHaveBeenCalled();
expect(postRuntimeMessageMock).toHaveBeenCalledWith(
expect.objectContaining({
code: "runtime_media_proxy_unavailable",
details: expect.objectContaining({ reason: "browser_safe_codec" }),
}),
);
});
it("a second zero-width metadata event does not loop (no second swap)", () => {
window.__HF_MEDIA_CODEC_MAP__ = {};
const el = createVideo("/video.mp4");
@@ -385,6 +406,23 @@ describe("handleErrorForProxy (tertiary trigger)", () => {
});
describe("swapToProxy", () => {
it("does not poison swap state when the source URL is malformed", () => {
const el = createVideo("/video.mp4");
Object.defineProperty(el, "currentSrc", {
value: "http://[",
configurable: true,
});
swapToProxy(el, HEVC_ENTRY, "reactive");
expect(el.load).not.toHaveBeenCalled();
expect(postRuntimeMessageMock).toHaveBeenCalledWith(
expect.objectContaining({
code: "runtime_media_proxy_unavailable",
details: expect.objectContaining({ reason: "invalid_source_url" }),
}),
);
});
it("evicts per-source sync state so the swapped element is treated as a first tick", () => {
const el = createVideo("/video.mp4");
// Populate sync state as a real active-clip tick would.
+10 -3
View File
@@ -123,7 +123,7 @@ function lookupCodecMapEntry(
if (!pathname.endsWith(key)) continue;
if (bestKey === null || key.length > bestKey.length) bestKey = key;
}
return bestKey ? map[bestKey] : null;
return bestKey ? (map[bestKey] ?? null) : null;
}
function appendProxyParam(src: string): string {
@@ -136,7 +136,8 @@ type UnavailableReason =
| "cross_origin"
| "proxy_playback_failed"
| "browser_safe_codec"
| "alpha_source";
| "alpha_source"
| "invalid_source_url";
const UNAVAILABLE_NOTES: Record<UnavailableReason, string> = {
cross_origin:
@@ -146,6 +147,7 @@ const UNAVAILABLE_NOTES: Record<UnavailableReason, string> = {
"the file errored but its codec is browser-decodable; an H.264 proxy cannot help (the file itself is likely corrupt)",
alpha_source:
"the source carries an alpha channel; an H.264 proxy would destroy the transparency, so it is never proxied",
invalid_source_url: "the media source URL is malformed and cannot be proxied",
};
function emitUnavailableDiagnostic(
@@ -184,15 +186,16 @@ export function swapToProxy(
trigger: ProxyTrigger = "reactive",
): void {
if (swappedElements.has(el)) return;
swappedElements.add(el);
const originalSrc = currentSrcValue(el);
let proxiedSrc: string;
try {
proxiedSrc = appendProxyParam(originalSrc);
} catch (err) {
swallow("runtime.mediaProxy.swap", err);
emitUnavailableDiagnostic(el, "invalid_source_url", originalSrc);
return;
}
swappedElements.add(el);
// The swapped src points at a different file — sync state (drift offsets,
// seek-retry latches, volume tracking) computed against the original
// source must not carry over, or the next tick misreads a fresh file's
@@ -281,6 +284,10 @@ export function handleMetadataForProxy(el: HTMLMediaElement): void {
return;
}
const entry = lookupCodecMapEntry(key, map);
if (entry && !entry.browserHostile) {
emitUnavailableDiagnostic(el, "browser_safe_codec", src);
return;
}
if (entry?.hasAlpha) {
emitUnavailableDiagnostic(el, "alpha_source", src);
return;
@@ -5,6 +5,7 @@ import { tmpdir } from "node:os";
import type { FfprobeRunner } from "./mediaMetadata.js";
import {
BROWSER_HOSTILE_CODECS,
decideMediaProxyEligibility,
createMediaCodecProbeCache,
probeAssetCodec,
scanProjectMediaCodecMap,
@@ -24,13 +25,22 @@ function makeRunner(
const entry = codecByPath[filePath];
const normalized = typeof entry === "string" ? { codecName: entry } : entry;
const streams = normalized
? [{ codec_type: "video", codec_name: normalized.codecName, pix_fmt: normalized.pixFmt }]
? [
{
codec_type: "video",
codec_name: normalized.codecName,
pix_fmt: normalized.pixFmt,
},
]
: [];
return { status: 0, stdout: JSON.stringify({ streams }), stderr: "" };
};
}
function countingRunner(runner: FfprobeRunner): { runner: FfprobeRunner; calls: () => number } {
function countingRunner(runner: FfprobeRunner): {
runner: FfprobeRunner;
calls: () => number;
} {
let calls = 0;
return {
runner: (command, args, options) => {
@@ -122,7 +132,9 @@ describe("probeAssetCodec", () => {
const facts = await probeAssetCodec(
videoPath,
makeRunner({ [videoPath]: { codecName: "prores", pixFmt: "yuva444p10le" } }),
makeRunner({
[videoPath]: { codecName: "prores", pixFmt: "yuva444p10le" },
}),
);
expect(facts).toEqual({
@@ -145,6 +157,42 @@ describe("probeAssetCodec", () => {
});
});
describe("decideMediaProxyEligibility", () => {
it("allows only hostile opaque video through the H.264 proxy path", () => {
expect(
decideMediaProxyEligibility({
codecName: "hevc",
browserHostile: true,
representativeMime: null,
hasAlpha: false,
}),
).toEqual({ eligible: true });
});
it("rejects alpha and browser-safe sources before transcoding", () => {
expect(
decideMediaProxyEligibility({
codecName: "prores",
browserHostile: true,
representativeMime: null,
hasAlpha: true,
}),
).toEqual({ eligible: false, reason: "alpha_source" });
expect(
decideMediaProxyEligibility({
codecName: "h264",
browserHostile: false,
representativeMime: null,
hasAlpha: false,
}),
).toEqual({ eligible: false, reason: "browser_safe_codec" });
expect(decideMediaProxyEligibility(null)).toEqual({
eligible: false,
reason: "unknown_codec",
});
});
});
describe("scanProjectMediaCodecMap", () => {
it("omits an asset from the map (no throw) when ffprobe is unresolvable", async () => {
const project = tmpProject();
@@ -172,9 +220,9 @@ describe("scanProjectMediaCodecMap", () => {
}),
});
expect(Object.keys(map).sort()).toEqual(["/assets/my clip.mp4", "/assets/sub/clip.mp4"]);
expect(Object.keys(map)).toEqual(["/assets/sub/clip.mp4"]);
expect(map["/assets/sub/clip.mp4"]?.browserHostile).toBe(true);
expect(map["/assets/my clip.mp4"]?.browserHostile).toBe(false);
expect(map["/assets/my clip.mp4"]).toBeUndefined();
});
it("rewrites a sub-composition's ../-traversing src via compSrcPath before resolving (rewriteAssetPath)", async () => {
@@ -185,7 +233,12 @@ describe("scanProjectMediaCodecMap", () => {
const map = await scanProjectMediaCodecMap(
project,
[{ html: videoHtml("../assets/clip.mp4"), compSrcPath: "compositions/scene.html" }],
[
{
html: videoHtml("../assets/clip.mp4"),
compSrcPath: "compositions/scene.html",
},
],
{ runner: makeRunner({ [join(project, "assets", "clip.mp4")]: "hevc" }) },
);
@@ -44,6 +44,20 @@ export const BROWSER_HOSTILE_CODECS: Record<string, string | null> = {
av1: 'video/mp4; codecs="av01.0.08M.08"',
};
export type MediaProxyIneligibilityReason = "alpha_source" | "browser_safe_codec" | "unknown_codec";
export type MediaProxyEligibility =
| { eligible: true }
| { eligible: false; reason: MediaProxyIneligibilityReason };
/** Single policy gate shared by proactive scans and on-demand proxy routes. */
export function decideMediaProxyEligibility(facts: AssetCodecFacts | null): MediaProxyEligibility {
if (!facts) return { eligible: false, reason: "unknown_codec" };
if (facts.hasAlpha) return { eligible: false, reason: "alpha_source" };
if (!facts.browserHostile) return { eligible: false, reason: "browser_safe_codec" };
return { eligible: true };
}
function codecFactsFor(codecName: string, hasAlpha: boolean): AssetCodecFacts {
const isHostile = Object.hasOwn(BROWSER_HOSTILE_CODECS, codecName);
return {
@@ -219,7 +233,7 @@ export async function scanProjectMediaCodecMap(
const map: MediaCodecMap = {};
entries.forEach(([, pathname], index) => {
const entryFacts = facts[index];
if (entryFacts) map[pathname] = entryFacts;
if (entryFacts?.browserHostile) map[pathname] = entryFacts;
});
return map;
}
@@ -78,8 +78,8 @@ function injectScriptTagIntoHead(html: string, scriptTag: string): string {
* The single shared implementation for every auto-proxy surface — the studio
* preview route (via `injectMediaCodecMap` below) and the CLI's composition /
* static project servers (via the `./media-proxy-preview` subpath export).
* The map is injected even when empty: the runtime treats map PRESENCE as
* "this surface serves `?hf-proxy=`", gating its reactive/tertiary swaps.
* Empty maps leave HTML untouched, preserving the normal no-hostile-media
* preview path. On-demand proxy requests enforce the same eligibility gate.
*/
export async function injectMediaCodecMapIntoHtml(
html: string,
@@ -98,6 +98,7 @@ export async function injectMediaCodecMapIntoHtml(
// Best-effort: a scan failure must never block serving the page.
return html;
}
if (Object.keys(map).length === 0) return html;
for (const [rootRelativePathname, facts] of Object.entries(map)) {
if (!facts.browserHostile || facts.hasAlpha) continue;
resolveProxy(projectDir, resolve(projectDir, rootRelativePathname.replace(/^\/+/, ""))).catch(
@@ -672,13 +672,23 @@ describe("hf-proxy negotiation and media codec map injection (U3)", () => {
) => Promise<
Record<
string,
{ codecName: string; browserHostile: boolean; representativeMime: string | null }
{
codecName: string;
browserHostile: boolean;
representativeMime: string | null;
}
>
>;
async function loadPreviewModule(opts: {
resolveProxyImpl?: (projectDir: string, absoluteSourcePath: string) => Promise<string>;
scanMapImpl?: ScanMapImpl;
probeAssetCodecImpl?: () => Promise<{
codecName: string;
browserHostile: boolean;
representativeMime: string | null;
hasAlpha: boolean;
} | null>;
}): Promise<typeof import("./preview.js")> {
vi.resetModules();
const resolveProxy =
@@ -699,6 +709,27 @@ describe("hf-proxy negotiation and media codec map injection (U3)", () => {
vi.doMock("../helpers/mediaCodecMap.js", () => ({
scanProjectMediaCodecMap: opts.scanMapImpl ?? (async () => ({})),
createMediaCodecProbeCache: () => new Map(),
probeAssetCodec:
opts.probeAssetCodecImpl ??
(async () => ({
codecName: "hevc",
browserHostile: true,
representativeMime: null,
hasAlpha: false,
})),
decideMediaProxyEligibility: (
facts: {
browserHostile: boolean;
hasAlpha: boolean;
} | null,
) => {
if (!facts) return { eligible: false, reason: "unknown_codec" };
if (facts.hasAlpha) return { eligible: false, reason: "alpha_source" };
if (!facts.browserHostile) {
return { eligible: false, reason: "browser_safe_codec" };
}
return { eligible: true };
},
}));
return import("./preview.js");
}
@@ -807,6 +838,39 @@ describe("hf-proxy negotiation and media codec map injection (U3)", () => {
expect(resolveProxyMock).not.toHaveBeenCalled();
});
it("rejects alpha-bearing and browser-safe sources before transcoding", async () => {
for (const facts of [
{
codecName: "prores",
browserHostile: true,
representativeMime: null,
hasAlpha: true,
},
{
codecName: "h264",
browserHostile: false,
representativeMime: null,
hasAlpha: false,
},
]) {
const projectDir = createProjectDir();
writeFileSync(join(projectDir, "clip.mp4"), "video-bytes");
const resolveProxyMock = vi.fn(async () => "should-not-be-called");
const { registerPreviewRoutes: register } = await loadPreviewModule({
resolveProxyImpl: resolveProxyMock,
probeAssetCodecImpl: async () => facts,
});
const app = new Hono();
register(app, createAdapter(projectDir));
const res = await app.request(
"http://localhost/projects/demo/preview/clip.mp4?hf-proxy=h264",
);
expect(res.status).toBe(422);
expect(resolveProxyMock).not.toHaveBeenCalled();
}
});
it("returns 404 without transcoding when the param value is not exactly h264", async () => {
const projectDir = createProjectDir();
writeFileSync(join(projectDir, "clip.mp4"), "original-hevc-bytes");
@@ -884,6 +948,18 @@ describe("hf-proxy negotiation and media codec map injection (U3)", () => {
});
describe("__HF_MEDIA_CODEC_MAP__ injection into composition HTML", () => {
it("keeps HTML byte-identical when the scan finds no proxy-eligible media", async () => {
const projectDir = createProjectDir();
const { registerPreviewRoutes: register } = await loadPreviewModule({
scanMapImpl: async () => ({}),
});
const app = new Hono();
register(app, createAdapter(projectDir));
const res = await app.request("http://localhost/projects/demo/preview");
const html = await res.text();
expect(html).not.toContain("data-hf-media-codec-map");
});
it("injects the scanned map naming the hostile fixture, and pre-warms resolveProxy for it", async () => {
const projectDir = createProjectDir();
const resolveProxyMock = vi.fn(async () => join(projectDir, ".transcode-cache", "x.mp4"));
@@ -924,7 +1000,11 @@ describe("hf-proxy negotiation and media codec map injection (U3)", () => {
writeFileSync(join(projectDir, "clip.mp4"), "bytes");
const resolveProxyMock = vi.fn(async () => "should-not-be-called");
const scanMapMock = vi.fn(async () => ({
"/clip.mp4": { codecName: "hevc", browserHostile: true, representativeMime: null },
"/clip.mp4": {
codecName: "hevc",
browserHostile: true,
representativeMime: null,
},
}));
const { registerPreviewRoutes: register } = await loadPreviewModule({
resolveProxyImpl: resolveProxyMock,
+34 -10
View File
@@ -16,6 +16,7 @@ import { ensureHfIds } from "@hyperframes/parsers/hf-ids";
import { persistHfIdsIfNeeded, stampFileHfIds } from "../helpers/hfIdPersist.js";
import { isVariablesPayload, VARIABLES_PAYLOAD_ERROR } from "../helpers/variablesPayload.js";
import { resolveProxy, ProxyTranscodeError } from "../helpers/proxyTranscoder.js";
import { decideMediaProxyEligibility, probeAssetCodec } from "../helpers/mediaCodecMap.js";
import {
isAutoProxyEnabled,
injectMediaCodecMap,
@@ -61,7 +62,9 @@ function parseStudioMotionManifestContent(content: string): {
hasCustomEase: boolean;
} {
try {
const parsed = JSON.parse(content) as { motions?: Array<{ customEase?: unknown }> };
const parsed = JSON.parse(content) as {
motions?: Array<{ customEase?: unknown }>;
};
const motions = Array.isArray(parsed.motions) ? parsed.motions : [];
return {
hasMotion: motions.length > 0,
@@ -247,11 +250,13 @@ function variablesEtagSalt(raw: string | undefined): string {
* route should 400; otherwise `values` is the override object (or null when
* the param is absent) and `raw` feeds the ETag salt.
*/
function previewVariablesFromRequest(
rawVariables: string | undefined,
):
function previewVariablesFromRequest(rawVariables: string | undefined):
| { error: string }
| { error?: undefined; raw: string | undefined; values: Record<string, unknown> | null } {
| {
error?: undefined;
raw: string | undefined;
values: Record<string, unknown> | null;
} {
const parse = parsePreviewVariablesParam(rawVariables);
if (!parse.ok) return { error: parse.error };
return { raw: rawVariables, values: parse.values };
@@ -299,11 +304,17 @@ function resolveProjectMainHtml(
): { html: string; compositionPath: string } | null {
const indexPath = join(projectDir, "index.html");
if (existsSync(indexPath)) {
return { html: readFileSync(indexPath, "utf-8"), compositionPath: "index.html" };
return {
html: readFileSync(indexPath, "utf-8"),
compositionPath: "index.html",
};
}
const blockHtmlPath = join(projectDir, `${projectId}.html`);
if (existsSync(blockHtmlPath)) {
return { html: readFileSync(blockHtmlPath, "utf-8"), compositionPath: `${projectId}.html` };
return {
html: readFileSync(blockHtmlPath, "utf-8"),
compositionPath: `${projectId}.html`,
};
}
return null;
}
@@ -334,7 +345,10 @@ export function registerPreviewRoutes(api: Hono, adapter: PreviewApiAdapter): vo
const etag = `"preview:${signature}${variablesEtagSalt(vars.raw)}"`;
const ifNoneMatch = c.req.header("If-None-Match");
if (ifNoneMatch === etag) {
return new Response(null, { status: 304, headers: previewCacheHeaders(etag) });
return new Response(null, {
status: 304,
headers: previewCacheHeaders(etag),
});
}
// Normalize + persist data-hf-id to disk before bundle reads it. Idempotent.
@@ -472,7 +486,10 @@ export function registerPreviewRoutes(api: Hono, adapter: PreviewApiAdapter): vo
const etag = `"comp:v2:${compPath}:${signature}${variablesEtagSalt(vars.raw)}"`;
const ifNoneMatch = c.req.header("If-None-Match");
if (ifNoneMatch === etag) {
return new Response(null, { status: 304, headers: previewCacheHeaders(etag) });
return new Response(null, {
status: 304,
headers: previewCacheHeaders(etag),
});
}
const stamped = pinSubCompHfIds(compFile, compPath);
@@ -527,12 +544,19 @@ export function registerPreviewRoutes(api: Hono, adapter: PreviewApiAdapter): vo
) {
return c.text("not found", 404);
}
const eligibility = decideMediaProxyEligibility(await probeAssetCodec(file));
if (!eligibility.eligible) {
return c.text(`media proxy unavailable: ${eligibility.reason}`, 422);
}
}
const etag = `"${stat.mtimeMs.toString(36)}-${stat.size.toString(36)}${proxyEtagSalt(proxyParam)}"`;
const cacheHeaders: Record<string, string> = isText
? { "Cache-Control": "no-store" }
: { "Cache-Control": "private, max-age=3600, must-revalidate", ETag: etag };
: {
"Cache-Control": "private, max-age=3600, must-revalidate",
ETag: etag,
};
if (!isText) {
const ifNoneMatch = c.req.header("If-None-Match");