almost working device selector

This commit is contained in:
2026-09-05 01:47:51 +02:00
parent 2dcd07b75a
commit 6b47abcd89
5 changed files with 186 additions and 55 deletions
+39 -19
View File
@@ -15,8 +15,8 @@ use tokio::sync::broadcast;
use crate::{ use crate::{
audio_streams::{audio_stream_receiver, audio_stream_spawner}, audio_streams::{audio_stream_receiver, audio_stream_spawner},
models::{ models::{
CliArgs, ClientEvent, ClientInfo, InputAudioStreamsConsumers, InputAudioStreamsEncoders, AudioControl, CliArgs, ClientEvent, ClientInfo, InputAudioStreamsConsumers,
InputAudioStreamsInfo, InputAudioStreamsProducers, InputAudioStreamsEncoders, InputAudioStreamsInfo, InputAudioStreamsProducers,
}, },
tcp_connection::tcp_connection, tcp_connection::tcp_connection,
tui::Tui, tui::Tui,
@@ -41,21 +41,18 @@ async fn main() {
let udp_socket = Arc::new(UdpSocket::bind("0.0.0.0:0").await.unwrap()); let udp_socket = Arc::new(UdpSocket::bind("0.0.0.0:0").await.unwrap());
if cli.connect_with_defaults == None { let audio_control: Arc<AudioControl> = Arc::new(AudioControl {
ratatui::run(|terminal| { active_input_audio_streams_producers: active_input_audio_streams_producers.clone(),
Tui::new().run( active_input_audio_streams_consumers: active_input_audio_streams_consumers.clone(),
terminal, active_input_audio_streams_encoders: active_input_audio_streams_encoders.clone(),
active_input_audio_streams_producers.clone(), active_input_audio_streams_info: active_input_audio_streams_info.clone(),
active_input_audio_streams_consumers.clone(), udp_socket: udp_socket.clone(),
active_input_audio_streams_encoders.clone(), });
active_input_audio_streams_info.clone(),
udp_socket.clone(), if let Some(server_ip) = cli.connect_with_defaults {
)
});
} else {
let client_info: Arc<RwLock<ClientInfo>> = Arc::new(RwLock::new(ClientInfo { let client_info: Arc<RwLock<ClientInfo>> = Arc::new(RwLock::new(ClientInfo {
client_id: 0, client_id: 0,
server_address: cli.connect_with_defaults.unwrap(), server_address: server_ip,
connected: false, connected: false,
})); }));
@@ -63,10 +60,7 @@ async fn main() {
client_info.clone(), client_info.clone(),
global_event_tx.clone(), global_event_tx.clone(),
global_event_tx.subscribe(), global_event_tx.subscribe(),
active_input_audio_streams_producers.clone(), audio_control.clone(),
active_input_audio_streams_consumers.clone(),
active_input_audio_streams_encoders.clone(),
active_input_audio_streams_info.clone(),
)); ));
let _ = global_event_tx.send(ClientEvent::NewConnection); let _ = global_event_tx.send(ClientEvent::NewConnection);
@@ -112,4 +106,30 @@ async fn main() {
tokio::signal::ctrl_c().await.expect("failed to close"); tokio::signal::ctrl_c().await.expect("failed to close");
println!("shutting down"); println!("shutting down");
} }
if let Some(server_ip) = cli.server_ip {
let client_info: Arc<RwLock<ClientInfo>> = Arc::new(RwLock::new(ClientInfo {
client_id: 0,
server_address: server_ip,
connected: false,
}));
tokio::spawn(tcp_connection(
client_info.clone(),
global_event_tx.clone(),
global_event_tx.subscribe(),
audio_control.clone(),
));
let _ = global_event_tx.send(ClientEvent::NewConnection);
ratatui::run(|terminal| {
Tui::new().run(
terminal,
audio_control.clone(),
global_event_tx.clone(),
client_info.clone(),
)
});
}
} }
+14 -1
View File
@@ -6,17 +6,22 @@ use std::{
collections::HashMap, collections::HashMap,
sync::{Arc, Mutex, RwLock}, sync::{Arc, Mutex, RwLock},
}; };
use tokio::net::UdpSocket;
#[derive(Parser)] #[derive(Parser)]
#[command(version, about)] #[command(version, about)]
pub struct CliArgs { pub struct CliArgs {
#[arg(short, long, required(false))] #[arg(short, long, required(false))]
pub connect_with_defaults: Option<String>, pub connect_with_defaults: Option<String>,
#[arg(short, long, required(false))]
pub server_ip: Option<String>,
} }
#[derive(Debug)] #[derive(Debug, Clone, Copy)]
pub enum TuiScreen { pub enum TuiScreen {
DeviceSelect, DeviceSelect,
Blank,
Exit, Exit,
} }
@@ -43,3 +48,11 @@ pub type InputAudioStreamsConsumers =
pub type InputAudioStreamsEncoders = Arc<RwLock<HashMap<AudioStreamKey, Mutex<Decoder>>>>; pub type InputAudioStreamsEncoders = Arc<RwLock<HashMap<AudioStreamKey, Mutex<Decoder>>>>;
pub type InputAudioStreamsInfo = Arc<RwLock<HashMap<AudioStreamKey, AudioStreamInfo>>>; pub type InputAudioStreamsInfo = Arc<RwLock<HashMap<AudioStreamKey, AudioStreamInfo>>>;
pub struct AudioControl {
pub active_input_audio_streams_producers: InputAudioStreamsProducers,
pub active_input_audio_streams_consumers: InputAudioStreamsConsumers,
pub active_input_audio_streams_encoders: InputAudioStreamsEncoders,
pub active_input_audio_streams_info: InputAudioStreamsInfo,
pub udp_socket: Arc<UdpSocket>,
}
+6 -12
View File
@@ -8,19 +8,13 @@ use tokio::{
sync::broadcast::{Receiver, Sender}, sync::broadcast::{Receiver, Sender},
}; };
use crate::models::{ use crate::models::{AudioControl, ClientEvent, ClientInfo};
ClientEvent, ClientInfo, InputAudioStreamsConsumers, InputAudioStreamsEncoders,
InputAudioStreamsInfo, InputAudioStreamsProducers,
};
pub async fn tcp_connection( pub async fn tcp_connection(
client_info: Arc<RwLock<ClientInfo>>, client_info: Arc<RwLock<ClientInfo>>,
global_event_tx: Sender<ClientEvent>, global_event_tx: Sender<ClientEvent>,
mut global_event_rx: Receiver<ClientEvent>, mut global_event_rx: Receiver<ClientEvent>,
active_input_audio_streams_producers: InputAudioStreamsProducers, audio_control: Arc<AudioControl>,
active_input_audio_streams_consumers: InputAudioStreamsConsumers,
active_input_audio_streams_encoders: InputAudioStreamsEncoders,
active_input_audio_streams_info: InputAudioStreamsInfo,
) { ) {
let server_address = { let server_address = {
let info = client_info.read().unwrap(); let info = client_info.read().unwrap();
@@ -62,15 +56,15 @@ pub async fn tcp_connection(
println!("user disconnected: {}", user_id); println!("user disconnected: {}", user_id);
} }
TcpResponseData::AudioStreamsFullUpdate { audio_stream_info } => { TcpResponseData::AudioStreamsFullUpdate { audio_stream_info } => {
let mut producer_map = active_input_audio_streams_producers.lock().unwrap(); let mut producer_map = audio_control.active_input_audio_streams_producers.lock().unwrap();
producer_map.clear(); producer_map.clear();
let mut consumer_map = active_input_audio_streams_consumers.lock().unwrap(); let mut consumer_map = audio_control.active_input_audio_streams_consumers.lock().unwrap();
consumer_map.clear(); consumer_map.clear();
let mut encoder_map = active_input_audio_streams_encoders.write().unwrap(); let mut encoder_map = audio_control.active_input_audio_streams_encoders.write().unwrap();
encoder_map.clear(); encoder_map.clear();
let mut info_map = active_input_audio_streams_info.write().unwrap(); let mut info_map = audio_control.active_input_audio_streams_info.write().unwrap();
info_map.clear(); info_map.clear();
+126 -22
View File
@@ -1,5 +1,5 @@
use cpal::{ use cpal::{
Device, SAMPLE_RATE_48K, StreamConfig, Device, SAMPLE_RATE_48K, SampleFormat, StreamConfig,
traits::{DeviceTrait, HostTrait}, traits::{DeviceTrait, HostTrait},
}; };
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind}; use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind};
@@ -8,14 +8,17 @@ use ratatui::{
DefaultTerminal, Frame, DefaultTerminal, Frame,
layout::{Constraint, Direction, Layout}, layout::{Constraint, Direction, Layout},
style::Style, style::Style,
widgets::{Block, Row, Table, TableState}, widgets::{Block, Paragraph, Row, Table, TableState},
}; };
use std::{sync::Arc, vec}; use std::{
use tokio::net::UdpSocket; sync::{Arc, RwLock},
vec,
};
use tokio::sync::broadcast::Sender;
use crate::models::{ use crate::{
InputAudioStreamsConsumers, InputAudioStreamsEncoders, InputAudioStreamsInfo, audio_streams::{audio_stream_receiver, audio_stream_spawner},
InputAudioStreamsProducers, TuiScreen, models::{AudioControl, ClientEvent, ClientInfo, TuiScreen},
}; };
pub struct Tui { pub struct Tui {
@@ -32,37 +35,86 @@ impl Tui {
pub fn run( pub fn run(
&mut self, &mut self,
terminal: &mut DefaultTerminal, terminal: &mut DefaultTerminal,
active_input_audio_streams_producers: InputAudioStreamsProducers, audio_control: Arc<AudioControl>,
active_input_audio_streams_consumers: InputAudioStreamsConsumers, global_event_tx: Sender<ClientEvent>,
active_input_audio_streams_encoders: InputAudioStreamsEncoders, clinet_info: Arc<RwLock<ClientInfo>>,
active_input_audio_streams_info: InputAudioStreamsInfo,
udp_socket: Arc<UdpSocket>,
) { ) {
loop { loop {
match self.current_screen { match self.current_screen {
TuiScreen::Exit => { TuiScreen::Exit => {
break; break;
} }
TuiScreen::Blank => self.current_screen = BlankScreen::new().run(terminal),
TuiScreen::DeviceSelect => { TuiScreen::DeviceSelect => {
self.current_screen = DeviceSelectScreen::new().run(terminal); self.current_screen = DeviceSelectScreen::new(
audio_control.clone(),
global_event_tx.clone(),
clinet_info.clone(),
)
.run(terminal);
} }
} }
} }
} }
} }
#[derive(Debug)] struct BlankScreen {
exit: bool,
}
impl BlankScreen {
pub fn new() -> Self {
Self { exit: false }
}
pub fn run(&mut self, terminal: &mut DefaultTerminal) -> TuiScreen {
while !self.exit {
terminal
.draw(|frame| self.draw(frame))
.expect("failed to draw");
self.handle_events();
}
return TuiScreen::Exit;
}
fn draw(&mut self, frame: &mut Frame) {
frame.render_widget(Paragraph::new("q - quit"), frame.area());
}
fn handle_events(&mut self) {
match event::read().expect("failed to read event") {
Event::Key(key_event) if key_event.kind == KeyEventKind::Press => {
match key_event.code {
KeyCode::Char('q') => self.exit = true,
_ => {}
}
}
_ => {}
}
}
}
struct DeviceSelectScreen { struct DeviceSelectScreen {
exit: bool, exit: bool,
exit_screen: TuiScreen,
input_device_state: TableState, input_device_state: TableState,
output_device_state: TableState, output_device_state: TableState,
input_devices: Vec<(Device, StreamConfig)>, input_devices: Vec<(Device, StreamConfig)>,
output_devices: Vec<(Device, StreamConfig)>, output_devices: Vec<(Device, StreamConfig)>,
selected_selection: u8, selected_selection: u8,
audio_control: Arc<AudioControl>,
global_event_tx: Sender<ClientEvent>,
clinet_info: Arc<RwLock<ClientInfo>>,
} }
impl DeviceSelectScreen { impl DeviceSelectScreen {
pub fn new() -> Self { pub fn new(
audio_control: Arc<AudioControl>,
global_event_tx: Sender<ClientEvent>,
clinet_info: Arc<RwLock<ClientInfo>>,
) -> Self {
let _print_gag = Gag::stderr().ok(); let _print_gag = Gag::stderr().ok();
let host = cpal::default_host(); let host = cpal::default_host();
@@ -76,8 +128,10 @@ impl DeviceSelectScreen {
if let Some(input_config) = if let Some(input_config) =
supported_config.try_with_sample_rate(SAMPLE_RATE_48K) supported_config.try_with_sample_rate(SAMPLE_RATE_48K)
{ {
input_devices.push((possible_input_device, input_config.into())); if input_config.sample_format() == SampleFormat::F32 {
break; input_devices.push((possible_input_device, input_config.into()));
break;
}
} }
} }
} }
@@ -93,8 +147,10 @@ impl DeviceSelectScreen {
if let Some(output_config) = if let Some(output_config) =
supported_config.try_with_sample_rate(SAMPLE_RATE_48K) supported_config.try_with_sample_rate(SAMPLE_RATE_48K)
{ {
output_devices.push((possible_output_device, output_config.into())); if output_config.sample_format() == SampleFormat::F32 {
break; output_devices.push((possible_output_device, output_config.into()));
break;
}
} }
} }
} }
@@ -103,11 +159,16 @@ impl DeviceSelectScreen {
Self { Self {
exit: false, exit: false,
exit_screen: TuiScreen::Exit,
input_device_state: TableState::default(), input_device_state: TableState::default(),
output_device_state: TableState::default(), output_device_state: TableState::default(),
input_devices: input_devices, input_devices: input_devices,
output_devices: output_devices, output_devices: output_devices,
selected_selection: 0, selected_selection: 0,
audio_control: audio_control,
global_event_tx: global_event_tx,
clinet_info: clinet_info,
} }
} }
@@ -119,7 +180,7 @@ impl DeviceSelectScreen {
self.handle_events(); self.handle_events();
} }
return TuiScreen::Exit; return self.exit_screen;
} }
fn draw(&mut self, frame: &mut Frame) { fn draw(&mut self, frame: &mut Frame) {
@@ -231,8 +292,7 @@ impl DeviceSelectScreen {
KeyCode::Up => self.decrement_table(), KeyCode::Up => self.decrement_table(),
KeyCode::Left => self.selected_selection = 0, KeyCode::Left => self.selected_selection = 0,
KeyCode::Right => self.selected_selection = 1, KeyCode::Right => self.selected_selection = 1,
// KeyCode::Down => self.increment_list(), KeyCode::Enter => self.connect_devices(),
// KeyCode::Enter => self.get_value(),
_ => {} _ => {}
} }
} }
@@ -257,6 +317,50 @@ impl DeviceSelectScreen {
} }
} }
fn connect_devices(&mut self) {
if let (Some(input_device_row), Some(output_device_row)) = (
self.input_device_state.selected(),
self.output_device_state.selected(),
) {
let (input_device, input_config) = self
.input_devices
.get(input_device_row)
.expect("failed to get input config");
let (output_device, output_config) = self
.output_devices
.get(output_device_row)
.expect("failed to get output config");
tokio::spawn(audio_stream_receiver(
output_device.clone(),
output_config.clone(),
self.audio_control
.active_input_audio_streams_producers
.clone(),
self.audio_control
.active_input_audio_streams_consumers
.clone(),
self.audio_control
.active_input_audio_streams_encoders
.clone(),
self.audio_control.active_input_audio_streams_info.clone(),
self.audio_control.udp_socket.clone(),
));
tokio::spawn(audio_stream_spawner(
input_device.clone(),
input_config.clone(),
self.global_event_tx.clone(),
self.global_event_tx.subscribe(),
self.audio_control.udp_socket.clone(),
self.clinet_info.clone(),
));
self.exit_screen = TuiScreen::Blank;
self.exit = true;
}
}
// fn get_value(&mut self) { // fn get_value(&mut self) {
// self.table_state.selected(); // self.table_state.selected();
// } // }
+1 -1
View File
@@ -11,7 +11,7 @@ use tokio::{
#[derive(Parser)] #[derive(Parser)]
#[command(version, about)] #[command(version, about)]
struct Cli { struct Cli {
#[arg(short, long, default_value("0.0.0.0:8080"))] #[arg(short, long, default_value("0.0.0.0:6767"))]
server_ip: Option<String>, server_ip: Option<String>,
} }