Auch verfügbar in: DE Deutsch

Video Pipeline

Inhalt

After upload, videos go through a single BullMQ job named transcode in the worker (apps/worker). The five stages below are steps inside that job, not separate jobs, and in the code the thumbnails are extracted after transcoding. The frontend receives no push updates, the Studio polls the status every two seconds.


Pipeline Overview

[1] Intake → [2] Probe → [3] Thumbnails → [4] Transcode → [5] Finalize
Stage Where DB status during
1 — Intake API route POST /api/upload PENDING
2 — Probe job transcode PROCESSING
3 — Thumbnails job transcode PROCESSING
4 — Transcode job transcode PROCESSING
5 — Finalize job transcode LIVE (on error FAILED)

Stage 1: Intake

The browser sends the file in a single request via XMLHttpRequest to POST /api/upload, and the API streams the body straight into MinIO. Resumable uploads (tus.io) are not implemented yet, an interrupted upload starts over.

  • The browser measures upload progress itself via xhr.upload.onprogress
  • Beforehand the API compares the Content-Length header with MAX_UPLOAD_SIZE_MB (default 8192) and answers 413 if it is exceeded
  • The file is checked via magic bytes and stored as play-raw/[videoId].[ext]
  • The API then creates the record with status PENDING and visibility PRIVATE and enqueues the transcode job
Client → POST /api/upload → API → MinIO play-raw/[videoId].[ext]
                                       ↓
                               BullMQ: transcode job enqueued

Stage 2: Probe

ffprobe runs against the uploaded file to extract technical metadata.

Extracted fields:

Field Description
Duration Total length in seconds
Resolution Width × height in pixels
Video codec e.g. h264, hevc, vp9
Bitrate Total bitrate in kbps

The extracted data is written to the video record when the job finishes. The source height decides which HLS levels are rendered.


Stage 3: Thumbnails

FFmpeg extracts 5 candidate frames evenly distributed across the video duration.

  • Frames are taken at 10, 30, 50, 70 and 90 % of the duration, 1280 px wide, and saved as WebP to play-thumbs/[videoId]-cand-1.webp … [videoId]-cand-5.webp
  • The creator can pick the preferred thumbnail in the Studio editor (/studio/[id]/edit)
  • If no selection is made, the middle candidate (50 %) is the default

Stage 4: Transcode

FFmpeg converts the source file to HLS (HTTP Live Streaming) with multiple quality variants.

Quality Variants

Resolution Video bitrate Audio bitrate Condition
1080p max. 5 Mbps 128 kbps Source height ≥ 1080
720p max. 2.8 Mbps 128 kbps Source height ≥ 720
480p max. 1.2 Mbps 128 kbps Always, even for smaller sources

Variants above the source resolution are skipped. A 480p source only produces 480p. The ladder is fixed in the worker. TRANSCODE_QUALITIES appears in .env.example and the Unraid template but is not read by the code. With TRANSCODE_EXTRA_CODECS=av1 and/or vp9 the worker additionally renders AV1 (libsvtav1) or VP9 (libvpx-vp9) per level at about 30 % lower bitrate. This costs a lot more CPU, the default is empty.

HLS Output Format

  • Segment container: fMP4 (fragmented MP4)
  • Segment duration: 4 seconds
  • Video codec: H.264 (libx264), preset medium, CRF 22
  • Audio codec: AAC, 128 kbps
  • Master playlist: master.m3u8 — lists all quality variants with bandwidth hints
  • Per-quality playlists: [quality]/playlist.m3u8 with init.mp4 and seg-NNN.m4s
  • Output location: MinIO play-videos/[videoId]/

Transcode Progress

The job reports coarse progress steps to BullMQ (10, 20, up to 80 across the variants, then 85, 95 and 100 %). The Studio shows no percentage, it polls the status every two seconds while a video is PENDING or PROCESSING.


Stage 5: Finalize

Once all HLS segments are written to MinIO:

  1. The video record is set to LIVE, together with duration, resolution, codec, bitrate, thumbnails and format (SHORT for portrait videos up to 60 seconds, otherwise LONG)
  2. If AUTO_CAPTIONS_ENABLED=1, the caption job is enqueued
  3. If the video is public, subscribers get an in-app notification
  4. The video shows up in Studio on the next poll and in public feeds only if its visibility is PUBLIC

External Import (yt-dlp)

Creators can import videos from external platforms by providing a URL.

Workflow:

  1. Creator submits a URL in Studio → /studio/import
  2. Worker runs yt-dlp to download the video and extract metadata
  3. Title and description from yt-dlp are copied into the record
  4. Default visibility: private — the creator reviews and publishes manually
  5. The downloaded file goes through the normal transcode job

Supported sources: any platform supported by yt-dlp (YouTube, Vimeo, and hundreds of others).


Auto-Captions (Whisper) — behind a feature flag

Whisper.cpp integration for automatic speech-to-text transcription.

  • Activation: AUTO_CAPTIONS_ENABLED=1, off by default
  • Model: set via WHISPER_MODEL, default /models/ggml-small.bin
  • Language: the transcode job passes de as a fixed value, automatic detection is not wired up
  • Output format: WebVTT track stored in play-assets under captions/[videoId]/
  • Scope: according to the code only the first six HLS segments of the first variant are transcribed, with 4-second segments roughly the first 24 seconds
  • Transcription runs as a separate BullMQ job (caption-generate, concurrency 1) after Stage 5, so it does not block playback availability

Playback

Player: HLS.js (client-side, no plugin required)

  • Adaptive bitrate switching: HLS.js monitors bandwidth and switches quality automatically
  • Manual quality selection: not found in the player code
  • No signed URLs: HLS files sit in the public-read bucket play-videos and are served via /s3/. Visibility is checked by the API when the video data is fetched
  • CORS: the repo contains no MinIO CORS configuration. In the all-in-one container everything is loaded same-origin via /s3/

Storage Layout

All persistent video data lives in MinIO. The bucket structure:

play-raw/
  [videoId].[ext]            ← source file, retained post-transcode (trim reuses it)

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

play-thumbs/
  [videoId]-cand-1.webp      ← 5 candidate thumbnails
  ...
  [videoId]-cand-5.webp

play-assets/
  logo/<uuid>.png            ← Instance logo (set in admin)
  captions/[videoId]/[lang].vtt