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:
1 parent
4dfa0fc993
commit
be33fb9e7b
2 files changed
+15
-4
No files matched your search
@@ -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
@@ -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();
|
||||||
|
|||||||
Reference in new issue
Block a user