mirror of
https://github.com/librespot-org/librespot.git
synced 2024-12-18 17:11:53 +00:00
Improve initial loading time
- Configure the decoder according to Spotify's metadata, don't probe - Return from `AudioFile::open` as soon as possible, with the smallest possible block size suitable for opening the decoder, so the UI transitions from loading to playing/paused state. From there, the regular prefetching will take over.
This commit is contained in:
parent
eabdd79275
commit
3e09eff906
3 changed files with 60 additions and 81 deletions
|
@ -65,10 +65,7 @@ pub const MINIMUM_DOWNLOAD_SIZE: usize = 1024 * 128;
|
||||||
/// Note: if the file is opened to play from the beginning, the amount of data to
|
/// Note: if the file is opened to play from the beginning, the amount of data to
|
||||||
/// read ahead is requested in addition to this amount. If the file is opened to seek to
|
/// read ahead is requested in addition to this amount. If the file is opened to seek to
|
||||||
/// another position, then only this amount is requested on the first request.
|
/// another position, then only this amount is requested on the first request.
|
||||||
pub const INITIAL_DOWNLOAD_SIZE: usize = 1024 * 128;
|
pub const INITIAL_DOWNLOAD_SIZE: usize = 1024 * 8;
|
||||||
|
|
||||||
/// The ping time that is used for calculations before a ping time was actually measured.
|
|
||||||
pub const INITIAL_PING_TIME_ESTIMATE: Duration = Duration::from_millis(500);
|
|
||||||
|
|
||||||
/// If the measured ping time to the Spotify server is larger than this value, it is capped
|
/// If the measured ping time to the Spotify server is larger than this value, it is capped
|
||||||
/// to avoid run-away block sizes and pre-fetching.
|
/// to avoid run-away block sizes and pre-fetching.
|
||||||
|
@ -321,7 +318,6 @@ impl AudioFile {
|
||||||
session: &Session,
|
session: &Session,
|
||||||
file_id: FileId,
|
file_id: FileId,
|
||||||
bytes_per_second: usize,
|
bytes_per_second: usize,
|
||||||
play_from_beginning: bool,
|
|
||||||
) -> Result<AudioFile, Error> {
|
) -> Result<AudioFile, Error> {
|
||||||
if let Some(file) = session.cache().and_then(|cache| cache.file(file_id)) {
|
if let Some(file) = session.cache().and_then(|cache| cache.file(file_id)) {
|
||||||
debug!("File {} already in cache", file_id);
|
debug!("File {} already in cache", file_id);
|
||||||
|
@ -332,13 +328,8 @@ impl AudioFile {
|
||||||
|
|
||||||
let (complete_tx, complete_rx) = oneshot::channel();
|
let (complete_tx, complete_rx) = oneshot::channel();
|
||||||
|
|
||||||
let streaming = AudioFileStreaming::open(
|
let streaming =
|
||||||
session.clone(),
|
AudioFileStreaming::open(session.clone(), file_id, complete_tx, bytes_per_second);
|
||||||
file_id,
|
|
||||||
complete_tx,
|
|
||||||
bytes_per_second,
|
|
||||||
play_from_beginning,
|
|
||||||
);
|
|
||||||
|
|
||||||
let session_ = session.clone();
|
let session_ = session.clone();
|
||||||
session.spawn(complete_rx.map_ok(move |mut file| {
|
session.spawn(complete_rx.map_ok(move |mut file| {
|
||||||
|
@ -386,38 +377,26 @@ impl AudioFileStreaming {
|
||||||
file_id: FileId,
|
file_id: FileId,
|
||||||
complete_tx: oneshot::Sender<NamedTempFile>,
|
complete_tx: oneshot::Sender<NamedTempFile>,
|
||||||
bytes_per_second: usize,
|
bytes_per_second: usize,
|
||||||
play_from_beginning: bool,
|
|
||||||
) -> Result<AudioFileStreaming, Error> {
|
) -> Result<AudioFileStreaming, Error> {
|
||||||
// When the audio file is really small, this `download_size` may turn out to be
|
|
||||||
// larger than the audio file we're going to stream later on. This is OK; requesting
|
|
||||||
// `Content-Range` > `Content-Length` will return the complete file with status code
|
|
||||||
// 206 Partial Content.
|
|
||||||
let download_size = if play_from_beginning {
|
|
||||||
INITIAL_DOWNLOAD_SIZE
|
|
||||||
+ max(
|
|
||||||
(READ_AHEAD_DURING_PLAYBACK.as_secs_f32() * bytes_per_second as f32) as usize,
|
|
||||||
(INITIAL_PING_TIME_ESTIMATE.as_secs_f32()
|
|
||||||
* READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS
|
|
||||||
* bytes_per_second as f32) as usize,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
INITIAL_DOWNLOAD_SIZE
|
|
||||||
};
|
|
||||||
|
|
||||||
let cdn_url = CdnUrl::new(file_id).resolve_audio(&session).await?;
|
let cdn_url = CdnUrl::new(file_id).resolve_audio(&session).await?;
|
||||||
|
|
||||||
if let Ok(url) = cdn_url.try_get_url() {
|
if let Ok(url) = cdn_url.try_get_url() {
|
||||||
trace!("Streaming from {}", url);
|
trace!("Streaming from {}", url);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut streamer = session
|
// When the audio file is really small, this `download_size` may turn out to be
|
||||||
.spclient()
|
// larger than the audio file we're going to stream later on. This is OK; requesting
|
||||||
.stream_from_cdn(&cdn_url, 0, download_size)?;
|
// `Content-Range` > `Content-Length` will return the complete file with status code
|
||||||
let request_time = Instant::now();
|
// 206 Partial Content.
|
||||||
|
let mut streamer =
|
||||||
|
session
|
||||||
|
.spclient()
|
||||||
|
.stream_from_cdn(&cdn_url, 0, INITIAL_DOWNLOAD_SIZE)?;
|
||||||
|
|
||||||
// Get the first chunk with the headers to get the file size.
|
// Get the first chunk with the headers to get the file size.
|
||||||
// The remainder of that chunk with possibly also a response body is then
|
// The remainder of that chunk with possibly also a response body is then
|
||||||
// further processed in `audio_file_fetch`.
|
// further processed in `audio_file_fetch`.
|
||||||
|
let request_time = Instant::now();
|
||||||
let response = streamer.next().await.ok_or(AudioFileError::NoData)??;
|
let response = streamer.next().await.ok_or(AudioFileError::NoData)??;
|
||||||
|
|
||||||
let header_value = response
|
let header_value = response
|
||||||
|
@ -425,14 +404,16 @@ impl AudioFileStreaming {
|
||||||
.get(CONTENT_RANGE)
|
.get(CONTENT_RANGE)
|
||||||
.ok_or(AudioFileError::Header)?;
|
.ok_or(AudioFileError::Header)?;
|
||||||
let str_value = header_value.to_str()?;
|
let str_value = header_value.to_str()?;
|
||||||
let file_size_str = str_value.split('/').last().unwrap_or_default();
|
let hyphen_index = str_value.find('-').unwrap_or_default();
|
||||||
let file_size = file_size_str.parse()?;
|
let slash_index = str_value.find('/').unwrap_or_default();
|
||||||
|
let upper_bound: usize = str_value[hyphen_index + 1..slash_index].parse()?;
|
||||||
|
let file_size = str_value[slash_index + 1..].parse()?;
|
||||||
|
|
||||||
let initial_request = StreamingRequest {
|
let initial_request = StreamingRequest {
|
||||||
streamer,
|
streamer,
|
||||||
initial_response: Some(response),
|
initial_response: Some(response),
|
||||||
offset: 0,
|
offset: 0,
|
||||||
length: download_size,
|
length: upper_bound + 1,
|
||||||
request_time,
|
request_time,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,19 @@
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
use symphonia::core::{
|
use symphonia::{
|
||||||
audio::SampleBuffer,
|
core::{
|
||||||
codecs::{Decoder, DecoderOptions},
|
audio::SampleBuffer,
|
||||||
errors::Error,
|
codecs::{Decoder, DecoderOptions},
|
||||||
formats::{FormatReader, SeekMode, SeekTo},
|
errors::Error,
|
||||||
io::{MediaSource, MediaSourceStream, MediaSourceStreamOptions},
|
formats::{FormatReader, SeekMode, SeekTo},
|
||||||
meta::{MetadataOptions, StandardTagKey, Value},
|
io::{MediaSource, MediaSourceStream, MediaSourceStreamOptions},
|
||||||
probe::Hint,
|
meta::{StandardTagKey, Value},
|
||||||
units::Time,
|
units::Time,
|
||||||
|
},
|
||||||
|
default::{
|
||||||
|
codecs::{Mp3Decoder, VorbisDecoder},
|
||||||
|
formats::{Mp3Reader, OggReader},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{AudioDecoder, AudioPacket, DecoderError, DecoderResult};
|
use super::{AudioDecoder, AudioPacket, DecoderError, DecoderResult};
|
||||||
|
@ -20,13 +25,13 @@ use crate::{
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct SymphoniaDecoder {
|
pub struct SymphoniaDecoder {
|
||||||
decoder: Box<dyn Decoder>,
|
|
||||||
format: Box<dyn FormatReader>,
|
format: Box<dyn FormatReader>,
|
||||||
|
decoder: Box<dyn Decoder>,
|
||||||
sample_buffer: Option<SampleBuffer<f64>>,
|
sample_buffer: Option<SampleBuffer<f64>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SymphoniaDecoder {
|
impl SymphoniaDecoder {
|
||||||
pub fn new<R>(input: R, format: AudioFileFormat) -> DecoderResult<Self>
|
pub fn new<R>(input: R, file_format: AudioFileFormat) -> DecoderResult<Self>
|
||||||
where
|
where
|
||||||
R: MediaSource + 'static,
|
R: MediaSource + 'static,
|
||||||
{
|
{
|
||||||
|
@ -35,41 +40,37 @@ impl SymphoniaDecoder {
|
||||||
};
|
};
|
||||||
let mss = MediaSourceStream::new(Box::new(input), mss_opts);
|
let mss = MediaSourceStream::new(Box::new(input), mss_opts);
|
||||||
|
|
||||||
// Not necessary, but speeds up loading.
|
|
||||||
let mut hint = Hint::new();
|
|
||||||
if AudioFiles::is_ogg_vorbis(format) {
|
|
||||||
hint.with_extension("ogg");
|
|
||||||
hint.mime_type("audio/ogg");
|
|
||||||
} else if AudioFiles::is_mp3(format) {
|
|
||||||
hint.with_extension("mp3");
|
|
||||||
hint.mime_type("audio/mp3");
|
|
||||||
} else if AudioFiles::is_flac(format) {
|
|
||||||
hint.with_extension("flac");
|
|
||||||
hint.mime_type("audio/flac");
|
|
||||||
}
|
|
||||||
|
|
||||||
let format_opts = Default::default();
|
let format_opts = Default::default();
|
||||||
let metadata_opts: MetadataOptions = Default::default();
|
let format: Box<dyn FormatReader> = if AudioFiles::is_ogg_vorbis(file_format) {
|
||||||
let decoder_opts: DecoderOptions = Default::default();
|
Box::new(OggReader::try_new(mss, &format_opts)?)
|
||||||
|
} else if AudioFiles::is_mp3(file_format) {
|
||||||
let probed =
|
Box::new(Mp3Reader::try_new(mss, &format_opts)?)
|
||||||
symphonia::default::get_probe().format(&hint, mss, &format_opts, &metadata_opts)?;
|
} else {
|
||||||
let format = probed.format;
|
return Err(DecoderError::SymphoniaDecoder(format!(
|
||||||
|
"Unsupported format: {:?}",
|
||||||
|
file_format
|
||||||
|
)));
|
||||||
|
};
|
||||||
|
|
||||||
let track = format.default_track().ok_or_else(|| {
|
let track = format.default_track().ok_or_else(|| {
|
||||||
DecoderError::SymphoniaDecoder("Could not retrieve default track".into())
|
DecoderError::SymphoniaDecoder("Could not retrieve default track".into())
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let decoder = symphonia::default::get_codecs().make(&track.codec_params, &decoder_opts)?;
|
let decoder_opts: DecoderOptions = Default::default();
|
||||||
|
let decoder: Box<dyn Decoder> = if AudioFiles::is_ogg_vorbis(file_format) {
|
||||||
|
Box::new(VorbisDecoder::try_new(&track.codec_params, &decoder_opts)?)
|
||||||
|
} else if AudioFiles::is_mp3(file_format) {
|
||||||
|
Box::new(Mp3Decoder::try_new(&track.codec_params, &decoder_opts)?)
|
||||||
|
} else {
|
||||||
|
return Err(DecoderError::SymphoniaDecoder(format!(
|
||||||
|
"Unsupported decoder: {:?}",
|
||||||
|
file_format
|
||||||
|
)));
|
||||||
|
};
|
||||||
|
|
||||||
let codec_params = decoder.codec_params();
|
let rate = decoder.codec_params().sample_rate.ok_or_else(|| {
|
||||||
let rate = codec_params.sample_rate.ok_or_else(|| {
|
|
||||||
DecoderError::SymphoniaDecoder("Could not retrieve sample rate".into())
|
DecoderError::SymphoniaDecoder("Could not retrieve sample rate".into())
|
||||||
})?;
|
})?;
|
||||||
let channels = codec_params.channels.ok_or_else(|| {
|
|
||||||
DecoderError::SymphoniaDecoder("Could not retrieve channel configuration".into())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if rate != SAMPLE_RATE {
|
if rate != SAMPLE_RATE {
|
||||||
return Err(DecoderError::SymphoniaDecoder(format!(
|
return Err(DecoderError::SymphoniaDecoder(format!(
|
||||||
"Unsupported sample rate: {}",
|
"Unsupported sample rate: {}",
|
||||||
|
@ -77,6 +78,9 @@ impl SymphoniaDecoder {
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let channels = decoder.codec_params().channels.ok_or_else(|| {
|
||||||
|
DecoderError::SymphoniaDecoder("Could not retrieve channel configuration".into())
|
||||||
|
})?;
|
||||||
if channels.count() != NUM_CHANNELS as usize {
|
if channels.count() != NUM_CHANNELS as usize {
|
||||||
return Err(DecoderError::SymphoniaDecoder(format!(
|
return Err(DecoderError::SymphoniaDecoder(format!(
|
||||||
"Unsupported number of channels: {}",
|
"Unsupported number of channels: {}",
|
||||||
|
@ -85,8 +89,8 @@ impl SymphoniaDecoder {
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
decoder,
|
|
||||||
format,
|
format,
|
||||||
|
decoder,
|
||||||
|
|
||||||
// We set the sample buffer when decoding the first full packet,
|
// We set the sample buffer when decoding the first full packet,
|
||||||
// whose duration is also the ideal sample buffer size.
|
// whose duration is also the ideal sample buffer size.
|
||||||
|
|
|
@ -875,17 +875,11 @@ impl PlayerTrackLoader {
|
||||||
};
|
};
|
||||||
|
|
||||||
let bytes_per_second = self.stream_data_rate(format);
|
let bytes_per_second = self.stream_data_rate(format);
|
||||||
let play_from_beginning = position_ms == 0;
|
|
||||||
|
|
||||||
// This is only a loop to be able to reload the file if an error occured
|
// This is only a loop to be able to reload the file if an error occured
|
||||||
// while opening a cached file.
|
// while opening a cached file.
|
||||||
loop {
|
loop {
|
||||||
let encrypted_file = AudioFile::open(
|
let encrypted_file = AudioFile::open(&self.session, file_id, bytes_per_second);
|
||||||
&self.session,
|
|
||||||
file_id,
|
|
||||||
bytes_per_second,
|
|
||||||
play_from_beginning,
|
|
||||||
);
|
|
||||||
|
|
||||||
let encrypted_file = match encrypted_file.await {
|
let encrypted_file = match encrypted_file.await {
|
||||||
Ok(encrypted_file) => encrypted_file,
|
Ok(encrypted_file) => encrypted_file,
|
||||||
|
|
Loading…
Reference in a new issue