Files
remote/linux-agent/src/capture.rs
T

147 lines
5.1 KiB
Rust
Raw Normal View History

//! 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<u8>,
last_capture: Option<Instant>,
}
impl X11Capturer {
pub fn new() -> Result<Self> {
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<bool> {
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<bool> {
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<Vec<u8>> {
anyhow::ensure!(!self.last.is_empty(), "尚无帧数据可拷贝");
Ok(self.last.clone())
}
}