mcpField
P
playwright-recording

playwright-recording

github.com/digitalsambaクリエイティブ・メディア2.4k1.4k 回インストール今日更新
リポジトリ

playwright-recording について

playwright-recordingは、Playwrightでブラウザー操作を動画として記録するスキルで、特にRemotionコンポジション向けの素材作成を扱います。デモ、ページ遷移、スクロールなどの録画パターンと設定方法を紹介します。

主な機能

  1. 01ビューポートと録画サイズの設定
  2. 02スクリプトによるブラウザー操作の録画
  3. 03ページ遷移とフォーム操作の記録
  4. 04カーソルとクリック表示の追加
  5. 05WebM録画のMP4変換
  6. 06Cookieバナーと手動録画への対応

活用シーン

  1. 01Remotion向けブラウザーデモの録画
  2. 02フォーム送信やログイン操作の記録
  3. 03ページ遷移の手順を録画

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/digitalsamba/claude-code-video-toolkit --skill playwright-recording

インストール後、Claude Code で/playwright-recordingを入力して読み込まれたか確認します。

同梱ファイルreference.md

詳細

Playwright の動画録画

Playwright ではブラウザーの操作を動画として録画できます。Remotion のコンポジション用のデモ映像に最適です。

クイックスタート

インストール
# In your video project
npm init -y
npm install -D playwright @playwright/test
npx playwright install chromium
基本的な録画スクリプト
// scripts/record-demo.ts
import { 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();

次のコマンドで実行します:

npx ts-node scripts/record-demo.ts
# or
npx tsx scripts/record-demo.ts

録画の設定

ビューポートのサイズ
// 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 }

// Mobile
viewport: { width: 390, height: 844 } // iPhone 14
動画品質の設定
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 slowMo
const browser = await chromium.launch({
  slowMo: 100 // 100ms delay between actions
});

録画パターン

フォーム送信のデモ
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();
}
複数ページのナビゲーション
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();
}
スクロールのデモ
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();
}
ログインフロー
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();
}

カーソルの強調表示

Playwright では、デフォルトでカーソルが表示されません。視覚的なインジケーターを追加します:

CSS によるカーソルの強調表示
// Inject cursor visualization
await 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 element
await 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'));
});
クリック時の波紋エフェクト
// Add click ripple visualization
await 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 ripple
async 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();
}

Remotion 用の出力

録画を public/demos/ に移動
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();
}
WebM を MP4 に変換

Playwright は WebM を出力します。Remotion との互換性を高めるために変換します:

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

インタラクティブ録画

手動で操作を行う、ユーザー主導の録画の場合:

// Inject ESC key listener to stop recording
async 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 gracefully
while (!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));
}

重要なポイント: page.evaluate() はナビゲーション中にエラーをスローします。try/catch を使って処理を続けてください。エラーを停止シグナルとして扱わないでください。

ノート PC 向けのウィンドウスケーリング

小さなウィンドウを表示しながら、フル 1080p で録画します:

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

Cookie バナーの非表示

一般的な同意管理プラットフォーム向けの包括的なセレクター一覧:

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 */ }
  }
}

page.goto() の後と、ナビゲーション時に page.on('load') で呼び出してください。

重要:挿入した要素は動画に表示されます

警告: 挿入した DOM 要素(カーソル、コントロールパネル、オーバーレイなど)はすべて録画されます。UI を含めずに録画する場合は、ターミナルベースの操作(Ctrl+C、最大時間タイマー)のみを使用してください。

デモ録画をうまく作成するためのヒント

  1. slowMo を使う - 50~100ms にすると操作が見やすくなります
  2. waitForTimeout を追加する - 理解しやすいように、操作の合間に一時停止します
  3. アニメーションの完了を待つ - waitForLoadState('networkidle') を使用します
  4. Remotion の寸法に合わせる - 一般的な設定は 1920x1080、30fps です
  5. 録画せずに先にテストする - 最終的なキャプチャの前にデバッグします
  6. ブラウザーの状態をクリアする - デモをクリーンに保つには、新しいコンテキストを使用します
  7. Cookie バナーを非表示にする - 上記の包括的なセレクター一覧を使用します
  8. ナビゲーション時に再挿入する - ページの読み込み時にカーソルやリスナーはリセットされます

フィードバックとコントリビューション

このスキルに情報が不足している場合や、改善の余地がある場合:

  • パターンが足りませんか? 必要だったものを説明してください
  • エラーを見つけましたか? 問題の内容をお知らせください
  • コントリビュートしますか? 次のことをお手伝いできます:
    1. 改善内容を反映してこのスキルを更新する
    2. github.com/digitalsamba/claude-code-video-toolkit に PR を作成する

「improve this skill」と伝えていただければ、.claude/skills/playwright-recording/SKILL.md の更新をご案内します。

コメント