summaryrefslogtreecommitdiff
path: root/apps/web/src/core/managers/renderer-manager.ts
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/core/managers/renderer-manager.ts')
-rw-r--r--apps/web/src/core/managers/renderer-manager.ts129
1 files changed, 129 insertions, 0 deletions
diff --git a/apps/web/src/core/managers/renderer-manager.ts b/apps/web/src/core/managers/renderer-manager.ts
new file mode 100644
index 00000000..22f1ebb8
--- /dev/null
+++ b/apps/web/src/core/managers/renderer-manager.ts
@@ -0,0 +1,129 @@
+import type { EditorCore } from "@/core";
+import type { RootNode } from "@/services/renderer/nodes/root-node";
+import type { ExportOptions, ExportResult } from "@/types/export";
+import { SceneExporter } from "@/services/renderer/scene-exporter";
+import { buildScene } from "@/services/renderer/scene-builder";
+import { createTimelineAudioBuffer } from "@/lib/media/audio";
+
+export class RendererManager {
+ private renderTree: RootNode | null = null;
+ private listeners = new Set<() => void>();
+
+ constructor(private editor: EditorCore) {}
+
+ setRenderTree({ renderTree }: { renderTree: RootNode | null }): void {
+ this.renderTree = renderTree;
+ this.notify();
+ }
+
+ getRenderTree(): RootNode | null {
+ return this.renderTree;
+ }
+
+ async exportProject({
+ options,
+ }: {
+ options: ExportOptions;
+ }): Promise<ExportResult> {
+ const { format, quality, fps, includeAudio, onProgress, onCancel } =
+ options;
+
+ try {
+ const tracks = this.editor.timeline.getTracks();
+ const mediaAssets = this.editor.media.getAssets();
+ const activeProject = this.editor.project.getActive();
+
+ if (!activeProject) {
+ return { success: false, error: "No active project" };
+ }
+
+ const duration = this.editor.timeline.getTotalDuration();
+ if (duration === 0) {
+ return { success: false, error: "Project is empty" };
+ }
+
+ const exportFps = fps || activeProject.settings.fps;
+ const canvasSize = activeProject.settings.canvasSize;
+
+ let audioBuffer: AudioBuffer | null = null;
+ if (includeAudio) {
+ onProgress?.({ progress: 0.05 });
+ audioBuffer = await createTimelineAudioBuffer({
+ tracks,
+ mediaAssets,
+ duration,
+ });
+ }
+
+ const scene = buildScene({
+ tracks,
+ mediaAssets,
+ duration,
+ canvasSize,
+ background: activeProject.settings.background,
+ });
+
+ const exporter = new SceneExporter({
+ width: canvasSize.width,
+ height: canvasSize.height,
+ fps: exportFps,
+ format,
+ quality,
+ shouldIncludeAudio: !!includeAudio,
+ audioBuffer: audioBuffer || undefined,
+ });
+
+ exporter.on("progress", (progress) => {
+ const adjustedProgress = includeAudio
+ ? 0.05 + progress * 0.95
+ : progress;
+ onProgress?.({ progress: adjustedProgress });
+ });
+
+ let cancelled = false;
+ const checkCancel = () => {
+ if (onCancel?.()) {
+ cancelled = true;
+ exporter.cancel();
+ }
+ };
+
+ const cancelInterval = setInterval(checkCancel, 100);
+
+ try {
+ const buffer = await exporter.export({ rootNode: scene });
+ clearInterval(cancelInterval);
+
+ if (cancelled) {
+ return { success: false, cancelled: true };
+ }
+
+ if (!buffer) {
+ return { success: false, error: "Export failed to produce buffer" };
+ }
+
+ return {
+ success: true,
+ buffer,
+ };
+ } finally {
+ clearInterval(cancelInterval);
+ }
+ } catch (error) {
+ console.error("Export failed:", error);
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : "Unknown export error",
+ };
+ }
+ }
+
+ subscribe(listener: () => void): () => void {
+ this.listeners.add(listener);
+ return () => this.listeners.delete(listener);
+ }
+
+ private notify(): void {
+ this.listeners.forEach((fn) => fn());
+ }
+}