Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
3 changes: 3 additions & 0 deletions assets/README.md
Original file line number Diff line number Diff line change
@@ -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.
Binary file added assets/video_with_audio.mp4
Binary file not shown.
9 changes: 4 additions & 5 deletions src/decoder/symphonia.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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());

Expand Down
40 changes: 40 additions & 0 deletions tests/video_container_test.rs
Original file line number Diff line number Diff line change
@@ -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<std::fs::File> {
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"
);
}
Loading