录制功能提交

This commit is contained in:
zhaozilong12 2025-08-20 10:30:51 +08:00
parent d9544122fc
commit f07af4cd61
8 changed files with 626 additions and 379 deletions

2
.gitignore vendored
View File

@ -46,6 +46,7 @@ build/
# 前端构建输出
frontend/src/renderer/dist/
frontend/src/renderer/dist-electron/
backend/data/patients/
# 临时文件
*.tmp
@ -21413,3 +21414,4 @@ frontend/src/renderer/dist-electron/win-unpacked/resources/backend/BodyBalanceBa
frontend/src/renderer/dist-electron/win-unpacked/resources/backend/BodyBalanceBackend/dll/smitsense/SMiTSenseUsb-F3.0d.dll
frontend/src/renderer/dist-electron/win-unpacked/resources/backend/BodyBalanceBackend/dll/smitsense/SMiTSenseUsbWrapper.dll
frontend/src/renderer/dist-electron/win-unpacked/resources/backend/BodyBalanceBackend/dll/smitsense/Wrapper.dll
backend/data/patients/202508060001/20250820102556/feet.mp4

View File

@ -28,6 +28,8 @@ sys.path.append(os.path.dirname(os.path.abspath(__file__)))
# 导入自定义模块
from database import DatabaseManager
from device_manager import DeviceManager, VideoStreamManager
from devices.screen_recorder import RecordingManager
from devices.camera_manager import CameraManager
from utils import config as app_config
# 确定日志文件路径
@ -108,6 +110,8 @@ device_manager = None
current_detection = None
detection_thread = None
video_stream_manager = None
recording_manager = None
camera_manager = None
@ -160,6 +164,14 @@ def init_app():
# 初始化设备管理器(不自动初始化设备)
device_manager = DeviceManager(db_manager)
# 初始化相机管理器
global camera_manager, recording_manager
camera_manager = CameraManager()
# 初始化录制管理器
recording_manager = RecordingManager(camera_manager=camera_manager, db_manager=db_manager)
if socketio is not None:
logger.info('SocketIO已启用')
device_manager.set_socketio(socketio) # 设置WebSocket连接
@ -701,7 +713,7 @@ def start_detection():
# 开始同步录制
recording_response = None
try:
recording_response = device_manager.start_recording(session_id, patient_id)
recording_response = recording_manager.start_recording(session_id, patient_id)
except Exception as rec_e:
logger.error(f'开始同步录制失败: {rec_e}')
@ -716,9 +728,9 @@ def start_detection():
def stop_detection(session_id):
"""停止检测"""
try:
if not db_manager or not device_manager:
logger.error('数据库管理器或设备管理器未初始化')
return jsonify({'success': False, 'error': '数据库管理器或设备管理器未初始化'}), 500
if not db_manager or not recording_manager:
logger.error('数据库管理器或录制管理器未初始化')
return jsonify({'success': False, 'error': '数据库管理器或录制管理器未初始化'}), 500
if not session_id:
logger.error('缺少会话ID')
@ -749,15 +761,17 @@ def stop_detection(session_id):
'success': False,
'message': f'视频数据解码失败: {str(e)}'
}), 400
# 停止同步录制,传递视频数据
# 停止同步录制
try:
# logger.debug(f'调用device_manager.stop_recordingsession_id: {session_id}, video_data长度: {len(video_data) if video_data else 0}')
# if video_data is None:
# logger.warning(f'视频数据为空session_id: {session_id}')
# else:
# logger.debug(f'视频数据长度: {len(video_data)} 字符,约 {len(video_data)*3/4/1024:.2f} KB, session_id: {session_id}')
restrt=device_manager.stop_recording(session_id, video_data_base64=video_bytes)
logger.error(restrt)
# 使用新的录制管理器停止录制
stop_result = recording_manager.stop_recording()
logger.info(f'录制停止结果: {stop_result}')
# 处理前端传来的视频数据(如果需要保存)
if video_bytes:
# 可以在这里添加保存前端视频数据的逻辑
logger.info(f'接收到前端视频数据,大小: {len(video_bytes)} 字节')
except Exception as rec_e:
logger.error(f'停止同步录制失败: {rec_e}', exc_info=True)
raise

View File

