Auch verfügbar in: EN English

Video-Pipeline

Inhalt

Videos durchlaufen nach dem Upload einen einzigen BullMQ-Job namens transcode. Die fünf Stufen unten sind Schritte innerhalb dieses Jobs, keine eigenen Jobs. Im Code laufen die Thumbnails erst nach der Transcodierung. Der Status wechselt von PENDING über PROCESSING auf LIVE, bei einem Fehler auf FAILED.


Überblick: Pipeline-Stufen

[1] Eingang → [2] Analyse → [3] Thumbnails → [4] Transcodierung → [5] Abschluss
Stufe Name Zuständigkeit
1 Eingang Datei empfangen, in MinIO ablegen
2 Analyse Metadaten mit ffprobe extrahieren
3 Thumbnails Kandidaten-Frames generieren
4 Transcodierung HLS-Varianten mit FFmpeg erzeugen
5 Abschluss Status setzen, Clients benachrichtigen

Stufe 1: Eingang

Der Browser schickt die Datei in einem einzigen Request per XMLHttpRequest an POST /api/upload, die API streamt den Body direkt nach MinIO. Fortsetzbare Uploads (tus.io) sind noch nicht umgesetzt, ein abgebrochener Upload startet von vorn.

  • Den Upload-Fortschritt misst der Browser selbst über xhr.upload.onprogress
  • Vorab vergleicht die API den Content-Length-Header mit MAX_UPLOAD_SIZE_MB (Standard 8192) und antwortet bei Überschreitung mit 413
  • Die Datei wird per Magic-Bytes als Video geprüft und landet in MinIO unter play-raw/[videoId].[ext]
  • Danach legt die API den Datensatz mit Status PENDING und Sichtbarkeit PRIVATE an und reiht den Job transcode ein

Stufe 2: Analyse

ffprobe extrahiert die technischen Metadaten der Rohdatei:

Metadatum Beschreibung
Dauer Länge des Videos in Sekunden
Auflösung Breite x Höhe in Pixeln
Codec Video-Codec (z. B. h264, av1)
Bitrate Durchschnittliche Bitrate in kbit/s

Die extrahierten Werte werden in der Datenbank gespeichert und bestimmen, welche HLS-Qualitätsstufen in Stufe 4 erzeugt werden.


Stufe 3: Thumbnails

FFmpeg extrahiert 5 Thumbnail-Kandidaten aus gleichmäßig verteilten Zeitpunkten im Video. Der Nutzer kann anschließend im Studio einen davon als Titelbild auswählen oder ein eigenes Bild hochladen.

  • Die Kandidaten entstehen bei 10, 30, 50, 70 und 90 % der Laufzeit und werden als WebP unter play-thumbs/[videoId]-cand-[1-5].webp gespeichert
  • Die Breite ist 1280 px, die Höhe folgt dem Seitenverhältnis
  • Voreingestellt ist der mittlere Kandidat (50 %)
  • Die Auswahl des Nutzers wird in der Datenbank vermerkt

Stufe 4: Transcodierung

FFmpeg erzeugt einen HLS-Stream mit mehreren Qualitätsvarianten (Adaptive Bitrate Streaming).

Qualitätsvarianten

Auflösung Bitrate Bedingung
1080p max. 5 Mbit/s Nur wenn Quelle >= 1080p
720p max. 2,8 Mbit/s Nur wenn Quelle >= 720p
480p max. 1,2 Mbit/s Immer, auch bei kleineren Quellen

Die Leiter ist im Worker fest hinterlegt. TRANSCODE_QUALITIES steht zwar in .env.example und im Unraid-Template, wird vom Code aber nicht ausgelesen. Video wird mit H.264 (libx264, Preset medium, CRF 22) kodiert, Audio mit AAC 128 kbit/s. Mit TRANSCODE_EXTRA_CODECS=av1 und/oder vp9 rendert der Worker pro Stufe zusätzlich AV1 (libsvtav1) oder VP9 (libvpx-vp9) mit rund 30 % weniger Bitrate. Das kostet deutlich mehr CPU, der Standard ist leer.

