From 902f818c6e248853f7159d96f4d2a4fb7606736f Mon Sep 17 00:00:00 2001 From: Nick Szaunig Date: Sat, 12 Sep 2026 03:38:06 +0200 Subject: [PATCH] fix: ffmpeg-Binary im Container fehlte (ENOENT bei jedem Video-Upload) Live-Log zeigte: spawn .../ffmpeg-static/ffmpeg ENOENT bei jedem Transcode-Versuch, unabhaengig vom Videoformat (.mov war nur zufaellig der zuletzt getestete Fall). Die ffmpeg-static-Binary aus der migrator-Docker-Stage kam nicht zuverlaessig im finalen Image an. Root-Fix: ffmpeg jetzt per "apk add ffmpeg" als System-Paket in der runner-Stage installiert - liegt fest unter /usr/bin auf PATH, unabhaengig von jeder Node/pnpm-Kopiermechanik. lib/video-transcode.ts versucht zuerst "ffmpeg" ueber PATH und faellt nur bei ENOENT (kein System-ffmpeg vorhanden, typischerweise lokale Windows-/Mac-Entwicklung) auf die per npm mitgelieferte ffmpeg-static-Binary zurueck, damit "npm run dev" weiterhin ohne manuelle Einrichtung funktioniert. Beide Pfade lokal gegen ein synthetisches Testvideo verifiziert. Co-Authored-By: Claude Sonnet 5 --- Dockerfile | 25 ++++++-------- src/lib/video-transcode.ts | 69 ++++++++++++++++++++++---------------- 2 files changed, 51 insertions(+), 43 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1b6d095..2f0eef1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,25 +27,13 @@ ENV DATABASE_URL="postgresql://placeholder:placeholder@localhost:5432/placeholde # Datenbanken wechseln kann, siehe lib/prisma.ts. RUN pnpm build -# --- Migrator-Tools + Laufzeit-Zusatzpakete -------------------------------- +# --- Migrator-Tools ----------------------------------------------------- # Eigenstaendiger, schlanker node_modules-Baum nur mit Prisma-CLI, tsx und # den paar Laufzeit-Paketen, die prisma/seed.ts importiert — NICHT # next/react/sharp/stripe/alle devDependencies, die der volle builder-Stage # oben mitbringt. Wird unten in die runner-Stage kopiert, damit die App sich # beim Start selbst migrieren/seeden kann (kein separates Migrations-Image/ # kein Kubernetes-Job mehr noetig). -# -# ffmpeg-static (Video-Transcoding, siehe lib/video-transcode.ts) landet -# bewusst HIER statt per Next.js' automatischer Standalone-Output-Tracing -# aus der builder-Stage: dessen heruntergeladene ffmpeg-Binary liegt im -# pnpm-Store hinter einem Symlink, den Next.js' Tracer (anders als bei -# sharp, dafuer gibt es eine Sonderbehandlung) nicht zuverlaessig erkennt, -# UND ein direktes COPY einzelner Pakete aus dem pnpm-Store der -# builder-Stage wuerde denselben Symlink lediglich unaufgeloest (und damit -# kaputt) in die runner-Stage kopieren. Diese Stage installiert stattdessen -# jedes benoetigte Laufzeitpaket ueber ein eigenes, in sich geschlossenes -# "pnpm add" neu — genau das Muster, das fuer Prisma/tsx hier bereits -# funktioniert. FROM base AS migrator # pnpm-workspace.yaml zuerst kopieren: pnpm v11 blockt Postinstall-Skripte # (u.a. @prisma/engines, laedt die Schema-Engine-Binary fuer "migrate @@ -62,12 +50,19 @@ COPY pnpm-workspace.yaml ./ RUN pnpm add --save-prod \ prisma@^7.9.1 tsx@^4 bcryptjs@^3.0.3 dotenv@^17.4.2 \ @prisma/client@^7.9.1 @prisma/adapter-pg@^7.9.1 \ - @prisma/adapter-mariadb@^7.9.1 mariadb@^3.5.3 \ - ffmpeg-static@^5.3.0 + @prisma/adapter-mariadb@^7.9.1 mariadb@^3.5.3 # --- Runtime -------------------------------------------------------------- FROM base AS runner ENV NODE_ENV=production +# System-ffmpeg statt des npm-Pakets ffmpeg-static (lib/video-transcode.ts +# nutzt es lokal per "npm run dev" trotzdem als Fallback, siehe dort): +# ffmpeg-static laedt seine Binary in den pnpm-Store, dessen Pfad ueber +# mehrere Docker-Build-Stages hinweg (migrator -> runner) per COPY nicht +# zuverlaessig ankam (spawn ... ffmpeg ENOENT im Betrieb beobachtet). Ein +# per apk installiertes System-Paket liegt dagegen fest unter /usr/bin auf +# PATH, unabhaengig von jeder Node/pnpm-Verzeichnisstruktur. +RUN apk add --no-cache ffmpeg RUN addgroup --system --gid 1001 nodejs \ && adduser --system --uid 1001 nextjs diff --git a/src/lib/video-transcode.ts b/src/lib/video-transcode.ts index 133aa65..667f3cf 100644 --- a/src/lib/video-transcode.ts +++ b/src/lib/video-transcode.ts @@ -2,7 +2,7 @@ import { spawn } from "node:child_process"; import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; -import ffmpegPath from "ffmpeg-static"; +import ffmpegStaticPath from "ffmpeg-static"; /** * Harte Obergrenze fuer einen einzelnen ffmpeg-Lauf: verhindert, dass ein @@ -40,39 +40,52 @@ const TRANSCODE_TIMEOUT_MS = 5 * 60 * 1000; * Magic-Bytes am Dateianfang). */ export async function transcodeVideoForWeb(buffer: Buffer): Promise { - if (!ffmpegPath) { - throw new Error("ffmpeg-Binary (ffmpeg-static) nicht gefunden."); - } - const dir = await mkdtemp(path.join(tmpdir(), "wabenhain-video-")); const inputPath = path.join(dir, "input"); const outputPath = path.join(dir, "output.mp4"); + const args = [ + "-y", + "-i", + inputPath, + "-an", + "-vf", + "scale='min(1920,iw)':-2", + "-c:v", + "libx264", + "-preset", + "medium", + "-crf", + "26", + "-maxrate", + "3M", + "-bufsize", + "6M", + "-pix_fmt", + "yuv420p", + "-movflags", + "+faststart", + outputPath, + ]; try { await writeFile(inputPath, buffer); - await runFfmpeg(ffmpegPath, [ - "-y", - "-i", - inputPath, - "-an", - "-vf", - "scale='min(1920,iw)':-2", - "-c:v", - "libx264", - "-preset", - "medium", - "-crf", - "26", - "-maxrate", - "3M", - "-bufsize", - "6M", - "-pix_fmt", - "yuv420p", - "-movflags", - "+faststart", - outputPath, - ]); + try { + // Bevorzugt "ffmpeg" ueber PATH — im Produktions-Image per "apk add + // ffmpeg" installiert (siehe Dockerfile "runner"-Stage), fest unter + // /usr/bin und unabhaengig von jeder Node/pnpm-Verzeichnisstruktur. + await runFfmpeg("ffmpeg", args); + } catch (error: unknown) { + // ENOENT = Binary ueber PATH nicht gefunden (typischerweise ein + // lokaler Windows-/Mac-Rechner ohne System-ffmpeg) — dann auf die per + // npm mitgelieferte ffmpeg-static-Binary ausweichen, damit "npm run + // dev" ohne manuelle Einrichtung funktioniert. Jeder andere Fehler + // (z.B. eine tatsaechlich nicht dekodierbare Datei) soll dagegen + // unveraendert durchschlagen, nicht stillschweigend erneut versucht + // werden. + const isMissingBinary = error instanceof Error && "code" in error && error.code === "ENOENT"; + if (!isMissingBinary || !ffmpegStaticPath) throw error; + await runFfmpeg(ffmpegStaticPath, args); + } return await readFile(outputPath); } finally { await rm(dir, { recursive: true, force: true });