import type { Scene } from '@/types/scene' import { drawSceneFrame, loadSceneImages } from './render' function effectiveEnd(scene: Scene): number { let max = 0 for (const layer of scene.layers) { for (const kf of layer.keyframes) { if (kf.time > max) max = kf.time } } if (max <= 0) return scene.duration return Math.min(max, scene.duration) } export interface VideoExportResult { blob: Blob mimeType: string extension: string } export interface VideoExportOptions { onProgress?: (progress: number) => void signal?: AbortSignal bitsPerSecond?: number } const MIME_CANDIDATES = [ 'video/webm;codecs=vp9', 'video/webm;codecs=vp8', 'video/webm', 'video/mp4;codecs=h264', 'video/mp4', ] export function pickMimeType(): string | null { if (typeof MediaRecorder === 'undefined') return null for (const mime of MIME_CANDIDATES) { if (MediaRecorder.isTypeSupported?.(mime)) return mime } return null } export async function renderSceneToVideo( scene: Scene, opts: VideoExportOptions = {}, ): Promise { const mimeType = pickMimeType() if (!mimeType) { throw new Error('MediaRecorder wird von diesem Browser nicht unterstützt.') } const canvas = document.createElement('canvas') canvas.width = scene.canvas.width canvas.height = scene.canvas.height const ctx = canvas.getContext('2d') if (!ctx) throw new Error('2D context unavailable') const images = await loadSceneImages(scene) drawSceneFrame(ctx, scene, 0, images) const stream = canvas.captureStream(scene.fps) const recorder = new MediaRecorder(stream, { mimeType, videoBitsPerSecond: opts.bitsPerSecond ?? 8_000_000, }) const chunks: Blob[] = [] recorder.ondataavailable = (e) => { if (e.data && e.data.size > 0) chunks.push(e.data) } const stopped = new Promise((resolve) => { recorder.onstop = () => resolve() }) recorder.start() const startedAt = performance.now() const renderDuration = effectiveEnd(scene) const finished = new Promise((resolve, reject) => { let raf = 0 const step = () => { if (opts.signal?.aborted) { cancelAnimationFrame(raf) try { recorder.stop() } catch { /* ignore */ } stream.getTracks().forEach((t) => t.stop()) reject(new DOMException('aborted', 'AbortError')) return } const elapsed = (performance.now() - startedAt) / 1000 const t = Math.min(elapsed, renderDuration) drawSceneFrame(ctx, scene, t, images) opts.onProgress?.(renderDuration > 0 ? Math.min(1, t / renderDuration) : 1) if (elapsed >= renderDuration) { try { recorder.stop() } catch { /* ignore */ } stream.getTracks().forEach((t) => t.stop()) resolve() return } raf = requestAnimationFrame(step) } raf = requestAnimationFrame(step) }) await finished await stopped const blob = new Blob(chunks, { type: mimeType }) const extension = mimeType.includes('mp4') ? 'mp4' : 'webm' return { blob, mimeType, extension } } export async function downloadSceneAsVideo( scene: Scene, opts: VideoExportOptions = {}, ): Promise { const { blob, extension } = await renderSceneToVideo(scene, opts) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = `animation.${extension}` document.body.appendChild(a) a.click() document.body.removeChild(a) setTimeout(() => URL.revokeObjectURL(url), 1000) }