Skip to content

xguz/playwright-video-recording

Repository files navigation

playwright-video-recording

A reusable toolkit for recording scripted browser flows as polished WebM videos — with a visible cursor overlay, natural character-by-character typing, and click/typing sounds mixed in via ffmpeg post-processing.

Designed to work as a Cursor Agent Skill or Claude Code skill that scaffolds itself into any web project.

Features

  • Visible cursor overlay injected via Playwright's addInitScript — follows the mouse naturally with smooth page.mouse.move animations
  • Natural typing — characters typed one by one with human-like random delays (not fill() paste)
  • Click & typing sounds — post-processed by ffmpeg from bundled MP3 assets
  • Framework-agnostic — works with any web app (Rails, Next.js, Django, etc.)
  • Mobile + desktop viewports — record both in a single record_all.mjs run

Prerequisites

  • Node.js ≥ 18
  • Playwright (npm install playwright)
  • ffmpeg (for audio post-processing)

Installation

Clone this repo as a git submodule into your project's skills folder:

git submodule add https://github.com/xguz/playwright-video-recording.git .agents/skills/video-recording
git commit -m "Add playwright-video-recording skill"

To update to the latest version later:

git submodule update --remote .agents/skills/video-recording

Scaffolding a new project

  1. Copy templates into your project's scripts/video_recording/ directory:

    mkdir -p scripts/video_recording
    cp .agents/skills/video-recording/config.mjs scripts/video_recording/
    cp .agents/skills/video-recording/add_click_sounds.mjs scripts/video_recording/
  2. Edit scripts/video_recording/config.mjs:

    • Set BASE_URL to your dev server URL (e.g. http://localhost:3000)
    • Add project-specific helpers (login, seed data, etc.)
  3. Create scripts/video_recording/record_all.mjs listing your flows:

    import { execSync } from 'child_process';
    
    const flows = [
      { name: 'my-flow', script: 'flow_my_flow.mjs', description: 'Description', viewports: ['mobile', 'desktop'] },
    ];
    
    const args = process.argv.slice(2);
    const onlyFlow = args.find(a => a.startsWith('--flow='))?.split('=')[1];
    const onlyViewport = args.includes('--mobile') ? 'mobile' : args.includes('--desktop') ? 'desktop' : null;
    
    for (const flow of flows) {
      if (onlyFlow && flow.name !== onlyFlow) continue;
      for (const vp of flow.viewports) {
        if (onlyViewport && vp !== onlyViewport) continue;
        console.log(`\n▶ ${flow.name} [${vp}]`);
        execSync(`node scripts/video_recording/${flow.script} ${vp}`, { stdio: 'inherit' });
      }
    }
  4. Create flow scripts using flow_template.mjs as the starting point:

    cp .agents/skills/video-recording/flow_template.mjs scripts/video_recording/flow_my_flow.mjs

Running flows

# All flows, all viewports
node scripts/video_recording/record_all.mjs

# Specific flow
node scripts/video_recording/record_all.mjs --flow=my-flow

# Specific viewport
node scripts/video_recording/record_all.mjs --mobile
node scripts/video_recording/record_all.mjs --desktop

# Combine
node scripts/video_recording/record_all.mjs --flow=my-flow --mobile

# Single script directly
node scripts/video_recording/flow_my_flow.mjs mobile

Output lands in videos/<subdir>/:

  • *-with-audio.webm — final video with sounds
  • *.webm — raw recording (no audio)

Key rules

Rule Reason
Use naturalFill() instead of locator.fill() fill() sets value instantly (looks like paste)
Use naturalClick() instead of locator.click() Moves cursor to element before clicking
Call injectCursor(page) once per page object Sets up the visible cursor overlay for all navigations
After fetch/Turbo form submits: pause(1500) then page.goto(nextUrl) waitForLoadState('networkidle') returns before server commits

Customization

Adjust timing

Edit the pause() durations in flow scripts:

await pause(2000); // slower, more dramatic
await pause(500);  // faster, quick demo

Change viewports

Edit DEVICES in scripts/video_recording/config.mjs:

export const DEVICES = {
  mobile: {
    viewport: { width: 375, height: 667 }, // iPhone SE
    // ...
  }
};

Convert to MP4

# Single video
ffmpeg -i input.webm -c:v libx264 -c:a aac output.mp4

# All videos in a directory
for f in videos/my-flow/*.webm; do
  ffmpeg -i "$f" -c:v libx264 -c:a aac "${f%.webm}.mp4"
done

Sound assets

Two MP3 files are bundled in this repo:

  • computer-mouse-click-352734.mp3 — played on each tracked click
  • keyboard-typing-asmr-356118.mp3 — looped during typing events

Scripts resolve them automatically via a SKILL_DIR constant — no manual path configuration needed.

File reference

File Purpose
config.mjs Core helpers: cursor, click tracker, natural typing, sound post-processing
add_click_sounds.mjs ffmpeg wrapper — mixes click/typing sounds into a raw .webm
flow_template.mjs Minimal starter template for new flow scripts
SKILL.md Instructions for Cursor Agent (auto-read by the agent when this skill is active)

Troubleshooting

"Failed to get login token" / server errors

  • Ensure your dev server is running
  • Check that any test helper endpoints are enabled in your environment

Videos are blank or corrupt

  • Ensure context.close() is called in the finally block — this is what saves the video
  • Check that the browser didn't crash during recording

No audio in output

  • Make sure ffmpeg is installed (ffmpeg -version)
  • Use the *-with-audio.webm file, not the raw *.webm

Elements not found

  • Add longer pause() calls if the page loads slowly
  • Use page.screenshot({ path: 'error.png', fullPage: true }) in the catch block to capture what the page looked like when it failed

License

MIT

About

a claude / cursor skill to record videos of web apps, with visible cursor, clicking and typing sounds

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors