dash-runtime 0.3.0

Sans-IO MPEG-DASH (ISO/IEC 23009-1): MPD model and parser, segment addressing, UTC clock and live-edge arithmetic, and a client for static and dynamic presentations. No I/O, no clock reads.
Documentation
use dash_runtime::address::{
    AddressError, PlanError, PlanOptions, base_index, plan_representation,
};
use dash_runtime::mpd::Mpd;
use dash_runtime::mpd::inherit::InheritError;
use dash_runtime::selection::RepRef;

fn mpd_full(kind: &str, period_attrs: &str, set_body: &str, representation_body: &str) -> Mpd {
    let xml = format!(
        r#"<?xml version="1.0"?>
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" type="{kind}" mediaPresentationDuration="PT10S" minBufferTime="PT2S" profiles="urn:mpeg:dash:profile:isoff-live:2011">
<Period {period_attrs}><AdaptationSet mimeType="video/mp4">{set_body}<Representation id="v" bandwidth="1000" codecs="avc1.42C01E">{representation_body}</Representation></AdaptationSet></Period>
</MPD>"#
    );
    Mpd::parse(xml.as_bytes()).expect("test MPD parses")
}

fn mpd_with(kind: &str, representation_body: &str) -> Mpd {
    mpd_full(kind, "", "", representation_body)
}

fn plan(mpd: &Mpd) -> Result<(), PlanError> {
    plan_representation(mpd, RepRef::at(0, 0, 0), &PlanOptions::default()).map(|_| ())
}

fn static_plan(representation_body: &str) -> Result<(), PlanError> {
    plan(&mpd_with("static", representation_body))
}

#[test]
fn a_template_and_a_list_on_one_chain_cannot_be_inherited() {
    let mpd = mpd_full(
        "static",
        "",
        r#"<SegmentTemplate media="a$Number$.m4s" duration="2" timescale="1"/>"#,
        r#"<SegmentList duration="2"><SegmentURL media="a"/></SegmentList>"#,
    );
    assert_eq!(
        plan(&mpd),
        Err(PlanError::Inherit(InheritError::TemplateAndList))
    );
}

#[test]
fn a_duration_and_a_timeline_on_one_template_cannot_be_inherited() {
    let body = r#"<SegmentTemplate media="a$Number$.m4s" duration="2" timescale="1"><SegmentTimeline><S d="2"/></SegmentTimeline></SegmentTemplate>"#;
    assert_eq!(
        static_plan(body),
        Err(PlanError::Inherit(InheritError::DurationAndTimeline))
    );
}

#[test]
fn a_template_without_media_has_nothing_to_address() {
    assert_eq!(
        static_plan(r#"<SegmentTemplate duration="2" timescale="1"/>"#),
        Err(PlanError::Address(AddressError::MissingMediaTemplate))
    );
}

#[test]
fn a_rep_ref_outside_the_mpd_is_no_such_representation() {
    let mpd = mpd_with("static", "");
    assert_eq!(
        plan_representation(&mpd, RepRef::at(5, 0, 0), &PlanOptions::default()).map(|_| ()),
        Err(PlanError::NoSuchRepresentation)
    );
}

#[test]
fn a_dynamic_mpd_without_availability_start_time_cannot_be_planned() {
    let mpd = mpd_full(
        "dynamic",
        r#"start="PT0S""#,
        "",
        r#"<SegmentTemplate media="a$Number$.m4s" duration="2" timescale="1"/>"#,
    );
    assert_eq!(plan(&mpd), Err(PlanError::MissingAvailabilityStartTime));
}

#[test]
fn a_multi_entry_list_without_duration_or_timeline_is_missing_its_duration() {
    let body = r#"<SegmentList><SegmentURL media="a"/><SegmentURL media="b"/></SegmentList>"#;
    assert_eq!(
        static_plan(body),
        Err(PlanError::Address(AddressError::MissingDuration))
    );
}

#[test]
fn a_segment_url_with_neither_media_nor_range_is_empty() {
    let body = r#"<SegmentList duration="2"><SegmentURL/></SegmentList>"#;
    assert_eq!(
        static_plan(body),
        Err(PlanError::Address(AddressError::EmptySegmentUrl))
    );
}

#[test]
fn a_list_and_its_timeline_must_agree_on_the_segment_count() {
    let body = r#"<SegmentList><SegmentTimeline><S d="2"/></SegmentTimeline><SegmentURL media="a"/><SegmentURL media="b"/></SegmentList>"#;
    assert_eq!(
        static_plan(body),
        Err(PlanError::Address(AddressError::ListTimelineMismatch {
            urls: 2,
            segments: 1,
        }))
    );
}

#[test]
fn a_list_duration_that_overflows_the_segment_start_is_rejected() {
    let body = r#"<SegmentList duration="9223372036854775808"><SegmentURL media="a"/><SegmentURL media="b"/><SegmentURL media="c"/></SegmentList>"#;
    assert_eq!(
        static_plan(body),
        Err(PlanError::Address(AddressError::Overflow))
    );
}

#[test]
fn bytes_that_are_not_a_sidx_box_are_a_sidx_error() {
    match base_index::parse_index(&[0u8; 4]) {
        Err(AddressError::Sidx(message)) => assert_eq!(
            message,
            "buffer too short: need 16 bytes, have 4 (while parsing sidx box)"
        ),
        other => panic!("expected AddressError::Sidx, got {other:?}"),
    }
}