init: MVP remote desktop (agent/viewer/common) with H.264 pipeline
This commit is contained in:
commit
61e8a75cdd
14 files changed
+3098
No files matched your search
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user