A small multi-tenant VOD service in Go. You upload an MP4 over gRPC, it gets transcoded into HLS with three bitrate ladders (1080p / 720p / 480p), and a playback HTTP service hands out signed manifests and segments. Postgres holds the catalog, Redis caches manifests, and a tiny hls.js page plays it back.
Two services, one repo:
- ingest — gRPC. Takes an upload, runs FFmpeg, writes HLS output, updates status.
- playback — HTTP. Serves master/variant playlists and
.tssegments. Rewrites segment URLs with an HMAC signature and expiry, so only signed requests can fetch bytes. Tenant isolation enforced in middleware.
Storage is behind an interface — today it's disk, swap in S3 later without touching callers.
- Docker + Docker Compose
grpcurl(for manually kicking off an upload):go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest- An MP4 file lying around somewhere
Bring everything up (postgres, redis, ingest, playback):
make up-all
make migrateThat seeds two tenants: demo and acme.
Quick health check:
curl -sf http://localhost:9005/healthz && echo OKThe ingest service reads the source file from its own filesystem, so copy your MP4 into the ingest container first:
docker compose cp ./myvideo.mp4 ingest:/tmp/myvideo.mp4Then kick off the upload:
grpcurl -plaintext \
-import-path proto -proto ingest.proto \
-d '{"tenant_slug":"demo","title":"my video","source_path":"/tmp/myvideo.mp4"}' \
localhost:9090 ingest.v1.Ingest/UploadYou'll get back a videoId. Poll status until it's ready:
grpcurl -plaintext \
-import-path proto -proto ingest.proto \
-d '{"video_id":1}' \
localhost:9090 ingest.v1.Ingest/GetStatusIf something goes wrong, docker compose logs -f ingest shows the FFmpeg output.
Open the demo player:
make webGo to http://localhost:9000, leave tenant as demo, punch in your video ID, hit Load. You should see the player pick up 480p/720p/1080p automatically.
Or hit the manifest directly:
curl -s http://localhost:9005/tenants/demo/videos/1/master.m3u8Signed segments — fetching a .ts without a signature should be forbidden:
curl -i http://localhost:9005/tenants/demo/videos/1/720p/seg_00000.ts
# 403Pull a signed URL out of the variant playlist and it works:
curl -s http://localhost:9005/tenants/demo/videos/1/720p/playlist.m3u8
# copy a seg_*.ts?exp=...&sig=... line and curl it — 200Tenant isolation — unknown tenants are 404:
curl -i http://localhost:9005/tenants/nope/videos/1/master.m3u8Redis cache — after hitting the master manifest a couple of times:
docker compose exec redis redis-cli KEYS 'manifest:*'go test -race ./...The playback handler tests cover tenant resolution, signing, expiry, path tampering, and cross-tenant isolation — no postgres or redis required (fakes in-memory).
docker compose down -vThe -v wipes the media volume too, which you'll want if you're changing ingest permissions.