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 任务标识。
*