吉安感知网项目-后端
xiebin
2026-01-06 d207a86cdf1ab52ef8cb7cd83bad8fceab8038cf
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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
package org.sxkj.odm.utils;
 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
import java.io.*;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
 
/**
 * 视频转换图片工具类
 * @author zhongrj
 */
public class VideoConverterUtils {
 
    private static final Logger logger = LoggerFactory.getLogger(VideoConverterUtils.class);
 
    /**
     * 视频切图
     * @param inputPath 视频路径
     * @param outputPattern 图片输出路径
     * @param interval 每多少帧取一个图片保存
     * @throws IOException
     * @throws InterruptedException
     * @return
     */
    public static boolean extractFrames(String inputPath, String outputPattern, int interval) throws IOException, InterruptedException {
        boolean flag = false;
        // 构建FFmpeg命令
        String[] ffmpegCommand = {
            "ffmpeg",
            "-i", inputPath,                  // 输入文件
            "-vf", "select=not(mod(n\\," + interval + "))",  // 选择每interval帧中的一帧
            "-vsync", "vfr",                 // 可变帧率处理
            "-q:v", "2",                      // 输出图片质量(2-31,2为最高质量)
            "-frame_pts", "1",                // 使用帧号作为输出文件名的一部分
            outputPattern                     // 输出文件模式
        };
 
        try {
            // 执行FFmpeg命令
            Process process = new ProcessBuilder(ffmpegCommand).start();
 
            // 读取错误流(FFmpeg的输出通常写在错误流)
            BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
            String line;
            while ((line = errorReader.readLine()) != null) {
                System.out.println(line);
            }
 
            // 等待命令执行完成
            int exitCode = process.waitFor();
            if (exitCode == 0) {
                flag = true;
                logger.info("视频切图完成!");
            } else {
                flag = false;
                logger.info("视频切图失败,退出码: " + exitCode);
            }
        } catch (IOException | InterruptedException e) {
            logger.error("视频切图失败,异常消息:{}",e.getMessage());
            e.printStackTrace();
            throw e;
        }
        return flag;
    }
 
    /**
     * 视频切图-限制cpu,内存使用
     * @param inputPath 视频路径
     * @param outputPattern 图片输出路径
     * @param interval 每多少帧取一个图片保存
     * @throws IOException
     * @throws InterruptedException
     * @return
     */
    public static boolean extractFramesRestriction(String inputPath, String outputPattern, int interval) throws IOException, InterruptedException {
        // 防止接口重复写入(重试时)
        if(hasJpegFileNIO(inputPath)){
            return false;
        }
        boolean flag = false;
        // 构建FFmpeg命令
        String[] ffmpegCommand = {
            "ffmpeg",
            "-loglevel", "error",                // 减少日志输出量
            "-threads", "1",                    // 限制线程数
            "-hwaccel", "auto",               // 启用硬件加速
            "-i", inputPath,                  // 输入文件
            "-max_muxing_queue_size", "100",  // 限制复用队列大小
            "-vf", "select=not(mod(n\\," + interval + "))",  // 选择每interval帧中的一帧
            "-vsync", "0",                    // 禁用帧同步缓冲
            "-q:v", "3",                      // 输出图片质量(2-31,2为最高质量)
            "-frame_pts", "1",                // 使用帧号作为输出文件名的一部分
            "-f", "image2",                   // 明确指定图像格式
            outputPattern                     // 输出文件模式
        };
 
        try {
            ProcessBuilder pb = new ProcessBuilder(ffmpegCommand);
 
            // 重定向错误流到临时文件,避免内存堆积
            File errorLog = File.createTempFile("ffmpeg_error", ".log");
            pb.redirectError(errorLog);
 
            Process process = pb.start();
 
            // 不缓冲输出流,立即释放内存
            try (InputStream ignored = process.getInputStream();
                 InputStream ignored2 = process.getErrorStream()) {
                int exitCode = process.waitFor();
 
                if (exitCode == 0) {
                    flag = true;
                    logger.info("视频切图完成!");
                } else {
                    flag = false;
                    logger.error("视频切图失败,退出码: {}。错误日志: {}",
                        exitCode, errorLog.getAbsolutePath());
                }
            }
            errorLog.delete(); // 清理临时文件
        } catch (IOException | InterruptedException e) {
            logger.error("视频切图失败,异常消息:{}", e.getMessage());
            throw e;
        }
        return flag;
    }
 
