use std::io::Read;
use std::path::Path;
use rff_core::{CodecId, Error, MediaType, Rational, Result};
use crate::Engine;
#[derive(Debug, Clone)]
pub struct MediaInfo {
pub format_name: String,
pub streams: Vec<StreamInfo>,
}
#[derive(Debug, Clone)]
pub struct StreamInfo {
pub index: usize,
pub media_type: MediaType,
pub codec_id: CodecId,
pub time_base: Rational,
pub width: u32,
pub height: u32,
pub sample_rate: u32,
pub channels: u16,
}
pub fn probe(engine: &Engine, path: impl AsRef<Path>) -> Result<MediaInfo> {
let path = path.as_ref();
let path_str = path
.to_str()
.ok_or_else(|| Error::Option("input path is not valid UTF-8".into()))?;
let (format_name, source) = open_source(engine, path_str, None)?;
let mut demuxer = source.open_demuxer(engine, &format_name)?;
let streams = demuxer.read_header()?;
let streams = streams
.into_iter()
.map(|s| StreamInfo {
index: s.index,
media_type: s.media_type,
codec_id: s.codec_id,
time_base: s.time_base,
width: s.width,
height: s.height,
sample_rate: s.sample_rate,
channels: s.channels,
})
.collect();
Ok(MediaInfo {
format_name,
streams,
})
}
pub(crate) fn detect_input_format(
engine: &Engine,
path: &Path,
forced: Option<&str>,
) -> Result<String> {
if let Some(name) = forced {
return Ok(name.to_string());
}
if let Ok(head) = read_head(path, 4096) {
if let Some(format) = engine.formats.probe(&head) {
return Ok(format.name.to_string());
}
}
let ext = path
.extension()
.and_then(|e| e.to_str())
.unwrap_or_default();
engine
.formats
.by_extension(ext)
.map(|f| f.name.to_string())
.ok_or_else(|| Error::DemuxerNotFound(path.display().to_string()))
}
fn read_head(path: &Path, max: usize) -> std::io::Result<Vec<u8>> {
let file = std::fs::File::open(path)?;
let mut buf = Vec::new();
file.take(max as u64).read_to_end(&mut buf)?;
Ok(buf)
}
pub(crate) enum Source {
File(rff_format::SeekInput),
Stream(Box<dyn Read + Send>),
}
impl Source {
pub(crate) fn open_demuxer(
self,
engine: &Engine,
format: &str,
) -> Result<Box<dyn rff_format::Demuxer>> {
match self {
Source::File(file) => engine.formats.open_demuxer_seekable(format, file),
Source::Stream(stream) => engine.formats.open_demuxer(format, stream),
}
}
}
pub(crate) fn open_source(
engine: &Engine,
path: &str,
forced: Option<&str>,
) -> Result<(String, Source)> {
let stem = path.split(['?', '#']).next().unwrap_or(path);
if forced == Some("hls") || stem.to_ascii_lowercase().ends_with(".m3u8") {
let reader = rff_format_hls::open_input(path)?;
return sniff_stream(engine, reader, None, path);
}
if !rff_io::is_stream(path) {
let format = detect_input_format(engine, Path::new(path), forced)?;
let file = std::io::BufReader::new(std::fs::File::open(path)?);
return Ok((format, Source::File(Box::new(file))));
}
if rff_io::is_rtp(path) {
let reader = rff_io::RtpReader::bind(path)?;
let format = forced
.map(str::to_string)
.unwrap_or_else(|| reader.format_name().to_string());
return Ok((format, Source::Stream(Box::new(reader))));
}
let reader = rff_io::open(path)?;
sniff_stream(engine, reader, forced, path)
}
fn sniff_stream(
engine: &Engine,
mut reader: Box<dyn Read + Send>,
forced: Option<&str>,
path: &str,
) -> Result<(String, Source)> {
let mut head = vec![0u8; 4096];
let n = read_some(&mut reader, &mut head)?;
head.truncate(n);
if forced.is_none() && head.starts_with(b"#EXTM3U") {
if rff_io::is_pipe(path) {
return Err(Error::unsupported(
"hls input from a pipe (the playlist's segments cannot be fetched relative to stdin)",
));
}
let reader = rff_format_hls::open_input(path)?;
return sniff_stream(engine, reader, None, path);
}
let format = match forced {
Some(f) => f.to_string(),
None => engine
.formats
.probe(&head)
.map(|f| f.name.to_string())
.or_else(|| url_extension_format(engine, path))
.ok_or_else(|| Error::DemuxerNotFound(format!("{path} (pass -f)")))?,
};
let chained: Box<dyn Read + Send> = Box::new(std::io::Cursor::new(head).chain(reader));
Ok((format, Source::Stream(chained)))
}
fn read_some(reader: &mut dyn Read, buf: &mut [u8]) -> Result<usize> {
let mut filled = 0;
while filled < buf.len() {
match reader.read(&mut buf[filled..]) {
Ok(0) => break,
Ok(n) => filled += n,
Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e.into()),
}
}
Ok(filled)
}
fn url_extension_format(engine: &Engine, url: &str) -> Option<String> {
let stem = url.split(['?', '#']).next().unwrap_or(url);
let ext = stem.rsplit('.').next().unwrap_or("");
engine.formats.by_extension(ext).map(|f| f.name.to_string())
}