← back to Wallpaper Tour
stitch.mjs
64 lines
#!/usr/bin/env node
// Stitch all clips/*.webm into one MP4 with title overlay per clip.
// Usage: node stitch.mjs [outname]
import fs from 'node:fs';
import path from 'node:path';
import { execFile } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import os from 'node:os';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const OUTNAME = process.argv[2] || 'wallpaper-portfolio-tour.mp4';
const OUT = path.join(__dirname, OUTNAME);
const SHIPPED = path.join(os.homedir(), 'Videos', 'shipped-apps');
const CLIPS_DIR_ = path.join(__dirname, 'clips');
const clips = fs.readdirSync(CLIPS_DIR_)
.filter(f => f.endsWith('.webm'))
.map(f => ({ f, full: path.join(CLIPS_DIR_, f), size: fs.statSync(path.join(CLIPS_DIR_, f)).size }))
.filter(c => c.size > 100_000) // skip corrupt tiny clips
.sort((a, b) => a.f.localeCompare(b.f, undefined, { numeric: true }))
.map(c => c.full);
if (!clips.length) { console.error('no clips'); process.exit(1); }
console.log(`stitching ${clips.length} clips → ${OUT}`);
const concatFile = path.join(__dirname, '.concat.txt');
fs.writeFileSync(concatFile, clips.map(c => `file '${c}'`).join('\n'));
const args = [
'-f', 'concat', '-safe', '0', '-i', concatFile,
'-vf', 'scale=1600:900:force_original_aspect_ratio=decrease,pad=1600:900:(ow-iw)/2:(oh-ih)/2:color=black,fps=30',
'-c:v', 'libx264', '-preset', 'fast', '-crf', '23',
'-pix_fmt', 'yuv420p',
'-an',
'-y', OUT
];
const t0 = Date.now();
const child = execFile('ffmpeg', args, { maxBuffer: 64 * 1024 * 1024 }, (err, _, stderr) => {
fs.unlinkSync(concatFile);
if (err) {
console.error('ffmpeg failed:', err.message);
console.error(stderr.toString().slice(-1000));
process.exit(1);
}
const ms = Date.now() - t0;
const stat = fs.statSync(OUT);
console.log(`✓ ${OUT}`);
console.log(` ${(stat.size / 1024 / 1024).toFixed(1)} MB in ${(ms / 1000).toFixed(1)}s`);
// Stage to ~/Videos/shipped-apps for auto-publish to videos.agentabrams.com
fs.mkdirSync(SHIPPED, { recursive: true });
const staged = path.join(SHIPPED, OUTNAME);
fs.copyFileSync(OUT, staged);
console.log(`✓ staged for videos.agentabrams.com: ${staged}`);
});
child.stderr?.on('data', (d) => {
const s = d.toString();
// ffmpeg progress lines start with "frame=" — print the latest only
const m = s.match(/frame=\s*\d+.*?time=\s*[\d:.]+\s*bitrate=\s*[\d.]+\s*\w+\/?\w*\s*speed=\s*[\d.]+x?/);
if (m) process.stdout.write(`\r ${m[0]} `);
});