Files
meetingroom-netscreen/dual_screen_push.py

439 lines
16 KiB
Python
Raw Permalink Normal View History

import subprocess
import psutil
import os
import sys
import json
import socket
import time
import re
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, QComboBox,
QSpinBox, QCheckBox)
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__)), "dual_screen_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",
"front_stream_path": "front",
"back_stream_path": "back"
}
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}")
def get_displays():
displays = []
try:
result = subprocess.run(
['powershell', '-Command',
'Add-Type -AssemblyName System.Windows.Forms; '
'[System.Windows.Forms.Screen]::AllScreens | ForEach-Object { '
'$dev = $_.DeviceName; '
'$w = $_.Bounds.Width; '
'$h = $_.Bounds.Height; '
'$x = $_.Bounds.X; '
'$y = $_.Bounds.Y; '
'$isPrimary = $_.Primary; '
'Write-Output "$dev|$w|$h|$x|$y|$isPrimary" }'],
capture_output=True, text=True, timeout=10
)
for line in result.stdout.strip().split('\n'):
if line:
parts = line.split('|')
if len(parts) >= 6:
displays.append({
'name': parts[0],
'width': int(parts[1]),
'height': int(parts[2]),
'x': int(parts[3]),
'y': int(parts[4]),
'primary': parts[5] == 'True'
})
except Exception as e:
logger.error(f"获取显示器信息失败: {e}")
return displays
class ConfigDialog(QDialog):
def __init__(self, config, parent=None):
super().__init__(parent)
self.config = config
self.setWindowTitle("设置")
self.setFixedSize(450, 280)
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.front_stream_edit = QLineEdit(config["front_stream_path"])
self.back_stream_edit = QLineEdit(config["back_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_layout = QGridLayout()
path_layout.addWidget(self.mediamtx_path_edit, 0, 0)
path_layout.addWidget(self.mediamtx_btn, 0, 1)
path_layout.addWidget(self.ffmpeg_path_edit, 1, 0)
path_layout.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.front_stream_edit)
layout.addRow("后置屏幕流名:", self.back_stream_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(),
"front_stream_path": self.front_stream_edit.text().strip() or "front",
"back_stream_path": self.back_stream_edit.text().strip() or "back"
}
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.processes = []
self.setWindowTitle("双屏投屏控制")
self.setFixedSize(600, 500)
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)
self.displays = get_displays()
display_info = QLabel(f"检测到 {len(self.displays)} 个显示器")
display_info.setAlignment(Qt.AlignCenter)
main_layout.addWidget(display_info)
self.display_group = QGroupBox("显示器信息")
display_layout = QGridLayout()
self.display_labels = []
for i, display in enumerate(self.displays):
label = QLabel(f"显示器{i+1}: {display['width']}x{display['height']} "
f"(@ {display['x']},{display['y']}) "
f"{'主屏' if display['primary'] else ''}")
label.setStyleSheet("padding: 5px;")
display_layout.addWidget(label, i, 0)
self.display_labels.append(label)
self.display_group.setLayout(display_layout)
main_layout.addWidget(self.display_group)
config_group = QGroupBox("本地配置")
config_layout = QGridLayout()
self.mediamtx_label = QLabel("MediaMTX: 未检测")
self.ffmpeg_label = QLabel("FFmpeg: 未检测")
self.server_label = QLabel("服务器连接: 未检测")
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)
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_group.setLayout(config_layout)
main_layout.addWidget(config_group)
self.info_group = QGroupBox("流信息")
self.update_info_group()
main_layout.addWidget(self.info_group)
btn_layout = QHBoxLayout()
self.btn_front = QPushButton("🎬 投送前置屏幕")
self.btn_front.setFixedHeight(45)
self.btn_front.setFont(QFont("Microsoft YaHei", 10))
self.btn_front.setStyleSheet("""
QPushButton {
background-color: #1976D2;
color: white;
border: none;
border-radius: 8px;
font-weight: bold;
}
QPushButton:hover {
background-color: #1565C0;
}
""")
self.btn_front.clicked.connect(lambda: self.push_screen("front"))
self.btn_back = QPushButton("🎬 投送后置屏幕")
self.btn_back.setFixedHeight(45)
self.btn_back.setFont(QFont("Microsoft YaHei", 10))
self.btn_back.setStyleSheet("""
QPushButton {
background-color: #388E3C;
color: white;
border: none;
border-radius: 8px;
font-weight: bold;
}
QPushButton:hover {
background-color: #2E7D32;
}
""")
self.btn_back.clicked.connect(lambda: self.push_screen("back"))
btn_layout.addWidget(self.btn_front)
btn_layout.addWidget(self.btn_back)
main_layout.addLayout(btn_layout)
self.btn_stop = QPushButton("⏹ 停止所有推流")
self.btn_stop.setFixedHeight(40)
self.btn_stop.setFont(QFont("Microsoft YaHei", 10))
self.btn_stop.setStyleSheet("""
QPushButton {
background-color: #D32F2F;
color: white;
border: none;
border-radius: 8px;
font-weight: bold;
}
QPushButton:hover {
background-color: #C62828;
}
""")
self.btn_stop.clicked.connect(self.stop_all)
main_layout.addWidget(self.btn_stop)
self.log_label = QLabel("日志: 等待操作...")
self.log_label.setWordWrap(True)
self.log_label.setFrameShape(QFrame.Shape.StyledPanel)
self.log_label.setFixedHeight(50)
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['front_stream_path']}"), 1, 1)
info_layout.addWidget(QLabel("后置屏幕流:"), 2, 0)
info_layout.addWidget(QLabel(f"rtsp://{cfg['server_ip']}:8554/{cfg['back_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("服务器连接: ✗ 连接失败")
def push_screen(self, screen_type):
cfg = self.config
displays = self.displays
if len(displays) < 2:
QMessageBox.warning(self, "错误", "未检测到足够的显示器!")
return
if screen_type == "front":
display = displays[0]
stream_path = cfg["front_stream_path"]
screen_name = "前置屏幕"
else:
display = displays[1] if len(displays) > 1 else displays[0]
stream_path = cfg["back_stream_path"]
screen_name = "后置屏幕"
logger.info(f"开始投送{screen_name}: {display['width']}x{display['height']}")
cmd = [
cfg["ffmpeg_path"],
"-f", "gdigrab",
"-offset_x", str(display['x']),
"-offset_y", str(display['y']),
"-video_size", f"{display['width']}x{display['height']}",
"-framerate", "30",
"-i", "desktop",
"-c:v", "libx264",
"-preset", "ultrafast",
"-tune", "zerolatency",
"-f", "rtsp",
f"rtsp://{cfg['server_ip']}:8554/{stream_path}"
]
logger.info(f"执行FFmpeg命令: {' '.join(cmd)}")
process = subprocess.Popen(cmd, creationflags=subprocess.CREATE_NEW_CONSOLE)
self.processes.append(process)
self.log_label.setText(f"日志: {screen_name}正在投送... "
f"访问 http://{cfg['server_ip']}:8889/{stream_path}")
QMessageBox.information(self, "提示",
f"{screen_name}投送已启动!\n\n"
f"观众访问http://{cfg['server_ip']}:8889/{stream_path}")
def stop_all(self):
for proc in self.processes:
try:
proc.terminate()
except:
pass
self.processes.clear()
for proc in psutil.process_iter(['name']):
try:
if proc.info['name'] == 'ffmpeg.exe':
proc.terminate()
except:
pass
self.log_label.setText("日志: 所有推流已停止")
logger.info("所有推流已停止")
def closeEvent(self, event):
self.stop_all()
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())