From 88da798cc018a86ad4eb7e6c9edd9648dbf1c425 Mon Sep 17 00:00:00 2001 From: 16337 <1633794139@qq.com> Date: Wed, 4 Feb 2026 09:59:00 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8DROI=E6=88=AA=E5=9B=BE?= =?UTF-8?q?=EF=BC=9A=E6=94=B9=E7=94=A8ZLM=E5=86=85=E9=83=A8RTSP=E5=9C=B0?= =?UTF-8?q?=E5=9D=80=EF=BC=8C=E8=A7=A3=E5=86=B3ffmpeg=E6=97=A0=E6=B3=95?= =?UTF-8?q?=E7=9B=B4=E8=BF=9E=E6=91=84=E5=83=8F=E5=A4=B4=E6=BA=90=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端改用app/stream参数构建ZLM内部地址(rtsp://127.0.0.1:{port}/{app}/{stream}), 避免ffmpeg从Docker内直接连接摄像头RTSP源。增加47255字节默认logo检测。 前端同步更新snap URL构建方式。 Co-Authored-By: Claude Opus 4.5 --- .../vmp/aiot/controller/AiRoiController.java | 27 ++++++++++++------- web/src/api/cameraConfig.js | 5 ++-- web/src/views/cameraConfig/roiConfig.vue | 18 ++++++++----- 3 files changed, 32 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/genersoft/iot/vmp/aiot/controller/AiRoiController.java b/src/main/java/com/genersoft/iot/vmp/aiot/controller/AiRoiController.java index 43cc48997..6cb023357 100644 --- a/src/main/java/com/genersoft/iot/vmp/aiot/controller/AiRoiController.java +++ b/src/main/java/com/genersoft/iot/vmp/aiot/controller/AiRoiController.java @@ -91,14 +91,23 @@ public class AiRoiController { @Operation(summary = "获取摄像头截图") @GetMapping("/snap") public void getSnap(HttpServletResponse resp, - @RequestParam String url) { + @RequestParam String app, + @RequestParam String stream) { MediaServer mediaServer = mediaServerService.getDefaultMediaServer(); if (mediaServer == null) { log.warn("[AI截图] 无可用媒体服务器"); resp.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE); return; } - // 直接调用ZLM的getSnap同步接口,传入RTSP源地址 + // 使用ZLM内部RTSP流地址截图(流必须在线) + String internalUrl; + if (mediaServer.getRtspPort() != 0) { + internalUrl = String.format("rtsp://127.0.0.1:%s/%s/%s", mediaServer.getRtspPort(), app, stream); + } else { + internalUrl = String.format("http://127.0.0.1:%s/%s/%s.live.mp4", mediaServer.getHttpPort(), app, stream); + } + log.info("[AI截图] app={}, stream={}, 内部地址={}", app, stream, internalUrl); + String zlmApi = String.format("http://%s:%s/index/api/getSnap", mediaServer.getIp(), mediaServer.getHttpPort()); HttpUrl httpUrl = HttpUrl.parse(zlmApi); @@ -108,14 +117,14 @@ public class AiRoiController { } HttpUrl requestUrl = httpUrl.newBuilder() .addQueryParameter("secret", mediaServer.getSecret()) - .addQueryParameter("url", url) - .addQueryParameter("timeout_sec", "15") + .addQueryParameter("url", internalUrl) + .addQueryParameter("timeout_sec", "10") .addQueryParameter("expire_sec", "1") .build(); OkHttpClient client = new OkHttpClient.Builder() .connectTimeout(5, TimeUnit.SECONDS) - .readTimeout(20, TimeUnit.SECONDS) + .readTimeout(15, TimeUnit.SECONDS) .build(); try { @@ -123,14 +132,14 @@ public class AiRoiController { Response response = client.newCall(request).execute(); if (response.isSuccessful() && response.body() != null) { String contentType = response.header("Content-Type", ""); - if (contentType.contains("image")) { + byte[] bytes = response.body().bytes(); + // ZLM默认logo是47255字节,跳过它 + if (contentType.contains("image") && bytes.length != 47255) { resp.setContentType(MediaType.IMAGE_JPEG_VALUE); - byte[] bytes = response.body().bytes(); resp.getOutputStream().write(bytes); resp.getOutputStream().flush(); } else { - // ZLM返回的是JSON错误信息,不是图片 - log.warn("[AI截图] ZLM未返回图片,可能流未在线: {}", response.body().string()); + log.warn("[AI截图] 截图返回默认logo或非图片,流可能未在线,size={}", bytes.length); resp.setStatus(HttpServletResponse.SC_NO_CONTENT); } } else { diff --git a/web/src/api/cameraConfig.js b/web/src/api/cameraConfig.js index cf1021304..99ab373d9 100644 --- a/web/src/api/cameraConfig.js +++ b/web/src/api/cameraConfig.js @@ -25,6 +25,7 @@ export function stopCamera(id) { }) } -export function getSnapUrl(srcUrl) { - return `/api/ai/roi/snap?url=${encodeURIComponent(srcUrl)}` +export function getSnapUrl(app, stream) { + const base = process.env.NODE_ENV === 'development' ? process.env.VUE_APP_BASE_API : '' + return `${base}/api/ai/roi/snap?app=${encodeURIComponent(app)}&stream=${encodeURIComponent(stream)}` } diff --git a/web/src/views/cameraConfig/roiConfig.vue b/web/src/views/cameraConfig/roiConfig.vue index 0dab22cfc..c4b978345 100644 --- a/web/src/views/cameraConfig/roiConfig.vue +++ b/web/src/views/cameraConfig/roiConfig.vue @@ -97,6 +97,8 @@ export default { return { cameraId: '', srcUrl: '', + app: '', + stream: '', drawMode: null, roiList: [], selectedRoiId: null, @@ -113,10 +115,9 @@ export default { mounted() { this.cameraId = decodeURIComponent(this.$route.params.cameraId) this.srcUrl = this.$route.query.srcUrl || '' - if (this.srcUrl) { - const base = process.env.NODE_ENV === 'development' ? process.env.VUE_APP_BASE_API : '' - this.snapUrl = `${base}/api/ai/roi/snap?url=${encodeURIComponent(this.srcUrl)}` - } + this.app = this.$route.query.app || '' + this.stream = this.$route.query.stream || '' + this.buildSnapUrl() this.loadRois() }, methods: { @@ -145,12 +146,15 @@ export default { startDraw(mode) { this.drawMode = mode }, - refreshSnap() { - if (this.srcUrl) { + buildSnapUrl() { + if (this.app && this.stream) { const base = process.env.NODE_ENV === 'development' ? process.env.VUE_APP_BASE_API : '' - this.snapUrl = `${base}/api/ai/roi/snap?url=${encodeURIComponent(this.srcUrl)}&t=${Date.now()}` + this.snapUrl = `${base}/api/ai/roi/snap?app=${encodeURIComponent(this.app)}&stream=${encodeURIComponent(this.stream)}&t=${Date.now()}` } }, + refreshSnap() { + this.buildSnapUrl() + }, onRoiDrawn(data) { this.drawMode = null const newRoi = {