playwright-recording について
Playwrightでブラウザー操作を録画し、Remotionのコンポジション向けデモ映像を作るスキルです。スクリプト録画と手動録画、視覚効果、動画プロジェクト用の素材準備を扱います。
主な機能
- 01ブラウザー動画のスクリプト録画
- 02フォーム、画面遷移、スクロール、ログインのデモ
- 03カーソルとクリックの視覚効果
- 04ビューポートと録画品質の設定
- 05Remotion向けのWebM変換
活用シーン
- 01製品デモ用のフォーム送信を録画
- 02複数ページのブラウザー遷移を記録
- 03スクロールやログインの映像を作成
- 04Remotionのコンポジション用に録画素材を準備
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ calesthio/ openmontage --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、最大時間タイマー)のみを使用してください。
デモ録画をうまく作成するためのヒント
- slowMo を使う - 50~100ms にすると操作が見やすくなります
- waitForTimeout を追加する - 理解しやすいように、操作の合間に一時停止します
- アニメーションの完了を待つ -
waitForLoadState('networkidle')を使用します - Remotion の寸法に合わせる - 一般的な設定は 1920x1080、30fps です
- 録画せずに先にテストする - 最終的なキャプチャの前にデバッグします
- ブラウザーの状態をクリアする - デモをクリーンに保つには、新しいコンテキストを使用します
- Cookie バナーを非表示にする - 上記の包括的なセレクター一覧を使用します
- ナビゲーション時に再挿入する - ページの読み込み時にカーソルやリスナーはリセットされます
フィードバックとコントリビューション
このスキルに情報が不足している場合や、改善の余地がある場合:
- パターンが足りませんか? 必要だったものを説明してください
- エラーを見つけましたか? 問題の内容をお知らせください
- コントリビュートしますか? 次のことをお手伝いできます:
- 改善内容を反映してこのスキルを更新する
- github.com/digitalsamba/claude-code-video-toolkit に PR を作成する
「improve this skill」と伝えていただければ、.claude/skills/playwright-recording/SKILL.md の更新をご案内します。