Skip to content

fix: route HLS video streams through /stream/hls proxy (#51)#57

Merged
ravirajsinh45 merged 1 commit into
mainfrom
fix/hls-proxy-routing-51
Apr 14, 2026
Merged

fix: route HLS video streams through /stream/hls proxy (#51)#57
ravirajsinh45 merged 1 commit into
mainfrom
fix/hls-proxy-routing-51

Conversation

@ravirajsinh45

Copy link
Copy Markdown
Contributor

Summary

Fixes #51. The /stream/hls/{path} proxy router was already built, tested, and registered in main.py — it just wasn't wired up. The three stream endpoints (get_stream_url, validate_share_link, get_share_stream_url) all handed out a direct presigned URL to master.m3u8, which forced the HLS player to fetch variant playlists and .ts segments as unsigned requests. That only works on buckets with public-read, which non-AWS providers (Exoscale SOS, Cloudflare R2, etc.) don't grant by default to new objects — hence the 403s in the reporter's setup.

  • Each stream endpoint now mints a short-lived HLS JWT scoped to MediaFile.s3_key_processed and returns /stream/hls/master.m3u8?token=…
  • The proxy rewrites variant .m3u8 references to stay inside the proxy (same token) and rewrites .ts references to freshly-presigned S3 URLs
  • Bucket can stay fully private on every S3-compatible provider — no ACL, no public bucket policy
  • Captured URLs expire in 24h instead of living forever via public-read
  • Zero frontend changesvideo-player.tsx already resolved relative /stream/hls/… paths
  • Bumped HLS token + segment presign TTLs from 4h → 24h so pause-and-resume works without refresh logic

Security model

Before After
Bucket ACL Had to be public-read Fully private
master.m3u8 access Indefinite presigned S3 link Token-scoped via API (24h)
Captured .ts URL lifetime Forever ≤24h
Works on Exoscale/R2/MinIO/AWS No (non-AWS 403) Yes, identical on all

Segments still stream directly from S3 via presigned URLs (industry standard, matches Frame.io / Vimeo). Full tunneling through the API would cost 2x egress and add Range-request complexity — deferred as a follow-up if a specific threat model needs it.

Test plan

  • New test_assets_stream_url.py — video returns /stream/hls/…?token=… with a valid HLS JWT scoped to s3_key_processed; download=true still returns a presigned raw URL; image/audio paths still presigned
  • Updated test_share_video_stream.py — the share/{token} endpoint returns folder path instead of master.m3u8 for video stream URLs #45 regression test now expects the proxy URL, plus a new test for /share/{token}/stream/{asset_id}
  • Updated test_hls_proxy.py — segment presign expectation bumped to 86400s
  • Full backend suite: 58 passed, 1 warning
  • Browser end-to-end via chrome-devtools-mcp against MinIO: /assets/{id}/stream/stream/hls/master.m3u8?token=…/stream/hls/0/playlist.m3u8?token=… → 14 presigned .ts segment requests, all 200, video readyState: 4, buffered 0 → 38.94s, zero console errors

🤖 Generated with Claude Code

…ivate (#51)

The /stream/hls/{path} proxy router was already built, tested, and
registered in main.py, but it was never actually called — the three
stream endpoints (assets.get_stream_url, share.validate_share_link,
share.get_share_stream_url) all handed out a direct presigned URL to
master.m3u8. That forced the HLS player to fetch variant playlists and
.ts segments as unsigned requests, which only works if the bucket is
public-read. Non-AWS providers (Exoscale SOS, R2, etc.) don't inherit
bucket-level ACL on new objects, so processed files 403'd.

The three stream endpoints now mint a short-lived HLS JWT scoped to the
asset's S3 prefix and return /stream/hls/master.m3u8?token=…. The proxy
rewrites variant playlists to stay inside the proxy (same token) and
rewrites .ts references to freshly-presigned S3 URLs. Result: bucket
can stay fully private on every S3-compatible provider, a leaked URL
expires in 24h instead of never, and zero frontend changes required —
video-player.tsx already resolved relative /stream/hls/ paths.

Bumped the HLS token and segment presign TTLs from 4h to 24h so
pause-and-resume works without refresh logic.

Tests:
- apps/api/tests/test_assets_stream_url.py (new) — asserts
  get_stream_url returns /stream/hls/master.m3u8?token=… with a valid
  HLS JWT whose pfx matches s3_key_processed; download=true still
  returns a presigned raw file; image/audio paths still presigned.
- test_share_video_stream.py — updated the #45 regression test to
  expect the proxy URL, added a new test for the /share/{token}/
  stream/{asset_id} endpoint.
- test_hls_proxy.py — updated segment presign expectation to 86400.

Verified end-to-end in the browser via chrome-devtools-mcp against
MinIO: /assets/{id}/stream → /stream/hls/master.m3u8?token=… →
/stream/hls/0/playlist.m3u8?token=… → 14 presigned .ts segment
requests, all 200, video readyState 4, buffered 0→38.94s, zero
console errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ravirajsinh45 ravirajsinh45 merged commit ad3907a into main Apr 14, 2026
4 checks passed
@ravirajsinh45 ravirajsinh45 deleted the fix/hls-proxy-routing-51 branch April 14, 2026 05:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Processed files return 403 - Worker does not set public-read ACL on S3 objects

1 participant