Full corrected diagram: Miro board (link)
The PRD has some intentional ambiguity. Here's how I filled the gaps:
- Profanity filter censors, not just detects. The PRD says "censor" and the audience is children, so the system extracts audio, runs speech-to-text, and mutes flagged segments before transcoding. If profanity density is too high, the video is rejected.
- Preview-first. To hit the ≤15s target, the 360p version is made playable as soon as it's done. Higher qualities keep processing in the background.
- Captions are static. If a
.vttfile is provided, it's stored and served as-is. No processing. - Single S3 bucket with structured paths:
videos/{video_id}/{360|720|1080}p.mp4, etc.
- File goes through the BFF. For files up to 1 GB with 500 concurrent uploads, this kills the server. It becomes a memory and bandwidth bottleneck.
- 201 response returns "thumbnail" — but processing hasn't started yet. No thumbnail exists.
- No retry or cancel mechanism. A failed upload means starting from zero.
- Everything is synchronous and sequential. No queue, no parallelism, no way to scale. Can't meet the ≤15s preview target.
- Missing: 1080p transcode, poster image, thumbnail sprite, metadata extraction, profanity filter, captions handling, status tracking. That's most of the PRD.
- "After video is completely downloaded, user can playback." This is the biggest issue. A 10-min 1080p video could be hundreds of MB. The user waits for a full download before seeing anything. Violates the ≤2.0s start time target.
- Query-param URLs (
?res=360) are not cache-friendly. The PRD asks for immutable, versioned URLs. - 600s CDN cache conflicts with the ≤60s deletion requirement.
- No manifest. The PRD explicitly asks for one.
- No database anywhere. No message queue.
- Client validates locally (file type, size).
- Client requests a pre-signed URL from the BFF.
- BFF generates a
video_id, creates a DB record (status: pending_upload), returns the URL + ID. - Client uploads directly to S3 via multipart upload — chunk retry, progress events, cancel via
AbortMultipartUpload. No file data touches the BFF. - Client calls
POST /videos/{video_id}/upload-complete. Backend updates status and enqueues the first processing job.
FastAPI backend with Celery (Redis as broker).
Sequential (each step depends on the previous):
- Virus scan — rejects infected/corrupted files.
- Metadata extraction — gets duration, dimensions, codec via FFmpeg. Determines which transcodes to produce.
- Profanity filter — speech-to-text → analysis → generates clean audio track. Must run before transcoding so all versions use the clean audio.
Parallel (fan-out after profanity filter):
- Transcode 360p / 720p / 1080p
- Poster image (single frame via FFmpeg)
- Thumbnail sprite (contact sheet + VTT for timestamp mapping)
When everything finishes, the callback generates a manifest.json, saves it to S3, and marks the video as ready.
All outputs use FFmpeg with -movflags +faststart so the MP4 moov atom is at the beginning, enabling immediate playback via range requests.
Why Celery over Lambda: Video transcoding is CPU-intensive and can run for several minutes. Lambda's 15-minute hard timeout and limited compute make it risky for the ≤10min P95 target. Celery workers on properly sized instances give full control over resources, and chain + chord model the pipeline naturally.
Status flow: pending_upload → scanning → extracting_metadata → filtering → processing → preview_ready → ready (or failed / rejected).
The playback path is fully static — no backend involved.
- Client polls
GET /videos/{video_id}/statusvia BFF untilpreview_readyorready. - Client fetches
manifest.jsonfrom CDN (→ S3 on cache miss). - Player renders poster, shows quality selector, defaults to 720p.
- HTML5
<video>makes range requests to the CDN. Browser fetches only what it needs to start decoding — no full download.faststartmakes this work. - Quality switch: save
currentTime, swapsrc, seek back. PRD says reloading is acceptable. - Seek preview: sprite sheet + VTT, fully client-side.
- Captions:
<track>element pointing to the VTT on CDN.
Cache: Immutable URLs → long TTL (days/weeks). Invalidation only on delete.
- Client calls
DELETE /videos/{video_id}via BFF → FastAPI. - Backend does three things: marks
deletedin DB, deletesmanifest.jsonfrom S3, purges CDN cache for the manifest URL. - Without a valid manifest, the player can't resolve any URLs — playback fails immediately.
Only the manifest needs purging. Video files expire naturally from CDN cache via TTL. Actual S3 cleanup happens async via a background job.
- S3 Event Notifications to trigger processing automatically on upload, removing the need for the client
upload-completecallback. - Observability — job duration per pipeline stage, failure rates, queue depth, playback start time, rebuffer rate.
- SSE or webhooks for real-time status updates instead of polling.