adds total_duration() impl to SymphoniaDecoder, makes seek saturating at source end if total_duration known

This commit is contained in:
dvdsk 2023-10-10 02:25:34 +02:00
parent 8416210628
commit 2b39d27aaf
No known key found for this signature in database
GPG key ID: 6CF9D20C5709A836
5 changed files with 69 additions and 26 deletions

View file

@ -75,6 +75,7 @@ where
self.decoder.seek_samples(pos)?; self.decoder.seek_samples(pos)?;
Ok(()) Ok(())
} }
}
impl<R> Iterator for Mp3Decoder<R> impl<R> Iterator for Mp3Decoder<R>
where where

View file

@ -26,6 +26,7 @@ pub struct SymphoniaDecoder {
decoder: Box<dyn Decoder>, decoder: Box<dyn Decoder>,
current_frame_offset: usize, current_frame_offset: usize,
format: Box<dyn FormatReader>, format: Box<dyn FormatReader>,
total_duration: Option<Time>,
buffer: SampleBuffer<i16>, buffer: SampleBuffer<i16>,
spec: SignalSpec, spec: SignalSpec,
} }
@ -79,6 +80,11 @@ impl SymphoniaDecoder {
..Default::default() ..Default::default()
}, },
)?; )?;
let total_duration = stream
.codec_params
.time_base
.zip(stream.codec_params.n_frames)
.map(|(base, frames)| base.calc_time(frames));
let mut decode_errors: usize = 0; let mut decode_errors: usize = 0;
let decoded = loop { let decoded = loop {
@ -105,6 +111,7 @@ impl SymphoniaDecoder {
decoder, decoder,
current_frame_offset: 0, current_frame_offset: 0,
format: probed.format, format: probed.format,
total_duration,
buffer, buffer,
spec, spec,
})); }));
@ -137,7 +144,8 @@ impl Source for SymphoniaDecoder {
#[inline] #[inline]
fn total_duration(&self) -> Option<Duration> { fn total_duration(&self) -> Option<Duration> {
None self.total_duration
.map(|Time { seconds, frac }| Duration::new(seconds, (1f64 / frac) as u32))
} }
// TODO: do we return till where we seeked? <dvdsk noreply@davidsk.dev> // TODO: do we return till where we seeked? <dvdsk noreply@davidsk.dev>
@ -151,13 +159,21 @@ impl Source for SymphoniaDecoder {
1f64 / pos.subsec_nanos() as f64 1f64 / pos.subsec_nanos() as f64
}; };
let mut seek_to_time = Time::new(pos.as_secs(), pos_fract);
if let Some(total_duration) = self.total_duration() {
if total_duration.saturating_sub(pos).as_millis() < 1 {
seek_to_time = self.total_duration.unwrap();
}
}
let res = self.format.seek( let res = self.format.seek(
SeekMode::Accurate, SeekMode::Accurate,
SeekTo::Time { SeekTo::Time {
time: Time::new(pos.as_secs(), pos_fract), time: seek_to_time,
track_id: None, track_id: None,
}, },
); );
assert!(self.total_duration().is_some());
match res { match res {
Err(Error::IoError(e)) if e.kind() == ErrorKind::UnexpectedEof => Ok(()), Err(Error::IoError(e)) if e.kind() == ErrorKind::UnexpectedEof => Ok(()),

View file

@ -133,7 +133,10 @@ where
#[inline] #[inline]
fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> {
let samples = pos.as_secs_f32() * self.sample_rate() as f32; let samples = pos.as_secs_f32() * self.sample_rate() as f32;
self.reader.reader.seek(samples as u32).map_err(SeekError::HoundDecoder) self.reader
.reader
.seek(samples as u32)
.map_err(SeekError::HoundDecoder)
} }
} }

View file

@ -357,7 +357,10 @@ where
/// ///
/// Try to seek to a pos, returns [`SeekNotSupported`] if seeking is not /// Try to seek to a pos, returns [`SeekNotSupported`] if seeking is not
/// supported by the current source. /// supported by the current source.
fn try_seek(&mut self, _: Duration) -> Result<(), SeekError>; ///
/// If the duration of the source is known and the seek position lies beyond
/// it the saturates, that is the position is then at the end of the source.
fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError>;
} }
// we might add decoders requiring new error types, would non_exhaustive // we might add decoders requiring new error types, would non_exhaustive

View file

