summaryrefslogtreecommitdiff
path: root/apps/web/src/core/managers
diff options
context:
space:
mode:
authorcashmere <cashmere@autistici.org>2026-07-30 18:17:45 +0200
committercashmere <cashmere@autistici.org>2026-07-30 18:18:15 +0200
commited02ba872c02eafed45f406c68d389d0a1112a4e (patch)
tree40530d77cfe306621915f067c66531cb9e783a4d /apps/web/src/core/managers
parente837e7504379e100ff3b56282577ec67bb88bec9 (diff)
downloadopencut-ed02ba872c02eafed45f406c68d389d0a1112a4e.tar.gz
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.
Diffstat (limited to 'apps/web/src/core/managers')
-rw-r--r--apps/web/src/core/managers/renderer-manager.ts42
1 files changed, 37 insertions, 5 deletions
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<ArrayBuffer | string | null>;
+ };
+
+ 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);