diff --git a/packages/core/src/studio-api/routes/render.ts b/packages/core/src/studio-api/routes/render.ts index f21fc433..4948e8a1 100644 --- a/packages/core/src/studio-api/routes/render.ts +++ b/packages/core/src/studio-api/routes/render.ts @@ -216,6 +216,50 @@ export function registerRenderRoutes(api: Hono, adapter: StudioApiAdapter): void }); }); + // Upload a browser-rendered video (client-side renderer posts the blob here) + api.post("/projects/:id/renders/upload", async (c) => { + const project = await adapter.resolveProject(c.req.param("id")); + if (!project) return c.json({ error: "not found" }, 404); + + const rendersDir = adapter.rendersDir(project); + if (!existsSync(rendersDir)) mkdirSync(rendersDir, { recursive: true }); + + const body = await c.req.arrayBuffer(); + if (!body || body.byteLength === 0) { + return c.json({ error: "empty body" }, 400); + } + + const format = c.req.query("format") ?? "mp4"; + const now = new Date(); + const datePart = now.toISOString().slice(0, 10); + const timePart = now.toTimeString().slice(0, 8).replace(/:/g, "-"); + const jobId = `${project.id}_browser_${datePart}_${timePart}`; + const ext = format === "webm" ? ".webm" : ".mp4"; + const outputPath = join(rendersDir, `${jobId}${ext}`); + + const { writeFileSync: writeFile } = await import("node:fs"); + writeFile(outputPath, Buffer.from(body)); + writeFile( + outputPath.replace(/\.(mp4|webm)$/, ".meta.json"), + JSON.stringify({ + status: "complete", + source: "browser", + durationMs: Number(c.req.query("durationMs") ?? 0), + }), + ); + + // Register in job store so it shows in the renders list + renderJobs.set(jobId, { + id: jobId, + status: "complete", + progress: 100, + outputPath, + createdAt: Date.now(), + } as RenderJobState & { createdAt: number }); + + return c.json({ jobId, filename: `${jobId}${ext}`, size: body.byteLength }); + }); + // List renders api.get("/projects/:id/renders", async (c) => { const project = await adapter.resolveProject(c.req.param("id")); diff --git a/packages/renderer/package.json b/packages/renderer/package.json new file mode 100644 index 00000000..2670bc15 --- /dev/null +++ b/packages/renderer/package.json @@ -0,0 +1,52 @@ +{ + "name": "@hyperframes/renderer", + "version": "0.0.1", + "description": "Client-side video renderer for HyperFrames compositions", + "repository": { + "type": "git", + "url": "https://github.com/heygen-com/hyperframes", + "directory": "packages/renderer" + }, + "files": [ + "dist", + "README.md" + ], + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": { + "import": "./src/index.ts", + "types": "./src/index.ts" + } + }, + "publishConfig": { + "access": "public", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "scripts": { + "build": "tsc", + "test": "vitest run", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@zumer/snapdom": "^2.8.0", + "mediabunny": "^1.40.0" + }, + "devDependencies": { + "@types/jsdom": "^28.0.1", + "jsdom": "^29.0.2", + "typescript": "^5.0.0", + "vitest": "^3.2.4" + }, + "peerDependencies": { + "@hyperframes/core": "workspace:^" + } +} diff --git a/packages/renderer/public/turbo-worker.html b/packages/renderer/public/turbo-worker.html new file mode 100644 index 00000000..e64dc004 --- /dev/null +++ b/packages/renderer/public/turbo-worker.html @@ -0,0 +1,10 @@ + + +
+ +