FFmpeg → BullMQ → Cloudflare R2 → adaptive streaming, and the three things I would change now.

Dropping raw 4K MP4s onto a page is a great way to make a portfolio feel broken — multi-second loads, no quality adaptation, and a browser that chokes when several videos share a screen. A video editor's site failing to play video is the worst possible first impression.
The real requirement was adaptive streaming: serve the right quality for each viewer's connection, start fast, and let several players coexist without fighting over bandwidth or memory.
Upload bypasses the server, a queue does the heavy lifting, and the player just reads a playlist. Each stage is independent so a failure in one does not corrupt the others.
Large source files never touch the app server — the client uploads straight to R2 with a pre-signed URL, and only the completion event hits the backend. The server stays cheap and never streams gigabytes.
Transcoding is minutes of work; an HTTP request is the wrong place for it. BullMQ decouples “upload finished” from “encode done”, with retries and visibility into the job state.
Several HLS players on one page need to share state — only one plays at a time, others pause. A small React context coordinates them instead of each player reinventing its own controls.
I ran FFmpeg inside the main Hono process — fine at low traffic, but a heavy encode blocks the event loop. Next time: a dedicated worker service on a separate machine.
The upload UI shows a spinner until transcoding finishes. A progress bar streamed via server-sent events would be far better UX for long encodes — the data is already in BullMQ.
I built the pipeline from scratch to learn it end to end. For a production client deadline, Cloudflare Stream handles transcode + storage + CDN in one service — I would use it and spend the saved time on the UI.
“Building the pipeline took a week. Making it reliable took three more — and that ratio is the whole job.”— Parm, on AmarV4