mirror of
https://github.com/librespot-org/librespot.git
synced 2024-12-18 17:11:53 +00:00
e175a88f5b
This change collects all those audio fetch parameters that were defined as static constants into a dedicated struct, AudioFetchParams. This struct can be read and set statically, allowing a user of the library to modify those parameters without the need to recompile.
2446 lines
87 KiB
Rust
2446 lines
87 KiB
Rust
use std::{
|
|
collections::HashMap,
|
|
fmt,
|
|
future::Future,
|
|
io::{self, Read, Seek, SeekFrom},
|
|
mem,
|
|
pin::Pin,
|
|
process::exit,
|
|
sync::{
|
|
atomic::{AtomicUsize, Ordering},
|
|
Arc,
|
|
},
|
|
task::{Context, Poll},
|
|
thread,
|
|
time::{Duration, Instant},
|
|
};
|
|
|
|
use futures_util::{
|
|
future, future::FusedFuture, stream::futures_unordered::FuturesUnordered, StreamExt,
|
|
TryFutureExt,
|
|
};
|
|
use parking_lot::Mutex;
|
|
use symphonia::core::io::MediaSource;
|
|
use tokio::sync::{mpsc, oneshot};
|
|
|
|
use crate::{
|
|
audio::{AudioDecrypt, AudioFetchParams, AudioFile, StreamLoaderController},
|
|
audio_backend::Sink,
|
|
config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig},
|
|
convert::Converter,
|
|
core::{util::SeqGenerator, Error, Session, SpotifyId},
|
|
decoder::{AudioDecoder, AudioPacket, AudioPacketPosition, SymphoniaDecoder},
|
|
metadata::audio::{AudioFileFormat, AudioFiles, AudioItem},
|
|
mixer::VolumeGetter,
|
|
};
|
|
|
|
#[cfg(feature = "passthrough-decoder")]
|
|
use crate::decoder::PassthroughDecoder;
|
|
|
|
use crate::SAMPLES_PER_SECOND;
|
|
|
|
const PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS: u32 = 30000;
|
|
pub const DB_VOLTAGE_RATIO: f64 = 20.0;
|
|
pub const PCM_AT_0DBFS: f64 = 1.0;
|
|
|
|
// Spotify inserts a custom Ogg packet at the start with custom metadata values, that you would
|
|
// otherwise expect in Vorbis comments. This packet isn't well-formed and players may balk at it.
|
|
const SPOTIFY_OGG_HEADER_END: u64 = 0xa7;
|
|
|
|
pub type PlayerResult = Result<(), Error>;
|
|
|
|
pub struct Player {
|
|
commands: Option<mpsc::UnboundedSender<PlayerCommand>>,
|
|
thread_handle: Option<thread::JoinHandle<()>>,
|
|
}
|
|
|
|
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
|
|
pub enum SinkStatus {
|
|
Running,
|
|
Closed,
|
|
TemporarilyClosed,
|
|
}
|
|
|
|
pub type SinkEventCallback = Box<dyn Fn(SinkStatus) + Send>;
|
|
|
|
struct PlayerInternal {
|
|
session: Session,
|
|
config: PlayerConfig,
|
|
commands: mpsc::UnboundedReceiver<PlayerCommand>,
|
|
load_handles: Arc<Mutex<HashMap<thread::ThreadId, thread::JoinHandle<()>>>>,
|
|
|
|
state: PlayerState,
|
|
preload: PlayerPreload,
|
|
sink: Box<dyn Sink>,
|
|
sink_status: SinkStatus,
|
|
sink_event_callback: Option<SinkEventCallback>,
|
|
volume_getter: Box<dyn VolumeGetter + Send>,
|
|
event_senders: Vec<mpsc::UnboundedSender<PlayerEvent>>,
|
|
converter: Converter,
|
|
|
|
normalisation_integrator: f64,
|
|
normalisation_peak: f64,
|
|
|
|
auto_normalise_as_album: bool,
|
|
|
|
player_id: usize,
|
|
play_request_id_generator: SeqGenerator<u64>,
|
|
}
|
|
|
|
static PLAYER_COUNTER: AtomicUsize = AtomicUsize::new(0);
|
|
|
|
enum PlayerCommand {
|
|
Load {
|
|
track_id: SpotifyId,
|
|
play: bool,
|
|
position_ms: u32,
|
|
},
|
|
Preload {
|
|
track_id: SpotifyId,
|
|
},
|
|
Play,
|
|
Pause,
|
|
Stop,
|
|
Seek(u32),
|
|
SetSession(Session),
|
|
AddEventSender(mpsc::UnboundedSender<PlayerEvent>),
|
|
SetSinkEventCallback(Option<SinkEventCallback>),
|
|
EmitVolumeChangedEvent(u16),
|
|
SetAutoNormaliseAsAlbum(bool),
|
|
EmitSessionDisconnectedEvent {
|
|
connection_id: String,
|
|
user_name: String,
|
|
},
|
|
EmitSessionConnectedEvent {
|
|
connection_id: String,
|
|
user_name: String,
|
|
},
|
|
EmitSessionClientChangedEvent {
|
|
client_id: String,
|
|
client_name: String,
|
|
client_brand_name: String,
|
|
client_model_name: String,
|
|
},
|
|
EmitFilterExplicitContentChangedEvent(bool),
|
|
EmitShuffleChangedEvent(bool),
|
|
EmitRepeatChangedEvent(bool),
|
|
EmitAutoPlayChangedEvent(bool),
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum PlayerEvent {
|
|
// Play request id changed
|
|
PlayRequestIdChanged {
|
|
play_request_id: u64,
|
|
},
|
|
// Fired when the player is stopped (e.g. by issuing a "stop" command to the player).
|
|
Stopped {
|
|
play_request_id: u64,
|
|
track_id: SpotifyId,
|
|
},
|
|
// The player is delayed by loading a track.
|
|
Loading {
|
|
play_request_id: u64,
|
|
track_id: SpotifyId,
|
|
position_ms: u32,
|
|
},
|
|
// The player is preloading a track.
|
|
Preloading {
|
|
track_id: SpotifyId,
|
|
},
|
|
// The player is playing a track.
|
|
// This event is issued at the start of playback of whenever the position must be communicated
|
|
// because it is out of sync. This includes:
|
|
// start of a track
|
|
// un-pausing
|
|
// after a seek
|
|
// after a buffer-underrun
|
|
Playing {
|
|
play_request_id: u64,
|
|
track_id: SpotifyId,
|
|
position_ms: u32,
|
|
},
|
|
// The player entered a paused state.
|
|
Paused {
|
|
play_request_id: u64,
|
|
track_id: SpotifyId,
|
|
position_ms: u32,
|
|
},
|
|
// The player thinks it's a good idea to issue a preload command for the next track now.
|
|
// This event is intended for use within spirc.
|
|
TimeToPreloadNextTrack {
|
|
play_request_id: u64,
|
|
track_id: SpotifyId,
|
|
},
|
|
// The player reached the end of a track.
|
|
// This event is intended for use within spirc. Spirc will respond by issuing another command.
|
|
EndOfTrack {
|
|
play_request_id: u64,
|
|
track_id: SpotifyId,
|
|
},
|
|
// The player was unable to load the requested track.
|
|
Unavailable {
|
|
play_request_id: u64,
|
|
track_id: SpotifyId,
|
|
},
|
|
// The mixer volume was set to a new level.
|
|
VolumeChanged {
|
|
volume: u16,
|
|
},
|
|
PositionCorrection {
|
|
play_request_id: u64,
|
|
track_id: SpotifyId,
|
|
position_ms: u32,
|
|
},
|
|
Seeked {
|
|
play_request_id: u64,
|
|
track_id: SpotifyId,
|
|
position_ms: u32,
|
|
},
|
|
TrackChanged {
|
|
audio_item: Box<AudioItem>,
|
|
},
|
|
SessionConnected {
|
|
connection_id: String,
|
|
user_name: String,
|
|
},
|
|
SessionDisconnected {
|
|
connection_id: String,
|
|
user_name: String,
|
|
},
|
|
SessionClientChanged {
|
|
client_id: String,
|
|
client_name: String,
|
|
client_brand_name: String,
|
|
client_model_name: String,
|
|
},
|
|
ShuffleChanged {
|
|
shuffle: bool,
|
|
},
|
|
RepeatChanged {
|
|
repeat: bool,
|
|
},
|
|
AutoPlayChanged {
|
|
auto_play: bool,
|
|
},
|
|
FilterExplicitContentChanged {
|
|
filter: bool,
|
|
},
|
|
}
|
|
|
|
impl PlayerEvent {
|
|
pub fn get_play_request_id(&self) -> Option<u64> {
|
|
use PlayerEvent::*;
|
|
match self {
|
|
Loading {
|
|
play_request_id, ..
|
|
}
|
|
| Unavailable {
|
|
play_request_id, ..
|
|
}
|
|
| Playing {
|
|
play_request_id, ..
|
|
}
|
|
| TimeToPreloadNextTrack {
|
|
play_request_id, ..
|
|
}
|
|
| EndOfTrack {
|
|
play_request_id, ..
|
|
}
|
|
| Paused {
|
|
play_request_id, ..
|
|
}
|
|
| Stopped {
|
|
play_request_id, ..
|
|
}
|
|
| PositionCorrection {
|
|
play_request_id, ..
|
|
}
|
|
| Seeked {
|
|
play_request_id, ..
|
|
} => Some(*play_request_id),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub type PlayerEventChannel = mpsc::UnboundedReceiver<PlayerEvent>;
|
|
|
|
pub fn db_to_ratio(db: f64) -> f64 {
|
|
f64::powf(10.0, db / DB_VOLTAGE_RATIO)
|
|
}
|
|
|
|
pub fn ratio_to_db(ratio: f64) -> f64 {
|
|
ratio.log10() * DB_VOLTAGE_RATIO
|
|
}
|
|
|
|
pub fn duration_to_coefficient(duration: Duration) -> f64 {
|
|
f64::exp(-1.0 / (duration.as_secs_f64() * SAMPLES_PER_SECOND as f64))
|
|
}
|
|
|
|
pub fn coefficient_to_duration(coefficient: f64) -> Duration {
|
|
Duration::from_secs_f64(-1.0 / f64::ln(coefficient) / SAMPLES_PER_SECOND as f64)
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub struct NormalisationData {
|
|
// Spotify provides these as `f32`, but audio metadata can contain up to `f64`.
|
|
// Also, this negates the need for casting during sample processing.
|
|
pub track_gain_db: f64,
|
|
pub track_peak: f64,
|
|
pub album_gain_db: f64,
|
|
pub album_peak: f64,
|
|
}
|
|
|
|
impl Default for NormalisationData {
|
|
fn default() -> Self {
|
|
Self {
|
|
track_gain_db: 0.0,
|
|
track_peak: 1.0,
|
|
album_gain_db: 0.0,
|
|
album_peak: 1.0,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl NormalisationData {
|
|
fn parse_from_ogg<T: Read + Seek>(mut file: T) -> io::Result<NormalisationData> {
|
|
const SPOTIFY_NORMALIZATION_HEADER_START_OFFSET: u64 = 144;
|
|
const NORMALISATION_DATA_SIZE: usize = 16;
|
|
|
|
let newpos = file.seek(SeekFrom::Start(SPOTIFY_NORMALIZATION_HEADER_START_OFFSET))?;
|
|
if newpos != SPOTIFY_NORMALIZATION_HEADER_START_OFFSET {
|
|
error!(
|
|
"NormalisationData::parse_from_file seeking to {} but position is now {}",
|
|
SPOTIFY_NORMALIZATION_HEADER_START_OFFSET, newpos
|
|
);
|
|
|
|
error!("Falling back to default (non-track and non-album) normalisation data.");
|
|
|
|
return Ok(NormalisationData::default());
|
|
}
|
|
|
|
let mut buf = [0u8; NORMALISATION_DATA_SIZE];
|
|
|
|
file.read_exact(&mut buf)?;
|
|
|
|
let track_gain_db = f32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]) as f64;
|
|
let track_peak = f32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]) as f64;
|
|
let album_gain_db = f32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]) as f64;
|
|
let album_peak = f32::from_le_bytes([buf[12], buf[13], buf[14], buf[15]]) as f64;
|
|
|
|
Ok(Self {
|
|
track_gain_db,
|
|
track_peak,
|
|
album_gain_db,
|
|
album_peak,
|
|
})
|
|
}
|
|
|
|
fn get_factor(config: &PlayerConfig, data: NormalisationData) -> f64 {
|
|
if !config.normalisation {
|
|
return 1.0;
|
|
}
|
|
|
|
let (gain_db, gain_peak) = if config.normalisation_type == NormalisationType::Album {
|
|
(data.album_gain_db, data.album_peak)
|
|
} else {
|
|
(data.track_gain_db, data.track_peak)
|
|
};
|
|
|
|
// As per the ReplayGain 1.0 & 2.0 (proposed) spec:
|
|
// https://wiki.hydrogenaud.io/index.php?title=ReplayGain_1.0_specification#Clipping_prevention
|
|
// https://wiki.hydrogenaud.io/index.php?title=ReplayGain_2.0_specification#Clipping_prevention
|
|
let normalisation_factor = if config.normalisation_method == NormalisationMethod::Basic {
|
|
// For Basic Normalisation, factor = min(ratio of (ReplayGain + PreGain), 1.0 / peak level).
|
|
// https://wiki.hydrogenaud.io/index.php?title=ReplayGain_1.0_specification#Peak_amplitude
|
|
// https://wiki.hydrogenaud.io/index.php?title=ReplayGain_2.0_specification#Peak_amplitude
|
|
// We then limit that to 1.0 as not to exceed dBFS (0.0 dB).
|
|
let factor = f64::min(
|
|
db_to_ratio(gain_db + config.normalisation_pregain_db),
|
|
PCM_AT_0DBFS / gain_peak,
|
|
);
|
|
|
|
if factor > PCM_AT_0DBFS {
|
|
info!(
|
|
"Lowering gain by {:.2} dB for the duration of this track to avoid potentially exceeding dBFS.",
|
|
ratio_to_db(factor)
|
|
);
|
|
|
|
PCM_AT_0DBFS
|
|
} else {
|
|
factor
|
|
}
|
|
} else {
|
|
// For Dynamic Normalisation it's up to the player to decide,
|
|
// factor = ratio of (ReplayGain + PreGain).
|
|
// We then let the dynamic limiter handle gain reduction.
|
|
let factor = db_to_ratio(gain_db + config.normalisation_pregain_db);
|
|
let threshold_ratio = db_to_ratio(config.normalisation_threshold_dbfs);
|
|
|
|
if factor > PCM_AT_0DBFS {
|
|
let factor_db = gain_db + config.normalisation_pregain_db;
|
|
let limiting_db = factor_db + config.normalisation_threshold_dbfs.abs();
|
|
|
|
warn!(
|
|
"This track may exceed dBFS by {:.2} dB and be subject to {:.2} dB of dynamic limiting at its peak.",
|
|
factor_db, limiting_db
|
|
);
|
|
} else if factor > threshold_ratio {
|
|
let limiting_db = gain_db
|
|
+ config.normalisation_pregain_db
|
|
+ config.normalisation_threshold_dbfs.abs();
|
|
|
|
info!(
|
|
"This track may be subject to {:.2} dB of dynamic limiting at its peak.",
|
|
limiting_db
|
|
);
|
|
}
|
|
|
|
factor
|
|
};
|
|
|
|
debug!("Normalisation Data: {:?}", data);
|
|
debug!(
|
|
"Calculated Normalisation Factor for {:?}: {:.2}%",
|
|
config.normalisation_type,
|
|
normalisation_factor * 100.0
|
|
);
|
|
|
|
normalisation_factor
|
|
}
|
|
}
|
|
|
|
impl Player {
|
|
pub fn new<F>(
|
|
config: PlayerConfig,
|
|
session: Session,
|
|
volume_getter: Box<dyn VolumeGetter + Send>,
|
|
sink_builder: F,
|
|
) -> Arc<Self>
|
|
where
|
|
F: FnOnce() -> Box<dyn Sink> + Send + 'static,
|
|
{
|
|
let (cmd_tx, cmd_rx) = mpsc::unbounded_channel();
|
|
|
|
if config.normalisation {
|
|
debug!("Normalisation Type: {:?}", config.normalisation_type);
|
|
debug!(
|
|
"Normalisation Pregain: {:.1} dB",
|
|
config.normalisation_pregain_db
|
|
);
|
|
debug!(
|
|
"Normalisation Threshold: {:.1} dBFS",
|
|
config.normalisation_threshold_dbfs
|
|
);
|
|
debug!("Normalisation Method: {:?}", config.normalisation_method);
|
|
|
|
if config.normalisation_method == NormalisationMethod::Dynamic {
|
|
// as_millis() has rounding errors (truncates)
|
|
debug!(
|
|
"Normalisation Attack: {:.0} ms",
|
|
coefficient_to_duration(config.normalisation_attack_cf).as_secs_f64() * 1000.
|
|
);
|
|
debug!(
|
|
"Normalisation Release: {:.0} ms",
|
|
coefficient_to_duration(config.normalisation_release_cf).as_secs_f64() * 1000.
|
|
);
|
|
debug!("Normalisation Knee: {} dB", config.normalisation_knee_db);
|
|
}
|
|
}
|
|
|
|
let handle = thread::spawn(move || {
|
|
let player_id = PLAYER_COUNTER.fetch_add(1, Ordering::AcqRel);
|
|
debug!("new Player [{}]", player_id);
|
|
|
|
let converter = Converter::new(config.ditherer);
|
|
|
|
let internal = PlayerInternal {
|
|
session,
|
|
config,
|
|
commands: cmd_rx,
|
|
load_handles: Arc::new(Mutex::new(HashMap::new())),
|
|
|
|
state: PlayerState::Stopped,
|
|
preload: PlayerPreload::None,
|
|
sink: sink_builder(),
|
|
sink_status: SinkStatus::Closed,
|
|
sink_event_callback: None,
|
|
volume_getter,
|
|
event_senders: vec![],
|
|
converter,
|
|
|
|
normalisation_peak: 0.0,
|
|
normalisation_integrator: 0.0,
|
|
|
|
auto_normalise_as_album: false,
|
|
|
|
player_id,
|
|
play_request_id_generator: SeqGenerator::new(0),
|
|
};
|
|
|
|
// While PlayerInternal is written as a future, it still contains blocking code.
|
|
// It must be run by using block_on() in a dedicated thread.
|
|
let runtime = tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime");
|
|
runtime.block_on(internal);
|
|
|
|
debug!("PlayerInternal thread finished.");
|
|
});
|
|
|
|
Arc::new(Self {
|
|
commands: Some(cmd_tx),
|
|
thread_handle: Some(handle),
|
|
})
|
|
}
|
|
|
|
pub fn is_invalid(&self) -> bool {
|
|
if let Some(handle) = self.thread_handle.as_ref() {
|
|
return handle.is_finished();
|
|
}
|
|
true
|
|
}
|
|
|
|
fn command(&self, cmd: PlayerCommand) {
|
|
if let Some(commands) = self.commands.as_ref() {
|
|
if let Err(e) = commands.send(cmd) {
|
|
error!("Player Commands Error: {}", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn load(&self, track_id: SpotifyId, start_playing: bool, position_ms: u32) {
|
|
self.command(PlayerCommand::Load {
|
|
track_id,
|
|
play: start_playing,
|
|
position_ms,
|
|
});
|
|
}
|
|
|
|
pub fn preload(&self, track_id: SpotifyId) {
|
|
self.command(PlayerCommand::Preload { track_id });
|
|
}
|
|
|
|
pub fn play(&self) {
|
|
self.command(PlayerCommand::Play)
|
|
}
|
|
|
|
pub fn pause(&self) {
|
|
self.command(PlayerCommand::Pause)
|
|
}
|
|
|
|
pub fn stop(&self) {
|
|
self.command(PlayerCommand::Stop)
|
|
}
|
|
|
|
pub fn seek(&self, position_ms: u32) {
|
|
self.command(PlayerCommand::Seek(position_ms));
|
|
}
|
|
|
|
pub fn set_session(&self, session: Session) {
|
|
self.command(PlayerCommand::SetSession(session));
|
|
}
|
|
|
|
pub fn get_player_event_channel(&self) -> PlayerEventChannel {
|
|
let (event_sender, event_receiver) = mpsc::unbounded_channel();
|
|
self.command(PlayerCommand::AddEventSender(event_sender));
|
|
event_receiver
|
|
}
|
|
|
|
pub async fn await_end_of_track(&self) {
|
|
let mut channel = self.get_player_event_channel();
|
|
while let Some(event) = channel.recv().await {
|
|
if matches!(
|
|
event,
|
|
PlayerEvent::EndOfTrack { .. } | PlayerEvent::Stopped { .. }
|
|
) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn set_sink_event_callback(&self, callback: Option<SinkEventCallback>) {
|
|
self.command(PlayerCommand::SetSinkEventCallback(callback));
|
|
}
|
|
|
|
pub fn emit_volume_changed_event(&self, volume: u16) {
|
|
self.command(PlayerCommand::EmitVolumeChangedEvent(volume));
|
|
}
|
|
|
|
pub fn set_auto_normalise_as_album(&self, setting: bool) {
|
|
self.command(PlayerCommand::SetAutoNormaliseAsAlbum(setting));
|
|
}
|
|
|
|
pub fn emit_filter_explicit_content_changed_event(&self, filter: bool) {
|
|
self.command(PlayerCommand::EmitFilterExplicitContentChangedEvent(filter));
|
|
}
|
|
|
|
pub fn emit_session_connected_event(&self, connection_id: String, user_name: String) {
|
|
self.command(PlayerCommand::EmitSessionConnectedEvent {
|
|
connection_id,
|
|
user_name,
|
|
});
|
|
}
|
|
|
|
pub fn emit_session_disconnected_event(&self, connection_id: String, user_name: String) {
|
|
self.command(PlayerCommand::EmitSessionDisconnectedEvent {
|
|
connection_id,
|
|
user_name,
|
|
});
|
|
}
|
|
|
|
pub fn emit_session_client_changed_event(
|
|
&self,
|
|
client_id: String,
|
|
client_name: String,
|
|
client_brand_name: String,
|
|
client_model_name: String,
|
|
) {
|
|
self.command(PlayerCommand::EmitSessionClientChangedEvent {
|
|
client_id,
|
|
client_name,
|
|
client_brand_name,
|
|
client_model_name,
|
|
});
|
|
}
|
|
|
|
pub fn emit_shuffle_changed_event(&self, shuffle: bool) {
|
|
self.command(PlayerCommand::EmitShuffleChangedEvent(shuffle));
|
|
}
|
|
|
|
pub fn emit_repeat_changed_event(&self, repeat: bool) {
|
|
self.command(PlayerCommand::EmitRepeatChangedEvent(repeat));
|
|
}
|
|
|
|
pub fn emit_auto_play_changed_event(&self, auto_play: bool) {
|
|
self.command(PlayerCommand::EmitAutoPlayChangedEvent(auto_play));
|
|
}
|
|
}
|
|
|
|
impl Drop for Player {
|
|
fn drop(&mut self) {
|
|
debug!("Shutting down player thread ...");
|
|
self.commands = None;
|
|
if let Some(handle) = self.thread_handle.take() {
|
|
if let Err(e) = handle.join() {
|
|
error!("Player thread Error: {:?}", e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct PlayerLoadedTrackData {
|
|
decoder: Decoder,
|
|
normalisation_data: NormalisationData,
|
|
stream_loader_controller: StreamLoaderController,
|
|
audio_item: AudioItem,
|
|
bytes_per_second: usize,
|
|
duration_ms: u32,
|
|
stream_position_ms: u32,
|
|
is_explicit: bool,
|
|
}
|
|
|
|
enum PlayerPreload {
|
|
None,
|
|
Loading {
|
|
track_id: SpotifyId,
|
|
loader: Pin<Box<dyn FusedFuture<Output = Result<PlayerLoadedTrackData, ()>> + Send>>,
|
|
},
|
|
Ready {
|
|
track_id: SpotifyId,
|
|
loaded_track: Box<PlayerLoadedTrackData>,
|
|
},
|
|
}
|
|
|
|
type Decoder = Box<dyn AudioDecoder + Send>;
|
|
|
|
enum PlayerState {
|
|
Stopped,
|
|
Loading {
|
|
track_id: SpotifyId,
|
|
play_request_id: u64,
|
|
start_playback: bool,
|
|
loader: Pin<Box<dyn FusedFuture<Output = Result<PlayerLoadedTrackData, ()>> + Send>>,
|
|
},
|
|
Paused {
|
|
track_id: SpotifyId,
|
|
play_request_id: u64,
|
|
decoder: Decoder,
|
|
audio_item: AudioItem,
|
|
normalisation_data: NormalisationData,
|
|
normalisation_factor: f64,
|
|
stream_loader_controller: StreamLoaderController,
|
|
bytes_per_second: usize,
|
|
duration_ms: u32,
|
|
stream_position_ms: u32,
|
|
suggested_to_preload_next_track: bool,
|
|
is_explicit: bool,
|
|
},
|
|
Playing {
|
|
track_id: SpotifyId,
|
|
play_request_id: u64,
|
|
decoder: Decoder,
|
|
normalisation_data: NormalisationData,
|
|
audio_item: AudioItem,
|
|
normalisation_factor: f64,
|
|
stream_loader_controller: StreamLoaderController,
|
|
bytes_per_second: usize,
|
|
duration_ms: u32,
|
|
stream_position_ms: u32,
|
|
reported_nominal_start_time: Option<Instant>,
|
|
suggested_to_preload_next_track: bool,
|
|
is_explicit: bool,
|
|
},
|
|
EndOfTrack {
|
|
track_id: SpotifyId,
|
|
play_request_id: u64,
|
|
loaded_track: PlayerLoadedTrackData,
|
|
},
|
|
Invalid,
|
|
}
|
|
|
|
impl PlayerState {
|
|
fn is_playing(&self) -> bool {
|
|
use self::PlayerState::*;
|
|
match *self {
|
|
Stopped | EndOfTrack { .. } | Paused { .. } | Loading { .. } => false,
|
|
Playing { .. } => true,
|
|
Invalid => {
|
|
error!("PlayerState::is_playing in invalid state");
|
|
exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
fn is_stopped(&self) -> bool {
|
|
use self::PlayerState::*;
|
|
matches!(self, Stopped)
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
fn is_loading(&self) -> bool {
|
|
use self::PlayerState::*;
|
|
matches!(self, Loading { .. })
|
|
}
|
|
|
|
fn decoder(&mut self) -> Option<&mut Decoder> {
|
|
use self::PlayerState::*;
|
|
match *self {
|
|
Stopped | EndOfTrack { .. } | Loading { .. } => None,
|
|
Paused {
|
|
ref mut decoder, ..
|
|
}
|
|
| Playing {
|
|
ref mut decoder, ..
|
|
} => Some(decoder),
|
|
Invalid => {
|
|
error!("PlayerState::decoder in invalid state");
|
|
exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn playing_to_end_of_track(&mut self) {
|
|
use self::PlayerState::*;
|
|
let new_state = mem::replace(self, Invalid);
|
|
match new_state {
|
|
Playing {
|
|
track_id,
|
|
play_request_id,
|
|
decoder,
|
|
duration_ms,
|
|
bytes_per_second,
|
|
normalisation_data,
|
|
stream_loader_controller,
|
|
stream_position_ms,
|
|
is_explicit,
|
|
audio_item,
|
|
..
|
|
} => {
|
|
*self = EndOfTrack {
|
|
track_id,
|
|
play_request_id,
|
|
loaded_track: PlayerLoadedTrackData {
|
|
decoder,
|
|
normalisation_data,
|
|
stream_loader_controller,
|
|
audio_item,
|
|
bytes_per_second,
|
|
duration_ms,
|
|
stream_position_ms,
|
|
is_explicit,
|
|
},
|
|
};
|
|
}
|
|
_ => {
|
|
error!(
|
|
"Called playing_to_end_of_track in non-playing state: {:?}",
|
|
new_state
|
|
);
|
|
exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn paused_to_playing(&mut self) {
|
|
use self::PlayerState::*;
|
|
let new_state = mem::replace(self, Invalid);
|
|
match new_state {
|
|
Paused {
|
|
track_id,
|
|
play_request_id,
|
|
decoder,
|
|
audio_item,
|
|
normalisation_data,
|
|
normalisation_factor,
|
|
stream_loader_controller,
|
|
duration_ms,
|
|
bytes_per_second,
|
|
stream_position_ms,
|
|
suggested_to_preload_next_track,
|
|
is_explicit,
|
|
} => {
|
|
*self = Playing {
|
|
track_id,
|
|
play_request_id,
|
|
decoder,
|
|
audio_item,
|
|
normalisation_data,
|
|
normalisation_factor,
|
|
stream_loader_controller,
|
|
duration_ms,
|
|
bytes_per_second,
|
|
stream_position_ms,
|
|
reported_nominal_start_time: Instant::now()
|
|
.checked_sub(Duration::from_millis(stream_position_ms as u64)),
|
|
suggested_to_preload_next_track,
|
|
is_explicit,
|
|
};
|
|
}
|
|
_ => {
|
|
error!(
|
|
"PlayerState::paused_to_playing in invalid state: {:?}",
|
|
new_state
|
|
);
|
|
exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn playing_to_paused(&mut self) {
|
|
use self::PlayerState::*;
|
|
let new_state = mem::replace(self, Invalid);
|
|
match new_state {
|
|
Playing {
|
|
track_id,
|
|
play_request_id,
|
|
decoder,
|
|
audio_item,
|
|
normalisation_data,
|
|
normalisation_factor,
|
|
stream_loader_controller,
|
|
duration_ms,
|
|
bytes_per_second,
|
|
stream_position_ms,
|
|
suggested_to_preload_next_track,
|
|
is_explicit,
|
|
..
|
|
} => {
|
|
*self = Paused {
|
|
track_id,
|
|
play_request_id,
|
|
decoder,
|
|
audio_item,
|
|
normalisation_data,
|
|
normalisation_factor,
|
|
stream_loader_controller,
|
|
duration_ms,
|
|
bytes_per_second,
|
|
stream_position_ms,
|
|
suggested_to_preload_next_track,
|
|
is_explicit,
|
|
};
|
|
}
|
|
_ => {
|
|
error!(
|
|
"PlayerState::playing_to_paused in invalid state: {:?}",
|
|
new_state
|
|
);
|
|
exit(1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct PlayerTrackLoader {
|
|
session: Session,
|
|
config: PlayerConfig,
|
|
}
|
|
|
|
impl PlayerTrackLoader {
|
|
async fn find_available_alternative(&self, audio_item: AudioItem) -> Option<AudioItem> {
|
|
if let Err(e) = audio_item.availability {
|
|
error!("Track is unavailable: {}", e);
|
|
None
|
|
} else if !audio_item.files.is_empty() {
|
|
Some(audio_item)
|
|
} else if let Some(alternatives) = &audio_item.alternatives {
|
|
let alternatives: FuturesUnordered<_> = alternatives
|
|
.iter()
|
|
.map(|alt_id| AudioItem::get_file(&self.session, *alt_id))
|
|
.collect();
|
|
|
|
alternatives
|
|
.filter_map(|x| future::ready(x.ok()))
|
|
.filter(|x| future::ready(x.availability.is_ok()))
|
|
.next()
|
|
.await
|
|
} else {
|
|
error!("Track should be available, but no alternatives found.");
|
|
None
|
|
}
|
|
}
|
|
|
|
fn stream_data_rate(&self, format: AudioFileFormat) -> usize {
|
|
let kbps = match format {
|
|
AudioFileFormat::OGG_VORBIS_96 => 12,
|
|
AudioFileFormat::OGG_VORBIS_160 => 20,
|
|
AudioFileFormat::OGG_VORBIS_320 => 40,
|
|
AudioFileFormat::MP3_256 => 32,
|
|
AudioFileFormat::MP3_320 => 40,
|
|
AudioFileFormat::MP3_160 => 20,
|
|
AudioFileFormat::MP3_96 => 12,
|
|
AudioFileFormat::MP3_160_ENC => 20,
|
|
AudioFileFormat::AAC_24 => 3,
|
|
AudioFileFormat::AAC_48 => 6,
|
|
AudioFileFormat::FLAC_FLAC => 112, // assume 900 kbit/s on average
|
|
};
|
|
kbps * 1024
|
|
}
|
|
|
|
async fn load_track(
|
|
&self,
|
|
spotify_id: SpotifyId,
|
|
position_ms: u32,
|
|
) -> Option<PlayerLoadedTrackData> {
|
|
let audio_item = match AudioItem::get_file(&self.session, spotify_id).await {
|
|
Ok(audio) => match self.find_available_alternative(audio).await {
|
|
Some(audio) => audio,
|
|
None => {
|
|
warn!(
|
|
"<{}> is not available",
|
|
spotify_id.to_uri().unwrap_or_default()
|
|
);
|
|
return None;
|
|
}
|
|
},
|
|
Err(e) => {
|
|
error!("Unable to load audio item: {:?}", e);
|
|
return None;
|
|
}
|
|
};
|
|
|
|
info!(
|
|
"Loading <{}> with Spotify URI <{}>",
|
|
audio_item.name, audio_item.uri
|
|
);
|
|
|
|
// (Most) podcasts seem to support only 96 kbps Ogg Vorbis, so fall back to it
|
|
let formats = match self.config.bitrate {
|
|
Bitrate::Bitrate96 => [
|
|
AudioFileFormat::OGG_VORBIS_96,
|
|
AudioFileFormat::MP3_96,
|
|
AudioFileFormat::OGG_VORBIS_160,
|
|
AudioFileFormat::MP3_160,
|
|
AudioFileFormat::MP3_256,
|
|
AudioFileFormat::OGG_VORBIS_320,
|
|
AudioFileFormat::MP3_320,
|
|
],
|
|
Bitrate::Bitrate160 => [
|
|
AudioFileFormat::OGG_VORBIS_160,
|
|
AudioFileFormat::MP3_160,
|
|
AudioFileFormat::OGG_VORBIS_96,
|
|
AudioFileFormat::MP3_96,
|
|
AudioFileFormat::MP3_256,
|
|
AudioFileFormat::OGG_VORBIS_320,
|
|
AudioFileFormat::MP3_320,
|
|
],
|
|
Bitrate::Bitrate320 => [
|
|
AudioFileFormat::OGG_VORBIS_320,
|
|
AudioFileFormat::MP3_320,
|
|
AudioFileFormat::MP3_256,
|
|
AudioFileFormat::OGG_VORBIS_160,
|
|
AudioFileFormat::MP3_160,
|
|
AudioFileFormat::OGG_VORBIS_96,
|
|
AudioFileFormat::MP3_96,
|
|
],
|
|
};
|
|
|
|
let (format, file_id) =
|
|
match formats
|
|
.iter()
|
|
.find_map(|format| match audio_item.files.get(format) {
|
|
Some(&file_id) => Some((*format, file_id)),
|
|
_ => None,
|
|
}) {
|
|
Some(t) => t,
|
|
None => {
|
|
warn!(
|
|
"<{}> is not available in any supported format",
|
|
audio_item.name
|
|
);
|
|
return None;
|
|
}
|
|
};
|
|
|
|
let bytes_per_second = self.stream_data_rate(format);
|
|
|
|
// This is only a loop to be able to reload the file if an error occurred
|
|
// while opening a cached file.
|
|
loop {
|
|
let encrypted_file = AudioFile::open(&self.session, file_id, bytes_per_second);
|
|
|
|
let encrypted_file = match encrypted_file.await {
|
|
Ok(encrypted_file) => encrypted_file,
|
|
Err(e) => {
|
|
error!("Unable to load encrypted file: {:?}", e);
|
|
return None;
|
|
}
|
|
};
|
|
|
|
let is_cached = encrypted_file.is_cached();
|
|
|
|
let stream_loader_controller = encrypted_file.get_stream_loader_controller().ok()?;
|
|
|
|
// Not all audio files are encrypted. If we can't get a key, try loading the track
|
|
// without decryption. If the file was encrypted after all, the decoder will fail
|
|
// parsing and bail out, so we should be safe from outputting ear-piercing noise.
|
|
let key = match self.session.audio_key().request(spotify_id, file_id).await {
|
|
Ok(key) => Some(key),
|
|
Err(e) => {
|
|
warn!("Unable to load key, continuing without decryption: {}", e);
|
|
None
|
|
}
|
|
};
|
|
let mut decrypted_file = AudioDecrypt::new(key, encrypted_file);
|
|
|
|
let is_ogg_vorbis = AudioFiles::is_ogg_vorbis(format);
|
|
let (offset, mut normalisation_data) = if is_ogg_vorbis {
|
|
// Spotify stores normalisation data in a custom Ogg packet instead of Vorbis comments.
|
|
let normalisation_data =
|
|
NormalisationData::parse_from_ogg(&mut decrypted_file).ok();
|
|
(SPOTIFY_OGG_HEADER_END, normalisation_data)
|
|
} else {
|
|
(0, None)
|
|
};
|
|
|
|
let audio_file = match Subfile::new(
|
|
decrypted_file,
|
|
offset,
|
|
stream_loader_controller.len() as u64,
|
|
) {
|
|
Ok(audio_file) => audio_file,
|
|
Err(e) => {
|
|
error!("PlayerTrackLoader::load_track error opening subfile: {}", e);
|
|
return None;
|
|
}
|
|
};
|
|
|
|
let mut symphonia_decoder = |audio_file, format| {
|
|
SymphoniaDecoder::new(audio_file, format).map(|mut decoder| {
|
|
// For formats other that Vorbis, we'll try getting normalisation data from
|
|
// ReplayGain metadata fields, if present.
|
|
if normalisation_data.is_none() {
|
|
normalisation_data = decoder.normalisation_data();
|
|
}
|
|
Box::new(decoder) as Decoder
|
|
})
|
|
};
|
|
|
|
#[cfg(feature = "passthrough-decoder")]
|
|
let decoder_type = if self.config.passthrough {
|
|
PassthroughDecoder::new(audio_file, format).map(|x| Box::new(x) as Decoder)
|
|
} else {
|
|
symphonia_decoder(audio_file, format)
|
|
};
|
|
|
|
#[cfg(not(feature = "passthrough-decoder"))]
|
|
let decoder_type = symphonia_decoder(audio_file, format);
|
|
|
|
let normalisation_data = normalisation_data.unwrap_or_else(|| {
|
|
warn!("Unable to get normalisation data, continuing with defaults.");
|
|
NormalisationData::default()
|
|
});
|
|
|
|
let mut decoder = match decoder_type {
|
|
Ok(decoder) => decoder,
|
|
Err(e) if is_cached => {
|
|
warn!(
|
|
"Unable to read cached audio file: {}. Trying to download it.",
|
|
e
|
|
);
|
|
|
|
match self.session.cache() {
|
|
Some(cache) => {
|
|
if cache.remove_file(file_id).is_err() {
|
|
error!("Error removing file from cache");
|
|
return None;
|
|
}
|
|
}
|
|
None => {
|
|
error!("If the audio file is cached, a cache should exist");
|
|
return None;
|
|
}
|
|
}
|
|
|
|
// Just try it again
|
|
continue;
|
|
}
|
|
Err(e) => {
|
|
error!("Unable to read audio file: {}", e);
|
|
return None;
|
|
}
|
|
};
|
|
|
|
let duration_ms = audio_item.duration_ms;
|
|
// Don't try to seek past the track's duration.
|
|
// If the position is invalid just start from
|
|
// the beginning of the track.
|
|
let position_ms = if position_ms > duration_ms {
|
|
warn!("Invalid start position of {} ms exceeds track's duration of {} ms, starting track from the beginning", position_ms, duration_ms);
|
|
0
|
|
} else {
|
|
position_ms
|
|
};
|
|
|
|
// Ensure the starting position. Even when we want to play from the beginning,
|
|
// the cursor may have been moved by parsing normalisation data. This may not
|
|
// matter for playback (but won't hurt either), but may be useful for the
|
|
// passthrough decoder.
|
|
let stream_position_ms = match decoder.seek(position_ms) {
|
|
Ok(new_position_ms) => new_position_ms,
|
|
Err(e) => {
|
|
error!(
|
|
"PlayerTrackLoader::load_track error seeking to starting position {}: {}",
|
|
position_ms, e
|
|
);
|
|
return None;
|
|
}
|
|
};
|
|
|
|
// Ensure streaming mode now that we are ready to play from the requested position.
|
|
stream_loader_controller.set_stream_mode();
|
|
|
|
let is_explicit = audio_item.is_explicit;
|
|
|
|
info!("<{}> ({} ms) loaded", audio_item.name, duration_ms);
|
|
|
|
return Some(PlayerLoadedTrackData {
|
|
decoder,
|
|
normalisation_data,
|
|
stream_loader_controller,
|
|
audio_item,
|
|
bytes_per_second,
|
|
duration_ms,
|
|
stream_position_ms,
|
|
is_explicit,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Future for PlayerInternal {
|
|
type Output = ();
|
|
|
|
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
|
|
// While this is written as a future, it still contains blocking code.
|
|
// It must be run on its own thread.
|
|
let passthrough = self.config.passthrough;
|
|
|
|
loop {
|
|
let mut all_futures_completed_or_not_ready = true;
|
|
|
|
// process commands that were sent to us
|
|
let cmd = match self.commands.poll_recv(cx) {
|
|
Poll::Ready(None) => return Poll::Ready(()), // client has disconnected - shut down.
|
|
Poll::Ready(Some(cmd)) => {
|
|
all_futures_completed_or_not_ready = false;
|
|
Some(cmd)
|
|
}
|
|
_ => None,
|
|
};
|
|
|
|
if let Some(cmd) = cmd {
|
|
if let Err(e) = self.handle_command(cmd) {
|
|
error!("Error handling command: {}", e);
|
|
}
|
|
}
|
|
|
|
// Handle loading of a new track to play
|
|
if let PlayerState::Loading {
|
|
ref mut loader,
|
|
track_id,
|
|
start_playback,
|
|
play_request_id,
|
|
} = self.state
|
|
{
|
|
// The loader may be terminated if we are trying to load the same track
|
|
// as before, and that track failed to open before.
|
|
if !loader.as_mut().is_terminated() {
|
|
match loader.as_mut().poll(cx) {
|
|
Poll::Ready(Ok(loaded_track)) => {
|
|
self.start_playback(
|
|
track_id,
|
|
play_request_id,
|
|
loaded_track,
|
|
start_playback,
|
|
);
|
|
if let PlayerState::Loading { .. } = self.state {
|
|
error!("The state wasn't changed by start_playback()");
|
|
exit(1);
|
|
}
|
|
}
|
|
Poll::Ready(Err(e)) => {
|
|
error!(
|
|
"Skipping to next track, unable to load track <{:?}>: {:?}",
|
|
track_id, e
|
|
);
|
|
self.send_event(PlayerEvent::Unavailable {
|
|
track_id,
|
|
play_request_id,
|
|
})
|
|
}
|
|
Poll::Pending => (),
|
|
}
|
|
}
|
|
}
|
|
|
|
// handle pending preload requests.
|
|
if let PlayerPreload::Loading {
|
|
ref mut loader,
|
|
track_id,
|
|
} = self.preload
|
|
{
|
|
match loader.as_mut().poll(cx) {
|
|
Poll::Ready(Ok(loaded_track)) => {
|
|
self.send_event(PlayerEvent::Preloading { track_id });
|
|
self.preload = PlayerPreload::Ready {
|
|
track_id,
|
|
loaded_track: Box::new(loaded_track),
|
|
};
|
|
}
|
|
Poll::Ready(Err(_)) => {
|
|
debug!("Unable to preload {:?}", track_id);
|
|
self.preload = PlayerPreload::None;
|
|
// Let Spirc know that the track was unavailable.
|
|
if let PlayerState::Playing {
|
|
play_request_id, ..
|
|
}
|
|
| PlayerState::Paused {
|
|
play_request_id, ..
|
|
} = self.state
|
|
{
|
|
self.send_event(PlayerEvent::Unavailable {
|
|
track_id,
|
|
play_request_id,
|
|
});
|
|
}
|
|
}
|
|
Poll::Pending => (),
|
|
}
|
|
}
|
|
|
|
if self.state.is_playing() {
|
|
self.ensure_sink_running();
|
|
|
|
if let PlayerState::Playing {
|
|
track_id,
|
|
play_request_id,
|
|
ref mut decoder,
|
|
normalisation_factor,
|
|
ref mut stream_position_ms,
|
|
ref mut reported_nominal_start_time,
|
|
..
|
|
} = self.state
|
|
{
|
|
match decoder.next_packet() {
|
|
Ok(result) => {
|
|
if let Some((ref packet_position, ref packet)) = result {
|
|
let new_stream_position_ms = packet_position.position_ms;
|
|
let expected_position_ms = std::mem::replace(
|
|
&mut *stream_position_ms,
|
|
new_stream_position_ms,
|
|
);
|
|
|
|
if !passthrough {
|
|
match packet.samples() {
|
|
Ok(_) => {
|
|
let new_stream_position = Duration::from_millis(
|
|
new_stream_position_ms as u64,
|
|
);
|
|
|
|
let now = Instant::now();
|
|
|
|
// Only notify if we're skipped some packets *or* we are behind.
|
|
// If we're ahead it's probably due to a buffer of the backend
|
|
// and we're actually in time.
|
|
let notify_about_position =
|
|
match *reported_nominal_start_time {
|
|
None => true,
|
|
Some(reported_nominal_start_time) => {
|
|
let mut notify = false;
|
|
|
|
if packet_position.skipped {
|
|
if let Some(ahead) = new_stream_position
|
|
.checked_sub(Duration::from_millis(
|
|
expected_position_ms as u64,
|
|
))
|
|
{
|
|
notify |=
|
|
ahead >= Duration::from_secs(1)
|
|
}
|
|
}
|
|
|
|
if let Some(lag) = now
|
|
.checked_duration_since(
|
|
reported_nominal_start_time,
|
|
)
|
|
{
|
|
if let Some(lag) =
|
|
lag.checked_sub(new_stream_position)
|
|
{
|
|
notify |=
|
|
lag >= Duration::from_secs(1)
|
|
}
|
|
}
|
|
|
|
notify
|
|
}
|
|
};
|
|
|
|
if notify_about_position {
|
|
*reported_nominal_start_time =
|
|
now.checked_sub(new_stream_position);
|
|
self.send_event(PlayerEvent::PositionCorrection {
|
|
play_request_id,
|
|
track_id,
|
|
position_ms: new_stream_position_ms,
|
|
});
|
|
}
|
|
}
|
|
Err(e) => {
|
|
error!("Skipping to next track, unable to decode samples for track <{:?}>: {:?}", track_id, e);
|
|
self.send_event(PlayerEvent::EndOfTrack {
|
|
track_id,
|
|
play_request_id,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
self.handle_packet(result, normalisation_factor);
|
|
}
|
|
Err(e) => {
|
|
error!("Skipping to next track, unable to get next packet for track <{:?}>: {:?}", track_id, e);
|
|
self.send_event(PlayerEvent::EndOfTrack {
|
|
track_id,
|
|
play_request_id,
|
|
})
|
|
}
|
|
}
|
|
} else {
|
|
error!("PlayerInternal poll: Invalid PlayerState");
|
|
exit(1);
|
|
};
|
|
}
|
|
|
|
if let PlayerState::Playing {
|
|
track_id,
|
|
play_request_id,
|
|
duration_ms,
|
|
stream_position_ms,
|
|
ref mut stream_loader_controller,
|
|
ref mut suggested_to_preload_next_track,
|
|
..
|
|
}
|
|
| PlayerState::Paused {
|
|
track_id,
|
|
play_request_id,
|
|
duration_ms,
|
|
stream_position_ms,
|
|
ref mut stream_loader_controller,
|
|
ref mut suggested_to_preload_next_track,
|
|
..
|
|
} = self.state
|
|
{
|
|
if (!*suggested_to_preload_next_track)
|
|
&& ((duration_ms as i64 - stream_position_ms as i64)
|
|
< PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS as i64)
|
|
&& stream_loader_controller.range_to_end_available()
|
|
{
|
|
*suggested_to_preload_next_track = true;
|
|
self.send_event(PlayerEvent::TimeToPreloadNextTrack {
|
|
track_id,
|
|
play_request_id,
|
|
});
|
|
}
|
|
}
|
|
|
|
if (!self.state.is_playing()) && all_futures_completed_or_not_ready {
|
|
return Poll::Pending;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl PlayerInternal {
|
|
fn ensure_sink_running(&mut self) {
|
|
if self.sink_status != SinkStatus::Running {
|
|
trace!("== Starting sink ==");
|
|
if let Some(callback) = &mut self.sink_event_callback {
|
|
callback(SinkStatus::Running);
|
|
}
|
|
match self.sink.start() {
|
|
Ok(()) => self.sink_status = SinkStatus::Running,
|
|
Err(e) => {
|
|
error!("{}", e);
|
|
self.handle_pause();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn ensure_sink_stopped(&mut self, temporarily: bool) {
|
|
match self.sink_status {
|
|
SinkStatus::Running => {
|
|
trace!("== Stopping sink ==");
|
|
match self.sink.stop() {
|
|
Ok(()) => {
|
|
self.sink_status = if temporarily {
|
|
SinkStatus::TemporarilyClosed
|
|
} else {
|
|
SinkStatus::Closed
|
|
};
|
|
if let Some(callback) = &mut self.sink_event_callback {
|
|
callback(self.sink_status);
|
|
}
|
|
}
|
|
Err(e) => {
|
|
error!("{}", e);
|
|
exit(1);
|
|
}
|
|
}
|
|
}
|
|
SinkStatus::TemporarilyClosed => {
|
|
if !temporarily {
|
|
self.sink_status = SinkStatus::Closed;
|
|
if let Some(callback) = &mut self.sink_event_callback {
|
|
callback(SinkStatus::Closed);
|
|
}
|
|
}
|
|
}
|
|
SinkStatus::Closed => (),
|
|
}
|
|
}
|
|
|
|
fn handle_player_stop(&mut self) {
|
|
match self.state {
|
|
PlayerState::Playing {
|
|
track_id,
|
|
play_request_id,
|
|
..
|
|
}
|
|
| PlayerState::Paused {
|
|
track_id,
|
|
play_request_id,
|
|
..
|
|
}
|
|
| PlayerState::EndOfTrack {
|
|
track_id,
|
|
play_request_id,
|
|
..
|
|
}
|
|
| PlayerState::Loading {
|
|
track_id,
|
|
play_request_id,
|
|
..
|
|
} => {
|
|
self.ensure_sink_stopped(false);
|
|
self.send_event(PlayerEvent::Stopped {
|
|
track_id,
|
|
play_request_id,
|
|
});
|
|
self.state = PlayerState::Stopped;
|
|
}
|
|
PlayerState::Stopped => (),
|
|
PlayerState::Invalid => {
|
|
error!("PlayerInternal::handle_player_stop in invalid state");
|
|
exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn handle_play(&mut self) {
|
|
match self.state {
|
|
PlayerState::Paused {
|
|
track_id,
|
|
play_request_id,
|
|
stream_position_ms,
|
|
..
|
|
} => {
|
|
self.state.paused_to_playing();
|
|
self.send_event(PlayerEvent::Playing {
|
|
track_id,
|
|
play_request_id,
|
|
position_ms: stream_position_ms,
|
|
});
|
|
self.ensure_sink_running();
|
|
}
|
|
PlayerState::Loading {
|
|
ref mut start_playback,
|
|
..
|
|
} => {
|
|
*start_playback = true;
|
|
}
|
|
_ => error!("Player::play called from invalid state: {:?}", self.state),
|
|
}
|
|
}
|
|
|
|
fn handle_pause(&mut self) {
|
|
match self.state {
|
|
PlayerState::Paused { .. } => self.ensure_sink_stopped(false),
|
|
PlayerState::Playing {
|
|
track_id,
|
|
play_request_id,
|
|
stream_position_ms,
|
|
..
|
|
} => {
|
|
self.state.playing_to_paused();
|
|
|
|
self.ensure_sink_stopped(false);
|
|
self.send_event(PlayerEvent::Paused {
|
|
track_id,
|
|
play_request_id,
|
|
position_ms: stream_position_ms,
|
|
});
|
|
}
|
|
PlayerState::Loading {
|
|
ref mut start_playback,
|
|
..
|
|
} => {
|
|
*start_playback = false;
|
|
}
|
|
_ => error!("Player::pause called from invalid state: {:?}", self.state),
|
|
}
|
|
}
|
|
|
|
fn handle_packet(
|
|
&mut self,
|
|
packet: Option<(AudioPacketPosition, AudioPacket)>,
|
|
normalisation_factor: f64,
|
|
) {
|
|
match packet {
|
|
Some((_, mut packet)) => {
|
|
if !packet.is_empty() {
|
|
if let AudioPacket::Samples(ref mut data) = packet {
|
|
// Get the volume for the packet.
|
|
// In the case of hardware volume control this will
|
|
// always be 1.0 (no change).
|
|
let volume = self.volume_getter.attenuation_factor();
|
|
|
|
// For the basic normalisation method, a normalisation factor of 1.0 indicates that
|
|
// there is nothing to normalise (all samples should pass unaltered). For the
|
|
// dynamic method, there may still be peaks that we want to shave off.
|
|
|
|
// No matter the case we apply volume attenuation last if there is any.
|
|
if !self.config.normalisation {
|
|
if volume < 1.0 {
|
|
for sample in data.iter_mut() {
|
|
*sample *= volume;
|
|
}
|
|
}
|
|
} else if self.config.normalisation_method == NormalisationMethod::Basic
|
|
&& (normalisation_factor < 1.0 || volume < 1.0)
|
|
{
|
|
for sample in data.iter_mut() {
|
|
*sample *= normalisation_factor * volume;
|
|
}
|
|
} else if self.config.normalisation_method == NormalisationMethod::Dynamic {
|
|
// zero-cost shorthands
|
|
let threshold_db = self.config.normalisation_threshold_dbfs;
|
|
let knee_db = self.config.normalisation_knee_db;
|
|
let attack_cf = self.config.normalisation_attack_cf;
|
|
let release_cf = self.config.normalisation_release_cf;
|
|
|
|
for sample in data.iter_mut() {
|
|
*sample *= normalisation_factor;
|
|
|
|
// Feedforward limiter in the log domain
|
|
// After: Giannoulis, D., Massberg, M., & Reiss, J.D. (2012). Digital Dynamic
|
|
// Range Compressor Design—A Tutorial and Analysis. Journal of The Audio
|
|
// Engineering Society, 60, 399-408.
|
|
|
|
// Some tracks have samples that are precisely 0.0. That's silence
|
|
// and we know we don't need to limit that, in which we can spare
|
|
// the CPU cycles.
|
|
//
|
|
// Also, calling `ratio_to_db(0.0)` returns `inf` and would get the
|
|
// peak detector stuck. Also catch the unlikely case where a sample
|
|
// is decoded as `NaN` or some other non-normal value.
|
|
let limiter_db = if sample.is_normal() {
|
|
// step 1-4: half-wave rectification and conversion into dB
|
|
// and gain computer with soft knee and subtractor
|
|
let bias_db = ratio_to_db(sample.abs()) - threshold_db;
|
|
let knee_boundary_db = bias_db * 2.0;
|
|
|
|
if knee_boundary_db < -knee_db {
|
|
0.0
|
|
} else if knee_boundary_db.abs() <= knee_db {
|
|
// The textbook equation:
|
|
// ratio_to_db(sample.abs()) - (ratio_to_db(sample.abs()) - (bias_db + knee_db / 2.0).powi(2) / (2.0 * knee_db))
|
|
// Simplifies to:
|
|
// ((2.0 * bias_db) + knee_db).powi(2) / (8.0 * knee_db)
|
|
// Which in our case further simplifies to:
|
|
// (knee_boundary_db + knee_db).powi(2) / (8.0 * knee_db)
|
|
// because knee_boundary_db is 2.0 * bias_db.
|
|
(knee_boundary_db + knee_db).powi(2) / (8.0 * knee_db)
|
|
} else {
|
|
// Textbook:
|
|
// ratio_to_db(sample.abs()) - threshold_db, which is already our bias_db.
|
|
bias_db
|
|
}
|
|
} else {
|
|
0.0
|
|
};
|
|
|
|
// Spare the CPU unless (1) the limiter is engaged, (2) we
|
|
// were in attack or (3) we were in release, and that attack/
|
|
// release wasn't finished yet.
|
|
if limiter_db > 0.0
|
|
|| self.normalisation_integrator > 0.0
|
|
|| self.normalisation_peak > 0.0
|
|
{
|
|
// step 5: smooth, decoupled peak detector
|
|
// Textbook:
|
|
// release_cf * self.normalisation_integrator + (1.0 - release_cf) * limiter_db
|
|
// Simplifies to:
|
|
// release_cf * self.normalisation_integrator - release_cf * limiter_db + limiter_db
|
|
self.normalisation_integrator = f64::max(
|
|
limiter_db,
|
|
release_cf * self.normalisation_integrator
|
|
- release_cf * limiter_db
|
|
+ limiter_db,
|
|
);
|
|
// Textbook:
|
|
// attack_cf * self.normalisation_peak + (1.0 - attack_cf) * self.normalisation_integrator
|
|
// Simplifies to:
|
|
// attack_cf * self.normalisation_peak - attack_cf * self.normalisation_integrator + self.normalisation_integrator
|
|
self.normalisation_peak = attack_cf * self.normalisation_peak
|
|
- attack_cf * self.normalisation_integrator
|
|
+ self.normalisation_integrator;
|
|
|
|
// step 6: make-up gain applied later (volume attenuation)
|
|
// Applying the standard normalisation factor here won't work,
|
|
// because there are tracks with peaks as high as 6 dB above
|
|
// the default threshold, so that would clip.
|
|
|
|
// steps 7-8: conversion into level and multiplication into gain stage
|
|
*sample *= db_to_ratio(-self.normalisation_peak);
|
|
}
|
|
|
|
*sample *= volume;
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Err(e) = self.sink.write(packet, &mut self.converter) {
|
|
error!("{}", e);
|
|
self.handle_pause();
|
|
}
|
|
}
|
|
}
|
|
|
|
None => {
|
|
self.state.playing_to_end_of_track();
|
|
if let PlayerState::EndOfTrack {
|
|
track_id,
|
|
play_request_id,
|
|
..
|
|
} = self.state
|
|
{
|
|
self.send_event(PlayerEvent::EndOfTrack {
|
|
track_id,
|
|
play_request_id,
|
|
})
|
|
} else {
|
|
error!("PlayerInternal handle_packet: Invalid PlayerState");
|
|
exit(1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn start_playback(
|
|
&mut self,
|
|
track_id: SpotifyId,
|
|
play_request_id: u64,
|
|
loaded_track: PlayerLoadedTrackData,
|
|
start_playback: bool,
|
|
) {
|
|
let audio_item = Box::new(loaded_track.audio_item.clone());
|
|
|
|
self.send_event(PlayerEvent::TrackChanged { audio_item });
|
|
|
|
let position_ms = loaded_track.stream_position_ms;
|
|
|
|
let mut config = self.config.clone();
|
|
if config.normalisation_type == NormalisationType::Auto {
|
|
if self.auto_normalise_as_album {
|
|
config.normalisation_type = NormalisationType::Album;
|
|
} else {
|
|
config.normalisation_type = NormalisationType::Track;
|
|
}
|
|
};
|
|
let normalisation_factor =
|
|
NormalisationData::get_factor(&config, loaded_track.normalisation_data);
|
|
|
|
if start_playback {
|
|
self.ensure_sink_running();
|
|
self.send_event(PlayerEvent::Playing {
|
|
track_id,
|
|
play_request_id,
|
|
position_ms,
|
|
});
|
|
|
|
self.state = PlayerState::Playing {
|
|
track_id,
|
|
play_request_id,
|
|
decoder: loaded_track.decoder,
|
|
audio_item: loaded_track.audio_item,
|
|
normalisation_data: loaded_track.normalisation_data,
|
|
normalisation_factor,
|
|
stream_loader_controller: loaded_track.stream_loader_controller,
|
|
duration_ms: loaded_track.duration_ms,
|
|
bytes_per_second: loaded_track.bytes_per_second,
|
|
stream_position_ms: loaded_track.stream_position_ms,
|
|
reported_nominal_start_time: Instant::now()
|
|
.checked_sub(Duration::from_millis(position_ms as u64)),
|
|
suggested_to_preload_next_track: false,
|
|
is_explicit: loaded_track.is_explicit,
|
|
};
|
|
} else {
|
|
self.ensure_sink_stopped(false);
|
|
|
|
self.state = PlayerState::Paused {
|
|
track_id,
|
|
play_request_id,
|
|
decoder: loaded_track.decoder,
|
|
audio_item: loaded_track.audio_item,
|
|
normalisation_data: loaded_track.normalisation_data,
|
|
normalisation_factor,
|
|
stream_loader_controller: loaded_track.stream_loader_controller,
|
|
duration_ms: loaded_track.duration_ms,
|
|
bytes_per_second: loaded_track.bytes_per_second,
|
|
stream_position_ms: loaded_track.stream_position_ms,
|
|
suggested_to_preload_next_track: false,
|
|
is_explicit: loaded_track.is_explicit,
|
|
};
|
|
|
|
self.send_event(PlayerEvent::Paused {
|
|
track_id,
|
|
play_request_id,
|
|
position_ms,
|
|
});
|
|
}
|
|
}
|
|
|
|
fn handle_command_load(
|
|
&mut self,
|
|
track_id: SpotifyId,
|
|
play_request_id_option: Option<u64>,
|
|
play: bool,
|
|
position_ms: u32,
|
|
) -> PlayerResult {
|
|
let play_request_id =
|
|
play_request_id_option.unwrap_or(self.play_request_id_generator.get());
|
|
|
|
self.send_event(PlayerEvent::PlayRequestIdChanged { play_request_id });
|
|
|
|
if !self.config.gapless {
|
|
self.ensure_sink_stopped(play);
|
|
}
|
|
|
|
if matches!(self.state, PlayerState::Invalid { .. }) {
|
|
return Err(Error::internal(format!(
|
|
"Player::handle_command_load called from invalid state: {:?}",
|
|
self.state
|
|
)));
|
|
}
|
|
|
|
// Now we check at different positions whether we already have a pre-loaded version
|
|
// of this track somewhere. If so, use it and return.
|
|
|
|
// Check if there's a matching loaded track in the EndOfTrack player state.
|
|
// This is the case if we're repeating the same track again.
|
|
if let PlayerState::EndOfTrack {
|
|
track_id: previous_track_id,
|
|
..
|
|
} = self.state
|
|
{
|
|
if previous_track_id == track_id {
|
|
let mut loaded_track = match mem::replace(&mut self.state, PlayerState::Invalid) {
|
|
PlayerState::EndOfTrack { loaded_track, .. } => loaded_track,
|
|
_ => {
|
|
return Err(Error::internal(format!("PlayerInternal::handle_command_load repeating the same track: invalid state: {:?}", self.state)));
|
|
}
|
|
};
|
|
|
|
if position_ms != loaded_track.stream_position_ms {
|
|
// This may be blocking.
|
|
loaded_track.stream_position_ms = loaded_track.decoder.seek(position_ms)?;
|
|
}
|
|
self.preload = PlayerPreload::None;
|
|
self.start_playback(track_id, play_request_id, loaded_track, play);
|
|
if let PlayerState::Invalid = self.state {
|
|
return Err(Error::internal(format!("PlayerInternal::handle_command_load repeating the same track: start_playback() did not transition to valid player state: {:?}", self.state)));
|
|
}
|
|
return Ok(());
|
|
}
|
|
}
|
|
|
|
// Check if we are already playing the track. If so, just do a seek and update our info.
|
|
if let PlayerState::Playing {
|
|
track_id: current_track_id,
|
|
ref mut stream_position_ms,
|
|
ref mut decoder,
|
|
..
|
|
}
|
|
| PlayerState::Paused {
|
|
track_id: current_track_id,
|
|
ref mut stream_position_ms,
|
|
ref mut decoder,
|
|
..
|
|
} = self.state
|
|
{
|
|
if current_track_id == track_id {
|
|
// we can use the current decoder. Ensure it's at the correct position.
|
|
if position_ms != *stream_position_ms {
|
|
// This may be blocking.
|
|
*stream_position_ms = decoder.seek(position_ms)?;
|
|
}
|
|
|
|
// Move the info from the current state into a PlayerLoadedTrackData so we can use
|
|
// the usual code path to start playback.
|
|
let old_state = mem::replace(&mut self.state, PlayerState::Invalid);
|
|
|
|
if let PlayerState::Playing {
|
|
stream_position_ms,
|
|
decoder,
|
|
audio_item,
|
|
stream_loader_controller,
|
|
bytes_per_second,
|
|
duration_ms,
|
|
normalisation_data,
|
|
is_explicit,
|
|
..
|
|
}
|
|
| PlayerState::Paused {
|
|
stream_position_ms,
|
|
decoder,
|
|
audio_item,
|
|
stream_loader_controller,
|
|
bytes_per_second,
|
|
duration_ms,
|
|
normalisation_data,
|
|
is_explicit,
|
|
..
|
|
} = old_state
|
|
{
|
|
let loaded_track = PlayerLoadedTrackData {
|
|
decoder,
|
|
normalisation_data,
|
|
stream_loader_controller,
|
|
audio_item,
|
|
bytes_per_second,
|
|
duration_ms,
|
|
stream_position_ms,
|
|
is_explicit,
|
|
};
|
|
|
|
self.preload = PlayerPreload::None;
|
|
self.start_playback(track_id, play_request_id, loaded_track, play);
|
|
|
|
if let PlayerState::Invalid = self.state {
|
|
return Err(Error::internal(format!("PlayerInternal::handle_command_load already playing this track: start_playback() did not transition to valid player state: {:?}", self.state)));
|
|
}
|
|
|
|
return Ok(());
|
|
} else {
|
|
return Err(Error::internal(format!("PlayerInternal::handle_command_load already playing this track: invalid state: {:?}", self.state)));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if the requested track has been preloaded already. If so use the preloaded data.
|
|
if let PlayerPreload::Ready {
|
|
track_id: loaded_track_id,
|
|
..
|
|
} = self.preload
|
|
{
|
|
if track_id == loaded_track_id {
|
|
let preload = std::mem::replace(&mut self.preload, PlayerPreload::None);
|
|
if let PlayerPreload::Ready {
|
|
track_id,
|
|
mut loaded_track,
|
|
} = preload
|
|
{
|
|
if position_ms != loaded_track.stream_position_ms {
|
|
// This may be blocking
|
|
loaded_track.stream_position_ms = loaded_track.decoder.seek(position_ms)?;
|
|
}
|
|
self.start_playback(track_id, play_request_id, *loaded_track, play);
|
|
return Ok(());
|
|
} else {
|
|
return Err(Error::internal(format!("PlayerInternal::handle_command_loading preloaded track: invalid state: {:?}", self.state)));
|
|
}
|
|
}
|
|
}
|
|
|
|
self.send_event(PlayerEvent::Loading {
|
|
track_id,
|
|
play_request_id,
|
|
position_ms,
|
|
});
|
|
|
|
// Try to extract a pending loader from the preloading mechanism
|
|
let loader = if let PlayerPreload::Loading {
|
|
track_id: loaded_track_id,
|
|
..
|
|
} = self.preload
|
|
{
|
|
if (track_id == loaded_track_id) && (position_ms == 0) {
|
|
let mut preload = PlayerPreload::None;
|
|
std::mem::swap(&mut preload, &mut self.preload);
|
|
if let PlayerPreload::Loading { loader, .. } = preload {
|
|
Some(loader)
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
None
|
|
};
|
|
|
|
self.preload = PlayerPreload::None;
|
|
|
|
// If we don't have a loader yet, create one from scratch.
|
|
let loader = loader.unwrap_or_else(|| Box::pin(self.load_track(track_id, position_ms)));
|
|
|
|
// Set ourselves to a loading state.
|
|
self.state = PlayerState::Loading {
|
|
track_id,
|
|
play_request_id,
|
|
start_playback: play,
|
|
loader,
|
|
};
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn handle_command_preload(&mut self, track_id: SpotifyId) {
|
|
debug!("Preloading track");
|
|
let mut preload_track = true;
|
|
// check whether the track is already loaded somewhere or being loaded.
|
|
if let PlayerPreload::Loading {
|
|
track_id: currently_loading,
|
|
..
|
|
}
|
|
| PlayerPreload::Ready {
|
|
track_id: currently_loading,
|
|
..
|
|
} = self.preload
|
|
{
|
|
if currently_loading == track_id {
|
|
// we're already preloading the requested track.
|
|
preload_track = false;
|
|
} else {
|
|
// we're preloading something else - cancel it.
|
|
self.preload = PlayerPreload::None;
|
|
}
|
|
}
|
|
|
|
if let PlayerState::Playing {
|
|
track_id: current_track_id,
|
|
..
|
|
}
|
|
| PlayerState::Paused {
|
|
track_id: current_track_id,
|
|
..
|
|
}
|
|
| PlayerState::EndOfTrack {
|
|
track_id: current_track_id,
|
|
..
|
|
} = self.state
|
|
{
|
|
if current_track_id == track_id {
|
|
// we already have the requested track loaded.
|
|
preload_track = false;
|
|
}
|
|
}
|
|
|
|
// schedule the preload of the current track if desired.
|
|
if preload_track {
|
|
let loader = self.load_track(track_id, 0);
|
|
self.preload = PlayerPreload::Loading {
|
|
track_id,
|
|
loader: Box::pin(loader),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn handle_command_seek(&mut self, position_ms: u32) -> PlayerResult {
|
|
// When we are still loading, the user may immediately ask to
|
|
// seek to another position yet the decoder won't be ready for
|
|
// that. In this case just restart the loading process but
|
|
// with the requested position.
|
|
if let PlayerState::Loading {
|
|
track_id,
|
|
play_request_id,
|
|
start_playback,
|
|
..
|
|
} = self.state
|
|
{
|
|
return self.handle_command_load(
|
|
track_id,
|
|
Some(play_request_id),
|
|
start_playback,
|
|
position_ms,
|
|
);
|
|
}
|
|
|
|
if let Some(decoder) = self.state.decoder() {
|
|
match decoder.seek(position_ms) {
|
|
Ok(new_position_ms) => {
|
|
if let PlayerState::Playing {
|
|
ref mut stream_position_ms,
|
|
track_id,
|
|
play_request_id,
|
|
..
|
|
}
|
|
| PlayerState::Paused {
|
|
ref mut stream_position_ms,
|
|
track_id,
|
|
play_request_id,
|
|
..
|
|
} = self.state
|
|
{
|
|
*stream_position_ms = new_position_ms;
|
|
|
|
self.send_event(PlayerEvent::Seeked {
|
|
play_request_id,
|
|
track_id,
|
|
position_ms: new_position_ms,
|
|
});
|
|
}
|
|
}
|
|
Err(e) => error!("PlayerInternal::handle_command_seek error: {}", e),
|
|
}
|
|
} else {
|
|
error!("Player::seek called from invalid state: {:?}", self.state);
|
|
}
|
|
|
|
// ensure we have a bit of a buffer of downloaded data
|
|
self.preload_data_before_playback()?;
|
|
|
|
if let PlayerState::Playing {
|
|
ref mut reported_nominal_start_time,
|
|
..
|
|
} = self.state
|
|
{
|
|
*reported_nominal_start_time =
|
|
Instant::now().checked_sub(Duration::from_millis(position_ms as u64));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn handle_command(&mut self, cmd: PlayerCommand) -> PlayerResult {
|
|
debug!("command={:?}", cmd);
|
|
match cmd {
|
|
PlayerCommand::Load {
|
|
track_id,
|
|
play,
|
|
position_ms,
|
|
} => self.handle_command_load(track_id, None, play, position_ms)?,
|
|
|
|
PlayerCommand::Preload { track_id } => self.handle_command_preload(track_id),
|
|
|
|
PlayerCommand::Seek(position_ms) => self.handle_command_seek(position_ms)?,
|
|
|
|
PlayerCommand::Play => self.handle_play(),
|
|
|
|
PlayerCommand::Pause => self.handle_pause(),
|
|
|
|
PlayerCommand::Stop => self.handle_player_stop(),
|
|
|
|
PlayerCommand::SetSession(session) => self.session = session,
|
|
|
|
PlayerCommand::AddEventSender(sender) => self.event_senders.push(sender),
|
|
|
|
PlayerCommand::SetSinkEventCallback(callback) => self.sink_event_callback = callback,
|
|
|
|
PlayerCommand::EmitVolumeChangedEvent(volume) => {
|
|
self.send_event(PlayerEvent::VolumeChanged { volume })
|
|
}
|
|
|
|
PlayerCommand::EmitRepeatChangedEvent(repeat) => {
|
|
self.send_event(PlayerEvent::RepeatChanged { repeat })
|
|
}
|
|
|
|
PlayerCommand::EmitShuffleChangedEvent(shuffle) => {
|
|
self.send_event(PlayerEvent::ShuffleChanged { shuffle })
|
|
}
|
|
|
|
PlayerCommand::EmitAutoPlayChangedEvent(auto_play) => {
|
|
self.send_event(PlayerEvent::AutoPlayChanged { auto_play })
|
|
}
|
|
|
|
PlayerCommand::EmitSessionClientChangedEvent {
|
|
client_id,
|
|
client_name,
|
|
client_brand_name,
|
|
client_model_name,
|
|
} => self.send_event(PlayerEvent::SessionClientChanged {
|
|
client_id,
|
|
client_name,
|
|
client_brand_name,
|
|
client_model_name,
|
|
}),
|
|
|
|
PlayerCommand::EmitSessionConnectedEvent {
|
|
connection_id,
|
|
user_name,
|
|
} => self.send_event(PlayerEvent::SessionConnected {
|
|
connection_id,
|
|
user_name,
|
|
}),
|
|
|
|
PlayerCommand::EmitSessionDisconnectedEvent {
|
|
connection_id,
|
|
user_name,
|
|
} => self.send_event(PlayerEvent::SessionDisconnected {
|
|
connection_id,
|
|
user_name,
|
|
}),
|
|
|
|
PlayerCommand::SetAutoNormaliseAsAlbum(setting) => {
|
|
self.auto_normalise_as_album = setting
|
|
}
|
|
|
|
PlayerCommand::EmitFilterExplicitContentChangedEvent(filter) => {
|
|
self.send_event(PlayerEvent::FilterExplicitContentChanged { filter });
|
|
|
|
if filter {
|
|
if let PlayerState::Playing {
|
|
track_id,
|
|
play_request_id,
|
|
is_explicit,
|
|
..
|
|
}
|
|
| PlayerState::Paused {
|
|
track_id,
|
|
play_request_id,
|
|
is_explicit,
|
|
..
|
|
} = self.state
|
|
{
|
|
if is_explicit {
|
|
warn!("Currently loaded track is explicit, which client setting forbids -- skipping to next track.");
|
|
self.send_event(PlayerEvent::EndOfTrack {
|
|
track_id,
|
|
play_request_id,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn send_event(&mut self, event: PlayerEvent) {
|
|
self.event_senders
|
|
.retain(|sender| sender.send(event.clone()).is_ok());
|
|
}
|
|
|
|
fn load_track(
|
|
&mut self,
|
|
spotify_id: SpotifyId,
|
|
position_ms: u32,
|
|
) -> impl FusedFuture<Output = Result<PlayerLoadedTrackData, ()>> + Send + 'static {
|
|
// This method creates a future that returns the loaded stream and associated info.
|
|
// Ideally all work should be done using asynchronous code. However, seek() on the
|
|
// audio stream is implemented in a blocking fashion. Thus, we can't turn it into future
|
|
// easily. Instead we spawn a thread to do the work and return a one-shot channel as the
|
|
// future to work with.
|
|
|
|
let loader = PlayerTrackLoader {
|
|
session: self.session.clone(),
|
|
config: self.config.clone(),
|
|
};
|
|
|
|
let (result_tx, result_rx) = oneshot::channel();
|
|
|
|
let load_handles_clone = self.load_handles.clone();
|
|
let handle = tokio::runtime::Handle::current();
|
|
let load_handle = thread::spawn(move || {
|
|
let data = handle.block_on(loader.load_track(spotify_id, position_ms));
|
|
if let Some(data) = data {
|
|
let _ = result_tx.send(data);
|
|
}
|
|
|
|
let mut load_handles = load_handles_clone.lock();
|
|
load_handles.remove(&thread::current().id());
|
|
});
|
|
|
|
let mut load_handles = self.load_handles.lock();
|
|
load_handles.insert(load_handle.thread().id(), load_handle);
|
|
|
|
result_rx.map_err(|_| ())
|
|
}
|
|
|
|
fn preload_data_before_playback(&mut self) -> PlayerResult {
|
|
if let PlayerState::Playing {
|
|
bytes_per_second,
|
|
ref mut stream_loader_controller,
|
|
..
|
|
} = self.state
|
|
{
|
|
let read_ahead_during_playback = AudioFetchParams::get().read_ahead_during_playback;
|
|
// Request our read ahead range
|
|
let request_data_length =
|
|
(read_ahead_during_playback.as_secs_f32() * bytes_per_second as f32) as usize;
|
|
|
|
// Request the part we want to wait for blocking. This effectively means we wait for the previous request to partially complete.
|
|
let wait_for_data_length =
|
|
(read_ahead_during_playback.as_secs_f32() * bytes_per_second as f32) as usize;
|
|
|
|
stream_loader_controller
|
|
.fetch_next_and_wait(request_data_length, wait_for_data_length)
|
|
.map_err(Into::into)
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Drop for PlayerInternal {
|
|
fn drop(&mut self) {
|
|
debug!("drop PlayerInternal[{}]", self.player_id);
|
|
|
|
let handles: Vec<thread::JoinHandle<()>> = {
|
|
// waiting for the thread while holding the mutex would result in a deadlock
|
|
let mut load_handles = self.load_handles.lock();
|
|
|
|
load_handles
|
|
.drain()
|
|
.map(|(_thread_id, handle)| handle)
|
|
.collect()
|
|
};
|
|
|
|
for handle in handles {
|
|
let _ = handle.join();
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Debug for PlayerCommand {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
PlayerCommand::Load {
|
|
track_id,
|
|
play,
|
|
position_ms,
|
|
..
|
|
} => f
|
|
.debug_tuple("Load")
|
|
.field(&track_id)
|
|
.field(&play)
|
|
.field(&position_ms)
|
|
.finish(),
|
|
PlayerCommand::Preload { track_id } => {
|
|
f.debug_tuple("Preload").field(&track_id).finish()
|
|
}
|
|
PlayerCommand::Play => f.debug_tuple("Play").finish(),
|
|
PlayerCommand::Pause => f.debug_tuple("Pause").finish(),
|
|
PlayerCommand::Stop => f.debug_tuple("Stop").finish(),
|
|
PlayerCommand::Seek(position) => f.debug_tuple("Seek").field(&position).finish(),
|
|
PlayerCommand::SetSession(_) => f.debug_tuple("SetSession").finish(),
|
|
PlayerCommand::AddEventSender(_) => f.debug_tuple("AddEventSender").finish(),
|
|
PlayerCommand::SetSinkEventCallback(_) => {
|
|
f.debug_tuple("SetSinkEventCallback").finish()
|
|
}
|
|
PlayerCommand::EmitVolumeChangedEvent(volume) => f
|
|
.debug_tuple("EmitVolumeChangedEvent")
|
|
.field(&volume)
|
|
.finish(),
|
|
PlayerCommand::SetAutoNormaliseAsAlbum(setting) => f
|
|
.debug_tuple("SetAutoNormaliseAsAlbum")
|
|
.field(&setting)
|
|
.finish(),
|
|
PlayerCommand::EmitFilterExplicitContentChangedEvent(filter) => f
|
|
.debug_tuple("EmitFilterExplicitContentChangedEvent")
|
|
.field(&filter)
|
|
.finish(),
|
|
PlayerCommand::EmitSessionConnectedEvent {
|
|
connection_id,
|
|
user_name,
|
|
} => f
|
|
.debug_tuple("EmitSessionConnectedEvent")
|
|
.field(&connection_id)
|
|
.field(&user_name)
|
|
.finish(),
|
|
PlayerCommand::EmitSessionDisconnectedEvent {
|
|
connection_id,
|
|
user_name,
|
|
} => f
|
|
.debug_tuple("EmitSessionDisconnectedEvent")
|
|
.field(&connection_id)
|
|
.field(&user_name)
|
|
.finish(),
|
|
PlayerCommand::EmitSessionClientChangedEvent {
|
|
client_id,
|
|
client_name,
|
|
client_brand_name,
|
|
client_model_name,
|
|
} => f
|
|
.debug_tuple("EmitSessionClientChangedEvent")
|
|
.field(&client_id)
|
|
.field(&client_name)
|
|
.field(&client_brand_name)
|
|
.field(&client_model_name)
|
|
.finish(),
|
|
PlayerCommand::EmitShuffleChangedEvent(shuffle) => f
|
|
.debug_tuple("EmitShuffleChangedEvent")
|
|
.field(&shuffle)
|
|
.finish(),
|
|
PlayerCommand::EmitRepeatChangedEvent(repeat) => f
|
|
.debug_tuple("EmitRepeatChangedEvent")
|
|
.field(&repeat)
|
|
.finish(),
|
|
PlayerCommand::EmitAutoPlayChangedEvent(auto_play) => f
|
|
.debug_tuple("EmitAutoPlayChangedEvent")
|
|
.field(&auto_play)
|
|
.finish(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Debug for PlayerState {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
use PlayerState::*;
|
|
match *self {
|
|
Stopped => f.debug_struct("Stopped").finish(),
|
|
Loading {
|
|
track_id,
|
|
play_request_id,
|
|
..
|
|
} => f
|
|
.debug_struct("Loading")
|
|
.field("track_id", &track_id)
|
|
.field("play_request_id", &play_request_id)
|
|
.finish(),
|
|
Paused {
|
|
track_id,
|
|
play_request_id,
|
|
..
|
|
} => f
|
|
.debug_struct("Paused")
|
|
.field("track_id", &track_id)
|
|
.field("play_request_id", &play_request_id)
|
|
.finish(),
|
|
Playing {
|
|
track_id,
|
|
play_request_id,
|
|
..
|
|
} => f
|
|
.debug_struct("Playing")
|
|
.field("track_id", &track_id)
|
|
.field("play_request_id", &play_request_id)
|
|
.finish(),
|
|
EndOfTrack {
|
|
track_id,
|
|
play_request_id,
|
|
..
|
|
} => f
|
|
.debug_struct("EndOfTrack")
|
|
.field("track_id", &track_id)
|
|
.field("play_request_id", &play_request_id)
|
|
.finish(),
|
|
Invalid => f.debug_struct("Invalid").finish(),
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Subfile<T: Read + Seek> {
|
|
stream: T,
|
|
offset: u64,
|
|
length: u64,
|
|
}
|
|
|
|
impl<T: Read + Seek> Subfile<T> {
|
|
pub fn new(mut stream: T, offset: u64, length: u64) -> Result<Subfile<T>, io::Error> {
|
|
let target = SeekFrom::Start(offset);
|
|
stream.seek(target)?;
|
|
|
|
Ok(Subfile {
|
|
stream,
|
|
offset,
|
|
length,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl<T: Read + Seek> Read for Subfile<T> {
|
|
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
|
self.stream.read(buf)
|
|
}
|
|
}
|
|
|
|
impl<T: Read + Seek> Seek for Subfile<T> {
|
|
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
|
let pos = match pos {
|
|
SeekFrom::Start(offset) => SeekFrom::Start(offset + self.offset),
|
|
SeekFrom::End(offset) => {
|
|
if (self.length as i64 - offset) < self.offset as i64 {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
"newpos would be < self.offset",
|
|
));
|
|
}
|
|
pos
|
|
}
|
|
_ => pos,
|
|
};
|
|
|
|
let newpos = self.stream.seek(pos)?;
|
|
Ok(newpos - self.offset)
|
|
}
|
|
}
|
|
|
|
impl<R> MediaSource for Subfile<R>
|
|
where
|
|
R: Read + Seek + Send + Sync,
|
|
{
|
|
fn is_seekable(&self) -> bool {
|
|
true
|
|
}
|
|
|
|
fn byte_len(&self) -> Option<u64> {
|
|
Some(self.length)
|
|
}
|
|
}
|