From 1a12bde368725a19326b1da5a44da5f2d3f0f31f Mon Sep 17 00:00:00 2001 From: WangGuangYuan Date: Tue, 20 Oct 2026 17:31:17 +0800 Subject: [PATCH] =?UTF-8?q?fix:=E5=A4=84=E7=90=86=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E8=B6=85=E6=97=B6=20/=20timeout=20of=208000ms=20exceeded?= =?UTF-8?q?=EF=BC=8CNVR=20=E7=9A=84=E8=B5=B7=E6=B5=81=E7=94=B1=E5=AE=8C?= =?UTF-8?q?=E5=85=A8=E4=B8=B2=E8=A1=8C=E7=9A=84=E6=94=B9=E6=88=90=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=202=20=E8=B7=AF=E5=B9=B6=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hik/service/HikNvrLiveStreamService.java | 74 ++++++++++++------- .../nvr/hik/vo/HikNvrLiveStreamConfigVo.java | 18 +++++ 2 files changed, 65 insertions(+), 27 deletions(-) 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 0004b335..a1c8306f 100644 --- a/src/main/java/com/inspect/nvr/hik/service/HikNvrLiveStreamService.java +++ b/src/main/java/com/inspect/nvr/hik/service/HikNvrLiveStreamService.java @@ -127,10 +127,14 @@ public class HikNvrLiveStreamService { private String zlmApiSecret; /** 单个实时流从启动到明确成功或失败的总时限。 */ private long startupTimeoutMillis; + /** 首次起流时同步等待结果的最长时间;超时后返回 STARTING 由页面轮询状态。 */ + private long responseWaitMillis; /** 同一 NVR 相邻起流握手的最小间隔。 */ private long startSpacingMillis; /** 每台 NVR 等待队列的最大任务数。 */ private int maxStartQueueSize; + /** 同一台 NVR 允许同时进行的起流任务数。 */ + private int maxConcurrentStartsPerNvr; /** ZLMediaKit API 单次连接和读取超时。 */ private int zlmApiTimeoutMillis; /** 单路实时流在返回失败前的最大尝试次数。 */ @@ -185,10 +189,15 @@ public class HikNvrLiveStreamService { ? "" : config.getApiSecret().trim(); // 最低一秒避免正常 LAN 握手被误杀;现场实测流注册需 2~4 秒、闪断后更慢,默认 10 秒。 this.startupTimeoutMillis = Math.max(1000L, config.getStartupTimeoutMillis()); + // 同步返回必须短于页面 8 秒请求超时,超出部分由前端轮询状态接口补上。 + this.responseWaitMillis = Math.max(1000L, Math.min( + config.getResponseWaitMillis(), this.startupTimeoutMillis)); // 相邻握手间隔限制在不小于零,默认使用现场验证稳定的 600ms。 this.startSpacingMillis = Math.max(0L, config.getStartSpacingMillis()); // 至少保留一个等待位;默认八个等待位加一个执行位可接收九路同时起流。 this.maxStartQueueSize = Math.max(1, config.getMaxStartQueueSize()); + // 至少一个执行位;默认 2 允许等待出流阶段重叠,握手间隔仍由 start-spacing 保证。 + this.maxConcurrentStartsPerNvr = Math.max(1, config.getMaxConcurrentStartsPerNvr()); // ZLM 探测本身必须显著短于总启动时限,避免探测请求反向拖死队列。 this.zlmApiTimeoutMillis = Math.max(100, config.getZlmApiTimeoutMillis()); // 设备端在 SETUP 阶段的拒绝现场实测是可恢复的,因此允许有限次退避重试。 @@ -280,9 +289,10 @@ public class HikNvrLiveStreamService { // 新起流覆盖该摄像机上一次失败,状态接口从此返回本次任务状态。 failedStarts.remove(sessionKey); String nvrKey = nvrInfo.getNvrIp().trim().toLowerCase(Locale.ROOT); - // 为目标 NVR 创建或复用有界单线程队列,其他 NVR 不受该设备阻塞影响。 + // 为目标 NVR 创建或复用有界起流通道,其他 NVR 不受该设备阻塞影响。 NvrStartLane lane = startLanes.computeIfAbsent( - nvrKey, key -> new NvrStartLane(key, maxStartQueueSize)); + nvrKey, key -> new NvrStartLane( + key, maxStartQueueSize, maxConcurrentStartsPerNvr)); boolean firstInLane; try { // 同步提交计数,准确判断本请求是否是当前 NVR 的首个任务。 @@ -308,11 +318,16 @@ public class HikNvrLiveStreamService { return toPendingResponse(pending, "实时预览排队中", publicBaseUrl); } try { - // 首个任务最多等待总启动时限加少量调度余量,避免 Tomcat 请求长时间挂死。 + // 首个任务只阻塞到同步返回时限:即使起流仍在进行,也要先给页面一个响应, + // 否则会撞上页面 8 秒超时,让用户看到一条“系统接口请求超时”。 return pending.future.get( - startupTimeoutMillis + 500L, TimeUnit.MILLISECONDS); + responseWaitMillis + 500L, TimeUnit.MILLISECONDS); } catch (TimeoutException timeout) { - // 原生 SDK 若超出预期仍在工作,HTTP 先返回 STARTING,后台结果由状态接口提供。 + // 起流仍在进行时返回 STARTING,后台结果由状态接口提供。 + log.info("[live] 同步返回超时转状态轮询 cameraCode={} nvrIp={} waitMillis={}", + pending.target.getCameraCode(), + pending.target.getNvrInfo().getNvrIp(), + responseWaitMillis); return toPendingResponse(pending, "实时预览启动中", publicBaseUrl); } catch (InterruptedException interrupted) { // 请求线程被中断时保留后台任务,并恢复线程中断标志。 @@ -2090,20 +2105,23 @@ public class HikNvrLiveStreamService { } } - /** 每台 NVR 独立的有界单线程起流队列和握手节流状态。 */ + /** 每台 NVR 独立的有界起流通道和握手节流状态。 */ private static final class NvrStartLane { - /** 串行执行该 NVR 起流任务的有界线程池。 */ + /** 执行该 NVR 起流任务的有界线程池,并发度由配置决定。 */ private final ThreadPoolExecutor executor; /** 最近一次 RTSP 起流尝试的开始时间。 */ private final AtomicLong lastStartAt = new AtomicLong(); + /** 只用于串行分配握手时隙,避免等待期间占住 outstanding 的监视器。 */ + private final Object spacingLock = new Object(); /** 已提交但未完成的任务数,用于判断 HTTP 是否需要等待首个结果。 */ private int outstanding; - /** 为一台 NVR 创建单线程、有界等待队列。 */ - private NvrStartLane(String nvrKey, int queueSize) { + /** 为一台 NVR 创建有界等待队列,允许指定数量的起流任务并行等待出流。 */ + private NvrStartLane(String nvrKey, int queueSize, int concurrency) { + int permits = Math.max(1, concurrency); this.executor = new ThreadPoolExecutor( - 1, - 1, + permits, + permits, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(queueSize), @@ -2151,25 +2169,27 @@ public class HikNvrLiveStreamService { * @return 截止时间前取得起流时隙时返回 true */ private boolean awaitSpacing(long deadline, long spacingMillis) { - long now = System.currentTimeMillis(); - long waitMillis = Math.max( - 0L, lastStartAt.get() + spacingMillis - now); - // 等待时间已经超过总启动时限时不再发起新的 NVR 握手。 - if (now + waitMillis >= deadline) { - return false; - } - // 需要节流时只阻塞该 NVR 的专用线程,不占用 Tomcat 请求线程。 - if (waitMillis > 0L) { - try { - Thread.sleep(waitMillis); - } catch (InterruptedException interrupted) { - // 应用关闭时恢复中断标志并取消本次握手。 - Thread.currentThread().interrupt(); + // 并发起流时两个线程可能同时到达,必须串行分配时隙,否则会同时发起握手的 。 + synchronized (spacingLock) { + long now = System.currentTimeMillis(); + long slotAt = Math.max(now, lastStartAt.get() + spacingMillis); + // 预留的时隙已经越过总启动时限时不再发起新的 NVR 握手。 + if (slotAt >= deadline) { return false; } + // 需要节流时只阻塞该 NVR 的起流线程,不占用 Tomcat 请求线程。 + if (slotAt > now) { + try { + Thread.sleep(slotAt - now); + } catch (InterruptedException interrupted) { + // 应用关闭时恢复中断标志并取消本次握手。 + Thread.currentThread().interrupt(); + return false; + } + } + lastStartAt.set(System.currentTimeMillis()); + return true; } - lastStartAt.set(System.currentTimeMillis()); - return true; } } } 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 1343e5c8..2c074533 100644 --- a/src/main/java/com/inspect/nvr/hik/vo/HikNvrLiveStreamConfigVo.java +++ b/src/main/java/com/inspect/nvr/hik/vo/HikNvrLiveStreamConfigVo.java @@ -27,12 +27,30 @@ public class HikNvrLiveStreamConfigVo { /** 实时流启动探测超时时间。现场实测流注册需 2~4 秒,网络闪断后更慢,默认 10 秒。 */ @Value("${hik.playback.live.startup-timeout-millis:10000}") private long startupTimeoutMillis = 10000L; + /** + * 首次起流时阻塞等待并直接返回播放结果的最长时间。 + * + *

页面只给起流请求 8 秒,超过就整条请求超时并弹出“系统接口请求超时”;后台起流 + * 仍会继续,结果由状态接口提供。因此同步等待必须明显短于 8 秒并留出网关转发余量, + * 超时后返回 STARTING,由页面轮询 /realtime/status 拿到播放地址。

+ */ + @Value("${hik.playback.live.response-wait-millis:6000}") + private long responseWaitMillis = 6000L; /** 同一 NVR 相邻 RTSP 握手的最小间隔。 */ @Value("${hik.playback.live.start-spacing-millis:600}") private long startSpacingMillis = 600L; /** 每台 NVR 等待起流的最大任务数,不含正在执行的任务。 */ @Value("${hik.playback.live.max-start-queue-size:8}") private int maxStartQueueSize = 8; + /** + * 同一台 NVR 允许同时进行的起流任务数。 + * + *

相邻握手间隔仍由 {@code start-spacing-millis} 保证;本参数只允许相邻任务在 + * “等待设备出流”阶段重叠,用于缩短一次打开多画面时最后一路的等待时间。 + * 设为 1 即恢复完全串行;现场若发现设备拒绝并发拉流,可调回 1。

+ */ + @Value("${hik.playback.live.max-concurrent-starts-per-nvr:2}") + private int maxConcurrentStartsPerNvr = 2; /** 单次 ZLMediaKit API 连接和读取超时时间。 */ @Value("${hik.playback.live.zlm-api-timeout-millis:300}") private int zlmApiTimeoutMillis = 300;