mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 00:56:23 +00:00
feat(media): alpha-capable authoring proxies (#2598)
* feat(media): alpha-capable authoring proxies Alpha sources were refused a proxy before the codec map ever asked whether the browser could decode them, so a ProRes 4444 alpha file (which no browser previews at all) rendered black forever, while an alpha WebM (which previews fine) was already covered by the browser-safe check on the next line. The alpha veto earned nothing and cost the one case that needed help. Alpha is now a target-codec choice rather than a veto: alpha sources transcode to VP9 + yuva420p in WebM, everything else keeps the existing H.264/MP4 path byte for byte. Only files no browser can preview are proxied, which is the rule the runtime already followed everywhere else. WebM cannot carry AAC, so the VP9 path uses Opus and drops the MP4-only faststart flag. PROXY_PARAMS_VERSION moves to v3 so clients stop serving the previously cached proxies. Safari does not decode VP9 alpha and still shows black for alpha sources, as it does today: this is better on Chromium and Firefox and no worse anywhere. * fix(media): infer proxy variant for rescue * fix(media): preserve alpha proxy hardening after restack
This commit is contained in:
@@ -69,6 +69,7 @@ vi.mock("./staticProjectServer.js", () => ({
|
||||
vi.mock("@hyperframes/studio-server/media-codec-map", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("@hyperframes/studio-server/media-codec-map")>()),
|
||||
scanProjectMediaCodecMap: mocks.scanProjectMediaCodecMap,
|
||||
proxyVariantFor: (facts: { hasAlpha?: boolean }) => (facts.hasAlpha ? "vp9" : "h264"),
|
||||
}));
|
||||
vi.mock("@hyperframes/studio-server/proxy-transcoder", () => ({
|
||||
resolveProxy: mocks.resolveProxy,
|
||||
@@ -341,7 +342,7 @@ it("surfaces the runtime's media-proxy-fallback console.info line as an info fin
|
||||
const fallbackMessage = fakeConsoleMessage(
|
||||
"info",
|
||||
'[hyperframes] runtime_media_proxy_fallback: "assets/clip.mp4" uses a codec (hevc) this browser can\'t decode; ' +
|
||||
"auto-swapped to an H.264 proxy for this preview only. Render output is unaffected.",
|
||||
"auto-swapped to an authoring proxy for this preview only. Render output is unaffected.",
|
||||
);
|
||||
const unrelatedInfo = fakeConsoleMessage("info", "[hyperframes] render runtime fps 30");
|
||||
const authorInfo = fakeConsoleMessage("info", "debug runtime_media_proxy_probe");
|
||||
@@ -455,7 +456,11 @@ describe("preResolveHostileMediaProxies", () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
expect(mocks.resolveProxy).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.resolveProxy).toHaveBeenCalledWith(projectDir, join(projectDir, "clip.mp4"));
|
||||
expect(mocks.resolveProxy).toHaveBeenCalledWith(
|
||||
projectDir,
|
||||
join(projectDir, "clip.mp4"),
|
||||
"h264",
|
||||
);
|
||||
expect(settled).toBe(false); // still waiting on the hostile entry's transcode
|
||||
|
||||
resolveTranscode?.();
|
||||
@@ -500,10 +505,10 @@ describe("preResolveHostileMediaProxies", () => {
|
||||
it("does not pre-resolve a hostile asset rejected by the shared proxy policy", async () => {
|
||||
const projectDir = mkProjectDir();
|
||||
mocks.scanProjectMediaCodecMap.mockResolvedValue({
|
||||
"/alpha.mov": {
|
||||
codecName: "prores",
|
||||
"/alpha.webm": {
|
||||
codecName: "vp9",
|
||||
browserHostile: true,
|
||||
representativeMime: null,
|
||||
representativeMime: 'video/webm; codecs="vp09.00.10.08"',
|
||||
hasAlpha: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -21,6 +21,7 @@ import { serveStaticProjectHtml } from "./staticProjectServer.js";
|
||||
import { resolveAutoProxy } from "./projectConfig.js";
|
||||
import {
|
||||
decideMediaProxyEligibility,
|
||||
proxyVariantFor,
|
||||
scanProjectMediaCodecMap,
|
||||
} from "@hyperframes/studio-server/media-codec-map";
|
||||
import { resolveProxy } from "@hyperframes/studio-server/proxy-transcoder";
|
||||
@@ -115,15 +116,19 @@ export async function preResolveHostileMediaProxies(
|
||||
);
|
||||
return;
|
||||
}
|
||||
const hostilePathnames = Object.entries(codecMap)
|
||||
.filter(([, facts]) => decideMediaProxyEligibility(facts).eligible)
|
||||
.map(([pathname]) => pathname);
|
||||
if (hostilePathnames.length === 0) return;
|
||||
const hostileEntries = Object.entries(codecMap).filter(
|
||||
([, facts]) => decideMediaProxyEligibility(facts).eligible,
|
||||
);
|
||||
if (hostileEntries.length === 0) return;
|
||||
|
||||
const startedAt = Date.now();
|
||||
const results = await Promise.allSettled(
|
||||
hostilePathnames.map((pathname) =>
|
||||
resolveProxy(projectDir, resolve(projectDir, pathname.replace(/^\/+/, ""))),
|
||||
hostileEntries.map(([pathname, facts]) =>
|
||||
resolveProxy(
|
||||
projectDir,
|
||||
resolve(projectDir, pathname.replace(/^\/+/, "")),
|
||||
proxyVariantFor(facts),
|
||||
),
|
||||
),
|
||||
);
|
||||
const failed = results.filter((result) => result.status === "rejected").length;
|
||||
|
||||
@@ -26,7 +26,7 @@ export interface ProjectConfigPaths {
|
||||
export interface ProjectConfigMedia {
|
||||
/**
|
||||
* Auto-transcode browser-hostile video codecs (e.g. HEVC) to a cached
|
||||
* H.264 proxy for supported preview surfaces. Render always uses the
|
||||
* alpha-aware authoring proxy for supported preview surfaces. Render always uses the
|
||||
* original file regardless of this setting. Default true.
|
||||
*/
|
||||
autoProxy?: boolean;
|
||||
|
||||
@@ -49,6 +49,7 @@ vi.mock("@hyperframes/studio-server/proxy-transcoder", () => ({
|
||||
|
||||
vi.mock("@hyperframes/studio-server/media-codec-map", () => ({
|
||||
scanProjectMediaCodecMap: mocks.scanProjectMediaCodecMap,
|
||||
proxyVariantFor: (facts: { hasAlpha?: boolean }) => (facts.hasAlpha ? "vp9" : "h264"),
|
||||
}));
|
||||
|
||||
const { bakeMediaProxies, PROXY_ARCHIVE_PREFIX } = await import("./publishProxyBake.js");
|
||||
@@ -60,10 +61,10 @@ const { bakeMediaProxies, PROXY_ARCHIVE_PREFIX } = await import("./publishProxyB
|
||||
const PROJECT_DIR = resolve("/project");
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
function tmpProxyFile(content: string): string {
|
||||
function tmpProxyFile(content: string, extension = ".mp4"): string {
|
||||
const dir = mkdtempSync(join(tmpdir(), "hf-publish-proxy-bake-"));
|
||||
tempDirs.push(dir);
|
||||
const path = join(dir, "proxy.mp4");
|
||||
const path = join(dir, `proxy${extension}`);
|
||||
writeFileSync(path, content, "utf-8");
|
||||
return path;
|
||||
}
|
||||
@@ -108,7 +109,11 @@ describe("bakeMediaProxies", () => {
|
||||
expect(html).toContain(proxyEntries[0]!);
|
||||
expect(html).not.toContain('src="clip.mp4"');
|
||||
|
||||
expect(mocks.resolveProxy).toHaveBeenCalledWith(PROJECT_DIR, join(PROJECT_DIR, "clip.mp4"));
|
||||
expect(mocks.resolveProxy).toHaveBeenCalledWith(
|
||||
PROJECT_DIR,
|
||||
join(PROJECT_DIR, "clip.mp4"),
|
||||
"h264",
|
||||
);
|
||||
expect(mocks.waitForProxy).toHaveBeenCalledWith(expect.any(Promise), 15 * 60 * 1000);
|
||||
expect(manifest).toEqual({ proxied: ["/clip.mp4"], skippedAlpha: [], failed: [] });
|
||||
});
|
||||
@@ -191,7 +196,7 @@ describe("bakeMediaProxies", () => {
|
||||
expect(html).not.toContain("assets/my%20clip.mp4");
|
||||
});
|
||||
|
||||
it("reports an alpha-bearing hostile asset as skipped while keeping HTML on the original", async () => {
|
||||
it("bakes an alpha-bearing hostile asset as a VP9 WebM proxy", async () => {
|
||||
mocks.scanProjectMediaCodecMap.mockResolvedValue({
|
||||
"/clip.mov": {
|
||||
codecName: "prores",
|
||||
@@ -200,6 +205,8 @@ describe("bakeMediaProxies", () => {
|
||||
hasAlpha: true,
|
||||
},
|
||||
});
|
||||
const proxyPath = tmpProxyFile("PROXY_VP9_ALPHA_BYTES", ".webm");
|
||||
mocks.resolveProxy.mockResolvedValue(proxyPath);
|
||||
const fileContents = new Map<string, Buffer>([
|
||||
["index.html", indexHtml(`<video src="clip.mov" muted></video>`)],
|
||||
["clip.mov", Buffer.from("ORIGINAL_PRORES_4444_BYTES", "utf-8")],
|
||||
@@ -207,12 +214,18 @@ describe("bakeMediaProxies", () => {
|
||||
|
||||
const manifest = await bakeMediaProxies(PROJECT_DIR, fileContents);
|
||||
|
||||
expect(mocks.resolveProxy).not.toHaveBeenCalled();
|
||||
expect([...fileContents.keys()].some((k) => k.startsWith(`${PROXY_ARCHIVE_PREFIX}/`))).toBe(
|
||||
false,
|
||||
expect(mocks.resolveProxy).toHaveBeenCalledWith(
|
||||
PROJECT_DIR,
|
||||
join(PROJECT_DIR, "clip.mov"),
|
||||
"vp9",
|
||||
);
|
||||
expect(fileContents.get("index.html")?.toString("utf-8")).toContain('src="clip.mov"');
|
||||
expect(manifest).toEqual({ proxied: [], skippedAlpha: ["/clip.mov"], failed: [] });
|
||||
const proxyEntries = [...fileContents.keys()].filter((key) =>
|
||||
key.startsWith(`${PROXY_ARCHIVE_PREFIX}/`),
|
||||
);
|
||||
expect(proxyEntries).toEqual([`${PROXY_ARCHIVE_PREFIX}/proxy.webm`]);
|
||||
expect(fileContents.get(proxyEntries[0]!)?.toString("utf-8")).toBe("PROXY_VP9_ALPHA_BYTES");
|
||||
expect(fileContents.get("index.html")?.toString("utf-8")).toContain(proxyEntries[0]!);
|
||||
expect(manifest).toEqual({ proxied: ["/clip.mov"], skippedAlpha: [], failed: [] });
|
||||
});
|
||||
|
||||
it("returns deterministic manifest ordering across concurrent transcodes", async () => {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Publish-time proxy baking (U6 of
|
||||
* docs/plans/2026-07-14-002-feat-transparent-media-proxies-plan.md).
|
||||
*
|
||||
* Published pages are static (no server), so the on-demand `?hf-proxy=h264`
|
||||
* Published pages are static (no server), so the on-demand `?hf-proxy=`
|
||||
* negotiation the preview/play surfaces use (U3/U4) isn't possible there.
|
||||
* Instead this scans the archive's HTML entries for local `<video src>`
|
||||
* references to browser-hostile codecs (HEVC, ProRes, ...), transcodes each
|
||||
@@ -20,8 +20,8 @@
|
||||
* `cloud render` never calls this: it uses `createPublishArchive` directly,
|
||||
* which has no baking hook (R2 in the plan).
|
||||
*
|
||||
* Alpha-bearing sources remain explicit skips because H.264 would destroy
|
||||
* transparency. A failed opaque-hostile transcode aborts publish with a
|
||||
* Alpha-bearing sources bake as VP9/WebM so transparency survives. A failed
|
||||
* hostile transcode aborts publish with a
|
||||
* structured manifest rather than silently shipping an unplayable asset.
|
||||
*/
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
resolveLocalAssetCandidates,
|
||||
} from "@hyperframes/parsers/asset-resolution";
|
||||
import {
|
||||
proxyVariantFor,
|
||||
scanProjectMediaCodecMap,
|
||||
type HtmlSourceLike,
|
||||
} from "@hyperframes/studio-server/media-codec-map";
|
||||
@@ -51,6 +52,7 @@ export const PROXY_ARCHIVE_PREFIX = "_proxy";
|
||||
|
||||
export interface ProxyBakeManifest {
|
||||
proxied: string[];
|
||||
/** @deprecated Alpha sources are proxied as VP9; retained as an always-empty compatibility field. */
|
||||
skippedAlpha: string[];
|
||||
failed: Array<{ path: string; error: string }>;
|
||||
}
|
||||
@@ -75,11 +77,11 @@ function isHtmlEntry(path: string): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutates `fileContents` in place: adds a `_proxy/<hash>.mp4` entry for every
|
||||
* Mutates `fileContents` in place: adds a variant-specific `_proxy/<hash>` entry for every
|
||||
* browser-hostile local video asset referenced from the archive's HTML, and
|
||||
* rewrites those HTML entries' matching `<video src>` attributes to point at
|
||||
* the proxy. Returns a structured manifest; throws ProxyBakeError when any
|
||||
* required opaque proxy cannot be prepared.
|
||||
* required proxy cannot be prepared.
|
||||
*/
|
||||
export async function bakeMediaProxies(
|
||||
projectDir: string,
|
||||
@@ -97,17 +99,7 @@ export async function bakeMediaProxies(
|
||||
|
||||
const codecMap = await scanProjectMediaCodecMap(absProjectDir, htmlSources);
|
||||
const hostileEntries = Object.entries(codecMap).filter(([, facts]) => facts.browserHostile);
|
||||
const hostilePathnames: string[] = [];
|
||||
for (const [pathname, facts] of hostileEntries) {
|
||||
if (facts.hasAlpha) {
|
||||
// Alpha sources are never proxied: an H.264 proxy would destroy the
|
||||
// transparency (e.g. ProRes 4444 alpha). Keep the original in place.
|
||||
manifest.skippedAlpha.push(pathname);
|
||||
continue;
|
||||
}
|
||||
hostilePathnames.push(pathname);
|
||||
}
|
||||
if (hostilePathnames.length === 0) return manifest;
|
||||
if (hostileEntries.length === 0) return manifest;
|
||||
|
||||
// Absolute source path -> archive path of its baked proxy. Built by
|
||||
// resolving each map key back to an absolute path the same way
|
||||
@@ -117,11 +109,11 @@ export async function bakeMediaProxies(
|
||||
const proxyByAbsolutePath = new Map<string, string>();
|
||||
|
||||
await Promise.all(
|
||||
hostilePathnames.map(async (pathname) => {
|
||||
hostileEntries.map(async ([pathname, facts]) => {
|
||||
const absoluteSourcePath = resolve(absProjectDir, pathname.replace(/^\/+/, ""));
|
||||
try {
|
||||
const proxyPath = await waitForProxy(
|
||||
resolveProxy(absProjectDir, absoluteSourcePath),
|
||||
resolveProxy(absProjectDir, absoluteSourcePath, proxyVariantFor(facts)),
|
||||
TRANSCODE_TIMEOUT_MS,
|
||||
);
|
||||
const archivePath = `${PROXY_ARCHIVE_PREFIX}/${basename(proxyPath)}`;
|
||||
|
||||
@@ -46,14 +46,13 @@ const mocks = vi.hoisted(() => {
|
||||
>
|
||||
>(async () => ({})),
|
||||
probeAssetCodec: vi.fn(async () => ({
|
||||
codecName: "prores",
|
||||
pixelFormat: "yuva444p10le",
|
||||
hasAlpha: true,
|
||||
codecName: "hevc",
|
||||
hasAlpha: false,
|
||||
browserHostile: true,
|
||||
representativeMime: null,
|
||||
})),
|
||||
decideMediaProxyEligibility: vi.fn<
|
||||
() => { eligible: true } | { eligible: false; reason: "alpha_source" }
|
||||
() => { eligible: true } | { eligible: false; reason: "browser_safe_codec" }
|
||||
>(() => ({ eligible: true })),
|
||||
ProxyTranscodeError: FakeProxyTranscodeError,
|
||||
ProxyCapacityError: FakeProxyCapacityError,
|
||||
@@ -70,6 +69,17 @@ vi.mock("@hyperframes/studio-server/proxy-transcoder", () => ({
|
||||
vi.mock("@hyperframes/studio-server/media-codec-map", () => ({
|
||||
probeAssetCodec: mocks.probeAssetCodec,
|
||||
decideMediaProxyEligibility: mocks.decideMediaProxyEligibility,
|
||||
isProxyVariant: (value: string) => value === "h264" || value === "vp9",
|
||||
isProxyVariantRequest: (value: string) => value === "auto" || value === "h264" || value === "vp9",
|
||||
proxyVariantFor: (facts: { hasAlpha?: boolean }) => (facts.hasAlpha ? "vp9" : "h264"),
|
||||
resolveProxyVariantRequest: (request: "auto" | "h264" | "vp9", facts: { hasAlpha?: boolean }) => {
|
||||
const expected = facts.hasAlpha ? "vp9" : "h264";
|
||||
return request === "auto" || request === expected ? expected : null;
|
||||
},
|
||||
PROXY_VARIANT_CONFIG: {
|
||||
h264: { extension: ".mp4", contentType: "video/mp4" },
|
||||
vp9: { extension: ".webm", contentType: "video/webm" },
|
||||
},
|
||||
}));
|
||||
|
||||
// The shared injection helper ships as a self-contained dist bundle (its copy
|
||||
@@ -276,7 +286,11 @@ describe("serveStaticProjectHtml transparent media proxies", () => {
|
||||
const res = await fetch(`${server.url}clip.mp4?hf-proxy=h264`);
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.text()).toBe("transcoded-h264-bytes");
|
||||
expect(mocks.resolveProxy).toHaveBeenCalledWith(projectDir, join(projectDir, "clip.mp4"));
|
||||
expect(mocks.resolveProxy).toHaveBeenCalledWith(
|
||||
projectDir,
|
||||
join(projectDir, "clip.mp4"),
|
||||
"h264",
|
||||
);
|
||||
});
|
||||
|
||||
it.each(["mxf", "mts", "m2ts", "ts", "mkv", "m4v"])(
|
||||
@@ -293,25 +307,34 @@ describe("serveStaticProjectHtml transparent media proxies", () => {
|
||||
const res = await fetch(`${server.url}clip.${extension}?hf-proxy=h264`);
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.text()).toBe("transcoded-h264-bytes");
|
||||
expect(mocks.resolveProxy).toHaveBeenCalledWith(projectDir, sourcePath);
|
||||
expect(mocks.resolveProxy).toHaveBeenCalledWith(projectDir, sourcePath, "h264");
|
||||
},
|
||||
);
|
||||
|
||||
it("rejects an alpha-bearing video before attempting a static-server proxy transcode", async () => {
|
||||
it("serves an alpha-bearing video through a VP9 WebM proxy", async () => {
|
||||
const projectDir = mk();
|
||||
writeFileSync(join(projectDir, "clip.mov"), "prores-4444-alpha-bytes");
|
||||
mocks.decideMediaProxyEligibility.mockReturnValueOnce({
|
||||
eligible: false,
|
||||
reason: "alpha_source",
|
||||
mocks.probeAssetCodec.mockResolvedValueOnce({
|
||||
codecName: "prores",
|
||||
hasAlpha: true,
|
||||
browserHostile: true,
|
||||
representativeMime: null,
|
||||
});
|
||||
const proxyPath = join(projectDir, "proxy.webm");
|
||||
writeFileSync(proxyPath, "vp9-alpha-proxy");
|
||||
mocks.resolveProxy.mockResolvedValueOnce(proxyPath);
|
||||
server = await serveStaticProjectHtml(projectDir, "<html></html>");
|
||||
|
||||
const res = await fetch(`${server.url}clip.mov?hf-proxy=h264`);
|
||||
const res = await fetch(`${server.url}clip.mov?hf-proxy=auto`);
|
||||
|
||||
expect(res.status).toBe(422);
|
||||
expect(await res.text()).toContain("alpha_source");
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get("content-type")).toBe("video/webm");
|
||||
expect(mocks.probeAssetCodec).toHaveBeenCalledWith(join(projectDir, "clip.mov"));
|
||||
expect(mocks.resolveProxy).not.toHaveBeenCalled();
|
||||
expect(mocks.resolveProxy).toHaveBeenCalledWith(
|
||||
projectDir,
|
||||
join(projectDir, "clip.mov"),
|
||||
"vp9",
|
||||
);
|
||||
});
|
||||
|
||||
it("answers 502 when the proxy transcode fails", async () => {
|
||||
|
||||
@@ -11,7 +11,11 @@ import {
|
||||
} from "@hyperframes/studio-server/proxy-transcoder";
|
||||
import {
|
||||
decideMediaProxyEligibility,
|
||||
isProxyVariantRequest,
|
||||
probeAssetCodec,
|
||||
resolveProxyVariantRequest,
|
||||
PROXY_VARIANT_CONFIG,
|
||||
type ProxyVariantRequest,
|
||||
} from "@hyperframes/studio-server/media-codec-map";
|
||||
|
||||
export interface StaticProjectServer {
|
||||
@@ -32,10 +36,11 @@ function serveFileWithRange(
|
||||
filePath: string,
|
||||
rangeHeader: string | undefined,
|
||||
res: ServerResponse,
|
||||
contentType = getMimeType(filePath),
|
||||
) {
|
||||
const size = statSync(filePath).size;
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": getMimeType(filePath),
|
||||
"Content-Type": contentType,
|
||||
"Accept-Ranges": "bytes",
|
||||
};
|
||||
|
||||
@@ -79,17 +84,34 @@ function serveFileWithRange(
|
||||
}
|
||||
|
||||
/**
|
||||
* Serves `?hf-proxy=h264` for a media request: 404s (no transcode attempted)
|
||||
* Serves an alpha-aware `?hf-proxy=` variant for a media request: 404s (no transcode attempted)
|
||||
* when auto-proxying is off or the asset isn't a video, resolves+serves the
|
||||
* cached H.264 proxy with Range support on success, and answers 502 on a
|
||||
* cached proxy with Range support on success, and answers 502 on a
|
||||
* transcode failure (never a silent black frame). Shared by every one of
|
||||
* `serveStaticProjectHtml`'s seven callers (check/snapshot/validate/compare/
|
||||
* grade-compare/motionShot/layout) — see the KTD in
|
||||
* docs/plans/2026-07-14-002-feat-transparent-media-proxies-plan.md.
|
||||
*/
|
||||
/** Map a transcode failure to a response; never a silent black frame. */
|
||||
function writeProxyError(err: unknown, res: ServerResponse): void {
|
||||
if (err instanceof ProxyCapacityError) {
|
||||
res.writeHead(503, { "Content-Type": "text/plain", "Retry-After": "1" });
|
||||
res.end(`Proxy transcode deferred: ${err.message}`);
|
||||
return;
|
||||
}
|
||||
if (err instanceof ProxyTranscodeError) {
|
||||
res.writeHead(502, { "Content-Type": "text/plain" });
|
||||
res.end(`Proxy transcode failed: ${err.message}`);
|
||||
return;
|
||||
}
|
||||
res.writeHead(500);
|
||||
res.end();
|
||||
}
|
||||
|
||||
async function serveProxyRequest(
|
||||
projectDir: string,
|
||||
filePath: string,
|
||||
request: ProxyVariantRequest,
|
||||
autoProxy: boolean,
|
||||
rangeHeader: string | undefined,
|
||||
res: ServerResponse,
|
||||
@@ -101,29 +123,30 @@ async function serveProxyRequest(
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const eligibility = decideMediaProxyEligibility(await probeAssetCodec(filePath));
|
||||
const facts = await probeAssetCodec(filePath);
|
||||
const eligibility = decideMediaProxyEligibility(facts);
|
||||
if (!eligibility.eligible) {
|
||||
res.writeHead(422, { "Content-Type": "text/plain" });
|
||||
res.end(`media proxy unavailable: ${eligibility.reason}`);
|
||||
return;
|
||||
}
|
||||
const proxyPath = await resolveProxy(projectDir, filePath);
|
||||
if (!facts) {
|
||||
res.writeHead(422, { "Content-Type": "text/plain" });
|
||||
res.end("media proxy unavailable: unknown_codec");
|
||||
return;
|
||||
}
|
||||
const variant = resolveProxyVariantRequest(request, facts);
|
||||
if (!variant) {
|
||||
res.writeHead(422, { "Content-Type": "text/plain" });
|
||||
res.end("media proxy variant does not match asset");
|
||||
return;
|
||||
}
|
||||
const proxyPath = await resolveProxy(projectDir, filePath, variant);
|
||||
// The await above can span a whole transcode; the client may be gone.
|
||||
if (res.writableEnded || res.destroyed) return;
|
||||
serveFileWithRange(proxyPath, rangeHeader, res);
|
||||
serveFileWithRange(proxyPath, rangeHeader, res, PROXY_VARIANT_CONFIG[variant].contentType);
|
||||
} catch (err) {
|
||||
if (err instanceof ProxyCapacityError) {
|
||||
res.writeHead(503, { "Content-Type": "text/plain", "Retry-After": "1" });
|
||||
res.end(`Proxy transcode deferred: ${err.message}`);
|
||||
return;
|
||||
}
|
||||
if (err instanceof ProxyTranscodeError) {
|
||||
res.writeHead(502, { "Content-Type": "text/plain" });
|
||||
res.end(`Proxy transcode failed: ${err.message}`);
|
||||
return;
|
||||
}
|
||||
res.writeHead(500);
|
||||
res.end();
|
||||
writeProxyError(err, res);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,9 +179,10 @@ export async function serveStaticProjectHtml(
|
||||
|
||||
const queryIndex = url.indexOf("?");
|
||||
const pathOnly = queryIndex === -1 ? url : url.slice(0, queryIndex);
|
||||
const wantsProxy =
|
||||
queryIndex !== -1 &&
|
||||
new URLSearchParams(url.slice(queryIndex + 1)).get("hf-proxy") === "h264";
|
||||
const proxyParam =
|
||||
queryIndex === -1 ? null : new URLSearchParams(url.slice(queryIndex + 1)).get("hf-proxy");
|
||||
const proxyRequest =
|
||||
proxyParam !== null && isProxyVariantRequest(proxyParam) ? proxyParam : null;
|
||||
|
||||
const requestPath = decodeURIComponent(pathOnly).replace(/^\//, "");
|
||||
for (const root of roots) {
|
||||
@@ -166,8 +190,15 @@ export async function serveStaticProjectHtml(
|
||||
const rel = relative(root, filePath);
|
||||
if (rel.startsWith("..") || isAbsolute(rel)) continue; // traversal guard; try next root
|
||||
if (existsSync(filePath)) {
|
||||
if (wantsProxy) {
|
||||
void serveProxyRequest(projectDir, filePath, autoProxy, req.headers.range, res);
|
||||
if (proxyRequest) {
|
||||
void serveProxyRequest(
|
||||
projectDir,
|
||||
filePath,
|
||||
proxyRequest,
|
||||
autoProxy,
|
||||
req.headers.range,
|
||||
res,
|
||||
);
|
||||
} else {
|
||||
serveFileWithRange(filePath, req.headers.range, res);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user