init: MVP remote desktop (agent/viewer/common) with H.264 pipeline

This commit is contained in:
dev-team committed 2026-10-23 09:17:55 +08:00
commit 61e8a75cdd
14 files changed
+3098

No files matched your search

+73
View File
@@ -0,0 +1,73 @@
//! minifb::Key → Set-1 扫描码映射。返回 (扫描码, 是否扩展键)。
use minifb::Key;
pub fn map(k: Key) -> Option<(u16, bool)> {
use Key::*;
Some(match k {
A => (0x1E, false),
B => (0x30, false),
C => (0x2E, false),
D => (0x20, false),
E => (0x12, false),
F => (0x21, false),
G => (0x22, false),
H => (0x23, false),
I => (0x17, false),
J => (0x24, false),
K => (0x25, false),
L => (0x26, false),
M => (0x32, false),
N => (0x31, false),
O => (0x18, false),
P => (0x19, false),
Q => (0x10, false),
R => (0x13, false),
S => (0x1F, false),
T => (0x14, false),
U => (0x16, false),
V => (0x2F, false),
W => (0x11, false),
X => (0x2D, false),
Y => (0x15, false),
Z => (0x2C, false),
Key1 => (0x02, false),
Key2 => (0x03, false),
Key3 => (0x04, false),
Key4 => (0x05, false),
Key5 => (0x06, false),
Key6 => (0x07, false),
Key7 => (0x08, false),
Key8 => (0x09, false),
Key9 => (0x0A, false),
Key0 => (0x0B, false),
Space => (0x39, false),
Enter => (0x1C, false),
Backspace => (0x0E, false),
Tab => (0x0F, false),
CapsLock => (0x3A, false),
Backquote => (0x29, false),
Minus => (0x0C, false),
Equal => (0x0D, false),
Backslash => (0x2B, false),
Comma => (0x33, false),
Period => (0x34, false),
Slash => (0x35, false),
Semicolon => (0x27, false),
Apostrophe => (0x28, false),
LeftShift => (0x2A, false),
LeftCtrl => (0x1D, false),
LeftAlt => (0x38, false),
LeftSuper => (0x5B, true),
Up => (0x48, true),
Down => (0x50, true),
Left => (0x4B, true),
Right => (0x4D, true),
Home => (0x47, true),
End => (0x4F, true),
Delete => (0x53, true),
PageUp => (0x49, true),
PageDown => (0x51, true),
_ => return None,
})
}
+554
View File
@@ -0,0 +1,554 @@
//! 主控端 Viewer:QUIC 连接被控端 → 解压帧 → minifb 渲染;采集键鼠上行。
//! HUD:右上角半透明白底参数面板(FPS / RTT / 端到端延迟 / 码率),延迟绿-黄-红分级。
mod keymap;
use anyhow::{bail, Result};
use common::{read_control, write_control, ControlMsg, InputEvent, MouseButton};
use minifb::{Key, MouseMode, Window, WindowOptions};
use openh264::formats::YUVSource;
use rustls::client::danger::{
HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier,
};
use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
use rustls::{DigitallySignedStruct, SignatureScheme};
use std::collections::HashSet;
use std::net::ToSocketAddrs;
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, Ordering};
use std::sync::{Arc, OnceLock};
use std::time::{Duration, Instant};
/// MVP:跳过服务端证书校验(自签名)。正式版固定公钥/账号绑定。
#[derive(Debug)]
struct SkipVerify;
impl ServerCertVerifier for SkipVerify {
fn verify_server_cert(
&self,
_end_entity: &CertificateDer<'_>,
_intermediates: &[CertificateDer<'_>],
_server_name: &ServerName<'_>,
_ocsp_response: &[u8],
_now: UnixTime,
) -> Result<ServerCertVerified, rustls::Error> {
Ok(ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_certificate: &CertificateDer<'_>,
_dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
Ok(HandshakeSignatureValid::assertion())
}
fn verify_tls13_signature(
&self,
_message: &[u8],
_certificate: &CertificateDer<'_>,
_dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
Ok(HandshakeSignatureValid::assertion())
}
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
vec![
SignatureScheme::RSA_PKCS1_SHA256,
SignatureScheme::ECDSA_NISTP256_SHA256,
SignatureScheme::ED25519,
SignatureScheme::RSA_PSS_SHA256,
]
}
}
fn parse_args() -> Result<(String, u16, String)> {
let mut addr = "127.0.0.1".to_string();
let mut port = 47800u16;
let mut code = "123456".to_string();
let a: Vec<String> = std::env::args().collect();
let mut i = 1;
while i < a.len() {
match a[i].as_str() {
"--addr" => addr = a[i + 1].clone(),
"--port" => port = a[i + 1].parse()?,
"--code" => code = a[i + 1].clone(),
_ => {}
}
i += 2;
}
Ok((addr, port, code))
}
static T0: OnceLock<Instant> = OnceLock::new();
fn now_ms() -> u64 {
T0.get().unwrap().elapsed().as_millis() as u64
}
/// 会话统计(HUD 数据源,全部原子量,跨任务共享)
#[derive(Default)]
struct Stats {
/// 最近 RTT ms(0=未知)
rtt: AtomicU64,
/// 被控时钟 - 主控时钟 偏移估计 ms(EMA 平滑)
offset: AtomicI64,
offset_valid: AtomicBool,
/// 端到端延迟估计 ms(采集→网络→接收,EMA 平滑)
e2e: AtomicU64,
e2e_valid: AtomicBool,
/// 累计收帧数 / 字节数
frames: AtomicU64,
bytes: AtomicU64,
}
fn main() -> Result<()> {
let (addr, port, code) = parse_args()?;
T0.set(Instant::now()).ok();
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
rt.block_on(run(addr, port, code))
}
async fn run(addr: String, port: u16, code: String) -> Result<()> {
rustls::crypto::ring::default_provider()
.install_default()
.ok();
let tls = rustls::ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(Arc::new(SkipVerify))
.with_no_client_auth();
let qc: quinn::crypto::rustls::QuicClientConfig = Arc::new(tls).try_into()?;
// 接收窗口放大:视频是下行大流量方向,默认 512KB 流窗口会死锁吞吐
let mut transport = quinn::TransportConfig::default();
transport.receive_window(quinn::VarInt::from_u32(16 * 1024 * 1024));
transport.stream_receive_window(quinn::VarInt::from_u32(8 * 1024 * 1024));
let mut client_config = quinn::ClientConfig::new(Arc::new(qc));
client_config.transport_config(Arc::new(transport));
let mut endpoint = quinn::Endpoint::client("0.0.0.0:0".parse()?)?;
endpoint.set_default_client_config(client_config);
let sock_addr = (addr.as_str(), port)
.to_socket_addrs()?
.next()
.ok_or_else(|| anyhow::anyhow!("resolve failed: {addr}"))?;
let conn = endpoint
.connect(sock_addr, "localhost")?
.await
.map_err(|e| anyhow::anyhow!("connect {addr}:{port} failed: {e}"))?;
println!("[viewer] connected to {addr}:{port}");
let (mut send, mut recv) = conn.open_bi().await?;
write_control(&mut send, &ControlMsg::Auth { code }).await?;
match read_control(&mut recv).await? {
ControlMsg::AuthOk { width, height } => {
println!("[viewer] auth ok, remote desktop {width}x{height}");
// 流 2:键鼠输入 + Ping/Pong 独立通道,不与视频流竞争
let (isend, irecv) = conn.open_bi().await?;
session(isend, irecv, recv, width, height)
}
ControlMsg::AuthFail(e) => bail!("auth failed: {e}"),
m => bail!("unexpected message: {m:?}"),
}
}
fn session(
send: quinn::SendStream,
mut irecv: quinn::RecvStream,
mut recv: quinn::RecvStream,
width: u32,
height: u32,
) -> Result<()> {
let (w, h) = (width as usize, height as usize);
let stats = Arc::new(Stats::default());
// 读任务(流 1):H.264 解码 → RGB → mpsc;同时统计端到端延迟与流量
let (frame_tx, frame_rx) = std::sync::mpsc::channel::<Vec<u8>>();
{
let stats = stats.clone();
let mut decoder = openh264::decoder::Decoder::new()
.map_err(|e| anyhow::anyhow!("decoder init failed: {e}"))?;
tokio::spawn(async move {
loop {
match common::read_video_frame(&mut recv).await {
Ok((hdr, payload)) => {
stats.bytes.fetch_add(payload.len() as u64, Ordering::Relaxed);
stats.frames.fetch_add(1, Ordering::Relaxed);
// 端到端延迟 = 接收时刻 - 时钟偏移 - 采集时刻(EMA 平滑)
if stats.offset_valid.load(Ordering::Relaxed) {
let est =
now_ms() as i64 - stats.offset.load(Ordering::Relaxed)
- hdr.ts_ms as i64;
if (0..5000).contains(&est) {
let old = stats.e2e.load(Ordering::Relaxed) as i64;
let sm = if stats.e2e_valid.load(Ordering::Relaxed) {
(old * 4 + est) / 5
} else {
est
};
stats.e2e.store(sm as u64, Ordering::Relaxed);
stats.e2e_valid.store(true, Ordering::Relaxed);
}
}
match decoder.decode(&payload) {
Ok(Some(yuv)) => {
let (dw, dh) = yuv.dimensions();
let mut rgb = vec![0u8; dw * dh * 3];
yuv.write_rgb8(&mut rgb);
if frame_tx.send(rgb).is_err() {
break;
}
}
Ok(None) => {}
Err(e) => {
// 解码异常:请求被控端强制关键帧
eprintln!("[viewer] decode failed: {e}, request keyframe");
let _ = frame_tx.send(Vec::new()); // 空帧占位唤醒渲染循环
}
}
}
Err(e) => {
eprintln!("[viewer] stream closed: {e}");
break;
}
}
}
});
}
// 读任务(流 2):Pong → RTT 与时钟偏移估计
{
let stats = stats.clone();
tokio::spawn(async move {
loop {
match read_control(&mut irecv).await {
Ok(ControlMsg::Pong { t, agent_ts }) => {
let r = now_ms();
stats.rtt.store(r.saturating_sub(t), Ordering::Relaxed);
// agent_ts ≈ (t+r)/2 时刻的 agent 时钟读数 → 偏移 EMA
let est = agent_ts as i64 - ((t + r) / 2) as i64;
let old = stats.offset.load(Ordering::Relaxed);
let sm = if stats.offset_valid.load(Ordering::Relaxed) {
(old * 3 + est) / 4
} else {
est
};
stats.offset.store(sm, Ordering::Relaxed);
stats.offset_valid.store(true, Ordering::Relaxed);
}
Ok(_) => {}
Err(_) => break,
}
}
});
}
// 写任务(流 2):输入 / Ping
let (msg_tx, mut msg_rx) = tokio::sync::mpsc::unbounded_channel::<Vec<u8>>();
tokio::spawn(async move {
let mut send = send;
while let Some(b) = msg_rx.recv().await {
if send.write_all(&b).await.is_err() {
break;
}
}
let _ = send.finish();
});
let push = |tx: &tokio::sync::mpsc::UnboundedSender<Vec<u8>>, m: ControlMsg| {
let _ = tx.send(common::encode_control(&m));
};
let mut window = Window::new(
"mvp-remote viewer (Esc 退出)",
w,
h,
WindowOptions {
resize: false,
..Default::default()
},
)?;
let mut buffer = vec![0u32; w * h];
let mut prev_keys: HashSet<Key> = HashSet::new();
let mut prev_btns = [false; 3];
let mut last_pos: Option<(u16, u16)> = None;
let mut dirty = false;
let mut last_ping = Instant::now() - Duration::from_secs(1);
// HUD 每秒刷新的统计基线
let mut hud_last = Instant::now();
let mut hud_frames = 0u64;
let mut hud_bytes = 0u64;
let mut hud_fps = 0u64;
let mut hud_mbps = 0f64;
loop {
if !window.is_open() || window.is_key_down(Key::Escape) {
break;
}
// 0) 每 500ms 发一次 Ping(RTT/时钟偏移测量)
if last_ping.elapsed() >= Duration::from_millis(500) {
last_ping = Instant::now();
push(&msg_tx, ControlMsg::Ping(now_ms()));
}
// 1) 渲染最新帧(只保留最新,丢弃积压);空帧=解码异常,请求关键帧
loop {
match frame_rx.try_recv() {
Ok(raw) => {
if raw.is_empty() {
push(&msg_tx, ControlMsg::KeyFrameReq);
} else if raw.len() == w * h * 3 {
blit(&mut buffer, &raw);
dirty = true;
}
}
Err(_) => break,
}
}
// 2) 键盘
let keys: HashSet<Key> = window.get_keys().into_iter().collect();
for k in keys.difference(&prev_keys) {
if let Some((sc, ext)) = keymap::map(*k) {
push(
&msg_tx,
ControlMsg::Input(InputEvent::KeyDown {
scancode: sc,
extended: ext,
}),
);
}
}
for k in prev_keys.difference(&keys) {
if let Some((sc, ext)) = keymap::map(*k) {
push(
&msg_tx,
ControlMsg::Input(InputEvent::KeyUp {
scancode: sc,
extended: ext,
}),
);
}
}
prev_keys = keys;
// 3) 鼠标移动(窗口坐标 → 虚拟桌面归一化 0..65535;位置变化才发送)
if let Some((x, y)) = window.get_mouse_pos(MouseMode::Discard) {
let nx = ((x / w as f32) * 65535.0).clamp(0.0, 65535.0) as u16;
let ny = ((y / h as f32) * 65535.0).clamp(0.0, 65535.0) as u16;
if last_pos != Some((nx, ny)) {
last_pos = Some((nx, ny));
push(&msg_tx, ControlMsg::Input(InputEvent::MouseMove { x: nx, y: ny }));
}
}
// 4) 鼠标按键(边沿触发)
for (i, (btn, mfb)) in BTN_MAP.iter().enumerate() {
let down = window.get_mouse_down(*mfb);
if down != prev_btns[i] {
prev_btns[i] = down;
push(
&msg_tx,
ControlMsg::Input(if down {
InputEvent::MouseDown(*btn)
} else {
InputEvent::MouseUp(*btn)
}),
);
}
}
// 5) 滚轮
if let Some((_, dy)) = window.get_scroll_wheel() {
if dy.abs() >= 1.0 {
push(
&msg_tx,
ControlMsg::Input(InputEvent::Wheel { dy: dy as i32 * 120 }),
);
}
}
// 6) HUD:每秒刷新一次指标,画在右上角
if hud_last.elapsed() >= Duration::from_secs(1) {
let f = stats.frames.load(Ordering::Relaxed);
let b = stats.bytes.load(Ordering::Relaxed);
hud_fps = f.saturating_sub(hud_frames);
hud_mbps = (b.saturating_sub(hud_bytes)) as f64 * 8.0 / 1e6;
hud_frames = f;
hud_bytes = b;
hud_last = Instant::now();
}
if dirty {
let rtt = stats.rtt.load(Ordering::Relaxed);
let e2e = stats.e2e.load(Ordering::Relaxed);
let e2e_ok = stats.e2e_valid.load(Ordering::Relaxed);
draw_hud(
&mut buffer,
w,
&[
("FPS", &format!("{hud_fps}"), lat_color(hud_fps >= 25, hud_fps >= 15)),
("RTT", &format!("{rtt}ms"), lat_color(rtt < 50 && rtt > 0, rtt < 100)),
(
"E2E",
&if e2e_ok { format!("{e2e}ms") } else { "--".into() },
lat_color(e2e < 100, e2e < 200),
),
("RATE", &format!("{:.0}MBP", hud_mbps), C_GRAY),
("RES", &format!("{w}x{h}"), C_GRAY),
],
);
window.update_with_buffer(&buffer, w, h)?;
dirty = false;
} else {
window.update();
std::thread::sleep(Duration::from_millis(4));
}
}
println!("[viewer] closed");
Ok(())
}
const BTN_MAP: [(MouseButton, minifb::MouseButton); 3] = [
(MouseButton::Left, minifb::MouseButton::Left),
(MouseButton::Right, minifb::MouseButton::Right),
(MouseButton::Middle, minifb::MouseButton::Middle),
];
// ---------- HUD ----------
const C_GREEN: u32 = 0x0A8F3C;
const C_AMBER: u32 = 0xC07000;
const C_RED: u32 = 0xC02222;
const C_GRAY: u32 = 0x3A3A3A;
fn lat_color(good: bool, mid: bool) -> u32 {
if good {
C_GREEN
} else if mid {
C_AMBER
} else {
C_RED
}
}
/// 5x7 点阵字体(列优先,bit0=顶行)。覆盖 HUD 用到的字符。
fn glyph(c: u8) -> [u8; 5] {
match c {
b'0' => [0x3E, 0x51, 0x49, 0x45, 0x3E],
b'1' => [0x00, 0x42, 0x7F, 0x40, 0x00],
b'2' => [0x42, 0x61, 0x51, 0x49, 0x46],
b'3' => [0x21, 0x41, 0x45, 0x4B, 0x31],
b'4' => [0x18, 0x14, 0x12, 0x7F, 0x10],
b'5' => [0x27, 0x45, 0x45, 0x45, 0x39],
b'6' => [0x3C, 0x4A, 0x49, 0x49, 0x30],
b'7' => [0x01, 0x71, 0x09, 0x05, 0x03],
b'8' => [0x36, 0x49, 0x49, 0x49, 0x36],
b'9' => [0x06, 0x49, 0x49, 0x29, 0x1E],
b'A' => [0x7E, 0x11, 0x11, 0x11, 0x7E],
b'B' => [0x7F, 0x49, 0x49, 0x49, 0x36],
b'C' => [0x3E, 0x41, 0x41, 0x41, 0x22],
b'D' => [0x7F, 0x41, 0x41, 0x22, 0x1C],
b'E' => [0x7F, 0x49, 0x49, 0x49, 0x41],
b'F' => [0x7F, 0x09, 0x09, 0x09, 0x01],
b'G' => [0x3E, 0x41, 0x49, 0x49, 0x7A],
b'H' => [0x7F, 0x08, 0x08, 0x08, 0x7F],
b'I' => [0x00, 0x41, 0x7F, 0x41, 0x00],
b'J' => [0x20, 0x40, 0x41, 0x3F, 0x01],
b'K' => [0x7F, 0x08, 0x14, 0x22, 0x41],
b'L' => [0x7F, 0x40, 0x40, 0x40, 0x40],
b'M' => [0x7F, 0x02, 0x0C, 0x02, 0x7F],
b'N' => [0x7F, 0x04, 0x08, 0x10, 0x7F],
b'O' => [0x3E, 0x41, 0x41, 0x41, 0x3E],
b'P' => [0x7F, 0x09, 0x09, 0x09, 0x06],
b'Q' => [0x3E, 0x41, 0x51, 0x21, 0x5E],
b'R' => [0x7F, 0x09, 0x19, 0x29, 0x46],
b'S' => [0x46, 0x49, 0x49, 0x49, 0x31],
b'T' => [0x01, 0x01, 0x7F, 0x01, 0x01],
b'U' => [0x3F, 0x40, 0x40, 0x40, 0x3F],
b'V' => [0x1F, 0x20, 0x40, 0x20, 0x1F],
b'W' => [0x7F, 0x20, 0x18, 0x20, 0x7F],
b'X' => [0x63, 0x14, 0x08, 0x14, 0x63],
b'Y' => [0x03, 0x04, 0x78, 0x04, 0x03],
b'Z' => [0x61, 0x51, 0x49, 0x45, 0x43],
b':' => [0x00, 0x36, 0x36, 0x00, 0x00],
b'.' => [0x00, 0x60, 0x60, 0x00, 0x00],
b'-' => [0x08, 0x08, 0x08, 0x08, 0x08],
b' ' => [0x00; 5],
_ => [0x00; 5],
}
}
/// 在 (x,y) 处以 scale 倍率画一行文字
fn draw_text(
buffer: &mut [u32],
w: usize,
mut x: usize,
y: usize,
s: &str,
color: u32,
scale: usize,
) {
let h = buffer.len() / w.max(1);
for ch in s.bytes() {
let g = glyph(ch.to_ascii_uppercase());
for (col, bits) in g.iter().enumerate() {
for row in 0..7 {
if bits & (1 << row) != 0 {
for sy in 0..scale {
for sx in 0..scale {
let px = x + col * scale + sx;
let py = y + row * scale + sy;
if px < w && py < h {
buffer[py * w + px] = color;
}
}
}
}
}
}
x += 6 * scale;
}
}
/// 右上角半透明白底 HUD:lines = [(标签, 值, 颜色)]
fn draw_hud(buffer: &mut [u32], w: usize, lines: &[(&str, &str, u32)]) {
const SCALE: usize = 2;
const PAD: usize = 10;
const MARGIN: usize = 12;
const LINE: usize = 8 * SCALE;
let h = buffer.len() / w.max(1);
let line_w = |l: &(&str, &str, u32)| (l.0.len() + 2 + l.1.len()) * 6 * SCALE;
let box_w = lines.iter().map(line_w).max().unwrap_or(0) + PAD * 2;
let box_h = lines.len() * LINE + PAD * 2;
let x0 = w.saturating_sub(box_w + MARGIN);
let y0 = MARGIN;
// 半透明白底:px' = px*0.35 + 255*0.65
for by in y0..(y0 + box_h).min(h) {
for bx in x0..(x0 + box_w).min(w) {
let p = &mut buffer[by * w + bx];
let v = *p;
let r = (v >> 16) & 0xFF;
let g = (v >> 8) & 0xFF;
let b = v & 0xFF;
let mix = |c: u32| c * 89 / 255 + 166;
*p = (mix(r) << 16) | (mix(g) << 8) | mix(b);
}
}
for (i, (label, value, color)) in lines.iter().enumerate() {
let mut x = x0 + PAD;
let y = y0 + PAD + i * LINE;
draw_text(buffer, w, x, y, label, C_GRAY, SCALE);
x += (label.len() + 2) * 6 * SCALE;
draw_text(buffer, w, x, y, value, *color, SCALE);
}
}
/// RGB888 字节(openh264 解码输出)→ minifb 0RGB u32 缓冲
fn blit(buffer: &mut [u32], raw: &[u8]) {
for (px, chunk) in buffer.iter_mut().zip(raw.chunks_exact(3)) {
*px = ((chunk[0] as u32) << 16) | ((chunk[1] as u32) << 8) | chunk[2] as u32;
}
}