diff --git a/CHANGELOG.md b/CHANGELOG.md index e0f149dec..4acc1aefb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,7 +51,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed sources to correctly handle sample rate and channel count changes at span boundaries. - Fixed sources to detect parameter updates after mid-span seeks. - Fixed `Stoppable` and `Skippable` not signaling exhaustion. -- Fixed `SpatialAudio` left and write channel swapping +- Fixed `SpatialAudio` left and write channel swapping. +- Fixed the Symphonia decoder reporting a wrong `total_duration` for containers whose default track is not the decoded audio track. ## Version [0.22.2] (2026-03-05) diff --git a/assets/README.md b/assets/README.md index c65c31664..d47067c56 100644 --- a/assets/README.md +++ b/assets/README.md @@ -1,3 +1,6 @@ ## License The `music.wav` and `music.ogg` files in this directory are under cc-by-sa. + +`video_with_audio.mp4` is synthetic, generated from ffmpeg's `testsrc` (video) +and `sine` (audio) sources, and carries no third-party licensing. diff --git a/assets/video_with_audio.mp4 b/assets/video_with_audio.mp4 new file mode 100644 index 000000000..57db0fdf2 Binary files /dev/null and b/assets/video_with_audio.mp4 differ diff --git a/src/decoder/symphonia.rs b/src/decoder/symphonia.rs index 5e1c68849..82f3ea302 100644 --- a/src/decoder/symphonia.rs +++ b/src/decoder/symphonia.rs @@ -107,10 +107,9 @@ impl SymphoniaDecoder { }; let mut probed = get_probe().format(&hint, mss, &format_opts, &metadata_opts)?; - let stream = match probed.format.default_track() { - Some(stream) => stream, - None => return Ok(None), - }; + if probed.format.default_track().is_none() { + return Ok(None); + } // Select the first supported track let track = probed @@ -131,7 +130,7 @@ impl SymphoniaDecoder { let total_duration = track .codec_params .time_base - .zip(stream.codec_params.n_frames) + .zip(track.codec_params.n_frames) .map(|(base, spans)| base.calc_time(spans).into()) .filter(|d: &Duration| !d.is_zero()); diff --git a/tests/video_container_test.rs b/tests/video_container_test.rs new file mode 100644 index 000000000..8cbacc1df --- /dev/null +++ b/tests/video_container_test.rs @@ -0,0 +1,40 @@ +//! Regression tests for decoding the audio track of a container whose +//! *default* track is not the audio track — i.e. a video file. +//! +//! `assets/video_with_audio.mp4` holds a 2 s H.264 video track (the default, +//! 60 frames) plus a 2 s mono AAC audio track. + +#![cfg(all(feature = "symphonia-aac", feature = "symphonia-isomp4"))] + +use rodio::{Decoder, Source}; + +fn decode_video_asset() -> Decoder { + let file = std::fs::File::open("assets/video_with_audio.mp4").unwrap(); + let len = file.metadata().unwrap().len(); + Decoder::builder() + .with_data(file) + .with_byte_len(len) + .with_seekable(true) + .build() + .unwrap() +} + +/// `total_duration` must come from the audio track (~2.02 s), not the video +/// track. The buggy pairing reported ~0.7 s, so a generous lower bound alone +/// distinguishes the two, and the tight check pins the correct value. +#[test] +fn reports_audio_track_duration_not_default_track() { + let duration = decode_video_asset() + .total_duration() + .expect("video container should report a total duration") + .as_secs_f64(); + assert!( + duration > 1.5, + "duration {duration}s looks like the video track's frame count, not the audio track" + ); + let expected = 2.023_219_954; + assert!( + (duration - expected).abs() < 0.01, + "got {duration}s, expected about {expected}s" + ); +}