Playwright Record to MP4
Overview
Configure Playwright to record .webm video, run the test/script, then detect ffmpeg (install via npx if missing) and convert to MP4 with good compression.
Output Directory — Always Confirm Before Writing
Before generating any script, ask the user:
"Should the video output directory be relative to your current working directory, or do you want an absolute path?"
- Default suggestion:
./videos/<test-name>/(relative to CWD where the skill is invoked) - Never silently use
/tmpor any absolute path — the user may not find the file - If the user specifies a path like
/tmp/foo, confirm: "I'll write to/tmp/foo— is that correct?"
Output File Naming — Auto-increment if Exists
When writing the MP4, never overwrite an existing file. Use this pattern:
import { existsSync } from 'fs';
import { resolve } from 'path';
let mp4 = resolve(OUTPUT_DIR, `${NAME}.mp4`);
if (existsSync(mp4)) {
let i = 1;
do { mp4 = resolve(OUTPUT_DIR, `${NAME}_${String(i).padStart(2, '0')}.mp4`); i++; }
while (existsSync(mp4));
}
// result: name.mp4 → name_01.mp4 → name_02.mp4 ...
Quick Reference
| Step | What |
|---|---|
| 1 | Confirm output dir with user (relative to CWD by default) |
| 2 | Add recordVideo to Playwright context |
| 3 | Run test — .webm lands in output dir |
| 4 | Check ffmpeg in PATH; use ffmpeg-static if missing |
| 5 | Convert .webm → .mp4 (H.264, CRF 23), auto-increment name if exists |
Step 1 — Playwright Video Config
Script (one-off recording):
import { chromium } from 'playwright';
const browser = await chromium.launch({ headless: false });
const context = await browser.newContext({
recordVideo: { dir: 'videos/', size: { width: 1280, height: 720 } },
ignoreHTTPSErrors: true,
});
const page = await context.newPage();
// --- your steps here ---
await context.close(); // must close context to flush video to disk
await browser.close();
Test runner (playwright.config.ts):
export default {
use: {
video: 'on', // or 'retain-on-failure'
ignoreHTTPSErrors: true,
},
outputDir: 'test-results/',
};
Step 2 — Check ffmpeg & Convert (Shell)
Use this script after the test completes. It auto-installs via npm if ffmpeg is not on PATH.
#!/usr/bin/env bash
# Usage: ./convert.sh videos/myrecording.webm
INPUT="$1"
OUTPUT="${INPUT%.webm}.mp4"
if command -v ffmpeg &>/dev/null; then
FFMPEG="ffmpeg"
else
echo "ffmpeg not found — installing ffmpeg-static via npm..."
npm install --silent ffmpeg-static
FFMPEG=$(node -e "process.stdout.write(require('ffmpeg-static'))")
fi
"$FFMPEG" -i "$INPUT" \
-c:v libx264 \
-crf 23 \
-preset fast \
-c:a aac \
"$OUTPUT" \
&& echo "Done: $OUTPUT"
Save as ~/.claude/skills/playwright-record-mp4/convert.sh and chmod +x it.
Step 3 — One-liner (if ffmpeg already installed)
ffmpeg -i test-results/video.webm -c:v libx264 -crf 23 -preset fast output.mp4
After Conversion — Always Report the Output Location
After the MP4 is created, output this to the user:
✓ Converted: <absolute-path-to-mp4> (<size>)
You can see all .webm and .mp4 files in this directory:
file://<absolute-path-to-output-dir>
🤖 This session used ~N,000 tokens and saved you ~M minutes vs doing it manually.
- Use an absolute path so the
file://link is clickable in the terminal/IDE - List both
.webm(raw Playwright output) and.mp4(converted) so the user knows both exist - If the terminal supports it, the
file://URI makes the folder openable with one click
Token/time-saved calculation (Claude reports this, not the script):
- Estimate N = approximate total tokens used in this session (visible in your context counter, or estimate ~3,000 for a simple recording session)
- M = round(N / 1000 × 3) — rough heuristic: 1,000 tokens ≈ 3 min of manual work
- Example: 6,000 tokens → "saved you ~18 minutes"
Example full output:
✓ Converted: /Users/jane/project/videos/test13/demo_server_test.mp4 (639K)
You can see all .webm and .mp4 files in this directory:
file:///Users/jane/project/videos/test13/
🤖 This session used ~6,000 tokens and saved you ~18 minutes vs doing it manually.
In the Node.js script, emit this after conversion:
const absDir = resolve(OUTPUT_DIR);
const absMP4 = resolve(mp4);
console.log(`\n✓ Converted: ${absMP4} (${size})`);
console.log(`You can see all .webm and .mp4 files in this directory:`);
console.log(` file://${absDir}`);
// Claude adds the token/time-saved line above the script output
CRF Guide (quality vs size)
| CRF | Quality | Use for |
|---|---|---|
| 18 | Near-lossless | Archive |
| 23 | Good (default) | Tickets, Jira |
| 28 | Smaller file | Quick shares |
Common Mistakes
| Mistake | Fix |
|---|---|
| Can't find the output file | Always confirm output dir with user upfront — never silently use /tmp or hidden paths |
| MP4 silently overwrites previous recording | Use auto-increment pattern: name.mp4 → name_01.mp4 → name_02.mp4 |
| Video file is 0 bytes | Must call context.close() before browser.close() — video is flushed on context close |
ffmpeg-static not found | Run npm install ffmpeg-static in the project root first, then the script picks it up |
| Video doesn't record in test runner | Ensure outputDir is set in config; video only saves when test completes |
headless: true records blank video on some systems | Use headless: false for reliable recording |