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,6 +39,14 @@ public class StartConfig implements CommandLineRunner { | ||
| 39 | logger.error("[userSettings.record]配置错误,请检查路径是否存在"); | 39 | logger.error("[userSettings.record]配置错误,请检查路径是否存在"); |
| 40 | System.exit(1); | 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 | try { | 50 | try { |
| 43 | String ffmpegPath = userSettings.getFfmpeg(); | 51 | String ffmpegPath = userSettings.getFfmpeg(); |
| 44 | String ffprobePath = userSettings.getFfprobe(); | 52 | String ffprobePath = userSettings.getFfprobe(); |
src/main/java/top/panll/assist/service/FFmpegExecUtils.java
| 1 | package top.panll.assist.service; | 1 | package top.panll.assist.service; |
| 2 | 2 | ||
| 3 | import net.bramp.ffmpeg.FFmpeg; | 3 | import net.bramp.ffmpeg.FFmpeg; |
| 4 | +import net.bramp.ffmpeg.FFmpegExecutor; | ||
| 5 | +import net.bramp.ffmpeg.FFmpegUtils; | ||
| 4 | import net.bramp.ffmpeg.FFprobe; | 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 | public class FFmpegExecUtils { | 21 | public class FFmpegExecUtils { |
| 7 | 22 | ||
| @@ -24,4 +39,82 @@ public class FFmpegExecUtils { | @@ -24,4 +39,82 @@ public class FFmpegExecUtils { | ||
| 24 | public FFprobe ffprobe; | 39 | public FFprobe ffprobe; |
| 25 | public FFmpeg ffmpeg; | 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,15 +8,15 @@ import org.springframework.beans.factory.annotation.Autowired; | ||
| 8 | import org.springframework.stereotype.Service; | 8 | import org.springframework.stereotype.Service; |
| 9 | import top.panll.assist.config.StartConfig; | 9 | import top.panll.assist.config.StartConfig; |
| 10 | import top.panll.assist.dto.UserSettings; | 10 | import top.panll.assist.dto.UserSettings; |
| 11 | +import top.panll.assist.utils.DateUtils; | ||
| 11 | 12 | ||
| 12 | import java.io.File; | 13 | import java.io.File; |
| 13 | import java.io.FilenameFilter; | 14 | import java.io.FilenameFilter; |
| 14 | import java.io.IOException; | 15 | import java.io.IOException; |
| 16 | +import java.lang.reflect.Array; | ||
| 15 | import java.text.ParseException; | 17 | import java.text.ParseException; |
| 16 | import java.text.SimpleDateFormat; | 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 | @Service | 21 | @Service |
| 22 | public class VideoFileService { | 22 | public class VideoFileService { |
| @@ -27,6 +27,11 @@ public class VideoFileService { | @@ -27,6 +27,11 @@ public class VideoFileService { | ||
| 27 | private UserSettings userSettings; | 27 | private UserSettings userSettings; |
| 28 | 28 | ||
| 29 | 29 | ||
| 30 | + /** | ||
| 31 | + * 对视频文件重命名, 00:00:00-00:00:00 | ||
| 32 | + * @param file | ||
| 33 | + * @throws ParseException | ||
| 34 | + */ | ||
| 30 | public void handFile(File file) throws ParseException { | 35 | public void handFile(File file) throws ParseException { |
| 31 | FFprobe ffprobe = FFmpegExecUtils.getInstance().ffprobe; | 36 | FFprobe ffprobe = FFmpegExecUtils.getInstance().ffprobe; |
| 32 | if(file.isFile() && !file.getName().startsWith(".") && file.getName().indexOf(":") < 0) { | 37 | if(file.isFile() && !file.getName().startsWith(".") && file.getName().indexOf(":") < 0) { |
| @@ -54,6 +59,14 @@ public class VideoFileService { | @@ -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 | public List<File> getFilesInTime(String app, String stream, Date startTime, Date endTime){ | 70 | public List<File> getFilesInTime(String app, String stream, Date startTime, Date endTime){ |
| 58 | 71 | ||
| 59 | List<File> result = new ArrayList<>(); | 72 | List<File> result = new ArrayList<>(); |
| @@ -63,17 +76,15 @@ public class VideoFileService { | @@ -63,17 +76,15 @@ public class VideoFileService { | ||
| 63 | } | 76 | } |
| 64 | 77 | ||
| 65 | SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); | 78 | SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); |
| 79 | + SimpleDateFormat formatterForDate = new SimpleDateFormat("yyyy-MM-dd"); | ||
| 66 | String startTimeStr = formatter.format(startTime); | 80 | String startTimeStr = formatter.format(startTime); |
| 67 | String endTimeStr = formatter.format(endTime); | 81 | String endTimeStr = formatter.format(endTime); |
| 68 | logger.debug("获取[app: {}, stream: {}, statime: {}, endTime: {}]的视频", app, stream, | 82 | logger.debug("获取[app: {}, stream: {}, statime: {}, endTime: {}]的视频", app, stream, |
| 69 | startTimeStr, endTimeStr); | 83 | startTimeStr, endTimeStr); |
| 70 | 84 | ||
| 71 | File file = new File(userSettings.getRecord()); | 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 | return app.equals(name); | 87 | return app.equals(name); |
| 76 | - } | ||
| 77 | }); | 88 | }); |
| 78 | if (appFiles.length == 0) { | 89 | if (appFiles.length == 0) { |
| 79 | logger.warn("获取[app: {}, stream: {}, statime: {}, endTime: {}]的视频时未找到目录: {}", app, stream, | 90 | logger.warn("获取[app: {}, stream: {}, statime: {}, endTime: {}]的视频时未找到目录: {}", app, stream, |
| @@ -81,11 +92,8 @@ public class VideoFileService { | @@ -81,11 +92,8 @@ public class VideoFileService { | ||
| 81 | return result; | 92 | return result; |
| 82 | } | 93 | } |
| 83 | File appFile = appFiles[0]; | 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 | return stream.equals(name); | 96 | return stream.equals(name); |
| 88 | - } | ||
| 89 | }); | 97 | }); |
| 90 | if (streamFiles.length == 0) { | 98 | if (streamFiles.length == 0) { |
| 91 | logger.warn("获取[app: {}, stream: {}, statime: {}, endTime: {}]的视频时未找到目录: {}", app, stream, | 99 | logger.warn("获取[app: {}, stream: {}, statime: {}, endTime: {}]的视频时未找到目录: {}", app, stream, |
| @@ -93,10 +101,69 @@ public class VideoFileService { | @@ -93,10 +101,69 @@ public class VideoFileService { | ||
| 93 | return result; | 101 | return result; |
| 94 | } | 102 | } |
| 95 | File streamFile = streamFiles[0]; | 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 | return result; | 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 | +} |