重构界面 - 按照 Python 版本重新设计 UI
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user