O que demo-reel faz?
Transforme um roteiro de navegação por um aplicativo web em um vídeo que parece uma gravação de tela polida: movimento suave a 60 fps, cursor visível com um halo, ondulações de clique e legendas. Ele opera o aplicativo real em Chromium headless — a filmagem é o produto, não uma simulação.
Por que o vídeo é suave
Uma gravação de tela normal captura quadros quando o navegador decide renderizar; em uma máquina lenta (ou WebGL via software) isso fica em ~10–15 fps e treme. O demo-reel injeta um relógio virtual (scripts/inject.js): requestAnimationFrame, performance.now e Date só avançam quando o gravador permite, e animações CSS/Web são controladas via document.getAnimations(). Cada quadro é avançado exatamente 1/fps, capturado em screenshot e enviado para o ffmpeg. Não importa quanto tempo um quadro leva para renderizar, o resultado final é perfeitamente suave.
Os temporizadores (setTimeout/setInterval) são mantidos reais deliberadamente — falsificá-los faz com que cadeias de temporizadores sem atraso em carregadores e agendadores rodem para sempre (é por isso que o page.clock do Playwright trava em muitas páginas WebGL).
Requisitos
- Node 18+, ffmpeg no PATH.
- Uma vez, neste diretório da habilidade:
npm install && npx playwright install chromium.
Fluxo de trabalho
- Sirva o aplicativo da forma que os usuários o veem — idealmente uma build de produção (
vite build && vite preview,next build && next start) com dados locais ou simulados. Servidores de desenvolvimento funcionam, mas são mais lentos e podem mostrar overlays de desenvolvimento. Nunca grave dados reais ou sensíveis — use dados de demonstração. - Escreva um roteiro (
*.scenario.mjs, veja a API abaixo). Comece a partir deexamples/tasks.scenario.mjs. Planeje como uma história: 20–60 s, uma ideia por legenda, pausas de 1–2 s após cada resultado para que os espectadores possam ler. - Grave:
node scripts/record.mjs meu.roteiro.mjs --out demo.mp4Um clipe de 60 s em 1080p leva aproximadamente 3–10 min (páginas WebGL pesadas são as mais lentas). Execute em segundo plano. - QA:
scripts/sheet.sh demo.mp4cria uma folha de contato (um quadro a cada 2,5 s). Observe: as legendas correspondem ao que está na tela, sem spinners de carregamento, nada coberto pela legenda, selo/cursor visíveis. Verifique a suavidade extraindo alguns quadros consecutivos — cada um deve diferir ligeiramente durante o movimento. - Entrega (veja "Usando o vídeo").
API do Roteiro
export default {
size: "1920x1080", // viewport = tamanho do vídeo
fps: 60,
theme: { captionPosition: "bottom", haloFill: "rgba(250,204,21,.30)" }, // opcional
async run(d) {
await d.open("http://localhost:4173/"); // navegar + aguardar estabilização (não gravado)
d.badge("NOVO", "#15803d"); // selo no canto (null oculta)
d.caption("O novo painel"); // pílula de legenda (null oculta)
await d.hold(1500); // gravar 1,5 s como está
await d.click(d.page.getByRole("button", { name: "Filtros" }));
d.caption("Filtros em um só painel"); // definir legendas APÓS a ação
await d.hold(1200);
await d.type("Estocolmo"); // digitação em velocidade humana
await d.press("Enter");
await d.scroll(600); // rolagem suave com roda
await d.step("parte opcional", async () => { /* falhas são registradas, gravação continua */ });
},
};
| Chamada | Faz |
|---|---|
d.open(url, { settle }) | Navegar, deixar a página estabilizar (não gravado, padrão 2500 ms). |
d.hold(ms) | Gravar a página como está. |
d.settle(ms) | Avançar o tempo sem gravar — pular estados de carregamento, chunks lentos, animações de layout que você não quer mostrar. |
d.move(target, { ms }) / d.hover | Deslizar o cursor (com easing) até um alvo. |
d.click(target, { ms, button }) | Mover, clicar com ondulação. button: "right" para menus de contexto. |
d.doubleClick(target) | Clique duplo. |
d.type(text, { delay }) | Digitar uma tecla por vez (padrão 90 ms/tecla). |
d.press(key) | Atalho de teclado, ex. "Escape", "Control+K". |
d.scroll(dy, { ms }) | Rolagem suave com roda. |
d.caption(text) / d.badge(text, color) / d.cursor(bool) | Estado da sobreposição; sobrevive a navegações completas da página. |
d.step(name, fn) | Etapa nomeada; erros são registrados, não fatais. |
d.page, d.point(target), d.width, d.height | Escapes. |
Alvos podem ser um Locator do Playwright, uma string de seletor CSS, { x, y }, ou uma função assíncrona (page) => ({ x, y }).
UIs renderizadas em canvas (gráficos, mapas, charts) não têm elemento DOM por item — passe uma função que peça as coordenadas de tela à biblioteca. Exemplo 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"));
Escrevendo boas demonstrações
- Legendas dizem a verdade. Afirme apenas o que a filmagem mostra. Se um bug que você quer mostrar não se reproduz na gravação, mude a legenda — não a mantenha.
- Legende após a ação, não antes — caso contrário, a legenda da próxima tela aparece sobre a anterior.
- Mostre resultados, não cliques. Pause 1–2 s em cada resultado; corte carregamentos com
d.settle. - Antes/depois: grave dois roteiros com a mesma história e passos, selo
ANTES/DEPOIS(vermelho/verde), e mantenha arquivos separados além de uma concatenação opcional. - Momentos destaque (uma página inicial, uma animação): dê 5–10 s — os espectadores precisam de um momento para absorver.
- Mantenha o cursor calmo: 600–900 ms por movimento, estacione-o longe do conteúdo durante as pausas.
- Os temporizadores reais continuam rodando enquanto os quadros são renderizados (~50–300 ms de tempo real por quadro), então UIs baseadas em temporizadores — toasts, pesquisa com debounce, tooltips que desaparecem automaticamente — podem desaparecer mais cedo no vídeo do que na vida real. Dispare-os imediatamente antes de querê-los na tela, ou pause menos.
Usando o vídeo
- PowerPoint: Insert → Video → Este Dispositivo. Guia Reprodução: Iniciar: Automaticamente e Repetir até Parar. MP4 H.264
yuv420p(o que o demo-reel gera) funciona no Windows e macOS. - Keynote: arraste para dentro; Movie → Repeat: Loop.
- 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 clipes do mesmo tamanho/fps:
printf "file 'a.mp4'\nfile 'b.mp4'\n" > l.txt && ffmpeg -f concat -safe 0 -i l.txt -c copy ambos.mp4
Solução de problemas
| Sintoma | Solução |
|---|---|
| Quadros em branco / "Carregando…" | Aumentar settle em d.open, ou await d.settle(1500) após um clique de navegação. |
| Alvo não visível | Está fora da tela ou dentro de um menu fechado — abra/role primeiro; para itens em canvas use uma função de ponto. |
| WebGL/3D renderiza preto | Mantenha as flags de inicialização padrão (--use-angle=swiftshader); é lento (~0,25 s/quadro) mas correto. |
| Vídeo muito curto/longo vs planejado | As durações são exatas: soma de hold + movimentos. O tempo settle nunca é gravado. |
| Cores parecem desbotadas no PowerPoint | Não recodifique sem -pix_fmt yuv420p; a saída do demo-reel já está correta. |
| Texto ligeiramente suave | --png captura quadros sem perda (mais lento, maior). |