use std::path::PathBuf;
#[derive(Clone, Debug)]
pub struct FfmpegConfig {
pub enabled: bool,
pub path: Option<PathBuf>,
pub hwaccel: bool,
}
impl Default for FfmpegConfig {
fn default() -> Self {
Self { enabled: true, path: None, hwaccel: true }
}
}
#[cfg(feature = "ffmpeg-cli")]
mod audio;
#[cfg(all(windows, feature = "ffmpeg-cli"))]
mod job;
#[cfg(feature = "ffmpeg-cli")]
pub mod locate;
#[cfg(feature = "ffmpeg-cli")]
pub mod mkv;
#[cfg(feature = "ffmpeg-cli")]
pub(crate) mod process;
#[cfg(feature = "ffmpeg-cli")]
mod video;
#[cfg(feature = "ffmpeg-cli")]
pub use audio::FfmpegAudioDecoder;
#[cfg(feature = "ffmpeg-cli")]
pub use backend::FfmpegCliBackend;
#[cfg(feature = "ffmpeg-cli")]
pub use video::FfmpegVideoDecoder;
#[cfg(feature = "ffmpeg-cli")]
mod backend {
use std::sync::{Arc, Mutex};
use super::FfmpegConfig;
use super::locate::{FfmpegInfo, find};
use crate::backend::Backend;
use crate::decode::{AudioDecoder, VideoDecoder};
use crate::demux::{Codec, ContainerFormat, Demuxer, StreamInfo, StreamKind};
use crate::source::MediaSource;
use crate::{Error, Result};
pub struct FfmpegCliBackend {
config: FfmpegConfig,
info: Mutex<Option<Arc<FfmpegInfo>>>,
}
impl FfmpegCliBackend {
pub fn new(config: FfmpegConfig) -> Self {
Self { config, info: Mutex::new(None) }
}
pub fn ffmpeg(&self) -> Option<Arc<FfmpegInfo>> {
let mut info = self.info.lock().unwrap();
if info.is_none() {
*info = find(self.config.path.as_deref());
}
info.clone()
}
fn supports(&self, stream: &StreamInfo, kind: StreamKind) -> bool {
stream.kind == kind
&& super::mkv::matroska_codec_id(stream).is_some()
&& self.ffmpeg().is_some_and(|f| f.has_decoder(decoder_names(&stream.codec)))
}
}
impl Backend for FfmpegCliBackend {
fn name(&self) -> &'static str {
"ffmpeg-cli"
}
fn priority(&self) -> i32 {
-10
}
fn supports_container(&self, _format: ContainerFormat) -> bool {
false
}
fn open_demuxer(&self, _format: ContainerFormat, _src: Box<dyn MediaSource>) -> Result<Box<dyn Demuxer>> {
Err(Error::Unsupported("ffmpeg-cli demuxing"))
}
fn supports_video(&self, stream: &StreamInfo) -> bool {
self.supports(stream, StreamKind::Video)
}
fn open_video_decoder(&self, stream: &StreamInfo, _threads: usize) -> Result<Box<dyn VideoDecoder>> {
let info = self.ffmpeg().ok_or(Error::Unsupported("ffmpeg not found"))?;
Ok(Box::new(super::FfmpegVideoDecoder::new(info, stream, self.config.hwaccel)?))
}
fn supports_audio(&self, stream: &StreamInfo) -> bool {
self.supports(stream, StreamKind::Audio)
}
fn open_audio_decoder(&self, stream: &StreamInfo) -> Result<Box<dyn AudioDecoder>> {
let info = self.ffmpeg().ok_or(Error::Unsupported("ffmpeg not found"))?;
Ok(Box::new(super::FfmpegAudioDecoder::new(info, stream)?))
}
}
pub fn decoder_names(codec: &Codec) -> &'static [&'static str] {
match codec {
Codec::Av1 => &["libdav1d", "av1", "libaom-av1"],
Codec::Vp8 => &["vp8", "libvpx"],
Codec::Vp9 => &["vp9", "libvpx-vp9"],
Codec::H264 => &["h264"],
Codec::Hevc => &["hevc"],
Codec::ProRes => &["prores"],
Codec::Opus => &["opus", "libopus"],
Codec::Vorbis => &["vorbis", "libvorbis"],
Codec::Aac => &["aac", "aac_fixed", "libfdk_aac"],
Codec::Mp3 => &["mp3", "mp3float"],
Codec::Ac3 => &["ac3", "ac3_fixed"],
Codec::Eac3 => &["eac3"],
Codec::Flac => &["flac"],
Codec::Alac => &["alac"],
Codec::Pcm(_) => &[],
Codec::Other(id) => match id.as_str() {
"V_MPEG1" => &["mpeg1video"],
"V_MPEG2" => &["mpeg2video"],
"V_MPEG4/ISO/ASP" | "V_MPEG4/ISO/SP" | "V_MPEG4/ISO/AP" => &["mpeg4"],
"V_THEORA" => &["theora"],
"V_MJPEG" => &["mjpeg"],
"A_DTS" => &["dca"],
"A_MPEG/L2" => &["mp2", "mp2float"],
"A_TRUEHD" => &["truehd"],
_ => &[],
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nonexistent_ffmpeg_claims_nothing() {
let b = FfmpegCliBackend::new(FfmpegConfig {
path: Some("/nonexistent/ffmpeg-for-alhazen-core-tests".into()),
..FfmpegConfig::default()
});
let h264 = StreamInfo::new(1, StreamKind::Video, Codec::H264);
assert!(!b.supports_video(&h264));
assert!(b.open_video_decoder(&h264, 1).is_err());
}
#[test]
fn every_modelled_codec_has_decoder_names() {
for c in [Codec::Av1, Codec::Vp8, Codec::Vp9, Codec::H264, Codec::Hevc, Codec::ProRes, Codec::Opus, Codec::Vorbis, Codec::Aac] {
assert!(!decoder_names(&c).is_empty(), "{c}");
}
assert!(decoder_names(&Codec::Other("V_UNKNOWN".into())).is_empty());
}
}
}