From 4bbbff266d8dfe82f9ed6987f642a9a7748c460d Mon Sep 17 00:00:00 2001 From: 86156 <823267011@qq.com> Date: Thu, 2 Oct 2025 14:32:42 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=B7=A5=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thread/MediaTransferFlvByFFmpeg.java | 208 +++++++++++++----- 1 file changed, 157 insertions(+), 51 deletions(-) diff --git a/ruoyi-video/src/main/java/com/ruoyi/video/thread/MediaTransferFlvByFFmpeg.java b/ruoyi-video/src/main/java/com/ruoyi/video/thread/MediaTransferFlvByFFmpeg.java index 4e8e7ac..af4fa6d 100644 --- a/ruoyi-video/src/main/java/com/ruoyi/video/thread/MediaTransferFlvByFFmpeg.java +++ b/ruoyi-video/src/main/java/com/ruoyi/video/thread/MediaTransferFlvByFFmpeg.java @@ -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 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>() { @@ -607,35 +702,46 @@ public class MediaTransferFlvByFFmpeg extends MediaTransfer { public void operationComplete(Future 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; } } }