727 lines
24 KiB
Python
727 lines
24 KiB
Python
# camera_player_qt.py
|
||
import sys
|
||
import cv2
|
||
import av
|
||
import numpy as np
|
||
import grpc
|
||
import threading
|
||
import time
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
|
||
QHBoxLayout, QLabel, QPushButton, QLineEdit,
|
||
QFileDialog, QMessageBox, QStatusBar, QGroupBox,
|
||
QGridLayout, QComboBox, QSpinBox, QCheckBox, QFrame)
|
||
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer
|
||
from PyQt5.QtGui import QImage, QPixmap
|
||
|
||
# 导入生成的protobuf模块
|
||
from cmvr.api import camera_service_pb2_grpc
|
||
from cmvr.api import camera_command_pb2
|
||
from cmvr.api.common_pb2 import CommandHeader
|
||
from google.protobuf.timestamp_pb2 import Timestamp
|
||
|
||
|
||
class StreamThread(QThread):
|
||
"""流接收线程"""
|
||
frame_received = pyqtSignal(object, int, int) # frame, width, height
|
||
status_update = pyqtSignal(dict)
|
||
error_occurred = pyqtSignal(str)
|
||
keyframe_received = pyqtSignal()
|
||
|
||
# 支持的编码格式
|
||
CODECS = {
|
||
'H.265 (HEVC)': 'hevc',
|
||
'H.264 (AVC)': 'h264'
|
||
}
|
||
|
||
def __init__(self, device_id, server_addr, codec='hevc', wait_for_keyframe=True):
|
||
super().__init__()
|
||
self.device_id = device_id
|
||
self.server_addr = server_addr
|
||
self.codec_name = codec
|
||
self.wait_for_keyframe = wait_for_keyframe
|
||
self.running = False
|
||
self.streaming = False
|
||
|
||
# gRPC
|
||
self.channel = None
|
||
self.stub = None
|
||
|
||
# 解码器
|
||
self.codec = None
|
||
self._init_decoder()
|
||
|
||
# 统计
|
||
self.frame_count = 0
|
||
self.fps_counter = 0
|
||
self.fps_time = time.time()
|
||
self.fps = 0
|
||
self.keyframe_count = 0
|
||
|
||
def _init_decoder(self):
|
||
"""初始化解码器"""
|
||
try:
|
||
# 根据选择的编码创建解码器
|
||
self.codec = av.CodecContext.create(self.codec_name, "r")
|
||
self.codec.thread_count = 1
|
||
|
||
# 设置低延迟模式
|
||
if hasattr(self.codec, 'flags'):
|
||
try:
|
||
self.codec.flags |= av.codec.context.AV_CODEC_FLAG_LOW_DELAY
|
||
except:
|
||
pass
|
||
|
||
print(f"✓ {self.codec_name.upper()}解码器初始化成功")
|
||
except Exception as e:
|
||
print(f"✗ 解码器初始化失败: {e}")
|
||
self.codec = None
|
||
|
||
def _create_header(self):
|
||
"""创建请求头"""
|
||
timestamp = Timestamp()
|
||
timestamp.GetCurrentTime()
|
||
return CommandHeader.Request(
|
||
device_id=self.device_id,
|
||
timestamp=timestamp
|
||
)
|
||
|
||
def connect(self):
|
||
"""连接服务器"""
|
||
try:
|
||
self.channel = grpc.insecure_channel(self.server_addr)
|
||
grpc.channel_ready_future(self.channel).result(timeout=3)
|
||
self.stub = camera_service_pb2_grpc.CameraServiceStub(self.channel)
|
||
return True
|
||
except Exception as e:
|
||
self.error_occurred.emit(f"连接失败: {e}")
|
||
return False
|
||
|
||
def start_camera(self):
|
||
"""启动摄像头"""
|
||
try:
|
||
request = camera_command_pb2.StartCameraCommand.Request(
|
||
header=self._create_header()
|
||
)
|
||
response = self.stub.StartCamera(request, timeout=3)
|
||
return response.header.success
|
||
except Exception as e:
|
||
self.error_occurred.emit(f"启动摄像头失败: {e}")
|
||
return False
|
||
|
||
def stop_camera(self):
|
||
"""停止摄像头"""
|
||
try:
|
||
request = camera_command_pb2.StopCameraCommand.Request(
|
||
header=self._create_header()
|
||
)
|
||
self.stub.StopCamera(request, timeout=3)
|
||
except:
|
||
pass
|
||
|
||
def run(self):
|
||
"""线程主函数"""
|
||
if not self.connect():
|
||
return
|
||
|
||
if not self.start_camera():
|
||
return
|
||
|
||
self.running = True
|
||
self.streaming = True
|
||
self.frame_count = 0
|
||
self.fps_counter = 0
|
||
self.fps_time = time.time()
|
||
keyframe_received = False
|
||
|
||
def request_gen():
|
||
while self.streaming:
|
||
yield camera_command_pb2.GetRGBImageStreamCommand.Request(
|
||
header=self._create_header(),
|
||
eof=False
|
||
)
|
||
time.sleep(0.001)
|
||
yield camera_command_pb2.GetRGBImageStreamCommand.Request(
|
||
header=self._create_header(),
|
||
eof=True
|
||
)
|
||
|
||
try:
|
||
responses = self.stub.GetRGBImageStream(request_gen())
|
||
|
||
for response in responses:
|
||
if not self.streaming:
|
||
break
|
||
|
||
if response.header.success:
|
||
frame_data = response.color_frame.data
|
||
is_keyframe = response.color_frame.is_key_frame
|
||
|
||
# 等待首个关键帧
|
||
if self.wait_for_keyframe and not keyframe_received and not is_keyframe:
|
||
continue
|
||
|
||
if is_keyframe and not keyframe_received:
|
||
keyframe_received = True
|
||
self.keyframe_count += 1
|
||
self.keyframe_received.emit()
|
||
|
||
# 解码
|
||
if self.codec and frame_data:
|
||
packet = av.Packet(frame_data)
|
||
frames = self.codec.decode(packet)
|
||
|
||
if frames:
|
||
# 转换为RGB (已经是RGB格式)
|
||
img = frames[0].to_ndarray(format='rgb24')
|
||
|
||
# 更新统计
|
||
self.fps_counter += 1
|
||
self.frame_count += 1
|
||
|
||
# 计算FPS
|
||
now = time.time()
|
||
if now - self.fps_time >= 1.0:
|
||
self.fps = self.fps_counter
|
||
self.fps_counter = 0
|
||
self.fps_time = now
|
||
|
||
# 发送状态更新
|
||
self.status_update.emit({
|
||
'fps': self.fps,
|
||
'frames': self.frame_count,
|
||
'keyframes': self.keyframe_count
|
||
})
|
||
|
||
# 发送帧到主线程
|
||
h, w = img.shape[:2]
|
||
self.frame_received.emit(img, w, h)
|
||
|
||
else:
|
||
print(f"流错误: {response.header.error_message}")
|
||
|
||
except Exception as e:
|
||
self.error_occurred.emit(f"流错误: {e}")
|
||
finally:
|
||
self.stop_camera()
|
||
if self.channel:
|
||
self.channel.close()
|
||
self.running = False
|
||
self.streaming = False
|
||
|
||
def stop(self):
|
||
"""停止线程"""
|
||
self.streaming = False
|
||
self.wait(2000) # 等待最多2秒
|
||
|
||
|
||
class CameraPlayerQt(QMainWindow):
|
||
"""基于Qt的摄像头拉流播放器"""
|
||
|
||
def __init__(self):
|
||
super().__init__()
|
||
self.setWindowTitle("摄像头拉流播放器 (Qt版)")
|
||
self.setMinimumSize(1000, 800)
|
||
|
||
# 状态变量
|
||
self.connected = False
|
||
self.streaming = False
|
||
self.recording = False
|
||
self.stream_thread = None
|
||
self.video_writer = None
|
||
|
||
# 录制目录
|
||
self.record_path = Path("./recordings")
|
||
self.record_path.mkdir(exist_ok=True)
|
||
|
||
# 创建UI
|
||
self._create_ui()
|
||
|
||
# 状态栏
|
||
self.status_bar = QStatusBar()
|
||
self.setStatusBar(self.status_bar)
|
||
self.status_bar.showMessage("就绪")
|
||
|
||
# 定时器用于更新显示
|
||
self.display_timer = QTimer()
|
||
self.display_timer.timeout.connect(self._update_display)
|
||
self.display_timer.start(30) # 约33fps
|
||
|
||
# 当前帧缓存
|
||
self.current_frame = None
|
||
self.frame_width = 0
|
||
self.frame_height = 0
|
||
|
||
def _create_ui(self):
|
||
"""创建UI"""
|
||
central_widget = QWidget()
|
||
self.setCentralWidget(central_widget)
|
||
main_layout = QVBoxLayout(central_widget)
|
||
|
||
# ========== 连接设置区域 ==========
|
||
conn_group = QGroupBox("连接设置")
|
||
conn_layout = QGridLayout()
|
||
|
||
# 第一行:设备ID和编码格式
|
||
conn_layout.addWidget(QLabel("设备ID:"), 0, 0)
|
||
self.device_id = QLineEdit("right_hand_cam")
|
||
conn_layout.addWidget(self.device_id, 0, 1)
|
||
|
||
conn_layout.addWidget(QLabel("编码格式:"), 0, 2)
|
||
self.codec_combo = QComboBox()
|
||
self.codec_combo.addItems(['H.265 (HEVC)', 'H.264 (AVC)'])
|
||
self.codec_combo.setCurrentIndex(0)
|
||
self.codec_combo.setMinimumWidth(120)
|
||
conn_layout.addWidget(self.codec_combo, 0, 3)
|
||
|
||
# 等待关键帧选项
|
||
self.wait_keyframe = QCheckBox("等待关键帧")
|
||
self.wait_keyframe.setChecked(True)
|
||
conn_layout.addWidget(self.wait_keyframe, 0, 4)
|
||
|
||
# 第二行:服务器地址
|
||
conn_layout.addWidget(QLabel("服务器IP:"), 1, 0)
|
||
self.server_ip = QLineEdit("192.168.0.222")
|
||
conn_layout.addWidget(self.server_ip, 1, 1)
|
||
|
||
conn_layout.addWidget(QLabel("端口:"), 1, 2)
|
||
self.server_port = QLineEdit("50052")
|
||
self.server_port.setMaximumWidth(80)
|
||
conn_layout.addWidget(self.server_port, 1, 3)
|
||
|
||
# 超时设置
|
||
conn_layout.addWidget(QLabel("超时(秒):"), 1, 4)
|
||
self.timeout_spin = QSpinBox()
|
||
self.timeout_spin.setRange(1, 10)
|
||
self.timeout_spin.setValue(3)
|
||
self.timeout_spin.setMaximumWidth(60)
|
||
conn_layout.addWidget(self.timeout_spin, 1, 5)
|
||
|
||
# 第三行:录制目录
|
||
conn_layout.addWidget(QLabel("录制目录:"), 2, 0)
|
||
self.record_dir = QLineEdit(str(self.record_path))
|
||
conn_layout.addWidget(self.record_dir, 2, 1, 1, 4)
|
||
|
||
self.browse_btn = QPushButton("浏览")
|
||
self.browse_btn.clicked.connect(self._browse_dir)
|
||
conn_layout.addWidget(self.browse_btn, 2, 5)
|
||
|
||
# 第四行:控制按钮
|
||
btn_layout = QHBoxLayout()
|
||
|
||
self.connect_btn = QPushButton("连接")
|
||
self.connect_btn.clicked.connect(self._connect)
|
||
self.connect_btn.setMinimumWidth(80)
|
||
btn_layout.addWidget(self.connect_btn)
|
||
|
||
self.preview_btn = QPushButton("预览")
|
||
self.preview_btn.clicked.connect(self._toggle_preview)
|
||
self.preview_btn.setEnabled(False)
|
||
self.preview_btn.setMinimumWidth(80)
|
||
btn_layout.addWidget(self.preview_btn)
|
||
|
||
self.record_btn = QPushButton("录制")
|
||
self.record_btn.clicked.connect(self._toggle_recording)
|
||
self.record_btn.setEnabled(False)
|
||
self.record_btn.setMinimumWidth(80)
|
||
btn_layout.addWidget(self.record_btn)
|
||
|
||
self.disconnect_btn = QPushButton("断开")
|
||
self.disconnect_btn.clicked.connect(self._disconnect)
|
||
self.disconnect_btn.setEnabled(False)
|
||
self.disconnect_btn.setMinimumWidth(80)
|
||
btn_layout.addWidget(self.disconnect_btn)
|
||
|
||
btn_layout.addStretch()
|
||
|
||
conn_layout.addLayout(btn_layout, 3, 0, 1, 6)
|
||
|
||
conn_group.setLayout(conn_layout)
|
||
main_layout.addWidget(conn_group)
|
||
|
||
# ========== 状态显示区域 ==========
|
||
status_group = QGroupBox("状态信息")
|
||
status_layout = QHBoxLayout()
|
||
|
||
self.conn_status = QLabel("● 未连接")
|
||
self.conn_status.setStyleSheet("color: red;")
|
||
status_layout.addWidget(self.conn_status)
|
||
|
||
status_layout.addWidget(QLabel("|"))
|
||
|
||
self.codec_status = QLabel("编码: H.265")
|
||
status_layout.addWidget(self.codec_status)
|
||
|
||
status_layout.addWidget(QLabel("|"))
|
||
|
||
self.fps_status = QLabel("FPS: 0")
|
||
status_layout.addWidget(self.fps_status)
|
||
|
||
status_layout.addWidget(QLabel("|"))
|
||
|
||
self.frame_status = QLabel("帧数: 0")
|
||
status_layout.addWidget(self.frame_status)
|
||
|
||
status_layout.addWidget(QLabel("|"))
|
||
|
||
self.resolution_status = QLabel("分辨率: -")
|
||
status_layout.addWidget(self.resolution_status)
|
||
|
||
status_layout.addWidget(QLabel("|"))
|
||
|
||
self.keyframe_status = QLabel("关键帧: 等待")
|
||
self.keyframe_status.setStyleSheet("color: orange;")
|
||
status_layout.addWidget(self.keyframe_status)
|
||
|
||
status_layout.addWidget(QLabel("|"))
|
||
|
||
self.record_status = QLabel("录制: 未开始")
|
||
self.record_status.setStyleSheet("color: gray;")
|
||
status_layout.addWidget(self.record_status)
|
||
|
||
status_layout.addStretch()
|
||
status_group.setLayout(status_layout)
|
||
main_layout.addWidget(status_group)
|
||
|
||
# ========== 视频预览区域 ==========
|
||
video_group = QGroupBox("视频预览")
|
||
video_layout = QVBoxLayout()
|
||
|
||
self.video_label = QLabel()
|
||
self.video_label.setAlignment(Qt.AlignCenter)
|
||
self.video_label.setMinimumSize(640, 480)
|
||
self.video_label.setStyleSheet("background-color: black; border: 1px solid gray;")
|
||
video_layout.addWidget(self.video_label)
|
||
|
||
video_group.setLayout(video_layout)
|
||
main_layout.addWidget(video_group, 1) # 1表示拉伸因子
|
||
|
||
# ========== 操作说明 ==========
|
||
info_frame = QFrame()
|
||
info_frame.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken)
|
||
info_layout = QHBoxLayout(info_frame)
|
||
|
||
info_label = QLabel("操作说明: ①选择编码格式 ②连接 ③预览 ④录制 (按'停止预览'结束)")
|
||
info_label.setStyleSheet("color: gray; padding: 5px;")
|
||
info_layout.addWidget(info_label)
|
||
|
||
main_layout.addWidget(info_frame)
|
||
|
||
def _browse_dir(self):
|
||
"""浏览目录"""
|
||
directory = QFileDialog.getExistingDirectory(self, "选择录制目录",
|
||
self.record_dir.text())
|
||
if directory:
|
||
self.record_dir.setText(directory)
|
||
self.record_path = Path(directory)
|
||
self.record_path.mkdir(exist_ok=True)
|
||
|
||
def _get_server_addr(self):
|
||
"""获取服务器地址"""
|
||
return f"{self.server_ip.text()}:{self.server_port.text()}"
|
||
|
||
def _get_codec(self):
|
||
"""获取编码格式"""
|
||
codec_map = {
|
||
'H.265 (HEVC)': 'hevc',
|
||
'H.264 (AVC)': 'h264'
|
||
}
|
||
return codec_map[self.codec_combo.currentText()]
|
||
|
||
def _connect(self):
|
||
"""连接服务器"""
|
||
codec = self._get_codec()
|
||
self.status_bar.showMessage(f"正在连接服务器 (编码: {codec.upper()})...")
|
||
|
||
# 更新编码状态显示
|
||
self.codec_status.setText(f"编码: {self.codec_combo.currentText()}")
|
||
|
||
# 创建流线程
|
||
self.stream_thread = StreamThread(
|
||
self.device_id.text(),
|
||
self._get_server_addr(),
|
||
codec=codec,
|
||
wait_for_keyframe=self.wait_keyframe.isChecked()
|
||
)
|
||
|
||
# 连接信号
|
||
self.stream_thread.frame_received.connect(self._on_frame_received)
|
||
self.stream_thread.status_update.connect(self._on_status_update)
|
||
self.stream_thread.error_occurred.connect(self._on_error)
|
||
self.stream_thread.keyframe_received.connect(self._on_keyframe_received)
|
||
|
||
# 连接测试
|
||
if self.stream_thread.connect():
|
||
self.connected = True
|
||
self.conn_status.setText("● 已连接")
|
||
self.conn_status.setStyleSheet("color: green;")
|
||
self.connect_btn.setEnabled(False)
|
||
self.preview_btn.setEnabled(True)
|
||
self.disconnect_btn.setEnabled(True)
|
||
self.codec_combo.setEnabled(False)
|
||
self.wait_keyframe.setEnabled(False)
|
||
self.status_bar.showMessage("服务器连接成功", 3000)
|
||
else:
|
||
self.stream_thread = None
|
||
|
||
def _disconnect(self):
|
||
"""断开连接"""
|
||
# 停止预览
|
||
if self.streaming:
|
||
self._stop_preview()
|
||
|
||
# 停止录制
|
||
if self.recording:
|
||
self._stop_recording()
|
||
|
||
# 清理线程
|
||
if self.stream_thread:
|
||
self.stream_thread.stop()
|
||
self.stream_thread = None
|
||
|
||
self.connected = False
|
||
self.conn_status.setText("● 未连接")
|
||
self.conn_status.setStyleSheet("color: red;")
|
||
self.connect_btn.setEnabled(True)
|
||
self.preview_btn.setEnabled(False)
|
||
self.preview_btn.setText("预览")
|
||
self.record_btn.setEnabled(False)
|
||
self.record_btn.setText("录制")
|
||
self.disconnect_btn.setEnabled(False)
|
||
self.codec_combo.setEnabled(True)
|
||
self.wait_keyframe.setEnabled(True)
|
||
|
||
# 清空画面
|
||
self.video_label.clear()
|
||
self.current_frame = None
|
||
|
||
# 重置状态
|
||
self.keyframe_status.setText("关键帧: 等待")
|
||
self.keyframe_status.setStyleSheet("color: orange;")
|
||
self.fps_status.setText("FPS: 0")
|
||
self.frame_status.setText("帧数: 0")
|
||
self.resolution_status.setText("分辨率: -")
|
||
|
||
self.status_bar.showMessage("已断开连接", 3000)
|
||
|
||
def _toggle_preview(self):
|
||
"""切换预览"""
|
||
if not self.streaming:
|
||
self._start_preview()
|
||
else:
|
||
self._stop_preview()
|
||
|
||
def _start_preview(self):
|
||
"""开始预览"""
|
||
if not self.stream_thread:
|
||
return
|
||
|
||
self.streaming = True
|
||
self.stream_thread.streaming = True
|
||
self.stream_thread.start()
|
||
|
||
self.preview_btn.setText("停止")
|
||
self.record_btn.setEnabled(True)
|
||
|
||
if self.wait_keyframe.isChecked():
|
||
self.keyframe_status.setText("关键帧: 等待中...")
|
||
self.keyframe_status.setStyleSheet("color: orange;")
|
||
else:
|
||
self.keyframe_status.setText("关键帧: 不等待")
|
||
self.keyframe_status.setStyleSheet("color: gray;")
|
||
|
||
self.status_bar.showMessage("预览已启动...")
|
||
|
||
def _stop_preview(self):
|
||
"""停止预览"""
|
||
if self.stream_thread:
|
||
self.stream_thread.streaming = False
|
||
self.stream_thread.wait(2000)
|
||
|
||
self.streaming = False
|
||
self.preview_btn.setText("预览")
|
||
self.record_btn.setEnabled(False)
|
||
|
||
# 停止录制
|
||
if self.recording:
|
||
self._stop_recording()
|
||
|
||
self.keyframe_status.setText("关键帧: 等待")
|
||
self.keyframe_status.setStyleSheet("color: orange;")
|
||
|
||
# 清空画面
|
||
self.video_label.clear()
|
||
self.current_frame = None
|
||
|
||
self.status_bar.showMessage("预览已停止", 3000)
|
||
|
||
def _toggle_recording(self):
|
||
"""切换录制"""
|
||
if not self.recording:
|
||
self._start_recording()
|
||
else:
|
||
self._stop_recording()
|
||
|
||
def _start_recording(self):
|
||
"""开始录制"""
|
||
if not self.streaming:
|
||
QMessageBox.warning(self, "警告", "请先启动预览")
|
||
return
|
||
|
||
# 更新录制目录
|
||
self.record_path = Path(self.record_dir.text())
|
||
self.record_path.mkdir(parents=True, exist_ok=True)
|
||
|
||
# 生成文件名
|
||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||
codec_name = self.codec_combo.currentText().replace(' ', '_')
|
||
filename = self.record_path / f"rec_{self.device_id.text()}_{codec_name}_{timestamp}.mp4"
|
||
|
||
# 获取画面尺寸
|
||
if self.current_frame is not None:
|
||
height, width = self.current_frame.shape[:2]
|
||
else:
|
||
width, height = 640, 480
|
||
|
||
# 创建VideoWriter
|
||
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
|
||
self.video_writer = cv2.VideoWriter(str(filename), fourcc, 30.0, (width, height))
|
||
|
||
if self.video_writer.isOpened():
|
||
self.recording = True
|
||
self.record_btn.setText("停止")
|
||
self.record_status.setText("● 录制中")
|
||
self.record_status.setStyleSheet("color: red;")
|
||
self.status_bar.showMessage(f"开始录制: {filename.name}", 3000)
|
||
else:
|
||
QMessageBox.critical(self, "错误", "无法创建视频文件")
|
||
|
||
def _stop_recording(self):
|
||
"""停止录制"""
|
||
if self.video_writer:
|
||
self.video_writer.release()
|
||
self.video_writer = None
|
||
|
||
self.recording = False
|
||
self.record_btn.setText("录制")
|
||
self.record_status.setText("录制: 未开始")
|
||
self.record_status.setStyleSheet("color: gray;")
|
||
self.status_bar.showMessage("录制已停止", 3000)
|
||
|
||
def _on_frame_received(self, frame, width, height):
|
||
"""收到新帧"""
|
||
self.current_frame = frame
|
||
self.frame_width = width
|
||
self.frame_height = height
|
||
|
||
# 录制
|
||
if self.recording and self.video_writer:
|
||
# 转换为BGR用于录制
|
||
frame_bgr = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
|
||
self.video_writer.write(frame_bgr)
|
||
|
||
def _update_display(self):
|
||
"""更新显示"""
|
||
if self.current_frame is not None:
|
||
# 直接使用QImage,避免转换
|
||
bytes_per_line = 3 * self.frame_width
|
||
qimage = QImage(self.current_frame.data, self.frame_width,
|
||
self.frame_height, bytes_per_line, QImage.Format_RGB888)
|
||
|
||
# 缩放以适应标签大小
|
||
label_size = self.video_label.size()
|
||
pixmap = QPixmap.fromImage(qimage).scaled(
|
||
label_size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
||
|
||
self.video_label.setPixmap(pixmap)
|
||
|
||
# 更新分辨率显示
|
||
self.resolution_status.setText(f"分辨率: {self.frame_width}x{self.frame_height}")
|
||
|
||
def _on_status_update(self, stats):
|
||
"""状态更新"""
|
||
self.fps_status.setText(f"FPS: {stats['fps']}")
|
||
self.frame_status.setText(f"帧数: {stats['frames']}")
|
||
|
||
def _on_keyframe_received(self):
|
||
"""收到关键帧"""
|
||
if self.wait_keyframe.isChecked():
|
||
self.keyframe_status.setText("关键帧: 已接收")
|
||
self.keyframe_status.setStyleSheet("color: green;")
|
||
|
||
def _on_error(self, msg):
|
||
"""错误处理"""
|
||
QMessageBox.critical(self, "错误", msg)
|
||
self.status_bar.showMessage(f"错误: {msg}")
|
||
|
||
def closeEvent(self, event):
|
||
"""窗口关闭事件"""
|
||
# 停止预览
|
||
if self.streaming:
|
||
self._stop_preview()
|
||
|
||
# 停止录制
|
||
if self.recording:
|
||
self._stop_recording()
|
||
|
||
# 断开连接
|
||
if self.connected:
|
||
self._disconnect()
|
||
|
||
event.accept()
|
||
|
||
|
||
def main():
|
||
app = QApplication(sys.argv)
|
||
|
||
# 设置应用样式
|
||
app.setStyle('Fusion')
|
||
|
||
# 设置全局样式表
|
||
app.setStyleSheet("""
|
||
QGroupBox {
|
||
font-weight: bold;
|
||
border: 1px solid gray;
|
||
border-radius: 5px;
|
||
margin-top: 10px;
|
||
padding-top: 10px;
|
||
}
|
||
QGroupBox::title {
|
||
subcontrol-origin: margin;
|
||
left: 10px;
|
||
padding: 0 5px 0 5px;
|
||
}
|
||
QPushButton {
|
||
padding: 5px 10px;
|
||
}
|
||
QLineEdit {
|
||
padding: 3px;
|
||
}
|
||
QComboBox {
|
||
padding: 3px;
|
||
}
|
||
""")
|
||
|
||
window = CameraPlayerQt()
|
||
window.show()
|
||
|
||
print("\n" + "=" * 60)
|
||
print("摄像头拉流播放器 (Qt版) 启动")
|
||
print("=" * 60)
|
||
print("支持编码:")
|
||
print(" • H.265 (HEVC) - 高压缩率")
|
||
print(" • H.264 (AVC) - 兼容性好")
|
||
print("\n操作步骤:")
|
||
print("1. 选择编码格式")
|
||
print("2. 输入设备ID、服务器IP和端口")
|
||
print("3. 点击【连接】按钮")
|
||
print("4. 点击【预览】按钮开始观看")
|
||
print("5. 点击【录制】按钮开始/停止录制")
|
||
print("=" * 60 + "\n")
|
||
|
||
sys.exit(app.exec_())
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main() |