liusuyi
2026-06-01 3496700a5ba18be8ca0590a79e21a867782d1ea9
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
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;
    }
}