package com.ard.work.sdk.zlxd.record; import lombok.extern.slf4j.Slf4j; import java.io.BufferedReader; import java.io.File; import java.io.InputStreamReader; import java.io.OutputStream; @Slf4j public class RtspRecorder { private final String uniqueCamId; private final String rtspUrl; private final String saveDir; private Process ffmpegProcess; private boolean running = false; private Thread recorderThread; private Thread logThread; // 单独管理日志线程,方便停止 public RtspRecorder(String uniqueCamId, String rtspUrl, String saveDir) { this.uniqueCamId = uniqueCamId; this.rtspUrl = rtspUrl; this.saveDir = saveDir; File dir = new File(saveDir); if (!dir.exists()) { boolean mkdirs = dir.mkdirs(); if (!mkdirs) { log.error("[{}] 创建保存目录失败:{}", uniqueCamId, saveDir); } } } public synchronized void start() { if (running) { log.warn("[{}] 录制已在运行中,无需重复启动", uniqueCamId); return; } running = true; recorderThread = new Thread(this::run, "RTSP-Recorder-" + uniqueCamId); recorderThread.start(); } private void run() { String fileName = uniqueCamId + ".mp4"; String outPath = new File(saveDir, fileName).getAbsolutePath(); // 最兼容、最稳定的 FFmpeg 命令 ProcessBuilder pb = new ProcessBuilder( "ffmpeg", "-rtsp_transport", "tcp", "-i", rtspUrl, "-c:v", "copy", "-an", "-fs", "1073741824", // ⭐ 限制1GB "-y", outPath ); pb.redirectErrorStream(true); try { ffmpegProcess = pb.start(); log.info("[{}] 开始录制:{}", uniqueCamId, outPath); // 单独管理日志线程,方便后续停止 logThread = new Thread(() -> { try (BufferedReader br = new BufferedReader( new InputStreamReader(ffmpegProcess.getInputStream()))) { String line; while ((line = br.readLine()) != null && running) { log.debug("[{}] FFmpeg日志:{}", uniqueCamId, line); // 改为debug级别,减少日志量 } } catch (Exception e) { if (running) { // 只有运行中发生异常才打印错误 log.error("[{}] 日志读取异常", uniqueCamId, e); } } }, "FFmpeg-Log-" + uniqueCamId); logThread.start(); // 等待进程结束 int exitCode = ffmpegProcess.waitFor(); log.info("[{}] FFmpeg进程退出,退出码:{}", uniqueCamId, exitCode); } catch (Exception e) { if (running) { // 排除主动停止导致的异常 log.error("[{}] 录制异常", uniqueCamId, e); } } finally { if (running) { // 异常导致的退出才主动停止 stop(); } } } public synchronized void stop() { if (!running) { log.warn("[{}] 录制未运行,无需停止", uniqueCamId); return; } running = false; log.info("[{}] 开始停止录制...", uniqueCamId); // 1. 停止日志读取线程 if (logThread != null && logThread.isAlive()) { logThread.interrupt(); try { logThread.join(1000); // 等待日志线程退出 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } // 2. 优雅停止FFmpeg进程 if (ffmpegProcess != null && ffmpegProcess.isAlive()) { try { // 发送q指令让FFmpeg优雅退出 OutputStream outputStream = ffmpegProcess.getOutputStream(); outputStream.write('q'); outputStream.flush(); outputStream.close(); // 关闭输出流 log.info("[{}] 已发送停止指令给FFmpeg,等待优雅退出...", uniqueCamId); // 延长等待时间,给FFmpeg足够时间完成文件写入和释放 Thread.sleep(2000); // 检查是否还存活,未退出则强制销毁 if (ffmpegProcess.isAlive()) { log.warn("[{}] FFmpeg未优雅退出,强制销毁进程...", uniqueCamId); // 先尝试销毁,再强制杀死 ffmpegProcess.destroy(); Thread.sleep(500); if (ffmpegProcess.isAlive()) { ffmpegProcess.destroyForcibly(); } } // 等待进程完全退出 int exitCode = ffmpegProcess.waitFor(); log.info("[{}] FFmpeg进程已终止,退出码:{}", uniqueCamId, exitCode); } catch (Exception e) { log.error("[{}] 停止FFmpeg进程异常", uniqueCamId, e); // 最后的兜底:强制销毁 if (ffmpegProcess.isAlive()) { ffmpegProcess.destroyForcibly(); } } finally { ffmpegProcess = null; // 置空,释放引用 } } // 3. 等待录制线程退出 if (recorderThread != null && recorderThread.isAlive()) { recorderThread.interrupt(); try { recorderThread.join(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } try { ffmpegProcess.getInputStream().close(); ffmpegProcess.getErrorStream().close(); ffmpegProcess.getOutputStream().close(); } catch (Exception ignored) {} log.info("[{}] 已停止录制,文件已释放", uniqueCamId); } // 对外暴露运行状态 public boolean isRunning() { return running; } }