use crate::box_types::{BoxRef, box_iter};
use crate::movie_fragment::{
MovieFragmentBox, MovieFragmentHeaderBox, TFHD_DEFAULT_BASE_IS_MOOF,
TrackFragmentBaseMediaDecodeTimeBox, TrackFragmentHeaderBox, TrackFragmentRunBox,
};
use crate::segments::SegmentTypeBox;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
const FTYP: [u8; 4] = *b"ftyp";
const MOOV: [u8; 4] = *b"moov";
const MVHD: [u8; 4] = *b"mvhd";
const TRAK: [u8; 4] = *b"trak";
const TKHD: [u8; 4] = *b"tkhd";
const MDIA: [u8; 4] = *b"mdia";
const MDHD: [u8; 4] = *b"mdhd";
const HDLR: [u8; 4] = *b"hdlr";
const MINF: [u8; 4] = *b"minf";
const STBL: [u8; 4] = *b"stbl";
const STSD: [u8; 4] = *b"stsd";
const MVEX: [u8; 4] = *b"mvex";
const TREX: [u8; 4] = *b"trex";
const STYP: [u8; 4] = *b"styp";
const MOOF: [u8; 4] = *b"moof";
const MFHD: [u8; 4] = *b"mfhd";
const TRAF: [u8; 4] = *b"traf";
const TFHD: [u8; 4] = *b"tfhd";
const TFDT: [u8; 4] = *b"tfdt";
const TRUN: [u8; 4] = *b"trun";
const MDAT: [u8; 4] = *b"mdat";
fn is_segment_brand(b: &[u8; 4]) -> bool {
b == b"msdh" || b == b"msix" || &b[..3] == b"cmf"
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum Severity {
Error,
Warning,
}
impl Severity {
pub fn name(&self) -> &'static str {
match self {
Severity::Error => "error",
Severity::Warning => "warning",
}
}
}
broadcast_common::impl_spec_display!(Severity);
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct ConformanceIssue {
pub severity: Severity,
pub code: &'static str,
pub message: String,
}
impl ConformanceIssue {
fn error(code: &'static str, message: String) -> Self {
Self {
severity: Severity::Error,
code,
message,
}
}
fn warning(code: &'static str, message: String) -> Self {
Self {
severity: Severity::Warning,
code,
message,
}
}
}
fn children(body: &[u8]) -> Vec<([u8; 4], BoxRef<'_>)> {
box_iter(body)
.map_while(|step| step.ok())
.map(|(bx, _)| (bx.header.box_type.0, bx))
.collect()
}
fn has_child(body: &[u8], fourcc: &[u8; 4]) -> bool {
children(body).iter().any(|(t, _)| t == fourcc)
}
fn child_body<'a>(body: &'a [u8], fourcc: &[u8; 4]) -> Option<&'a [u8]> {
children(body)
.into_iter()
.find(|(t, _)| t == fourcc)
.map(|(_, bx)| bx.body)
}
pub fn validate_init_segment(bytes: &[u8]) -> Vec<ConformanceIssue> {
let mut issues = Vec::new();
let top = children(bytes);
match top.iter().position(|(t, _)| t == &FTYP) {
None => issues.push(ConformanceIssue::error(
"init.ftyp.missing",
"no ftyp box: an initialization segment must begin with ftyp (ISO/IEC 14496-12 §4.3)"
.to_string(),
)),
Some(0) => {}
Some(pos) => issues.push(ConformanceIssue::error(
"init.ftyp.not-first",
format!(
"ftyp is box #{} but must be the first box (ISO/IEC 14496-12 §4.3, §6.2.3)",
pos + 1
),
)),
}
let Some(moov) = child_body(bytes, &MOOV) else {
issues.push(ConformanceIssue::error(
"init.moov.missing",
"no moov box: an initialization segment requires a movie box (ISO/IEC 14496-12 §8.2.1)"
.to_string(),
));
return issues;
};
validate_moov(moov, &mut issues);
issues
}
fn validate_moov(moov: &[u8], issues: &mut Vec<ConformanceIssue>) {
if !has_child(moov, &MVHD) {
issues.push(ConformanceIssue::error(
"init.mvhd.missing",
"moov has no mvhd (ISO/IEC 14496-12 §8.2.2)".to_string(),
));
}
let traks: Vec<_> = children(moov)
.into_iter()
.filter(|(t, _)| t == &TRAK)
.collect();
if traks.is_empty() {
issues.push(ConformanceIssue::error(
"init.trak.missing",
"moov has no trak: at least one track is required (ISO/IEC 14496-12 §8.3.1)"
.to_string(),
));
}
for (idx, (_, trak)) in traks.iter().enumerate() {
validate_trak(trak.body, idx + 1, issues);
}
match child_body(moov, &MVEX) {
None => issues.push(ConformanceIssue::warning(
"init.mvex.missing",
"moov has no mvex: the init segment is not marked as a fragmented movie \
(ISO/IEC 14496-12 §8.8.1) — required for a fragmented (CMAF/DASH) workflow"
.to_string(),
)),
Some(mvex) => {
if !has_child(mvex, &TREX) {
issues.push(ConformanceIssue::warning(
"init.mvex.missing",
"mvex has no trex: fragmented tracks need per-track defaults \
(ISO/IEC 14496-12 §8.8.3)"
.to_string(),
));
}
}
}
}
fn validate_trak(trak: &[u8], track_no: usize, issues: &mut Vec<ConformanceIssue>) {
let mut missing: Vec<&str> = Vec::new();
if !has_child(trak, &TKHD) {
missing.push("tkhd");
}
match child_body(trak, &MDIA) {
None => missing.push("mdia"),
Some(mdia) => {
if !has_child(mdia, &MDHD) {
missing.push("mdia>mdhd");
}
if !has_child(mdia, &HDLR) {
missing.push("mdia>hdlr");
}
match child_body(mdia, &MINF) {
None => missing.push("mdia>minf"),
Some(minf) => match child_body(minf, &STBL) {
None => missing.push("mdia>minf>stbl"),
Some(stbl) => {
if !has_child(stbl, &STSD) {
missing.push("mdia>minf>stbl>stsd");
}
}
},
}
}
}
if !missing.is_empty() {
issues.push(ConformanceIssue::error(
"init.trak.incomplete",
format!(
"trak #{track_no} is missing required box(es): {} \
(ISO/IEC 14496-12 §8.3.2/§8.4)",
missing.join(", ")
),
));
}
}
struct TrafInfo {
track_id: Option<u32>,
tfdt: Option<u64>,
total_duration: u64,
}
struct MediaInfo {
sequence_number: Option<u32>,
tracks: Vec<TrackFragmentInfo>,
}
struct TrackFragmentInfo {
track_id: u32,
first_tfdt: Option<u64>,
last_decode_end: Option<u64>,
total_duration: u64,
}
pub fn validate_media_segment(bytes: &[u8]) -> Vec<ConformanceIssue> {
let mut issues = Vec::new();
validate_media_inner(bytes, &mut issues);
issues
}
fn validate_media_inner(bytes: &[u8], issues: &mut Vec<ConformanceIssue>) -> Option<MediaInfo> {
let top = children(bytes);
match top.iter().find(|(t, _)| t == &STYP) {
None => issues.push(ConformanceIssue::warning(
"media.styp.missing",
"no styp box: a CMAF media segment begins with styp \
(ISO/IEC 14496-12 §8.16.2, ISO/IEC 23000-19 §7.3.2.3)"
.to_string(),
)),
Some((_, bx)) => {
let whole = styp_whole(bytes, bx);
match whole.and_then(|w| SegmentTypeBox::parse_box(w).ok()) {
Some(styp) => {
let ok = is_segment_brand(&styp.major_brand)
|| styp.compatible_brands.iter().any(is_segment_brand);
if !ok {
issues.push(ConformanceIssue::warning(
"media.styp.brand",
"styp carries no recognised segment brand (msdh/msix/cmf*) \
(ISO/IEC 23000-19 §7.3.2.3)"
.to_string(),
));
}
}
None => issues.push(ConformanceIssue::warning(
"media.styp.brand",
"styp box could not be parsed for its brand list".to_string(),
)),
}
}
}
let moof_positions: Vec<usize> = top
.iter()
.enumerate()
.filter(|(_, (t, _))| t == &MOOF)
.map(|(i, _)| i)
.collect();
let mdat_positions: Vec<usize> = top
.iter()
.enumerate()
.filter(|(_, (t, _))| t == &MDAT)
.map(|(i, _)| i)
.collect();
if moof_positions.is_empty() {
issues.push(ConformanceIssue::error(
"media.moof.missing",
"no moof box: a media segment requires a movie fragment (ISO/IEC 14496-12 §8.8.4)"
.to_string(),
));
}
for &mp in &mdat_positions {
if mp == 0 || top[mp - 1].0 != MOOF {
issues.push(ConformanceIssue::error(
"media.mdat.orphan",
format!(
"mdat (box #{}) is not immediately preceded by a moof \
(ISO/IEC 14496-12 §8.1.1/§8.8.4)",
mp + 1
),
));
}
}
let mut aggregated = MediaInfo {
sequence_number: None,
tracks: Vec::new(),
};
let mdat_extents = crate::frag_offsets::mdat_ranges(bytes);
for &mp in &moof_positions {
let (_, moof_bx) = &top[mp];
let (seq_no, info) = validate_moof(moof_bx.body, mp + 1, issues);
let moof_parsed = MovieFragmentBox::parse_body(moof_bx.body).ok();
let base = bytes.as_ptr() as usize;
let body_ptr = moof_bx.body.as_ptr() as usize;
let moof_byte_off = (body_ptr >= base)
.then(|| body_ptr - base)
.and_then(|body_off| body_off.checked_sub(moof_bx.header.header_size()));
if aggregated.sequence_number.is_none() {
aggregated.sequence_number = seq_no;
}
let has_mdat_after = match top.get(mp + 1) {
Some((t, _)) if t == &MDAT => true,
_ => {
issues.push(ConformanceIssue::error(
"media.mdat.missing",
format!(
"moof (box #{}) is not followed by an mdat box \
(ISO/IEC 14496-12 §8.8.4)",
mp + 1
),
));
false
}
};
let _ = has_mdat_after;
let mut checked: Vec<(i64, i64)> = Vec::new();
for (ti, traf) in info.iter().enumerate() {
let (Some(track_id), Some(moof_box), Some(moof_off)) =
(traf.track_id, moof_parsed.as_ref(), moof_byte_off)
else {
continue;
};
let ranges = match crate::frag_offsets::sample_ranges_in(
bytes,
&mdat_extents,
moof_off,
moof_box,
track_id,
) {
Ok(r) => r,
Err(_) => {
issues.push(ConformanceIssue::error(
"media.mdat.overrun",
format!(
"moof #{}, traf #{}: sample data offsets do not resolve to a range inside an mdat (ISO/IEC 14496-12 §8.8.7)",
mp + 1,
ti + 1
),
));
continue;
}
};
if let (Some(first), Some(last)) = (ranges.first(), ranges.last()) {
checked.push((
i64::try_from(first.start).unwrap_or(i64::MAX),
i64::try_from(last.end).unwrap_or(i64::MAX),
));
}
}
checked.sort_unstable();
for w in checked.windows(2) {
let ((a_lo, a_hi), (b_lo, b_hi)) = (w[0], w[1]);
if a_hi > b_lo && b_hi > a_lo {
issues.push(ConformanceIssue::error(
"media.mdat.overlap",
format!(
"moof #{}: trun data ranges {}..{} and {}..{} overlap (ISO/IEC 14496-12 §8.8.8)",
mp + 1,
a_lo,
a_hi,
b_lo,
b_hi
),
));
}
}
for traf in info {
if let Some(track_id) = traf.track_id {
match aggregated
.tracks
.iter_mut()
.find(|t| t.track_id == track_id)
{
Some(t) => {
t.total_duration += traf.total_duration;
if let Some(tfdt) = traf.tfdt {
t.first_tfdt = Some(t.first_tfdt.map_or(tfdt, |f| f.min(tfdt)));
let end = tfdt.saturating_add(traf.total_duration);
t.last_decode_end = Some(t.last_decode_end.map_or(end, |e| e.max(end)));
}
}
None => {
let end = traf
.tfdt
.map(|tfdt| tfdt.saturating_add(traf.total_duration));
aggregated.tracks.push(TrackFragmentInfo {
track_id,
first_tfdt: traf.tfdt,
last_decode_end: end,
total_duration: traf.total_duration,
});
}
}
}
}
}
Some(aggregated)
}
fn styp_whole<'a>(bytes: &'a [u8], bx: &BoxRef<'a>) -> Option<&'a [u8]> {
let hdr = bx.header.header_size();
let base = bytes.as_ptr() as usize;
let body_ptr = bx.body.as_ptr() as usize;
if body_ptr < base {
return None;
}
let body_off = body_ptr - base;
let start = body_off.checked_sub(hdr)?;
let end = body_off.checked_add(bx.body.len())?;
bytes.get(start..end)
}
fn validate_moof(
moof: &[u8],
moof_no: usize,
issues: &mut Vec<ConformanceIssue>,
) -> (Option<u32>, Vec<TrafInfo>) {
let mut sequence_number = None;
match child_body(moof, &MFHD) {
None => issues.push(ConformanceIssue::error(
"media.mfhd.missing",
format!("moof #{moof_no} has no mfhd (ISO/IEC 14496-12 §8.8.5)"),
)),
Some(mfhd) => match MovieFragmentHeaderBox::parse_body(mfhd) {
Ok(h) => sequence_number = Some(h.sequence_number),
Err(_) => issues.push(ConformanceIssue::error(
"media.mfhd.missing",
format!("moof #{moof_no} mfhd could not be parsed (ISO/IEC 14496-12 §8.8.5)"),
)),
},
}
let trafs: Vec<_> = children(moof)
.into_iter()
.filter(|(t, _)| t == &TRAF)
.collect();
if trafs.is_empty() {
issues.push(ConformanceIssue::error(
"media.traf.missing",
format!("moof #{moof_no} has no traf (ISO/IEC 14496-12 §8.8.6)"),
));
}
if trafs.len() > 1 {
issues.push(ConformanceIssue::warning(
"media.moof.multi-traf",
format!(
"moof #{moof_no} carries {} traf boxes; a CMAF fragment SHOULD be single-track \
(ISO/IEC 23000-19 §7.3.2.3)",
trafs.len()
),
));
}
let mut traf_infos = Vec::with_capacity(trafs.len());
for (idx, (_, traf)) in trafs.iter().enumerate() {
traf_infos.push(validate_traf(traf.body, moof_no, idx + 1, issues));
}
(sequence_number, traf_infos)
}
fn validate_traf(
traf: &[u8],
moof_no: usize,
traf_no: usize,
issues: &mut Vec<ConformanceIssue>,
) -> TrafInfo {
let tfhd = child_body(traf, &TFHD);
if tfhd.is_none() {
issues.push(ConformanceIssue::error(
"media.tfhd.missing",
format!("moof #{moof_no}, traf #{traf_no} has no tfhd (ISO/IEC 14496-12 §8.8.7)"),
));
}
let tfhd = tfhd.and_then(|b| TrackFragmentHeaderBox::parse_body(b).ok());
let track_id = tfhd.as_ref().map(|h| h.track_id);
let default_duration = tfhd.as_ref().and_then(|h| h.default_sample_duration);
if let Some(h) = &tfhd
&& h.flags & TFHD_DEFAULT_BASE_IS_MOOF == 0
{
issues.push(ConformanceIssue::error(
"media.tfhd.default-base-is-moof",
format!(
"moof #{moof_no}, traf #{traf_no}: tfhd does not set default-base-is-moof (ISO/IEC 23000-19 §7.3.2.3)"
),
));
}
let tfdt = match child_body(traf, &TFDT) {
None => {
issues.push(ConformanceIssue::error(
"media.tfdt.missing",
format!(
"moof #{moof_no}, traf #{traf_no} has no tfdt: CMAF requires the \
baseMediaDecodeTime (ISO/IEC 14496-12 §8.8.12, ISO/IEC 23000-19 §7.5.19)"
),
));
None
}
Some(b) => match TrackFragmentBaseMediaDecodeTimeBox::parse_body(b) {
Ok(t) => Some(t.base_media_decode_time()),
Err(_) => {
issues.push(ConformanceIssue::error(
"media.tfdt.missing",
format!("moof #{moof_no}, traf #{traf_no} tfdt could not be parsed"),
));
None
}
},
};
let truns: Vec<_> = children(traf)
.into_iter()
.filter(|(t, _)| t == &TRUN)
.collect();
if truns.is_empty() {
issues.push(ConformanceIssue::error(
"media.trun.missing",
format!("moof #{moof_no}, traf #{traf_no} has no trun (ISO/IEC 14496-12 §8.8.8)"),
));
}
let mut total_duration: u64 = 0;
let mut zero_duration_samples: u64 = 0;
for (_, trun_bx) in &truns {
let Ok(run) = TrackFragmentRunBox::parse_body(trun_bx.body) else {
continue;
};
for s in &run.samples {
let dur = s.sample_duration.or(default_duration).unwrap_or(0);
total_duration += u64::from(dur);
if dur == 0 {
zero_duration_samples += 1;
}
}
}
if zero_duration_samples > 0 {
issues.push(ConformanceIssue::error(
"media.sample.zero-duration",
format!(
"moof #{moof_no}, traf #{traf_no}: {zero_duration_samples} sample(s) have \
zero duration (ISO/IEC 14496-12 §8.8.8)"
),
));
}
if let Some(first_run) = truns
.first()
.and_then(|(_, bx)| TrackFragmentRunBox::parse_body(bx.body).ok())
{
let sync = first_run
.first_sample_flags
.map(|f| f & SAMPLE_FLAGS_IS_NON_SYNC == 0)
.or_else(|| {
first_run
.samples
.first()
.and_then(|s| s.sample_flags)
.map(|f| f & SAMPLE_FLAGS_IS_NON_SYNC == 0)
})
.or_else(|| {
tfhd.as_ref()
.and_then(|h| h.default_sample_flags)
.map(|f| f & SAMPLE_FLAGS_IS_NON_SYNC == 0)
});
if sync == Some(false) {
issues.push(ConformanceIssue::warning(
"media.sample.non-sync-first",
format!(
"moof #{moof_no}, traf #{traf_no}: the fragment's first sample is not a \
sync sample; a CMAF segment must begin at a random-access point, an \
LL part/chunk need not (ISO/IEC 23000-19 §7.3.2.3)"
),
));
}
}
TrafInfo {
track_id,
tfdt,
total_duration,
}
}
const SAMPLE_FLAGS_IS_NON_SYNC: u32 = 1 << 16;
fn child_whole<'a>(owner: &'a [u8], parent_body: &'a [u8], fourcc: &[u8; 4]) -> Option<&'a [u8]> {
let bx_body = child_body(parent_body, fourcc)?;
let base = owner.as_ptr() as usize;
let body_ptr = bx_body.as_ptr() as usize;
if body_ptr < base {
return None;
}
let body_off = body_ptr - base;
let start = body_off.checked_sub(8)?;
let end = body_off.checked_add(bx_body.len())?;
owner.get(start..end)
}
fn init_track_ids(init: &[u8]) -> Vec<u32> {
use crate::init_segment::TrackHeaderBox;
use broadcast_common::Parse;
let Some(moov) = child_body(init, &MOOV) else {
return Vec::new();
};
let mut ids = Vec::new();
for (t, trak) in children(moov) {
if &t != b"trak" {
continue;
}
if let Some(bytes) = child_whole(moov, trak.body, &TKHD)
&& let Ok(h) = TrackHeaderBox::parse(bytes)
{
ids.push(h.track_id);
}
}
ids
}
pub fn validate_cmaf_track(init: &[u8], segments: &[&[u8]]) -> Vec<ConformanceIssue> {
let mut issues = validate_init_segment(init);
let declared = init_track_ids(init);
let mut infos: Vec<MediaInfo> = Vec::with_capacity(segments.len());
for seg in segments {
if let Some(info) = validate_media_inner(seg, &mut issues) {
for track in &info.tracks {
if !declared.is_empty() && !declared.contains(&track.track_id) {
issues.push(ConformanceIssue::error(
"track.tfhd.unknown-track",
format!(
"media segment names track_id {} which the init segment's moov \
does not declare (ISO/IEC 14496-12 §8.8.7)",
track.track_id
),
));
}
}
infos.push(info);
}
}
for pair in infos.windows(2) {
let (prev, next) = (&pair[0], &pair[1]);
if let (Some(a), Some(b)) = (prev.sequence_number, next.sequence_number)
&& b <= a
{
issues.push(ConformanceIssue::warning(
"track.mfhd.sequence",
format!(
"mfhd sequence_number not strictly increasing: {a} then {b} \
(ISO/IEC 14496-12 §8.8.5)"
),
));
}
for cur in &next.tracks {
let Some(prev_track) = prev.tracks.iter().find(|t| t.track_id == cur.track_id) else {
continue;
};
let (Some(prev_first), Some(prev_end), Some(cur_first)) = (
prev_track.first_tfdt,
prev_track.last_decode_end,
cur.first_tfdt,
) else {
continue;
};
if cur_first != prev_end {
issues.push(ConformanceIssue::error(
"track.tfdt.discontinuity",
format!(
"track {}: tfdt baseMediaDecodeTime discontinuity - expected {} \
(prev first tfdt {} + sum durations), got {} \
(ISO/IEC 14496-12 §8.8.12, ISO/IEC 23000-19 §7.5.19)",
cur.track_id, prev_end, prev_first, cur_first
),
));
}
}
}
issues
}