feat(edge): 截图支持临时 RTSP 连接,解决无 ROI 摄像头无法截图

_capture_frame 增加 rtsp_url 参数,优先走已有流,无流时降级
临时连接 RTSP 抓帧(_capture_ondemand),用完即释放。
提取 _encode_jpeg 公共方法。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-02 09:39:14 +08:00
parent 3ca6c93479
commit d9d58dfafa

View File

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