use crate::box_types::{parse_box, BoxRef};
use crate::movie_fragment::{
MovieFragmentHeaderBox, 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<'_>)> {
let mut out = Vec::new();
let mut remaining = body;
while !remaining.is_empty() {
match parse_box(remaining) {
Ok((bx, consumed)) => {
out.push((bx.header.box_type.0, bx));
if consumed == 0 || consumed > remaining.len() {
break;
}
remaining = &remaining[consumed..];
}
Err(_) => break,
}
}
out
}
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 {
tfdt: Option<u64>,
total_duration: u64,
total_size: u64,
min_data_offset: Option<i64>,
}
struct MediaInfo {
sequence_number: Option<u32>,
trafs: Vec<TrafInfo>,
}
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 first_info: Option<MediaInfo> = None;
for &mp in &moof_positions {
let (_, moof_bx) = &top[mp];
let info = validate_moof(moof_bx.body, mp + 1, issues);
let mdat_body = match top.get(mp + 1) {
Some((t, bx)) if t == &MDAT => Some(bx.body),
_ => {
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
),
));
None
}
};
if let Some(mdat_body) = mdat_body {
let moof_size = moof_bx.header.size;
let mdat_hdr = top[mp + 1].1.header.header_size() as u64;
let mdat_payload_end = moof_size + mdat_hdr + mdat_body.len() as u64;
for (ti, traf) in info.trafs.iter().enumerate() {
if let Some(off) = traf.min_data_offset {
let start = off;
let end = start + traf.total_size as i64;
if start < 0 || (end as u64) > mdat_payload_end {
issues.push(ConformanceIssue::error(
"media.mdat.overrun",
format!(
"moof #{}, traf #{}: trun samples span offset {}..{} but the \
mdat payload ends at {} (ISO/IEC 14496-12 §8.8.8)",
mp + 1,
ti + 1,
start,
end,
mdat_payload_end
),
));
}
}
}
}
if first_info.is_none() {
first_info = Some(info);
}
}
first_info
}
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>) -> MediaInfo {
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));
}
MediaInfo {
sequence_number,
trafs: 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 default_duration = tfhd.as_ref().and_then(|h| h.default_sample_duration);
let default_size = tfhd.as_ref().and_then(|h| h.default_sample_size);
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 total_size: u64 = 0;
let mut min_data_offset: Option<i64> = None;
for (_, trun_bx) in &truns {
let Ok(run) = TrackFragmentRunBox::parse_body(trun_bx.body) else {
continue;
};
if let Some(off) = run.data_offset {
let off = off as i64;
min_data_offset = Some(min_data_offset.map_or(off, |m: i64| m.min(off)));
}
for s in &run.samples {
let dur = s.sample_duration.or(default_duration).unwrap_or(0);
let sz = s.sample_size.or(default_size).unwrap_or(0);
total_duration += dur as u64;
total_size += sz as u64;
if dur == 0 {
issues.push(ConformanceIssue::error(
"media.sample.zero-duration",
format!(
"moof #{moof_no}, traf #{traf_no}: a sample has zero duration \
(ISO/IEC 14496-12 §8.8.8)"
),
));
}
}
}
TrafInfo {
tfdt,
total_duration,
total_size,
min_data_offset,
}
}
pub fn validate_cmaf_track(init: &[u8], segments: &[&[u8]]) -> Vec<ConformanceIssue> {
let mut issues = validate_init_segment(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) {
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) {
if 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)"
),
));
}
}
let n = prev.trafs.len().min(next.trafs.len());
for i in 0..n {
if let (Some(pt), Some(nt)) = (prev.trafs[i].tfdt, next.trafs[i].tfdt) {
let expected = pt.saturating_add(prev.trafs[i].total_duration);
if nt != expected {
issues.push(ConformanceIssue::error(
"track.tfdt.discontinuity",
format!(
"traf #{}: tfdt baseMediaDecodeTime discontinuity — expected {} \
(prev tfdt {} + Σ durations {}), got {} \
(ISO/IEC 14496-12 §8.8.12, ISO/IEC 23000-19 §7.5.19)",
i + 1,
expected,
pt,
prev.trafs[i].total_duration,
nt
),
));
}
}
}
}
issues
}