Skip to main content

rtc_media/io/ogg_reader/
mod.rs

1#[cfg(test)]
2mod ogg_reader_test;
3
4use std::io::{Cursor, Read};
5
6use byteorder::{LittleEndian, ReadBytesExt};
7use bytes::BytesMut;
8
9use crate::io::ResetFn;
10use shared::error::{Error, Result};
11
12pub const PAGE_HEADER_TYPE_CONTINUATION_OF_STREAM: u8 = 0x00;
13pub const PAGE_HEADER_TYPE_BEGINNING_OF_STREAM: u8 = 0x02;
14pub const PAGE_HEADER_TYPE_END_OF_STREAM: u8 = 0x04;
15pub const DEFAULT_PRE_SKIP: u16 = 3840; // 3840 recommended in the RFC
16pub const PAGE_HEADER_SIGNATURE: &[u8] = b"OggS";
17pub const ID_PAGE_SIGNATURE: &[u8] = b"OpusHead";
18pub const COMMENT_PAGE_SIGNATURE: &[u8] = b"OpusTags";
19pub const PAGE_HEADER_SIZE: usize = 27;
20pub const ID_PAGE_PAYLOAD_SIZE: usize = 19;
21
22/// Header type classification for Opus pages
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum OggHeaderType {
25    /// OpusHead - Opus ID page
26    OpusHead,
27    /// OpusTags - Opus comment/metadata page
28    OpusTags,
29}
30
31/// OggReader is used to read Ogg files and return page payloads
32pub struct OggReader<R: Read> {
33    reader: R,
34    bytes_read: usize,
35    checksum_table: [u32; 256],
36    do_checksum: bool,
37}
38
39/// OggHeader is the metadata from the first two pages
40/// in the file (ID and Comment)
41/// <https://tools.ietf.org/html/rfc7845.html#section-3>
42#[derive(Debug, Clone)]
43pub struct OggHeader {
44    pub channel_map: u8,
45    pub channels: u8,
46    pub output_gain: u16,
47    pub pre_skip: u16,
48    pub sample_rate: u32,
49    pub version: u8,
50    pub stream_count: u8,
51    pub coupled_count: u8,
52    pub channel_mapping: Vec<u8>,
53}
54
55/// OpusTags contains Vorbis comment metadata from an OpusTags page
56/// <https://www.xiph.org/vorbis/doc/v-comment.html>
57#[derive(Debug, Clone, Default)]
58pub struct OpusTags {
59    pub vendor: String,
60    pub user_comments: Vec<UserComment>,
61}
62
63/// A key-value pair from Vorbis comments
64#[derive(Debug, Clone)]
65pub struct UserComment {
66    pub comment: String,
67    pub value: String,
68}
69
70/// OggPageHeader is the metadata for a Page
71/// Pages are the fundamental unit of multiplexing in an Ogg stream
72/// <https://tools.ietf.org/html/rfc7845.html#section-1>
73#[derive(Debug, Clone)]
74pub struct OggPageHeader {
75    pub granule_position: u64,
76    /// Serial number of the logical bitstream (track)
77    pub serial: u32,
78    /// Page header type flags
79    pub header_type: u8,
80
81    sig: [u8; 4],
82    version: u8,
83    index: u32,
84    segments_count: u8,
85}
86
87impl OggPageHeader {
88    /// Classify the page payload as OpusHead or OpusTags header
89    pub fn opus_header_type(&self, payload: &[u8]) -> Option<OggHeaderType> {
90        if payload.len() < 8 {
91            return None;
92        }
93
94        let sig = &payload[..8];
95        if sig == ID_PAGE_SIGNATURE {
96            // OpusHead must be beginning of stream
97            if self.header_type == PAGE_HEADER_TYPE_BEGINNING_OF_STREAM {
98                return Some(OggHeaderType::OpusHead);
99            }
100            return None;
101        }
102        if sig == COMMENT_PAGE_SIGNATURE {
103            return Some(OggHeaderType::OpusTags);
104        }
105
106        None
107    }
108
109    /// Check if this is the beginning of a stream
110    pub fn is_beginning_of_stream(&self) -> bool {
111        self.header_type == PAGE_HEADER_TYPE_BEGINNING_OF_STREAM
112    }
113
114    /// Check if this is the end of a stream
115    pub fn is_end_of_stream(&self) -> bool {
116        self.header_type == PAGE_HEADER_TYPE_END_OF_STREAM
117    }
118}
119
120/// Parse an OpusHead from a page payload
121/// <https://tools.ietf.org/html/rfc7845.html#section-5.1>
122pub fn parse_opus_head(payload: &[u8]) -> Result<OggHeader> {
123    if payload.len() < ID_PAGE_PAYLOAD_SIZE {
124        return Err(Error::ErrBadIDPageLength);
125    }
126
127    if &payload[..8] != ID_PAGE_SIGNATURE {
128        return Err(Error::ErrBadIDPagePayloadSignature);
129    }
130
131    let mut reader = Cursor::new(&payload[8..]);
132    let version = reader.read_u8()?;
133    let channels = reader.read_u8()?;
134    let pre_skip = reader.read_u16::<LittleEndian>()?;
135    let sample_rate = reader.read_u32::<LittleEndian>()?;
136    let output_gain = reader.read_u16::<LittleEndian>()?;
137    let channel_map = reader.read_u8()?;
138
139    let (stream_count, coupled_count, channel_mapping) = match channel_map {
140        0 => {
141            // Family 0: mono or stereo, no mapping table
142            if payload.len() != ID_PAGE_PAYLOAD_SIZE {
143                return Err(Error::ErrBadIDPageLength);
144            }
145            (0, 0, vec![])
146        }
147        1 | 2 | 255 => {
148            // Extended channel mapping
149            let expected_len = 21 + channels as usize;
150            if payload.len() < expected_len {
151                return Err(Error::ErrBadIDPageLength);
152            }
153            let stream_count = payload[19];
154            let coupled_count = payload[20];
155            let channel_mapping = payload[21..expected_len].to_vec();
156            (stream_count, coupled_count, channel_mapping)
157        }
158        3 => {
159            return Err(Error::ErrUnsupportedChannelMappingFamily);
160        }
161        _ => {
162            return Err(Error::ErrUnsupportedChannelMappingFamily);
163        }
164    };
165
166    Ok(OggHeader {
167        channel_map,
168        channels,
169        output_gain,
170        pre_skip,
171        sample_rate,
172        version,
173        stream_count,
174        coupled_count,
175        channel_mapping,
176    })
177}
178
179/// Parse OpusTags from a page payload
180/// <https://tools.ietf.org/html/rfc7845.html#section-5.2>
181pub fn parse_opus_tags(payload: &[u8]) -> Result<OpusTags> {
182    const HEADER_MAGIC_LEN: usize = 8;
183    const U32_SIZE: usize = 4;
184    const MIN_HEADER_LEN: usize = HEADER_MAGIC_LEN + U32_SIZE + U32_SIZE;
185
186    if payload.len() < MIN_HEADER_LEN {
187        return Err(Error::ErrBadOpusTagsSignature);
188    }
189
190    if &payload[..8] != COMMENT_PAGE_SIGNATURE {
191        return Err(Error::ErrBadOpusTagsSignature);
192    }
193
194    // Parse vendor string
195    let vendor_len = u32::from_le_bytes([
196        payload[HEADER_MAGIC_LEN],
197        payload[HEADER_MAGIC_LEN + 1],
198        payload[HEADER_MAGIC_LEN + 2],
199        payload[HEADER_MAGIC_LEN + 3],
200    ]) as usize;
201
202    let vendor_start = HEADER_MAGIC_LEN + U32_SIZE;
203    let vendor_end = vendor_start + vendor_len;
204
205    if vendor_end + U32_SIZE > payload.len() {
206        return Err(Error::ErrBadOpusTagsSignature);
207    }
208
209    let vendor = String::from_utf8_lossy(&payload[vendor_start..vendor_end]).to_string();
210
211    // Parse user comments
212    let comment_count = u32::from_le_bytes([
213        payload[vendor_end],
214        payload[vendor_end + 1],
215        payload[vendor_end + 2],
216        payload[vendor_end + 3],
217    ]) as usize;
218
219    let mut pos = vendor_end + U32_SIZE;
220    let mut user_comments = Vec::with_capacity(comment_count);
221
222    for _ in 0..comment_count {
223        if pos + U32_SIZE > payload.len() {
224            return Err(Error::ErrBadOpusTagsSignature);
225        }
226
227        let comment_len = u32::from_le_bytes([
228            payload[pos],
229            payload[pos + 1],
230            payload[pos + 2],
231            payload[pos + 3],
232        ]) as usize;
233        pos += U32_SIZE;
234
235        if pos + comment_len > payload.len() {
236            return Err(Error::ErrBadOpusTagsSignature);
237        }
238
239        let comment_str = String::from_utf8_lossy(&payload[pos..pos + comment_len]).to_string();
240        pos += comment_len;
241
242        // Split on first '=' to get key=value pair
243        if let Some(eq_pos) = comment_str.find('=') {
244            user_comments.push(UserComment {
245                comment: comment_str[..eq_pos].to_string(),
246                value: comment_str[eq_pos + 1..].to_string(),
247            });
248        }
249    }
250
251    Ok(OpusTags {
252        vendor,
253        user_comments,
254    })
255}
256
257impl<R: Read> OggReader<R> {
258    /// new returns a new Ogg reader and Ogg header
259    /// with an io.Reader input
260    ///
261    /// Warning: This only parses the first OpusHead (a single logical bitstream/track)
262    /// and returns a single OggHeader. If you need to handle Ogg containers with multiple
263    /// Opus headers/tracks, use new_with_options and scan pages via parse_next_page
264    /// to find and parse each OpusHead.
265    pub fn new(reader: R, do_checksum: bool) -> Result<(OggReader<R>, OggHeader)> {
266        let mut r = OggReader {
267            reader,
268            bytes_read: 0,
269            checksum_table: generate_checksum_table(),
270            do_checksum,
271        };
272
273        let header = r.read_headers()?;
274
275        Ok((r, header))
276    }
277
278    /// Create a new OggReader without consuming headers
279    ///
280    /// Use this when you need to handle Ogg containers with multiple
281    /// logical bitstreams (tracks). You can then use parse_next_page
282    /// to iterate through pages and parse_opus_head/parse_opus_tags
283    /// to parse the header pages for each track.
284    pub fn new_with_options(reader: R, do_checksum: bool) -> OggReader<R> {
285        OggReader {
286            reader,
287            bytes_read: 0,
288            checksum_table: generate_checksum_table(),
289            do_checksum,
290        }
291    }
292
293    fn read_headers(&mut self) -> Result<OggHeader> {
294        let (payload, page_header) = self.parse_next_page()?;
295
296        if page_header.sig != PAGE_HEADER_SIGNATURE {
297            return Err(Error::ErrBadIDPageSignature);
298        }
299
300        if page_header.header_type != PAGE_HEADER_TYPE_BEGINNING_OF_STREAM {
301            return Err(Error::ErrBadIDPageType);
302        }
303
304        parse_opus_head(&payload)
305    }
306
307    // parse_next_page reads from stream and returns Ogg page payload, header,
308    // and an error if there is incomplete page data.
309    pub fn parse_next_page(&mut self) -> Result<(BytesMut, OggPageHeader)> {
310        let mut h = [0u8; PAGE_HEADER_SIZE];
311        self.reader.read_exact(&mut h)?;
312
313        let mut head_reader = Cursor::new(h);
314        let mut sig = [0u8; 4]; //0-3
315        head_reader.read_exact(&mut sig)?;
316        let version = head_reader.read_u8()?; //4
317        let header_type = head_reader.read_u8()?; //5
318        let granule_position = head_reader.read_u64::<LittleEndian>()?; //6-13
319        let serial = head_reader.read_u32::<LittleEndian>()?; //14-17
320        let index = head_reader.read_u32::<LittleEndian>()?; //18-21
321        let checksum = head_reader.read_u32::<LittleEndian>()?; //22-25
322        let segments_count = head_reader.read_u8()?; //26
323
324        let mut size_buffer = vec![0u8; segments_count as usize];
325        self.reader.read_exact(&mut size_buffer)?;
326
327        let mut payload_size = 0usize;
328        for s in &size_buffer {
329            payload_size += *s as usize;
330        }
331
332        let mut payload = BytesMut::with_capacity(payload_size);
333        payload.resize(payload_size, 0);
334        self.reader.read_exact(&mut payload)?;
335
336        if self.do_checksum {
337            let mut sum = 0;
338
339            for (index, v) in h.iter().enumerate() {
340                // Don't include expected checksum in our generation
341                if index > 21 && index < 26 {
342                    sum = self.update_checksum(0, sum);
343                    continue;
344                }
345                sum = self.update_checksum(*v, sum);
346            }
347
348            for v in &size_buffer {
349                sum = self.update_checksum(*v, sum);
350            }
351            for v in &payload[..] {
352                sum = self.update_checksum(*v, sum);
353            }
354
355            if sum != checksum {
356                return Err(Error::ErrChecksumMismatch);
357            }
358        }
359
360        let page_header = OggPageHeader {
361            granule_position,
362            sig,
363            version,
364            header_type,
365            serial,
366            index,
367            segments_count,
368        };
369
370        Ok((payload, page_header))
371    }
372
373    /// reset_reader resets the internal stream of OggReader. This is useful
374    /// for live streams, where the end of the file might be read without the
375    /// data being finished.
376    pub fn reset_reader(&mut self, mut reset: ResetFn<R>) {
377        self.reader = reset(self.bytes_read);
378    }
379
380    fn update_checksum(&self, v: u8, sum: u32) -> u32 {
381        (sum << 8) ^ self.checksum_table[(((sum >> 24) as u8) ^ v) as usize]
382    }
383}
384
385pub(crate) fn generate_checksum_table() -> [u32; 256] {
386    let mut table = [0u32; 256];
387    const POLY: u32 = 0x04c11db7;
388
389    for (i, t) in table.iter_mut().enumerate() {
390        let mut r = (i as u32) << 24;
391        for _ in 0..8 {
392            if (r & 0x80000000) != 0 {
393                r = (r << 1) ^ POLY;
394            } else {
395                r <<= 1;
396            }
397        }
398        *t = r;
399    }
400    table
401}