82 lines
3.2 KiB
Rust
82 lines
3.2 KiB
Rust
//! 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 <host_id> <password> [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<String> = std::env::args().collect();
|
|
if a.len() < 3 {
|
|
eprintln!("usage: netdesk-voice-send <host_id> <password> [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::<SessionEvent>();
|
|
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(())
|
|
}
|