228 lines
11 KiB
Rust
228 lines
11 KiB
Rust
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 microphone‑stream failures will be logged at DEBUG");
|
||
} else {
|
||
debug!("❌🎤 reconnect failed: {e}");
|
||
}
|
||
delay = app_support::next_delay(delay);
|
||
}
|
||
}
|
||
|
||
queue.close();
|
||
tokio::time::sleep(delay).await;
|
||
}
|
||
}
|
||
}
|