mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 10:37:03 -06:00
Media: damaged audio no longer hangs the demux loop or panics in symphonia
This commit is contained in:
@@ -26,6 +26,7 @@ deck and UI icons are original work of the DeckCraft contributors (drawn or gene
|
||||
| `docs/images/editor.png`, `docs/images/shape-format.png`, `docs/images/format-pane.png`, `docs/images/slide-sorter.png`, `docs/images/dark-mode.png`, `docs/images/sample-deck.png` | DeckCraft contributors | Screenshots of DeckCraft and slides rendered by `deckcraft-cli` from the built-in sample deck (original content; Inter font, OFL-1.1) | MIT OR Apache-2.0 | README screenshots |
|
||||
| `crates/media/tests/fixtures/tone*` (`tone.mp3`, `.m4a`, `.aac`, `.ogg`, `.opus`, `.flac`, `.wma`, `tone-alac.m4a`) | DeckCraft contributors | synthesised 0.5 s 440 Hz sine, encoded by `crates/media/tests/fixtures/make.sh` (ffmpeg as the encoder only) | CC0-1.0 | Audio decoding tests |
|
||||
| `crates/media/tests/fixtures/clip-*` (`clip-h264.mp4`, `clip-h264.mov`, `clip-hevc.mp4`, `clip-vp9.webm`, `clip-av1.mkv`) | DeckCraft contributors | 6 frames of a colour gradient generated by `make.sh` (python3), encoded by ffmpeg (x264 / x265 / libvpx / SVT-AV1 as encoders only) | CC0-1.0 | Video decoding tests |
|
||||
| `crates/media/tests/fixtures/damaged/*` (`ebml-header.mkv`, `ebml-position.webm`, `demux-loop.mp4`, `demux-loop.m4a`) | DeckCraft contributors | the `clip-*` and `tone*` fixtures above with bytes changed by a mutation fuzzer | CC0-1.0 | Damaged media must not panic or hang |
|
||||
| `crates/media/tests/fixtures/make.sh` | DeckCraft contributors | this repository | MIT OR Apache-2.0 | Regenerates the media fixtures |
|
||||
| `crates/render/tests/fixtures/colr-squares.ttf` | DeckCraft contributors | two squares as a COLRv0/CPAL test font, built by `make_colr_font.py` (fontTools as the builder only) | CC0-1.0 | Colour glyph rendering tests |
|
||||
| `crates/fonts/tests/fixtures/legacy-semibold.ttf` | DeckCraft contributors | a one-glyph Semibold face named like Segoe UI Semibold (its Windows family differs from the typographic one), built by `make_legacy_font.py` (fontTools as the builder only) | CC0-1.0 | Font lookup by Windows family name tests |
|
||||
|
||||
@@ -71,13 +71,13 @@ pub fn decode(bytes: &Bytes) -> Result<Pcm> {
|
||||
Container::Asf => Err(MediaError::Unsupported("Windows Media Audio (WMA)".into())),
|
||||
Container::Ogg => match decode_ogg_opus(bytes) {
|
||||
Some(r) => r,
|
||||
None => decode_symphonia(bytes, "ogg"),
|
||||
None => contained(|| decode_symphonia(bytes, "ogg")),
|
||||
},
|
||||
Container::Matroska => match decode_mkv_opus(bytes) {
|
||||
Some(r) => r,
|
||||
None => decode_symphonia(bytes, "mkv"),
|
||||
None => contained(|| decode_symphonia(bytes, "mkv")),
|
||||
},
|
||||
c => decode_symphonia(bytes, c.name()),
|
||||
c => contained(|| decode_symphonia(bytes, c.name())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,6 +142,15 @@ fn sym_err(e: SymError) -> MediaError {
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs symphonia, whose demuxers can panic on damaged files (Matroska's EBML reader asserts
|
||||
/// its position, for one): the panic becomes a corrupt-file error instead of ending the app.
|
||||
fn contained<T>(f: impl FnOnce() -> Result<T>) -> Result<T> {
|
||||
std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).unwrap_or_else(|_| Err(MediaError::Corrupt("the audio could not be read".into())))
|
||||
}
|
||||
|
||||
/// Demux errors in a row after which a file is taken to have no more audio.
|
||||
const MAX_DEMUX_ERRORS: u32 = 64;
|
||||
|
||||
fn decode_symphonia(bytes: &Bytes, ext: &str) -> Result<Pcm> {
|
||||
let mss = MediaSourceStream::new(Box::new(Cursor::new(Shared(bytes.clone()))), MediaSourceStreamOptions::default());
|
||||
let mut hint = Hint::new();
|
||||
@@ -172,12 +181,21 @@ fn decode_symphonia(bytes: &Bytes, ext: &str) -> Result<Pcm> {
|
||||
let mut rate = 0;
|
||||
let mut channels = 0u16;
|
||||
let mut buf: Option<SampleBuffer<f32>> = None;
|
||||
// A damaged file can make the demuxer report the same error forever without moving on.
|
||||
let mut demux_errors = 0u32;
|
||||
loop {
|
||||
let packet = match format.next_packet() {
|
||||
Ok(p) => p,
|
||||
Ok(p) => {
|
||||
demux_errors = 0;
|
||||
p
|
||||
}
|
||||
Err(SymError::IoError(_)) | Err(SymError::ResetRequired) => break,
|
||||
Err(SymError::DecodeError(e)) => {
|
||||
log::debug!("audio demux: {e}");
|
||||
demux_errors += 1;
|
||||
if demux_errors > MAX_DEMUX_ERRORS {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
|
||||
@@ -58,8 +58,10 @@ pub fn probe(bytes: &Bytes) -> Result<MediaInfo> {
|
||||
};
|
||||
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.
|
||||
let (codec, rate, channels, frames) =
|
||||
crate::audio::probe_symphonia(bytes, c.name()).ok_or_else(|| MediaError::Unsupported("this kind of media file".into()))?;
|
||||
let (codec, rate, channels, frames) = std::panic::catch_unwind(|| crate::audio::probe_symphonia(bytes, c.name()))
|
||||
.ok()
|
||||
.flatten()
|
||||
.ok_or_else(|| MediaError::Unsupported("this kind of media file".into()))?;
|
||||
info.audio = Some(AudioInfo { codec: codec_label(&codec), sample_rate: rate, channels, decodable: true });
|
||||
if info.duration_ms == 0
|
||||
&& let Some(n) = frames
|
||||
|
||||
@@ -325,3 +325,33 @@ fn player_plays_decoded_audio_through_the_mixer() {
|
||||
assert!((rms - 0.0884 * 0.5).abs() < 0.005, "{rms}");
|
||||
assert!((p.status(1).map(|s| s.position).unwrap_or(0.0) - 0.1).abs() < 1e-6);
|
||||
}
|
||||
|
||||
/// Damaged files found by mutation fuzzing the fixtures above (so CC0 like them): each must come
|
||||
/// back as an error or as audio — not panic (symphonia's Matroska reader panics on the first two)
|
||||
/// and not hang (the MP4 demuxer reported the same error forever on the last two).
|
||||
#[test]
|
||||
fn damaged_media_neither_panics_nor_hangs() {
|
||||
for name in ["damaged/ebml-header.mkv", "damaged/ebml-position.webm", "damaged/demux-loop.mp4", "damaged/demux-loop.m4a"] {
|
||||
let b = fixture(name);
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
std::thread::spawn(move || {
|
||||
let ok = std::panic::catch_unwind(|| {
|
||||
let _ = probe::probe(&b);
|
||||
let _ = audio::decode(&b);
|
||||
if let Ok(mut d) = video::VideoDecoder::open(b.clone()) {
|
||||
for _ in 0..12 {
|
||||
if !matches!(d.next_frame(), Ok(Some(_))) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.is_ok();
|
||||
let _ = tx.send(ok);
|
||||
});
|
||||
match rx.recv_timeout(Duration::from_secs(20)) {
|
||||
Ok(ok) => assert!(ok, "{name} panicked"),
|
||||
Err(_) => panic!("{name} still decoding after 20 s"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user