import subprocess import psutil import os import sys import socket import time import json from loguru import logger from PySide6.QtWidgets import (QApplication, QMainWindow, QPushButton, QMessageBox, QVBoxLayout, QWidget, QLabel, QGroupBox, QGridLayout, QStatusBar, QFrame, QDialog, QLineEdit, QFormLayout, QDialogButtonBox, QFileDialog, QHBoxLayout) from PySide6.QtCore import Qt, QThread, Signal from PySide6.QtGui import QFont logger.add(sys.stderr, format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}", level="INFO") CONFIG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.json") def load_config(): if os.path.exists(CONFIG_FILE): with open(CONFIG_FILE, 'r', encoding='utf-8') as f: return json.load(f) return { "server_ip": "192.168.1.100", "mediamtx_path": r"D:\ScreenCast\mediamtx\mediamtx.exe", "ffmpeg_path": r"D:\ScreenCast\ffmpeg\bin\ffmpeg.exe", "stream_path": "screen" } def save_config(config): with open(CONFIG_FILE, 'w', encoding='utf-8') as f: json.dump(config, f, ensure_ascii=False, indent=4) logger.info(f"配置已保存到 {CONFIG_FILE}") class ConfigDialog(QDialog): def __init__(self, config, parent=None): super().__init__(parent) self.config = config self.setWindowTitle("设置") self.setFixedSize(450, 220) layout = QFormLayout(self) self.server_ip_edit = QLineEdit(config["server_ip"]) self.mediamtx_path_edit = QLineEdit(config["mediamtx_path"]) self.ffmpeg_path_edit = QLineEdit(config["ffmpeg_path"]) self.stream_path_edit = QLineEdit(config["stream_path"]) self.mediamtx_btn = QPushButton("浏览...") self.mediamtx_btn.clicked.connect(lambda: self.browse("mediamtx")) self.ffmpeg_btn = QPushButton("浏览...") self.ffmpeg_btn.clicked.connect(lambda: self.browse("ffmpeg")) path_layout1 = QGridLayout() path_layout1.addWidget(self.mediamtx_path_edit, 0, 0) path_layout1.addWidget(self.mediamtx_btn, 0, 1) path_layout1.addWidget(self.ffmpeg_path_edit, 1, 0) path_layout1.addWidget(self.ffmpeg_btn, 1, 1) layout.addRow("服务器IP:", self.server_ip_edit) layout.addRow("MediaMTX路径:", self.mediamtx_path_edit) layout.addRow("FFmpeg路径:", self.ffmpeg_path_edit) layout.addRow("推流路径:", self.stream_path_edit) buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) layout.addRow(buttons) def browse(self, field): if field == "mediamtx": path = QFileDialog.getOpenFileName(self, "选择MediaMTX", "", "Executable (*.exe)")[0] if path: self.mediamtx_path_edit.setText(path) elif field == "ffmpeg": path = QFileDialog.getOpenFileName(self, "选择FFmpeg", "", "Executable (*.exe)")[0] if path: self.ffmpeg_path_edit.setText(path) def get_config(self): return { "server_ip": self.server_ip_edit.text().strip(), "mediamtx_path": self.mediamtx_path_edit.text().strip(), "ffmpeg_path": self.ffmpeg_path_edit.text().strip(), "stream_path": self.stream_path_edit.text().strip() } class ConnectionChecker(QThread): status_update = Signal(dict) def run(self): while True: cfg = self.config status = { "mediamtx": self.check_mediamtx(cfg["mediamtx_path"]), "ffmpeg": self.check_ffmpeg(cfg["ffmpeg_path"]), "server": self.check_server(cfg["server_ip"]), "server_port": self.check_port(cfg["server_ip"], 8554), } self.status_update.emit(status) time.sleep(3) def set_config(self, config): self.config = config def check_mediamtx(self, path): for proc in psutil.process_iter(['name']): if proc.info['name'] == 'mediamtx.exe': return True return False def check_ffmpeg(self, path): return os.path.exists(path) def check_server(self, ip): try: socket.create_connection((ip, 8554), timeout=2) return True except: return False def check_port(self, ip, port): try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(2) result = sock.connect_ex((ip, port)) sock.close() return result == 0 except: return False class MainWindow(QMainWindow): def __init__(self): super().__init__() self.config = load_config() self.setWindowTitle("投屏源控制") self.setFixedSize(520, 420) self.status_bar = QStatusBar() self.setStatusBar(self.status_bar) central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QVBoxLayout(central_widget) title_layout = QHBoxLayout() title_layout.addStretch() self.btn_settings = QPushButton("⚙ 设置") self.btn_settings.setFixedSize(70, 30) self.btn_settings.clicked.connect(self.open_settings) title_layout.addWidget(self.btn_settings) title_label = QLabel("会议投屏系统 - 主播端") title_font = QFont("Microsoft YaHei", 14, QFont.Bold) title_label.setFont(title_font) title_label.setAlignment(Qt.AlignCenter) title_main_layout = QVBoxLayout() title_main_layout.addLayout(title_layout) title_main_layout.addWidget(title_label) main_layout.addLayout(title_main_layout) config_group = QGroupBox("本地配置") config_layout = QGridLayout() self.mediamtx_label = QLabel("MediaMTX: 未检测") self.ffmpeg_label = QLabel("FFmpeg: 未检测") self.server_label = QLabel("服务器连接: 未检测") self.port_label = QLabel("RTSP端口: 未检测") self.mediamtx_indicator = QLabel("●") self.mediamtx_indicator.setFixedWidth(20) self.ffmpeg_indicator = QLabel("●") self.ffmpeg_indicator.setFixedWidth(20) self.server_indicator = QLabel("●") self.server_indicator.setFixedWidth(20) self.port_indicator = QLabel("●") self.port_indicator.setFixedWidth(20) config_layout.addWidget(self.mediamtx_indicator, 0, 0) config_layout.addWidget(self.mediamtx_label, 0, 1) config_layout.addWidget(self.ffmpeg_indicator, 1, 0) config_layout.addWidget(self.ffmpeg_label, 1, 1) config_layout.addWidget(self.server_indicator, 2, 0) config_layout.addWidget(self.server_label, 2, 1) config_layout.addWidget(self.port_indicator, 3, 0) config_layout.addWidget(self.port_label, 3, 1) config_group.setLayout(config_layout) main_layout.addWidget(config_group) self.info_group = QGroupBox("流信息") self.update_info_group() main_layout.addWidget(self.info_group) self.btn_push = QPushButton("🎬 开始全屏投屏") self.btn_push.setFixedHeight(50) self.btn_push.setFont(QFont("Microsoft YaHei", 11)) self.btn_push.setStyleSheet(""" QPushButton { background-color: #2E7D32; color: white; border: none; border-radius: 8px; font-weight: bold; } QPushButton:hover { background-color: #388E3C; } QPushButton:pressed { background-color: #1B5E20; } """) self.btn_push.clicked.connect(self.push_full_screen) main_layout.addWidget(self.btn_push) self.log_label = QLabel("日志: 等待启动...") self.log_label.setWordWrap(True) self.log_label.setFrameShape(QFrame.Shape.StyledPanel) self.log_label.setFixedHeight(40) main_layout.addWidget(self.log_label) self.checker = ConnectionChecker() self.checker.set_config(self.config) self.checker.status_update.connect(self.update_status) self.checker.start() logger.info("投屏源控制界面已初始化") def update_info_group(self): cfg = self.config if self.info_group.layout(): QWidget().setLayout(self.info_group.layout()) info_layout = QGridLayout() info_layout.addWidget(QLabel("服务器IP:"), 0, 0) info_layout.addWidget(QLabel(cfg["server_ip"]), 0, 1) info_layout.addWidget(QLabel("推流地址:"), 1, 0) info_layout.addWidget(QLabel(f"rtsp://{cfg['server_ip']}:8554/{cfg['stream_path']}"), 1, 1) info_layout.addWidget(QLabel("访问地址:"), 2, 0) info_layout.addWidget(QLabel(f"http://{cfg['server_ip']}:8889/webrtc.html?src={cfg['stream_path']}"), 2, 1) self.info_group.setLayout(info_layout) def open_settings(self): dialog = ConfigDialog(self.config, self) if dialog.exec(): self.config = dialog.get_config() save_config(self.config) self.checker.set_config(self.config) self.update_info_group() self.log_label.setText("日志: 配置已更新") def update_status(self, status): if status["mediamtx"]: self.mediamtx_indicator.setStyleSheet("color: green; font-size: 16px;") self.mediamtx_label.setText("MediaMTX: ✓ 运行中") else: self.mediamtx_indicator.setStyleSheet("color: red; font-size: 16px;") self.mediamtx_label.setText("MediaMTX: ✗ 未运行") if status["ffmpeg"]: self.ffmpeg_indicator.setStyleSheet("color: green; font-size: 16px;") self.ffmpeg_label.setText("FFmpeg: ✓ 已安装") else: self.ffmpeg_indicator.setStyleSheet("color: red; font-size: 16px;") self.ffmpeg_label.setText("FFmpeg: ✗ 未找到") if status["server"]: self.server_indicator.setStyleSheet("color: green; font-size: 16px;") self.server_label.setText("服务器连接: ✓ 可连接") else: self.server_indicator.setStyleSheet("color: orange; font-size: 16px;") self.server_label.setText("服务器连接: ✗ 连接失败") if status["server_port"]: self.port_indicator.setStyleSheet("color: green; font-size: 16px;") self.port_label.setText("RTSP端口: ✓ 端口开放") else: self.port_indicator.setStyleSheet("color: orange; font-size: 16px;") self.port_label.setText("RTSP端口: ✗ 端口关闭") def start_mediamtx(self): cfg = self.config if not self.checker.check_mediamtx(cfg["mediamtx_path"]): logger.info("启动MediaMTX服务") subprocess.Popen([cfg["mediamtx_path"]], cwd=os.path.dirname(cfg["mediamtx_path"]), creationflags=subprocess.CREATE_NEW_CONSOLE) time.sleep(3) logger.info("MediaMTX服务已启动") self.log_label.setText("日志: MediaMTX服务已启动") else: logger.info("MediaMTX已在运行") self.log_label.setText("日志: MediaMTX已在运行") def push_full_screen(self): cfg = self.config logger.info("开始全屏投屏") self.start_mediamtx() if not os.path.exists(cfg["ffmpeg_path"]): logger.error("FFmpeg未安装") QMessageBox.warning(self, "错误", "FFmpeg未安装,请检查配置!") return cmd = [ cfg["ffmpeg_path"], "-f", "gdigrab", "-framerate", "30", "-i", "desktop", "-c:v", "libx264", "-preset", "ultrafast", "-tune", "zerolatency", "-f", "rtsp", f"rtsp://{cfg['server_ip']}:8554/{cfg['stream_path']}" ] logger.info(f"执行FFmpeg命令: {' '.join(cmd)}") subprocess.Popen(cmd, creationflags=subprocess.CREATE_NEW_CONSOLE) logger.info("FFmpeg推流已启动") self.log_label.setText("日志: FFmpeg推流已启动,正在向服务器推送...") QMessageBox.information(self, "提示", f"全屏投屏已启动!\n\n接收端可打开浏览器访问:\nhttp://{cfg['server_ip']}:8889/webrtc.html?src={cfg['stream_path']}") def closeEvent(self, event): self.checker.terminate() event.accept() if __name__ == "__main__": logger.info("应用程序启动") app = QApplication(sys.argv) app.setStyle("Fusion") window = MainWindow() window.show() sys.exit(app.exec())