修复ROI截图:改用ZLM内部RTSP地址,解决ffmpeg无法直连摄像头源的问题
后端改用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 <noreply@anthropic.com>
This commit is contained in:
@@ -91,14 +91,23 @@ public class AiRoiController {
|
|||||||
@Operation(summary = "获取摄像头截图")
|
@Operation(summary = "获取摄像头截图")
|
||||||
@GetMapping("/snap")
|
@GetMapping("/snap")
|
||||||
public void getSnap(HttpServletResponse resp,
|
public void getSnap(HttpServletResponse resp,
|
||||||
@RequestParam String url) {
|
@RequestParam String app,
|
||||||
|
@RequestParam String stream) {
|
||||||
MediaServer mediaServer = mediaServerService.getDefaultMediaServer();
|
MediaServer mediaServer = mediaServerService.getDefaultMediaServer();
|
||||||
if (mediaServer == null) {
|
if (mediaServer == null) {
|
||||||
log.warn("[AI截图] 无可用媒体服务器");
|
log.warn("[AI截图] 无可用媒体服务器");
|
||||||
resp.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
|
resp.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
|
||||||
return;
|
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",
|
String zlmApi = String.format("http://%s:%s/index/api/getSnap",
|
||||||
mediaServer.getIp(), mediaServer.getHttpPort());
|
mediaServer.getIp(), mediaServer.getHttpPort());
|
||||||
HttpUrl httpUrl = HttpUrl.parse(zlmApi);
|
HttpUrl httpUrl = HttpUrl.parse(zlmApi);
|
||||||
@@ -108,14 +117,14 @@ public class AiRoiController {
|
|||||||
}
|
}
|
||||||
HttpUrl requestUrl = httpUrl.newBuilder()
|
HttpUrl requestUrl = httpUrl.newBuilder()
|
||||||
.addQueryParameter("secret", mediaServer.getSecret())
|
.addQueryParameter("secret", mediaServer.getSecret())
|
||||||
.addQueryParameter("url", url)
|
.addQueryParameter("url", internalUrl)
|
||||||
.addQueryParameter("timeout_sec", "15")
|
.addQueryParameter("timeout_sec", "10")
|
||||||
.addQueryParameter("expire_sec", "1")
|
.addQueryParameter("expire_sec", "1")
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
OkHttpClient client = new OkHttpClient.Builder()
|
OkHttpClient client = new OkHttpClient.Builder()
|
||||||
.connectTimeout(5, TimeUnit.SECONDS)
|
.connectTimeout(5, TimeUnit.SECONDS)
|
||||||
.readTimeout(20, TimeUnit.SECONDS)
|
.readTimeout(15, TimeUnit.SECONDS)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -123,14 +132,14 @@ public class AiRoiController {
|
|||||||
Response response = client.newCall(request).execute();
|
Response response = client.newCall(request).execute();
|
||||||
if (response.isSuccessful() && response.body() != null) {
|
if (response.isSuccessful() && response.body() != null) {
|
||||||
String contentType = response.header("Content-Type", "");
|
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);
|
resp.setContentType(MediaType.IMAGE_JPEG_VALUE);
|
||||||
byte[] bytes = response.body().bytes();
|
|
||||||
resp.getOutputStream().write(bytes);
|
resp.getOutputStream().write(bytes);
|
||||||
resp.getOutputStream().flush();
|
resp.getOutputStream().flush();
|
||||||
} else {
|
} else {
|
||||||
// ZLM返回的是JSON错误信息,不是图片
|
log.warn("[AI截图] 截图返回默认logo或非图片,流可能未在线,size={}", bytes.length);
|
||||||
log.warn("[AI截图] ZLM未返回图片,可能流未在线: {}", response.body().string());
|
|
||||||
resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
|
resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export function stopCamera(id) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSnapUrl(srcUrl) {
|
export function getSnapUrl(app, stream) {
|
||||||
return `/api/ai/roi/snap?url=${encodeURIComponent(srcUrl)}`
|
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)}`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,6 +97,8 @@ export default {
|
|||||||
return {
|
return {
|
||||||
cameraId: '',
|
cameraId: '',
|
||||||
srcUrl: '',
|
srcUrl: '',
|
||||||
|
app: '',
|
||||||
|
stream: '',
|
||||||
drawMode: null,
|
drawMode: null,
|
||||||
roiList: [],
|
roiList: [],
|
||||||
selectedRoiId: null,
|
selectedRoiId: null,
|
||||||
@@ -113,10 +115,9 @@ export default {
|
|||||||
mounted() {
|
mounted() {
|
||||||
this.cameraId = decodeURIComponent(this.$route.params.cameraId)
|
this.cameraId = decodeURIComponent(this.$route.params.cameraId)
|
||||||
this.srcUrl = this.$route.query.srcUrl || ''
|
this.srcUrl = this.$route.query.srcUrl || ''
|
||||||
if (this.srcUrl) {
|
this.app = this.$route.query.app || ''
|
||||||
const base = process.env.NODE_ENV === 'development' ? process.env.VUE_APP_BASE_API : ''
|
this.stream = this.$route.query.stream || ''
|
||||||
this.snapUrl = `${base}/api/ai/roi/snap?url=${encodeURIComponent(this.srcUrl)}`
|
this.buildSnapUrl()
|
||||||
}
|
|
||||||
this.loadRois()
|
this.loadRois()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -145,12 +146,15 @@ export default {
|
|||||||
startDraw(mode) {
|
startDraw(mode) {
|
||||||
this.drawMode = mode
|
this.drawMode = mode
|
||||||
},
|
},
|
||||||
refreshSnap() {
|
buildSnapUrl() {
|
||||||
if (this.srcUrl) {
|
if (this.app && this.stream) {
|
||||||
const base = process.env.NODE_ENV === 'development' ? process.env.VUE_APP_BASE_API : ''
|
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) {
|
onRoiDrawn(data) {
|
||||||
this.drawMode = null
|
this.drawMode = null
|
||||||
const newRoi = {
|
const newRoi = {
|
||||||
|
|||||||
Reference in New Issue
Block a user