use crate::config;
use crate::player::Player;
use std::error::Error;
use std::io;
use std::sync::Arc;
use tokio::task::JoinHandle;
pub(crate) mod grpc;
mod midi;
mod osc;
pub trait Driver: Send + Sync + 'static {
fn monitor_events(&self) -> JoinHandle<Result<(), io::Error>>;
}
#[derive(Clone, serde::Serialize)]
pub struct ControllerStatus {
pub kind: String,
pub status: String,
pub detail: Option<String>,
pub error: Option<String>,
}
pub struct Controller {
handles: Vec<JoinHandle<Result<(), io::Error>>>,
statuses: Vec<ControllerStatus>,
}
impl Controller {
pub fn new(config: Vec<config::Controller>, player: Arc<Player>) -> Controller {
let mut controller_drivers = Vec::new();
let mut statuses = Vec::new();
for config in config {
let player = player.clone();
let (kind, detail) = match &config {
config::Controller::Grpc(c) => {
("grpc".to_string(), Some(format!("port {}", c.port())))
}
config::Controller::Osc(c) => {
("osc".to_string(), Some(format!("port {}", c.port())))
}
config::Controller::Midi(_) => ("midi".to_string(), None),
_ => ("unknown".to_string(), None),
};
let result: Result<Arc<dyn Driver>, Box<dyn Error>> = match config {
config::Controller::Grpc(config) => {
grpc::Driver::new(config, player).map(|d| d as Arc<dyn Driver>)
}
config::Controller::Osc(config) => {
osc::Driver::new(config, player).map(|d| d as Arc<dyn Driver>)
}
config::Controller::Midi(config) => {
midi::Driver::new(config, player).map(|d| d as Arc<dyn Driver>)
}
_ => Err("unexpected controller type".into()),
};
match result {
Ok(driver) => {
controller_drivers.push(driver);
statuses.push(ControllerStatus {
kind,
status: "running".to_string(),
detail,
error: None,
});
}
Err(e) => {
tracing::warn!(kind = %kind, error = %e, "Controller failed to start");
statuses.push(ControllerStatus {
kind,
status: "error".to_string(),
detail,
error: Some(e.to_string()),
});
}
}
}
let mut handles = Vec::new();
for driver in controller_drivers {
handles.push(driver.monitor_events());
}
Controller { handles, statuses }
}
pub fn new_from_drivers(drivers: Vec<Arc<dyn Driver>>) -> Controller {
let mut handles = Vec::new();
for driver in &drivers {
handles.push(driver.monitor_events());
}
Controller {
handles,
statuses: drivers
.iter()
.map(|_| ControllerStatus {
kind: "unknown".to_string(),
status: "running".to_string(),
detail: None,
error: None,
})
.collect(),
}
}
pub fn statuses(&self) -> &[ControllerStatus] {
&self.statuses
}
pub fn shutdown(self) {
for handle in self.handles {
handle.abort();
}
}
}
#[cfg(test)]
mod test {
use std::{collections::HashMap, error::Error, io, path::Path, sync::Arc};
use tokio::{
sync::{Barrier, Mutex},
task::JoinHandle,
};
use tracing::error;
use crate::{
config, player::Player, playlist, playlist::Playlist, songs, testutil::eventually,
};
use super::Driver;
#[derive(Debug)]
enum TestEvent {
Unset,
Play,
Prev,
Next,
Stop,
AllSongs,
Playlist,
Close,
}
struct TestDriver {
player: Arc<Player>,
current_event: Arc<Mutex<TestEvent>>,
barrier: Arc<Barrier>,
}
impl TestDriver {
fn new(player: Arc<Player>, current_event: TestEvent) -> TestDriver {
let current_event = Arc::new(Mutex::new(current_event));
let barrier = Arc::new(Barrier::new(2));
TestDriver {
player,
current_event,
barrier,
}
}
async fn next_event(&self, event: TestEvent) {
{
let mut current_event = self.current_event.lock().await;
*current_event = event;
}
self.barrier.wait().await;
self.barrier.wait().await;
}
}
impl Driver for TestDriver {
fn monitor_events(&self) -> JoinHandle<Result<(), io::Error>> {
let barrier = self.barrier.clone();
let current_event = self.current_event.clone();
let player = self.player.clone();
let result: JoinHandle<Result<(), io::Error>> = tokio::spawn(async move {
loop {
barrier.wait().await;
let current_event = current_event.lock().await;
barrier.wait().await;
match *current_event {
TestEvent::Unset => unreachable!("current event should not be unset"),
TestEvent::Play => {
if let Err(e) = player.play().await {
error!(err = e.as_ref(), "Error playing song");
}
}
TestEvent::Prev => {
player.prev().await;
}
TestEvent::Next => {
player.next().await;
}
TestEvent::Stop => {
player.stop().await;
}
TestEvent::AllSongs => {
player.switch_to_playlist("all_songs").await.unwrap();
}
TestEvent::Playlist => {
player.switch_to_playlist("playlist").await.unwrap();
}
TestEvent::Close => return Ok(()),
}
}
});
result
}
}
#[tokio::test(flavor = "multi_thread")]
async fn test_controller_new_with_grpc() -> Result<(), Box<dyn Error>> {
let songs = songs::get_all_songs(Path::new("assets/songs"))?;
let playlist = Playlist::new(
"playlist",
&config::Playlist::deserialize(Path::new("assets/playlist.yaml"))?,
songs.clone(),
)?;
let mut playlists = HashMap::new();
playlists.insert(
"all_songs".to_string(),
playlist::from_songs(songs.clone())?,
);
playlists.insert("playlist".to_string(), playlist);
let player = Player::new(
playlists,
"playlist".to_string(),
&config::Player::new(
vec![],
Some(config::Audio::new("mock-device")),
None,
None,
HashMap::new(),
"assets/songs",
),
None,
)?;
player.await_hardware_ready().await;
let grpc_config = config::GrpcController::new(0);
let controller =
super::Controller::new(vec![config::Controller::Grpc(grpc_config)], player);
assert!(controller.statuses().iter().all(|s| s.status == "running"));
controller.shutdown();
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_controller_new_empty_config() -> Result<(), Box<dyn Error>> {
let songs = songs::get_all_songs(Path::new("assets/songs"))?;
let playlist = Playlist::new(
"playlist",
&config::Playlist::deserialize(Path::new("assets/playlist.yaml"))?,
songs.clone(),
)?;
let mut playlists = HashMap::new();
playlists.insert(
"all_songs".to_string(),
playlist::from_songs(songs.clone())?,
);
playlists.insert("playlist".to_string(), playlist);
let player = Player::new(
playlists,
"playlist".to_string(),
&config::Player::new(
vec![],
Some(config::Audio::new("mock-device")),
None,
None,
HashMap::new(),
"assets/songs",
),
None,
)?;
player.await_hardware_ready().await;
let controller = super::Controller::new(vec![], player);
assert!(controller.statuses().is_empty());
controller.shutdown();
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_controller_new_with_osc() -> Result<(), Box<dyn Error>> {
let songs = songs::get_all_songs(Path::new("assets/songs"))?;
let playlist = Playlist::new(
"playlist",
&config::Playlist::deserialize(Path::new("assets/playlist.yaml"))?,
songs.clone(),
)?;
let mut playlists = HashMap::new();
playlists.insert(
"all_songs".to_string(),
playlist::from_songs(songs.clone())?,
);
playlists.insert("playlist".to_string(), playlist);
let player = Player::new(
playlists,
"playlist".to_string(),
&config::Player::new(
vec![],
Some(config::Audio::new("mock-device")),
None,
None,
HashMap::new(),
"assets/songs",
),
None,
)?;
player.await_hardware_ready().await;
let osc_config = config::OscController::new();
let controller =
super::Controller::new(vec![config::Controller::Osc(Box::new(osc_config))], player);
assert!(controller.statuses().iter().all(|s| s.status == "running"));
controller.shutdown();
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_controller() -> Result<(), Box<dyn Error>> {
let songs = songs::get_all_songs(Path::new("assets/songs"))?;
let playlist = Playlist::new(
"playlist",
&config::Playlist::deserialize(Path::new("assets/playlist.yaml"))?,
songs.clone(),
)?;
let mut playlists = HashMap::new();
playlists.insert(
"all_songs".to_string(),
playlist::from_songs(songs.clone())?,
);
playlists.insert("playlist".to_string(), playlist);
let player = Player::new(
playlists,
"playlist".to_string(),
&config::Player::new(
vec![],
Some(config::Audio::new("mock-device")),
None,
None,
HashMap::new(),
"assets/songs",
),
None,
)?;
player.await_hardware_ready().await;
let playlist = player.get_playlist();
let binding = player
.audio_device()
.expect("audio device should be present");
let device = binding.to_mock()?;
let driver = Arc::new(TestDriver::new(player.clone(), TestEvent::Unset));
let controller = super::Controller::new_from_drivers(vec![driver.clone()]);
println!("Playlist: {}", playlist);
println!("Playlist -> Song 1");
eventually(
|| playlist.current().unwrap().name() == "Song 1",
"Playlist never became Song 1",
);
driver.next_event(TestEvent::Next).await;
println!("Playlist -> Song 3");
eventually(
|| playlist.current().unwrap().name() == "Song 3",
"Playlist never became Song 3",
);
driver.next_event(TestEvent::Next).await;
println!("Playlist -> Song 5");
eventually(
|| playlist.current().unwrap().name() == "Song 5",
"Playlist never became Song 5",
);
driver.next_event(TestEvent::Next).await;
println!("Playlist -> Song 7");
eventually(
|| playlist.current().unwrap().name() == "Song 7",
"Playlist never became Song 7",
);
driver.next_event(TestEvent::Prev).await;
println!("Playlist -> Song 5");
eventually(
|| playlist.current().unwrap().name() == "Song 5",
"Playlist never became Song 5",
);
println!("Switch to AllSongs");
driver.next_event(TestEvent::AllSongs).await;
eventually(
|| player.get_playlist().current().unwrap().name() == "Song 1",
"All Songs Playlist never became Song 1",
);
println!("AllSongs -> Song 10");
driver.next_event(TestEvent::Next).await;
eventually(
|| player.get_playlist().current().unwrap().name() == "Song 10",
"All Songs Playlist never became Song 10",
);
println!("AllSongs -> Song 2");
driver.next_event(TestEvent::Next).await;
eventually(
|| player.get_playlist().current().unwrap().name() == "Song 2",
"All Songs Playlist never became Song 2",
);
println!("AllSongs -> Song 10");
driver.next_event(TestEvent::Prev).await;
eventually(
|| player.get_playlist().current().unwrap().name() == "Song 10",
"All Songs Playlist never became Song 10",
);
println!("Switch to Playlist");
driver.next_event(TestEvent::Playlist).await;
eventually(
|| playlist.current().unwrap().name() == "Song 5",
"Playlist never became Song 5",
);
println!("Playlist -> Song 7");
driver.next_event(TestEvent::Next).await;
eventually(
|| playlist.current().unwrap().name() == "Song 7",
"Playlist never became Song 7",
);
driver.next_event(TestEvent::Play).await;
eventually(|| device.is_playing(), "Song never started playing");
driver.next_event(TestEvent::Stop).await;
eventually(|| !device.is_playing(), "Song never stopped playing");
println!("Close");
driver.next_event(TestEvent::Close).await;
controller.shutdown();
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_controller_shutdown() -> Result<(), Box<dyn Error>> {
let songs = songs::get_all_songs(Path::new("assets/songs"))?;
let playlist = Playlist::new(
"playlist",
&config::Playlist::deserialize(Path::new("assets/playlist.yaml"))?,
songs.clone(),
)?;
let mut playlists = HashMap::new();
playlists.insert(
"all_songs".to_string(),
playlist::from_songs(songs.clone())?,
);
playlists.insert("playlist".to_string(), playlist);
let player = Player::new(
playlists,
"playlist".to_string(),
&config::Player::new(
vec![],
Some(config::Audio::new("mock-device")),
None,
None,
HashMap::new(),
"assets/songs",
),
None,
)?;
player.await_hardware_ready().await;
let driver = Arc::new(TestDriver::new(player.clone(), TestEvent::Unset));
let controller = super::Controller::new_from_drivers(vec![driver.clone()]);
controller.shutdown();
Ok(())
}
}