Browse Source

fix:修复多镜头相机两个通道取到同一镜头,导致视频画面重复,并补充直连失败原因日志

master
wangguangyuan 2 months ago
parent
commit
e531caf6fd
5 changed files with 232 additions and 55 deletions
  1. +168
    -39
      src/main/java/com/inspect/nvr/hik/service/HikNvrLiveStreamService.java
  2. +9
    -4
      src/main/java/com/inspect/nvr/hik/vo/HikNvrLiveStreamConfigVo.java
  3. +14
    -4
      src/main/java/com/inspect/nvr/ivs/mapper/BasedataEqpbookChannelMapper.java
  4. +28
    -0
      src/main/java/com/inspect/nvr/ivs/pojo/BasedataEqpbookChannel.java
  5. +13
    -8
      src/main/resources/mapper/ivs/BasedataEqpbookChannelMapper.xml

+ 168
- 39
src/main/java/com/inspect/nvr/hik/service/HikNvrLiveStreamService.java View File

@ -13,6 +13,7 @@ import com.inspect.nvr.hik.exception.HikNvrPlaybackException;
import com.inspect.nvr.hik.vo.HikNvrLiveStreamConfigVo;
import com.inspect.nvr.ivs.mapper.BasedataEqpbookChannelMapper;
import com.inspect.nvr.ivs.mapper.IvsCameraMapper;
import com.inspect.nvr.ivs.pojo.BasedataEqpbookChannel;
import com.inspect.nvr.ivs.pojo.IvsCamera;
import com.inspect.nvr.ivs.service.IvsOpenApiService;
import com.inspect.nvr.service.HikLoginService;
@ -36,6 +37,9 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@ -1328,15 +1332,21 @@ public class HikNvrLiveStreamService {
}
// 没有设备 IP 时无法直连,保持原有 NVR 取流行为。
if (camera == null || !hasText(camera.getDevIP())) {
log.info("[live] 跳过相机直连 cameraCode={} 原因=ivs_camera未配置dev_ip",
target.getCameraCode());
return null;
}
String devIp = camera.getDevIP().trim();
// 已确认该相机凭据不可用时不再重复尝试直连。
if (cameraDirectAuthFailed.contains(devIp)) {
log.info("[live] 跳过相机直连 cameraCode={} devIp={} 原因=该相机凭据此前鉴权失败",
target.getCameraCode(), devIp);
return null;
}
// RTSP 端口不可达(未开放或被网络隔离)时直接回退,避免白等一次尝试。
if (!isCameraRtspReachable(devIp)) {
log.info("[live] 跳过相机直连 cameraCode={} devIp={} 原因=RTSP端口{}不可达",
target.getCameraCode(), devIp, cameraRtspPort);
return null;
}
NvrInfo nvrInfo = target.getNvrInfo();
@ -1346,6 +1356,8 @@ public class HikNvrLiveStreamService {
? cameraPassword : nvrInfo.getPassword();
// 账号缺失时无法建立 RTSP 会话,回退 NVR 路径由 IVS 侧给出明确错误。
if (!hasText(username) || password == null) {
log.info("[live] 跳过相机直连 cameraCode={} devIp={} 原因=缺少账号或密码",
target.getCameraCode(), devIp);
return null;
}
// 复用统一 URL 构造器,保证中文或特殊字符账号的百分号编码规则与 NVR 路径一致。
@ -1367,19 +1379,25 @@ public class HikNvrLiveStreamService {
* 计算平台通道对应的相机内通道号。
*
* <p>同一台多镜头相机(双光谱:可见光 + 热成像)在平台上占多个通道,直连 RTSP 时
* 必须带上相机自己的通道号,否则两个平台通道会拉到同一个镜头。判定顺序:</p>
* 必须带上相机自己的通道号,否则两个平台通道会拉到同一个镜头。台账与通道名都只
* 描述单个通道的镜头类型,必须先确认它们确实把同一 IP 下的兄弟通道区分开了才可用,
* 否则两个平台通道会一起被映射到同一个相机通道。判定顺序:</p>
* <ol>
* <li>台账 {@code basedata_eqpbook_channel.channel_type},{@code vl} 可见光、
* {@code ir} 红外热成像。这是镜头类型的权威来源,{@code ir} 用的相机内通道号
* 由 {@code hik.playback.live.ir-camera-channel} 指定(默认 2,即 1=可见光、
* 2=热成像);</li>
* <li>台账缺失或类型与镜头无关(ptz/ng 等)时按平台通道名关键字判断;</li>
* <li>名称也判断不出来时按平台通道编码顺序分配 1..N,整站顺序相反时用
* {@code hik.playback.live.camera-channel-order=desc} 反转。</li>
* {@code ir} 红外热成像。{@code ir} 用的相机内通道号由
* {@code hik.playback.live.ir-camera-channel} 指定(默认 2,即 1=可见光、
* 2=热成像),{@code vl} 用另一个通道。台账只有在确实把同一 IP 下的兄弟通道
* 区分成不同镜头时才生效,见 {@link #resolveLedgerChannel};</li>
* <li>台账缺失、类型与镜头无关(ptz/ng 等)或不足以区分镜头时,按平台通道名
* 关键字判断,名称同样要能区分镜头才生效,见 {@link #resolveNamedChannel};</li>
* <li>上述信号都判断不出镜头类型时按平台通道编码顺序分配 1..N,整站顺序相反时用
* {@code hik.playback.live.camera-channel-order=desc} 反转。顺序分配天然让
* 兄弟通道落在不同的相机通道上,默认升序即多数现场的 1=可见光、2=热成像。</li>
* </ol>
*
* <p>只有同 IP 多通道(真双光谱机身机)才做镜头判定:单通道设备的相机内通道号
* 恒为 1,单镜头热成像相机套用镜头映射反而会取到不存在的通道。</p>
* 恒为 1,单镜头热成像相机套用镜头映射反而会取到不存在的通道,因此单通道判断
* 排在所有镜头映射之前。</p>
*
* @param cameraCode 当前平台通道编码
* @param devIp 摄像机 IP
@ -1395,32 +1413,28 @@ public class HikNvrLiveStreamService {
devIp, queryFailure.getMessage());
return 1;
}
// 单通道相机(绝大多数)直接使用相机通道 1,不必再查台账。
// 单通道相机(绝大多数)直接使用相机通道 1,不参与任何镜头映射。
if (peers == null || peers.size() <= 1) {
return 1;
}
// 同 IP 多通道说明是多镜头机身相机,先按台账镜头类型判定,避免依赖通道名。
String channelType = resolveChannelType(cameraCode);
int typedChannel = cameraChannelByChannelType(channelType);
if (typedChannel > 0) {
log.info("[live] 按台账镜头类型分配相机通道 cameraCode={} channelType={} cameraChannel={}",
cameraCode, channelType, typedChannel);
return typedChannel;
}
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);
// 1.同 IP 多通道说明是多镜头机身相机,先按台账镜头类型判定,避免依赖通道名。
int ledgerChannel = resolveLedgerChannel(peers, index);
if (ledgerChannel > 0) {
return ledgerChannel;
}
// 2.台账不具备区分镜头的能力时退回名称判断。
int namedChannel = resolveNamedChannel(peers, index);
if (namedChannel > 0) {
return namedChannel;
}
@ -1428,33 +1442,147 @@ public class HikNvrLiveStreamService {
// 当前通道不在同 IP 列表中(数据异常)时退回相机通道 1。
return 1;
}
// 名称也无法区分(例如两个通道都叫“双光谱”)时按平台通道顺序分配。
int ordered = reverse ? peers.size() - 1 - index : index;
return ordered + 1;
// 3.名称也无法区分(例如两个通道都叫“双光谱”,或都是可见光)时按平台通道顺序分配。
boolean reverse = "desc".equals(cameraChannelOrder);
int orderedChannel = (reverse ? peers.size() - 1 - index : index) + 1;
log.info("[live] 台账与通道名都未区分镜头,按平台通道顺序分配相机通道"
+ " cameraCode={} index={}/{} order={} cameraChannel={}",
cameraCode, index, peers.size(), cameraChannelOrder, orderedChannel);
return orderedChannel;
}
/**
* 读取台账 basedata_eqpbook_channel.channel_type。
* 按台账镜头类型判定当前通道的相机内通道号。
*
* @param cameraCode IVS 完整摄像机编码
* @return 台账通道类型(vl/ir 等);无记录、字段为空或查询异常时返回 null
* <p>台账能用来判定镜头的前提,是它确实把同一 IP 下的兄弟通道区分成了不同镜头。
* 现场既有“1=可见光、2=热成像”的双光谱相机,也有两个镜头都是可见光、台账两个
* 通道都记为 {@code vl} 的机身机;后者逐通道套用类型会让两个平台通道解析到同一个
* 相机通道,两个窗口拉到同一个镜头。因此这里先统计兄弟通道按台账解析出的相机
* 通道,只有一个取值时视为台账不具备区分能力,返回 0 交给通道名与平台通道顺序
* 兜底,由顺序分配把两个通道分别落到相机通道 1 和 2:双可见光相机两路各自取到
* 一个可见光镜头,双光谱相机则分别取到可见光与热成像镜头。</p>
*
* @param peers 同一 IP 下的平台通道,按完整编码升序
* @param index 当前通道在 {@code peers} 中的下标,-1 表示不在列表中
* @return 台账判定的相机内通道号;台账不具备区分能力时返回 0
*/
private int resolveLedgerChannel(List<IvsCamera> peers, int index) {
if (index < 0) {
return 0;
}
// 同一ip下的key:channelCode,value:channelType
Map<String, String> channelTypes = loadChannelTypes(peers);
Set<Integer> resolvedChannels = new HashSet<>(peers.size());
for (IvsCamera peer : peers) {
if (peer == null) {
continue;
}
int peerChannel = cameraChannelByChannelType(channelTypes.get(peer.getCode()));
if (peerChannel > 0) {
resolvedChannels.add(peerChannel);
}
}
// 兄弟通道全部落在同一个相机通道上,说明台账没有区分镜头,不能据此分配。
if (resolvedChannels.size() <= 1) {
return 0;
}
String cameraCode = peers.get(index).getCode();
String channelType = channelTypes.get(cameraCode);
int typedChannel = cameraChannelByChannelType(channelType);
if (typedChannel > 0) {
log.info("[live] 按台账镜头类型分配相机通道 cameraCode={} channelType={} cameraChannel={}",
cameraCode, channelType, typedChannel);
}
return typedChannel;
}
/**
* 按平台通道名判定当前通道的相机内通道号。
*
* <p>与台账一样必须先确认信号能区分镜头:两个通道都带“可见光”的相机(双可见光
* 机身机,或热成像那一路名称没标注)逐个通道套用关键字会让两个平台通道落到同一个
* 相机通道。因此兄弟通道名称解析出的相机通道只有一个取值时返回 0,交给平台通道
* 顺序兜底。</p>
*
* @param peers 同一 IP 下的平台通道,按完整编码升序
* @param index 当前通道在 {@code peers} 中的下标,-1 表示不在列表中
* @return 名称判定的相机内通道号;名称不足以区分镜头时返回 0
*/
private String resolveChannelType(String cameraCode) {
private int resolveNamedChannel(List<IvsCamera> peers, int index) {
if (index < 0) {
return 0;
}
Set<Integer> resolvedChannels = new HashSet<>(peers.size());
for (IvsCamera peer : peers) {
if (peer == null) {
continue;
}
int peerChannel = cameraChannelByThermalName(peer.getName());
if (peerChannel > 0) {
resolvedChannels.add(peerChannel);
}
}
// 各通道名称指向同一个镜头,说明名称没有区分镜头,不能据此分配。
if (resolvedChannels.size() <= 1) {
return 0;
}
IvsCamera current = peers.get(index);
int namedChannel = cameraChannelByThermalName(current.getName());
if (namedChannel > 0) {
log.info("[live] 按通道名分配相机通道 cameraCode={} cameraName={} cameraChannel={}",
current.getCode(), current.getName(), namedChannel);
}
return namedChannel;
}
/**
* 批量读取同一 IP 下各平台通道的台账镜头类型。
*
* @param peers 同一 IP 下的平台通道
* @return 摄像机编码到台账 channel_type 的映射;同一编码有多条历史台账时取最新一条,
* 无记录或查询异常时该编码不出现在映射中
*/
private Map<String, String> loadChannelTypes(List<IvsCamera> peers) {
List<String> codes = new ArrayList<>(peers.size());
for (IvsCamera peer : peers) {
if (peer != null && hasText(peer.getCode())) {
codes.add(peer.getCode());
}
}
if (codes.isEmpty()) {
return Collections.emptyMap();
}
Map<String, String> channelTypes = new HashMap<>(codes.size());
try {
String channelType = eqpbookChannelMapper.selectChannelTypeByChannelCode(cameraCode);
// 台账没有该编码时保持为空,由调用方回退名称与顺序判定。
return hasText(channelType) ? channelType.trim() : null;
List<BasedataEqpbookChannel> records =
eqpbookChannelMapper.selectChannelTypesByChannelCodes(codes);
for (BasedataEqpbookChannel record : records) {
if (record == null || !hasText(record.getChannelCode())) {
continue;
}
// 同一编码存在历史重复台账时查询按 channel_id 倒序返回,保留首条即最新记录。
if (channelTypes.containsKey(record.getChannelCode())) {
continue;
}
channelTypes.put(record.getChannelCode(),
hasText(record.getChannelType()) ? record.getChannelType().trim() : null);
}
} catch (RuntimeException queryFailure) {
// 台账不是取流必需数据,查询失败只记录原因,不阻断实时预览。
log.warn("[live] 读取basedata_eqpbook_channel失败,回退名称判定 cameraCode={}:{}",
cameraCode, queryFailure.getMessage());
return null;
log.warn("[live] 读取basedata_eqpbook_channel失败,回退通道名判定 同IP通道数={}:{}",
codes.size(), queryFailure.getMessage());
return Collections.emptyMap();
}
return channelTypes;
}
/**
* 按台账通道类型返回可见光/热成像对应的相机内通道号。
*
* <p>只做单点映射,不判断台账是否真的区分了兄弟通道:两个通道都记 {@code vl} 时
* 会一起落到同一个相机通道。因此调用方必须先确认台账具备区分能力,见
* {@link #resolveLedgerChannel}。</p>
*
* @param channelType 台账 channel_type,vl 可见光 / ir 红外热成像
* @return {@code ir} 返回配置的相机内通道号,{@code vl} 返回另一个通道;无法判定返回 0
*/
@ -1486,8 +1614,9 @@ public class HikNvrLiveStreamService {
/**
* 按通道名判断镜头类型并返回对应的相机内通道号。
*
* <p>只在台账 {@code channel_type} 缺失时使用:部分平台通道名带“热成像/可见光”
* 字样,但名称由台账人员维护、可能两个通道同名,因此不作为首选依据。</p>
* <p>只在台账 {@code channel_type} 缺失或不具备区分能力时使用:部分平台通道名带
* “热成像/可见光”字样,但名称由台账人员维护、可能两个通道同名。只做单点映射,
* 调用方必须先确认名称能区分兄弟通道,见 {@link #resolveNamedChannel}。</p>
*
* @param cameraName 平台通道名
* @return 热成像/测温返回配置的相机内通道号,可见光返回另一个通道,无法判断返回 0
@ -1569,9 +1698,9 @@ public class HikNvrLiveStreamService {
command.add("error");
command.add("-nostats");
// 海康 RTSP 不在 SDP 中给出帧率,ffmpeg 会自愿读满默认 5 秒探测窗口来估帧率;
// 显式压到 3 秒让推流提前约 2 秒开始,否则首次尝试 5 秒预算会在探测完成前杀掉进程。
// 压到 1.5 秒让推流提前约 3.5 秒开始:25fps 子码流下约 37 帧已足够估准帧率。
command.add("-analyzeduration");
command.add("3000000");
command.add("1500000");
command.add("-rtsp_transport");
command.add("tcp");
command.add("-i");


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

@ -54,8 +54,8 @@ public class HikNvrLiveStreamConfigVo {
@Value("${hik.playback.live.stop-grace-millis:2000}")
private long stopGraceMillis = 2000L;
/** 单台 NVR 允许同时播放的实时流上限,0 表示不限制。 */
@Value("${hik.playback.live.max-sessions-per-nvr:8}")
private int maxSessionsPerNvr = 8;
@Value("${hik.playback.live.max-sessions-per-nvr:0}")
private int maxSessionsPerNvr = 0;
/** 是否回收长时间无人观看的实时流。 */
@Value("${hik.playback.live.idle-reap-enabled:true}")
private boolean idleReapEnabled = true;
@ -90,6 +90,9 @@ public class HikNvrLiveStreamConfigVo {
* 是可见光、{@code ir} 是热成像,本值决定 {@code ir} 使用哪个相机内通道:
* 默认 2,即 1=可见光、2=热成像;{@code vl} 自动使用另一个通道。</p>
*
* <p>台账只在该相机的多个平台通道确实被区分成不同镜头时才生效;两个通道都记为
* {@code vl} 时台账不具备区分能力,会退回通道名与平台通道顺序判定。</p>
*
* <p>现场若发现两个镜头画面互换,把本值改成 1 即可,台账判定与通道名判定会
* 一起反转,不需要再动 {@code camera-channel-order}。</p>
*/
@ -99,8 +102,10 @@ public class HikNvrLiveStreamConfigVo {
* 同一 IP 多通道相机的相机内通道分配顺序。
*
* <p>{@code asc}(默认)按平台通道编码升序分配 1..N,{@code desc} 反向。
* 只在台账 {@code channel_type} 与平台通道名都判断不出可见光/热成像时才生效;
* 镜头判定结果不由本配置调整,改镜头方向请用 {@code ir-camera-channel}。</p>
* 台账与平台通道名都判断不出可见光/热成像,或台账把兄弟通道记成了同一种类型
* (例如两个通道都是 {@code vl})时生效,此时升序正好对应多数现场的
* 1=可见光、2=热成像;镜头判定结果不由本配置调整,改镜头方向请用
* {@code ir-camera-channel}。</p>
*/
@Value("${hik.playback.live.camera-channel-order:asc}")
private String cameraChannelOrder = "asc";


+ 14
- 4
src/main/java/com/inspect/nvr/ivs/mapper/BasedataEqpbookChannelMapper.java View File

@ -1,8 +1,13 @@
package com.inspect.nvr.ivs.mapper;
import com.inspect.nvr.ivs.pojo.BasedataEqpbookChannel;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.Collection;
import java.util.List;
/**
* 台账通道表 basedata_eqpbook_channel 的 MyBatis Mapper。
*
@ -16,10 +21,15 @@ import org.apache.ibatis.annotations.Param;
public interface BasedataEqpbookChannelMapper {
/**
* 按 IVS 完整摄像机编码读取台账通道类型。
* 按 IVS 完整摄像机编码批量读取台账通道类型。
*
* <p>判定镜头必须同时看同一 IP 下的兄弟通道,以确认台账是否真的把两个镜头
* 区分开了,因此这里一次取回整组编码,避免逐通道查询。</p>
*
* @param channelCode IVS 完整摄像机编码,与 {@code ivs_camera.code} 同值
* @return 台账 channel_type(vl/ir 等);无记录或字段为空时返回 null
* @param channelCodes IVS 完整摄像机编码集合,与 {@code ivs_camera.code} 同值,不可为空
* @return 按 channel_code 升序、同一编码内按 channel_id 倒序排列的记录;调用方
* 对每个编码取首条即为最新台账记录,无记录的编码不会出现在结果中
*/
String selectChannelTypeByChannelCode(@Param("channelCode") String channelCode);
List<BasedataEqpbookChannel> selectChannelTypesByChannelCodes(
@Param("channelCodes") Collection<String> channelCodes);
}

+ 28
- 0
src/main/java/com/inspect/nvr/ivs/pojo/BasedataEqpbookChannel.java View File

@ -0,0 +1,28 @@
package com.inspect.nvr.ivs.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 台账通道信息,对应 basedata_eqpbook_channel 表的镜头判定投影。
*
* <p>实时预览直连摄像机时只关心两列:{@code channel_code} 用于关联平台通道,
* {@code channel_type} 用于判定可见光/热成像镜头。台账同一编码存在历史重复
* 记录时,查询按 channel_id 倒序返回,调用方保留首条即可取到最新记录。</p>
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BasedataEqpbookChannel implements Serializable {
/** 序列化版本号。 */
private static final long serialVersionUID = 1L;
/** 完整摄像机编码,与 ivs_camera.code 同值。 */
private String channelCode;
/** 通道镜头类型:vl 可见光、ir 红外(热成像),另有 ptz 云台、ng 导航等。 */
private String channelType;
}

+ 13
- 8
src/main/resources/mapper/ivs/BasedataEqpbookChannelMapper.xml View File

@ -5,17 +5,22 @@
<mapper namespace="com.inspect.nvr.ivs.mapper.BasedataEqpbookChannelMapper">
<!--
按完整摄像机编码读取台账镜头类型。channel_code 与 ivs_camera.code 同为
按完整摄像机编码批量读取台账镜头类型。channel_code 与 ivs_camera.code 同为
“设备编码#域编码”,台账中同一编码理论上只有一条有效记录;出现历史重复数据时
取最新的一条,避免旧记录决定镜头类型。命中 ivs_camera.code 一样的等值条件,
因此保留 channel_code 上的索引可用性,不做 TRIM 包装。
按 channel_id 倒序排在同一编码前面,调用方对每个编码保留首条即可取到最新记录。
命中 channel_code 上的等值条件,因此保留索引可用性,不做 TRIM 包装。
-->
<select id="selectChannelTypeByChannelCode" resultType="java.lang.String">
SELECT channel_type
<select id="selectChannelTypesByChannelCodes"
resultType="com.inspect.nvr.ivs.pojo.BasedataEqpbookChannel">
SELECT
channel_code,
channel_type
FROM basedata_eqpbook_channel
WHERE channel_code = #{channelCode}
ORDER BY channel_id DESC
LIMIT 1
WHERE channel_code IN
<foreach collection="channelCodes" item="channelCode" open="(" separator="," close=")">
#{channelCode}
</foreach>
ORDER BY channel_code, channel_id DESC
</select>
</mapper>

Loading…
Cancel
Save