1use std::path::Path;
5
6use crate::decode::{Decoder, Input, Packet, Scaler};
7use crate::ffi;
8use crate::sync;
9use crate::{Error, Frame, Info, Result};
10
11pub fn probe(path: &Path) -> Result<Info> {
13 let input = Input::open(path)?;
14 let (index, codec) = input.best(ffi::AVMEDIA_TYPE_VIDEO).ok_or_else(|| Error("no video stream FFmpeg can decode".into()))?;
15 let video = Decoder::open(&input, index, codec)?;
16 Ok(info(&input, &video))
17}
18
19pub(crate) fn info(input: &Input, video: &Decoder) -> Info {
20 let s = &video.info;
21 let (width, height) = if s.turns % 2 == 1 { (s.height, s.width) } else { (s.width, s.height) };
22 Info {
23 duration: input.duration(Some(s.index)),
24 width: width.max(0) as u32,
25 height: height.max(0) as u32,
26 fps: s.fps as f32,
27 has_audio: input.best(ffi::AVMEDIA_TYPE_AUDIO).is_some(),
28 turns: s.turns,
29 }
30}
31
32pub struct Grabber {
36 input: Input,
37 video: Decoder,
38 scaler: Scaler,
39 packet: Packet,
40 max_side: u32,
41 info: Info,
42 last: Option<f64>,
45}
46
47const DECODE_ON: f64 = 1.0;
50const MAX_PACKETS: usize = 5000;
52
53impl Grabber {
54 pub fn open(path: &Path, max_side: u32) -> Result<Grabber> {
57 let input = Input::open(path)?;
58 let (index, codec) = input.best(ffi::AVMEDIA_TYPE_VIDEO).ok_or_else(|| Error("no video stream FFmpeg can decode".into()))?;
59 let video = Decoder::open(&input, index, codec)?;
60 let info = info(&input, &video);
61 let (scaler, packet) = (Scaler::new(input.api()), Packet::new(input.api())?);
62 Ok(Grabber { input, video, scaler, packet, max_side, info, last: None })
63 }
64
65 pub fn info(&self) -> &Info {
66 &self.info
67 }
68
69 pub fn grab(&mut self, secs: f64) -> Result<Frame> {
73 let secs = secs.max(0.0);
74 let on = self.last.is_some_and(|l| secs >= l && secs - l < DECODE_ON);
75 if !on {
76 self.input.seek(secs)?;
77 self.video.flush();
78 }
79 let fps = self.video.info.fps;
80 let index = self.video.info.index;
81 let mut ended = false;
82 let mut held = false;
83 for _ in 0..MAX_PACKETS {
84 if !ended {
85 self.packet.unref();
86 if self.input.read(&mut self.packet)? {
87 if self.packet.stream_index() != index {
88 continue;
89 }
90 self.video.send(Some(&self.packet))?;
91 } else {
92 ended = true;
93 self.video.send(None)?;
94 }
95 }
96 let mut got = false;
97 while self.video.receive() {
98 got = true;
99 let f = self.video.frame();
100 let pts = self.video.pts(f);
101 if pts.is_some_and(|t| sync::before_target(t, secs, fps)) {
102 self.video.hold();
103 held = true;
104 continue;
105 }
106 let frame = self.scaler.convert(f, &self.video.info, self.max_side).ok_or_else(|| Error("could not convert the picture".into()))?;
107 self.last = pts;
108 return Ok(frame);
109 }
110 if ended && !got {
111 break;
112 }
113 }
114 self.last = None;
117 if held && let Some(f) = self.video.held() {
118 return self.scaler.convert(f, &self.video.info, self.max_side).ok_or_else(|| Error("could not convert the picture".into()));
119 }
120 Err(Error(format!("no picture at {secs:.2} s")))
121 }
122}
123
124pub fn frame_at(path: &Path, secs: f64) -> Result<Frame> {
126 Grabber::open(path, u32::MAX)?.grab(secs)
127}
128
129const BLACK_RETRIES: usize = 5;
131
132pub fn thumbnail(path: &Path) -> Result<(Info, Frame, f64)> {
136 let mut g = Grabber::open(path, u32::MAX)?;
137 let info = g.info().clone();
138 let first_t = sync::thumbnail_time(info.duration);
139 let first = g.grab(first_t)?;
140 if !first.is_black() || info.duration <= 0.0 {
141 return Ok((info, first, first_t));
142 }
143 let step = (info.duration - first_t) / (BLACK_RETRIES + 1) as f64;
144 for k in 1..=BLACK_RETRIES {
145 let t = first_t + step * k as f64;
146 if let Ok(f) = g.grab(t)
147 && !f.is_black()
148 {
149 return Ok((info, f, t));
150 }
151 }
152 Ok((info, first, first_t))
153}