Skip to main content

ffmpeg_frame_grabber/
ffprobe.rs

1use crate::error::{CommandSpawnError, FFMpegError, IOError};
2use crate::utils::{fractional_from_str, from_str};
3use io::Read;
4use serde::Deserialize;
5use snafu::ResultExt;
6use std::result::Result;
7use std::{collections::HashMap, io::BufReader};
8use std::{fmt::Debug, time::Duration};
9use std::{io, path::PathBuf};
10use std::{
11    path::Path,
12    process::{Command, Stdio},
13};
14
15pub struct FFProbeInfo {
16    pub duration: Duration,
17    streams: Vec<StreamInfo>,
18}
19
20enum StreamInfo {
21    Video(VideoStreamInfo),
22    // TODO to be extended with AudioStreamInfo
23}
24
25#[derive(Clone, Debug)]
26pub struct VideoStreamInfo {
27    /// The width of each frame.
28    pub width: u32,
29
30    // The height of each frames.
31    pub height: u32,
32
33    // The frame rate of this stream.
34    pub frame_rate: f64,
35
36    /// The total count of frames in this stream as set in the metadata.
37    /// The actual count of frames that can be read might differ.
38    pub frames_count: u64,
39}
40
41impl FFProbeInfo {
42    pub fn of(
43        input_video_path: &Path,
44        ffprobe_path: Option<PathBuf>,
45    ) -> Result<FFProbeInfo, FFMpegError> {
46        let output = FFProbeOutput::of(input_video_path, ffprobe_path)?;
47        Ok(FFProbeInfo {
48            duration: Duration::from_secs_f64(output.format.duration),
49            streams: output
50                .streams
51                .iter()
52                .filter(|s| s.width.is_some() && s.height.is_some())
53                .map(|s| {
54                    StreamInfo::Video(VideoStreamInfo {
55                        width: s.width.unwrap(),
56                        height: s.height.unwrap(),
57                        frame_rate: s.avg_frame_rate,
58                        frames_count: s.nb_frames,
59                    })
60                })
61                .collect(),
62        })
63    }
64
65    pub fn duration(&self) -> Duration {
66        self.duration
67    }
68
69    #[allow(unreachable_patterns)]
70    pub fn primary_video_stream(&self) -> Option<&VideoStreamInfo> {
71        let video_streams = self
72            .streams
73            .iter()
74            .filter_map(|s| match s {
75                StreamInfo::Video(v) => Some(v),
76                _ => None,
77            })
78            .collect::<Vec<_>>();
79
80        if video_streams.len() == 1 {
81            video_streams.first().cloned()
82        } else {
83            None
84        }
85    }
86}
87
88#[derive(Deserialize, Debug)]
89struct FFProbeOutput {
90    streams: Vec<FFProbeStreamInfo>,
91    format: FFProbeFormat,
92}
93
94impl FFProbeOutput {
95    pub fn of(
96        input_video_path: &Path,
97        ffprobe_path: Option<PathBuf>,
98    ) -> Result<FFProbeOutput, FFMpegError> {
99        if !input_video_path.exists() {
100            return Err(FFMpegError::FileDoesNotExistsError {
101                file: input_video_path.to_path_buf(),
102            });
103        }
104
105        let mut cmd =
106            Command::new(ffprobe_path.map_or("ffprobe".to_owned(), |p| p.to_string_lossy().into()))
107                .args(&[
108                    "-v",
109                    "error",
110                    "-show_entries",
111                    "stream",
112                    "-show_entries",
113                    "format",
114                    "-of",
115                    "json",
116                    &input_video_path.to_string_lossy(),
117                ])
118                .stdout(Stdio::piped())
119                .spawn()
120                .context(CommandSpawnError)?;
121
122        let stdout = cmd.stdout.as_mut().unwrap();
123        let mut stdout_reader = BufReader::new(stdout);
124
125        let mut json: String = String::new();
126        stdout_reader.read_to_string(&mut json).context(IOError)?;
127
128        let output: FFProbeOutput = match serde_json::from_str(&json) {
129            Ok(e) => e,
130            Err(_) => return Err(FFMpegError::ParseError),
131        };
132        Ok(output)
133    }
134}
135
136#[derive(Deserialize, Debug)]
137struct FFProbeStreamInfo {
138    codec_name: String,
139    codec_type: String,
140
141    width: Option<u32>,
142    height: Option<u32>,
143
144    #[serde(deserialize_with = "fractional_from_str")]
145    r_frame_rate: f64,
146    #[serde(deserialize_with = "fractional_from_str")]
147    avg_frame_rate: f64,
148    #[serde(deserialize_with = "from_str")]
149    nb_frames: u64,
150}
151
152#[derive(Deserialize, Debug)]
153struct FFProbeFormat {
154    #[serde(deserialize_with = "from_str")]
155    duration: f64,
156    tags: HashMap<String, String>,
157}