修复工作
This commit is contained in:
@@ -86,18 +86,50 @@ public class MediaTransferFlvByFFmpeg extends MediaTransfer {
|
||||
public MediaTransferFlvByFFmpeg(CameraDto cameraDto) {
|
||||
// 优先使用系统 FFmpeg(包含 libx264),如果不存在则使用 JavaCV 的
|
||||
String ffmpegPath = System.getProperty(MediaConstant.ffmpegPathKey);
|
||||
// 尝试使用系统 FFmpeg
|
||||
if (new File("/usr/bin/ffmpeg").exists()) {
|
||||
command.add("/usr/bin/ffmpeg");
|
||||
log.info("使用系统 FFmpeg: /usr/bin/ffmpeg");
|
||||
String osName = System.getProperty("os.name").toLowerCase();
|
||||
|
||||
// 根据操作系统选择 FFmpeg
|
||||
if (osName.contains("win")) {
|
||||
// Windows 系统:尝试从 PATH 环境变量中查找 ffmpeg
|
||||
String systemFFmpeg = findFFmpegInPath();
|
||||
if (systemFFmpeg != null) {
|
||||
command.add(systemFFmpeg);
|
||||
log.info("✅ 使用系统 FFmpeg (Windows): {}", systemFFmpeg);
|
||||
} else {
|
||||
command.add(ffmpegPath);
|
||||
log.warn("⚠️ 未找到系统 FFmpeg,使用 JavaCV FFmpeg: {}(注意:可能不支持 libx264)", ffmpegPath);
|
||||
}
|
||||
} else {
|
||||
command.add(ffmpegPath);
|
||||
log.info("使用 JavaCV FFmpeg: {}", ffmpegPath);
|
||||
// Linux/Unix 系统
|
||||
if (new File("/usr/bin/ffmpeg").exists()) {
|
||||
command.add("/usr/bin/ffmpeg");
|
||||
log.info("✅ 使用系统 FFmpeg (Linux): /usr/bin/ffmpeg");
|
||||
} else {
|
||||
command.add(ffmpegPath);
|
||||
log.warn("⚠️ 未找到系统 FFmpeg,使用 JavaCV FFmpeg: {}(注意:可能不支持 libx264)", ffmpegPath);
|
||||
}
|
||||
}
|
||||
this.cameraDto = cameraDto;
|
||||
this.enableLog = true; // 临时启用日志,用于调试
|
||||
buildCommand();
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 PATH 环境变量中查找 ffmpeg.exe
|
||||
*/
|
||||
private String findFFmpegInPath() {
|
||||
String pathEnv = System.getenv("PATH");
|
||||
if (pathEnv == null) return null;
|
||||
|
||||
String[] paths = pathEnv.split(File.pathSeparator);
|
||||
for (String path : paths) {
|
||||
File ffmpegFile = new File(path, "ffmpeg.exe");
|
||||
if (ffmpegFile.exists() && ffmpegFile.canExecute()) {
|
||||
return ffmpegFile.getAbsolutePath();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public MediaTransferFlvByFFmpeg(final String executable, CameraDto cameraDto) {
|
||||
command.add(executable);
|
||||
@@ -139,28 +171,40 @@ public class MediaTransferFlvByFFmpeg extends MediaTransfer {
|
||||
* 使用 libx264 编码器(系统 FFmpeg 包含)
|
||||
*/
|
||||
private void buildCommand() {
|
||||
// 检查是否使用系统 FFmpeg(包含 libx264)
|
||||
boolean useSystemFFmpeg = command.get(0).contains("ffmpeg.exe") || command.get(0).equals("/usr/bin/ffmpeg");
|
||||
|
||||
// 添加日志级别参数(临时调试用)
|
||||
this.addArgument("-loglevel").addArgument("info")
|
||||
.addArgument("-rtsp_transport").addArgument("tcp")
|
||||
.addArgument("-i").addArgument(cameraDto.getUrl())
|
||||
.addArgument("-max_delay").addArgument("1")
|
||||
.addArgument("-g").addArgument("25")
|
||||
.addArgument("-r").addArgument("25")
|
||||
// 使用 libx264 编码器(H.264,FLV 播放器标准支持)
|
||||
.addArgument("-c:v").addArgument("libx264")
|
||||
.addArgument("-preset").addArgument("ultrafast")
|
||||
.addArgument("-tune").addArgument("zerolatency")
|
||||
.addArgument("-profile:v").addArgument("baseline") // baseline 兼容性最好
|
||||
.addArgument("-level").addArgument("3.0")
|
||||
.addArgument("-b:v").addArgument("1000k") // 视频比特率
|
||||
.addArgument("-maxrate").addArgument("1000k")
|
||||
.addArgument("-bufsize").addArgument("2000k")
|
||||
// 音频编码
|
||||
.addArgument("-c:a").addArgument("aac")
|
||||
.addArgument("-strict").addArgument("experimental")
|
||||
.addArgument("-b:a").addArgument("64k")
|
||||
.addArgument("-ar").addArgument("44100") // 音频采样率
|
||||
.addArgument("-f").addArgument("flv");
|
||||
.addArgument("-max_delay").addArgument("1");
|
||||
|
||||
if (useSystemFFmpeg) {
|
||||
// 系统 FFmpeg:使用 libx264 重新编码
|
||||
log.info("🎬 使用 libx264 编码器(重新编码)");
|
||||
this.addArgument("-g").addArgument("25")
|
||||
.addArgument("-r").addArgument("25")
|
||||
.addArgument("-c:v").addArgument("libx264")
|
||||
.addArgument("-preset").addArgument("ultrafast")
|
||||
.addArgument("-tune").addArgument("zerolatency")
|
||||
.addArgument("-profile:v").addArgument("baseline")
|
||||
.addArgument("-level").addArgument("3.0")
|
||||
.addArgument("-b:v").addArgument("1000k")
|
||||
.addArgument("-maxrate").addArgument("1000k")
|
||||
.addArgument("-bufsize").addArgument("2000k")
|
||||
.addArgument("-c:a").addArgument("aac")
|
||||
.addArgument("-strict").addArgument("experimental")
|
||||
.addArgument("-b:a").addArgument("64k")
|
||||
.addArgument("-ar").addArgument("44100");
|
||||
} else {
|
||||
// JavaCV FFmpeg:使用 copy(不重新编码,只重新封装)
|
||||
log.info("📦 使用 copy 编码器(不重新编码,只重新封装)");
|
||||
this.addArgument("-c:v").addArgument("copy")
|
||||
.addArgument("-c:a").addArgument("copy");
|
||||
}
|
||||
|
||||
this.addArgument("-f").addArgument("flv");
|
||||
}
|
||||
|
||||
// private void buildCommand() {
|
||||
@@ -201,16 +245,26 @@ public class MediaTransferFlvByFFmpeg extends MediaTransfer {
|
||||
log.info("输出地址: {}", output);
|
||||
log.info("=================================");
|
||||
try {
|
||||
process = new ProcessBuilder(command).start();
|
||||
ProcessBuilder pb = new ProcessBuilder(command);
|
||||
// 重要:合并错误流到输出流,便于日志收集
|
||||
pb.redirectErrorStream(false);
|
||||
process = pb.start();
|
||||
running = true;
|
||||
log.info("✅ FFmpeg 进程已启动,等待数据...");
|
||||
log.info("✅ FFmpeg 进程已启动 PID={},等待数据...", process.pid());
|
||||
|
||||
// 先启动输出数据线程(监听TCP连接)
|
||||
outputData();
|
||||
// 再启动日志监听线程
|
||||
listenNetTimeout();
|
||||
dealStream(process);
|
||||
outputData();
|
||||
// 最后启动客户端监听
|
||||
listenClient();
|
||||
|
||||
log.info("✅ FFmpeg 所有线程已启动,等待客户端连接...");
|
||||
} catch (IOException e) {
|
||||
log.error("❌ FFmpeg 启动失败: {}", e.getMessage());
|
||||
e.printStackTrace();
|
||||
log.error("❌ FFmpeg 启动失败: {}", e.getMessage(), e);
|
||||
running = false;
|
||||
MediaService.cameras.remove(cameraDto.getMediaKey());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
@@ -223,20 +277,32 @@ public class MediaTransferFlvByFFmpeg extends MediaTransfer {
|
||||
public void run() {
|
||||
Socket client = null;
|
||||
try {
|
||||
log.info("🎬 开始监听 TCP Socket,等待 FFmpeg 连接...");
|
||||
log.info("🎬 开始监听 TCP Socket: {} 端口: {},等待 FFmpeg 连接...",
|
||||
tcpServer.getInetAddress(), tcpServer.getLocalPort());
|
||||
|
||||
client = tcpServer.accept();
|
||||
log.info("✅ FFmpeg 已连接到 TCP Socket");
|
||||
log.info("✅ FFmpeg 已连接到 TCP Socket,远程地址: {}", client.getRemoteSocketAddress());
|
||||
|
||||
DataInputStream input = new DataInputStream(client.getInputStream());
|
||||
|
||||
byte[] buffer = new byte[4096]; // 增加缓冲区到4KB
|
||||
int len = 0;
|
||||
boolean headerSent = false;
|
||||
int totalBytes = 0;
|
||||
|
||||
log.info("📡 开始读取 FLV 数据流...");
|
||||
|
||||
while (running) {
|
||||
len = input.read(buffer);
|
||||
if (len == -1) {
|
||||
log.warn("⚠️ FFmpeg 连接关闭(读取到EOF)");
|
||||
break;
|
||||
}
|
||||
|
||||
totalBytes += len;
|
||||
if (totalBytes % 102400 == 0) { // 每100KB输出一次
|
||||
log.debug("📊 已接收 {} KB 数据", totalBytes / 1024);
|
||||
}
|
||||
|
||||
// 第一次读取的是FLV header(13字节)
|
||||
if (header == null && len >= 13) {
|
||||
@@ -254,12 +320,14 @@ public class MediaTransferFlvByFFmpeg extends MediaTransfer {
|
||||
|
||||
headerSent = true;
|
||||
|
||||
// 如果有剩余数据(包含header+数据),一起发送
|
||||
if (len > 0) {
|
||||
byte[] data = new byte[len];
|
||||
System.arraycopy(buffer, 0, data, 0, len);
|
||||
log.info("📤 发送第一批数据: {} bytes,客户端数量: HTTP={}, WS={}",
|
||||
len, httpClients.size(), wsClients.size());
|
||||
// 如果有剩余数据(header之后的数据),发送剩余部分
|
||||
// 注意:header 已经在 addClient() 中发送给客户端了,这里只发送 header 之后的数据
|
||||
if (len > 13) {
|
||||
int remainingLen = len - 13;
|
||||
byte[] data = new byte[remainingLen];
|
||||
System.arraycopy(buffer, 13, data, 0, remainingLen);
|
||||
log.info("📤 发送第一批数据(不含header): {} bytes,客户端数量: HTTP={}, WS={}",
|
||||
remainingLen, httpClients.size(), wsClients.size());
|
||||
sendFrameData(data);
|
||||
}
|
||||
continue;
|
||||
@@ -414,30 +482,45 @@ public class MediaTransferFlvByFFmpeg extends MediaTransfer {
|
||||
public void run() {
|
||||
BufferedReader err = new BufferedReader(new InputStreamReader(process.getErrorStream()));
|
||||
String line = null;
|
||||
boolean streamStarted = false;
|
||||
try {
|
||||
while (running) {
|
||||
line = err.readLine();
|
||||
currentTimeMillis = System.currentTimeMillis();
|
||||
if (line == null) {
|
||||
log.warn("⚠️ FFmpeg 错误流已关闭");
|
||||
break;
|
||||
}
|
||||
if (enableLog) {
|
||||
|
||||
// 检测关键信息
|
||||
if (line.contains("Stream #0") || line.contains("Output #0")) {
|
||||
streamStarted = true;
|
||||
log.info("[FFmpeg] " + line);
|
||||
}
|
||||
// 即使不启用日志,也输出关键错误信息
|
||||
if (!enableLog && (line.contains("error") || line.contains("failed") ||
|
||||
line.contains("Connection") || line.contains("timeout"))) {
|
||||
} else if (line.contains("frame=") && line.contains("fps=")) {
|
||||
// 这是输出进度信息,定期输出
|
||||
if (enableLog) {
|
||||
log.debug("[FFmpeg Progress] " + line.trim());
|
||||
}
|
||||
} else if (line.toLowerCase().contains("error") ||
|
||||
line.toLowerCase().contains("failed") ||
|
||||
line.toLowerCase().contains("invalid") ||
|
||||
line.toLowerCase().contains("connection") ||
|
||||
line.toLowerCase().contains("timeout") ||
|
||||
line.toLowerCase().contains("could not")) {
|
||||
// 错误信息始终输出
|
||||
log.error("[FFmpeg Error] " + line);
|
||||
} else if (enableLog) {
|
||||
log.info("[FFmpeg] " + line);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
log.error("❌ 读取 FFmpeg 错误流异常: {}", e.getMessage());
|
||||
} finally {
|
||||
try {
|
||||
running = false;
|
||||
err.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
log.error("关闭错误流异常: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -579,12 +662,20 @@ public class MediaTransferFlvByFFmpeg extends MediaTransfer {
|
||||
* @param ctype enum,ClientType
|
||||
*/
|
||||
public void addClient(ChannelHandlerContext ctx, ClientType ctype) {
|
||||
log.info("🔗 新客户端连接: 类型={}, Channel={}", ctype, ctx.channel().remoteAddress());
|
||||
log.info("🔗 新客户端连接: 类型={}, Channel={}, FFmpeg运行状态={}",
|
||||
ctype, ctx.channel().remoteAddress(), running);
|
||||
|
||||
// 检查FFmpeg进程状态
|
||||
if (process != null && !process.isAlive()) {
|
||||
log.error("❌ FFmpeg 进程已死亡,无法添加客户端");
|
||||
return;
|
||||
}
|
||||
|
||||
int timeout = 0;
|
||||
while (true) {
|
||||
try {
|
||||
if (header != null) {
|
||||
log.info("✅ FLV header 已就绪,准备发送给客户端");
|
||||
log.info("✅ FLV header 已就绪,准备发送给客户端 (总共 {} bytes)", header.length);
|
||||
try {
|
||||
if (ctx.channel().isWritable()) {
|
||||
// 发送帧前先发送header
|
||||
@@ -596,10 +687,14 @@ public class MediaTransferFlvByFFmpeg extends MediaTransfer {
|
||||
public void operationComplete(Future<? super Void> future) throws FrameGrabber.Exception {
|
||||
if (future.isSuccess()) {
|
||||
httpClients.put(ctx.channel().id().toString(), ctx);
|
||||
log.info("✅ HTTP 客户端已添加,当前客户端数: {}", httpClients.size());
|
||||
} else {
|
||||
log.error("❌ 发送 FLV header 失败: {}", future.cause());
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (ClientType.WEBSOCKET == ctype) {
|
||||
log.info("📤 发送 FLV header 到 WebSocket 客户端: {} bytes", header.length);
|
||||
ChannelFuture future = ctx
|
||||
.writeAndFlush(new BinaryWebSocketFrame(Unpooled.copiedBuffer(header)));
|
||||
future.addListener(new GenericFutureListener<Future<? super Void>>() {
|
||||
@@ -607,35 +702,46 @@ public class MediaTransferFlvByFFmpeg extends MediaTransfer {
|
||||
public void operationComplete(Future<? super Void> future) throws FrameGrabber.Exception {
|
||||
if (future.isSuccess()) {
|
||||
wsClients.put(ctx.channel().id().toString(), ctx);
|
||||
log.info("✅ WebSocket 客户端已添加,当前客户端数: {}", wsClients.size());
|
||||
} else {
|
||||
log.error("❌ 发送 FLV header 失败: {}", future.cause());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
log.warn("⚠️ 客户端通道不可写");
|
||||
}
|
||||
|
||||
} catch (java.lang.Exception e) {
|
||||
e.printStackTrace();
|
||||
log.error("❌ 添加客户端异常: {}", e.getMessage(), e);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// 等待推拉流启动
|
||||
if (timeout == 0) {
|
||||
log.info("⏳ 等待 FLV header...");
|
||||
log.info("⏳ 等待 FLV header...(FFmpeg 进程状态: {})",
|
||||
process != null && process.isAlive() ? "运行中" : "未启动/已停止");
|
||||
}
|
||||
Thread.sleep(50);
|
||||
// 启动录制器失败
|
||||
timeout += 50;
|
||||
if (timeout % 5000 == 0) {
|
||||
log.warn("⚠️ 等待 FLV header 超时: {} ms,可能 FFmpeg 拉流失败", timeout);
|
||||
log.warn("⚠️ 等待 FLV header 超时: {} ms,FFmpeg 可能拉流失败或未连接到TCP Socket", timeout);
|
||||
if (process != null && !process.isAlive()) {
|
||||
log.error("❌ FFmpeg 进程已死亡!退出码: {}", process.exitValue());
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (timeout > 30000) {
|
||||
log.error("❌ 等待 FLV header 超时 30 秒,放弃连接");
|
||||
|
||||
log.error(" 可能原因: 1) FFmpeg 拉流失败 2) RTSP 地址不可达 3) 网络问题");
|
||||
break;
|
||||
}
|
||||
} catch (java.lang.Exception e) {
|
||||
e.printStackTrace();
|
||||
log.error("❌ 等待 header 异常: {}", e.getMessage(), e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user