From be33fb9e7be4bbb6bd86f641d7c1810d6cb202cf Mon Sep 17 00:00:00 2001 From: luciano Date: Tue, 8 Sep 2026 17:41:34 +0200 Subject: [PATCH] Recording quality: 30 Mbps intermediate, CRF 18 medium, CFR 30fps The realtime VP9 first pass at 12 Mbps was the bottleneck on full-screen abstract motion (visible blocking the MP4 pass could not recover), and the x264 pass added its own loss at CRF 20 veryfast. The WebM intermediate is deleted after muxing, so it can afford 30 Mbps; the MP4 pass is offline, so it can afford CRF 18 preset medium, constant 30 fps and 256k AAC. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01XVMQ2JhsJ77pGKqcvvFHqc --- main.js | 9 ++++++--- src/output.js | 10 +++++++++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/main.js b/main.js index fa65519..4c0bcbe 100644 --- a/main.js +++ b/main.js @@ -310,9 +310,12 @@ function transcodeToMp4(input, output, opts) { // Cover the target frame then centre-crop, so the chosen aspect is filled // without distortion. const vf = `scale=${w}:${h}:force_original_aspect_ratio=increase,crop=${w}:${h}`; - const args = ['-y', '-i', input, '-vf', vf, - '-c:v', 'libx264', '-preset', 'veryfast', '-crf', '20', '-pix_fmt', 'yuv420p', - '-c:a', 'aac', '-b:a', '192k', '-movflags', '+faststart', output]; + // Post-processing, not realtime: spend encode time on quality. CRF 18 + + // medium is visually transparent for VJ motion; -r 30 turns the recorder's + // variable frame timing into constant 30fps (players and socials want CFR). + const args = ['-y', '-i', input, '-vf', vf, '-r', '30', + '-c:v', 'libx264', '-preset', 'medium', '-crf', '18', '-pix_fmt', 'yuv420p', + '-c:a', 'aac', '-b:a', '256k', '-movflags', '+faststart', output]; return new Promise((resolve, reject) => { execFile(ffmpegPath(), args, { maxBuffer: 1 << 24 }, (err, stdout, stderr) => { if (err) reject(new Error('ffmpeg: ' + String(stderr || err.message).slice(-500))); diff --git a/src/output.js b/src/output.js index 0e3b168..9833fde 100644 --- a/src/output.js +++ b/src/output.js @@ -173,7 +173,15 @@ async function startRecording(opts) { const vstream = recCanvas.captureStream(30); const astream = audio.recordDest.stream; const stream = new MediaStream([...vstream.getVideoTracks(), ...astream.getAudioTracks()]); - recorder = new MediaRecorder(stream, { mimeType: pickMime(), videoBitsPerSecond: 12e6 }); + // Generous bitrate: this WebM is only an intermediate (deleted after the + // MP4 mux) and the realtime VP9 encoder needs headroom on full-screen + // abstract motion — 12 Mbps left visible blocking, the ffmpeg pass can + // only preserve what survives this first encode. + recorder = new MediaRecorder(stream, { + mimeType: pickMime(), + videoBitsPerSecond: 30e6, + audioBitsPerSecond: 192e3 + }); // Serialise chunk delivery: ondataavailable is async, so without a chain the // header chunk could be sent after a later one and corrupt the WebM. let chain = Promise.resolve();