#![no_std]
#![forbid(unsafe_code)]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(doctest, doc = include_str!("../README.md"))]
extern crate alloc;
#[cfg(test)]
extern crate std;
mod adts;
mod annexb;
mod asf;
mod ebml;
mod flv;
mod isobmff;
mod mp3;
mod mpegps;
mod mxf;
mod ogg;
mod riff;
mod ts;
use alloc::vec::Vec;
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PartitionKind {
Header,
Body,
Footer,
#[non_exhaustive]
Other(u8),
}
impl PartitionKind {
pub fn name(&self) -> &'static str {
match self {
PartitionKind::Header => "Header",
PartitionKind::Body => "Body",
PartitionKind::Footer => "Footer",
PartitionKind::Other(_) => "reserved",
}
}
pub fn as_u8(&self) -> u8 {
match self {
PartitionKind::Header => 0x02,
PartitionKind::Body => 0x03,
PartitionKind::Footer => 0x04,
PartitionKind::Other(b) => *b,
}
}
}
broadcast_common::impl_spec_display!(PartitionKind, Other);
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Format {
MpegTs,
Isobmff,
MpegPs,
Matroska,
WebM,
Flv,
Mxf,
Wav,
Ogg,
Asf,
AdtsAac,
Mp3,
AnnexB,
}
impl Format {
pub fn name(&self) -> &'static str {
match self {
Format::MpegTs => "MpegTs",
Format::Isobmff => "Isobmff",
Format::MpegPs => "MpegPs",
Format::Matroska => "Matroska",
Format::WebM => "WebM",
Format::Flv => "Flv",
Format::Mxf => "Mxf",
Format::Wav => "Wav",
Format::Ogg => "Ogg",
Format::Asf => "Asf",
Format::AdtsAac => "AdtsAac",
Format::Mp3 => "Mp3",
Format::AnnexB => "AnnexB",
}
}
pub fn is_elementary_stream(&self) -> bool {
matches!(self, Format::AdtsAac | Format::Mp3 | Format::AnnexB)
}
}
broadcast_common::impl_spec_display!(Format);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Confidence(u8);
impl Confidence {
#[must_use]
pub const fn as_u8(self) -> u8 {
self.0
}
pub fn name(&self) -> &'static str {
match self.0 {
TIER_CERTAIN => "CERTAIN",
TIER_STRONG => "STRONG",
TIER_STRUCTURAL => "STRUCTURAL",
TIER_LATTICE_STRONG => "LATTICE_STRONG",
TIER_LATTICE_WEAK => "LATTICE_WEAK",
TIER_HEURISTIC => "HEURISTIC",
_ => "unknown",
}
}
}
broadcast_common::impl_spec_display!(Confidence);
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IsobmffLayout {
Fragmented,
Progressive,
Unknown,
}
impl IsobmffLayout {
pub fn name(&self) -> &'static str {
match self {
IsobmffLayout::Fragmented => "fragmented",
IsobmffLayout::Progressive => "progressive",
IsobmffLayout::Unknown => "unknown",
}
}
}
broadcast_common::impl_spec_display!(IsobmffLayout);
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Detail {
#[non_exhaustive]
Ts {
stride: u16,
phase: u16,
},
#[non_exhaustive]
Isobmff {
major_brand: Option<[u8; 4]>,
boxes_walked: u8,
layout: IsobmffLayout,
},
#[non_exhaustive]
Ebml {
doc_type: DocType,
},
#[non_exhaustive]
Flv {
has_audio: bool,
has_video: bool,
data_offset: u32,
},
#[non_exhaustive]
Mxf {
partition_kind: PartitionKind,
},
#[non_exhaustive]
MpegPs {
structurally_valid: bool,
},
None,
}
impl Detail {
pub fn name(&self) -> &'static str {
match self {
Detail::Ts { .. } => "Ts",
Detail::Isobmff { .. } => "Isobmff",
Detail::Ebml { .. } => "Ebml",
Detail::Flv { .. } => "Flv",
Detail::Mxf { .. } => "Mxf",
Detail::MpegPs { .. } => "MpegPs",
Detail::None => "None",
}
}
#[must_use]
pub fn major_brand_str(&self) -> Option<&str> {
if let Detail::Isobmff { major_brand, .. } = self {
let bytes: &[u8; 4] = major_brand.as_ref()?;
core::str::from_utf8(&bytes[..]).ok()
} else {
None
}
}
}
impl core::fmt::Display for Detail {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(self.name())?;
match self {
Detail::Ts { stride, phase } => {
write!(f, " {{ stride: {stride}, phase: {phase} }}")
}
Detail::Isobmff {
major_brand,
boxes_walked,
layout,
} => {
let brand = major_brand
.map(|b| alloc::string::String::from_utf8_lossy(&b[..]).into_owned())
.unwrap_or_else(|| "<none>".into());
write!(
f,
" {{ major_brand: {brand:?}, boxes_walked: {boxes_walked}, layout: {layout} }}"
)
}
Detail::Ebml { doc_type } => write!(f, " {{ doc_type: {doc_type} }}"),
Detail::Flv {
has_audio,
has_video,
data_offset,
} => write!(
f,
" {{ has_audio: {has_audio}, has_video: {has_video}, data_offset: {data_offset} }}"
),
Detail::Mxf { partition_kind } => {
write!(f, " {{ partition_kind: {partition_kind} }}")
}
Detail::MpegPs { structurally_valid } => {
write!(f, " {{ structural: {structurally_valid} }}")
}
Detail::None => Ok(()),
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DocType {
Webm,
Matroska,
Other,
}
impl DocType {
pub fn name(&self) -> &'static str {
match self {
DocType::Webm => "Webm",
DocType::Matroska => "Matroska",
DocType::Other => "Other",
}
}
}
broadcast_common::impl_spec_display!(DocType);
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Candidate {
pub format: Format,
pub confidence: Confidence,
pub detail: Detail,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Probe {
#[non_exhaustive]
Identified {
format: Format,
confidence: Confidence,
detail: Detail,
},
#[non_exhaustive]
Ambiguous {
candidates: Vec<Candidate>,
},
#[non_exhaustive]
Insufficient {
need_at_least: usize,
},
Unknown,
}
impl Probe {
pub fn name(&self) -> &'static str {
match self {
Probe::Identified { .. } => "Identified",
Probe::Ambiguous { .. } => "Ambiguous",
Probe::Insufficient { .. } => "Insufficient",
Probe::Unknown => "Unknown",
}
}
}
broadcast_common::impl_spec_display!(Probe);
impl Probe {
#[must_use]
pub fn best_confidence(&self) -> Option<Confidence> {
match self {
Probe::Identified { confidence, .. } => Some(*confidence),
Probe::Ambiguous { candidates } => candidates.first().map(|c| c.confidence),
Probe::Insufficient { .. } | Probe::Unknown => None,
}
}
}
const TIER_CERTAIN: u8 = 240;
const TIER_STRONG: u8 = 192;
const TIER_STRUCTURAL: u8 = 160;
const TIER_LATTICE_STRONG: u8 = 128;
const TIER_LATTICE_WEAK: u8 = 96;
const TIER_HEURISTIC: u8 = 64;
const TIE_THRESHOLD: u8 = 16;
pub const DEFAULT_BUDGET: usize = 64 * 1024;
impl Confidence {
pub const CERTAIN: Confidence = Confidence(TIER_CERTAIN);
pub const STRONG: Confidence = Confidence(TIER_STRONG);
pub const STRUCTURAL: Confidence = Confidence(TIER_STRUCTURAL);
pub const LATTICE_STRONG: Confidence = Confidence(TIER_LATTICE_STRONG);
pub const LATTICE_WEAK: Confidence = Confidence(TIER_LATTICE_WEAK);
pub const HEURISTIC: Confidence = Confidence(TIER_HEURISTIC);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Evidence {
confidence: Confidence,
detail: Detail,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Outcome {
Match(Evidence),
Insufficient(usize),
None,
}
pub(crate) fn ran_out_or_ruled_out(ran_out: bool, need: usize) -> Outcome {
if ran_out {
Outcome::Insufficient(need)
} else {
Outcome::None
}
}
fn normalise_need(need: usize, limit: usize) -> usize {
core::cmp::max(need, geometric_floor(limit))
}
fn geometric_floor(limit: usize) -> usize {
limit.saturating_add(limit / 2).saturating_add(1)
}
type Prober = fn(&[u8], limit: usize) -> Outcome;
const PROBERS: &[(Format, Prober)] = &[
(Format::MpegTs, ts::probe),
(Format::Isobmff, isobmff::probe),
(Format::Matroska, ebml::probe),
(Format::Mxf, mxf::probe),
(Format::MpegPs, mpegps::probe),
(Format::Flv, flv::probe),
(Format::Wav, riff::probe),
(Format::Ogg, ogg::probe),
(Format::Asf, asf::probe),
(Format::AdtsAac, adts::probe),
(Format::Mp3, mp3::probe),
(Format::AnnexB, annexb::probe),
];
fn candidate_format(format: Format, detail: Detail) -> Format {
match detail {
Detail::Ebml { doc_type } => match doc_type {
DocType::Webm => Format::WebM,
DocType::Matroska | DocType::Other => Format::Matroska,
},
_ => format,
}
}
fn suppress_elementary_streams(candidates: &mut Vec<Candidate>) {
let container_wins = candidates
.iter()
.any(|c| !c.format.is_elementary_stream() && c.confidence.as_u8() >= TIER_LATTICE_STRONG);
if container_wins {
candidates.retain(|c| !c.format.is_elementary_stream());
}
}
pub fn probe(data: &[u8]) -> Probe {
probe_with_budget(data, DEFAULT_BUDGET)
}
pub fn probe_with_budget(data: &[u8], budget: usize) -> Probe {
let limit = core::cmp::min(data.len(), budget);
let mut candidates: Vec<Candidate> = Vec::new();
let mut need_more: Option<usize> = None;
for (format, prober) in PROBERS {
match prober(data, limit) {
Outcome::Match(ev) => candidates.push(Candidate {
format: candidate_format(*format, ev.detail),
confidence: ev.confidence,
detail: ev.detail,
}),
Outcome::Insufficient(need) => {
need_more = Some(match need_more {
Some(prev) => core::cmp::min(prev, need),
None => need,
});
}
Outcome::None => {}
}
}
suppress_elementary_streams(&mut candidates);
match candidates.len() {
0 => match need_more {
Some(need) => Probe::Insufficient {
need_at_least: normalise_need(need, limit),
},
None => Probe::Unknown,
},
_ => {
candidates.sort_by(|a, b| {
b.confidence
.cmp(&a.confidence)
.then_with(|| a.format.name().cmp(b.format.name()))
});
probe_to_identify(&candidates)
}
}
}
fn probe_to_identify(candidates: &[Candidate]) -> Probe {
let top = &candidates[0];
if let Some(second) = candidates.get(1) {
if top
.confidence
.as_u8()
.saturating_sub(second.confidence.as_u8())
<= TIE_THRESHOLD
{
let tied: Vec<Candidate> = candidates
.iter()
.copied()
.take_while(|c| {
top.confidence.as_u8().saturating_sub(c.confidence.as_u8()) <= TIE_THRESHOLD
})
.collect();
return Probe::Ambiguous { candidates: tied };
}
}
Probe::Identified {
format: top.format,
confidence: top.confidence,
detail: top.detail,
}
}
#[cfg(test)]
mod sampled_prober_needs {
use super::*;
use alloc::vec;
use alloc::vec::Vec;
fn seeds() -> Vec<(&'static str, Vec<u8>)> {
vec![
("ts-sync", vec![0x47]),
(
"ebml-magic",
vec![
0x1A, 0x45, 0xDF, 0xA3, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE,
],
),
(
"isobmff-largesize",
vec![0x00, 0x00, 0x00, 0x01, b'f', b't', b'y', b'p'],
),
("adts-sync", vec![0xFF, 0xF1, 0x4C, 0x80, 0x22, 0x3F, 0xFC]),
(
"adts-mixed",
vec![
0xFF, 0xF1, 0x00, 0x00, 0x00, 0xE0, 0x00, 0xFF, 0xF1, 0x00, 0x03, 0xFF, 0xE0,
0x00,
],
),
("mp3-sync", vec![0xFF, 0xFB, 0x90, 0x64]),
("mp3-mixed-bitrate", {
let mut v = vec![0xFF, 0xFB, 0x10, 0x00];
v.resize(104, 0x00);
v.extend_from_slice(&[0xFF, 0xFB, 0xE0, 0x00]);
v
}),
(
"id3",
vec![b'I', b'D', b'3', 0x04, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00],
),
("annexb-startcode", vec![0x00, 0x00, 0x00, 0x01, 0x67]),
("riff", vec![b'R', b'I', b'F', b'F']),
("ogg", vec![b'O', b'g', b'g']),
(
"mxf-key",
vec![0x06, 0x0E, 0x2B, 0x34, 0x02, 0x05, 0x01, 0x01],
),
("ps-startcode", vec![0x00, 0x00, 0x01, 0xBA]),
("flv", vec![b'F', b'L', b'V']),
(
"asf-guid",
vec![0x30, 0x26, 0xB2, 0x75, 0x8E, 0x66, 0xCF, 0x11],
),
]
}
#[test]
fn sampled_prober_needs_do_not_move_with_the_buffer() {
let lengths = [
1usize, 2, 3, 5, 6, 7, 10, 12, 14, 15, 20, 64, 256, 1024,
1400, 4096,
];
let mut failures: Vec<alloc::string::String> = Vec::new();
let mut exercised = 0usize;
let mut covered: Vec<&Format> = Vec::new();
for (format, prober) in PROBERS {
for (seed_name, seed) in seeds() {
let mut seen: Vec<(usize, usize)> = Vec::new();
for len in lengths {
if len < seed.len() {
continue;
}
let mut buf = seed.clone();
buf.resize(len, 0x00);
if let Outcome::Insufficient(need) = prober(&buf, buf.len()) {
seen.push((len, need));
}
}
if seen.len() < 2 {
continue;
}
exercised += 1;
covered.push(format);
let first = seen[0].1;
if seen.iter().any(|(_, n)| *n != first) {
failures.push(alloc::format!(
" {:?} on seed {seed_name}: {seen:?} -- the need moves with the \
buffer length, so a caller advances one unit per read",
format
));
}
}
}
let missed: Vec<&Format> = PROBERS
.iter()
.map(|(f, _)| f)
.filter(|f| !covered.contains(f))
.collect();
assert!(
missed.is_empty(),
"no seed drives {missed:?} to Insufficient at two or more lengths, so this \
guard cannot see a length-relative need in {} of {} probers. Add a seed \
that reaches them rather than lowering the bar.",
missed.len(),
PROBERS.len()
);
let _ = exercised;
assert!(
failures.is_empty(),
"a prober's need moved with the buffer at a SAMPLED length (this check is \
sampled, not exhaustive -- see the module docs):\n{}",
failures.join("\n")
);
}
}
#[cfg(test)]
mod need_normalisation {
use super::*;
#[test]
fn an_under_reported_need_is_raised_past_the_examined_bytes() {
assert_eq!(normalise_need(10, 65536), 98_305);
assert_eq!(normalise_need(4096, 4096), 6145);
assert_eq!(normalise_need(4097, 4096), 6145);
assert_eq!(normalise_need(268_435_465, 65536), 268_435_465);
assert_eq!(normalise_need(0, 0), 1);
}
#[test]
fn the_result_always_exceeds_the_limit() {
for limit in [0usize, 1, 4, 188, 4095, 4096, 65535, 65536, 131_072] {
for need in [0usize, 1, 4, 100, 65536, 65537, 1_000_000] {
let got = normalise_need(need, limit);
assert!(
got > limit,
"normalise_need({need}, {limit}) = {got}, which does not exceed the \
{limit} bytes examined -- a caller would re-probe forever"
);
assert!(
got >= limit + limit / 2,
"normalise_need({need}, {limit}) = {got} grows arithmetically; \
the floor must be at least 1.5x the bytes examined or the \
documented caller loop crawls"
);
assert!(
got >= need,
"normalise_need({need}, {limit}) = {got} discarded the prober's \
larger structural need {need}"
);
}
}
}
}
#[cfg(test)]
mod tier_spacing {
use super::*;
const TIERS: [(&str, u8); 6] = [
("CERTAIN", TIER_CERTAIN),
("STRONG", TIER_STRONG),
("STRUCTURAL", TIER_STRUCTURAL),
("LATTICE_STRONG", TIER_LATTICE_STRONG),
("LATTICE_WEAK", TIER_LATTICE_WEAK),
("HEURISTIC", TIER_HEURISTIC),
];
#[test]
fn adjacent_tiers_are_further_apart_than_the_tie_threshold() {
for pair in TIERS.windows(2) {
let (hi_name, hi) = pair[0];
let (lo_name, lo) = pair[1];
assert!(
hi > lo,
"TIERS must be listed strictly descending: {hi_name} ({hi}) is not above {lo_name} ({lo})"
);
let gap = hi - lo;
assert!(
gap > TIE_THRESHOLD,
"{hi_name} ({hi}) and {lo_name} ({lo}) are {gap} apart, which is not more than \
TIE_THRESHOLD ({TIE_THRESHOLD}): two candidates on these different tiers would \
be reported as Ambiguous even though the model ranks one strictly above the other"
);
}
}
#[test]
fn tiers_covers_every_public_confidence_constant() {
let public = [
("CERTAIN", Confidence::CERTAIN),
("STRONG", Confidence::STRONG),
("STRUCTURAL", Confidence::STRUCTURAL),
("LATTICE_STRONG", Confidence::LATTICE_STRONG),
("LATTICE_WEAK", Confidence::LATTICE_WEAK),
("HEURISTIC", Confidence::HEURISTIC),
];
assert_eq!(
public.len(),
TIERS.len(),
"TIERS and Confidence's public tier constants have drifted apart"
);
for (name, c) in public {
let found = TIERS.iter().find(|(n, _)| *n == name);
let (_, v) = found.unwrap_or_else(|| panic!("tier {name} is missing from TIERS"));
assert_eq!(
*v,
c.as_u8(),
"tier {name}: TIERS says {v}, Confidence::{name} says {}",
c.as_u8()
);
}
}
}
#[cfg(test)]
mod dispatch {
use super::*;
use alloc::vec;
#[test]
fn every_format_variant_is_reachable() {
enum Reach {
Prober,
Derived,
}
fn reachability(f: Format) -> Reach {
match f {
Format::MpegTs => Reach::Prober,
Format::Isobmff => Reach::Prober,
Format::MpegPs => Reach::Prober,
Format::Matroska => Reach::Prober,
Format::Flv => Reach::Prober,
Format::Mxf => Reach::Prober,
Format::Wav => Reach::Prober,
Format::Ogg => Reach::Prober,
Format::Asf => Reach::Prober,
Format::AdtsAac => Reach::Prober,
Format::Mp3 => Reach::Prober,
Format::AnnexB => Reach::Prober,
Format::WebM => Reach::Derived,
}
}
const ALL_FORMATS: [Format; 13] = [
Format::MpegTs,
Format::Isobmff,
Format::MpegPs,
Format::Matroska,
Format::WebM,
Format::Flv,
Format::Mxf,
Format::Wav,
Format::Ogg,
Format::Asf,
Format::AdtsAac,
Format::Mp3,
Format::AnnexB,
];
const DERIVED: usize = 1;
let registered: Vec<Format> = PROBERS.iter().map(|(f, _)| *f).collect();
assert_eq!(
ALL_FORMATS.len(),
registered.len() + DERIVED,
"ALL_FORMATS ({}) must equal PROBERS ({}) plus the {DERIVED} derived \
format(s); one of the three has drifted",
ALL_FORMATS.len(),
registered.len()
);
assert_eq!(
candidate_format(
Format::Matroska,
Detail::Ebml {
doc_type: DocType::Webm,
},
),
Format::WebM,
"WebM is declared Derived but candidate_format does not produce it"
);
for variant in ALL_FORMATS {
match reachability(variant) {
Reach::Prober => assert!(
registered.contains(&variant),
"Format::{} is declared as having its own prober but is not a \
PROBERS row",
variant.name()
),
Reach::Derived => assert!(
!registered.contains(&variant),
"Format::{} is declared Derived but also has a PROBERS row — \
one of the two is wrong",
variant.name()
),
}
}
for f in ®istered {
assert!(
ALL_FORMATS.contains(f),
"Format::{} has a PROBERS row but is missing from ALL_FORMATS",
f.name()
);
}
}
#[test]
fn ambiguous_lists_only_genuinely_tied_candidates() {
let mk = |format: Format, score: u8| Candidate {
format,
confidence: Confidence(score),
detail: Detail::None,
};
let candidates = vec![
mk(Format::MpegTs, 240),
mk(Format::Isobmff, 232),
mk(Format::MpegPs, 192),
];
let p = probe_to_identify(&candidates);
match p {
Probe::Ambiguous { candidates: tied } => {
assert_eq!(tied.len(), 2, "only the top two are within TIE_THRESHOLD");
assert_eq!(tied[0].format, Format::MpegTs);
assert_eq!(tied[1].format, Format::Isobmff);
}
other => panic!("expected Ambiguous, got {other:?}"),
}
}
#[test]
fn decisive_winner_is_not_ambiguous() {
let mk = |format: Format, score: u8| Candidate {
format,
confidence: Confidence(score),
detail: Detail::None,
};
let candidates = vec![
mk(Format::MpegTs, 240),
mk(Format::Isobmff, 200),
mk(Format::MpegPs, 64),
];
match probe_to_identify(&candidates) {
Probe::Identified { format, .. } => assert_eq!(format, Format::MpegTs),
other => panic!("expected Identified, got {other:?}"),
}
}
}