Skip to main content

oxide_browser/
audio_format.rs

1//! Magic-byte sniffing and MIME mapping for supported guest audio containers.
2
3/// Unknown or not recognized as a supported Oxide audio format.
4pub const AUDIO_FORMAT_UNKNOWN: u32 = 0;
5/// WAV / RIFF WAVE.
6pub const AUDIO_FORMAT_WAV: u32 = 1;
7/// MP3 (MPEG-1/2 Audio Layer III).
8pub const AUDIO_FORMAT_MP3: u32 = 2;
9/// Ogg (Vorbis, Opus, etc.).
10pub const AUDIO_FORMAT_OGG: u32 = 3;
11/// FLAC lossless.
12pub const AUDIO_FORMAT_FLAC: u32 = 4;
13
14/// `Accept` header sent with [`super::capabilities`] URL fetches so servers can pick a codec/container.
15pub const AUDIO_HTTP_ACCEPT: &str = "audio/wav,audio/wave,audio/x-wav;q=0.9,audio/mpeg,audio/mp3;q=0.9,audio/ogg,audio/flac,audio/*;q=0.5,*/*;q=0.1";
16
17fn skip_id3_prefix(data: &[u8]) -> usize {
18    if data.len() >= 10 && data[0..3] == *b"ID3" {
19        let size = ((data[6] as usize) << 21)
20            | ((data[7] as usize) << 14)
21            | ((data[8] as usize) << 7)
22            | (data[9] as usize);
23        10 + size
24    } else {
25        0
26    }
27}
28
29fn mp3_sync_at(data: &[u8], offset: usize) -> bool {
30    if offset + 2 > data.len() {
31        return false;
32    }
33    let b0 = data[offset];
34    let b1 = data[offset + 1];
35    (b0 == 0xFF) && ((b1 & 0xE0) == 0xE0)
36}
37
38/// Inspect leading bytes (and minimal MP3 frame sync after ID3) to guess the container/codec.
39pub fn sniff_audio_format(data: &[u8]) -> u32 {
40    if data.len() < 4 {
41        return AUDIO_FORMAT_UNKNOWN;
42    }
43    if data.len() >= 12 && &data[0..4] == b"RIFF" && &data[8..12] == b"WAVE" {
44        return AUDIO_FORMAT_WAV;
45    }
46    if &data[0..4] == b"fLaC" {
47        return AUDIO_FORMAT_FLAC;
48    }
49    if &data[0..4] == b"OggS" {
50        return AUDIO_FORMAT_OGG;
51    }
52    let off = skip_id3_prefix(data);
53    if off < data.len() && mp3_sync_at(data, off) {
54        return AUDIO_FORMAT_MP3;
55    }
56    if mp3_sync_at(data, 0) {
57        return AUDIO_FORMAT_MP3;
58    }
59    AUDIO_FORMAT_UNKNOWN
60}
61
62/// Map a `Content-Type` (or similar) value to an `AUDIO_FORMAT_*` constant, or [`AUDIO_FORMAT_UNKNOWN`].
63pub fn mime_to_audio_format(mime: &str) -> u32 {
64    let s = mime
65        .split(';')
66        .next()
67        .unwrap_or(mime)
68        .trim()
69        .to_ascii_lowercase();
70    match s.as_str() {
71        "audio/wav" | "audio/wave" | "audio/x-wav" => AUDIO_FORMAT_WAV,
72        "audio/mpeg" | "audio/mp3" => AUDIO_FORMAT_MP3,
73        "audio/ogg" | "application/ogg" => AUDIO_FORMAT_OGG,
74        "audio/flac" | "audio/x-flac" => AUDIO_FORMAT_FLAC,
75        _ => AUDIO_FORMAT_UNKNOWN,
76    }
77}
78
79/// MIME types that usually indicate an HTML/JSON error body rather than a media stream.
80pub fn is_likely_non_audio_document(mime: &str) -> bool {
81    let s = mime
82        .split(';')
83        .next()
84        .unwrap_or(mime)
85        .trim()
86        .to_ascii_lowercase();
87    s.starts_with("text/html")
88        || s.starts_with("text/plain")
89        || s.starts_with("text/css")
90        || s == "application/json"
91        || s == "application/javascript"
92        || s.starts_with("application/xml")
93        || s.starts_with("text/")
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn sniff_wav() {
102        let mut b = vec![0u8; 12];
103        b[0..4].copy_from_slice(b"RIFF");
104        b[8..12].copy_from_slice(b"WAVE");
105        assert_eq!(sniff_audio_format(&b), AUDIO_FORMAT_WAV);
106    }
107
108    #[test]
109    fn sniff_flac() {
110        assert_eq!(sniff_audio_format(b"fLaCxxxx"), AUDIO_FORMAT_FLAC);
111    }
112
113    #[test]
114    fn sniff_ogg() {
115        assert_eq!(sniff_audio_format(b"OggSxxxx"), AUDIO_FORMAT_OGG);
116    }
117
118    #[test]
119    fn sniff_mp3_sync() {
120        let b = [0xFF, 0xFB, 0x90, 0x00];
121        assert_eq!(sniff_audio_format(&b), AUDIO_FORMAT_MP3);
122    }
123
124    #[test]
125    fn mime_maps() {
126        assert_eq!(mime_to_audio_format("audio/mpeg"), AUDIO_FORMAT_MP3);
127        assert_eq!(
128            mime_to_audio_format("audio/ogg; codecs=vorbis"),
129            AUDIO_FORMAT_OGG
130        );
131        assert!(is_likely_non_audio_document("text/html; charset=utf-8"));
132    }
133}