#[cfg(feature = "vapoursynth")]
use std::collections::HashMap;
use std::fs::File;
use std::io::{BufReader, Read, stdin};
use std::path::Path;
use v_frame::chroma::ChromaSubsampling;
use v_frame::frame::Frame;
use v_frame::pixel::Pixel;
#[cfg(feature = "vapoursynth")]
use vapoursynth::node::Node;
#[cfg(feature = "vapoursynth")]
use vapoursynth::prelude::Environment;
mod error;
mod helpers {
#[cfg(feature = "ffmpeg")]
pub(crate) mod ffmpeg;
#[cfg(feature = "ffms2")]
pub(crate) mod ffms2;
#[cfg(feature = "vapoursynth")]
pub(crate) mod vapoursynth;
pub(crate) mod y4m;
}
mod util;
#[cfg(feature = "ffmpeg")]
pub use crate::helpers::ffmpeg::FfmpegDecoder;
#[cfg(feature = "ffms2")]
pub use crate::helpers::ffms2::Ffms2Decoder;
#[cfg(feature = "vapoursynth")]
pub use crate::helpers::vapoursynth::ModifyNode;
#[cfg(feature = "vapoursynth")]
pub use crate::helpers::vapoursynth::VapoursynthDecoder;
#[cfg(feature = "vapoursynth")]
use crate::helpers::vapoursynth::{VariableName, VariableValue};
pub use error::DecoderError;
pub use num_rational::Rational32;
pub use v_frame;
pub use y4m::Decoder as Y4mDecoder;
const Y4M_EXTENSIONS: &[&str] = &["y4m", "yuv"];
const SB_SIZE_LOG2: usize = 6;
const SB_SIZE: usize = 1 << SB_SIZE_LOG2;
const SUBPEL_FILTER_SIZE: usize = 8;
const FRAME_MARGIN: usize = 16 + SUBPEL_FILTER_SIZE;
const LUMA_PADDING: usize = SB_SIZE + FRAME_MARGIN;
#[derive(Debug, Clone, Copy)]
pub struct VideoDetails {
pub width: usize,
pub height: usize,
pub bit_depth: usize,
pub chroma_sampling: ChromaSubsampling,
pub frame_rate: Rational32,
pub total_frames: Option<usize>,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct DecoderConfig {
pub luma_only: bool,
}
#[cfg(test)]
impl Default for VideoDetails {
#[inline]
fn default() -> Self {
VideoDetails {
width: 640,
height: 480,
bit_depth: 8,
chroma_sampling: ChromaSubsampling::Yuv420,
frame_rate: Rational32::new(30, 1),
total_frames: None,
}
}
}
pub struct Decoder {
decoder: DecoderImpl,
video_details: VideoDetails,
frames_read: usize,
config: DecoderConfig,
}
impl Decoder {
#[inline]
#[expect(clippy::allow_attributes)]
#[allow(
unreachable_code,
reason = "some branches are unreachable with some combinations of features"
)]
pub fn from_file<P: AsRef<Path>>(input: P) -> Result<Decoder, DecoderError> {
let ext = input
.as_ref()
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_ascii_lowercase());
if let Some(ext) = ext.as_deref() {
if Y4M_EXTENSIONS.contains(&ext) {
let reader =
BufReader::new(File::open(input).map_err(|e| DecoderError::FileReadError {
cause: e.to_string(),
})?);
let decoder = DecoderImpl::Y4m(
y4m::decode(Box::new(reader) as Box<dyn Read>).map_err(|e| match e {
y4m::Error::EOF => DecoderError::EndOfFile,
_ => DecoderError::GenericDecodeError {
cause: e.to_string(),
},
})?,
);
let video_details = decoder.video_details()?;
return Ok(Decoder {
decoder,
video_details,
frames_read: 0,
config: DecoderConfig::default(),
});
}
#[cfg(feature = "vapoursynth")]
if ext == "vpy" {
let decoder =
DecoderImpl::Vapoursynth(VapoursynthDecoder::from_file(input, HashMap::new())?);
let video_details = decoder.video_details()?;
return Ok(Decoder {
decoder,
video_details,
frames_read: 0,
config: DecoderConfig::default(),
});
}
}
#[cfg(feature = "ffms2")]
{
let decoder = DecoderImpl::Ffms2(Ffms2Decoder::new(input)?);
let video_details = decoder.video_details()?;
return Ok(Decoder {
decoder,
video_details,
frames_read: 0,
config: DecoderConfig::default(),
});
}
#[cfg(feature = "ffmpeg")]
{
let decoder = DecoderImpl::Ffmpeg(FfmpegDecoder::new(input)?);
let video_details = decoder.video_details()?;
return Ok(Decoder {
decoder,
video_details,
frames_read: 0,
config: DecoderConfig::default(),
});
}
#[cfg(feature = "vapoursynth")]
{
use crate::util::escape_python_string;
let script = format!(
r#"
import vapoursynth as vs
core = vs.core
clip = core.ffms2.Source("{}")
clip.set_output()
"#,
escape_python_string(
&std::path::absolute(input)
.map_err(|e| DecoderError::FileReadError {
cause: e.to_string()
})?
.to_string_lossy()
)
);
let decoder =
DecoderImpl::Vapoursynth(VapoursynthDecoder::from_script(&script, HashMap::new())?);
let video_details = decoder.video_details()?;
return Ok(Decoder {
decoder,
video_details,
frames_read: 0,
config: DecoderConfig::default(),
});
}
Err(DecoderError::NoDecoder)
}
#[inline]
#[cfg(feature = "vapoursynth")]
pub fn from_script(
script: &str,
variables: HashMap<VariableName, VariableValue>,
) -> Result<Decoder, DecoderError> {
let dec = VapoursynthDecoder::from_script(script, variables)?;
let decoder = DecoderImpl::Vapoursynth(dec);
let video_details = decoder.video_details()?;
Ok(Decoder {
decoder,
video_details,
frames_read: 0,
config: DecoderConfig::default(),
})
}
#[inline]
pub fn from_stdin() -> Result<Decoder, DecoderError> {
let reader = BufReader::new(stdin());
let decoder = DecoderImpl::Y4m(y4m::decode(Box::new(reader) as Box<dyn Read>).map_err(
|e| match e {
y4m::Error::EOF => DecoderError::EndOfFile,
_ => DecoderError::GenericDecodeError {
cause: e.to_string(),
},
},
)?);
let video_details: VideoDetails = decoder.video_details()?;
Ok(Decoder {
decoder,
video_details,
frames_read: 0,
config: DecoderConfig::default(),
})
}
#[inline]
pub fn from_decoder_impl(decoder_impl: DecoderImpl) -> Result<Decoder, DecoderError> {
let video_details = decoder_impl.video_details()?;
Ok(Decoder {
decoder: decoder_impl,
video_details,
frames_read: 0,
config: DecoderConfig::default(),
})
}
#[inline]
#[must_use]
pub fn get_video_details(&self) -> &VideoDetails {
&self.video_details
}
#[inline]
pub fn set_luma_only(&mut self, enabled: bool) {
self.config.luma_only = enabled;
}
#[inline]
pub fn read_video_frame<T: Pixel>(&mut self) -> Result<Frame<T>, DecoderError> {
let result = self.decoder.read_video_frame(
&self.video_details,
#[cfg(any(feature = "ffmpeg", feature = "vapoursynth", feature = "ffms2"))]
self.frames_read,
self.config.luma_only,
);
if result.is_ok() {
self.frames_read += 1;
}
result
}
#[inline]
#[cfg(feature = "vapoursynth")]
pub fn get_video_frame<T: Pixel>(
&mut self,
frame_index: usize,
) -> Result<Frame<T>, DecoderError> {
self.decoder.get_video_frame(
#[cfg(feature = "vapoursynth")]
&self.video_details,
#[cfg(feature = "vapoursynth")]
frame_index,
self.config.luma_only,
)
}
#[inline]
#[cfg(feature = "vapoursynth")]
pub fn get_vapoursynth_env(&mut self) -> Result<&mut Environment, DecoderError> {
match self.decoder {
DecoderImpl::Vapoursynth(ref mut dec) => Ok(dec.get_env()),
_ => Err(DecoderError::UnsupportedDecoder),
}
}
#[inline]
#[cfg(feature = "vapoursynth")]
pub fn get_vapoursynth_node(&self) -> Result<Node<'_>, DecoderError> {
match self.decoder {
DecoderImpl::Vapoursynth(ref dec) => Ok(dec.get_output_node()),
_ => Err(DecoderError::UnsupportedDecoder),
}
}
}
pub enum DecoderImpl {
Y4m(Y4mDecoder<Box<dyn Read>>),
#[cfg(feature = "vapoursynth")]
Vapoursynth(VapoursynthDecoder),
#[cfg(feature = "ffmpeg")]
Ffmpeg(FfmpegDecoder),
#[cfg(feature = "ffms2")]
Ffms2(Ffms2Decoder),
}
impl DecoderImpl {
pub(crate) fn video_details(&self) -> Result<VideoDetails, DecoderError> {
match self {
Self::Y4m(dec) => Ok(helpers::y4m::get_video_details(dec)),
#[cfg(feature = "vapoursynth")]
Self::Vapoursynth(dec) => dec.get_video_details(),
#[cfg(feature = "ffmpeg")]
Self::Ffmpeg(dec) => Ok(dec.video_details),
#[cfg(feature = "ffms2")]
Self::Ffms2(dec) => Ok(dec.video_details),
}
}
pub(crate) fn read_video_frame<T: Pixel>(
&mut self,
cfg: &VideoDetails,
#[cfg(any(feature = "ffmpeg", feature = "vapoursynth", feature = "ffms2"))]
frame_index: usize,
luma_only: bool,
) -> Result<Frame<T>, DecoderError> {
match self {
Self::Y4m(dec) => {
helpers::y4m::read_video_frame::<Box<dyn Read>, T>(dec, cfg, luma_only)
}
#[cfg(feature = "vapoursynth")]
Self::Vapoursynth(dec) => dec.read_video_frame::<T>(cfg, frame_index, luma_only),
#[cfg(feature = "ffmpeg")]
Self::Ffmpeg(dec) => dec.read_video_frame::<T>(frame_index, luma_only),
#[cfg(feature = "ffms2")]
Self::Ffms2(dec) => dec.read_video_frame::<T>(frame_index, luma_only),
}
}
#[cfg(feature = "vapoursynth")]
pub(crate) fn get_video_frame<T: Pixel>(
&mut self,
cfg: &VideoDetails,
frame_index: usize,
luma_only: bool,
) -> Result<Frame<T>, DecoderError> {
match self {
#[cfg(feature = "vapoursynth")]
Self::Vapoursynth(dec) => dec.read_video_frame::<T>(cfg, frame_index, luma_only),
_ => Err(DecoderError::UnsupportedDecoder),
}
}
}