use std::fmt::Write as _;
use std::path::Path;
use nc_av1::Av1Backend;
use nc_encoder::{
Codec, CodecBackend, InterLayerMode, ParamOverrides, ScalabilityMode, Speed, engine_specs,
preset_params, speeds,
};
use nc_h264::H264Backend;
use nc_hevc::HevcBackend;
const INTER_LAYER_PARAM: &str = "spatial.inter_layer";
pub fn overrides_from_args_with_scalability(
args: &[String],
codec: Codec,
named: &ParamOverrides,
scalability: Option<ScalabilityMode>,
) -> Result<ParamOverrides, String> {
let preset = flag_value(args, "--preset").unwrap_or_else(|| Speed::Medium.name().to_owned());
let spatial_layers = scalability.map_or(1, ScalabilityMode::spatial_layers);
let mut overrides =
preset_params(codec, spatial_layers, &preset).map_err(|e| format!("--preset: {e}"))?;
overrides = overrides.overlaid_with(named);
if let Some(path) = flag_value(args, "--params") {
let text = crate::stdio::read_text(Path::new(&path))?;
apply_file_text(&mut overrides, &text, &path)?;
}
if let Some(mode) = scalability {
if mode.spatial_layers() >= 2 {
overrides.set("spatial.layers", f32::from(mode.spatial_layers()));
let inter_layer = overrides
.get(INTER_LAYER_PARAM)
.map(InterLayerMode::from_param);
if mode.is_key_mode() {
overrides.set(INTER_LAYER_PARAM, InterLayerMode::KeyOnly.param_value());
} else if inter_layer == Some(InterLayerMode::KeyOnly) {
overrides.set(INTER_LAYER_PARAM, InterLayerMode::On.param_value());
}
} else if overrides.contains("spatial.layers") {
overrides.set("spatial.layers", 1.0);
}
if overrides.contains("layers.count") {
overrides.set("layers.count", f32::from(mode.temporal_layers()));
}
if mode.temporal_layers() == 1 && overrides.contains("layers.adaptive") {
overrides.set("layers.adaptive", 0.0);
}
}
for (i, arg) in args.iter().enumerate() {
if arg == "--param" {
let setting = args.get(i + 1).ok_or("missing value after --param")?;
apply_setting(&mut overrides, setting, "--param")?;
}
}
Ok(overrides)
}
#[cfg(test)]
pub fn overrides_from_args(args: &[String], codec: Codec) -> Result<ParamOverrides, String> {
overrides_from_args_with_scalability(args, codec, &ParamOverrides::default(), None)
}
pub fn listing(codec: Option<&str>) -> Result<String, String> {
let (codec_defaults, codec): (&[(&str, f32)], _) = match codec {
None => (&[], None),
Some("av1") => (Av1Backend::DEFAULT_PARAMS, Some(Codec::Av1)),
Some("hevc") => (HevcBackend::DEFAULT_PARAMS, Some(Codec::Hevc)),
Some("vvc" | "h266") => (nc_vvc::VvcBackend::DEFAULT_PARAMS, Some(Codec::Vvc)),
Some("h264") => (H264Backend::DEFAULT_PARAMS, Some(Codec::H264)),
Some("vp9") => (nc_vp9::Vp9Backend::DEFAULT_PARAMS, Some(Codec::Vp9)),
Some("av2") => (nc_av2::Av2Backend::DEFAULT_PARAMS, Some(Codec::Av2)),
Some(other) => return Err(format!("unknown codec '{other}'")),
};
let mut out = String::from("# name default [min, max] what raising it trades\n");
let backends = [
Av1Backend::PARAM_SPECS,
HevcBackend::PARAM_SPECS,
H264Backend::PARAM_SPECS,
nc_vp9::Vp9Backend::PARAM_SPECS,
nc_av2::Av2Backend::PARAM_SPECS,
nc_vvc::VvcBackend::PARAM_SPECS,
];
let groups = engine_specs()
.into_iter()
.chain(backends.into_iter().flatten().copied());
for spec in groups.flatten() {
let default = codec_defaults
.iter()
.find(|(name, _)| *name == spec.name)
.map_or(spec.default, |&(_, value)| value);
let _ = writeln!(
out,
"{} {} [{}, {}] {}",
spec.name, default, spec.min, spec.max, spec.doc
);
}
let _ = writeln!(
out,
"# nonbase.<name> = value: <name> on non-base inter frames only (engine decision.*, planner.*, analysis.*, and per-frame codec groups such as av1.*)"
);
let _ = writeln!(
out,
"# presets: {} (medium = the codec's Default)",
codec
.map_or(Speed::LADDER.to_vec(), speeds)
.iter()
.map(|s| s.name())
.collect::<Vec<_>>()
.join(", ")
);
Ok(out)
}
pub(crate) fn flag_value(args: &[String], flag: &str) -> Option<String> {
let i = args.iter().position(|a| a == flag)?;
args.get(i + 1).cloned()
}
fn apply_file_text(overrides: &mut ParamOverrides, text: &str, origin: &str) -> Result<(), String> {
for (n, line) in text.lines().enumerate() {
let line = line.split('#').next().unwrap_or("").trim();
if !line.is_empty() {
apply_setting(overrides, line, &format!("{origin}:{}", n + 1))?;
}
}
Ok(())
}
fn apply_setting(
overrides: &mut ParamOverrides,
setting: &str,
origin: &str,
) -> Result<(), String> {
let (name, value) = setting
.split_once('=')
.ok_or_else(|| format!("{origin}: expected name=value, got '{setting}'"))?;
let value: f32 = value
.trim()
.parse()
.map_err(|_| format!("{origin}: bad number in '{setting}'"))?;
overrides.set(name.trim(), value);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn args(list: &[&str]) -> Vec<String> {
list.iter().map(|s| (*s).to_string()).collect()
}
#[test]
fn codec_listing_shows_backend_defaults() {
let line = |codec| {
listing(codec)
.unwrap()
.lines()
.find(|l| l.starts_with("decision.codec_subpel_seed_lambdas "))
.map(str::to_owned)
.unwrap()
};
assert!(line(None).starts_with("decision.codec_subpel_seed_lambdas 0.03 "));
assert!(line(Some("vp9")).starts_with("decision.codec_subpel_seed_lambdas 0 "));
assert!(line(Some("vvc")).starts_with("decision.codec_subpel_seed_lambdas 0.05 "));
assert!(listing(Some("mpeg2")).is_err());
}
#[test]
fn later_settings_win_and_files_parse() {
let mut o = ParamOverrides::default();
apply_file_text(&mut o, "# c\nplanner.lambda_scale = 3 # x\n\n", "t").unwrap();
let o2 =
overrides_from_args(&args(&["--param", "planner.lambda_scale=4"]), Codec::Av1).unwrap();
assert_ne!(o, o2);
assert!(
overrides_from_args(&args(&["--preset", "medium"]), Codec::Av1)
.unwrap()
.is_empty()
);
for old in ["default", "av1-fast", "nope"] {
assert!(overrides_from_args(&args(&["--preset", old]), Codec::Av1).is_err());
}
}
#[test]
fn scalability_takes_the_layered_file_of_the_rung() {
let layered = |mode: &str, preset: &str| {
overrides_from_args_with_scalability(
&args(&["--preset", preset]),
Codec::Vp9,
&ParamOverrides::default(),
ScalabilityMode::parse(mode),
)
.unwrap()
};
assert_eq!(layered("L3T1", "fast").get("spatial.layers"), Some(3.0));
assert_eq!(
layered("L2T3", "medium").get("spatial.base_rate_share"),
Some(0.3)
);
assert_eq!(
layered("L1T1", "fast"),
overrides_from_args(&args(&["--preset", "fast"]), Codec::Vp9).unwrap()
);
}
#[test]
fn named_settings_lie_over_the_preset_and_under_params() {
let named = nc_encoder::still_camera_params(nc_encoder::Codec::Av1);
let o = overrides_from_args_with_scalability(
&args(&["--preset", "fast", "--param", "decision.noise_skip=2"]),
Codec::Av1,
&named,
None,
)
.unwrap();
assert_eq!(o.get("rate.skip_budget"), named.get("rate.skip_budget"));
assert_eq!(
o.get("planner.noise_split"),
named.get("planner.noise_split")
);
assert_eq!(o.get("decision.noise_skip"), Some(2.0));
}
#[test]
fn av1_fast_preserves_frozen_controls_and_accepts_chroma_override() {
let mut expected = ParamOverrides::default();
apply_file_text(
&mut expected,
include_str!("../tests/data/av1-joint-fast.toml"),
"historical AV1 fast",
)
.unwrap();
expected.set("av1.chroma_delta_q", -24.0);
expected.set("av1.chroma_noise_scale", 2.0);
expected.set("decision.codec_subpel_seed_lambdas", 0.1);
expected.set("decision.codec_fullpel_probe", 1.0);
expected.set("decision.codec_fullpel_probe_min_lambdas", 0.0094);
expected.set("av1.cdef_per_superblock", 1.0);
expected.set("av1.cdef_local_strong_luma", 1.0);
expected.set("av1.cdef_local_strong_primary", 15.0);
expected.set("av1.cdef_local_strong_variance_min", 0.0);
expected.set("av1.cdef_local_noise_full_search_sigma", 1.0);
expected.set("av1.cdef_local_strong_model_scale", 3.0);
expected.set("av1.pre_tx_skip_model", 0.0);
expected.set("decision.subpel_move_bias", 1.1);
expected.set("decision.subpel_move_bias_nonzero", 1.1);
expected.set("decision.subpel_fractional_bias", 1.1);
expected.set("decision.refine_cheap_vector", 0.0);
expected.set("av1.inter_tx_split", 0.0);
expected.set("av1.loop_filter", 0.0);
expected.set("planner.depth_removal_min_quantizer", 0.0);
expected.set("quant.trellis", 0.0);
expected.set("av1.keyframe_directional", 0.0);
expected.set("av1.keyframe_angle_deltas", 0.0);
expected.set("av1.keyframe_filter_intra", 0.0);
expected.set("decision.intra_directional", 0.0);
expected.set("av1.inter_intra_search", 0.0);
expected.set("av1.partial_forward", 2.0);
expected.set("chroma.policy", 0.0);
expected.set("av1.inter_rounding_q7", 41.0);
expected.set("av1.keyframe_intra_modes", 0.0);
expected.set("decision.seed_refine_ratio", 0.0);
expected.set("rate.period_ceiling", 0.0);
expected.set("structure.unified", 0.0);
expected.set("rate.key_still_boost", 0.0);
expected.set("structure.inherited_depth", 0.0);
expected.set("planner.depth_removal_th_32_16", 25.0);
expected.set("planner.depth_removal_th_16_8", 50.0);
assert_eq!(
overrides_from_args(&args(&["--preset", "fast"]), Codec::Av1).unwrap(),
expected
);
expected.set("av1.chroma_delta_q", -12.0);
assert_eq!(
overrides_from_args(
&args(&["--preset", "fast", "--param", "av1.chroma_delta_q=-12"]),
Codec::Av1
)
.unwrap(),
expected
);
}
}