docs(videos): script de recodificacao para rolagem controlada e README da vitrine
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
1 parent
ba05cdfe61
commit
01bbebcf52
2 files changed
+116
-3
No files matched your search
@@ -47,11 +47,23 @@ O número "Mais de X projetos desenvolvidos" no topo da home vem da API do GitHu
|
|||||||
|
|
||||||
Para incluir os privados, defina `GITHUB_STATS_TOKEN` no ambiente de build e de execução (não use o prefixo `NEXT_PUBLIC_`). O token deve ser um [fine-grained personal access token](https://github.com/settings/personal-access-tokens/new) com acesso a todos os repositórios da conta e uma única permissão, **Metadata: Read-only**. Sem o token, ou se a consulta falhar, o número simplesmente não aparece.
|
Para incluir os privados, defina `GITHUB_STATS_TOKEN` no ambiente de build e de execução (não use o prefixo `NEXT_PUBLIC_`). O token deve ser um [fine-grained personal access token](https://github.com/settings/personal-access-tokens/new) com acesso a todos os repositórios da conta e uma única permissão, **Metadata: Read-only**. Sem o token, ou se a consulta falhar, o número simplesmente não aparece.
|
||||||
|
|
||||||
## Vídeos dos projetos
|
## Vitrine de vídeos dos projetos
|
||||||
|
|
||||||
Cada card de projeto da home mostra um vídeo curto, em loop e sem áudio, da página rolando em velocidade constante. Os arquivos ficam em `public/videos/<id do projeto>/` (`desktop` em WebM, MP4 e poster WebP, até 3 MB cada vídeo, mais a versão `mobile`) e são ligados aos cards em `lib/project-videos.ts`.
|
Cada projeto com site gravado aparece de três formas na seção Projetos da home. Tudo mora em `app/components/showcase/`, os dados em `lib/project-showcase.ts` e os estilos em `showcase.css`.
|
||||||
|
|
||||||
Em `app/components/ProjectVideo.tsx`, o vídeo só começa a ser baixado quando o card entra na tela (IntersectionObserver), pausa ao sair, e quem tem `prefers-reduced-motion` vê só o poster. Para gravar ou regravar um projeto, use `scripts/record-project-videos.mjs` (as instruções de instalação do Playwright e do ffmpeg estão no cabeçalho do script).
|
- **Cards.** O vídeo de rolagem fica numa moldura de navegador com o endereço real do site. Só um vídeo toca por vez: o do card sob o mouse (ou com foco) e, sem mouse, o mais perto do centro da tela. Nada é baixado até o card tocar pela primeira vez, e um botão pausa os vídeos que se mexem sozinhos. Clicar na moldura abre o vídeo ampliado (`<dialog>` nativo, com troca computador/celular que mantém o ponto do site, capítulos clicáveis e controles próprios).
|
||||||
|
- **Vitrine animada.** Logo abaixo dos cards, uma cena presa na rolagem (`position: sticky`) em que o site de cada projeto rola dentro de um notebook, com um celular ao lado, conforme o visitante rola a página. Os capítulos de cada projeto (`chapters` em `lib/project-showcase.ts`, marcados nos quadros dos vídeos) nomeiam o trecho na tela e mantêm notebook e celular no mesmo ponto do site. Só o projeto em cena e os vizinhos baixam vídeo, e nada baixa com "economizar dados" ligado. Com `prefers-reduced-motion` ou sem JavaScript ela some e os cards continuam mostrando os posters.
|
||||||
|
- **Capa de compartilhamento.** `/cases/<id>` usa o poster do projeto como fundo da imagem Open Graph (`lib/og-card.tsx`), quando existe.
|
||||||
|
|
||||||
|
Arquivos de cada projeto em `public/videos/<id>/`:
|
||||||
|
|
||||||
|
| Arquivo | Para quê |
|
||||||
|
| --- | --- |
|
||||||
|
| `desktop.{webm,mp4,webp}` e `mobile.{webm,mp4,webp}` | cards e vídeo ampliado (até 3 MB cada vídeo) |
|
||||||
|
| `scrub-desktop.mp4` e `scrub-mobile.mp4` | vitrine animada: quadro-chave a cada 6 quadros, sem B-frames, para o tempo mudar a cada quadro da rolagem |
|
||||||
|
| `og.jpg` | capa 1200x630 de compartilhamento |
|
||||||
|
|
||||||
|
Para gravar ou regravar um projeto, use `scripts/record-project-videos.mjs`; para gerar os `scrub-*.mp4`, `scripts/encode-scrub-video.mjs` (as instruções de instalação do Playwright e do ffmpeg estão no cabeçalho de cada script). Depois de gravar, marque os capítulos do projeto em `lib/project-showcase.ts`.
|
||||||
|
|
||||||
## Stack
|
## Stack
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
/**
|
||||||
|
* Recodifica um vídeo para a vitrine animada, em que o tempo do vídeo muda a cada quadro da rolagem.
|
||||||
|
* Para a busca ser instantânea o vídeo precisa de um quadro-chave a cada 6 quadros e nenhum B-frame; o tamanho
|
||||||
|
* de cada arquivo fica em até 3 MB (sobe o CRF e, se preciso, reduz a imagem até caber).
|
||||||
|
*
|
||||||
|
* ffmpeg-static não é dependência do site: instale-o numa pasta temporária e rode o script a partir dela
|
||||||
|
* (as dependências são resolvidas a partir da pasta onde o comando foi executado):
|
||||||
|
*
|
||||||
|
* mkdir rec && cd rec && npm init -y && npm i ffmpeg-static
|
||||||
|
* node <repo>/scripts/encode-scrub-video.mjs --in desktop-original.mp4 --out <repo>/public/videos/<id>/scrub-desktop.mp4 --size 1120x700
|
||||||
|
* node <repo>/scripts/encode-scrub-video.mjs --in mobile-original.mp4 --out <repo>/public/videos/<id>/scrub-mobile.mp4 --size 390x844
|
||||||
|
*
|
||||||
|
* Use como entrada o vídeo de melhor qualidade que existir (a entrada de record-project-videos.mjs antes do corte
|
||||||
|
* para MP4/WebM), não os MP4 já comprimidos de public/videos.
|
||||||
|
*
|
||||||
|
* A capa de compartilhamento (og.jpg, 1200x630) sai do poster desktop:
|
||||||
|
* ffmpeg -i public/videos/<id>/desktop.webp -vf "scale=1200:750:flags=lanczos,crop=1200:630:0:0" -q:v 4 public/videos/<id>/og.jpg
|
||||||
|
*/
|
||||||
|
import { spawnSync } from "node:child_process";
|
||||||
|
import { statSync } from "node:fs";
|
||||||
|
import { createRequire } from "node:module";
|
||||||
|
import { join } from "node:path";
|
||||||
|
|
||||||
|
const require = createRequire(join(process.cwd(), "noop.js"));
|
||||||
|
const ffmpegPath = require("ffmpeg-static");
|
||||||
|
|
||||||
|
const args = Object.fromEntries(
|
||||||
|
process.argv.slice(2).reduce((acc, cur, i, all) => {
|
||||||
|
if (cur.startsWith("--")) acc.push([cur.slice(2), all[i + 1]]);
|
||||||
|
return acc;
|
||||||
|
}, []),
|
||||||
|
);
|
||||||
|
const { in: input, out: output } = args;
|
||||||
|
const [width, height] = (args.size ?? "1120x700").split("x").map(Number);
|
||||||
|
const MAX_BYTES = Number(args["max-mb"] ?? 3) * 1024 * 1024;
|
||||||
|
|
||||||
|
if (!input || !output || !width || !height) {
|
||||||
|
console.error(
|
||||||
|
"uso: --in <video> --out <video.mp4> --size 1120x700 [--max-mb 3]",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function encode(w, h, crf) {
|
||||||
|
const result = spawnSync(
|
||||||
|
ffmpegPath,
|
||||||
|
[
|
||||||
|
"-hide_banner",
|
||||||
|
"-loglevel",
|
||||||
|
"error",
|
||||||
|
"-y",
|
||||||
|
"-i",
|
||||||
|
input,
|
||||||
|
"-vf",
|
||||||
|
`scale=${w}:${h}:flags=lanczos,fps=24`,
|
||||||
|
"-an",
|
||||||
|
"-c:v",
|
||||||
|
"libx264",
|
||||||
|
"-preset",
|
||||||
|
"slow",
|
||||||
|
"-profile:v",
|
||||||
|
"main",
|
||||||
|
"-crf",
|
||||||
|
String(crf),
|
||||||
|
"-g",
|
||||||
|
"6",
|
||||||
|
"-keyint_min",
|
||||||
|
"6",
|
||||||
|
"-sc_threshold",
|
||||||
|
"0",
|
||||||
|
"-bf",
|
||||||
|
"0",
|
||||||
|
"-refs",
|
||||||
|
"1",
|
||||||
|
"-pix_fmt",
|
||||||
|
"yuv420p",
|
||||||
|
"-movflags",
|
||||||
|
"+faststart",
|
||||||
|
output,
|
||||||
|
],
|
||||||
|
{ encoding: "utf8" },
|
||||||
|
);
|
||||||
|
if (result.status !== 0) throw new Error(result.stderr);
|
||||||
|
return statSync(output).size;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const scale of [1, 0.9, 0.8]) {
|
||||||
|
const w = Math.round((width * scale) / 2) * 2;
|
||||||
|
const h = Math.round((height * scale) / 2) * 2;
|
||||||
|
for (const crf of [28, 31, 34, 37]) {
|
||||||
|
const bytes = encode(w, h, crf);
|
||||||
|
if (bytes <= MAX_BYTES) {
|
||||||
|
console.log(
|
||||||
|
`${output}: ${w}x${h}, crf ${crf}, ${Math.round(bytes / 1024)} KB`,
|
||||||
|
);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.error("não coube no limite de tamanho, nem reduzindo a imagem");
|
||||||
|
process.exit(1);
|
||||||
Reference in new issue
Block a user