修复ROI截图功能:改用ZLM同步getSnap直接返回图片
- 重写snap端点:直接调用ZLM的getSnap同步API,传入RTSP源地址, 流式返回图片字节流,不再依赖异步文件存储 - 前端改为传srcUrl参数给snap接口,不需要先拉流即可截图 - 修复之前async=1导致ZLM返回JSON而非图片的问题 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,19 +10,18 @@ import com.github.pagehelper.PageInfo;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.ServletOutputStream;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.compress.utils.IOUtils;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@@ -92,40 +91,56 @@ public class AiRoiController {
|
||||
@Operation(summary = "获取摄像头截图")
|
||||
@GetMapping("/snap")
|
||||
public void getSnap(HttpServletResponse resp,
|
||||
@RequestParam String app,
|
||||
@RequestParam String stream) {
|
||||
@RequestParam String url) {
|
||||
MediaServer mediaServer = mediaServerService.getDefaultMediaServer();
|
||||
if (mediaServer == null) {
|
||||
log.warn("[AI截图] 无可用媒体服务器");
|
||||
resp.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
|
||||
return;
|
||||
}
|
||||
String snapDir = "snap" + File.separator + "ai";
|
||||
String fileName = app + "_" + stream + ".jpg";
|
||||
// 调用ZLM截图
|
||||
mediaServerService.getSnap(mediaServer, app, stream, 10, 1, snapDir, fileName);
|
||||
// 等待截图生成
|
||||
File snapFile = new File(snapDir + File.separator + fileName);
|
||||
int retries = 0;
|
||||
while (!snapFile.exists() && retries < 50) {
|
||||
try {
|
||||
Thread.sleep(200);
|
||||
} catch (InterruptedException ignored) {}
|
||||
retries++;
|
||||
}
|
||||
if (!snapFile.exists()) {
|
||||
resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
|
||||
// 直接调用ZLM的getSnap同步接口,传入RTSP源地址
|
||||
String zlmApi = String.format("http://%s:%s/index/api/getSnap",
|
||||
mediaServer.getIp(), mediaServer.getHttpPort());
|
||||
HttpUrl httpUrl = HttpUrl.parse(zlmApi);
|
||||
if (httpUrl == null) {
|
||||
resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
return;
|
||||
}
|
||||
HttpUrl requestUrl = httpUrl.newBuilder()
|
||||
.addQueryParameter("secret", mediaServer.getSecret())
|
||||
.addQueryParameter("url", url)
|
||||
.addQueryParameter("timeout_sec", "15")
|
||||
.addQueryParameter("expire_sec", "1")
|
||||
.build();
|
||||
|
||||
OkHttpClient client = new OkHttpClient.Builder()
|
||||
.connectTimeout(5, TimeUnit.SECONDS)
|
||||
.readTimeout(20, TimeUnit.SECONDS)
|
||||
.build();
|
||||
|
||||
try {
|
||||
InputStream in = Files.newInputStream(snapFile.toPath());
|
||||
Request request = new Request.Builder().url(requestUrl).build();
|
||||
Response response = client.newCall(request).execute();
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
String contentType = response.header("Content-Type", "");
|
||||
if (contentType.contains("image")) {
|
||||
resp.setContentType(MediaType.IMAGE_JPEG_VALUE);
|
||||
ServletOutputStream outputStream = resp.getOutputStream();
|
||||
IOUtils.copy(in, outputStream);
|
||||
in.close();
|
||||
outputStream.close();
|
||||
} catch (IOException e) {
|
||||
log.warn("[AI截图] 读取截图失败: {}", e.getMessage());
|
||||
byte[] bytes = response.body().bytes();
|
||||
resp.getOutputStream().write(bytes);
|
||||
resp.getOutputStream().flush();
|
||||
} else {
|
||||
// ZLM返回的是JSON错误信息,不是图片
|
||||
log.warn("[AI截图] ZLM未返回图片,可能流未在线: {}", response.body().string());
|
||||
resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
|
||||
}
|
||||
} else {
|
||||
log.warn("[AI截图] ZLM请求失败: {} {}", response.code(), response.message());
|
||||
resp.setStatus(HttpServletResponse.SC_BAD_GATEWAY);
|
||||
}
|
||||
response.close();
|
||||
} catch (Exception e) {
|
||||
log.warn("[AI截图] 截图异常: {}", e.getMessage());
|
||||
resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,6 @@ export function stopCamera(id) {
|
||||
})
|
||||
}
|
||||
|
||||
export function getSnapUrl(cameraId) {
|
||||
return `/api/ai/roi/snap?cameraId=${encodeURIComponent(cameraId)}`
|
||||
export function getSnapUrl(srcUrl) {
|
||||
return `/api/ai/roi/snap?url=${encodeURIComponent(srcUrl)}`
|
||||
}
|
||||
|
||||
@@ -113,10 +113,8 @@ export default {
|
||||
mounted() {
|
||||
this.cameraId = decodeURIComponent(this.$route.params.cameraId)
|
||||
this.srcUrl = this.$route.query.srcUrl || ''
|
||||
const app = this.$route.query.app || ''
|
||||
const stream = this.$route.query.stream || ''
|
||||
if (app && stream) {
|
||||
this.snapUrl = `/api/ai/roi/snap?app=${encodeURIComponent(app)}&stream=${encodeURIComponent(stream)}`
|
||||
if (this.srcUrl) {
|
||||
this.snapUrl = `/api/ai/roi/snap?url=${encodeURIComponent(this.srcUrl)}`
|
||||
}
|
||||
this.loadRois()
|
||||
},
|
||||
@@ -147,10 +145,8 @@ export default {
|
||||
this.drawMode = mode
|
||||
},
|
||||
refreshSnap() {
|
||||
const app = this.$route.query.app || ''
|
||||
const stream = this.$route.query.stream || ''
|
||||
if (app && stream) {
|
||||
this.snapUrl = `/api/ai/roi/snap?app=${encodeURIComponent(app)}&stream=${encodeURIComponent(stream)}&t=${Date.now()}`
|
||||
if (this.srcUrl) {
|
||||
this.snapUrl = `/api/ai/roi/snap?url=${encodeURIComponent(this.srcUrl)}&t=${Date.now()}`
|
||||
}
|
||||
},
|
||||
onRoiDrawn(data) {
|
||||
|
||||
Reference in New Issue
Block a user