use std::process::Command;
use std::sync::OnceLock;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AacEncoder {
AppleSilicon,
LibFdk,
Native,
}
impl AacEncoder {
pub fn name(&self) -> &'static str {
match self {
Self::AppleSilicon => "aac_at",
Self::LibFdk => "libfdk_aac",
Self::Native => "aac",
}
}
pub fn supports_threading(&self) -> bool {
match self {
Self::AppleSilicon => false, Self::LibFdk => false, Self::Native => true, }
}
pub fn from_str(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"aac_at" => Some(Self::AppleSilicon),
"libfdk_aac" | "libfdk" => Some(Self::LibFdk),
"aac" => Some(Self::Native),
_ => None,
}
}
}
impl std::fmt::Display for AacEncoder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name())
}
}
pub struct EncoderDetector;
impl EncoderDetector {
pub fn detect_best_encoder() -> AacEncoder {
let candidates = [
AacEncoder::AppleSilicon,
AacEncoder::LibFdk,
AacEncoder::Native,
];
for encoder in candidates {
if Self::is_encoder_available(encoder) {
tracing::info!("Detected AAC encoder: {}", encoder.name());
return encoder;
}
}
tracing::warn!("No AAC encoder detected, defaulting to 'aac'");
AacEncoder::Native
}
pub fn is_encoder_available(encoder: AacEncoder) -> bool {
let output = Command::new("ffmpeg").args(&["-encoders"]).output();
if let Ok(output) = output {
let stdout = String::from_utf8_lossy(&output.stdout);
stdout.lines().any(|line| {
let trimmed = line.trim_start();
trimmed.starts_with('A') && line.contains(encoder.name())
})
} else {
false
}
}
pub fn get_available_encoders() -> Vec<AacEncoder> {
let all_encoders = [
AacEncoder::AppleSilicon,
AacEncoder::LibFdk,
AacEncoder::Native,
];
all_encoders
.into_iter()
.filter(|&encoder| Self::is_encoder_available(encoder))
.collect()
}
}
static DETECTED_ENCODER: OnceLock<AacEncoder> = OnceLock::new();
pub fn get_encoder() -> AacEncoder {
*DETECTED_ENCODER.get_or_init(|| EncoderDetector::detect_best_encoder())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encoder_name() {
assert_eq!(AacEncoder::AppleSilicon.name(), "aac_at");
assert_eq!(AacEncoder::LibFdk.name(), "libfdk_aac");
assert_eq!(AacEncoder::Native.name(), "aac");
}
#[test]
fn test_encoder_threading() {
assert!(!AacEncoder::AppleSilicon.supports_threading());
assert!(!AacEncoder::LibFdk.supports_threading());
assert!(AacEncoder::Native.supports_threading());
}
#[test]
fn test_encoder_from_str() {
assert_eq!(AacEncoder::from_str("aac_at"), Some(AacEncoder::AppleSilicon));
assert_eq!(AacEncoder::from_str("libfdk_aac"), Some(AacEncoder::LibFdk));
assert_eq!(AacEncoder::from_str("libfdk"), Some(AacEncoder::LibFdk));
assert_eq!(AacEncoder::from_str("aac"), Some(AacEncoder::Native));
assert_eq!(AacEncoder::from_str("unknown"), None);
}
#[test]
fn test_encoder_display() {
assert_eq!(format!("{}", AacEncoder::AppleSilicon), "aac_at");
assert_eq!(format!("{}", AacEncoder::LibFdk), "libfdk_aac");
assert_eq!(format!("{}", AacEncoder::Native), "aac");
}
#[test]
fn test_detect_encoder() {
let encoder = EncoderDetector::detect_best_encoder();
assert!(matches!(
encoder,
AacEncoder::AppleSilicon | AacEncoder::LibFdk | AacEncoder::Native
));
}
#[test]
fn test_get_available_encoders() {
let encoders = EncoderDetector::get_available_encoders();
assert!(!encoders.is_empty());
assert!(encoders.contains(&AacEncoder::Native) || encoders.len() > 0);
}
#[test]
fn test_get_encoder_cached() {
let encoder1 = get_encoder();
let encoder2 = get_encoder();
assert_eq!(encoder1, encoder2);
}
}