# Video Bug Analysis

> Extract frames from a screen recording or video and reason over them — to diagnose a UI/rendering bug (glitch, flicker, crash, freeze, wrong render) OR to read and inventory on-screen text/UI (catalog a site's features, transcribe a demo, describe what's shown). Use whenever the user shares a video or screen recording (.mov/.mp4/.webm) and wants it analyzed or read, especially with an approximate timestamp.

- Skill: `cportka/video-bug-analysis` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add cportka/video-bug-analysis`
- Raw SKILL.md: https://api.skillmd.com/api/skills/cportka/video-bug-analysis/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: cportka (https://skillmd.com/u/cportka)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/cportka/video-bug-analysis

---


# Video Bug Analysis

You can't watch video — only **still frames** `ffmpeg` extracts. Reconstruct what happened
from those snapshots plus what the user tells you. Despite the name, this also handles
**non-bug "read the screen" tasks** — inventorying a site's features, transcribing a demo,
or describing on-screen UI from a recording. Work this way:

> **Reading dense text/UI (not a bug)?** Skip the contact sheet — extract **full-resolution
> individual frames** (`--fps 1` to `2`, no `--contact`) and read them one by one. Contact
> tiles downscale too far for small text, especially on tall **portrait** phone captures
> (where the script auto-drops to `--cols 2`, but individual frames still read best).

## 1. Get context first

Ask for whatever's missing:

- **When** the bug appears (a timestamp/range like "~0:12") — the biggest accuracy lever.
- **Repro steps** and **expected vs. actual**.
- **Console / network / logs** — invisible in the video unless devtools are on screen, and
  often the real cause.
- **Relevant code area**, if known.

A **still screenshot of the bad moment** beats hunting inside a clip — **prefer it, and ask
for one** whenever a timestamp is fuzzy or ffmpeg isn't available (see the ffmpeg note below).

## 2. Extract frames

```
${CLAUDE_PLUGIN_ROOT}/skills/video-bug-analysis/scripts/extract-frames.sh \
  --video <path> [--fps <n>] [--scene <thr>] [--contact] [--timestamps <t1,t2>]
