Turns driving an app into a captioned MP4: screenrecord captures the screen,
you stamp an explanation card at each beat while it happens, and ffmpeg burns
the cards in afterwards. scripts/walkthrough.py runs the three phases.
The constraint that shapes everything: you cannot inject an overlay into a
third-party Android app the way a web recorder injects DOM into a page. So cards
are composited post-hoc against a timeline captured live — which is why
mark exists instead of a scenario file with guessed timings.
When NOT to use
- A still screenshot, or tap/read/assert →
android-ui-driver(this skill drives nothing; you drive with that one while recording). - A written review with evidence →
android-app-review. - Getting the app onto the device →
play-store-app-install. - Recording a web app → that is a browser-recorder job, not adb.
- Generic video editing or format conversion → plain ffmpeg.
Prerequisites
adb, plus ffmpeg and Pillow (pip install pillow) for rendering — both
only needed at render time. Cards are drawn as PNGs and composited with ffmpeg's
overlay, deliberately not drawtext: a stock brew install ffmpeg has no
freetype, so drawtext is simply absent on many machines. overlay is universal.
Workflow
Run as python3 "${CLAUDE_SKILL_DIR}/scripts/walkthrough.py" --out DIR <cmd>.
-
Plan the beats first. 3–6 steps, each one sentence. Write the ending first: what the viewer should be able to do. Keep titles 3–8 words and bodies 8–20 words — a card that needs two lines of body is two beats.
-
Rehearse, then start. Walk the flow once with
android-ui-driverso you know the taps work and, ideally, their coordinates. Then:walkthrough.py --out ./demo start --theme glassglass(default) is a minimal frosted overlay: a translucent panel the app shows through, a hairline border, no accent stripe.coral/indigo/paperare solid-panel alternatives.Enables Android's touch indicators (
show_touches) so taps are visible — the only "cursor" available — and recordst0from when the file actually starts growing, not when the command returned. -
Drive and mark. Use
android-ui-driverfor the actions. At each beat, in the moment:walkthrough.py --out ./demo mark "Browse by category" \ --kicker "Step 2" --body "Each row is a category carousel." --duration 4--style herodims the frame for an opening/closing title;--pos top|center|bottommoves a normal card off whatever it would cover. A--bodymay carry newlines to structure it into short rows (e.g.● 2 dex · 4 ABIs), and a kicker that starts with/(a skill name like/android-ux-audit) keeps its case instead of being upper-cased. -
Stop and render.
walkthrough.py --out ./demo stop --name app-tour --gifSends SIGINT to
screenrecord, waits for the MP4 to finalise, pulls it, restoresshow_touches, then composites every card at its recorded timestamp.Rendering tightens the take by default: stretches with no card on screen are compressed (4x), while every card window plus a beat either side stays at 1.0x. A typical take loses 25–30% of its length — all of it hierarchy-dump dead air — without touching the pace of the actual interactions.
--no-tightenkeeps the raw timing,--gap-speed Ntunes it,--srtalso writes a sidecar caption track. -
Verify before claiming success. Read the
mp4 check:line, then look at a frame inside a card's window:ffmpeg -y -ss 15 -i demo/app-tour.mp4 -frames:v 1 /tmp/f.pngConfirm the card is readable, inside the frame, and not covering the thing it describes.
-
Iterate without re-recording. Card text wrong? Edit
demo/session.jsonand re-render — the footage and timings are already captured:walkthrough.py --out ./demo render --name app-tour
Output spec
<out>/<name>.mp4— H.264, CRF 20, yuv420p, faststart, constant frame rate.<out>/<name>.gifwith--gif(two-pass palette, 480 px, 12 fps).<out>/session.json— the timeline; the editable source of truth for re-renders.<out>/raw.mp4and<out>/cards/*.png— intermediates, kept for iteration.- A verification note: the probe line plus which frame you actually looked at.
Gotchas
screenrecordis variable frame rate. It only encodes frames when the screen changes, so a static stretch (an idle feed, an empty screen) has almost no frames — andoverlaycomposites a card only onto frames that exist, so a card whose window lands on a static stretch would silently never appear. The renderer normalises the take to CFR (--fps, default 30) before compositing so every card window has frames; if playback still stutters, re-render — do not re-record.- Driving with
--textinserts ~2 s of dead air per action, because resolving a selector needs a hierarchy dump. Default tightening removes most of it automatically. For the tightest takes, collect coordinates withsnapduring the rehearsal and drive withtap --at X,Y. Prefer tightening over--speed: a global speed-up distorts the touch animations and screen transitions too, which is exactly what a viewer reads as "fake". - Cards are drawn over live footage — there is no separate title slide. A
herodims the whole frame and centres its text, so it reads as a title card; a plaincardis a corner panel that must not cover the control it describes. Still put a hero on a quiet moment: the footage keeps playing underneath. - The 3-minute default limit is removed here (
--time-limit 0), but long takes make big files and slow renders. Keep tours 30–90 s. - Rotating the device mid-recording ends the recording. Set the orientation
before
start. FLAG_SECUREscreens record as black, exactly as they screenshot black. There is no adb-only way around it — plan the route to avoid them.- Recording captures whatever is on screen, including notifications and personal data. Use a disposable emulator; the script refuses to auto-select a physical device for this reason.
- No audio.
screenrecorddoes not capture it; add narration in an editor. - A
stopthat finds no file meansscreenrecorddied early — usually an unsupported recording size. Retry withstart --size 720x1600.
Files
scripts/walkthrough.py— start / mark / stop / render, plus--self-test(offline card-rendering checks; no device, adb or ffmpeg needed).references/card-craft.md— beat structure, card copy limits, theming, pacing, and the export budgets for README/socials.