¿Qué hace demo-reel?
Convierte un recorrido guionizado por una aplicación web en un video que parece una grabación de pantalla profesional: movimiento suave a 60 fps, un cursor visible con halo, ondas al hacer clic, leyendas. Maneja la aplicación real en Chromium sin cabeza; el metraje es el producto, no una maqueta.
Por qué el video es suave
Una grabación de pantalla normal captura fotogramas cuando el navegador pinta; en una máquina lenta (o WebGL por software) eso es ~10–15 fps y tartamudea. demo-reel inyecta un reloj virtual (scripts/inject.js): requestAnimationFrame, performance.now y Date solo avanzan cuando el grabador lo indica, y las animaciones CSS/Web se avanzan paso a paso mediante document.getAnimations(). Cada fotograma se avanza exactamente 1/fps, se captura una captura de pantalla y se envía a ffmpeg. Por mucho que tarde un fotograma en renderizarse, el resultado se reproduce perfectamente suave.
Los temporizadores (setTimeout/setInterval) se dejan deliberadamente reales — falsificarlos hace que las cadenas de temporizadores con retardo cero en cargadores y planificadores se ejecuten para siempre (por eso el page.clock de Playwright se cuelga en muchas páginas WebGL).
Requisitos
- Node 18+, ffmpeg en PATH.
- Una vez, en el directorio de esta habilidad:
npm install && npx playwright install chromium.
Flujo de trabajo
- Sirve la aplicación como la ven los usuarios — idealmente una compilación de producción (
vite build && vite preview,next build && next start) contra datos locales o simulados. Los servidores de desarrollo funcionan pero son más lentos y pueden mostrar superposiciones de desarrollo. Nunca grabes datos reales o sensibles — siembra datos de demostración. - Escribe un escenario (
*.scenario.mjs, ver API debajo). Empieza desdeexamples/tasks.scenario.mjs. Planifícalo como una historia: 20–60 s, una idea por leyenda, pausas de 1–2 s después de cada resultado para que los espectadores puedan leerlo. - Graba:
node scripts/record.mjs mi.escenario.mjs --out demo.mp4Un clip de 60 s a 1080p toma aproximadamente 3–10 min (las páginas WebGL pesadas son las más lentas). Ejecútalo en segundo plano. - QA:
scripts/sheet.sh demo.mp4escribe una hoja de contactos (un fotograma cada 2.5 s). Mírala: las leyendas coinciden con lo que está en pantalla, no hay indicadores de carga, nada cubierto por la leyenda, insignia/cursor visibles. Verifica la suavidad extrayendo algunos fotogramas consecutivos — cada uno debería diferir ligeramente durante el movimiento. - Entrega (ver "Uso del video").
API del escenario
export default {
size: "1920x1080", // viewport = tamaño del video
fps: 60,
theme: { captionPosition: "bottom", haloFill: "rgba(250,204,21,.30)" }, // opcional
async run(d) {
await d.open("http://localhost:4173/"); // navegar + estabilizar (no grabado)
d.badge("NUEVO", "#15803d"); // insignia en la esquina (null oculta)
d.caption("El nuevo panel"); // píldora de leyenda (null oculta)
await d.hold(1500); // grabar 1.5 s tal cual
await d.click(d.page.getByRole("button", { name: "Filtros" }));
d.caption("Los filtros viven en un panel"); // establecer leyendas DESPUÉS de la acción
await d.hold(1200);
await d.type("Estocolmo"); // escritura a velocidad humana
await d.press("Enter");
await d.scroll(600); // desplazamiento suave con rueda
await d.step("parte opcional", async () => { /* los errores se registran, la grabación continúa */ });
},
};
| Llamada | Hace |
|---|---|
d.open(url, { settle }) | Navegar, dejar que la página se estabilice (no grabado, predeterminado 2500 ms). |
d.hold(ms) | Grabar la página tal como está. |
d.settle(ms) | Avanzar el tiempo sin grabar — saltar estados de carga, partes perezosas, animaciones de diseño que no quieras mostrar. |
d.move(target, { ms }) / d.hover | Deslizar el cursor (con easing) hacia un objetivo. |
d.click(target, { ms, button }) | Moverse, hacer clic con una onda. button: "right" para menús contextuales. |
d.doubleClick(target) | Doble clic. |
d.type(text, { delay }) | Escribir una tecla a la vez (predeterminado 90 ms/tecla). |
d.press(key) | Atajo de teclado, ej. "Escape", "Control+K". |
d.scroll(dy, { ms }) | Desplazamiento suave con rueda. |
d.caption(text) / d.badge(text, color) / d.cursor(bool) | Estado de la superposición; sobrevive a navegaciones completas de la página. |
d.step(name, fn) | Paso con nombre; los errores se registran, no son fatales. |
d.page, d.point(target), d.width, d.height | Vías de escape. |
Objetivos pueden ser un Locator de Playwright, una cadena de selector CSS, { x, y }, o una función asíncrona (page) => ({ x, y }).
UIs renderizadas en canvas (gráficos, mapas, gráficos de barras) no tienen un elemento DOM por elemento — pasa una función que pida a la biblioteca las coordenadas de pantalla. Ejemplo para Cytoscape.js:
const node = (label) => (page) => page.evaluate((label) => {
const el = [...document.querySelectorAll("div")].find((e) => e._cyreg?.cy);
const n = el._cyreg.cy.nodes().filter((n) => n.data("label") === label)[0];
const r = el.getBoundingClientRect(), p = n.renderedPosition();
return { x: r.left + p.x, y: r.top + p.y };
}, label);
await d.click(node("Servidor A"));
Escribir buenas demos
- Las leyendas dicen la verdad. Solo afirma lo que muestra el metraje. Si un error que quieres mostrar no se reproduce en la toma, cambia la leyenda — no la mantengas.
- Leyenda después de la acción, no antes — de lo contrario, la leyenda de la siguiente pantalla aparece sobre la anterior.
- Muestra resultados, no clics. Mantén 1–2 s en cada resultado; corta la carga con
d.settle. - Antes/Después: graba dos escenarios con la misma historia y pasos, insignia
ANTES/DESPUÉS(rojo/verde), y mantenlos en archivos separados más una concatenación opcional. - Momentos estelares (una página de aterrizaje, una animación): dales 5–10 s — los espectadores necesitan un momento para asimilarlo.
- Mantén el cursor tranquilo: 600–900 ms por movimiento, apárcarlo lejos del contenido mientras se mantiene.
- Los temporizadores reales siguen funcionando mientras se renderizan los fotogramas (~50–300 ms de tiempo real por fotograma), por lo que las interfaces basadas en temporizadores — toasts, búsqueda con debounce, tooltips que se cierran solos — pueden desaparecer antes en el video que en la vida real. Dispáralos justo antes de que quieras que aparezcan en pantalla, o mantén menos.
Uso del video
- PowerPoint: Insertar → Video → Este dispositivo. Pestaña Reproducción: Inicio: Automáticamente y Repetir hasta detener. H.264
yuv420pMP4 (lo que escribe demo-reel) se reproduce en Windows y macOS. - Keynote: arrastrar; Película → Repetir: Bucle.
- Web:
<video src="demo.mp4" autoplay loop muted playsinline></video>. - GIF (README, chat):
ffmpeg -i demo.mp4 -vf "fps=20,scale=960:-1:flags=lanczos,split[a][b];[a]palettegen[p];[b][p]paletteuse" demo.gif - Concatenar clips del mismo tamaño/fps:
printf "file 'a.mp4'\nfile 'b.mp4'\n" > l.txt && ffmpeg -f concat -safe 0 -i l.txt -c copy both.mp4
Solución de problemas
| Síntoma | Solución |
|---|---|
| Fotogramas en blanco / "Cargando…" | Mayor settle en d.open, o await d.settle(1500) después de un clic de navegación. |
| Objetivo no visible | Está fuera de pantalla o dentro de un menú cerrado — abre/desplázate primero; para elementos de canvas usa una función de punto. |
| WebGL/3D se renderiza en negro | Mantén las banderas de lanzamiento predeterminadas (--use-angle=swiftshader); es lento (~0.25 s/fotograma) pero correcto. |
| Video demasiado corto/largo vs plan | Las duraciones son exactas: suma de hold + movimientos. El tiempo de settle nunca se graba. |
| Colores se ven deslavados en PowerPoint | No re-codifiques sin -pix_fmt yuv420p; la salida de demo-reel ya es correcta. |
| Texto ligeramente borroso | --png captura fotogramas sin pérdida (más lento, más grande). |