Loading...
Loading...
Compare original and translation side by side
recordVideo| Parameter | Default | Effect |
|---|---|---|
| | Game runs at half speed → 50 FPS output |
| | Record for 2× to get correct game-time |
| | 9:16 mobile portrait (always default unless user specifies otherwise) |
| | ~13s of game-time → ~6.5s promo clip |
recordVideo| 参数 | 默认值 | 作用 |
|---|---|---|
| | 游戏以半速运行 → 输出50FPS视频 |
| | 录制2倍时长以得到正确的游戏内时长 |
| | 9:16移动端竖屏(无用户特殊指定时默认使用该配置) |
| | 约13秒游戏内时长 → 输出约6.5秒宣传片段 |
npm install -D @playwright/test && npx playwright install chromiumbrew install ffmpegnpx playwright --version
ffmpeg -version | head -1npm install -D @playwright/test && npx playwright install chromiumbrew install ffmpegnpx playwright --version
ffmpeg -version | head -1scripts/capture-promo.mjsscripts/capture-promo.mjsGameScene.jsthis.triggerGameOver()this.takeDamage()this.lives <= 0this.gameOver()eventBus.emit(Events.PLAYER_HIT)eventBus.emit(Events.GAME_OVER)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.jsthis.triggerGameOver()this.takeDamage()this.lives <= 0this.gameOver()eventBus.emit(Events.PLAYER_HIT)eventBus.emit(Events.GAME_OVER)await page.evaluate(() => {
const scene = window.__GAME__.scene.getScene('GameScene');
if (scene) {
// 屏蔽所有触发游戏结束的路径
scene.triggerGameOver = () => {};
scene.onPlayerHit = () => {};
// 多生命类游戏还需要屏蔽受伤逻辑:
// scene.takeDamage = () => {};
// scene.playerDied = () => {};
}
});| Game Type | Input Keys | Pattern |
|---|---|---|
| Side dodger | ArrowLeft, ArrowRight | Alternating holds (150-600ms) with variable pauses, occasional double-taps |
| Platformer / Flappy | Space | Rhythmic taps (80-150ms hold) with variable gaps (200-800ms) |
| Top-down | WASD / Arrows | Mixed directional holds, figure-eight patterns |
| Shooter | ArrowLeft/Right + Space | Movement interleaved with rapid fire |
| Clicker/Tapper | Mouse click / Space | Rapid bursts separated by brief pauses |
const holdMs = 150 + Math.floor(Math.random() * 450);
const pauseMs = 50 + Math.floor(Math.random() * 250);| 游戏类型 | 输入按键 | 模式 |
|---|---|---|
| 横向闪避类 | 左方向键、右方向键 | 交替长按(150-600毫秒)搭配可变时长停顿,偶尔穿插双击 |
| 平台跳跃 / 类Flappy游戏 | 空格 | 有节奏的点按(80-150毫秒长按)搭配可变间隔(200-800毫秒) |
| 俯视视角游戏 | WASD / 方向键 | 混合方向长按,走八字形路线 |
| 射击类 | 左/右方向键 + 空格 | 移动和快速射击交替进行 |
| 点击类 | 鼠标点击 / 空格 | 快速点按 burst 搭配短暂停顿 |
const holdMs = 150 + Math.floor(Math.random() * 450);
const pauseMs = 50 + Math.floor(Math.random() * 250);window.__GAME__window.__GAME_STATE__window.__EVENT_BUS__await page.waitForFunction(() => window.__GAME__?.isBooted, { timeout: 15000 });
await page.waitForFunction(() => window.__GAME_STATE__?.started, { timeout: 10000 });window.__GAME__window.__GAME_STATE__window.__EVENT_BUS__await page.waitForFunction(() => window.__GAME__?.isBooted, { timeout: 15000 });
await page.waitForFunction(() => window.__GAME_STATE__?.started, { timeout: 10000 });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 });scene.update(time, delta * factor)scene.tweens.timeScalescene.time.timeScalescene.time.addEvent()scene.physics.world.timeScale1/factorscene.anims.globalTimeScaleawait 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 });scene.update(time, delta * factor)scene.tweens.timeScalescene.time.timeScalescene.time.addEvent()scene.physics.world.timeScale1/factorscene.anims.globalTimeScaleconst video = page.video();
await context.close(); // MUST close context to finalize the video file
const videoPath = await video.path();const video = page.video();
await context.close(); // 必须关闭context才能完成视频文件的写入
const videoPath = await video.path();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); });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); });convert-highfps.shskills/promo-video/scripts/convert-highfps.shundefinedconvert-highfps.shskills/promo-video/scripts/convert-highfps.shundefined
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
该脚本会执行以下操作:
- 应用`setpts`参数将视频加速`1/factor`倍
- 将输出帧率设置为`25 / factor`(0.5倍慢动作时对应50FPS)
- 使用`crf 23`、`yuv420p`、`faststart`参数编码为H.264格式
- 校验输出文件的时长、帧率和文件大小是否正常| Aspect Ratio | Viewport | Use Case |
|---|---|---|
| 9:16 (default) | | Mobile portrait — TikTok, Reels, Shorts, Moltbook |
| 1:1 | | Square — Instagram feed, X posts |
| 16:9 | | Landscape — YouTube, trailers, desktop games |
| 宽高比 | 视口大小 | 使用场景 |
|---|---|---|
| 9:16(默认) | | 移动端竖屏 —— TikTok、Reels、Shorts、Moltbook |
| 1:1 | | 正方形 —— Instagram信息流、X帖子 |
| 16:9 | | 横屏 —— YouTube、预告片、桌面端游戏 |
| Game Type | Recommended Duration | Why |
|---|---|---|
| Arcade / dodger | 10-15s | Fast action, multiple dodge cycles |
| Platformer | 15-20s | Show jump timing, level progression |
| Shooter | 12-18s | Show targeting, enemy waves |
| Puzzle | 8-12s | Show one solve sequence |
| 游戏类型 | 推荐时长 | 原因 |
|---|---|---|
| 街机 / 闪避类 | 10-15秒 | 快节奏动作,可以展示多个闪避循环 |
| 平台跳跃类 | 15-20秒 | 展示跳跃时机、关卡进度 |
| 射击类 | 12-18秒 | 展示瞄准操作、敌人波次 |
| 解谜类 | 8-12秒 | 展示一个完整的解谜流程 |