Portfolio-grade mini OTT stack: FFmpeg HLS packaging with AES-128 segment encryption (key delivery like a simplified Widevine/FairPlay-style flow), a FastAPI manifest and metadata API backed by PostgreSQL, AWS S3 storage with optional CloudFront signed URLs, and a React + Shaka Player dark UI for adaptive playback.
┌─────────────┐ ┌──────────────────┐ ┌─────────────┐ ┌────────────┐
│ FFmpeg │────▶│ S3 (segments + │◀────│ CloudFront │◀────│ Viewers │
│ transcode │ │ manifests) │ │ (optional) │ │ (browser) │
└─────────────┘ └────────▲─────────┘ └──────▲──────┘ └─────▲──────┘
│ │ │ │
│ segment_uploader.py │ signed URLs │
│ │ │ (optional) │
▼ │ │ │
┌──────────────────────────────────────────────────────────────────────────────┐
│ FastAPI (manifest / content / drm) │
│ GET /manifest/{id} → master.m3u8 (+ URL rewrite / CF signing) │
│ GET /hls/{id}/* → proxy segments/playlists from S3 (local-style CDN) │
│ GET /drm/key/{id} → 16-byte AES key (application/octet-stream) │
│ GET /content → catalog + renditions │
└──────────────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────┐
│ PostgreSQL │
└──────────────┘
| Layer | Technology |
|---|---|
| Transcoding | FFmpeg, HLS, AES-128 |
| API | FastAPI, asyncpg, Pydantic, boto3 |
| Data | PostgreSQL 15 |
| Frontend | React, TypeScript, Vite, Axios, Shaka Player |
| Delivery | AWS S3, CloudFront (optional signed URLs) |
| Orchestration | Docker Compose |
-
Environment — copy variables (optional but recommended for AWS):
cp .env.example .env # Edit .env: S3_BUCKET_NAME, AWS keys, etc.Docker Compose reads
.envautomatically for variable substitution. -
Run the stack
docker compose up --build
- API: http://localhost:8000 (
GET /health) - UI: http://localhost:3000
- API: http://localhost:8000 (
-
Configure AWS — set
S3_BUCKET_NAMEand credentials so the API can read manifests and segments. Without S3, the catalog may load but playback will fail until objects exist.
Requires FFmpeg and OpenSSL on your machine (not only Docker). The FFmpeg graph expects an audio track on the input (stereo AAC per variant); silent inputs need a different filter graph.
-
Pick a stable content UUID (must match the database row you will create):
export CONTENT_ID="$(uuidgen | tr '[:upper:]' '[:lower:]')" mkdir -p ./media/out chmod +x ingestion/transcode.sh
-
Transcode (three ABR rungs, 6s segments, encrypted HLS):
./ingestion/transcode.sh ./your_video.mp4 ./media/out "$CONTENT_ID"- Writes
master.m3u8, variant playlists,.tssegments,enc.key, andstreamvault_meta.json. enc.keyinfousesBACKEND_URL(defaulthttp://localhost:8000) for the key URI embedded in playlists.
- Writes
-
Python uploader (install deps on the host or in a venv):
cd ingestion && pip install -r requirements.txt export AWS_REGION=ca-central-1 export S3_BUCKET_NAME=your-bucket export DATABASE_URL=postgresql://postgres:password@localhost:5432/streamvault python segment_uploader.py ../media/out --title "My trailer"
This uploads everything under
s3://$S3_BUCKET_NAME/$CONTENT_ID/and upsertscontent,rendition, anddrm_keyrows. -
Open the app, select the title, and play. Shaka resolves
EXT-X-KEYagainst/drm/key/{id}; a request filter also forces the key URL to matchVITE_BACKEND_URL/BACKEND_URLwhen needed.
When CLOUDFRONT_DOMAIN, CLOUDFRONT_KEY_PAIR_ID, and CLOUDFRONT_PRIVATE_KEY_PATH are set, /manifest/{id} rewrites media URLs to signed CloudFront URLs instead of /hls/... proxy paths. See infra/README.md and infra/s3_setup.py.
ingestion/—transcode.sh,segment_uploader.pybackend/— FastAPI appfrontend/— React + Shaka Playerdb/init.sql— schemainfra/— S3 / CloudFront helpers