App and AudioController now send messages as peers
This commit is contained in:
parent
f941d1fb66
commit
f555804f10
|
@ -4,101 +4,122 @@ use std::{
|
||||||
sync::{Arc, RwLock},
|
sync::{Arc, RwLock},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use tokio::{
|
||||||
audio_control::{AudioControl, AudioControlBackend},
|
sync::mpsc::{Receiver, Sender},
|
||||||
types::{AppError, TrackInfo},
|
task::JoinHandle,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct AppState {
|
use crate::{
|
||||||
device_list: Vec<String>,
|
audio_control::{AudioControl},
|
||||||
track_list: Vec<PathBuf>,
|
types::{AppError, AudioControlMessage, AudioStatusMessage, TrackInfo, TrackSpec, Volume},
|
||||||
|
};
|
||||||
audio_control: AudioControl,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
/*
|
struct AppState {
|
||||||
|
playing: bool,
|
||||||
|
|
||||||
|
device_list: Vec<String>,
|
||||||
|
track_list: Vec<PathBuf>,
|
||||||
|
|
||||||
|
track_status: Vec<TrackInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for AppState {
|
impl Default for AppState {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
device_list: vec![],
|
playing: false,
|
||||||
track_list: vec![
|
|
||||||
"/home/savanni/Music/Travis Savoie/RPG Toolkit Volume II/01 - A Day to Rebuild.mp3.mp3".to_owned(),
|
|
||||||
"/home/savanni/Music/Travis Savoie/RPG Toolkit Volume II/02 - Against the Clock.mp3.mp3".to_owned(),
|
|
||||||
"/home/savanni/Music/Travis Savoie/RPG Toolkit Volume II/03 - Alleyway Cutthroat.mp3.mp3".to_owned(),
|
|
||||||
"/home/savanni/Music/Travis Savoie/RPG Toolkit Volume II/04 - Beasts Of Legend.mp3.mp3".to_owned(),
|
|
||||||
"/home/savanni/Music/Travis Savoie/RPG Toolkit Volume II/05 - Books and Spellcrafting.mp3.mp3".to_owned(),
|
|
||||||
],
|
|
||||||
currently_playing: HashSet::default(),
|
|
||||||
|
|
||||||
audio_control: AudioControl::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct App {
|
|
||||||
internal: Arc<RwLock<AppState>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for App {
|
|
||||||
fn default() -> Self {
|
|
||||||
let internal = AppState {
|
|
||||||
device_list: vec![],
|
device_list: vec![],
|
||||||
track_list: vec![],
|
track_list: vec![],
|
||||||
audio_control: AudioControl::default(),
|
|
||||||
};
|
|
||||||
|
|
||||||
Self {
|
track_status: vec![],
|
||||||
internal: Arc::new(RwLock::new(internal)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct App {
|
||||||
|
state: Arc<RwLock<AppState>>,
|
||||||
|
|
||||||
|
audio_control: Sender<AudioControlMessage>,
|
||||||
|
|
||||||
|
listener: JoinHandle<()>,
|
||||||
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
fn new(backend: impl AudioControlBackend + 'static) -> App {
|
pub fn new(
|
||||||
let internal = AppState {
|
audio_control: Sender<AudioControlMessage>,
|
||||||
device_list: vec![],
|
mut audio_status: Receiver<AudioStatusMessage>,
|
||||||
track_list: vec![],
|
) -> App {
|
||||||
audio_control: AudioControl::new(backend),
|
let state = Arc::new(RwLock::new(AppState::default()));
|
||||||
};
|
|
||||||
|
|
||||||
App {
|
let listener = tokio::spawn({
|
||||||
internal: Arc::new(RwLock::new(internal)),
|
let state = state.clone();
|
||||||
|
async move {
|
||||||
|
println!("listener started");
|
||||||
|
loop {
|
||||||
|
match audio_status.recv().await {
|
||||||
|
Some(AudioStatusMessage::Playing) => {
|
||||||
|
state.write().unwrap().playing = true;
|
||||||
|
}
|
||||||
|
Some(AudioStatusMessage::Status(track_status)) => {
|
||||||
|
state.write().unwrap().track_status = track_status;
|
||||||
|
}
|
||||||
|
msg => println!("message received from audio controller: {:?}", msg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
state,
|
||||||
|
audio_control,
|
||||||
|
listener,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn playing(&self) -> bool {
|
pub fn playing(&self) -> bool {
|
||||||
self.internal.read().unwrap().audio_control.playing()
|
self.state.read().unwrap().playing
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_audio(&self, device: String) {
|
pub fn add_audio(&self, device: String) {
|
||||||
|
/*
|
||||||
let mut st = self.internal.write().unwrap();
|
let mut st = self.internal.write().unwrap();
|
||||||
st.device_list.push(device);
|
st.device_list.push(device);
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn audio_devices(&self) -> Vec<String> {
|
pub fn audio_devices(&self) -> Vec<String> {
|
||||||
|
/*
|
||||||
let st = self.internal.read().unwrap();
|
let st = self.internal.read().unwrap();
|
||||||
st.device_list.clone()
|
st.device_list.clone()
|
||||||
|
*/
|
||||||
|
vec![]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn enabled_tracks(&self) -> Vec<PathBuf> {
|
pub fn enabled_tracks(&self) -> Vec<PathBuf> {
|
||||||
let st = self.internal.read().unwrap();
|
let st = self.state.read().unwrap();
|
||||||
st.audio_control.tracks().into_iter().map(|ti| ti.path.clone()).collect()
|
st.track_status.iter().map(|ti| ti.path.clone()).collect()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn enable_track(&self, path: PathBuf) -> Result<(), AppError> {
|
pub async fn enable_track(&self, path: PathBuf) -> Result<(), AppError> {
|
||||||
|
self.audio_control
|
||||||
|
.send(AudioControlMessage::EnableTrack(TrackSpec {
|
||||||
|
path,
|
||||||
|
volume: Volume::try_from(1.0).unwrap(),
|
||||||
|
}))
|
||||||
|
.await
|
||||||
|
.expect("audio control send to succeed");
|
||||||
|
/*
|
||||||
let st = self.internal.write().unwrap();
|
let st = self.internal.write().unwrap();
|
||||||
st.audio_control.add_track(TrackInfo{ path })?;
|
st.audio_control.add_track(TrackSpec{ path })?;
|
||||||
|
*/
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn disable_track(&self, _track: &str) -> Result<(), AppError> {
|
pub async fn disable_track(&self, _track: &str) -> Result<(), AppError> {
|
||||||
/*
|
/*
|
||||||
let mut st = self.internal.write().unwrap();
|
let mut st = self.internal.write().unwrap();
|
||||||
if st.currently_playing.contains_key(track) {
|
if st.currently_playing.contains_key(track) {
|
||||||
|
@ -109,15 +130,19 @@ impl App {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn play(&self) -> Result<(), AppError> {
|
pub async fn play(&self) -> Result<(), AppError> {
|
||||||
|
/*
|
||||||
let st = self.internal.write().unwrap();
|
let st = self.internal.write().unwrap();
|
||||||
st.audio_control.play()?;
|
st.audio_control.play()?;
|
||||||
|
*/
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stop(&self) -> Result<(), AppError> {
|
pub async fn stop(&self) -> Result<(), AppError> {
|
||||||
|
/*
|
||||||
let st = self.internal.write().unwrap();
|
let st = self.internal.write().unwrap();
|
||||||
st.audio_control.stop()?;
|
st.audio_control.stop()?;
|
||||||
|
*/
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
|
|
|
@ -1,55 +1,117 @@
|
||||||
use std::path::PathBuf;
|
use std::{future::Future, path::PathBuf, time::Duration};
|
||||||
|
|
||||||
use cool_asserts::assert_matches;
|
use cool_asserts::assert_matches;
|
||||||
|
use tokio::sync::mpsc::{Receiver, Sender};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{App, AppError},
|
app::{App, AppError},
|
||||||
audio_control::MemoryBackend,
|
types::{AudioControlMessage, AudioStatusMessage, Progress, TrackInfo, TrackSpec, Volume},
|
||||||
};
|
};
|
||||||
|
|
||||||
fn with_memory_app<F>(f: F)
|
fn memory_app() -> (
|
||||||
where
|
App,
|
||||||
F: Fn(App),
|
Receiver<AudioControlMessage>,
|
||||||
{
|
Sender<AudioStatusMessage>,
|
||||||
let app = App::new(MemoryBackend::default());
|
) {
|
||||||
|
let (audio_control_tx, audio_control_rx) = tokio::sync::mpsc::channel(5);
|
||||||
|
let (audio_status_tx, audio_status_rx) = tokio::sync::mpsc::channel(5);
|
||||||
|
let app = App::new(audio_control_tx, audio_status_rx);
|
||||||
|
|
||||||
f(app)
|
(app, audio_control_rx, audio_status_tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn app_starts_in_stopped_state() {
|
async fn app_starts_in_stopped_state() {
|
||||||
with_memory_app(|app| {
|
let (app, _control_rx, _status_tx) = memory_app();
|
||||||
|
|
||||||
assert!(!app.playing());
|
assert!(!app.playing());
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn can_add_a_track_without_starting_playback() {
|
async fn can_add_a_track_without_starting_playback() {
|
||||||
with_memory_app(|app| {
|
let (app, mut control_rx, status_tx) = memory_app();
|
||||||
app.enable_track(PathBuf::from("/home/savanni/Music/Travis Savoie/RPG Toolkit Volume II/05 - Books and Spellcrafting.mp3.mp3")).expect("to enable a track");
|
let path_1 = PathBuf::from("/home/savanni/Music/Travis Savoie/RPG Toolkit Volume II/05 - Books and Spellcrafting.mp3.mp3");
|
||||||
assert!(!app.playing());
|
let path_2 = PathBuf::from(
|
||||||
app.enable_track(PathBuf::from(
|
|
||||||
"/home/savanni/Music/Travis Savoie/RPG Toolkit Volume II/01 - A Day to Rebuild.mp3.mp3",
|
"/home/savanni/Music/Travis Savoie/RPG Toolkit Volume II/01 - A Day to Rebuild.mp3.mp3",
|
||||||
))
|
);
|
||||||
|
|
||||||
|
{
|
||||||
|
app.enable_track(path_1.clone())
|
||||||
|
.await
|
||||||
.expect("to enable a track");
|
.expect("to enable a track");
|
||||||
assert!(!app.playing());
|
|
||||||
|
assert_matches!(control_rx.recv().await, Some(AudioControlMessage::EnableTrack(trackspec)) => {
|
||||||
|
assert_eq!(trackspec, TrackSpec{ path: path_1.clone(), volume: Volume::try_from(1.0).unwrap() });
|
||||||
|
});
|
||||||
|
|
||||||
|
status_tx
|
||||||
|
.send(AudioStatusMessage::Status(vec![TrackInfo {
|
||||||
|
path: path_1.clone(),
|
||||||
|
volume: Volume::try_from(1.0).unwrap(),
|
||||||
|
progress: Progress {
|
||||||
|
current: Duration::from_secs(0),
|
||||||
|
length: Duration::from_secs(100),
|
||||||
|
},
|
||||||
|
}]))
|
||||||
|
.await
|
||||||
|
.expect("status send to work");
|
||||||
|
|
||||||
|
tokio::time::sleep(Duration::from_millis(1)).await;
|
||||||
|
|
||||||
let tracks = app.enabled_tracks();
|
let tracks = app.enabled_tracks();
|
||||||
tracks.iter().find(|p| **p == PathBuf::from("/home/savanni/Music/Travis Savoie/RPG Toolkit Volume II/05 - Books and Spellcrafting.mp3.mp3")).expect("the books and spellcrafting track to be enabled");
|
tracks.iter().find(|p| **p == path_1);
|
||||||
tracks.iter().find(|p| **p == PathBuf::from("/home/savanni/Music/Travis Savoie/RPG Toolkit Volume II/01 - A Day to Rebuild.mp3.mp3")).expect("the day to rebuild track to be enabled");
|
assert!(!app.playing());
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
{
|
||||||
fn cannot_start_playback_with_no_tracks() {
|
app.enable_track(path_2.clone())
|
||||||
with_memory_app(|app| {
|
.await
|
||||||
assert_matches!(app.play(), Err(AppError::NoTracks));
|
.expect("to enable a track");
|
||||||
|
|
||||||
|
assert_matches!(control_rx.recv().await, Some(AudioControlMessage::EnableTrack(trackspec)) => {
|
||||||
|
assert_eq!(trackspec, TrackSpec{ path: path_2.clone(), volume: Volume::try_from(1.0).unwrap() });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
status_tx
|
||||||
|
.send(AudioStatusMessage::Status(vec![
|
||||||
|
TrackInfo {
|
||||||
|
path: path_1.clone(),
|
||||||
|
volume: Volume::try_from(1.0).unwrap(),
|
||||||
|
progress: Progress {
|
||||||
|
current: Duration::from_secs(0),
|
||||||
|
length: Duration::from_secs(100),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TrackInfo {
|
||||||
|
path: path_2.clone(),
|
||||||
|
volume: Volume::try_from(1.0).unwrap(),
|
||||||
|
progress: Progress {
|
||||||
|
current: Duration::from_secs(0),
|
||||||
|
length: Duration::from_secs(100),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]))
|
||||||
|
.await
|
||||||
|
.expect("status send to work");
|
||||||
|
|
||||||
|
tokio::time::sleep(Duration::from_millis(1)).await;
|
||||||
|
let tracks = app.enabled_tracks();
|
||||||
|
tracks.iter().find(|p| **p == path_1);
|
||||||
|
tracks.iter().find(|p| **p == path_2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn can_add_a_track_during_playback() {
|
async fn cannot_start_playback_with_no_tracks() {
|
||||||
with_memory_app(|app| {
|
let (app, control_rx, status_tx) = memory_app();
|
||||||
|
// assert_matches!(app.play(), Err(AppError::NoTracks));
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn can_add_a_track_during_playback() {
|
||||||
|
let (app, control_rx, status_tx) = memory_app();
|
||||||
|
/*
|
||||||
app.enable_track(PathBuf::from("/home/savanni/Music/Travis Savoie/RPG Toolkit Volume II/05 - Books and Spellcrafting.mp3.mp3")).expect("to enable a track");
|
app.enable_track(PathBuf::from("/home/savanni/Music/Travis Savoie/RPG Toolkit Volume II/05 - Books and Spellcrafting.mp3.mp3")).expect("to enable a track");
|
||||||
app.play().expect("to start playback");
|
app.play().expect("to start playback");
|
||||||
|
|
||||||
|
@ -59,5 +121,6 @@ fn can_add_a_track_during_playback() {
|
||||||
"/home/savanni/Music/Travis Savoie/RPG Toolkit Volume II/01 - A Day to Rebuild.mp3.mp3",
|
"/home/savanni/Music/Travis Savoie/RPG Toolkit Volume II/01 - A Day to Rebuild.mp3.mp3",
|
||||||
))
|
))
|
||||||
.expect("to enable another track during playback");
|
.expect("to enable another track during playback");
|
||||||
});
|
*/
|
||||||
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashSet,
|
collections::HashMap,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::{Arc, RwLock},
|
sync::{Arc, RwLock}, time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use gstreamer::{prelude::*, ClockTime, MessageType, MessageView};
|
use gstreamer::{prelude::*, ClockTime, MessageType, MessageView};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use tokio::sync::mpsc::{Receiver, Sender};
|
||||||
|
|
||||||
use crate::types::TrackInfo;
|
use crate::types::{AudioControlMessage, AudioStatusMessage, TrackSpec};
|
||||||
|
|
||||||
#[derive(Debug, Error, PartialEq)]
|
#[derive(Debug, Error, PartialEq)]
|
||||||
pub enum AudioError {
|
pub enum AudioError {
|
||||||
|
@ -19,27 +20,75 @@ pub enum AudioError {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AudioControl {
|
pub struct AudioControl {
|
||||||
backend: Arc<RwLock<dyn AudioControlBackend>>,
|
control_rx: Receiver<AudioControlMessage>,
|
||||||
|
status_tx: Sender<AudioStatusMessage>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
impl Default for AudioControl {
|
impl Default for AudioControl {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new(GStreamerBackend::default())
|
Self::new(GStreamerBackend::default())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
impl AudioControl {
|
impl AudioControl {
|
||||||
pub fn new(backend: impl AudioControlBackend + 'static) -> Self {
|
pub fn new() -> (
|
||||||
|
Self,
|
||||||
|
Sender<AudioControlMessage>,
|
||||||
|
Receiver<AudioStatusMessage>,
|
||||||
|
) {
|
||||||
|
let (control_tx, control_rx) = tokio::sync::mpsc::channel(5);
|
||||||
|
let (status_tx, status_rx) = tokio::sync::mpsc::channel(5);
|
||||||
|
(
|
||||||
Self {
|
Self {
|
||||||
backend: Arc::new(RwLock::new(backend)),
|
control_rx,
|
||||||
|
status_tx,
|
||||||
|
},
|
||||||
|
control_tx,
|
||||||
|
status_rx,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn listen(&mut self) {
|
||||||
|
while let Some(msg) = self.control_rx.recv().await {
|
||||||
|
match msg {
|
||||||
|
AudioControlMessage::Play => {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
AudioControlMessage::Pause => {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
AudioControlMessage::EnableTrack(_) => {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
AudioControlMessage::DisableTrack(_) => {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
AudioControlMessage::ReportStatus => {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn report(&self) {
|
||||||
|
loop {
|
||||||
|
self.status_tx
|
||||||
|
.send(AudioStatusMessage::Status(vec![]))
|
||||||
|
.await
|
||||||
|
.expect("to successfully send a message");
|
||||||
|
tokio::time::sleep(Duration::from_secs(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
pub fn playing(&self) -> bool {
|
pub fn playing(&self) -> bool {
|
||||||
self.backend.read().unwrap().playing()
|
self.backend.read().unwrap().playing()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tracks(&self) -> Vec<TrackInfo> {
|
pub fn tracks(&self) -> Vec<TrackSpec> {
|
||||||
self.backend.read().unwrap().tracks()
|
self.backend.read().unwrap().tracks()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,7 +100,7 @@ impl AudioControl {
|
||||||
self.backend.read().unwrap().stop()
|
self.backend.read().unwrap().stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_track(&self, track: TrackInfo) -> Result<(), AudioError> {
|
pub fn add_track(&self, track: TrackSpec) -> Result<(), AudioError> {
|
||||||
self.backend.write().unwrap().add_track(track)
|
self.backend.write().unwrap().add_track(track)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,27 +108,27 @@ impl AudioControl {
|
||||||
pub trait AudioControlBackend: Send + Sync {
|
pub trait AudioControlBackend: Send + Sync {
|
||||||
fn playing(&self) -> bool;
|
fn playing(&self) -> bool;
|
||||||
|
|
||||||
fn tracks(&self) -> Vec<TrackInfo>;
|
fn tracks(&self) -> Vec<TrackSpec>;
|
||||||
|
|
||||||
fn play(&self) -> Result<(), AudioError>;
|
fn play(&self) -> Result<(), AudioError>;
|
||||||
|
|
||||||
fn stop(&self) -> Result<(), AudioError>;
|
fn stop(&self) -> Result<(), AudioError>;
|
||||||
|
|
||||||
fn add_track(&mut self, track: TrackInfo) -> Result<(), AudioError>;
|
fn add_track(&mut self, track: TrackSpec) -> Result<(), AudioError>;
|
||||||
|
|
||||||
fn remove_track(&mut self, track: TrackInfo) -> Result<(), AudioError>;
|
fn remove_track(&mut self, track: TrackSpec) -> Result<(), AudioError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct MemoryBackend {
|
pub struct MemoryBackend {
|
||||||
playing: Arc<RwLock<bool>>,
|
playing: Arc<RwLock<bool>>,
|
||||||
tracks: HashSet<TrackInfo>,
|
tracks: HashMap<PathBuf, TrackSpec>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for MemoryBackend {
|
impl Default for MemoryBackend {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
playing: Arc::new(RwLock::new(false)),
|
playing: Arc::new(RwLock::new(false)),
|
||||||
tracks: HashSet::new(),
|
tracks: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -89,8 +138,11 @@ impl AudioControlBackend for MemoryBackend {
|
||||||
*self.playing.read().unwrap()
|
*self.playing.read().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tracks(&self) -> Vec<TrackInfo> {
|
fn tracks(&self) -> Vec<TrackSpec> {
|
||||||
|
/*
|
||||||
self.tracks.iter().cloned().collect()
|
self.tracks.iter().cloned().collect()
|
||||||
|
*/
|
||||||
|
vec![]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn play(&self) -> Result<(), AudioError> {
|
fn play(&self) -> Result<(), AudioError> {
|
||||||
|
@ -117,13 +169,17 @@ impl AudioControlBackend for MemoryBackend {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_track(&mut self, track: TrackInfo) -> Result<(), AudioError> {
|
fn add_track(&mut self, track: TrackSpec) -> Result<(), AudioError> {
|
||||||
|
/*
|
||||||
self.tracks.insert(track);
|
self.tracks.insert(track);
|
||||||
|
*/
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_track(&mut self, track: TrackInfo) -> Result<(), AudioError> {
|
fn remove_track(&mut self, track: TrackSpec) -> Result<(), AudioError> {
|
||||||
|
/*
|
||||||
self.tracks.remove(&track);
|
self.tracks.remove(&track);
|
||||||
|
*/
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -216,7 +272,7 @@ impl AudioControlBackend for GStreamerBackend {
|
||||||
*self.playing.read().unwrap()
|
*self.playing.read().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tracks(&self) -> Vec<TrackInfo> {
|
fn tracks(&self) -> Vec<TrackSpec> {
|
||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -242,7 +298,7 @@ impl AudioControlBackend for GStreamerBackend {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_track(&mut self, track: TrackInfo) -> Result<(), AudioError> {
|
fn add_track(&mut self, track: TrackSpec) -> Result<(), AudioError> {
|
||||||
let source = gstreamer::ElementFactory::find("filesrc")
|
let source = gstreamer::ElementFactory::find("filesrc")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.load()
|
.load()
|
||||||
|
@ -283,8 +339,9 @@ impl AudioControlBackend for GStreamerBackend {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_track(&mut self, _path: TrackInfo) -> Result<(), AudioError> {
|
fn remove_track(&mut self, _path: TrackSpec) -> Result<(), AudioError> {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
/* Need to run EOS through to a probe on the trailing end of the volume element */
|
/* Need to run EOS through to a probe on the trailing end of the volume element */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::{net::{Ipv6Addr, SocketAddrV6}, path::PathBuf};
|
use std::{net::{Ipv6Addr, SocketAddrV6}, path::PathBuf, sync::Arc};
|
||||||
|
|
||||||
use app::App;
|
use app::App;
|
||||||
use pipewire::{context::Context, main_loop::MainLoop};
|
use pipewire::{context::Context, main_loop::MainLoop};
|
||||||
|
@ -15,23 +15,23 @@ struct PlayTrackParams {
|
||||||
track_name: String,
|
track_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn server_main(state: App) {
|
async fn server_main(app: Arc<App>) {
|
||||||
let localhost: Ipv6Addr = "::1".parse().unwrap();
|
let localhost: Ipv6Addr = "::1".parse().unwrap();
|
||||||
let server_addr = SocketAddrV6::new(localhost, 3001, 0, 0);
|
let server_addr = SocketAddrV6::new(localhost, 3001, 0, 0);
|
||||||
|
|
||||||
let root = warp::path!().map(|| "ok".to_string());
|
let root = warp::path!().map(|| "ok".to_string());
|
||||||
let list_output_devices = warp::path!("output_devices").map({
|
let list_output_devices = warp::path!("output_devices").map({
|
||||||
let state = state.clone();
|
let app = app.clone();
|
||||||
move || {
|
move || {
|
||||||
let devices = state.audio_devices();
|
let devices = app.audio_devices();
|
||||||
serde_json::to_string(&devices).unwrap()
|
serde_json::to_string(&devices).unwrap()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
let list_tracks = warp::path!("tracks").map({
|
let list_tracks = warp::path!("tracks").map({
|
||||||
let state = state.clone();
|
let app = app.clone();
|
||||||
move || serde_json::to_string(&state.tracks()).unwrap()
|
move || serde_json::to_string(&app.tracks()).unwrap()
|
||||||
});
|
});
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -39,9 +39,9 @@ async fn server_main(state: App) {
|
||||||
.and(warp::path!("playing"))
|
.and(warp::path!("playing"))
|
||||||
.and(warp::body::json())
|
.and(warp::body::json())
|
||||||
.map({
|
.map({
|
||||||
let state = state.clone();
|
let app = app.clone();
|
||||||
move |params: PlayTrackParams| {
|
move |params: PlayTrackParams| {
|
||||||
let _ = state.enable_track(PathBuf::from(params.track_name));
|
let _ = app.enable_track(PathBuf::from(params.track_name));
|
||||||
"".to_owned()
|
"".to_owned()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -50,32 +50,32 @@ async fn server_main(state: App) {
|
||||||
.and(warp::path!("playing"))
|
.and(warp::path!("playing"))
|
||||||
.and(warp::body::json())
|
.and(warp::body::json())
|
||||||
.map({
|
.map({
|
||||||
let state = state.clone();
|
let app = app.clone();
|
||||||
move |params: PlayTrackParams| {
|
move |params: PlayTrackParams| {
|
||||||
let _ = state.disable_track(¶ms.track_name);
|
let _ = app.disable_track(¶ms.track_name);
|
||||||
"".to_owned()
|
"".to_owned()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let play_all = warp::put().and(warp::path!("playing")).map({
|
let play_all = warp::put().and(warp::path!("playing")).map({
|
||||||
let state = state.clone();
|
let app = app.clone();
|
||||||
move || {
|
move || {
|
||||||
let _ = state.play();
|
let _ = app.play();
|
||||||
"".to_owned()
|
"".to_owned()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let stop_all = warp::delete().and(warp::path!("playing")).map({
|
let stop_all = warp::delete().and(warp::path!("playing")).map({
|
||||||
let state = state.clone();
|
let app = app.clone();
|
||||||
move || {
|
move || {
|
||||||
let _ = state.stop();
|
let _ = app.stop();
|
||||||
"".to_owned()
|
"".to_owned()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let now_playing = warp::path!("playing").map({
|
let now_playing = warp::path!("playing").map({
|
||||||
let state = state.clone();
|
let app = app.clone();
|
||||||
move || serde_json::to_string(&state.playing()).unwrap()
|
move || serde_json::to_string(&app.playing()).unwrap()
|
||||||
});
|
});
|
||||||
|
|
||||||
let routes = root
|
let routes = root
|
||||||
|
@ -90,15 +90,16 @@ async fn server_main(state: App) {
|
||||||
serve(routes).run(server_addr).await;
|
serve(routes).run(server_addr).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_add_audio_device(state: App, props: &pipewire::spa::utils::dict::DictRef) {
|
fn handle_add_audio_device(app: App, props: &pipewire::spa::utils::dict::DictRef) {
|
||||||
if props.get("media.class") == Some("Audio/Sink") {
|
if props.get("media.class") == Some("Audio/Sink") {
|
||||||
if let Some(device_name) = props.get("node.description") {
|
if let Some(device_name) = props.get("node.description") {
|
||||||
state.add_audio(device_name.to_owned());
|
app.add_audio(device_name.to_owned());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pipewire_loop(state: App) -> Result<(), Box<dyn std::error::Error>> {
|
/*
|
||||||
|
fn pipewire_loop(app: App) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let mainloop = MainLoop::new(None)?;
|
let mainloop = MainLoop::new(None)?;
|
||||||
let context = Context::new(&mainloop)?;
|
let context = Context::new(&mainloop)?;
|
||||||
let core = context.connect(None)?;
|
let core = context.connect(None)?;
|
||||||
|
@ -107,10 +108,10 @@ fn pipewire_loop(state: App) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let _listener = registry
|
let _listener = registry
|
||||||
.add_listener_local()
|
.add_listener_local()
|
||||||
.global({
|
.global({
|
||||||
let state = state.clone();
|
let app = app.clone();
|
||||||
move |global_data| {
|
move |global_data| {
|
||||||
if let Some(props) = global_data.props {
|
if let Some(props) = global_data.props {
|
||||||
handle_add_audio_device(state.clone(), props);
|
handle_add_audio_device(app.clone(), props);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -120,20 +121,29 @@ fn pipewire_loop(state: App) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
fn pipewire_main(state: App) {
|
fn pipewire_main(state: App) {
|
||||||
pipewire_loop(state).expect("pipewire should not error");
|
pipewire_loop(state).expect("pipewire should not error");
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
gstreamer::init();
|
gstreamer::init();
|
||||||
let state = App::default();
|
|
||||||
|
|
||||||
|
let (audio_control_tx, audio_control_rx) = tokio::sync::mpsc::channel(5);
|
||||||
|
let (audio_status_tx, audio_status_rx) = tokio::sync::mpsc::channel(5);
|
||||||
|
|
||||||
|
let app = Arc::new(App::new(audio_control_tx, audio_status_rx));
|
||||||
|
|
||||||
|
/*
|
||||||
spawn_blocking({
|
spawn_blocking({
|
||||||
let state = state.clone();
|
let app = app.clone();
|
||||||
move || pipewire_main(state)
|
move || pipewire_main(state)
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
server_main(state.clone()).await;
|
server_main(app.clone()).await;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::path::PathBuf;
|
use std::{ops::Deref, path::PathBuf, time::Duration};
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ pub enum AppError {
|
||||||
NoTracks,
|
NoTracks,
|
||||||
|
|
||||||
#[error("Operation is invalid in the current state")]
|
#[error("Operation is invalid in the current state")]
|
||||||
InvalidState
|
InvalidState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<AudioError> for AppError {
|
impl From<AudioError> for AppError {
|
||||||
|
@ -22,9 +22,64 @@ impl From<AudioError> for AppError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Hash, PartialEq, PartialOrd, Eq)]
|
#[derive(Clone, Debug, PartialEq, PartialOrd)]
|
||||||
pub struct TrackInfo {
|
pub struct TrackSpec {
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
|
pub volume: Volume,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Progress {
|
||||||
|
pub current: Duration,
|
||||||
|
pub length: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error, PartialEq)]
|
||||||
|
pub enum VolumeError {
|
||||||
|
#[error("The specified volume is out of range and must be between 0.0 and 1.0")]
|
||||||
|
OutOfRange,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
|
||||||
|
pub struct Volume(f32);
|
||||||
|
|
||||||
|
impl TryFrom<f32> for Volume {
|
||||||
|
type Error = VolumeError;
|
||||||
|
fn try_from(val: f32) -> Result<Self, Self::Error> {
|
||||||
|
if val < 0. || val > 1. {
|
||||||
|
return Err(VolumeError::OutOfRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self(val))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Volume> for f32 {
|
||||||
|
fn from(val: Volume) -> f32 {
|
||||||
|
val.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum AudioControlMessage {
|
||||||
|
Play,
|
||||||
|
Pause,
|
||||||
|
EnableTrack(TrackSpec),
|
||||||
|
DisableTrack(PathBuf),
|
||||||
|
ReportStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct TrackInfo {
|
||||||
|
pub path: PathBuf,
|
||||||
|
pub volume: Volume,
|
||||||
|
pub progress: Progress,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum AudioStatusMessage {
|
||||||
|
Playing,
|
||||||
|
Pausing,
|
||||||
|
Status(Vec<TrackInfo>),
|
||||||
|
AudioError(AudioError),
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue