This commit is contained in:
godnight10061 2026-04-20 12:57:03 +00:00 committed by GitHub
commit c0afed8711
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 137 additions and 11 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,22 @@ impl<T: InvokeUiSession> Remote<T> {
}
}
_ = 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<T: InvokeUiSession> Remote<T> {
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<T: InvokeUiSession> Remote<T> {
}
self.video_format = CodecFormat::from(&vf);
let display = vf.display as usize;
if !self.video_threads.contains_key(&display) {
self.new_video_thread(display);
}
@ -2470,3 +2493,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());
}
}

View file

@ -3627,6 +3627,12 @@ impl Connection {
}
fn refresh_video_display(&self, display: Option<usize>) {
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(

View file

@ -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);
}
}