844 lines
32 KiB
Python
844 lines
32 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
设备管理模块
|
||
负责摄像头、IMU传感器和压力传感器的连接和数据采集
|
||
以及视频推流功能
|
||
"""
|
||
|
||
import cv2
|
||
import numpy as np
|
||
import time
|
||
import threading
|
||
import json
|
||
import queue
|
||
import base64
|
||
import gc
|
||
import os
|
||
import psutil
|
||
import configparser
|
||
from datetime import datetime
|
||
from typing import Dict, List, Optional, Any
|
||
from concurrent.futures import ThreadPoolExecutor
|
||
import logging
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
class DeviceManager:
|
||
"""设备管理器"""
|
||
|
||
def __init__(self):
|
||
self.camera = None
|
||
self.imu_device = None
|
||
self.pressure_device = None
|
||
self.device_status = {
|
||
'camera': False,
|
||
'imu': False,
|
||
'pressure': False
|
||
}
|
||
self.calibration_data = {}
|
||
self.data_lock = threading.Lock()
|
||
self.latest_data = {}
|
||
|
||
# 初始化设备
|
||
self._init_devices()
|
||
|
||
def _init_devices(self):
|
||
"""初始化所有设备"""
|
||
try:
|
||
self._init_camera()
|
||
self._init_imu()
|
||
self._init_pressure_sensor()
|
||
logger.info('设备初始化完成')
|
||
except Exception as e:
|
||
logger.error(f'设备初始化失败: {e}')
|
||
|
||
def _init_camera(self):
|
||
"""初始化摄像头"""
|
||
try:
|
||
# 尝试连接默认摄像头
|
||
self.camera = cv2.VideoCapture(0)
|
||
if self.camera.isOpened():
|
||
# 设置摄像头参数
|
||
self.camera.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
|
||
self.camera.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)
|
||
self.camera.set(cv2.CAP_PROP_FPS, 30)
|
||
|
||
self.device_status['camera'] = True
|
||
logger.info('摄像头初始化成功')
|
||
else:
|
||
logger.warning('摄像头连接失败')
|
||
self.camera = None
|
||
except Exception as e:
|
||
logger.error(f'摄像头初始化异常: {e}')
|
||
self.camera = None
|
||
|
||
def _init_imu(self):
|
||
"""初始化IMU传感器"""
|
||
try:
|
||
# 这里应该连接实际的IMU设备
|
||
# 目前使用模拟数据
|
||
self.imu_device = MockIMUDevice()
|
||
self.device_status['imu'] = True
|
||
logger.info('IMU传感器初始化成功(模拟)')
|
||
except Exception as e:
|
||
logger.error(f'IMU传感器初始化失败: {e}')
|
||
self.imu_device = None
|
||
|
||
def _init_pressure_sensor(self):
|
||
"""初始化压力传感器"""
|
||
try:
|
||
# 这里应该连接实际的压力传感器
|
||
# 目前使用模拟数据
|
||
self.pressure_device = MockPressureDevice()
|
||
self.device_status['pressure'] = True
|
||
logger.info('压力传感器初始化成功(模拟)')
|
||
except Exception as e:
|
||
logger.error(f'压力传感器初始化失败: {e}')
|
||
self.pressure_device = None
|
||
|
||
def get_device_status(self) -> Dict[str, bool]:
|
||
"""获取设备状态"""
|
||
return self.device_status.copy()
|
||
|
||
def get_connected_devices(self) -> List[str]:
|
||
"""获取已连接的设备列表"""
|
||
return [device for device, status in self.device_status.items() if status]
|
||
|
||
def refresh_devices(self):
|
||
"""刷新设备连接"""
|
||
logger.info('刷新设备连接...')
|
||
|
||
# 重新初始化所有设备
|
||
if self.camera:
|
||
self.camera.release()
|
||
|
||
self._init_devices()
|
||
|
||
def calibrate_devices(self) -> Dict[str, Any]:
|
||
"""校准设备"""
|
||
calibration_result = {}
|
||
|
||
try:
|
||
# 摄像头校准
|
||
if self.device_status['camera']:
|
||
camera_calibration = self._calibrate_camera()
|
||
calibration_result['camera'] = camera_calibration
|
||
|
||
# IMU校准
|
||
if self.device_status['imu']:
|
||
imu_calibration = self._calibrate_imu()
|
||
calibration_result['imu'] = imu_calibration
|
||
|
||
# 压力传感器校准
|
||
if self.device_status['pressure']:
|
||
pressure_calibration = self._calibrate_pressure()
|
||
calibration_result['pressure'] = pressure_calibration
|
||
|
||
self.calibration_data = calibration_result
|
||
logger.info('设备校准完成')
|
||
|
||
except Exception as e:
|
||
logger.error(f'设备校准失败: {e}')
|
||
raise
|
||
|
||
return calibration_result
|
||
|
||
def _calibrate_camera(self) -> Dict[str, Any]:
|
||
"""校准摄像头"""
|
||
if not self.camera or not self.camera.isOpened():
|
||
return {'status': 'failed', 'error': '摄像头未连接'}
|
||
|
||
try:
|
||
# 获取几帧图像进行校准
|
||
frames = []
|
||
for _ in range(10):
|
||
ret, frame = self.camera.read()
|
||
if ret:
|
||
frames.append(frame)
|
||
time.sleep(0.1)
|
||
|
||
if not frames:
|
||
return {'status': 'failed', 'error': '无法获取图像'}
|
||
|
||
# 计算平均亮度和对比度
|
||
avg_brightness = np.mean([np.mean(cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)) for frame in frames])
|
||
|
||
calibration = {
|
||
'status': 'success',
|
||
'brightness': float(avg_brightness),
|
||
'resolution': (int(self.camera.get(cv2.CAP_PROP_FRAME_WIDTH)),
|
||
int(self.camera.get(cv2.CAP_PROP_FRAME_HEIGHT))),
|
||
'fps': float(self.camera.get(cv2.CAP_PROP_FPS)),
|
||
'timestamp': datetime.now().isoformat()
|
||
}
|
||
|
||
return calibration
|
||
|
||
except Exception as e:
|
||
return {'status': 'failed', 'error': str(e)}
|
||
|
||
def _calibrate_imu(self) -> Dict[str, Any]:
|
||
"""校准IMU传感器"""
|
||
if not self.imu_device:
|
||
return {'status': 'failed', 'error': 'IMU设备未连接'}
|
||
|
||
try:
|
||
# 收集静态数据进行零点校准
|
||
samples = []
|
||
for _ in range(100):
|
||
data = self.imu_device.read_data()
|
||
samples.append(data)
|
||
time.sleep(0.01)
|
||
|
||
# 计算零点偏移
|
||
accel_offset = {
|
||
'x': np.mean([s['accel']['x'] for s in samples]),
|
||
'y': np.mean([s['accel']['y'] for s in samples]),
|
||
'z': np.mean([s['accel']['z'] for s in samples]) - 9.8 # 重力补偿
|
||
}
|
||
|
||
gyro_offset = {
|
||
'x': np.mean([s['gyro']['x'] for s in samples]),
|
||
'y': np.mean([s['gyro']['y'] for s in samples]),
|
||
'z': np.mean([s['gyro']['z'] for s in samples])
|
||
}
|
||
|
||
calibration = {
|
||
'status': 'success',
|
||
'accel_offset': accel_offset,
|
||
'gyro_offset': gyro_offset,
|
||
'timestamp': datetime.now().isoformat()
|
||
}
|
||
|
||
return calibration
|
||
|
||
except Exception as e:
|
||
return {'status': 'failed', 'error': str(e)}
|
||
|
||
def _calibrate_pressure(self) -> Dict[str, Any]:
|
||
"""校准压力传感器"""
|
||
if not self.pressure_device:
|
||
return {'status': 'failed', 'error': '压力传感器未连接'}
|
||
|
||
try:
|
||
# 收集零压力数据
|
||
samples = []
|
||
for _ in range(50):
|
||
data = self.pressure_device.read_data()
|
||
samples.append(data)
|
||
time.sleep(0.02)
|
||
|
||
# 计算零点偏移
|
||
zero_offset = {
|
||
'left_foot': np.mean([s['left_foot'] for s in samples]),
|
||
'right_foot': np.mean([s['right_foot'] for s in samples])
|
||
}
|
||
|
||
calibration = {
|
||
'status': 'success',
|
||
'zero_offset': zero_offset,
|
||
'timestamp': datetime.now().isoformat()
|
||
}
|
||
|
||
return calibration
|
||
|
||
except Exception as e:
|
||
return {'status': 'failed', 'error': str(e)}
|
||
|
||
def collect_data(self) -> Dict[str, Any]:
|
||
"""采集所有设备数据"""
|
||
data = {}
|
||
timestamp = datetime.now().isoformat()
|
||
|
||
try:
|
||
# 采集摄像头数据
|
||
if self.device_status['camera']:
|
||
camera_data = self._collect_camera_data()
|
||
if camera_data:
|
||
data['camera'] = camera_data
|
||
|
||
# 采集IMU数据
|
||
if self.device_status['imu']:
|
||
imu_data = self._collect_imu_data()
|
||
if imu_data:
|
||
data['imu'] = imu_data
|
||
|
||
# 采集压力传感器数据
|
||
if self.device_status['pressure']:
|
||
pressure_data = self._collect_pressure_data()
|
||
if pressure_data:
|
||
data['pressure'] = pressure_data
|
||
|
||
# 添加时间戳
|
||
data['timestamp'] = timestamp
|
||
|
||
# 更新最新数据
|
||
with self.data_lock:
|
||
self.latest_data = data.copy()
|
||
|
||
except Exception as e:
|
||
logger.error(f'数据采集失败: {e}')
|
||
|
||
return data
|
||
|
||
def _collect_camera_data(self) -> Optional[Dict[str, Any]]:
|
||
"""采集摄像头数据"""
|
||
if not self.camera or not self.camera.isOpened():
|
||
return None
|
||
|
||
try:
|
||
ret, frame = self.camera.read()
|
||
if not ret:
|
||
return None
|
||
|
||
# 进行姿态检测(这里应该集成实际的姿态检测算法)
|
||
pose_data = self._detect_pose(frame)
|
||
|
||
return {
|
||
'frame_available': True,
|
||
'frame_size': frame.shape,
|
||
'pose_data': pose_data,
|
||
'timestamp': datetime.now().isoformat()
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f'摄像头数据采集失败: {e}')
|
||
return None
|
||
|
||
def _detect_pose(self, frame: np.ndarray) -> Dict[str, Any]:
|
||
"""姿态检测(模拟实现)"""
|
||
# 这里应该集成实际的姿态检测算法,如MediaPipe或OpenPose
|
||
# 目前返回模拟数据
|
||
return {
|
||
'center_of_gravity': {
|
||
'x': np.random.normal(0, 5),
|
||
'y': np.random.normal(0, 5)
|
||
},
|
||
'body_angle': {
|
||
'pitch': np.random.normal(0, 2),
|
||
'roll': np.random.normal(0, 2),
|
||
'yaw': np.random.normal(0, 1)
|
||
},
|
||
'confidence': np.random.uniform(0.8, 1.0)
|
||
}
|
||
|
||
def _collect_imu_data(self) -> Optional[Dict[str, Any]]:
|
||
"""采集IMU数据"""
|
||
if not self.imu_device:
|
||
return None
|
||
|
||
try:
|
||
raw_data = self.imu_device.read_data()
|
||
|
||
# 应用校准偏移
|
||
if 'imu' in self.calibration_data:
|
||
calibration = self.calibration_data['imu']
|
||
if calibration.get('status') == 'success':
|
||
accel_offset = calibration.get('accel_offset', {})
|
||
gyro_offset = calibration.get('gyro_offset', {})
|
||
|
||
# 校正加速度数据
|
||
raw_data['accel']['x'] -= accel_offset.get('x', 0)
|
||
raw_data['accel']['y'] -= accel_offset.get('y', 0)
|
||
raw_data['accel']['z'] -= accel_offset.get('z', 0)
|
||
|
||
# 校正陀螺仪数据
|
||
raw_data['gyro']['x'] -= gyro_offset.get('x', 0)
|
||
raw_data['gyro']['y'] -= gyro_offset.get('y', 0)
|
||
raw_data['gyro']['z'] -= gyro_offset.get('z', 0)
|
||
|
||
return raw_data
|
||
|
||
except Exception as e:
|
||
logger.error(f'IMU数据采集失败: {e}')
|
||
return None
|
||
|
||
def _collect_pressure_data(self) -> Optional[Dict[str, Any]]:
|
||
"""采集压力传感器数据"""
|
||
if not self.pressure_device:
|
||
return None
|
||
|
||
try:
|
||
raw_data = self.pressure_device.read_data()
|
||
|
||
# 应用校准偏移
|
||
if 'pressure' in self.calibration_data:
|
||
calibration = self.calibration_data['pressure']
|
||
if calibration.get('status') == 'success':
|
||
zero_offset = calibration.get('zero_offset', {})
|
||
|
||
raw_data['left_foot'] -= zero_offset.get('left_foot', 0)
|
||
raw_data['right_foot'] -= zero_offset.get('right_foot', 0)
|
||
|
||
# 计算重心位置
|
||
total_pressure = raw_data['left_foot'] + raw_data['right_foot']
|
||
if total_pressure > 0:
|
||
center_of_pressure = {
|
||
'x': (raw_data['right_foot'] - raw_data['left_foot']) / total_pressure * 100,
|
||
'y': 0 # 简化模型
|
||
}
|
||
else:
|
||
center_of_pressure = {'x': 0, 'y': 0}
|
||
|
||
raw_data['center_of_pressure'] = center_of_pressure
|
||
raw_data['total_pressure'] = total_pressure
|
||
|
||
return raw_data
|
||
|
||
except Exception as e:
|
||
logger.error(f'压力传感器数据采集失败: {e}')
|
||
return None
|
||
|
||
def get_latest_data(self) -> Dict[str, Any]:
|
||
"""获取最新采集的数据"""
|
||
with self.data_lock:
|
||
return self.latest_data.copy()
|
||
|
||
def start_video_recording(self, output_path: str) -> bool:
|
||
"""开始视频录制"""
|
||
if not self.camera or not self.camera.isOpened():
|
||
return False
|
||
|
||
try:
|
||
# 获取摄像头参数
|
||
width = int(self.camera.get(cv2.CAP_PROP_FRAME_WIDTH))
|
||
height = int(self.camera.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
||
fps = int(self.camera.get(cv2.CAP_PROP_FPS))
|
||
|
||
# 创建视频写入器
|
||
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
|
||
self.video_writer = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
|
||
|
||
if self.video_writer.isOpened():
|
||
logger.info(f'开始视频录制: {output_path}')
|
||
return True
|
||
else:
|
||
logger.error('视频写入器创建失败')
|
||
return False
|
||
|
||
except Exception as e:
|
||
logger.error(f'开始视频录制失败: {e}')
|
||
return False
|
||
|
||
def stop_video_recording(self):
|
||
"""停止视频录制"""
|
||
if hasattr(self, 'video_writer') and self.video_writer:
|
||
self.video_writer.release()
|
||
self.video_writer = None
|
||
logger.info('视频录制已停止')
|
||
|
||
def cleanup(self):
|
||
"""清理资源"""
|
||
try:
|
||
if self.camera:
|
||
self.camera.release()
|
||
|
||
if hasattr(self, 'video_writer') and self.video_writer:
|
||
self.video_writer.release()
|
||
|
||
logger.info('设备资源已清理')
|
||
|
||
except Exception as e:
|
||
logger.error(f'清理设备资源失败: {e}')
|
||
|
||
|
||
class MockIMUDevice:
|
||
"""模拟IMU设备"""
|
||
|
||
def __init__(self):
|
||
self.noise_level = 0.1
|
||
|
||
def read_data(self) -> Dict[str, Any]:
|
||
"""读取IMU数据"""
|
||
return {
|
||
'accel': {
|
||
'x': np.random.normal(0, self.noise_level),
|
||
'y': np.random.normal(0, self.noise_level),
|
||
'z': np.random.normal(9.8, self.noise_level) # 重力加速度
|
||
},
|
||
'gyro': {
|
||
'x': np.random.normal(0, self.noise_level),
|
||
'y': np.random.normal(0, self.noise_level),
|
||
'z': np.random.normal(0, self.noise_level)
|
||
},
|
||
'temperature': np.random.normal(25, 2),
|
||
'timestamp': datetime.now().isoformat()
|
||
}
|
||
|
||
|
||
class MockPressureDevice:
|
||
"""模拟压力传感器设备"""
|
||
|
||
def __init__(self):
|
||
self.base_pressure = 500 # 基础压力值
|
||
self.noise_level = 10
|
||
|
||
def read_data(self) -> Dict[str, Any]:
|
||
"""读取压力数据"""
|
||
# 模拟轻微的左右脚压力差异
|
||
left_pressure = self.base_pressure + np.random.normal(0, self.noise_level)
|
||
right_pressure = self.base_pressure + np.random.normal(0, self.noise_level)
|
||
|
||
return {
|
||
'left_foot': max(0, left_pressure),
|
||
'right_foot': max(0, right_pressure),
|
||
'timestamp': datetime.now().isoformat()
|
||
}
|
||
|
||
|
||
class VideoStreamManager:
|
||
"""视频推流管理器"""
|
||
|
||
def __init__(self, socketio=None):
|
||
self.socketio = socketio
|
||
self.rtsp_url = None
|
||
self.rtsp_thread = None
|
||
self.rtsp_running = False
|
||
|
||
# 用于异步编码的线程池和队列
|
||
self.encoding_executor = ThreadPoolExecutor(max_workers=2)
|
||
self.frame_queue = queue.Queue(maxsize=1) # 只保留最新的一帧
|
||
|
||
# 内存优化配置
|
||
self.frame_skip_counter = 0
|
||
self.FRAME_SKIP_RATIO = 1 # 每3帧发送1帧,减少网络和内存压力
|
||
self.MAX_FRAME_SIZE = (640, 480) # 进一步减小帧尺寸以节省内存
|
||
self.MAX_MEMORY_USAGE = 200 * 1024 * 1024 # 200MB内存限制
|
||
self.memory_check_counter = 0
|
||
self.MEMORY_CHECK_INTERVAL = 50 # 每50帧检查一次内存
|
||
|
||
# 读取RTSP配置
|
||
self._load_rtsp_config()
|
||
|
||
def _load_rtsp_config(self):
|
||
"""加载RTSP配置"""
|
||
try:
|
||
config = configparser.ConfigParser()
|
||
config_path = os.path.join(os.path.dirname(__file__), 'config.ini')
|
||
config.read(config_path, encoding='utf-8')
|
||
self.rtsp_url = config.get('CAMERA', 'rtsp_url', fallback=None)
|
||
logger.info(f'RTSP配置加载完成: {self.rtsp_url}')
|
||
except Exception as e:
|
||
logger.error(f'加载RTSP配置失败: {e}')
|
||
self.rtsp_url = None
|
||
|
||
def get_memory_usage(self):
|
||
"""获取当前进程内存使用量(字节)"""
|
||
try:
|
||
process = psutil.Process(os.getpid())
|
||
return process.memory_info().rss
|
||
except:
|
||
return 0
|
||
|
||
def async_encode_frame(self, frame, frame_count):
|
||
"""异步编码帧 - 内存优化版本"""
|
||
try:
|
||
# 内存检查
|
||
self.memory_check_counter += 1
|
||
if self.memory_check_counter >= self.MEMORY_CHECK_INTERVAL:
|
||
self.memory_check_counter = 0
|
||
current_memory = self.get_memory_usage()
|
||
if current_memory > self.MAX_MEMORY_USAGE:
|
||
logger.warning(f"内存使用过高: {current_memory / 1024 / 1024:.2f}MB,强制清理")
|
||
gc.collect()
|
||
# 如果内存仍然过高,跳过此帧
|
||
if self.get_memory_usage() > self.MAX_MEMORY_USAGE:
|
||
del frame
|
||
return
|
||
|
||
# 更激进的图像尺寸压缩以节省内存
|
||
height, width = frame.shape[:2]
|
||
target_width, target_height = self.MAX_FRAME_SIZE
|
||
|
||
if width > target_width or height > target_height:
|
||
# 计算缩放比例,保持宽高比
|
||
scale_w = target_width / width
|
||
scale_h = target_height / height
|
||
scale = min(scale_w, scale_h)
|
||
|
||
new_width = int(width * scale)
|
||
new_height = int(height * scale)
|
||
|
||
# 使用更快的插值方法减少CPU使用
|
||
frame = cv2.resize(frame, (new_width, new_height), interpolation=cv2.INTER_AREA)
|
||
|
||
# 优化JPEG编码参数:优先考虑速度和内存
|
||
encode_param = [
|
||
int(cv2.IMWRITE_JPEG_QUALITY), 50, # 进一步降低质量以减少内存使用
|
||
int(cv2.IMWRITE_JPEG_OPTIMIZE), 1, # 启用优化
|
||
int(cv2.IMWRITE_JPEG_PROGRESSIVE), 0 # 禁用渐进式以减少内存
|
||
]
|
||
|
||
success, buffer = cv2.imencode('.jpg', frame, encode_param)
|
||
if not success:
|
||
logger.error('图像编码失败')
|
||
return
|
||
|
||
# 立即释放frame内存
|
||
del frame
|
||
|
||
jpg_as_text = base64.b64encode(buffer).decode('utf-8')
|
||
|
||
# 立即释放buffer内存
|
||
del buffer
|
||
|
||
# 发送数据
|
||
if self.socketio:
|
||
self.socketio.emit('rtsp_frame', {
|
||
'image': jpg_as_text,
|
||
'frame_id': frame_count,
|
||
'timestamp': time.time()
|
||
})
|
||
|
||
# 立即释放base64字符串
|
||
del jpg_as_text
|
||
|
||
except Exception as e:
|
||
logger.error(f'异步编码帧失败: {e}')
|
||
finally:
|
||
# 定期强制垃圾回收
|
||
if self.memory_check_counter % 10 == 0:
|
||
gc.collect()
|
||
|
||
def frame_encoding_worker(self):
|
||
"""帧编码工作线程"""
|
||
while self.rtsp_running:
|
||
try:
|
||
# 从队列获取帧
|
||
frame, frame_count = self.frame_queue.get(timeout=1)
|
||
# 提交到线程池进行异步编码
|
||
self.encoding_executor.submit(self.async_encode_frame, frame, frame_count)
|
||
except queue.Empty:
|
||
continue
|
||
except Exception as e:
|
||
logger.error(f'帧编码工作线程异常: {e}')
|
||
|
||
def generate_test_frame(self, frame_count):
|
||
"""生成测试帧"""
|
||
width, height = 640, 480
|
||
|
||
# 创建黑色背景
|
||
frame = np.zeros((height, width, 3), dtype=np.uint8)
|
||
|
||
# 添加动态元素
|
||
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
|
||
|
||
# 添加时间戳
|
||
cv2.putText(frame, timestamp, (10, 90), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 255), 2)
|
||
|
||
# 添加帧计数
|
||
cv2.putText(frame, f'TEST Frame: {frame_count}', (10, 120), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 255), 2)
|
||
|
||
# 添加移动的圆形
|
||
center_x = int(320 + 200 * np.sin(frame_count * 0.1))
|
||
center_y = int(240 + 100 * np.cos(frame_count * 0.1))
|
||
cv2.circle(frame, (center_x, center_y), 30, (255, 0, 0), -1)
|
||
|
||
# 添加变化的矩形
|
||
rect_size = int(50 + 30 * np.sin(frame_count * 0.05))
|
||
cv2.rectangle(frame, (500, 200), (500 + rect_size, 200 + rect_size), (0, 0, 255), -1)
|
||
|
||
return frame
|
||
|
||
def generate_rtsp_frames(self):
|
||
"""生成RTSP帧"""
|
||
frame_count = 0
|
||
error_count = 0
|
||
use_test_mode = False
|
||
last_frame_time = time.time()
|
||
|
||
logger.info(f'开始生成RTSP帧,URL: {self.rtsp_url}')
|
||
|
||
try:
|
||
cap = cv2.VideoCapture(self.rtsp_url)
|
||
if not cap.isOpened():
|
||
logger.warning(f'无法打开RTSP流: {self.rtsp_url},切换到测试模式')
|
||
use_test_mode = True
|
||
if self.socketio:
|
||
self.socketio.emit('rtsp_status', {'status': 'started', 'message': '使用测试视频源'})
|
||
else:
|
||
# 最激进的实时优化设置
|
||
cap.set(cv2.CAP_PROP_BUFFERSIZE, 0) # 完全禁用缓冲区
|
||
cap.set(cv2.CAP_PROP_FPS, 60) # 提高帧率到60fps
|
||
cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('M', 'J', 'P', 'G')) # MJPEG编码
|
||
# 设置更低的分辨率以减少处理时间
|
||
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
|
||
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
|
||
logger.info('RTSP流已打开,开始推送帧(激进实时模式)')
|
||
if self.socketio:
|
||
self.socketio.emit('rtsp_status', {'status': 'started', 'message': '使用RTSP视频源(激进实时模式)'})
|
||
|
||
self.rtsp_running = True
|
||
|
||
# 启动帧编码工作线程
|
||
encoding_thread = threading.Thread(target=self.frame_encoding_worker)
|
||
encoding_thread.daemon = True
|
||
encoding_thread.start()
|
||
|
||
while self.rtsp_running:
|
||
if use_test_mode:
|
||
# 使用测试模式生成帧
|
||
frame = self.generate_test_frame(frame_count)
|
||
ret = True
|
||
else:
|
||
# 使用RTSP流,添加帧跳过机制减少延迟
|
||
ret, frame = cap.read()
|
||
if not ret:
|
||
error_count += 1
|
||
logger.warning(f'RTSP读取帧失败(第{error_count}次),尝试重连...')
|
||
if 'cap' in locals():
|
||
cap.release()
|
||
|
||
if error_count > 5:
|
||
logger.warning('RTSP连接失败次数过多,切换到测试模式')
|
||
use_test_mode = True
|
||
if self.socketio:
|
||
self.socketio.emit('rtsp_status', {'status': 'switched', 'message': '已切换到测试视频源'})
|
||
continue
|
||
|
||
# 立即重连,不等待
|
||
cap = cv2.VideoCapture(self.rtsp_url)
|
||
if cap.isOpened():
|
||
# 重连时应用相同的激进实时设置
|
||
cap.set(cv2.CAP_PROP_BUFFERSIZE, 0)
|
||
cap.set(cv2.CAP_PROP_FPS, 60)
|
||
cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('M', 'J', 'P', 'G'))
|
||
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
|
||
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
|
||
continue
|
||
|
||
error_count = 0 # 重置错误计数
|
||
|
||
# 内存优化的帧跳过策略
|
||
# 减少跳帧数量,避免过度内存使用
|
||
skip_count = 0
|
||
while skip_count < 3: # 减少到最多跳过3帧
|
||
temp_ret, temp_frame = cap.read()
|
||
if temp_ret:
|
||
# 立即释放之前的帧
|
||
if 'frame' in locals():
|
||
del frame
|
||
frame = temp_frame
|
||
skip_count += 1
|
||
else:
|
||
break
|
||
|
||
# 降低帧率以减少内存压力
|
||
current_time = time.time()
|
||
if current_time - last_frame_time < 1/20: # 降低到20fps最大频率
|
||
continue
|
||
last_frame_time = current_time
|
||
|
||
frame_count += 1
|
||
|
||
# 实现帧跳过以减少内存和网络压力
|
||
self.frame_skip_counter += 1
|
||
|
||
if self.frame_skip_counter % (self.FRAME_SKIP_RATIO + 1) != 0:
|
||
# 跳过此帧,立即释放内存
|
||
del frame
|
||
continue
|
||
|
||
try:
|
||
# 将帧放入队列进行异步处理
|
||
try:
|
||
# 非阻塞方式放入队列,如果队列满了就丢弃旧帧
|
||
self.frame_queue.put_nowait((frame.copy(), frame_count))
|
||
except queue.Full:
|
||
# 队列满了,清空队列并放入新帧
|
||
try:
|
||
old_frame, _ = self.frame_queue.get_nowait()
|
||
del old_frame # 立即释放旧帧内存
|
||
except queue.Empty:
|
||
pass
|
||
self.frame_queue.put_nowait((frame.copy(), frame_count))
|
||
|
||
# 立即释放原始帧内存
|
||
del frame
|
||
|
||
if frame_count % 60 == 0: # 每60帧记录一次
|
||
logger.info(f'已推送 {frame_count} 帧到编码队列,跳过率: {self.FRAME_SKIP_RATIO}/{self.FRAME_SKIP_RATIO+1}')
|
||
# 定期强制垃圾回收
|
||
gc.collect()
|
||
|
||
except Exception as e:
|
||
logger.error(f'帧队列处理失败: {e}')
|
||
|
||
except Exception as e:
|
||
logger.error(f'RTSP推流异常: {e}')
|
||
if self.socketio:
|
||
self.socketio.emit('rtsp_status', {'status': 'error', 'message': f'推流异常: {str(e)}'})
|
||
finally:
|
||
if 'cap' in locals():
|
||
cap.release()
|
||
self.rtsp_running = False
|
||
logger.info(f'RTSP推流结束,总共推送了 {frame_count} 帧')
|
||
|
||
def start_rtsp_stream(self):
|
||
"""启动RTSP推流"""
|
||
try:
|
||
if self.rtsp_thread and self.rtsp_thread.is_alive():
|
||
logger.warning('RTSP线程已在运行')
|
||
return {'status': 'already_running', 'message': 'RTSP已在运行'}
|
||
|
||
if not self.rtsp_url:
|
||
logger.error('RTSP URL未配置')
|
||
return {'status': 'error', 'message': 'RTSP URL未配置'}
|
||
|
||
logger.info(f'启动RTSP线程,URL: {self.rtsp_url}')
|
||
self.rtsp_thread = threading.Thread(target=self.generate_rtsp_frames)
|
||
self.rtsp_thread.daemon = True
|
||
self.rtsp_thread.start()
|
||
|
||
logger.info('RTSP线程已启动')
|
||
return {'status': 'started', 'message': 'RTSP推流已启动'}
|
||
|
||
except Exception as e:
|
||
logger.error(f'启动RTSP失败: {e}')
|
||
return {'status': 'error', 'message': f'启动失败: {str(e)}'}
|
||
|
||
def stop_rtsp_stream(self):
|
||
"""停止RTSP推流"""
|
||
try:
|
||
self.rtsp_running = False
|
||
logger.info('RTSP推流已停止')
|
||
return {'status': 'stopped', 'message': 'RTSP推流已停止'}
|
||
|
||
except Exception as e:
|
||
logger.error(f'停止RTSP失败: {e}')
|
||
return {'status': 'error', 'message': f'停止失败: {str(e)}'}
|
||
|
||
def is_streaming(self):
|
||
"""检查是否正在推流"""
|
||
return self.rtsp_running
|
||
|
||
def get_stream_status(self):
|
||
"""获取推流状态"""
|
||
return {
|
||
'running': self.rtsp_running,
|
||
'rtsp_url': self.rtsp_url,
|
||
'thread_alive': self.rtsp_thread.is_alive() if self.rtsp_thread else False
|
||
}
|
||
|
||
def cleanup(self):
|
||
"""清理资源"""
|
||
try:
|
||
self.rtsp_running = False
|
||
if self.rtsp_thread and self.rtsp_thread.is_alive():
|
||
self.rtsp_thread.join(timeout=2)
|
||
|
||
self.encoding_executor.shutdown(wait=False)
|
||
|
||
# 清空队列
|
||
while not self.frame_queue.empty():
|
||
try:
|
||
frame, _ = self.frame_queue.get_nowait()
|
||
del frame
|
||
except queue.Empty:
|
||
break
|
||
|
||
logger.info('视频推流资源已清理')
|
||
|
||
except Exception as e:
|
||
logger.error(f'清理视频推流资源失败: {e}') |