|
|
|
@ -11,6 +11,7 @@ import com.inspect.nvr.hik.domain.HikNvrLiveStreamResponse; |
|
|
|
import com.inspect.nvr.hik.domain.HikNvrPlaybackRequest; |
|
|
|
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.IvsCamera; |
|
|
|
import com.inspect.nvr.ivs.service.IvsOpenApiService; |
|
|
|
@ -80,6 +81,10 @@ public class HikNvrLiveStreamService { |
|
|
|
private static final String SOURCE_CAMERA = "camera"; |
|
|
|
/** 取流来源:经 IVS/NVR(含 CVR 等中心存储设备)转发。 */ |
|
|
|
private static final String SOURCE_NVR = "nvr"; |
|
|
|
/** 台账 channel_type:红外(热成像)。 */ |
|
|
|
private static final String CHANNEL_TYPE_INFRARED = "ir"; |
|
|
|
/** 台账 channel_type:可见光。 */ |
|
|
|
private static final String CHANNEL_TYPE_VISIBLE = "vl"; |
|
|
|
/** 从 SDK 异常链中提取海康原始错误码。 */ |
|
|
|
private static final Pattern SDK_ERROR_PATTERN = |
|
|
|
Pattern.compile("错误码[::]\\s*(\\d+)"); |
|
|
|
@ -102,6 +107,9 @@ public class HikNvrLiveStreamService { |
|
|
|
/** 复用既有海康 RTSP 地址构造器,保证直连摄像机的鉴权编码规则一致。 */ |
|
|
|
@Resource |
|
|
|
private HikPlaybackUrlBuilder urlBuilder; |
|
|
|
/** 读取 basedata_eqpbook_channel.channel_type,按台账判定双光谱相机镜头类型。 */ |
|
|
|
@Resource |
|
|
|
private BasedataEqpbookChannelMapper eqpbookChannelMapper; |
|
|
|
|
|
|
|
/** FFmpeg 可执行文件路径。 */ |
|
|
|
private String ffmpegPath; |
|
|
|
@ -145,6 +153,8 @@ public class HikNvrLiveStreamService { |
|
|
|
private String cameraPassword; |
|
|
|
/** 直连摄像机 RTSP 端口连通性探测超时。 */ |
|
|
|
private int cameraProbeTimeoutMillis; |
|
|
|
/** 台账 channel_type=ir(热成像)对应的相机内通道号。 */ |
|
|
|
private int irCameraChannel; |
|
|
|
/** 同一 IP 多通道相机的相机内通道分配顺序:asc / desc。 */ |
|
|
|
private String cameraChannelOrder; |
|
|
|
/** 已确认直连鉴权失败(相机凭据与 NVR 不同)的摄像机 IP,避免反复浪费一次尝试。 */ |
|
|
|
@ -200,6 +210,8 @@ public class HikNvrLiveStreamService { |
|
|
|
? "" : config.getCameraPassword(); |
|
|
|
// 探测必须显著短于启动时限,避免相机不可达时拖慢起流。 |
|
|
|
this.cameraProbeTimeoutMillis = Math.max(100, config.getCameraProbeTimeoutMillis()); |
|
|
|
// 台账 channel_type=ir 的相机内通道号,只接受 1/2,其他取值按默认的 2 处理。 |
|
|
|
this.irCameraChannel = config.getIrCameraChannel() == 1 ? 1 : 2; |
|
|
|
// 多通道相机(双光谱等)的相机内通道分配顺序,默认按平台通道编码升序。 |
|
|
|
this.cameraChannelOrder = "desc".equalsIgnoreCase( |
|
|
|
config.getCameraChannelOrder() == null ? "" : config.getCameraChannelOrder().trim()) |
|
|
|
@ -1355,10 +1367,19 @@ public class HikNvrLiveStreamService { |
|
|
|
* 计算平台通道对应的相机内通道号。 |
|
|
|
* |
|
|
|
* <p>同一台多镜头相机(双光谱:可见光 + 热成像)在平台上占多个通道,直连 RTSP 时 |
|
|
|
* 必须带上相机自己的通道号,否则两个平台通道会拉到同一个镜头。优先按平台通道名判断 |
|
|
|
* 镜头类型(热成像/测温 → 相机通道 1,可见光 → 相机通道 2,与海康双光谱默认一致), |
|
|
|
* 名称无法区分时按平台通道编码顺序分配 1..N;整站顺序相反时用 |
|
|
|
* {@code hik.playback.live.camera-channel-order=desc} 反转。</p> |
|
|
|
* 必须带上相机自己的通道号,否则两个平台通道会拉到同一个镜头。判定顺序:</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> |
|
|
|
* </ol> |
|
|
|
* |
|
|
|
* <p>只有同 IP 多通道(真双光谱机身机)才做镜头判定:单通道设备的相机内通道号 |
|
|
|
* 恒为 1,单镜头热成像相机套用镜头映射反而会取到不存在的通道。</p> |
|
|
|
* |
|
|
|
* @param cameraCode 当前平台通道编码 |
|
|
|
* @param devIp 摄像机 IP |
|
|
|
@ -1374,10 +1395,18 @@ 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 升序返回,包含当前通道自己。 |
|
|
|
@ -1390,39 +1419,90 @@ public class HikNvrLiveStreamService { |
|
|
|
} |
|
|
|
} |
|
|
|
boolean reverse = "desc".equals(cameraChannelOrder); |
|
|
|
// 先按名称判断镜头类型:双光谱相机两个通道名通常能区分可见光/热成像。 |
|
|
|
// 台账没有可用的镜头类型时退回名称判断。 |
|
|
|
int namedChannel = cameraChannelByThermalName(cameraName); |
|
|
|
if (namedChannel > 0) { |
|
|
|
// 顺序反转时交换通道 1 与通道 2。 |
|
|
|
return reverse ? (namedChannel == 1 ? 2 : 1) : namedChannel; |
|
|
|
return namedChannel; |
|
|
|
} |
|
|
|
if (index < 0) { |
|
|
|
// 当前通道不在同 IP 列表中(数据异常)时退回相机通道 1。 |
|
|
|
return 1; |
|
|
|
} |
|
|
|
// 名称无法区分(例如两个通道都叫“双光谱”)时按平台通道顺序分配。 |
|
|
|
// 名称也无法区分(例如两个通道都叫“双光谱”)时按平台通道顺序分配。 |
|
|
|
int ordered = reverse ? peers.size() - 1 - index : index; |
|
|
|
return ordered + 1; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 读取台账 basedata_eqpbook_channel.channel_type。 |
|
|
|
* |
|
|
|
* @param cameraCode IVS 完整摄像机编码 |
|
|
|
* @return 台账通道类型(vl/ir 等);无记录、字段为空或查询异常时返回 null |
|
|
|
*/ |
|
|
|
private String resolveChannelType(String cameraCode) { |
|
|
|
try { |
|
|
|
String channelType = eqpbookChannelMapper.selectChannelTypeByChannelCode(cameraCode); |
|
|
|
// 台账没有该编码时保持为空,由调用方回退名称与顺序判定。 |
|
|
|
return hasText(channelType) ? channelType.trim() : null; |
|
|
|
} catch (RuntimeException queryFailure) { |
|
|
|
// 台账不是取流必需数据,查询失败只记录原因,不阻断实时预览。 |
|
|
|
log.warn("[live] 读取basedata_eqpbook_channel失败,回退名称判定 cameraCode={}:{}", |
|
|
|
cameraCode, queryFailure.getMessage()); |
|
|
|
return null; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 按台账通道类型返回可见光/热成像对应的相机内通道号。 |
|
|
|
* |
|
|
|
* @param channelType 台账 channel_type,vl 可见光 / ir 红外热成像 |
|
|
|
* @return {@code ir} 返回配置的相机内通道号,{@code vl} 返回另一个通道;无法判定返回 0 |
|
|
|
*/ |
|
|
|
private int cameraChannelByChannelType(String channelType) { |
|
|
|
if (!hasText(channelType)) { |
|
|
|
return 0; |
|
|
|
} |
|
|
|
String type = channelType.toLowerCase(Locale.ROOT); |
|
|
|
if (CHANNEL_TYPE_INFRARED.equals(type)) { |
|
|
|
return irCameraChannel; |
|
|
|
} |
|
|
|
if (CHANNEL_TYPE_VISIBLE.equals(type)) { |
|
|
|
return otherCameraChannel(irCameraChannel); |
|
|
|
} |
|
|
|
// ptz/ng 等与镜头无关的类型不参与判定,交给名称和顺序兜底。 |
|
|
|
return 0; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 返回双光谱相机另一个镜头的相机内通道号。 |
|
|
|
* |
|
|
|
* @param cameraChannel 已确定的相机内通道号 |
|
|
|
* @return 双光谱相机只有两个镜头,返回 1 与 2 中的另一个 |
|
|
|
*/ |
|
|
|
private int otherCameraChannel(int cameraChannel) { |
|
|
|
return cameraChannel == 1 ? 2 : 1; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 按通道名判断镜头类型并返回对应的相机内通道号。 |
|
|
|
* |
|
|
|
* <p>只在台账 {@code channel_type} 缺失时使用:部分平台通道名带“热成像/可见光” |
|
|
|
* 字样,但名称由台账人员维护、可能两个通道同名,因此不作为首选依据。</p> |
|
|
|
* |
|
|
|
* @param cameraName 平台通道名 |
|
|
|
* @return 热成像/测温返回 1,可见光返回 2,无法判断返回 0 |
|
|
|
* @return 热成像/测温返回配置的相机内通道号,可见光返回另一个通道,无法判断返回 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; |
|
|
|
return irCameraChannel; |
|
|
|
} |
|
|
|
if (name.contains("可见光") || name.contains("optical") |
|
|
|
|| name.contains("visual") || name.contains("白光")) { |
|
|
|
return 2; |
|
|
|
return otherCameraChannel(irCameraChannel); |
|
|
|
} |
|
|
|
return 0; |
|
|
|
} |
|
|
|
|