playlists 0.1.2

M3u8 playlist and ring buffers
Documentation
use crate::Options;

use bytes::Bytes;
use chrono::{DateTime, Duration, Utc};

pub struct M3u8Manifest {
    dur: u32,
    seq: u32,
    seg_dur: u32,
    seg_id: u32,
    seg_durs: Vec<u32>,
    seg_parts: Vec<Vec<(u32, u32, bool)>>,
    start_time: DateTime<Utc>,
    idx: u32,
    options: Options,
}

impl M3u8Manifest {
    pub fn new(options: Options) -> Self {
        let seg_parts_size = options.max_segments;
        let mut seg_parts = Vec::with_capacity(seg_parts_size);
        for _ in 0..seg_parts_size {
            seg_parts.push(Vec::new());
        }

        Self {
            dur: 0,
            seq: 0,
            seg_dur: 0,
            seg_id: 1,
            seg_durs: Vec::new(),
            seg_parts,
            start_time: Utc::now(),
            idx: 0,
            options,
        }
    }

    fn full_segments(&self) -> Vec<(u32, u32)> {
        let start = if self.seg_id <= 7 { 1 } else { self.seg_id - 7 };

        let len = self.seg_id - start;
        let mut res = Vec::with_capacity(len as usize);

        for i in start..self.seg_id {
            res.push((i, self.seg_durs[(i - 1) as usize]));
        }

        res
    }

    pub fn add_part(&mut self, duration: u32, key: bool) -> (Bytes, usize, usize, usize, bool) {
        let mut new_seg = false;
        if key && (self.seg_dur) >= self.options.segment_min_ms {
            self.seg_durs.push(self.seg_dur);
            self.seg_id += 1;
            self.seg_dur = 0;
            self.idx = 0;

            let seg_index = (self.seg_id as usize % self.options.max_segments as usize) as usize;
            self.seg_parts[seg_index].clear();
            new_seg = true;
        }
        let idx = self.idx;
        self.idx += 1;
        self.seq += 1;
        self.dur += duration;
        self.seg_dur += duration;
        let seg_index = (self.seg_id as usize % self.options.max_segments as usize) as usize;

        self.seg_parts[seg_index].push((self.seq, duration, key));

        (
            self.m3u8(),
            self.seg_id as usize,
            self.seq as usize,
            idx as usize,
            new_seg,
        )
    }

    pub fn m3u8(&self) -> Bytes {
        let mut ph = String::new();
        let mut ps = String::new();

        let mut pt = self.start_time;

        let segs = self.full_segments();

        let mut gaps = 0;
        if segs.len() < 7 {
            for _ in 0..(7 - segs.len()) {
                gaps += 1;
                ps.push_str("#EXT-X-GAP\n#EXTINF:4.00000,\ngap.mp4\n");
                pt += Duration::milliseconds(1000);
            }
        }

        let mut durs = Vec::new();

        for (i, seg) in segs.iter().enumerate() {
            if gaps + i <= 4 {
                let secs = seg.1 as f64 / 1000.0;
                ps.push_str(&format!("#EXTINF:{:.5},\n", secs));
                ps.push_str(&format!("s{}.mp4\n", seg.0));
                pt += Duration::milliseconds(seg.1 as i64);
            } else {
                ps.push_str(&format!(
                    "#EXT-X-PROGRAM-DATE-TIME:{}\n",
                    pt.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)
                ));
                for p in &self.seg_parts[seg.0 as usize % self.options.max_segments as usize] {
                    durs.push(p.1);
                    let secs = p.1 as f64 / 1000.0;
                    let mut str = format!("#EXT-X-PART:DURATION={:.5},URI=\"p{}.mp4\"", secs, p.0);
                    if p.2 {
                        str += ",INDEPENDENT=YES\n"
                    } else {
                        str += "\n"
                    }
                    ps.push_str(&str);
                }
                let secs = seg.1 as f64 / 1000.0;
                ps.push_str(&format!("#EXTINF:{:.5},\n", secs));
                ps.push_str(&format!("s{}.mp4\n", seg.0));
                pt += Duration::milliseconds(seg.1 as i64);
            }
        }

        let mut id = 0;
        let seg_index = (self.seg_id as usize % self.options.max_segments as usize) as usize;
        for p in &self.seg_parts[seg_index] {
            durs.push(p.1);
            let secs = p.1 as f64 / 1000.0;
            let mut str = format!("#EXT-X-PART:DURATION={:.5},URI=\"p{}.mp4\"", secs, p.0);
            if p.2 {
                str += ",INDEPENDENT=YES\n"
            } else {
                str += "\n"
            }
            ps.push_str(&str);
            id = p.0;
        }

        ps.push_str(&format!(
            "#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"p{}.mp4\"\n",
            id + 1
        ));

        let target_duration = segs
            .iter()
            .map(|(_, duration)| (*duration as f64 / 1000.0).round() as u32)
            .max()
            .unwrap_or(0);

        let mut duration_counts = std::collections::HashMap::new();
        for parts in &self.seg_parts {
            for &(_, duration, _) in parts {
                *duration_counts.entry(duration).or_insert(0) += 1;
            }
        }

        let max_duration = durs.iter().max().cloned().unwrap_or(0);
        let part_target = max_duration as f64 / 1000.0;

        let part_hold_back = part_target * 3 as f64;
        let can_skip_until = target_duration * 6;

        ph.push_str("#EXTM3U\n");
        ph.push_str("#EXT-X-VERSION:9\n");
        ph.push_str(&format!("#EXT-X-TARGETDURATION:{}\n", target_duration));

        ph.push_str(&format!(
            "#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK={:.5},CAN-SKIP-UNTIL={:.5}\n",
            part_hold_back, can_skip_until as f64
        ));

        let mut seq = self.seg_id;
        if self.seg_id > 7 {
            seq = self.seg_id - 7
        }
        ph.push_str(&format!("#EXT-X-PART-INF:PART-TARGET={:.5}\n", part_target));
        ph.push_str(&format!("#EXT-X-MEDIA-SEQUENCE:{}\n", seq));
        ph.push_str("#EXT-X-MAP:URI=\"init.mp4\"\n");

        format!("{}{}", ph, ps).into()
    }
}