use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
use ff_format::{ContainerInfo, NetworkOptions, PixelFormat, VideoStreamInfo};
use crate::HardwareAccel;
use crate::error::DecodeError;
use crate::video::decoder_inner::VideoDecoderInner;
use ff_common::FramePool;
mod decode;
mod format;
mod hw;
mod network;
mod scale;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum OutputScale {
Exact {
width: u32,
height: u32,
},
FitWidth(u32),
FitHeight(u32),
}
#[derive(Debug)]
pub struct VideoDecoderBuilder {
path: PathBuf,
output_format: Option<PixelFormat>,
output_scale: Option<OutputScale>,
hardware_accel: HardwareAccel,
thread_count: usize,
frame_pool: Option<Arc<dyn FramePool>>,
frame_rate: Option<u32>,
network_opts: Option<NetworkOptions>,
}
impl VideoDecoderBuilder {
pub(crate) fn new(path: PathBuf) -> Self {
Self {
path,
output_format: None,
output_scale: None,
hardware_accel: HardwareAccel::Auto,
thread_count: 0,
frame_pool: None,
frame_rate: None,
network_opts: None,
}
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
#[must_use]
pub fn get_output_format(&self) -> Option<PixelFormat> {
self.output_format
}
#[must_use]
pub fn get_hardware_accel(&self) -> HardwareAccel {
self.hardware_accel
}
#[must_use]
pub fn get_thread_count(&self) -> usize {
self.thread_count
}
pub fn build(self) -> Result<VideoDecoder, DecodeError> {
if let Some(scale) = self.output_scale {
let (w, h) = match scale {
OutputScale::Exact { width, height } => (width, height),
OutputScale::FitWidth(w) => (w, 1), OutputScale::FitHeight(h) => (1, h), };
if w == 0 || h == 0 {
return Err(DecodeError::InvalidOutputDimensions {
width: w,
height: h,
});
}
}
let path_str = self.path.to_str().unwrap_or("");
let is_image_sequence = path_str.contains('%');
let is_network_url = crate::network::is_url(path_str);
if !is_image_sequence && !is_network_url && !self.path.exists() {
return Err(DecodeError::FileNotFound {
path: self.path.clone(),
});
}
let (inner, stream_info, container_info) = VideoDecoderInner::new(
&self.path,
self.output_format,
self.output_scale,
self.hardware_accel,
self.thread_count,
self.frame_rate,
self.frame_pool.clone(),
self.network_opts,
)?;
Ok(VideoDecoder {
path: self.path,
frame_pool: self.frame_pool,
inner,
stream_info,
container_info,
fused: false,
})
}
}
pub struct VideoDecoder {
path: PathBuf,
frame_pool: Option<Arc<dyn FramePool>>,
inner: VideoDecoderInner,
stream_info: VideoStreamInfo,
container_info: ContainerInfo,
fused: bool,
}
impl VideoDecoder {
pub fn open(path: impl AsRef<Path>) -> VideoDecoderBuilder {
VideoDecoderBuilder::new(path.as_ref().to_path_buf())
}
#[must_use]
pub fn stream_info(&self) -> &VideoStreamInfo {
&self.stream_info
}
#[must_use]
pub fn width(&self) -> u32 {
self.stream_info.width()
}
#[must_use]
pub fn height(&self) -> u32 {
self.stream_info.height()
}
#[must_use]
pub fn frame_rate(&self) -> f64 {
self.stream_info.fps()
}
#[must_use]
pub fn duration(&self) -> Duration {
self.stream_info.duration().unwrap_or(Duration::ZERO)
}
#[must_use]
pub fn duration_opt(&self) -> Option<Duration> {
self.stream_info.duration()
}
#[must_use]
pub fn container_info(&self) -> &ContainerInfo {
&self.container_info
}
#[must_use]
pub fn position(&self) -> Duration {
self.inner.position()
}
#[must_use]
pub fn is_eof(&self) -> bool {
self.inner.is_eof()
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
#[must_use]
pub fn frame_pool(&self) -> Option<&Arc<dyn FramePool>> {
self.frame_pool.as_ref()
}
#[must_use]
pub fn hardware_accel(&self) -> HardwareAccel {
self.inner.hardware_accel()
}
}
#[cfg(test)]
#[allow(clippy::panic, clippy::expect_used)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn builder_default_values_should_have_auto_hw_and_zero_threads() {
let builder = VideoDecoderBuilder::new(PathBuf::from("test.mp4"));
assert_eq!(builder.path(), Path::new("test.mp4"));
assert!(builder.get_output_format().is_none());
assert_eq!(builder.get_hardware_accel(), HardwareAccel::Auto);
assert_eq!(builder.get_thread_count(), 0);
}
#[test]
fn builder_chaining_should_set_all_fields() {
let builder = VideoDecoderBuilder::new(PathBuf::from("test.mp4"))
.output_format(PixelFormat::Bgra)
.hardware_accel(HardwareAccel::Qsv)
.thread_count(4);
assert_eq!(builder.get_output_format(), Some(PixelFormat::Bgra));
assert_eq!(builder.get_hardware_accel(), HardwareAccel::Qsv);
assert_eq!(builder.get_thread_count(), 4);
}
#[test]
fn decoder_open_should_return_builder_with_path() {
let builder = VideoDecoder::open("video.mp4");
assert_eq!(builder.path(), Path::new("video.mp4"));
}
#[test]
fn decoder_open_pathbuf_should_preserve_path() {
let path = PathBuf::from("/path/to/video.mp4");
let builder = VideoDecoder::open(&path);
assert_eq!(builder.path(), path.as_path());
}
#[test]
fn build_nonexistent_file_should_return_file_not_found() {
let result = VideoDecoder::open("nonexistent_file_12345.mp4").build();
assert!(result.is_err());
match result {
Err(DecodeError::FileNotFound { path }) => {
assert!(
path.to_string_lossy()
.contains("nonexistent_file_12345.mp4")
);
}
Err(e) => panic!("Expected FileNotFound error, got: {e:?}"),
Ok(_) => panic!("Expected error, got Ok"),
}
}
#[test]
fn build_invalid_video_file_should_fail() {
let temp_dir = std::env::temp_dir();
let test_file = temp_dir.join("ff_decode_test_file.txt");
std::fs::write(&test_file, "test").expect("Failed to create test file");
let result = VideoDecoder::open(&test_file).build();
let _ = std::fs::remove_file(&test_file);
assert!(result.is_err());
if let Err(e) = result {
assert!(
matches!(e, DecodeError::NoVideoStream { .. })
|| matches!(e, DecodeError::Ffmpeg { .. })
);
}
}
}