promo-video

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Promo Video Recording

宣传视频录制

Record smooth, autonomous promo footage of a Phaser game for marketing / social media. The output is a 50 FPS MP4 in mobile portrait (9:16) — ready for TikTok, Reels, Moltbook, or X.
为营销/社交媒体平台录制流畅的Phaser游戏自动宣传片段,输出为9:16移动端竖屏的50 FPS MP4文件,可直接用于TikTok、Reels、Moltbook或X平台。

Technique

实现技术

Playwright's
recordVideo
caps at 25 FPS with no config option. We work around it:
  1. Slow the game to 0.5× by patching all 5 Phaser time subsystems
  2. Record for 2× the desired duration at Playwright's native 25 FPS
  3. FFmpeg speed-up 2× → effective 50 FPS output
ParameterDefaultEffect
SLOW_MO_FACTOR
0.5
Game runs at half speed → 50 FPS output
WALL_CLOCK_DURATION
DESIRED_GAME_DURATION / SLOW_MO_FACTOR
Record for 2× to get correct game-time
VIEWPORT
{ width: 1080, height: 1920 }
9:16 mobile portrait (always default unless user specifies otherwise)
DESIRED_GAME_DURATION
13000
(ms)
~13s of game-time → ~6.5s promo clip
Playwright的
recordVideo
功能默认帧率上限为25FPS且无自定义配置选项,我们通过以下方案绕开限制:
  1. 将游戏速度降至0.5倍:通过修改Phaser的全部5个时间子系统实现
  2. 以Playwright原生25FPS录制2倍所需时长的内容
  3. 通过FFmpeg将视频2倍速加速 → 最终得到等效50FPS的输出
参数默认值作用
SLOW_MO_FACTOR
0.5
游戏以半速运行 → 输出50FPS视频
WALL_CLOCK_DURATION
DESIRED_GAME_DURATION / SLOW_MO_FACTOR
录制2倍时长以得到正确的游戏内时长
VIEWPORT
{ width: 1080, height: 1920 }
9:16移动端竖屏(无用户特殊指定时默认使用该配置)
DESIRED_GAME_DURATION
13000
(毫秒)
约13秒游戏内时长 → 输出约6.5秒宣传片段

Prerequisites

前置依赖

  • Playwright — must be installed (
    npm install -D @playwright/test && npx playwright install chromium
    )
  • FFmpeg — must be available on PATH (
    brew install ffmpeg
    on macOS)
  • Dev server running — game must be served on localhost
