Skip to content

Latest commit

 

History

History
124 lines (93 loc) · 5.16 KB

File metadata and controls

124 lines (93 loc) · 5.16 KB
name video-recording
description Records browser usage flow videos using Playwright Firefox with natural typing, a visible cursor overlay, and click/typing sounds mixed in via ffmpeg. Includes scaffolding for new projects. Use when the user asks to record a video, run a flow script, create a new flow, set up video recording in a project, or debug a recording issue.

Video Recording

Records scripted browser flows as WebM videos with a visible cursor, natural character-by-character typing, and click/typing sounds post-processed via ffmpeg.

Installation

To install this skill in another project:

git clone https://github.com/xguz/playwright-video-recording .agents/skills/video-recording

Sound assets

Two MP3 files are bundled in this skill folder:

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

Scripts resolve them via a SKILL_DIR constant (see config.mjs).

Scaffold a new project

When setting up video recording in a project for the first time:

  1. Copy config.mjs and add_click_sounds.mjs from this skill folder into scripts/video_recording/
  2. In the copied config.mjs, set BASE_URL to the app's local dev server URL and add any project-specific test/setup helpers
  3. Create scripts/video_recording/record_all.mjs listing all flows (see structure in this project)
  4. Create flow scripts using flow_template.mjs as the starter (copy and fill in the steps)

Running flows

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

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

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

Output: videos/<subdir>/*-with-audio.webm (plain .webm alongside it is the raw recording without audio).

Creating a new flow

Use flow_template.mjs from this skill folder as the starting point. The canonical structure:

import { firefox } from 'playwright';
import { mkdir, rename } from 'fs/promises';
import { BASE_URL, VIDEO_DIR, DEVICES, cleanup, pause,
         injectCursor, createClickTracker, naturalClick,
         naturalFill, saveAndAddSound } from './config.mjs';

async function recordFlow(deviceType = 'mobile') {
  const device   = DEVICES[deviceType];
  const videoDir = /* path to output subfolder */;
  await mkdir(videoDir, { recursive: true });

  const browser = await firefox.launch({ headless: true, slowMo: 80 });
  const context = await browser.newContext({ ...device, recordVideo: { dir: videoDir, size: device.viewport } });
  const page    = await context.newPage();
  const tracker = createClickTracker();
  let finalVideoPath = null;

  try {
    await cleanup(page);      // clear test data
    await injectCursor(page); // visible cursor overlay

    // --- scripted steps ---
    await page.goto(`${BASE_URL}/some/path`);
    await page.waitForLoadState('networkidle');
    await naturalFill(page, page.locator('input[name="field"]'), 'value', tracker, 'label');
    await naturalClick(page, page.locator('button[type="submit"]'), tracker, 'submit');

    // IMPORTANT: after fetch/Turbo form submits, wait before navigating
    await pause(1500);
    await page.goto(`${BASE_URL}/next/step`);
    await page.waitForLoadState('networkidle');

  } catch (err) {
    console.error(`Error: ${err.message}`);
    await page.screenshot({ path: 'error.png', fullPage: true });
  } finally {
    const tmpPath = page.video() ? await page.video().path() : null;
    await context.close();
    await browser.close();
    if (tmpPath) {
      const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
      finalVideoPath = /* timestamped output path */;
      await rename(tmpPath, finalVideoPath).catch(() => { finalVideoPath = tmpPath; });
      await saveAndAddSound(finalVideoPath, tracker);
    }
  }
}

recordFlow(process.argv[2] || 'mobile');

Then register the flow in record_all.mjs:

{ name: 'my-flow', script: 'flow_my_flow.mjs', description: '...', viewports: ['mobile', 'desktop'] }

Key rules

  • Never use locator.fill() — it sets the value instantly (looks like paste). Always use naturalFill() which types character by character.
  • naturalClick(page, locator, tracker, label) — moves the cursor to the element before clicking; always use instead of locator.click().
  • injectCursor(page) — call once per page object; it auto-reinjects on every navigation via addInitScript.
  • After fetch/Turbo form submitswaitForLoadState('networkidle') returns before the server commits. Always follow with pause(1500) then an explicit page.goto(nextUrl).
  • Email confirmation links — if the mailer uses a different host/port than BASE_URL, extract the token from the email HTML and rebuild the URL. See extractConfirmUrl pattern in this project's config.mjs.

Additional resources

  • Generic scripts: config.mjs, add_click_sounds.mjs, flow_template.mjs in this skill folder
  • This project's flows: scripts/video_recording/flow_*.mjs
  • Orchestrator: scripts/video_recording/record_all.mjs
  • Full helper reference: scripts/video_recording/README.md