feat: 添加视频采集卡查看器 (Nokhwa + egui, 独立线程捕获)
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -31,3 +31,8 @@ build/
|
||||
dist/
|
||||
*.egg-info/
|
||||
*.spec
|
||||
|
||||
# Rust 构建产物
|
||||
target/
|
||||
*.exe
|
||||
*.res
|
||||
2636
capture-card-viewer/Cargo.lock
generated
Normal file
2636
capture-card-viewer/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
capture-card-viewer/Cargo.toml
Normal file
19
capture-card-viewer/Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "capture-card-viewer"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
nokhwa = { version = "0.10", features = ["input-msmf"] }
|
||||
eframe = { version = "0.24", default-features = false, features = ["default_fonts", "glow"] }
|
||||
egui = "0.24"
|
||||
winapi = { version = "0.3", features = ["winuser", "windef", "processthreadsapi", "handleapi", "winbase"] }
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
lto = true
|
||||
strip = true
|
||||
|
||||
[[bin]]
|
||||
name = "capture-card-viewer"
|
||||
path = "src/main.rs"
|
||||
233
capture-card-viewer/src/main.rs
Normal file
233
capture-card-viewer/src/main.rs
Normal file
@@ -0,0 +1,233 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use eframe::{egui, App, Frame, NativeOptions};
|
||||
use egui::{Color32, FontFamily, FontId, RichText, TextureHandle, TextureOptions};
|
||||
use nokhwa::utils::{ApiBackend, RequestedFormat, RequestedFormatType};
|
||||
use nokhwa::Camera;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
struct CaptureState {
|
||||
device_name: Arc<Mutex<String>>,
|
||||
frame_data: Arc<Mutex<Option<(usize, usize, Vec<u8>)>>>,
|
||||
running: Arc<Mutex<bool>>,
|
||||
error: Arc<Mutex<Option<String>>>,
|
||||
}
|
||||
|
||||
struct CaptureCardViewer {
|
||||
state: CaptureState,
|
||||
texture: Option<TextureHandle>,
|
||||
}
|
||||
|
||||
impl CaptureCardViewer {
|
||||
fn new() -> Self {
|
||||
let state = CaptureState {
|
||||
device_name: Arc::new(Mutex::new(String::new())),
|
||||
frame_data: Arc::new(Mutex::new(None)),
|
||||
running: Arc::new(Mutex::new(true)),
|
||||
error: Arc::new(Mutex::new(None)),
|
||||
};
|
||||
|
||||
let capture_state = CaptureState {
|
||||
device_name: state.device_name.clone(),
|
||||
frame_data: state.frame_data.clone(),
|
||||
running: state.running.clone(),
|
||||
error: state.error.clone(),
|
||||
};
|
||||
|
||||
thread::spawn(move || {
|
||||
capture_thread(capture_state);
|
||||
});
|
||||
|
||||
CaptureCardViewer {
|
||||
state,
|
||||
texture: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn capture_thread(state: CaptureState) {
|
||||
let devices = match nokhwa::query(ApiBackend::MediaFoundation) {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
*state.error.lock().unwrap() = Some(format!("查询设备失败: {}", e));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if devices.is_empty() {
|
||||
*state.error.lock().unwrap() = Some("未找到视频采集设备".to_string());
|
||||
return;
|
||||
}
|
||||
|
||||
let device_info = &devices[0];
|
||||
*state.device_name.lock().unwrap() = device_info.human_name().to_string();
|
||||
|
||||
let index = device_info.index().clone();
|
||||
let requested = RequestedFormat::new::<nokhwa::pixel_format::RgbFormat>(
|
||||
RequestedFormatType::AbsoluteHighestFrameRate,
|
||||
);
|
||||
|
||||
let mut camera = match Camera::new(index, requested) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
*state.error.lock().unwrap() = Some(format!("打开摄像头失败: {}", e));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = camera.open_stream() {
|
||||
*state.error.lock().unwrap() = Some(format!("启动视频流失败: {}", e));
|
||||
return;
|
||||
}
|
||||
|
||||
while *state.running.lock().unwrap() {
|
||||
match camera.frame() {
|
||||
Ok(buffer) => {
|
||||
match buffer.decode_image::<nokhwa::pixel_format::RgbFormat>() {
|
||||
Ok(rgb_image) => {
|
||||
let w = rgb_image.width() as usize;
|
||||
let h = rgb_image.height() as usize;
|
||||
let raw = rgb_image.into_raw();
|
||||
let mut rgba = Vec::with_capacity(w * h * 4);
|
||||
for chunk in raw.chunks(3) {
|
||||
rgba.push(chunk[0]);
|
||||
rgba.push(chunk[1]);
|
||||
rgba.push(chunk[2]);
|
||||
rgba.push(255);
|
||||
}
|
||||
*state.frame_data.lock().unwrap() = Some((w, h, rgba));
|
||||
*state.error.lock().unwrap() = None;
|
||||
}
|
||||
Err(e) => {
|
||||
*state.error.lock().unwrap() = Some(format!("解码帧失败: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
*state.error.lock().unwrap() = Some(format!("捕获帧失败: {}", e));
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl App for CaptureCardViewer {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut Frame) {
|
||||
static FONT_LOADED: std::sync::Once = std::sync::Once::new();
|
||||
FONT_LOADED.call_once(|| {
|
||||
let mut fonts = egui::FontDefinitions::default();
|
||||
let font_paths = [
|
||||
"C:/Windows/Fonts/msyh.ttc",
|
||||
"C:/Windows/Fonts/simhei.ttf",
|
||||
"C:/Windows/Fonts/simsun.ttc",
|
||||
];
|
||||
for font_path in &font_paths {
|
||||
if let Ok(font_data) = std::fs::read(font_path) {
|
||||
fonts.font_data.insert(
|
||||
"chinese_font".to_owned(),
|
||||
egui::FontData::from_owned(font_data),
|
||||
);
|
||||
fonts
|
||||
.families
|
||||
.get_mut(&egui::FontFamily::Proportional)
|
||||
.unwrap()
|
||||
.insert(0, "chinese_font".to_owned());
|
||||
fonts
|
||||
.families
|
||||
.get_mut(&egui::FontFamily::Monospace)
|
||||
.unwrap()
|
||||
.push("chinese_font".to_owned());
|
||||
ctx.set_fonts(fonts);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let frame_update = {
|
||||
let frame_guard = self.state.frame_data.lock().unwrap();
|
||||
frame_guard.as_ref().map(|(w, h, rgba)| {
|
||||
if *w > 0 && *h > 0 {
|
||||
let color_image = egui::ColorImage::from_rgba_unmultiplied([*w, *h], rgba);
|
||||
Some(color_image)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
if let Some(Some(color_image)) = frame_update {
|
||||
if let Some(tex) = self.texture.as_mut() {
|
||||
tex.set(color_image, TextureOptions::LINEAR);
|
||||
} else {
|
||||
self.texture = Some(
|
||||
ctx.load_texture("capture_frame", color_image, TextureOptions::LINEAR),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add_space(10.0);
|
||||
|
||||
let device_name = self.state.device_name.lock().unwrap().clone();
|
||||
ui.label(
|
||||
RichText::new(if device_name.is_empty() {
|
||||
"正在查找设备..."
|
||||
} else {
|
||||
&device_name
|
||||
})
|
||||
.font(FontId::new(18.0, FontFamily::Proportional))
|
||||
.strong(),
|
||||
);
|
||||
|
||||
ui.add_space(5.0);
|
||||
|
||||
let error = self.state.error.lock().unwrap();
|
||||
if let Some(e) = error.as_ref() {
|
||||
ui.label(
|
||||
RichText::new(e)
|
||||
.font(FontId::new(12.0, FontFamily::Proportional))
|
||||
.color(Color32::RED),
|
||||
);
|
||||
}
|
||||
drop(error);
|
||||
|
||||
ui.add_space(5.0);
|
||||
|
||||
if let Some(tex) = &self.texture {
|
||||
let avail_width = ui.available_width();
|
||||
let size = tex.size_vec2();
|
||||
let scale = (avail_width / size.x).min(1.0);
|
||||
let display_size = size * scale;
|
||||
ui.add(egui::Image::new(tex).max_size(display_size));
|
||||
} else {
|
||||
ui.add_space(100.0);
|
||||
ui.label(
|
||||
RichText::new("等待视频画面...")
|
||||
.font(FontId::new(14.0, FontFamily::Proportional))
|
||||
.color(Color32::from_rgb(128, 128, 128)),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ctx.request_repaint_after(Duration::from_millis(33));
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), eframe::Error> {
|
||||
let options = NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default()
|
||||
.with_inner_size([960.0, 600.0])
|
||||
.with_resizable(true),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
eframe::run_native(
|
||||
"视频采集卡查看器",
|
||||
options,
|
||||
Box::new(|_cc| Box::new(CaptureCardViewer::new())),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user