@ -1400,7 +1400,6 @@ class DeviceManager:
'recording_start_time': None,
'video_paths': {
'feet_video': None,
'body_video': None,
'screen_video': None
},
'message': ''
@ -1457,10 +1456,10 @@ class DeviceManager:
# 定义视频文件路径
feet_video_path = os.path.join(base_path, 'feet.mp4')
body_video_path = os.path.join(base_path, 'body.mp4')
screen_video_path = os.path.join(base_path, 'screen.webm')
result['video_paths']['feet_video'] = feet_video_path
result['video_paths']['body_video'] = body_video_path
result['video_paths']['screen_video'] = screen_video_path
# 更新数据库中的视频路径
@ -1472,7 +1471,7 @@ class DeviceManager:
# 更新视频文件路径
self.db_manager.update_session_normal_video_path(session_id, feet_video_path)
self.db_manager.update_session_femtobolt_video_path(session_id, body_video_path)
self.db_manager.update_session_screen_video_path(session_id, screen_video_path)
logger.debug(f'数据库视频路径更新成功 - 会话ID: {session_id}')
@ -1510,63 +1509,6 @@ class DeviceManager:
# logger.error('摄像头未打开,无法初始化脚部视频写入器')
else:
logger.warning('摄像头设备未启用,跳过脚部视频写入器初始化')
if self.device_status['femtobolt']:
frame1, frame_timestamp1 = self._get_latest_frame_from_cache('femtobolt')
if frame1 is not None:
actual_height,actual_width=frame1.shape[:2]
logger.info(f'初始化身体视频写入器 裁剪后分辨率: {actual_height}x{actual_width}')
# 确保图像数据类型正确
if frame1.dtype != np.uint8:
logger.warning(f'身体帧数据类型不是uint8: {frame1.dtype},将进行转换')
self.body_video_writer = cv2.VideoWriter(
body_video_path, fourcc, fps, (actual_width, actual_height)
)
if self.body_video_writer.isOpened():
logger.info(f'身体视频写入器初始化成功: {body_video_path}, 分辨率: {actual_width}x{actual_height}')
else:
logger.error(f'身体视频写入器初始化失败: {body_video_path}, 分辨率: {actual_width}x{actual_height}')
# 尝试使用默认分辨率重新初始化
logger.info('尝试使用默认分辨率重新初始化身体视频写入器')
self.body_video_writer = cv2.VideoWriter(
body_video_path, fourcc, fps, (288, 576) # 默认分辨率
)
if self.body_video_writer.isOpened():
logger.info(f'身体视频写入器使用默认分辨率初始化成功: {body_video_path}')
else:
logger.error(f'身体视频写入器使用默认分辨率初始化仍然失败: {body_video_path}')
else:
logger.warning('无法从缓存获取FemtoBolt帧数据使用默认分辨率初始化身体视频写入器')
self.body_video_writer = cv2.VideoWriter(
body_video_path, fourcc, fps, (288, 576) # 默认分辨率
)
if self.body_video_writer.isOpened():
logger.info(f'身体视频写入器使用默认分辨率初始化成功: {body_video_path}')
else:
logger.error(f'身体视频写入器使用默认分辨率初始化失败: {body_video_path}')
# FemtoBolt默认分辨率
# capture = self.femtobolt_camera.update()
# if capture is not None:
# ret, depth_image = capture.get_depth_image()
# femtoboltheight, femtoboltwidth = depth_image.shape[:2]
# # 计算裁剪后的实际分辨率(与推流处理保持一致)
# target_width = femtoboltheight // 2
# actual_height = femtoboltheight
# actual_width = target_width
# logger.info(f'初始化身体视频写入器,原始分辨率: {femtoboltheight}x{femtoboltwidth}, 裁剪后分辨率: {actual_height}x{actual_width}')
# self.body_video_writer = cv2.VideoWriter(
# body_video_path, fourcc, fps, (actual_width, actual_height)
# )
# if self.body_video_writer.isOpened():
# logger.info(f'身体视频写入器初始化成功: {body_video_path}, 分辨率: {actual_width}x{actual_height}')
# else:
# logger.error(f'身体视频写入器初始化失败: {body_video_path}, 分辨率: {actual_width}x{actual_height}')
else:
logger.warning('FemtoBolt设备未启用跳过身体视频写入器初始化')
# # 屏幕录制写入器(默认分辨率,后续根据实际帧调整)
# self.screen_video_writer = cv2.VideoWriter(
# screen_video_path, fourcc, fps, (1920, 1080)
@ -1583,14 +1525,7 @@ class DeviceManager:
name='FeetRecordingThread'
)
self.feet_recording_thread.start()
# if self.body_video_writer:
# self.body_recording_thread = threading.Thread(
# target=self._body_recording_thread,
# daemon=True,
# name='BodyRecordingThread'
# )
# self.body_recording_thread.start()
# #屏幕录制
# if self.screen_video_writer:
# self.screen_recording_thread = threading.Thread(

View File

