//! X11 屏幕采集。 //! //! MVP 走核心协议 `GetImage`:零扩展依赖、零 unsafe,先把「采集 → 编码 → QUIC」整条链路打通。 //! 变更检测用整帧字节比较,桌面静止时不上行视频(省掉编码开销)。 //! //! 二期优化锚点(已在 `ScreenCapturer` 语义内预留,无需改 agent-core): //! 1. **MIT-SHM**:共享内存替代 `GetImage` 的 socket 传输,省掉 X server 一次全画布拷贝; //! 2. **XDamage**:只取变化矩形 + 只编码脏区,静止时零拷贝。 use agent_core::ScreenCapturer; use anyhow::{anyhow, Result}; use std::time::{Duration, Instant}; use x11rb::connection::Connection; use x11rb::protocol::xproto::{ConnectionExt as _, ImageFormat, VisualClass, Window}; use x11rb::rust_connection::RustConnection; /// 帧节流上限,与 agent-core 默认编码档位(30fps)对齐 const FRAME_INTERVAL: Duration = Duration::from_millis(33); pub struct X11Capturer { conn: RustConnection, root: Window, width: u16, height: u16, /// 最近一帧 BGRA(既是变更检测基线,也是 copy_frame 的数据源) last: Vec, last_capture: Option, } impl X11Capturer { pub fn new() -> Result { let (conn, screen_num) = x11rb::connect(None).map_err(|e| { anyhow!( "连接 X11 失败(DISPLAY={:?}): {e}", std::env::var("DISPLAY").ok() ) })?; // 先把需要的屏幕参数拷出来:随后 conn 要被移进结构体,不能再持有对它的借用 let (root, width, height, depth, visual_class, bpp) = { let setup = conn.setup(); let screen = &setup.roots[screen_num]; // bits_per_pixel 不在 visual 上,而在 Setup 的 pixmap_formats 里(按 depth 索引) let fmt = setup .pixmap_formats .iter() .find(|f| f.depth == screen.root_depth) .ok_or_else(|| anyhow!("找不到 depth={} 对应的像素格式", screen.root_depth))?; let visual_class = screen .allowed_depths .iter() .flat_map(|d| d.visuals.iter()) .find(|v| v.visual_id == screen.root_visual) .map(|v| v.class) .ok_or_else(|| anyhow!("找不到 root visual"))?; ( screen.root, screen.width_in_pixels, screen.height_in_pixels, screen.root_depth, visual_class, fmt.bits_per_pixel, ) }; anyhow::ensure!(width > 0 && height > 0, "屏幕尺寸异常 {width}x{height}"); anyhow::ensure!( bpp == 32, "当前仅支持 32bpp 真彩采集(depth={depth},实际 {bpp}bpp)" ); // BGRA 字节序假设只在 TrueColor 下成立 anyhow::ensure!( visual_class == VisualClass::TRUE_COLOR, "当前仅支持 TrueColor 视觉(实际 {visual_class:?})" ); println!("[capture] X11 screen {width}x{height}, depth={depth}, {bpp}bpp"); Ok(Self { conn, root, width, height, last: Vec::new(), last_capture: None, }) } /// 抓一帧整屏到 `last`,返回画面是否发生变化 fn grab(&mut self) -> Result { let reply = self .conn .get_image( ImageFormat::Z_PIXMAP, self.root, 0, 0, self.width, self.height, !0, ) .map_err(|e| anyhow!("get_image 请求失败: {e}"))? .reply() .map_err(|e| anyhow!("get_image 应答失败: {e}"))?; let expect = self.width as usize * self.height as usize * 4; anyhow::ensure!( reply.data.len() == expect, "帧数据长度异常:期望 {expect} 字节,实际 {}", reply.data.len() ); let changed = reply.data != self.last; if changed { self.last = reply.data; } Ok(changed) } } impl ScreenCapturer for X11Capturer { fn size(&self) -> (u32, u32) { (self.width as u32, self.height as u32) } /// X11 核心协议没有「阻塞至新帧」语义,故这里按帧槽节流后主动抓帧: /// 睡到下一个 1/30s 槽位 → 抓帧 → 与上一帧逐字节比较。 /// 因此 `timeout_ms` 在 Linux 实现中被忽略(agent-core 的 min_frame 节流也随之自然满足)。 fn acquire(&mut self, _timeout_ms: u32) -> Result { if let Some(t) = self.last_capture { let elapsed = t.elapsed(); if elapsed < FRAME_INTERVAL { std::thread::sleep(FRAME_INTERVAL - elapsed); } } self.last_capture = Some(Instant::now()); self.grab() } fn copy_frame(&mut self) -> Result> { anyhow::ensure!(!self.last.is_empty(), "尚无帧数据可拷贝"); Ok(self.last.clone()) } }