mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-01 19:42:03 +00:00
fix: address code review findings for MOV format support
- Add missing -pix_fmt to chunkEncoder ProRes path (critical: alpha would silently be lost in non-streaming encode mode) - Fix chunked concat using wrong extension for MOV chunks - Add MOV to render listing filter, deletion cleanup, and ID regex - Fix Content-Type for MOV renders (video/quicktime via shared helper) - Add MOV to meta file path regex in studioServer and vite.config
This commit is contained in:
@@ -177,7 +177,7 @@ export function createStudioServer(options: StudioServerOptions): StudioServer {
|
||||
await executeRenderJob(job, opts.project.dir, opts.outputPath, onProgress);
|
||||
state.status = "complete";
|
||||
state.progress = 100;
|
||||
const metaPath = opts.outputPath.replace(/\.(mp4|webm)$/, ".meta.json");
|
||||
const metaPath = opts.outputPath.replace(/\.(mp4|webm|mov)$/, ".meta.json");
|
||||
writeFileSync(
|
||||
metaPath,
|
||||
JSON.stringify({ status: "complete", durationMs: Date.now() - startTime }),
|
||||
@@ -186,7 +186,7 @@ export function createStudioServer(options: StudioServerOptions): StudioServer {
|
||||
state.status = "failed";
|
||||
state.error = err instanceof Error ? err.message : String(err);
|
||||
try {
|
||||
const metaPath = opts.outputPath.replace(/\.(mp4|webm)$/, ".meta.json");
|
||||
const metaPath = opts.outputPath.replace(/\.(mp4|webm|mov)$/, ".meta.json");
|
||||
writeFileSync(metaPath, JSON.stringify({ status: "failed" }));
|
||||
} catch {
|
||||
/* ignore */
|
||||
|
||||
@@ -128,6 +128,18 @@ export function registerRenderRoutes(api: Hono, adapter: StudioApiAdapter): void
|
||||
});
|
||||
});
|
||||
|
||||
const RENDER_MIME: Record<string, string> = {
|
||||
".mp4": "video/mp4",
|
||||
".webm": "video/webm",
|
||||
".mov": "video/quicktime",
|
||||
};
|
||||
const RENDER_EXTENSIONS = Object.keys(RENDER_MIME);
|
||||
|
||||
function renderContentType(filePath: string): string {
|
||||
const ext = RENDER_EXTENSIONS.find((e) => filePath.endsWith(e));
|
||||
return (ext && RENDER_MIME[ext]) ?? "video/mp4";
|
||||
}
|
||||
|
||||
// Serve render inline (for in-browser playback — opens in a new tab)
|
||||
api.get("/render/:jobId/view", (c) => {
|
||||
const { jobId } = c.req.param();
|
||||
@@ -135,8 +147,7 @@ export function registerRenderRoutes(api: Hono, adapter: StudioApiAdapter): void
|
||||
if (!job?.outputPath || !existsSync(job.outputPath)) {
|
||||
return c.json({ error: "not found" }, 404);
|
||||
}
|
||||
const isWebm = job.outputPath.endsWith(".webm");
|
||||
const contentType = isWebm ? "video/webm" : "video/mp4";
|
||||
const contentType = renderContentType(job.outputPath);
|
||||
const filename = job.outputPath.split("/").pop() ?? `render.mp4`;
|
||||
const content = readFileSync(job.outputPath);
|
||||
return new Response(content, {
|
||||
@@ -156,8 +167,7 @@ export function registerRenderRoutes(api: Hono, adapter: StudioApiAdapter): void
|
||||
if (!job?.outputPath || !existsSync(job.outputPath)) {
|
||||
return c.json({ error: "not found" }, 404);
|
||||
}
|
||||
const isWebm = job.outputPath.endsWith(".webm");
|
||||
const contentType = isWebm ? "video/webm" : "video/mp4";
|
||||
const contentType = renderContentType(job.outputPath);
|
||||
const filename = job.outputPath.split("/").pop() ?? `render.mp4`;
|
||||
const content = readFileSync(job.outputPath);
|
||||
return new Response(content, {
|
||||
@@ -174,7 +184,7 @@ export function registerRenderRoutes(api: Hono, adapter: StudioApiAdapter): void
|
||||
for (const [, state] of renderJobs) {
|
||||
if (state.id === jobId && state.outputPath) {
|
||||
const dir = state.outputPath.replace(/\/[^/]+$/, "");
|
||||
for (const ext of [".mp4", ".webm", ".meta.json"]) {
|
||||
for (const ext of [".mp4", ".webm", ".mov", ".meta.json"]) {
|
||||
const fp = join(dir, `${jobId}${ext}`);
|
||||
if (existsSync(fp)) unlinkSync(fp);
|
||||
}
|
||||
@@ -194,8 +204,7 @@ export function registerRenderRoutes(api: Hono, adapter: StudioApiAdapter): void
|
||||
const rendersDir = adapter.rendersDir(project);
|
||||
const fp = join(rendersDir, filename);
|
||||
if (!existsSync(fp)) return c.json({ error: "not found" }, 404);
|
||||
const isWebm = fp.endsWith(".webm");
|
||||
const contentType = isWebm ? "video/webm" : "video/mp4";
|
||||
const contentType = renderContentType(fp);
|
||||
const content = readFileSync(fp);
|
||||
return new Response(content, {
|
||||
headers: {
|
||||
@@ -214,11 +223,11 @@ export function registerRenderRoutes(api: Hono, adapter: StudioApiAdapter): void
|
||||
const rendersDir = adapter.rendersDir(project);
|
||||
if (!existsSync(rendersDir)) return c.json({ renders: [] });
|
||||
const files = readdirSync(rendersDir)
|
||||
.filter((f) => f.endsWith(".mp4") || f.endsWith(".webm"))
|
||||
.filter((f) => f.endsWith(".mp4") || f.endsWith(".webm") || f.endsWith(".mov"))
|
||||
.map((f) => {
|
||||
const fp = join(rendersDir, f);
|
||||
const stat = statSync(fp);
|
||||
const rid = f.replace(/\.(mp4|webm)$/, "");
|
||||
const rid = f.replace(/\.(mp4|webm|mov)$/, "");
|
||||
const metaPath = join(rendersDir, `${rid}.meta.json`);
|
||||
let status: "complete" | "failed" = "complete";
|
||||
let durationMs: number | undefined;
|
||||
|
||||
@@ -130,6 +130,7 @@ export function buildEncoderArgs(
|
||||
}
|
||||
} else if (codec === "prores") {
|
||||
args.push("-c:v", "prores_ks", "-profile:v", preset, "-vendor", "apl0");
|
||||
args.push("-pix_fmt", pixelFormat);
|
||||
return [...args, "-y", outputPath];
|
||||
}
|
||||
|
||||
@@ -318,7 +319,11 @@ export async function encodeFramesChunkedConcat(
|
||||
}
|
||||
const startNumber = i * chunkSize;
|
||||
const framesInChunk = Math.min(chunkSize, files.length - startNumber);
|
||||
const ext = outputPath.endsWith(".webm") ? ".webm" : ".mp4";
|
||||
const ext = outputPath.endsWith(".webm")
|
||||
? ".webm"
|
||||
: outputPath.endsWith(".mov")
|
||||
? ".mov"
|
||||
: ".mp4";
|
||||
const chunkPath = join(chunkDir, `chunk_${String(i).padStart(4, "0")}${ext}`);
|
||||
const inputPath = join(framesDir, framePattern);
|
||||
const inputArgs = [
|
||||
|
||||
@@ -201,7 +201,7 @@ function createViteAdapter(dataDir: string, server: ViteDevServer): StudioApiAda
|
||||
if (evt.type === "complete") {
|
||||
state.status = "complete";
|
||||
state.outputPath = evt.outputPath || opts.outputPath;
|
||||
const metaPath = opts.outputPath.replace(/\.(mp4|webm)$/, ".meta.json");
|
||||
const metaPath = opts.outputPath.replace(/\.(mp4|webm|mov)$/, ".meta.json");
|
||||
writeFileSync(
|
||||
metaPath,
|
||||
JSON.stringify({ status: "complete", durationMs: Date.now() - startTime }),
|
||||
@@ -209,7 +209,7 @@ function createViteAdapter(dataDir: string, server: ViteDevServer): StudioApiAda
|
||||
}
|
||||
if (evt.type === "error") {
|
||||
state.status = "failed";
|
||||
const metaPath = opts.outputPath.replace(/\.(mp4|webm)$/, ".meta.json");
|
||||
const metaPath = opts.outputPath.replace(/\.(mp4|webm|mov)$/, ".meta.json");
|
||||
writeFileSync(metaPath, JSON.stringify({ status: "failed" }));
|
||||
}
|
||||
} catch {
|
||||
@@ -219,7 +219,7 @@ function createViteAdapter(dataDir: string, server: ViteDevServer): StudioApiAda
|
||||
}
|
||||
if (state.status === "rendering") {
|
||||
state.status = "complete";
|
||||
const metaPath = opts.outputPath.replace(/\.(mp4|webm)$/, ".meta.json");
|
||||
const metaPath = opts.outputPath.replace(/\.(mp4|webm|mov)$/, ".meta.json");
|
||||
writeFileSync(
|
||||
metaPath,
|
||||
JSON.stringify({ status: "complete", durationMs: Date.now() - startTime }),
|
||||
@@ -229,7 +229,7 @@ function createViteAdapter(dataDir: string, server: ViteDevServer): StudioApiAda
|
||||
.catch(() => {
|
||||
state.status = "failed";
|
||||
try {
|
||||
const metaPath = opts.outputPath.replace(/\.(mp4|webm)$/, ".meta.json");
|
||||
const metaPath = opts.outputPath.replace(/\.(mp4|webm|mov)$/, ".meta.json");
|
||||
writeFileSync(metaPath, JSON.stringify({ status: "failed" }));
|
||||
} catch {
|
||||
/* ignore */
|
||||
|
||||
Reference in New Issue
Block a user