Commit 428e2657f4106b81e58d1ffd567f76c536e2c54b
1 parent
aad3e404
添加录像查询和录像裁剪合并
Showing
4 changed files
with
205 additions
and
12 deletions
src/main/java/top/panll/assist/config/StartConfig.java
| ... | ... | @@ -39,6 +39,14 @@ public class StartConfig implements CommandLineRunner { |
| 39 | 39 | logger.error("[userSettings.record]配置错误,请检查路径是否存在"); |
| 40 | 40 | System.exit(1); |
| 41 | 41 | } |
| 42 | + if (!recordFile.canRead()) { | |
| 43 | + logger.error("[userSettings.record]路径无法读取"); | |
| 44 | + System.exit(1); | |
| 45 | + } | |
| 46 | + if (!recordFile.canWrite()) { | |
| 47 | + logger.error("[userSettings.record]路径无法写入"); | |
| 48 | + System.exit(1); | |
| 49 | + } | |
| 42 | 50 | try { |
| 43 | 51 | String ffmpegPath = userSettings.getFfmpeg(); |
| 44 | 52 | String ffprobePath = userSettings.getFfprobe(); | ... | ... |
src/main/java/top/panll/assist/service/FFmpegExecUtils.java
| 1 | 1 | package top.panll.assist.service; |
| 2 | 2 | |
| 3 | 3 | import net.bramp.ffmpeg.FFmpeg; |
| 4 | +import net.bramp.ffmpeg.FFmpegExecutor; | |
| 5 | +import net.bramp.ffmpeg.FFmpegUtils; | |
| 4 | 6 | import net.bramp.ffmpeg.FFprobe; |
| 7 | +import net.bramp.ffmpeg.builder.FFmpegBuilder; | |
| 8 | +import net.bramp.ffmpeg.job.FFmpegJob; | |
| 9 | +import net.bramp.ffmpeg.probe.FFmpegProbeResult; | |
| 10 | +import net.bramp.ffmpeg.progress.Progress; | |
| 11 | +import net.bramp.ffmpeg.progress.ProgressListener; | |
| 12 | +import org.springframework.util.DigestUtils; | |
| 13 | + | |
| 14 | +import java.io.BufferedWriter; | |
| 15 | +import java.io.File; | |
| 16 | +import java.io.FileWriter; | |
| 17 | +import java.io.IOException; | |
| 18 | +import java.util.List; | |
| 19 | +import java.util.concurrent.TimeUnit; | |
| 5 | 20 | |
| 6 | 21 | public class FFmpegExecUtils { |
| 7 | 22 | |
| ... | ... | @@ -24,4 +39,82 @@ public class FFmpegExecUtils { |
| 24 | 39 | public FFprobe ffprobe; |
| 25 | 40 | public FFmpeg ffmpeg; |
| 26 | 41 | |
| 42 | + public interface VideoHandEndCallBack { | |
| 43 | + public void run(String status, double percentage, String result); | |
| 44 | + } | |
| 45 | + | |
| 46 | + public static void mergeOrCutFile(List<File> fils, File dest, VideoHandEndCallBack callBack){ | |
| 47 | + FFmpeg ffmpeg = FFmpegExecUtils.getInstance().ffmpeg; | |
| 48 | + FFprobe ffprobe = FFmpegExecUtils.getInstance().ffprobe; | |
| 49 | + if (fils == null || fils.size() == 0 || ffmpeg == null || ffprobe == null || dest== null || !dest.exists()){ | |
| 50 | + callBack.run("error", 0.0, null); | |
| 51 | + return; | |
| 52 | + } | |
| 53 | + | |
| 54 | + String temp = DigestUtils.md5DigestAsHex(String.valueOf(System.currentTimeMillis()).getBytes()); | |
| 55 | + File tempFile = new File(dest.getAbsolutePath() + File.separator + temp); | |
| 56 | + if (!tempFile.exists()) { | |
| 57 | + tempFile.mkdirs(); | |
| 58 | + } | |
| 59 | + FFmpegExecutor executor = new FFmpegExecutor(ffmpeg, ffprobe); | |
| 60 | + String fileListName = tempFile.getName() + File.separator + "fileList"; | |
| 61 | + double durationAll = 0.0; | |
| 62 | + try { | |
| 63 | + BufferedWriter bw =new BufferedWriter(new FileWriter(fileListName)); | |
| 64 | + for (File file : fils) { | |
| 65 | + FFmpegProbeResult in = ffprobe.probe(file.getAbsolutePath()); | |
| 66 | + double duration = in.getFormat().duration; | |
| 67 | + System.out.println(duration); | |
| 68 | + bw.write("file " + file.getAbsolutePath()); | |
| 69 | + bw.newLine(); | |
| 70 | + durationAll += duration; | |
| 71 | + } | |
| 72 | + bw.flush(); | |
| 73 | + bw.close(); | |
| 74 | + System.out.println(durationAll); | |
| 75 | + } catch (IOException e) { | |
| 76 | + e.printStackTrace(); | |
| 77 | + callBack.run("error", 0.0, null); | |
| 78 | + } | |
| 79 | + long startTime = System.currentTimeMillis(); | |
| 80 | + FFmpegBuilder builder = new FFmpegBuilder() | |
| 81 | + | |
| 82 | + .setFormat("concat") | |
| 83 | + .overrideOutputFiles(true) | |
| 84 | + .setInput(fileListName) // Or filename | |
| 85 | + .addExtraArgs("-safe", "0") | |
| 86 | + .addOutput(dest.getAbsolutePath() + File.separator + temp + ".mp4") | |
| 87 | + .setVideoCodec("copy") | |
| 88 | + .setFormat("mp4") | |
| 89 | + | |
| 90 | + .done(); | |
| 91 | + | |
| 92 | + | |
| 93 | + double finalDurationAll = durationAll; | |
| 94 | + FFmpegJob job = executor.createJob(builder, (Progress progress) -> { | |
| 95 | + final double duration_ns = finalDurationAll * TimeUnit.SECONDS.toNanos(1); | |
| 96 | + double percentage = progress.out_time_ns / duration_ns; | |
| 97 | + | |
| 98 | + // Print out interesting information about the progress | |
| 99 | + System.out.println(String.format( | |
| 100 | + "[%.0f%%] status:%s frame:%d time:%s ms fps:%.0f speed:%.2fx", | |
| 101 | + percentage * 100, | |
| 102 | + progress.status, | |
| 103 | + progress.frame, | |
| 104 | + FFmpegUtils.toTimecode(progress.out_time_ns, TimeUnit.NANOSECONDS), | |
| 105 | + progress.fps.doubleValue(), | |
| 106 | + progress.speed | |
| 107 | + )); | |
| 108 | + if (progress.status.equals(Progress.Status.END)){ | |
| 109 | + callBack.run(progress.status.name(), percentage, | |
| 110 | + dest.getAbsolutePath() + File.separator + temp + ".mp4"); | |
| 111 | + System.out.println(System.currentTimeMillis() - startTime); | |
| 112 | + }else { | |
| 113 | + callBack.run(progress.status.name(), percentage, null); | |
| 114 | + } | |
| 115 | + }); | |
| 116 | + | |
| 117 | + job.run(); | |
| 118 | + } | |
| 119 | + | |
| 27 | 120 | } | ... | ... |
src/main/java/top/panll/assist/service/VideoFileService.java
| ... | ... | @@ -8,15 +8,15 @@ import org.springframework.beans.factory.annotation.Autowired; |
| 8 | 8 | import org.springframework.stereotype.Service; |
| 9 | 9 | import top.panll.assist.config.StartConfig; |
| 10 | 10 | import top.panll.assist.dto.UserSettings; |
| 11 | +import top.panll.assist.utils.DateUtils; | |
| 11 | 12 | |
| 12 | 13 | import java.io.File; |
| 13 | 14 | import java.io.FilenameFilter; |
| 14 | 15 | import java.io.IOException; |
| 16 | +import java.lang.reflect.Array; | |
| 15 | 17 | import java.text.ParseException; |
| 16 | 18 | import java.text.SimpleDateFormat; |
| 17 | -import java.util.ArrayList; | |
| 18 | -import java.util.Date; | |
| 19 | -import java.util.List; | |
| 19 | +import java.util.*; | |
| 20 | 20 | |
| 21 | 21 | @Service |
| 22 | 22 | public class VideoFileService { |
| ... | ... | @@ -27,6 +27,11 @@ public class VideoFileService { |
| 27 | 27 | private UserSettings userSettings; |
| 28 | 28 | |
| 29 | 29 | |
| 30 | + /** | |
| 31 | + * 对视频文件重命名, 00:00:00-00:00:00 | |
| 32 | + * @param file | |
| 33 | + * @throws ParseException | |
| 34 | + */ | |
| 30 | 35 | public void handFile(File file) throws ParseException { |
| 31 | 36 | FFprobe ffprobe = FFmpegExecUtils.getInstance().ffprobe; |
| 32 | 37 | if(file.isFile() && !file.getName().startsWith(".") && file.getName().indexOf(":") < 0) { |
| ... | ... | @@ -54,6 +59,14 @@ public class VideoFileService { |
| 54 | 59 | } |
| 55 | 60 | } |
| 56 | 61 | |
| 62 | + /** | |
| 63 | + * 获取制定推流的指定时间段内的推流 | |
| 64 | + * @param app | |
| 65 | + * @param stream | |
| 66 | + * @param startTime | |
| 67 | + * @param endTime | |
| 68 | + * @return | |
| 69 | + */ | |
| 57 | 70 | public List<File> getFilesInTime(String app, String stream, Date startTime, Date endTime){ |
| 58 | 71 | |
| 59 | 72 | List<File> result = new ArrayList<>(); |
| ... | ... | @@ -63,17 +76,15 @@ public class VideoFileService { |
| 63 | 76 | } |
| 64 | 77 | |
| 65 | 78 | SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); |
| 79 | + SimpleDateFormat formatterForDate = new SimpleDateFormat("yyyy-MM-dd"); | |
| 66 | 80 | String startTimeStr = formatter.format(startTime); |
| 67 | 81 | String endTimeStr = formatter.format(endTime); |
| 68 | 82 | logger.debug("获取[app: {}, stream: {}, statime: {}, endTime: {}]的视频", app, stream, |
| 69 | 83 | startTimeStr, endTimeStr); |
| 70 | 84 | |
| 71 | 85 | File file = new File(userSettings.getRecord()); |
| 72 | - File[] appFiles = file.listFiles(new FilenameFilter() { | |
| 73 | - @Override | |
| 74 | - public boolean accept(File dir, String name) { | |
| 86 | + File[] appFiles = file.listFiles((File dir, String name) -> { | |
| 75 | 87 | return app.equals(name); |
| 76 | - } | |
| 77 | 88 | }); |
| 78 | 89 | if (appFiles.length == 0) { |
| 79 | 90 | logger.warn("获取[app: {}, stream: {}, statime: {}, endTime: {}]的视频时未找到目录: {}", app, stream, |
| ... | ... | @@ -81,11 +92,8 @@ public class VideoFileService { |
| 81 | 92 | return result; |
| 82 | 93 | } |
| 83 | 94 | File appFile = appFiles[0]; |
| 84 | - File[] streamFiles = appFile.listFiles(new FilenameFilter() { | |
| 85 | - @Override | |
| 86 | - public boolean accept(File dir, String name) { | |
| 95 | + File[] streamFiles = appFile.listFiles((File dir, String name)-> { | |
| 87 | 96 | return stream.equals(name); |
| 88 | - } | |
| 89 | 97 | }); |
| 90 | 98 | if (streamFiles.length == 0) { |
| 91 | 99 | logger.warn("获取[app: {}, stream: {}, statime: {}, endTime: {}]的视频时未找到目录: {}", app, stream, |
| ... | ... | @@ -93,10 +101,69 @@ public class VideoFileService { |
| 93 | 101 | return result; |
| 94 | 102 | } |
| 95 | 103 | File streamFile = streamFiles[0]; |
| 96 | - // TODO 按时间获取文件 | |
| 97 | 104 | |
| 105 | + File[] dateFiles = streamFile.listFiles((File dir, String name) -> { | |
| 106 | + Date fileDate = null; | |
| 107 | + try { | |
| 108 | + fileDate = formatterForDate.parse(name); | |
| 109 | + } catch (ParseException e) { | |
| 110 | + logger.error("过滤日期文件时异常: {}-{}", name, e.getMessage()); | |
| 111 | + return false; | |
| 112 | + } | |
| 113 | + return DateUtils.getStartOfDay(fileDate).after(startTime) && DateUtils.getEndOfDay(fileDate).before(endTime); | |
| 114 | + }); | |
| 98 | 115 | |
| 116 | + if (dateFiles != null && dateFiles.length > 0) { | |
| 117 | + for (File dateFile : dateFiles) { | |
| 118 | + // TODO 按时间获取文件 | |
| 119 | + File[] files = dateFile.listFiles((File dir, String name) ->{ | |
| 120 | + boolean filterResult = false; | |
| 121 | + if (name.contains(":") && name.endsWith(".mp4") && !name.startsWith(".")){ | |
| 122 | + String[] timeArray = name.split("-"); | |
| 123 | + if (timeArray.length == 2){ | |
| 124 | + String fileStartTimeStr = dateFile.getName() + " " + timeArray[0]; | |
| 125 | + String fileEndTimeStr = dateFile.getName() + " " + timeArray[1]; | |
| 126 | + try { | |
| 127 | + filterResult = formatter.parse(fileStartTimeStr).after(startTime) && formatter.parse(fileEndTimeStr).before(endTime); | |
| 128 | + } catch (ParseException e) { | |
| 129 | + logger.error("过滤视频文件时异常: {}-{}", name, e.getMessage()); | |
| 130 | + return false; | |
| 131 | + } | |
| 132 | + } | |
| 133 | + } | |
| 134 | + return filterResult; | |
| 135 | + }); | |
| 136 | + | |
| 137 | + List<File> fileList = Arrays.asList(files); | |
| 138 | + result.addAll(fileList); | |
| 139 | + } | |
| 140 | + } | |
| 141 | + if (result.size() > 0) { | |
| 142 | + result.sort((File f1, File f2) -> { | |
| 143 | + boolean sortResult = false; | |
| 144 | + String[] timeArray1 = f1.getName().split("-"); | |
| 145 | + String[] timeArray2 = f2.getName().split("-"); | |
| 146 | + if (timeArray1.length == 2 && timeArray2.length == 2){ | |
| 147 | + File dateFile1 = f1.getParentFile(); | |
| 148 | + File dateFile2 = f2.getParentFile(); | |
| 149 | + String fileStartTimeStr1 = dateFile1.getName() + " " + timeArray1[0]; | |
| 150 | + String fileStartTimeStr2 = dateFile2.getName() + " " + timeArray1[0]; | |
| 151 | + try { | |
| 152 | + sortResult = formatter.parse(fileStartTimeStr1).before(formatter.parse(fileStartTimeStr2)) ; | |
| 153 | + } catch (ParseException e) { | |
| 154 | + logger.error("排序视频文件时异常: {}-{}", fileStartTimeStr1, fileStartTimeStr2); | |
| 155 | + } | |
| 156 | + } | |
| 157 | + return sortResult?1 : 0; | |
| 158 | + }); | |
| 159 | + } | |
| 99 | 160 | return result; |
| 100 | 161 | } |
| 101 | 162 | |
| 163 | + | |
| 164 | + public void mergeOrCut(String app, String stream, Date startTime, Date endTime) { | |
| 165 | + List<File> filesInTime = this.getFilesInTime(app, stream, startTime, endTime); | |
| 166 | + | |
| 167 | + } | |
| 168 | + | |
| 102 | 169 | } | ... | ... |
src/main/java/top/panll/assist/utils/DateUtils.java
0 → 100644
| 1 | +package top.panll.assist.utils; | |
| 2 | + | |
| 3 | +import java.time.Instant; | |
| 4 | +import java.time.LocalDateTime; | |
| 5 | +import java.time.LocalTime; | |
| 6 | +import java.time.ZoneId; | |
| 7 | +import java.util.Date; | |
| 8 | + | |
| 9 | +public class DateUtils { | |
| 10 | + | |
| 11 | + // 获得某天最大时间 2020-02-19 23:59:59 | |
| 12 | + public static Date getEndOfDay(Date date) { | |
| 13 | + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(date.getTime()), ZoneId.systemDefault());; | |
| 14 | + LocalDateTime endOfDay = localDateTime.with(LocalTime.MAX); | |
| 15 | + return Date.from(endOfDay.atZone(ZoneId.systemDefault()).toInstant()); | |
| 16 | + } | |
| 17 | + | |
| 18 | + // 获得某天最小时间 2020-02-17 00:00:00 | |
| 19 | + public static Date getStartOfDay(Date date) { | |
| 20 | + LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(date.getTime()), ZoneId.systemDefault()); | |
| 21 | + LocalDateTime startOfDay = localDateTime.with(LocalTime.MIN); | |
| 22 | + return Date.from(startOfDay.atZone(ZoneId.systemDefault()).toInstant()); | |
| 23 | + } | |
| 24 | + | |
| 25 | +} | ... | ... |