From ed02ba872c02eafed45f406c68d389d0a1112a4e Mon Sep 17 00:00:00 2001 From: cashmere Date: Thu, 30 Jul 2026 18:17:45 +0200 Subject: feat: Electron desktop app, native ffmpeg export, fully static web - apps/desktop: Electron shell serving the static web build over a privileged opencut:// protocol; exports stream frames over IPC to a bundled ffmpeg (libx264/libvpx-vp9, BT.709 tagged) with backpressure. electron-builder packaging: AppImage, deb, rpm. - apps/web: dead backend deleted (better-auth, drizzle/pg, upstash, botid, freesound proxy, blog, rss, env validation); next build emits a zero-env static export; /editor/[project_id] -> /editor?project=. - Native exporter mirrors SceneExporter; activates when the preload bridge is present; web keeps the mediabunny path. - Docker self-hosting now builds and serves the static export via nginx. - Release workflow: v* tags publish static web tarball + Linux packages. - Changelog 0.4.0; README/AGENTS/CONTRIBUTING/roadmap updated. - GPUI/Rust rewrite parked on branch rust-rewrite. --- apps/web/src/core/managers/renderer-manager.ts | 42 +++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 5 deletions(-) (limited to 'apps/web/src/core') diff --git a/apps/web/src/core/managers/renderer-manager.ts b/apps/web/src/core/managers/renderer-manager.ts index d73e3e58..29324eed 100644 --- a/apps/web/src/core/managers/renderer-manager.ts +++ b/apps/web/src/core/managers/renderer-manager.ts @@ -3,6 +3,8 @@ import type { RootNode } from "@/services/renderer/nodes/root-node"; import type { ExportOptions, ExportResult } from "@/export"; import { CanvasRenderer } from "@/services/renderer/canvas-renderer"; import { SceneExporter } from "@/services/renderer/scene-exporter"; +import { NativeExporter } from "@/services/renderer/native-exporter"; +import { getDesktopBridge } from "@/desktop/bridge"; import { buildScene } from "@/services/renderer/scene-builder"; import { createTimelineAudioBuffer } from "@/media/audio"; import { formatTimecode } from "opencut-wasm"; @@ -214,7 +216,7 @@ export class RendererManager { background: activeProject.settings.background, }); - const exporter = new SceneExporter({ + const exportParams = { width: exportSize.width, height: exportSize.height, fps: exportFps, @@ -222,7 +224,33 @@ export class RendererManager { quality, shouldIncludeAudio: !!includeAudio, audioBuffer: audioBuffer || undefined, - }); + }; + + type ProjectExporter = { + on( + event: "progress", + listener: (progress: number) => void, + ): unknown; + cancel(): void; + export({ + rootNode, + }: { + rootNode: RootNode; + }): Promise; + }; + + let exporter: ProjectExporter; + if (getDesktopBridge()) { + const safeName = + activeProject.metadata.name.replace(/[<>:"/\\|?*]/g, "-").trim() || + "export"; + exporter = new NativeExporter({ + ...exportParams, + fileName: `${safeName}.${format}`, + }); + } else { + exporter = new SceneExporter(exportParams); + } exporter.on("progress", (progress) => { const adjustedProgress = includeAudio @@ -242,20 +270,24 @@ export class RendererManager { const cancelInterval = setInterval(checkCancel, 100); try { - const buffer = await exporter.export({ rootNode: scene }); + const result = await exporter.export({ rootNode: scene }); clearInterval(cancelInterval); if (cancelled) { return { success: false, cancelled: true }; } - if (!buffer) { + if (typeof result === "string") { + return { success: true, filePath: result }; + } + + if (!result) { return { success: false, error: "Export failed to produce buffer" }; } return { success: true, - buffer, + buffer: result, }; } finally { clearInterval(cancelInterval); -- cgit v1.3.1