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
+38 -18
View File
@@ -15,8 +15,8 @@ use tokio::sync::broadcast;
use crate::{
audio_streams::{audio_stream_receiver, audio_stream_spawner},
models::{
CliArgs, ClientEvent, ClientInfo, InputAudioStreamsConsumers, InputAudioStreamsEncoders,
InputAudioStreamsInfo, InputAudioStreamsProducers,
AudioControl, CliArgs, ClientEvent, ClientInfo, InputAudioStreamsConsumers,
InputAudioStreamsEncoders, InputAudioStreamsInfo, InputAudioStreamsProducers,
},
tcp_connection::tcp_connection,
tui::Tui,
@@ -41,21 +41,18 @@ async fn main() {
let udp_socket = Arc::new(UdpSocket::bind("0.0.0.0:0").await.unwrap());
if cli.connect_with_defaults == None {
ratatui::run(|terminal| {
Tui::new().run(
terminal,
active_input_audio_streams_producers.clone(),
active_input_audio_streams_consumers.clone(),
active_input_audio_streams_encoders.clone(),
active_input_audio_streams_info.clone(),
udp_socket.clone(),
)
let audio_control: Arc<AudioControl> = Arc::new(AudioControl {
active_input_audio_streams_producers: active_input_audio_streams_producers.clone(),
active_input_audio_streams_consumers: active_input_audio_streams_consumers.clone(),
active_input_audio_streams_encoders: active_input_audio_streams_encoders.clone(),
active_input_audio_streams_info: active_input_audio_streams_info.clone(),
udp_socket: udp_socket.clone(),
});
} else {
if let Some(server_ip) = cli.connect_with_defaults {
let client_info: Arc<RwLock<ClientInfo>> = Arc::new(RwLock::new(ClientInfo {
client_id: 0,
server_address: cli.connect_with_defaults.unwrap(),
server_address: server_ip,
connected: false,
}));
@@ -63,10 +60,7 @@ async fn main() {
client_info.clone(),
global_event_tx.clone(),
global_event_tx.subscribe(),
active_input_audio_streams_producers.clone(),
active_input_audio_streams_consumers.clone(),
active_input_audio_streams_encoders.clone(),
active_input_audio_streams_info.clone(),
audio_control.clone(),
));
let _ = global_event_tx.send(ClientEvent::NewConnection);
@@ -112,4 +106,30 @@ async fn main() {
tokio::signal::ctrl_c().await.expect("failed to close");
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,
sync::{Arc, Mutex, RwLock},
};
use tokio::net::UdpSocket;
#[derive(Parser)]
#[command(version, about)]
pub struct CliArgs {
#[arg(short, long, required(false))]
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 {
DeviceSelect,
Blank,
Exit,
}
@@ -43,3 +48,11 @@ pub type InputAudioStreamsConsumers =
pub type InputAudioStreamsEncoders = Arc<RwLock<HashMap<AudioStreamKey, Mutex<Decoder>>>>;
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},
};
use crate::models::{
ClientEvent, ClientInfo, InputAudioStreamsConsumers, InputAudioStreamsEncoders,
InputAudioStreamsInfo, InputAudioStreamsProducers,
};
use crate::models::{AudioControl, ClientEvent, ClientInfo};
pub async fn tcp_connection(
client_info: Arc<RwLock<ClientInfo>>,
global_event_tx: Sender<ClientEvent>,
mut global_event_rx: Receiver<ClientEvent>,
active_input_audio_streams_producers: InputAudioStreamsProducers,
active_input_audio_streams_consumers: InputAudioStreamsConsumers,
active_input_audio_streams_encoders: InputAudioStreamsEncoders,
active_input_audio_streams_info: InputAudioStreamsInfo,
audio_control: Arc<AudioControl>,
) {
let server_address = {
let info = client_info.read().unwrap();
@@ -62,15 +56,15 @@ pub async fn tcp_connection(
println!("user disconnected: {}", user_id);
}
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();
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();
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();
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();
+122 -18
View File
@@ -1,5 +1,5 @@
use cpal::{
Device, SAMPLE_RATE_48K, StreamConfig,
Device, SAMPLE_RATE_48K, SampleFormat, StreamConfig,
traits::{DeviceTrait, HostTrait},
};
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind};
@@ -8,14 +8,17 @@ use ratatui::{
DefaultTerminal, Frame,
layout::{Constraint, Direction, Layout},
style::Style,
widgets::{Block, Row, Table, TableState},
widgets::{Block, Paragraph, Row, Table, TableState},
};
use std::{sync::Arc, vec};
use tokio::net::UdpSocket;
use std::{
sync::{Arc, RwLock},
vec,
};
use tokio::sync::broadcast::Sender;
use crate::models::{
InputAudioStreamsConsumers, InputAudioStreamsEncoders, InputAudioStreamsInfo,
InputAudioStreamsProducers, TuiScreen,
use crate::{
audio_streams::{audio_stream_receiver, audio_stream_spawner},
models::{AudioControl, ClientEvent, ClientInfo, TuiScreen},
};
pub struct Tui {
@@ -32,37 +35,86 @@ impl Tui {
pub fn run(
&mut self,
terminal: &mut DefaultTerminal,
active_input_audio_streams_producers: InputAudioStreamsProducers,
active_input_audio_streams_consumers: InputAudioStreamsConsumers,
active_input_audio_streams_encoders: InputAudioStreamsEncoders,
active_input_audio_streams_info: InputAudioStreamsInfo,
udp_socket: Arc<UdpSocket>,
audio_control: Arc<AudioControl>,
global_event_tx: Sender<ClientEvent>,
clinet_info: Arc<RwLock<ClientInfo>>,
) {
loop {
match self.current_screen {
TuiScreen::Exit => {
break;
}
TuiScreen::Blank => self.current_screen = BlankScreen::new().run(terminal),
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 {
exit: bool,
exit_screen: TuiScreen,
input_device_state: TableState,
output_device_state: TableState,
input_devices: Vec<(Device, StreamConfig)>,
output_devices: Vec<(Device, StreamConfig)>,
selected_selection: u8,
audio_control: Arc<AudioControl>,
global_event_tx: Sender<ClientEvent>,
clinet_info: Arc<RwLock<ClientInfo>>,
}
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 host = cpal::default_host();
@@ -76,6 +128,7 @@ impl DeviceSelectScreen {
if let Some(input_config) =
supported_config.try_with_sample_rate(SAMPLE_RATE_48K)
{
if input_config.sample_format() == SampleFormat::F32 {
input_devices.push((possible_input_device, input_config.into()));
break;
}
@@ -83,6 +136,7 @@ impl DeviceSelectScreen {
}
}
}
}
let mut output_devices: Vec<(Device, StreamConfig)> = Vec::new();
@@ -93,6 +147,7 @@ impl DeviceSelectScreen {
if let Some(output_config) =
supported_config.try_with_sample_rate(SAMPLE_RATE_48K)
{
if output_config.sample_format() == SampleFormat::F32 {
output_devices.push((possible_output_device, output_config.into()));
break;
}
@@ -100,14 +155,20 @@ impl DeviceSelectScreen {
}
}
}
}
Self {
exit: false,
exit_screen: TuiScreen::Exit,
input_device_state: TableState::default(),
output_device_state: TableState::default(),
input_devices: input_devices,
output_devices: output_devices,
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();
}
return TuiScreen::Exit;
return self.exit_screen;
}
fn draw(&mut self, frame: &mut Frame) {
@@ -231,8 +292,7 @@ impl DeviceSelectScreen {
KeyCode::Up => self.decrement_table(),
KeyCode::Left => self.selected_selection = 0,
KeyCode::Right => self.selected_selection = 1,
// KeyCode::Down => self.increment_list(),
// KeyCode::Enter => self.get_value(),
KeyCode::Enter => self.connect_devices(),
_ => {}
}
}
@@ -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) {
// self.table_state.selected();
// }
+1 -1
View File
@@ -11,7 +11,7 @@ use tokio::{
#[derive(Parser)]
#[command(version, about)]
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>,
}