Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
opusgamelabs avatar

Promo Video

  • 438 installs
  • 305 repo stars
  • Updated May 25, 2026
  • opusgamelabs/game-creator

promo-video is a game-creator agent skill that records autonomous browser gameplay with Playwright and FFmpeg to produce 50 FPS 1080x1920 portrait promo clips for developers launching social game marketing.

About

promo-video is an OpusGameLabs game-creator pipeline skill that captures short promotional gameplay trailers without manual screen recording. It loads game context from progress.md and source files, adapts a capture-promo template (typically scripts/capture-promo.mjs), and uses Playwright to drive the browser game while FFmpeg processes output into mobile portrait clips at 50 FPS and 1080x1920 resolution. The skill can patch death or game-over logic so footage stays continuous for viral-ready social posts. It runs as Step 2.5 in the make-game pipeline after visual polish and before audio, without modifying core gameplay code. Developers reach for promo-video when an unreleased or updated browser game needs TikTok-style vertical trailers showcasing hooks, key art, and branding. Prerequisites include a running dev server, Playwright with Chromium, and FFmpeg for conversion to MP4 social assets ready to publish.

  • Trailer scripting structure
  • Gameplay highlight selection
  • Platform-specific cut formats
  • Launch teaser workflows
  • Brand-forward pacing guidance

Promo Video by the numbers

  • 438 all-time installs (skills.sh)
  • +12 installs in the week ending Aug 4, 2026 (Skillselion tracking)
  • Ranked #423 of 1,335 Generative Media skills by installs in the Skillselion catalog
  • Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/opusgamelabs/game-creator --skill promo-video

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs438
repo stars305
Last updatedMay 25, 2026
Repositoryopusgamelabs/game-creator

How do you capture gameplay promo videos automatically?

Produce short promotional trailers and social clips that showcase gameplay hooks, key art, and branding for an unreleased or updated game title.

Who is it for?

Game developers finishing a browser game who need automated vertical trailer footage for social launch posts without manual OBS recording.

Skip if: Teams building native console titles without a browser dev server, or projects needing cinematic non-gameplay trailers only.

When should I use this skill?

User requests a promo video, social gameplay clip, trailer capture, or Step 2.5 promo recording in the game-creator pipeline.

What you get

Mobile portrait MP4 promo clip, capture-promo.mjs script, and social-ready gameplay footage at 50 FPS.

  • 1080x1920 promo MP4 clip
  • scripts/capture-promo.mjs capture script

By the numbers

  • Outputs 50 FPS portrait gameplay at 1080x1920 resolution
  • Runs as Step 2.5 in the opusgamelabs/game-creator pipeline

Files

SKILL.mdMarkdownGitHub ↗

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:

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_FACTOR0.5Game runs at half speed → 50 FPS output
WALL_CLOCK_DURATIONDESIRED_GAME_DURATION / SLOW_MO_FACTORRecord for 2× to get correct game-time
VIEWPORT{ width: 1080, height: 1920 }9:16 mobile portrait (always default unless user specifies otherwise)
DESIRED_GAME_DURATION13000 (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) {
    // Patch ALL paths to game over
    scene.triggerGameOver = () => {};
    scene.onPlayerHit = () => {};
    // For multi-life games, also prevent damage:
    // scene.takeDamage = () => {};
    // scene.playerDied = () => {};
  }
});

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 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:

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");

    // 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 deltascene.update(time, delta * factor) slows frame-delta-dependent logic 2. Tweensscene.tweens.timeScale slows all tween animations 3. Scene timersscene.time.timeScale slows scene.time.addEvent() timers 4. Physicsscene.physics.world.timeScale slows Arcade/Matter physics (uses inverse: 1/factor) 5. Animationsscene.anims.globalTimeScale slows sprite animation playback

5. Video Finalization

const video = page.video();
await context.close(); // MUST close context to finalize the video file
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, "..");

// --- 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);
});

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.

# Copy to project (orchestrator does this)
cp <plugin-root>/skills/promo-video/scripts/convert-highfps.sh <project-dir>/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

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 × 1920Mobile portrait — TikTok, Reels, Shorts, Moltbook
1:11080 × 1080Square — Instagram feed, X posts
16:91920 × 1080Landscape — YouTube, trailers, desktop games

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

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)

Related skills

How it compares

Choose promo-video for automated in-browser gameplay capture; use manual video editors when you need non-interactive cinematic b-roll outside the game loop.

FAQ

What resolution and frame rate does promo-video produce?

promo-video targets mobile portrait output at 1080x1920 resolution and 50 FPS. Playwright drives autonomous gameplay in the browser while FFmpeg converts the capture into an MP4 clip formatted for social distribution.

Does promo-video modify core game code?

promo-video primarily records footage and may patch death or game-over handlers so capture stays continuous. It follows the game-creator Step 2.5 workflow, reading progress.md and adapting scripts/capture-promo.mjs without rewriting main gameplay systems.

What tools does promo-video require?

promo-video depends on Playwright with Chromium for browser automation and FFmpeg for video conversion. The game dev server must be running on the configured port so the capture script can drive gameplay and export a social-ready trailer.

Generative Mediacontentdistribution

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.