Video de juego y video publicitario desde el juego en ejecución
Dos capas:
- Capturar el juego real en ejecución a mp4 (
scripts/capture-video.cjs) — imagen, opcionalmente su sonido, y una línea de tiempo por fotograma de lo que sucedió. - Editar las tomas en el anuncio (
scripts/ad-edit.cjs) a partir de una lista de cortes, o entregar los clips sin editar a un editor no lineal (NLE).
Entre ellas están las cosas que hacen que el metraje sea lo suficientemente bueno para un anuncio: una ejecución reproducible, un piloto automático similar al humano que siempre supera el nivel, y (opcionalmente) un script de nivel exclusivo para grabación para que el escenario muestre sus trucos con un buen ritmo.
npm run dev # el juego, ej. http://localhost:5173
node ${CLAUDE_SKILL_DIR}/scripts/capture-video.cjs game.capture.cjs 40 auto out take1
node ${CLAUDE_SKILL_DIR}/scripts/ad-edit.cjs ads/a.json out # lista de cortes -> out/<list.out>.mp4
node ${CLAUDE_SKILL_DIR}/scripts/probe-trajectory.cjs probe.cjs 30 t.json # ajustar el piloto automático (headless, rápido)
python3 ${CLAUDE_SKILL_DIR}/scripts/plot-trajectory.py t.png t.json
Abre cada renderizado para el usuario (open <file> en macOS) tan pronto como se escriba; revisar es el bucle, así que no hagas que tengan que pegar rutas.
1. Captura (scripts/capture-video.cjs)
canvas.captureStream(fps) graba el búfer de respaldo del canvas, no la ventana, ni el DOM. Por lo tanto:
- Sin cursor / sin HUD gratis. Las superposiciones del DOM (puntuación, botones, cursor de dedo de desarrollo) nunca aparecen en el video. Una imagen limpia es lo que suele querer un anuncio; añade texto en la edición.
- La resolución de captura es independiente de la ventana. Aumenta el tamaño de renderizado interno del motor al tamaño del anuncio (o al doble y deja que la edición reduzca la escala — el sobremuestreo supera al antialiasing del motor) mientras la ventana se mantiene pequeña (ver Desacoplamiento).
- Sonido (
audio: true): cada conexión a los altavoces de un AudioContext se bifurca a unMediaStreamDestinationy se graba con su propio MediaRecorder; ffmpeg los multiplexa. Desbloquea el AudioContext del juego enbeforeRecord(Chromium se ejecuta con--autoplay-policy=no-user-gesture-required). - Línea de tiempo (
timeline: () => ({state, score, beat, ...})): se muestrea en cada fotograma de la página y se escribe como<name>.timeline.json({t, ...}en cada cambio). A partir de esto se corta: cuándo se pasó cada puerta, cuándo se superó el nivel, dónde está cada pulso de la música. Expón el pulso musical del juego como un enterobeatsi lo tiene. - Con o sin ventana (headed/headless). Headed es el valor predeterminado seguro. Headless funciona y no roba el foco (
headless: trueañade banderas de GPU) — pero verifica el conteo de fotogramas de una toma corta primero (ver Problemas).
Configuración
| campo | significado |
|---|---|
url (oblig.) | URL del juego en ejecución, con su propia consulta de captura (semilla, paso fijo, script…) |
ready (oblig.) | () => boolean en la página |
target | tamaño de captura {width,height} (1080×1920 por defecto; ej. 1600×2000 para un anuncio de 800×1000) |
displayHeight | altura del canvas en pantalla (760) |
canvasSelector | el canvas del juego (canvas) |
setRenderSize / setDisplaySize | en la página; aumentar el tamaño de renderizado del motor / reducir CSS y reasignar entrada |
beforeRecord | en la página, una vez antes de grabar (desbloquear audio, ocultar superposición de depuración…) |
autopilot | en la página, una vez justo después de iniciar la grabación, en modo auto |
audio, timeline, headless, settle (ms) | ver arriba |
bitrate, fps, seconds, outDir, name | anulaciones (CLI: config seconds mode outDir name) |
Las funciones se serializan (toString), así que mantenlas autocontenidas. Salidas: <name>.mp4 (H.264/AAC, para editar), <name>.webm (+ .audio.webm, los maestros — consérvalos), <name>.timeline.json.
Desacoplar la resolución de captura de la ventana
Por motor (la ventana se mantiene pequeña; captureStream graba el búfer de respaldo):
- Phaser:
g.scale.resize(w, h)y luego reinicia la escena (los juegos almacenan en caché unscaleFactorencreate()); para juego manual tambiéng.canvas.style.width/height+g.scale.refresh()para que los toques se asignen correctamente. - Three.js:
renderer.setPixelRatio(w / cssW)y luego el redimensionamiento propio del juego con el tamaño CSS (para que la relación de aspecto de la cámara y los búferes de postFX lo sigan). Un juego con relación de aspecto limitada a vertical (con letterboxing hasta una relación máxima) debe tener el límite eliminado para una relación de anuncio más ancha como 4:5. - PixiJS:
renderer.resize(w, h)+ estilo del canvas.
Modos
manual— juegas la ventana a mano (solo headed). Lo más auténtico.auto— elautopilotde la configuración conduce. Para anuncios, haz que parezca humano y que nunca falle — consultareferences/autopilot.md.
2. Hacer la ejecución reproducible (vale la pena antes de ajustar nada)
Ajustar un corte, un piloto automático o un nivel significa regrabar muchas veces; si cada toma se desarrolla de manera diferente, nunca se puede converger. Añade dos interruptores exclusivos de captura al juego (parámetros de consulta; el juego distribuido los ignora):
- seed — la aleatoriedad del juego (disposición de obstáculos y las elecciones del piloto automático) a partir de un PRNG con semilla en lugar de
Math.random. Deja la aleatoriedad puramente visual intacta. - fixed step — simular en pasos fijos de 1/60 s con un acumulador (renderizar una vez por fotograma, ejecutar 0..n pasos). Los tiempos de fotograma reales fluctúan; un paso fijo hace que la simulación sea idéntica independientemente de la velocidad de fotogramas (60 Hz, 120 Hz, un fotograma perdido bajo una carga pesada de captura).
Entonces la misma URL es la misma ejecución — en el navegador para que el usuario la verifique, en la sonda y en la captura. Elige una buena ejecución por semilla (barre algunas con la sonda y elige según criterios medibles), no rehaciendo capturas al azar. Detalles y problemas: references/recording-scripts.md.
3. Edición (scripts/ad-edit.cjs)
Una lista de cortes (JSON) → un mp4 en el tamaño del anuncio (reducción de escala lanczos), CRF 17, AAC, volumen normalizado (por defecto −14 LUFS, a lo que normalizan las redes sociales). Campos principales:
{
"out": "ad-a", "size": [800, 1000], "speed": 1.111,
"fade": 0, "fadeOut": 0, "audioFadeIn": 0.3, "audioFadeOut": 0.8,
"cuts": [{"take": "stage1", "from": 20.3, "to": 24.6, "passAt": 2}, {"take": "stage3", "from": 37.2, "to": 41.2}],
"music": {"take": "music4", "snap": 4, "gates": true, "speedRange": [1, 1.25], "passAt": 2, "align": 4}
}
speedes la velocidad de reproducción. "Time stretch 85%" de After Effects es velocidad 1/0.85. El sonido mantiene su tono (atempo). Acelerado → se escribe a 60 fps (fotogramas descartados); ralentizado → a 60·velocidad fps (sin fotogramas repetidos).musiccoloca UNA pista continua (una toma grabada con los efectos de sonido del juego silenciados) debajo de los cortes directos — el montaje entonces tiene una sola canción en lugar de una canción nueva por corte.snaprecorta los cortes a compases enteros;gatesda a cada corte su propia velocidad e inicio para que sus pases de puerta caigan en los pulsos de la pista;passAtcoloca el primer pase N pulsos después del corte;alignalinea la pista con un corte de la misma toma (su finalización y la cadencia de la canción coinciden).
Lo que hizo que los cortes se sintieran "en" o "fuera" de ritmo, y la receta de la pista musical: references/ad-editing.md.
4. Scripts de nivel exclusivos para grabación (opcional)
Cuando el escenario es demasiado uniforme para un anuncio, añade una disposición solo para captura: agujeros/obstáculos colocados por pulso con alturas y trucos explícitos, más puntos de ruta invisibles que solo el piloto automático sigue (un movimiento adicional sin obstáculos extra). Ponle ritmo para que un nuevo truco aparezca cada ~5 s y el primero se alcance en ~5 s. Consulta references/recording-scripts.md.
Problemas / lo que no funcionó (conserva el "por qué")
- Un solo MediaRecorder con pistas de video + audio se atascó justo después del encabezado webm (decenas de bytes, ffmpeg "Error al analizar el encabezado EBML"). Dos grabadores iniciados en el mismo tick, multiplexados por ffmpeg, funcionan. Un "Error al analizar el encabezado del paquete Opus" de ffmpeg es el último fragmento parcial y es inofensivo.
- Ctrl-C dejó un webm pero no mp4. Los manejadores de SIGINT/SIGTERM/SIGHUP propios de Playwright cierran el navegador y salen primero; inicia con
handleSIG*: falsey usa unprocess.on("SIGINT")persistente (un listeneroncese elimina a sí mismo y Node muere con la segunda señal). - Headless congeló el sujeto al inicio en un juego de Phaser con mucho bloom; en un juego de Three.js headless funcionó bien una vez que se habilitó la GPU (
--use-angle=metal|d3d11|vulkan --enable-gpu --ignore-gpu-blocklist). Cuenta los fotogramas de una toma corta (ffprobe -count_frames) — ~60/s significa que es real. - Ventana sobredimensionada. No escalar la ventana; aumentar el tamaño de renderizado interno.
- Redimensionar sin reiniciar → escala de juego incorrecta (
scaleFactoren caché). - H.264 rechazó dimensiones impares —
scale=trunc(iw/2)*2:trunc(ih/2)*2. - Consultar el juego desde Node cada 100 ms dio tiempos de paso de ±0.1 s — inútil para cortar al ritmo. Muestrea en la página cada fotograma (
timeline). - Iniciar el piloto automático en un momento diferente dio una ejecución diferente con la misma semilla (cambia cómo se consume el PRNG compartido en relación con la disposición). Inícialo de la misma manera en la sonda y en la captura, y vuelve a probar después de cada cambio de script/piloto automático.
- Un rectángulo negro ocupó la pantalla a mitad de la toma, solo en la resolución de captura. Sprites de puntos divididos por la profundidad de vista (
size / -mv.z, colas de estelaxy / w); una partícula que cruzaba el plano de la cámara produjo un sprite cuadrado de tamaño infinito/NaN, y el historial temporal del bloom mantuvo el NaN para siempre. Soluciona ambos extremos: salta puntos en el plano de la cámara y limita los tamaños en los sombreadores; haz que la cadena de bloom reemplace valores no finitos (isnan/isinf→ 0, clamp) en su paso brillante y al mezclar el historial. Diagnostica leyendo cada búfer de postprocesamiento (readRenderTargetPixels) y contando half-floats con exponente0x7c00. - Perder una toma al detenerse temprano — los fragmentos se transmiten al disco cada segundo; el maestro webm sobrevive a una transcodificación interrumpida:
ffmpeg -y -i take.webm -i take.audio.webm -r 60 -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" \ -c:v libx264 -preset slow -crf 16 -pix_fmt yuv420p -c:a aac -movflags +faststart take.mp4
Reutilizable vs específico del juego
- Reutilizable (esta habilidad): el andamiaje de captura (bifurcación de sonido, línea de tiempo, desacoplamiento de redimensionamiento, manejo de detención, transcodificación), el editor de lista de cortes (velocidad, fundidos, pista musical, puertas bloqueadas al ritmo, volumen), la sonda/gráfico de trayectoria y los métodos en
references/. - Específico del juego (tu configuración / tu juego): URL y consulta,
ready, enganches de redimensionamiento, el muestreador de línea de tiempo, el piloto automático en sí, los interruptores de semilla/paso fijo, los scripts de nivel.