Browse Source

feat: 生成相机直连 RTSP 地址

master
wangguangyuan 5 hours ago
parent
commit
d3a4f4d7a3
6 changed files with 188 additions and 0 deletions
  1. +12
    -0
      src/main/java/com/inspect/nvr/controller/IvsServerController.java
  2. +85
    -0
      src/main/java/com/inspect/nvr/dahua/DahuaIvsVendorService.java
  3. +10
    -0
      src/main/java/com/inspect/nvr/hik/controller/HikNvrUiActionController.java
  4. +54
    -0
      src/main/java/com/inspect/nvr/hik/service/HikIvsVendorService.java
  5. +16
    -0
      src/main/java/com/inspect/nvr/ivs/service/IvsOpenApiService.java
  6. +11
    -0
      src/main/java/com/inspect/nvr/ivs/service/IvsVendorAdapter.java

+ 12
- 0
src/main/java/com/inspect/nvr/controller/IvsServerController.java View File

@ -145,6 +145,18 @@ public class IvsServerController {
return ResponseEntity.ok(ivsOpenApiService.rtspUrl(rtspVo));
}
/**
* 生成相机直连 RTSP 地址
*
* <p>入参和出参沿用 v1.0区别是只返回相机自身地址不返回 NVR/IVS
* 转发地址也不改变 v1.0 的既有行为</p>
*/
@PostMapping("/video/rtspurl/v2.0")
public ResponseEntity<IvsRtspUrlResponse> videoRtspUrlV2(
@RequestBody IvsVideoRtspVo rtspVo) {
return ResponseEntity.ok(ivsOpenApiService.directRtspUrl(rtspVo));
}
/**
* 创建 IVS 平台抓图任务对应 IVS3800 文档 4.1.12.1
*


+ 85
- 0
src/main/java/com/inspect/nvr/dahua/DahuaIvsVendorService.java View File

@ -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 转发
*
* <p> v1.0 的差异地址主机取自 {@code ivs_camera.dev_ip}而不是 NVR IP
* 凭据沿用所属 NVR 的数据库记录与海康 v2.0 的相机直连做法保持一致
* 回放地址仍由 v1.0 NVR 转发路径提供</p>
*/
@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
*
* <p>双光谱等多镜头相机在平台上一个 IP 占多个通道直连取流必须带上相机自己的
* 通道号否则两个平台通道会拉到同一个镜头</p>
*/
private int directCameraChannel(IvsCamera camera) {
List<IvsCamera> 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) {


+ 10
- 0
src/main/java/com/inspect/nvr/hik/controller/HikNvrUiActionController.java View File

@ -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));


+ 54
- 0
src/main/java/com/inspect/nvr/hik/service/HikIvsVendorService.java View File

@ -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<IvsCamera> 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 平台抓图任务
*


+ 16
- 0
src/main/java/com/inspect/nvr/ivs/service/IvsOpenApiService.java View File

@ -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) {


+ 11
- 0
src/main/java/com/inspect/nvr/ivs/service/IvsVendorAdapter.java View File

@ -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 地址
*
* <p>厂商未提供相机直连能力时沿用不支持语义避免误把 v1.0 的转发地址
* 当成直连地址返回</p>
*/
default IvsRtspUrlResponse directRtspUrl(IvsVideoRtspVo request) {
throw HikNvrPlaybackException.notFound("当前厂商未配置相机直连RTSP地址");
}
/**
* 创建平台抓图任务并返回 IVS 任务标识
*


Loading…
Cancel
Save