1use std::io::Read;
8use std::path::Path;
9
10use rff_core::{CodecId, Error, MediaType, Rational, Result};
11
12use crate::Engine;
13
14#[derive(Debug, Clone)]
16pub struct MediaInfo {
17 pub format_name: String,
19 pub streams: Vec<StreamInfo>,
21}
22
23#[derive(Debug, Clone)]
25pub struct StreamInfo {
26 pub index: usize,
27 pub media_type: MediaType,
28 pub codec_id: CodecId,
29 pub time_base: Rational,
30 pub width: u32,
31 pub height: u32,
32 pub sample_rate: u32,
33 pub channels: u16,
34}
35
36pub fn probe(engine: &Engine, path: impl AsRef<Path>) -> Result<MediaInfo> {
45 let path = path.as_ref();
46 let path_str = path
47 .to_str()
48 .ok_or_else(|| Error::Option("input path is not valid UTF-8".into()))?;
49 let (format_name, source) = open_source(engine, path_str, None)?;
50 let mut demuxer = source.open_demuxer(engine, &format_name)?;
51
52 let streams = demuxer.read_header()?;
53 let streams = streams
54 .into_iter()
55 .map(|s| StreamInfo {
56 index: s.index,
57 media_type: s.media_type,
58 codec_id: s.codec_id,
59 time_base: s.time_base,
60 width: s.width,
61 height: s.height,
62 sample_rate: s.sample_rate,
63 channels: s.channels,
64 })
65 .collect();
66
67 Ok(MediaInfo {
68 format_name,
69 streams,
70 })
71}
72
73pub(crate) fn detect_input_format(
78 engine: &Engine,
79 path: &Path,
80 forced: Option<&str>,
81) -> Result<String> {
82 if let Some(name) = forced {
83 return Ok(name.to_string());
84 }
85 if let Ok(head) = read_head(path, 4096) {
87 if let Some(format) = engine.formats.probe(&head) {
88 return Ok(format.name.to_string());
89 }
90 }
91 let ext = path
93 .extension()
94 .and_then(|e| e.to_str())
95 .unwrap_or_default();
96 engine
97 .formats
98 .by_extension(ext)
99 .map(|f| f.name.to_string())
100 .ok_or_else(|| Error::DemuxerNotFound(path.display().to_string()))
101}
102
103fn read_head(path: &Path, max: usize) -> std::io::Result<Vec<u8>> {
105 let file = std::fs::File::open(path)?;
106 let mut buf = Vec::new();
107 file.take(max as u64).read_to_end(&mut buf)?;
108 Ok(buf)
109}
110
111pub(crate) enum Source {
115 File(rff_format::SeekInput),
116 Stream(Box<dyn Read + Send>),
117}
118
119impl Source {
120 pub(crate) fn open_demuxer(
123 self,
124 engine: &Engine,
125 format: &str,
126 ) -> Result<Box<dyn rff_format::Demuxer>> {
127 match self {
128 Source::File(file) => engine.formats.open_demuxer_seekable(format, file),
129 Source::Stream(stream) => engine.formats.open_demuxer(format, stream),
130 }
131 }
132}
133
134pub(crate) fn open_source(
144 engine: &Engine,
145 path: &str,
146 forced: Option<&str>,
147) -> Result<(String, Source)> {
148 let stem = path.split(['?', '#']).next().unwrap_or(path);
149 if forced == Some("hls") || stem.to_ascii_lowercase().ends_with(".m3u8") {
150 let reader = rff_format_hls::open_input(path)?;
151 return sniff_stream(engine, reader, None, path);
152 }
153 if !rff_io::is_stream(path) {
154 let format = detect_input_format(engine, Path::new(path), forced)?;
155 let file = std::io::BufReader::new(std::fs::File::open(path)?);
156 return Ok((format, Source::File(Box::new(file))));
157 }
158 if rff_io::is_rtp(path) {
161 let reader = rff_io::RtpReader::bind(path)?;
162 let format = forced
163 .map(str::to_string)
164 .unwrap_or_else(|| reader.format_name().to_string());
165 return Ok((format, Source::Stream(Box::new(reader))));
166 }
167 let reader = rff_io::open(path)?;
168 sniff_stream(engine, reader, forced, path)
169}
170
171fn sniff_stream(
175 engine: &Engine,
176 mut reader: Box<dyn Read + Send>,
177 forced: Option<&str>,
178 path: &str,
179) -> Result<(String, Source)> {
180 let mut head = vec![0u8; 4096];
181 let n = read_some(&mut reader, &mut head)?;
182 head.truncate(n);
183 if forced.is_none() && head.starts_with(b"#EXTM3U") {
184 if rff_io::is_pipe(path) {
185 return Err(Error::unsupported(
186 "hls input from a pipe (the playlist's segments cannot be fetched relative to stdin)",
187 ));
188 }
189 let reader = rff_format_hls::open_input(path)?;
190 return sniff_stream(engine, reader, None, path);
191 }
192 let format = match forced {
193 Some(f) => f.to_string(),
194 None => engine
195 .formats
196 .probe(&head)
197 .map(|f| f.name.to_string())
198 .or_else(|| url_extension_format(engine, path))
199 .ok_or_else(|| Error::DemuxerNotFound(format!("{path} (pass -f)")))?,
200 };
201 let chained: Box<dyn Read + Send> = Box::new(std::io::Cursor::new(head).chain(reader));
202 Ok((format, Source::Stream(chained)))
203}
204
205fn read_some(reader: &mut dyn Read, buf: &mut [u8]) -> Result<usize> {
208 let mut filled = 0;
209 while filled < buf.len() {
210 match reader.read(&mut buf[filled..]) {
211 Ok(0) => break,
212 Ok(n) => filled += n,
213 Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
214 Err(e) => return Err(e.into()),
215 }
216 }
217 Ok(filled)
218}
219
220fn url_extension_format(engine: &Engine, url: &str) -> Option<String> {
223 let stem = url.split(['?', '#']).next().unwrap_or(url);
224 let ext = stem.rsplit('.').next().unwrap_or("");
225 engine.formats.by_extension(ext).map(|f| f.name.to_string())
226}