Playwright Recording

digitalsamba/claude-code-video-toolkit/.claude/skills/playwright-recording

作者 digitalsamba2c99460a5d0cba253f983283fb8d0fb9c4edb4a7无许可证2.1K 个星标收录于 2026年10月9日更新于 2026年10月9日仓库3天前更新

Record browser interactions as video using Playwright. Use for capturing demo videos, app walkthroughs, and UI flows for Remotion videos. Triggers include recording a demo, capturing browser video, screen recording a website, or creating walkthrough footage.

AI 生成的概览

指导使用 Playwright 将浏览器交互录制为视频,用于演示和操作流程素材。

功能
该技能提供使用 Playwright 将浏览器会话录制为视频的说明和 TypeScript 代码示例。内容涵盖安装、视口与画质设置,以及表单提交、多页导航、滚动和登录流程等录制模式,还包括光标与点击涟漪可视化、Cookie 横幅关闭,以及为 Remotion 合成导出录制文件。它还介绍了用 ffmpeg 将 WebM 输出转换为 MP4,以及处理交互式、用户驱动的录制。
适用场景
当你需要采集演示视频、应用操作流程或 UI 流程素材时使用,尤其适用于 Remotion 视频项目。它适合录制网站演示、采集浏览器视频或制作操作流程素材等任务。
运行要求
需要安装 Playwright 和 @playwright/test 的 Node.js 环境,并通过 npx playwright install chromium 安装 Chromium 浏览器。运行示例脚本需要 ts-node 或 tsx,转换为 MP4 需要 ffmpeg。该技能不附带脚本,只有说明和一份参考文档。

Playwright Video Recording

Playwright can record browser interactions as video - perfect for demo footage in Remotion compositions.

Quick Start

Installation

bash
# In your video projectnpm init -ynpm install -D playwright @playwright/testnpx playwright install chromium

Basic Recording Script

typescript
// scripts/record-demo.tsimport { chromium } from 'playwright';
async function recordDemo() {  const browser = await chromium.launch();  const context = await browser.newContext({    viewport: { width: 1920, height: 1080 },    recordVideo: {      dir: './recordings',      size: { width: 1920, height: 1080 }    }  });
  const page = await context.newPage();
  // Your recording actions  await page.goto('https://example.com');  await page.waitForTimeout(2000);  await page.click('button.demo');  await page.waitForTimeout(3000);
  // Close to save video  await context.close();  await browser.close();
  console.log('Recording saved to ./recordings/');}
recordDemo();

Run with:

bash
npx ts-node scripts/record-demo.ts# ornpx tsx scripts/record-demo.ts

Recording Configuration

Viewport Sizes

typescript
// Standard 1080p (recommended for Remotion)viewport: { width: 1920, height: 1080 }
// 720p (smaller files)viewport: { width: 1280, height: 720 }
// Square (social media)viewport: { width: 1080, height: 1080 }
// Mobileviewport: { width: 390, height: 844 } // iPhone 14

Video Quality Settings

