Skip to main content

dash_mpd/
sidx.rs

1// Parsing sidx boxes in ISOBMFF containers and WebM Cue information.
2//
3// Manifests to test with:
4//  (WebM) https://storage.googleapis.com/shaka-demo-assets/sintel/dash.mpd
5//  (MP4)  https://turtle-tube.appspot.com/t/t2/dash.mpd
6
7
8use std::io::{Cursor, Read};
9use byteorder::{BigEndian, ReadBytesExt};
10use tracing::{warn, trace};
11
12
13// A Segment Index Box provides a compact index of one media stream within the media segment to which 
14// it applies.
15#[derive(Debug, Clone, PartialEq)]
16pub struct SidxBox {
17    pub version: u8,
18    pub flags: u32,   // actually only u24
19    pub reference_id: u32,
20    pub timescale: u32,
21    pub earliest_presentation_time: u64,
22    pub first_offset: u64,
23    pub reference_count: u16,
24    pub references: Vec<SidxReference>,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct SidxReference {
29    pub reference_type: u8,
30    pub referenced_size: u32,
31    pub subsegment_duration: u32,
32    pub starts_with_sap: u8,  // (actually a boolean)
33    pub sap_type: u8,
34    pub sap_delta_time: u32,
35}
36
37
38impl SidxBox {
39    pub fn parse(data: &[u8]) -> Result<Self, Box<dyn std::error::Error>> {
40        let mut rdr = Cursor::new(data);
41        let _box_size = rdr.read_u32::<BigEndian>()?;
42        let mut box_header = [0u8; 4];
43        if rdr.read_exact(&mut box_header).is_err() {
44            return Err("reading box header".into());
45        }
46        if !box_header.eq(b"sidx") {
47            return Err("expecting sidx BMFF header".into());
48        }
49        let version = rdr.read_u8()?;
50        let flags = rdr.read_u24::<BigEndian>()?;
51        let reference_id = rdr.read_u32::<BigEndian>()?;
52        let timescale = rdr.read_u32::<BigEndian>()?;
53        let earliest_presentation_time = if version == 0 {
54            u64::from(rdr.read_u32::<BigEndian>()?)
55        } else {
56            rdr.read_u64::<BigEndian>()?
57        };
58        let first_offset = if version == 0 {
59            u64::from(rdr.read_u32::<BigEndian>()?)
60        } else {
61            rdr.read_u64::<BigEndian>()?
62        };
63        let _reserved = rdr.read_u16::<BigEndian>()?;
64        let reference_count = rdr.read_u16::<BigEndian>()?;
65        let mut references = Vec::with_capacity(reference_count as usize);
66        for _ in 0..reference_count {
67            // chunk is 1 bit for reference_type, and 31 bits for referenced_size.
68            let chunk = rdr.read_u32::<BigEndian>()?;
69            // Reference_type = 1 means a reference to another sidx (hierarchical sidx)
70            let reference_type = ((chunk & 0x8000_0000) >> 31) as u8;
71            if reference_type != 0 {
72                warn!("Don't know how to handle hierarchical sidx");
73            }
74            let referenced_size = chunk & 0x7FFF_FFFF;
75            let subsegment_duration = rdr.read_u32::<BigEndian>()?;
76            let fields = rdr.read_u32::<BigEndian>()?;
77            let starts_with_sap = if (fields >> 31) == 1 { 1 } else { 0 };
78            let sap_type = ((fields >> 28) & 0b0111) as u8;
79            let sap_delta_time = fields & !(0b1111 << 28);
80
81            references.push(SidxReference {
82                reference_type,
83                referenced_size,
84                subsegment_duration,
85                starts_with_sap,
86                sap_type,
87                sap_delta_time,
88            });
89        }
90        Ok(SidxBox {
91            version,
92            flags,
93            reference_id,
94            timescale,
95            earliest_presentation_time,
96            first_offset,
97            reference_count,
98            references,
99        })
100    }
101}
102
103
104#[derive(Debug, Clone, PartialEq)]
105pub struct SegmentChunk {
106    pub start: u64,
107    pub end: u64
108}
109
110pub fn from_isobmff_sidx(data: &[u8], index_start: u64) -> Result<Vec<SegmentChunk>, Box<dyn std::error::Error>> {
111    let mut chunks = Vec::new();
112    let sidx = SidxBox::parse(data)?;
113    let mut current_pos = index_start;
114    for sref in sidx.references {
115        let start = current_pos;
116        let end = current_pos - 1 + u64::from(sref.referenced_size);
117        chunks.push(SegmentChunk{ start, end });
118        current_pos += u64::from(sref.referenced_size);
119    }
120    Ok(chunks)
121}
122
123
124// WebM files include a Cue box. The Matroska Cues Element contains one or many CuePoint Elements
125// which each MUST reference an absolute timestamp (via the CueTime Element), a Track (via the
126// CueTrack Element), and a Segment Position (via the CueClusterPosition Element). Additional
127// non-mandated Elements are part of the CuePoint Element such as CueDuration, CueRelativePosition,
128// CueCodecState and others which provide any Matroska Reader with additional information to use in
129// the optimization of seeking performance.
130//
131//   https://www.matroska.org/technical/cues.html
132pub fn from_webm_cue(data: &[u8]) -> Result<Vec<SegmentChunk>, Box<dyn std::error::Error>> {
133    use webm_iterable::WebmIterator;
134    use webm_iterable::matroska_spec::MatroskaSpec::{
135        CueClusterPosition,
136        CueRefCluster,
137        CueRefNumber,
138        CueRelativePosition
139    };
140
141    // CueTrackPositions in a WebM are absolute positions, rather than relative as in an sidx box.
142    let mut chunks = Vec::new();
143    let mut current_pos: u64 = 0;
144    let mut copy = data;
145    for tag in WebmIterator::new(&mut copy, &[]) {
146        match tag {
147            Err(e) => warn!("Error decoding WebM cues: {e:#?}"),
148            Ok(CueRelativePosition(val)) => {
149                trace!("Saw CueRelativePosition {}", val);
150            },
151            Ok(CueRefCluster(val)) => {
152                trace!("Saw CueRefCluster {val}");
153            },
154            Ok(CueRefNumber(val)) => {
155                trace!("Saw CueRefNumber {val}");
156            },
157            Ok(CueClusterPosition(val)) => {
158                trace!("Saw CueClusterPosition {val}");
159                if current_pos != 0 {
160                    chunks.push(SegmentChunk{ start: current_pos, end: val - 1});
161                }
162                current_pos = val;
163            },
164            _ => (),
165        }
166    }
167    Ok(chunks)
168}