effy 0.11.0

A small and friendly terminal FFmpeg helper that simplifies common tasks
use tracing::debug;

use crate::{
    params::{
        Parameter, ParameterData, PresetParameter, SelectOption, macros::select_non_default_option,
    },
    visitors::{CommandBuilder, HWAccel, VisitorContext},
};

pub(crate) struct HardwareAcceleration;

impl HardwareAcceleration {
    pub(crate) const ID: &'static str = "hwaccel";
    pub(crate) const NAME: &'static str = "HW Acceleration";
    const DEFAULT: &'static str = "none";

    pub fn _new_parameter() -> Parameter {
        Self::new_parameter_ignoring(&[])
    }

    pub fn new_parameter_ignoring(hidden_options: &[String]) -> Parameter {
        let mut options = SelectOption::from_pairs(&[
            ("none", "none"),
            #[cfg(any(target_os = "windows", target_os = "linux"))]
            ("nvidia", "nvenc"),
            #[cfg(any(target_os = "windows", target_os = "linux"))]
            ("intel", "qsv"),
            #[cfg(any(target_os = "windows", target_os = "linux"))]
            ("amd", "amf"),
            #[cfg(target_os = "linux")]
            ("vaapi", "vaapi"),
            #[cfg(target_os = "macos")]
            ("macos", "videotoolbox"),
        ]);
        options.retain(|option| !hidden_options.contains(&option.name));

        Parameter::new(
            Self::ID,
            Self::NAME,
            ParameterData::Select {
                options,
                selected_index: 0,
            },
        )
        .with_order(2000)
    }

    pub fn build_command(cb: &mut CommandBuilder, data: &ParameterData) {
        if let Some(option) = select_non_default_option!(data) {
            debug!(value = option.value, "build_command");
            match option.value.as_str() {
                #[cfg(any(target_os = "windows", target_os = "linux"))]
                "nvenc" => {
                    cb.hwaccel = HWAccel::Nvenc;
                    cb.pre_input_args.push("-hwaccel".into());
                    cb.pre_input_args.push("cuda".into());
                    cb.pre_output_args.push("-c:v".into());
                    cb.pre_output_args.push("h264_nvenc".into());
                }
                #[cfg(any(target_os = "windows", target_os = "linux"))]
                "amf" => {
                    cb.hwaccel = HWAccel::Amf;
                    cb.pre_output_args.push("-c:v".into());
                    cb.pre_output_args.push("h264_amf".into());
                }
                #[cfg(any(target_os = "windows", target_os = "linux"))]
                "qsv" => {
                    cb.hwaccel = HWAccel::Qsv;
                    cb.pre_input_args.push("-init_hw_device".into());
                    cb.pre_input_args.push("qsv=hw".into());
                    cb.pre_input_args.push("-filter_hw_device".into());
                    cb.pre_input_args.push("hw".into());
                    // For recompress only enable full qsv processing
                    if cb.video_filters.is_empty() {
                        cb.pre_input_args.push("-hwaccel".into());
                        cb.pre_input_args.push("qsv".into());
                        cb.pre_input_args.push("-c:v".into());
                        cb.pre_input_args.push("h264_qsv".into());
                    }
                    cb.pre_input_args.push("-hwaccel_output_format".into());
                    cb.pre_input_args.push("qsv".into());
                    cb.pre_output_args.push("-c:v".into());
                    cb.pre_output_args.push("h264_qsv".into());
                }
                #[cfg(target_os = "linux")]
                "vaapi" => {
                    cb.hwaccel = HWAccel::Vaapi;
                    cb.pre_input_args.push("-vaapi_device".into());
                    let device = std::env::var("EFFY_VAAPI_DEVICE")
                        .ok()
                        .filter(|s| {
                            s.chars()
                                .all(|c| c.is_alphanumeric() || c == '/' || c == '.')
                        })
                        .unwrap_or_else(|| "/dev/dri/renderD128".into());
                    cb.pre_input_args.push(device);
                    cb.video_filters.push("format=nv12".into());
                    cb.video_filters.push("hwupload".into());
                    cb.pre_output_args.push("-c:v".into());
                    cb.pre_output_args.push("h264_vaapi".into());
                }
                #[cfg(target_os = "macos")]
                "videotoolbox" => {
                    cb.hwaccel = HWAccel::VideoToolbox;
                    cb.pre_output_args.push("-c:v".into());
                    cb.pre_output_args.push("h264_videotoolbox".into());
                }
                _ => cb.hwaccel = HWAccel::None,
            }
        } else {
            cb.hwaccel = HWAccel::None;
        }
    }
}

impl<'a> PresetParameter<'a> for HardwareAcceleration {
    fn apply_preset(_ctx: &VisitorContext, data: &mut ParameterData, preset_value: &str) {
        Self::set_parameter_value(data, preset_value);
    }

    fn save_preset(_ctx: &VisitorContext, data: &'a ParameterData) -> Option<String> {
        select_non_default_option!(data).map(|option| option.value.clone())
    }
}

#[cfg(test)]
mod tests {
    use super::{HardwareAcceleration, ParameterData};

    #[test]
    fn hides_configured_options() {
        let hidden_options = ["nvidia", "intel", "amd", "vaapi", "macos"]
            .map(str::to_owned)
            .to_vec();
        let parameter = HardwareAcceleration::new_parameter_ignoring(&hidden_options);

        let ParameterData::Select { options, .. } = parameter.data else {
            panic!("hardware acceleration must be a select parameter");
        };
        assert_eq!(
            options
                .iter()
                .map(|option| option.name.as_str())
                .collect::<Vec<_>>(),
            ["none"]
        );
    }
}