Add a video export quality choice: Small file, Standard or High

A Quality dropdown under Video on Setup, --video-quality on the command
line, and "quality" in a project's video settings (written only when it is
not Standard). One table, videoQualities(), holds the ffmpeg settings per
codec; Standard is what every video was written with before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Si
2026-09-26 19:41:00 +02:00
co-authored by Claude Opus 5.5
parent 1b30a079e6
commit 6df4bcb079
10 changed files with 160 additions and 10 deletions
+35 -4
View File
@@ -51,6 +51,31 @@ const VideoPreset* videoPresetByKey(const std::string& key) {
return nullptr;
}
const std::vector<VideoQuality>& videoQualities() {
// Standard is what every video was written with before there was a
// choice: CRF 18 is close to what the eye can tell from the frames.
static const std::vector<VideoQuality> qualities = {
{"small", "Small file", "smaller and quicker to share, with visible softening", 26, "fast",
40, 128},
{kDefaultVideoQuality, "Standard", "close to the rendered frames at a moderate size", 18,
"medium", 32, 256},
{"high", "High", "as faithful as it gets, larger and slower to write", 12, "slow", 24, 256},
};
return qualities;
}
const VideoQuality* videoQualityByKey(const std::string& key) {
for (const auto& quality : videoQualities()) {
if (quality.key == key) return &quality;
}
return nullptr;
}
const VideoQuality& videoQualityOrDefault(const std::string& key) {
if (const auto* quality = videoQualityByKey(key)) return *quality;
return *videoQualityByKey(kDefaultVideoQuality);
}
// In the C locale whatever the user's is, for the same reason as the
// millimetres of a paper size: "7.5 s", never "7,5 s".
std::string formatSeconds(double seconds) {
@@ -106,7 +131,8 @@ bool ffmpegAvailable() {
return false;
}
VideoWriter::VideoWriter(const std::string& path, int width, int height, int fps)
VideoWriter::VideoWriter(const std::string& path, int width, int height, int fps,
const std::string& quality)
: path_(path), width_(width), height_(height) {
const std::string muxer = muxerFor(lowerExtension(path));
if (muxer.empty()) {
@@ -120,6 +146,7 @@ VideoWriter::VideoWriter(const std::string& path, int width, int height, int fps
static std::once_flag ignorePipe;
std::call_once(ignorePipe, [] { ::signal(SIGPIPE, SIG_IGN); });
const VideoQuality& squeeze = videoQualityOrDefault(quality);
std::vector<std::string> args = {
"ffmpeg", "-hide_banner", "-loglevel", "error", "-y",
"-f", "rawvideo", "-pix_fmt", "rgb24",
@@ -129,16 +156,20 @@ VideoWriter::VideoWriter(const std::string& path, int width, int height, int fps
};
if (muxer == "gif") {
// One palette made for the whole clip, rather than the fixed web one.
args.insert(args.end(), {"-vf", "split[a][b];[b]palettegen[p];[a][p]paletteuse",
args.insert(args.end(), {"-vf", "split[a][b];[b]palettegen=max_colors=" +
std::to_string(squeeze.gifColors) +
"[p];[a][p]paletteuse",
"-loop", "0"});
} else {
// 4:2:0 wants even sides; a pixel of padding beats refusing the canvas.
args.insert(args.end(), {"-vf", "pad=ceil(iw/2)*2:ceil(ih/2)*2"});
if (muxer == "webm") {
args.insert(args.end(), {"-c:v", "libvpx-vp9", "-crf", "32", "-b:v", "0",
args.insert(args.end(), {"-c:v", "libvpx-vp9", "-crf", std::to_string(squeeze.vp9Crf),
"-b:v", "0",
"-row-mt", "1"});
} else {
args.insert(args.end(), {"-c:v", "libx264", "-preset", "medium", "-crf", "18"});
args.insert(args.end(), {"-c:v", "libx264", "-preset", squeeze.h264Preset, "-crf",
std::to_string(squeeze.h264Crf)});
}
args.insert(args.end(), {"-pix_fmt", "yuv420p"});
if (muxer == "mp4" || muxer == "mov") args.insert(args.end(), {"-movflags", "+faststart"});