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 上传 ====================