diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 5eb7a273a..add2db40c 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -68,6 +68,7 @@ pub struct Remote { last_update_jobs_status: (Instant, HashMap), is_connected: bool, first_frame: bool, + watchdog: ClientFirstFrameWatchdog, #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] client_conn_id: i32, // used for file clipboard data_count: Arc, @@ -115,6 +116,7 @@ impl Remote { last_update_jobs_status: (Instant::now(), Default::default()), is_connected: false, first_frame: false, + watchdog: ClientFirstFrameWatchdog::new(Duration::from_secs(3)), #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] client_conn_id: 0, data_count: Arc::new(AtomicUsize::new(0)), @@ -279,6 +281,22 @@ impl Remote { } } _ = status_timer.tick() => { + if self.watchdog.check(self.is_connected, self.first_frame, self.handler.is_default(), Instant::now()) { + let (id, display) = { + let lch = self.handler.lc.read().unwrap(); + let display = lch + .peer_info + .as_ref() + .map(|p| p.current_display) + .unwrap_or(0); + (lch.id.clone(), display) + }; + log::warn!( + "No first video frame received, id={id}, display={display}, sending refresh" + ); + self.handler.refresh_video(display as _); + } + let elapsed = fps_instant.elapsed().as_millis(); if elapsed < 1000 { continue; @@ -1286,8 +1304,14 @@ impl Remote { if let Ok(msg_in) = Message::parse_from_bytes(&data) { match msg_in.union { Some(message::Union::VideoFrame(vf)) => { + let display = vf.display as usize; if !self.first_frame { self.first_frame = true; + log::info!( + "First video frame received, id={}, display={}", + self.handler.get_id(), + display + ); self.handler.close_success(); self.handler.adapt_size(); self.send_toggle_virtual_display_msg(peer).await; @@ -1295,7 +1319,6 @@ impl Remote { } self.video_format = CodecFormat::from(&vf); - let display = vf.display as usize; if !self.video_threads.contains_key(&display) { self.new_video_thread(display); } @@ -2504,3 +2527,72 @@ impl Drop for VideoThread { *self.discard_queue.write().unwrap() = true; } } + +struct ClientFirstFrameWatchdog { + deadline: Option, + timeout: Duration, +} + +impl ClientFirstFrameWatchdog { + fn new(timeout: Duration) -> Self { + Self { deadline: None, timeout } + } + + // Returns true if refresh should be triggered + fn check(&mut self, is_connected: bool, first_frame_received: bool, is_default: bool, now: Instant) -> bool { + if is_connected && !first_frame_received && is_default { + if let Some(d) = self.deadline { + if now > d { + self.deadline = Some(now + self.timeout); + return true; + } + } else { + self.deadline = Some(now + self.timeout); + } + } else { + self.deadline = None; + } + false + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::time::Duration; + + #[test] + fn test_client_first_frame_watchdog() { + let timeout = Duration::from_secs(3); + let mut watchdog = ClientFirstFrameWatchdog::new(timeout); + let start = Instant::now(); + + // Not connected: no trigger, no deadline set + assert!(!watchdog.check(false, false, true, start)); + assert!(watchdog.deadline.is_none()); + + // Connected, default, no frame: deadline set to start + 3s + assert!(!watchdog.check(true, false, true, start)); + assert!(watchdog.deadline.is_some()); + assert_eq!(watchdog.deadline.unwrap(), start + timeout); + + // Advance 2s: no trigger + assert!(!watchdog.check(true, false, true, start + Duration::from_secs(2))); + + // Advance 4s: trigger! + assert!(watchdog.check(true, false, true, start + Duration::from_secs(4))); + // Deadline reset to +3s from now (start+4s) -> start+7s + assert_eq!(watchdog.deadline.unwrap(), start + Duration::from_secs(4) + timeout); + + // Frame received: reset + assert!(!watchdog.check(true, true, true, start + Duration::from_secs(5))); + assert!(watchdog.deadline.is_none()); + + // Not default: reset + watchdog.check(true, false, true, start); // set deadline + assert!(watchdog.deadline.is_some()); + watchdog.check(true, false, false, start); // reset + assert!(watchdog.deadline.is_none()); + } +} + diff --git a/src/server/connection.rs b/src/server/connection.rs index 538503d9c..550379678 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -3952,6 +3952,12 @@ impl Connection { } fn refresh_video_display(&self, display: Option) { + log::debug!( + "refresh_video_display: conn_id={}, display={:?}, source={:?}", + self.inner.id, + display, + self.video_source() + ); video_service::refresh(); self.server.upgrade().map(|s| { s.read().unwrap().set_video_service_opt( diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 13a781c28..d276a0ef3 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -192,6 +192,8 @@ impl VideoFrameController { } } + + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum VideoSource { Monitor, @@ -648,6 +650,7 @@ fn run(vs: VideoService) -> ResultType<()> { let repeat_encode_max = 10; let mut encode_fail_counter = 0; let mut first_frame = true; + let mut logged_invalid_frame = false; let capture_width = c.width; let capture_height = c.height; let (mut second_instant, mut send_counter) = (Instant::now(), 0); @@ -719,8 +722,29 @@ fn run(vs: VideoService) -> ResultType<()> { let ms = (time.as_secs() * 1000 + time.subsec_millis() as u64) as i64; let res = match c.frame(spf) { Ok(frame) => { - repeat_encode_counter = 0; - if frame.valid() { + if !frame.valid() { + if !logged_invalid_frame { + logged_invalid_frame = true; + match &frame { + scrap::Frame::PixelBuffer(f) => { + log::debug!( + "capturer returned invalid frame (pixelbuffer), len={}, w={}, h={}, treating as WouldBlock", + f.data().len(), + f.width(), + f.height() + ); + } + scrap::Frame::Texture((texture, _)) => { + log::debug!( + "capturer returned invalid frame (texture={texture:?}), treating as WouldBlock" + ); + } + } + } + Err(std::io::Error::new(WouldBlock, "empty frame")) + } else { + repeat_encode_counter = 0; + let screenshot = SCREENSHOTS.lock().unwrap().remove(&display_idx); if let Some(mut screenshot) = screenshot { let restore_vram = screenshot.restore_vram; @@ -779,16 +803,18 @@ fn run(vs: VideoService) -> ResultType<()> { )?; frame_controller.set_send(now, send_conn_ids); send_counter += 1; - } - #[cfg(windows)] - { - #[cfg(feature = "vram")] - if try_gdi == 1 && !c.is_gdi() { - VRamEncoder::set_fallback_gdi(sp.name(), false); + + #[cfg(windows)] + { + #[cfg(feature = "vram")] + if try_gdi == 1 && !c.is_gdi() { + VRamEncoder::set_fallback_gdi(sp.name(), false); + } + try_gdi = 0; } - try_gdi = 0; + + Ok(()) } - Ok(()) } Err(err) => Err(err), }; @@ -1417,3 +1443,5 @@ fn handle_screenshot(screenshot: Screenshot, msg: String, w: usize, h: usize, da log::error!("Failed to send screenshot, {}", e); } } + +