mcpField
A
app-studio-demo-capture

app-studio-demo-capture

github.com/stahuraクリエイティブ・メディア2435 回インストール26 週間前に更新
リポジトリ

app-studio-demo-capture について

認証済みブラウザーでデプロイ済みの Domo App Studio アプリを録画する Playwright ベースのスキルです。実際のページとライブデータをスクリーンショットやウォークスルー動画に収め、ffmpeg で後処理できます。

主な機能

  1. 01アプリの各ページをスクリーンショットで記録
  2. 02ページごとのスクロール動画を録画
  3. 03複数ページを連続した動画に収録
  4. 04認証済みの Domo アプリ画面を実データ付きで取得
  5. 05ffmpeg によるメディアの後処理と結合

活用シーン

  1. 01関係者レビュー用のアプリ紹介動画を作成
  2. 02評価やドキュメント用のスクリーンショットを取得
  3. 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 アプリをデプロイした後、ウォークスルー動画を作成したい場合
  • ユーザーが「デモ」、「ウォークスルー」、「アニメーションプレビュー」、「アプリを録画して」または「どのように見えるか見せて」と言った場合
  • 比較やドキュメント用の評価スクリーンショットを生成する場合
  • ピッチデッキ、ステークホルダー向けレビュー、マーケティング用のコンテンツを作成する場合

前提条件

  1. アプリがデプロイ済みであること — すべてのページ、カード、テーマ、データを含む App Studio アプリが Domo インスタンスに存在している
  2. Playwright がインストール済みであること — プロジェクトディレクトリで npm install playwright を実行している
  3. Domo 認証 — domo login または upload_bridge.get_sid() ヘルパーで取得できる有効なセッション ID
  4. 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

タイミングの推奨値

ページのコンテンツholdPageMsscrollDistanceloadWaitMs
KPI 行+チャート(一般的なケース)30004008000
情報量の多いダッシュボード(カード多数)400080010000
単一のチャート(プロコード)25002008000
データテーブル30006006000
最初のページ(コールドロード)300060010000
動画の合計時間の目安
ページ数ページごとの表示時間スクロール時間切り替え時間合計
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 配列
ChromeApp Studio 全体(ナビゲーション、レイアウト、カード、テーマ)コンポーネントのみ(ナビゲーション/レイアウトなし)
認証Domo セッション ID が必要認証不要
ツールPlaywright+ffmpegRemotion+@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 に値がある)

コメント