@ -81,6 +81,12 @@ class CameraManager(BaseDevice):
'actual_fps': 0,
'dropped_frames': 0
}
# 全局帧缓存(用于录制)
self.global_frame_cache = {}
self.frame_cache_lock = threading.Lock()
self.max_cache_size = 10
self.cache_timeout = 5.0 # 5秒超时
# OpenCV优化开关
try:
@ -328,12 +334,15 @@ class CameraManager(BaseDevice):
# 重置丢帧计数
self.dropped_frames = 0
# 保存原始帧到全局缓存(用于录制)
self._save_frame_to_cache(frame, 'camera')
# 处理帧(降采样以优化传输负载)
processed_frame = self._process_frame(frame)
# 缓存帧(不复制,减少内存占用)
# self.last_frame = processed_frame
# self.frame_cache.append(processed_frame)
self.last_frame = processed_frame
self.frame_cache.append(processed_frame)
# 发送帧数据
self._send_frame_data(processed_frame)
@ -568,8 +577,89 @@ class CameraManager(BaseDevice):
self.frame_cache.clear()
self.last_frame = None
# 清理全局帧缓存
with self.frame_cache_lock:
self.global_frame_cache.clear()
super().cleanup()
self.logger.info("相机资源清理完成")
except Exception as e:
self.logger.error(f"清理相机资源失败: {e}")
def _save_frame_to_cache(self, frame, frame_type='camera'):
"""保存帧到全局缓存"""
try:
with self.frame_cache_lock:
current_time = time.time()
# 清理过期帧
self._cleanup_expired_frames()
# 如果缓存已满,移除最旧的帧
if frame_type in self.global_frame_cache and len(self.global_frame_cache[frame_type]) >= self.max_cache_size:
oldest_key = min(self.global_frame_cache[frame_type].keys())
del self.global_frame_cache[frame_type][oldest_key]
# 初始化帧类型缓存
if frame_type not in self.global_frame_cache:
self.global_frame_cache[frame_type] = {}
# 保存帧(深拷贝避免引用问题)
frame_data = {
'frame': frame.copy(),
'timestamp': current_time,
'frame_id': len(self.global_frame_cache[frame_type])
}
self.global_frame_cache[frame_type][current_time] = frame_data
except Exception as e:
self.logger.error(f'保存帧到缓存失败: {e}')
def _get_latest_frame_from_cache(self, frame_type='camera'):
"""从缓存获取最新帧"""
try:
with self.frame_cache_lock:
if frame_type not in self.global_frame_cache:
self.logger.debug(f'缓存中不存在帧类型: {frame_type}, 可用类型: {list(self.global_frame_cache.keys())}')
return None, None
if not self.global_frame_cache[frame_type]:
self.logger.debug(f'帧类型 {frame_type} 的缓存为空')
return None, None
# 清理过期帧
self._cleanup_expired_frames()
if not self.global_frame_cache[frame_type]:
self.logger.debug(f'清理过期帧后,帧类型 {frame_type} 的缓存为空')
return None, None
# 获取最新帧
latest_timestamp = max(self.global_frame_cache[frame_type].keys())
frame_data = self.global_frame_cache[frame_type][latest_timestamp]
return frame_data['frame'].copy(), frame_data['timestamp']
except Exception as e:
self.logger.error(f'从缓存获取帧失败: {e}')
return None, None
def _cleanup_expired_frames(self):
"""清理过期的缓存帧"""
try:
current_time = time.time()
for frame_type in list(self.global_frame_cache.keys()):
expired_keys = []
for timestamp in self.global_frame_cache[frame_type].keys():
if current_time - timestamp > self.cache_timeout:
expired_keys.append(timestamp)
# 删除过期帧
for key in expired_keys:
del self.global_frame_cache[frame_type][key]
except Exception as e:
self.logger.error(f'清理过期帧失败: {e}')

View File

