playwright-recording
Playwrightを使用してブラウザ操作を動画として録画します。デモ動画やアプリのウォークスルー、RemotionビデオのUIフロー撮影に活用できます。「デモを録画したい」「ブラウザ操作を動画に残したい」「Webサイトのスクリーン録画を作成したい」といった場面でトリガーされます。
description の原文を見る
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.
SKILL.md 本文
Playwrightビデオ記録
Playwrightはブラウザインタラクションをビデオとして記録できます。Remotion コンポジションのデモフッテージに最適です。
クイックスタート
インストール
# ビデオプロジェクト内で実行
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();
// 記録するアクション
await page.goto('https://example.com');
await page.waitForTimeout(2000);
await page.click('button.demo');
await page.waitForTimeout(3000);
// クローズしてビデオを保存
await context.close();
await browser.close();
console.log('Recording saved to ./recordings/');
}
recordDemo();
実行方法:
npx ts-node scripts/record-demo.ts
# または
npx tsx scripts/record-demo.ts
記録の設定
ビューポートサイズ
// 標準1080p (Remotion推奨)
viewport: { width: 1920, height: 1080 }
// 720p (ファイルサイズ小)
viewport: { width: 1280, height: 720 }
// スクエア (ソーシャルメディア)
viewport: { width: 1080, height: 1080 }
// モバイル
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 } // クリアな出力のためビューポートに合わせる
},
// 表示性のためにスロー実行
// 注: slowMoはブラウザ起動時であり、コンテキストではありません
});
// スローモーション用にブラウザを起動
const browser = await chromium.launch({
slowMo: 100 // アクション間に100msの遅延
});
記録パターン
フォーム送信デモ
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);
// リアルな速度で入力
await page.fill('#name', 'John Smith', { timeout: 5000 });
await page.waitForTimeout(500);
await page.fill('#email', 'john@example.com');
await page.waitForTimeout(500);
// 送信をクリック
await page.click('button[type="submit"]');
// 結果を待つ
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();
// ページ1
await page.goto('https://myapp.com');
await page.waitForTimeout(2000);
// ページ2へナビゲート
await page.click('nav a[href="/features"]');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(2000);
// ページ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);
// スムーズスクロール
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', 'demo@example.com');
await page.waitForTimeout(300);
await page.fill('#password', '••••••••');
await page.waitForTimeout(500);
await page.click('button[type="submit"]');
// ダッシュボードを待つ
await page.waitForURL('**/dashboard');
await page.waitForTimeout(3000);
await context.close();
await browser.close();
}
カーソルハイライト
Playwrightはデフォルトではカーソルを表示しません。ビジュアルインジケータを追加します:
CSSカーソルハイライト
// カーソル可視化を注入
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);
}
`
});
// カーソル要素を追加
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'));
});
クリックリップルエフェクト
// クリックリップル可視化を追加
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;
}
}
`
});
// リップル付きカスタムクリック関数
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();
// ... 記録するアクション ...
await context.close();
// ビデオパスを取得
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}`);
// 設定の再生時間を取得
// 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
インタラクティブ記録
ユーザーが手動でアクションを実行する記録:
// ESCキーリスナーを注入して記録を停止
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;
}
});
});
}
// 停止シグナルをポーリング - ナビゲーションエラーを優雅に処理
while (!stopped) {
try {
const shouldStop = await page.evaluate(() => (window as any).__stopRecording === true);
if (shouldStop) break;
} catch {
// ページがナビゲーション中 - 記録を継続
}
await new Promise(r => setTimeout(r, 200));
}
重要: page.evaluate() はナビゲーション中にスローされます。try/catchを使用して続行し、エラーを停止シグナルとして扱わないでください。
ラップトップのウィンドウスケーリング
小さいウィンドウを表示しながら1080pで記録:
const scale = 0.75; // ウィンドウサイズ75%
const context = await browser.newContext({
viewport: { width: 1920 * scale, height: 1080 * scale },
deviceScaleFactor: 1 / scale,
recordVideo: { dir: './recordings', size: { width: 1920, height: 1080 } },
});
クッキーバナーの却下
一般的なコンセントプラットフォーム用の包括的なセレクタリスト:
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 { /* 次を試す */ }
}
}
page.goto() の後およびナビゲーション用の page.on('load') で呼び出します。
重要: 注入された要素はビデオに表示されます
警告: 注入したDOM要素(カーソル、制御パネル、オーバーレイ)は記録されます。UI フリーの記録の場合は、ターミナルベースのコントロールのみを使用します(Ctrl+C、最大期間タイマー)。
良いデモ記録のためのヒント
- slowMoを使用 - 50-100msでアクションが見えるようになります
- waitForTimeoutを追加 - アクション間に一時停止して理解を助けます
- アニメーションを待つ -
waitForLoadState('networkidle')を使用します - Remotionサイズに合わせる - 1920x1080を30fpsで使用が一般的
- 記録前にテスト - 最終キャプチャ前にデバッグします
- ブラウザ状態をクリア - クリーンなデモのために新しいコンテキストを使用
- クッキーバナーを却下 - 上記の包括的なセレクタリストを使用
- ナビゲーション時に再注入 - カーソル/リスナーはページロード時にリセットされます
フィードバックと貢献
このスキルが情報を欠いているか改善できる場合:
- パターンが不足していますか? 必要なものを説明してください
- エラーを見つけましたか? 何が間違っているか教えてください
- 貢献したいですか? 以下の方法でお手伝いします:
- 改善でこのスキルを更新する
- github.com/digitalsamba/claude-code-video-toolkit に PR を作成する
「improve this skill」と言えば、.claude/skills/playwright-recording/SKILL.md の更新をガイドします。
ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- digitalsamba
- ライセンス
- MIT
- 最終更新
- 不明
Source: https://github.com/digitalsamba/claude-code-video-toolkit / ライセンス: MIT
関連スキル
nano-banana-2
inference.sh CLIを通じてGoogle Gemini 3.1 Flash Image Preview(Nano Banana 2)で画像を生成します。テキストから画像を生成する機能、画像編集、最大14枚の複数画像入力、Google Searchグラウンディング機能に対応しています。トリガーワード:「nano banana 2」「nanobanana 2」「gemini 3.1 flash image」「gemini 3 1 flash image preview」「google image generation」
octocode-slides
洗練されたマルチファイル形式のHTMLプレゼンテーションを生成します。6段階のフロー(概要 → リサーチ → アウトライン → デザイン → 実装 → レビュー)で構成されています。各スライドは独立したHTMLファイルとなり、iframeで読み込まれます。「スライドを作成してほしい」「プレゼンテーションを作ってほしい」「HTMLスライドを生成してほしい」「デックを構築してほしい」といった依頼や、ノート・ドキュメント・コードを洗練されたプレゼンテーションに変換する際に使用できます。
gpt-image2-ppt
OpenAIのgpt-image-2を使用して、視覚的に優れたPPTスライドを生成します。Spatial Glass、Tech Blue、Editorial Monoなど10種類のキュレーション済みスタイルに対応し、ユーザーが提供したPPTXファイルを模倣するテンプレートクローンモードも搭載しています。HTMLビューアと16:9形式のPPTXファイルを出力します。プレゼンテーション、スライド、ピッチデック、投資家向けPPT、雑誌風PPTの作成依頼などで活用してください。
nano-banana
Nano Banana PRO(Gemini 3 Pro Image)およびNano Banana(Gemini 2.5 Flash Image)を使用したAI画像生成機能です。以下の場合に活用できます:(1)テキストプロンプトからの画像生成、(2)既存画像の編集、(3)インフォグラフィックス、ロゴ、商品写真、ステッカーなどのプロフェッショナルなビジュアルアセット制作、(4)複数画像での人物キャラクターの一貫性保持、(5)正確なテキスト描画を含む画像生成、(6)AI生成ビジュアルが必要なあらゆるタスク。「画像を生成」「画像を作成」「写真を作る」「ロゴをデザイン」「インフォグラフィックスを作成」「AI画像」「nano banana」またはその他の画像生成リクエストをトリガーとして機能します。
oiloil-ui-ux-guide
モダンでクリーンなUI/UXガイダンス・レビュースキルです。新機能や既存システム(Webアプリ)に対して、実行可能なUI/UX改善提案、デザイン原則、デザインレビューチェックリストが必要な場合に活用できます。CRAP(コントラスト・反復・配置・近接)をベースに、タスクファーストなUX、情報設計、フィードバック・システムステータス、一貫性、affordances、エラー防止・復旧、認知負荷を重視します。モダンミニマルスタイル(クリーン・余白・タイポグラフィ主導)を強制し、不要なテキストを削減、アイコンとしての絵文字を禁止し、統一されたアイコンセットから直感的で洗練されたアイコンを推奨します。
axiom-hig-ref
Apple Human Interface Guidelines リファレンス — 色(セマンティックカラー、カスタムカラー、パターン)、背景(マテリアル階層、ダイナミック背景)、タイポグラフィ(標準スタイル、カスタムフォント、Dynamic Type)、SF Symbols(レンダリングモード、色、多言語対応)、ダークモード、アクセシビリティ、プラットフォーム固有の考慮事項を網羅したガイドラインです。