重构界面 - 按照 Python 版本重新设计 UI

This commit is contained in:
xiaji
2026-03-30 17:04:42 +08:00
parent 4846b0fe4b
commit bbf77c0143
23 changed files with 407 additions and 457 deletions

View File

@@ -2,10 +2,12 @@
use eframe::egui;
use std::process::{Command, Stdio};
use std::sync::mpsc::{channel, Receiver};
use std::sync::mpsc::{channel, Receiver, Sender};
use std::thread;
use std::time::Duration;
use tracing::{info, error};
use std::net::TcpStream;
use std::path::Path;
use std::fs;
// Windows API 用于设置控制台编码
#[cfg(windows)]
@@ -25,20 +27,63 @@ fn set_utf8_encoding() {
}
}
struct PushScreenApp {
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct Config {
server_ip: String,
server_port: String,
ffmpeg_path: String,
device_name: String,
status: String,
is_streaming: bool,
rx: Receiver<StatusMessage>,
stream_path: String,
}
impl Default for Config {
fn default() -> Self {
Self {
server_ip: "192.168.1.100".to_string(),
ffmpeg_path: r"D:\ScreenCast\ffmpeg\bin\ffmpeg.exe".to_string(),
stream_path: "screen".to_string(),
}
}
}
impl Config {
fn load() -> Self {
if let Ok(content) = fs::read_to_string("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("config.json", json);
}
}
}
#[derive(Debug, Clone)]
enum StatusMessage {
Status(String),
Error(String),
struct StatusInfo {
ffmpeg_ok: bool,
server_ok: bool,
port_ok: bool,
}
#[derive(Debug, Clone)]
enum Message {
StatusUpdate(StatusInfo),
Log(String),
ConfigChanged(Config),
}
struct PushScreenApp {
config: Config,
status: StatusInfo,
log_message: String,
is_streaming: bool,
rx: Receiver<Message>,
tx: Sender<Message>,
show_settings: bool,
temp_config: Config,
}
impl PushScreenApp {
@@ -46,28 +91,37 @@ impl PushScreenApp {
// 配置中文字体
configure_fonts(&cc.egui_ctx);
let config = Config::load();
let (tx, rx) = channel();
// 启动状态监控线程
// 启动状态检查线程
let tx_clone = tx.clone();
let config_clone = config.clone();
thread::spawn(move || {
loop {
if let Err(e) = tx_clone.send(StatusMessage::Status("运行中".to_string())) {
error!("状态发送失败: {}", e);
break;
}
thread::sleep(Duration::from_secs(5));
let status = StatusInfo {
ffmpeg_ok: Path::new(&config_clone.ffmpeg_path).exists(),
server_ok: check_server(&config_clone.server_ip, 8554),
port_ok: check_port(&config_clone.server_ip, 8554),
};
let _ = tx_clone.send(Message::StatusUpdate(status));
thread::sleep(Duration::from_secs(3));
}
});
Self {
server_ip: "192.168.1.100".to_string(),
server_port: "8080".to_string(),
ffmpeg_path: r"C:\ffmpeg\bin\ffmpeg.exe".to_string(),
device_name: "会议室主屏".to_string(),
status: "就绪".to_string(),
temp_config: config.clone(),
config,
status: StatusInfo {
ffmpeg_ok: false,
server_ok: false,
port_ok: false,
},
log_message: "日志: 等待启动...".to_string(),
is_streaming: false,
rx,
tx,
show_settings: false,
}
}
@@ -76,12 +130,14 @@ impl PushScreenApp {
return;
}
info!("开始推流: {}:{}", self.server_ip, self.server_port);
// 构建 FFmpeg 命令
if !self.status.ffmpeg_ok {
self.log_message = "日志: 错误 - FFmpeg未安装请检查配置".to_string();
return;
}
let output_url = format!(
"rtmp://{}:{}/live/{}",
self.server_ip, self.server_port, self.device_name
"rtsp://{}:8554/{}",
self.config.server_ip, self.config.stream_path
);
let args = vec![
@@ -91,75 +147,70 @@ impl PushScreenApp {
"-c:v", "libx264",
"-preset", "ultrafast",
"-tune", "zerolatency",
"-f", "flv",
"-f", "rtsp",
&output_url,
];
match Command::new(&self.ffmpeg_path)
match Command::new(&self.config.ffmpeg_path)
.args(&args)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
{
Ok(mut child) => {
info!("FFmpeg 进程启动成功, PID: {:?}", child.id());
Ok(_child) => {
self.is_streaming = true;
self.status = format!("推流中 - {}", self.device_name);
// 监控进程
thread::spawn(move || {
match child.wait() {
Ok(status) => {
info!("FFmpeg 进程结束: {:?}", status);
}
Err(e) => {
error!("等待 FFmpeg 进程失败: {}", e);
}
}
});
self.log_message = "日志: FFmpeg推流已启动正在向服务器推送...".to_string();
}
Err(e) => {
error!("启动 FFmpeg 失败: {}", e);
self.status = format!("错误: {}", e);
self.log_message = format!("日志: 错误 - 启动FFmpeg失败: {}", e);
}
}
}
fn stop_stream(&mut self) {
if !self.is_streaming {
return;
}
info!("停止推流");
fn update_config(&mut self) {
self.config = self.temp_config.clone();
self.config.save();
self.log_message = "日志: 配置已更新".to_string();
// 查找并终止 FFmpeg 进
#[cfg(windows)]
{
let _ = Command::new("taskkill")
.args(&["/F", "/IM", "ffmpeg.exe"])
.output();
}
self.is_streaming = false;
self.status = "已停止".to_string();
// 重启状态检查线
let tx_clone = self.tx.clone();
let config_clone = self.config.clone();
thread::spawn(move || {
loop {
let status = StatusInfo {
ffmpeg_ok: Path::new(&config_clone.ffmpeg_path).exists(),
server_ok: check_server(&config_clone.server_ip, 8554),
port_ok: check_port(&config_clone.server_ip, 8554),
};
let _ = tx_clone.send(Message::StatusUpdate(status));
thread::sleep(Duration::from_secs(3));
}
});
}
}
fn check_server(ip: &str, port: u16) -> bool {
TcpStream::connect_timeout(
&format!("{}:{}", ip, port).parse().unwrap(),
Duration::from_secs(2)
).is_ok()
}
fn check_port(ip: &str, port: u16) -> bool {
check_server(ip, port)
}
/// 配置中文字体支持
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\msyhbd.ttc", // 微软雅黑粗体
r"C:\Windows\Fonts\simsun.ttc", // 宋体
r"C:\Windows\Fonts\simhei.ttf", // 黑体
r"C:\Windows\Fonts\arial.ttf", // Arial (备用)
r"C:\Windows\Fonts\msyh.ttc",
r"C:\Windows\Fonts\msyhbd.ttc",
r"C:\Windows\Fonts\simsun.ttc",
r"C:\Windows\Fonts\simhei.ttf",
];
let mut font_loaded = false;
for font_path in &font_paths {
if let Ok(font_data) = std::fs::read(font_path) {
let font_name = std::path::Path::new(font_path)
@@ -172,118 +223,192 @@ fn configure_fonts(ctx: &egui::Context) {
egui::FontData::from_owned(font_data),
);
// 将字体添加到所有字体族
fonts.families.get_mut(&egui::FontFamily::Proportional).unwrap()
.insert(0, font_name.to_owned());
fonts.families.get_mut(&egui::FontFamily::Monospace).unwrap()
.push(font_name.to_owned());
font_loaded = true;
info!("成功加载字体: {}", font_path);
break;
}
}
if !font_loaded {
// 如果系统字体加载失败,使用 egui 默认字体并尝试嵌入基本中文字体数据
error!("无法加载系统中文字体,使用默认字体");
}
ctx.set_fonts(fonts);
}
impl eframe::App for PushScreenApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
// 接收状态更新
// 接收消息
while let Ok(msg) = self.rx.try_recv() {
match msg {
StatusMessage::Status(s) => {
if self.is_streaming {
self.status = format!("推流中 - {}", s);
}
}
StatusMessage::Error(e) => {
self.status = format!("错误: {}", e);
self.is_streaming = false;
}
Message::StatusUpdate(s) => self.status = s,
Message::Log(l) => self.log_message = l,
Message::ConfigChanged(c) => self.config = c,
}
}
// 设置窗口样式
let mut style = (*ctx.style()).clone();
style.spacing.window_margin = egui::Margin::same(10.0);
ctx.set_style(style);
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("投屏源控制系统");
ui.add_space(20.0);
// 服务器设置
ui.group(|ui| {
ui.label("服务器设置");
ui.horizontal(|ui| {
ui.label("服务器 IP:");
ui.text_edit_singleline(&mut self.server_ip);
});
ui.horizontal(|ui| {
ui.label("端口:");
ui.text_edit_singleline(&mut self.server_port);
});
});
ui.add_space(10.0);
// FFmpeg 设置
ui.group(|ui| {
ui.label("FFmpeg 设置");
ui.horizontal(|ui| {
ui.label("FFmpeg 路径:");
ui.text_edit_singleline(&mut self.ffmpeg_path);
});
});
ui.add_space(10.0);
// 设备设置
ui.group(|ui| {
ui.label("设备设置");
ui.horizontal(|ui| {
ui.label("设备名称:");
ui.text_edit_singleline(&mut self.device_name);
});
});
ui.add_space(20.0);
// 状态显示
ui.group(|ui| {
ui.label("状态:");
ui.label(&self.status);
});
ui.add_space(20.0);
// 控制按钮
// 标题栏 + 设置按钮
ui.horizontal(|ui| {
if ui.button("开始推流").clicked() {
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;
}
});
});
// 主标题
ui.heading("会议投屏系统 - 主播端");
ui.add_space(15.0);
// 本地配置组
egui::Frame::group(ui.style()).show(ui, |ui| {
ui.label("本地配置");
ui.add_space(5.0);
// FFmpeg 状态
ui.horizontal(|ui| {
let (color, text) = if self.status.ffmpeg_ok {
(egui::Color32::GREEN, "FFmpeg: ✓ 已安装")
} else {
(egui::Color32::RED, "FFmpeg: ✗ 未找到")
};
ui.colored_label(color, "");
ui.label(text);
});
// 服务器连接状态
ui.horizontal(|ui| {
let (color, text) = if self.status.server_ok {
(egui::Color32::GREEN, "服务器连接: ✓ 可连接")
} else {
(egui::Color32::from_rgb(255, 165, 0), "服务器连接: ✗ 连接失败")
};
ui.colored_label(color, "");
ui.label(text);
});
// RTSP端口状态
ui.horizontal(|ui| {
let (color, text) = if self.status.port_ok {
(egui::Color32::GREEN, "RTSP端口: ✓ 端口开放")
} else {
(egui::Color32::from_rgb(255, 165, 0), "RTSP端口: ✗ 端口关闭")
};
ui.colored_label(color, "");
ui.label(text);
});
});
ui.add_space(10.0);
// 流信息组
egui::Frame::group(ui.style()).show(ui, |ui| {
ui.label("流信息");
ui.add_space(5.0);
ui.horizontal(|ui| {
ui.label("服务器IP:");
ui.label(&self.config.server_ip);
});
ui.horizontal(|ui| {
ui.label("推流地址:");
ui.label(format!("rtsp://{}:8554/{}", self.config.server_ip, self.config.stream_path));
});
ui.horizontal(|ui| {
ui.label("访问地址:");
ui.label(format!("http://{}:8889/webrtc.html?src={}", self.config.server_ip, self.config.stream_path));
});
});
ui.add_space(15.0);
// 开始全屏投屏按钮
let button_text = if self.is_streaming { "🎬 停止投屏" } else { "🎬 开始全屏投屏" };
let button_color = if self.is_streaming {
egui::Color32::from_rgb(183, 28, 28)
} else {
egui::Color32::from_rgb(46, 125, 50)
};
let button_response = ui.add_sized(
[ui.available_width(), 50.0],
egui::Button::new(button_text)
.fill(button_color)
.rounding(8.0)
);
if button_response.clicked() {
if self.is_streaming {
// 停止推流
#[cfg(windows)]
{
let _ = Command::new("taskkill")
.args(&["/F", "/IM", "ffmpeg.exe"])
.output();
}
self.is_streaming = false;
self.log_message = "日志: 推流已停止".to_string();
} else {
self.start_stream();
}
if ui.button("停止推流").clicked() {
self.stop_stream();
}
if ui.button("退出").clicked() {
self.stop_stream();
std::process::exit(0);
}
}
ui.add_space(10.0);
// 日志显示区域
egui::Frame::group(ui.style()).show(ui, |ui| {
ui.label(&self.log_message);
});
});
// 设置对话框
if self.show_settings {
egui::Window::new("设置")
.collapsible(false)
.resizable(false)
.fixed_size([450.0, 200.0])
.show(ctx, |ui| {
ui.horizontal(|ui| {
ui.label("服务器IP:");
ui.text_edit_singleline(&mut self.temp_config.server_ip);
});
ui.horizontal(|ui| {
ui.label("FFmpeg路径:");
ui.text_edit_singleline(&mut self.temp_config.ffmpeg_path);
});
ui.horizontal(|ui| {
ui.label("推流路径:");
ui.text_edit_singleline(&mut self.temp_config.stream_path);
});
ui.add_space(10.0);
ui.horizontal(|ui| {
if ui.button("确定").clicked() {
self.update_config();
self.show_settings = false;
}
if ui.button("取消").clicked() {
self.show_settings = false;
}
});
});
}
}
}
fn main() {
// 设置 UTF-8 编码,解决中文乱码问题
set_utf8_encoding();
// 初始化日志
tracing_subscriber::fmt::init();
info!("应用程序启动");
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()