@ -1,8 +1,8 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
屏幕录制工具
支持录制当前屏幕并保存为视频文件
综合录制管理器
支持屏幕录制和足部视频录制
"""
import cv2
@ -12,286 +12,500 @@ import threading
import time
from datetime import datetime
import os
import logging
from typing import Optional, Dict, Any
class ScreenRecorder:
def __init__(self, output_dir="recordings", fps=20, quality=80, region=None):
try:
from .camera_manager import CameraManager
except ImportError:
from camera_manager import CameraManager
class RecordingManager:
def __init__(self, camera_manager: Optional[CameraManager] = None, db_manager=None):
"""
初始化屏幕录制器
初始化录制管理
Args:
output_dir (str): 输出目录
fps (int): 帧率
quality (int): 视频质量 (1-100)
region (tuple): 录制区域 (x, y, width, height)None表示全屏录制
camera_manager: 相机管理器实例
db_manager: 数据库管理器实例
"""
self.output_dir = output_dir
self.fps = fps
self.quality = quality
self.recording = False
self.paused = False
self.video_writer = None
self.thread = None
self.region = region
self.camera_manager = camera_manager
self.db_manager = db_manager
# 创建输出目
if not os.path.exists(output_dir):
os.makedirs(output_dir)
# 录制状态
self.sync_recording = False
self.recording_stop_event = threading.Event()
# 获取屏幕尺寸
# 会话信息
self.current_session_id = None
self.current_patient_id = None
self.recording_start_time = None
# 视频写入器
self.feet_video_writer = None
self.screen_video_writer = None
# 录制线程
self.feet_recording_thread = None
self.screen_recording_thread = None
# 屏幕录制参数
self.screen_fps = 20
self.screen_region = None
self.screen_size = pyautogui.size()
print(f"屏幕尺寸: {self.screen_size}")
# 设置录制区域
if self.region:
x, y, width, height = self.region
# 确保区域在屏幕范围内
x = max(0, min(x, self.screen_size[0] - 1))
y = max(0, min(y, self.screen_size[1] - 1))
width = min(width, self.screen_size[0] - x)
height = min(height, self.screen_size[1] - y)
self.region = (x, y, width, height)
self.record_size = (width, height)
print(f"录制区域: {self.region}")
else:
self.record_size = self.screen_size
print("录制模式: 全屏录制")
# 视频参数
self.MAX_FRAME_SIZE = (1280, 720) # 最大帧尺寸
# 日志
self.logger = logging.getLogger(__name__)
self.logger.info("录制管理器初始化完成")
def start_recording(self, filename=None):
def start_recording(self, session_id: str, patient_id: str) -> Dict[str, Any]:
"""
开始录制
启动同步录制
Args:
filename (str): 输出文件名如果为None则自动生成
"""
if self.recording:
print("录制已在进行中")
return
if filename is None:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"screen_record_{timestamp}.mp4"
self.output_path = os.path.join(self.output_dir, filename)
# 设置视频编码器
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
self.video_writer = cv2.VideoWriter(
self.output_path,
fourcc,
self.fps,
self.record_size
)
self.recording = True
self.paused = False
# 在新线程中开始录制
self.thread = threading.Thread(target=self._record_loop)
self.thread.daemon = True
self.thread.start()
print(f"开始录制: {self.output_path}")
def _record_loop(self):
"""
录制循环
"""
while self.recording:
if not self.paused:
# 截取屏幕
if self.region:
# 区域录制
x, y, width, height = self.region
screenshot = pyautogui.screenshot(region=(x, y, width, height))
else:
# 全屏录制
screenshot = pyautogui.screenshot()
# 转换为numpy数组
frame = np.array(screenshot)
# 转换颜色格式 (RGB -> BGR)
frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
# 写入视频文件
self.video_writer.write(frame)
session_id: 检测会话ID
patient_id: 患者ID
# 控制帧率
time.sleep(1.0 / self.fps)
def pause_recording(self):
Returns:
Dict: 录制启动状态和信息
"""
暂停录制
"""
if not self.recording:
print("当前没有在录制")
return
result = {
'success': False,
'session_id': session_id,
'patient_id': patient_id,
'recording_start_time': None,
'video_paths': {
'feet_video': None,
'screen_video': None
},
'message': ''
}
self.paused = not self.paused
status = "暂停" if self.paused else "继续"
print(f"录制{status}")
try:
# 检查是否已在录制
if self.sync_recording:
result['message'] = f'已在录制中当前会话ID: {self.current_session_id}'
return result
# 设置录制参数
self.current_session_id = session_id
self.current_patient_id = patient_id
self.recording_start_time = datetime.now()
# 创建存储目录
base_path = os.path.join('data', 'patients', patient_id, session_id)
try:
os.makedirs(base_path, exist_ok=True)
self.logger.info(f'录制目录创建成功: {base_path}')
# 设置目录权限
self._set_directory_permissions(base_path)
except Exception as dir_error:
self.logger.error(f'创建录制目录失败: {base_path}, 错误: {dir_error}')
result['success'] = False
result['message'] = f'创建录制目录失败: {dir_error}'
return result
# 定义视频文件路径
feet_video_path = os.path.join(base_path, 'feet.mp4')
screen_video_path = os.path.join(base_path, 'screen.mp4')
result['video_paths']['feet_video'] = feet_video_path
result['video_paths']['screen_video'] = screen_video_path
# 更新数据库中的视频路径
if self.db_manager:
try:
# 更新会话状态为录制中
if not self.db_manager.update_session_status(session_id, 'recording'):
self.logger.error(f'更新会话状态为录制中失败 - 会话ID: {session_id}')
# 更新视频文件路径
self.db_manager.update_session_normal_video_path(session_id, feet_video_path)
self.db_manager.update_session_screen_video_path(session_id, screen_video_path)
self.logger.debug(f'数据库视频路径更新成功 - 会话ID: {session_id}')
except Exception as db_error:
self.logger.error(f'更新数据库视频路径失败: {db_error}')
# 视频编码参数
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
fps = 30
# 初始化足部视频写入器
if self.camera_manager and self.camera_manager.is_connected:
target_width, target_height = self.MAX_FRAME_SIZE
self.feet_video_writer = cv2.VideoWriter(
feet_video_path, fourcc, fps, (target_width, target_height)
)
if self.feet_video_writer.isOpened():
self.logger.info(f'脚部视频写入器初始化成功: {feet_video_path}')
else:
self.logger.error(f'脚部视频写入器初始化失败: {feet_video_path}')
else:
self.logger.warning('相机设备未启用,跳过脚部视频写入器初始化')
# 初始化屏幕录制写入器
record_size = self.screen_region[2:4] if self.screen_region else self.screen_size
self.screen_video_writer = cv2.VideoWriter(
screen_video_path, fourcc, self.screen_fps, record_size
)
if self.screen_video_writer.isOpened():
self.logger.info(f'屏幕视频写入器初始化成功: {screen_video_path}')
else:
self.logger.error(f'屏幕视频写入器初始化失败: {screen_video_path}')
# 重置停止事件
self.recording_stop_event.clear()
self.sync_recording = True
# 启动录制线程
if self.feet_video_writer:
self.feet_recording_thread = threading.Thread(
target=self._feet_recording_thread,
daemon=True,
name='FeetRecordingThread'
)
self.feet_recording_thread.start()
if self.screen_video_writer:
self.screen_recording_thread = threading.Thread(
target=self._screen_recording_thread,
daemon=True,
name='ScreenRecordingThread'
)
self.screen_recording_thread.start()
result['success'] = True
result['recording_start_time'] = self.recording_start_time.isoformat()
result['message'] = '同步录制已启动'
self.logger.info(f'同步录制已启动 - 会话ID: {session_id}, 患者ID: {patient_id}')
except Exception as e:
self.logger.error(f'启动同步录制失败: {e}')
result['message'] = f'启动录制失败: {str(e)}'
# 清理已创建的写入器
self._cleanup_video_writers()
return result
def stop_recording(self):
def stop_recording(self, session_id: str = None) -> Dict[str, Any]:
"""
停止录制
"""
if not self.recording:
print("当前没有在录制")
return
self.recording = False
self.paused = False
# 等待录制线程结束
if self.thread:
self.thread.join()
# 释放视频写入器
if self.video_writer:
self.video_writer.release()
self.video_writer = None
print(f"录制完成: {self.output_path}")
def set_region(self, region):
"""
设置录制区域
Args:
region (tuple): 录制区域 (x, y, width, height)None表示全屏录制
session_id: 会话ID用于验证是否为当前录制会话
Returns:
Dict: 停止录制的结果
"""
if self.recording:
print("录制进行中,无法更改区域设置")
result = {
'success': False,
'session_id': self.current_session_id,
'message': ''
}
try:
# 验证会话ID
if session_id and session_id != self.current_session_id:
result['message'] = f'会话ID不匹配: 期望 {self.current_session_id}, 收到 {session_id}'
return result
if not self.sync_recording:
result['message'] = '当前没有进行录制'
return result
# 设置停止标志
self.sync_recording = False
self.recording_stop_event.set()
# 等待录制线程结束
if self.feet_recording_thread and self.feet_recording_thread.is_alive():
self.feet_recording_thread.join(timeout=5.0)
if self.screen_recording_thread and self.screen_recording_thread.is_alive():
self.screen_recording_thread.join(timeout=5.0)
# 清理视频写入器
self._cleanup_video_writers()
# 更新数据库状态
if self.db_manager and self.current_session_id:
try:
self.db_manager.update_session_status(self.current_session_id, 'completed')
self.logger.info(f'会话状态已更新为完成 - 会话ID: {self.current_session_id}')
except Exception as db_error:
self.logger.error(f'更新数据库状态失败: {db_error}')
result['success'] = True
result['message'] = '录制已停止'
self.logger.info(f'录制已停止 - 会话ID: {self.current_session_id}')
# 重置会话信息
self.current_session_id = None
self.current_patient_id = None
self.recording_start_time = None
except Exception as e:
self.logger.error(f'停止录制失败: {e}')
result['message'] = f'停止录制失败: {str(e)}'
return result
def _feet_recording_thread(self):
"""足部视频录制线程"""
consecutive_failures = 0
max_consecutive_failures = 10
recording_frame_count = 0
self.logger.info(f"足部录制线程已启动 - 会话ID: {self.current_session_id}")
self.logger.info(f"视频写入器状态: {self.feet_video_writer.isOpened() if self.feet_video_writer else 'None'}")
try:
# 使用与屏幕录制相同的帧率控制
target_fps = 30 # 目标帧率
frame_interval = 1.0 / target_fps
last_frame_time = time.time()
while self.sync_recording and not self.recording_stop_event.is_set():
current_time = time.time()
# 检查是否到了下一帧的时间
if current_time - last_frame_time >= frame_interval:
if self.feet_video_writer:
# 从相机管理器的全局缓存获取最新帧
frame, frame_timestamp = self.camera_manager._get_latest_frame_from_cache('camera')
if frame is not None:
self.logger.debug(f"成功获取帧 - 尺寸: {frame.shape}, 数据类型: {frame.dtype}, 时间戳: {frame_timestamp}")
# 检查视频写入器状态
if not self.feet_video_writer.isOpened():
self.logger.error(f"脚部视频写入器已关闭,无法写入帧 - 会话ID: {self.current_session_id}")
break
try:
# 调整帧尺寸到目标大小
resized_frame = cv2.resize(frame, self.MAX_FRAME_SIZE)
# 写入录制文件
write_success = self.feet_video_writer.write(resized_frame)
if write_success is False:
self.logger.error(f"视频帧写入返回False - 可能写入失败")
consecutive_failures += 1
else:
consecutive_failures = 0
recording_frame_count += 1
except Exception as write_error:
self.logger.error(f"写入脚部视频帧异常: {write_error}")
consecutive_failures += 1
if consecutive_failures >= 10:
self.logger.error("连续写入失败次数过多,停止录制")
break
else:
# 如果没有获取到帧,写入上一帧或黑色帧来保持帧率
consecutive_failures += 1
if consecutive_failures <= 3:
self.logger.warning(f"录制线程无法从缓存获取帧 (连续失败{consecutive_failures}次)")
elif consecutive_failures == max_consecutive_failures:
self.logger.error(f"录制线程连续失败{max_consecutive_failures}次,可能缓存无数据或推流已停止")
last_frame_time = current_time
else:
self.logger.error("足部视频写入器未初始化")
break
# 短暂休眠避免CPU占用过高
time.sleep(0.01)
# 检查连续失败情况
if consecutive_failures >= max_consecutive_failures:
self.logger.error(f"连续失败次数达到上限({max_consecutive_failures}),停止录制")
break
except Exception as e:
self.logger.error(f'足部录制线程异常: {e}')
finally:
self.logger.info(f"足部录制线程已结束 - 会话ID: {self.current_session_id}, 总录制帧数: {recording_frame_count}")
# 确保视频写入器被正确关闭
if self.feet_video_writer:
self.feet_video_writer.release()
self.feet_video_writer = None
self.logger.debug("足部视频写入器已释放")
def _screen_recording_thread(self):
"""屏幕录制线程"""
self.logger.info(f"屏幕录制线程已启动 - 会话ID: {self.current_session_id}")
recording_frame_count = 0
try:
# 使用与足部录制相同的帧率控制
target_fps = 30 # 目标帧率
frame_interval = 1.0 / target_fps
last_frame_time = time.time()
while self.sync_recording and not self.recording_stop_event.is_set():
current_time = time.time()
# 检查是否到了下一帧的时间
if current_time - last_frame_time >= frame_interval:
try:
# 截取屏幕
if self.screen_region:
x, y, width, height = self.screen_region
screenshot = pyautogui.screenshot(region=(x, y, width, height))
else:
screenshot = pyautogui.screenshot()
# 转换为numpy数组
frame = np.array(screenshot)
# 转换颜色格式 (RGB -> BGR)
frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
# 写入视频文件
if self.screen_video_writer and self.screen_video_writer.isOpened():
self.screen_video_writer.write(frame)
recording_frame_count += 1
last_frame_time = current_time
except Exception as e:
self.logger.error(f"屏幕录制异常: {e}")
# 短暂休眠避免CPU占用过高
time.sleep(0.01)
except Exception as e:
self.logger.error(f'屏幕录制线程异常: {e}')
finally:
self.logger.info(f"屏幕录制线程已结束 - 会话ID: {self.current_session_id}, 总录制帧数: {recording_frame_count}")
# 确保视频写入器被正确关闭
if self.screen_video_writer:
self.screen_video_writer.release()
self.screen_video_writer = None
self.logger.debug("屏幕视频写入器已释放")
def _cleanup_video_writers(self):
"""清理视频写入器"""
try:
if self.feet_video_writer:
self.feet_video_writer.release()
self.feet_video_writer = None
self.logger.debug("足部视频写入器已清理")
if self.screen_video_writer:
self.screen_video_writer.release()
self.screen_video_writer = None
self.logger.debug("屏幕视频写入器已清理")
except Exception as e:
self.logger.error(f"清理视频写入器失败: {e}")
def _set_directory_permissions(self, path):
"""设置目录权限"""
try:
import subprocess
import platform
if platform.system() == 'Windows':
try:
# 为Users用户组授予完全控制权限
subprocess.run([
'icacls', path, '/grant', 'Users:(OI)(CI)F'
], check=True, capture_output=True, text=True)
# 为Everyone用户组授予完全控制权限
subprocess.run([
'icacls', path, '/grant', 'Everyone:(OI)(CI)F'
], check=True, capture_output=True, text=True)
self.logger.info(f"已设置Windows目录权限Users和Everyone完全控制: {path}")
except subprocess.CalledProcessError as icacls_error:
self.logger.warning(f"Windows权限设置失败: {icacls_error}")
else:
self.logger.info(f"已设置目录权限为777: {path}")
except Exception as perm_error:
self.logger.warning(f"设置目录权限失败: {perm_error},但目录创建成功")
def set_screen_region(self, region):
"""设置屏幕录制区域"""
if self.sync_recording:
self.logger.warning("录制进行中,无法更改区域设置")
return False
self.region = region
self.screen_region = region
# 重新计算录制区域
if self.region:
x, y, width, height = self.region
if self.screen_region:
x, y, width, height = self.screen_region
# 确保区域在屏幕范围内
x = max(0, min(x, self.screen_size[0] - 1))
y = max(0, min(y, self.screen_size[1] - 1))
width = min(width, self.screen_size[0] - x)
height = min(height, self.screen_size[1] - y)
self.region = (x, y, width, height)
self.record_size = (width, height)
print(f"录制区域已设置: {self.region}")
self.screen_region = (x, y, width, height)
self.logger.info(f"录制区域已设置: {self.screen_region}")
else:
self.record_size = self.screen_size
print("录制模式已设置: 全屏录制")
self.logger.info("录制模式已设置: 全屏录制")
return True
def get_status(self):
"""
获取录制状态
Returns:
dict: 包含录制状态信息的字典
"""
"""获取录制状态"""
return {
'recording': self.recording,
'paused': self.paused,
'output_path': getattr(self, 'output_path', None),
'recording': self.sync_recording,
'session_id': self.current_session_id,
'patient_id': self.current_patient_id,
'recording_start_time': self.recording_start_time.isoformat() if self.recording_start_time else None,
'screen_size': self.screen_size,
'record_size': self.record_size,
'region': self.region,
'fps': self.fps
'screen_region': self.screen_region,
'screen_fps': self.screen_fps,
'feet_writer_active': self.feet_video_writer is not None and self.feet_video_writer.isOpened() if self.feet_video_writer else False,
'screen_writer_active': self.screen_video_writer is not None and self.screen_video_writer.isOpened() if self.screen_video_writer else False
}
def main():
"""
主函数 - 命令行界面
"""
recorder = ScreenRecorder()
print("\n=== 屏幕录制工具 ===")
print("命令:")
print(" start [filename] - 开始录制")
print(" pause - 暂停/恢复录制")
print(" stop - 停止录制")
print(" status - 查看状态")
print(" region x y w h - 设置录制区域 (x, y, 宽度, 高度)")
print(" fullscreen - 设置全屏录制")
print(" center w h - 设置居中区域录制 (宽度, 高度)")
print(" quit - 退出程序")
print("\n输入命令:")
while True:
try:
command = input("> ").strip().split()
if not command:
continue
cmd = command[0].lower()
if cmd == "start":
filename = command[1] if len(command) > 1 else None
recorder.start_recording(filename)
elif cmd == "pause":
recorder.pause_recording()
elif cmd == "stop":
recorder.stop_recording()
elif cmd == "status":
status = recorder.get_status()
print(f"录制状态: {'进行中' if status['recording'] else '已停止'}")
print(f"暂停状态: {'' if status['paused'] else ''}")
print(f"输出文件: {status['output_path']}")
print(f"屏幕尺寸: {status['screen_size']}")
print(f"录制尺寸: {status['record_size']}")
print(f"录制区域: {status['region'] if status['region'] else '全屏'}")
print(f"帧率: {status['fps']} FPS")
elif cmd == "region":
if len(command) != 5:
print("用法: region x y width height")
continue
try:
x, y, w, h = map(int, command[1:5])
if recorder.set_region((x, y, w, h)):
print(f"录制区域已设置: ({x}, {y}, {w}, {h})")
except ValueError:
print("请输入有效的数字")
elif cmd == "fullscreen":
if recorder.set_region(None):
print("已设置为全屏录制")
elif cmd == "center":
if len(command) != 3:
print("用法: center width height")
continue
try:
w, h = map(int, command[1:3])
screen_w, screen_h = recorder.screen_size
x = (screen_w - w) // 2
y = (screen_h - h) // 2
if recorder.set_region((x, y, w, h)):
print(f"居中录制区域已设置: ({x}, {y}, {w}, {h})")
except ValueError:
print("请输入有效的数字")
elif cmd == "quit":
if recorder.recording:
recorder.stop_recording()
print("程序退出")
break
else:
print("未知命令")
except KeyboardInterrupt:
if recorder.recording:
recorder.stop_recording()
print("\n程序退出")
break
except Exception as e:
print(f"错误: {e}")
if __name__ == "__main__":
main()
# 保持向后兼容的ScreenRecorder类
class ScreenRecorder:
def __init__(self, output_dir="recordings", fps=20, quality=80, region=None):
"""向后兼容的屏幕录制器"""
self.recording_manager = RecordingManager()
self.recording_manager.screen_fps = fps
self.recording_manager.set_screen_region(region)
self.output_dir = output_dir
# 创建输出目录
if not os.path.exists(output_dir):
os.makedirs(output_dir)
def start_recording(self, filename=None):
"""开始录制"""
if filename is None:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"screen_record_{timestamp}"
# 使用文件名作为会话ID
session_id = filename
patient_id = "default"
return self.recording_manager.start_recording(session_id, patient_id)
def stop_recording(self):
"""停止录制"""
return self.recording_manager.stop_recording()
def get_status(self):
"""获取状态"""
return self.recording_manager.get_status()

View File

@ -15,7 +15,7 @@ backup_interval = 24
max_backups = 7
[CAMERA]
device_index = 0
device_index = 3
width = 1280
height = 720
fps = 30
@ -30,7 +30,7 @@ depth_range_max = 1700
[DEVICES]
imu_device_type = real
imu_port = COM3
imu_baudrate = 115200
imu_baudrate = 9600
pressure_device_type = real
pressure_use_mock = False
pressure_port = COM5

View File

@ -25,29 +25,36 @@ sys.path.append(os.path.dirname(os.path.abspath(__file__)))
# 导入模块
from database import DatabaseManager
from utils import config as app_config
# 导入设备管理器
try:
from devices.camera_manager import CameraManager
from devices.imu_manager import IMUManager
from devices.pressure_manager import PressureManager
from devices.femtobolt_manager import FemtoBoltManager
from devices.device_coordinator import DeviceCoordinator
from devices.utils.config_manager import ConfigManager
except ImportError:
# 如果上面的导入失败,尝试直接导入
from camera_manager import CameraManager
import imu_manager
import pressure_manager
import femtobolt_manager
import device_coordinator
from utils import config_manager
from devices.camera_manager import CameraManager
from devices.imu_manager import IMUManager
from devices.pressure_manager import PressureManager
from devices.femtobolt_manager import FemtoBoltManager
from devices.device_coordinator import DeviceCoordinator
from devices.screen_recorder import RecordingManager
from devices.utils.config_manager import ConfigManager
# # 导入设备管理器
# try:
# from devices.camera_manager import CameraManager
# from devices.imu_manager import IMUManager
# from devices.pressure_manager import PressureManager
# from devices.femtobolt_manager import FemtoBoltManager
# from devices.device_coordinator import DeviceCoordinator
# from devices.screen_recorder import RecordingManager
# from devices.utils.config_manager import ConfigManager
# except ImportError:
# # 如果上面的导入失败,尝试直接导入
# # from camera_manager import CameraManager
# import imu_manager
# import pressure_manager
# import femtobolt_manager
# import device_coordinator
# from utils import config_manager
IMUManager = imu_manager.IMUManager
PressureManager = pressure_manager.PressureManager
FemtoBoltManager = femtobolt_manager.FemtoBoltManager
DeviceCoordinator = device_coordinator.DeviceCoordinator
ConfigManager = config_manager.ConfigManager
# IMUManager = imu_manager.IMUManager
# PressureManager = pressure_manager.PressureManager
# FemtoBoltManager = femtobolt_manager.FemtoBoltManager
# DeviceCoordinator = device_coordinator.DeviceCoordinator
# ConfigManager = config_manager.ConfigManager
class AppServer:
@ -222,6 +229,14 @@ class AppServer:
self.device_coordinator = DeviceCoordinator(self.socketio)
self.logger.info('设备协调器初始化完成')
# 初始化录制管理器
self.logger.info('正在初始化录制管理器...')
self.recording_manager = RecordingManager(
camera_manager=self.device_managers['camera'],
db_manager=self.db_manager
)
self.logger.info('录制管理器初始化完成')
# 启动Flask应用
host = self.host
port = self.port
@ -891,7 +906,7 @@ class AppServer:
# 开始同步录制
recording_response = None
try:
recording_response = self.device_coordinator.start_recording(session_id, patient_id)
recording_response = self.recording_manager.start_recording(session_id, patient_id)
except Exception as rec_e:
self.logger.error(f'开始同步录制失败: {rec_e}')
@ -937,8 +952,8 @@ class AppServer:
}), 400
# 停止同步录制,传递视频数据
try:
restrt = self.device_coordinator.stop_recording(session_id, video_data_base64=video_bytes)
self.logger.error(restrt)
restrt = self.recording_manager.stop_recording(session_id)
self.logger.info(f'停止录制结果: {restrt}')
except Exception as rec_e:
self.logger.error(f'停止同步录制失败: {rec_e}', exc_info=True)
raise

View File

@ -1621,28 +1621,6 @@ async function saveDetectionData() {
try {
screenshotLoading.value = true
//
ElMessage.info('正在生成截图...')
// DOM
const element = document.getElementById('detectare')
if (!element) {
throw new Error('未找到截图区域')
}
// 使html2canvas
const canvas = await html2canvas(element, {
useCORS: true,
allowTaint: true,
backgroundColor: '#ffffff',
scale: 1,
logging: false
})
// canvasbase64
const base64Image = canvas.toDataURL('image/png')
//
ElMessage.info('正在保存截图...')
@ -1656,7 +1634,6 @@ async function saveDetectionData() {
patientId: patientInfo.value.id,
patientName: patientInfo.value.name,
sessionId: patientInfo.value.sessionId,
imageData: base64Image,
head_pose: {},
body_pose: {},
foot_data: {}
@ -1664,7 +1641,7 @@ async function saveDetectionData() {
//
ElMessage.success({
message: `截图保存成功!文件路径: ${result.filepath}`,
message: `截图保存成功!`,
duration: 5000
})