ffmpeg_frame_grabber/
ffprobe.rs1use 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 }
24
25#[derive(Clone, Debug)]
26pub struct VideoStreamInfo {
27 pub width: u32,
29
30 pub height: u32,
32
33 pub frame_rate: f64,
35
36 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}