smooth-runtime 0.1.0

Sans-IO Smooth Streaming (MS-SSTR) engine: a pull client (manifest fetch/refresh, stream/quality selection, chunk enumeration, fragment URLs, init/tfxd handling, retry/backoff, live join) and a live origin that renders the client Manifest and serves fragments from a media-plane Trunk. No I/O, no clock reads.
Documentation
use smooth_runtime::{Dropped, Output, SmoothConfig};

use crate::common::{Host, Reply, fixture};

const STATIC_HEADER: &str = "Duration=\"30415941\"";
const VIDEO_URL: &str = "QualityLevels(64528)/Fragments(video=";
const SECOND_MICROS: u64 = 1_000_000;

fn live_manifest(attrs: &str) -> Vec<u8> {
    let text = String::from_utf8(fixture("av/Manifest")).expect("utf8");
    text.replace(
        STATIC_HEADER,
        &format!("{STATIC_HEADER} IsLive=\"TRUE\" {attrs}"),
    )
    .into_bytes()
}

fn live_host(attrs: &str) -> Host {
    let mut host = Host::new(SmoothConfig::default());
    host.serve_dir("av");
    host.route("Manifest", vec![Reply::Body(live_manifest(attrs))]);
    host
}

fn requested_video(host: &Host, start: u64) -> bool {
    host.requested
        .iter()
        .any(|u| u.ends_with(&format!("{VIDEO_URL}{start})")))
}

fn sample_count(o: &[Output]) -> usize {
    o.iter()
        .filter(|o| matches!(o, Output::Sample { .. }))
        .count()
}

#[test]
fn live_join_starts_look_ahead_fragments_behind_the_edge() {
    let mut host = live_host("");
    host.run_until(|o| sample_count(o) > 100);
    assert!(
        requested_video(&host, 0),
        "two fragments behind the edge of three is the first"
    );
}

#[test]
fn live_join_honours_a_zero_look_ahead_by_starting_at_the_edge() {
    let mut host = Host::new(SmoothConfig::default().with_live_join_fragments(0));
    host.serve_dir("av");
    host.route("Manifest", vec![Reply::Body(live_manifest(""))]);
    host.run_until(|o| sample_count(o) > 0 && o.iter().any(|o| matches!(o, Output::Tracks(_))));
    assert!(!requested_video(&host, 0));
    assert!(!requested_video(&host, 10_000_000));
    assert!(requested_video(&host, 20_000_000));
}

#[test]
fn live_join_is_clamped_to_the_dvr_window() {
    let mut host = Host::new(SmoothConfig::default().with_live_join_fragments(10));
    host.serve_dir("av");
    host.route(
        "Manifest",
        vec![Reply::Body(live_manifest("DVRWindowLength=\"10000000\""))],
    );
    host.run_until(|o| o.iter().any(|o| matches!(o, Output::Tracks(_))));
    assert!(!requested_video(&host, 0));
    assert!(requested_video(&host, 10_000_000));
}

#[test]
fn a_refresh_that_adds_a_chunk_fetches_exactly_that_chunk() {
    let refreshed = String::from_utf8(live_manifest(""))
        .expect("utf8")
        .replacen(
            "<c n=\"2\" d=\"10000000\" />",
            "<c n=\"2\" d=\"10000000\" />\n<c n=\"3\" d=\"10000000\" />",
            1,
        );
    let mut host = live_host("");
    host.route(
        "Manifest",
        vec![
            Reply::Body(live_manifest("")),
            Reply::Body(refreshed.into_bytes()),
        ],
    );
    host.route(
        "QualityLevels(64528)/Fragments(video=30000000)",
        vec![Reply::Body(fixture(
            "av/QualityLevels(64528)/Fragments(video=20000000)",
        ))],
    );
    host.run_for(20 * SECOND_MICROS);
    assert_eq!(host.count_requests(&format!("{VIDEO_URL}30000000)")), 1);
    assert_eq!(host.count_requests(&format!("{VIDEO_URL}20000000)")), 1);
    assert!(
        host.samples()
            .iter()
            .any(|(_, s)| s.dts.is_some_and(|d| d >= 30_000_000))
    );
}

