use crate::error::ProbeError;
use crate::report::{FormatSummary, ProbeReport, StreamSummary};
use mediaway_common::Rational;
use mediaway_container::mp4::{Demuxer, mp4_parser};
use std::path::Path;
const FORMAT_NAME: &str = "mp4";
pub(crate) fn build_report(path: &Path, bytes: &[u8]) -> Result<ProbeReport, ProbeError> {
let major_brand = major_brand(bytes);
let mut demuxer = Demuxer::new();
demuxer.push_bytes(bytes);
let streams = demuxer.streams().to_vec();
if streams.is_empty() {
return Err(ProbeError::Unsupported {
path: path.display().to_string(),
});
}
let mut extents = vec![(i64::MAX, i64::MIN, 0_u64); streams.len()];
while let Some(packet) = demuxer.poll_packet() {
let Some(idx) = streams.iter().position(|s| s.id() == packet.stream_id) else {
continue;
};
let (min_pts, max_end, count) = &mut extents[idx];
*min_pts = (*min_pts).min(packet.pts);
let end = packet
.pts
.saturating_add(i64::try_from(packet.duration).unwrap_or(i64::MAX));
*max_end = (*max_end).max(end);
*count += 1;
}
let stream_summaries: Vec<StreamSummary> = streams
.iter()
.zip(extents.iter())
.map(|(s, &(min_pts, max_end, count))| {
let has_duration = count > 0 && max_end > min_pts;
let duration_seconds =
has_duration.then_some(seconds(s.time_base(), max_end - min_pts));
let geometry = s.geometry();
StreamSummary {
index: s.id(),
codec: s.codec(),
width: geometry.map(|g| g.width),
height: geometry.map(|g| g.height),
time_base: s.time_base(),
packet_count: count,
duration_seconds,
}
})
.collect();
let mut duration_seconds: Option<f64> = None;
for s in &stream_summaries {
if let Some(d) = s.duration_seconds {
duration_seconds = Some(duration_seconds.map_or(d, |acc: f64| acc.max(d)));
}
}
Ok(ProbeReport {
format: FormatSummary {
format_name: FORMAT_NAME,
major_brand,
duration_seconds,
stream_count: stream_summaries.len(),
},
streams: stream_summaries,
})
}
#[allow(clippy::cast_precision_loss)]
fn seconds(time_base: Rational, ticks: i64) -> f64 {
if time_base.den == 0 || ticks <= 0 {
return 0.0;
}
(ticks as f64) * (time_base.num as f64) / f64::from(time_base.den)
}
fn major_brand(bytes: &[u8]) -> Option<String> {
let tree = mp4_parser::parse_box_tree(bytes, 0);
let ftyp = tree.iter().find(|n| &n.header.tag == b"ftyp")?;
let start = ftyp.payload_offset;
let brand = bytes.get(start..start + 4)?;
Some(String::from_utf8_lossy(brand).into_owned())
}