From d3a4f4d7a3d30e9e6abd74e6193d14d406d13e93 Mon Sep 17 00:00:00 2001 From: wangguangyuan Date: Thu, 24 Sep 2026 09:18:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=94=9F=E6=88=90=E7=9B=B8=E6=9C=BA?= =?UTF-8?q?=E7=9B=B4=E8=BF=9E=20RTSP=20=E5=9C=B0=E5=9D=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nvr/controller/IvsServerController.java | 12 +++ .../nvr/dahua/DahuaIvsVendorService.java | 85 +++++++++++++++++++ .../controller/HikNvrUiActionController.java | 10 +++ .../nvr/hik/service/HikIvsVendorService.java | 54 ++++++++++++ .../nvr/ivs/service/IvsOpenApiService.java | 16 ++++ .../nvr/ivs/service/IvsVendorAdapter.java | 11 +++ 6 files changed, 188 insertions(+) diff --git a/src/main/java/com/inspect/nvr/controller/IvsServerController.java b/src/main/java/com/inspect/nvr/controller/IvsServerController.java index 5534377a..703b4a4b 100644 --- a/src/main/java/com/inspect/nvr/controller/IvsServerController.java +++ b/src/main/java/com/inspect/nvr/controller/IvsServerController.java @@ -145,6 +145,18 @@ public class IvsServerController { return ResponseEntity.ok(ivsOpenApiService.rtspUrl(rtspVo)); } + /** + * 生成相机直连 RTSP 地址 + * + *

入参和出参沿用 v1.0;区别是只返回相机自身地址,不返回 NVR/IVS + * 转发地址,也不改变 v1.0 的既有行为。

+ */ + @PostMapping("/video/rtspurl/v2.0") + public ResponseEntity videoRtspUrlV2( + @RequestBody IvsVideoRtspVo rtspVo) { + return ResponseEntity.ok(ivsOpenApiService.directRtspUrl(rtspVo)); + } + /** * 创建 IVS 平台抓图任务,对应 IVS3800 文档 4.1.12.1。 * diff --git a/src/main/java/com/inspect/nvr/dahua/DahuaIvsVendorService.java b/src/main/java/com/inspect/nvr/dahua/DahuaIvsVendorService.java index b19b2e80..e3e30239 100644 --- a/src/main/java/com/inspect/nvr/dahua/DahuaIvsVendorService.java +++ b/src/main/java/com/inspect/nvr/dahua/DahuaIvsVendorService.java @@ -30,7 +30,9 @@ import com.inspect.nvr.hik.domain.HikNvrUiActionRequest; import com.inspect.nvr.hik.domain.HikNvrUiActionResponse; import com.inspect.nvr.hik.exception.HikNvrPlaybackException; import com.inspect.nvr.hik.service.HikNvrCameraTargetService; +import com.inspect.nvr.ivs.mapper.IvsCameraMapper; import com.inspect.nvr.ivs.mapper.IvsNvrMapper; +import com.inspect.nvr.ivs.pojo.IvsCamera; import com.inspect.nvr.ivs.pojo.IvsNvr; import com.inspect.nvr.ivs.service.IvsVendorAdapter; import com.inspect.nvr.service.DahuaCameraService; @@ -108,6 +110,8 @@ public class DahuaIvsVendorService implements IvsVendorAdapter { private static final java.nio.charset.Charset PRESET_CONFIG_CHARSET = StandardCharsets.UTF_8; /** PtzPreset 原始 JSON 配置缓冲区大小,与大华官方工具类保持一致。 */ private static final int PRESET_CONFIG_BUFFER_SIZE = 2 * 1024 * 1024; + /** 相机自身 RTSP 服务端口;直连相机不使用 NVR 转发端口配置。 */ + private static final int CAMERA_RTSP_PORT = 554; /** 数据库安全的 cameraCode 到 NVR/通道解析服务。 */ @Resource @@ -115,6 +119,9 @@ public class DahuaIvsVendorService implements IvsVendorAdapter { /** NVR 编码和厂商数据库访问。 */ @Resource private IvsNvrMapper nvrMapper; + /** IVS 摄像机表 Mapper,用于按完整编码读取相机 IP 和同 IP 通道。 */ + @Resource + private IvsCameraMapper cameraMapper; /** 大华录像查询、回放和下载能力。 */ @Resource private DahuaNvrPlaybackService playbackService; @@ -194,6 +201,84 @@ public class DahuaIvsVendorService implements IvsVendorAdapter { return IvsRtspUrlResponse.builder().resultCode(0).rtspURL(url).build(); } + /** + * 直接构造摄像机自身的大华实时 RTSP 地址,不经过所属 NVR 转发。 + * + *

与 v1.0 的差异:地址主机取自 {@code ivs_camera.dev_ip},而不是 NVR 的 IP; + * 凭据沿用所属 NVR 的数据库记录,与海康 v2.0 的相机直连做法保持一致。 + * 回放地址仍由 v1.0 的 NVR 转发路径提供。

+ */ + @Override + public IvsRtspUrlResponse directRtspUrl(IvsVideoRtspVo request) { + // 缺少摄像机编码时无法定位相机 IP 和所属 NVR 凭据。 + if (request == null || !hasText(request.getCameraCode())) { + throw HikNvrPlaybackException.badRequest("cameraCode不能为空"); + } + RealMediaURLParam media = request.getMediaURLParam(); + if (media == null) { + throw HikNvrPlaybackException.badRequest("mediaURLParam不能为空"); + } + int serviceType = media.getServiceType() == 0 ? 1 : media.getServiceType(); + // 相机直连只提供实时预览地址,避免把回放请求错误地映射到实时流。 + if (serviceType != 1) { + throw HikNvrPlaybackException.badRequest( + "相机直连RTSP仅支持实时预览serviceType=1"); + } + int streamType = media.getStreamType() == 0 ? 1 : media.getStreamType(); + // 与 v1.0 保持一致,只支持主、辅两档码流。 + if (streamType != 1 && streamType != 2) { + throw HikNvrPlaybackException.badRequest("streamType只能为1或2"); + } + // 先按 cameraCode 解析所属大华 NVR,既校验厂商也取得服务端凭据。 + HikNvrCameraTarget target = resolveTarget(request.getCameraCode(), null); + NvrInfo nvr = target.getNvrInfo(); + // 缺少库内账号或密码时无法建立相机 RTSP 会话,返回明确错误而不是拼出无效地址。 + if (nvr == null || !hasText(nvr.getAccount()) || nvr.getPassword() == null) { + throw HikNvrPlaybackException.notFound( + "所属NVR未配置相机直连所需的账号或密码"); + } + IvsCamera camera = cameraMapper.selectByCode(target.getCameraCode()); + if (camera == null || !hasText(camera.getDevIP())) { + throw HikNvrPlaybackException.notFound( + "未找到cameraCode对应的相机直连地址:" + request.getCameraCode()); + } + // 复用大华实时地址构造器,只把主机换成相机 IP、通道换成相机内通道。 + HikNvrPlaybackRequest cameraRequest = HikNvrPlaybackRequest.builder() + .nvrIp(camera.getDevIP().trim()) + .rtspPort(CAMERA_RTSP_PORT) + .username(nvr.getAccount()) + .password(nvr.getPassword()) + .channel(directCameraChannel(camera)) + .streamType(streamType) + .build(); + return IvsRtspUrlResponse.builder() + .resultCode(0) + .rtspURL(playbackService.liveUrl(cameraRequest)) + .build(); + } + + /** + * 根据同一 {@code dev_ip} 下的摄像机记录确定相机内通道,单镜头默认使用通道 1。 + * + *

双光谱等多镜头相机在平台上一个 IP 占多个通道,直连取流必须带上相机自己的 + * 通道号,否则两个平台通道会拉到同一个镜头。

+ */ + private int directCameraChannel(IvsCamera camera) { + List peers = cameraMapper.selectChannelPeersByDevIp( + camera.getDevIP().trim()); + if (peers == null || peers.size() <= 1) { + return 1; + } + for (int index = 0; index < peers.size(); index++) { + IvsCamera peer = peers.get(index); + if (peer != null && camera.getCode().equalsIgnoreCase(peer.getCode())) { + return index + 1; + } + } + // 当前通道不在同 IP 列表中(数据异常)时退回相机通道 1。 + return 1; + } + /** 使用大华 SDK 创建平台抓图任务。 */ @Override public IvsSnapshotCommandResponse platformSnapshot(IvsDevChanSnapVo request) { diff --git a/src/main/java/com/inspect/nvr/hik/controller/HikNvrUiActionController.java b/src/main/java/com/inspect/nvr/hik/controller/HikNvrUiActionController.java index 67b0c687..aef7fbbf 100644 --- a/src/main/java/com/inspect/nvr/hik/controller/HikNvrUiActionController.java +++ b/src/main/java/com/inspect/nvr/hik/controller/HikNvrUiActionController.java @@ -12,6 +12,7 @@ import com.inspect.nvr.hik.service.HikNvrCredentialService; import com.inspect.nvr.hik.service.HikNvrLivePublicBaseUrlResolver; import com.inspect.nvr.hik.service.HikNvrLiveStreamService; import com.inspect.nvr.hik.service.HikNvrUiActionService; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -26,6 +27,7 @@ import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; +@Slf4j @RestController @CrossOrigin @RequestMapping("/hik/nvr/playback/hls/ui/actions") @@ -129,6 +131,14 @@ public class HikNvrUiActionController { Integer streamType = request == null ? null : request.getStreamType(); // 内网与外网两个 IP 共用本服务时,flvUrl/hlsUrl 必须指向本次请求实际访问的地址。 String publicBaseUrl = publicBaseUrlResolver.resolve(httpRequest); + // 多跳网关会改写 Host,起流时记录一次候选头,便于现场定位播放地址为何与访问地址不一致。 + log.info("[live] 播放地址解析 host={} xForwardedHost={} xPublicHost={} xForwardedFor={} origin={} resolved={}", + httpRequest.getHeader("Host"), + httpRequest.getHeader("X-Forwarded-Host"), + httpRequest.getHeader("X-Public-Host"), + httpRequest.getHeader("X-Forwarded-For"), + httpRequest.getHeader("Origin"), + publicBaseUrl); // 实时服务通过现有 IVS 媒体接口解析 cameraCode 并取得 RTSP 源地址。 return ResponseEntity.ok( liveStreamService.start(cameraCode, streamType, publicBaseUrl)); diff --git a/src/main/java/com/inspect/nvr/hik/service/HikIvsVendorService.java b/src/main/java/com/inspect/nvr/hik/service/HikIvsVendorService.java index e629f752..f3545a94 100644 --- a/src/main/java/com/inspect/nvr/hik/service/HikIvsVendorService.java +++ b/src/main/java/com/inspect/nvr/hik/service/HikIvsVendorService.java @@ -204,6 +204,60 @@ public class HikIvsVendorService implements IvsVendorAdapter { return IvsRtspUrlResponse.builder().resultCode(0).rtspURL(url).build(); } + /** + * 直接构造摄像机自身的海康实时 RTSP 地址,不经过所属 NVR 转发。 + */ + @Override + public IvsRtspUrlResponse directRtspUrl(IvsVideoRtspVo request) { + if (request == null || !hasText(request.getCameraCode())) { + throw HikNvrPlaybackException.badRequest("cameraCode不能为空"); + } + validateFullCameraCode(request.getCameraCode()); + RealMediaURLParam media = request.getMediaURLParam(); + if (media == null) { + throw HikNvrPlaybackException.badRequest("mediaURLParam不能为空"); + } + validateRtspMedia(media); + int serviceType = media.getServiceType() == 0 ? 1 : media.getServiceType(); + if (serviceType != 1) { + throw HikNvrPlaybackException.badRequest("相机直连RTSP仅支持实时预览serviceType=1"); + } + int streamType = media.getStreamType() == 0 ? 1 : media.getStreamType(); + IvsCamera camera = cameraMapper.selectByCode(request.getCameraCode().trim()); + if (camera == null || !hasText(camera.getDevIP())) { + throw HikNvrPlaybackException.notFound( + "未找到cameraCode对应的相机直连地址:" + request.getCameraCode()); + } + Target target = resolveTarget(request.getCameraCode(), null); + HikNvrPlaybackRequest cameraRequest = HikNvrPlaybackRequest.builder() + .nvrIp(camera.getDevIP().trim()) + .rtspPort(554) + .username(target.nvr.getUsername()) + .password(target.nvr.getPassword()) + .streamType(streamType) + .build(); + int cameraChannel = directCameraChannel(camera); + return IvsRtspUrlResponse.builder() + .resultCode(0) + .rtspURL(urlBuilder.buildLive(cameraRequest, cameraChannel)) + .build(); + } + + /** 根据同一 dev_ip 下的摄像机记录确定相机内通道,单镜头默认使用通道 1。 */ + private int directCameraChannel(IvsCamera camera) { + List peers = cameraMapper.selectChannelPeersByDevIp(camera.getDevIP().trim()); + if (peers == null || peers.size() <= 1) { + return 1; + } + for (int index = 0; index < peers.size(); index++) { + IvsCamera peer = peers.get(index); + if (peer != null && camera.getCode().equalsIgnoreCase(peer.getCode())) { + return index + 1; + } + } + return 1; + } + /** * 通过海康统一操作服务创建 IVS 平台抓图任务。 * diff --git a/src/main/java/com/inspect/nvr/ivs/service/IvsOpenApiService.java b/src/main/java/com/inspect/nvr/ivs/service/IvsOpenApiService.java index 13686229..4a9e987a 100644 --- a/src/main/java/com/inspect/nvr/ivs/service/IvsOpenApiService.java +++ b/src/main/java/com/inspect/nvr/ivs/service/IvsOpenApiService.java @@ -63,6 +63,14 @@ public class IvsOpenApiService { return vendorAdapter.rtspUrl(request); } + /** + * 按摄像机所属厂商生成相机直连 RTSP 地址。 + */ + public IvsRtspUrlResponse directRtspUrl(IvsVideoRtspVo request) { + // 仍按 cameraCode 选择厂商,但调用独立的直连能力,绝不回退到 v1.0 转发地址。 + return vendorAdapter.directRtspUrl(request); + } + /** * 将 IVS 平台抓图请求转交给目标摄像机所属的厂商适配器。 * @@ -281,6 +289,14 @@ public class IvsOpenApiService { return adapter.rtspUrl(request); } + @Override + /** 按摄像机编码选择厂商并生成相机直连 RTSP 地址。 */ + public IvsRtspUrlResponse directRtspUrl(IvsVideoRtspVo request) { + String cameraCode = request == null ? null : request.getCameraCode(); + IvsVendorAdapter adapter = byCamera(cameraCode, null); + return adapter.directRtspUrl(request); + } + @Override /** 按摄像机编码选择厂商并创建平台抓图任务。 */ public IvsSnapshotCommandResponse platformSnapshot(IvsDevChanSnapVo request) { diff --git a/src/main/java/com/inspect/nvr/ivs/service/IvsVendorAdapter.java b/src/main/java/com/inspect/nvr/ivs/service/IvsVendorAdapter.java index cf25ba9c..3816182e 100644 --- a/src/main/java/com/inspect/nvr/ivs/service/IvsVendorAdapter.java +++ b/src/main/java/com/inspect/nvr/ivs/service/IvsVendorAdapter.java @@ -13,6 +13,7 @@ import com.inspect.nvr.domain.ivs.IvsPtzControlResponse; import com.inspect.nvr.domain.ivs.IvsRtspUrlResponse; import com.inspect.nvr.domain.ivs.IvsSnapshotCommandResponse; import com.inspect.nvr.domain.ivs.IvsVideoRtspVo; +import com.inspect.nvr.hik.exception.HikNvrPlaybackException; import com.inspect.nvr.view.IvsPlatformRecordListView; import com.inspect.nvr.view.IvsPlatformSnapshotView; @@ -34,6 +35,16 @@ public interface IvsVendorAdapter { */ IvsRtspUrlResponse rtspUrl(IvsVideoRtspVo request); + /** + * 生成不经过 NVR/IVS 转发的相机直连 RTSP 地址。 + * + *

厂商未提供相机直连能力时沿用不支持语义,避免误把 v1.0 的转发地址 + * 当成直连地址返回。

+ */ + default IvsRtspUrlResponse directRtspUrl(IvsVideoRtspVo request) { + throw HikNvrPlaybackException.notFound("当前厂商未配置相机直连RTSP地址"); + } + /** * 创建平台抓图任务,并返回 IVS 任务标识。 *