diff --git a/crates/media/src/audio.rs b/crates/media/src/audio.rs index e3769ee2f..09f9efa37 100644 --- a/crates/media/src/audio.rs +++ b/crates/media/src/audio.rs @@ -81,7 +81,9 @@ pub fn decode(bytes: &Bytes) -> Result { Some(r) => r, None => contained(|| decode_symphonia(bytes, "mkv")), }, - _ if !crate::mp4guard::probe_fits(bytes) => Err(MediaError::Corrupt("the MP4's sample tables do not fit the file".into())), + Container::Mp4 | Container::Unknown | Container::Mp3 if !crate::mp4guard::probe_fits(bytes) => { + Err(MediaError::Corrupt("the MP4's sample tables do not fit the file".into())) + } c => contained(|| decode_symphonia(bytes, c.name())), } } diff --git a/crates/media/src/mp4guard.rs b/crates/media/src/mp4guard.rs index 86ffc3528..56047979f 100644 --- a/crates/media/src/mp4guard.rs +++ b/crates/media/src/mp4guard.rs @@ -19,9 +19,18 @@ const PROBE_SEARCH_LIMIT: usize = 1024 * 1024; /// `ftyp` within its search window as the start of an MP4 (the size field 4 bytes before it), so /// the table check must start where that parser will, not at offset 0. pub(crate) fn probe_fits(b: &[u8]) -> bool { - let head = &b[..b.len().min(PROBE_SEARCH_LIMIT + 16)]; + if crate::Container::sniff(b) == crate::Container::Mp4 && !tables_fit(b) { + return false; + } + // The probe reads leading ID3v2 tags as metadata and searches again after the last one. + let mut rest = b; + while rest.starts_with(b"ID3") { + let size = rest.get(6..10).map(|s| s.iter().fold(0usize, |a, &x| (a << 7) | (x & 0x7f) as usize)).unwrap_or(0); + rest = rest.get(10 + size..).unwrap_or(&[]); + } + let head = &rest[..rest.len().min(PROBE_SEARCH_LIMIT + 16)]; match head.windows(4).position(|w| w == b"ftyp") { - Some(p) => tables_fit(&b[p.saturating_sub(4)..]), + Some(p) => tables_fit(&rest[p.saturating_sub(4)..]), None => true, } } diff --git a/crates/media/src/probe.rs b/crates/media/src/probe.rs index 4de516b1f..4e8e845c5 100644 --- a/crates/media/src/probe.rs +++ b/crates/media/src/probe.rs @@ -71,8 +71,8 @@ pub fn probe_headers(bytes: &Bytes) -> Result { }; if info.audio.is_none() && info.video.is_none() || info.audio.as_ref().is_some_and(|a| a.sample_rate == 0) { // Plain audio files (and anything our demuxers didn't describe): symphonia's probe. - // Known non-MP4 containers (a WAV's audio bytes can spell an `ftyp`) skip the MP4 table check. - if matches!(c, Container::Mp4 | Container::Unknown) && !crate::mp4guard::probe_fits(bytes) { + // Symphonia opens an MP4 wherever its probe finds an `ftyp` (after leading ID3v2 tags too); other known containers (a WAV's audio bytes can spell an `ftyp`) skip the check. + if matches!(c, Container::Mp4 | Container::Unknown | Container::Mp3) && !crate::mp4guard::probe_fits(bytes) { return Err(MediaError::Corrupt("the MP4's sample tables do not fit the file".into())); } let (codec, rate, channels, frames) = std::panic::catch_unwind(|| crate::audio::probe_symphonia(bytes, c.name()))