Browse Source

实时预览直连支持多镜头相机:按相机内通道取流

master
wangguangyuan 1 week ago
parent
commit
1d2c402f34
6 changed files with 158 additions and 16 deletions
  1. +2
    -0
      docker-compose.yml
  2. +122
    -16
      src/main/java/com/inspect/nvr/hik/service/HikNvrLiveStreamService.java
  3. +9
    -0
      src/main/java/com/inspect/nvr/hik/vo/HikNvrLiveStreamConfigVo.java
  4. +11
    -0
      src/main/java/com/inspect/nvr/ivs/mapper/IvsCameraMapper.java
  5. +4
    -0
      src/main/resources/application.yml
  6. +10
    -0
      src/main/resources/mapper/ivs/IvsCameraMapper.xml

+ 2
- 0
docker-compose.yml View File

@ -36,6 +36,8 @@ services:
# 实时预览优先直连摄像机取流(CVR 等设备远程转发额度小,直连可绕开该瓶颈)。
HIK_NVR_LIVE_PREFER_CAMERA_DIRECT: ${HIK_NVR_LIVE_PREFER_CAMERA_DIRECT:-true}
HIK_NVR_LIVE_CAMERA_RTSP_PORT: ${HIK_NVR_LIVE_CAMERA_RTSP_PORT:-554}
# 同一 IP 多通道相机(双光谱)的相机内通道顺序:asc / desc。
HIK_NVR_LIVE_CAMERA_CHANNEL_ORDER: ${HIK_NVR_LIVE_CAMERA_CHANNEL_ORDER:-asc}
JAVA_TOOL_OPTIONS: ${JAVA_TOOL_OPTIONS:--XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -Dfile.encoding=UTF-8 -Djava.security.egd=file:/dev/./urandom -Djava.library.path=/app/lib:/app/lib/HCNetSDKCom:/app/lib/linux64}
REDIS_HOST: ${REDIS_HOST:?REDIS_HOST_required_in_.env}
REDIS_PORT: ${REDIS_PORT:-6379}


+ 122
- 16
src/main/java/com/inspect/nvr/hik/service/HikNvrLiveStreamService.java View File

