use crate::report::{Finding, Location, Report, Severity};
use alloc::collections::BTreeSet;
pub fn check_dash_mpd(text: &str, report: &mut Report) {
let mpd = match transmux::Mpd::parse(text) {
Ok(mpd) => mpd,
Err(err) => {
report.push(Finding::new(
Severity::Error,
Location::new(1, 0),
"dash-parse-error",
alloc::format!("MPD XML failed to parse: {err}"),
));
return;
}
};
if mpd.mpd_type == transmux::MpdType::Static && mpd.media_presentation_duration.is_none() {
report.push(Finding::new(
Severity::Error,
Location::new(1, 0),
"dash-static-mpd-missing-duration",
"Static MPD is missing required @mediaPresentationDuration — ISO/IEC 23009-1:2012 §5.3.1.2 Table 3 (CM: mandatory for @type='static')",
));
}
if mpd.mpd_type == transmux::MpdType::Dynamic && mpd.availability_start_time.is_none() {
report.push(Finding::new(
Severity::Error,
Location::new(1, 0),
"dash-dynamic-mpd-no-availability-start",
"Dynamic MPD is missing required @availabilityStartTime — §5.3.1.2 Table 3",
));
}
for (period_idx, period) in mpd.periods.iter().enumerate() {
if period.adaptation_sets.is_empty() {
report.push(Finding::new(
Severity::Error,
Location::new(period_idx + 1, 0),
"dash-period-no-adaptation-sets",
alloc::format!(
"Period '{}' has no AdaptationSets — §5.3.2",
period.id.as_deref().unwrap_or("(unnamed)")
),
));
continue;
}
for (as_idx, aset) in period.adaptation_sets.iter().enumerate() {
let as_key = alloc::format!("{period_idx}.{as_idx}");
check_representation_id_duplicates(aset, &as_key, report);
for (r_idx, repr) in aset.representations.iter().enumerate() {
let r_key = alloc::format!("{as_key}.{r_idx}");
if let Some(ref st) = repr.segment_template
&& let Some(ref timeline) = st.timeline
{
validate_segment_timeline(timeline, st.timescale, &r_key, report);
}
}
}
}
}
fn check_representation_id_duplicates(
aset: &transmux::AdaptationSet,
as_key: &str,
report: &mut Report,
) {
let mut seen: BTreeSet<&str> = BTreeSet::new();
for repr in &aset.representations {
if !seen.insert(&repr.id) {
report.push(Finding::new(
Severity::Error,
Location::new(1, 0),
"dash-representation-id-duplicate",
alloc::format!(
"AdaptationSet {as_key}: duplicate Representation @id=\"{}\" — §5.3.5.2",
repr.id,
),
));
}
}
}
fn validate_segment_timeline(
timeline: &transmux::SegmentTimeline,
timescale: u64,
r_key: &str,
report: &mut Report,
) {
let mut prev_end_time: Option<u64> = None;
for (s_idx, s) in timeline.segments.iter().enumerate() {
let start_time = s.t.unwrap_or_else(|| {
prev_end_time.unwrap_or(0)
});
if let Some(prev_end) = prev_end_time
&& start_time < prev_end
{
let prev_str = timescale_str(prev_end, timescale);
let t_str = timescale_str(start_time, timescale);
report.push(Finding::new(
Severity::Error,
Location::new(s_idx + 1, 0),
"dash-segment-timeline-monotonic",
alloc::format!(
"SegmentTimeline {r_key}: <S t={t_str}> starts before previous segment ends (prev_end={prev_str}) — §5.3.9.6",
),
));
}
let repeat_count: u64 = if s.r >= 0 {
s.r as u64 + 1
} else {
1
};
prev_end_time = Some(start_time.saturating_add(s.d.saturating_mul(repeat_count)));
}
}
fn timescale_str(value: u64, timescale: u64) -> alloc::string::String {
if timescale == 0 || timescale == 1 {
alloc::format!("{value}")
} else {
let secs = value as f64 / timescale as f64;
alloc::format!("{value} ({secs:.3}s)")
}
}