This commit is contained in:
jiumikeji 2025-04-22 19:56:52 +08:00
commit 3f58700b38
41 changed files with 3115 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/target/
/.idea/
/logs/

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 panll
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# wvp-pro-assist
wvp-pro-assist是wvp-pro的辅助录像程序也可单独跟zlm一起使用提供录像控制,录像合并下载接口

BIN
lib-arm/ffmpeg Normal file

Binary file not shown.

BIN
lib-arm/ffprobe Normal file

Binary file not shown.

BIN
lib/ffmpeg Normal file

Binary file not shown.

BIN
lib/ffprobe Normal file

Binary file not shown.

121
pom.xml Normal file
View File

@ -0,0 +1,121 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.2</version>
</parent>
<groupId>top.panll.assist</groupId>
<artifactId>wvp-pro-assist</artifactId>
<version>2.6.9</version>
<name>wvp-pro-assist</name>
<description></description>
<properties>
<java.version>1.8</java.version>
<maven.build.timestamp.format>MMddHHmm</maven.build.timestamp.format>
<!-- <pagehelper.version>5.2.0</pagehelper.version>-->
</properties>
<repositories>
<repository>
<id>nexus-aliyun</id>
<name>Nexus aliyun</name>
<url>https://maven.aliyun.com/repository/public</url>
<layout>default</layout>
<snapshots>
<enabled>false</enabled>
</snapshots>
<releases>
<enabled>true</enabled>
</releases>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>net.bramp.ffmpeg</groupId>
<artifactId>ffmpeg</artifactId>
<version>0.6.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.73</version>
</dependency>
<!--在线文档 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.6.10</version>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-springdoc-ui</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>org.mp4parser</groupId>
<artifactId>muxer</artifactId>
<version>1.9.56</version>
</dependency>
<dependency>
<groupId>org.mp4parser</groupId>
<artifactId>streaming</artifactId>
<version>1.9.56</version>
</dependency>
<dependency>
<groupId>org.mp4parser</groupId>
<artifactId>isoparser</artifactId>
<version>1.9.27</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}-${project.version}-${maven.build.timestamp}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,15 @@
package top.panll.assist;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class WvpProAssistApplication {
public static void main(String[] args) {
SpringApplication.run(WvpProAssistApplication.class, args);
}
}

View File

@ -0,0 +1,44 @@
package top.panll.assist.config;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.serializer.SerializerFeature;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import java.nio.charset.Charset;
public class FastJsonRedisSerializer<T> implements RedisSerializer<T> {
private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
private Class<T> clazz;
/**
* 添加autotype白名单
* 解决redis反序列化对象时报错 com.alibaba.fastjson.JSONException: autoType is not support
*/
static {
ParserConfig.getGlobalInstance().addAccept("top.panll.assist");
}
public FastJsonRedisSerializer(Class<T> clazz) {
super();
this.clazz = clazz;
}
@Override
public byte[] serialize(T t) throws SerializationException {
if (null == t) {
return new byte[0];
}
return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
}
@Override
public T deserialize(byte[] bytes) throws SerializationException {
if (null == bytes || bytes.length <= 0) {
return null;
}
String str = new String(bytes, DEFAULT_CHARSET);
return JSON.parseObject(str, clazz);
}
}

View File

@ -0,0 +1,44 @@
package top.panll.assist.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import top.panll.assist.controller.bean.ControllerException;
import top.panll.assist.controller.bean.ErrorCode;
import top.panll.assist.controller.bean.WVPResult;
/**
* 全局异常处理
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
private final static Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 默认异常处理
* @param e 异常
* @return 统一返回结果
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public WVPResult<String> exceptionHandler(Exception e) {
logger.error("[全局异常] ", e);
return WVPResult.fail(ErrorCode.ERROR500.getCode(), e.getMessage());
}
/**
* 自定义异常处理 处理controller中返回的错误
* @param e 异常
* @return 统一返回结果
*/
@ExceptionHandler(ControllerException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public WVPResult<String> exceptionHandler(ControllerException e) {
return WVPResult.fail(e.getCode(), e.getMsg());
}
}

View File

@ -0,0 +1,54 @@
package top.panll.assist.config;
import com.alibaba.fastjson.JSON;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import top.panll.assist.controller.bean.ErrorCode;
import top.panll.assist.controller.bean.WVPResult;
import javax.validation.constraints.NotNull;
/**
* 全局统一返回结果
* @author lin
*/
@RestControllerAdvice
public class GlobalResponseAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(@NotNull MethodParameter returnType, @NotNull Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body, @NotNull MethodParameter returnType, @NotNull MediaType selectedContentType, @NotNull Class<? extends HttpMessageConverter<?>> selectedConverterType, @NotNull ServerHttpRequest request, @NotNull ServerHttpResponse response) {
// 排除api文档的接口这个接口不需要统一
String[] excludePath = {"/v3/api-docs","/api/v1","/index/hook"};
for (String path : excludePath) {
if (request.getURI().getPath().startsWith(path)) {
return body;
}
}
if (body instanceof WVPResult) {
return body;
}
if (body instanceof ErrorCode) {
ErrorCode errorCode = (ErrorCode) body;
return new WVPResult<>(errorCode.getCode(), errorCode.getMsg(), null);
}
if (body instanceof String) {
return JSON.toJSONString(WVPResult.success(body));
}
return WVPResult.success(body);
}
}

View File

@ -0,0 +1,66 @@
package top.panll.assist.config;
import com.alibaba.fastjson.parser.ParserConfig;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @Description:Redis中间件配置类使用spring-data-redis集成自动从application.yml中加载redis配置
* @author: swwheihei
* @date: 2019年5月30日 上午10:58:25
*
*/
@Configuration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
public class RedisConfig {
static {
ParserConfig.getGlobalInstance().addAccept("top.panll.assist");
}
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
// 使用fastjson进行序列化处理提高解析效率
FastJsonRedisSerializer<Object> serializer = new FastJsonRedisSerializer<>(Object.class);
// value值的序列化采用fastJsonRedisSerializer
template.setValueSerializer(serializer);
template.setHashValueSerializer(serializer);
// key的序列化采用StringRedisSerializer
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setConnectionFactory(redisConnectionFactory);
// 使用fastjson时需设置此项否则会报异常not support type
// ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
return template;
}
/**
* redis消息监听器容器 可以添加多个监听不同话题的redis监听器只需要把消息监听器和相应的消息订阅处理器绑定该消息监听器
* 通过反射技术调用消息订阅处理器的相关方法进行一些业务处理
*
* @param connectionFactory
* @return
*/
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}
}

View File

@ -0,0 +1,45 @@
package top.panll.assist.config;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import org.springdoc.core.GroupedOpenApi;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author lin
*/
@Configuration
public class SpringDocConfig {
@Value("${doc.enabled: true}")
private boolean enable;
@Bean
public OpenAPI springShopOpenApi() {
Contact contact = new Contact();
contact.setName("pan");
contact.setEmail("648540858@qq.com");
return new OpenAPI()
.info(new Info().title("WVP-PRO-ASSIST 接口文档")
.contact(contact)
.description("WVP-PRO助手补充ZLM功能")
.version("v2.0")
.license(new License().name("Apache 2.0").url("http://springdoc.org")));
}
/**
* 添加分组
* @return
*/
@Bean
public GroupedOpenApi publicApi() {
return GroupedOpenApi.builder()
.group("1. 全部")
.packagesToScan("top.panll.assist")
.build();
}
}

View File

@ -0,0 +1,59 @@
package top.panll.assist.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
@EnableAsync(proxyTargetClass = true)
public class ThreadPoolTaskConfig {
public static final int cpuNum = Runtime.getRuntime().availableProcessors();
/**
* 默认情况下在创建了线程池后线程池中的线程数为0当有任务来之后就会创建一个线程去执行任务
* 当线程池中的线程数目达到corePoolSize后就会把到达的任务放到缓存队列当中
* 当队列满了就继续创建线程当线程数量大于等于maxPoolSize后开始使用拒绝策略拒绝
*/
/**
* 核心线程数默认线程数
*/
private static final int corePoolSize = cpuNum;
/**
* 最大线程数
*/
private static final int maxPoolSize = cpuNum*2;
/**
* 允许线程空闲时间单位默认为秒
*/
private static final int keepAliveTime = 30;
/**
* 缓冲队列大小
*/
private static final int queueCapacity = 500;
/**
* 线程池名前缀
*/
private static final String threadNamePrefix = "wvp-assist-";
@Bean("taskExecutor") // bean的名称默认为首字母小写的方法名
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(corePoolSize);
executor.setMaxPoolSize(maxPoolSize);
executor.setQueueCapacity(queueCapacity);
executor.setKeepAliveSeconds(keepAliveTime);
executor.setThreadNamePrefix(threadNamePrefix);
// 线程池对拒绝任务的处理策略
// CallerRunsPolicy由调用线程提交任务的线程处理该任务
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 初始化
executor.initialize();
return executor;
}
}

