StreamSwitchLogAnalyzer.java
13.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
package com.genersoft.iot.vmp.jtt1078.subscriber;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 码流切换日志分析器
*
* 用于分析码流切换过程中可能出现的问题:
* 1. FFmpeg启动/关闭日志
* 2. 视频参数变化检测
* 3. 花屏/断流问题定位
* 4. 性能问题分析
*
* 使用方法:
* 1. 在出现问题是,查看控制台日志
* 2. 或者将日志保存到文件后分析
*
* 常见问题排查:
* - 如果看到"FFmpeg进程未能正常退出":可能是FFmpeg被阻塞
* - 如果看到"SPS变化但未重启":可能是冷却期内
* - 如果持续花屏:可能是SPS解析失败或FLV编码器问题
*/
public class StreamSwitchLogAnalyzer {
static Logger logger = LoggerFactory.getLogger(StreamSwitchLogAnalyzer.class);
/** 日志事件类型 */
public enum EventType {
FFmpeg_START("FFmpeg推流任务启动"),
FFmpeg_STOP("FFmpeg推流任务结束"),
FFmpeg_CLOSE("开始关闭FFmpeg推流"),
FFmpeg_RESTART("开始重启FFmpeg推流"),
SPS_CHANGE("检测到视频参数变化"),
SPS_HASH("SPS哈希变化"),
IFRAME_RECEIVE("收到新的I帧"),
STREAM_SWITCH("收到码流切换通知"),
COOLDOWN_SKIP("FFmpeg重启过于频繁,跳过本次重启"),
ERROR("错误"),
WARNING("警告");
private final String keyword;
EventType(String keyword) {
this.keyword = keyword;
}
public String getKeyword() {
return keyword;
}
}
/** 日志事件 */
public static class LogEvent {
public long timestamp;
public EventType type;
public String tag;
public String message;
public Map<String, Object> data;
public LogEvent(long timestamp, EventType type, String tag, String message) {
this.timestamp = timestamp;
this.type = type;
this.tag = tag;
this.message = message;
this.data = new HashMap<>();
}
@Override
public String toString() {
return String.format("[%s] [%s] [%s] %s",
new Date(timestamp), type.name(), tag, message);
}
}
/** 问题诊断结果 */
public static class DiagnosisResult {
public boolean hasProblem = false;
public String problemType; // 问题类型
public String description; // 问题描述
public String suggestion; // 解决建议
public List<LogEvent> relatedEvents = new ArrayList<>();
@Override
public String toString() {
if (!hasProblem) {
return "诊断结果:未发现问题";
}
return String.format(
"【问题诊断】\n" +
"问题类型: %s\n" +
"问题描述: %s\n" +
"解决建议: %s\n" +
"相关日志: %d条",
problemType, description, suggestion, relatedEvents.size()
);
}
}
/**
* 分析日志列表,诊断问题
*/
public static DiagnosisResult diagnose(List<String> rawLogs) {
DiagnosisResult result = new DiagnosisResult();
List<LogEvent> events = parseLogs(rawLogs);
if (events.isEmpty()) {
result.hasProblem = false;
result.description = "没有找到相关的日志记录";
return result;
}
// 检查问题1:FFmpeg重启过于频繁
List<LogEvent> cooldownSkips = filterEvents(events, EventType.COOLDOWN_SKIP);
if (cooldownSkips.size() >= 3) {
result.hasProblem = true;
result.problemType = "FFmpeg重启过于频繁";
result.description = String.format(
"在%d次码流切换中,有%d次因为冷却期被跳过,可能导致切换延迟",
events.size(), cooldownSkips.size()
);
result.suggestion = "建议将冷却时间从3000ms减少到1000ms,或者检查码流切换逻辑是否有问题";
result.relatedEvents.addAll(cooldownSkips);
return result;
}
// 检查问题2:FFmpeg启动后立即关闭
Map<String, List<LogEvent>> tagGroups = groupByTag(events);
for (Map.Entry<String, List<LogEvent>> entry : tagGroups.entrySet()) {
List<LogEvent> tagEvents = entry.getValue();
LogEvent firstStart = findFirst(tagEvents, EventType.FFmpeg_START);
LogEvent firstStop = findFirst(tagEvents, EventType.FFmpeg_STOP);
if (firstStart != null && firstStop != null) {
long duration = firstStop.timestamp - firstStart.timestamp;
if (duration < 1000) {
result.hasProblem = true;
result.problemType = "FFmpeg启动后立即关闭";
result.description = String.format(
"Tag[%s]的FFmpeg启动后%dms就关闭了,可能是源流有问题或FFmpeg配置错误",
entry.getKey(), duration
);
result.suggestion = "检查源流地址是否正确,FFmpeg是否有足够时间接收数据";
result.relatedEvents.add(firstStart);
result.relatedEvents.add(firstStop);
return result;
}
}
}
// 检查问题3:SPS变化但未检测到重启
List<LogEvent> spsChanges = filterEvents(events, EventType.SPS_CHANGE);
List<LogEvent> restarts = filterEvents(events, EventType.FFmpeg_RESTART);
if (spsChanges.size() > restarts.size() * 2) {
result.hasProblem = true;
result.problemType = "视频参数变化未触发FFmpeg重启";
result.description = String.format(
"检测到%d次SPS变化,但只触发了%d次FFmpeg重启",
spsChanges.size(), restarts.size()
);
result.suggestion = "检查日志中的冷却跳过记录,可能需要调整冷却时间";
result.relatedEvents.addAll(spsChanges);
return result;
}
// 检查问题4:持续的花屏迹象(ERROR日志)
List<LogEvent> errors = filterEvents(events, EventType.ERROR);
if (errors.size() >= 5) {
result.hasProblem = true;
result.problemType = "存在大量错误日志";
result.description = String.format(
"在%d条日志中发现%d个错误,可能存在持续性问题",
events.size(), errors.size()
);
result.suggestion = "查看错误日志详情,分析具体错误原因";
result.relatedEvents.addAll(errors);
return result;
}
// 问题5:没有找到SPS变化但也没有重启
if (!spsChanges.isEmpty() && restarts.isEmpty()) {
result.hasProblem = true;
result.problemType = "未检测到FFmpeg重启";
result.description = "检测到视频参数变化,但没有FFmpeg重启记录";
result.suggestion = "检查Channel.java中的restartRtmpPublisher是否被正确调用";
return result;
}
// 如果没有问题,返回成功
result.hasProblem = false;
result.description = "码流切换日志分析完成,未发现明显问题";
// 添加统计信息
StringBuilder stats = new StringBuilder();
stats.append(String.format("日志统计:\n"));
stats.append(String.format("- FFmpeg启动次数: %d\n", filterEvents(events, EventType.FFmpeg_START).size()));
stats.append(String.format("- FFmpeg关闭次数: %d\n", filterEvents(events, EventType.FFmpeg_STOP).size()));
stats.append(String.format("- 码流切换次数: %d\n", filterEvents(events, EventType.STREAM_SWITCH).size()));
stats.append(String.format("- SPS变化次数: %d\n", filterEvents(events, EventType.SPS_CHANGE).size()));
stats.append(String.format("- FFmpeg重启次数: %d\n", restarts.size()));
result.description = stats.toString();
return result;
}
/**
* 解析日志列表
*/
private static List<LogEvent> parseLogs(List<String> rawLogs) {
List<LogEvent> events = new ArrayList<>();
for (String line : rawLogs) {
LogEvent event = parseLogLine(line);
if (event != null) {
events.add(event);
}
}
// 按时间排序
events.sort(Comparator.comparingLong(e -> e.timestamp));
return events;
}
/**
* 解析单行日志
*/
private static LogEvent parseLogLine(String line) {
if (line == null || line.isEmpty()) {
return null;
}
// 提取时间戳(简化处理,假设格式为 [yyyy-MM-dd HH:mm:ss] 或类似)
long timestamp = System.currentTimeMillis(); // 默认当前时间
Pattern timePattern = Pattern.compile("(\\d{4}-\\d{2}-\\d{2}\\s+\\d{2}:\\d{2}:\\d{2})");
Matcher timeMatcher = timePattern.matcher(line);
if (timeMatcher.find()) {
try {
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = sdf.parse(timeMatcher.group(1));
if (date != null) {
timestamp = date.getTime();
}
} catch (Exception e) {
// 使用默认时间
}
}
// 提取tag
String tag = "unknown";
Pattern tagPattern = Pattern.compile("\\[([^\\]]+)\\]\\s*(?:RTMPPublisher|Channel|\\[)");
Matcher tagMatcher = tagPattern.matcher(line);
if (tagMatcher.find()) {
tag = tagMatcher.group(1);
}
// 判断事件类型
EventType type = null;
String message = line;
for (EventType t : EventType.values()) {
if (line.contains(t.getKeyword())) {
type = t;
break;
}
}
if (type == null) {
return null; // 不关心的日志行
}
return new LogEvent(timestamp, type, tag, message);
}
/**
* 过滤特定类型的事件
*/
private static List<LogEvent> filterEvents(List<LogEvent> events, EventType type) {
List<LogEvent> result = new ArrayList<>();
for (LogEvent event : events) {
if (event.type == type) {
result.add(event);
}
}
return result;
}
/**
* 按tag分组
*/
private static Map<String, List<LogEvent>> groupByTag(List<LogEvent> events) {
Map<String, List<LogEvent>> groups = new HashMap<>();
for (LogEvent event : events) {
groups.computeIfAbsent(event.tag, k -> new ArrayList<>()).add(event);
}
return groups;
}
/**
* 查找第一个指定类型的事件
*/
private static LogEvent findFirst(List<LogEvent> events, EventType type) {
for (LogEvent event : events) {
if (event.type == type) {
return event;
}
}
return null;
}
/**
* 生成诊断报告
*/
public static String generateReport(List<String> rawLogs) {
DiagnosisResult result = diagnose(rawLogs);
StringBuilder report = new StringBuilder();
report.append(repeatChar('=', 60)).append("\n");
report.append("码流切换问题诊断报告\n");
report.append("生成时间: ").append(new Date()).append("\n");
report.append(repeatChar('=', 60)).append("\n\n");
if (result.hasProblem) {
report.append("【发现问题】\n");
report.append(result.toString()).append("\n\n");
report.append("【相关日志详情】\n");
for (LogEvent event : result.relatedEvents) {
report.append(event.toString()).append("\n");
}
} else {
report.append("【诊断结果】\n");
report.append(result.description).append("\n");
}
report.append("\n").append(repeatChar('=', 60)).append("\n");
report.append("报告结束\n");
report.append(repeatChar('=', 60)).append("\n");
return report.toString();
}
/**
* Java 8兼容的字符重复方法
*/
private static String repeatChar(char c, int count) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < count; i++) {
sb.append(c);
}
return sb.toString();
}
/**
* 打印诊断报告到日志
*/
public static void logDiagnosis(List<String> rawLogs) {
DiagnosisResult result = diagnose(rawLogs);
if (result.hasProblem) {
logger.error("========== 码流切换问题诊断 ==========");
logger.error(result.toString());
if (!result.relatedEvents.isEmpty()) {
logger.error("---------- 相关日志 ----------");
for (LogEvent event : result.relatedEvents) {
logger.error(event.toString());
}
}
logger.error("====================================");
} else {
logger.info("========== 码流切换诊断 ==========");
logger.info(result.description);
logger.info("==================================");
}
}
}