impl LesavkaClientApp {
    /*──────────────── monitor stream ────────────────*/
    #[cfg(not(coverage))]
    async fn video_loop(ep: Channel, tx: tokio::sync::mpsc::Sender<VideoPacket>) {
        let max_bitrate = std::env::var("LESAVKA_VIDEO_MAX_KBIT")
            .ok()
            .and_then(|v| v.parse::<u32>().ok())
            .unwrap_or(6_000);
        for monitor_id in 0..=1 {
            let ep = ep.clone();
            let tx = tx.clone();
            tokio::spawn(async move {
                let mut dropped_packets = 0_u64;
                let mut wait_for_idr = false;
                loop {
                    let mut cli = RelayClient::new(ep.clone());
                    let req = MonitorRequest {
                        id: monitor_id,
                        max_bitrate,
                        requested_width: 0,
                        requested_height: 0,
                        requested_fps: 0,
                        source_id: None,
                    };
                    match cli.capture_video(Request::new(req)).await {
                        Ok(mut stream) => {
                            debug!("🎥🏁 cli video{monitor_id}: stream opened");
                            while let Some(res) = stream.get_mut().message().await.transpose() {
                                match res {
                                    Ok(pkt) => {
                                        trace!(
                                            "🎥📥 cli video{monitor_id}: got {} bytes",
                                            pkt.data.len()
                                        );
                                        let is_idr = crate::video_support::contains_idr(&pkt.data);
                                        if wait_for_idr && !is_idr {
                                            dropped_packets = dropped_packets.saturating_add(1);
                                            continue;
                                        }
                                        match tx.try_send(pkt) {
                                            Ok(()) => {
                                                if is_idr {
                                                    wait_for_idr = false;
                                                }
                                            }
                                            Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
                                                warn!("⚠️🎥 cli video{monitor_id}: GUI thread gone");
                                                break;
                                            }
                                            Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => {
                                                dropped_packets = dropped_packets.saturating_add(1);
                                                wait_for_idr = true;
                                                if dropped_packets <= 8
                                                    || dropped_packets.is_multiple_of(300)
                                                {
                                                    debug!(
                                                        dropped_packets,
                                                        "🎥🪂 cli video{monitor_id}: dropping stale packet; waiting for IDR before resuming decoder"
                                                    );
                                                }
                                            }
                                        }
                                    }
                                    Err(e) => {
                                        error!("❌🎥 cli video{monitor_id}: gRPC error: {e}");
                                        break;
                                    }
                                }
                            }
                            warn!("⚠️🎥 cli video{monitor_id}: stream ended");
                        }
                        Err(e) => error!("❌🎥 video {monitor_id}: {e}"),
                    }
                    tokio::time::sleep(Duration::from_secs(1)).await;
                }
            });
        }
    }

    /*──────────────── audio stream ───────────────*/
    #[cfg(not(coverage))]
    async fn audio_loop(
        ep: Channel,
        media_controls: crate::live_media_control::LiveMediaControls,
    ) {
        let mut consecutive_source_failures = 0_u32;
        let mut last_usb_recovery_at: Option<Instant> = None;
        let mut delay = Duration::from_secs(1);
        let mut audio_failure_log = AudioFailureLogLimiter::default();
        let mut paused = false;
        loop {
            if !media_controls.refresh().audio {
                if !paused {
                    tracing::info!("🔇 remote audio soft-paused");
                    paused = true;
                }
                tokio::time::sleep(Duration::from_millis(100)).await;
                continue;
            }
            if paused {
                tracing::info!("🔊 remote audio resumed");
                paused = false;
                delay = Duration::from_secs(1);
            }
            let audio_sink_choice = media_controls.refresh().audio_sink;
            let active_sink = audio_sink_choice.resolve(None);
            let out = match audio_sink_choice {
                crate::live_media_control::MediaDeviceChoice::Auto => AudioOut::new_default_sink(),
                crate::live_media_control::MediaDeviceChoice::Inherit => AudioOut::new(),
                crate::live_media_control::MediaDeviceChoice::Selected(ref sink) => {
                    AudioOut::new_with_sink(Some(sink))
                }
            };
            let out = match out {
                Ok(out) => out,
                Err(err) => {
                    audio_failure_log.record("sink", &err.to_string());
                    delay = app_support::next_delay(delay);
                    tokio::time::sleep(delay).await;
                    continue;
                }
            };
            let mut cli = RelayClient::new(ep.clone());
            let req = MonitorRequest {
                id: 0,
                max_bitrate: 0,
                requested_width: 0,
                requested_height: 0,
                requested_fps: 0,
                source_id: None,
            };
            match cli.capture_audio(Request::new(req)).await {
                Ok(mut stream) => {
                    tracing::info!("🔊 audio stream opened");
                    let mut packet_count: u64 = 0;
                    let mut warned_no_packets = false;
                    delay = Duration::from_secs(1);
                    loop {
                        let state = media_controls.refresh();
                        if !state.audio {
                            tracing::info!("🔇 remote audio soft-paused; closing capture stream");
                            break;
                        }
                        let desired_sink = state.audio_sink.resolve(None);
                        if desired_sink != active_sink {
                            tracing::info!(
                                from = active_sink.as_deref().unwrap_or("auto"),
                                to = desired_sink.as_deref().unwrap_or("auto"),
                                "🔊 speaker sink changed; restarting live audio output pipeline"
                            );
                            break;
                        }
                        match tokio::time::timeout(
                            Duration::from_secs(1),
                            stream.get_mut().message(),
                        )
                        .await
                        {
                            Ok(Ok(Some(pkt))) => {
                                packet_count = packet_count.saturating_add(1);
                                if packet_count <= 8 || packet_count.is_multiple_of(600) {
                                    tracing::info!(
                                        packet = packet_count,
                                        bytes = pkt.data.len(),
                                        remote_pts_us = pkt.pts,
                                        "🔊 audio packet received"
                                    );
                                }
                                out.push(pkt);
                                consecutive_source_failures = 0;
                            }
                            Ok(Ok(None)) => {
                                tracing::warn!(packets = packet_count, "⚠️🔊 audio stream ended");
                                if packet_count == 0 {
                                    delay = app_support::next_delay(delay);
                                }
                                break;
                            }
                            Ok(Err(err)) => {
                                let message = err.to_string();
                                audio_failure_log.record("recv", &message);
                                Self::maybe_recover_audio_usb(
                                    &ep,
                                    &mut consecutive_source_failures,
                                    &mut last_usb_recovery_at,
                                    &message,
                                )
                                .await;
                                delay = app_support::next_delay(delay);
                                break;
                            }
                            Err(_) => {
                                if packet_count == 0 && !warned_no_packets {
                                    warned_no_packets = true;
                                    tracing::warn!(
                                        "⚠️🔊 audio stream connected but no packets received yet; source may be idle or unavailable"
                                    );
                                }
                            }
                        }
                    }
                }
                Err(e) => {
                    let message = e.to_string();
                    audio_failure_log.record("connect", &message);
                    Self::maybe_recover_audio_usb(
                        &ep,
                        &mut consecutive_source_failures,
                        &mut last_usb_recovery_at,
                        &message,
                    )
                    .await;
                    delay = app_support::next_delay(delay);
                }
            }
            tokio::time::sleep(delay).await;
        }
    }

    #[cfg(not(coverage))]
    async fn maybe_recover_audio_usb(
        ep: &Channel,
        consecutive_source_failures: &mut u32,
        last_usb_recovery_at: &mut Option<Instant>,
        message: &str,
    ) {
        if !audio_usb_auto_recover_enabled() || !is_recoverable_remote_audio_error(message) {
            return;
        }

        *consecutive_source_failures = consecutive_source_failures.saturating_add(1);
        let threshold = audio_usb_recover_after();
        if *consecutive_source_failures < threshold {
            tracing::debug!(
                failures = *consecutive_source_failures,
                threshold,
                "🔊🛟 remote speaker capture is unhealthy; waiting before USB recovery"
            );
            return;
        }

        let cooldown = audio_usb_recover_cooldown();
        if last_usb_recovery_at.is_some_and(|last| last.elapsed() < cooldown) {
            tracing::warn!(
                cooldown_ms = cooldown.as_millis(),
                "🔊🛟 remote speaker capture is still unhealthy, but USB recovery is cooling down"
            );
            return;
        }

        *consecutive_source_failures = 0;
        *last_usb_recovery_at = Some(Instant::now());
        tracing::warn!("🔊🛟 requesting USB gadget recovery for stalled remote speaker capture");
        let mut cli = RelayClient::new(ep.clone());
        match cli.reset_usb(Request::new(Empty {})).await {
            Ok(reply) => {
                if reply.into_inner().ok {
                    tracing::warn!("🔊🛟 USB gadget recovery completed; audio will reconnect");
                } else {
                    tracing::warn!("🔊🛟 USB gadget recovery returned ok=false");
                }
            }
            Err(err) => {
                tracing::warn!("🔊🛟 USB gadget recovery failed: {err:#}");
            }
        }
    }

}
