app-studio-demo-capture について
認証済みブラウザーでデプロイ済みの Domo App Studio アプリを録画する Playwright ベースのスキルです。実際のページとライブデータをスクリーンショットやウォークスルー動画に収め、ffmpeg で後処理できます。
主な機能
- 01アプリの各ページをスクリーンショットで記録
- 02ページごとのスクロール動画を録画
- 03複数ページを連続した動画に収録
- 04認証済みの Domo アプリ画面を実データ付きで取得
- 05ffmpeg によるメディアの後処理と結合
活用シーン
- 01関係者レビュー用のアプリ紹介動画を作成
- 02評価やドキュメント用のスクリーンショットを取得
- 03デプロイ済み App Studio アプリのデモ動画を作成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ stahura/ domo-ai-vibe-rules --skill app-studio-demo-captureインストール後、Claude Code で/app-studio-demo-captureを入力して読み込まれたか確認します。
詳細
App Studio デモキャプチャ
これは何ですか:Playwright ベースのパイプラインです。認証済みのヘッドレスブラウザーで実際の Domo App Studio アプリを開き、ページを移動してスクロールし、洗練されたデモ動画を録画します。左側のナビゲーション、ネイティブカード、プロコードの iframe、適用済みのテーマ、ライブデータを含む、本番環境の体験全体をキャプチャします。
これは何ではありませんか:Remotion ではありません。Node.js で React コンポーネントをレンダリングしたり、サンプルデータを使ったりしません。合成データとアニメーションカーソルを使ったコンポーネント単位のレンダリングが必要な場合は、代わりに
basic-custom-app-build-w-videoを使用してください。
使用する場面
app-studioとapp-studio-pro-codeのパイプラインを通じて App Studio アプリをデプロイした後、ウォークスルー動画を作成したい場合- ユーザーが「デモ」、「ウォークスルー」、「アニメーションプレビュー」、「アプリを録画して」または「どのように見えるか見せて」と言った場合
- 比較やドキュメント用の評価スクリーンショットを生成する場合
- ピッチデッキ、ステークホルダー向けレビュー、マーケティング用のコンテンツを作成する場合
前提条件
- アプリがデプロイ済みであること — すべてのページ、カード、テーマ、データを含む App Studio アプリが Domo インスタンスに存在している
- Playwright がインストール済みであること — プロジェクトディレクトリで
npm install playwrightを実行している - Domo 認証 —
domo loginまたはupload_bridge.get_sid()ヘルパーで取得できる有効なセッション ID - ffmpeg がインストール済みであること(後処理用)— macOS では
brew install ffmpeg
パイプラインの概要
┌─────────────┐ ┌──────────────┐ ┌───────────────┐ ┌──────────────┐
│ Authenticate │ ──▶ │ Capture │ ──▶ │ Post-process │ ──▶ │ Output │
│ (get SID) │ │ (Playwright) │ │ (ffmpeg) │ │ (.mp4/.gif) │
└─────────────┘ └──────────────┘ └───────────────┘ └──────────────┘
複雑さの低い順に、3 つのキャプチャモードがあります。
| モード | 方法 | 出力 | 最適な用途 |
|---|---|---|---|
| スクリーンショット | ページごとに page.screenshot() | PNG | 静的な評価、デッキのスライド、ドキュメント |
| スクロール動画 | Playwright の recordVideo +スクリプトによるスクロール | ページごとの生 MP4 | 滑らかなページのウォークスルー |
| フルウォークスルー | Playwright の recordVideo +すべてのページをまたいだナビゲーションとスクロール | 単一の MP4 | 洗練されたデモ動画 |
モード 1:複数ページのスクリーンショット(実績のあるパターン)
これは既存の screenshot.js パターンです。多数の App Studio アプリで十分に検証されています。
使用方法
node screenshot.js app <appId> '<json-pageIds>' [outputDir]
例
node screenshot.js app 396231467 '{"Overview":"1942764543","Sales":"1942764544","Inventory":"1942764545","Performance":"1942764546"}' ./demo-output/screenshots
Python ビルドスクリプトへの統合
import subprocess, json
page_ids = {"Overview": str(overview_id), "Sales": str(sales_id), ...}
subprocess.run([
"node", "screenshot.js", "app", str(app_id),
json.dumps(page_ids), f"./{app_name}/screenshots"
], check=True)
主な実装の詳細(実績のある screenshot.js に基づく)
- ビューポート:
deviceScaleFactor: 2を設定した 1440x900(2880x1800 の Retina PNG が生成されます) - 認証:
page.route('**/*')によるインターセプトを通じて、X-Domo-Authenticationヘッダーを挿入します - 待機戦略:
waitUntil: 'networkidle'に加えてwaitForTimeout(8000)を実行し、プロコードの iframe がレンダリングされるのを待ちます - ページ全体:
fullPage: trueを設定すると、画面を下にスクロールした位置にあるコンテンツもキャプチャします - エラー復旧:ナビゲーションに失敗した場合、デバッグ用に
_error.pngをキャプチャします
補助アセットもキャプチャする
# Dataset details pages
node screenshot.js datasets '{"sales":"guid1","inventory":"guid2"}' ./demo-output/datasets
# Magic ETL dataflow graph
node screenshot.js etl <dataflowId> ./demo-output/etl
# Arbitrary Domo URLs
node screenshot.js urls '{"landing":"https://modocorp.domo.com/page/123"}' ./demo-output/urls
モード 2:ページごとのスクロール動画
各ページを個別に、滑らかにスクロールする様子を録画します。ページごとに 1 つの MP4 が生成されます。
スクリプト:demo-capture.js
const { chromium } = require('playwright');
const { execSync } = require('child_process');
const path = require('path');
const fs = require('fs');
const INSTANCE = 'modocorp';
function getSid() {
const result = execSync(
`cd "${path.join(__dirname, 'domo_data_generator')}" && python3 -c "from upload_bridge import get_sid; print(get_sid('${INSTANCE}'));"`,
{ encoding: 'utf-8' }
).trim();
return result.split('\n').pop().trim();
}
async function capturePageVideo(browser, sid, appId, pageName, pageId, outputDir, options = {}) {
const {
scrollDistance = 600,
scrollStepPx = 2,
scrollIntervalMs = 16,
holdTopMs = 3000,
holdBottomMs = 2000,
loadWaitMs = 8000,
} = options;
const videoDir = path.join(outputDir, '_raw_video');
fs.mkdirSync(videoDir, { recursive: true });
const context = await browser.newContext({
viewport: { width: 1440, height: 900 },
recordVideo: { dir: videoDir, size: { width: 1440, height: 900 } },
});
const page = await context.newPage();
await page.route('**/*', (route, request) => {
const url = request.url();
if (url.includes('.domo.com') || url.includes('domoapps.')) {
route.continue({ headers: { ...request.headers(), 'X-Domo-Authentication': sid } });
} else {
route.continue();
}
});
const url = `https://${INSTANCE}.domo.com/app-studio/${appId}/pages/${pageId}`;
console.log(` Recording ${pageName}: ${url}`);
await page.goto(url, { waitUntil: 'networkidle', timeout: 60000 });
await page.waitForTimeout(loadWaitMs);
// Hold at top
await page.waitForTimeout(holdTopMs);
// Smooth scroll down
const scrollSteps = Math.ceil(scrollDistance / scrollStepPx);
for (let i = 0; i < scrollSteps; i++) {
await page.evaluate((px) => window.scrollBy(0, px), scrollStepPx);
await page.waitForTimeout(scrollIntervalMs);
}
// Hold at bottom
await page.waitForTimeout(holdBottomMs);
// Close context to finalize video
const videoPath = await page.video().path();
await context.close();
// Move to final location
const finalPath = path.join(outputDir, `${pageName}.webm`);
fs.renameSync(videoPath, finalPath);
console.log(` Saved: ${finalPath}`);
return finalPath;
}
async function captureAllPages(appId, pageIds, outputDir, options = {}) {
fs.mkdirSync(outputDir, { recursive: true });
const sid = getSid();
console.log(`SID obtained (${sid.substring(0, 12)}...)`);
const browser = await chromium.launch({ headless: true });
const videoPaths = [];
for (const [pageName, pageId] of Object.entries(pageIds)) {
const vp = await capturePageVideo(browser, sid, appId, pageName, pageId, outputDir, options);
videoPaths.push(vp);
}
await browser.close();
console.log('All pages recorded.');
return videoPaths;
}
// CLI
const args = process.argv.slice(2);
if (args.length < 2) {
console.log('Usage: node demo-capture.js <appId> <json-pageIds> [outputDir] [scrollDistance]');
process.exit(1);
}
const appId = args[0];
const pageIds = JSON.parse(args[1]);
const outputDir = args[2] || path.join(__dirname, 'demo-output', `app-${appId}`);
const scrollDistance = args[3] ? parseInt(args[3]) : 600;
captureAllPages(appId, pageIds, outputDir, { scrollDistance })
.catch(err => { console.error('Fatal:', err); process.exit(1); });
使用方法
node demo-capture.js 396231467 '{"Overview":"1942764543","Sales":"1942764544"}' ./demo-output 800
モード 3:フルウォークスルー動画
すべてのページを移動する連続した動画を 1 つ録画し、ユーザーが左側のナビゲーションをクリックして進む様子を再現します。この方法で、最も洗練された出力を作成できます。
スクリプト:demo-walkthrough.js
const { chromium } = require('playwright');
const { execSync } = require('child_process');
const path = require('path');
const fs = require('fs');
const INSTANCE = 'modocorp';
function getSid() {
const result = execSync(
`cd "${path.join(__dirname, 'domo_data_generator')}" && python3 -c "from upload_bridge import get_sid; print(get_sid('${INSTANCE}'));"`,
{ encoding: 'utf-8' }
).trim();
return result.split('\n').pop().trim();
}
async function recordWalkthrough(appId, pageIds, outputDir, options = {}) {
const {
scrollDistance = 600,
scrollStepPx = 2,
scrollIntervalMs = 16,
holdPageMs = 3000,
loadWaitMs = 8000,
transitionPauseMs = 2000,
} = options;
fs.mkdirSync(outputDir, { recursive: true });
const videoDir = path.join(outputDir, '_raw_video');
fs.mkdirSync(videoDir, { recursive: true });
const sid = getSid();
console.log(`SID obtained (${sid.substring(0, 12)}...)`);
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
viewport: { width: 1440, height: 900 },
recordVideo: { dir: videoDir, size: { width: 1440, height: 900 } },
});
const page = await context.newPage();
await page.route('**/*', (route, request) => {
const url = request.url();
if (url.includes('.domo.com') || url.includes('domoapps.')) {
route.continue({ headers: { ...request.headers(), 'X-Domo-Authentication': sid } });
} else {
route.continue();
}
});
const entries = Object.entries(pageIds);
console.log(`Recording walkthrough: ${entries.length} pages`);
for (let i = 0; i < entries.length; i++) {
const [pageName, pageId] = entries[i];
const url = `https://${INSTANCE}.domo.com/app-studio/${appId}/pages/${pageId}`;
console.log(` Page ${i + 1}/${entries.length}: ${pageName}`);
await page.goto(url, { waitUntil: 'networkidle', timeout: 60000 });
await page.waitForTimeout(i === 0 ? loadWaitMs : loadWaitMs / 2);
// Hold at top
await page.waitForTimeout(holdPageMs);
// Smooth scroll
const scrollSteps = Math.ceil(scrollDistance / scrollStepPx);
for (let s = 0; s < scrollSteps; s++) {
await page.evaluate((px) => window.scrollBy(0, px), scrollStepPx);
await page.waitForTimeout(scrollIntervalMs);
}
// Hold at scroll position
await page.waitForTimeout(holdPageMs / 2);
// Scroll back to top before navigating to next page
await page.evaluate(() => window.scrollTo({ top: 0, behavior: 'instant' }));
await page.waitForTimeout(transitionPauseMs);
}
const videoPath = await page.video().path();
await context.close();
await browser.close();
// Move raw video to output
const rawPath = path.join(outputDir, 'walkthrough_raw.webm');
fs.renameSync(videoPath, rawPath);
console.log(`Raw walkthrough: ${rawPath}`);
return rawPath;
}
// CLI
const args = process.argv.slice(2);
if (args.length < 2) {
console.log('Usage: node demo-walkthrough.js <appId> <json-pageIds> [outputDir]');
process.exit(1);
}
recordWalkthrough(args[0], JSON.parse(args[1]), args[2] || `./demo-output/app-${args[0]}`)
.catch(err => { console.error('Fatal:', err); process.exit(1); });
使用方法
node demo-walkthrough.js 396231467 \
'{"Overview":"1942764543","Sales":"1942764544","Inventory":"1942764545","Performance":"1942764546"}' \
./demo-output
ffmpeg による後処理
WebM を MP4 に変換する
Playwright は WebM(VP8)で録画します。幅広く互換性のある MP4(H.264)に変換します。
ffmpeg -i walkthrough_raw.webm -c:v libx264 -preset slow -crf 18 -pix_fmt yuv420p -movflags +faststart walkthrough.mp4
ページごとの動画を 1 つにつなげる
モード 2(ページごとの動画)を使用する場合は、動画を連結します。
# Create concat list
echo "file 'Overview.webm'" > concat.txt
echo "file 'Sales.webm'" >> concat.txt
echo "file 'Inventory.webm'" >> concat.txt
echo "file 'Performance.webm'" >> concat.txt
# Concatenate and convert
ffmpeg -f concat -safe 0 -i concat.txt -c:v libx264 -preset slow -crf 18 -pix_fmt yuv420p demo.mp4
ページ間にクロスフェードトランジションを追加する
ページ間に 0.5 秒のクロスフェードを入れた、洗練された仕上がりにします。
ffmpeg \
-i Overview.webm -i Sales.webm -i Inventory.webm -i Performance.webm \
-filter_complex "\
[0:v]setpts=PTS-STARTPTS[v0]; \
[1:v]setpts=PTS-STARTPTS[v1]; \
[2:v]setpts=PTS-STARTPTS[v2]; \
[3:v]setpts=PTS-STARTPTS[v3]; \
[v0][v1]xfade=transition=fade:duration=0.5:offset=4[x01]; \
[x01][v2]xfade=transition=fade:duration=0.5:offset=8[x02]; \
[x02][v3]xfade=transition=fade:duration=0.5:offset=12[out]" \
-map "[out]" -c:v libx264 -preset slow -crf 18 -pix_fmt yuv420p demo.mp4
各クリップの長さに応じて offset の値を調整してください。offset はクロスフェードが始まる時刻(秒)です。
正確な長さにトリミングする
ffmpeg -i demo.mp4 -t 30 -c copy demo_30s.mp4
イントロ/アウトロのタイトルカードを追加する
PNG から 2 秒のタイトルカードを作成します。
# Generate title card video from image
ffmpeg -loop 1 -i title.png -c:v libx264 -t 2 -pix_fmt yuv420p -vf "scale=1440:900" title.mp4
# Prepend to walkthrough
ffmpeg -f concat -safe 0 -i <(echo -e "file 'title.mp4'\nfile 'walkthrough.mp4'") -c copy final.mp4
アニメーション GIF を生成する(Slack/ドキュメント用)
ffmpeg -i walkthrough.mp4 -vf "fps=12,scale=720:-1:flags=lanczos" -loop 0 demo.gif
タイミングの推奨値
| ページのコンテンツ | holdPageMs | scrollDistance | loadWaitMs |
|---|---|---|---|
| KPI 行+チャート(一般的なケース) | 3000 | 400 | 8000 |
| 情報量の多いダッシュボード(カード多数) | 4000 | 800 | 10000 |
| 単一のチャート(プロコード) | 2500 | 200 | 8000 |
| データテーブル | 3000 | 600 | 6000 |
| 最初のページ(コールドロード) | 3000 | 600 | 10000 |
動画の合計時間の目安
| ページ数 | ページごとの表示時間 | スクロール時間 | 切り替え時間 | 合計 |
|---|---|---|---|---|
| 4 | 各約5秒 | 各約3秒 | 各約2秒 | 約38秒 |
| 4(短縮版) | 各約4秒 | 各約2秒 | 各約1秒 | 約28秒 |
| 6 | 各約4秒 | 各約2秒 | 各約1秒 | 約42秒 |
洗練されたデモの目標時間は25~35秒です。20秒未満では慌ただしく感じられ、45秒を超えると視聴者の注意がそれてしまいます。
ビルドパイプラインとの統合
build_app.py の完了後(app-build-process-summary のステップ1~8)、キャプチャステップを追加します。
import subprocess, json
# Step 9: Capture demo
page_ids = {
"Overview": str(overview_page_id),
"Sales": str(sales_page_id),
"Inventory": str(inventory_page_id),
"Performance": str(performance_page_id),
}
# Screenshots (always — for eval and deck slides)
subprocess.run([
"node", "screenshot.js", "app", str(app_id),
json.dumps(page_ids), f"./{app_name}/screenshots"
], check=True)
# Animated walkthrough video
subprocess.run([
"node", "demo-walkthrough.js", str(app_id),
json.dumps(page_ids), f"./{app_name}/demo"
], check=True)
# Post-process to MP4
subprocess.run([
"ffmpeg", "-i", f"./{app_name}/demo/walkthrough_raw.webm",
"-c:v", "libx264", "-preset", "slow", "-crf", "18",
"-pix_fmt", "yuv420p", "-movflags", "+faststart",
f"./{app_name}/demo/walkthrough.mp4"
], check=True)
basic-custom-app-build-w-video との比較
| 項目 | app-studio-demo-capture(このスキル) | basic-custom-app-build-w-video |
|---|---|---|
| キャプチャ対象 | ブラウザー上の、デプロイ済みの実際の App Studio アプリ | Remotion の React コンポーネント |
| データ | Domo のライブデータ | 合成された TypeScript 配列 |
| Chrome | App Studio 全体(ナビゲーション、レイアウト、カード、テーマ) | コンポーネントのみ(ナビゲーション/レイアウトなし) |
| 認証 | Domo セッション ID が必要 | 認証不要 |
| ツール | Playwright+ffmpeg | Remotion+@remotion/cli |
| コードの変更 | 不要 — デプロイ済みの任意のアプリをキャプチャ | Remotion に対応したスタイル設定の制約が必要 |
| カーソルアニメーション | 含まれません(実際のブラウザーのカーソル) | AnimatedCursor コンポーネント |
| 最適な用途 | App Studio アプリ、評価用スクリーンショット、関係者向けデモ | スタンドアロンのカスタムアプリ、製品マーケティング |
両方を併用してください:アニメーションカーソル付きのコンポーネントレベルのヒーローショットには basic-custom-app-build-w-video を、Chrome を含む本番環境の一連の操作の紹介には app-studio-demo-capture を使用します。ffmpeg で両方をつなぎ合わせて、最終的な動画を作成します。
トラブルシューティング
| 症状 | 原因 | 解決方法 |
|---|---|---|
| プロコードのカードが空白/白く表示される | iframe の読み込みが完了していません | loadWaitMs を 10000~12000 に増やします |
| チャートにデータが表示されない | データクエリの処理がまだ完了していません | loadWaitMs を増やし、page.waitForSelector('.recharts-wrapper') を追加します |
| 左側のナビゲーションが折りたたまれる/非表示になる | ビューポートの幅が狭すぎます | ビューポートの幅が 1280 以上であることを確認します |
| ログインページへの認証リダイレクトが発生する | SID の有効期限が切れています | domo login を再実行し、SID を再取得します |
| 動画が途切れる/遅くなる | 負荷が高い状態でヘッドレスレンダリングを行っています | --disable-gpu フラグを使用し、scrollStepPx を 1 に減らします |
| WebM を再生できない | プレーヤーが VP8 に対応していません | ffmpeg で MP4 に変換します |
| スクリーンショットのファイルサイズが大きすぎる | deviceScaleFactor: 2 と fullPage: true の組み合わせ | 動画には deviceScaleFactor: 1 を使用し、スクリーンショットには 2x を維持します |
出力チェックリスト
パイプラインの実行後、以下を確認します。
- すべてのページのスクリーンショットが存在する(
_error.pngファイルがない) - プロコードのバナーとチャートが表示されている(空白ではない)
- テーマが適用されている(色、フォント、ナビゲーションのスタイルが正しい)
- 動画が滑らかである(フレーム落ちや、読み込み中のスピナーが映り込んでいない)
- 動画の長さが 25~35 秒である
- MP4 が QuickTime/VLC で再生できる(H.264 ベースラインプロファイル)
- 左側のナビゲーションが表示され、正しいページ名/アイコンが示されている
- データが表示されている(チャートに線/棒があり、KPI に値がある)