diff --git a/.env.example b/.env.example index 71e94233..35d90672 100644 --- a/.env.example +++ b/.env.example @@ -13,6 +13,18 @@ ZLM_HTTP_BASE_URL=http://192.168.1.200:18081 # An empty secret makes .../ui/actions/live/start fail fast with ZLM_PUSH_FAILED. ZLM_API_SECRET=change_me +# Live preview: how many simultaneous streams one NVR is allowed to serve through this service. +# Field measurements show some NVRs only accept 1 remote RTSP stream at a time; set this to the +# value measured for the site so that extra windows queue up instead of being refused by the device. +HIK_NVR_LIVE_MAX_SESSIONS_PER_NVR=1 + +# Live preview pulls RTSP directly from each camera (ivs_camera.dev_ip) first and automatically +# falls back to the NVR/CVR path when the camera is unreachable or rejects the credentials. +# Keep this enabled for sites whose recorder is a CVR (central storage) with a tiny remote +# forward quota; set to false to always stream through the NVR. +HIK_NVR_LIVE_PREFER_CAMERA_DIRECT=true +HIK_NVR_LIVE_CAMERA_RTSP_PORT=554 + # Maven mirror used while building the image MAVEN_MIRROR_URL=https://maven.aliyun.com/repository/public # MAVEN_MIRROR_URL=https://repo.maven.apache.org/maven2 diff --git a/docker-compose.yml b/docker-compose.yml index 5851bb55..e51dbeea 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,6 +24,18 @@ services: ZLM_HTTP_BASE_URL: ${ZLM_HTTP_BASE_URL:?remote_ZLM_HTTP_BASE_URL_required_in_.env} # 服务端调用 ZLMediaKit API 校验实时流是否注册;未配置时实时预览会直接返回配置错误。 ZLM_API_SECRET: ${ZLM_API_SECRET:-} + # 实时预览参数。注意:Compose 只把这里显式列出的变量传入容器,写在 .env 里但没在这里 + # 列出的 HIK_NVR_* 变量不会生效。默认值与 application.yml 保持一致,且必须非空, + # 否则 Spring 会把空字符串绑定到整型配置导致启动失败。 + # 单台 NVR 允许同时播放的实时流上限(现场实测部分 NVR 只给 1 路),0 表示不限制。 + HIK_NVR_LIVE_MAX_SESSIONS_PER_NVR: ${HIK_NVR_LIVE_MAX_SESSIONS_PER_NVR:-8} + HIK_NVR_LIVE_MAX_ATTEMPTS: ${HIK_NVR_LIVE_MAX_ATTEMPTS:-3} + HIK_NVR_LIVE_STOP_GRACE_MILLIS: ${HIK_NVR_LIVE_STOP_GRACE_MILLIS:-2000} + HIK_NVR_LIVE_IDLE_REAP_ENABLED: ${HIK_NVR_LIVE_IDLE_REAP_ENABLED:-true} + HIK_NVR_LIVE_IDLE_READER_TIMEOUT_MILLIS: ${HIK_NVR_LIVE_IDLE_READER_TIMEOUT_MILLIS:-600000} + # 实时预览优先直连摄像机取流(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} 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} diff --git a/src/main/java/com/inspect/nvr/hik/service/HikNvrLiveStreamService.java b/src/main/java/com/inspect/nvr/hik/service/HikNvrLiveStreamService.java index 8b70d7b5..e109f415 100644 --- a/src/main/java/com/inspect/nvr/hik/service/HikNvrLiveStreamService.java +++ b/src/main/java/com/inspect/nvr/hik/service/HikNvrLiveStreamService.java @@ -8,8 +8,11 @@ import com.inspect.nvr.domain.ivs.IvsRtspUrlResponse; import com.inspect.nvr.domain.ivs.IvsVideoRtspVo; import com.inspect.nvr.hik.domain.HikNvrCameraTarget; 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.IvsCameraMapper; +import com.inspect.nvr.ivs.pojo.IvsCamera; import com.inspect.nvr.ivs.service.IvsOpenApiService; import com.inspect.nvr.service.HikLoginService; import lombok.extern.slf4j.Slf4j; @@ -23,6 +26,8 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.Socket; import java.net.URL; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; @@ -33,6 +38,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Set; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; @@ -70,6 +76,10 @@ public class HikNvrLiveStreamService { private static final String NVR_LOGIN_FAILED = "NVR_LOGIN_FAILED"; /** 单台 NVR 起流队列已满时返回的稳定错误码。 */ private static final String NVR_START_QUEUE_FULL = "NVR_START_QUEUE_FULL"; + /** 取流来源:直连摄像机。 */ + private static final String SOURCE_CAMERA = "camera"; + /** 取流来源:经 IVS/NVR(含 CVR 等中心存储设备)转发。 */ + private static final String SOURCE_NVR = "nvr"; /** 从 SDK 异常链中提取海康原始错误码。 */ private static final Pattern SDK_ERROR_PATTERN = Pattern.compile("错误码[::]\\s*(\\d+)"); @@ -86,6 +96,12 @@ public class HikNvrLiveStreamService { /** 实时拉流配置。 */ @Resource private HikNvrLiveStreamConfigVo config; + /** 读取 ivs_camera.dev_ip,用于优先直连摄像机取流。 */ + @Resource + private IvsCameraMapper ivsCameraMapper; + /** 复用既有海康 RTSP 地址构造器,保证直连摄像机的鉴权编码规则一致。 */ + @Resource + private HikPlaybackUrlBuilder urlBuilder; /** FFmpeg 可执行文件路径。 */ private String ffmpegPath; @@ -119,6 +135,18 @@ public class HikNvrLiveStreamService { private boolean idleReapEnabled; /** 无人观看多少毫秒后回收实时流。 */ private long idleReaderTimeoutMillis; + /** 是否优先直连摄像机取流。 */ + private boolean preferCameraDirect; + /** 直连摄像机使用的 RTSP 端口。 */ + private int cameraRtspPort; + /** 直连摄像机使用的账号;为空时沿用 NVR 账号。 */ + private String cameraUsername; + /** 直连摄像机使用的密码;为空时沿用 NVR 密码。 */ + private String cameraPassword; + /** 直连摄像机 RTSP 端口连通性探测超时。 */ + private int cameraProbeTimeoutMillis; + /** 已确认直连鉴权失败(相机凭据与 NVR 不同)的摄像机 IP,避免反复浪费一次尝试。 */ + private final Set cameraDirectAuthFailed = ConcurrentHashMap.newKeySet(); /** 按 IVS 完整 cameraCode 保存本服务拥有的 FFmpeg 拉流会话。 */ private final Map sessions = new ConcurrentHashMap<>(); @@ -159,6 +187,17 @@ public class HikNvrLiveStreamService { this.maxSessionsPerNvr = Math.max(0, config.getMaxSessionsPerNvr()); this.idleReapEnabled = config.isIdleReapEnabled(); this.idleReaderTimeoutMillis = Math.max(10000L, config.getIdleReaderTimeoutMillis()); + // CVR 等设备远程转发额度很小,默认优先直连摄像机取流,不可达或鉴权失败时无缝回退 NVR。 + this.preferCameraDirect = config.isPreferCameraDirect(); + // RTSP 端口必须落在合法范围内,否则直连探测和地址拼装都会失败。 + this.cameraRtspPort = config.getCameraRtspPort() > 0 + && config.getCameraRtspPort() <= 65535 ? config.getCameraRtspPort() : 554; + this.cameraUsername = config.getCameraUsername() == null + ? "" : config.getCameraUsername().trim(); + this.cameraPassword = config.getCameraPassword() == null + ? "" : config.getCameraPassword(); + // 探测必须显著短于启动时限,避免相机不可达时拖慢起流。 + this.cameraProbeTimeoutMillis = Math.max(100, config.getCameraProbeTimeoutMillis()); } /** @@ -387,13 +426,6 @@ public class HikNvrLiveStreamService { if (attempt > 1 && !lane.awaitRelease(deadline, retryWaitMillis(attempt))) { break; } - // 本服务限制单台 NVR 的并发实时流数量,避免一次把设备远程预览预算打满。 - if (!awaitSessionQuota(nvrIp, deadline)) { - throw HikNvrPlaybackException.conflict( - NVR_LIVE_SESSION_LIMIT, - NVR_LIVE_SESSION_LIMIT + ":该NVR并发实时流已达上限" - + maxSessionsPerNvr + "路,请先关闭其他窗口后重试"); - } // 相同 NVR 的相邻握手保持配置间隔,避免设备瞬时并发 SETUP 500。 if (!lane.awaitSpacing(deadline, startSpacingMillis)) { break; @@ -401,11 +433,25 @@ public class HikNvrLiveStreamService { // 记录本次尝试前的日志长度,失败分类只看本次新增的 stderr,避免上一次尝试的原始错误干扰判断。 int logOffset = readLog(logFile).length(); - String rtspUrl; + String rtspUrl = null; + String source = SOURCE_NVR; + CameraDirectTarget cameraTarget = null; try { - // 通过 IVS 厂商路由生成当前码流的 RTSP 地址,海康路径会复用 SDK 登录布局。 - rtspUrl = resolveLiveRtspUrl( - pending.target.getCameraCode(), pending.streamType); + // 首次尝试优先直连摄像机:CVR(中心存储)等设备的远程转发额度很小, + // 直连摄像机可以完全绕开该瓶颈,多台相机之间也互不抢额度。 + if (preferCameraDirect && attempt == 1) { + cameraTarget = resolveCameraDirectTarget(pending.target, pending.streamType); + if (cameraTarget != null) { + rtspUrl = cameraTarget.rtspUrl; + source = SOURCE_CAMERA; + } + } + // 相机无直连地址、RTSP 端口不可达或已知鉴权失败时,保持原有 IVS/NVR 取流路径。 + if (rtspUrl == null) { + rtspUrl = resolveLiveRtspUrl( + pending.target.getCameraCode(), pending.streamType); + source = SOURCE_NVR; + } } catch (RuntimeException resolveFailure) { lastFailure = classifyFailure( readLogFrom(logFile, logOffset), ZlmProbeResult.ABSENT, resolveFailure); @@ -417,6 +463,15 @@ public class HikNvrLiveStreamService { } String rtmpUrl = rtmpBaseUrl + "/live/" + pending.streamId; + // 并发额度按"实际拉取的设备"统计:直连时是相机 IP,走转发时才是 NVR/CVR IP。 + // 否则直连摄像机取流会被所属 NVR 的额度限制住,等于没有绕开 CVR 的瓶颈。 + String streamHost = cameraTarget == null ? nvrIp : cameraTarget.devIp; + if (!awaitSessionQuota(streamHost, deadline)) { + throw HikNvrPlaybackException.conflict( + NVR_LIVE_SESSION_LIMIT, + NVR_LIVE_SESSION_LIMIT + ":取流设备" + streamHost + "的并发实时流已达上限" + + maxSessionsPerNvr + "路,请先关闭其他窗口后重试"); + } Process process; try { // 启动 FFmpeg 并把各次尝试的原始 stderr 追加到同一个每路日志文件。 @@ -434,6 +489,7 @@ public class HikNvrLiveStreamService { LiveSession session = new LiveSession( pending.target.getCameraCode(), pending.target.getNvrInfo().getNvrIp(), + streamHost, pending.target.getChannel(), pending.streamType, pending.streamId, @@ -442,9 +498,10 @@ public class HikNvrLiveStreamService { pending.flvUrl); // 注册本地会话后,停止和状态请求即可立即定位并释放正在启动的进程。 sessions.put(sessionKey, session); - log.info("[live] started cameraCode={} nvrIp={} streamType={} streamId={} attempt={} log={}", - session.cameraCode, session.nvrIp, session.streamType, - session.streamId, attempt, logFile); + log.info("[live] started cameraCode={} nvrIp={} host={} source={} streamType={} streamId={} attempt={} log={}", + session.cameraCode, session.nvrIp, + cameraTarget == null ? session.nvrIp : cameraTarget.devIp, + source, session.streamType, session.streamId, attempt, logFile); long remaining = Math.max(0L, deadline - System.currentTimeMillis()); // 第一次最多使用剩余时间的一半,为失效登录并重试保留预算。 @@ -464,6 +521,12 @@ public class HikNvrLiveStreamService { // 未注册或进程退出时立即销毁 FFmpeg,快速断开 NVR RTSP 会话。 stopInternal(sessionKey); lastFailure = classifyFailure(readLogFrom(logFile, logOffset), probe, null); + // 相机直连鉴权失败说明该相机凭据与 NVR 不同,记住后在后续起流中直接走 NVR,避免反复浪费一次尝试。 + if (cameraTarget != null && NVR_LOGIN_FAILED.equals(lastFailure.getErrorCode())) { + cameraDirectAuthFailed.add(cameraTarget.devIp); + log.warn("[live] 相机直连鉴权失败,后续改用NVR取流 cameraCode={} cameraIp={}", + pending.target.getCameraCode(), cameraTarget.devIp); + } // 设备端拒绝建立实时流属于瞬时可恢复的故障,允许退避重试; // 登录、网络、通道无数据等其他分类保持原有的一次重登重试。 int attemptLimit = isDeviceRefusal(lastFailure.getErrorCode()) @@ -1141,20 +1204,20 @@ public class HikNvrLiveStreamService { } /** - * 等待目标 NVR 的并发实时流名额。 + * 等待指定取流设备的并发实时流名额。 * - * @param nvrIp 目标 NVR 地址 + * @param host 实际取流设备地址(相机 IP 或 NVR/CVR IP) * @param deadline 本次起流总截止时间 * @return 取得名额或不限制并发时返回 true */ - private boolean awaitSessionQuota(String nvrIp, long deadline) { + private boolean awaitSessionQuota(String host, long deadline) { // 上限为 0 表示不限制,保持历史行为兼容。 if (maxSessionsPerNvr <= 0) { return true; } // 名额未满即可继续;已满则等其他窗口释放,避免直接冲击设备。 while (System.currentTimeMillis() < deadline) { - if (activeSessionsOn(nvrIp) < maxSessionsPerNvr) { + if (activeSessionsOn(host) < maxSessionsPerNvr) { return true; } try { @@ -1168,16 +1231,16 @@ public class HikNvrLiveStreamService { } /** - * 统计仍在运行且属于指定 NVR 的本地实时流数量。 + * 统计仍在运行且正在从指定设备取流的本地实时流数量。 * - * @param nvrIp 目标 NVR 地址 + * @param host 实际取流设备地址(相机 IP 或 NVR/CVR IP) * @return 本地活跃会话数 */ - private int activeSessionsOn(String nvrIp) { + private int activeSessionsOn(String host) { int count = 0; - // 本地会话表规模很小,逐条比对 NVR 地址即可。 + // 本地会话表规模很小,逐条比对取流设备地址即可。 for (LiveSession session : sessions.values()) { - if (session.isAlive() && nvrIp.equalsIgnoreCase(session.nvrIp)) { + if (session.isAlive() && host.equalsIgnoreCase(session.sourceHost)) { count++; } } @@ -1219,6 +1282,78 @@ public class HikNvrLiveStreamService { return offset >= text.length() ? "" : text.substring(offset); } + /** + * 解析摄像机直连取流目标。 + * + *

