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;
|
}
|
}
|