← Back to work

EVENT-DRIVEN / MEDIA

StreamHub — Distributed Video Pipeline

An event-driven video platform: presigned direct-to-storage uploads, BullMQ + ffmpeg HLS adaptive-bitrate transcoding, and a custom hls.js player — without the API ever buffering video bytes.

FastifyReactTypeScriptBullMQRedisPostgreSQLPrismaffmpegMinIO / S3Dockerhls.js

Overview

StreamHub decouples ingest from CPU-heavy transcoding the way production video platforms do: the client PUTs directly to MinIO/S3 via a presigned URL, the API only enqueues work, a separate transcoder service builds an HLS rendition ladder (1080p→360p) plus thumbnail, and status events flow back over a second BullMQ queue. Services scale independently and never call each other directly.

The Problem

Most upload demos stream video through the API and call ffmpeg inline — concurrent uploads pin the CPU the HTTP server needs. Production systems need direct-to-storage ingest, async retryable transcoding, and ownership-safe streaming without exposing raw bucket URLs.

Architecture

React Clienthls.js player
Upload APIFastify + JWT
MinIO / S3Presigned PUT
BullMQvideo.queue
Transcoderffmpeg HLS ladder
event.queuePROCESSED / FAILED
PostgresStatus + ownership

Key Decisions

Presigned uploads bypass the API

The client gets a presigned S3/MinIO URL and PUTs the file straight to object storage — the upload service never buffers video bytes.

Queue-isolated transcoder service

Transcoding is a BullMQ consumer with exponential backoff. The transcoder does not know the API exists; communication is only via queues and object storage.

HLS ladder + ownership-proxied playback

ffmpeg builds adaptive-bitrate HLS; the API proxies master.m3u8 and segments after ownership checks so raw bucket URLs never reach the browser.

Dual-queue status pipeline

Transcoder emits completion/failure on event.queue; an event.worker inside the upload service flips video status in Postgres — retries are independent of the HTTP path.

Failure Modes

Transcode burst under concurrent uploads

Jobs queue in Redis instead of taking down the API; workers scale to N replicas independently.

Transient ffmpeg / storage failure

BullMQ exponential backoff retries both the transcode and status-update queues.

Unauthorized stream access

Every stream/delete route enforces per-user ownership; cascading delete removes raw + all renditions from S3.

Token theft via localStorage

15-minute access + rotating 30-day refresh tokens live in httpOnly SameSite=strict cookies with bcrypt-hashed passwords.

Implementation

  • Presigned direct-to-storage uploads with live progress
  • BullMQ video.queue + event.queue with exponential backoff
  • ffmpeg/ffprobe HLS ladder (up to 1080p) and mid-clip thumbnails
  • Custom hls.js player with Auto ABR + manual quality override
  • JWT auth with rotating refresh cookies and Prisma/Postgres models
  • HLS proxy from object storage with ownership checks
  • Full Docker Compose stack: MinIO, Postgres, Redis, both services, nginx client

Lessons

  • Never put ffmpeg on the request path — ingest and transcoding must fail and scale independently.
  • Presigned uploads turn the API into a control plane instead of a bandwidth bottleneck.
  • Proxying HLS behind ownership checks keeps private media private without signed cookie gymnastics in the player.

Links