This commit is contained in:
th committed 2026-04-19 17:53:36 +02:00
commit c6863f2bfc
34 files changed
+6909

No files matched your search

+119
View File
@@ -0,0 +1,119 @@
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
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<ServerRenderResult> {
const baseUrl = SERVER_URL
const startRes = await fetch(`${baseUrl}/render`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
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 fehlgeschlagen (${startRes.status}): ${body.slice(0, 200)}`)
}
const { jobId } = (await startRes.json()) as { jobId: string }
if (!jobId) throw new Error('Server lieferte keine jobId')
const downloadUrl = await new Promise<string>((resolve, reject) => {
const es = new EventSource(`${baseUrl}/render/${jobId}/events`)
const onAbort = () => {
try {
es.close()
} catch {
/* noop */
}
fetch(`${baseUrl}/render/${jobId}`, { 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 meldete fertig ohne Download-URL'))
}
}
es.onerror = () => {
es.close()
opts.signal?.removeEventListener('abort', onAbort)
reject(new Error('Verbindung zum Render-Server verloren'))
}
})
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<void> {
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<boolean> {
try {
const res = await fetch(`${SERVER_URL}/health`, { method: 'GET' })
return res.ok
} catch {
return false
}
}