Skip to main content

falcon_ffmpeg/
media.rs

1//! Reading a file's facts and single pictures: what the viewer's probe,
2//! the seek bar and hover previews, and the import thumbnail need.
3
4use std::path::Path;
5
6use crate::decode::{Decoder, Input, Packet, Scaler};
7use crate::ffi;
8use crate::sync;
9use crate::{Error, Frame, Info, Result};
10
11/// The facts of a file with a video stream FFmpeg can decode.
12pub 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
32/// Decodes single pictures of one file, scaled to fit a size: for the seek
33/// bar's hover thumbnail, the grid's hover previews and thumbnails.
34/// Usable from any thread.
35pub struct Grabber {
36    input: Input,
37    video: Decoder,
38    scaler: Scaler,
39    packet: Packet,
40    max_side: u32,
41    info: Info,
42    /// The time of the last picture decoded, to decode on from it rather
43    /// than seek when the next request is just ahead.
44    last: Option<f64>,
45}
46
47/// How far ahead of the last picture a request is decoded to without
48/// seeking, in seconds.
49const DECODE_ON: f64 = 1.0;
50/// Packets read for one picture before giving up.
51const MAX_PACKETS: usize = 5000;
52
53impl Grabber {
54    /// Opens `path`; pictures are scaled so their long side is at most
55    /// `max_side` pixels.
56    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    /// The picture at `secs`: the first frame at or after it, decoded on
70    /// from the keyframe before it; the last frame when `secs` is past the
71    /// end.
72    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        // Past the end: the last frame decoded. The decoder is drained, so
115        // the next request seeks.
116        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
124/// The picture at `secs` at full size.
125pub fn frame_at(path: &Path, secs: f64) -> Result<Frame> {
126    Grabber::open(path, u32::MAX)?.grab(secs)
127}
128
129/// How many later pictures are tried when the thumbnail's picture is black.
130const BLACK_RETRIES: usize = 5;
131
132/// An import thumbnail: the picture where Eagle takes one
133/// ([`sync::thumbnail_time`]), or the first of a few later ones that is not
134/// black, at full size, with the file's facts and the time used.
135pub 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}