1#[cfg(feature = "vapoursynth")]
29use std::collections::HashMap;
30use std::fs::File;
31use std::io::{BufReader, Read, stdin};
32use std::path::Path;
33use v_frame::chroma::ChromaSubsampling;
34use v_frame::frame::Frame;
35use v_frame::pixel::Pixel;
36
37mod error;
38mod helpers {
39 #[cfg(feature = "ffmpeg")]
40 pub(crate) mod ffmpeg;
41 #[cfg(feature = "ffms2")]
42 pub(crate) mod ffms2;
43 mod frame_builder;
44 #[cfg(feature = "vapoursynth")]
45 pub(crate) mod vapoursynth;
46 pub(crate) mod y4m;
47}
48mod util;
49
50#[cfg(feature = "ffmpeg")]
51pub use crate::helpers::ffmpeg::FfmpegDecoder;
52#[cfg(feature = "ffms2")]
53pub use crate::helpers::ffms2::Ffms2Decoder;
54#[cfg(feature = "vapoursynth")]
55pub use crate::helpers::vapoursynth::ModifyNode;
56#[cfg(feature = "vapoursynth")]
57pub use crate::helpers::vapoursynth::VapoursynthDecoder;
58#[cfg(feature = "vapoursynth")]
59use crate::helpers::vapoursynth::{VariableName, VariableValue};
60pub use error::DecoderError;
61pub use num_rational::Rational32;
62pub use v_frame;
63pub use y4m::Decoder as Y4mDecoder;
64
65const Y4M_EXTENSIONS: &[&str] = &["y4m", "yuv"];
66
67const SB_SIZE_LOG2: usize = 6;
69const SB_SIZE: usize = 1 << SB_SIZE_LOG2;
70const SUBPEL_FILTER_SIZE: usize = 8;
71const FRAME_MARGIN: usize = 16 + SUBPEL_FILTER_SIZE;
72const LUMA_PADDING: usize = SB_SIZE + FRAME_MARGIN;
73
74#[derive(Debug, Clone, Copy)]
76pub struct VideoDetails {
77 pub width: usize,
79 pub height: usize,
81 pub bit_depth: usize,
83 pub chroma_sampling: ChromaSubsampling,
85 pub frame_rate: Rational32,
87 pub total_frames: Option<usize>,
89}
90
91#[derive(Debug, Clone, Copy, Default)]
93pub struct DecoderConfig {
94 pub luma_only: bool,
96}
97
98#[cfg(test)]
99impl Default for VideoDetails {
100 #[inline]
101 fn default() -> Self {
102 VideoDetails {
103 width: 640,
104 height: 480,
105 bit_depth: 8,
106 chroma_sampling: ChromaSubsampling::Yuv420,
107 frame_rate: Rational32::new(30, 1),
108 total_frames: None,
109 }
110 }
111}
112
113pub struct Decoder {
117 decoder: DecoderImpl,
118 video_details: VideoDetails,
119 frames_read: usize,
120 config: DecoderConfig,
121}
122
123impl Decoder {
124 #[inline]
133 #[expect(clippy::allow_attributes)]
134 #[allow(
135 unreachable_code,
136 reason = "some branches are unreachable with some combinations of features"
137 )]
138 pub fn from_file<P: AsRef<Path>>(input: P) -> Result<Decoder, DecoderError> {
139 let ext = input
142 .as_ref()
143 .extension()
144 .and_then(|ext| ext.to_str())
145 .map(|ext| ext.to_ascii_lowercase());
146 if let Some(ext) = ext.as_deref() {
147 if Y4M_EXTENSIONS.contains(&ext) {
148 let reader =
149 BufReader::new(File::open(input).map_err(|e| DecoderError::FileReadError {
150 cause: e.to_string(),
151 })?);
152 let decoder = DecoderImpl::Y4m(
153 y4m::decode(Box::new(reader) as Box<dyn Read>).map_err(|e| match e {
154 y4m::Error::EOF => DecoderError::EndOfFile,
155 _ => DecoderError::GenericDecodeError {
156 cause: e.to_string(),
157 },
158 })?,
159 );
160 let video_details = decoder.video_details()?;
161 return Ok(Decoder {
162 decoder,
163 video_details,
164 frames_read: 0,
165 config: DecoderConfig::default(),
166 });
167 }
168
169 #[cfg(feature = "vapoursynth")]
170 if ext == "vpy" {
171 let decoder = DecoderImpl::Vapoursynth(VapoursynthDecoder::from_file(
173 input,
174 HashMap::new(),
175 None,
176 )?);
177 let video_details = decoder.video_details()?;
178 return Ok(Decoder {
179 decoder,
180 video_details,
181 frames_read: 0,
182 config: DecoderConfig::default(),
183 });
184 }
185 }
186
187 #[cfg(feature = "ffms2")]
189 {
190 let decoder = DecoderImpl::Ffms2(Ffms2Decoder::new(input, None)?);
191 let video_details = decoder.video_details()?;
192 return Ok(Decoder {
193 decoder,
194 video_details,
195 frames_read: 0,
196 config: DecoderConfig::default(),
197 });
198 }
199
200 #[cfg(feature = "ffmpeg")]
203 {
204 let decoder = DecoderImpl::Ffmpeg(FfmpegDecoder::new(input)?);
205 let video_details = decoder.video_details()?;
206 return Ok(Decoder {
207 decoder,
208 video_details,
209 frames_read: 0,
210 config: DecoderConfig::default(),
211 });
212 }
213
214 #[cfg(feature = "vapoursynth")]
215 {
216 use crate::util::escape_python_string;
218
219 let script = format!(
220 r#"
221import vapoursynth as vs
222core = vs.core
223clip = core.ffms2.Source("{}")
224clip.set_output()
225"#,
226 escape_python_string(
227 &std::path::absolute(input)
228 .map_err(|e| DecoderError::FileReadError {
229 cause: e.to_string()
230 })?
231 .to_string_lossy()
232 )
233 );
234 let decoder = DecoderImpl::Vapoursynth(VapoursynthDecoder::from_script(
235 &script,
236 HashMap::new(),
237 None,
238 )?);
239 let video_details = decoder.video_details()?;
240 return Ok(Decoder {
241 decoder,
242 video_details,
243 frames_read: 0,
244 config: DecoderConfig::default(),
245 });
246 }
247
248 Err(DecoderError::NoDecoder)
249 }
250
251 #[inline]
261 #[cfg(feature = "vapoursynth")]
262 pub fn from_script(
263 script: &str,
264 variables: HashMap<VariableName, VariableValue>,
265 ) -> Result<Decoder, DecoderError> {
266 let dec = VapoursynthDecoder::from_script(script, variables, None)?;
267 let decoder = DecoderImpl::Vapoursynth(dec);
268 let video_details = decoder.video_details()?;
269 Ok(Decoder {
270 decoder,
271 video_details,
272 frames_read: 0,
273 config: DecoderConfig::default(),
274 })
275 }
276
277 #[inline]
284 pub fn from_stdin() -> Result<Decoder, DecoderError> {
285 let reader = BufReader::new(stdin());
287 let decoder = DecoderImpl::Y4m(y4m::decode(Box::new(reader) as Box<dyn Read>).map_err(
288 |e| match e {
289 y4m::Error::EOF => DecoderError::EndOfFile,
290 _ => DecoderError::GenericDecodeError {
291 cause: e.to_string(),
292 },
293 },
294 )?);
295 let video_details: VideoDetails = decoder.video_details()?;
296 Ok(Decoder {
297 decoder,
298 video_details,
299 frames_read: 0,
300 config: DecoderConfig::default(),
301 })
302 }
303
304 #[inline]
313 pub fn from_decoder_impl(decoder_impl: DecoderImpl) -> Result<Decoder, DecoderError> {
314 let video_details = decoder_impl.video_details()?;
315 Ok(Decoder {
316 decoder: decoder_impl,
317 video_details,
318 frames_read: 0,
319 config: DecoderConfig::default(),
320 })
321 }
322
323 #[inline]
325 #[must_use]
326 pub fn get_video_details(&self) -> &VideoDetails {
327 &self.video_details
328 }
329
330 #[inline]
333 pub fn set_luma_only(&mut self, enabled: bool) {
334 self.config.luma_only = enabled;
335 }
336
337 #[inline]
348 pub fn read_video_frame<T: Pixel>(&mut self) -> Result<Frame<T>, DecoderError> {
349 let result = self.decoder.read_video_frame(
350 &self.video_details,
351 #[cfg(any(feature = "ffmpeg", feature = "vapoursynth", feature = "ffms2"))]
352 self.frames_read,
353 self.config.luma_only,
354 );
355 if result.is_ok() {
356 self.frames_read += 1;
357 }
358 result
359 }
360
361 #[inline]
370 #[cfg(feature = "vapoursynth")]
371 pub fn get_video_frame<T: Pixel>(
372 &mut self,
373 frame_index: usize,
374 ) -> Result<Frame<T>, DecoderError> {
375 self.decoder.get_video_frame(
376 #[cfg(feature = "vapoursynth")]
377 &self.video_details,
378 #[cfg(feature = "vapoursynth")]
379 frame_index,
380 self.config.luma_only,
381 )
382 }
383
384 #[inline]
391 #[cfg(any(feature = "vapoursynth", feature = "ffms2"))]
392 pub fn seek_to_frame(&mut self, frame_index: usize) -> Result<(), DecoderError> {
393 match &self.decoder {
394 #[cfg(feature = "vapoursynth")]
395 DecoderImpl::Vapoursynth(_) => {
396 if self
397 .video_details
398 .total_frames
399 .is_some_and(|total_frames| frame_index >= total_frames)
400 {
401 return Err(DecoderError::EndOfFile);
402 }
403 self.frames_read = frame_index;
404 Ok(())
405 }
406 #[cfg(feature = "ffms2")]
407 DecoderImpl::Ffms2(_) => {
408 if self
409 .video_details
410 .total_frames
411 .is_some_and(|total_frames| frame_index >= total_frames)
412 {
413 return Err(DecoderError::EndOfFile);
414 }
415 self.frames_read = frame_index;
416 Ok(())
417 }
418 _ => Err(DecoderError::UnsupportedDecoder),
419 }
420 }
421
422 #[inline]
424 #[cfg(feature = "ffmpeg")]
425 pub fn get_ffmpeg_impl(&mut self) -> Option<&mut FfmpegDecoder> {
426 match &mut self.decoder {
427 DecoderImpl::Ffmpeg(dec) => Some(dec),
428 _ => None,
429 }
430 }
431
432 #[inline]
434 #[cfg(feature = "ffms2")]
435 pub fn get_ffms2_impl(&mut self) -> Option<&mut Ffms2Decoder> {
436 match &mut self.decoder {
437 DecoderImpl::Ffms2(dec) => Some(dec),
438 _ => None,
439 }
440 }
441
442 #[inline]
444 #[cfg(feature = "vapoursynth")]
445 pub fn get_vapoursynth_impl(&mut self) -> Option<&mut VapoursynthDecoder> {
446 match &mut self.decoder {
447 DecoderImpl::Vapoursynth(dec) => Some(dec),
448 _ => None,
449 }
450 }
451}
452
453pub enum DecoderImpl {
457 Y4m(Y4mDecoder<Box<dyn Read>>),
459
460 #[cfg(feature = "vapoursynth")]
462 Vapoursynth(VapoursynthDecoder),
463
464 #[cfg(feature = "ffmpeg")]
466 Ffmpeg(FfmpegDecoder),
467
468 #[cfg(feature = "ffms2")]
470 Ffms2(Ffms2Decoder),
471}
472
473impl DecoderImpl {
474 pub(crate) fn video_details(&self) -> Result<VideoDetails, DecoderError> {
475 match self {
476 Self::Y4m(dec) => Ok(helpers::y4m::get_video_details(dec)),
477 #[cfg(feature = "vapoursynth")]
478 Self::Vapoursynth(dec) => dec.get_video_details(),
479 #[cfg(feature = "ffmpeg")]
480 Self::Ffmpeg(dec) => Ok(dec.video_details),
481 #[cfg(feature = "ffms2")]
482 Self::Ffms2(dec) => Ok(dec.video_details),
483 }
484 }
485
486 pub(crate) fn read_video_frame<T: Pixel>(
487 &mut self,
488 cfg: &VideoDetails,
489 #[cfg(any(feature = "ffmpeg", feature = "vapoursynth", feature = "ffms2"))]
490 frame_index: usize,
491 luma_only: bool,
492 ) -> Result<Frame<T>, DecoderError> {
493 match self {
494 Self::Y4m(dec) => {
495 helpers::y4m::read_video_frame::<Box<dyn Read>, T>(dec, cfg, luma_only)
496 }
497 #[cfg(feature = "vapoursynth")]
498 Self::Vapoursynth(dec) => dec.read_video_frame::<T>(cfg, frame_index, luma_only),
499 #[cfg(feature = "ffmpeg")]
500 Self::Ffmpeg(dec) => dec.read_video_frame::<T>(frame_index, luma_only),
501 #[cfg(feature = "ffms2")]
502 Self::Ffms2(dec) => dec.read_video_frame::<T>(frame_index, luma_only),
503 }
504 }
505
506 #[cfg(feature = "vapoursynth")]
507 pub(crate) fn get_video_frame<T: Pixel>(
508 &mut self,
509 cfg: &VideoDetails,
510 frame_index: usize,
511 luma_only: bool,
512 ) -> Result<Frame<T>, DecoderError> {
513 match self {
514 #[cfg(feature = "vapoursynth")]
515 Self::Vapoursynth(dec) => dec.read_video_frame::<T>(cfg, frame_index, luma_only),
516 #[cfg(feature = "ffms2")]
517 Self::Ffms2(dec) => dec.read_video_frame::<T>(frame_index, luma_only),
518 _ => Err(DecoderError::UnsupportedDecoder),
519 }
520 }
521}