合并代码

This commit is contained in:
limengnan 2025-12-12 14:26:27 +08:00
commit 4ab00a7702
7 changed files with 529 additions and 89 deletions

Binary file not shown.

View File

@ -276,6 +276,17 @@ def copy_config_files():
else: else:
print(f"⚠️ 配置文件不存在: {config_file}") 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(): def install_build_dependencies():
"""安装打包依赖""" """安装打包依赖"""
print("检查并安装打包依赖...") print("检查并安装打包依赖...")

View File

@ -88,3 +88,21 @@ public_key = D:/BodyCheck/license/license_public_key.pem
grace_days = 7 grace_days = 7
dev_mode = False 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(sql, update_values)
cursor.execute('SELECT patient_id, creator_id, start_time, end_time, status FROM detection_sessions WHERE id = ?', (session_id,)) self._sync_patient_medical_history_from_session(cursor, 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))
conn.commit() conn.commit()
updated_info = [] updated_info = []
@ -791,6 +758,44 @@ class DatabaseManager:
logger.error(f'批量更新会话信息失败: {e}') logger.error(f'批量更新会话信息失败: {e}')
raise 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]: def get_detection_sessions(self, page: int = 1, size: int = 10, patient_id: str = None) -> List[Dict]:
"""获取检测会话列表""" """获取检测会话列表"""
conn = self.get_connection() conn = self.get_connection()
@ -940,15 +945,31 @@ class DatabaseManager:
cursor = conn.cursor() cursor = conn.cursor()
try: try:
cursor.execute( 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) (report_path, data_ids, session_id)
) )
self._sync_patient_medical_history_from_session(cursor, session_id)
conn.commit() conn.commit()
return cursor.rowcount > 0 return cursor.rowcount > 0
except Exception as e: except Exception as e:
logger.error(f'更新报告路径失败: {e}') logger.error(f'更新报告路径失败: {e}')
return False 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: def generate_detection_data_id(self) -> str:
"""生成检测数据记录唯一标识(YYYYMMDDHHMMSS)年月日时分秒""" """生成检测数据记录唯一标识(YYYYMMDDHHMMSS)年月日时分秒"""

View File

@ -18,6 +18,9 @@ import base64
from pathlib import Path from pathlib import Path
from typing import Optional, Dict, Any, List from typing import Optional, Dict, Any, List
import sys import sys
import subprocess
import signal
import queue
# 移除psutil导入不再需要性能监控 # 移除psutil导入不再需要性能监控
import gc import gc
@ -77,6 +80,21 @@ class RecordingManager:
self.camera1_recording_thread = None self.camera1_recording_thread = None
self.camera2_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.screen_fps = 25 # 屏幕录制帧率
self.camera1_fps = 20 # 相机1录制帧率 self.camera1_fps = 20 # 相机1录制帧率
@ -208,48 +226,351 @@ class RecordingManager:
if frame is None: if frame is None:
return 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 _, _, width, height = region
# 根据区域大小进行不同程度的降采样 if size_category == 'xlarge':
if size_category == 'xlarge': #screen录屏·超大区域 scale = 0.5
# 超大区域降采样到50%,极大减小文件大小
new_width = int(width * 1)
new_height = int(height * 1)
quality = 95 # 较高质量压缩
elif size_category == 'large': elif size_category == 'large':
# 大区域降采样到60%,显著优化文件大小 scale = 0.6
new_width = int(width * 1) elif size_category == 'medium':
new_height = int(height * 1) scale = 0.75
quality = 95 # 较高质量压缩 else:
elif size_category == 'medium': #足部视频录屏·中等区域 scale = 0.85
# 中等区域降采样到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)
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 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]: 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 return result
# 设置默认录制类型 # 设置默认录制类型
recording_types = ['screen', 'camera1',"camera2"] recording_types = ['screen']
# 验证录制区域参数(仅对启用的录制类型进行验证) # 验证录制区域参数(仅对启用的录制类型进行验证)
if 'screen' in recording_types: 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.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) 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: if 'screen' in recording_types:
self._set_adaptive_fps_by_region('screen', self.screen_region) self._set_adaptive_fps_by_region('screen', self.screen_region)
@ -371,10 +707,10 @@ class RecordingManager:
'session_id': session_id, 'session_id': session_id,
'status': 'recording', 'status': 'recording',
'video_paths': { 'video_paths': {
'camera1_video_path': os.path.join(db_base_path, 'camera1.mp4'), 'camera1_video_path': None,
'camera2_video_path': os.path.join(db_base_path, 'camera2.mp4'), 'camera2_video_path': None,
'screen_video_path': os.path.join(db_base_path, 'screen.mp4'), '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}') self.logger.debug(f'数据库更新信息已准备 - 会话ID: {session_id}')
@ -461,6 +797,20 @@ class RecordingManager:
self.recording_stop_event.clear() self.recording_stop_event.clear()
self.sync_recording = True 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(): if 'camera1' in recording_types and self.camera1_video_writer and self.camera1_video_writer.isOpened():
self.camera1_recording_thread = threading.Thread( self.camera1_recording_thread = threading.Thread(
@ -542,6 +892,26 @@ class RecordingManager:
result['message'] = '当前没有进行录制' result['message'] = '当前没有进行录制'
return result 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() recording_stop_time = time.time()
self.logger.info(f'开始停止录制,停止时间: {recording_stop_time}') self.logger.info(f'开始停止录制,停止时间: {recording_stop_time}')
@ -595,6 +965,8 @@ class RecordingManager:
# 清理视频写入器 # 清理视频写入器
self._cleanup_video_writers() self._cleanup_video_writers()
# 停止共享屏幕采集
self._stop_shared_screen_capture()
# 准备数据库更新信息,返回给调用方统一处理 # 准备数据库更新信息,返回给调用方统一处理
if self.current_session_id: if self.current_session_id:
@ -691,10 +1063,33 @@ class RecordingManager:
frame = None frame = None
# 获取帧数据 - 从屏幕截图生成 # 获取帧数据 - 从共享整屏帧进行区域裁剪
screenshot = pyautogui.screenshot(region=(x, y, w, h)) full_frame = self._get_latest_screen_frame()
frame = cv2.cvtColor(np.array(screenshot), cv2.COLOR_RGB2BGR) if full_frame is None:
frame = cv2.resize(frame, (w, h)) # 若共享帧尚未就绪,退化为单次全屏采集
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) 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 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': '报告已删除'}) return jsonify({'success': True, 'message': '报告已删除'})

View File

@ -7,13 +7,8 @@ const { spawn } = require('child_process');
let mainWindow; let mainWindow;
let localServer; let localServer;
let backendProcess; let backendProcess;
app.disableDomainBlockingFor3DAPIs();
// app.disableHardwareAcceleration(); // app.disableHardwareAcceleration();
app.commandLine.appendSwitch('ignore-gpu-blocklist'); app.disableDomainBlockingFor3DAPIs();
app.commandLine.appendSwitch('enable-webgl');
app.commandLine.appendSwitch('use-angle', 'd3d11');
ipcMain.handle('generate-report-pdf', async (event, payload) => { ipcMain.handle('generate-report-pdf', async (event, payload) => {
const win = BrowserWindow.fromWebContents(event.sender); const win = BrowserWindow.fromWebContents(event.sender);
if (!win) throw new Error('窗口未找到'); if (!win) throw new Error('窗口未找到');