diff --git a/backend/Log/OrbbecSDK.log.txt b/backend/Log/OrbbecSDK.log.txt index cfac73c7..afc10490 100644 Binary files a/backend/Log/OrbbecSDK.log.txt and b/backend/Log/OrbbecSDK.log.txt differ diff --git a/backend/build_app.py b/backend/build_app.py index e9ecd9a5..5780036d 100644 --- a/backend/build_app.py +++ b/backend/build_app.py @@ -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("检查并安装打包依赖...") @@ -390,4 +401,4 @@ def main(): input("按回车键退出...") if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/backend/config.ini b/backend/config.ini index f68e42aa..18761e2a 100644 --- a/backend/config.ini +++ b/backend/config.ini @@ -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 + diff --git a/backend/database.py b/backend/database.py index 828c822a..39b299fa 100644 --- a/backend/database.py +++ b/backend/database.py @@ -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 = [] @@ -790,6 +757,44 @@ class DatabaseManager: conn.rollback() 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]: """获取检测会话列表""" @@ -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)年月日时分秒""" diff --git a/backend/devices/screen_recorder.py b/backend/devices/screen_recorder.py index 0483a68c..d5b63cfa 100644 --- a/backend/devices/screen_recorder.py +++ b/backend/devices/screen_recorder.py @@ -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,47 +226,350 @@ class RecordingManager: if frame is None: return None + # 屏幕录制优先保证清晰度:不做降采样,避免字体和图标模糊 + 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}') @@ -460,6 +796,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(): @@ -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) diff --git a/backend/main.py b/backend/main.py index e544e087..3a3ebc39 100644 --- a/backend/main.py +++ b/backend/main.py @@ -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, None) + self.db_manager.delete_session_report_path(session_id) return jsonify({'success': True, 'message': '报告已删除'}) diff --git a/frontend/src/renderer/main/main.js b/frontend/src/renderer/main/main.js index d1de71c0..1668cd3d 100644 --- a/frontend/src/renderer/main/main.js +++ b/frontend/src/renderer/main/main.js @@ -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('窗口未找到');