fix: route HLS video streams through /stream/hls proxy (#51)#57
Merged
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #51. The
/stream/hls/{path}proxy router was already built, tested, and registered inmain.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 tomaster.m3u8, which forced the HLS player to fetch variant playlists and.tssegments 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.MediaFile.s3_key_processedand returns/stream/hls/master.m3u8?token=….m3u8references to stay inside the proxy (same token) and rewrites.tsreferences to freshly-presigned S3 URLsvideo-player.tsxalready resolved relative/stream/hls/…pathsSecurity model
master.m3u8access.tsURL lifetimeSegments 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
test_assets_stream_url.py— video returns/stream/hls/…?token=…with a valid HLS JWT scoped tos3_key_processed;download=truestill returns a presigned raw URL; image/audio paths still presignedtest_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}test_hls_proxy.py— segment presign expectation bumped to 86400s58 passed, 1 warning/assets/{id}/stream→/stream/hls/master.m3u8?token=…→/stream/hls/0/playlist.m3u8?token=…→ 14 presigned.tssegment requests, all 200, videoreadyState: 4, buffered0 → 38.94s, zero console errors🤖 Generated with Claude Code