add camera client
This commit is contained in:
parent
3ab4fbc3e0
commit
cf033e421b
503
clients/dexhand_client.py
Normal file
503
clients/dexhand_client.py
Normal file
@ -0,0 +1,503 @@
|
||||
import time
|
||||
import threading
|
||||
from typing import Optional, List
|
||||
import numpy as np
|
||||
|
||||
import grpc
|
||||
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
|
||||
QHBoxLayout, QLabel, QPushButton, QComboBox,
|
||||
QSpinBox, QGroupBox, QGridLayout, QLineEdit, QMessageBox)
|
||||
from PyQt5.QtCore import QTimer, Qt
|
||||
from PyQt5.QtGui import QFont
|
||||
import pyqtgraph as pg
|
||||
|
||||
from clients._path_setup import ensure_paths
|
||||
|
||||
ensure_paths()
|
||||
|
||||
from cmvr.api import dexhand_service_pb2_grpc
|
||||
from cmvr.api import dexhand_command_pb2
|
||||
from google.protobuf.timestamp_pb2 import Timestamp
|
||||
|
||||
|
||||
class DexHandSensorClient:
|
||||
"""灵巧手传感器数据客户端"""
|
||||
|
||||
def __init__(self, ip="192.168.0.222", port=50052, device_id="dexhand_001", timeout=2, retries=1):
|
||||
self.ip = ip
|
||||
self.port = port
|
||||
self.address = f"{ip}:{port}"
|
||||
self.device_id = device_id
|
||||
self.timeout = timeout
|
||||
self.retries = retries
|
||||
self.channel = None
|
||||
self.stub = None
|
||||
self._connect_with_retry()
|
||||
|
||||
def _connect_with_retry(self):
|
||||
"""连接gRPC服务器"""
|
||||
attempt = 0
|
||||
while attempt <= self.retries:
|
||||
print(f"Connecting to DexHand gRPC server at {self.address}... (Attempt {attempt + 1})")
|
||||
try:
|
||||
self.channel = grpc.insecure_channel(self.address)
|
||||
grpc.channel_ready_future(self.channel).result(timeout=self.timeout)
|
||||
self.stub = dexhand_service_pb2_grpc.DexHandServiceStub(self.channel)
|
||||
print(f"Successfully connected to DexHand gRPC server: {self.address}")
|
||||
print(f"Device ID: {self.device_id}")
|
||||
return True
|
||||
except grpc.FutureTimeoutError:
|
||||
attempt += 1
|
||||
print(f"Connection timed out, attempt {attempt} failed")
|
||||
if attempt > self.retries:
|
||||
return False
|
||||
print(f"Waiting {self.timeout}s before retrying...")
|
||||
time.sleep(self.timeout)
|
||||
except Exception as e:
|
||||
print(f"Connection error: {e}")
|
||||
return False
|
||||
return False
|
||||
|
||||
def get_sensor_data(self) -> Optional[dexhand_command_pb2.GetSensorDataCommand.Feedback]:
|
||||
"""获取传感器数据"""
|
||||
if not self.stub:
|
||||
print("No gRPC connection available")
|
||||
return None
|
||||
|
||||
try:
|
||||
# 创建请求
|
||||
timestamp = Timestamp()
|
||||
timestamp.GetCurrentTime()
|
||||
|
||||
request = dexhand_command_pb2.GetSensorDataCommand.Request()
|
||||
request.header.device_id = self.device_id
|
||||
request.header.timestamp.CopyFrom(timestamp)
|
||||
|
||||
# 发送请求
|
||||
feedback = self.stub.GetSensorData(request)
|
||||
return feedback
|
||||
|
||||
except grpc.RpcError as e:
|
||||
print(f"gRPC error: {e.code()} - {e.details()}")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"Error getting sensor data: {e}")
|
||||
return None
|
||||
|
||||
def close(self):
|
||||
"""关闭连接"""
|
||||
if self.channel:
|
||||
self.channel.close()
|
||||
print(f"Closed connection to DexHand gRPC server {self.address}")
|
||||
|
||||
def is_connected(self):
|
||||
"""检查是否已连接"""
|
||||
return self.channel is not None and self.stub is not None
|
||||
|
||||
|
||||
class SensorDataPlotWidget(QWidget):
|
||||
"""传感器数据绘制窗口"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.client = None
|
||||
self.update_timer = QTimer()
|
||||
self.data_buffer = {
|
||||
'sum': [], # sum数据缓冲区
|
||||
'peak': [] # peak数据缓冲区
|
||||
}
|
||||
self.time_buffer = [] # 时间戳缓冲区
|
||||
self.max_points = 100 # 最大显示点数
|
||||
self.current_finger = dexhand_command_pb2.SensorData.INDEX # 默认食指
|
||||
self.current_part = dexhand_command_pb2.SensorData.TIP # 默认指端
|
||||
|
||||
self.setup_ui()
|
||||
self.setup_plots()
|
||||
|
||||
def setup_ui(self):
|
||||
"""设置UI"""
|
||||
layout = QVBoxLayout()
|
||||
self.setLayout(layout)
|
||||
|
||||
# 标题
|
||||
title = QLabel("灵巧手传感器数据监控 - 食指指端")
|
||||
title_font = QFont()
|
||||
title_font.setPointSize(14)
|
||||
title_font.setBold(True)
|
||||
title.setFont(title_font)
|
||||
title.setAlignment(Qt.AlignCenter)
|
||||
layout.addWidget(title)
|
||||
|
||||
# 控制面板
|
||||
control_group = QGroupBox("连接配置")
|
||||
control_layout = QGridLayout()
|
||||
control_group.setLayout(control_layout)
|
||||
|
||||
# IP地址
|
||||
control_layout.addWidget(QLabel("服务器IP:"), 0, 0)
|
||||
self.ip_edit = QLineEdit("192.168.0.222")
|
||||
control_layout.addWidget(self.ip_edit, 0, 1)
|
||||
|
||||
# 端口
|
||||
control_layout.addWidget(QLabel("端口:"), 1, 0)
|
||||
self.port_edit = QLineEdit("50052")
|
||||
control_layout.addWidget(self.port_edit, 1, 1)
|
||||
|
||||
# 设备ID
|
||||
control_layout.addWidget(QLabel("设备ID:"), 2, 0)
|
||||
self.device_id_edit = QLineEdit("dexhand_001")
|
||||
control_layout.addWidget(self.device_id_edit, 2, 1)
|
||||
|
||||
# 完整地址显示
|
||||
control_layout.addWidget(QLabel("完整地址:"), 3, 0)
|
||||
self.address_label = QLabel("192.168.0.222:50052")
|
||||
self.address_label.setStyleSheet("font-weight: bold;")
|
||||
control_layout.addWidget(self.address_label, 3, 1)
|
||||
|
||||
# 连接按钮
|
||||
self.connect_btn = QPushButton("连接")
|
||||
self.connect_btn.clicked.connect(self.toggle_connection)
|
||||
self.connect_btn.setMinimumHeight(40)
|
||||
self.connect_btn.setStyleSheet("font-weight: bold;")
|
||||
control_layout.addWidget(self.connect_btn, 4, 0, 1, 2)
|
||||
|
||||
# 连接状态
|
||||
self.status_label = QLabel("未连接")
|
||||
self.status_label.setAlignment(Qt.AlignCenter)
|
||||
self.status_label.setStyleSheet("color: red; font-weight: bold; padding: 5px;")
|
||||
control_layout.addWidget(self.status_label, 5, 0, 1, 2)
|
||||
|
||||
layout.addWidget(control_group)
|
||||
|
||||
# 数据配置面板
|
||||
data_config_group = QGroupBox("数据配置")
|
||||
data_config_layout = QGridLayout()
|
||||
data_config_group.setLayout(data_config_layout)
|
||||
|
||||
# 更新频率
|
||||
data_config_layout.addWidget(QLabel("更新频率(ms):"), 0, 0)
|
||||
self.freq_spin = QSpinBox()
|
||||
self.freq_spin.setRange(10, 1000)
|
||||
self.freq_spin.setValue(50)
|
||||
self.freq_spin.valueChanged.connect(self.set_update_interval)
|
||||
data_config_layout.addWidget(self.freq_spin, 0, 1)
|
||||
|
||||
# 显示点数
|
||||
data_config_layout.addWidget(QLabel("显示点数:"), 1, 0)
|
||||
self.points_spin = QSpinBox()
|
||||
self.points_spin.setRange(10, 500)
|
||||
self.points_spin.setValue(100)
|
||||
self.points_spin.valueChanged.connect(self.set_max_points)
|
||||
data_config_layout.addWidget(self.points_spin, 1, 1)
|
||||
|
||||
# 清空数据按钮
|
||||
self.clear_btn = QPushButton("清空数据")
|
||||
self.clear_btn.clicked.connect(self.clear_data)
|
||||
self.clear_btn.setEnabled(False)
|
||||
data_config_layout.addWidget(self.clear_btn, 2, 0)
|
||||
|
||||
# 测试连接按钮
|
||||
self.test_btn = QPushButton("测试连接")
|
||||
self.test_btn.clicked.connect(self.test_connection)
|
||||
data_config_layout.addWidget(self.test_btn, 2, 1)
|
||||
|
||||
layout.addWidget(data_config_group)
|
||||
|
||||
# 创建绘图区域
|
||||
plots_widget = QWidget()
|
||||
plots_layout = QVBoxLayout()
|
||||
plots_widget.setLayout(plots_layout)
|
||||
|
||||
# SUM曲线图
|
||||
self.sum_plot_widget = pg.PlotWidget(title="食指指端传感器数据 - SUM值")
|
||||
self.sum_plot_widget.setLabel('left', 'SUM值')
|
||||
self.sum_plot_widget.setLabel('bottom', '时间 (秒)')
|
||||
self.sum_plot_widget.showGrid(x=True, y=True, alpha=0.3)
|
||||
self.sum_plot_widget.addLegend()
|
||||
self.sum_curve = self.sum_plot_widget.plot(pen='y', name='SUM值', width=2)
|
||||
plots_layout.addWidget(self.sum_plot_widget)
|
||||
|
||||
# PEAK曲线图
|
||||
self.peak_plot_widget = pg.PlotWidget(title="食指指端传感器数据 - PEAK值")
|
||||
self.peak_plot_widget.setLabel('left', 'PEAK值')
|
||||
self.peak_plot_widget.setLabel('bottom', '时间 (秒)')
|
||||
self.peak_plot_widget.showGrid(x=True, y=True, alpha=0.3)
|
||||
self.peak_plot_widget.addLegend()
|
||||
self.peak_curve = self.peak_plot_widget.plot(pen='r', name='PEAK值', width=2)
|
||||
plots_layout.addWidget(self.peak_plot_widget)
|
||||
|
||||
layout.addWidget(plots_widget)
|
||||
|
||||
# 实时数据展示
|
||||
data_group = QGroupBox("当前数据")
|
||||
data_layout = QHBoxLayout()
|
||||
data_group.setLayout(data_layout)
|
||||
|
||||
self.sum_label = QLabel("SUM: --")
|
||||
self.sum_label.setStyleSheet("font-weight: bold; color: orange; font-size: 12pt;")
|
||||
data_layout.addWidget(self.sum_label)
|
||||
|
||||
self.peak_label = QLabel("PEAK: --")
|
||||
self.peak_label.setStyleSheet("font-weight: bold; color: red; font-size: 12pt;")
|
||||
data_layout.addWidget(self.peak_label)
|
||||
|
||||
self.timestamp_label = QLabel("时间: --")
|
||||
self.timestamp_label.setStyleSheet("font-size: 10pt;")
|
||||
data_layout.addWidget(self.timestamp_label)
|
||||
|
||||
layout.addWidget(data_group)
|
||||
|
||||
# 连接输入变化信号
|
||||
self.ip_edit.textChanged.connect(self.update_address_label)
|
||||
self.port_edit.textChanged.connect(self.update_address_label)
|
||||
|
||||
def update_address_label(self):
|
||||
"""更新地址标签"""
|
||||
ip = self.ip_edit.text().strip()
|
||||
port = self.port_edit.text().strip()
|
||||
if ip and port:
|
||||
self.address_label.setText(f"{ip}:{port}")
|
||||
else:
|
||||
self.address_label.setText("--")
|
||||
|
||||
def setup_plots(self):
|
||||
"""初始化绘图"""
|
||||
self.sum_curve.setData([], [])
|
||||
self.peak_curve.setData([], [])
|
||||
|
||||
def test_connection(self):
|
||||
"""测试连接"""
|
||||
ip = self.ip_edit.text().strip()
|
||||
port = self.port_edit.text().strip()
|
||||
device_id = self.device_id_edit.text().strip()
|
||||
|
||||
if not ip or not port:
|
||||
QMessageBox.warning(self, "警告", "请输入IP地址和端口")
|
||||
return
|
||||
|
||||
try:
|
||||
port = int(port)
|
||||
except ValueError:
|
||||
QMessageBox.warning(self, "警告", "端口必须是数字")
|
||||
return
|
||||
|
||||
# 创建临时客户端测试连接
|
||||
test_client = DexHandSensorClient(ip, port, device_id, timeout=2, retries=0)
|
||||
if test_client.is_connected():
|
||||
QMessageBox.information(self, "成功", "连接测试成功!")
|
||||
test_client.close()
|
||||
else:
|
||||
QMessageBox.critical(self, "失败", "连接测试失败,请检查服务器地址和网络")
|
||||
|
||||
def set_client(self, client: DexHandSensorClient):
|
||||
"""设置客户端"""
|
||||
self.client = client
|
||||
self.status_label.setText(f"已连接 (设备: {client.device_id})")
|
||||
self.status_label.setStyleSheet("color: green; font-weight: bold; padding: 5px;")
|
||||
self.connect_btn.setText("断开连接")
|
||||
self.clear_btn.setEnabled(True)
|
||||
self.ip_edit.setEnabled(False)
|
||||
self.port_edit.setEnabled(False)
|
||||
self.device_id_edit.setEnabled(False)
|
||||
self.test_btn.setEnabled(False)
|
||||
|
||||
def clear_client(self):
|
||||
"""清除客户端"""
|
||||
self.client = None
|
||||
self.status_label.setText("未连接")
|
||||
self.status_label.setStyleSheet("color: red; font-weight: bold; padding: 5px;")
|
||||
self.connect_btn.setText("连接")
|
||||
self.clear_btn.setEnabled(False)
|
||||
self.ip_edit.setEnabled(True)
|
||||
self.port_edit.setEnabled(True)
|
||||
self.device_id_edit.setEnabled(True)
|
||||
self.test_btn.setEnabled(True)
|
||||
|
||||
def toggle_connection(self):
|
||||
"""切换连接状态"""
|
||||
if self.client is None or not self.client.is_connected():
|
||||
# 尝试连接
|
||||
ip = self.ip_edit.text().strip()
|
||||
port = self.port_edit.text().strip()
|
||||
device_id = self.device_id_edit.text().strip()
|
||||
|
||||
if not ip or not port:
|
||||
QMessageBox.warning(self, "警告", "请输入IP地址和端口")
|
||||
return
|
||||
|
||||
try:
|
||||
port = int(port)
|
||||
except ValueError:
|
||||
QMessageBox.warning(self, "警告", "端口必须是数字")
|
||||
return
|
||||
|
||||
# 显示连接中状态
|
||||
self.status_label.setText("连接中...")
|
||||
self.status_label.setStyleSheet("color: orange; font-weight: bold; padding: 5px;")
|
||||
QApplication.processEvents()
|
||||
|
||||
# 创建客户端
|
||||
try:
|
||||
client = DexHandSensorClient(ip, port, device_id)
|
||||
if client.is_connected():
|
||||
self.set_client(client)
|
||||
self.start_updating()
|
||||
else:
|
||||
self.clear_client()
|
||||
QMessageBox.critical(self, "错误", f"无法连接到服务器 {ip}:{port}")
|
||||
except Exception as e:
|
||||
self.clear_client()
|
||||
QMessageBox.critical(self, "错误", f"连接失败: {str(e)}")
|
||||
else:
|
||||
# 断开连接
|
||||
self.stop_updating()
|
||||
self.client.close()
|
||||
self.clear_client()
|
||||
self.clear_data()
|
||||
|
||||
def start_updating(self):
|
||||
"""开始更新数据"""
|
||||
self.update_timer.timeout.connect(self.update_data)
|
||||
self.update_timer.start(self.freq_spin.value())
|
||||
|
||||
def stop_updating(self):
|
||||
"""停止更新数据"""
|
||||
self.update_timer.stop()
|
||||
try:
|
||||
self.update_timer.timeout.disconnect()
|
||||
except:
|
||||
pass
|
||||
|
||||
def set_update_interval(self, ms: int):
|
||||
"""设置更新间隔"""
|
||||
if self.update_timer.isActive():
|
||||
self.update_timer.setInterval(ms)
|
||||
|
||||
def set_max_points(self, points: int):
|
||||
"""设置最大显示点数"""
|
||||
self.max_points = points
|
||||
# 如果当前数据超过新限制,截断数据
|
||||
if len(self.time_buffer) > self.max_points:
|
||||
self.time_buffer = self.time_buffer[-self.max_points:]
|
||||
self.data_buffer['sum'] = self.data_buffer['sum'][-self.max_points:]
|
||||
self.data_buffer['peak'] = self.data_buffer['peak'][-self.max_points:]
|
||||
|
||||
def clear_data(self):
|
||||
"""清空数据"""
|
||||
self.data_buffer['sum'].clear()
|
||||
self.data_buffer['peak'].clear()
|
||||
self.time_buffer.clear()
|
||||
self.sum_curve.setData([], [])
|
||||
self.peak_curve.setData([], [])
|
||||
self.sum_label.setText("SUM: --")
|
||||
self.peak_label.setText("PEAK: --")
|
||||
|
||||
def extract_finger_data(self, feedback):
|
||||
"""提取食指指端的传感器数据"""
|
||||
if not feedback or not feedback.sensor:
|
||||
return None, None
|
||||
|
||||
sum_values = []
|
||||
peak_values = []
|
||||
|
||||
for sensor in feedback.sensor:
|
||||
# 检查是否为食指指端
|
||||
if (sensor.finger_type == self.current_finger and
|
||||
sensor.part_type == self.current_part):
|
||||
|
||||
# 处理传感器数据
|
||||
if sensor.data:
|
||||
for row in sensor.data:
|
||||
if row.values:
|
||||
row_array = np.array(row.values)
|
||||
sum_values.append(np.sum(row_array))
|
||||
peak_values.append(np.max(row_array))
|
||||
|
||||
if sum_values and peak_values:
|
||||
return np.mean(sum_values), np.mean(peak_values)
|
||||
return None, None
|
||||
|
||||
def update_data(self):
|
||||
"""更新数据"""
|
||||
if not self.client or not self.client.is_connected():
|
||||
return
|
||||
|
||||
feedback = self.client.get_sensor_data()
|
||||
if not feedback:
|
||||
# 如果获取数据失败,可能连接已断开
|
||||
if self.client and not self.client.is_connected():
|
||||
self.toggle_connection() # 自动断开
|
||||
return
|
||||
|
||||
# 提取食指指端数据
|
||||
sum_val, peak_val = self.extract_finger_data(feedback)
|
||||
|
||||
if sum_val is not None and peak_val is not None:
|
||||
# 更新时间戳
|
||||
current_time = time.time()
|
||||
self.time_buffer.append(current_time)
|
||||
self.data_buffer['sum'].append(sum_val)
|
||||
self.data_buffer['peak'].append(peak_val)
|
||||
|
||||
# 限制缓冲区大小
|
||||
if len(self.time_buffer) > self.max_points:
|
||||
self.time_buffer.pop(0)
|
||||
self.data_buffer['sum'].pop(0)
|
||||
self.data_buffer['peak'].pop(0)
|
||||
|
||||
# 更新曲线
|
||||
if len(self.time_buffer) > 1:
|
||||
# 归一化时间显示(相对于第一个点)
|
||||
t0 = self.time_buffer[0]
|
||||
plot_time = [t - t0 for t in self.time_buffer]
|
||||
|
||||
self.sum_curve.setData(plot_time, self.data_buffer['sum'])
|
||||
self.peak_curve.setData(plot_time, self.data_buffer['peak'])
|
||||
|
||||
# 自动调整Y轴范围
|
||||
self.sum_plot_widget.autoRange()
|
||||
self.peak_plot_widget.autoRange()
|
||||
|
||||
# 更新标签
|
||||
self.sum_label.setText(f"SUM: {sum_val:.2f}")
|
||||
self.peak_label.setText(f"PEAK: {peak_val:.2f}")
|
||||
self.timestamp_label.setText(f"时间: {time.strftime('%H:%M:%S')}")
|
||||
|
||||
|
||||
class SensorDataWindow(QMainWindow):
|
||||
"""传感器数据监控主窗口"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setWindowTitle("灵巧手传感器数据监控 - 可配置版")
|
||||
self.setGeometry(100, 100, 900, 1000)
|
||||
|
||||
# 设置中央部件
|
||||
central_widget = SensorDataPlotWidget()
|
||||
self.setCentralWidget(central_widget)
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""关闭窗口时清理资源"""
|
||||
central_widget = self.centralWidget()
|
||||
if central_widget:
|
||||
central_widget.stop_updating()
|
||||
if central_widget.client:
|
||||
central_widget.client.close()
|
||||
event.accept()
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
import sys
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
app.setStyle('Fusion') # 使用Fusion风格,看起来更现代
|
||||
|
||||
window = SensorDataWindow()
|
||||
window.show()
|
||||
|
||||
sys.exit(app.exec_())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
727
clients/gui_camera_client..py
Normal file
727
clients/gui_camera_client..py
Normal file
@ -0,0 +1,727 @@
|
||||
# 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()
|
||||
@ -8,6 +8,8 @@ PyQt5==5.15.11
|
||||
requests==2.32.5
|
||||
yourdfpy
|
||||
uvicorn==0.38.0
|
||||
av>=10.0.0
|
||||
opencv-python>=4.0.0
|
||||
|
||||
numpy==1.26.4
|
||||
opencv-contrib-python==4.10.0.84
|
||||
|
||||
Loading…
Reference in New Issue
Block a user