2026-03-19 09:35:39 +08:00
|
|
|
|
import subprocess
|
|
|
|
|
|
import psutil
|
|
|
|
|
|
import os
|
|
|
|
|
|
import sys
|
2026-03-19 09:41:43 +08:00
|
|
|
|
import socket
|
2026-03-19 09:35:39 +08:00
|
|
|
|
import time
|
|
|
|
|
|
from loguru import logger
|
2026-03-19 09:41:43 +08:00
|
|
|
|
from PySide6.QtWidgets import (QApplication, QMainWindow, QPushButton, QMessageBox,
|
|
|
|
|
|
QVBoxLayout, QWidget, QLabel, QGroupBox, QGridLayout,
|
|
|
|
|
|
QStatusBar, QProgressBar, QFrame)
|
|
|
|
|
|
from PySide6.QtCore import QTimer, Qt, QThread, Signal
|
|
|
|
|
|
from PySide6.QtGui import QFont, QPalette, QColor
|
2026-03-19 09:35:39 +08:00
|
|
|
|
|
|
|
|
|
|
logger.add(sys.stderr, format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}", level="INFO")
|
|
|
|
|
|
|
|
|
|
|
|
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"
|
|
|
|
|
|
|
2026-03-19 09:41:43 +08:00
|
|
|
|
class ConnectionChecker(QThread):
|
|
|
|
|
|
status_update = Signal(dict)
|
|
|
|
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
|
|
while True:
|
|
|
|
|
|
status = {
|
|
|
|
|
|
"mediamtx": self.check_mediamtx(),
|
|
|
|
|
|
"ffmpeg": self.check_ffmpeg(),
|
|
|
|
|
|
"server": self.check_server(),
|
|
|
|
|
|
"server_port": self.check_port(SERVER_IP, 8554),
|
|
|
|
|
|
}
|
|
|
|
|
|
self.status_update.emit(status)
|
|
|
|
|
|
time.sleep(3)
|
|
|
|
|
|
|
|
|
|
|
|
def check_mediamtx(self):
|
|
|
|
|
|
for proc in psutil.process_iter(['name']):
|
|
|
|
|
|
if proc.info['name'] == 'mediamtx.exe':
|
|
|
|
|
|
return True
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def check_ffmpeg(self):
|
|
|
|
|
|
return os.path.exists(FFMPEG_PATH)
|
|
|
|
|
|
|
|
|
|
|
|
def check_server(self):
|
|
|
|
|
|
try:
|
|
|
|
|
|
socket.create_connection((SERVER_IP, 8554), timeout=2)
|
2026-03-19 09:35:39 +08:00
|
|
|
|
return True
|
2026-03-19 09:41:43 +08:00
|
|
|
|
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
|
2026-03-19 09:35:39 +08:00
|
|
|
|
|
|
|
|
|
|
class MainWindow(QMainWindow):
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
|
super().__init__()
|
|
|
|
|
|
self.setWindowTitle("投屏源控制")
|
2026-03-19 09:46:44 +08:00
|
|
|
|
self.setFixedSize(520, 420)
|
2026-03-19 09:41:43 +08:00
|
|
|
|
|
|
|
|
|
|
self.status_bar = QStatusBar()
|
|
|
|
|
|
self.setStatusBar(self.status_bar)
|
2026-03-19 09:35:39 +08:00
|
|
|
|
|
|
|
|
|
|
central_widget = QWidget()
|
|
|
|
|
|
self.setCentralWidget(central_widget)
|
2026-03-19 09:41:43 +08:00
|
|
|
|
main_layout = QVBoxLayout(central_widget)
|
|
|
|
|
|
|
|
|
|
|
|
title_label = QLabel("会议投屏系统 - 主播端")
|
|
|
|
|
|
title_font = QFont("Microsoft YaHei", 14, QFont.Bold)
|
|
|
|
|
|
title_label.setFont(title_font)
|
|
|
|
|
|
title_label.setAlignment(Qt.AlignCenter)
|
|
|
|
|
|
main_layout.addWidget(title_label)
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
2026-03-19 09:35:39 +08:00
|
|
|
|
|
2026-03-19 09:41:43 +08:00
|
|
|
|
info_group = QGroupBox("流信息")
|
|
|
|
|
|
info_layout = QGridLayout()
|
|
|
|
|
|
info_layout.addWidget(QLabel("服务器IP:"), 0, 0)
|
|
|
|
|
|
info_layout.addWidget(QLabel(SERVER_IP), 0, 1)
|
|
|
|
|
|
info_layout.addWidget(QLabel("推流地址:"), 1, 0)
|
|
|
|
|
|
info_layout.addWidget(QLabel(f"rtsp://{SERVER_IP}:8554/{STREAM_PATH}"), 1, 1)
|
|
|
|
|
|
info_layout.addWidget(QLabel("访问地址:"), 2, 0)
|
|
|
|
|
|
info_layout.addWidget(QLabel(f"http://{SERVER_IP}:8889/webrtc.html?src=screen"), 2, 1)
|
|
|
|
|
|
info_group.setLayout(info_layout)
|
|
|
|
|
|
main_layout.addWidget(info_group)
|
|
|
|
|
|
|
|
|
|
|
|
self.btn_push = QPushButton("🎬 开始全屏投屏")
|
2026-03-19 09:35:39 +08:00
|
|
|
|
self.btn_push.setFixedHeight(50)
|
2026-03-19 09:41:43 +08:00
|
|
|
|
self.btn_push.setFont(QFont("Microsoft YaHei", 11))
|
|
|
|
|
|
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.status_update.connect(self.update_status)
|
|
|
|
|
|
self.checker.start()
|
2026-03-19 09:35:39 +08:00
|
|
|
|
|
|
|
|
|
|
logger.info("投屏源控制界面已初始化")
|
|
|
|
|
|
|
2026-03-19 09:41:43 +08:00
|
|
|
|
def update_status(self, status):
|
|
|
|
|
|
if status["mediamtx"]:
|
|
|
|
|
|
self.mediamtx_indicator.setStyleSheet("color: green; font-size: 16px;")
|
|
|
|
|
|
self.mediamtx_label.setText(f"MediaMTX: ✓ 运行中")
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.mediamtx_indicator.setStyleSheet("color: red; font-size: 16px;")
|
|
|
|
|
|
self.mediamtx_label.setText(f"MediaMTX: ✗ 未运行")
|
|
|
|
|
|
|
|
|
|
|
|
if status["ffmpeg"]:
|
|
|
|
|
|
self.ffmpeg_indicator.setStyleSheet("color: green; font-size: 16px;")
|
|
|
|
|
|
self.ffmpeg_label.setText(f"FFmpeg: ✓ 已安装")
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.ffmpeg_indicator.setStyleSheet("color: red; font-size: 16px;")
|
|
|
|
|
|
self.ffmpeg_label.setText(f"FFmpeg: ✗ 未找到")
|
|
|
|
|
|
|
|
|
|
|
|
if status["server"]:
|
|
|
|
|
|
self.server_indicator.setStyleSheet("color: green; font-size: 16px;")
|
|
|
|
|
|
self.server_label.setText(f"服务器连接: ✓ 可连接")
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.server_indicator.setStyleSheet("color: orange; font-size: 16px;")
|
|
|
|
|
|
self.server_label.setText(f"服务器连接: ✗ 连接失败")
|
|
|
|
|
|
|
|
|
|
|
|
if status["server_port"]:
|
|
|
|
|
|
self.port_indicator.setStyleSheet("color: green; font-size: 16px;")
|
|
|
|
|
|
self.port_label.setText(f"RTSP端口: ✓ 端口开放")
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.port_indicator.setStyleSheet("color: orange; font-size: 16px;")
|
|
|
|
|
|
self.port_label.setText(f"RTSP端口: ✗ 端口关闭")
|
|
|
|
|
|
|
|
|
|
|
|
def start_mediamtx(self):
|
|
|
|
|
|
if not self.checker.check_mediamtx():
|
|
|
|
|
|
logger.info("启动MediaMTX服务")
|
|
|
|
|
|
subprocess.Popen([MEDIAMTX_PATH], cwd=r"D:\ScreenCast\mediamtx", 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):
|
|
|
|
|
|
logger.info("开始全屏投屏")
|
|
|
|
|
|
self.start_mediamtx()
|
|
|
|
|
|
|
|
|
|
|
|
if not os.path.exists(FFMPEG_PATH):
|
|
|
|
|
|
logger.error("FFmpeg未安装")
|
|
|
|
|
|
QMessageBox.warning(self, "错误", "FFmpeg未安装,请检查配置!")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
cmd = [
|
|
|
|
|
|
FFMPEG_PATH,
|
|
|
|
|
|
"-f", "gdigrab", "-framerate", "30", "-i", "desktop",
|
|
|
|
|
|
"-c:v", "libx264", "-preset", "ultrafast", "-tune", "zerolatency",
|
|
|
|
|
|
"-f", "rtsp", f"rtsp://{SERVER_IP}:8554/{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://{SERVER_IP}:8889/webrtc.html?src=screen")
|
|
|
|
|
|
|
|
|
|
|
|
def closeEvent(self, event):
|
|
|
|
|
|
self.checker.terminate()
|
|
|
|
|
|
event.accept()
|
|
|
|
|
|
|
2026-03-19 09:35:39 +08:00
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
logger.info("应用程序启动")
|
|
|
|
|
|
app = QApplication(sys.argv)
|
2026-03-19 09:41:43 +08:00
|
|
|
|
app.setStyle("Fusion")
|
2026-03-19 09:35:39 +08:00
|
|
|
|
window = MainWindow()
|
|
|
|
|
|
window.show()
|
|
|
|
|
|
sys.exit(app.exec())
|