```

Default workflow:

1. **Overview:** `--fps 2 --contact` → one contact sheet of the whole span; read it to find
   where the symptom is. **Add `--text` whenever any part of the answer lives in the chrome** —
   a button label, a toggle's selected state, a status pill, a counter — not just for
   "text-heavy" UIs. A pixel-art canvas with an 11px `☀️ day` button is exactly the case: the
   canvas reads fine at any tile size, and the label that proves the bug does not. Cheap
   insurance; the only cost is a bigger sheet. (Portrait captures additionally drop to
   `--cols 1` under `--text`.)
2. **Zoom:** `--timestamps 0:12,0:34 --fps 8` → per moment, a dense burst (catches
   sub-second transients) plus a **before/after strip** (`tsNN_strip.png`) that's the best
   way to show the user a one-frame change.

For a **load / splash / "the intro does X"** bug, jump straight to **`--intro`** — shorthand for
the first ~2s as a dense, labelled contact sheet (`--start 0 --end 2 --fps 12 --contact --label`,
portrait-aware); any part yields to an explicit flag.

Tighten the window rather than raising fps across the whole clip. Frames default to
`.frames/<video-name>/`; `--out` overrides. Add **`--dry-run`** to print the exact ffmpeg
commands without running them (replicate by hand when the plugin isn't loaded this session).

### More modes — pick by the question (full detail in `reference.md`, flags in `--help`)

Most of these are **analysis modes** that print a CSV/report and exit (no frames). All honor
`--start/--end`; CSV modes use `--fps` for the sample rate.

| The question | Mode |
| :-- | :-- |
| Where's the transition? | `--scene 0.1` (capture cuts) · `--list-scenes` (print cut times) |
| Before/after of two frames I have | `--strip a.png,b.png` |
| Did it move / which direction? | `--diff` (bright = changed pixels) |
| Is it moving, and *how much* over time? | `--motion` → `t,motion` (mean inter-frame delta) |
| Is it choppy, and *where*? | `--stutter` (alias `--cadence`) — verdict (worst freeze) + per-window unique fps; `--freeze-min` tunes |
| Does a freeze match the app's own timing marks? | `--stutter --marks perf.json` — overlays `performance.mark` entries; verdict names the aligned mark |
| Is it hung — dead canvas / infinite splash loop? | `--stall` — flags ≥ `--stall-min` s of *no change* (reads "smooth" to a jank detector) |
| Blown-white flash / mid-clip black dropout? | `--whiteout` — mean-luma spans with start/duration/peak (bright-side `--blackdetect`) |
| Content vanished then came back (words dropped)? | `--content-revert` — A→B→A flicker events; `--crop` to the text region |
| Part 2 of a split capture (timestamps off by the split)? | add `--t0 <sec>` to any mode — relabels all reported times into session clock |
| Uneven frame *timing* (content changes every frame)? | `--pacing` → `t,interval_ms` from real presentation timestamps; median/p95/max + worst hitches |
| Spinning in place vs spiralling inward? | `--flow` → `t,speed,curl,div` (swirl vs suck decomposition; `--flow-center fx:fy`) |
| How much of the frame does the subject fill? | `--occupancy` → `t,coverage_pct,bbox` (the "present but too small" case) |
| One region's evolution in a single image | `--stack --crop W:H:X:Y` → vertical ROI time-stack |
| Is content cut off / off-canvas at an edge? | `--stack --edge right:60` — `--edge side:px` is a coord-free crop of that edge band; feeds any mode `--crop` feeds |
| How many distinct poses / how fast does a region *really* animate? | `--unique` → deduped `uniq_*.png` (the poses) + a cadence verdict; scope with `--crop`/`--edge` |
| Too vivid / "clownish" vs muted? | `--saturation` → `t,saturation` (per-frame colour intensity) |
| Read a tiny region (FPS/HUD/label) | `--crop W:H:X:Y` (crop+zoom; combines with any mode) |
| Black / blank screen | `--blackdetect` (spans, flags PERMANENT vs transient) |
| A readout *number* changing (4→5→4) | `--ocr-roi W:H:X:Y` → `t,text` (needs tesseract) |
| How big / where is a feature, over time | `--measure W:H:X:Y` → diameter + center (% of viewport) |
| Is it **expanding** or just **fading**? | `--measure` also emits `area_px,mean_luma,peak_luma` — area climbing = growing; area flat while luma falls = dimming in place (a static ball reads as "something happening" to the eye) |
| Blown-white flash on a **dark** scene? | `--whiteout` flags ≥ `--white-rel` × the clip's own median luma (default 4), so a washout that never nears the absolute cutoff still trips it |
| Capture size / aspect / orientation | `--probe` (also: which axis CSS `vmin` is) |
| Dominant colours (palette) | `--palette [--colors n]` → hex swatches |
| Colour *arc* over the clip (art reference) | `--palette --over-time [--segments n]` → `t,[hex…]` per window |
| Does this GIF/video loop seamlessly? | `--loop-check` → first-vs-last-frame seam diff + a strip |
| Two captures of the same thing differ where | `--ab other.mov` → `t,ssim` divergence timeline |
| Two clips side-by-side, phase-aligned | `--compare-videos a,b` → one stacked sheet (row per clip) |

Also: `--label` burns the source timestamp onto frames (now incl. contact tiles &
`--compare-videos`); `--text`/`--tile-width` tune contact legibility; `--window`/`--frame-width`
tune bursts. **Every run prints a one-line `playback cadence:` header** (effective vs nominal fps
+ a dropped-frame estimate) — the quickest "is it choppy?" read. Read it as a *measurement, not a
verdict*: it describes frame timing only, so a frozen or wrong-state UI can post a perfectly
healthy cadence. Never let it lower your prior on a "nothing responds" report.

> **ffmpeg is not installed for you.** If it's missing, the script says so and stops: install it
> yourself (`sudo apt-get install -y ffmpeg` / `brew install ffmpeg` — these ship **ffprobe** too,
> which `--probe`/`--list-scenes`/`--pacing`/`--stutter` need), or re-run with the explicit opt-in
> `VBA_ALLOW_INSTALL=1` (add `VBA_ALLOW_DOWNLOAD=1` to permit a checksum-verified static build in
> a sandbox with no package manager). Nothing is installed, escalated, or downloaded without one
> of those.

**Key steer — frames can't see state.** If a tracked value changes (`--ocr-roi`) or you suspect
a logic/timing bug but **nothing near it changes in the frame**, the cause is off-screen
logic/state (a counter desynced, a body left the viewport) — say so and point the user at
console logs or a small headless repro instead of extracting more frames. Likewise report
feature sizes as **% of viewport** (`--measure`/`--probe`), since retina (dpr 2) device px
mislead. And for **"an animation didn't play"**, frames confirm *absence* but not *cause* (the
element may be in the DOM but paused, the first paint deferred, or JS threw) — pair the video
pass with a DOM/console capture before concluding (see reference.md).

**Art-reference steer — captures shift colours.** A compressed screen recording is
chroma-subsampled (yuv420): saturated colours drift (hot pink reads salmon), so treat
`--palette` hexes as *approximate* and never tune art direction to them — for exact colours ask
for the **source asset or a lossless screenshot**. For "match this animation" requests, lead
with `--unique` (the distinct poses + real cadence) rather than a fixed-fps burst that
duplicates or skips poses.

**ffmpeg note:** ffmpeg is already on PATH in many environments (incl. many web containers).
If it's missing, **the script reports that and stops — it does not install anything.** Don't keep
retrying: either ask the user to install it (`sudo apt-get install -y ffmpeg` / `brew install
ffmpeg` — these ship `ffprobe` too), re-run once with the explicit opt-in `VBA_ALLOW_INSTALL=1`
(or `VBA_ALLOW_DOWNLOAD=1` for a checksum-verified static build where no package manager exists),
or — simplest — **ask for a still screenshot of the bad moment.**

## 3. Build a timeline

Read the PNGs in filename order; note what's on screen, what changes between frames, and
where the symptom first appears. Cite frames by filename.

## 4. Confirm in the code

Frames give the symptom and its location; the fix comes from the **source**. Read the
implicated component/handler/state before proposing a change — never patch from pixels alone.

## 5. Report with confidence + caveats

Label what you saw vs. inferred, and how sure you are. Call out the limits that apply:

- Gaps between samples hide fast flickers / one-frame glitches.
- Timing/race bugs: frames give no real sense of duration.
- Small text / subtle diffs are easy to misread or hallucinate — verify against code.
- Off-screen state (console/network/memory) is invisible.

When unsure, ask for a denser extraction, a tighter timestamp, or a still — don't guess.

See `reference.md` for the reliability matrix, fps-per-bug-class table, and checklist.

## Reporting feedback

Surface the pre-filled feedback link each run prints on stderr (suppress:
`VBA_NO_FEEDBACK_HINT=1`); for a fuller report, `scripts/report-feedback.sh`. Skill not loaded
this session (plugins load at session start)? `--dry-run` prints the raw ffmpeg commands to run
by hand.