@ -145,6 +145,8 @@ public class HikNvrLiveStreamService {
private String cameraPassword;
/** 直连摄像机 RTSP 端口连通性探测超时。 */
private int cameraProbeTimeoutMillis;
/** 同一 IP 多通道相机的相机内通道分配顺序:asc / desc。 */
private String cameraChannelOrder;
/** 已确认直连鉴权失败(相机凭据与 NVR 不同)的摄像机 IP,避免反复浪费一次尝试。 */
private final Set<String> cameraDirectAuthFailed = ConcurrentHashMap.newKeySet();
@ -198,6 +200,10 @@ public class HikNvrLiveStreamService {
? "" : config.getCameraPassword();
// 探测必须显著短于启动时限避免相机不可达时拖慢起流
this.cameraProbeTimeoutMillis = Math.max(100, config.getCameraProbeTimeoutMillis());
// 多通道相机双光谱等的相机内通道分配顺序默认按平台通道编码升序
this.cameraChannelOrder = "desc".equalsIgnoreCase(
config.getCameraChannelOrder() == null ? "" : config.getCameraChannelOrder().trim())
? "desc" : "asc";
}
/**
@ -463,13 +469,17 @@ public class HikNvrLiveStreamService {
}
String rtmpUrl = rtmpBaseUrl + "/live/" + pending.streamId;
// 并发额度按"实际拉取的设备"统计直连时是相机 IP走转发时才是 NVR/CVR IP
// 否则直连摄像机取流会被所属 NVR 的额度限制住等于没绕开 CVR 的瓶颈
// 并发额度按"实际拉取的设备"统计直连时是相机 IP多镜头相机再细到相机内通道
// 走转发时才是 NVR/CVR IP否则直连取流会被所属 NVR 的额度限制住等于没绕开 CVR 的瓶颈
String streamHost = cameraTarget == null ? nvrIp : cameraTarget.devIp;
if (!awaitSessionQuota(streamHost, deadline)) {
int streamChannel = cameraTarget == null ? 0 : cameraTarget.cameraChannel;
String quotaKey = streamChannel > 0 ? streamHost + "#" + streamChannel : streamHost;
if (!awaitSessionQuota(quotaKey, deadline)) {
String target = streamChannel > 0
? streamHost + "(相机通道" + streamChannel + ")" : streamHost;
throw HikNvrPlaybackException.conflict(
NVR_LIVE_SESSION_LIMIT,
NVR_LIVE_SESSION_LIMIT + ":取流设备" + streamHost + "的并发实时流已达上限"
NVR_LIVE_SESSION_LIMIT + ":取流设备" + target + "的并发实时流已达上限"
+ maxSessionsPerNvr + "路,请先关闭其他窗口后重试");
}
Process process;
@ -490,6 +500,7 @@ public class HikNvrLiveStreamService {
pending.target.getCameraCode(),
pending.target.getNvrInfo().getNvrIp(),
streamHost,
streamChannel,
pending.target.getChannel(),
pending.streamType,
pending.streamId,
@ -498,9 +509,8 @@ public class HikNvrLiveStreamService {
pending.flvUrl);
// 注册本地会话后停止和状态请求即可立即定位并释放正在启动的进程
sessions.put(sessionKey, session);
log.info("[live] started cameraCode={} nvrIp={} host={} source={} streamType={} streamId={} attempt={} log={}",
session.cameraCode, session.nvrIp,
cameraTarget == null ? session.nvrIp : cameraTarget.devIp,
log.info("[live] started cameraCode={} nvrIp={} host={} cameraChannel={} source={} streamType={} streamId={} attempt={} log={}",
session.cameraCode, session.nvrIp, streamHost, streamChannel,
source, session.streamType, session.streamId, attempt, logFile);
long remaining = Math.max(0L, deadline - System.currentTimeMillis());
@ -1231,16 +1241,16 @@ public class HikNvrLiveStreamService {
}
/**
* 统计仍在运行且正在从指定设备取流的本地实时流数量
* 统计仍在运行命中同一并发额度键的本地实时流数量
*
* @param host 实际取流设备地址相机 IP NVR/CVR IP
* @param quotaKey 额度键直连相机为 "相机IP#相机通道"转发为设备 IP
* @return 本地活跃会话数
*/
private int activeSessionsOn(String host) {
private int activeSessionsOn(String quotaKey) {
int count = 0;
// 本地会话表规模很小逐条比对取流设备地址即可
// 本地会话表规模很小逐条比对额度键即可
for (LiveSession session : sessions.values()) {
if (session.isAlive() && host.equalsIgnoreCase(session.sourceHost)) {
if (session.isAlive() && quotaKey.equalsIgnoreCase(session.quotaKey())) {
count++;
}
}
@ -1334,8 +1344,87 @@ public class HikNvrLiveStreamService {
.password(password)
.streamType(streamType)
.build();
// 直连摄像机只有一个通道trackId = 1 * 100 + 码流类型 101/102
return new CameraDirectTarget(devIp, urlBuilder.buildLive(cameraRequest, 1));
// 双光谱等多镜头相机在平台上一个 IP 占多个通道必须用相机自己的通道号才能取到对应镜头
int cameraChannel = resolveCameraChannel(target.getCameraCode(), devIp);
// trackId = 相机内通道 * 100 + 码流类型例如通道 1 = 101/102通道 2 = 201/202
return new CameraDirectTarget(devIp, cameraChannel,
urlBuilder.buildLive(cameraRequest, cameraChannel));
}
/**
* 计算平台通道对应的相机内通道号
*
* <p>同一台多镜头相机双光谱可见光 + 热成像在平台上占多个通道直连 RTSP
* 必须带上相机自己的通道号否则两个平台通道会拉到同一个镜头优先按平台通道名判断
* 镜头类型热成像/测温 相机通道 1可见光 相机通道 2与海康双光谱默认一致
* 名称无法区分时按平台通道编码顺序分配 1..N整站顺序相反时用
* {@code hik.playback.live.camera-channel-order=desc} 反转</p>
*
* @param cameraCode 当前平台通道编码
* @param devIp 摄像机 IP
* @return 相机内通道号 1 开始
*/
private int resolveCameraChannel(String cameraCode, String devIp) {
List<IvsCamera> peers;
try {
// 该查询命中 ivs_camera.dev_ip 索引返回同一台相机在平台上的全部通道
peers = ivsCameraMapper.selectChannelPeersByDevIp(devIp);
} catch (RuntimeException queryFailure) {
log.warn("[live] 查询同IP通道失败,按单通道相机处理 devIp={}:{}",
devIp, queryFailure.getMessage());
return 1;
}
// 单通道相机绝大多数直接使用相机通道 1
if (peers == null || peers.size() <= 1) {
return 1;
}
int index = -1;
String cameraName = null;
// 通道记录按 code 升序返回包含当前通道自己
for (int position = 0; position < peers.size(); position++) {
IvsCamera peer = peers.get(position);
if (peer != null && cameraCode.equalsIgnoreCase(peer.getCode())) {
index = position;
cameraName = peer.getName();
break;
}
}
boolean reverse = "desc".equals(cameraChannelOrder);
// 先按名称判断镜头类型双光谱相机两个通道名通常能区分可见光/热成像
int namedChannel = cameraChannelByThermalName(cameraName);
if (namedChannel > 0) {
// 顺序反转时交换通道 1 与通道 2
return reverse ? (namedChannel == 1 ? 2 : 1) : namedChannel;
}
if (index < 0) {
// 当前通道不在同 IP 列表中数据异常时退回相机通道 1
return 1;
}
// 名称无法区分例如两个通道都叫双光谱时按平台通道顺序分配
int ordered = reverse ? peers.size() - 1 - index : index;
return ordered + 1;
}
/**
* 按通道名判断镜头类型并返回对应的相机内通道号
*
* @param cameraName 平台通道名
* @return 热成像/测温返回 1可见光返回 2无法判断返回 0
*/
private int cameraChannelByThermalName(String cameraName) {
if (!hasText(cameraName)) {
return 0;
}
String name = cameraName.toLowerCase(Locale.ROOT);
// 海康双光谱相机默认 1xx 为热成像通道2xx 为可见光通道
if (name.contains("热成像") || name.contains("测温") || name.contains("thermal")) {
return 1;
}
if (name.contains("可见光") || name.contains("optical")
|| name.contains("visual") || name.contains("白光")) {
return 2;
}
return 0;
}
/**
@ -1635,12 +1724,15 @@ public class HikNvrLiveStreamService {
private static final class CameraDirectTarget {
/** 摄像机 IP,用于鉴权失败后记忆该相机。 */
private final String devIp;
/** 相机内通道号(双光谱相机为 1/2),用于并发额度口径与日志核对。 */
private final int cameraChannel;
/** 可直接交给 FFmpeg 的摄像机 RTSP 地址。 */
private final String rtspUrl;
/** 保存摄像机 IP 与直连 RTSP 地址。 */
private CameraDirectTarget(String devIp, String rtspUrl) {
/** 保存摄像机 IP、相机内通道与直连 RTSP 地址。 */
private CameraDirectTarget(String devIp, int cameraChannel, String rtspUrl) {
this.devIp = devIp;
this.cameraChannel = cameraChannel;
this.rtspUrl = rtspUrl;
}
}
@ -1667,6 +1759,8 @@ public class HikNvrLiveStreamService {
private final String nvrIp;
/** 本路流实际拉取的设备地址:直连摄像机时为相机 IP,经转发时为 NVR/CVR IP。 */
private final String sourceHost;
/** 相机内通道号;走 NVR/CVR 转发时为 0。 */
private final int sourceChannel;
/** 页面逻辑通道号。 */
private final Integer channel;
/** 主码流 1 或子码流 2。 */
@ -1687,6 +1781,7 @@ public class HikNvrLiveStreamService {
String cameraCode,
String nvrIp,
String sourceHost,
int sourceChannel,
Integer channel,
int streamType,
String streamId,
@ -1696,6 +1791,7 @@ public class HikNvrLiveStreamService {
this.cameraCode = cameraCode;
this.nvrIp = nvrIp;
this.sourceHost = sourceHost;
this.sourceChannel = sourceChannel;
this.channel = channel;
this.streamType = streamType;
this.streamId = streamId;
@ -1708,6 +1804,16 @@ public class HikNvrLiveStreamService {
private boolean isAlive() {
return process != null && process.isAlive();
}
/**
* 返回并发额度统计键
*
* <p>直连摄像机按"相机 IP + 相机内通道"统计这样双光谱相机的可见光与热成像
* 可以同时播放而同一个镜头不会被重复拉取走转发时按设备 IP 统计</p>
*/
private String quotaKey() {
return sourceChannel > 0 ? sourceHost + "#" + sourceChannel : sourceHost;
}
}
/** 一次排队或正在执行的起流任务。 */


+ 9
- 0
src/main/java/com/inspect/nvr/hik/vo/HikNvrLiveStreamConfigVo.java View File

@ -82,4 +82,13 @@ public class HikNvrLiveStreamConfigVo {
/** 直连摄像机 RTSP 端口连通性探测超时时间。 */
@Value("${hik.playback.live.camera-probe-timeout-millis:500}")
private int cameraProbeTimeoutMillis = 500;
/**
* 同一 IP 多通道相机的相机内通道分配顺序
*
* <p>{@code asc}默认按平台通道编码升序分配 1..N{@code desc} 反向
* 双光谱相机优先按名称判断镜头热成像/测温 相机通道 1可见光 2
* 名称无法区分时才使用本顺序现场若发现两个镜头画面互换改成 {@code desc} 即可</p>
*/
@Value("${hik.playback.live.camera-channel-order:asc}")
private String cameraChannelOrder = "asc";
}

+ 11
- 0
src/main/java/com/inspect/nvr/ivs/mapper/IvsCameraMapper.java View File

@ -22,6 +22,17 @@ public interface IvsCameraMapper {
/** 按 IVS 摄像机完整编码查询摄像机记录,用于读取前端设备 IP。 */
IvsCamera selectByCode(@Param("code") String code);
/**
* 按设备 IP 查询共享该 IP 的摄像机通道
*
* <p>双光谱等多镜头相机在同一 IP 下会占用多个平台通道可见光/热成像
* 直连取流时需要据此计算相机内通道号</p>
*
* @param devIp 摄像机 IP
* @return 按完整编码升序排列的通道记录仅填充 code/name/devIP
*/
List<IvsCamera> selectChannelPeersByDevIp(@Param("devIp") String devIp);
/**
* NVR IP 查询所属摄像机供监控工作台从 ivs_camera 构建设备树
*


+ 4
- 0
src/main/resources/application.yml View File

@ -84,6 +84,10 @@ hik:
camera-password: "${HIK_NVR_LIVE_CAMERA_PASSWORD:}"
# 直连摄像机 RTSP 端口连通性探测超时(毫秒)。
camera-probe-timeout-millis: ${HIK_NVR_LIVE_CAMERA_PROBE_TIMEOUT_MILLIS:500}
# 同一 IP 多通道相机(双光谱等)的相机内通道分配顺序:asc / desc。
# 默认先按通道名判断镜头(热成像/测温 → 相机通道 1,可见光 → 2);
# 若现场发现某台双光相机的两个窗口画面互换,把这里改成 desc。
camera-channel-order: ${HIK_NVR_LIVE_CAMERA_CHANNEL_ORDER:asc}
clip:
# 一分钟录像先由SDK下载到临时目录,再由FFmpeg统一封装为MP4。
ffmpeg-path: ${FFMPEG_PATH:C:/tools/ffmpeg/bin/ffmpeg.exe}


+ 10
- 0
src/main/resources/mapper/ivs/IvsCameraMapper.xml View File

@ -147,6 +147,16 @@
LIMIT 1
</select>
<select id="selectChannelPeersByDevIp" resultMap="IvsCameraResultMap">
SELECT
code,
name,
dev_ip
FROM ivs_camera
WHERE dev_ip = #{devIp}
ORDER BY code
</select>
<select id="selectByNvrIp" resultMap="IvsCameraResultMap">
SELECT
camera.code,


Loading…
Cancel
Save