Skip to content

Commit

Permalink
Improve initial loading time
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
roderickvd committed Jan 4, 2022
1 parent eabdd79 commit 3e09eff
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 81 deletions.
53 changes: 17 additions & 36 deletions audio/src/fetch/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
/// 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.
pub const INITIAL_DOWNLOAD_SIZE: usize = 1024 * 128;

/// 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);
pub const INITIAL_DOWNLOAD_SIZE: usize = 1024 * 8;

/// 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.
Expand Down Expand Up @@ -321,7 +318,6 @@ impl AudioFile {
session: &Session,
file_id: FileId,
bytes_per_second: usize,
play_from_beginning: bool,
) -> Result<AudioFile, Error> {
if let Some(file) = session.cache().and_then(|cache| cache.file(file_id)) {
debug!("File {} already in cache", file_id);
Expand All @@ -332,13 +328,8 @@ impl AudioFile {

let (complete_tx, complete_rx) = oneshot::channel();

let streaming = AudioFileStreaming::open(
session.clone(),
file_id,
complete_tx,
bytes_per_second,
play_from_beginning,
);
let streaming =
AudioFileStreaming::open(session.clone(), file_id, complete_tx, bytes_per_second);

let session_ = session.clone();
session.spawn(complete_rx.map_ok(move |mut file| {
Expand Down Expand Up @@ -386,53 +377,43 @@ impl AudioFileStreaming {
file_id: FileId,
complete_tx: oneshot::Sender<NamedTempFile>,
bytes_per_second: usize,
play_from_beginning: bool,
) -> 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?;

if let Ok(url) = cdn_url.try_get_url() {
trace!("Streaming from {}", url);
}

let mut streamer = session
.spclient()
.stream_from_cdn(&cdn_url, 0, download_size)?;
let request_time = Instant::now();
// 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 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.
// The remainder of that chunk with possibly also a response body is then
// further processed in `audio_file_fetch`.
let request_time = Instant::now();
let response = streamer.next().await.ok_or(AudioFileError::NoData)??;

let header_value = response
.headers()
.get(CONTENT_RANGE)
.ok_or(AudioFileError::Header)?;
let str_value = header_value.to_str()?;
let file_size_str = str_value.split('/').last().unwrap_or_default();
let file_size = file_size_str.parse()?;
let hyphen_index = str_value.find('-').unwrap_or_default();
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 {
streamer,
initial_response: Some(response),
offset: 0,
length: download_size,
length: upper_bound + 1,
request_time,
};

Expand Down
80 changes: 42 additions & 38 deletions playback/src/decoder/symphonia_decoder.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
use std::io;

use symphonia::core::{
audio::SampleBuffer,
codecs::{Decoder, DecoderOptions},
errors::Error,
formats::{FormatReader, SeekMode, SeekTo},
io::{MediaSource, MediaSourceStream, MediaSourceStreamOptions},
meta::{MetadataOptions, StandardTagKey, Value},
probe::Hint,
units::Time,
use symphonia::{
core::{
audio::SampleBuffer,
codecs::{Decoder, DecoderOptions},
errors::Error,
formats::{FormatReader, SeekMode, SeekTo},
io::{MediaSource, MediaSourceStream, MediaSourceStreamOptions},
meta::{StandardTagKey, Value},
units::Time,
},
default::{
codecs::{Mp3Decoder, VorbisDecoder},
formats::{Mp3Reader, OggReader},
},
};

use super::{AudioDecoder, AudioPacket, DecoderError, DecoderResult};
Expand All @@ -20,13 +25,13 @@ use crate::{
};

pub struct SymphoniaDecoder {
decoder: Box<dyn Decoder>,
format: Box<dyn FormatReader>,
decoder: Box<dyn Decoder>,
sample_buffer: Option<SampleBuffer<f64>>,
}

impl SymphoniaDecoder {
pub fn new<R>(input: R, format: AudioFileFormat) -> DecoderResult<Self>
pub fn new<R>(input: R, file_format: AudioFileFormat) -> DecoderResult<Self>
where
R: MediaSource + 'static,
{
Expand All @@ -35,48 +40,47 @@ impl SymphoniaDecoder {
};
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 metadata_opts: MetadataOptions = Default::default();
let decoder_opts: DecoderOptions = Default::default();

let probed =
symphonia::default::get_probe().format(&hint, mss, &format_opts, &metadata_opts)?;
let format = probed.format;
let format: Box<dyn FormatReader> = if AudioFiles::is_ogg_vorbis(file_format) {
Box::new(OggReader::try_new(mss, &format_opts)?)
} else if AudioFiles::is_mp3(file_format) {
Box::new(Mp3Reader::try_new(mss, &format_opts)?)
} else {
return Err(DecoderError::SymphoniaDecoder(format!(
"Unsupported format: {:?}",
file_format
)));
};

let track = format.default_track().ok_or_else(|| {
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 = codec_params.sample_rate.ok_or_else(|| {
let rate = decoder.codec_params().sample_rate.ok_or_else(|| {
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 {
return Err(DecoderError::SymphoniaDecoder(format!(
"Unsupported sample rate: {}",
rate
)));
}

let channels = decoder.codec_params().channels.ok_or_else(|| {
DecoderError::SymphoniaDecoder("Could not retrieve channel configuration".into())
})?;
if channels.count() != NUM_CHANNELS as usize {
return Err(DecoderError::SymphoniaDecoder(format!(
"Unsupported number of channels: {}",
Expand All @@ -85,8 +89,8 @@ impl SymphoniaDecoder {
}

Ok(Self {
decoder,
format,
decoder,

// We set the sample buffer when decoding the first full packet,
// whose duration is also the ideal sample buffer size.
Expand Down
8 changes: 1 addition & 7 deletions playback/src/player.rs
Original file line number Diff line number Diff line change
Expand Up @@ -875,17 +875,11 @@ impl PlayerTrackLoader {
};

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
// while opening a cached file.
loop {
let encrypted_file = AudioFile::open(
&self.session,
file_id,
bytes_per_second,
play_from_beginning,
);
let encrypted_file = AudioFile::open(&self.session, file_id, bytes_per_second);

let encrypted_file = match encrypted_file.await {
Ok(encrypted_file) => encrypted_file,
Expand Down

0 comments on commit 3e09eff

Please sign in to comment.