refactor: 改用FFmpeg枚举DirectShow设备并推流到MediaMTX,移除Nokhwa依赖
This commit is contained in:
@@ -1,232 +1,582 @@
|
||||
#![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 egui::{Color32, FontFamily, FontId, RichText};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::net::TcpStream;
|
||||
use std::path::Path;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::mpsc::{channel, Receiver, Sender};
|
||||
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>>>,
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct Config {
|
||||
ffmpeg_path: String,
|
||||
server_ip: String,
|
||||
stream_path: String,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
ffmpeg_path: r"D:\ScreenCast\ffmpeg\bin\ffmpeg.exe".to_string(),
|
||||
server_ip: "192.168.1.100".to_string(),
|
||||
stream_path: "hdmi".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
fn load() -> Self {
|
||||
if let Ok(content) = fs::read_to_string("capture_card_config.json") {
|
||||
if let Ok(config) = serde_json::from_str(&content) {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn save(&self) {
|
||||
if let Ok(json) = serde_json::to_string_pretty(self) {
|
||||
let _ = fs::write("capture_card_config.json", json);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Message {
|
||||
DevicesFound(Vec<String>),
|
||||
ScanFailed(String),
|
||||
StatusUpdate { ffmpeg_ok: bool, server_ok: bool },
|
||||
}
|
||||
|
||||
struct CaptureCardViewer {
|
||||
state: CaptureState,
|
||||
texture: Option<TextureHandle>,
|
||||
config: Config,
|
||||
temp_config: Config,
|
||||
show_settings: bool,
|
||||
|
||||
video_devices: Vec<String>,
|
||||
selected_device_idx: usize,
|
||||
is_scanning: bool,
|
||||
|
||||
is_streaming: bool,
|
||||
ffmpeg_ok: bool,
|
||||
server_ok: bool,
|
||||
|
||||
log_message: String,
|
||||
|
||||
rx: Receiver<Message>,
|
||||
tx: Sender<Message>,
|
||||
}
|
||||
|
||||
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 config = Config::load();
|
||||
let (tx, rx) = channel();
|
||||
|
||||
let ffmpeg_ok = Path::new(&config.ffmpeg_path).exists();
|
||||
let server_ok = check_server(&config.server_ip, 8554);
|
||||
|
||||
let mut app = Self {
|
||||
temp_config: config.clone(),
|
||||
config,
|
||||
show_settings: false,
|
||||
video_devices: Vec::new(),
|
||||
selected_device_idx: 0,
|
||||
is_scanning: false,
|
||||
is_streaming: false,
|
||||
ffmpeg_ok,
|
||||
server_ok,
|
||||
log_message: "就绪,请点击「扫描设备」查找视频采集卡".to_string(),
|
||||
rx,
|
||||
tx,
|
||||
};
|
||||
|
||||
let capture_state = CaptureState {
|
||||
device_name: state.device_name.clone(),
|
||||
frame_data: state.frame_data.clone(),
|
||||
running: state.running.clone(),
|
||||
error: state.error.clone(),
|
||||
};
|
||||
app.scan_devices();
|
||||
app.start_status_thread();
|
||||
|
||||
app
|
||||
}
|
||||
|
||||
fn scan_devices(&mut self) {
|
||||
if self.is_scanning {
|
||||
return;
|
||||
}
|
||||
self.is_scanning = true;
|
||||
self.log_message = "正在扫描 DirectShow 视频设备...".to_string();
|
||||
|
||||
let ffmpeg_path = self.config.ffmpeg_path.clone();
|
||||
let tx = self.tx.clone();
|
||||
|
||||
thread::spawn(move || {
|
||||
capture_thread(capture_state);
|
||||
let result = enumerate_dshow_devices(&ffmpeg_path);
|
||||
match result {
|
||||
Ok(devices) => {
|
||||
if devices.is_empty() {
|
||||
let _ = tx.send(Message::ScanFailed(
|
||||
"未找到任何视频采集设备,请确认设备已连接".to_string(),
|
||||
));
|
||||
} else {
|
||||
let _ = tx.send(Message::DevicesFound(devices));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = tx.send(Message::ScanFailed(format!("扫描失败: {}", e)));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
CaptureCardViewer {
|
||||
state,
|
||||
texture: None,
|
||||
fn start_status_thread(&self) {
|
||||
let tx = self.tx.clone();
|
||||
let ffmpeg_path = self.config.ffmpeg_path.clone();
|
||||
let server_ip = self.config.server_ip.clone();
|
||||
|
||||
thread::spawn(move || loop {
|
||||
let ffmpeg_ok = Path::new(&ffmpeg_path).exists();
|
||||
let server_ok = check_server(&server_ip, 8554);
|
||||
let _ = tx.send(Message::StatusUpdate {
|
||||
ffmpeg_ok,
|
||||
server_ok,
|
||||
});
|
||||
thread::sleep(Duration::from_secs(3));
|
||||
});
|
||||
}
|
||||
|
||||
fn start_stream(&mut self) {
|
||||
if self.is_streaming {
|
||||
return;
|
||||
}
|
||||
if !self.ffmpeg_ok {
|
||||
self.log_message = "错误: FFmpeg 未找到,请检查配置".to_string();
|
||||
return;
|
||||
}
|
||||
if self.video_devices.is_empty() {
|
||||
self.log_message = "错误: 没有可用的视频设备,请先扫描设备".to_string();
|
||||
return;
|
||||
}
|
||||
|
||||
let device_name = self.video_devices[self.selected_device_idx].clone();
|
||||
let video_input = format!("video={}", device_name);
|
||||
let output_url = format!(
|
||||
"rtsp://{}:8554/{}",
|
||||
self.config.server_ip, self.config.stream_path
|
||||
);
|
||||
|
||||
let args = vec![
|
||||
"-f",
|
||||
"dshow",
|
||||
"-i",
|
||||
&video_input,
|
||||
"-c:v",
|
||||
"libx264",
|
||||
"-preset",
|
||||
"ultrafast",
|
||||
"-tune",
|
||||
"zerolatency",
|
||||
"-f",
|
||||
"rtsp",
|
||||
"-rtsp_transport",
|
||||
"tcp",
|
||||
&output_url,
|
||||
];
|
||||
|
||||
match Command::new(&self.config.ffmpeg_path)
|
||||
.args(&args)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
{
|
||||
Ok(_) => {
|
||||
self.is_streaming = true;
|
||||
self.log_message = format!(
|
||||
"推流已启动\n设备: {}\n地址: {}",
|
||||
device_name, output_url
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
self.log_message = format!("启动 FFmpeg 失败: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn stop_stream(&mut self) {
|
||||
let _ = Command::new("taskkill")
|
||||
.args(&["/F", "/IM", "ffmpeg.exe"])
|
||||
.output();
|
||||
self.is_streaming = false;
|
||||
self.log_message = "推流已停止".to_string();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
fn enumerate_dshow_devices(ffmpeg_path: &str) -> Result<Vec<String>, String> {
|
||||
if !Path::new(ffmpeg_path).exists() {
|
||||
return Err(format!("FFmpeg 不存在: {}", ffmpeg_path));
|
||||
}
|
||||
|
||||
let device_info = &devices[0];
|
||||
*state.device_name.lock().unwrap() = device_info.human_name().to_string();
|
||||
let output = Command::new(ffmpeg_path)
|
||||
.args(&["-list_devices", "true", "-f", "dshow", "-i", "dummy"])
|
||||
.stderr(Stdio::piped())
|
||||
.stdout(Stdio::null())
|
||||
.output()
|
||||
.map_err(|e| format!("执行 FFmpeg 失败: {}", e))?;
|
||||
|
||||
let index = device_info.index().clone();
|
||||
let requested = RequestedFormat::new::<nokhwa::pixel_format::RgbFormat>(
|
||||
RequestedFormatType::AbsoluteHighestFrameRate,
|
||||
);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let mut devices = Vec::new();
|
||||
let mut is_video = false;
|
||||
|
||||
let mut camera = match Camera::new(index, requested) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
*state.error.lock().unwrap() = Some(format!("打开摄像头失败: {}", e));
|
||||
return;
|
||||
for line in stderr.lines() {
|
||||
let trimmed = line.trim();
|
||||
|
||||
if trimmed.contains("(video)") {
|
||||
is_video = true;
|
||||
} else if trimmed.contains("(audio)") {
|
||||
is_video = false;
|
||||
}
|
||||
};
|
||||
|
||||
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));
|
||||
}
|
||||
if is_video {
|
||||
if let Some(name) = extract_device_name(trimmed) {
|
||||
if !devices.contains(&name) {
|
||||
devices.push(name);
|
||||
}
|
||||
is_video = false;
|
||||
}
|
||||
Err(e) => {
|
||||
*state.error.lock().unwrap() = Some(format!("捕获帧失败: {}", e));
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(devices)
|
||||
}
|
||||
|
||||
fn extract_device_name(line: &str) -> Option<String> {
|
||||
if let Some(start) = line.find('"') {
|
||||
if let Some(end) = line[start + 1..].find('"') {
|
||||
let name = line[start + 1..start + 1 + end].to_string();
|
||||
if !name.is_empty() {
|
||||
return Some(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn check_server(ip: &str, port: u16) -> bool {
|
||||
let addr = format!("{}:{}", ip, port);
|
||||
addr.parse::<std::net::SocketAddr>().is_ok()
|
||||
&& TcpStream::connect_timeout(&addr.parse().unwrap(), Duration::from_secs(2)).is_ok()
|
||||
}
|
||||
|
||||
fn configure_fonts(ctx: &egui::Context) {
|
||||
let mut fonts = egui::FontDefinitions::default();
|
||||
let font_paths = [
|
||||
r"C:\Windows\Fonts\msyh.ttc",
|
||||
r"C:\Windows\Fonts\simhei.ttf",
|
||||
r"C:\Windows\Fonts\simsun.ttc",
|
||||
];
|
||||
for font_path in &font_paths {
|
||||
if let Ok(font_data) = std::fs::read(font_path) {
|
||||
let font_name = Path::new(font_path)
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("chinese_font");
|
||||
fonts.font_data.insert(
|
||||
font_name.to_owned(),
|
||||
egui::FontData::from_owned(font_data),
|
||||
);
|
||||
fonts
|
||||
.families
|
||||
.get_mut(&FontFamily::Proportional)
|
||||
.unwrap()
|
||||
.insert(0, font_name.to_owned());
|
||||
fonts
|
||||
.families
|
||||
.get_mut(&FontFamily::Monospace)
|
||||
.unwrap()
|
||||
.push(font_name.to_owned());
|
||||
ctx.set_fonts(fonts);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
configure_fonts(ctx);
|
||||
});
|
||||
|
||||
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
|
||||
while let Ok(msg) = self.rx.try_recv() {
|
||||
match msg {
|
||||
Message::DevicesFound(devices) => {
|
||||
self.video_devices = devices;
|
||||
self.selected_device_idx = 0;
|
||||
self.is_scanning = false;
|
||||
if self.video_devices.is_empty() {
|
||||
self.log_message = "未找到视频采集设备".to_string();
|
||||
} else {
|
||||
self.log_message = format!(
|
||||
"找到 {} 个视频设备:\n{}",
|
||||
self.video_devices.len(),
|
||||
self.video_devices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, d)| format!(" {}. {}", i + 1, d))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
);
|
||||
}
|
||||
}
|
||||
Message::ScanFailed(e) => {
|
||||
self.is_scanning = false;
|
||||
self.video_devices.clear();
|
||||
self.log_message = e;
|
||||
}
|
||||
Message::StatusUpdate {
|
||||
ffmpeg_ok,
|
||||
server_ok,
|
||||
} => {
|
||||
self.ffmpeg_ok = ffmpeg_ok;
|
||||
self.server_ok = server_ok;
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
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.add_space(8.0);
|
||||
ui.label(
|
||||
RichText::new(if device_name.is_empty() {
|
||||
"正在查找设备..."
|
||||
} else {
|
||||
&device_name
|
||||
})
|
||||
.font(FontId::new(18.0, FontFamily::Proportional))
|
||||
.strong(),
|
||||
RichText::new("视频采集卡推流工具")
|
||||
.font(FontId::new(20.0, FontFamily::Proportional))
|
||||
.strong(),
|
||||
);
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
|
||||
ui.add_space(5.0);
|
||||
egui::Frame::group(ui.style()).show(ui, |ui| {
|
||||
ui.label(
|
||||
RichText::new("状态检查")
|
||||
.font(FontId::new(14.0, FontFamily::Proportional))
|
||||
.strong(),
|
||||
);
|
||||
ui.add_space(4.0);
|
||||
|
||||
let error = self.state.error.lock().unwrap();
|
||||
if let Some(e) = error.as_ref() {
|
||||
ui.horizontal(|ui| {
|
||||
let (color, text) = if self.ffmpeg_ok {
|
||||
(Color32::GREEN, "FFmpeg: 已就绪")
|
||||
} else {
|
||||
(Color32::RED, "FFmpeg: 未找到")
|
||||
};
|
||||
ui.colored_label(color, "●");
|
||||
ui.label(text);
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
let (color, text) = if self.server_ok {
|
||||
(Color32::GREEN, "MediaMTX 服务器: 已连接")
|
||||
} else {
|
||||
(Color32::from_rgb(255, 165, 0), "MediaMTX 服务器: 未连接")
|
||||
};
|
||||
ui.colored_label(color, "●");
|
||||
ui.label(text);
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(6.0);
|
||||
|
||||
egui::Frame::group(ui.style()).show(ui, |ui| {
|
||||
ui.label(
|
||||
RichText::new("设备选择")
|
||||
.font(FontId::new(14.0, FontFamily::Proportional))
|
||||
.strong(),
|
||||
);
|
||||
ui.add_space(4.0);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
let scan_text = if self.is_scanning {
|
||||
"扫描中..."
|
||||
} else {
|
||||
"扫描设备"
|
||||
};
|
||||
if ui
|
||||
.add_sized(
|
||||
[100.0, 28.0],
|
||||
egui::Button::new(scan_text),
|
||||
)
|
||||
.clicked()
|
||||
&& !self.is_scanning
|
||||
{
|
||||
self.scan_devices();
|
||||
}
|
||||
|
||||
if self.video_devices.is_empty() {
|
||||
ui.label(
|
||||
RichText::new("未检测到设备").color(Color32::from_rgb(180, 180, 180)),
|
||||
);
|
||||
} else {
|
||||
let selected_text = if self.selected_device_idx < self.video_devices.len() {
|
||||
&self.video_devices[self.selected_device_idx]
|
||||
} else {
|
||||
&String::new()
|
||||
};
|
||||
egui::ComboBox::from_id_source("device_select")
|
||||
.selected_text(selected_text)
|
||||
.show_ui(ui, |ui| {
|
||||
for (i, device) in self.video_devices.iter().enumerate() {
|
||||
ui.selectable_value(&mut self.selected_device_idx, i, device);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(6.0);
|
||||
|
||||
egui::Frame::group(ui.style()).show(ui, |ui| {
|
||||
ui.label(
|
||||
RichText::new("推流信息")
|
||||
.font(FontId::new(14.0, FontFamily::Proportional))
|
||||
.strong(),
|
||||
);
|
||||
ui.add_space(4.0);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("推流地址:");
|
||||
ui.label(
|
||||
RichText::new(e)
|
||||
.font(FontId::new(12.0, FontFamily::Proportional))
|
||||
.color(Color32::RED),
|
||||
RichText::new(format!(
|
||||
"rtsp://{}:8554/{}",
|
||||
self.config.server_ip, self.config.stream_path
|
||||
))
|
||||
.color(Color32::from_rgb(100, 149, 237)),
|
||||
);
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("观看地址:");
|
||||
ui.label(
|
||||
RichText::new(format!(
|
||||
"http://{}:8889/webrtc.html?src={}",
|
||||
self.config.server_ip, self.config.stream_path
|
||||
))
|
||||
.color(Color32::from_rgb(100, 149, 237)),
|
||||
);
|
||||
});
|
||||
|
||||
if !self.video_devices.is_empty() && self.selected_device_idx < self.video_devices.len() {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("当前设备:");
|
||||
ui.label(
|
||||
RichText::new(&self.video_devices[self.selected_device_idx])
|
||||
.color(Color32::from_rgb(144, 238, 144)),
|
||||
);
|
||||
});
|
||||
}
|
||||
drop(error);
|
||||
});
|
||||
|
||||
ui.add_space(5.0);
|
||||
ui.add_space(10.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));
|
||||
ui.vertical_centered(|ui| {
|
||||
let (btn_text, btn_color) = if self.is_streaming {
|
||||
("停止推流", Color32::from_rgb(183, 28, 28))
|
||||
} else {
|
||||
ui.add_space(100.0);
|
||||
ui.label(
|
||||
RichText::new("等待视频画面...")
|
||||
.font(FontId::new(14.0, FontFamily::Proportional))
|
||||
.color(Color32::from_rgb(128, 128, 128)),
|
||||
);
|
||||
("开始推流", Color32::from_rgb(46, 125, 50))
|
||||
};
|
||||
|
||||
if ui
|
||||
.add_sized(
|
||||
[ui.available_width(), 44.0],
|
||||
egui::Button::new(
|
||||
RichText::new(btn_text)
|
||||
.font(FontId::new(16.0, FontFamily::Proportional))
|
||||
.color(Color32::WHITE),
|
||||
)
|
||||
.fill(btn_color)
|
||||
.rounding(8.0),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
if self.is_streaming {
|
||||
self.stop_stream();
|
||||
} else {
|
||||
self.start_stream();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ui.add_space(8.0);
|
||||
|
||||
egui::Frame::group(ui.style()).show(ui, |ui| {
|
||||
ui.label(
|
||||
RichText::new("日志")
|
||||
.font(FontId::new(12.0, FontFamily::Proportional))
|
||||
.strong(),
|
||||
);
|
||||
ui.add_space(2.0);
|
||||
ui.label(
|
||||
RichText::new(&self.log_message)
|
||||
.font(FontId::new(11.0, FontFamily::Proportional))
|
||||
.color(Color32::from_rgb(200, 200, 200)),
|
||||
);
|
||||
});
|
||||
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if ui.button("⚙ 设置").clicked() {
|
||||
self.temp_config = self.config.clone();
|
||||
self.show_settings = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ctx.request_repaint_after(Duration::from_millis(33));
|
||||
if self.show_settings {
|
||||
egui::Window::new("设置")
|
||||
.collapsible(false)
|
||||
.resizable(false)
|
||||
.fixed_size([480.0, 220.0])
|
||||
.show(ctx, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("FFmpeg 路径:");
|
||||
ui.text_edit_singleline(&mut self.temp_config.ffmpeg_path);
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("服务器 IP:");
|
||||
ui.text_edit_singleline(&mut self.temp_config.server_ip);
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("推流路径:");
|
||||
ui.text_edit_singleline(&mut self.temp_config.stream_path);
|
||||
});
|
||||
|
||||
ui.add_space(12.0);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("确定").clicked() {
|
||||
self.config = self.temp_config.clone();
|
||||
self.config.save();
|
||||
self.show_settings = false;
|
||||
self.start_status_thread();
|
||||
self.log_message = "配置已更新".to_string();
|
||||
}
|
||||
if ui.button("取消").clicked() {
|
||||
self.show_settings = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ctx.request_repaint_after(Duration::from_millis(200));
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), eframe::Error> {
|
||||
let options = NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default()
|
||||
.with_inner_size([960.0, 600.0])
|
||||
.with_inner_size([560.0, 480.0])
|
||||
.with_resizable(true),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
eframe::run_native(
|
||||
"视频采集卡查看器",
|
||||
"视频采集卡推流",
|
||||
options,
|
||||
Box::new(|_cc| Box::new(CaptureCardViewer::new())),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user