|
|
|
@ -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); |
|
|
|
} |
|
|
|
|
|
|
|
/** 取文本中第一行非空内容。 */ |
|
|
|
|