#[test]
fn a_live_window_that_moves_past_the_last_fragment_is_a_discontinuity() {
    let moved = String::from_utf8(live_manifest("")).expect("utf8").replace(
        "<c n=\"0\" d=\"10000000\" />",
        "<c t=\"90000000\" d=\"10000000\" />",
    );
    let mut host = live_host("");
    host.route(
        "Manifest",
        vec![
            Reply::Body(live_manifest("")),
            Reply::Body(moved.into_bytes()),
        ],
    );
    host.run_until(|o| o.iter().any(|o| matches!(o, Output::Discontinuity)));
    assert!(host.has(|o| matches!(o, Output::Discontinuity)));
}

#[test]
fn a_live_fragment_that_keeps_failing_is_skipped_with_a_discontinuity() {
    let mut host = live_host("");
    host.route(
        "QualityLevels(64528)/Fragments(video=10000000)",
        vec![Reply::Status(404)],
    );
    host.run_until(|o| {
        o.iter()
            .any(|o| matches!(o, Output::Dropped(Dropped::Fragment { .. })))
    });
    assert!(host.has(|o| matches!(o, Output::Discontinuity)));
    host.run_for(20 * SECOND_MICROS);
    let video_after_gap = host.samples().iter().any(|(t, s)| {
        t.value() == 1 && s.dts.is_some_and(|d| (20_000_000..30_000_000).contains(&d))
    });
    assert!(
        video_after_gap,
        "playback continues past the skipped fragment"
    );
}

#[test]
fn a_live_manifest_is_refetched_repeatedly_while_nothing_new_appears() {
    let mut host = live_host("");
    host.run_for(20 * SECOND_MICROS);
    assert!(host.count_requests("Manifest") >= 4);
}

const AUDIO_STREAM: &str = r#"<StreamIndex Type="audio" QualityLevels="1" Chunks="3" Url="QualityLevels({bitrate})/Fragments(audio={start time})">"#;

fn two_audio_manifest(fra_chunks: &str) -> Vec<u8> {
    let named = AUDIO_STREAM.replace("Type=\"audio\"", "Type=\"audio\" Name=\"eng\"");
    let fra = format!(
        "<StreamIndex Type=\"audio\" Name=\"fra\" QualityLevels=\"1\" Url=\"fra/QualityLevels({{bitrate}})/Fragments(audio={{start time}})\">\n<QualityLevel Index=\"0\" Bitrate=\"98429\" FourCC=\"AACL\" SamplingRate=\"44100\" Channels=\"1\" BitsPerSample=\"16\" PacketSize=\"4\" AudioTag=\"255\" CodecPrivateData=\"1208\" />\n{fra_chunks}</StreamIndex>\n"
    );
    String::from_utf8(live_manifest(""))
        .expect("utf8")
        .replace(AUDIO_STREAM, &named)
        .replace(
            "</SmoothStreamingMedia>",
            &format!("{fra}</SmoothStreamingMedia>"),
        )
        .into_bytes()
}

#[test]
fn a_refresh_matches_each_stream_by_its_own_identity() {
    let chunks = "<c t=\"0\" d=\"10214580\" />\n";
    let grown = format!("{chunks}<c d=\"9984581\" />\n");
    let mut host = live_host("");
    host.route(
        "Manifest",
        vec![
            Reply::Body(two_audio_manifest(chunks)),
            Reply::Body(two_audio_manifest(&grown)),
        ],
    );
    let audio = fixture("av/QualityLevels(98429)/Fragments(audio=0)");
    for path in [
        "fra/QualityLevels(98429)/Fragments(audio=0)",
        "fra/QualityLevels(98429)/Fragments(audio=10214580)",
    ] {
        host.route(path, vec![Reply::Body(audio.clone())]);
    }
    host.run_for(20 * SECOND_MICROS);
    assert_eq!(
        host.count_requests("fra/QualityLevels(98429)/Fragments(audio=10214580)"),
        1,
        "the stream whose timeline grew fetches its new chunk"
    );
    assert_eq!(
        host.count_requests("QualityLevels(98429)/Fragments(audio=10214580)"),
        2,
        "once for eng's own timeline and once for fra's"
    );
}