1use crate::types::VideoMeta;
2use serde_json::Value;
3use std::io::Read;
4use std::path::Path;
5use std::process::{Command, Stdio};
6use std::thread;
7use std::time::Duration;
8use wait_timeout::ChildExt;
9
10#[derive(Debug, Clone)]
12pub struct FfprobeOptions {
13 pub timeout: Duration,
14}
15
16impl Default for FfprobeOptions {
17 fn default() -> Self {
18 Self {
19 timeout: Duration::from_secs(30),
20 }
21 }
22}
23
24pub fn read_video_ffprobe(path: &Path, opts: &FfprobeOptions) -> Option<VideoMeta> {
26 let mut child = Command::new("ffprobe")
27 .args([
28 "-v",
29 "quiet",
30 "-print_format",
31 "json",
32 "-show_format",
33 "-show_streams",
34 ])
35 .arg(path)
36 .stdout(Stdio::piped())
37 .stderr(Stdio::null())
38 .spawn()
39 .ok()?;
40
41 let mut stdout = child.stdout.take()?;
42 let reader = thread::spawn(move || {
43 let mut buf = Vec::new();
44 stdout.read_to_end(&mut buf).map(|_| buf)
45 });
46
47 let wait = child.wait_timeout(opts.timeout).ok()?;
48 match wait {
49 Some(status) if status.success() => {
50 let bytes = match reader.join() {
51 Ok(Ok(b)) => b,
52 _ => return None,
53 };
54 let text = String::from_utf8_lossy(&bytes);
55 let meta = parse_ffprobe_json(&text);
56 if meta.codec_name.is_none()
57 && meta.width.is_none()
58 && meta.height.is_none()
59 && meta.duration_secs.is_none()
60 {
61 None
62 } else {
63 Some(meta)
64 }
65 }
66 Some(_) => {
67 let _ = reader.join();
68 None
69 }
70 None => {
71 let _ = child.kill();
72 let _ = reader.join();
73 None
74 }
75 }
76}
77
78fn parse_ffprobe_json(json: &str) -> VideoMeta {
79 let v: Value = match serde_json::from_str(json) {
80 Ok(v) => v,
81 Err(_) => return VideoMeta::default(),
82 };
83
84 let mut meta = VideoMeta::default();
85
86 if let Some(fmt) = v.get("format").and_then(|f| f.as_object()) {
87 if let Some(d) = fmt.get("duration").and_then(|x| x.as_str()) {
88 meta.duration_secs = d.parse().ok();
89 }
90 }
91
92 if let Some(streams) = v.get("streams").and_then(|s| s.as_array()) {
93 for s in streams {
94 let kind = s.get("codec_type").and_then(|x| x.as_str());
95 if kind == Some("video") {
96 meta.codec_name = s
97 .get("codec_name")
98 .and_then(|x| x.as_str())
99 .map(String::from);
100 meta.width = s.get("width").and_then(|x| x.as_u64()).map(|u| u as u32);
101 meta.height = s.get("height").and_then(|x| x.as_u64()).map(|u| u as u32);
102 break;
103 }
104 }
105 }
106
107 meta
108}