|
| 1 | +--- |
| 2 | +name: onboarding-videos |
| 3 | +description: Add, replace, and optimize the looping demo videos in the onboarding "welcome" bento grid (packages/ui/src/features/onboarding). Covers the ffmpeg compression workflow (the scripts/optimize-onboarding-videos.mjs wrapper), the faststart + right-size best practices that keep playback smooth, the first-frame poster convention, and how to wire a new clip into WelcomeScreen. Use when onboarding demo clips feel laggy, when a clip is being added/swapped/re-recorded, or when posters and videos drift out of sync. |
| 4 | +allowed-tools: Bash(node scripts/optimize-onboarding-videos.mjs:*), Bash(pnpm optimize:onboarding-videos:*), Bash(ffmpeg:*), Bash(ffprobe:*), Bash(npx biome check:*), Bash(pnpm --filter @posthog/ui typecheck:*) |
| 5 | +--- |
| 6 | + |
| 7 | +# Onboarding bento demo videos |
| 8 | + |
| 9 | +The first onboarding step (`WelcomeScreen`) shows a bento grid of feature cards. |
| 10 | +A card with a `media` entry plays a short, muted, looping screen recording; cards |
| 11 | +without one show a static placeholder. The featured (large, top-left) card is the |
| 12 | +only one that plays at a time — hover just moves the highlight. |
| 13 | + |
| 14 | +| Thing | Where | |
| 15 | +| --- | --- | |
| 16 | +| Clips + posters | `packages/ui/src/features/onboarding/assets/<feature>-<light\|dark>.{mp4,jpg}` | |
| 17 | +| Wiring (`MEDIA` map, `startTime`) | `packages/ui/src/features/onboarding/components/WelcomeScreen.tsx` | |
| 18 | +| `<video>` element + play/seek logic | `packages/ui/src/features/onboarding/components/FeatureBentoCard.tsx` | |
| 19 | +| Optimizer script | `scripts/optimize-onboarding-videos.mjs` (`pnpm optimize:onboarding-videos`) | |
| 20 | + |
| 21 | +## Why clips need optimizing (the "it feels laggy" fix) |
| 22 | + |
| 23 | +Raw screen recordings hitch in this UI for two reasons, both fixed by re-encoding: |
| 24 | + |
| 25 | +1. **`moov` atom at the end of the file.** Without faststart the player must read |
| 26 | + to EOF before it can start, so first-play and every seek stutter. We seek on |
| 27 | + mount and on every loop (`FeatureBentoCard` parks the clip on `videoStartTime`), |
| 28 | + so this bites constantly. Fix: `-movflags +faststart`. |
| 29 | +2. **Resolution far larger than it renders.** The featured slot is never wider than |
| 30 | + ~500 CSS px (the grid is `max-w-[760px]`). Even at 2× retina that's ~1000px, but |
| 31 | + recordings come in at ~1876px. Decoding huge frames and downscaling them — on |
| 32 | + every play and during the framer-motion slot reflow — is wasted work. Fix: cap |
| 33 | + width at **1000px** (which also matches the poster width exactly). |
| 34 | + |
| 35 | +Bitrate/size are usually already modest; **decode cost and startup are the lag**, |
| 36 | +not bytes. Don't chase file size at the expense of width/faststart. |
| 37 | + |
| 38 | +## Canonical encode target |
| 39 | + |
| 40 | +H.264 · `yuv420p` · **≤1000px wide** (keep aspect, even height) · CRF 23 · |
| 41 | +`+faststart` · no audio. These live as constants at the top of |
| 42 | +`scripts/optimize-onboarding-videos.mjs` — change them there, not ad-hoc. |
| 43 | + |
| 44 | +## Optimize existing clips |
| 45 | + |
| 46 | +```bash |
| 47 | +pnpm optimize:onboarding-videos # encode any clip not already optimized |
| 48 | +pnpm optimize:onboarding-videos --dry-run # show what would change |
| 49 | +pnpm optimize:onboarding-videos --force # re-encode all (after changing the target constants) |
| 50 | +``` |
| 51 | + |
| 52 | +Requires ffmpeg (`brew install ffmpeg`). The script tags each output with a |
| 53 | +`comment` marker, so re-runs skip already-optimized files — it's safe to run any |
| 54 | +time, including right after dropping in a new clip. It rewrites files in place; |
| 55 | +review the `git diff --stat` and the printed before/after sizes. |
| 56 | + |
| 57 | +## Add or replace a clip |
| 58 | + |
| 59 | +1. **Record** light + dark variants at **≥1000px wide**. The optimizer downscales to |
| 60 | + 1000px but never upscales, so anything narrower ships soft — resolution is the one |
| 61 | + thing it can't fix for you. Keep it short (~10–13s); it loops. |
| 62 | +2. **Name + drop** the files as `assets/<feature>-light.mp4` and |
| 63 | + `assets/<feature>-dark.mp4`. Two things bite every time: |
| 64 | + - Recordings almost always arrive with the **light variant unsuffixed** |
| 65 | + (`foo.mp4`, only `foo-dark.mp4` is tagged). Rename it to `foo-light.mp4`. |
| 66 | + - The source filename (e.g. whatever's in `~/Downloads`) is irrelevant — the |
| 67 | + `<feature>` prefix must be the **media id** you'll use in `WelcomeScreen` and |
| 68 | + follow the existing convention, not whatever the file was called. |
| 69 | + |
| 70 | + No `assets.d.ts` change needed — `*.mp4`/`*.jpg` are wildcard modules. |
| 71 | +3. **Optimize**: `pnpm optimize:onboarding-videos`. |
| 72 | +4. **Make the poster** — the still shown before play. **Use the clip's first frame** |
| 73 | + and leave `startTime` at 0, so the poster, the first played frame, and the loop |
| 74 | + point are all the same with nothing to keep in sync: |
| 75 | + |
| 76 | + ```bash |
| 77 | + ffmpeg -y -i assets/<feature>-<theme>.mp4 \ |
| 78 | + -frames:v 1 -vf "scale=1000:-2:flags=lanczos" -q:v 3 \ |
| 79 | + assets/<feature>-<theme>.jpg |
| 80 | + ``` |
| 81 | + |
| 82 | +5. **Wire it up** in `WelcomeScreen.tsx`. For a *new* media id, four edits, all keyed |
| 83 | + by the same slug: import the `.mp4` + `.jpg`, add the id to the `MediaId` union, |
| 84 | + add its entry to the `MEDIA` map (`startTime: 0`), and set `media: "<id>"` on the |
| 85 | + target `FeatureDef`. |
| 86 | + |
| 87 | +### Replacing an existing clip |
| 88 | + |
| 89 | +Keep the **same asset filename** and the wiring is untouched — none of step 5 |
| 90 | +applies. Drop the new file over `assets/<feature>-<theme>.mp4`, run the optimizer (a |
| 91 | +fresh drop carries no skip-marker, so it re-encodes without `--force`), and |
| 92 | +**regenerate that poster** (step 4). If you replace only one theme, re-check that |
| 93 | +light and dark still share an aspect ratio — a clip with stray padding frames |
| 94 | +differently from its sibling, and the gap shows when the user toggles theme. |
| 95 | + |
| 96 | +## Poster = first frame (and startTime = 0) |
| 97 | + |
| 98 | +`FeatureBentoCard` shows the poster while a card rests, then seeks the `<video>` to |
| 99 | +`MEDIA[id].startTime` on mount and loops back there (not necessarily to 0). The |
| 100 | +simple, default contract: **the poster is the clip's first frame and `startTime` is |
| 101 | +0**, so the still, the first played frame, and the loop point are all identical — |
| 102 | +nothing to keep in sync. Keep the poster the same pixel width as the clip (1000px) |
| 103 | +so the poster→video swap is seamless. |
| 104 | + |
| 105 | +`startTime` can start/loop mid-clip if you ever need it (the `code-review` clip uses |
| 106 | +3s) — but then the poster MUST be that exact frame (`ffmpeg -ss <startTime> -i …`), |
| 107 | +or the still jumps the instant playback starts. Prefer the first-frame default |
| 108 | +unless you have a specific reason. |
| 109 | + |
| 110 | +## Verify |
| 111 | + |
| 112 | +```bash |
| 113 | +# clips: moov should print BEFORE mdat, width ≤ 1000: |
| 114 | +ffprobe -v trace <clip>.mp4 2>&1 | grep -o -m2 -E "type:'(moov|mdat)'" |
| 115 | +ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 <clip>.mp4 |
| 116 | + |
| 117 | +# wiring: a grown MediaId union may need a formatter reflow, so let biome fix it: |
| 118 | +npx biome check --write packages/ui/src/features/onboarding/components/WelcomeScreen.tsx |
| 119 | +pnpm --filter @posthog/ui typecheck |
| 120 | +``` |
| 121 | + |
| 122 | +To confirm playback feels smooth in the real app, use the `test-electron-app` |
| 123 | +skill to drive the running onboarding flow. |
0 commit comments