Check both before starting:
bash
npx playwright --version
ffmpeg -version | head -1
If FFmpeg is not found, warn the user and skip the promo video step (it's non-blocking — the game still works without it).
  • Playwright — 必须已安装(执行
    npm install -D @playwright/test && npx playwright install chromium
  • FFmpeg — 必须已添加到系统PATH中(macOS可通过
    brew install ffmpeg
    安装)
  • 开发服务正在运行 — 游戏必须已在本地localhost上启动
开始录制前请先检查依赖是否正常:
bash
npx playwright --version
ffmpeg -version | head -1
如果未找到FFmpeg,请向用户发出警告并跳过宣传视频生成步骤(该步骤是非阻塞的,没有FFmpeg游戏也可以正常运行)。

Capture Script — Game-Specific Adaptation

捕捉脚本 — 针对游戏适配调整

Every game gets a custom
scripts/capture-promo.mjs
. The subagent must read the game's source files to determine:
每个游戏都需要一个自定义的
scripts/capture-promo.mjs
脚本。子Agent 必须读取游戏的源文件 来确定以下配置:

1. Death/Failure Patching (CRITICAL)

1. 死亡/失败逻辑屏蔽(关键步骤)

The video must show continuous gameplay — never game over. Read
GameScene.js
(or equivalent) to find the death/failure method and monkey-patch it out.
How to find it: Search for the method called on collision/death. Common patterns:
  • this.triggerGameOver()
    — dodge games
  • this.takeDamage()
    this.lives <= 0
    — multi-life games
  • this.gameOver()
    — direct call
  • eventBus.emit(Events.PLAYER_HIT)
    /
    eventBus.emit(Events.GAME_OVER)
    — event-driven
Patch template (adapt per game):
js
await page.evaluate(() => {
  const scene = window.__GAME__.scene.getScene('GameScene');
  if (scene) {
    // Patch ALL paths to game over
    scene.triggerGameOver = () => {};
    scene.onPlayerHit = () => {};
    // For multi-life games, also prevent damage:
    // scene.takeDamage = () => {};
    // scene.playerDied = () => {};
  }
});
视频需要展示连续的游戏玩法,不能出现游戏结束的画面。读取
GameScene.js
(或对应的场景文件)找到死亡/失败触发方法,通过猴子补丁将其功能屏蔽。
查找方法:搜索碰撞/死亡时调用的方法,常见的模式包括:
  • this.triggerGameOver()
    — 闪避类游戏
  • this.takeDamage()
    this.lives <= 0
    — 多生命类游戏
  • this.gameOver()
    — 直接调用的结束方法
  • eventBus.emit(Events.PLAYER_HIT)
    /
    eventBus.emit(Events.GAME_OVER)
    — 事件驱动模式
补丁模板(根据游戏情况调整):
js
await page.evaluate(() => {
  const scene = window.__GAME__.scene.getScene('GameScene');
  if (scene) {
    // 屏蔽所有触发游戏结束的路径
    scene.triggerGameOver = () => {};
    scene.onPlayerHit = () => {};
    // 多生命类游戏还需要屏蔽受伤逻辑:
    // scene.takeDamage = () => {};
    // scene.playerDied = () => {};
  }
});

2. Input Sequence Generation

2. 输入序列生成

The video must show dynamic, natural-looking gameplay. Read the game's input handling to determine:
  • Which keys — ArrowLeft/ArrowRight? Space? WASD? Mouse clicks?
  • Input style — continuous hold (movement), tap (jump/shoot), or both?
  • Movement pattern — should the player sweep across the screen, dodge reactively, jump rhythmically?
Input patterns by game type:
Game TypeInput KeysPattern
Side dodgerArrowLeft, ArrowRightAlternating holds (150-600ms) with variable pauses, occasional double-taps
Platformer / FlappySpaceRhythmic taps (80-150ms hold) with variable gaps (200-800ms)
Top-downWASD / ArrowsMixed directional holds, figure-eight patterns
ShooterArrowLeft/Right + SpaceMovement interleaved with rapid fire
Clicker/TapperMouse click / SpaceRapid bursts separated by brief pauses
Randomize timing to avoid robotic-looking movement:
js
const holdMs = 150 + Math.floor(Math.random() * 450);
const pauseMs = 50 + Math.floor(Math.random() * 250);
Add a pause at the start (1-2s) to let the entrance animation play — this is the hook.
视频需要展示动态、自然的游戏玩法。读取游戏的输入处理逻辑来确定以下内容:
  • 支持的按键:是左/右方向键?空格?WASD?鼠标点击?
  • 输入风格:是长按(移动)、点按(跳跃/射击)还是两者结合?
  • 移动模式:玩家应该是横扫屏幕、反应式闪避、还是有节奏地跳跃?
不同游戏类型对应的输入模式:
游戏类型输入按键模式
横向闪避类左方向键、右方向键交替长按(150-600毫秒)搭配可变时长停顿,偶尔穿插双击
平台跳跃 / 类Flappy游戏空格有节奏的点按(80-150毫秒长按)搭配可变间隔(200-800毫秒)
俯视视角游戏WASD / 方向键混合方向长按,走八字形路线
射击类左/右方向键 + 空格移动和快速射击交替进行
点击类鼠标点击 / 空格快速点按 burst 搭配短暂停顿
随机化时间间隔避免动作看起来像机器人:
js
const holdMs = 150 + Math.floor(Math.random() * 450);
const pauseMs = 50 + Math.floor(Math.random() * 250);
在开头添加1-2秒停顿让入场动画播放完毕 —— 这是吸引用户的钩子。

3. Game Boot Detection

3. 游戏启动检测

All games built with the make-game pipeline expose these globals:
  • window.__GAME__
    — Phaser.Game instance
  • window.__GAME_STATE__
    — GameState singleton
  • window.__EVENT_BUS__
    — EventBus singleton
Wait for both boot and active gameplay:
js
await page.waitForFunction(() => window.__GAME__?.isBooted, { timeout: 15000 });
await page.waitForFunction(() => window.__GAME_STATE__?.started, { timeout: 10000 });
所有基于make-game流水线构建的游戏都会暴露以下全局变量:
  • window.__GAME__
    — Phaser.Game实例
  • window.__GAME_STATE__
    — GameState单例
  • window.__EVENT_BUS__
    — EventBus单例
等待游戏启动完成并进入可玩状态:
js
await page.waitForFunction(() => window.__GAME__?.isBooted, { timeout: 15000 });
await page.waitForFunction(() => window.__GAME_STATE__?.started, { timeout: 10000 });

4. Time Scaling Injection

4. 时间缩放注入

Slow all 5 Phaser time subsystems for the recording:
js
await page.evaluate(({ factor }) => {
  const game = window.__GAME__;
  const scene = game.scene.getScene('GameScene');

  // 1. Update delta — slows frame-delta-dependent logic
  const originalUpdate = scene.update.bind(scene);
  scene.update = function(time, delta) {
    originalUpdate(time, delta * factor);
  };

  // 2. Tweens — slows all tween animations
  scene.tweens.timeScale = factor;

  // 3. Scene timers — slows scene.time.addEvent() timers
  scene.time.timeScale = factor;

  // 4. Physics — slows Arcade/Matter physics
  // NOTE: Arcade physics timeScale is INVERSE (higher = slower)
  if (scene.physics?.world) {
    scene.physics.world.timeScale = 1 / factor;
  }

  // 5. Animations — slows sprite animation playback
  if (scene.anims) {
    scene.anims.globalTimeScale = factor;
  }
}, { factor: SLOW_MO_FACTOR });
The 5 subsystems:
  1. Update delta
    scene.update(time, delta * factor)
    slows frame-delta-dependent logic
  2. Tweens
    scene.tweens.timeScale
    slows all tween animations
  3. Scene timers
    scene.time.timeScale
    slows
    scene.time.addEvent()
    timers
  4. Physics
    scene.physics.world.timeScale
    slows Arcade/Matter physics (uses inverse:
    1/factor
    )
  5. Animations
    scene.anims.globalTimeScale
    slows sprite animation playback
录制时放慢Phaser的全部5个时间子系统:
js
await page.evaluate(({ factor }) => {
  const game = window.__GAME__;
  const scene = game.scene.getScene('GameScene');

  // 1. 更新delta —— 放慢依赖帧delta的逻辑
  const originalUpdate = scene.update.bind(scene);
  scene.update = function(time, delta) {
    originalUpdate(time, delta * factor);
  };

  // 2. 补间动画 —— 放慢所有tween动画
  scene.tweens.timeScale = factor;

  // 3. 场景计时器 —— 放慢scene.time.addEvent()创建的计时器
  scene.time.timeScale = factor;

  // 4. 物理引擎 —— 放慢Arcade/Matter物理引擎
  // 注意:Arcade物理引擎的timeScale是倒数关系(数值越大速度越慢)
  if (scene.physics?.world) {
    scene.physics.world.timeScale = 1 / factor;
  }

  // 5. 精灵动画 —— 放慢精灵动画播放速度
  if (scene.anims) {
    scene.anims.globalTimeScale = factor;
  }
}, { factor: SLOW_MO_FACTOR });
5个时间子系统说明:
  1. 更新delta ——
    scene.update(time, delta * factor)
    放慢依赖帧delta的逻辑
  2. 补间动画 ——
    scene.tweens.timeScale
    放慢所有tween动画
  3. 场景计时器 ——
    scene.time.timeScale
    放慢
    scene.time.addEvent()
    创建的计时器
  4. 物理引擎 ——
    scene.physics.world.timeScale
    放慢Arcade/Matter物理引擎(使用倒数:
    1/factor
  5. 精灵动画 ——
    scene.anims.globalTimeScale
    放慢精灵动画播放速度

5. Video Finalization

5. 视频生成完成

js
const video = page.video();
await context.close();  // MUST close context to finalize the video file
const videoPath = await video.path();
js
const video = page.video();
await context.close();  // 必须关闭context才能完成视频文件的写入
const videoPath = await video.path();

Full Capture Script Template

完整捕捉脚本模板

js
import { chromium } from 'playwright';
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const PROJECT_DIR = path.resolve(__dirname, '..');

// --- Config ---
const args = process.argv.slice(2);
function getArg(name, fallback) {
  const i = args.indexOf(`--${name}`);
  return i !== -1 && args[i + 1] ? args[i + 1] : fallback;
}

const PORT = getArg('port', '3000');
const GAME_URL = `http://localhost:${PORT}/`;
const VIEWPORT = { width: 1080, height: 1920 }; // 9:16 mobile portrait
const SLOW_MO_FACTOR = 0.5;
const DESIRED_GAME_DURATION = parseInt(getArg('duration', '13000'), 10);
const WALL_CLOCK_DURATION = DESIRED_GAME_DURATION / SLOW_MO_FACTOR;
const OUTPUT_DIR = path.resolve(PROJECT_DIR, getArg('output-dir', 'output'));
const OUTPUT_FILE = path.join(OUTPUT_DIR, 'promo-raw.webm');

// <ADAPT: Generate game-specific input sequence>
function generateInputSequence(totalMs) {
  const sequence = [];
  let elapsed = 0;

  // Pause for entrance animation
  sequence.push({ key: null, holdMs: 0, pauseMs: 1500 });
  elapsed += 1500;

  // <ADAPT: Replace with game-specific keys and timing>
  const keys = ['ArrowLeft', 'ArrowRight'];
  let keyIdx = 0;

  while (elapsed < totalMs) {
    const holdMs = 150 + Math.floor(Math.random() * 450);
    const pauseMs = 50 + Math.floor(Math.random() * 250);

    // Occasional double-tap for variety
    if (Math.random() < 0.15) {
      sequence.push({ key: keys[keyIdx], holdMs: 100, pauseMs: 60 });
      elapsed += 160;
    }

    sequence.push({ key: keys[keyIdx], holdMs, pauseMs });
    elapsed += holdMs + pauseMs;

    // Alternate direction (with occasional same-direction repeats)
    if (Math.random() < 0.75) keyIdx = 1 - keyIdx;
  }

  return sequence;
}

async function captureGameplay() {
  console.log('Capturing promo video...');
  console.log(`  URL: ${GAME_URL} | Viewport: ${VIEWPORT.width}x${VIEWPORT.height}`);
  console.log(`  Game duration: ${DESIRED_GAME_DURATION}ms | Wall clock: ${WALL_CLOCK_DURATION}ms`);

  fs.mkdirSync(OUTPUT_DIR, { recursive: true });

  const browser = await chromium.launch({ headless: true });
  const context = await browser.newContext({
    viewport: VIEWPORT,
    recordVideo: { dir: OUTPUT_DIR, size: VIEWPORT },
  });

  const page = await context.newPage();
  await page.goto(GAME_URL, { waitUntil: 'networkidle' });

  // Wait for game boot + gameplay active
  await page.waitForFunction(() => window.__GAME__?.isBooted, { timeout: 15000 });
  await page.waitForFunction(() => window.__GAME_STATE__?.started, { timeout: 10000 });
  await page.waitForTimeout(300);
  console.log('  Game active.');

  // <ADAPT: Patch out death — find the actual methods from GameScene.js>
  await page.evaluate(() => {
    const scene = window.__GAME__.scene.getScene('GameScene');
    if (scene) {
      scene.triggerGameOver = () => {};
      scene.onPlayerHit = () => {};
    }
  });
  console.log('  Death patched.');

  // Slow all 5 Phaser time subsystems
  await page.evaluate(({ factor }) => {
    const game = window.__GAME__;
    const scene = game.scene.getScene('GameScene');
    const originalUpdate = scene.update.bind(scene);
    scene.update = function(time, delta) { originalUpdate(time, delta * factor); };
    scene.tweens.timeScale = factor;
    scene.time.timeScale = factor;
    if (scene.physics?.world) scene.physics.world.timeScale = 1 / factor;
    if (scene.anims) scene.anims.globalTimeScale = factor;
  }, { factor: SLOW_MO_FACTOR });
  console.log(`  Slowed to ${SLOW_MO_FACTOR}x.`);

  // Execute input sequence
  const sequence = generateInputSequence(WALL_CLOCK_DURATION);
  console.log(`  Playing ${sequence.length} inputs over ${WALL_CLOCK_DURATION}ms...`);

  for (const seg of sequence) {
    if (!seg.key) { await page.waitForTimeout(seg.pauseMs); continue; }
    await page.keyboard.down(seg.key);
    await page.waitForTimeout(seg.holdMs);
    await page.keyboard.up(seg.key);
    if (seg.pauseMs > 0) await page.waitForTimeout(seg.pauseMs);
  }

  console.log('  Input complete.');

  // Finalize video
  const video = page.video();
  await context.close();
  const videoPath = await video.path();

  if (videoPath !== OUTPUT_FILE) {
    fs.renameSync(videoPath, OUTPUT_FILE);
  }

  await browser.close();
  console.log(`  Raw recording: ${OUTPUT_FILE}`);
  console.log('Done.');
}

captureGameplay().catch(err => { console.error('Capture failed:', err); process.exit(1); });
js
import { chromium } from 'playwright';
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const PROJECT_DIR = path.resolve(__dirname, '..');

// --- 配置项 ---
const args = process.argv.slice(2);
function getArg(name, fallback) {
  const i = args.indexOf(`--${name}`);
  return i !== -1 && args[i + 1] ? args[i + 1] : fallback;
}

const PORT = getArg('port', '3000');
const GAME_URL = `http://localhost:${PORT}/`;
const VIEWPORT = { width: 1080, height: 1920 }; // 9:16移动端竖屏
const SLOW_MO_FACTOR = 0.5;
const DESIRED_GAME_DURATION = parseInt(getArg('duration', '13000'), 10);
const WALL_CLOCK_DURATION = DESIRED_GAME_DURATION / SLOW_MO_FACTOR;
const OUTPUT_DIR = path.resolve(PROJECT_DIR, getArg('output-dir', 'output'));
const OUTPUT_FILE = path.join(OUTPUT_DIR, 'promo-raw.webm');

// <适配调整:生成对应游戏的输入序列>
function generateInputSequence(totalMs) {
  const sequence = [];
  let elapsed = 0;

  // 停顿等待入场动画播放
  sequence.push({ key: null, holdMs: 0, pauseMs: 1500 });
  elapsed += 1500;

  // <适配调整:替换为对应游戏的按键和时间逻辑>
  const keys = ['ArrowLeft', 'ArrowRight'];
  let keyIdx = 0;

  while (elapsed < totalMs) {
    const holdMs = 150 + Math.floor(Math.random() * 450);
    const pauseMs = 50 + Math.floor(Math.random() * 250);

    // 偶尔添加双击增加真实感
    if (Math.random() < 0.15) {
      sequence.push({ key: keys[keyIdx], holdMs: 100, pauseMs: 60 });
      elapsed += 160;
    }

    sequence.push({ key: keys[keyIdx], holdMs, pauseMs });
    elapsed += holdMs + pauseMs;

    // 交替方向(偶尔保持同方向增加随机性)
    if (Math.random() < 0.75) keyIdx = 1 - keyIdx;
  }

  return sequence;
}

async function captureGameplay() {
  console.log('正在捕捉宣传视频...');
  console.log(`  访问地址: ${GAME_URL} | 视口大小: ${VIEWPORT.width}x${VIEWPORT.height}`);
  console.log(`  游戏内时长: ${DESIRED_GAME_DURATION}ms | 实际录制时长: ${WALL_CLOCK_DURATION}ms`);

  fs.mkdirSync(OUTPUT_DIR, { recursive: true });

  const browser = await chromium.launch({ headless: true });
  const context = await browser.newContext({
    viewport: VIEWPORT,
    recordVideo: { dir: OUTPUT_DIR, size: VIEWPORT },
  });

  const page = await context.newPage();
  await page.goto(GAME_URL, { waitUntil: 'networkidle' });

  // 等待游戏启动完成并进入可玩状态
  await page.waitForFunction(() => window.__GAME__?.isBooted, { timeout: 15000 });
  await page.waitForFunction(() => window.__GAME_STATE__?.started, { timeout: 10000 });
  await page.waitForTimeout(300);
  console.log('  游戏已进入可玩状态。');

  // <适配调整:屏蔽死亡逻辑 —— 从GameScene.js中找到实际的方法名>
  await page.evaluate(() => {
    const scene = window.__GAME__.scene.getScene('GameScene');
    if (scene) {
      scene.triggerGameOver = () => {};
      scene.onPlayerHit = () => {};
    }
  });
  console.log('  死亡逻辑已屏蔽。');

  // 放慢Phaser的全部5个时间子系统
  await page.evaluate(({ factor }) => {
    const game = window.__GAME__;
    const scene = game.scene.getScene('GameScene');
    const originalUpdate = scene.update.bind(scene);
    scene.update = function(time, delta) { originalUpdate(time, delta * factor); };
    scene.tweens.timeScale = factor;
    scene.time.timeScale = factor;
    if (scene.physics?.world) scene.physics.world.timeScale = 1 / factor;
    if (scene.anims) scene.anims.globalTimeScale = factor;
  }, { factor: SLOW_MO_FACTOR });
  console.log(`  游戏已放慢至${SLOW_MO_FACTOR}倍速。`);

  // 执行输入序列
  const sequence = generateInputSequence(WALL_CLOCK_DURATION);
  console.log(`  正在执行${sequence.length}个输入操作,总时长${WALL_CLOCK_DURATION}ms...`);

  for (const seg of sequence) {
    if (!seg.key) { await page.waitForTimeout(seg.pauseMs); continue; }
    await page.keyboard.down(seg.key);
    await page.waitForTimeout(seg.holdMs);
    await page.keyboard.up(seg.key);
    if (seg.pauseMs > 0) await page.waitForTimeout(seg.pauseMs);
  }

  console.log('  输入操作执行完成。');

  // 完成视频写入
  const video = page.video();
  await context.close();
  const videoPath = await video.path();

  if (videoPath !== OUTPUT_FILE) {
    fs.renameSync(videoPath, OUTPUT_FILE);
  }

  await browser.close();
  console.log(`  原始录制文件: ${OUTPUT_FILE}`);
  console.log('处理完成。');
}

captureGameplay().catch(err => { console.error('捕捉失败:', err); process.exit(1); });

FFmpeg Conversion

FFmpeg格式转换

After recording, convert the raw slow-mo WebM to a high-FPS MP4. The
convert-highfps.sh
script is bundled with this skill at
skills/promo-video/scripts/convert-highfps.sh
.
bash
undefined
录制完成后,将慢动作的原始WebM文件转换为高帧率MP4文件。
convert-highfps.sh
脚本已经随该Skill打包在
skills/promo-video/scripts/convert-highfps.sh
路径下。
bash
undefined

Copy to project (orchestrator does this)

复制到项目目录(编排器会自动执行该步骤)

cp <plugin-root>/skills/promo-video/scripts/convert-highfps.sh <project-dir>/scripts/
cp <插件根目录>/skills/promo-video/scripts/convert-highfps.sh <项目目录>/scripts/

Run conversion

执行转换

bash scripts/convert-highfps.sh output/promo-raw.webm output/promo.mp4 0.5

The script:
- Applies `setpts` to speed up the video by `1/factor`
- Sets output framerate to `25 / factor` (= 50 FPS for 0.5× slow-mo)
- Encodes H.264 with `crf 23`, `yuv420p`, `faststart`
- Verifies output duration, frame rate, and file size
bash scripts/convert-highfps.sh output/promo-raw.webm output/promo.mp4 0.5

该脚本会执行以下操作:
- 应用`setpts`参数将视频加速`1/factor`倍
- 将输出帧率设置为`25 / factor`(0.5倍慢动作时对应50FPS)
- 使用`crf 23`、`yuv420p`、`faststart`参数编码为H.264格式
- 校验输出文件的时长、帧率和文件大小是否正常

Viewport Defaults

视口默认配置

Always record in mobile portrait (9:16) unless the user explicitly requests otherwise. Rationale:
  • Games are played on phones — promo footage should show the real mobile experience
  • 9:16 is native for TikTok, Instagram Reels, YouTube Shorts
  • 1080×1920 is the standard resolution
Aspect RatioViewportUse Case
9:16 (default)
1080 × 1920
Mobile portrait — TikTok, Reels, Shorts, Moltbook
1:1
1080 × 1080
Square — Instagram feed, X posts
16:9
1920 × 1080
Landscape — YouTube, trailers, desktop games
除非用户明确要求,否则始终使用移动端竖屏(9:16)录制,原因如下:
  • 游戏主要在手机上游玩,宣传片段应该展示真实的移动端体验
  • 9:16是TikTok、Instagram Reels、YouTube Shorts的原生比例
  • 1080×1920是标准分辨率
宽高比视口大小使用场景
9:16(默认)
1080 × 1920
移动端竖屏 —— TikTok、Reels、Shorts、Moltbook
1:1
1080 × 1080
正方形 —— Instagram信息流、X帖子
16:9
1920 × 1080
横屏 —— YouTube、预告片、桌面端游戏

Duration Guidelines

时长指导

Game TypeRecommended DurationWhy
Arcade / dodger10-15sFast action, multiple dodge cycles
Platformer15-20sShow jump timing, level progression
Shooter12-18sShow targeting, enemy waves
Puzzle8-12sShow one solve sequence
游戏类型推荐时长原因
街机 / 闪避类10-15秒快节奏动作,可以展示多个闪避循环
平台跳跃类15-20秒展示跳跃时机、关卡进度
射击类12-18秒展示瞄准操作、敌人波次
解谜类8-12秒展示一个完整的解谜流程

Checklist

检查清单

Before running the capture:
  • Dev server is running and responding
  • FFmpeg is installed on the system
  • Playwright is installed with Chromium
  • Game boots directly into gameplay (no menu blocking)
  • Death/failure method identified and patched
  • Input keys match the game's actual controls
  • Entrance animation pause is included (1-2s)
  • Output directory exists
After capture:
  • Raw WebM exists in output/
  • FFmpeg conversion produces valid MP4
  • Duration is ~half the raw recording (speed-up worked)
  • Frame rate is 50 FPS
  • Video shows gameplay (not a black screen)
运行录制前:
  • 开发服务正在运行且可以正常访问
  • 系统已安装FFmpeg
  • Playwright已安装且包含Chromium
  • 游戏启动后直接进入玩法界面(没有菜单阻挡)
  • 已找到死亡/失败方法并完成屏蔽
  • 输入按键和游戏实际控制逻辑匹配
  • 已添加1-2秒的入场动画停顿
  • 输出目录已存在
录制完成后:
  • 原始WebM文件已生成在output/目录下
  • FFmpeg转换生成了有效的MP4文件
  • 输出视频时长约为原始录制时长的一半(加速生效)
  • 输出视频帧率为50FPS
  • 视频展示的是正常游戏画面(不是黑屏)