vsd 0.5.0

A command-line utility and library for downloading streams from DASH manifests and HLS playlists.
Documentation
use crate::{
    PlaylistDownloadConfig,
    dash::{Template, parse_range},
    error::{Error, Result},
    playlist::{Map, Range, Segment},
    utils,
};
use dash_mpd::SegmentTemplate;
use log::debug;
use reqwest::{Url, header};
use vsd_mp4::boxes::SidxBox;

pub fn parse_init(
    initialization: &dash_mpd::Initialization,
    base_url: &Url,
    template: &Template,
) -> Result<Map> {
    Ok(Map {
        range: parse_range(&initialization.range),
        uri: if let Some(source_url) = &initialization.sourceURL {
            base_url.join(&template.resolve(source_url))?.to_string()
        } else {
            base_url.to_string()
        },
    })
}

pub fn resolve_segment_list(
    segment_list: &dash_mpd::SegmentList,
    base_url: &Url,
    template: &Template,
    has_base_url: bool,
) -> Result<Vec<Segment>> {
    let mut segments = Vec::new();

    for segment_url in &segment_list.segment_urls {
        // We are ignoring SegmentURL@indexRange
        let byte_range = parse_range(&segment_url.mediaRange);

        if let Some(media) = &segment_url.media {
            segments.push(Segment {
                range: byte_range,
                uri: base_url.join(media)?.to_string(),
                ..Default::default()
            });
        } else if has_base_url {
            segments.push(Segment {
                range: byte_range,
                uri: base_url.to_string(),
                ..Default::default()
            });
        }
    }

    if let (Some(first), Some(init)) = (segments.first_mut(), &segment_list.Initialization) {
        first.map = Some(parse_init(init, base_url, template)?);
    }

    Ok(segments)
}

pub fn resolve_segment_template_init(
    rt: Option<&SegmentTemplate>,
    at: Option<&SegmentTemplate>,
    base_url: &Url,
    template: &Template,
) -> Result<Option<Map>> {
    // Try @initialization attribute
    let tmpl_init = rt
        .and_then(|t| t.initialization.clone())
        .or(at.and_then(|t| t.initialization.clone()));

    if let Some(init) = tmpl_init {
        return Ok(Some(Map {
            range: None,
            uri: base_url.join(&template.resolve(&init))?.to_string(),
        }));
    }

    // Try <Initialization> child element
    let tmpl_init = rt
        .and_then(|t| t.Initialization.as_ref())
        .or(at.and_then(|t| t.Initialization.as_ref()));

    if let Some(init) = tmpl_init {
        return Ok(Some(parse_init(init, base_url, template)?));
    }

    Ok(None)
}

pub fn resolve_segment_timeline(
    segment_timeline: &dash_mpd::SegmentTimeline,
    base_url: &Url,
    template: &mut Template,
    period_duration_secs: f64,
    media: &str,
    start_number: u64,
    timescale: u64,
) -> Result<Vec<Segment>> {
    let media = template.resolve(media);
    let timescale = timescale as f64;
    let mut number = start_number;
    let mut segments = Vec::new();
    let mut segment_time: u64 = 0;

    for s in &segment_timeline.segments {
        if let Some(t) = s.t {
            segment_time = t;
        }

        template.insert("Time", segment_time);
        template.insert("Number", number);

        segments.push(Segment {
            duration: (s.d as f64 / timescale) as f32,
            uri: base_url.join(&template.resolve(&media))?.to_string(),
            ..Default::default()
        });

        number += 1;

        if let Some(r) = s.r {
            let mut count = 0;
            // FIXME - Perhaps we also need to account for startTime?
            let end_time = period_duration_secs * timescale;

            loop {
                count += 1;
                // Exit from the loop after @r iterations (if @r is positive).
                // A negative value of the @r attribute indicates that the duration
                // indicated in @d attribute repeats until the start of the next S
                // element, the end of the Period or until the next MPD update.
                if r >= 0 {
                    if count > r {
                        break;
                    }
                } else if segment_time as f64 > end_time {
                    break;
                }

                segment_time += s.d;

                template.insert("Time", segment_time);
                template.insert("Number", number);

                segments.push(Segment {
                    duration: (s.d as f64 / timescale) as f32,
                    uri: base_url.join(&template.resolve(&media))?.to_string(),
                    ..Default::default()
                });

                number += 1;
            }
        }

        segment_time += s.d;
    }

    Ok(segments)
}

pub fn resolve_segment_template_duration(
    base_url: &Url,
    template: &mut Template,
    period_duration_secs: f64,
    duration: f64,
    media: &str,
    start_number: u64,
    timescale: u64,
) -> Result<Vec<Segment>> {
    let media = template.resolve(media);
    let timescale = timescale as f64;
    let segment_duration = duration / timescale;
    let segment_count = (period_duration_secs / segment_duration).ceil() as i64;
    let start_number = start_number as i64;
    let mut segments = Vec::new();

    for number in start_number..start_number + segment_count {
        template.insert("Number", number);
        segments.push(Segment {
            duration: segment_duration as f32,
            uri: base_url.join(&template.resolve(&media))?.to_string(),
            ..Default::default()
        });
    }

    Ok(segments)
}

pub async fn resolve_segment_base(
    segment_base: &dash_mpd::SegmentBase,
    base_url: &Url,
    template: &Template,
    config: &PlaylistDownloadConfig,
) -> Result<Vec<Segment>> {
    let mut segments = Vec::new();

    if let Some(index_range) = parse_range(&segment_base.indexRange) {
        debug!(
            "Fetching {} (sidx@{}-{})",
            base_url, index_range.0, index_range.1
        );
        let request = config
            .client
            .get(base_url.clone())
            .query(&*config.query)
            .header(header::RANGE, &index_range);
        let response = request.send().await?;
        let bytes = utils::fetch_bytes(response).await?;
        let Some(sidx) = SidxBox::from_init(&bytes, index_range.0)? else {
            return Err(Error::DashParse(
                "Missing sidx box in initialization.".into(),
            ));
        };

        for range in sidx.ranges {
            segments.push(Segment {
                range: Some(Range(range.start, range.end)),
                uri: base_url.to_string(),
                ..Default::default()
            });
        }

        if let (Some(first), Some(init)) = (segments.first_mut(), &segment_base.Initialization) {
            let mut map = parse_init(init, base_url, template)?;
            map.range = Some(Range(0, index_range.1));
            first.map = Some(map);
        }
    } else {
        segments.push(Segment {
            map: segment_base
                .Initialization
                .as_ref()
                .map(|init| parse_init(init, base_url, template))
                .transpose()?,
            uri: base_url.to_string(),
            ..Default::default()
        });
    }

    Ok(segments)
}