228 lines
11 KiB
Rust
Raw Normal View History

impl LesavkaClientApp {
/*──────────────── mic stream ─────────────────*/
#[cfg(not(coverage))]
/// Keeps `voice_loop` explicit because it sits on the live uplink path, where stale media must be dropped instead of queued into latency.
/// Inputs are the typed parameters; output is the return value or side effect.
async fn voice_loop(
ep: Channel,
initial_source: Option<String>,
telemetry: crate::uplink_telemetry::UplinkTelemetryHandle,
media_controls: crate::live_media_control::LiveMediaControls,
pause_when_camera_active: bool,
) {
let mut delay = Duration::from_secs(1);
static FAIL_CNT: AtomicUsize = AtomicUsize::new(0);
loop {
let state = media_controls.refresh();
if pause_when_camera_active && state.camera {
tokio::time::sleep(Duration::from_millis(100)).await;
continue;
}
if !state.microphone {
telemetry.record_enabled(false);
tokio::time::sleep(Duration::from_millis(100)).await;
continue;
}
let microphone_source_choice = state.microphone_source.clone();
let active_source = microphone_source_choice.resolve(initial_source.as_deref());
let active_audio_codec = state
.audio_codec
.resolve(lesavka_common::audio_transport::UpstreamAudioCodec::Opus);
let active_noise_suppression = state.noise_suppression.resolve(false);
let use_default_source = matches!(
microphone_source_choice,
crate::live_media_control::MediaDeviceChoice::Auto
) && active_source.is_none();
let setup_source = active_source.clone();
let result = tokio::task::spawn_blocking(move || {
if use_default_source {
MicrophoneCapture::new_default_source_options(
active_audio_codec,
active_noise_suppression,
)
} else {
MicrophoneCapture::new_with_source_options(
setup_source.as_deref(),
active_audio_codec,
active_noise_suppression,
)
}
})
.await;
let mic = match result {
Ok(Ok(mic)) => Arc::new(mic),
Ok(Err(err)) => {
telemetry.record_disconnect(format!("microphone uplink setup failed: {err:#}"));
warn!(
"🎤 microphone uplink setup failed for {:?}: {err:#}",
active_source.as_deref().unwrap_or("auto")
);
abort_if_required_media_source_failed("microphone", "🎤", active_source.as_deref(), &err);
delay = app_support::next_delay(delay);
tokio::time::sleep(delay).await;
continue;
}
Err(err) => {
telemetry.record_disconnect(format!("microphone uplink setup task failed: {err}"));
warn!("🎤 microphone uplink setup task failed before StreamMicrophone could start: {err}");
abort_if_required_media_source_failed(
"microphone",
"🎤",
active_source.as_deref(),
&err,
);
delay = app_support::next_delay(delay);
tokio::time::sleep(delay).await;
continue;
}
};
telemetry.record_reconnect_attempt();
let mut cli = RelayClient::new(ep.clone());
let queue = crate::uplink_fresh_queue::FreshPacketQueue::new(AUDIO_UPLINK_QUEUE);
let drop_log = Arc::new(std::sync::Mutex::new(UplinkDropLogLimiter::new(
"microphone",
"🎤",
)));
let queue_stream = queue.clone();
let telemetry_stream = telemetry.clone();
let drop_log_stream = Arc::clone(&drop_log);
let outbound = async_stream::stream! {
loop {
let next = queue_stream.pop_fresh().await;
if next.dropped_stale > 0 {
telemetry_stream.record_stale_drop(next.dropped_stale);
log_uplink_drop(
&drop_log_stream,
UplinkDropReason::Stale,
next.dropped_stale,
next.queue_depth,
duration_ms(next.delivery_age),
);
}
if let Some(mut packet) = next.packet {
telemetry_stream.record_streamed(
queue_depth_u32(next.queue_depth),
duration_ms(next.delivery_age),
);
attach_audio_queue_metadata(
&mut packet,
next.queue_depth,
next.delivery_age,
);
yield packet;
continue;
}
break;
}
};
match cli.stream_microphone(Request::new(outbound)).await {
Ok(mut resp) => {
let (stop_tx, stop_rx) = std::sync::mpsc::channel::<()>();
let mic_clone = mic.clone();
let telemetry_thread = telemetry.clone();
let queue_thread = queue.clone();
let drop_log_thread = Arc::clone(&drop_log);
let media_controls_thread = media_controls.clone();
let initial_source_thread = initial_source.clone();
let active_source_thread = active_source.clone();
let active_audio_codec_thread = active_audio_codec;
let active_noise_suppression_thread = active_noise_suppression;
let mic_worker = std::thread::spawn(move || {
let mut paused = false;
while stop_rx.try_recv().is_err() {
let state = media_controls_thread.refresh();
let desired_source = state
.microphone_source
.resolve(initial_source_thread.as_deref());
let desired_audio_codec = state
.audio_codec
.resolve(lesavka_common::audio_transport::UpstreamAudioCodec::Opus);
let desired_noise_suppression =
state.noise_suppression.resolve(false);
if pause_when_camera_active && state.camera {
tracing::info!(
"🎤 microphone-only uplink yielding to bundled webcam A/V"
);
break;
}
if desired_source != active_source_thread
|| desired_audio_codec != active_audio_codec_thread
|| desired_noise_suppression != active_noise_suppression_thread
{
tracing::info!(
from = active_source_thread.as_deref().unwrap_or("auto"),
to = desired_source.as_deref().unwrap_or("auto"),
from_codec = active_audio_codec_thread.as_id(),
to_codec = desired_audio_codec.as_id(),
from_noise_suppression = active_noise_suppression_thread,
to_noise_suppression = desired_noise_suppression,
"🎤 microphone route changed; restarting live uplink pipeline"
);
break;
}
if !state.microphone {
if !paused {
telemetry_thread.record_enabled(false);
tracing::info!("🎤 microphone uplink soft-paused");
paused = true;
}
std::thread::sleep(Duration::from_millis(20));
continue;
}
if paused {
telemetry_thread.record_enabled(true);
tracing::info!("🎤 microphone uplink resumed");
paused = false;
}
if let Some(mut pkt) = mic_clone.pull() {
trace!("🎤📤 cli {} bytes → gRPC", pkt.data.len());
let enqueue_age = stamp_audio_timing_metadata_at_enqueue(&mut pkt);
let stats = queue_thread.push(pkt, enqueue_age);
if stats.dropped_queue_full > 0 {
telemetry_thread.record_queue_full_drop(stats.dropped_queue_full);
log_uplink_drop(
&drop_log_thread,
UplinkDropReason::QueueFull,
stats.dropped_queue_full,
stats.queue_depth,
duration_ms(enqueue_age),
);
}
telemetry_thread.record_enqueue(
queue_depth_u32(stats.queue_depth),
duration_ms(enqueue_age),
0.0,
);
}
}
});
delay = Duration::from_secs(1);
telemetry.record_connected();
while resp.get_mut().message().await.transpose().is_some() {}
telemetry.record_disconnect("microphone uplink stream ended");
queue.close();
let _ = stop_tx.send(());
let _ = mic_worker.join();
}
Err(e) => {
telemetry.record_disconnect(format!("microphone uplink connect failed: {e}"));
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}");
}
delay = app_support::next_delay(delay);
}
}
queue.close();
tokio::time::sleep(delay).await;
}
}
}