@ -30,6 +30,10 @@ fn sink_and_decoder(format: &str) -> (Sink, Decoder<impl Read + Seek>) {
(sink, decoder) (sink, decoder)
} }
// run the following to get the other configuration:
// cargo test --no-default-features
// --features symphonia-wav --features symphonia-vorbis
// --features symphonia-flac --features symphonia-isomp4 --features minimp3
fn format_decoder_info() -> &'static [(&'static str, bool, &'static str)] { fn format_decoder_info() -> &'static [(&'static str, bool, &'static str)] {
&[ &[
#[cfg(feature = "minimp3")] #[cfg(feature = "minimp3")]
@ -42,14 +46,16 @@ fn format_decoder_info() -> &'static [(&'static str, bool, &'static str)] {
("wav", true, "symphonia"), ("wav", true, "symphonia"),
#[cfg(feature = "lewton")] #[cfg(feature = "lewton")]
("ogg", true, "lewton"), ("ogg", true, "lewton"),
#[cfg(feature = "symphonia-vorbis")] // note: disabled, symphonia returns error unsupported format
("ogg", true, "symphonia"), // #[cfg(feature = "symphonia-vorbis")]
// ("ogg", true, "symphonia"),
#[cfg(feature = "claxon")] #[cfg(feature = "claxon")]
("flac", false, "claxon"), ("flac", false, "claxon"),
#[cfg(feature = "symphonia-flac")] #[cfg(feature = "symphonia-flac")]
("flac", true, "symphonia"), ("flac", true, "symphonia"),
#[cfg(feature = "symphonia-isomp4")] // note: disabled, symphonia returns error unsupported format
("m4a", true, "_"), // #[cfg(feature = "symphonia-isomp4")]
// ("m4a", true, "symphonia"),
] ]
} }
@ -64,17 +70,38 @@ fn seek_returns_err_if_unsupported() {
} }
} }
#[test] // #[ignore]
fn seek_beyond_end_does_not_crash() { #[test] // in the future use PR #510 (playback position) to speed this up
fn seek_beyond_end_saturates() {
for (format, _, decoder_name) in format_decoder_info() for (format, _, decoder_name) in format_decoder_info()
.iter() .iter()
.cloned() .cloned()
.filter(|(_, supported, _)| *supported) .filter(|(_, supported, _)| *supported)
{ {
let (sink, decoder) = sink_and_decoder(format); let (sink, decoder) = sink_and_decoder(format);
println!("seeking beyond end in: {format}\t decoded by: {decoder_name}");
sink.append(decoder); sink.append(decoder);
sink.try_seek(Duration::from_secs(999)).unwrap();
println!("seeking beyond end for: {format}\t decoded by: {decoder_name}");
let res = sink.try_seek(Duration::from_secs(999));
assert!(res.is_ok());
let now = Instant::now();
sink.sleep_until_end();
let elapsed = now.elapsed();
assert!(elapsed.as_secs() < 1);
}
}
fn total_duration(format: &'static str) -> Duration {
let (sink, decoder) = sink_and_decoder(format);
match decoder.total_duration() {
Some(d) => d,
None => {
let now = Instant::now();
sink.append(decoder);
sink.sleep_until_end();
now.elapsed()
}
} }
} }
@ -86,23 +113,14 @@ fn seek_results_in_correct_remaining_playtime() {
.cloned() .cloned()
.filter(|(_, supported, _)| *supported) .filter(|(_, supported, _)| *supported)
{ {
let (sink, decoder) = sink_and_decoder(format); println!("checking seek duration for: {format}\t decoded by: {decoder_name}");
println!("checking seek time in: {format}\t decoded by: {decoder_name}");
let total_duration = match decoder.total_duration() {
Some(d) => d,
None => {
let now = Instant::now();
sink.append(decoder);
sink.sleep_until_end();
now.elapsed()
}
};
let (sink, decoder) = sink_and_decoder(format); let (sink, decoder) = sink_and_decoder(format);
sink.append(decoder); sink.append(decoder);
const SEEK_BEFORE_END: Duration = Duration::from_secs(5); const SEEK_BEFORE_END: Duration = Duration::from_secs(5);
sink.try_seek(total_duration - SEEK_BEFORE_END).unwrap(); sink.try_seek(total_duration(format) - SEEK_BEFORE_END)
.unwrap();
let now = Instant::now(); let now = Instant::now();
sink.sleep_until_end(); sink.sleep_until_end();
@ -110,9 +128,11 @@ fn seek_results_in_correct_remaining_playtime() {
let expected = SEEK_BEFORE_END; let expected = SEEK_BEFORE_END;
if elapsed.as_millis().abs_diff(expected.as_millis()) > 250 { if elapsed.as_millis().abs_diff(expected.as_millis()) > 250 {
panic!("Seek did not result in expected leftover playtime panic!(
"Seek did not result in expected leftover playtime
leftover time: {elapsed:?} leftover time: {elapsed:?}
expected time left in source: {SEEK_BEFORE_END:?}"); expected time left in source: {SEEK_BEFORE_END:?}"
);
} }
} }
} }