录制功能提交
This commit is contained in:
parent
d9544122fc
commit
f07af4cd61
2
.gitignore
vendored
2
.gitignore
vendored
@ -46,6 +46,7 @@ build/
|
|||||||
# 前端构建输出
|
# 前端构建输出
|
||||||
frontend/src/renderer/dist/
|
frontend/src/renderer/dist/
|
||||||
frontend/src/renderer/dist-electron/
|
frontend/src/renderer/dist-electron/
|
||||||
|
backend/data/patients/
|
||||||
|
|
||||||
# 临时文件
|
# 临时文件
|
||||||
*.tmp
|
*.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/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/SMiTSenseUsbWrapper.dll
|
||||||
frontend/src/renderer/dist-electron/win-unpacked/resources/backend/BodyBalanceBackend/dll/smitsense/Wrapper.dll
|
frontend/src/renderer/dist-electron/win-unpacked/resources/backend/BodyBalanceBackend/dll/smitsense/Wrapper.dll
|
||||||
|
backend/data/patients/202508060001/20250820102556/feet.mp4
|
||||||
|
@ -28,6 +28,8 @@ sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
|||||||
# 导入自定义模块
|
# 导入自定义模块
|
||||||
from database import DatabaseManager
|
from database import DatabaseManager
|
||||||
from device_manager import DeviceManager, VideoStreamManager
|
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
|
from utils import config as app_config
|
||||||
|
|
||||||
# 确定日志文件路径
|
# 确定日志文件路径
|
||||||
@ -108,6 +110,8 @@ device_manager = None
|
|||||||
current_detection = None
|
current_detection = None
|
||||||
detection_thread = None
|
detection_thread = None
|
||||||
video_stream_manager = None
|
video_stream_manager = None
|
||||||
|
recording_manager = None
|
||||||
|
camera_manager = None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -160,6 +164,14 @@ def init_app():
|
|||||||
|
|
||||||
# 初始化设备管理器(不自动初始化设备)
|
# 初始化设备管理器(不自动初始化设备)
|
||||||
device_manager = DeviceManager(db_manager)
|
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:
|
if socketio is not None:
|
||||||
logger.info('SocketIO已启用')
|
logger.info('SocketIO已启用')
|
||||||
device_manager.set_socketio(socketio) # 设置WebSocket连接
|
device_manager.set_socketio(socketio) # 设置WebSocket连接
|
||||||
@ -701,7 +713,7 @@ def start_detection():
|
|||||||
# 开始同步录制
|
# 开始同步录制
|
||||||
recording_response = None
|
recording_response = None
|
||||||
try:
|
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:
|
except Exception as rec_e:
|
||||||
logger.error(f'开始同步录制失败: {rec_e}')
|
logger.error(f'开始同步录制失败: {rec_e}')
|
||||||
|
|
||||||
@ -716,9 +728,9 @@ def start_detection():
|
|||||||
def stop_detection(session_id):
|
def stop_detection(session_id):
|
||||||
"""停止检测"""
|
"""停止检测"""
|
||||||
try:
|
try:
|
||||||
if not db_manager or not device_manager:
|
if not db_manager or not recording_manager:
|
||||||
logger.error('数据库管理器或设备管理器未初始化')
|
logger.error('数据库管理器或录制管理器未初始化')
|
||||||
return jsonify({'success': False, 'error': '数据库管理器或设备管理器未初始化'}), 500
|
return jsonify({'success': False, 'error': '数据库管理器或录制管理器未初始化'}), 500
|
||||||
|
|
||||||
if not session_id:
|
if not session_id:
|
||||||
logger.error('缺少会话ID')
|
logger.error('缺少会话ID')
|
||||||
@ -749,15 +761,17 @@ def stop_detection(session_id):
|
|||||||
'success': False,
|
'success': False,
|
||||||
'message': f'视频数据解码失败: {str(e)}'
|
'message': f'视频数据解码失败: {str(e)}'
|
||||||
}), 400
|
}), 400
|
||||||
# 停止同步录制,传递视频数据
|
# 停止同步录制
|
||||||
try:
|
try:
|
||||||
# logger.debug(f'调用device_manager.stop_recording,session_id: {session_id}, video_data长度: {len(video_data) if video_data else 0}')
|
# 使用新的录制管理器停止录制
|
||||||
# if video_data is None:
|
stop_result = recording_manager.stop_recording()
|
||||||
# logger.warning(f'视频数据为空,session_id: {session_id}')
|
logger.info(f'录制停止结果: {stop_result}')
|
||||||
# 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)
|
if video_bytes:
|
||||||
logger.error(restrt)
|
# 可以在这里添加保存前端视频数据的逻辑
|
||||||
|
logger.info(f'接收到前端视频数据,大小: {len(video_bytes)} 字节')
|
||||||
|
|
||||||
except Exception as rec_e:
|
except Exception as rec_e:
|
||||||
logger.error(f'停止同步录制失败: {rec_e}', exc_info=True)
|
logger.error(f'停止同步录制失败: {rec_e}', exc_info=True)
|
||||||
raise
|
raise
|
||||||
|
@ -1400,7 +1400,6 @@ class DeviceManager:
|
|||||||
'recording_start_time': None,
|
'recording_start_time': None,
|
||||||
'video_paths': {
|
'video_paths': {
|
||||||
'feet_video': None,
|
'feet_video': None,
|
||||||
'body_video': None,
|
|
||||||
'screen_video': None
|
'screen_video': None
|
||||||
},
|
},
|
||||||
'message': ''
|
'message': ''
|
||||||
@ -1457,10 +1456,10 @@ class DeviceManager:
|
|||||||
|
|
||||||
# 定义视频文件路径
|
# 定义视频文件路径
|
||||||
feet_video_path = os.path.join(base_path, 'feet.mp4')
|
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')
|
screen_video_path = os.path.join(base_path, 'screen.webm')
|
||||||
result['video_paths']['feet_video'] = feet_video_path
|
result['video_paths']['feet_video'] = feet_video_path
|
||||||
result['video_paths']['body_video'] = body_video_path
|
|
||||||
result['video_paths']['screen_video'] = screen_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_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)
|
self.db_manager.update_session_screen_video_path(session_id, screen_video_path)
|
||||||
|
|
||||||
logger.debug(f'数据库视频路径更新成功 - 会话ID: {session_id}')
|
logger.debug(f'数据库视频路径更新成功 - 会话ID: {session_id}')
|
||||||
@ -1510,63 +1509,6 @@ class DeviceManager:
|
|||||||
# logger.error('摄像头未打开,无法初始化脚部视频写入器')
|
# logger.error('摄像头未打开,无法初始化脚部视频写入器')
|
||||||
else:
|
else:
|
||||||
logger.warning('摄像头设备未启用,跳过脚部视频写入器初始化')
|
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(
|
# self.screen_video_writer = cv2.VideoWriter(
|
||||||
# screen_video_path, fourcc, fps, (1920, 1080)
|
# screen_video_path, fourcc, fps, (1920, 1080)
|
||||||
@ -1583,14 +1525,7 @@ class DeviceManager:
|
|||||||
name='FeetRecordingThread'
|
name='FeetRecordingThread'
|
||||||
)
|
)
|
||||||
self.feet_recording_thread.start()
|
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:
|
# if self.screen_video_writer:
|
||||||
# self.screen_recording_thread = threading.Thread(
|
# self.screen_recording_thread = threading.Thread(
|
||||||
|
@ -81,6 +81,12 @@ class CameraManager(BaseDevice):
|
|||||||
'actual_fps': 0,
|
'actual_fps': 0,
|
||||||
'dropped_frames': 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优化开关
|
# OpenCV优化开关
|
||||||
try:
|
try:
|
||||||
@ -328,12 +334,15 @@ class CameraManager(BaseDevice):
|
|||||||
# 重置丢帧计数
|
# 重置丢帧计数
|
||||||
self.dropped_frames = 0
|
self.dropped_frames = 0
|
||||||
|
|
||||||
|
# 保存原始帧到全局缓存(用于录制)
|
||||||
|
self._save_frame_to_cache(frame, 'camera')
|
||||||
|
|
||||||
# 处理帧(降采样以优化传输负载)
|
# 处理帧(降采样以优化传输负载)
|
||||||
processed_frame = self._process_frame(frame)
|
processed_frame = self._process_frame(frame)
|
||||||
|
|
||||||
# 缓存帧(不复制,减少内存占用)
|
# 缓存帧(不复制,减少内存占用)
|
||||||
# self.last_frame = processed_frame
|
self.last_frame = processed_frame
|
||||||
# self.frame_cache.append(processed_frame)
|
self.frame_cache.append(processed_frame)
|
||||||
|
|
||||||
# 发送帧数据
|
# 发送帧数据
|
||||||
self._send_frame_data(processed_frame)
|
self._send_frame_data(processed_frame)
|
||||||
@ -568,8 +577,89 @@ class CameraManager(BaseDevice):
|
|||||||
self.frame_cache.clear()
|
self.frame_cache.clear()
|
||||||
self.last_frame = None
|
self.last_frame = None
|
||||||
|
|
||||||
|
# 清理全局帧缓存
|
||||||
|
with self.frame_cache_lock:
|
||||||
|
self.global_frame_cache.clear()
|
||||||
|
|
||||||
super().cleanup()
|
super().cleanup()
|
||||||
self.logger.info("相机资源清理完成")
|
self.logger.info("相机资源清理完成")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"清理相机资源失败: {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}')
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
屏幕录制工具
|
综合录制管理器
|
||||||
支持录制当前屏幕并保存为视频文件
|
支持屏幕录制和足部视频录制
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
@ -12,286 +12,500 @@ import threading
|
|||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import os
|
import os
|
||||||
|
import logging
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
|
||||||
class ScreenRecorder:
|
try:
|
||||||
def __init__(self, output_dir="recordings", fps=20, quality=80, region=None):
|
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:
|
Args:
|
||||||
output_dir (str): 输出目录
|
camera_manager: 相机管理器实例
|
||||||
fps (int): 帧率
|
db_manager: 数据库管理器实例
|
||||||
quality (int): 视频质量 (1-100)
|
|
||||||
region (tuple): 录制区域 (x, y, width, height),None表示全屏录制
|
|
||||||
"""
|
"""
|
||||||
self.output_dir = output_dir
|
self.camera_manager = camera_manager
|
||||||
self.fps = fps
|
self.db_manager = db_manager
|
||||||
self.quality = quality
|
|
||||||
self.recording = False
|
|
||||||
self.paused = False
|
|
||||||
self.video_writer = None
|
|
||||||
self.thread = None
|
|
||||||
self.region = region
|
|
||||||
|
|
||||||
# 创建输出目录
|
# 录制状态
|
||||||
if not os.path.exists(output_dir):
|
self.sync_recording = False
|
||||||
os.makedirs(output_dir)
|
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()
|
self.screen_size = pyautogui.size()
|
||||||
print(f"屏幕尺寸: {self.screen_size}")
|
|
||||||
|
|
||||||
# 设置录制区域
|
# 视频参数
|
||||||
if self.region:
|
self.MAX_FRAME_SIZE = (1280, 720) # 最大帧尺寸
|
||||||
x, y, width, height = self.region
|
|
||||||
# 确保区域在屏幕范围内
|
# 日志
|
||||||
x = max(0, min(x, self.screen_size[0] - 1))
|
self.logger = logging.getLogger(__name__)
|
||||||
y = max(0, min(y, self.screen_size[1] - 1))
|
|
||||||
width = min(width, self.screen_size[0] - x)
|
self.logger.info("录制管理器初始化完成")
|
||||||
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("录制模式: 全屏录制")
|
|
||||||
|
|
||||||
def start_recording(self, filename=None):
|
def start_recording(self, session_id: str, patient_id: str) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
开始录制
|
启动同步录制
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
filename (str): 输出文件名,如果为None则自动生成
|
session_id: 检测会话ID
|
||||||
"""
|
patient_id: 患者ID
|
||||||
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)
|
|
||||||
|
|
||||||
# 控制帧率
|
Returns:
|
||||||
time.sleep(1.0 / self.fps)
|
Dict: 录制启动状态和信息
|
||||||
|
|
||||||
def pause_recording(self):
|
|
||||||
"""
|
"""
|
||||||
暂停录制
|
result = {
|
||||||
"""
|
'success': False,
|
||||||
if not self.recording:
|
'session_id': session_id,
|
||||||
print("当前没有在录制")
|
'patient_id': patient_id,
|
||||||
return
|
'recording_start_time': None,
|
||||||
|
'video_paths': {
|
||||||
|
'feet_video': None,
|
||||||
|
'screen_video': None
|
||||||
|
},
|
||||||
|
'message': ''
|
||||||
|
}
|
||||||
|
|
||||||
self.paused = not self.paused
|
try:
|
||||||
status = "暂停" if self.paused else "继续"
|
# 检查是否已在录制
|
||||||
print(f"录制{status}")
|
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:
|
Args:
|
||||||
region (tuple): 录制区域 (x, y, width, height),None表示全屏录制
|
session_id: 会话ID,用于验证是否为当前录制会话
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict: 停止录制的结果
|
||||||
"""
|
"""
|
||||||
if self.recording:
|
result = {
|
||||||
print("录制进行中,无法更改区域设置")
|
'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
|
return False
|
||||||
|
|
||||||
self.region = region
|
self.screen_region = region
|
||||||
|
|
||||||
# 重新计算录制区域
|
if self.screen_region:
|
||||||
if self.region:
|
x, y, width, height = self.screen_region
|
||||||
x, y, width, height = self.region
|
|
||||||
# 确保区域在屏幕范围内
|
# 确保区域在屏幕范围内
|
||||||
x = max(0, min(x, self.screen_size[0] - 1))
|
x = max(0, min(x, self.screen_size[0] - 1))
|
||||||
y = max(0, min(y, self.screen_size[1] - 1))
|
y = max(0, min(y, self.screen_size[1] - 1))
|
||||||
width = min(width, self.screen_size[0] - x)
|
width = min(width, self.screen_size[0] - x)
|
||||||
height = min(height, self.screen_size[1] - y)
|
height = min(height, self.screen_size[1] - y)
|
||||||
self.region = (x, y, width, height)
|
self.screen_region = (x, y, width, height)
|
||||||
self.record_size = (width, height)
|
self.logger.info(f"录制区域已设置: {self.screen_region}")
|
||||||
print(f"录制区域已设置: {self.region}")
|
|
||||||
else:
|
else:
|
||||||
self.record_size = self.screen_size
|
self.logger.info("录制模式已设置: 全屏录制")
|
||||||
print("录制模式已设置: 全屏录制")
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def get_status(self):
|
def get_status(self):
|
||||||
"""
|
"""获取录制状态"""
|
||||||
获取录制状态
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: 包含录制状态信息的字典
|
|
||||||
"""
|
|
||||||
return {
|
return {
|
||||||
'recording': self.recording,
|
'recording': self.sync_recording,
|
||||||
'paused': self.paused,
|
'session_id': self.current_session_id,
|
||||||
'output_path': getattr(self, 'output_path', None),
|
'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,
|
'screen_size': self.screen_size,
|
||||||
'record_size': self.record_size,
|
'screen_region': self.screen_region,
|
||||||
'region': self.region,
|
'screen_fps': self.screen_fps,
|
||||||
'fps': self.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__":
|
# 保持向后兼容的ScreenRecorder类
|
||||||
main()
|
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()
|
@ -15,7 +15,7 @@ backup_interval = 24
|
|||||||
max_backups = 7
|
max_backups = 7
|
||||||
|
|
||||||
[CAMERA]
|
[CAMERA]
|
||||||
device_index = 0
|
device_index = 3
|
||||||
width = 1280
|
width = 1280
|
||||||
height = 720
|
height = 720
|
||||||
fps = 30
|
fps = 30
|
||||||
@ -30,7 +30,7 @@ depth_range_max = 1700
|
|||||||
[DEVICES]
|
[DEVICES]
|
||||||
imu_device_type = real
|
imu_device_type = real
|
||||||
imu_port = COM3
|
imu_port = COM3
|
||||||
imu_baudrate = 115200
|
imu_baudrate = 9600
|
||||||
pressure_device_type = real
|
pressure_device_type = real
|
||||||
pressure_use_mock = False
|
pressure_use_mock = False
|
||||||
pressure_port = COM5
|
pressure_port = COM5
|
||||||
|
@ -25,29 +25,36 @@ sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
|||||||
# 导入模块
|
# 导入模块
|
||||||
from database import DatabaseManager
|
from database import DatabaseManager
|
||||||
from utils import config as app_config
|
from utils import config as app_config
|
||||||
|
from devices.camera_manager import CameraManager
|
||||||
# 导入设备管理器
|
from devices.imu_manager import IMUManager
|
||||||
try:
|
from devices.pressure_manager import PressureManager
|
||||||
from devices.camera_manager import CameraManager
|
from devices.femtobolt_manager import FemtoBoltManager
|
||||||
from devices.imu_manager import IMUManager
|
from devices.device_coordinator import DeviceCoordinator
|
||||||
from devices.pressure_manager import PressureManager
|
from devices.screen_recorder import RecordingManager
|
||||||
from devices.femtobolt_manager import FemtoBoltManager
|
from devices.utils.config_manager import ConfigManager
|
||||||
from devices.device_coordinator import DeviceCoordinator
|
# # 导入设备管理器
|
||||||
from devices.utils.config_manager import ConfigManager
|
# try:
|
||||||
except ImportError:
|
# from devices.camera_manager import CameraManager
|
||||||
# 如果上面的导入失败,尝试直接导入
|
# from devices.imu_manager import IMUManager
|
||||||
from camera_manager import CameraManager
|
# from devices.pressure_manager import PressureManager
|
||||||
import imu_manager
|
# from devices.femtobolt_manager import FemtoBoltManager
|
||||||
import pressure_manager
|
# from devices.device_coordinator import DeviceCoordinator
|
||||||
import femtobolt_manager
|
# from devices.screen_recorder import RecordingManager
|
||||||
import device_coordinator
|
# from devices.utils.config_manager import ConfigManager
|
||||||
from utils import config_manager
|
# 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
|
# IMUManager = imu_manager.IMUManager
|
||||||
PressureManager = pressure_manager.PressureManager
|
# PressureManager = pressure_manager.PressureManager
|
||||||
FemtoBoltManager = femtobolt_manager.FemtoBoltManager
|
# FemtoBoltManager = femtobolt_manager.FemtoBoltManager
|
||||||
DeviceCoordinator = device_coordinator.DeviceCoordinator
|
# DeviceCoordinator = device_coordinator.DeviceCoordinator
|
||||||
ConfigManager = config_manager.ConfigManager
|
# ConfigManager = config_manager.ConfigManager
|
||||||
|
|
||||||
|
|
||||||
class AppServer:
|
class AppServer:
|
||||||
@ -222,6 +229,14 @@ class AppServer:
|
|||||||
self.device_coordinator = DeviceCoordinator(self.socketio)
|
self.device_coordinator = DeviceCoordinator(self.socketio)
|
||||||
self.logger.info('设备协调器初始化完成')
|
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应用
|
# 启动Flask应用
|
||||||
host = self.host
|
host = self.host
|
||||||
port = self.port
|
port = self.port
|
||||||
@ -891,7 +906,7 @@ class AppServer:
|
|||||||
# 开始同步录制
|
# 开始同步录制
|
||||||
recording_response = None
|
recording_response = None
|
||||||
try:
|
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:
|
except Exception as rec_e:
|
||||||
self.logger.error(f'开始同步录制失败: {rec_e}')
|
self.logger.error(f'开始同步录制失败: {rec_e}')
|
||||||
|
|
||||||
@ -937,8 +952,8 @@ class AppServer:
|
|||||||
}), 400
|
}), 400
|
||||||
# 停止同步录制,传递视频数据
|
# 停止同步录制,传递视频数据
|
||||||
try:
|
try:
|
||||||
restrt = self.device_coordinator.stop_recording(session_id, video_data_base64=video_bytes)
|
restrt = self.recording_manager.stop_recording(session_id)
|
||||||
self.logger.error(restrt)
|
self.logger.info(f'停止录制结果: {restrt}')
|
||||||
except Exception as rec_e:
|
except Exception as rec_e:
|
||||||
self.logger.error(f'停止同步录制失败: {rec_e}', exc_info=True)
|
self.logger.error(f'停止同步录制失败: {rec_e}', exc_info=True)
|
||||||
raise
|
raise
|
||||||
|
@ -1621,28 +1621,6 @@ async function saveDetectionData() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
screenshotLoading.value = true
|
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
|
|
||||||
})
|
|
||||||
|
|
||||||
// 将canvas转换为base64
|
|
||||||
const base64Image = canvas.toDataURL('image/png')
|
|
||||||
|
|
||||||
// 显示保存进度
|
// 显示保存进度
|
||||||
ElMessage.info('正在保存截图...')
|
ElMessage.info('正在保存截图...')
|
||||||
|
|
||||||
@ -1656,7 +1634,6 @@ async function saveDetectionData() {
|
|||||||
patientId: patientInfo.value.id,
|
patientId: patientInfo.value.id,
|
||||||
patientName: patientInfo.value.name,
|
patientName: patientInfo.value.name,
|
||||||
sessionId: patientInfo.value.sessionId,
|
sessionId: patientInfo.value.sessionId,
|
||||||
imageData: base64Image,
|
|
||||||
head_pose: {},
|
head_pose: {},
|
||||||
body_pose: {},
|
body_pose: {},
|
||||||
foot_data: {}
|
foot_data: {}
|
||||||
@ -1664,7 +1641,7 @@ async function saveDetectionData() {
|
|||||||
|
|
||||||
// 显示成功消息和文件路径
|
// 显示成功消息和文件路径
|
||||||
ElMessage.success({
|
ElMessage.success({
|
||||||
message: `截图保存成功!文件路径: ${result.filepath}`,
|
message: `截图保存成功!`,
|
||||||
duration: 5000
|
duration: 5000
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user