| name | promo-video |
| description | Record a high-FPS autonomous promo video of a Phaser game using Playwright. Triggers on: promo video, gameplay recording, marketing video, game capture.
|
| argument-hint | [path-to-game] |
| license | MIT |
| compatibility | Requires Playwright and FFmpeg installed locally for video capture and encoding. |
| metadata | {"author":"OpusGameLabs","version":"1.3.0","tags":["game","promo","video","recording","playwright","ffmpeg"]} |
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.
Technique
Playwright's recordVideo caps at 25 FPS with no config option. We work around it:
- Slow the game to 0.5× by patching all 5 Phaser time subsystems
- Record for 2× the desired duration at Playwright's native 25 FPS
- FFmpeg speed-up 2× → effective 50 FPS output
| Parameter | Default | Effect |
|---|
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 |
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:
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).
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:
1. Death/Failure Patching (CRITICAL)
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):
await page.evaluate(() => {
const scene = window.__GAME__.scene.getScene("GameScene");
if (scene) {
scene.triggerGameOver = () => {};
scene.onPlayerHit = () => {};
}
});
2. Input Sequence Generation
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 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 |
Randomize timing to avoid robotic-looking movement:
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.
3. Game Boot Detection
All Phaser games built with the /viral-game or /make-game pipelines 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:
await page.waitForFunction(() => window.__GAME__?.isBooted, { timeout: 15000 });
await page.waitForFunction(() => window.__GAME_STATE__?.started, {
timeout: 10000,
});
4. Time Scaling Injection
Slow all 5 Phaser time subsystems for the recording:
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 },
);
The 5 subsystems:
- Update delta —
scene.update(time, delta * factor) slows frame-delta-dependent logic
- Tweens —
scene.tweens.timeScale slows all tween animations
- Scene timers —
scene.time.timeScale slows scene.time.addEvent() timers
- Physics —
scene.physics.world.timeScale slows Arcade/Matter physics (uses inverse: 1/factor)
- Animations —
scene.anims.globalTimeScale slows sprite animation playback
5. Video Finalization
const video = page.video();
await context.close();
const videoPath = await video.path();
Full Capture Script Template
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 };
= ;
= ((, ), );
= / ;
= path.(, (, ));
= path.(, );
() {
sequence = [];
elapsed = ;
sequence.({ : , : , : });
elapsed += ;
keys = [, ];
keyIdx = ;
(elapsed < totalMs) {
holdMs = + .(.() * );
pauseMs = + .(.() * );
(.() < ) {
sequence.({ : keys[keyIdx], : , : });
elapsed += ;
}
sequence.({ : keys[keyIdx], holdMs, pauseMs });
elapsed += holdMs + pauseMs;
(.() < ) keyIdx = - keyIdx;
}
sequence;
}
() {
.();
.(
,
);
.(
,
);
fs.(, { : });
browser = chromium.({ : });
context = browser.({
: ,
: { : , : },
});
page = context.();
page.(, { : });
page.( .?., {
: ,
});
page.( .?., {
: ,
});
page.();
.();
page.( {
scene = ...();
(scene) {
scene. = {};
scene. = {};
}
});
.();
page.(
{
game = .;
scene = game..();
originalUpdate = scene..(scene);
scene. = () {
(time, delta * factor);
};
scene.. = factor;
scene.. = factor;
(scene.?.) scene... = / factor;
(scene.) scene.. = factor;
},
{ : },
);
.();
sequence = ();
.(
,
);
( seg sequence) {
(!seg.) {
page.(seg.);
;
}
page..(seg.);
page.(seg.);
page..(seg.);
(seg. > ) page.(seg.);
}
.();
video = page.();
context.();
videoPath = video.();
(videoPath !== ) {
fs.(videoPath, );
}
browser.();
.();
.();
}
().( {
.(, err);
process.();
});
FFmpeg Conversion
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.
cp <plugin-root>/skills/promo-video/scripts/convert-highfps.sh <project-dir>/scripts/
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
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 Ratio | Viewport | Use 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 |
Duration Guidelines
| 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 |
Checklist
Before running the capture:
After capture: