Skip to content

Commit ccd1a72

Browse files
authored
Merge pull request #1234 from lelloman/lelloman/tunable-audio-fetch-params
Make audio fetch parameters tunable
2 parents a245a3c + e175a88 commit ccd1a72

5 files changed

Lines changed: 99 additions & 59 deletions

File tree

audio/src/fetch/mod.rs

Lines changed: 76 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::{
66
io::{self, Read, Seek, SeekFrom},
77
sync::{
88
atomic::{AtomicBool, AtomicUsize, Ordering},
9-
Arc,
9+
Arc, OnceLock,
1010
},
1111
time::Duration,
1212
};
@@ -55,42 +55,75 @@ impl From<AudioFileError> for Error {
5555
}
5656
}
5757

58-
/// The minimum size of a block that is requested from the Spotify servers in one request.
59-
/// This is the block size that is typically requested while doing a `seek()` on a file.
60-
/// The Symphonia decoder requires this to be a power of 2 and > 32 kB.
61-
/// Note: smaller requests can happen if part of the block is downloaded already.
62-
pub const MINIMUM_DOWNLOAD_SIZE: usize = 64 * 1024;
63-
64-
/// The minimum network throughput that we expect. Together with the minimum download size,
65-
/// this will determine the time we will wait for a response.
66-
pub const MINIMUM_THROUGHPUT: usize = 8 * 1024;
67-
68-
/// The ping time that is used for calculations before a ping time was actually measured.
69-
pub const INITIAL_PING_TIME_ESTIMATE: Duration = Duration::from_millis(500);
70-
71-
/// If the measured ping time to the Spotify server is larger than this value, it is capped
72-
/// to avoid run-away block sizes and pre-fetching.
73-
pub const MAXIMUM_ASSUMED_PING_TIME: Duration = Duration::from_millis(1500);
58+
#[derive(Clone)]
59+
pub struct AudioFetchParams {
60+
/// The minimum size of a block that is requested from the Spotify servers in one request.
61+
/// This is the block size that is typically requested while doing a `seek()` on a file.
62+
/// The Symphonia decoder requires this to be a power of 2 and > 32 kB.
63+
/// Note: smaller requests can happen if part of the block is downloaded already.
64+
pub minimum_download_size: usize,
65+
66+
/// The minimum network throughput that we expect. Together with the minimum download size,
67+
/// this will determine the time we will wait for a response.
68+
pub minimum_throughput: usize,
69+
70+
/// The ping time that is used for calculations before a ping time was actually measured.
71+
pub initial_ping_time_estimate: Duration,
72+
73+
/// If the measured ping time to the Spotify server is larger than this value, it is capped
74+
/// to avoid run-away block sizes and pre-fetching.
75+
pub maximum_assumed_ping_time: Duration,
76+
77+
/// Before playback starts, this many seconds of data must be present.
78+
/// Note: the calculations are done using the nominal bitrate of the file. The actual amount
79+
/// of audio data may be larger or smaller.
80+
pub read_ahead_before_playback: Duration,
81+
82+
/// While playing back, this many seconds of data ahead of the current read position are
83+
/// requested.
84+
/// Note: the calculations are done using the nominal bitrate of the file. The actual amount
85+
/// of audio data may be larger or smaller.
86+
pub read_ahead_during_playback: Duration,
87+
88+
/// If the amount of data that is pending (requested but not received) is less than a certain amount,
89+
/// data is pre-fetched in addition to the read ahead settings above. The threshold for requesting more
90+
/// data is calculated as `<pending bytes> < PREFETCH_THRESHOLD_FACTOR * <ping time> * <nominal data rate>`
91+
pub prefetch_threshold_factor: f32,
92+
93+
/// The time we will wait to obtain status updates on downloading.
94+
pub download_timeout: Duration,
95+
}
7496

75-
/// Before playback starts, this many seconds of data must be present.
76-
/// Note: the calculations are done using the nominal bitrate of the file. The actual amount
77-
/// of audio data may be larger or smaller.
78-
pub const READ_AHEAD_BEFORE_PLAYBACK: Duration = Duration::from_secs(1);
97+
impl Default for AudioFetchParams {
98+
fn default() -> Self {
99+
let minimum_download_size = 64 * 1024;
100+
let minimum_throughput = 8 * 1024;
101+
Self {
102+
minimum_download_size,
103+
minimum_throughput,
104+
initial_ping_time_estimate: Duration::from_millis(500),
105+
maximum_assumed_ping_time: Duration::from_millis(1500),
106+
read_ahead_before_playback: Duration::from_secs(1),
107+
read_ahead_during_playback: Duration::from_secs(5),
108+
prefetch_threshold_factor: 4.0,
109+
download_timeout: Duration::from_secs(
110+
(minimum_download_size / minimum_throughput) as u64,
111+
),
112+
}
113+
}
114+
}
79115

80-
/// While playing back, this many seconds of data ahead of the current read position are
81-
/// requested.
82-
/// Note: the calculations are done using the nominal bitrate of the file. The actual amount
83-
/// of audio data may be larger or smaller.
84-
pub const READ_AHEAD_DURING_PLAYBACK: Duration = Duration::from_secs(5);
116+
static AUDIO_FETCH_PARAMS: OnceLock<AudioFetchParams> = OnceLock::new();
85117

86-
/// If the amount of data that is pending (requested but not received) is less than a certain amount,
87-
/// data is pre-fetched in addition to the read ahead settings above. The threshold for requesting more
88-
/// data is calculated as `<pending bytes> < PREFETCH_THRESHOLD_FACTOR * <ping time> * <nominal data rate>`
89-
pub const PREFETCH_THRESHOLD_FACTOR: f32 = 4.0;
118+
impl AudioFetchParams {
119+
pub fn set(params: AudioFetchParams) -> Result<(), AudioFetchParams> {
120+
AUDIO_FETCH_PARAMS.set(params)
121+
}
90122

91-
/// The time we will wait to obtain status updates on downloading.
92-
pub const DOWNLOAD_TIMEOUT: Duration =
93-
Duration::from_secs((MINIMUM_DOWNLOAD_SIZE / MINIMUM_THROUGHPUT) as u64);
123+
pub fn get() -> &'static AudioFetchParams {
124+
AUDIO_FETCH_PARAMS.get_or_init(AudioFetchParams::default)
125+
}
126+
}
94127

95128
pub enum AudioFile {
96129
Cached(fs::File),
@@ -183,6 +216,7 @@ impl StreamLoaderController {
183216

184217
if let Some(ref shared) = self.stream_shared {
185218
let mut download_status = shared.download_status.lock();
219+
let download_timeout = AudioFetchParams::get().download_timeout;
186220

187221
while range.length
188222
> download_status
@@ -191,7 +225,7 @@ impl StreamLoaderController {
191225
{
192226
if shared
193227
.cond
194-
.wait_for(&mut download_status, DOWNLOAD_TIMEOUT)
228+
.wait_for(&mut download_status, download_timeout)
195229
.timed_out()
196230
{
197231
return Err(AudioFileError::WaitTimeout.into());
@@ -297,7 +331,7 @@ impl AudioFileShared {
297331
if ping_time_ms > 0 {
298332
Duration::from_millis(ping_time_ms as u64)
299333
} else {
300-
INITIAL_PING_TIME_ESTIMATE
334+
AudioFetchParams::get().initial_ping_time_estimate
301335
}
302336
}
303337

@@ -395,14 +429,16 @@ impl AudioFileStreaming {
395429
trace!("Streaming from {}", url);
396430
}
397431

432+
let minimum_download_size = AudioFetchParams::get().minimum_download_size;
433+
398434
// When the audio file is really small, this `download_size` may turn out to be
399435
// larger than the audio file we're going to stream later on. This is OK; requesting
400436
// `Content-Range` > `Content-Length` will return the complete file with status code
401437
// 206 Partial Content.
402438
let mut streamer =
403439
session
404440
.spclient()
405-
.stream_from_cdn(&cdn_url, 0, MINIMUM_DOWNLOAD_SIZE)?;
441+
.stream_from_cdn(&cdn_url, 0, minimum_download_size)?;
406442

407443
// Get the first chunk with the headers to get the file size.
408444
// The remainder of that chunk with possibly also a response body is then
@@ -490,9 +526,10 @@ impl Read for AudioFileStreaming {
490526
return Ok(0);
491527
}
492528

529+
let read_ahead_during_playback = AudioFetchParams::get().read_ahead_during_playback;
493530
let length_to_request = if self.shared.is_download_streaming() {
494531
let length_to_request = length
495-
+ (READ_AHEAD_DURING_PLAYBACK.as_secs_f32() * self.shared.bytes_per_second as f32)
532+
+ (read_ahead_during_playback.as_secs_f32() * self.shared.bytes_per_second as f32)
496533
as usize;
497534

498535
// Due to the read-ahead stuff, we potentially request more than the actual request demanded.
@@ -515,11 +552,12 @@ impl Read for AudioFileStreaming {
515552
.map_err(|err| io::Error::new(io::ErrorKind::BrokenPipe, err))?;
516553
}
517554

555+
let download_timeout = AudioFetchParams::get().download_timeout;
518556
while !download_status.downloaded.contains(offset) {
519557
if self
520558
.shared
521559
.cond
522-
.wait_for(&mut download_status, DOWNLOAD_TIMEOUT)
560+
.wait_for(&mut download_status, download_timeout)
523561
.timed_out()
524562
{
525563
return Err(io::Error::new(

audio/src/fetch/receive.rs

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,8 @@ use librespot_core::{http_client::HttpClient, session::Session, Error};
1616
use crate::range_set::{Range, RangeSet};
1717

1818
use super::{
19-
AudioFileError, AudioFileResult, AudioFileShared, StreamLoaderCommand, StreamingRequest,
20-
MAXIMUM_ASSUMED_PING_TIME, MINIMUM_DOWNLOAD_SIZE, MINIMUM_THROUGHPUT,
21-
PREFETCH_THRESHOLD_FACTOR,
19+
AudioFetchParams, AudioFileError, AudioFileResult, AudioFileShared, StreamLoaderCommand,
20+
StreamingRequest,
2221
};
2322

2423
struct PartialFileData {
@@ -151,6 +150,8 @@ struct AudioFileFetch {
151150
file_data_tx: mpsc::UnboundedSender<ReceivedData>,
152151
complete_tx: Option<oneshot::Sender<NamedTempFile>>,
153152
network_response_times: Vec<Duration>,
153+
154+
params: AudioFetchParams,
154155
}
155156

156157
// Might be replaced by enum from std once stable
@@ -166,8 +167,8 @@ impl AudioFileFetch {
166167
}
167168

168169
fn download_range(&mut self, offset: usize, mut length: usize) -> AudioFileResult {
169-
if length < MINIMUM_DOWNLOAD_SIZE {
170-
length = MINIMUM_DOWNLOAD_SIZE;
170+
if length < self.params.minimum_download_size {
171+
length = self.params.minimum_download_size;
171172
}
172173

173174
// If we are in streaming mode (so not seeking) then start downloading as large
@@ -258,13 +259,13 @@ impl AudioFileFetch {
258259
fn handle_file_data(&mut self, data: ReceivedData) -> Result<ControlFlow, Error> {
259260
match data {
260261
ReceivedData::Throughput(mut throughput) => {
261-
if throughput < MINIMUM_THROUGHPUT {
262+
if throughput < self.params.minimum_throughput {
262263
warn!(
263264
"Throughput {} kbps lower than minimum {}, setting to minimum",
264265
throughput / 1000,
265-
MINIMUM_THROUGHPUT / 1000,
266+
self.params.minimum_throughput / 1000,
266267
);
267-
throughput = MINIMUM_THROUGHPUT;
268+
throughput = self.params.minimum_throughput;
268269
}
269270

270271
let old_throughput = self.shared.throughput();
@@ -287,13 +288,13 @@ impl AudioFileFetch {
287288
self.shared.set_throughput(avg_throughput);
288289
}
289290
ReceivedData::ResponseTime(mut response_time) => {
290-
if response_time > MAXIMUM_ASSUMED_PING_TIME {
291+
if response_time > self.params.maximum_assumed_ping_time {
291292
warn!(
292293
"Time to first byte {} ms exceeds maximum {}, setting to maximum",
293294
response_time.as_millis(),
294-
MAXIMUM_ASSUMED_PING_TIME.as_millis()
295+
self.params.maximum_assumed_ping_time.as_millis()
295296
);
296-
response_time = MAXIMUM_ASSUMED_PING_TIME;
297+
response_time = self.params.maximum_assumed_ping_time;
297298
}
298299

299300
let old_ping_time_ms = self.shared.ping_time().as_millis();
@@ -423,6 +424,8 @@ pub(super) async fn audio_file_fetch(
423424
initial_request,
424425
));
425426

427+
let params = AudioFetchParams::get();
428+
426429
let mut fetch = AudioFileFetch {
427430
session: session.clone(),
428431
shared,
@@ -431,6 +434,8 @@ pub(super) async fn audio_file_fetch(
431434
file_data_tx,
432435
complete_tx: Some(complete_tx),
433436
network_response_times: Vec::with_capacity(3),
437+
438+
params: params.clone(),
434439
};
435440

436441
loop {
@@ -472,7 +477,7 @@ pub(super) async fn audio_file_fetch(
472477
let throughput = fetch.shared.throughput();
473478

474479
let desired_pending_bytes = max(
475-
(PREFETCH_THRESHOLD_FACTOR
480+
(params.prefetch_threshold_factor
476481
* ping_time_seconds
477482
* fetch.shared.bytes_per_second as f32) as usize,
478483
(ping_time_seconds * throughput as f32) as usize,

audio/src/lib.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,4 @@ mod fetch;
77
mod range_set;
88

99
pub use decrypt::AudioDecrypt;
10-
pub use fetch::{AudioFile, AudioFileError, StreamLoaderController};
11-
pub use fetch::{MINIMUM_DOWNLOAD_SIZE, READ_AHEAD_BEFORE_PLAYBACK, READ_AHEAD_DURING_PLAYBACK};
10+
pub use fetch::{AudioFetchParams, AudioFile, AudioFileError, StreamLoaderController};

playback/src/decoder/symphonia_decoder.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ impl SymphoniaDecoder {
3636
R: MediaSource + 'static,
3737
{
3838
let mss_opts = MediaSourceStreamOptions {
39-
buffer_len: librespot_audio::MINIMUM_DOWNLOAD_SIZE,
39+
buffer_len: librespot_audio::AudioFetchParams::get().minimum_download_size,
4040
};
4141
let mss = MediaSourceStream::new(Box::new(input), mss_opts);
4242

playback/src/player.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,7 @@ use symphonia::core::io::MediaSource;
2424
use tokio::sync::{mpsc, oneshot};
2525

2626
use crate::{
27-
audio::{
28-
AudioDecrypt, AudioFile, StreamLoaderController, READ_AHEAD_BEFORE_PLAYBACK,
29-
READ_AHEAD_DURING_PLAYBACK,
30-
},
27+
audio::{AudioDecrypt, AudioFetchParams, AudioFile, StreamLoaderController},
3128
audio_backend::Sink,
3229
config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig},
3330
convert::Converter,
@@ -2223,13 +2220,14 @@ impl PlayerInternal {
22232220
..
22242221
} = self.state
22252222
{
2223+
let read_ahead_during_playback = AudioFetchParams::get().read_ahead_during_playback;
22262224
// Request our read ahead range
22272225
let request_data_length =
2228-
(READ_AHEAD_DURING_PLAYBACK.as_secs_f32() * bytes_per_second as f32) as usize;
2226+
(read_ahead_during_playback.as_secs_f32() * bytes_per_second as f32) as usize;
22292227

22302228
// Request the part we want to wait for blocking. This effectively means we wait for the previous request to partially complete.
22312229
let wait_for_data_length =
2232-
(READ_AHEAD_BEFORE_PLAYBACK.as_secs_f32() * bytes_per_second as f32) as usize;
2230+
(read_ahead_during_playback.as_secs_f32() * bytes_per_second as f32) as usize;
22332231

22342232
stream_loader_controller
22352233
.fetch_next_and_wait(request_data_length, wait_for_data_length)

0 commit comments

Comments
 (0)