Problem
Multi-GB source videos take a long time to start and keep buffering during playback.
Today playback is a plain progressive download:
frontend/src/components/MediaPlayerDialog.tsx:123 renders <video src={mediaUrl}> against a presigned R2 URL. There is no adaptive streaming layer — the browser just range-requests one big MP4.
frontend/src/components/review/VideoPlayer.tsx / frontend/src/hooks/useVideoPlayer.ts do the same.
- The only transcode we do is a single fixed rendition:
vms/proxy.py → _ffmpeg_proxy() produces one 720p H.264 CRF-28 MP4 with +faststart.
So there is exactly one quality level. A viewer on a slow connection has no lower rung to fall back to, and a viewer on a fast connection can't get better than 720p. If the proxy isn't ready yet, the player falls back to the multi-GB original, which is the worst case.
What we want
Bunny.net-style playback: one upload → an ABR ladder of renditions → an HLS manifest the player switches between automatically based on bandwidth, plus instant seek and a quality selector.
Proposed scope
Backend (vms/proxy.py)
- Replace the single-output ffmpeg call with an ABR ladder, e.g. 240p / 360p / 480p / 720p / 1080p (skip rungs above the source resolution).
- Emit HLS: per-rendition
.m3u8 + segments and a master playlist, -hls_time 4 -hls_playlist_type vod, keyframe-aligned across rungs (-g/-force_key_frames) so switching is seamless.
- Upload the segment tree to R2 under something like
hls/<asset>/…; store the master playlist key on VMS Asset (new field alongside proxy_r2_key, e.g. hls_master_key) and keep proxy_status semantics.
- Preserve the existing enqueue/progress/realtime flow (
_publish_proxy_progress) — the ladder is longer, so per-rendition progress would help.
Delivery
- HLS segments need signed access. Either presign each segment, or serve the manifest through a whitelisted endpoint that rewrites segment URLs with presigned links (mind expiry vs. long videos).
Frontend
- Swap the raw
<video> for an HLS-capable player (hls.js, with native HLS on Safari/iOS) in MediaPlayerDialog.tsx and review/VideoPlayer.tsx, driven from useVideoPlayer.ts so both share one implementation.
- Auto quality by default + a manual quality selector.
- Fall back to the existing progressive proxy URL when no HLS manifest exists (older assets), and never fall back to the raw multi-GB original.
Open questions
- Do we transcode the ladder ourselves on the
long queue (ffmpeg CPU cost on the bench box, hours per GB-scale file), or push transcoding to a service (Bunny Stream / Cloudflare Stream / Mux) and just store the playback ID?
- Backfill: re-encode existing assets, or generate HLS lazily on first play?
- Storage cost of keeping original + ladder in R2.
Problem
Multi-GB source videos take a long time to start and keep buffering during playback.
Today playback is a plain progressive download:
frontend/src/components/MediaPlayerDialog.tsx:123renders<video src={mediaUrl}>against a presigned R2 URL. There is no adaptive streaming layer — the browser just range-requests one big MP4.frontend/src/components/review/VideoPlayer.tsx/frontend/src/hooks/useVideoPlayer.tsdo the same.vms/proxy.py→_ffmpeg_proxy()produces one 720p H.264 CRF-28 MP4 with+faststart.So there is exactly one quality level. A viewer on a slow connection has no lower rung to fall back to, and a viewer on a fast connection can't get better than 720p. If the proxy isn't ready yet, the player falls back to the multi-GB original, which is the worst case.
What we want
Bunny.net-style playback: one upload → an ABR ladder of renditions → an HLS manifest the player switches between automatically based on bandwidth, plus instant seek and a quality selector.
Proposed scope
Backend (
vms/proxy.py).m3u8+ segments and a master playlist,-hls_time 4 -hls_playlist_type vod, keyframe-aligned across rungs (-g/-force_key_frames) so switching is seamless.hls/<asset>/…; store the master playlist key onVMS Asset(new field alongsideproxy_r2_key, e.g.hls_master_key) and keepproxy_statussemantics._publish_proxy_progress) — the ladder is longer, so per-rendition progress would help.Delivery
Frontend
<video>for an HLS-capable player (hls.js, with native HLS on Safari/iOS) inMediaPlayerDialog.tsxandreview/VideoPlayer.tsx, driven fromuseVideoPlayer.tsso both share one implementation.Open questions
longqueue (ffmpeg CPU cost on the bench box, hours per GB-scale file), or push transcoding to a service (Bunny Stream / Cloudflare Stream / Mux) and just store the playback ID?