304 lines
8.5 KiB
Rust
304 lines
8.5 KiB
Rust
#![windows_subsystem = "windows"]
|
|
|
|
use eframe::egui;
|
|
use std::process::{Command, Stdio};
|
|
use std::sync::mpsc::{channel, Receiver};
|
|
use std::thread;
|
|
use std::time::Duration;
|
|
use tracing::{info, error};
|
|
|
|
// Windows API 用于设置控制台编码
|
|
#[cfg(windows)]
|
|
extern "system" {
|
|
fn SetConsoleOutputCP(wCodePageID: u32) -> i32;
|
|
fn SetConsoleCP(wCodePageID: u32) -> i32;
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
const CP_UTF8: u32 = 65001;
|
|
|
|
fn set_utf8_encoding() {
|
|
#[cfg(windows)]
|
|
unsafe {
|
|
SetConsoleOutputCP(CP_UTF8);
|
|
SetConsoleCP(CP_UTF8);
|
|
}
|
|
}
|
|
|
|
struct PushScreenApp {
|
|
server_ip: String,
|
|
server_port: String,
|
|
ffmpeg_path: String,
|
|
device_name: String,
|
|
status: String,
|
|
is_streaming: bool,
|
|
rx: Receiver<StatusMessage>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
enum StatusMessage {
|
|
Status(String),
|
|
Error(String),
|
|
}
|
|
|
|
impl PushScreenApp {
|
|
fn new(cc: &eframe::CreationContext<'_>) -> Self {
|
|
// 配置中文字体
|
|
configure_fonts(&cc.egui_ctx);
|
|
|
|
let (tx, rx) = channel();
|
|
|
|
// 启动状态监控线程
|
|
let tx_clone = tx.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));
|
|
}
|
|
});
|
|
|
|
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(),
|
|
is_streaming: false,
|
|
rx,
|
|
}
|
|
}
|
|
|
|
fn start_stream(&mut self) {
|
|
if self.is_streaming {
|
|
return;
|
|
}
|
|
|
|
info!("开始推流: {}:{}", self.server_ip, self.server_port);
|
|
|
|
// 构建 FFmpeg 命令
|
|
let output_url = format!(
|
|
"rtmp://{}:{}/live/{}",
|
|
self.server_ip, self.server_port, self.device_name
|
|
);
|
|
|
|
let args = vec![
|
|
"-f", "gdigrab",
|
|
"-framerate", "30",
|
|
"-i", "desktop",
|
|
"-c:v", "libx264",
|
|
"-preset", "ultrafast",
|
|
"-tune", "zerolatency",
|
|
"-f", "flv",
|
|
&output_url,
|
|
];
|
|
|
|
match Command::new(&self.ffmpeg_path)
|
|
.args(&args)
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::null())
|
|
.spawn()
|
|
{
|
|
Ok(mut child) => {
|
|
info!("FFmpeg 进程启动成功, PID: {:?}", child.id());
|
|
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);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
Err(e) => {
|
|
error!("启动 FFmpeg 失败: {}", e);
|
|
self.status = format!("错误: {}", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn stop_stream(&mut self) {
|
|
if !self.is_streaming {
|
|
return;
|
|
}
|
|
|
|
info!("停止推流");
|
|
|
|
// 查找并终止 FFmpeg 进程
|
|
#[cfg(windows)]
|
|
{
|
|
let _ = Command::new("taskkill")
|
|
.args(&["/F", "/IM", "ffmpeg.exe"])
|
|
.output();
|
|
}
|
|
|
|
self.is_streaming = false;
|
|
self.status = "已停止".to_string();
|
|
}
|
|
}
|
|
|
|
/// 配置中文字体支持
|
|
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 (备用)
|
|
];
|
|
|
|
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)
|
|
.file_stem()
|
|
.and_then(|s| s.to_str())
|
|
.unwrap_or("custom_font");
|
|
|
|
fonts.font_data.insert(
|
|
font_name.to_owned(),
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
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() {
|
|
self.start_stream();
|
|
}
|
|
|
|
if ui.button("停止推流").clicked() {
|
|
self.stop_stream();
|
|
}
|
|
|
|
if ui.button("退出").clicked() {
|
|
self.stop_stream();
|
|
std::process::exit(0);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
fn main() {
|
|
// 设置 UTF-8 编码,解决中文乱码问题
|
|
set_utf8_encoding();
|
|
|
|
// 初始化日志
|
|
tracing_subscriber::fmt::init();
|
|
info!("应用程序启动");
|
|
|
|
let options = eframe::NativeOptions {
|
|
viewport: egui::ViewportBuilder::default()
|
|
.with_inner_size([520.0, 420.0])
|
|
.with_resizable(false),
|
|
..Default::default()
|
|
};
|
|
|
|
if let Err(e) = eframe::run_native(
|
|
"投屏源控制",
|
|
options,
|
|
Box::new(|cc| Box::new(PushScreenApp::new(cc))),
|
|
) {
|
|
eprintln!("应用程序错误: {}", e);
|
|
std::process::exit(1);
|
|
}
|
|
}
|