typescript
const context = await browser.newContext({  viewport: { width: 1920, height: 1080 },  recordVideo: {    dir: './recordings',    size: { width: 1920, height: 1080 } // Match viewport for crisp output  },  // Slow down for visibility  // Note: slowMo is on browser launch, not context});
// For slow motion, launch browser with slowMoconst browser = await chromium.launch({  slowMo: 100 // 100ms delay between actions});

Recording Patterns

Form Submission Demo

typescript
import { chromium } from 'playwright';
async function recordFormDemo() {  const browser = await chromium.launch({ slowMo: 50 });  const context = await browser.newContext({    viewport: { width: 1920, height: 1080 },    recordVideo: { dir: './recordings', size: { width: 1920, height: 1080 } }  });  const page = await context.newPage();
  await page.goto('https://myapp.com/form');  await page.waitForTimeout(1000);
  // Type with realistic speed  await page.fill('#name', 'John Smith', { timeout: 5000 });  await page.waitForTimeout(500);
  await page.fill('#email', '[email protected]');  await page.waitForTimeout(500);
  // Click submit  await page.click('button[type="submit"]');
  // Wait for result  await page.waitForSelector('.success-message');  await page.waitForTimeout(2000);
  await context.close();  await browser.close();}

Multi-Page Navigation

typescript
async function recordNavDemo() {  const browser = await chromium.launch({ slowMo: 100 });  const context = await browser.newContext({    viewport: { width: 1920, height: 1080 },    recordVideo: { dir: './recordings', size: { width: 1920, height: 1080 } }  });  const page = await context.newPage();
  // Page 1  await page.goto('https://myapp.com');  await page.waitForTimeout(2000);
  // Navigate to page 2  await page.click('nav a[href="/features"]');  await page.waitForLoadState('networkidle');  await page.waitForTimeout(2000);
  // Navigate to page 3  await page.click('nav a[href="/pricing"]');  await page.waitForLoadState('networkidle');  await page.waitForTimeout(2000);
  await context.close();  await browser.close();}

Scroll Demo

typescript
async function recordScrollDemo() {  const browser = await chromium.launch();  const context = await browser.newContext({    viewport: { width: 1920, height: 1080 },    recordVideo: { dir: './recordings', size: { width: 1920, height: 1080 } }  });  const page = await context.newPage();
  await page.goto('https://myapp.com/long-page');  await page.waitForTimeout(1000);
  // Smooth scroll  await page.evaluate(async () => {    const delay = (ms: number) => new Promise(r => setTimeout(r, ms));    for (let i = 0; i < 10; i++) {      window.scrollBy({ top: 200, behavior: 'smooth' });      await delay(300);    }  });
  await page.waitForTimeout(1000);  await context.close();  await browser.close();}

Login Flow

typescript
async function recordLoginDemo() {  const browser = await chromium.launch({ slowMo: 75 });  const context = await browser.newContext({    viewport: { width: 1920, height: 1080 },    recordVideo: { dir: './recordings', size: { width: 1920, height: 1080 } }  });  const page = await context.newPage();
  await page.goto('https://myapp.com/login');  await page.waitForTimeout(1000);
  await page.fill('#email', '[email protected]');  await page.waitForTimeout(300);
  await page.fill('#password', '••••••••');  await page.waitForTimeout(500);
  await page.click('button[type="submit"]');
  // Wait for dashboard  await page.waitForURL('**/dashboard');  await page.waitForTimeout(3000);
  await context.close();  await browser.close();}

Cursor Highlighting

Playwright doesn't show cursor by default. Add visual indicators:

CSS Cursor Highlight

typescript
// Inject cursor visualizationawait page.addStyleTag({  content: `    * { cursor: none !important; }    .playwright-cursor {      position: fixed;      width: 24px;      height: 24px;      background: rgba(255, 100, 100, 0.5);      border: 2px solid rgba(255, 50, 50, 0.8);      border-radius: 50%;      pointer-events: none;      z-index: 999999;      transform: translate(-50%, -50%);      transition: transform 0.1s ease;    }    .playwright-cursor.clicking {      transform: translate(-50%, -50%) scale(0.8);      background: rgba(255, 50, 50, 0.8);    }  `});
// Add cursor elementawait page.evaluate(() => {  const cursor = document.createElement('div');  cursor.className = 'playwright-cursor';  document.body.appendChild(cursor);
  document.addEventListener('mousemove', (e) => {    cursor.style.left = e.clientX + 'px';    cursor.style.top = e.clientY + 'px';  });
  document.addEventListener('mousedown', () => cursor.classList.add('clicking'));  document.addEventListener('mouseup', () => cursor.classList.remove('clicking'));});

Click Ripple Effect

typescript
// Add click ripple visualizationawait page.addStyleTag({  content: `    .click-ripple {      position: fixed;      width: 40px;      height: 40px;      border-radius: 50%;      background: rgba(234, 88, 12, 0.4);      pointer-events: none;      z-index: 999998;      transform: translate(-50%, -50%) scale(0);      animation: ripple 0.4s ease-out forwards;    }    @keyframes ripple {      to {        transform: translate(-50%, -50%) scale(2);        opacity: 0;      }    }  `});
// Custom click function with rippleasync function clickWithRipple(page, selector) {  const element = await page.locator(selector);  const box = await element.boundingBox();
  await page.evaluate(({ x, y }) => {    const ripple = document.createElement('div');    ripple.className = 'click-ripple';    ripple.style.left = x + 'px';    ripple.style.top = y + 'px';    document.body.appendChild(ripple);    setTimeout(() => ripple.remove(), 400);  }, { x: box.x + box.width / 2, y: box.y + box.height / 2 });
  await element.click();}

Output for Remotion

Move Recording to public/demos/

