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();