    /**
     * 判断路径下是否已经包含了 jpeg 文件
     * @param inputPath
     * @return
     */
    public static boolean hasJpegFileNIO(String inputPath) {
        try {
            Path dir = Paths.get(inputPath);
 
            if (!Files.isDirectory(dir)) {
                return false;
            }
 
            // 使用DirectoryStream逐项处理,不一次性加载所有文件
            try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
                for (Path path : stream) {
                    if (Files.isRegularFile(path)) {
                        String fileName = path.getFileName().toString().toLowerCase();
                        if (fileName.endsWith(".jpeg") || fileName.endsWith(".jpg")) {
                            return true;
                        }
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }
 
    /**
     * 从视频中提取字幕为SRT文件
     * @param inputPath 视频路径
     * @param srtPath 输出SRT文件路径
     * @return true表示成功,false表示失败
     * @throws InterruptedException 进程被中断
     * @throws IOException 文件操作失败
     * @return
     */
    public static boolean extractSrt(String inputPath, String srtPath)
        throws InterruptedException, IOException {
 
        // 1. 先检查视频是否有字幕流
        if (!hasSubtitleStream(inputPath)) {
            logger.warn("视频文件没有字幕流: {}", inputPath);
            return false;
        }
 
        // 2. 构建FFmpeg命令
        String[] ffmpegCommand = {
            "ffmpeg",
            "-i", inputPath,
            "-map", "0:s:0",             // 提取第一条字幕流
            "-c:s", "srt",               // 强制转换为SRT格式
            "-y",                       // 覆盖已存在文件
            srtPath
        };
 
        try {
            Process process = new ProcessBuilder(ffmpegCommand)
                .redirectErrorStream(true)  // 合并错误流和标准输出
                .start();
 
            // 读取输出
            try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(process.getInputStream()))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    logger.debug("FFmpeg输出: {}", line);
                }
            }
 
            int exitCode = process.waitFor();
            if (exitCode == 0) {
                logger.info("成功提取SRT字幕到: {}", srtPath);
                return true;
            } else {
                logger.error("提取SRT字幕失败,退出码: {}", exitCode);
                return false;
            }
        } catch (IOException | InterruptedException e) {
            logger.error("提取SRT字幕时出错: {}", e.getMessage());
            throw e;
        }
    }
 
    /**
     * 检查视频是否包含字幕流
     * @param inputPath
     * @return
     * @throws IOException
     */
    private static boolean hasSubtitleStream(String inputPath) throws IOException {
        String[] probeCommand = {
            "ffprobe",
            "-v", "error",
            "-select_streams", "s",
            "-show_entries", "stream=codec_type",
            "-of", "csv=p=0",
            inputPath
        };
 
        Process process = new ProcessBuilder(probeCommand).start();
        try (BufferedReader reader = new BufferedReader(
            new InputStreamReader(process.getInputStream()))) {
 
            return reader.readLine() != null;  // 有输出表示有字幕流
        }
    }
 
    /**
     * 根据输入的路径获取输出路径
     * @param inputPath
     * @return
     */
    public static String getOutputPathByInputPath(String inputPath){
        // 获取输入文件的父目录和文件名(不带扩展名)
        Path path = Paths.get(inputPath);
        String parentDir = path.getParent().toString();
        String fileName = path.getFileName().toString();
        String baseName = fileName.substring(0, fileName.lastIndexOf('.'));
 
        // 创建以MP4文件名命名的子目录
        String outputDir = parentDir + File.separator + baseName + File.separator;
 
        // 确保输出目录存在
        File file = new File(outputDir);
        if (!file.exists()){
            // 目录不存在则创建
            file.mkdirs();
        }
        return outputDir;
    }
 
    /**
     * 根据输入的路径获取输出路径
     * @param inputPath
     * @return
     */
    public static String getFileBaseNamePathByInputPath(String inputPath){
        // 获取输入文件的父目录和文件名(不带扩展名)
        Path path = Paths.get(inputPath);
        String fileName = path.getFileName().toString();
        String baseName = fileName.substring(0, fileName.lastIndexOf('.'));
        return baseName;
    }
 
    /**
     * mp4 视频转 jpg 图片
     * @param inputVideoPath 视频输入路径
     * @param frameInterval 图片帧保持间隔,每多少帧保存一次
     * @return
     * @throws IOException
     * @throws InterruptedException
     */
    public static boolean mp4ToJpg(String inputVideoPath,int frameInterval) throws IOException, InterruptedException {
        // 设置 图片 保存的目录
        String outputDir = getOutputPathByInputPath(inputVideoPath);
        String outputImagePattern = outputDir + "frame_%04d.jpg";
        // 转换生成图片
        boolean flag = extractFrames(inputVideoPath, outputImagePattern, frameInterval);
        // 返回处理结果
        return flag;
    }
 
    /**
     * mp4 视频转 jpg 图片-优化版本
     * @param inputVideoPath 视频输入路径
     * @param frameInterval 图片帧保持间隔,每多少帧保存一次
     * @return
     * @throws IOException
     * @throws InterruptedException
     */
    public static boolean mp4ToJpgRestriction(String inputVideoPath,int frameInterval) throws IOException, InterruptedException {
        // 设置 图片 保存的目录
        String outputDir = getOutputPathByInputPath(inputVideoPath);
        String outputImagePattern = outputDir + "frame_%04d.jpg";
        // 转换生成图片
        boolean flag = extractFramesRestriction(inputVideoPath, outputImagePattern, frameInterval);
        // 返回处理结果
        return flag;
    }
 
    /**
     * 从 mp4 视频获取 srt 文件
     * @param inputVideoPath 视频输入路径
     * @return
     * @throws InterruptedException
     * @throws IOException
     */
    public static boolean mp4ToCreateStr(String inputVideoPath) throws InterruptedException, IOException {
        // 设置 srt 保存的目录
        String outputDir = getOutputPathByInputPath(inputVideoPath);
        String strPath = outputDir + "out.srt";
        // 转换生成 srt 文件
        boolean flag = extractSrt(inputVideoPath, strPath);
        // 返回结果
        return flag;
    }
 
 
//    public static void main(String[] args) throws InterruptedException, IOException {
//        String cc = "E:\\temp\\video\\倾斜视频\\DJI_20250723102452_0001_V";  // 输入视频文件路径
////        boolean b = mp4ToJpg(inputVideoPath,30);
////        boolean b1 = mp4ToCreateStr(inputVideoPath);
//        boolean b2 = hasJpegFileNIO(cc);
//        System.out.println("b2 = " + b2);
//    }
}