直连地址来自 {@code ivs_camera.dev_ip},凭据默认沿用所属 NVR 的数据库记录 + * (与项目既有直连抓图、3D 定位的做法一致),可用配置覆盖。只有在地址存在且 RTSP + * 端口探测可达时才返回结果,其余情况返回 {@code null} 让调用方回退 NVR 取流。

+ * + * @param target 已解析的摄像机目标 + * @param streamType 主码流 1 或子码流 2 + * @return 可直连的摄像机目标;不可直连时返回 null + */ + private CameraDirectTarget resolveCameraDirectTarget( + HikNvrCameraTarget target, int streamType) { + IvsCamera camera; + try { + // 摄像机表查询失败不能影响实时预览,直接回退 NVR 路径。 + camera = ivsCameraMapper.selectByCode(target.getCameraCode()); + } catch (RuntimeException queryFailure) { + log.warn("[live] 读取 ivs_camera 失败,回退NVR取流 cameraCode={}:{}", + target.getCameraCode(), queryFailure.getMessage()); + return null; + } + // 没有设备 IP 时无法直连,保持原有 NVR 取流行为。 + if (camera == null || !hasText(camera.getDevIP())) { + return null; + } + String devIp = camera.getDevIP().trim(); + // 已确认该相机凭据不可用时不再重复尝试直连。 + if (cameraDirectAuthFailed.contains(devIp)) { + return null; + } + // RTSP 端口不可达(未开放或被网络隔离)时直接回退,避免白等一次尝试。 + if (!isCameraRtspReachable(devIp)) { + return null; + } + NvrInfo nvrInfo = target.getNvrInfo(); + String username = hasText(cameraUsername) + ? cameraUsername : nvrInfo.getAccount(); + String password = hasText(cameraPassword) + ? cameraPassword : nvrInfo.getPassword(); + // 账号缺失时无法建立 RTSP 会话,回退 NVR 路径由 IVS 侧给出明确错误。 + if (!hasText(username) || password == null) { + return null; + } + // 复用统一 URL 构造器,保证中文或特殊字符账号的百分号编码规则与 NVR 路径一致。 + HikNvrPlaybackRequest cameraRequest = HikNvrPlaybackRequest.builder() + .nvrIp(devIp) + .rtspPort(cameraRtspPort) + .username(username) + .password(password) + .streamType(streamType) + .build(); + // 直连摄像机只有一个通道,trackId = 1 * 100 + 码流类型,即 101/102。 + return new CameraDirectTarget(devIp, urlBuilder.buildLive(cameraRequest, 1)); + } + + /** + * 探测摄像机 RTSP 端口是否可达。 + * + * @param devIp 摄像机 IP + * @return 限定时间内可建立 TCP 连接时返回 true + */ + private boolean isCameraRtspReachable(String devIp) { + try (Socket socket = new Socket()) { + socket.connect(new InetSocketAddress(devIp, cameraRtspPort), cameraProbeTimeoutMillis); + return true; + } catch (Exception unreachable) { + // 不可达、被拒绝或超时都按"不可直连"处理,由调用方回退 NVR。 + return false; + } + } + /** * 调用现有 IVS 实时媒体能力取得 cameraCode 对应的 RTSP 地址。 * @@ -1496,6 +1631,20 @@ public class HikNvrLiveStreamService { UNAVAILABLE } + /** 一次直连摄像机取流的目标地址。 */ + private static final class CameraDirectTarget { + /** 摄像机 IP,用于鉴权失败后记忆该相机。 */ + private final String devIp; + /** 可直接交给 FFmpeg 的摄像机 RTSP 地址。 */ + private final String rtspUrl; + + /** 保存摄像机 IP 与直连 RTSP 地址。 */ + private CameraDirectTarget(String devIp, String rtspUrl) { + this.devIp = devIp; + this.rtspUrl = rtspUrl; + } + } + /** ZLMediaKit 单次流查询的完整结果,包含注册状态和当前观众数量。 */ private static final class ZlmStreamProbe { /** 查询结果分类。 */ @@ -1514,8 +1663,10 @@ public class HikNvrLiveStreamService { private static final class LiveSession { /** IVS 完整摄像机编码。 */ private final String cameraCode; - /** 摄像机所属 NVR IP。 */ + /** 摄像机所属 NVR/CVR IP,用于定位设备与展示。 */ private final String nvrIp; + /** 本路流实际拉取的设备地址:直连摄像机时为相机 IP,经转发时为 NVR/CVR IP。 */ + private final String sourceHost; /** 页面逻辑通道号。 */ private final Integer channel; /** 主码流 1 或子码流 2。 */ @@ -1535,6 +1686,7 @@ public class HikNvrLiveStreamService { private LiveSession( String cameraCode, String nvrIp, + String sourceHost, Integer channel, int streamType, String streamId, @@ -1543,6 +1695,7 @@ public class HikNvrLiveStreamService { String flvUrl) { this.cameraCode = cameraCode; this.nvrIp = nvrIp; + this.sourceHost = sourceHost; this.channel = channel; this.streamType = streamType; this.streamId = streamId; diff --git a/src/main/java/com/inspect/nvr/hik/vo/HikNvrLiveStreamConfigVo.java b/src/main/java/com/inspect/nvr/hik/vo/HikNvrLiveStreamConfigVo.java index 776e6332..2f574e8e 100644 --- a/src/main/java/com/inspect/nvr/hik/vo/HikNvrLiveStreamConfigVo.java +++ b/src/main/java/com/inspect/nvr/hik/vo/HikNvrLiveStreamConfigVo.java @@ -62,4 +62,24 @@ public class HikNvrLiveStreamConfigVo { /** 无人观看多少毫秒后回收实时流。 */ @Value("${hik.playback.live.idle-reader-timeout-millis:600000}") private long idleReaderTimeoutMillis = 600000L; + /** + * 实时预览是否优先直连摄像机取流。 + * + *

CVR(中心存储)等设备的远程转发额度很小,直连摄像机可以避开该瓶颈;直连不可达或 + * 鉴权失败时会自动回退到原有的 IVS/NVR 取流路径。

+ */ + @Value("${hik.playback.live.prefer-camera-direct:true}") + private boolean preferCameraDirect = true; + /** 直连摄像机使用的 RTSP 端口。 */ + @Value("${hik.playback.live.camera-rtsp-port:554}") + private int cameraRtspPort = 554; + /** 直连摄像机使用的账号;为空时沿用所属 NVR 的数据库账号。 */ + @Value("${hik.playback.live.camera-username:}") + private String cameraUsername = ""; + /** 直连摄像机使用的密码;为空时沿用所属 NVR 的数据库密码。 */ + @Value("${hik.playback.live.camera-password:}") + private String cameraPassword = ""; + /** 直连摄像机 RTSP 端口连通性探测超时时间。 */ + @Value("${hik.playback.live.camera-probe-timeout-millis:500}") + private int cameraProbeTimeoutMillis = 500; } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 9087f371..477d303f 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -66,13 +66,24 @@ hik: retry-release-wait-max-millis: ${HIK_NVR_LIVE_RETRY_RELEASE_WAIT_MAX_MILLIS:5000} # 停流时等待 FFmpeg 优雅退出(发送 RTSP TEARDOWN)的最长时间(毫秒)。 stop-grace-millis: ${HIK_NVR_LIVE_STOP_GRACE_MILLIS:2000} - # 单台 NVR 允许同时播放的实时流上限,0 表示不限制;请按设备远程预览预算调整。 + # 单个取流设备允许同时播放的实时流上限,0 表示不限制;请按设备远程预览预算调整。 + # 统计粒度是"实际拉取的设备":直连摄像机时为相机 IP,经转发时为 NVR/CVR IP。 max-sessions-per-nvr: ${HIK_NVR_LIVE_MAX_SESSIONS_PER_NVR:8} # 长时间无人观看(ZLMediaKit 观众数为 0)时回收实时流,避免浏览器异常退出后占住 NVR 会话。 idle-reap-enabled: ${HIK_NVR_LIVE_IDLE_REAP_ENABLED:true} idle-reader-timeout-millis: ${HIK_NVR_LIVE_IDLE_READER_TIMEOUT_MILLIS:600000} # 空闲回收扫描间隔(毫秒)。 idle-reap-interval-millis: ${HIK_NVR_LIVE_IDLE_REAP_INTERVAL_MILLIS:60000} + # 实时预览是否优先直连摄像机取流:CVR(中心存储)等设备的远程转发额度很小, + # 直连摄像机可绕开该瓶颈;地址缺失、端口不可达或鉴权失败时自动回退 NVR。 + prefer-camera-direct: ${HIK_NVR_LIVE_PREFER_CAMERA_DIRECT:true} + # 直连摄像机使用的 RTSP 端口。 + camera-rtsp-port: ${HIK_NVR_LIVE_CAMERA_RTSP_PORT:554} + # 直连摄像机的账号密码;留空表示沿用所属 NVR 在数据库中的凭据。 + camera-username: "${HIK_NVR_LIVE_CAMERA_USERNAME:}" + camera-password: "${HIK_NVR_LIVE_CAMERA_PASSWORD:}" + # 直连摄像机 RTSP 端口连通性探测超时(毫秒)。 + camera-probe-timeout-millis: ${HIK_NVR_LIVE_CAMERA_PROBE_TIMEOUT_MILLIS:500} clip: # 一分钟录像先由SDK下载到临时目录,再由FFmpeg统一封装为MP4。 ffmpeg-path: ${FFMPEG_PATH:C:/tools/ffmpeg/bin/ffmpeg.exe}