View File

@ -0,0 +1,24 @@
package top.panll.assist.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import top.panll.assist.dto.UserSettings;
import java.io.File;
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Autowired
private UserSettings userSettings;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
File file = new File(userSettings.getRecordTempPath());
registry.addResourceHandler("/download/**").addResourceLocations("file://" + file.getAbsolutePath() + "/");
super.addResourceHandlers(registry);
}
}

View File

@ -0,0 +1,130 @@
package top.panll.assist.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import top.panll.assist.controller.bean.*;
import top.panll.assist.dto.*;
import top.panll.assist.service.VideoFileService;
import top.panll.assist.utils.RedisUtil;
import java.text.SimpleDateFormat;
import java.util.*;
@Tag(name = "录像管理", description = "录像管理")
@CrossOrigin
@RestController
@RequestMapping("/api/record")
public class RecordController {
private final static Logger logger = LoggerFactory.getLogger(RecordController.class);
@Autowired
private VideoFileService videoFileService;
@Autowired
private RedisUtil redisUtil;
@Autowired
private UserSettings userSettings;
private SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
/**
* 获取Assist服务配置信息
*/
@Operation(summary ="获取Assist服务配置信息")
@GetMapping(value = "/info")
@ResponseBody
public UserSettings getInfo(){
return userSettings;
}
/**
* 添加视频裁剪合并任务
*/
@Operation(summary ="添加视频裁剪合并任务")
@Parameter(name = "videoTaskInfo", description = "视频合并任务的信息", required = true)
@PostMapping(value = "/file/download/task/add")
@ResponseBody
public String addTaskForDownload(@RequestBody VideoTaskInfo videoTaskInfo ){
if (videoTaskInfo.getFilePathList() == null || videoTaskInfo.getFilePathList().isEmpty()) {
throw new ControllerException(ErrorCode.ERROR100.getCode(), "视频文件列表不可为空");
}
String id = videoFileService.mergeOrCut(videoTaskInfo);
if (id== null) {
throw new ControllerException(ErrorCode.ERROR100.getCode(), "可能未找到视频文件");
}
return id;
}
/**
* 查询视频裁剪合并任务列表
*/
@Operation(summary ="查询视频裁剪合并任务列表")
@Parameter(name = "taskId", description = "任务ID", required = true)
@Parameter(name = "isEnd", description = "是否结束", required = true)
@GetMapping(value = "/file/download/task/list")
@ResponseBody
public List<MergeOrCutTaskInfo> getTaskListForDownload(
@RequestParam(required = false) String app,
@RequestParam(required = false) String stream,
@RequestParam(required = false) String callId,
@RequestParam(required = false) String taskId,
@RequestParam(required = false) Boolean isEnd){
List<MergeOrCutTaskInfo> taskList = videoFileService.getTaskListForDownload(app, stream, callId, isEnd, taskId);
if (taskList == null) {
throw new ControllerException(ErrorCode.ERROR100);
}
return taskList;
}
/**
* 中止视频裁剪合并任务列表
*/
@Operation(summary ="中止视频裁剪合并任务列表(暂不支持)")
@GetMapping(value = "/file/download/task/stop")
@ResponseBody
public WVPResult<String> stopTaskForDownload(@RequestParam String taskId){
// WVPResult<String> result = new WVPResult<>();
// if (taskId == null) {
// result.setCode(400);
// result.setMsg("taskId 不能为空");
// return result;
// }
// boolean stopResult = videoFileService.stopTask(taskId);
// result.setCode(0);
// result.setMsg(stopResult ? "success": "fail");
return null;
}
/**
* 磁盘空间查询
*/
@Operation(summary ="磁盘空间查询")
@ResponseBody
@GetMapping(value = "/space", produces = "application/json;charset=UTF-8")
public SpaceInfo getSpace() {
return videoFileService.getSpaceInfo();
}
/**
* 录像文件的时长
*/
@Operation(summary ="录像文件的时长")
@Parameter(name = "app", description = "应用名", required = true)
@Parameter(name = "stream", description = "流ID", required = true)
@Parameter(name = "recordIng", description = "是否录制中", required = true)
@ResponseBody
@GetMapping(value = "/file/duration", produces = "application/json;charset=UTF-8")
@PostMapping(value = "/file/duration", produces = "application/json;charset=UTF-8")
public long fileDuration( @RequestParam String app, @RequestParam String stream) {
return videoFileService.fileDuration(app, stream);
}
}

View File

