perf(player): use Symphonia native seek for CMAF streaming seeks
Replaces skip_duration's decode-every-sample-from-zero loop with FormatReader::seek(SeekMode::Accurate, SeekTo::Time) on a fresh IncrementalStreamingSource. For FLAC this jumps via the seek table to the nearest seek point, then decodes forward to the exact sample — instead of grinding through millions of samples under the buffered source's mutex (the cause of post-seek stuttering). Added IncrementalStreamingSource::seek_to(Duration) which also resets the decoder, clears the sample queue, and rearms the finished flag so the source is cleanly re-primed for the iterator. Non-streaming seeks (cache, offline-cache, local library) still use skip_duration — they hit in-memory bytes with no lock contention, so the linear decode is effectively free and there's no reason to change the path. Part of #335. cc
This commit is contained in:
2 files changed
+49
-6
No files matched your search
@@ -2226,15 +2226,29 @@ impl Player {
|
||||
engine.set_volume(volume);
|
||||
|
||||
// Build the decoded source for the seek. Streaming:
|
||||
// create a fresh IncrementalStreamingSource on the
|
||||
// shared BufferedMediaSource — new reader at byte 0,
|
||||
// same downloaded bytes (no re-download). Non-streaming:
|
||||
// decode the in-RAM full file as before.
|
||||
// create a fresh IncrementalStreamingSource and use
|
||||
// Symphonia's native seek — jumps via FLAC seek
|
||||
// table / MP3 TOC to the target byte, then decodes
|
||||
// forward to the exact sample. Far cheaper than
|
||||
// skip_duration's decode-every-sample-from-zero
|
||||
// loop, which stuttered under the buffered-source
|
||||
// mutex. Non-streaming: decode the in-RAM full file
|
||||
// and skip_duration as before (no lock contention,
|
||||
// fast).
|
||||
let skip_duration = Duration::from_secs(position_secs);
|
||||
let skipped_source: Box<dyn Source<Item = f32> + Send> =
|
||||
if let Some(ref stream_src) = *current_streaming_source {
|
||||
match IncrementalStreamingSource::new(stream_src.clone()) {
|
||||
Ok(s) => Box::new(s.skip_duration(skip_duration)),
|
||||
Ok(mut s) => {
|
||||
if let Err(e) = s.seek_to(skip_duration) {
|
||||
log::error!(
|
||||
"Failed to native-seek streaming source: {}",
|
||||
e
|
||||
);
|
||||
return;
|
||||
}
|
||||
Box::new(s)
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"Failed to create streaming source for seek: {}",
|
||||
|
||||
@@ -32,7 +32,7 @@ use rodio::Source;
|
||||
use symphonia::core::audio::SampleBuffer;
|
||||
use symphonia::core::codecs::{Decoder, DecoderOptions};
|
||||
use symphonia::core::errors::Error as SymphoniaError;
|
||||
use symphonia::core::formats::{FormatOptions, FormatReader};
|
||||
use symphonia::core::formats::{FormatOptions, FormatReader, SeekMode, SeekTo};
|
||||
use symphonia::core::io::{MediaSource, MediaSourceStream};
|
||||
use symphonia::core::meta::MetadataOptions;
|
||||
use symphonia::core::probe::Hint;
|
||||
@@ -587,6 +587,35 @@ impl IncrementalStreamingSource {
|
||||
&self.buffered_source
|
||||
}
|
||||
|
||||
/// Seek the decoder to the given time using Symphonia's native seek.
|
||||
///
|
||||
/// For FLAC this uses the seek table to jump directly to the nearest
|
||||
/// seek point, then decodes forward to the exact sample — far cheaper
|
||||
/// than skip_duration's decode-every-sample-from-zero path. For MP3
|
||||
/// with Xing/VBRI headers it uses the TOC; without headers, Symphonia
|
||||
/// falls back to a binary search, still much cheaper than linear decode.
|
||||
///
|
||||
/// The underlying BufferedMediaSource::seek is the I/O target. If the
|
||||
/// requested byte offset isn't buffered yet it will block on the
|
||||
/// condition variable — callers must only invoke this for times within
|
||||
/// the downloaded watermark.
|
||||
pub fn seek_to(&mut self, time: Duration) -> Result<(), String> {
|
||||
self.format
|
||||
.seek(
|
||||
SeekMode::Accurate,
|
||||
SeekTo::Time {
|
||||
time: time.into(),
|
||||
track_id: Some(self.track_id),
|
||||
},
|
||||
)
|
||||
.map_err(|e| format!("Symphonia seek failed: {}", e))?;
|
||||
self.decoder.reset();
|
||||
self.sample_queue.clear();
|
||||
self.packets_decoded = 0;
|
||||
self.finished = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Decode more packets to fill the sample queue.
|
||||
///
|
||||
/// This is called when the sample queue is running low.
|
||||
|
||||
Reference in new issue
Block a user