Paint Frame Reel Renderer
ヘッドレスChromeでピクセル単位の正確な1080x1920縦型ビデオフレームをレンダリングし、マイクロタイミングのFFmpeg concat demuxerスクリプトを構築、最終的なH.264 MP4を320kbps AACオーディオでエンコードします。
flowchart TD
A[Web Canvas UI 1080x1920] --> B[Puppeteerヘッドレスキャプチャ]
B --> C[ビジュアルフェーズスクリーンショット]
C --> D[Concat Demuxerスクリプト]
D --> E[FFmpegエンコード libx264 + AAC + faststart]
E --> F[完成したリールMP4]
Puppeteer + Concat Demuxerの理由
- ドロップフレームゼロ:画面録画によるドロップフレームやスタッターを排除。
- マイクロ秒精度:各ワードやフレーム状態に秒単位の正確な浮動小数点数時間を割り当て。
- 乱流インクウィグル:SVGの
feTurbulenceとfeDisplacementMapに回転シードを適用し、静的テキストに手描きマーカーのような躍動感を付与。 - 速度:1倍速の画面録画と比較して、3~6倍のリアルタイム速度でエンコード。
コアレンダースクリプト構造
const puppeteer = require('puppeteer-core');
const { spawnSync } = require('child_process');
// 1. Puppeteerを1080x1920で起動
const browser = await puppeteer.launch({
executablePath: CHROME_PATH,
args: ['--no-sandbox', '--disable-gpu']
});
const page = await browser.newPage();
await page.setViewport({ width: 1080, height: 1920, deviceScaleFactor: 1 });
await page.goto('file:///' + HTML_PATH);
// 2. ワード/ラインフレームをキャプチャ
const concatLines = [];
for (const line of linesData) {
// スクリーンショットを取得...
concatLines.push(`file '${framePath}'`);
concatLines.push(`duration ${duration.toFixed(3)}`);
}
// 3. Concat Demuxerの要件(最終フレームを繰り返す)
concatLines.push(concatLines[concatLines.length - 2]);
fs.writeFileSync('concat.txt', concatLines.join('\n'));
// 4. FFmpegでエンコード
spawnSync(FFMPEG_PATH, [
'-f', 'concat', '-safe', '0', '-i', 'concat.txt',
'-ss', String(audioStartOffset),
'-i', songAudioPath,
'-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-r', '30',
'-c:a', 'aac', '-b:a', '320k',
'-shortest', '-movflags', '+faststart',
'-y', outputMp4Path
]);
主要な最適化ルール
- スナッピーな表示:高速歌唱中は表示アニメーションが0.24秒を超えないようにする。
- デバイススケールファクター:
deviceScaleFactor: 1に設定し、1080x1920解像度を正確に、かつメモリ肥大化なく実現。 - +faststart:常に
-movflags +faststartを含め、InstagramやTelegramでリールが即座にバッファリングされるようにする。