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 e9d5c0ff..8b70d7b5 100644 --- a/src/main/java/com/inspect/nvr/hik/service/HikNvrLiveStreamService.java +++ b/src/main/java/com/inspect/nvr/hik/service/HikNvrLiveStreamService.java @@ -371,7 +371,11 @@ public class HikNvrLiveStreamService { return toStoppedResponse(pending, "实时预览启动已取消"); } // 第二次尝试前主动重建海康 SDK 登录会话,清除缓存中的僵死 userID。 - if (attempt == 2 && usesHikLogin(pending.target.getNvrInfo())) { + // 设备端明确拒绝建立实时流(RTSP 与 SDK 登录无关)时重登没有意义, + // 只会在设备上产生额外的登录/登出抖动,因此这类失败跳过重登。 + if (attempt == 2 && usesHikLogin(pending.target.getNvrInfo()) + && !(lastFailure != null + && isDeviceRefusal(lastFailure.getErrorCode()))) { try { // 先登出旧句柄再登录,后续 IVS RTSP 解析会复用这次新会话。 hikLoginService.relogin(pending.target.getNvrInfo()); @@ -1067,27 +1071,36 @@ public class HikNvrLiveStreamService { return line.isEmpty() ? "" : ";原始信息:" + line; } - /** 取 FFmpeg stderr 中最后一条包含失败语义的行。 */ + /** 取 FFmpeg stderr 中最能说明设备拒绝原因的一行。 */ private String lastErrorLine(String ffmpegLog) { // 日志缺失时没有可提取的原文。 if (ffmpegLog == null || ffmpegLog.isEmpty()) { return ""; } - String matched = ""; - // 逐行扫描并保留最后一条命中行,FFmpeg 的最后一条错误通常最具体。 + String best = ""; + int bestScore = 0; + // 逐行评分:RTSP 状态行(method SETUP failed: 500 ...)最能说明设备为什么拒绝, + // 其次是普通 error/failed 行;同分保留后出现的一行,FFmpeg 的最后一条通常更具体。 for (String raw : ffmpegLog.split("\\r?\\n")) { String line = raw.trim(); if (line.isEmpty()) { continue; } String lower = line.toLowerCase(Locale.ROOT); - if (lower.contains("error") || lower.contains("failed") + int score = 0; + if (lower.contains("method ") && lower.contains("failed")) { + score = 3; + } else if (lower.contains("error") || lower.contains("failed") || lower.contains("denied") || lower.contains("refused") || lower.contains("timeout") || lower.contains("timed out")) { - matched = line; + score = 2; + } + if (score > 0 && score >= bestScore) { + best = line; + bestScore = score; } } - return truncate(matched); + return truncate(best); } /** 取文本中第一行非空内容。 */