Browse Source

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

master
wangguangyuan 4 days 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_typevl 可见光 / 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_typevl/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