HLS-Konfiguration

  • Segmentformat: fmp4 (fragmentiertes MP4)
  • Segmentlänge: 4 Sekunden
  • Playlist: master.m3u8 + je eine Playlist pro Qualitätsstufe
  • Ausgabe: MinIO play-videos/[videoId]/

Der Job meldet seinen Fortschritt in groben Schritten an BullMQ (10, 20, je Variante bis 80, dann 85, 95 und 100 %). Das Studio zeigt davon keinen Prozentwert, sondern fragt den Status alle zwei Sekunden ab, solange ein Video PENDING oder PROCESSING ist.


Stufe 5: Abschluss

Nach erfolgreicher Transcodierung:

  1. DB-Status des Videos wird auf LIVE gesetzt, dazu Dauer, Auflösung, Codec, Bitrate, Thumbnails und das Format (SHORT bei Hochformat bis 60 Sekunden, sonst LONG)
  2. Ist AUTO_CAPTIONS_ENABLED=1, wird der Untertitel-Job eingereiht
  3. Ist das Video öffentlich, bekommen Abonnenten eine In-App-Benachrichtigung
  4. Das Studio sieht den neuen Status beim nächsten Abruf

Externer Import (yt-dlp)

Nutzer können externe Video-URLs importieren (YouTube, Vimeo und andere yt-dlp-kompatible Plattformen).

Ablauf:

  1. Nutzer gibt URL im Studio ein
  2. Worker führt yt-dlp aus und lädt die Originaldatei herunter
  3. Titel und Beschreibung aus yt-dlp werden in den Datensatz übernommen
  4. Standard-Sichtbarkeit ist privat — Nutzer muss aktiv veröffentlichen
  5. Danach durchläuft das Video die normale 5-stufige Pipeline

Auto-Untertitel (Whisper) — hinter Feature-Flag

Die Integration von whisper.cpp ermöglicht automatische Untertitelgenerierung nach der Transcodierung.

  • Aktivierung über AUTO_CAPTIONS_ENABLED=1, Standard ist aus
  • Der Transcode-Job übergibt fest die Sprache de, eine automatische Spracherkennung ist nicht verdrahtet
  • Modell über WHISPER_MODEL, Standard /models/ggml-small.bin
  • Ausgabe als WebVTT-Spur am Video (VideoCaption)
  • Laut Code werden nur die ersten sechs HLS-Segmente der ersten Variante transkribiert, bei 4-Sekunden-Segmenten also etwa die ersten 24 Sekunden
  • Läuft als eigene Queue caption-generate mit Concurrency 1

Wiedergabe

Der HLS.js-Player übernimmt die Videowiedergabe im Browser.

  • Adaptive Bitraten-Umschaltung (ABR) basierend auf Bandbreite und Puffer
  • Eine manuelle Qualitätsauswahl ist im Player-Code nicht zu finden
  • Die HLS-Dateien liegen im öffentlich lesbaren Bucket play-videos und werden über /s3/ ausgeliefert. Signierte URLs gibt es nicht, die Sichtbarkeit prüft die API beim Abruf der Videodaten

Speicher-Layout (MinIO)

play-raw/
  [videoId].[ext]           ← Original (bleibt erhalten, Trim nutzt es erneut)

play-videos/
  [videoId]/
    master.m3u8             ← HLS-Master-Playlist
    1080p/
      playlist.m3u8
      init.mp4
      seg-000.m4s
      seg-001.m4s
      ...
    720p/
      playlist.m3u8
      init.mp4
      seg-000.m4s
      ...
    480p/  ...

play-thumbs/
  [videoId]-cand-1.webp     ← Kandidaten bei 10/30/50/70/90 %
  ...
  [videoId]-cand-5.webp

play-assets/
  logo/<uuid>.png           ← Instanz-Logo
  captions/[videoId]/[lang].vtt