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:
Miguel Ángel
2026-04-08 02:48:09 +02:00
parent ae0ec5793e
commit f1182c7f15
4 changed files with 30 additions and 16 deletions
+2 -2
View File
@@ -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 */
+18 -9
View File
@@ -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;
+6 -1
View File
@@ -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 = [
+4 -4
View File
@@ -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 */