lesavka/client/src/app.rs

343 lines
15 KiB
Rust
Raw Normal View History

2025-06-15 20:19:27 -05:00
#![forbid(unsafe_code)]
2025-06-17 20:54:31 -05:00
2025-06-11 00:37:01 -05:00
use anyhow::Result;
use std::time::Duration;
2025-06-30 19:41:35 -05:00
use std::sync::Arc;
2025-07-01 22:38:56 -05:00
use std::sync::atomic::{AtomicUsize, Ordering};
2025-06-24 23:48:06 -05:00
use tokio::sync::broadcast;
use tokio_stream::{wrappers::BroadcastStream, StreamExt};
2025-06-26 17:26:28 -05:00
use tonic::{transport::Channel, Request};
2025-06-30 11:38:57 -05:00
use tracing::{error, trace, debug, info, warn};
2025-06-24 23:48:06 -05:00
use winit::{
2025-06-26 17:26:28 -05:00
event::Event,
2025-06-28 03:34:48 -05:00
event_loop::{EventLoopBuilder, ControlFlow},
2025-06-25 09:21:39 -05:00
platform::wayland::EventLoopBuilderExtWayland,
2025-06-24 23:48:06 -05:00
};
2025-06-15 20:19:27 -05:00
2025-06-26 17:26:28 -05:00
use lesavka_common::lesavka::{
2025-07-01 10:23:51 -05:00
relay_client::RelayClient, KeyboardReport,
MonitorRequest, MouseReport, VideoPacket, AudioPacket
2025-06-26 17:26:28 -05:00
};
2025-06-17 08:17:23 -05:00
2025-06-29 03:46:34 -05:00
use crate::{input::inputs::InputAggregator,
2025-06-30 19:35:38 -05:00
input::microphone::MicrophoneCapture,
2025-07-03 15:22:30 -05:00
input::camera::CameraCapture,
2025-06-29 03:46:34 -05:00
output::video::MonitorWindow,
output::audio::AudioOut};
2025-06-08 04:11:58 -05:00
2025-06-23 07:18:26 -05:00
pub struct LesavkaClientApp {
2025-06-11 00:37:01 -05:00
aggregator: Option<InputAggregator>,
2025-06-08 04:11:58 -05:00
server_addr: String,
2025-06-08 13:11:31 -05:00
dev_mode: bool,
2025-06-17 08:17:23 -05:00
kbd_tx: broadcast::Sender<KeyboardReport>,
mou_tx: broadcast::Sender<MouseReport>,
2025-06-08 04:11:58 -05:00
}
2025-06-23 07:18:26 -05:00
impl LesavkaClientApp {
2025-06-08 04:11:58 -05:00
pub fn new() -> Result<Self> {
2025-06-26 17:26:28 -05:00
let dev_mode = std::env::var("LESAVKA_DEV_MODE").is_ok();
2025-06-17 20:54:31 -05:00
let server_addr = std::env::args()
2025-06-08 04:11:58 -05:00
.nth(1)
2025-06-26 14:05:23 -05:00
.or_else(|| std::env::var("LESAVKA_SERVER_ADDR").ok())
2025-06-17 20:54:31 -05:00
.unwrap_or_else(|| "http://127.0.0.1:50051".into());
2025-06-15 20:19:27 -05:00
2025-06-26 17:26:28 -05:00
let (kbd_tx, _) = broadcast::channel(1024);
let (mou_tx, _) = broadcast::channel(4096);
2025-06-17 20:54:31 -05:00
2025-06-17 08:17:23 -05:00
let mut agg = InputAggregator::new(dev_mode, kbd_tx.clone(), mou_tx.clone());
2025-06-26 17:26:28 -05:00
agg.init()?; // grab devices immediately
2025-06-17 20:54:31 -05:00
2025-07-03 15:22:30 -05:00
let cam = if std::env::var("LESAVKA_CAM_DISABLE").is_ok() {
None
} else {
Some(Arc::new(CameraCapture::new(
std::env::var("LESAVKA_CAM_SOURCE").ok().as_deref()
)?))
};
2025-06-26 17:26:28 -05:00
Ok(Self { aggregator: Some(agg), server_addr, dev_mode, kbd_tx, mou_tx })
2025-06-08 04:11:58 -05:00
}
pub async fn run(&mut self) -> Result<()> {
2025-06-26 17:26:28 -05:00
/*────────── persistent gRPC channels ──────────*/
let hid_ep = Channel::from_shared(self.server_addr.clone())?
2025-06-26 14:05:23 -05:00
.tcp_nodelay(true)
2025-07-01 10:23:51 -05:00
.concurrency_limit(4)
2025-06-26 14:05:23 -05:00
.http2_keep_alive_interval(Duration::from_secs(15))
.connect_lazy();
2025-06-26 17:26:28 -05:00
let vid_ep = Channel::from_shared(self.server_addr.clone())?
2025-06-27 22:51:50 -05:00
.initial_connection_window_size(4<<20)
.initial_stream_window_size(4<<20)
2025-06-26 14:05:23 -05:00
.tcp_nodelay(true)
.connect_lazy();
2025-06-26 17:26:28 -05:00
/*────────── input aggregator task ─────────────*/
2025-06-17 20:54:31 -05:00
let aggregator = self.aggregator.take().expect("InputAggregator present");
2025-06-26 17:26:28 -05:00
let agg_task = tokio::spawn(async move {
let mut a = aggregator;
a.run().await
2025-06-17 22:02:33 -05:00
});
2025-06-08 04:11:58 -05:00
2025-06-26 17:26:28 -05:00
/*────────── HID streams (never return) ────────*/
2025-06-26 14:05:23 -05:00
let kbd_loop = self.stream_loop_keyboard(hid_ep.clone());
let mou_loop = self.stream_loop_mouse(hid_ep.clone());
2025-06-17 08:17:23 -05:00
2025-06-29 22:39:17 -05:00
/*───────── optional 300s auto-exit in dev mode */
2025-06-17 20:54:31 -05:00
let suicide = async {
2025-06-08 18:11:44 -05:00
if self.dev_mode {
2025-06-29 22:39:17 -05:00
tokio::time::sleep(Duration::from_secs(300)).await;
2025-06-28 15:45:35 -05:00
warn!("💀 dev-mode timeout");
2025-06-17 20:54:31 -05:00
std::process::exit(0);
2025-06-26 17:26:28 -05:00
} else {
std::future::pending::<()>().await
}
2025-06-08 13:35:23 -05:00
};
2025-06-08 13:11:31 -05:00
2025-06-26 17:26:28 -05:00
/*────────── video rendering thread (winit) ────*/
2025-06-21 05:21:57 -05:00
let (video_tx, mut video_rx) = tokio::sync::mpsc::unbounded_channel::<VideoPacket>();
std::thread::spawn(move || {
2025-06-29 22:39:17 -05:00
gtk::init().expect("GTK initialisation failed");
2025-06-26 17:26:28 -05:00
let el = EventLoopBuilder::<()>::new().with_any_thread(true).build().unwrap();
2025-06-29 03:46:34 -05:00
let win0 = MonitorWindow::new(0).expect("win0");
let win1 = MonitorWindow::new(1).expect("win1");
2025-06-26 17:26:28 -05:00
2025-06-28 03:34:48 -05:00
let _ = el.run(move |_: Event<()>, _elwt| {
_elwt.set_control_flow(ControlFlow::WaitUntil(
std::time::Instant::now() + std::time::Duration::from_millis(16)));
2025-06-28 00:05:13 -05:00
static CNT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
2025-06-28 03:34:48 -05:00
static DUMP_CNT: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
2025-06-21 05:21:57 -05:00
while let Ok(pkt) = video_rx.try_recv() {
2025-06-28 00:05:13 -05:00
CNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if CNT.load(std::sync::atomic::Ordering::Relaxed) % 300 == 0 {
2025-06-30 19:35:38 -05:00
debug!("🎥 received {} video packets", CNT.load(std::sync::atomic::Ordering::Relaxed));
2025-06-28 00:05:13 -05:00
}
2025-06-28 03:34:48 -05:00
let n = DUMP_CNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if n % 120 == 0 {
let eye = if pkt.id == 0 { "l" } else { "r" };
let path = format!("/tmp/eye{eye}-cli-{n:05}.h264");
std::fs::write(&path, &pkt.data).ok();
}
2025-06-21 05:21:57 -05:00
match pkt.id {
0 => win0.push_packet(pkt),
1 => win1.push_packet(pkt),
_ => {}
}
}
});
});
2025-06-26 17:26:28 -05:00
/*────────── start video gRPC pullers ──────────*/
2025-06-29 03:46:34 -05:00
let ep_video = vid_ep.clone();
tokio::spawn(Self::video_loop(ep_video, video_tx));
/*────────── audio renderer & puller ───────────*/
let audio_out = AudioOut::new()?;
let ep_audio = vid_ep.clone();
tokio::spawn(Self::audio_loop(ep_audio, audio_out));
2025-06-21 05:21:57 -05:00
2025-07-03 16:08:30 -05:00
/*────────── camera & mic tasks ───────────────*/
let cam = Arc::new(CameraCapture::new(
std::env::var("LESAVKA_CAM_SOURCE").ok().as_deref()
)?);
tokio::spawn(Self::cam_loop(vid_ep.clone(), cam));
2025-06-30 19:35:38 -05:00
2025-07-03 16:08:30 -05:00
let mic = Arc::new(MicrophoneCapture::new()?);
tokio::spawn(Self::mic_loop(vid_ep.clone(), mic));
2025-07-03 15:22:30 -05:00
2025-06-26 17:26:28 -05:00
/*────────── central reactor ───────────────────*/
2025-06-08 13:35:23 -05:00
tokio::select! {
2025-06-26 17:26:28 -05:00
_ = kbd_loop => { warn!("⚠️⌨️ keyboard stream finished"); },
_ = mou_loop => { warn!("⚠️🖱️ mouse stream finished"); },
_ = suicide => { /* handled above */ },
r = agg_task => {
match r {
Ok(Ok(())) => warn!("input aggregator terminated cleanly"),
Ok(Err(e)) => error!("input aggregator error: {e:?}"),
Err(join_err) => error!("aggregator task panicked: {join_err:?}"),
}
std::process::exit(1);
2025-06-08 18:11:44 -05:00
}
2025-06-08 04:11:58 -05:00
}
2025-06-26 17:26:28 -05:00
// The branches above either loop forever or exit the process; this
// point is unreachable but satisfies the type checker.
#[allow(unreachable_code)]
Ok(())
2025-06-08 18:11:44 -05:00
}
2025-06-17 20:54:31 -05:00
/*──────────────── keyboard stream ───────────────*/
2025-06-26 14:05:23 -05:00
async fn stream_loop_keyboard(&self, ep: Channel) {
2025-06-08 18:11:44 -05:00
loop {
2025-06-30 19:35:38 -05:00
info!("⌨️🤙 Keyboard dial {}", self.server_addr);
2025-06-26 14:05:23 -05:00
let mut cli = RelayClient::new(ep.clone());
2025-06-26 18:29:14 -05:00
let outbound = BroadcastStream::new(self.kbd_tx.subscribe())
.filter_map(|r| r.ok());
2025-06-26 15:12:23 -05:00
match cli.stream_keyboard(Request::new(outbound)).await {
Ok(mut resp) => {
2025-06-26 18:29:14 -05:00
while let Some(msg) = resp.get_mut().message().await.transpose() {
2025-06-30 19:35:38 -05:00
if let Err(e) = msg { warn!("⌨️ server err: {e}"); break; }
2025-06-26 18:29:14 -05:00
}
2025-06-26 15:12:23 -05:00
}
2025-07-01 19:42:34 -05:00
Err(e) => warn!("❌⌨️ connect failed: {e}"),
2025-06-26 15:12:23 -05:00
}
2025-06-26 18:29:14 -05:00
tokio::time::sleep(Duration::from_secs(1)).await; // retry
2025-06-17 08:17:23 -05:00
}
}
2025-06-17 20:54:31 -05:00
/*──────────────── mouse stream ──────────────────*/
2025-06-26 14:05:23 -05:00
async fn stream_loop_mouse(&self, ep: Channel) {
2025-06-17 08:17:23 -05:00
loop {
2025-06-30 19:35:38 -05:00
info!("🖱️🤙 Mouse dial {}", self.server_addr);
2025-06-26 14:05:23 -05:00
let mut cli = RelayClient::new(ep.clone());
2025-06-26 18:29:14 -05:00
let outbound = BroadcastStream::new(self.mou_tx.subscribe())
.filter_map(|r| r.ok());
2025-06-26 15:12:23 -05:00
match cli.stream_mouse(Request::new(outbound)).await {
Ok(mut resp) => {
2025-06-26 18:29:14 -05:00
while let Some(msg) = resp.get_mut().message().await.transpose() {
2025-06-30 19:35:38 -05:00
if let Err(e) = msg { warn!("🖱️ server err: {e}"); break; }
2025-06-26 18:29:14 -05:00
}
2025-06-26 15:12:23 -05:00
}
2025-07-01 19:42:34 -05:00
Err(e) => warn!("❌🖱️ connect failed: {e}"),
2025-06-26 15:12:23 -05:00
}
2025-06-26 18:29:14 -05:00
tokio::time::sleep(Duration::from_secs(1)).await;
2025-06-08 18:11:44 -05:00
}
}
2025-06-17 20:54:31 -05:00
2025-06-21 05:21:57 -05:00
/*──────────────── monitor stream ────────────────*/
2025-06-26 16:17:31 -05:00
async fn video_loop(
ep: Channel,
tx: tokio::sync::mpsc::UnboundedSender<VideoPacket>,
) {
for monitor_id in 0..=1 {
2025-06-26 17:26:28 -05:00
let ep = ep.clone();
let tx = tx.clone();
2025-06-26 16:17:31 -05:00
tokio::spawn(async move {
loop {
let mut cli = RelayClient::new(ep.clone());
2025-06-26 14:05:23 -05:00
let req = MonitorRequest { id: monitor_id, max_bitrate: 6_000 };
2025-06-26 16:17:31 -05:00
match cli.capture_video(Request::new(req)).await {
Ok(mut stream) => {
2025-07-01 12:11:52 -05:00
debug!("🎥🏁 cli video{monitor_id}: stream opened");
2025-06-28 03:34:48 -05:00
while let Some(res) = stream.get_mut().message().await.transpose() {
match res {
Ok(pkt) => {
2025-07-01 12:11:52 -05:00
trace!("🎥📥 cli video{monitor_id}: got {}bytes", pkt.data.len());
2025-06-28 03:34:48 -05:00
if tx.send(pkt).is_err() {
2025-06-30 11:38:57 -05:00
warn!("⚠️🎥 cli video{monitor_id}: GUI thread gone");
2025-06-28 03:34:48 -05:00
break;
}
}
Err(e) => {
2025-06-30 11:38:57 -05:00
error!("❌🎥 cli video{monitor_id}: gRPC error: {e}");
2025-06-28 03:34:48 -05:00
break;
}
2025-06-26 16:17:31 -05:00
}
2025-06-21 05:21:57 -05:00
}
2025-06-30 11:38:57 -05:00
warn!("⚠️🎥 cli video{monitor_id}: stream ended");
2025-06-21 05:21:57 -05:00
}
2025-06-28 03:46:39 -05:00
Err(e) => error!("❌🎥 video {monitor_id}: {e}"),
2025-06-21 05:21:57 -05:00
}
2025-06-26 16:17:31 -05:00
tokio::time::sleep(Duration::from_secs(1)).await;
2025-06-21 05:21:57 -05:00
}
2025-06-26 16:17:31 -05:00
});
2025-06-21 05:21:57 -05:00
}
2025-06-26 14:05:23 -05:00
}
2025-06-29 03:46:34 -05:00
/*──────────────── audio stream ───────────────*/
async fn audio_loop(ep: Channel, out: AudioOut) {
loop {
let mut cli = RelayClient::new(ep.clone());
let req = MonitorRequest { id: 0, max_bitrate: 0 };
match cli.capture_audio(Request::new(req)).await {
Ok(mut stream) => {
while let Some(res) = stream.get_mut().message().await.transpose() {
if let Ok(pkt) = res { out.push(pkt); }
}
}
2025-07-01 19:42:34 -05:00
Err(e) => tracing::warn!("❌🔊 audio stream err: {e}"),
2025-06-29 03:46:34 -05:00
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
}
2025-06-30 19:35:38 -05:00
/*──────────────── mic stream ─────────────────*/
async fn mic_loop(ep: Channel, mic: Arc<MicrophoneCapture>) {
2025-07-01 22:38:56 -05:00
static FAIL_CNT: AtomicUsize = AtomicUsize::new(0);
2025-06-30 19:35:38 -05:00
loop {
let mut cli = RelayClient::new(ep.clone());
2025-07-01 10:23:51 -05:00
// 1. create a Tokio MPSC channel
let (tx, rx) = tokio::sync::mpsc::channel::<AudioPacket>(256);
let (stop_tx, stop_rx) = std::sync::mpsc::channel::<()>();
// 2. spawn a real thread that does the blocking `pull()`
2025-06-30 19:35:38 -05:00
let mic_clone = mic.clone();
2025-07-01 10:23:51 -05:00
std::thread::spawn(move || {
while stop_rx.try_recv().is_err() {
2025-06-30 19:35:38 -05:00
if let Some(pkt) = mic_clone.pull() {
2025-07-01 10:23:51 -05:00
trace!("🎤📤 cli {} bytes → gRPC", pkt.data.len());
let _ = tx.blocking_send(pkt);
2025-06-30 19:35:38 -05:00
}
}
2025-07-01 10:23:51 -05:00
});
// 3. turn `rx` into an async stream for gRPC
let outbound = tokio_stream::wrappers::ReceiverStream::new(rx);
2025-06-30 19:35:38 -05:00
2025-07-01 10:23:51 -05:00
match cli.stream_microphone(Request::new(outbound)).await {
2025-06-30 19:35:38 -05:00
Ok(mut resp) => {
2025-07-01 10:23:51 -05:00
while resp.get_mut().message().await.transpose().is_some() {}
2025-06-30 19:35:38 -05:00
}
2025-07-01 22:38:56 -05:00
Err(e) => {
// first failure → warn, subsequent ones → debug
if FAIL_CNT.fetch_add(1, Ordering::Relaxed) == 0 {
warn!("❌🎤 connect failed: {e}");
warn!("⚠️🎤 further microphonestream failures will be logged at DEBUG");
} else {
debug!("❌🎤 reconnect failed: {e}");
}
}
2025-06-30 19:35:38 -05:00
}
2025-07-01 10:23:51 -05:00
let _ = stop_tx.send(());
tokio::time::sleep(Duration::from_secs(1)).await;
2025-06-30 19:35:38 -05:00
}
2025-07-03 15:22:30 -05:00
}
/*──────────────── cam stream ───────────────────*/
async fn cam_loop(ep: Channel, cam: Arc<CameraCapture>) {
loop {
let mut cli = RelayClient::new(ep.clone());
2025-07-03 16:08:30 -05:00
let (tx, rx) = tokio::sync::mpsc::channel::<VideoPacket>(256);
2025-07-03 15:22:30 -05:00
std::thread::spawn({
let cam = cam.clone();
move || {
while let Some(pkt) = cam.pull() {
// TRACE every 120 frames only
static CNT: std::sync::atomic::AtomicU64 =
std::sync::atomic::AtomicU64::new(0);
let n = CNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if n < 10 || n % 120 == 0 {
tracing::trace!("📸 cli frame#{n} {} B", pkt.data.len());
}
2025-07-03 16:08:30 -05:00
let _ = tx.blocking_send(pkt);
2025-07-03 15:22:30 -05:00
}
}
});
2025-07-03 16:08:30 -05:00
let outbound = tokio_stream::wrappers::ReceiverStream::new(rx);
if let Err(e) = cli.stream_camera(Request::new(outbound)).await {
tracing::warn!("❌📸 connect failed: {e}");
2025-07-03 15:22:30 -05:00
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
}
2025-06-08 04:11:58 -05:00
}