From 6f28b0486fd3110c504e3b39d3a452b6f54e6dce Mon Sep 17 00:00:00 2001 From: Konstantin Seiler Date: Tue, 10 Mar 2020 23:26:01 +1100 Subject: [PATCH 1/2] Emit blocking sink events --- playback/src/player.rs | 70 +++++++++++++++++++++++++++++-------- src/main.rs | 16 ++++++++- src/player_event_handler.rs | 16 +++++++++ 3 files changed, 87 insertions(+), 15 deletions(-) diff --git a/playback/src/player.rs b/playback/src/player.rs index ef7484c7..4b207790 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -33,6 +33,15 @@ pub struct Player { play_request_id_generator: SeqGenerator, } +#[derive(PartialEq, Debug, Clone, Copy)] +pub enum SinkStatus { + Running, + Closed, + TemporarilyClosed, +} + +pub type SinkEventCallback = Box; + struct PlayerInternal { session: Session, config: PlayerConfig, @@ -41,7 +50,8 @@ struct PlayerInternal { state: PlayerState, preload: PlayerPreload, sink: Box, - sink_running: bool, + sink_status: SinkStatus, + sink_event_callback: Option, audio_filter: Option>, event_senders: Vec>, } @@ -61,6 +71,7 @@ enum PlayerCommand { Stop, Seek(u32), AddEventSender(futures::sync::mpsc::UnboundedSender), + SetSinkEventCallback(Option), EmitVolumeSetEvent(u16), } @@ -212,7 +223,8 @@ impl Player { state: PlayerState::Stopped, preload: PlayerPreload::None, sink: sink_builder(), - sink_running: false, + sink_status: SinkStatus::Closed, + sink_event_callback: None, audio_filter: audio_filter, event_senders: [event_sender].to_vec(), }; @@ -288,6 +300,10 @@ impl Player { Box::new(result) } + pub fn set_sink_event_callback(&self, callback: Option) { + self.command(PlayerCommand::SetSinkEventCallback(callback)); + } + pub fn emit_volume_set_event(&self, volume: u16) { self.command(PlayerCommand::EmitVolumeSetEvent(volume)); } @@ -862,20 +878,41 @@ impl PlayerInternal { } fn ensure_sink_running(&mut self) { - if !self.sink_running { + 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_running = true, + Ok(()) => self.sink_status = SinkStatus::Running, Err(err) => error!("Could not start audio: {}", err), } } } - fn ensure_sink_stopped(&mut self) { - if self.sink_running { - trace!("== Stopping sink =="); - self.sink.stop().unwrap(); - self.sink_running = false; + fn ensure_sink_stopped(&mut self, temporarily: bool) { + match self.sink_status { + SinkStatus::Running => { + trace!("== Stopping sink =="); + self.sink.stop().unwrap(); + self.sink_status = if temporarily { + SinkStatus::TemporarilyClosed + } else { + SinkStatus::Closed + }; + if let Some(callback) = &mut self.sink_event_callback { + callback(self.sink_status); + } + } + SinkStatus::TemporarilyClosed => { + if !temporarily { + self.sink_status = SinkStatus::Closed; + if let Some(callback) = &mut self.sink_event_callback { + callback(SinkStatus::Closed); + } + } + } + SinkStatus::Closed => (), } } @@ -901,7 +938,7 @@ impl PlayerInternal { play_request_id, .. } => { - self.ensure_sink_stopped(); + self.ensure_sink_stopped(false); self.send_event(PlayerEvent::Stopped { track_id, play_request_id, @@ -948,7 +985,7 @@ impl PlayerInternal { { self.state.playing_to_paused(); - self.ensure_sink_stopped(); + self.ensure_sink_stopped(false); let position_ms = Self::position_pcm_to_ms(stream_position_pcm); self.send_event(PlayerEvent::Paused { track_id, @@ -977,7 +1014,7 @@ impl PlayerInternal { if let Err(err) = self.sink.write(&packet.data()) { error!("Could not write audio: {}", err); - self.ensure_sink_stopped(); + self.ensure_sink_stopped(false); } } } @@ -1035,7 +1072,7 @@ impl PlayerInternal { suggested_to_preload_next_track: false, }; } else { - self.ensure_sink_stopped(); + self.ensure_sink_stopped(false); self.state = PlayerState::Paused { track_id: track_id, @@ -1227,7 +1264,7 @@ impl PlayerInternal { // We need to load the track - either from scratch or by completing a preload. // In any case we go into a Loading state to load the track. - self.ensure_sink_stopped(); + self.ensure_sink_stopped(play); self.send_event(PlayerEvent::Loading { track_id, @@ -1409,6 +1446,8 @@ impl PlayerInternal { PlayerCommand::AddEventSender(sender) => self.event_senders.push(sender), + PlayerCommand::SetSinkEventCallback(callback) => self.sink_event_callback = callback, + PlayerCommand::EmitVolumeSetEvent(volume) => { self.send_event(PlayerEvent::VolumeSet { volume }) } @@ -1513,6 +1552,9 @@ impl ::std::fmt::Debug for PlayerCommand { PlayerCommand::Stop => f.debug_tuple("Stop").finish(), PlayerCommand::Seek(position) => f.debug_tuple("Seek").field(&position).finish(), PlayerCommand::AddEventSender(_) => f.debug_tuple("AddEventSender").finish(), + PlayerCommand::SetSinkEventCallback(_) => { + f.debug_tuple("SetSinkEventCallback").finish() + } PlayerCommand::EmitVolumeSetEvent(volume) => { f.debug_tuple("VolumeSet").field(&volume).finish() } diff --git a/src/main.rs b/src/main.rs index f749a525..a6afb46a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,7 +27,7 @@ use librespot::playback::mixer::{self, Mixer, MixerConfig}; use librespot::playback::player::{Player, PlayerEvent}; mod player_event_handler; -use crate::player_event_handler::run_program_on_events; +use crate::player_event_handler::{emit_sink_event, run_program_on_events}; fn device_id(name: &str) -> String { hex::encode(Sha1::digest(name.as_bytes())) @@ -87,6 +87,7 @@ struct Setup { enable_discovery: bool, zeroconf_port: u16, player_event_program: Option, + emit_sink_events: bool, } fn setup(args: &[String]) -> Setup { @@ -111,6 +112,7 @@ fn setup(args: &[String]) -> Setup { "Run PROGRAM when playback is about to begin.", "PROGRAM", ) + .optflag("", "emit-sink-events", "Run program set by --onevent before sink is opened and after it is closed.") .optflag("v", "verbose", "Enable verbose output") .optopt("u", "username", "Username to sign in with", "USERNAME") .optopt("p", "password", "Password", "PASSWORD") @@ -354,6 +356,7 @@ fn setup(args: &[String]) -> Setup { mixer: mixer, mixer_config: mixer_config, player_event_program: matches.opt_str("onevent"), + emit_sink_events: matches.opt_present("emit-sink-events"), } } @@ -381,6 +384,7 @@ struct Main { player_event_channel: Option>, player_event_program: Option, + emit_sink_events: bool, } impl Main { @@ -407,6 +411,7 @@ impl Main { player_event_channel: None, player_event_program: setup.player_event_program, + emit_sink_events: setup.emit_sink_events, }; if setup.enable_discovery { @@ -476,6 +481,15 @@ impl Future for Main { (backend)(device) }); + if self.emit_sink_events { + if let Some(player_event_program) = &self.player_event_program { + let player_event_program = player_event_program.clone(); + player.set_sink_event_callback(Some(Box::new(move |sink_status| { + emit_sink_event(sink_status, &player_event_program) + }))); + } + } + let (spirc, spirc_task) = Spirc::new(connect_config, session, player, mixer); self.spirc = Some(spirc); self.spirc_task = Some(spirc_task); diff --git a/src/player_event_handler.rs b/src/player_event_handler.rs index 2fa34d2b..f9dc2007 100644 --- a/src/player_event_handler.rs +++ b/src/player_event_handler.rs @@ -5,6 +5,9 @@ use std::io; use std::process::Command; use tokio_process::{Child, CommandExt}; +use futures::Future; +use librespot::playback::player::SinkStatus; + fn run_program(program: &str, env_vars: HashMap<&str, String>) -> io::Result { let mut v: Vec<&str> = program.split_whitespace().collect(); info!("Running {:?} with environment variables {:?}", v, env_vars); @@ -37,3 +40,16 @@ pub fn run_program_on_events(event: PlayerEvent, onevent: &str) -> Option "running", + SinkStatus::TemporarilyClosed => "temporarily_closed", + SinkStatus::Closed => "closed", + }; + env_vars.insert("SINK_STATUS", sink_status.to_string()); + + let _ = run_program(onevent, env_vars).and_then(|child| child.wait()); +} From d4d55254b035db3a9374835fbe420d5a2340a2e6 Mon Sep 17 00:00:00 2001 From: Konstantin Seiler Date: Tue, 10 Mar 2020 23:53:58 +1100 Subject: [PATCH 2/2] address merge conflict --- playback/src/player.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playback/src/player.rs b/playback/src/player.rs index 30faadc6..f685fd71 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -1103,7 +1103,7 @@ impl PlayerInternal { position_ms: u32, ) { if !self.config.gapless { - self.ensure_sink_stopped(); + self.ensure_sink_stopped(play); } // emit the correct player event match self.state {