148 lines
5.1 KiB
Rust
148 lines
5.1 KiB
Rust
|
|
//! 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())
|
|||
|
|
}
|
|||
|
|
}
|