BodyBalanceEvaluation/backend/device_manager.py

846 lines
32 KiB
Python
Raw Normal View History

2025-07-28 11:59:56 +08:00
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
设备管理模块
负责摄像头IMU传感器和压力传感器的连接和数据采集
2025-07-31 17:23:05 +08:00
以及视频推流功能
2025-07-28 11:59:56 +08:00
"""
import cv2
import numpy as np
import time
import threading
import json
2025-07-31 17:23:05 +08:00
import queue
import base64
import gc
import os
import psutil
import configparser
2025-07-28 11:59:56 +08:00
from datetime import datetime
from typing import Dict, List, Optional, Any
2025-07-31 17:23:05 +08:00
from concurrent.futures import ThreadPoolExecutor
2025-07-28 11:59:56 +08:00
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()
2025-07-31 17:23:05 +08:00
}
class VideoStreamManager:
"""视频推流管理器"""
def __init__(self, socketio=None):
self.socketio = socketio
2025-08-02 16:52:17 +08:00
self.device_index = None
self.video_thread = None
self.video_running = False
2025-07-31 17:23:05 +08:00
# 用于异步编码的线程池和队列
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()
2025-08-02 16:52:17 +08:00
config_path = os.path.join(os.path.dirname(__file__), '..', 'config.ini')
2025-07-31 17:23:05 +08:00
config.read(config_path, encoding='utf-8')
2025-08-02 16:52:17 +08:00
device_index_str = config.get('CAMERA', 'device_index', fallback='0')
self.device_index = int(device_index_str) if device_index_str else 0
logger.info(f'视频监控设备配置加载完成,设备号: {self.device_index}')
2025-07-31 17:23:05 +08:00
except Exception as e:
2025-08-02 16:52:17 +08:00
logger.error(f'视频监控设备配置失败: {e}')
self.device_index = None
2025-07-31 17:23:05 +08:00
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:
2025-08-02 16:52:17 +08:00
self.socketio.emit('video_frame', {
2025-07-31 17:23:05 +08:00
'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):
"""帧编码工作线程"""
2025-08-02 16:52:17 +08:00
while self.video_running:
2025-07-31 17:23:05 +08:00
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
2025-08-02 16:52:17 +08:00
def generate_video_frames(self):
"""生成视频监控帧"""
2025-07-31 17:23:05 +08:00
frame_count = 0
error_count = 0
use_test_mode = False
last_frame_time = time.time()
2025-08-02 16:52:17 +08:00
logger.info(f'开始生成视频监控帧,设备号: {self.device_index}')
2025-07-31 17:23:05 +08:00
try:
2025-08-02 16:52:17 +08:00
cap = cv2.VideoCapture(self.device_index)
2025-07-31 17:23:05 +08:00
if not cap.isOpened():
2025-08-02 16:52:17 +08:00
logger.warning(f'无法打开视频监控流: {self.device_index},切换到测试模式')
2025-07-31 17:23:05 +08:00
use_test_mode = True
if self.socketio:
2025-08-02 16:52:17 +08:00
self.socketio.emit('video_status', {'status': 'started', 'message': '使用测试视频源'})
2025-07-31 17:23:05 +08:00
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)
2025-08-02 16:52:17 +08:00
logger.info('视频监控流已打开,开始推送帧(激进实时模式)')
2025-07-31 17:23:05 +08:00
if self.socketio:
2025-08-02 16:52:17 +08:00
self.socketio.emit('video_status', {'status': 'started', 'message': '使用视频监控视频源(激进实时模式)'})
2025-07-31 17:23:05 +08:00
2025-08-02 16:52:17 +08:00
self.video_running = True
2025-07-31 17:23:05 +08:00
# 启动帧编码工作线程
encoding_thread = threading.Thread(target=self.frame_encoding_worker)
encoding_thread.daemon = True
encoding_thread.start()
2025-08-02 16:52:17 +08:00
while self.video_running:
2025-07-31 17:23:05 +08:00
if use_test_mode:
# 使用测试模式生成帧
frame = self.generate_test_frame(frame_count)
ret = True
else:
2025-08-02 16:52:17 +08:00
# 使用视频监控流,添加帧跳过机制减少延迟
2025-07-31 17:23:05 +08:00
ret, frame = cap.read()
if not ret:
error_count += 1
2025-08-02 16:52:17 +08:00
logger.warning(f'视频监控读取帧失败(第{error_count}次),尝试重连...')
2025-07-31 17:23:05 +08:00
if 'cap' in locals():
cap.release()
if error_count > 5:
2025-08-02 16:52:17 +08:00
logger.warning('视频监控连接失败次数过多,切换到测试模式')
2025-07-31 17:23:05 +08:00
use_test_mode = True
if self.socketio:
2025-08-02 16:52:17 +08:00
self.socketio.emit('video_status', {'status': 'switched', 'message': '已切换到测试视频源'})
2025-07-31 17:23:05 +08:00
continue
# 立即重连,不等待
2025-08-02 16:52:17 +08:00
cap = cv2.VideoCapture(self.device_index)
2025-07-31 17:23:05 +08:00
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:
2025-08-02 16:52:17 +08:00
logger.error(f'监控视频推流异常: {e}')
2025-07-31 17:23:05 +08:00
if self.socketio:
2025-08-02 16:52:17 +08:00
self.socketio.emit('video_status', {'status': 'error', 'message': f'推流异常: {str(e)}'})
2025-07-31 17:23:05 +08:00
finally:
if 'cap' in locals():
cap.release()
2025-08-02 16:52:17 +08:00
self.video_running = False
logger.info(f'视频监控推流结束,总共推送了 {frame_count}')
2025-07-31 17:23:05 +08:00
2025-08-02 16:52:17 +08:00
def start_video_stream(self):
"""启动视频监控推流"""
2025-07-31 17:23:05 +08:00
try:
2025-08-02 16:52:17 +08:00
if self.video_thread and self.video_thread.is_alive():
logger.warning('视频监控线程已在运行')
return {'status': 'already_running', 'message': '视频监控已在运行'}
2025-07-31 17:23:05 +08:00
2025-08-02 16:52:17 +08:00
if not self.device_index:
logger.error('视频监控相机未配置')
return {'status': 'error', 'message': '视频监控相机未配置'}
2025-07-31 17:23:05 +08:00
2025-08-02 16:52:17 +08:00
logger.info(f'视频启动监控线程,设备号: {self.device_index}')
self.video_thread = threading.Thread(target=self.generate_video_frames)
self.video_thread.daemon = True
self.video_thread.start()
self.video_running = True
2025-07-31 17:23:05 +08:00
2025-08-02 16:52:17 +08:00
logger.info('视频监控线程已启动')
return {'status': 'started', 'message': '视频监控线程已启动'}
2025-07-31 17:23:05 +08:00
except Exception as e:
2025-08-02 16:52:17 +08:00
logger.error(f'视频监控线程启动失败: {e}')
return {'status': 'error', 'message': f'视频监控线程启动失败: {str(e)}'}
2025-07-31 17:23:05 +08:00
2025-08-02 16:52:17 +08:00
def stop_video_stream(self):
"""停止视频监控推流"""
2025-07-31 17:23:05 +08:00
try:
2025-08-02 16:52:17 +08:00
self.video_running = False
logger.info('视频监控推流已停止')
return {'status': 'stopped', 'message': '视频监控推流已停止'}
2025-07-31 17:23:05 +08:00
except Exception as e:
2025-08-02 16:52:17 +08:00
logger.error(f'停止视频监控推流失败: {e}')
2025-07-31 17:23:05 +08:00
return {'status': 'error', 'message': f'停止失败: {str(e)}'}
def is_streaming(self):
"""检查是否正在推流"""
2025-08-02 16:52:17 +08:00
return self.video_running
2025-07-31 17:23:05 +08:00
def get_stream_status(self):
"""获取推流状态"""
return {
2025-08-02 16:52:17 +08:00
'running': self.video_running,
'device_index': self.device_index,
'thread_alive': self.video_thread.is_alive() if self.video_thread else False
2025-07-31 17:23:05 +08:00
}
def cleanup(self):
"""清理资源"""
try:
2025-08-02 16:52:17 +08:00
self.video_running = False
if self.video_thread and self.video_thread.is_alive():
self.video_thread.join(timeout=2)
2025-07-31 17:23:05 +08:00
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}')