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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XVMQ2JhsJ77pGKqcvvFHqc
This commit is contained in:
lucianoandClaude Fable 5 committed 2026-09-08 17:41:34 +02:00
1 parent 4dfa0fc993
commit be33fb9e7b
2 files changed
+15 -4

No files matched your search

+6 -3
View File
@@ -310,9 +310,12 @@ function transcodeToMp4(input, output, opts) {
// Cover the target frame then centre-crop, so the chosen aspect is filled // Cover the target frame then centre-crop, so the chosen aspect is filled
// without distortion. // without distortion.
const vf = `scale=${w}:${h}:force_original_aspect_ratio=increase,crop=${w}:${h}`; const vf = `scale=${w}:${h}:force_original_aspect_ratio=increase,crop=${w}:${h}`;
const args = ['-y', '-i', input, '-vf', vf, // Post-processing, not realtime: spend encode time on quality. CRF 18 +
'-c:v', 'libx264', '-preset', 'veryfast', '-crf', '20', '-pix_fmt', 'yuv420p', // medium is visually transparent for VJ motion; -r 30 turns the recorder's
'-c:a', 'aac', '-b:a', '192k', '-movflags', '+faststart', output]; // 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) => { return new Promise((resolve, reject) => {
execFile(ffmpegPath(), args, { maxBuffer: 1 << 24 }, (err, stdout, stderr) => { execFile(ffmpegPath(), args, { maxBuffer: 1 << 24 }, (err, stdout, stderr) => {
if (err) reject(new Error('ffmpeg: ' + String(stderr || err.message).slice(-500))); if (err) reject(new Error('ffmpeg: ' + String(stderr || err.message).slice(-500)));
+9 -1
View File
@@ -173,7 +173,15 @@ async function startRecording(opts) {
const vstream = recCanvas.captureStream(30); const vstream = recCanvas.captureStream(30);
const astream = audio.recordDest.stream; const astream = audio.recordDest.stream;
const stream = new MediaStream([...vstream.getVideoTracks(), ...astream.getAudioTracks()]); 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 // Serialise chunk delivery: ondataavailable is async, so without a chain the
// header chunk could be sent after a later one and corrupt the WebM. // header chunk could be sent after a later one and corrupt the WebM.
let chain = Promise.resolve(); let chain = Promise.resolve();