更新了屏幕录制方法。
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
|
# Build Output
|
||||||
# ==========================
|
# ==========================
|
||||||
dist-electron-install/
|
dist-electron-install/
|
||||||
|
backend/ffmpeg/
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@ -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("检查并安装打包依赖...")
|
||||||
|
|||||||
@ -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
|
||||||
|
# 编码器: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(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)年月日时分秒"""
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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)
|
self.db_manager.delete_session_report_path(session_id)
|
||||||
|
|
||||||
return jsonify({'success': True, 'message': '报告已删除'})
|
return jsonify({'success': True, 'message': '报告已删除'})
|
||||||
|
|
||||||
|
|||||||
@ -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('窗口未找到');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user