From d9d58dfafaefce669482360cdc24f622174114d0 Mon Sep 17 00:00:00 2001 From: 16337 <1633794139@qq.com> Date: Mon, 2 Mar 2026 09:39:14 +0800 Subject: [PATCH] =?UTF-8?q?feat(edge):=20=E6=88=AA=E5=9B=BE=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E4=B8=B4=E6=97=B6=20RTSP=20=E8=BF=9E=E6=8E=A5?= =?UTF-8?q?=EF=BC=8C=E8=A7=A3=E5=86=B3=E6=97=A0=20ROI=20=E6=91=84=E5=83=8F?= =?UTF-8?q?=E5=A4=B4=E6=97=A0=E6=B3=95=E6=88=AA=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _capture_frame 增加 rtsp_url 参数,优先走已有流,无流时降级 临时连接 RTSP 抓帧(_capture_ondemand),用完即释放。 提取 _encode_jpeg 公共方法。 Co-Authored-By: Claude Opus 4.6 --- core/screenshot_handler.py | 76 ++++++++++++++++++++++++++------------ 1 file changed, 52 insertions(+), 24 deletions(-) diff --git a/core/screenshot_handler.py b/core/screenshot_handler.py index 238a435..92421ea 100644 --- a/core/screenshot_handler.py +++ b/core/screenshot_handler.py @@ -181,11 +181,13 @@ class ScreenshotHandler: }) return + rtsp_url = fields.get("rtsp_url", "") + logger.info("[截图] 收到截图请求: request_id=%s, camera=%s, callback=%s", request_id, camera_code, callback_url or "(无)") # 1. 抓帧 - jpeg_bytes = self._capture_frame(camera_code) + jpeg_bytes = self._capture_frame(camera_code, rtsp_url) if jpeg_bytes is None: self._send_result(callback_url, request_id, camera_code, { "status": "error", @@ -217,36 +219,62 @@ class ScreenshotHandler: # ==================== 抓帧 ==================== - def _capture_frame(self, camera_code: str) -> Optional[bytes]: - """从 MultiStreamManager 获取最新帧并编码为 JPEG""" + def _capture_frame(self, camera_code: str, rtsp_url: str = "") -> Optional[bytes]: + """从 MultiStreamManager 获取最新帧,无流时降级临时 RTSP 连接""" + # 优先从已有视频流获取(有 ROI 的摄像头,已连接) stream = self._stream_manager.get_stream(camera_code) - if stream is None: - logger.warning("[截图] 未找到摄像头流: %s", camera_code) - return None + if stream is not None and stream.is_connected: + frame = stream.get_latest_frame(timeout=2.0) + if frame is None: + frame = stream.read(timeout=2.0) + if frame is not None: + return self._encode_jpeg(frame.image) - if not stream.is_connected: - logger.warning("[截图] 摄像头未连接: %s", camera_code) - return None + # 降级:临时 RTSP 连接抓帧(无 ROI 或流未连接的摄像头) + if rtsp_url: + return self._capture_ondemand(camera_code, rtsp_url) - frame = stream.get_latest_frame(timeout=2.0) - if frame is None: - # 回退:直接从缓冲区读一帧 - frame = stream.read(timeout=2.0) + logger.warning("[截图] 未找到摄像头流且无 rtsp_url: %s", camera_code) + return None - if frame is None: - logger.warning("[截图] 获取帧超时: %s", camera_code) - return None - - # JPEG 编码 + def _capture_ondemand(self, camera_code: str, rtsp_url: str) -> Optional[bytes]: + """临时连接 RTSP 抓取一帧,用完即断""" + cap = None try: - encode_params = [cv2.IMWRITE_JPEG_QUALITY, 85] - success, buffer = cv2.imencode(".jpg", frame.image, encode_params) - if not success: - logger.error("[截图] JPEG 编码失败: %s", camera_code) + logger.info("[截图] 临时连接 RTSP: %s → %s", camera_code, rtsp_url) + cap = cv2.VideoCapture(rtsp_url, cv2.CAP_FFMPEG) + cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) + if not cap.isOpened(): + logger.warning("[截图] 临时 RTSP 连接失败: %s", camera_code) return None - return buffer.tobytes() + + # 读取几帧丢弃(跳过关键帧解码延迟),取最后一帧 + frame = None + for _ in range(5): + ret, f = cap.read() + if ret and f is not None: + frame = f + + if frame is None: + logger.warning("[截图] 临时连接读帧失败: %s", camera_code) + return None + + return self._encode_jpeg(frame) except Exception as e: - logger.error("[截图] 帧编码异常: %s", e) + logger.error("[截图] 临时截图异常: %s, %s", camera_code, e) + return None + finally: + if cap is not None: + cap.release() + logger.debug("[截图] 临时 RTSP 连接已释放: %s", camera_code) + + def _encode_jpeg(self, image) -> Optional[bytes]: + """将帧编码为 JPEG 字节""" + try: + success, buffer = cv2.imencode(".jpg", image, [cv2.IMWRITE_JPEG_QUALITY, 85]) + return buffer.tobytes() if success else None + except Exception as e: + logger.error("[截图] JPEG 编码失败: %s", e) return None # ==================== COS 上传 ====================