typescript
import { chromium } from 'playwright';import * as fs from 'fs';import * as path from 'path';
async function recordForRemotion(outputName: string) {  const browser = await chromium.launch({ slowMo: 50 });  const context = await browser.newContext({    viewport: { width: 1920, height: 1080 },    recordVideo: { dir: './temp-recordings', size: { width: 1920, height: 1080 } }  });  const page = await context.newPage();
  // ... recording actions ...
  await context.close();
  // Get the video path  const video = page.video();  const videoPath = await video?.path();
  if (videoPath) {    const destPath = `./public/demos/${outputName}.webm`;    fs.mkdirSync(path.dirname(destPath), { recursive: true });    fs.renameSync(videoPath, destPath);    console.log(`Recording saved to: ${destPath}`);
    // Get duration for config    // Use ffprobe: ffprobe -v error -show_entries format=duration -of csv=p=0 file.webm  }
  await browser.close();}

Convert WebM to MP4

Playwright outputs WebM. Convert for better Remotion compatibility:

bash
ffmpeg -i recording.webm -c:v libx264 -crf 20 -preset medium -movflags faststart public/demos/demo.mp4

Interactive Recording

For user-driven recordings where you manually perform actions:

typescript
// Inject ESC key listener to stop recordingasync function injectStopListener(page: Page): Promise<void> {  await page.evaluate(() => {    if ((window as any).__escListenerAdded) return;    (window as any).__escListenerAdded = true;    (window as any).__stopRecording = false;    document.addEventListener('keydown', (e) => {      if (e.key === 'Escape') {        e.preventDefault();        (window as any).__stopRecording = true;      }    });  });}
// Poll for stop signal - handle navigation errors gracefullywhile (!stopped) {  try {    const shouldStop = await page.evaluate(() => (window as any).__stopRecording === true);    if (shouldStop) break;  } catch {    // Page navigating - continue recording  }  await new Promise(r => setTimeout(r, 200));}

Key insight: page.evaluate() throws during navigation. Use try/catch and continue - don't treat errors as stop signals.

Window Scaling for Laptops

Record at full 1080p while showing a smaller window:

typescript
const scale = 0.75; // 75% window sizeconst context = await browser.newContext({  viewport: { width: 1920 * scale, height: 1080 * scale },  deviceScaleFactor: 1 / scale,  recordVideo: { dir: './recordings', size: { width: 1920, height: 1080 } },});

Cookie Banner Dismissal

Comprehensive selector list for common consent platforms:

typescript
const COOKIE_SELECTORS = [  '#onetrust-accept-btn-handler',           // OneTrust  '#CybotCookiebotDialogBodyButtonAccept',  // Cookiebot  '.cc-btn.cc-dismiss',                      // Cookie Consent by Insites  '[class*="cookie"] button[class*="accept"]',  '[class*="consent"] button[class*="accept"]',  'button:has-text("Accept all")',  'button:has-text("Accept cookies")',  'button:has-text("Got it")',];
async function dismissCookieBanners(page: Page): Promise<void> {  await page.waitForTimeout(500);  for (const selector of COOKIE_SELECTORS) {    try {      const btn = page.locator(selector).first();      if (await btn.isVisible({ timeout: 100 })) {        await btn.click({ timeout: 500 });        return;      }    } catch { /* try next */ }  }}

Call after page.goto() and on page.on('load') for navigation.

Important: Injected Elements Appear in Video

Warning: Any DOM elements you inject (cursors, control panels, overlays) will be recorded. For UI-free recordings, use terminal-based controls only (Ctrl+C, max duration timer).

Tips for Good Demo Recordings

  1. Use slowMo - 50-100ms makes actions visible
  2. Add waitForTimeout - Pause between actions for comprehension
  3. Wait for animations - Use waitForLoadState('networkidle')
  4. Match Remotion dimensions - 1920x1080 at 30fps typical
  5. Test without recording first - Debug before final capture
  6. Clear browser state - Use fresh context for clean demos
  7. Dismiss cookie banners - Use comprehensive selector list above
  8. Re-inject on navigation - Cursor/listeners reset on page load

Feedback & Contributions

If this skill is missing information or could be improved:

  • Missing a pattern? Describe what you needed
  • Found an error? Let me know what's wrong
  • Want to contribute? I can help you:
    1. Update this skill with improvements
    2. Create a PR to github.com/digitalsamba/claude-code-video-toolkit

Just say "improve this skill" and I'll guide you through updating .claude/skills/playwright-recording/SKILL.md.

来源与署名

来源:digitalsamba/claude-code-video-toolkit位于.claude/skills/playwright-recording提交2c99460

许可证: 无许可证

内容归原作者所有。SourceWeft 从公开仓库中收录这些内容。

举报或申请下架