Commit 60dfc69c58f9fcaea1baea2f76cbf78fe19c86dd
1 parent
7360de7f
优化录像路径配置,不再使用zlm默认http服务器
Showing
32 changed files
with
504 additions
and
88 deletions
.gitignore
100644 → 100755
LICENSE
100644 → 100755
README.md
100644 → 100755
pom.xml
100644 → 100755
| ... | ... | @@ -69,10 +69,28 @@ |
| 69 | 69 | </dependency> |
| 70 | 70 | |
| 71 | 71 | <dependency> |
| 72 | + <groupId>org.mp4parser</groupId> | |
| 73 | + <artifactId>muxer</artifactId> | |
| 74 | + <version>1.9.56</version> | |
| 75 | + </dependency> | |
| 76 | + <dependency> | |
| 77 | + <groupId>org.mp4parser</groupId> | |
| 78 | + <artifactId>streaming</artifactId> | |
| 79 | + <version>1.9.56</version> | |
| 80 | + </dependency> | |
| 81 | + | |
| 82 | + <dependency> | |
| 83 | + <groupId>org.mp4parser</groupId> | |
| 84 | + <artifactId>isoparser</artifactId> | |
| 85 | + <version>1.9.27</version> | |
| 86 | + </dependency> | |
| 87 | + | |
| 88 | + <dependency> | |
| 72 | 89 | <groupId>org.springframework.boot</groupId> |
| 73 | 90 | <artifactId>spring-boot-starter-test</artifactId> |
| 74 | 91 | <scope>test</scope> |
| 75 | 92 | </dependency> |
| 93 | + | |
| 76 | 94 | </dependencies> |
| 77 | 95 | |
| 78 | 96 | <build> | ... | ... |
src/main/java/top/panll/assist/WvpProAssistApplication.java
100644 → 100755
src/main/java/top/panll/assist/config/FastJsonRedisSerializer.java
100644 → 100755
src/main/java/top/panll/assist/config/GlobalExceptionHandler.java
100644 → 100755
src/main/java/top/panll/assist/config/GlobalResponseAdvice.java
100644 → 100755
src/main/java/top/panll/assist/config/RedisConfig.java
100644 → 100755
src/main/java/top/panll/assist/config/SpringDocConfig.java
100644 → 100755
src/main/java/top/panll/assist/config/StartConfig.java
100644 → 100755
| ... | ... | @@ -41,29 +41,39 @@ public class StartConfig implements CommandLineRunner { |
| 41 | 41 | if (!record.endsWith(File.separator)) { |
| 42 | 42 | userSettings.setRecord(userSettings.getRecord() + File.separator); |
| 43 | 43 | } |
| 44 | + | |
| 44 | 45 | File recordFile = new File(record); |
| 45 | - if (!recordFile.exists() || !recordFile.isDirectory()) { | |
| 46 | - logger.error("[userSettings.record]配置错误,请检查路径是否存在"); | |
| 47 | - System.exit(1); | |
| 48 | - } | |
| 49 | - if (!recordFile.canRead()) { | |
| 50 | - logger.error("[userSettings.record]路径无法读取"); | |
| 51 | - System.exit(1); | |
| 52 | - } | |
| 53 | - if (!recordFile.canWrite()) { | |
| 54 | - logger.error("[userSettings.record]路径无法写入"); | |
| 55 | - System.exit(1); | |
| 46 | + if (!recordFile.exists()){ | |
| 47 | + logger.warn("[userSettings.record]路径不存在,开始创建"); | |
| 48 | + boolean mkResult = recordFile.mkdirs(); | |
| 49 | + if (!mkResult) { | |
| 50 | + logger.info("[userSettings.record]目录创建失败"); | |
| 51 | + System.exit(1); | |
| 52 | + } | |
| 53 | + }else { | |
| 54 | + if ( !recordFile.isDirectory()) { | |
| 55 | + logger.warn("[userSettings.record]路径是文件,请修改为目录"); | |
| 56 | + System.exit(1); | |
| 57 | + } | |
| 58 | + if (!recordFile.canRead()) { | |
| 59 | + logger.error("[userSettings.record]路径无法读取"); | |
| 60 | + System.exit(1); | |
| 61 | + } | |
| 62 | + if (!recordFile.canWrite()) { | |
| 63 | + logger.error("[userSettings.record]路径无法写入"); | |
| 64 | + System.exit(1); | |
| 65 | + } | |
| 56 | 66 | } |
| 57 | - // 在zlm目录写入assist下载页面 | |
| 58 | - writeAssistDownPage(recordFile); | |
| 67 | + | |
| 59 | 68 | try { |
| 60 | 69 | |
| 61 | -// FFmpegExecUtils.getInstance().ffmpeg = ffmpeg; | |
| 62 | -// FFmpegExecUtils.getInstance().ffprobe = ffprobe; | |
| 63 | 70 | // 对目录进行预整理 |
| 64 | 71 | File[] appFiles = recordFile.listFiles(); |
| 65 | 72 | if (appFiles != null && appFiles.length > 0) { |
| 66 | 73 | for (File appFile : appFiles) { |
| 74 | + if (appFile.getName().equals("recordTemp")) { | |
| 75 | + continue; | |
| 76 | + } | |
| 67 | 77 | File[] streamFiles = appFile.listFiles(); |
| 68 | 78 | if (streamFiles != null && streamFiles.length > 0) { |
| 69 | 79 | for (File streamFile : streamFiles) { |
| ... | ... | @@ -91,48 +101,48 @@ public class StartConfig implements CommandLineRunner { |
| 91 | 101 | } |
| 92 | 102 | } |
| 93 | 103 | |
| 94 | - private void writeAssistDownPage(File recordFile) { | |
| 95 | - try { | |
| 96 | - File file = new File(recordFile.getParentFile().getAbsolutePath(), "download.html"); | |
| 97 | - if (file.exists()) { | |
| 98 | - file.delete(); | |
| 99 | - } | |
| 100 | - file.createNewFile(); | |
| 101 | - FileOutputStream fs = new FileOutputStream(file); | |
| 102 | - StringBuffer stringBuffer = new StringBuffer(); | |
| 103 | - String content = "<!DOCTYPE html>\n" + | |
| 104 | - "<html lang=\"en\">\n" + | |
| 105 | - "<head>\n" + | |
| 106 | - " <meta charset=\"UTF-8\">\n" + | |
| 107 | - " <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n" + | |
| 108 | - " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n" + | |
| 109 | - " <title>下载</title>\n" + | |
| 110 | - "</head>\n" + | |
| 111 | - "<body>\n" + | |
| 112 | - " <a id=\"download\" download />\n" + | |
| 113 | - " <script>\n" + | |
| 114 | - " (function(){\n" + | |
| 115 | - " let searchParams = new URLSearchParams(location.search);\n" + | |
| 116 | - " var download = document.getElementById(\"download\");\n" + | |
| 117 | - " download.setAttribute(\"href\", searchParams.get(\"url\"))\n" + | |
| 118 | - " download.click()\n" + | |
| 119 | - " setTimeout(()=>{\n" + | |
| 120 | - " window.location.href=\"about:blank\";\n" + | |
| 121 | - "\t\t\t window.close();\n" + | |
| 122 | - " },200)\n" + | |
| 123 | - " })();\n" + | |
| 124 | - " \n" + | |
| 125 | - " </script>\n" + | |
| 126 | - "</body>\n" + | |
| 127 | - "</html>"; | |
| 128 | - fs.write(content.getBytes(StandardCharsets.UTF_8)); | |
| 129 | - logger.info("已写入html配置页面: " + file.getAbsolutePath()); | |
| 130 | - } catch (FileNotFoundException e) { | |
| 131 | - logger.error("写入html页面错误", e); | |
| 132 | - } catch (IOException e) { | |
| 133 | - logger.error("写入html页面错误", e); | |
| 134 | - } | |
| 135 | - | |
| 136 | - | |
| 137 | - } | |
| 104 | +// private void writeAssistDownPage(File recordFile) { | |
| 105 | +// try { | |
| 106 | +// File file = new File(recordFile.getParentFile().getAbsolutePath(), "download.html"); | |
| 107 | +// if (file.exists()) { | |
| 108 | +// file.delete(); | |
| 109 | +// } | |
| 110 | +// file.createNewFile(); | |
| 111 | +// FileOutputStream fs = new FileOutputStream(file); | |
| 112 | +// StringBuffer stringBuffer = new StringBuffer(); | |
| 113 | +// String content = "<!DOCTYPE html>\n" + | |
| 114 | +// "<html lang=\"en\">\n" + | |
| 115 | +// "<head>\n" + | |
| 116 | +// " <meta charset=\"UTF-8\">\n" + | |
| 117 | +// " <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n" + | |
| 118 | +// " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n" + | |
| 119 | +// " <title>下载</title>\n" + | |
| 120 | +// "</head>\n" + | |
| 121 | +// "<body>\n" + | |
| 122 | +// " <a id=\"download\" download />\n" + | |
| 123 | +// " <script>\n" + | |
| 124 | +// " (function(){\n" + | |
| 125 | +// " let searchParams = new URLSearchParams(location.search);\n" + | |
| 126 | +// " var download = document.getElementById(\"download\");\n" + | |
| 127 | +// " download.setAttribute(\"href\", searchParams.get(\"url\"))\n" + | |
| 128 | +// " download.click()\n" + | |
| 129 | +// " setTimeout(()=>{\n" + | |
| 130 | +// " window.location.href=\"about:blank\";\n" + | |
| 131 | +// "\t\t\t window.close();\n" + | |
| 132 | +// " },200)\n" + | |
| 133 | +// " })();\n" + | |
| 134 | +// " \n" + | |
| 135 | +// " </script>\n" + | |
| 136 | +// "</body>\n" + | |
| 137 | +// "</html>"; | |
| 138 | +// fs.write(content.getBytes(StandardCharsets.UTF_8)); | |
| 139 | +// logger.info("已写入html配置页面: " + file.getAbsolutePath()); | |
| 140 | +// } catch (FileNotFoundException e) { | |
| 141 | +// logger.error("写入html页面错误", e); | |
| 142 | +// } catch (IOException e) { | |
| 143 | +// logger.error("写入html页面错误", e); | |
| 144 | +// } | |
| 145 | +// | |
| 146 | +// | |
| 147 | +// } | |
| 138 | 148 | } | ... | ... |
src/main/java/top/panll/assist/config/ThreadPoolTaskConfig.java
100644 → 100755
src/main/java/top/panll/assist/controller/DownController.java
0 → 100644
| 1 | +package top.panll.assist.controller; | |
| 2 | + | |
| 3 | + | |
| 4 | +import org.apache.catalina.connector.ClientAbortException; | |
| 5 | +import org.mp4parser.BasicContainer; | |
| 6 | +import org.mp4parser.Container; | |
| 7 | +import org.mp4parser.muxer.Movie; | |
| 8 | +import org.mp4parser.muxer.Track; | |
| 9 | +import org.mp4parser.muxer.builder.DefaultMp4Builder; | |
| 10 | +import org.mp4parser.muxer.builder.Mp4Builder; | |
| 11 | +import org.mp4parser.muxer.container.mp4.MovieCreator; | |
| 12 | +import org.mp4parser.muxer.tracks.AppendTrack; | |
| 13 | +import org.slf4j.Logger; | |
| 14 | +import org.slf4j.LoggerFactory; | |
| 15 | +import org.springframework.beans.factory.annotation.Autowired; | |
| 16 | +import org.springframework.stereotype.Controller; | |
| 17 | +import org.springframework.web.bind.annotation.GetMapping; | |
| 18 | +import org.springframework.web.bind.annotation.RequestMapping; | |
| 19 | +import org.springframework.web.bind.annotation.ResponseBody; | |
| 20 | +import top.panll.assist.dto.UserSettings; | |
| 21 | + | |
| 22 | +import javax.servlet.http.HttpServletRequest; | |
| 23 | +import javax.servlet.http.HttpServletResponse; | |
| 24 | +import java.io.*; | |
| 25 | +import java.nio.channels.Channels; | |
| 26 | +import java.nio.channels.WritableByteChannel; | |
| 27 | +import java.nio.charset.StandardCharsets; | |
| 28 | +import java.util.ArrayList; | |
| 29 | +import java.util.LinkedList; | |
| 30 | +import java.util.List; | |
| 31 | + | |
| 32 | +@Controller | |
| 33 | +@RequestMapping("/down") | |
| 34 | +public class DownController { | |
| 35 | + | |
| 36 | + private final static Logger logger = LoggerFactory.getLogger(DownController.class); | |
| 37 | + | |
| 38 | + @Autowired | |
| 39 | + private UserSettings userSettings; | |
| 40 | + | |
| 41 | + /** | |
| 42 | + * 获取app+stream列表 | |
| 43 | + * | |
| 44 | + * @return | |
| 45 | + */ | |
| 46 | + @GetMapping(value = "/**") | |
| 47 | + @ResponseBody | |
| 48 | + public void download(HttpServletRequest request, HttpServletResponse response) throws IOException { | |
| 49 | + | |
| 50 | + List<String> videoList = new ArrayList<>(); | |
| 51 | + videoList.add("/home/lin/server/test/zlm/Debug/www/record/rtp/34020000002000000003_34020000001310000001/2023-03-20/16-09-07.mp4"); | |
| 52 | + videoList.add("/home/lin/server/test/zlm/Debug/www/record/rtp/34020000002000000003_34020000001310000001/2023-03-20/17-12-10.mp4"); | |
| 53 | + videoList.add("/home/lin/server/test/zlm/Debug/www/record/rtp/34020000002000000003_34020000001310000001/2023-03-20/17-38-36.mp4"); | |
| 54 | + List<Movie> sourceMovies = new ArrayList<>(); | |
| 55 | + for (String video : videoList) { | |
| 56 | + sourceMovies.add(MovieCreator.build(video)); | |
| 57 | + } | |
| 58 | + | |
| 59 | + List<Track> videoTracks = new LinkedList<>(); | |
| 60 | + List<Track> audioTracks = new LinkedList<>(); | |
| 61 | + for (Movie movie : sourceMovies) { | |
| 62 | + for (Track track : movie.getTracks()) { | |
| 63 | + if ("soun".equals(track.getHandler())) { | |
| 64 | + audioTracks.add(track); | |
| 65 | + } | |
| 66 | + | |
| 67 | + if ("vide".equals(track.getHandler())) { | |
| 68 | + videoTracks.add(track); | |
| 69 | + } | |
| 70 | + } | |
| 71 | + } | |
| 72 | + Movie mergeMovie = new Movie(); | |
| 73 | + if (audioTracks.size() > 0) { | |
| 74 | + mergeMovie.addTrack(new AppendTrack(audioTracks.toArray(new Track[audioTracks.size()]))); | |
| 75 | + } | |
| 76 | + | |
| 77 | + if (videoTracks.size() > 0) { | |
| 78 | + mergeMovie.addTrack(new AppendTrack(videoTracks.toArray(new Track[videoTracks.size()]))); | |
| 79 | + } | |
| 80 | + | |
| 81 | + BasicContainer out = (BasicContainer)new DefaultMp4Builder().build(mergeMovie); | |
| 82 | + | |
| 83 | + // 文件名 | |
| 84 | + String fileName = "测试.mp4"; | |
| 85 | + // 文件类型 | |
| 86 | + String contentType = request.getServletContext().getMimeType(fileName); | |
| 87 | + | |
| 88 | + // 解决下载文件时文件名乱码问题 | |
| 89 | + byte[] fileNameBytes = fileName.getBytes(StandardCharsets.UTF_8); | |
| 90 | + fileName = new String(fileNameBytes, 0, fileNameBytes.length, StandardCharsets.ISO_8859_1); | |
| 91 | + | |
| 92 | + response.setHeader("Content-Type", contentType); | |
| 93 | + response.setHeader("Content-Length", String.valueOf(out)); | |
| 94 | + //inline表示浏览器直接使用,attachment表示下载,fileName表示下载的文件名 | |
| 95 | + response.setHeader("Content-Disposition", "inline;filename=" + fileName); | |
| 96 | + response.setContentType(contentType); | |
| 97 | + | |
| 98 | + WritableByteChannel writableByteChannel = Channels.newChannel(response.getOutputStream()); | |
| 99 | + out.writeContainer(writableByteChannel); | |
| 100 | + writableByteChannel.close(); | |
| 101 | + | |
| 102 | + } | |
| 103 | +} | ... | ... |
src/main/java/top/panll/assist/controller/DownloadController.java
0 → 100644
| 1 | +package top.panll.assist.controller; | |
| 2 | + | |
| 3 | + | |
| 4 | +import io.swagger.v3.oas.annotations.Operation; | |
| 5 | +import io.swagger.v3.oas.annotations.Parameter; | |
| 6 | +import io.swagger.v3.oas.annotations.tags.Tag; | |
| 7 | +import org.apache.catalina.connector.ClientAbortException; | |
| 8 | +import org.slf4j.Logger; | |
| 9 | +import org.slf4j.LoggerFactory; | |
| 10 | +import org.springframework.beans.factory.annotation.Autowired; | |
| 11 | +import org.springframework.stereotype.Controller; | |
| 12 | +import org.springframework.web.bind.annotation.*; | |
| 13 | +import top.panll.assist.dto.UserSettings; | |
| 14 | +import top.panll.assist.utils.PageInfo; | |
| 15 | + | |
| 16 | +import javax.servlet.http.HttpServletRequest; | |
| 17 | +import javax.servlet.http.HttpServletResponse; | |
| 18 | +import java.io.BufferedOutputStream; | |
| 19 | +import java.io.File; | |
| 20 | +import java.io.IOException; | |
| 21 | +import java.io.RandomAccessFile; | |
| 22 | +import java.nio.charset.StandardCharsets; | |
| 23 | +import java.util.List; | |
| 24 | +import java.util.Map; | |
| 25 | + | |
| 26 | +@Controller | |
| 27 | +@RequestMapping("/download") | |
| 28 | +public class DownloadController { | |
| 29 | + | |
| 30 | + private final static Logger logger = LoggerFactory.getLogger(DownloadController.class); | |
| 31 | + | |
| 32 | + @Autowired | |
| 33 | + private UserSettings userSettings; | |
| 34 | + | |
| 35 | + /** | |
| 36 | + * 获取app+stream列表 | |
| 37 | + * | |
| 38 | + * @return | |
| 39 | + */ | |
| 40 | + @GetMapping(value = "/**") | |
| 41 | + @ResponseBody | |
| 42 | + public void download(HttpServletRequest request, HttpServletResponse response) { | |
| 43 | + | |
| 44 | + String resourcePath = request.getServletPath(); | |
| 45 | + System.out.println(resourcePath); | |
| 46 | + resourcePath = resourcePath.substring("/download".length() + 1, resourcePath.length()); | |
| 47 | + String record = userSettings.getRecord(); | |
| 48 | +// if (record.endsWith("/")) { | |
| 49 | +// record = record.substring(0, record.length() - 1); | |
| 50 | +// System.out.println(record); | |
| 51 | +// } | |
| 52 | + System.out.println(record + resourcePath); | |
| 53 | + File file = new File(record + resourcePath); | |
| 54 | + if (!file.exists()) { | |
| 55 | + response.setStatus(HttpServletResponse.SC_NOT_FOUND); | |
| 56 | + return; | |
| 57 | + } | |
| 58 | + | |
| 59 | + /** | |
| 60 | + * 参考实现来自: CSDN 进修的CODER SpringBoot Java实现Http方式分片下载断点续传+实现H5大视频渐进式播放 | |
| 61 | + * https://blog.csdn.net/lovequanquqn/article/details/104562945 | |
| 62 | + */ | |
| 63 | + String range = request.getHeader("Range"); | |
| 64 | + logger.info("current request rang:" + range); | |
| 65 | + //开始下载位置 | |
| 66 | + long startByte = 0; | |
| 67 | + //结束下载位置 | |
| 68 | + long endByte = file.length() - 1; | |
| 69 | + logger.info("文件开始位置:{},文件结束位置:{},文件总长度:{}", startByte, endByte, file.length()); | |
| 70 | + | |
| 71 | + //有range的话 | |
| 72 | + if (range != null && range.contains("bytes=") && range.contains("-")) { | |
| 73 | + range = range.substring(range.lastIndexOf("=") + 1).trim(); | |
| 74 | + String[] ranges = range.split("-"); | |
| 75 | + try { | |
| 76 | + //判断range的类型 | |
| 77 | + if (ranges.length == 1) { | |
| 78 | + // 类型一:bytes=-2343, | |
| 79 | + if (range.startsWith("-")) { | |
| 80 | + endByte = Long.parseLong(ranges[0]); | |
| 81 | + } | |
| 82 | + //类型二:bytes=2343- | |
| 83 | + else if (range.endsWith("-")) { | |
| 84 | + startByte = Long.parseLong(ranges[0]); | |
| 85 | + } | |
| 86 | + } | |
| 87 | + //类型三:bytes=22-2343 | |
| 88 | + else if (ranges.length == 2) { | |
| 89 | + startByte = Long.parseLong(ranges[0]); | |
| 90 | + endByte = Long.parseLong(ranges[1]); | |
| 91 | + } | |
| 92 | + | |
| 93 | + } catch (NumberFormatException e) { | |
| 94 | + startByte = 0; | |
| 95 | + endByte = file.length() - 1; | |
| 96 | + logger.error("Range Occur Error,Message:{}", e.getLocalizedMessage()); | |
| 97 | + } | |
| 98 | + | |
| 99 | + | |
| 100 | + } | |
| 101 | + | |
| 102 | + // 要下载的长度 | |
| 103 | + long contentLength = endByte - startByte + 1; | |
| 104 | + // 文件名 | |
| 105 | + String fileName = file.getName(); | |
| 106 | + // 文件类型 | |
| 107 | + String contentType = request.getServletContext().getMimeType(fileName); | |
| 108 | + | |
| 109 | + // 解决下载文件时文件名乱码问题 | |
| 110 | + byte[] fileNameBytes = fileName.getBytes(StandardCharsets.UTF_8); | |
| 111 | + fileName = new String(fileNameBytes, 0, fileNameBytes.length, StandardCharsets.ISO_8859_1); | |
| 112 | + | |
| 113 | + response.setHeader("Content-Type", contentType); | |
| 114 | + response.setHeader("Content-Length", String.valueOf(contentLength)); | |
| 115 | + //inline表示浏览器直接使用,attachment表示下载,fileName表示下载的文件名 | |
| 116 | + response.setHeader("Content-Disposition", "inline;filename=" + fileName); | |
| 117 | + response.setContentType(contentType); | |
| 118 | + if (range != null) { | |
| 119 | + //各种响应头设置 | |
| 120 | + //支持断点续传,获取部分字节内容: | |
| 121 | + response.setHeader("Accept-Ranges", "bytes"); | |
| 122 | + //http状态码要为206:表示获取部分内容 | |
| 123 | + response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); | |
| 124 | + // Content-Range,格式为:[要下载的开始位置]-[结束位置]/[文件总大小] | |
| 125 | + response.setHeader("Content-Range", "bytes " + startByte + "-" + endByte + "/" + file.length()); | |
| 126 | + } else { | |
| 127 | + response.setStatus(HttpServletResponse.SC_OK); | |
| 128 | + } | |
| 129 | + | |
| 130 | + | |
| 131 | + BufferedOutputStream outputStream = null; | |
| 132 | + RandomAccessFile randomAccessFile = null; | |
| 133 | + //已传送数据大小 | |
| 134 | + long transmitted = 0; | |
| 135 | + try { | |
| 136 | + randomAccessFile = new RandomAccessFile(file, "r"); | |
| 137 | + | |
| 138 | + outputStream = new BufferedOutputStream(response.getOutputStream()); | |
| 139 | + byte[] buff = new byte[4096]; | |
| 140 | + int len = 0; | |
| 141 | + randomAccessFile.seek(startByte); | |
| 142 | + //warning:判断是否到了最后不足4096(buff的length)个byte这个逻辑((transmitted + len) <= contentLength)要放前面 | |
| 143 | + //不然会会先读取randomAccessFile,造成后面读取位置出错; | |
| 144 | + while ((transmitted + len) <= contentLength && (len = randomAccessFile.read(buff)) != -1) { | |
| 145 | + outputStream.write(buff, 0, len); | |
| 146 | + transmitted += len; | |
| 147 | + } | |
| 148 | + //处理不足buff.length部分 | |
| 149 | + if (transmitted < contentLength) { | |
| 150 | + len = randomAccessFile.read(buff, 0, (int) (contentLength - transmitted)); | |
| 151 | + outputStream.write(buff, 0, len); | |
| 152 | + transmitted += len; | |
| 153 | + } | |
| 154 | + | |
| 155 | + outputStream.flush(); | |
| 156 | + response.flushBuffer(); | |
| 157 | + randomAccessFile.close(); | |
| 158 | + logger.info("下载完毕:" + startByte + "-" + endByte + ":" + transmitted); | |
| 159 | + } catch (ClientAbortException e) { | |
| 160 | + logger.warn("用户停止下载:" + startByte + "-" + endByte + ":" + transmitted); | |
| 161 | + //捕获此异常表示拥护停止下载 | |
| 162 | + } catch (IOException e) { | |
| 163 | + e.printStackTrace(); | |
| 164 | + logger.error("用户下载IO异常,Message:{}", e.getLocalizedMessage()); | |
| 165 | + } finally { | |
| 166 | + try { | |
| 167 | + if (randomAccessFile != null) { | |
| 168 | + randomAccessFile.close(); | |
| 169 | + } | |
| 170 | + } catch (IOException e) { | |
| 171 | + e.printStackTrace(); | |
| 172 | + } | |
| 173 | + }///end try | |
| 174 | + } | |
| 175 | +} | ... | ... |
src/main/java/top/panll/assist/controller/RecordController.java
100644 → 100755
| ... | ... | @@ -24,10 +24,8 @@ import javax.servlet.http.HttpServletRequest; |
| 24 | 24 | import java.io.File; |
| 25 | 25 | import java.text.ParseException; |
| 26 | 26 | import java.text.SimpleDateFormat; |
| 27 | -import java.util.ArrayList; | |
| 28 | -import java.util.Date; | |
| 29 | -import java.util.List; | |
| 30 | -import java.util.Map; | |
| 27 | +import java.util.*; | |
| 28 | + | |
| 31 | 29 | @Tag(name = "录像管理", description = "录像管理") |
| 32 | 30 | @CrossOrigin |
| 33 | 31 | @RestController |
| ... | ... | @@ -49,6 +47,16 @@ public class RecordController { |
| 49 | 47 | |
| 50 | 48 | |
| 51 | 49 | /** |
| 50 | + * 获取Assist服务配置信息 | |
| 51 | + */ | |
| 52 | + @Operation(summary ="获取Assist服务配置信息") | |
| 53 | + @GetMapping(value = "/info") | |
| 54 | + @ResponseBody | |
| 55 | + public UserSettings getInfo(){ | |
| 56 | + return userSettings; | |
| 57 | + } | |
| 58 | + | |
| 59 | + /** | |
| 52 | 60 | * 获取app+stream列表 |
| 53 | 61 | * @return |
| 54 | 62 | */ |
| ... | ... | @@ -58,7 +66,7 @@ public class RecordController { |
| 58 | 66 | @GetMapping(value = "/list") |
| 59 | 67 | @ResponseBody |
| 60 | 68 | public PageInfo<Map<String, String>> getList(@RequestParam int page, |
| 61 | - @RequestParam int count){ | |
| 69 | + @RequestParam int count){ | |
| 62 | 70 | List<Map<String, String>> appList = videoFileService.getList(); |
| 63 | 71 | |
| 64 | 72 | PageInfo<Map<String, String>> stringPageInfo = new PageInfo<>(appList); |
| ... | ... | @@ -84,6 +92,7 @@ public class RecordController { |
| 84 | 92 | resultData.add(file.getName()); |
| 85 | 93 | } |
| 86 | 94 | } |
| 95 | + Collections.sort(resultData); | |
| 87 | 96 | |
| 88 | 97 | PageInfo<String> stringPageInfo = new PageInfo<>(resultData); |
| 89 | 98 | stringPageInfo.startPage(page, count); |
| ... | ... | @@ -342,9 +351,10 @@ public class RecordController { |
| 342 | 351 | ret.put("code", 0); |
| 343 | 352 | ret.put("msg", "success"); |
| 344 | 353 | String file_path = json.getString("file_path"); |
| 354 | + | |
| 345 | 355 | String app = json.getString("app"); |
| 346 | 356 | String stream = json.getString("stream"); |
| 347 | - logger.debug("ZLM 录制完成,参数:" + file_path); | |
| 357 | + logger.debug("ZLM 录制完成,文件路径:" + file_path); | |
| 348 | 358 | |
| 349 | 359 | if (file_path == null) { |
| 350 | 360 | return new ResponseEntity<String>(ret.toString(), HttpStatus.OK); |
| ... | ... | @@ -356,7 +366,6 @@ public class RecordController { |
| 356 | 366 | videoFileService.handFile(new File(file_path), app, stream); |
| 357 | 367 | } |
| 358 | 368 | |
| 359 | - | |
| 360 | 369 | return new ResponseEntity<String>(ret.toString(), HttpStatus.OK); |
| 361 | 370 | } |
| 362 | 371 | ... | ... |
src/main/java/top/panll/assist/controller/bean/ErrorCode.java
100644 → 100755
src/main/java/top/panll/assist/controller/bean/WVPResult.java
100644 → 100755
src/main/java/top/panll/assist/dto/MergeOrCutTaskInfo.java
100644 → 100755
src/main/java/top/panll/assist/dto/SignInfo.java
100644 → 100755
src/main/java/top/panll/assist/dto/SpaceInfo.java
100644 → 100755
src/main/java/top/panll/assist/dto/UserSettings.java
100644 → 100755
src/main/java/top/panll/assist/service/FFmpegExecUtils.java
100644 → 100755
src/main/java/top/panll/assist/service/FileManagerTimer.java
100644 → 100755
| ... | ... | @@ -39,6 +39,9 @@ public class FileManagerTimer { |
| 39 | 39 | // @Scheduled(fixedDelay = 2000) //测试 20秒执行一次 |
| 40 | 40 | @Scheduled(cron = "0 0 0 * * ?") //每天的0点执行 |
| 41 | 41 | public void execute(){ |
| 42 | + if (userSettings.getRecord() == null) { | |
| 43 | + return; | |
| 44 | + } | |
| 42 | 45 | int recordDay = userSettings.getRecordDay(); |
| 43 | 46 | Date lastDate=new Date(); |
| 44 | 47 | Calendar lastCalendar = Calendar.getInstance(); |
| ... | ... | @@ -115,7 +118,7 @@ public class FileManagerTimer { |
| 115 | 118 | lastTempCalendar.add(Calendar.DAY_OF_MONTH, 0 - recordTempDay); |
| 116 | 119 | lastTempDate = lastTempCalendar.getTime(); |
| 117 | 120 | logger.info("[录像巡查]移除合并任务临时文件 {} 之前的文件", formatter.format(lastTempDate)); |
| 118 | - File recordTempFile = new File(recordFileDir.getParentFile().getAbsolutePath() + File.separator + "recordTemp"); | |
| 121 | + File recordTempFile = new File(userSettings.getRecord() + "recordTemp"); | |
| 119 | 122 | if (recordTempFile.exists() && recordTempFile.isDirectory() && recordTempFile.canWrite()) { |
| 120 | 123 | File[] tempFiles = recordTempFile.listFiles(); |
| 121 | 124 | for (File tempFile : tempFiles) { | ... | ... |
src/main/java/top/panll/assist/service/VideoFileService.java
100644 → 100755
| ... | ... | @@ -48,7 +48,7 @@ public class VideoFileService { |
| 48 | 48 | if (recordFile.isDirectory()) { |
| 49 | 49 | File[] files = recordFile.listFiles((File dir, String name) -> { |
| 50 | 50 | File currentFile = new File(dir.getAbsolutePath() + File.separator + name); |
| 51 | - return currentFile.isDirectory(); | |
| 51 | + return currentFile.isDirectory() && !name.equals("recordTemp"); | |
| 52 | 52 | }); |
| 53 | 53 | List<File> result = Arrays.asList(files); |
| 54 | 54 | if (sort != null && sort) { |
| ... | ... | @@ -144,22 +144,17 @@ public class VideoFileService { |
| 144 | 144 | |
| 145 | 145 | String key = AssistConstants.STREAM_CALL_INFO + userSettings.getId() + "_" + app + "_" + stream; |
| 146 | 146 | String callId = (String) redisUtil.get(key); |
| 147 | - if (callId != null) { | |
| 148 | - | |
| 149 | - File newPath = new File(file.getParentFile().getParent() + "_" + callId + File.separator + file.getParentFile().getName()); | |
| 150 | - if (!newPath.exists()) { | |
| 151 | - newPath.mkdirs(); | |
| 152 | - } | |
| 153 | - String newName = newPath.getAbsolutePath() + File.separator+ simpleDateFormat.format(startTime) + "-" + simpleDateFormat.format(endTime) + "-" + durationLong + ".mp4"; | |
| 154 | - file.renameTo(new File(newName)); | |
| 155 | - }else { | |
| 156 | - String newName = file.getAbsolutePath().replace(file.getName(), | |
| 157 | - simpleDateFormat.format(startTime) + "-" + simpleDateFormat.format(endTime) + "-" + durationLong + ".mp4"); | |
| 158 | 147 | |
| 159 | - file.renameTo(new File(newName)); | |
| 148 | + String streamNew = (callId == null? stream : stream + "_" + callId); | |
| 149 | + File newPath = new File(userSettings.getRecord() + File.separator + app + File.separator + streamNew + File.separator + DateUtils.getDateStr(new Date(startTime))); | |
| 150 | + if (!newPath.exists()) { | |
| 151 | + newPath.mkdirs(); | |
| 160 | 152 | } |
| 161 | 153 | |
| 162 | - logger.debug("[处理文件] {}", file.getName()); | |
| 154 | + String newName = newPath.getAbsolutePath() + File.separator+ simpleDateFormat.format(startTime) + "-" + simpleDateFormat.format(endTime) + "-" + durationLong + ".mp4"; | |
| 155 | + file.renameTo(new File(newName)); | |
| 156 | + System.out.println(file.getAbsolutePath()); | |
| 157 | + logger.info("[处理文件] {}", file.getName()); | |
| 163 | 158 | } catch (IOException e) { |
| 164 | 159 | logger.warn("文件可能以损坏[{}]", file.getAbsolutePath()); |
| 165 | 160 | } catch (ParseException e) { |
| ... | ... | @@ -339,13 +334,13 @@ public class VideoFileService { |
| 339 | 334 | public String mergeOrCut(String app, String stream, Date startTime, Date endTime, String remoteHost) { |
| 340 | 335 | List<File> filesInTime = this.getFilesInTime(app, stream, startTime, endTime); |
| 341 | 336 | if (filesInTime== null || filesInTime.size() == 0){ |
| 342 | - logger.info("此时间段未未找到视频文件"); | |
| 337 | + logger.info("此时间段未未找到视频文件, {}/{} {}->{}", app, stream, DateUtils.getDateTimeStr(startTime), DateUtils.getDateTimeStr(endTime)); | |
| 343 | 338 | return null; |
| 344 | 339 | } |
| 345 | 340 | String taskId = DigestUtils.md5DigestAsHex(String.valueOf(System.currentTimeMillis()).getBytes()); |
| 346 | 341 | logger.info("[录像合并] 开始合并,APP:{}, STREAM: {}, 任务ID:{}", app, stream, taskId); |
| 347 | 342 | String destDir = "recordTemp" + File.separator + taskId + File.separator + app; |
| 348 | - File recordFile = new File(new File(userSettings.getRecord()).getParentFile().getAbsolutePath() + File.separator + destDir ); | |
| 343 | + File recordFile = new File(userSettings.getRecord() + destDir ); | |
| 349 | 344 | if (!recordFile.exists()) { |
| 350 | 345 | recordFile.mkdirs(); |
| 351 | 346 | } |
| ... | ... | @@ -374,7 +369,7 @@ public class VideoFileService { |
| 374 | 369 | mergeOrCutTaskInfo.setPercentage("1"); |
| 375 | 370 | // 处理文件路径 |
| 376 | 371 | String recordFileResultPath = recordFile.getAbsolutePath() + File.separator + stream + ".mp4"; |
| 377 | - Path relativize = Paths.get(userSettings.getRecord()).getParent().relativize(Paths.get(recordFileResultPath)); | |
| 372 | + Path relativize = Paths.get(userSettings.getRecord()).relativize(Paths.get(recordFileResultPath)); | |
| 378 | 373 | try { |
| 379 | 374 | Files.copy(filesInTime.get(0).toPath(), Paths.get(recordFileResultPath)); |
| 380 | 375 | } catch (IOException e) { |
| ... | ... | @@ -384,8 +379,8 @@ public class VideoFileService { |
| 384 | 379 | } |
| 385 | 380 | mergeOrCutTaskInfo.setRecordFile(relativize.toString()); |
| 386 | 381 | if (remoteHost != null) { |
| 387 | - mergeOrCutTaskInfo.setDownloadFile(remoteHost + "/download.html?url=" + relativize); | |
| 388 | - mergeOrCutTaskInfo.setPlayFile(remoteHost + "/" + relativize); | |
| 382 | + mergeOrCutTaskInfo.setDownloadFile(remoteHost + "/download.html?url=download/" + relativize); | |
| 383 | + mergeOrCutTaskInfo.setPlayFile(remoteHost + "/download/" + relativize); | |
| 389 | 384 | } |
| 390 | 385 | String key = String.format("%S_%S_%S_%S_%S", AssistConstants.MERGEORCUT , userSettings.getId(), mergeOrCutTaskInfo.getApp(), mergeOrCutTaskInfo.getStream(), mergeOrCutTaskInfo.getId()); |
| 391 | 386 | redisUtil.set(key, mergeOrCutTaskInfo); |
| ... | ... | @@ -397,7 +392,7 @@ public class VideoFileService { |
| 397 | 392 | mergeOrCutTaskInfo.setPercentage("1"); |
| 398 | 393 | |
| 399 | 394 | // 处理文件路径 |
| 400 | - Path relativize = Paths.get(userSettings.getRecord()).getParent().relativize(Paths.get(result)); | |
| 395 | + Path relativize = Paths.get(userSettings.getRecord()).relativize(Paths.get(result)); | |
| 401 | 396 | mergeOrCutTaskInfo.setRecordFile(relativize.toString()); |
| 402 | 397 | if (remoteHost != null) { |
| 403 | 398 | mergeOrCutTaskInfo.setDownloadFile(remoteHost + "/download.html?url=" + relativize); | ... | ... |
src/main/java/top/panll/assist/utils/DateUtils.java
100644 → 100755
| 1 | 1 | package top.panll.assist.utils; |
| 2 | 2 | |
| 3 | +import java.text.SimpleDateFormat; | |
| 3 | 4 | import java.time.Instant; |
| 4 | 5 | import java.time.LocalDateTime; |
| 5 | 6 | import java.time.LocalTime; |
| 6 | 7 | import java.time.ZoneId; |
| 8 | +import java.time.format.DateTimeFormatter; | |
| 7 | 9 | import java.util.Date; |
| 10 | +import java.util.Locale; | |
| 8 | 11 | |
| 9 | 12 | public class DateUtils { |
| 10 | 13 | |
| 14 | + public static final String PATTERNForDateTime = "yyyy-MM-dd HH:mm:ss"; | |
| 15 | + | |
| 16 | + public static final String PATTERNForDate = "yyyy-MM-dd"; | |
| 17 | + | |
| 18 | + public static final String zoneStr = "Asia/Shanghai"; | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 11 | 22 | // 获得某天最大时间 2020-02-19 23:59:59 |
| 12 | 23 | public static Date getEndOfDay(Date date) { |
| 13 | 24 | LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(date.getTime()), ZoneId.systemDefault());; |
| ... | ... | @@ -22,4 +33,14 @@ public class DateUtils { |
| 22 | 33 | return Date.from(startOfDay.atZone(ZoneId.systemDefault()).toInstant()); |
| 23 | 34 | } |
| 24 | 35 | |
| 36 | + public static String getDateStr(Date date) { | |
| 37 | + SimpleDateFormat formatter = new SimpleDateFormat(PATTERNForDate); | |
| 38 | + return formatter.format(date); | |
| 39 | + } | |
| 40 | + | |
| 41 | + public static String getDateTimeStr(Date date) { | |
| 42 | + SimpleDateFormat formatter = new SimpleDateFormat(PATTERNForDateTime); | |
| 43 | + return formatter.format(date); | |
| 44 | + } | |
| 45 | + | |
| 25 | 46 | } | ... | ... |
src/main/java/top/panll/assist/utils/PageInfo.java
100644 → 100755
src/main/java/top/panll/assist/utils/RedisUtil.java
100644 → 100755
src/main/resources/all-application.yml
0 → 100755
| 1 | +spring: | |
| 2 | + # REDIS数据库配置 | |
| 3 | + redis: | |
| 4 | + # [必须修改] Redis服务器IP, REDIS安装在本机的,使用127.0.0.1 | |
| 5 | + host: 127.0.0.1 | |
| 6 | + # [必须修改] 端口号 | |
| 7 | + port: 6379 | |
| 8 | + # [可选] 数据库 DB | |
| 9 | + database: 8 | |
| 10 | + # [可选] 访问密码,若你的redis服务器没有设置密码,就不需要用密码去连接 | |
| 11 | + password: | |
| 12 | + # [可选] 超时时间 | |
| 13 | + timeout: 10000 | |
| 14 | + | |
| 15 | +# [必选] WVP监听的HTTP端口, 网页和接口调用都是这个端口 | |
| 16 | +server: | |
| 17 | + port: 18081 | |
| 18 | + # [可选] HTTPS配置, 默认不开启 | |
| 19 | + ssl: | |
| 20 | + # [可选] 是否开启HTTPS访问 | |
| 21 | + enabled: false | |
| 22 | + # [可选] 证书文件路径,放置在resource/目录下即可,修改xxx为文件名 | |
| 23 | + key-store: classpath:xxx.jks | |
| 24 | + # [可选] 证书密码 | |
| 25 | + key-password: password | |
| 26 | + # [可选] 证书类型, 默认为jks,根据实际修改 | |
| 27 | + key-store-type: JKS | |
| 28 | + | |
| 29 | +# [根据业务需求配置] | |
| 30 | +user-settings: | |
| 31 | + # [可选 ] zlm配置的录像路径,不配置则使用当前目录下的record目录 即: ./record | |
| 32 | + record: /media/lin/Server/ZLMediaKit/dev/ZLMediaKit/release/linux/Debug/www/record | |
| 33 | + # [可选 ] 录像保存时长(单位: 天)每天晚12点自动对过期文件执行清理, 不配置则不删除 | |
| 34 | + recordDay: 7 | |
| 35 | + # [可选 ] 录像下载合成临时文件保存时长, 不配置默认取值recordDay(单位: 天)每天晚12点自动对过期文件执行清理 | |
| 36 | + # recordTempDay: 7 | |
| 37 | + # [必选 ] ffmpeg路径 | |
| 38 | + ffmpeg: /usr/bin/ffmpeg | |
| 39 | + # [必选 ] ffprobe路径, 一般安装ffmpeg就会自带, 一般跟ffmpeg在同一目录,用于查询文件的信息 | |
| 40 | + ffprobe: /usr/bin/ffprobe | |
| 41 | + # [可选 ] 限制 ffmpeg 合并文件使用的线程数,间接限制cpu使用率, 默认2 限制到50% | |
| 42 | + threads: 2 | |
| 43 | + | |
| 44 | +swagger-ui: | |
| 45 | + | |
| 46 | +# [可选] 日志配置, 一般不需要改 | |
| 47 | +logging: | |
| 48 | + file: | |
| 49 | + name: logs/wvp.log | |
| 50 | + max-history: 30 | |
| 51 | + max-size: 10MB | |
| 52 | + total-size-cap: 300MB | |
| 53 | + level: | |
| 54 | + root: WARN | |
| 55 | + top: | |
| 56 | + panll: | |
| 57 | + assist: info | |
| 0 | 58 | \ No newline at end of file | ... | ... |
src/main/resources/application-dev.yml
100644 → 100755
src/main/resources/application.yml
100644 → 100755
src/main/resources/static/download.html
0 → 100644
| 1 | +<!DOCTYPE html> | |
| 2 | +<html lang="en"> | |
| 3 | +<head> | |
| 4 | + <meta charset="UTF-8"> | |
| 5 | + <meta http-equiv="X-UA-Compatible" content="IE=edge"> | |
| 6 | + <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| 7 | + <title>下载</title> | |
| 8 | +</head> | |
| 9 | +<body> | |
| 10 | +<a id="download" download></a> | |
| 11 | +<script> | |
| 12 | + (function () { | |
| 13 | + let searchParams = new URLSearchParams(location.search); | |
| 14 | + var download = document.getElementById("download"); | |
| 15 | + download.setAttribute("href", searchParams.get("url")) | |
| 16 | + download.click() | |
| 17 | + setTimeout(() => { | |
| 18 | + window.location.href = "about:blank"; | |
| 19 | + window.close(); | |
| 20 | + }, 200) | |
| 21 | + })(); | |
| 22 | + | |
| 23 | +</script> | |
| 24 | +</body> | |
| 25 | +</html> | |
| 0 | 26 | \ No newline at end of file | ... | ... |
src/test/java/top/panll/assist/WvpProAssistApplicationTests.java
100644 → 100755