This project ships short, looping videos in src/assets/. Workflow clips
are capped at 30 fps. MP4 outputs are opaque (yuv420p); WebM
workflow outputs keep alpha (yuva420p) where the source is
transparent. The hero background assets are opaque.
The high-quality sources live in videos/ (gitignored, large) and never
ship in the bundle:
| Source | Output assets | Preset |
|---|---|---|
videos/hero-bg.mp4 |
src/assets/hero-bg.mp4, src/assets/hero-bg.webm |
separate hero encode |
videos/research.webm |
src/assets/research.mp4, src/assets/research.webm |
900p, 30 fps |
videos/slack.webm |
src/assets/slack.mp4, src/assets/slack.webm |
900p, 30 fps |
The current workflow sources carry VP9 alpha_mode=1. The optimizer decodes
them with libvpx-vp9. MP4 is flattened onto a solid white canvas for
Safari and other H.264 fallbacks. WebM keeps the alpha plane for
browsers that pick the VP9 source first.
hero-bg.mp4— full-bleed hero background. Encoded with the most generous quality budget because blockiness is most visible here.research.mp4/.webm,slack.mp4/.webm— workflow demo panels. They sit inside a smaller container, so 900p remains sharper than the rendered desktop panel while keeping each shipped file under ~1 MB.
Whenever a source video changes, run the optimizer:
bun run videos:optimizeThe script:
- Reads source dimensions + duration with
ffprobe. - Scales each source to
-2:900and drops to 30 fps. - MP4: transparent sources are overlaid onto a 900-tall white canvas
(
-filter_complex→yuv420p). Opaque sources use a plain-vfchain. - WebM: transparent sources stay
yuva420p(no matting), with-auto-alt-ref 0for correct VP9 alpha. Opaque sources useyuv420p. - Encodes MP4 with libx264 (
-crf 27,-preset slow,-movflags +faststart,-pix_fmt yuv420p). - Encodes WebM with libvpx-vp9 (
-crf 35,-b:v 0,-row-mt 1,-cpu-used 4,-pix_fmt yuva420poryuv420pby source). - Writes everything to a temp directory first, prints a before/after table,
then copies only
research.mp4,research.webm,slack.mp4, andslack.webmintosrc/assets/after every encode succeeds.
By default the script runs ffmpeg through nix:
nix-shell -p ffmpeg --run '<ffmpeg command>'If you already have ffmpeg and ffprobe on PATH (e.g. via Homebrew),
skip the nix wrapper:
bun scripts/optimize-videos.ts --no-nixTo preview the commands without running them, add --dry-run.
Per-asset CRF values live in scripts/optimize-videos.ts in the jobs
array. The current preset was chosen after comparing 720p, 900p, 1080p,
CRF, and bitrate-targeted candidates. If a particular output is visibly soft
or blocky, lower its CRF by 1–2 and re-run the script. Approximate
sensitivity:
- −1 CRF → ~10–15 % larger file, modest quality bump.
- −3 CRF → noticeably crisper, ~30–50 % larger.
Hero quality should stay near the 700-800 KB target and is not replaced by
the workflow optimizer. If the source changes, sample CRF values around 15
and keep the result under that budget.
After re-encoding:
nix-shell -p ffmpeg --run 'for f in src/assets/{research,slack}.{mp4,webm}; do
echo "=== $f ===";
ffprobe -v error -select_streams v:0 \
-show_entries stream=codec_name,width,height,r_frame_rate,pix_fmt \
-show_entries format=duration,size,bit_rate \
-of default=noprint_wrappers=1 "$f";
done'Expect each workflow output to report r_frame_rate=30/1 and height=900.
MP4 should be pix_fmt=yuv420p; WebM from transparent sources should be
pix_fmt=yuva420p. There should be no audio stream.
Then:
bun run dev # eyeball hero + workflow panels in the browser
bun run build:prodConfirm the bundle table in build:prod lists each <name>-<hash>.mp4
at the new (smaller) size.
- Do not feed a transparent WebM into x264 directly — the alpha plane
becomes a solid black background. The script composites onto white only
for MP4. WebM keeps alpha (
yuva420p+-auto-alt-ref 0). - Do not drop the hero quality budget further to chase bytes. The hero is above the fold and visibly sensitive to blockiness.
- If
nix-shellcold-starts, the first invocation can spend a minute or two fetching theffmpegderivation. Subsequent runs are instant.