@ -0,0 +1,35 @@
package top.panll.assist.controller.bean;
/**
* 自定义异常controller出现错误时直接抛出异常由全局异常捕获并返回结果
*/
public class ControllerException extends RuntimeException{
private int code;
private String msg;
public ControllerException(int code, String msg) {
this.code = code;
this.msg = msg;
}
public ControllerException(ErrorCode errorCode) {
this.code = errorCode.getCode();
this.msg = errorCode.getMsg();
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}

View File

@ -0,0 +1,29 @@
package top.panll.assist.controller.bean;
/**
* 全局错误码
*/
public enum ErrorCode {
SUCCESS(0, "成功"),
ERROR100(100, "失败"),
ERROR400(400, "参数不全或者错误"),
ERROR403(403, "无权限操作"),
ERROR401(401, "请登录后重新请求"),
ERROR500(500, "系统异常");
private final int code;
private final String msg;
ErrorCode(int code, String msg) {
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}

View File

@ -0,0 +1,16 @@
package top.panll.assist.controller.bean;
import java.util.List;
public class FileLIstInfo {
private List<String> filePathList;
public List<String> getFilePathList() {
return filePathList;
}
public void setFilePathList(List<String> filePathList) {
this.filePathList = filePathList;
}
}

View File

@ -0,0 +1,53 @@
package top.panll.assist.controller.bean;
public class RecordFile {
private String app;
private String stream;
private String fileName;
private String date;
public static RecordFile instance(String app, String stream, String fileName, String date) {
RecordFile recordFile = new RecordFile();
recordFile.setApp(app);
recordFile.setStream(stream);
recordFile.setFileName(fileName);
recordFile.setDate(date);
return recordFile;
}
public String getApp() {
return app;
}
public void setApp(String app) {
this.app = app;
}
public String getStream() {
return stream;
}
public void setStream(String stream) {
this.stream = stream;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
}

View File

@ -0,0 +1,66 @@
package top.panll.assist.controller.bean;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "统一返回结果")
public class WVPResult<T> {
public WVPResult() {
}
public WVPResult(int code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
@Schema(description = "错误码0为成功")
private int code;
@Schema(description = "描述,错误时描述错误原因")
private String msg;
@Schema(description = "数据")
private T data;
public static <T> WVPResult<T> success(T t, String msg) {
return new WVPResult<>(ErrorCode.SUCCESS.getCode(), msg, t);
}
public static <T> WVPResult<T> success(T t) {
return success(t, ErrorCode.SUCCESS.getMsg());
}
public static <T> WVPResult<T> fail(int code, String msg) {
return new WVPResult<>(code, msg, null);
}
public static <T> WVPResult<T> fail(ErrorCode errorCode) {
return fail(errorCode.getCode(), errorCode.getMsg());
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}

View File

@ -0,0 +1,8 @@
package top.panll.assist.dto;
public class AssistConstants {
public final static String STREAM_CALL_INFO = "STREAM_CALL_INFO_";
public final static String MERGEORCUT = "MERGEORCUT_";
}

View File

@ -0,0 +1,108 @@
package top.panll.assist.dto;
public class MergeOrCutTaskInfo {
private String id;
private String createTime;
private String percentage;
private String recordFile;
private String downloadFile;
private String playFile;
private String app;
private String stream;
private String startTime;
private String endTime;
private String callId;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getPercentage() {
return percentage;
}
public void setPercentage(String percentage) {
this.percentage = percentage;
}
public String getRecordFile() {
return recordFile;
}
public void setRecordFile(String recordFile) {
this.recordFile = recordFile;
}
public String getDownloadFile() {
return downloadFile;
}
public void setDownloadFile(String downloadFile) {
this.downloadFile = downloadFile;
}
public String getPlayFile() {
return playFile;
}
public void setPlayFile(String playFile) {
this.playFile = playFile;
}
public String getCreateTime() {
return createTime;
}
public void setCreateTime(String createTime) {
this.createTime = createTime;
}
public String getApp() {
return app;
}
public void setApp(String app) {
this.app = app;
}
public String getStream() {
return stream;
}
public void setStream(String stream) {
this.stream = stream;
}
public String getStartTime() {
return startTime;
}
public void setStartTime(String startTime) {
this.startTime = startTime;
}
public String getEndTime() {
return endTime;
}
public void setEndTime(String endTime) {
this.endTime = endTime;
}
public String getCallId() {
return callId;
}
public void setCallId(String callId) {
this.callId = callId;
}
}

View File

@ -0,0 +1,31 @@
package top.panll.assist.dto;
public class SignInfo {
private String app;
private String stream;
private String type;
public String getApp() {
return app;
}
public void setApp(String app) {
this.app = app;
}
public String getStream() {
return stream;
}
public void setStream(String stream) {
this.stream = stream;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
}

View File

@ -0,0 +1,23 @@
package top.panll.assist.dto;
public class SpaceInfo {
private long total;
private long free;
public long getTotal() {
return total;
}
public void setTotal(long total) {
this.total = total;
}
public long getFree() {
return free;
}
public void setFree(long free) {
this.free = free;
}
}

View File

@ -0,0 +1,78 @@
package top.panll.assist.dto;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* @author lin
*/
@Component
public class UserSettings {
@Value("${userSettings.id}")
private String id;
@Value("${userSettings.record-temp:./recordTemp}")
private String recordTempPath;
@Value("${userSettings.record-temp-day:7}")
private int recordTempDay;
@Value("${userSettings.ffmpeg}")
private String ffmpeg;
@Value("${userSettings.ffprobe}")
private String ffprobe;
@Value("${userSettings.threads:2}")
private int threads;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getFfmpeg() {
return ffmpeg;
}
public void setFfmpeg(String ffmpeg) {
this.ffmpeg = ffmpeg;
}
public String getFfprobe() {
return ffprobe;
}
public void setFfprobe(String ffprobe) {
this.ffprobe = ffprobe;
}
public int getRecordTempDay() {
return recordTempDay;
}
public void setRecordTempDay(int recordTempDay) {
this.recordTempDay = recordTempDay;
}
public int getThreads() {
return threads;
}
public void setThreads(int threads) {
this.threads = threads;
}
public String getRecordTempPath() {
return recordTempPath;
}
public void setRecordTempPath(String recordTempPath) {
this.recordTempPath = recordTempPath;
}
}

View File

@ -0,0 +1,77 @@
package top.panll.assist.dto;
import java.io.File;
import java.util.Date;
/**
* 视频文件
*/
public class VideoFile {
/**
* 文件对象
*/
private File file;
/**
* 文件开始时间
*/
private Date startTime;
/**
* 文件结束时间
*/
private Date endTime;
/**
* 时长, 单位
*/
private long duration;
/**
* 是否是目标格式
*/
private boolean targetFormat;
public File getFile() {
return file;
}
public void setFile(File file) {
this.file = file;
}
public Date getStartTime() {
return startTime;
}
public void setStartTime(Date startTime) {
this.startTime = startTime;
}
public Date getEndTime() {
return endTime;
}
public void setEndTime(Date endTime) {
this.endTime = endTime;
}
public long getDuration() {
return duration;
}
public void setDuration(long duration) {
this.duration = duration;
}
public boolean isTargetFormat() {
return targetFormat;
}
public void setTargetFormat(boolean targetFormat) {
this.targetFormat = targetFormat;
}
}

View File

@ -0,0 +1,79 @@
package top.panll.assist.dto;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
@Schema(description = "视频合并任务的信息")
public class VideoTaskInfo {
private String app;
private String stream;
private String startTime;
private String endTime;
private String callId;
@Schema(description = "视频文件路径列表")
private List<String> filePathList;
@Schema(description = "返回地址时的远程地址")
private String remoteHost;
public List<String> getFilePathList() {
return filePathList;
}
public void setFilePathList(List<String> filePathList) {
this.filePathList = filePathList;
}
public String getRemoteHost() {
return remoteHost;
}
public void setRemoteHost(String remoteHost) {
this.remoteHost = remoteHost;
}
public String getApp() {
return app;
}
public void setApp(String app) {
this.app = app;
}
public String getStream() {
return stream;
}
public void setStream(String stream) {
this.stream = stream;
}
public String getStartTime() {
return startTime;
}
public void setStartTime(String startTime) {
this.startTime = startTime;
}
public String getEndTime() {
return endTime;
}
public void setEndTime(String endTime) {
this.endTime = endTime;
}
public String getCallId() {
return callId;
}
public void setCallId(String callId) {
this.callId = callId;
}
}

View File

@ -0,0 +1,159 @@
package top.panll.assist.service;
import net.bramp.ffmpeg.FFmpeg;
import net.bramp.ffmpeg.FFmpegExecutor;
import net.bramp.ffmpeg.FFmpegUtils;
import net.bramp.ffmpeg.FFprobe;
import net.bramp.ffmpeg.builder.FFmpegBuilder;
import net.bramp.ffmpeg.job.FFmpegJob;
import net.bramp.ffmpeg.probe.FFmpegProbeResult;
import net.bramp.ffmpeg.progress.Progress;
import net.bramp.ffmpeg.progress.ProgressListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;
import top.panll.assist.dto.UserSettings;
import top.panll.assist.dto.VideoFile;
import top.panll.assist.utils.RedisUtil;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Component
public class FFmpegExecUtils implements InitializingBean{
private final static Logger logger = LoggerFactory.getLogger(FFmpegExecUtils.class);
// private static FFmpegExecUtils instance;
//
// public FFmpegExecUtils() {
// }
//
// public static FFmpegExecUtils getInstance(){
// if(instance==null){
// synchronized (FFmpegExecUtils.class){
// if(instance==null){
// instance=new FFmpegExecUtils();
// }
// }
// }
// return instance;
// }
@Autowired
private UserSettings userSettings;
private FFprobe ffprobe;
private FFmpeg ffmpeg;
public FFprobe getFfprobe() {
return ffprobe;
}
public FFmpeg getFfmpeg() {
return ffmpeg;
}
@Override
public void afterPropertiesSet() throws Exception {
String ffmpegPath = userSettings.getFfmpeg();
String ffprobePath = userSettings.getFfprobe();
this.ffmpeg = new FFmpeg(ffmpegPath);
this.ffprobe = new FFprobe(ffprobePath);
logger.info("wvp-pro辅助程序启动成功。 \n{}\n{} ", this.ffmpeg.version(), this.ffprobe.version());
}
public interface VideoHandEndCallBack {
void run(String status, double percentage, String result);
}
@Async
public void mergeOrCutFile(List<File> fils, File dest, String destFileName, VideoHandEndCallBack callBack){
if (fils == null || fils.size() == 0 || ffmpeg == null || ffprobe == null || dest== null || !dest.exists()){
callBack.run("error", 0.0, null);
return;
}
File tempFile = new File(dest.getAbsolutePath());
if (!tempFile.exists()) {
tempFile.mkdirs();
}
FFmpegExecutor executor = new FFmpegExecutor(ffmpeg, ffprobe);
String fileListName = tempFile.getAbsolutePath() + File.separator + "fileList";
double durationAll = 0.0;
try {
BufferedWriter bw =new BufferedWriter(new FileWriter(fileListName));
for (File file : fils) {
VideoFile videoFile = VideoFileFactory.createFile(this, file);
if (videoFile == null) {
return;
}
bw.write("file " + file.getAbsolutePath());
bw.newLine();
durationAll += videoFile.getDuration();
}
bw.flush();
bw.close();
} catch (IOException e) {
e.printStackTrace();
callBack.run("error", 0.0, null);
}
String recordFileResultPath = dest.getAbsolutePath() + File.separator + destFileName + ".mp4";
long startTime = System.currentTimeMillis();
FFmpegBuilder builder = new FFmpegBuilder()
.setFormat("concat")
.overrideOutputFiles(true)
.setInput(fileListName) // Or filename
.addExtraArgs("-safe", "0")
.addExtraArgs("-threads", userSettings.getThreads() + "")
.addOutput(recordFileResultPath)
.setVideoCodec("copy")
.setAudioCodec("aac")
.setFormat("mp4")
.done();
double finalDurationAll = durationAll;
FFmpegJob job = executor.createJob(builder, (Progress progress) -> {
final double duration_ns = finalDurationAll * TimeUnit.SECONDS.toNanos(1);
double percentage = progress.out_time_ns / duration_ns;
// Print out interesting information about the progress
// System.out.println(String.format(
// "[%.0f%%] status:%s frame:%d time:%s ms fps:%.0f speed:%.2fx",
// percentage * 100,
// progress.status,
// progress.frame,
// FFmpegUtils.toTimecode(progress.out_time_ns, TimeUnit.NANOSECONDS),
// progress.fps.doubleValue(),
// progress.speed
// ));
if (progress.status.equals(Progress.Status.END)){
callBack.run(progress.status.name(), percentage, recordFileResultPath);
}else {
callBack.run(progress.status.name(), percentage, null);
}
});
job.run();
}
public long duration(File file) throws IOException {
FFmpegProbeResult in = ffprobe.probe(file.getAbsolutePath());
double duration = in.getFormat().duration * 1000;
long durationLong = new Double(duration).longValue();
return durationLong;
}
}

View File

@ -0,0 +1,86 @@
package top.panll.assist.service;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import top.panll.assist.dto.AssistConstants;
import top.panll.assist.dto.MergeOrCutTaskInfo;
import top.panll.assist.dto.UserSettings;
import top.panll.assist.utils.RedisUtil;
import java.io.File;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
@Component
public class FileManagerTimer {
private SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
private final SimpleDateFormat simpleDateFormatForTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private final static Logger logger = LoggerFactory.getLogger(FileManagerTimer.class);
@Autowired
private UserSettings userSettings;
@Autowired
private VideoFileService videoFileService;
@Autowired
private RedisUtil redisUtil;
// @Scheduled(fixedDelay = 2000) //测试 20秒执行一次
@Scheduled(cron = "0 0 0 * * ?") //每天的0点执行
public void execute(){
if (userSettings.getRecordTempPath() == null) {
return;
}
// 清理任务临时文件
int recordTempDay = userSettings.getRecordTempDay();
Date lastTempDate = new Date();
Calendar lastTempCalendar = Calendar.getInstance();
lastTempCalendar.setTime(lastTempDate);
lastTempCalendar.add(Calendar.DAY_OF_MONTH, -recordTempDay);
lastTempDate = lastTempCalendar.getTime();
logger.info("[录像巡查]移除合并任务临时文件 {} 之前的文件", formatter.format(lastTempDate));
File recordTempFile = new File(userSettings.getRecordTempPath());
if (recordTempFile.exists() && recordTempFile.isDirectory() && recordTempFile.canWrite()) {
File[] tempFiles = recordTempFile.listFiles();
if (tempFiles != null) {
for (File tempFile : tempFiles) {
if (tempFile.isFile() && tempFile.lastModified() < lastTempDate.getTime()) {
boolean result = FileUtils.deleteQuietly(tempFile);
if (result) {
logger.info("[录像巡查]成功移除合并任务临时文件 {} ", tempFile.getAbsolutePath());
}else {
logger.info("[录像巡查]合并任务临时文件移除失败 {} ", tempFile.getAbsolutePath());
}
}
}
}
}
// 清理redis记录
String key = String.format("%S_%S_*", AssistConstants.MERGEORCUT, userSettings.getId());
List<Object> taskKeys = redisUtil.scan(key);
for (Object taskKeyObj : taskKeys) {
String taskKey = (String) taskKeyObj;
MergeOrCutTaskInfo mergeOrCutTaskInfo = (MergeOrCutTaskInfo)redisUtil.get(taskKey);
try {
if (StringUtils.hasLength(mergeOrCutTaskInfo.getCreateTime())
|| simpleDateFormatForTime.parse(mergeOrCutTaskInfo.getCreateTime()).before(lastTempDate)) {
redisUtil.del(taskKey);
}
} catch (ParseException e) {
logger.error("[清理过期的redis合并任务信息] 失败", e);
}
}
}
}

View File

@ -0,0 +1,152 @@
package top.panll.assist.service;
import net.bramp.ffmpeg.probe.FFmpegProbeResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import top.panll.assist.dto.VideoFile;
import java.io.File;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class VideoFileFactory {
private final static Logger logger = LoggerFactory.getLogger(VideoFileFactory.class);
public static VideoFile createFile(FFmpegExecUtils ffmpegExecUtils, File file){
if (!file.exists()) {
return null;
}
if (!file.isFile()){
return null;
}
if (!file.getName().endsWith(".mp4")){
return null;
}
if (file.isHidden()){
return null;
}
String date = file.getParentFile().getName();
if (file.getName().indexOf(":") > 0) {
// 格式为 HH:mm:ss-HH:mm:ss-时长
String[] split = file.getName().split("-");
if (split.length != 3) {
return null;
}
String startTimeStr = date + " " + split[0];
String endTimeStr = date + " " + split[1];
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
VideoFile videoFile = new VideoFile();
videoFile.setFile(file);
videoFile.setTargetFormat(false);
try {
Date startTimeDate = simpleDateFormat.parse(startTimeStr);
videoFile.setStartTime(startTimeDate);
Date endTimeDate = simpleDateFormat.parse(endTimeStr);
videoFile.setEndTime(endTimeDate);
videoFile.setDuration((endTimeDate.getTime() - startTimeDate.getTime()));
} catch (ParseException e) {
logger.error("[构建视频文件对象] 格式化时间失败, file:{}", file.getAbsolutePath(), e);
return null;
}
return videoFile;
}else if (getStrCountInStr(file.getName(), "-") == 3){
// 格式为zlm的录制格式 HH-mm-ss-序号
String startStr = file.getName().substring(0, file.getName().lastIndexOf("-"));
String startTimeStr = date + " " + startStr;
VideoFile videoFile = null;
try {
FFmpegProbeResult fFmpegProbeResult = ffmpegExecUtils.getFfprobe().probe(file.getAbsolutePath());
double duration = fFmpegProbeResult.getFormat().duration * 1000;
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss");
Date startTimeDate = simpleDateFormat.parse(startTimeStr);
Date endTimeDate = new Date(startTimeDate.getTime() + new Double(duration).longValue());
videoFile = new VideoFile();
videoFile.setTargetFormat(false);
videoFile.setFile(file);
videoFile.setStartTime(startTimeDate);
videoFile.setEndTime(endTimeDate);
videoFile.setDuration((endTimeDate.getTime() - startTimeDate.getTime())/1000);
} catch (IOException e) {
logger.error("[构建视频文件对象] 获取视频时长失败, file:{}", file.getAbsolutePath(), e);
return null;
} catch (ParseException e) {
logger.error("[构建视频文件对象] 格式化时间失败, file:{}", file.getAbsolutePath(), e);
return null;
}
return videoFile;
}else if (getStrCountInStr(file.getName(), "-") == 2 && file.getName().length() == 10 ){
// 格式为zlm的录制格式 HH-mm-ss
String startTimeStr = date + " " + file.getName();
VideoFile videoFile = null;
try {
FFmpegProbeResult fFmpegProbeResult = ffmpegExecUtils.getFfprobe().probe(file.getAbsolutePath());
double duration = fFmpegProbeResult.getFormat().duration * 1000;
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss");
Date startTimeDate = simpleDateFormat.parse(startTimeStr);
Date endTimeDate = new Date(startTimeDate.getTime() + new Double(duration).longValue());
videoFile = new VideoFile();
videoFile.setTargetFormat(false);
videoFile.setFile(file);
videoFile.setStartTime(startTimeDate);
videoFile.setEndTime(endTimeDate);
videoFile.setDuration((endTimeDate.getTime() - startTimeDate.getTime())/1000);
} catch (IOException e) {
logger.error("[构建视频文件对象] 获取视频时长失败, file:{}", file.getAbsolutePath(), e);
return null;
} catch (ParseException e) {
logger.warn("[构建视频文件对象] 格式化时间失败, file:{}", file.getAbsolutePath(), e);
return null;
}
return videoFile;
}else if (getStrCountInStr(file.getName(), "-") == 1 ){
// 格式为zlm的录制格式 HH-mm-ss
// 格式为 HH:mm:ss-HH:mm:ss-时长
String[] split = file.getName().split("-");
if (split.length != 2) {
return null;
}
String startTimeStr = date + " " + split[0];
String endTimeStr = date + " " + split[1];
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HHmmss");
VideoFile videoFile = new VideoFile();
videoFile.setTargetFormat(true);
videoFile.setFile(file);
try {
Date startTimeDate = simpleDateFormat.parse(startTimeStr);
videoFile.setStartTime(startTimeDate);
Date endTimeDate = simpleDateFormat.parse(endTimeStr);
videoFile.setEndTime(endTimeDate);
videoFile.setDuration((endTimeDate.getTime() - startTimeDate.getTime()));
} catch (ParseException e) {
logger.error("[构建视频文件对象] 格式化时间失败, file:{}", file.getAbsolutePath(), e);
return null;
}
return videoFile;
}else {
return null;
}
}
public static int getStrCountInStr(String sourceStr, String content) {
int index = sourceStr.indexOf(content);
if (index < 0) {
return 0;
}
int count = 1;
int lastIndex = sourceStr.lastIndexOf(content);
while (index != lastIndex) {
index = sourceStr.indexOf(content, index + 1);
count++;
}
return count;
}
}

View File

@ -0,0 +1,505 @@
package top.panll.assist.service;
import net.bramp.ffmpeg.FFprobe;
import net.bramp.ffmpeg.probe.FFmpegProbeResult;
import net.bramp.ffmpeg.progress.Progress;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
import org.springframework.util.ObjectUtils;
import top.panll.assist.controller.bean.ControllerException;
import top.panll.assist.controller.bean.ErrorCode;
import top.panll.assist.dto.*;
import top.panll.assist.utils.RedisUtil;
import top.panll.assist.utils.DateUtils;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
@Service
public class VideoFileService {
private final static Logger logger = LoggerFactory.getLogger(VideoFileService.class);
@Autowired
private UserSettings userSettings;
@Autowired
private RedisUtil redisUtil;
@Autowired
private FFmpegExecUtils ffmpegExecUtils;
private final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
private final SimpleDateFormat simpleDateFormatForTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public List<File> getAppList(Boolean sort) {
File recordFile = new File(userSettings.getRecordTempPath());
if (recordFile.isDirectory()) {
File[] files = recordFile.listFiles((File dir, String name) -> {
File currentFile = new File(dir.getAbsolutePath() + File.separator + name);
return currentFile.isDirectory() && !name.equals("recordTemp");
});
List<File> result = Arrays.asList(files);
if (sort != null && sort) {
Collections.sort(result);
}
return result;
}else {
return null;
}
}
public SpaceInfo getSpaceInfo(){
File recordFile = new File(userSettings.getRecordTempPath());
SpaceInfo spaceInfo = new SpaceInfo();
spaceInfo.setFree(recordFile.getFreeSpace());
spaceInfo.setTotal(recordFile.getTotalSpace());
return spaceInfo;
}
public List<File> getStreamList(File appFile, Boolean sort) {
if (appFile != null && appFile.isDirectory()) {
File[] files = appFile.listFiles((File dir, String name) -> {
File currentFile = new File(dir.getAbsolutePath() + File.separator + name);
return currentFile.isDirectory();
});
List<File> result = Arrays.asList(files);
if (sort != null && sort) {
Collections.sort(result);
}
return result;
}else {
return null;
}
}
/**
* 获取制定推流的指定时间段内的推流
* @param app
* @param stream
* @param startTime
* @param endTime
* @return
*/
public List<File> getFilesInTime(String app, String stream, Date startTime, Date endTime){
List<File> result = new ArrayList<>();
if (app == null || stream == null) {
return result;
}
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HHmmss");
SimpleDateFormat formatterForDate = new SimpleDateFormat("yyyy-MM-dd");
String startTimeStr = null;
String endTimeStr = null;
if (startTime != null) {
startTimeStr = formatter.format(startTime);
}
if (endTime != null) {
endTimeStr = formatter.format(endTime);
}
logger.debug("获取[app: {}, stream: {}, statime: {}, endTime: {}]的视频", app, stream,
startTimeStr, endTimeStr);
File recordFile = new File(userSettings.getRecordTempPath());
File streamFile = new File(recordFile.getAbsolutePath() + File.separator + app + File.separator + stream + File.separator);
if (!streamFile.exists()) {
logger.warn("获取[app: {}, stream: {}, statime: {}, endTime: {}]的视频时未找到目录: {}", app, stream,
startTimeStr, endTimeStr, stream);
return null;
}
File[] dateFiles = streamFile.listFiles((File dir, String name) -> {
Date fileDate = null;
Date startDate = null;
Date endDate = null;
if (new File(dir + File.separator + name).isFile()) {
return false;
}
if (startTime != null) {
startDate = new Date(startTime.getTime() - ((startTime.getTime() + 28800000) % (86400000)));
}
if (endTime != null) {
endDate = new Date(endTime.getTime() - ((endTime.getTime() + 28800000) % (86400000)));
}
try {
fileDate = formatterForDate.parse(name);
} catch (ParseException e) {
logger.error("过滤日期文件时异常: {}-{}", name, e.getMessage());
return false;
}
boolean filterResult = true;
if (startDate != null) {
filterResult = filterResult && DateUtils.getStartOfDay(startDate).compareTo(fileDate) <= 0;
}
if (endDate != null) {
filterResult = filterResult && DateUtils.getEndOfDay(endDate).compareTo(fileDate) >= 0;
}
return filterResult ;
});
if (dateFiles != null && dateFiles.length > 0) {
for (File dateFile : dateFiles) {
File[] files = dateFile.listFiles((File dir, String name) ->{
File currentFile = new File(dir + File.separator + name);
VideoFile videoFile = VideoFileFactory.createFile(ffmpegExecUtils, currentFile);
if (videoFile == null ) {
return false;
}else {
if (!videoFile.isTargetFormat()) {
return false;
}
if (startTime == null && endTime == null) {
return true;
}else if (startTime == null && endTime != null) {
return videoFile.getEndTime().before(endTime)
|| videoFile.getEndTime().equals(endTime)
|| (videoFile.getEndTime().after(endTime) && videoFile.getStartTime().before(endTime));
}else if (startTime != null && endTime == null) {
return videoFile.getStartTime().after(startTime)
|| videoFile.getStartTime().equals(startTime)
|| (videoFile.getStartTime().before(startTime) && videoFile.getEndTime().after(startTime));
}else {
return videoFile.getStartTime().after(startTime)
|| videoFile.getStartTime().equals(startTime)
|| (videoFile.getStartTime().before(startTime) && videoFile.getEndTime().after(startTime))
|| videoFile.getEndTime().before(endTime)
|| videoFile.getEndTime().equals(endTime)
|| (videoFile.getEndTime().after(endTime) && videoFile.getStartTime().before(endTime));
}
}
});
if (files != null && files.length > 0) {
result.addAll(Arrays.asList(files));
}
}
}
if (!result.isEmpty()) {
result.sort((File f1, File f2) -> {
VideoFile videoFile1 = VideoFileFactory.createFile(ffmpegExecUtils, f1);
VideoFile videoFile2 = VideoFileFactory.createFile(ffmpegExecUtils, f2);
if (videoFile1 == null || !videoFile1.isTargetFormat() || videoFile2 == null || !videoFile2.isTargetFormat()) {
logger.warn("[根据时间获取视频文件] 排序错误,文件错误: {}/{}", f1.getName(), f2.getName());
return 0;
}
return videoFile1.getStartTime().compareTo(videoFile2.getStartTime());
});
}
return result;
}
public String mergeOrCut(VideoTaskInfo videoTaskInfo) {
assert videoTaskInfo.getFilePathList() != null;
assert !videoTaskInfo.getFilePathList().isEmpty();
String taskId = DigestUtils.md5DigestAsHex(String.valueOf(System.currentTimeMillis()).getBytes());
String logInfo = String.format("app: %S, stream: %S, callId: %S, 任务ID%S",
videoTaskInfo.getApp(), videoTaskInfo.getStream(), videoTaskInfo.getCallId(), taskId);
logger.info("[录像合并] 开始合并,{} ", logInfo);
List<File> fileList = new ArrayList<>();
for (String filePath : videoTaskInfo.getFilePathList()) {
File file = new File(filePath);
if (!file.exists()) {
logger.info("[录像合并] 失败,{} ", logInfo);
throw new ControllerException(ErrorCode.ERROR100.getCode(), filePath + "文件不存在");
}
logger.info("[录像合并] 添加文件,{}, 文件: {}", logInfo, filePath);
fileList.add(file);
}
File recordFile = new File(userSettings.getRecordTempPath() );
if (!recordFile.exists()) {
if (!recordFile.mkdirs()) {
logger.info("[录像合并] 失败,{}, 创建临时目录失败", logInfo);
throw new ControllerException(ErrorCode.ERROR100.getCode(), "创建临时目录失败");
}
}
MergeOrCutTaskInfo mergeOrCutTaskInfo = new MergeOrCutTaskInfo();
mergeOrCutTaskInfo.setId(taskId);
mergeOrCutTaskInfo.setApp(videoTaskInfo.getApp());
mergeOrCutTaskInfo.setStream(videoTaskInfo.getStream());
mergeOrCutTaskInfo.setCallId(videoTaskInfo.getCallId());
mergeOrCutTaskInfo.setStartTime(videoTaskInfo.getStartTime());
mergeOrCutTaskInfo.setEndTime(videoTaskInfo.getEndTime());
mergeOrCutTaskInfo.setCreateTime(simpleDateFormatForTime.format(System.currentTimeMillis()));
String destFileName = videoTaskInfo.getStream() + "_" + videoTaskInfo.getCallId();
if (fileList.size() == 1) {
// 文件只有一个则不合并直接复制过去
mergeOrCutTaskInfo.setPercentage("1");
// 处理文件路径
String recordFileResultPath = recordFile.getAbsolutePath() + File.separator + destFileName + ".mp4";
File destFile = new File(recordFileResultPath);
destFile.deleteOnExit();
try {
Files.copy(fileList.get(0).toPath(), Paths.get(recordFileResultPath));
} catch (IOException e) {
logger.info("[录像合并] 失败, {}", logInfo, e);
throw new ControllerException(ErrorCode.ERROR100.getCode(), e.getMessage());
}
mergeOrCutTaskInfo.setRecordFile("/download/" + destFileName + ".mp4");
if (videoTaskInfo.getRemoteHost() != null) {
mergeOrCutTaskInfo.setDownloadFile(videoTaskInfo.getRemoteHost() + "/download.html?url=download/" + destFileName + ".mp4");
mergeOrCutTaskInfo.setPlayFile(videoTaskInfo.getRemoteHost() + "/download/" + destFileName + ".mp4");
}
String key = String.format("%S_%S_%S", AssistConstants.MERGEORCUT , userSettings.getId(), mergeOrCutTaskInfo.getId());
redisUtil.set(key, mergeOrCutTaskInfo);
logger.info("[录像合并] 成功, 任务ID{}", taskId);
}else {
ffmpegExecUtils.mergeOrCutFile(fileList, recordFile, destFileName, (status, percentage, result)->{
// 发出redis通知
if (status.equals(Progress.Status.END.name())) {
mergeOrCutTaskInfo.setPercentage("1");
// 处理文件路径
String relativize = new File(result).getName();
mergeOrCutTaskInfo.setRecordFile(relativize.toString());
if (videoTaskInfo.getRemoteHost() != null) {
mergeOrCutTaskInfo.setDownloadFile(videoTaskInfo.getRemoteHost() + "/download.html?url=download/" + relativize);
mergeOrCutTaskInfo.setPlayFile(videoTaskInfo.getRemoteHost() + "/download/" + relativize);
}
logger.info("[录像合并] 成功, {}", logInfo);
}else {
mergeOrCutTaskInfo.setPercentage(percentage + "");
}
String key = String.format("%S_%S_%S", AssistConstants.MERGEORCUT, userSettings.getId(), mergeOrCutTaskInfo.getId());
redisUtil.set(key, mergeOrCutTaskInfo);
});
}
return taskId;
}
public List<File> getDateList(File streamFile, Integer year, Integer month, Boolean sort) {
if (!streamFile.exists() && streamFile.isDirectory()) {
logger.warn("获取[]的视频时未找到目录: {}",streamFile.getName());
return null;
}
File[] dateFiles = streamFile.listFiles((File dir, String name)->{
File currentFile = new File(dir.getAbsolutePath() + File.separator + name);
if (!currentFile.isDirectory()){
return false;
}
Date date = null;
try {
date = simpleDateFormat.parse(name);
} catch (ParseException e) {
logger.error("格式化时间{}错误", name);
return false;
}
Calendar c = Calendar.getInstance();
c.setTime(date);
int y = c.get(Calendar.YEAR);
int m = c.get(Calendar.MONTH) + 1;
if (year != null) {
if (month != null) {
return y == year && m == month;
}else {
return y == year;
}
}else {
return true;
}
});
if (dateFiles == null) {
return new ArrayList<>();
}
List<File> dateFileList = Arrays.asList(dateFiles);
if (sort != null && sort) {
dateFileList.sort((File f1, File f2)->{
int sortResult = 0;
try {
sortResult = simpleDateFormat.parse(f1.getName()).compareTo(simpleDateFormat.parse(f2.getName()));
} catch (ParseException e) {
logger.error("格式化时间{}/{}错误", f1.getName(), f2.getName());
}
return sortResult;
});
}
return dateFileList;
}
public List<MergeOrCutTaskInfo> getTaskListForDownload(String app, String stream, String callId, Boolean isEnd, String taskId) {
logger.info("[查询录像合成列表] app {} stream {} callId {}, isEnd: {}, taskId: {}",
app, stream, callId, isEnd, taskId);
ArrayList<MergeOrCutTaskInfo> result = new ArrayList<>();
if (taskId == null) {
taskId = "*";
}
List<Object> taskCatch = redisUtil.scan(String.format("%S_%S_%S", AssistConstants.MERGEORCUT,
userSettings.getId(), taskId));
for (int i = 0; i < taskCatch.size(); i++) {
String keyItem = taskCatch.get(i).toString();
MergeOrCutTaskInfo mergeOrCutTaskInfo = (MergeOrCutTaskInfo)redisUtil.get(keyItem);
if (mergeOrCutTaskInfo != null){
if ((!ObjectUtils.isEmpty(app) && !mergeOrCutTaskInfo.getApp().equals(app))
|| (!ObjectUtils.isEmpty(stream) && !mergeOrCutTaskInfo.getStream().equals(stream))
|| (!ObjectUtils.isEmpty(callId) && !mergeOrCutTaskInfo.getCallId().equals(callId))
) {
continue;
}
if (mergeOrCutTaskInfo.getPercentage() != null){
if (isEnd != null ) {
if (isEnd) {
if (Double.parseDouble(mergeOrCutTaskInfo.getPercentage()) == 1){
result.add(mergeOrCutTaskInfo);
}
}else {
if (Double.parseDouble(mergeOrCutTaskInfo.getPercentage()) < 1){
result.add((MergeOrCutTaskInfo)redisUtil.get(keyItem));
}
}
}else {
result.add((MergeOrCutTaskInfo)redisUtil.get(keyItem));
}
}
}
}
result.sort((MergeOrCutTaskInfo m1, MergeOrCutTaskInfo m2)->{
int sortResult = 0;
try {
sortResult = simpleDateFormatForTime.parse(m1.getCreateTime()).compareTo(simpleDateFormatForTime.parse(m2.getCreateTime()));
if (sortResult == 0) {
sortResult = simpleDateFormatForTime.parse(m1.getCreateTime()).compareTo(simpleDateFormatForTime.parse(m2.getCreateTime()));
}
if (sortResult == 0) {
sortResult = simpleDateFormatForTime.parse(m1.getCreateTime()).compareTo(simpleDateFormatForTime.parse(m2.getCreateTime()));
}
} catch (ParseException e) {
e.printStackTrace();
}
return sortResult * -1;
});
return result;
}
public boolean collection(String app, String stream, String type) {
File streamFile = new File(userSettings.getRecordTempPath() + File.separator + app + File.separator + stream);
boolean result = false;
if (streamFile.exists() && streamFile.isDirectory() && streamFile.canWrite()) {
File signFile = new File(streamFile.getAbsolutePath() + File.separator + type + ".sign");
try {
result = signFile.createNewFile();
} catch (IOException e) {
logger.error("[收藏文件]失败,{}/{}", app, stream);
}
}
return result;
}
public boolean removeCollection(String app, String stream, String type) {
File signFile = new File(userSettings.getRecordTempPath() + File.separator + app + File.separator + stream + File.separator + type + ".sign");
boolean result = false;
if (signFile.exists() && signFile.isFile()) {
result = signFile.delete();
}
return result;
}
public List<SignInfo> getCollectionList(String app, String stream, String type) {
List<File> appList = this.getAppList(true);
List<SignInfo> result = new ArrayList<>();
if (appList.size() > 0) {
for (File appFile : appList) {
if (app != null) {
if (!app.equals(appFile.getName())) {
continue;
}
}
List<File> streamList = getStreamList(appFile, true);
if (streamList.size() > 0) {
for (File streamFile : streamList) {
if (stream != null) {
if (!stream.equals(streamFile.getName())) {
continue;
}
}
if (type != null) {
File signFile = new File(streamFile.getAbsolutePath() + File.separator + type + ".sign");
if (signFile.exists()) {
SignInfo signInfo = new SignInfo();
signInfo.setApp(appFile.getName());
signInfo.setStream(streamFile.getName());
signInfo.setType(type);
result.add(signInfo);
}
}else {
streamFile.listFiles((File dir, String name) -> {
File currentFile = new File(dir.getAbsolutePath() + File.separator + name);
if (currentFile.isFile() && name.endsWith(".sign")){
String currentType = name.substring(0, name.length() - ".sign".length());
SignInfo signInfo = new SignInfo();
signInfo.setApp(appFile.getName());
signInfo.setStream(streamFile.getName());
signInfo.setType(currentType);
result.add(signInfo);
}
return false;
});
}
}
}
}
}
return result;
}
public long fileDuration(String app, String stream) {
List<File> allFiles = getFilesInTime(app, stream, null, null);
long durationResult = 0;
if (allFiles != null && allFiles.size() > 0) {
for (File file : allFiles) {
try {
durationResult += ffmpegExecUtils.duration(file);
} catch (IOException e) {
logger.error("获取{}视频时长错误:{}", file.getAbsolutePath(), e.getMessage());
}
}
}
return durationResult;
}
public int deleteFile(List<String> filePathList) {
assert filePathList != null;
assert filePathList.isEmpty();
int deleteResult = 0;
for (String filePath : filePathList) {
File file = new File(filePath);
if (file.exists()) {
if (file.delete()) {
deleteResult ++;
}
}else {
logger.warn("[删除文件] 文件不存在,{}", filePath);
}
}
if (deleteResult == 0) {
throw new ControllerException(ErrorCode.ERROR100.getCode(), "未删除任何文件");
}
return deleteResult;
}
}

View File

@ -0,0 +1,46 @@
package top.panll.assist.utils;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.Locale;
public class DateUtils {
public static final String PATTERNForDateTime = "yyyy-MM-dd HH:mm:ss";
public static final String PATTERNForDate = "yyyy-MM-dd";
public static final String zoneStr = "Asia/Shanghai";
// 获得某天最大时间 2020-02-19 23:59:59
public static Date getEndOfDay(Date date) {
LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(date.getTime()), ZoneId.systemDefault());;
LocalDateTime endOfDay = localDateTime.with(LocalTime.MAX);
return Date.from(endOfDay.atZone(ZoneId.systemDefault()).toInstant());
}
// 获得某天最小时间 2020-02-17 00:00:00
public static Date getStartOfDay(Date date) {
LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(date.getTime()), ZoneId.systemDefault());
LocalDateTime startOfDay = localDateTime.with(LocalTime.MIN);
return Date.from(startOfDay.atZone(ZoneId.systemDefault()).toInstant());
}
public static String getDateStr(Date date) {
SimpleDateFormat formatter = new SimpleDateFormat(PATTERNForDate);
return formatter.format(date);
}
public static String getDateTimeStr(Date date) {
SimpleDateFormat formatter = new SimpleDateFormat(PATTERNForDateTime);
return formatter.format(date);
}
}

View File

@ -0,0 +1,723 @@
package top.panll.assist.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* @Description:Redis工具类
* @author: swwheihei
* @date: 2020年5月6日 下午8:27:29
*/
@Component
@SuppressWarnings(value = {"rawtypes", "unchecked"})
public class RedisUtil {
@Autowired
private RedisTemplate<Object, Object> redisTemplate;
/**
* 指定缓存失效时间
* @param key
* @param time 时间
* @return true / false
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据 key 获取过期时间
* @param key
* @return
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断 key 是否存在
* @param key
* @return true / false
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
* @SuppressWarnings("unchecked") 忽略类型转换警告
* @param key 一个或者多个
*/
public boolean del(String... key) {
try {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
// 传入一个 Collection<String> 集合
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
// ============================== String ==============================
/**
* 普通缓存获取
* @param key
* @return
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
* @param key
* @param value
* @return true / false
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
// e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
* @param key
* @param value
* @param time 时间如果 time < 0 则设置无限时间
* @return true / false
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
* @param key
* @param delta 递增大小
* @return
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于 0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
* @param key
* @param delta 递减大小
* @return
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于 0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
// ============================== Map ==============================
/**
* HashGet
* @param key no null
* @param item no null
* @return
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取 key 对应的 map
* @param key no null
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
* @param key
* @param map
* @return true / false
*/
public boolean hmset(String key, Map<Object, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
* @param key
* @param map
* @param time 时间
* @return true / false
*/
public boolean hmset(String key, Map<Object, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张 Hash表 中放入数据如不存在则创建
* @param key
* @param item
* @param value
* @return true / false
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张 Hash表 中放入数据并设置时间如不存在则创建
* @param key
* @param item
* @param value
* @param time 时间如果原来的 Hash表 设置了时间这里会覆盖
* @return true / false
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除 Hash表 中的值
* @param key
* @param item 可以多个no null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断 Hash表 中是否有该键的值
* @param key no null
* @param item no null
* @return true / false
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* Hash递增如果不存在则创建一个并把新增的值返回
* @param key
* @param item
* @param by 递增大小 > 0
* @return
*/
public Double hincr(String key, String item, Double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* Hash递减
* @param key
* @param item
* @param by 递减大小
* @return
*/
public Double hdecr(String key, String item, Double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
// ============================== Set ==============================
/**
* 根据 key 获取 set 中的所有值
* @param key
* @return
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 从键为 key set 根据 value 查询是否存在
* @param key
* @param value
* @return true / false
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入 set缓存
* @param key 键值
* @param values 可以多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将数据放入 set缓存并设置时间
* @param key
* @param time 时间
* @param values 可以多个
* @return 成功放入个数
*/
public long sSet(String key, long time, Object... values) {
try {
long count = redisTemplate.opsForSet().add(key, values);
if (time > 0) {
expire(key, time);
}
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取 set缓存的长度
* @param key
* @return 长度
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除 set缓存中值为 value
* @param key
* @param values
* @return 成功移除个数
*/
public long setRemove(String key, Object... values) {
try {
return redisTemplate.opsForSet().remove(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
// ============================== ZSet ==============================
/**
* 添加一个元素, zset与set最大的区别就是每个元素都有一个score因此有个排序的辅助功能; zadd
*
* @param key
* @param value
* @param score
*/
public void zAdd(Object key, Object value, double score) {
redisTemplate.opsForZSet().add(key, value, score);
}
/**
* 删除元素 zrem
*
* @param key
* @param value
*/
public void zRemove(Object key, Object value) {
redisTemplate.opsForZSet().remove(key, value);
}
/**
* score的增加or减少 zincrby
*
* @param key
* @param value
* @param score
*/
public Double zIncrScore(Object key, Object value, double score) {
return redisTemplate.opsForZSet().incrementScore(key, value, score);
}
/**
* 查询value对应的score zscore
*
* @param key
* @param value
* @return
*/
public Double zScore(Object key, Object value) {
return redisTemplate.opsForZSet().score(key, value);
}
/**
* 判断value在zset中的排名 zrank
*
* @param key
* @param value
* @return
*/
public Long zRank(Object key, Object value) {
return redisTemplate.opsForZSet().rank(key, value);
}
/**
* 返回集合的长度
*
* @param key
* @return
*/
public Long zSize(Object key) {
return redisTemplate.opsForZSet().zCard(key);
}
/**
* 查询集合中指定顺序的值 0 -1 表示获取全部的集合内容 zrange
*
* 返回有序的集合score小的在前面
*
* @param key
* @param start
* @param end
* @return
*/
public Set<Object> ZRange(Object key, int start, int end) {
return redisTemplate.opsForZSet().range(key, start, end);
}
/**
* 查询集合中指定顺序的值和score0, -1 表示获取全部的集合内容
*
* @param key
* @param start
* @param end
* @return
*/
public Set<ZSetOperations.TypedTuple<Object>> zRangeWithScore(Object key, int start, int end) {
return redisTemplate.opsForZSet().rangeWithScores(key, start, end);
}
/**
* 查询集合中指定顺序的值 zrevrange
*
* 返回有序的集合中score大的在前面
*
* @param key
* @param start
* @param end
* @return
*/
public Set<Object> zRevRange(Object key, int start, int end) {
return redisTemplate.opsForZSet().reverseRange(key, start, end);
}
/**
* 根据score的值来获取满足条件的集合 zrangebyscore
*
* @param key
* @param min
* @param max
* @return
*/
public Set<Object> zSortRange(Object key, int min, int max) {
return redisTemplate.opsForZSet().rangeByScore(key, min, max);
}
// ============================== List ==============================
/**
* 获取 list缓存的内容
* @param key
* @param start 开始
* @param end 结束0 -1 代表所有值
* @return
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取 list缓存的长度
* @param key
* @return 长度
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 根据索引 index 获取键为 key list 中的元素
* @param key
* @param index 索引
* index >= 0 {0:表头, 1:第二个元素}
* index < 0 {-1:表尾, -2:倒数第二个元素}
* @return
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将值 value 插入键为 key list 如果 list 不存在则创建空 list
* @param key
* @param value
* @return true / false
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将值 value 插入键为 key list 并设置时间
* @param key
* @param value
* @param time 时间
* @return true / false
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* values 插入键为 key list
* @param key
* @param values
* @return true / false
*/
public boolean lSetList(String key, List<Object> values) {
try {
redisTemplate.opsForList().rightPushAll(key, values);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* values 插入键为 key list 并设置时间
* @param key
* @param values
* @param time 时间
* @return true / false
*/
public boolean lSetList(String key, List<Object> values, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, values);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引 index 修改键为 key 的值
* @param key
* @param index 索引
* @param value
* @return true / false
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 在键为 key list 中删除值为 value 的元素
* @param key
* @param count 如果 count == 0 则删除 list 中所有值为 value 的元素
* 如果 count > 0 则删除 list 中最左边那个值为 value 的元素
* 如果 count < 0 则删除 list 中最右边那个值为 value 的元素
* @param value
* @return
*/
public long lRemove(String key, long count, Object value) {
try {
return redisTemplate.opsForList().remove(key, count, value);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 模糊查询
* @param key
* @return true / false
*/
public List<Object> keys(String key) {
try {
Set<Object> set = redisTemplate.keys(key);
return new ArrayList<>(set);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 模糊查询
* @param query 查询参数
* @return
*/
// public List<Object> scan(String query) {
// List<Object> result = new ArrayList<>();
// try {
// Cursor<Map.Entry<Object,Object>> cursor = redisTemplate.opsForHash().scan("field",
// ScanOptions.scanOptions().match(query).count(1000).build());
// while (cursor.hasNext()) {
// Map.Entry<Object,Object> entry = cursor.next();
// result.add(entry.getKey());
// Object key = entry.getKey();
// Object valueSet = entry.getValue();
// }
// //关闭cursor
// cursor.close();
// } catch (Exception e) {
// e.printStackTrace();
// }
// return result;
// }
/**
* 模糊查询
* @param query 查询参数
* @return
*/
public List<Object> scan(String query) {
Set<String> resultKeys = redisTemplate.execute((RedisCallback<Set<String>>) connection -> {
ScanOptions scanOptions = ScanOptions.scanOptions().match("*" + query + "*").count(1000).build();
Cursor<byte[]> scan = connection.scan(scanOptions);
Set<String> keys = new HashSet<>();
while (scan.hasNext()) {
byte[] next = scan.next();
keys.add(new String(next));
}
return keys;
});
return new ArrayList<>(resultKeys);
}
}

View File

@ -0,0 +1,57 @@
spring:
# REDIS数据库配置
redis:
# [必须修改] Redis服务器IP, REDIS安装在本机的,使用127.0.0.1
host: 127.0.0.1
# [必须修改] 端口号
port: 6379
# [可选] 数据库 DB
database: 8
# [可选] 访问密码,若你的redis服务器没有设置密码就不需要用密码去连接
password:
# [可选] 超时时间
timeout: 10000
# [必选] WVP监听的HTTP端口, 网页和接口调用都是这个端口
server:
port: 18081
# [可选] HTTPS配置 默认不开启
ssl:
# [可选] 是否开启HTTPS访问
enabled: false
# [可选] 证书文件路径放置在resource/目录下即可修改xxx为文件名
key-store: classpath:xxx.jks
# [可选] 证书密码
key-password: password
# [可选] 证书类型, 默认为jks根据实际修改
key-store-type: JKS
# [根据业务需求配置]
user-settings:
# [可选 ] 临时录像路径
record-temp-path: ./recordTemp
# [可选 ] 录像保存时长(单位: 天每天晚12点自动对过期文件执行清理, 不配置则不删除
record-temp-day: 7
# [可选 ] 录像下载合成临时文件保存时长, 不配置默认取值recordDay单位每天晚12点自动对过期文件执行清理
# recordTempDay: 7
# [必选 ] ffmpeg路径
ffmpeg: /usr/bin/ffmpeg
# [必选 ] ffprobe路径 一般安装ffmpeg就会自带 一般跟ffmpeg在同一目录,用于查询文件的信息
ffprobe: /usr/bin/ffprobe
# [可选 ] 限制 ffmpeg 合并文件使用的线程数间接限制cpu使用率 默认2 限制到50%
threads: 2
swagger-ui:
# [可选] 日志配置, 一般不需要改
logging:
file:
name: logs/wvp.log
max-history: 30
max-size: 10MB
total-size-cap: 300MB
level:
root: WARN
top:
panll:
assist: info

View File

@ -0,0 +1,57 @@
spring:
# REDIS数据库配置
redis:
# [必须修改] Redis服务器IP, REDIS安装在本机的,使用127.0.0.1
host: 127.0.0.1
# [必须修改] 端口号
port: 6379
# [可选] 数据库 DB
database: 8
# [可选] 访问密码,若你的redis服务器没有设置密码就不需要用密码去连接
password:
# [可选] 超时时间
timeout: 10000
# [可选] WVP监听的HTTP端口, 网页和接口调用都是这个端口
server:
port: 18081
# [可选] HTTPS配置 默认不开启
ssl:
# [可选] 是否开启HTTPS访问
enabled: false
# [可选] 证书文件路径放置在resource/目录下即可修改xxx为文件名
key-store: classpath:xxx.jks
# [可选] 证书密码
key-password: password
# [可选] 证书类型, 默认为jks根据实际修改
key-store-type: JKS
# [根据业务需求配置]
userSettings:
# [可选 ] zlm配置的录像路径
record: /media/lin/Server/ZLMediaKit/dev/ZLMediaKit/release/linux/Debug/www/record
# [可选 ] 录像保存时长(单位: 天每天晚12点自动对过期文件执行清理
recordDay: 7
# [可选 ] 录像下载合成临时文件保存时长, 不配置默认取值recordDay单位每天晚12点自动对过期文件执行清理
# recordTempDay: 7
# [必选 ] ffmpeg路径
ffmpeg: /usr/bin/ffmpeg
# [必选 ] ffprobe路径 一般安装ffmpeg就会自带 一般跟ffmpeg在同一目录,用于查询文件的信息
ffprobe: /usr/bin/ffprobe
# [可选 ] 限制 ffmpeg 合并文件使用的线程数间接限制cpu使用率 默认2 限制到50%
threads: 2
swagger-ui:
# [可选] 日志配置, 一般不需要改
logging:
file:
name: logs/wvp.log
max-history: 30
max-size: 10MB
total-size-cap: 300MB
level:
root: WARN
top:
panll:
assist: info

View File

@ -0,0 +1,57 @@
spring:
# REDIS数据库配置
redis:
# [可选] 超时时间
timeout: 10000
# 以下为单机配置
# [必须修改] Redis服务器IP, REDIS安装在本机的,使用127.0.0.1
host: 127.0.0.1
# [必须修改] 端口号
port: 6379
# [可选] 数据库 DB
database: 1
# [可选] 访问密码,若你的redis服务器没有设置密码就不需要用密码去连接
password:
# 以下为集群配置
# cluster:
# nodes: 192.168.1.242:7001
# password: 4767cb971b40a1300fa09b7f87b09d1c
# [可选] WVP监听的HTTP端口, 网页和接口调用都是这个端口
server:
port: 18089
# [可选] HTTPS配置 默认不开启
ssl:
# [可选] 是否开启HTTPS访问
enabled: false
# [可选] 证书文件路径放置在resource/目录下即可修改xxx为文件名
key-store: classpath:xxx.jks
# [可选] 证书密码
key-password: password
# [可选] 证书类型, 默认为jks根据实际修改
key-store-type: JKS
# [根据业务需求配置]
userSettings:
id: 111
# [可选 ] 录像保存时长(单位: 天每天晚12点自动对过期文件执行清理, 不配置则不删除
record-temp-day: 7
# [必选 ] ffmpeg路径
ffmpeg: ./lib/ffmpeg
# [必选 ] ffprobe路径 一般安装ffmpeg就会自带 一般跟ffmpeg在同一目录,用于查询文件的信息,
ffprobe: ./lib/ffprobe
# [可选 ] 限制 ffmpeg 合并文件使用的线程数间接限制cpu使用率 默认2 限制到50%
threads: 2
# [可选] 日志配置, 一般不需要改
logging:
file:
name: logs/wvp.log
max-history: 30
max-size: 10MB
total-size-cap: 300MB
level:
root: WARN
top:
panll:
assist: info

View File

@ -0,0 +1,3 @@
spring:
profiles:
active: local

View File

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>下载</title>
</head>
<body>
<a id="download" download></a>
<script>
(function () {
let searchParams = new URLSearchParams(location.search);
var download = document.getElementById("download");
download.setAttribute("href", searchParams.get("url"))
download.click()
setTimeout(() => {
window.location.href = "about:blank";
window.close();
}, 200)
})();
</script>
</body>
</html>

View File

@ -0,0 +1,13 @@
package top.panll.assist;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class WvpProAssistApplicationTests {
@Test
void contextLoads() {
}
}