Video
Adaptive HLS video served from R2 through the colo cache, with sprite-sheet scrubbing.
Prepare
Needs ffmpeg/ffprobe (libx264, AAC) and cwebp.
bun run video:prepare --source /path/to/video.mov --id intro --version v1 [--captions subs.srt --lang en]
Writes .artifacts/videos/<id>/<version>/: master.m3u8, one fMP4 rendition per rung
(<height>/index.m3u8, init.mp4, NNNNN.m4s, 4 s segments, 2 s keyframes, no upscaling),
poster.jpg, optional captions/<lang>.vtt (converted from SRT or VTT), and scrub/ (256x144 tiles, 10x10 per WebP sheet, one tile per second, at most 1000 tiles).
Upload
bun run video:upload --directory .artifacts/videos/intro/v1 [--bucket gitfor] [--local]
Objects land in the BUCKET R2 binding under videos/<id>/<version>/. Media goes first and
master.m3u8 last; an already-published version is refused. Versions are immutable, so ship
changes as a new version.
Delivery
src/server/videos.ts serves /videos/<id>/<version>/<asset>, only for the prepared layout.
Responses are immutable and stored in the Workers cache on the first full fetch; later requests
(including Range and If-None-Match) are answered from cache without touching R2.
Player
<VideoPlayer id="intro" version="v1" title="Intro" />
src/components/VideoPlayer.tsx uses hls.js (lazy-loaded) or native HLS on Safari. Hovering the
timeline shows a sprite thumbnail; dragging fills the frame with sprite frames and seeks on release.