Replace server-side first-frame watchdog with client-side implementation

This change addresses robustness issues by moving the responsibility of detecting missing first video frames to the controlling side (client). A new ClientFirstFrameWatchdog triggers a refresh if the first frame is not received within 3 seconds of connection, ensuring recovery even if the server thinks it sent a frame but it was lost or not rendered.
This commit is contained in:
Godnight1006 2026-01-02 14:50:20 +08:00
parent e196ede420
commit caa74a6712
2 changed files with 80 additions and 50 deletions

View file

@ -68,6 +68,7 @@ pub struct Remote<T: InvokeUiSession> {
last_update_jobs_status: (Instant, HashMap<i32, u64>),
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<AtomicUsize>,
@ -115,6 +116,7 @@ impl<T: InvokeUiSession> Remote<T> {
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,15 @@ impl<T: InvokeUiSession> Remote<T> {
}
}
_ = status_timer.tick() => {
if self.watchdog.check(self.is_connected, self.first_frame, self.handler.is_default(), Instant::now()) {
log::warn!("No first video frame received, sending refresh");
let mut misc = Misc::new();
misc.set_refresh_video(true);
let mut msg = Message::new();
msg.set_misc(misc);
allow_err!(peer.send(&msg).await);
}
let elapsed = fps_instant.elapsed().as_millis();
if elapsed < 1000 {
continue;
@ -2439,3 +2450,72 @@ impl Drop for VideoThread {
*self.discard_queue.write().unwrap() = true;
}
}
struct ClientFirstFrameWatchdog {
deadline: Option<Instant>,
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());
}
}