import type { Scene } from '@/types/scene' const DEFAULT_SERVER = 'http://localhost:6000' const SERVER_URL = (import.meta.env.VITE_RENDER_SERVER as string | undefined) ?? DEFAULT_SERVER const RENDER_TOKEN = import.meta.env.VITE_RENDER_TOKEN as string | undefined export interface ServerRenderOptions { onProgress?: (progress: number) => void signal?: AbortSignal fps?: number crf?: number preset?: string } export interface ServerRenderResult { blob: Blob extension: string } export function getServerUrl(): string { return SERVER_URL } export async function renderOnServer( scene: Scene, opts: ServerRenderOptions = {}, ): Promise { const baseUrl = SERVER_URL const startRes = await fetch(`${baseUrl}/render`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(RENDER_TOKEN ? { Authorization: `Bearer ${RENDER_TOKEN}` } : {}), }, body: JSON.stringify({ scene, options: { fps: opts.fps, crf: opts.crf, preset: opts.preset, }, }), signal: opts.signal, }) if (!startRes.ok) { const body = await startRes.text().catch(() => '') throw new Error(`Server start failed (${startRes.status}): ${body.slice(0, 200)}`) } const { jobId, accessToken } = (await startRes.json()) as { jobId: string; accessToken: string } if (!jobId || !accessToken) throw new Error('Server did not return job credentials') const downloadUrl = await new Promise((resolve, reject) => { const jobToken = encodeURIComponent(accessToken) const es = new EventSource(`${baseUrl}/render/${jobId}/events?token=${jobToken}`) const onAbort = () => { try { es.close() } catch { /* noop */ } fetch(`${baseUrl}/render/${jobId}?token=${jobToken}`, { method: 'DELETE' }).catch(() => {}) reject(new DOMException('aborted', 'AbortError')) } if (opts.signal) { if (opts.signal.aborted) return onAbort() opts.signal.addEventListener('abort', onAbort, { once: true }) } es.onmessage = (evt) => { let data: { progress?: number done?: boolean error?: string | null url?: string | null } try { data = JSON.parse(evt.data) } catch { return } if (typeof data.progress === 'number') opts.onProgress?.(data.progress) if (data.done) { es.close() opts.signal?.removeEventListener('abort', onAbort) if (data.error) reject(new Error(data.error)) else if (data.url) resolve(`${baseUrl}${data.url}`) else reject(new Error('Server reported done without download URL')) } } es.onerror = () => { es.close() opts.signal?.removeEventListener('abort', onAbort) reject(new Error('Connection to render server lost')) } }) const fileRes = await fetch(downloadUrl, { signal: opts.signal }) if (!fileRes.ok) throw new Error(`Download fehlgeschlagen (${fileRes.status})`) const blob = await fileRes.blob() return { blob, extension: 'mp4' } } export async function downloadFromServer( scene: Scene, opts: ServerRenderOptions = {}, ): Promise { const { blob, extension } = await renderOnServer(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) } export async function isServerReachable(): Promise { try { const res = await fetch(`${SERVER_URL}/health`, { method: 'GET' }) return res.ok } catch { return false } }