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:
parent
e196ede420
commit
caa74a6712
2 changed files with 80 additions and 50 deletions
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -192,27 +192,7 @@ impl VideoFrameController {
|
|||
}
|
||||
}
|
||||
|
||||
struct FirstFrameWatchdog {
|
||||
start: Instant,
|
||||
timeout: Duration,
|
||||
}
|
||||
|
||||
impl FirstFrameWatchdog {
|
||||
fn new(timeout: Duration) -> Self {
|
||||
Self {
|
||||
start: Instant::now(),
|
||||
timeout,
|
||||
}
|
||||
}
|
||||
|
||||
fn has_timed_out(&self) -> bool {
|
||||
self.has_timed_out_at(Instant::now())
|
||||
}
|
||||
|
||||
fn has_timed_out_at(&self, now: Instant) -> bool {
|
||||
now.saturating_duration_since(self.start) >= self.timeout
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum VideoSource {
|
||||
|
|
@ -670,9 +650,6 @@ fn run(vs: VideoService) -> ResultType<()> {
|
|||
let repeat_encode_max = 10;
|
||||
let mut encode_fail_counter = 0;
|
||||
let mut first_frame = true;
|
||||
let first_frame_timeout = Duration::from_secs(3);
|
||||
let first_frame_watchdog = FirstFrameWatchdog::new(first_frame_timeout);
|
||||
let mut sent_first_frame = false;
|
||||
let capture_width = c.width;
|
||||
let capture_height = c.height;
|
||||
let (mut second_instant, mut send_counter) = (Instant::now(), 0);
|
||||
|
|
@ -901,17 +878,6 @@ fn run(vs: VideoService) -> ResultType<()> {
|
|||
}
|
||||
}
|
||||
|
||||
if !sent_first_frame {
|
||||
if produced_frame {
|
||||
sent_first_frame = true;
|
||||
} else if first_frame_watchdog.has_timed_out() {
|
||||
log::warn!(
|
||||
"No first video frame for {first_frame_timeout:?}, restarting video service"
|
||||
);
|
||||
bail!("SWITCH");
|
||||
}
|
||||
}
|
||||
|
||||
let mut fetched_conn_ids = HashSet::new();
|
||||
let timeout_millis = 3_000u64;
|
||||
let wait_begin = Instant::now();
|
||||
|
|
@ -1466,20 +1432,4 @@ fn handle_screenshot(screenshot: Screenshot, msg: String, w: usize, h: usize, da
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod first_frame_watchdog_tests {
|
||||
use super::FirstFrameWatchdog;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
#[test]
|
||||
fn triggers_after_timeout() {
|
||||
let t0 = Instant::now();
|
||||
let w = FirstFrameWatchdog {
|
||||
start: t0,
|
||||
timeout: Duration::from_secs(3),
|
||||
};
|
||||
assert!(!w.has_timed_out_at(t0 + Duration::from_secs(1)));
|
||||
assert!(!w.has_timed_out_at(t0 + Duration::from_secs(2)));
|
||||
assert!(w.has_timed_out_at(t0 + Duration::from_secs(3)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue