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:
@@ -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 上传 ====================
|
||||
|
||||
Reference in New Issue
Block a user