//! NetDesk voice send-test — drives the EXACT path the native viewer's voice //! button uses (`netdesk_host::audio::run_audio` with source = microphone, and //! `Session::voice` per Opus packet), headlessly, so we can prove viewer->host //! two-way voice end to end against a scratch host. Developed by Akronlogic. //! //! Usage: netdesk-voice-send [ws_url] [secs] //! Watch the HOST log for `voice: rx N pkts from viewer mic, recent peak X`. use netdesk_core::{connect, ConnectConfig, SessionEvent}; use std::sync::atomic::{AtomicBool, AtomicU8, AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; use std::time::Duration; use tokio::sync::mpsc; #[tokio::main] async fn main() -> anyhow::Result<()> { let a: Vec = std::env::args().collect(); if a.len() < 3 { eprintln!("usage: netdesk-voice-send [ws_url] [secs]"); std::process::exit(2); } let secs: u64 = a.get(4).and_then(|s| s.parse().ok()).unwrap_or(6); let cfg = ConnectConfig { host_id: a[1].clone(), password: a[2].clone(), ws_url: a.get(3).cloned().unwrap_or_else(|| "wss://netdesk.space/ws".into()), totp: String::new(), takeover: false, }; let (tx, mut rx) = mpsc::unbounded_channel::(); eprintln!("[voice-send] connecting to host {} …", cfg.host_id); let session = connect(cfg, tx).await.map_err(|e| anyhow::anyhow!("connect: {e}"))?; eprintln!("[voice-send] connected; capturing microphone and streaming CHAN_VOICE for {secs}s"); // Mirror viewer::toggle_voice exactly: share the Session behind a Mutex and // feed each captured Opus packet to Session::voice from the capture thread. let sess = Arc::new(Mutex::new(session)); let stop = Arc::new(AtomicBool::new(false)); let sent = Arc::new(AtomicUsize::new(0)); #[cfg(windows)] { let s2 = sess.clone(); let st2 = stop.clone(); let n2 = sent.clone(); std::thread::spawn(move || { let src = Arc::new(AtomicU8::new(2)); // 2 = microphone (default capture device) netdesk_host::audio::run_audio(st2, || true, src, move |opus| { s2.lock().unwrap().voice(&opus); n2.fetch_add(1, Ordering::Relaxed); }); }); } #[cfg(not(windows))] { eprintln!("[voice-send] microphone capture is Windows-only"); } // Drain session events (ignore them) while the capture thread streams. let deadline = tokio::time::Instant::now() + Duration::from_secs(secs); loop { tokio::select! { _ = tokio::time::sleep_until(deadline) => break, ev = rx.recv() => match ev { Some(SessionEvent::Ended { .. }) | None => break, Some(_) => {} } } } stop.store(true, Ordering::Relaxed); let n = sent.load(Ordering::Relaxed); sess.lock().unwrap().close(); println!("VOICE_SEND_DONE sent={n} packets"); if n == 0 { eprintln!("[voice-send] WARNING: 0 packets — no microphone/capture device?"); std::process::exit(3); } Ok(()) }