更新了屏幕录制方法。
This commit is contained in:
parent
b74d9129fe
commit
fbe368ee20
1
.gitignore
vendored
1
.gitignore
vendored
@ -99,3 +99,4 @@ frontend/src/renderer/build/
|
||||
# Build Output
|
||||
# ==========================
|
||||
dist-electron-install/
|
||||
backend/ffmpeg/
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -276,6 +276,17 @@ def copy_config_files():
|
||||
else:
|
||||
print(f"⚠️ 配置文件不存在: {config_file}")
|
||||
|
||||
try:
|
||||
ffmpeg_src = 'ffmpeg'
|
||||
ffmpeg_dst = os.path.join(dist_dir, 'ffmpeg')
|
||||
if os.path.exists(ffmpeg_src):
|
||||
shutil.copytree(ffmpeg_src, ffmpeg_dst, dirs_exist_ok=True)
|
||||
print(f"✓ 已复制 ffmpeg 目录到 {ffmpeg_dst}")
|
||||
else:
|
||||
print("⚠️ 未找到 ffmpeg 源目录:ffmpeg")
|
||||
except Exception as e:
|
||||
print(f"⚠️ 复制 ffmpeg 目录失败: {e}")
|
||||
|
||||
def install_build_dependencies():
|
||||
"""安装打包依赖"""
|
||||
print("检查并安装打包依赖...")
|
||||
|
||||
@ -88,3 +88,21 @@ public_key = D:/BodyCheck/license/license_public_key.pem
|
||||
grace_days = 7
|
||||
dev_mode = False
|
||||
|
||||
[SCREEN_RECORDING]
|
||||
# 录屏策略:ffmpeg(外部进程录制)或 threaded(内部线程录制)
|
||||
strategy = ffmpeg
|
||||
# ffmpeg可执行文件绝对路径(Windows示例:D:/BodyCheck/ffmpeg/bin/ffmpeg.exe)
|
||||
ffmpeg_path = D:/BodyCheck/ffmpeg/bin/ffmpeg.exe
|
||||
# 编码器:libx264(CPU)或 h264_nvenc(GPU,CPU占用更低,需显卡支持)
|
||||
ffmpeg_codec = libx264
|
||||
# 编码预设:ultrafast(CPU更低、文件更大);NVENC用 p1/p2 更快
|
||||
ffmpeg_preset = ultrafast
|
||||
# 编码线程数:限制CPU占用,按机器性能调整
|
||||
ffmpeg_threads = 2
|
||||
# B帧数量:0可降低编码复杂度与CPU占用
|
||||
ffmpeg_bframes = 0
|
||||
# 关键帧间隔(GOP,单位:帧):数值越大CPU越低,seek精度下降
|
||||
ffmpeg_gop = 50
|
||||
# 是否录制鼠标:0关闭,1开启
|
||||
ffmpeg_draw_mouse = 0
|
||||
|
||||
|
||||
@ -738,40 +738,7 @@ class DatabaseManager:
|
||||
'''
|
||||
|
||||
cursor.execute(sql, update_values)
|
||||
cursor.execute('SELECT patient_id, creator_id, start_time, end_time, status FROM detection_sessions WHERE id = ?', (session_id,))
|
||||
srow = cursor.fetchone()
|
||||
if srow:
|
||||
patient_id = srow['patient_id']
|
||||
creator_id = srow['creator_id']
|
||||
session_status = srow['status']
|
||||
start_time = srow['start_time']
|
||||
end_time = srow['end_time']
|
||||
doctor_name = ''
|
||||
if creator_id:
|
||||
cursor.execute('SELECT name FROM users WHERE id = ?', (creator_id,))
|
||||
urow = cursor.fetchone()
|
||||
if urow:
|
||||
try:
|
||||
doctor_name = dict(urow).get('name') or ''
|
||||
except Exception:
|
||||
doctor_name = urow[0] if urow and len(urow) > 0 else ''
|
||||
check_time = end_time or start_time or self.get_china_time()
|
||||
mh_obj = {}
|
||||
try:
|
||||
cursor.execute('SELECT medical_history FROM patients WHERE id = ?', (patient_id,))
|
||||
prow = cursor.fetchone()
|
||||
if prow and prow[0]:
|
||||
try:
|
||||
mh_obj = json.loads(prow[0]) if isinstance(prow[0], str) else {}
|
||||
except Exception:
|
||||
mh_obj = {}
|
||||
except Exception:
|
||||
mh_obj = {}
|
||||
mh_obj['doctor'] = doctor_name
|
||||
mh_obj['status'] = session_status or ''
|
||||
mh_obj['lastcheck_time'] = str(check_time) if check_time is not None else self.get_china_time()
|
||||
now_str = self.get_china_time()
|
||||
cursor.execute('UPDATE patients SET medical_history = ?, updated_at = ? WHERE id = ?', (json.dumps(mh_obj, ensure_ascii=False), now_str, patient_id))
|
||||
self._sync_patient_medical_history_from_session(cursor, session_id)
|
||||
conn.commit()
|
||||
|
||||
updated_info = []
|
||||
@ -791,6 +758,44 @@ class DatabaseManager:
|
||||
logger.error(f'批量更新会话信息失败: {e}')
|
||||
raise
|
||||
|
||||
def _sync_patient_medical_history_from_session(self, cursor, session_id: str) -> None:
|
||||
"""根据会话信息同步患者 medical_history 的 doctor/status/lastcheck_time 字段"""
|
||||
cursor.execute('SELECT patient_id, creator_id, start_time, end_time, status FROM detection_sessions WHERE id = ?', (session_id,))
|
||||
srow = cursor.fetchone()
|
||||
if not srow:
|
||||
return
|
||||
patient_id = srow['patient_id']
|
||||
creator_id = srow['creator_id']
|
||||
session_status = srow['status']
|
||||
start_time = srow['start_time']
|
||||
end_time = srow['end_time']
|
||||
doctor_name = ''
|
||||
if creator_id:
|
||||
cursor.execute('SELECT name FROM users WHERE id = ?', (creator_id,))
|
||||
urow = cursor.fetchone()
|
||||
if urow:
|
||||
try:
|
||||
doctor_name = dict(urow).get('name') or ''
|
||||
except Exception:
|
||||
doctor_name = urow[0] if urow and len(urow) > 0 else ''
|
||||
check_time = end_time or start_time or self.get_china_time()
|
||||
mh_obj = {}
|
||||
try:
|
||||
cursor.execute('SELECT medical_history FROM patients WHERE id = ?', (patient_id,))
|
||||
prow = cursor.fetchone()
|
||||
if prow and prow[0]:
|
||||
try:
|
||||
mh_obj = json.loads(prow[0]) if isinstance(prow[0], str) else {}
|
||||
except Exception:
|
||||
mh_obj = {}
|
||||
except Exception:
|
||||
mh_obj = {}
|
||||
mh_obj['doctor'] = doctor_name
|
||||
mh_obj['status'] = session_status or ''
|
||||
mh_obj['lastcheck_time'] = str(check_time) if check_time is not None else self.get_china_time()
|
||||
now_str = self.get_china_time()
|
||||
cursor.execute('UPDATE patients SET medical_history = ?, updated_at = ? WHERE id = ?', (json.dumps(mh_obj, ensure_ascii=False), now_str, patient_id))
|
||||
|
||||
def get_detection_sessions(self, page: int = 1, size: int = 10, patient_id: str = None) -> List[Dict]:
|
||||
"""获取检测会话列表"""
|
||||
conn = self.get_connection()
|
||||
@ -940,15 +945,31 @@ class DatabaseManager:
|
||||
cursor = conn.cursor()
|
||||
try:
|
||||
cursor.execute(
|
||||
'UPDATE detection_sessions SET detection_report = ?, data_ids = ?, status = COALESCE(status, "reported") WHERE id = ?',
|
||||
'UPDATE detection_sessions SET detection_report = ?, data_ids = ?, status = "reported" WHERE id = ?',
|
||||
(report_path, data_ids, session_id)
|
||||
)
|
||||
self._sync_patient_medical_history_from_session(cursor, session_id)
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
except Exception as e:
|
||||
logger.error(f'更新报告路径失败: {e}')
|
||||
return False
|
||||
|
||||
def delete_session_report_path(self, session_id: str) -> bool:
|
||||
"""删除检测会话的报告路径并将状态置为 completed"""
|
||||
conn = self.get_connection()
|
||||
cursor = conn.cursor()
|
||||
try:
|
||||
cursor.execute(
|
||||
'UPDATE detection_sessions SET detection_report = ?,data_ids = ?, status = "completed" WHERE id = ?',
|
||||
(None, None, session_id)
|
||||
)
|
||||
self._sync_patient_medical_history_from_session(cursor, session_id)
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
except Exception as e:
|
||||
logger.error(f'更新报告路径失败: {e}')
|
||||
return False
|
||||
# ==================== 检测截图数据管理 ==================== #
|
||||
def generate_detection_data_id(self) -> str:
|
||||
"""生成检测数据记录唯一标识(YYYYMMDDHHMMSS)年月日时分秒"""
|
||||
|
||||
@ -18,6 +18,9 @@ import base64
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any, List
|
||||
import sys
|
||||
import subprocess
|
||||
import signal
|
||||
import queue
|
||||
# 移除psutil导入,不再需要性能监控
|
||||
import gc
|
||||
|
||||
@ -77,6 +80,21 @@ class RecordingManager:
|
||||
self.camera1_recording_thread = None
|
||||
self.camera2_recording_thread = None
|
||||
|
||||
# 共享屏幕采集资源
|
||||
self._shared_screen_thread = None
|
||||
self._screen_capture_stop_event = threading.Event()
|
||||
self._screen_frame_lock = threading.Lock()
|
||||
self._latest_screen_frame = None
|
||||
self._latest_screen_time = 0.0
|
||||
self._screen_frame_event = threading.Event()
|
||||
|
||||
self._ffmpeg_processes = {}
|
||||
self._ffmpeg_meta = {}
|
||||
|
||||
self._threaded_queues = {}
|
||||
self._threaded_threads = {}
|
||||
self._threaded_stop_events = {}
|
||||
|
||||
# 独立的录制参数配置
|
||||
self.screen_fps = 25 # 屏幕录制帧率
|
||||
self.camera1_fps = 20 # 相机1录制帧率
|
||||
@ -208,48 +226,351 @@ class RecordingManager:
|
||||
if frame is None:
|
||||
return None
|
||||
|
||||
size_category = self._calculate_region_size_category(region)
|
||||
# 屏幕录制优先保证清晰度:不做降采样,避免字体和图标模糊
|
||||
if recording_type == 'screen':
|
||||
return frame
|
||||
|
||||
# 对所有区域进行优化处理以减小文件大小
|
||||
size_category = self._calculate_region_size_category(region)
|
||||
_, _, width, height = region
|
||||
|
||||
# 根据区域大小进行不同程度的降采样
|
||||
if size_category == 'xlarge': #screen录屏·超大区域
|
||||
# 超大区域:降采样到50%,极大减小文件大小
|
||||
new_width = int(width * 1)
|
||||
new_height = int(height * 1)
|
||||
quality = 95 # 较高质量压缩
|
||||
if size_category == 'xlarge':
|
||||
scale = 0.5
|
||||
elif size_category == 'large':
|
||||
# 大区域:降采样到60%,显著优化文件大小
|
||||
new_width = int(width * 1)
|
||||
new_height = int(height * 1)
|
||||
quality = 95 # 较高质量压缩
|
||||
elif size_category == 'medium': #足部视频录屏·中等区域
|
||||
# 中等区域:降采样到75%,适度优化
|
||||
new_width = int(width * 1)
|
||||
new_height = int(height * 1)
|
||||
quality = 100 # 较高质量压缩
|
||||
else: # small
|
||||
# 小区域:降采样到85%,轻度优化
|
||||
new_width = int(width * 1)
|
||||
new_height = int(height * 1)
|
||||
quality = 100 # 较高质量压缩
|
||||
|
||||
# 应用降采样
|
||||
frame = cv2.resize(frame, (new_width, new_height), interpolation=cv2.INTER_AREA)
|
||||
self.logger.debug(f"{recording_type}区域降采样({size_category}): {width}x{height} -> {new_width}x{new_height}")
|
||||
|
||||
# 应用激进的JPEG压缩以进一步减小文件大小
|
||||
encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), quality]
|
||||
_, encoded_frame = cv2.imencode('.jpg', frame, encode_param)
|
||||
frame = cv2.imdecode(encoded_frame, cv2.IMREAD_COLOR)
|
||||
|
||||
# 重要:将帧尺寸调整回VideoWriter期望的原始尺寸
|
||||
# 这样可以保持压缩优化的同时确保与VideoWriter兼容
|
||||
frame = cv2.resize(frame, (width, height), interpolation=cv2.INTER_LINEAR)
|
||||
scale = 0.6
|
||||
elif size_category == 'medium':
|
||||
scale = 0.75
|
||||
else:
|
||||
scale = 0.85
|
||||
|
||||
if scale < 1.0:
|
||||
new_width = max(1, int(width * scale))
|
||||
new_height = max(1, int(height * scale))
|
||||
downsampled = cv2.resize(frame, (new_width, new_height), interpolation=cv2.INTER_AREA)
|
||||
self.logger.debug(f"{recording_type}区域降采样({size_category}): {width}x{height} -> {new_width}x{new_height}")
|
||||
frame = cv2.resize(downsampled, (width, height), interpolation=cv2.INTER_LINEAR)
|
||||
return frame
|
||||
|
||||
def _start_shared_screen_capture(self, fps: int):
|
||||
"""启动共享屏幕采集线程,单次采集整屏并供各录制线程区域裁剪使用"""
|
||||
# 如果线程已在运行,直接返回
|
||||
if self._shared_screen_thread and self._shared_screen_thread.is_alive():
|
||||
return
|
||||
|
||||
self._screen_capture_stop_event.clear()
|
||||
|
||||
def _capture_loop():
|
||||
interval = 1.0 / max(1, fps)
|
||||
while not self._screen_capture_stop_event.is_set() and self.sync_recording:
|
||||
start_t = time.time()
|
||||
try:
|
||||
screenshot = pyautogui.screenshot() # 全屏一次采集
|
||||
full_frame = cv2.cvtColor(np.array(screenshot), cv2.COLOR_RGB2BGR)
|
||||
with self._screen_frame_lock:
|
||||
self._latest_screen_frame = full_frame
|
||||
self._latest_screen_time = start_t
|
||||
# 通知有新帧
|
||||
self._screen_frame_event.set()
|
||||
except Exception as e:
|
||||
self.logger.error(f'共享屏幕采集错误: {e}')
|
||||
time.sleep(0.01)
|
||||
# 精确控制帧率
|
||||
elapsed = time.time() - start_t
|
||||
sleep_t = interval - elapsed
|
||||
if sleep_t > 0:
|
||||
time.sleep(sleep_t)
|
||||
|
||||
self._shared_screen_thread = threading.Thread(target=_capture_loop, daemon=True, name='SharedScreenCaptureThread')
|
||||
self._shared_screen_thread.start()
|
||||
|
||||
def _stop_shared_screen_capture(self):
|
||||
"""停止共享屏幕采集线程并清理资源"""
|
||||
self._screen_capture_stop_event.set()
|
||||
if self._shared_screen_thread and self._shared_screen_thread.is_alive():
|
||||
self._shared_screen_thread.join(timeout=2.0)
|
||||
self._shared_screen_thread = None
|
||||
with self._screen_frame_lock:
|
||||
self._latest_screen_frame = None
|
||||
self._latest_screen_time = 0.0
|
||||
self._screen_frame_event.clear()
|
||||
|
||||
def _get_latest_screen_frame(self):
|
||||
"""线程安全获取最新整屏帧"""
|
||||
with self._screen_frame_lock:
|
||||
return None if self._latest_screen_frame is None else self._latest_screen_frame.copy()
|
||||
|
||||
def _get_primary_screen_bounds(self) -> Dict[str, int]:
|
||||
try:
|
||||
import ctypes
|
||||
user32 = ctypes.windll.user32
|
||||
w = user32.GetSystemMetrics(0)
|
||||
h = user32.GetSystemMetrics(1)
|
||||
return {'x': 0, 'y': 0, 'width': int(w), 'height': int(h)}
|
||||
except Exception:
|
||||
sw, sh = self.screen_size
|
||||
return {'x': 0, 'y': 0, 'width': int(sw), 'height': int(sh)}
|
||||
|
||||
def _get_virtual_desktop_bounds(self) -> Dict[str, int]:
|
||||
try:
|
||||
import ctypes
|
||||
user32 = ctypes.windll.user32
|
||||
x = user32.GetSystemMetrics(76) # SM_XVIRTUALSCREEN
|
||||
y = user32.GetSystemMetrics(77) # SM_YVIRTUALSCREEN
|
||||
w = user32.GetSystemMetrics(78) # SM_CXVIRTUALSCREEN
|
||||
h = user32.GetSystemMetrics(79) # SM_CYVIRTUALSCREEN
|
||||
return {'x': int(x), 'y': int(y), 'width': int(w), 'height': int(h)}
|
||||
except Exception:
|
||||
# 回退:使用主屏尺寸,从(0,0)开始
|
||||
sw, sh = self.screen_size
|
||||
return {'x': 0, 'y': 0, 'width': int(sw), 'height': int(sh)}
|
||||
|
||||
def start_recording_ffmpeg(self, session_id: str, patient_id: str, screen_location: List[int], fps: int = None) -> Dict[str, Any]:
|
||||
result = {'success': False, 'message': ''}
|
||||
try:
|
||||
x, y, w, h = screen_location
|
||||
bounds = self._get_primary_screen_bounds()
|
||||
x_clamped = max(bounds['x'], min(int(x), bounds['x'] + bounds['width'] - 1))
|
||||
y_clamped = max(bounds['y'], min(int(y), bounds['y'] + bounds['height'] - 1))
|
||||
max_w = (bounds['x'] + bounds['width']) - x_clamped
|
||||
max_h = (bounds['y'] + bounds['height']) - y_clamped
|
||||
w_clamped = max(1, min(int(w), int(max_w)))
|
||||
h_clamped = max(1, min(int(h), int(max_h)))
|
||||
off_x = x_clamped - bounds['x']
|
||||
off_y = y_clamped - bounds['y']
|
||||
|
||||
file_dir = self.config_manager.get_config_value('FILEPATH', 'path')
|
||||
timestamp = datetime.now().strftime('%H%M%S%f')[:-3]
|
||||
base_path = os.path.join(file_dir, patient_id, session_id, f'video_{timestamp}')
|
||||
os.makedirs(base_path, exist_ok=True)
|
||||
screen_video_path = os.path.join(base_path, 'screen.mp4')
|
||||
target_fps = fps or self.screen_fps
|
||||
ffmpeg_path = None
|
||||
if self.config_manager:
|
||||
ffmpeg_path = (
|
||||
self.config_manager.get_config_value('SCREEN_RECORDING', 'ffmpeg_path', fallback=None) or
|
||||
self.config_manager.get_config_value('RECORDING', 'ffmpeg_path', fallback=None)
|
||||
)
|
||||
if not ffmpeg_path or not os.path.isfile(str(ffmpeg_path)):
|
||||
base_dir = os.path.dirname(sys.executable) if getattr(sys, 'frozen', False) else os.path.dirname(os.path.abspath(__file__))
|
||||
alt_path = os.path.join(base_dir, 'ffmpeg', 'bin', 'ffmpeg.exe')
|
||||
if os.path.isfile(alt_path):
|
||||
ffmpeg_path = alt_path
|
||||
else:
|
||||
result['message'] = '未配置有效的ffmpeg_path,请在配置中设置 SCREEN_RECORDING.ffmpeg_path 或 RECORDING.ffmpeg_path'
|
||||
return result
|
||||
|
||||
cmd = [
|
||||
str(ffmpeg_path),
|
||||
'-y',
|
||||
'-f', 'gdigrab',
|
||||
'-framerate', str(target_fps),
|
||||
'-draw_mouse', str(int((self.config_manager.get_config_value('SCREEN_RECORDING', 'ffmpeg_draw_mouse', fallback='0') or '0'))),
|
||||
'-offset_x', str(off_x),
|
||||
'-offset_y', str(off_y),
|
||||
'-video_size', f'{w_clamped}x{h_clamped}',
|
||||
'-i', 'desktop',
|
||||
]
|
||||
|
||||
codec = (self.config_manager.get_config_value('SCREEN_RECORDING', 'ffmpeg_codec', fallback=None) or
|
||||
self.config_manager.get_config_value('RECORDING', 'ffmpeg_codec', fallback=None) or
|
||||
'libx264')
|
||||
preset = (self.config_manager.get_config_value('SCREEN_RECORDING', 'ffmpeg_preset', fallback=None) or
|
||||
self.config_manager.get_config_value('RECORDING', 'ffmpeg_preset', fallback=None) or
|
||||
('p1' if codec == 'h264_nvenc' else 'ultrafast'))
|
||||
threads = int((self.config_manager.get_config_value('SCREEN_RECORDING', 'ffmpeg_threads', fallback='2') or '2'))
|
||||
bframes = int((self.config_manager.get_config_value('SCREEN_RECORDING', 'ffmpeg_bframes', fallback='0') or '0'))
|
||||
gop = int((self.config_manager.get_config_value('SCREEN_RECORDING', 'ffmpeg_gop', fallback=str(max(1, int(target_fps*2)))) or str(max(1, int(target_fps*2)))))
|
||||
|
||||
cmd += ['-c:v', codec]
|
||||
# 统一的低CPU选项
|
||||
cmd += ['-preset', str(preset)]
|
||||
cmd += ['-bf', str(bframes)]
|
||||
cmd += ['-g', str(gop)]
|
||||
cmd += ['-pix_fmt', 'yuv420p']
|
||||
cmd += ['-threads', str(threads)]
|
||||
cmd += ['-r', str(target_fps)]
|
||||
cmd += [screen_video_path]
|
||||
proc = subprocess.Popen(
|
||||
cmd,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
creationflags=getattr(subprocess, 'CREATE_NEW_PROCESS_GROUP', 0)
|
||||
)
|
||||
self._ffmpeg_processes['screen'] = proc
|
||||
self._ffmpeg_meta['screen'] = {'base_path': base_path, 'patient_id': patient_id, 'session_id': session_id, 'video_path': screen_video_path}
|
||||
result['success'] = True
|
||||
result['message'] = 'ffmpeg录制已启动'
|
||||
result['database_updates'] = {
|
||||
'session_id': session_id,
|
||||
'status': 'recording',
|
||||
'video_paths': {
|
||||
'screen_video_path': os.path.relpath(screen_video_path, file_dir)
|
||||
}
|
||||
}
|
||||
return result
|
||||
except Exception as e:
|
||||
result['message'] = f'ffmpeg启动失败: {e}'
|
||||
return result
|
||||
|
||||
def stop_recording_ffmpeg(self, session_id: str = None) -> Dict[str, Any]:
|
||||
result = {'success': False, 'message': ''}
|
||||
try:
|
||||
proc = self._ffmpeg_processes.get('screen')
|
||||
meta = self._ffmpeg_meta.get('screen')
|
||||
if proc:
|
||||
try:
|
||||
if proc.stdin and proc.poll() is None:
|
||||
try:
|
||||
proc.communicate(input=b'q', timeout=2.0)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
proc.send_signal(getattr(signal, 'CTRL_BREAK_EVENT', signal.SIGTERM))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
proc.terminate()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
proc.wait(timeout=3.0)
|
||||
except Exception:
|
||||
try:
|
||||
proc.kill()
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
self._ffmpeg_processes.pop('screen', None)
|
||||
result['success'] = True
|
||||
result['message'] = 'ffmpeg录制已停止'
|
||||
if meta:
|
||||
result['database_updates'] = {
|
||||
'session_id': meta.get('session_id'),
|
||||
'status': 'recorded'
|
||||
}
|
||||
return result
|
||||
except Exception as e:
|
||||
result['message'] = f'ffmpeg停止失败: {e}'
|
||||
return result
|
||||
|
||||
def start_recording_threaded(self, session_id: str, patient_id: str, screen_location: List[int], fps: int = None) -> Dict[str, Any]:
|
||||
result = {'success': False, 'message': ''}
|
||||
try:
|
||||
x, y, w, h = screen_location
|
||||
file_dir = self.config_manager.get_config_value('FILEPATH', 'path')
|
||||
timestamp = datetime.now().strftime('%H%M%S%f')[:-3]
|
||||
base_path = os.path.join(file_dir, patient_id, session_id, f'video_{timestamp}')
|
||||
os.makedirs(base_path, exist_ok=True)
|
||||
screen_video_path = os.path.join(base_path, 'screen.mp4')
|
||||
target_fps = fps or self.screen_fps
|
||||
try:
|
||||
fourcc = cv2.VideoWriter_fourcc(*'avc1')
|
||||
except Exception:
|
||||
try:
|
||||
fourcc = cv2.VideoWriter_fourcc(*'H264')
|
||||
except Exception:
|
||||
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
|
||||
vw = cv2.VideoWriter(screen_video_path, fourcc, target_fps, (w, h))
|
||||
if not vw or not vw.isOpened():
|
||||
result['message'] = 'VideoWriter初始化失败'
|
||||
return result
|
||||
q = queue.Queue(maxsize=target_fps * 2)
|
||||
stop_event = threading.Event()
|
||||
self._threaded_queues['screen'] = q
|
||||
self._threaded_stop_events['screen'] = stop_event
|
||||
|
||||
def _capture():
|
||||
self._start_shared_screen_capture(target_fps)
|
||||
while not stop_event.is_set():
|
||||
full = self._get_latest_screen_frame()
|
||||
if full is None:
|
||||
time.sleep(0.001)
|
||||
continue
|
||||
H, W = full.shape[:2]
|
||||
x0 = max(0, min(x, W - 1))
|
||||
y0 = max(0, min(y, H - 1))
|
||||
x1 = max(0, min(x0 + w, W))
|
||||
y1 = max(0, min(y0 + h, H))
|
||||
if x1 > x0 and y1 > y0:
|
||||
crop = full[y0:y1, x0:x1]
|
||||
if crop.shape[1] != w or crop.shape[0] != h:
|
||||
frame = cv2.resize(crop, (w, h), interpolation=cv2.INTER_AREA)
|
||||
else:
|
||||
frame = crop
|
||||
try:
|
||||
q.put_nowait(frame)
|
||||
except Exception:
|
||||
try:
|
||||
_ = q.get_nowait()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
q.put_nowait(frame)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
time.sleep(0.002)
|
||||
|
||||
def _writer():
|
||||
last = time.time()
|
||||
interval = 1.0 / max(1, target_fps)
|
||||
while not stop_event.is_set():
|
||||
try:
|
||||
frame = q.get(timeout=0.05)
|
||||
except Exception:
|
||||
frame = None
|
||||
now = time.time()
|
||||
if frame is not None:
|
||||
vw.write(frame)
|
||||
else:
|
||||
if now - last < interval:
|
||||
time.sleep(interval - (now - last))
|
||||
last = now
|
||||
try:
|
||||
vw.release()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
t1 = threading.Thread(target=_capture, daemon=True, name='ThreadedScreenCapture')
|
||||
t2 = threading.Thread(target=_writer, daemon=True, name='ThreadedScreenWriter')
|
||||
self._threaded_threads['screen'] = (t1, t2)
|
||||
t1.start(); t2.start()
|
||||
result['success'] = True
|
||||
result['message'] = '线程录制已启动'
|
||||
result['database_updates'] = {
|
||||
'session_id': session_id,
|
||||
'status': 'recording',
|
||||
'video_paths': {
|
||||
'screen_video_path': os.path.relpath(screen_video_path, file_dir)
|
||||
}
|
||||
}
|
||||
return result
|
||||
except Exception as e:
|
||||
result['message'] = f'线程录制启动失败: {e}'
|
||||
return result
|
||||
|
||||
def stop_recording_threaded(self, session_id: str = None) -> Dict[str, Any]:
|
||||
result = {'success': False, 'message': ''}
|
||||
try:
|
||||
stop_event = self._threaded_stop_events.get('screen')
|
||||
threads = self._threaded_threads.get('screen')
|
||||
if stop_event:
|
||||
stop_event.set()
|
||||
if threads:
|
||||
for t in threads:
|
||||
try:
|
||||
t.join(timeout=2.0)
|
||||
except Exception:
|
||||
pass
|
||||
self._threaded_threads.pop('screen', None)
|
||||
self._stop_shared_screen_capture()
|
||||
result['success'] = True
|
||||
result['message'] = '线程录制已停止'
|
||||
return result
|
||||
except Exception as e:
|
||||
result['message'] = f'线程录制停止失败: {e}'
|
||||
return result
|
||||
|
||||
def start_recording(self, session_id: str, patient_id: str, screen_location: List[int], camera1_location: List[int], camera2_location: List[int], femtobolt_location: List[int], recording_types: List[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
启动同步录制
|
||||
@ -288,7 +609,7 @@ class RecordingManager:
|
||||
return result
|
||||
|
||||
# 设置默认录制类型
|
||||
recording_types = ['screen', 'camera1',"camera2"]
|
||||
recording_types = ['screen']
|
||||
|
||||
# 验证录制区域参数(仅对启用的录制类型进行验证)
|
||||
if 'screen' in recording_types:
|
||||
@ -324,6 +645,21 @@ class RecordingManager:
|
||||
self.camera2_region = tuple(camera2_location) # [x, y, w, h] -> (x, y, w, h)
|
||||
self.femtobolt_region = tuple(femtobolt_location) # [x, y, w, h] -> (x, y, w, h)
|
||||
|
||||
strategy = None
|
||||
if self.config_manager:
|
||||
strategy = (
|
||||
self.config_manager.get_config_value('SCREEN_RECORDING', 'strategy', fallback=None) or
|
||||
self.config_manager.get_config_value('RECORDING', 'screen_strategy', fallback=None)
|
||||
)
|
||||
if strategy:
|
||||
strategy = str(strategy).lower()
|
||||
if strategy in ['ffmpeg', 'threaded']:
|
||||
self.sync_recording = True
|
||||
if strategy == 'ffmpeg':
|
||||
return self.start_recording_ffmpeg(session_id, patient_id, screen_location, fps=self.screen_fps)
|
||||
else:
|
||||
return self.start_recording_threaded(session_id, patient_id, screen_location, fps=self.screen_fps)
|
||||
|
||||
# 根据录制区域大小设置自适应帧率
|
||||
if 'screen' in recording_types:
|
||||
self._set_adaptive_fps_by_region('screen', self.screen_region)
|
||||
@ -371,10 +707,10 @@ class RecordingManager:
|
||||
'session_id': session_id,
|
||||
'status': 'recording',
|
||||
'video_paths': {
|
||||
'camera1_video_path': os.path.join(db_base_path, 'camera1.mp4'),
|
||||
'camera2_video_path': os.path.join(db_base_path, 'camera2.mp4'),
|
||||
'camera1_video_path': None,
|
||||
'camera2_video_path': None,
|
||||
'screen_video_path': os.path.join(db_base_path, 'screen.mp4'),
|
||||
'femtobolt_video_path': os.path.join(db_base_path, 'femtobolt.mp4')
|
||||
'femtobolt_video_path': None
|
||||
}
|
||||
}
|
||||
self.logger.debug(f'数据库更新信息已准备 - 会话ID: {session_id}')
|
||||
@ -461,6 +797,20 @@ class RecordingManager:
|
||||
self.recording_stop_event.clear()
|
||||
self.sync_recording = True
|
||||
|
||||
# 启动共享屏幕采集(取所需类型的最大帧率)
|
||||
max_needed_fps = 0
|
||||
for t in recording_types:
|
||||
if t == 'screen':
|
||||
max_needed_fps = max(max_needed_fps, self.screen_current_fps)
|
||||
elif t == 'camera1':
|
||||
max_needed_fps = max(max_needed_fps, self.camera1_current_fps)
|
||||
elif t == 'camera2':
|
||||
max_needed_fps = max(max_needed_fps, self.camera2_current_fps)
|
||||
elif t == 'femtobolt':
|
||||
max_needed_fps = max(max_needed_fps, self.femtobolt_current_fps)
|
||||
if max_needed_fps > 0:
|
||||
self._start_shared_screen_capture(max_needed_fps)
|
||||
|
||||
# 根据录制类型启动对应的录制线程
|
||||
if 'camera1' in recording_types and self.camera1_video_writer and self.camera1_video_writer.isOpened():
|
||||
self.camera1_recording_thread = threading.Thread(
|
||||
@ -542,6 +892,26 @@ class RecordingManager:
|
||||
result['message'] = '当前没有进行录制'
|
||||
return result
|
||||
|
||||
strategy = None
|
||||
if self.config_manager:
|
||||
strategy = (
|
||||
self.config_manager.get_config_value('SCREEN_RECORDING', 'strategy', fallback=None) or
|
||||
self.config_manager.get_config_value('RECORDING', 'screen_strategy', fallback=None)
|
||||
)
|
||||
if strategy:
|
||||
strategy = str(strategy).lower()
|
||||
if strategy in ['ffmpeg', 'threaded']:
|
||||
self.sync_recording = False
|
||||
self.recording_stop_event.set()
|
||||
if strategy == 'ffmpeg':
|
||||
res = self.stop_recording_ffmpeg(session_id)
|
||||
else:
|
||||
res = self.stop_recording_threaded(session_id)
|
||||
self.current_session_id = None
|
||||
self.current_patient_id = None
|
||||
self.recording_start_time = None
|
||||
return res
|
||||
|
||||
# 记录停止时间,确保所有录制线程同时结束
|
||||
recording_stop_time = time.time()
|
||||
self.logger.info(f'开始停止录制,停止时间: {recording_stop_time}')
|
||||
@ -595,6 +965,8 @@ class RecordingManager:
|
||||
|
||||
# 清理视频写入器
|
||||
self._cleanup_video_writers()
|
||||
# 停止共享屏幕采集
|
||||
self._stop_shared_screen_capture()
|
||||
|
||||
# 准备数据库更新信息,返回给调用方统一处理
|
||||
if self.current_session_id:
|
||||
@ -691,10 +1063,33 @@ class RecordingManager:
|
||||
|
||||
frame = None
|
||||
|
||||
# 获取帧数据 - 从屏幕截图生成
|
||||
screenshot = pyautogui.screenshot(region=(x, y, w, h))
|
||||
frame = cv2.cvtColor(np.array(screenshot), cv2.COLOR_RGB2BGR)
|
||||
frame = cv2.resize(frame, (w, h))
|
||||
# 获取帧数据 - 从共享整屏帧进行区域裁剪
|
||||
full_frame = self._get_latest_screen_frame()
|
||||
if full_frame is None:
|
||||
# 若共享帧尚未就绪,退化为单次全屏采集
|
||||
try:
|
||||
screenshot = pyautogui.screenshot()
|
||||
full_frame = cv2.cvtColor(np.array(screenshot), cv2.COLOR_RGB2BGR)
|
||||
except Exception as e:
|
||||
self.logger.error(f'{recording_type}备用屏幕采集失败: {e}')
|
||||
full_frame = None
|
||||
|
||||
if full_frame is not None:
|
||||
H, W = full_frame.shape[:2]
|
||||
x0 = max(0, min(x, W-1))
|
||||
y0 = max(0, min(y, H-1))
|
||||
x1 = max(0, min(x0 + w, W))
|
||||
y1 = max(0, min(y0 + h, H))
|
||||
if x1 > x0 and y1 > y0:
|
||||
crop = full_frame[y0:y1, x0:x1]
|
||||
if (x1 - x0) != w or (y1 - y0) != h:
|
||||
frame = cv2.resize(crop, (w, h), interpolation=cv2.INTER_AREA)
|
||||
else:
|
||||
frame = crop
|
||||
else:
|
||||
frame = None
|
||||
else:
|
||||
frame = None
|
||||
|
||||
# 对所有区域录制进行优化以减小文件大小
|
||||
frame = self._optimize_frame_for_large_region(frame, region, recording_type)
|
||||
|
||||
@ -1525,7 +1525,7 @@ class AppServer:
|
||||
return jsonify({'success': False, 'error': f'删除文件失败: {str(e)}'}), 500
|
||||
|
||||
# 更新数据库
|
||||
self.db_manager.update_session_report_path(session_id, None)
|
||||
self.db_manager.delete_session_report_path(session_id)
|
||||
|
||||
return jsonify({'success': True, 'message': '报告已删除'})
|
||||
|
||||
|
||||
@ -7,13 +7,8 @@ const { spawn } = require('child_process');
|
||||
let mainWindow;
|
||||
let localServer;
|
||||
let backendProcess;
|
||||
|
||||
app.disableDomainBlockingFor3DAPIs();
|
||||
// app.disableHardwareAcceleration();
|
||||
app.commandLine.appendSwitch('ignore-gpu-blocklist');
|
||||
app.commandLine.appendSwitch('enable-webgl');
|
||||
app.commandLine.appendSwitch('use-angle', 'd3d11');
|
||||
|
||||
app.disableDomainBlockingFor3DAPIs();
|
||||
ipcMain.handle('generate-report-pdf', async (event, payload) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender);
|
||||
if (!win) throw new Error('窗口未找到');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user