更新了屏幕录制方法。

This commit is contained in:
root 2025-12-12 13:12:39 +08:00
parent b74d9129fe
commit fbe368ee20
9 changed files with 530 additions and 89 deletions

1
.gitignore vendored
View File

@ -99,3 +99,4 @@ frontend/src/renderer/build/
# Build Output
# ==========================
dist-electron-install/
backend/ffmpeg/

Binary file not shown.

Binary file not shown.

View File

@ -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()
main()

View File

@ -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
# 编码器libx264CPU或 h264_nvencGPUCPU占用更低需显卡支持
ffmpeg_codec = libx264
# 编码预设ultrafastCPU更低、文件更大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

View File

@ -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)年月日时分秒"""

View File

@ -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)

View File

@ -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': '报告已删除'})

View File

@ -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('窗口未找到');