第一次提交
This commit is contained in:
3
fastbee-record/README.md
Normal file
3
fastbee-record/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# fastbee-record
|
||||
|
||||
fastbee-record录像程序,需要和zlm一起使用,提供录像控制,录像合并下载接口
|
151
fastbee-record/pom.xml
Normal file
151
fastbee-record/pom.xml
Normal file
@ -0,0 +1,151 @@
|
||||
<?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>
|
||||
<artifactId>fastbee</artifactId>
|
||||
<groupId>com.fastbee</groupId>
|
||||
<version>3.8.5</version>
|
||||
</parent>
|
||||
|
||||
<groupId>com.fastbee.record</groupId>
|
||||
<artifactId>fastbee-record</artifactId>
|
||||
<version>3.8.5</version>
|
||||
<name>fastbee-record</name>
|
||||
<description>zlm录制服务</description>
|
||||
|
||||
<properties>
|
||||
<java.version>1.8</java.version>
|
||||
<druid.version>1.2.15</druid.version>
|
||||
<mybatis-spring-boot.version>2.2.0</mybatis-spring-boot.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>com.fastbee</groupId>
|
||||
<artifactId>fastbee-common</artifactId>
|
||||
<version>3.8.5</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.fastbee</groupId>
|
||||
<artifactId>fastbee-oss</artifactId>
|
||||
<version>3.8.5</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</dependency>
|
||||
|
||||
<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>org.mybatis.spring.boot</groupId>
|
||||
<artifactId>mybatis-spring-boot-starter</artifactId>
|
||||
<version>${mybatis-spring-boot.version}</version>
|
||||
</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>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<version>2.1.1.RELEASE</version>
|
||||
<configuration>
|
||||
<fork>true</fork> <!-- 如果没有该配置,devtools不会生效 -->
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>repackage</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-war-plugin</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<configuration>
|
||||
<failOnMissingWebXml>false</failOnMissingWebXml>
|
||||
<warName>${project.artifactId}</warName>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
<finalName>${project.artifactId}</finalName>
|
||||
</build>
|
||||
|
||||
</project>
|
@ -0,0 +1,18 @@
|
||||
package com.fastbee.record;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@SpringBootApplication(exclude = {SecurityAutoConfiguration.class })
|
||||
@EnableScheduling
|
||||
@ComponentScan(basePackages = {"com.fastbee.common.core.redis","com.fastbee.record"})
|
||||
public class ZlmRecordApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(ZlmRecordApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
package com.fastbee.record.config;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONReader;
|
||||
import com.alibaba.fastjson2.JSONWriter;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.data.redis.serializer.SerializationException;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
/**
|
||||
* Redis使用FastJson序列化
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
|
||||
{
|
||||
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
|
||||
|
||||
private Class<T> clazz;
|
||||
|
||||
public FastJson2JsonRedisSerializer(Class<T> clazz)
|
||||
{
|
||||
super();
|
||||
this.clazz = clazz;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serialize(T t) throws SerializationException
|
||||
{
|
||||
if (t == null)
|
||||
{
|
||||
return new byte[0];
|
||||
}
|
||||
return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET);
|
||||
}
|
||||
|
||||
@Override
|
||||
public T deserialize(byte[] bytes) throws SerializationException
|
||||
{
|
||||
if (bytes == null || bytes.length <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
String str = new String(bytes, DEFAULT_CHARSET);
|
||||
|
||||
return JSON.parseObject(str, clazz, JSONReader.Feature.SupportAutoType);
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package com.fastbee.record.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;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class FastJsonRedisSerializer<T> implements RedisSerializer<T> {
|
||||
private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
|
||||
private final Class<T> clazz;
|
||||
|
||||
static {
|
||||
ParserConfig.getGlobalInstance().addAccept("com.fastbee.record");
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package com.fastbee.record.config;
|
||||
|
||||
import com.fastbee.record.controller.bean.Result;
|
||||
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 com.fastbee.record.controller.bean.ControllerException;
|
||||
import com.fastbee.record.controller.bean.ErrorCode;
|
||||
|
||||
/**
|
||||
* 全局异常处理
|
||||
*/
|
||||
@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 Result<String> exceptionHandler(Exception e) {
|
||||
logger.error("[全局异常]: ", e);
|
||||
return Result.fail(ErrorCode.ERROR500.getCode(), e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义异常处理, 处理controller中返回的错误
|
||||
* @param e 异常
|
||||
* @return 统一返回结果
|
||||
*/
|
||||
@ExceptionHandler(ControllerException.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public Result<String> exceptionHandler(ControllerException e) {
|
||||
return Result.fail(e.getCode(), e.getMsg());
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
package com.fastbee.record.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 com.fastbee.record.controller.bean.ErrorCode;
|
||||
import com.fastbee.record.controller.bean.Result;
|
||||
|
||||
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 Result) {
|
||||
return body;
|
||||
}
|
||||
|
||||
if (body instanceof ErrorCode) {
|
||||
ErrorCode errorCode = (ErrorCode) body;
|
||||
return new Result<>(errorCode.getCode(), errorCode.getMsg(), null);
|
||||
}
|
||||
|
||||
if (body instanceof String) {
|
||||
return JSON.toJSONString(Result.success(body));
|
||||
}
|
||||
|
||||
return Result.success(body);
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
package com.fastbee.record.config;
|
||||
|
||||
import org.springframework.cache.annotation.CachingConfigurerSupport;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
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.RedisTemplate;
|
||||
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
|
||||
@Configuration
|
||||
@EnableCaching
|
||||
public class RedisConfig extends CachingConfigurerSupport
|
||||
{
|
||||
@Bean
|
||||
@SuppressWarnings(value = { "unchecked", "rawtypes" })
|
||||
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
|
||||
{
|
||||
RedisTemplate<Object, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
|
||||
FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
|
||||
|
||||
// 使用StringRedisSerializer来序列化和反序列化redis的key值
|
||||
template.setKeySerializer(new StringRedisSerializer());
|
||||
template.setValueSerializer(serializer);
|
||||
|
||||
// Hash的key也采用StringRedisSerializer的序列化方式
|
||||
template.setHashKeySerializer(new StringRedisSerializer());
|
||||
template.setHashValueSerializer(serializer);
|
||||
|
||||
template.afterPropertiesSet();
|
||||
return template;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DefaultRedisScript<Long> limitScript()
|
||||
{
|
||||
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
|
||||
redisScript.setScriptText(limitScriptText());
|
||||
redisScript.setResultType(Long.class);
|
||||
return redisScript;
|
||||
}
|
||||
|
||||
/**
|
||||
* 限流脚本
|
||||
*/
|
||||
private String limitScriptText()
|
||||
{
|
||||
return "local key = KEYS[1]\n" +
|
||||
"local count = tonumber(ARGV[1])\n" +
|
||||
"local time = tonumber(ARGV[2])\n" +
|
||||
"local current = redis.call('get', key);\n" +
|
||||
"if current and tonumber(current) > count then\n" +
|
||||
" return tonumber(current);\n" +
|
||||
"end\n" +
|
||||
"current = redis.call('incr', key)\n" +
|
||||
"if tonumber(current) == 1 then\n" +
|
||||
" redis.call('expire', key, time)\n" +
|
||||
"end\n" +
|
||||
"return tonumber(current);";
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package com.fastbee.record.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();
|
||||
|
||||
return new OpenAPI()
|
||||
.info(new Info().title("ZLM录像助手 接口文档")
|
||||
.contact(contact)
|
||||
.description("录像助手,补充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("com.fastbee.record")
|
||||
.build();
|
||||
}
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
package com.fastbee.record.config;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
import com.fastbee.record.dto.UserSettings;
|
||||
import com.fastbee.record.service.VideoFileService;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
/**
|
||||
* 用于启动检查环境
|
||||
*/
|
||||
@Component
|
||||
@Order(value=1)
|
||||
public class StartConfig implements CommandLineRunner {
|
||||
|
||||
private final static Logger logger = LoggerFactory.getLogger(StartConfig.class);
|
||||
|
||||
@Value("${server.port}")
|
||||
private String port;
|
||||
|
||||
@Autowired
|
||||
private UserSettings userSettings;
|
||||
|
||||
@Autowired
|
||||
private VideoFileService videoFileService;
|
||||
|
||||
|
||||
@Override
|
||||
public void run(String... args) {
|
||||
String record = userSettings.getRecord();
|
||||
if (!record.endsWith(File.separator)) {
|
||||
userSettings.setRecord(userSettings.getRecord() + File.separator);
|
||||
}
|
||||
|
||||
File recordFile = new File(record);
|
||||
if (!recordFile.exists()){
|
||||
logger.warn("[userSettings.record]路径不存在,开始创建");
|
||||
boolean mkResult = recordFile.mkdirs();
|
||||
if (!mkResult) {
|
||||
logger.info("[userSettings.record]目录创建失败");
|
||||
System.exit(1);
|
||||
}
|
||||
}else {
|
||||
if ( !recordFile.isDirectory()) {
|
||||
logger.warn("[userSettings.record]路径是文件,请修改为目录");
|
||||
System.exit(1);
|
||||
}
|
||||
if (!recordFile.canRead()) {
|
||||
logger.error("[userSettings.record]路径无法读取");
|
||||
System.exit(1);
|
||||
}
|
||||
if (!recordFile.canWrite()) {
|
||||
logger.error("[userSettings.record]路径无法写入");
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
// 对目录进行预整理
|
||||
File[] appFiles = recordFile.listFiles();
|
||||
if (appFiles != null && appFiles.length > 0) {
|
||||
for (File appFile : appFiles) {
|
||||
if (appFile.getName().equals("recordTemp")) {
|
||||
continue;
|
||||
}
|
||||
File[] streamFiles = appFile.listFiles();
|
||||
if (streamFiles != null && streamFiles.length > 0) {
|
||||
for (File streamFile : streamFiles) {
|
||||
File[] dateFiles = streamFile.listFiles();
|
||||
if (dateFiles != null && dateFiles.length > 0) {
|
||||
for (File dateFile : dateFiles) {
|
||||
File[] files = dateFile.listFiles();
|
||||
if (files != null && files.length > 0) {
|
||||
for (File file : files) {
|
||||
videoFileService.handFile(file, appFile.getName(), streamFile.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception exception){
|
||||
exception.printStackTrace();
|
||||
logger.error("环境错误: " + exception.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
package com.fastbee.record.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 = "fastbee-record-";
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
package com.fastbee.record.controller;
|
||||
|
||||
|
||||
import com.fastbee.record.dto.UserSettings;
|
||||
import org.mp4parser.BasicContainer;
|
||||
import org.mp4parser.muxer.Movie;
|
||||
import org.mp4parser.muxer.Track;
|
||||
import org.mp4parser.muxer.builder.DefaultMp4Builder;
|
||||
import org.mp4parser.muxer.container.mp4.MovieCreator;
|
||||
import org.mp4parser.muxer.tracks.AppendTrack;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.Channels;
|
||||
import java.nio.channels.WritableByteChannel;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/down")
|
||||
public class DownController {
|
||||
|
||||
@Autowired
|
||||
private UserSettings userSettings;
|
||||
|
||||
/**
|
||||
* 获取app+stream列表
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@GetMapping(value = "/**")
|
||||
@ResponseBody
|
||||
public void download(HttpServletRequest request, HttpServletResponse response) throws IOException {
|
||||
|
||||
List<String> videoList = new ArrayList<>();
|
||||
videoList.add("/opt/media/bin/www/record/rtp/34020000002000000003_34020000001310000001/2023-04-26/16-09-07.mp4");
|
||||
List<Movie> sourceMovies = new ArrayList<>();
|
||||
for (String video : videoList) {
|
||||
sourceMovies.add(MovieCreator.build(video));
|
||||
}
|
||||
|
||||
List<Track> videoTracks = new LinkedList<>();
|
||||
List<Track> audioTracks = new LinkedList<>();
|
||||
for (Movie movie : sourceMovies) {
|
||||
for (Track track : movie.getTracks()) {
|
||||
if ("soun".equals(track.getHandler())) {
|
||||
audioTracks.add(track);
|
||||
}
|
||||
|
||||
if ("vide".equals(track.getHandler())) {
|
||||
videoTracks.add(track);
|
||||
}
|
||||
}
|
||||
}
|
||||
Movie mergeMovie = new Movie();
|
||||
if (audioTracks.size() > 0) {
|
||||
mergeMovie.addTrack(new AppendTrack(audioTracks.toArray(new Track[audioTracks.size()])));
|
||||
}
|
||||
|
||||
if (videoTracks.size() > 0) {
|
||||
mergeMovie.addTrack(new AppendTrack(videoTracks.toArray(new Track[videoTracks.size()])));
|
||||
}
|
||||
|
||||
BasicContainer out = (BasicContainer)new DefaultMp4Builder().build(mergeMovie);
|
||||
|
||||
// 文件名
|
||||
String fileName = "测试.mp4";
|
||||
// 文件类型
|
||||
String contentType = request.getServletContext().getMimeType(fileName);
|
||||
|
||||
// 解决下载文件时文件名乱码问题
|
||||
byte[] fileNameBytes = fileName.getBytes(StandardCharsets.UTF_8);
|
||||
fileName = new String(fileNameBytes, 0, fileNameBytes.length, StandardCharsets.ISO_8859_1);
|
||||
|
||||
response.setHeader("Content-Type", contentType);
|
||||
response.setHeader("Content-Length", String.valueOf(out));
|
||||
//inline表示浏览器直接使用,attachment表示下载,fileName表示下载的文件名
|
||||
response.setHeader("Content-Disposition", "inline;filename=" + fileName);
|
||||
response.setContentType(contentType);
|
||||
|
||||
WritableByteChannel writableByteChannel = Channels.newChannel(response.getOutputStream());
|
||||
out.writeContainer(writableByteChannel);
|
||||
writableByteChannel.close();
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,214 @@
|
||||
package com.fastbee.record.controller;
|
||||
|
||||
import com.fastbee.common.core.controller.BaseController;
|
||||
import com.fastbee.common.core.domain.AjaxResult;
|
||||
import com.fastbee.common.core.redis.RedisCache;
|
||||
import com.fastbee.common.utils.StringUtils;
|
||||
import com.fastbee.oss.domain.OssDetail;
|
||||
import com.fastbee.oss.entity.UploadResult;
|
||||
import com.fastbee.oss.enums.AccessPolicyType;
|
||||
import com.fastbee.oss.service.OssClient;
|
||||
import com.fastbee.oss.service.OssFactory;
|
||||
import org.apache.catalina.connector.ClientAbortException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import com.fastbee.record.dto.UserSettings;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/file")
|
||||
public class DownloadController extends BaseController {
|
||||
|
||||
private final static Logger logger = LoggerFactory.getLogger(DownloadController.class);
|
||||
|
||||
@Autowired
|
||||
private UserSettings userSettings;
|
||||
|
||||
@Autowired
|
||||
private RedisCache redisCache;
|
||||
/**
|
||||
* 获取app+stream列表
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@GetMapping(value = "/download/**")
|
||||
@ResponseBody
|
||||
public void download(HttpServletRequest request, HttpServletResponse response) {
|
||||
|
||||
String resourcePath = request.getServletPath();
|
||||
resourcePath = resourcePath.substring("/file/download".length() + 1, resourcePath.length());
|
||||
String record = userSettings.getRecord();
|
||||
File file = new File(record + resourcePath);
|
||||
if (!file.exists()) {
|
||||
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* 参考实现来自: CSDN 进修的CODER SpringBoot Java实现Http方式分片下载断点续传+实现H5大视频渐进式播放
|
||||
* https://blog.csdn.net/lovequanquqn/article/details/104562945
|
||||
*/
|
||||
String range = request.getHeader("Range");
|
||||
logger.info("current request rang:" + range);
|
||||
//开始下载位置
|
||||
long startByte = 0;
|
||||
//结束下载位置
|
||||
long endByte = file.length() - 1;
|
||||
logger.info("文件开始位置:{},文件结束位置:{},文件总长度:{}", startByte, endByte, file.length());
|
||||
|
||||
//有range的话
|
||||
if (range != null && range.contains("bytes=") && range.contains("-")) {
|
||||
range = range.substring(range.lastIndexOf("=") + 1).trim();
|
||||
String[] ranges = range.split("-");
|
||||
try {
|
||||
//判断range的类型
|
||||
if (ranges.length == 1) {
|
||||
// 类型一:bytes=-2343,
|
||||
if (range.startsWith("-")) {
|
||||
endByte = Long.parseLong(ranges[0]);
|
||||
}
|
||||
//类型二:bytes=2343-
|
||||
else if (range.endsWith("-")) {
|
||||
startByte = Long.parseLong(ranges[0]);
|
||||
}
|
||||
}
|
||||
//类型三:bytes=22-2343
|
||||
else if (ranges.length == 2) {
|
||||
startByte = Long.parseLong(ranges[0]);
|
||||
endByte = Long.parseLong(ranges[1]);
|
||||
}
|
||||
|
||||
} catch (NumberFormatException e) {
|
||||
startByte = 0;
|
||||
endByte = file.length() - 1;
|
||||
logger.error("Range Occur Error,Message:{}", e.getLocalizedMessage());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// 要下载的长度
|
||||
long contentLength = endByte - startByte + 1;
|
||||
// 文件名
|
||||
String fileName = file.getName();
|
||||
// 文件类型
|
||||
String contentType = request.getServletContext().getMimeType(fileName);
|
||||
|
||||
// 解决下载文件时文件名乱码问题
|
||||
byte[] fileNameBytes = fileName.getBytes(StandardCharsets.UTF_8);
|
||||
fileName = new String(fileNameBytes, 0, fileNameBytes.length, StandardCharsets.ISO_8859_1);
|
||||
|
||||
response.setHeader("Content-Type", contentType);
|
||||
response.setHeader("Content-Length", String.valueOf(contentLength));
|
||||
//inline表示浏览器直接使用,attachment表示下载,fileName表示下载的文件名
|
||||
response.setHeader("Content-Disposition", "inline;filename=" + fileName);
|
||||
response.setContentType(contentType);
|
||||
if (range != null) {
|
||||
//各种响应头设置
|
||||
//支持断点续传,获取部分字节内容:
|
||||
response.setHeader("Accept-Ranges", "bytes");
|
||||
//http状态码要为206:表示获取部分内容
|
||||
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
|
||||
// Content-Range,格式为:[要下载的开始位置]-[结束位置]/[文件总大小]
|
||||
response.setHeader("Content-Range", "bytes " + startByte + "-" + endByte + "/" + file.length());
|
||||
} else {
|
||||
response.setStatus(HttpServletResponse.SC_OK);
|
||||
}
|
||||
|
||||
|
||||
BufferedOutputStream outputStream = null;
|
||||
RandomAccessFile randomAccessFile = null;
|
||||
//已传送数据大小
|
||||
long transmitted = 0;
|
||||
try {
|
||||
randomAccessFile = new RandomAccessFile(file, "r");
|
||||
|
||||
outputStream = new BufferedOutputStream(response.getOutputStream());
|
||||
byte[] buff = new byte[4096];
|
||||
int len = 0;
|
||||
randomAccessFile.seek(startByte);
|
||||
//warning:判断是否到了最后不足4096(buff的length)个byte这个逻辑((transmitted + len) <= contentLength)要放前面
|
||||
//不然会会先读取randomAccessFile,造成后面读取位置出错;
|
||||
while ((transmitted + len) <= contentLength && (len = randomAccessFile.read(buff)) != -1) {
|
||||
outputStream.write(buff, 0, len);
|
||||
transmitted += len;
|
||||
}
|
||||
//处理不足buff.length部分
|
||||
if (transmitted < contentLength) {
|
||||
len = randomAccessFile.read(buff, 0, (int) (contentLength - transmitted));
|
||||
outputStream.write(buff, 0, len);
|
||||
transmitted += len;
|
||||
}
|
||||
|
||||
outputStream.flush();
|
||||
response.flushBuffer();
|
||||
randomAccessFile.close();
|
||||
logger.info("下载完毕:" + startByte + "-" + endByte + ":" + transmitted);
|
||||
} catch (ClientAbortException e) {
|
||||
logger.warn("用户停止下载:" + startByte + "-" + endByte + ":" + transmitted);
|
||||
//捕获此异常表示拥护停止下载
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
logger.error("用户下载IO异常,Message:{}", e.getLocalizedMessage());
|
||||
} finally {
|
||||
try {
|
||||
if (randomAccessFile != null) {
|
||||
randomAccessFile.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping(value = "/upload")
|
||||
@ResponseBody
|
||||
public AjaxResult upload(@RequestParam String resourcePath, HttpServletResponse response) {
|
||||
String record = userSettings.getRecord();
|
||||
File file = new File(record + resourcePath);
|
||||
if (!file.exists()) {
|
||||
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
||||
return AjaxResult.error();
|
||||
}
|
||||
|
||||
String originalfileName = file.getName();
|
||||
String suffix = StringUtils.substring(originalfileName, originalfileName.lastIndexOf("."), originalfileName.length());
|
||||
OssClient storage = OssFactory.instance(redisCache);
|
||||
UploadResult uploadResult = storage.uploadSuffix(file, suffix);
|
||||
// 保存文件信息
|
||||
return AjaxResult.success(buildResultEntity(originalfileName, suffix, storage.getConfigKey(), uploadResult));
|
||||
}
|
||||
|
||||
private OssDetail buildResultEntity(String originalfileName, String suffix, String configKey, UploadResult uploadResult) {
|
||||
OssDetail oss = OssDetail.builder()
|
||||
.url(uploadResult.getUrl())
|
||||
.fileSuffix(suffix)
|
||||
.fileName(uploadResult.getFilename())
|
||||
.originalName(originalfileName)
|
||||
.service(configKey)
|
||||
.build();
|
||||
return this.matchingUrl(oss);
|
||||
}
|
||||
|
||||
private OssDetail matchingUrl(OssDetail oss) {
|
||||
OssClient storage = OssFactory.instance(oss.getService(),redisCache);
|
||||
// 仅修改桶类型为 private 的URL,临时URL时长为120s
|
||||
if (AccessPolicyType.PRIVATE == storage.getAccessPolicy()) {
|
||||
oss.setUrl(storage.getPrivateUrl(oss.getFileName(), 120));
|
||||
}
|
||||
return oss;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,343 @@
|
||||
package com.fastbee.record.controller;
|
||||
|
||||
import com.fastbee.record.controller.bean.ControllerException;
|
||||
import com.fastbee.record.controller.bean.ErrorCode;
|
||||
import com.fastbee.record.controller.bean.RecordFile;
|
||||
import com.fastbee.record.controller.bean.Result;
|
||||
import com.fastbee.record.dto.*;
|
||||
import com.fastbee.record.service.VideoFileService;
|
||||
import com.fastbee.record.utils.Constants;
|
||||
import com.fastbee.record.utils.PageInfo;
|
||||
import com.fastbee.record.utils.RedisUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@CrossOrigin
|
||||
@RestController
|
||||
@RequestMapping("/zlm/record")
|
||||
public class RecordController {
|
||||
|
||||
@Autowired
|
||||
private VideoFileService videoFileService;
|
||||
|
||||
@Autowired
|
||||
private UserSettings userSettings;
|
||||
|
||||
@Autowired
|
||||
private RedisUtil redisUtil;
|
||||
|
||||
private SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
/**
|
||||
* 获取Assist服务配置信息
|
||||
*/
|
||||
|
||||
@GetMapping(value = "/info")
|
||||
@ResponseBody
|
||||
public UserSettings getInfo(){
|
||||
return userSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取app+stream列表
|
||||
*/
|
||||
@GetMapping(value = "/list")
|
||||
@ResponseBody
|
||||
public PageInfo<RecordInfo> getList(@RequestParam int pageNum,
|
||||
@RequestParam int pageSize){
|
||||
List<RecordInfo> appList = videoFileService.getList();
|
||||
|
||||
PageInfo<RecordInfo> stringPageInfo = new PageInfo<>(appList);
|
||||
stringPageInfo.startPage(pageNum, pageSize);
|
||||
return stringPageInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页获取app列表
|
||||
*/
|
||||
@GetMapping(value = "/app/list")
|
||||
@ResponseBody
|
||||
public PageInfo<String> getAppList(@RequestParam int page,
|
||||
@RequestParam int count){
|
||||
List<String> resultData = new ArrayList<>();
|
||||
List<File> appList = videoFileService.getAppList(true);
|
||||
if (appList.size() > 0) {
|
||||
for (File file : appList) {
|
||||
resultData.add(file.getName());
|
||||
}
|
||||
}
|
||||
Collections.sort(resultData);
|
||||
|
||||
PageInfo<String> stringPageInfo = new PageInfo<>(resultData);
|
||||
stringPageInfo.startPage(page, count);
|
||||
return stringPageInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页stream列表
|
||||
*/
|
||||
@GetMapping(value = "/stream/list")
|
||||
@ResponseBody
|
||||
public PageInfo<String> getStreamList(@RequestParam int page,
|
||||
@RequestParam int count,
|
||||
@RequestParam String app ){
|
||||
List<String> resultData = new ArrayList<>();
|
||||
if (app == null) {
|
||||
throw new ControllerException(ErrorCode.ERROR400.getCode(), "app不能为空");
|
||||
}
|
||||
List<File> streamList = videoFileService.getStreamList(app, true);
|
||||
if (streamList != null) {
|
||||
for (File file : streamList) {
|
||||
resultData.add(file.getName());
|
||||
}
|
||||
}
|
||||
PageInfo<String> stringPageInfo = new PageInfo<>(resultData);
|
||||
stringPageInfo.startPage(page, count);
|
||||
return stringPageInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取日期文件夹列表
|
||||
*/
|
||||
@GetMapping(value = "/date/list")
|
||||
@ResponseBody
|
||||
public List<String> getDateList( @RequestParam(required = false) Integer year,
|
||||
@RequestParam(required = false) Integer month,
|
||||
@RequestParam String app,
|
||||
@RequestParam String stream ){
|
||||
List<String> resultData = new ArrayList<>();
|
||||
if (app == null) {
|
||||
throw new ControllerException(ErrorCode.ERROR400.getCode(), "app不能为空");
|
||||
};
|
||||
if (stream == null) {
|
||||
throw new ControllerException(ErrorCode.ERROR400.getCode(), "stream不能为空");
|
||||
}
|
||||
List<File> dateList = videoFileService.getDateList(app, stream, year, month, true);
|
||||
for (File file : dateList) {
|
||||
resultData.add(file.getName());
|
||||
}
|
||||
return resultData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取视频文件列表
|
||||
*/
|
||||
@GetMapping(value = "/file/list")
|
||||
@ResponseBody
|
||||
public PageInfo<String> getRecordList(@RequestParam int page,
|
||||
@RequestParam int count,
|
||||
@RequestParam String app,
|
||||
@RequestParam String stream,
|
||||
@RequestParam(required = false) String startTime,
|
||||
@RequestParam(required = false) String endTime
|
||||
){
|
||||
// 开始时间与结束时间可不传或只传其一
|
||||
List<String> recordList = new ArrayList<>();
|
||||
try {
|
||||
Date startTimeDate = null;
|
||||
Date endTimeDate = null;
|
||||
if (startTime != null ) {
|
||||
startTimeDate = formatter.parse(startTime);
|
||||
}
|
||||
if (endTime != null ) {
|
||||
endTimeDate = formatter.parse(endTime);
|
||||
}
|
||||
|
||||
List<File> filesInTime = videoFileService.getFilesInTime(app, stream, startTimeDate, endTimeDate);
|
||||
if (filesInTime != null && filesInTime.size() > 0) {
|
||||
for (File file : filesInTime) {
|
||||
recordList.add(file.getName());
|
||||
}
|
||||
}
|
||||
PageInfo<String> stringPageInfo = new PageInfo<>(recordList);
|
||||
stringPageInfo.startPage(page, count);
|
||||
return stringPageInfo;
|
||||
} catch (ParseException e) {
|
||||
log.error("错误的开始时间[{}]或结束时间[{}]", startTime, endTime);
|
||||
throw new ControllerException(ErrorCode.ERROR400.getCode(), "错误的开始时间或结束时间, e=" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取视频文件列表
|
||||
* @return
|
||||
*/
|
||||
@GetMapping(value = "/file/listWithDate")
|
||||
@ResponseBody
|
||||
public PageInfo<RecordFile> getRecordListWithDate(@RequestParam int page,
|
||||
@RequestParam int count,
|
||||
@RequestParam String app,
|
||||
@RequestParam String stream,
|
||||
@RequestParam(required = false) String startTime,
|
||||
@RequestParam(required = false) String endTime
|
||||
){
|
||||
|
||||
// 开始时间与结束时间可不传或只传其一
|
||||
List<RecordFile> recordList = new ArrayList<>();
|
||||
try {
|
||||
Date startTimeDate = null;
|
||||
Date endTimeDate = null;
|
||||
if (startTime != null ) {
|
||||
startTimeDate = formatter.parse(startTime);
|
||||
}
|
||||
if (endTime != null ) {
|
||||
endTimeDate = formatter.parse(endTime);
|
||||
}
|
||||
|
||||
List<File> filesInTime = videoFileService.getFilesInTime(app, stream, startTimeDate, endTimeDate);
|
||||
if (filesInTime != null && filesInTime.size() > 0) {
|
||||
for (File file : filesInTime) {
|
||||
recordList.add(RecordFile.instance(app, stream, file.getName(), file.getParentFile().getName()));
|
||||
}
|
||||
}
|
||||
PageInfo<RecordFile> stringPageInfo = new PageInfo<>(recordList);
|
||||
stringPageInfo.startPage(page, count);
|
||||
return stringPageInfo;
|
||||
} catch (ParseException e) {
|
||||
log.error("错误的开始时间[{}]或结束时间[{}]", startTime, endTime);
|
||||
throw new ControllerException(ErrorCode.ERROR400.getCode(), "错误的开始时间或结束时间, e=" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 添加视频裁剪合并任务
|
||||
*/
|
||||
@GetMapping(value = "/file/download/task/add")
|
||||
@ResponseBody
|
||||
public String addTaskForDownload(@RequestParam String app,
|
||||
@RequestParam String stream,
|
||||
@RequestParam(required = false) String startTime,
|
||||
@RequestParam(required = false) String endTime,
|
||||
@RequestParam(required = false) String remoteHost
|
||||
){
|
||||
Date startTimeDate = null;
|
||||
Date endTimeDate = null;
|
||||
try {
|
||||
if (startTime != null ) {
|
||||
startTimeDate = formatter.parse(startTime);
|
||||
}
|
||||
if (endTime != null ) {
|
||||
endTimeDate = formatter.parse(endTime);
|
||||
}
|
||||
} catch (ParseException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
String id = videoFileService.mergeOrCut(app, stream, startTimeDate, endTimeDate, remoteHost);
|
||||
if (id== null) {
|
||||
throw new ControllerException(ErrorCode.ERROR100.getCode(), "可能未找到视频文件");
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询视频裁剪合并任务列表
|
||||
*/
|
||||
@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 taskId,
|
||||
@RequestParam(required = false) Boolean isEnd){
|
||||
List<MergeOrCutTaskInfo> taskList = videoFileService.getTaskListForDownload(isEnd, app, stream, taskId);
|
||||
if (taskList == null) {
|
||||
throw new ControllerException(ErrorCode.ERROR100);
|
||||
}
|
||||
return taskList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 收藏录像(被收藏的录像不会被清理任务清理)
|
||||
*/
|
||||
|
||||
@GetMapping(value = "/file/collection/add")
|
||||
@ResponseBody
|
||||
public void collection(
|
||||
@RequestParam(required = true) String type,
|
||||
@RequestParam(required = true) String app,
|
||||
@RequestParam(required = true) String stream){
|
||||
|
||||
boolean collectionResult = videoFileService.collection(app, stream, type);
|
||||
if (!collectionResult) {
|
||||
throw new ControllerException(ErrorCode.ERROR100);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除收藏录像
|
||||
*/
|
||||
@GetMapping(value = "/file/collection/remove")
|
||||
@ResponseBody
|
||||
public void removeCollection(
|
||||
@RequestParam(required = true) String type,
|
||||
@RequestParam(required = true) String app,
|
||||
@RequestParam(required = true) String stream){
|
||||
|
||||
boolean collectionResult = videoFileService.removeCollection(app, stream, type);
|
||||
if (!collectionResult) {
|
||||
throw new ControllerException(ErrorCode.ERROR100);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 收藏录像列表
|
||||
*/
|
||||
@GetMapping(value = "/file/collection/list")
|
||||
@ResponseBody
|
||||
public List<SignInfo> collectionList(
|
||||
@RequestParam(required = false) String type,
|
||||
@RequestParam(required = false) String app,
|
||||
@RequestParam(required = false) String stream){
|
||||
|
||||
List<SignInfo> signInfos = videoFileService.getCollectionList(app, stream, type);
|
||||
return signInfos;
|
||||
}
|
||||
|
||||
/**
|
||||
* 中止视频裁剪合并任务列表
|
||||
*/
|
||||
@GetMapping(value = "/file/download/task/stop")
|
||||
@ResponseBody
|
||||
public Result<String> stopTaskForDownload(@RequestParam String taskId){
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 磁盘空间查询
|
||||
*/
|
||||
@ResponseBody
|
||||
@GetMapping(value = "/space", produces = "application/json;charset=UTF-8")
|
||||
public SpaceInfo getSpace() {
|
||||
return videoFileService.getSpaceInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加推流的鉴权信息,用于录像存储使用
|
||||
*/
|
||||
@ResponseBody
|
||||
@GetMapping(value = "/addStreamCallInfo", produces = "application/json;charset=UTF-8")
|
||||
@PostMapping(value = "/addStreamCallInfo", produces = "application/json;charset=UTF-8")
|
||||
public void addStreamCallInfo(String app, String stream, String callId) {
|
||||
String key = Constants.STREAM_CALL_INFO + userSettings.getId() + "_" + app + "_" + stream;
|
||||
redisUtil.set(key, callId, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 录像文件的时长
|
||||
*/
|
||||
@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);
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package com.fastbee.record.controller;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.fastbee.record.dto.UserSettings;
|
||||
import com.fastbee.record.service.VideoFileService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
@Slf4j
|
||||
@CrossOrigin
|
||||
@RestController
|
||||
@RequestMapping("/zlmhook")
|
||||
public class ZmlHookController {
|
||||
|
||||
@Autowired
|
||||
private UserSettings userSettings;
|
||||
@Autowired
|
||||
private VideoFileService videoFileService;
|
||||
|
||||
/**
|
||||
* 录制完成的通知, 对用zlm的hook
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@ResponseBody
|
||||
@PostMapping(value = "/on_record_mp4", produces = "application/json;charset=UTF-8")
|
||||
public ResponseEntity<String> onRecordMp4(@RequestBody JSONObject json) {
|
||||
JSONObject ret = new JSONObject();
|
||||
ret.put("code", 0);
|
||||
ret.put("msg", "success");
|
||||
String file_path = json.getString("file_path");
|
||||
|
||||
String app = json.getString("app");
|
||||
String stream = json.getString("stream");
|
||||
log.info("ZLM 录制完成,文件路径:" + file_path);
|
||||
|
||||
if (file_path == null) {
|
||||
return new ResponseEntity<String>(ret.toString(), HttpStatus.OK);
|
||||
}
|
||||
if (userSettings.getRecordDay() <= 0) {
|
||||
log.info("录像保存事件为{}天,直接删除: {}", userSettings.getRecordDay(), file_path);
|
||||
FileUtils.deleteQuietly(new File(file_path));
|
||||
} else {
|
||||
videoFileService.handFile(new File(file_path), app, stream);
|
||||
}
|
||||
|
||||
return new ResponseEntity<String>(ret.toString(), HttpStatus.OK);
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package com.fastbee.record.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;
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package com.fastbee.record.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;
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
package com.fastbee.record.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;
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
package com.fastbee.record.controller.bean;
|
||||
|
||||
public class Result<T> {
|
||||
|
||||
public Result() {
|
||||
}
|
||||
|
||||
public Result(int code, String msg, T data) {
|
||||
this.code = code;
|
||||
this.msg = msg;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
private int code;
|
||||
private String msg;
|
||||
private T data;
|
||||
|
||||
|
||||
public static <T> Result<T> success(T t, String msg) {
|
||||
return new Result<>(ErrorCode.SUCCESS.getCode(), msg, t);
|
||||
}
|
||||
|
||||
public static <T> Result<T> success(T t) {
|
||||
return success(t, ErrorCode.SUCCESS.getMsg());
|
||||
}
|
||||
|
||||
public static <T> Result<T> fail(int code, String msg) {
|
||||
return new Result<>(code, msg, null);
|
||||
}
|
||||
|
||||
public static <T> Result<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;
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package com.fastbee.record.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class MergeOrCutTaskInfo {
|
||||
private String id;
|
||||
private String app;
|
||||
private String stream;
|
||||
private String startTime;
|
||||
private String endTime;
|
||||
private String createTime;
|
||||
private String percentage;
|
||||
private String recordFile;
|
||||
private String downloadFile;
|
||||
private String playFile;
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package com.fastbee.record.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class RecordInfo {
|
||||
private String app;
|
||||
private String stream;
|
||||
private String time;
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package com.fastbee.record.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class SignInfo {
|
||||
private String app;
|
||||
private String stream;
|
||||
private String type;
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package com.fastbee.record.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class SpaceInfo {
|
||||
private long total;
|
||||
private long free;
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package com.fastbee.record.dto;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
|
||||
@Component
|
||||
@Setter
|
||||
@Getter
|
||||
public class UserSettings {
|
||||
|
||||
@Value("${userSettings.id}")
|
||||
private String id;
|
||||
|
||||
@Value("${userSettings.record}")
|
||||
private String record;
|
||||
|
||||
@Value("${userSettings.recordDay:7}")
|
||||
private int recordDay;
|
||||
|
||||
@Value("${userSettings.recordTempDay:-1}")
|
||||
private int recordTempDay;
|
||||
|
||||
@Value("${userSettings.ffmpeg}")
|
||||
private String ffmpeg;
|
||||
|
||||
@Value("${userSettings.ffprobe}")
|
||||
private String ffprobe;
|
||||
|
||||
@Value("${userSettings.threads:2}")
|
||||
private int threads;
|
||||
}
|
@ -0,0 +1,124 @@
|
||||
package com.fastbee.record.service;
|
||||
|
||||
import net.bramp.ffmpeg.FFmpeg;
|
||||
import net.bramp.ffmpeg.FFmpegExecutor;
|
||||
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 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 com.fastbee.record.dto.UserSettings;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Component
|
||||
public class FFmpegExecUtils implements InitializingBean{
|
||||
private final static Logger logger = LoggerFactory.getLogger(FFmpegExecUtils.class);
|
||||
@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("录像程序启动成功。 \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) {
|
||||
String[] split = file.getName().split("-");
|
||||
if (split.length != 3) continue;
|
||||
String durationStr = split[2].replace(".mp4", "");
|
||||
Double duration = Double.parseDouble(durationStr)/1000;
|
||||
bw.write("file " + file.getAbsolutePath());
|
||||
bw.newLine();
|
||||
durationAll += duration;
|
||||
}
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,151 @@
|
||||
package com.fastbee.record.service;
|
||||
|
||||
import com.fastbee.record.utils.Constants;
|
||||
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 com.fastbee.record.dto.MergeOrCutTaskInfo;
|
||||
import com.fastbee.record.dto.UserSettings;
|
||||
import com.fastbee.record.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.getRecord() == null) {
|
||||
return;
|
||||
}
|
||||
int recordDay = userSettings.getRecordDay();
|
||||
Date lastDate=new Date();
|
||||
Calendar lastCalendar = Calendar.getInstance();
|
||||
if (recordDay > 0) {
|
||||
lastCalendar.setTime(lastDate);
|
||||
lastCalendar.add(Calendar.DAY_OF_MONTH, 0 - recordDay);
|
||||
lastDate = lastCalendar.getTime();
|
||||
}
|
||||
|
||||
logger.info("[录像巡查]移除 {} 之前的文件", formatter.format(lastDate));
|
||||
File recordFileDir = new File(userSettings.getRecord());
|
||||
if (recordFileDir.canWrite()) {
|
||||
List<File> appList = videoFileService.getAppList(false);
|
||||
if (appList != null && appList.size() > 0) {
|
||||
for (File appFile : appList) {
|
||||
if ("download.html".equals(appFile.getName())) {
|
||||
continue;
|
||||
}
|
||||
List<File> streamList = videoFileService.getStreamList(appFile, false);
|
||||
if (streamList != null && streamList.size() > 0) {
|
||||
for (File streamFile : streamList) {
|
||||
// 带有sig标记文件的为收藏文件,不被自动清理任务移除
|
||||
File[] signFiles = streamFile.listFiles((File dir, String name) -> {
|
||||
File currentFile = new File(dir.getAbsolutePath() + File.separator + name);
|
||||
return currentFile.isFile() && name.endsWith(".sign");
|
||||
});
|
||||
if (signFiles != null && signFiles.length > 0) {
|
||||
continue;
|
||||
}
|
||||
List<File> dateList = videoFileService.getDateList(streamFile, null, null, false);
|
||||
if (dateList != null && dateList.size() > 0) {
|
||||
for (File dateFile : dateList) {
|
||||
try {
|
||||
Date parse = formatter.parse(dateFile.getName());
|
||||
if (parse.before(lastDate)) {
|
||||
boolean result = FileUtils.deleteQuietly(dateFile);
|
||||
if (result) {
|
||||
logger.info("[录像巡查]成功移除 {} ", dateFile.getAbsolutePath());
|
||||
}else {
|
||||
logger.info("[录像巡查]移除失败 {} ", dateFile.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
} catch (ParseException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (streamFile.listFiles() == null || streamFile.listFiles().length == 0) {
|
||||
boolean result = FileUtils.deleteQuietly(streamFile);
|
||||
if (result) {
|
||||
logger.info("[录像巡查]成功移除 {} ", streamFile.getAbsolutePath());
|
||||
}else {
|
||||
logger.info("[录像巡查]移除失败 {} ", streamFile.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (appFile.listFiles() == null || appFile.listFiles().length == 0) {
|
||||
boolean result = FileUtils.deleteQuietly(appFile);
|
||||
if (result) {
|
||||
logger.info("[录像巡查]成功移除 {} ", appFile.getAbsolutePath());
|
||||
}else {
|
||||
logger.info("[录像巡查]移除失败 {} ", appFile.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 清理任务临时文件
|
||||
int recordTempDay = userSettings.getRecordTempDay();
|
||||
Date lastTempDate = new Date();
|
||||
Calendar lastTempCalendar = Calendar.getInstance();
|
||||
lastTempCalendar.setTime(lastTempDate);
|
||||
lastTempCalendar.add(Calendar.DAY_OF_MONTH, 0 - recordTempDay);
|
||||
lastTempDate = lastTempCalendar.getTime();
|
||||
logger.info("[录像巡查]移除合并任务临时文件 {} 之前的文件", formatter.format(lastTempDate));
|
||||
File recordTempFile = new File(userSettings.getRecord() + "recordTemp");
|
||||
if (recordTempFile.exists() && recordTempFile.isDirectory() && recordTempFile.canWrite()) {
|
||||
File[] tempFiles = recordTempFile.listFiles();
|
||||
for (File tempFile : tempFiles) {
|
||||
if (tempFile.isDirectory() && new Date(tempFile.lastModified()).before(lastTempDate)) {
|
||||
boolean result = FileUtils.deleteQuietly(tempFile);
|
||||
if (result) {
|
||||
logger.info("[录像巡查]成功移除合并任务临时文件 {} ", tempFile.getAbsolutePath());
|
||||
}else {
|
||||
logger.info("[录像巡查]合并任务临时文件移除失败 {} ", tempFile.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 清理redis记录
|
||||
String key = String.format("%S_%S_*_*_*", Constants.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.isEmpty(mergeOrCutTaskInfo.getCreateTime())
|
||||
|| simpleDateFormatForTime.parse(mergeOrCutTaskInfo.getCreateTime()).before(lastTempDate)) {
|
||||
redisUtil.del(taskKey);
|
||||
}
|
||||
} catch (ParseException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,619 @@
|
||||
package com.fastbee.record.service;
|
||||
|
||||
import com.fastbee.record.dto.*;
|
||||
import com.fastbee.record.utils.Constants;
|
||||
import net.bramp.ffmpeg.FFprobe;
|
||||
import net.bramp.ffmpeg.probe.FFmpegProbeResult;
|
||||
import net.bramp.ffmpeg.progress.Progress;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.DigestUtils;
|
||||
import com.fastbee.record.utils.RedisUtil;
|
||||
import com.fastbee.record.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.getRecord());
|
||||
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.getRecord());
|
||||
SpaceInfo spaceInfo = new SpaceInfo();
|
||||
spaceInfo.setFree(recordFile.getFreeSpace());
|
||||
spaceInfo.setTotal(recordFile.getTotalSpace());
|
||||
return spaceInfo;
|
||||
}
|
||||
|
||||
public List<File> getStreamList(String app, Boolean sort) {
|
||||
File appFile = new File(userSettings.getRecord() + File.separator + app);
|
||||
return getStreamList(appFile, sort);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对视频文件重命名, 00:00:00-00:00:00
|
||||
*
|
||||
* @param file
|
||||
* @throws ParseException
|
||||
*/
|
||||
public void handFile(File file, String app, String stream) {
|
||||
FFprobe ffprobe = ffmpegExecUtils.getFfprobe();
|
||||
if (file.exists() && file.isFile() && !file.getName().startsWith(".")
|
||||
&& file.getName().endsWith(".mp4") && file.getName().indexOf(":") < 0) {
|
||||
try {
|
||||
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss");
|
||||
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss");
|
||||
//获取录像时间长度
|
||||
FFmpegProbeResult in = ffprobe.probe(file.getAbsolutePath());
|
||||
double duration = in.getFormat().duration * 1000;
|
||||
long durationLong = new Double(duration).longValue();
|
||||
//获取录像开始时间和结束时间
|
||||
File dateFile = new File(file.getParent());
|
||||
String endTimeStr = file.getName().replace(".mp4", "");
|
||||
long startTime = formatter.parse(dateFile.getName() + " " + endTimeStr).getTime();
|
||||
long endTime = startTime + durationLong;
|
||||
endTime = endTime - endTime % 1000;
|
||||
//获取缓存的callId
|
||||
String key = Constants.STREAM_CALL_INFO + userSettings.getId() + "_" + app + "_" + stream;
|
||||
String callId = (String) redisUtil.get(key);
|
||||
String streamNew = (callId == null ? stream : stream + "_" + callId);
|
||||
//拼接文件夹路径
|
||||
File newPath = new File(userSettings.getRecord() + File.separator + app + File.separator + streamNew + File.separator + DateUtils.getDateStr(new Date(startTime)));
|
||||
if (!newPath.exists()) {
|
||||
newPath.mkdirs();
|
||||
}
|
||||
logger.info("[开始时间]:{},[结束时间]:{},[文件长度]:{}", simpleDateFormat.format(startTime), simpleDateFormat.format(endTime), durationLong);
|
||||
//录像文件名
|
||||
String newName = newPath.getAbsolutePath() + File.separator + simpleDateFormat.format(startTime) + "-" + simpleDateFormat.format(endTime) + "-" + durationLong + ".mp4";
|
||||
file.renameTo(new File(newName));
|
||||
logger.info("[处理文件] {} 重命名为: {}", file.getName(), newName);
|
||||
} catch (IOException e) {
|
||||
logger.warn("文件可能以损坏[{}]", file.getAbsolutePath());
|
||||
} catch (ParseException e) {
|
||||
logger.error("时间格式化失败", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<RecordInfo> getList() {
|
||||
|
||||
List<RecordInfo> result = new ArrayList<>();
|
||||
|
||||
List<File> appList = getAppList(true);
|
||||
if (appList != null && appList.size() > 0) {
|
||||
for (File appFile : appList) {
|
||||
if (appFile.getName().equals("snap")) {
|
||||
continue;
|
||||
}
|
||||
if (appFile.isDirectory()) {
|
||||
List<File> streamList = getStreamList(appFile.getName(), true);
|
||||
if (streamList != null && streamList.size() > 0) {
|
||||
for (File streamFile : streamList) {
|
||||
RecordInfo data = new RecordInfo();
|
||||
data.setApp(appFile.getName());
|
||||
data.setStream(streamFile.getName());
|
||||
|
||||
BasicFileAttributes bAttributes = null;
|
||||
try {
|
||||
bAttributes = Files.readAttributes(streamFile.toPath(),
|
||||
BasicFileAttributes.class);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
assert bAttributes != null;
|
||||
data.setTime(simpleDateFormatForTime.format(new Date(bAttributes.lastModifiedTime().toMillis())));
|
||||
result.add(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
result.sort((RecordInfo f1, RecordInfo f2) -> {
|
||||
Date time1 = null;
|
||||
Date time2 = null;
|
||||
try {
|
||||
time1 = simpleDateFormatForTime.parse(f1.getTime());
|
||||
time2 = simpleDateFormatForTime.parse(f2.getTime());
|
||||
} catch (ParseException e) {
|
||||
logger.error("时间格式化失败", e.getMessage());
|
||||
}
|
||||
assert time1 != null;
|
||||
return time1.compareTo(time2) * -1;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取制定推流的指定时间段内的推流
|
||||
*
|
||||
* @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 HH:mm:ss");
|
||||
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.info("获取[app: {}, stream: {}, statime: {}, endTime: {}]的视频", app, stream,
|
||||
startTimeStr, endTimeStr);
|
||||
|
||||
File recordFile = new File(userSettings.getRecord());
|
||||
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) -> {
|
||||
boolean filterResult = true;
|
||||
File currentFile = new File(dir + File.separator + name);
|
||||
//文件格式:HH:mm:ss-HH:mm:ss-13204.mp4
|
||||
if (currentFile.isFile() && name.contains(":") && name.endsWith(".mp4")
|
||||
&& !name.startsWith(".") && currentFile.length() > 0) {
|
||||
|
||||
String[] timeArray = name.split("-");
|
||||
if (timeArray.length == 3) {
|
||||
// 构建文件的开始和结束时间字符串
|
||||
String fileStartTimeStr = dateFile.getName() + " " + timeArray[0];
|
||||
String fileEndTimeStr = dateFile.getName() + " " + timeArray[1];
|
||||
try {
|
||||
// 如果有开始时间,则判断文件开始时间是否在指定开始时间之后,或文件开始时间在指定开始时间之前且文件结束时间在指定开始时间之后
|
||||
if (startTime != null) {
|
||||
filterResult = filterResult && (formatter.parse(fileStartTimeStr).after(startTime) || (formatter.parse(fileStartTimeStr).before(startTime) && formatter.parse(fileEndTimeStr).after(startTime)));
|
||||
}
|
||||
// 如果有结束时间,则判断文件结束时间是否在指定结束时间之前,或文件结束时间在指定结束时间之后且文件开始时间在指定结束时间之前
|
||||
if (endTime != null) {
|
||||
filterResult = filterResult && (formatter.parse(fileEndTimeStr).before(endTime) || (formatter.parse(fileEndTimeStr).after(endTime) && formatter.parse(fileStartTimeStr).before(endTime)));
|
||||
}
|
||||
} catch (ParseException e) {
|
||||
logger.error("过滤视频文件时异常: {}-{}", name, e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
filterResult = false;
|
||||
}
|
||||
return filterResult;
|
||||
});
|
||||
|
||||
List<File> fileList = Arrays.asList(files);
|
||||
result.addAll(fileList);
|
||||
}
|
||||
}
|
||||
|
||||
if (result.size() > 0) {
|
||||
result.sort((File f1, File f2) -> {
|
||||
int sortResult = 0;
|
||||
String[] timeArray1 = f1.getName().split("-");
|
||||
String[] timeArray2 = f2.getName().split("-");
|
||||
if (timeArray1.length == 3 && timeArray2.length == 3) {
|
||||
File dateFile1 = f1.getParentFile();
|
||||
File dateFile2 = f2.getParentFile();
|
||||
String fileStartTimeStr1 = dateFile1.getName() + " " + timeArray1[0];
|
||||
String fileStartTimeStr2 = dateFile2.getName() + " " + timeArray2[0];
|
||||
try {
|
||||
sortResult = formatter.parse(fileStartTimeStr1).compareTo(formatter.parse(fileStartTimeStr2));
|
||||
} catch (ParseException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
return sortResult;
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
public String mergeOrCut(String app, String stream, Date startTime, Date endTime, String remoteHost) {
|
||||
List<File> filesInTime = this.getFilesInTime(app, stream, startTime, endTime);
|
||||
if (filesInTime == null || filesInTime.size() == 0) {
|
||||
logger.info("此时间段未未找到视频文件, {}/{} {}->{}", app, stream,
|
||||
startTime == null ? null : DateUtils.getDateTimeStr(startTime),
|
||||
endTime == null ? null : DateUtils.getDateTimeStr(endTime));
|
||||
return null;
|
||||
}
|
||||
String taskId = DigestUtils.md5DigestAsHex(String.valueOf(System.currentTimeMillis()).getBytes());
|
||||
logger.info("[录像合并] 开始合并,APP:{}, STREAM: {}, 任务ID:{}", app, stream, taskId);
|
||||
String destDir = "recordTemp" + File.separator + taskId + File.separator + app;
|
||||
File recordFile = new File(userSettings.getRecord() + destDir);
|
||||
if (!recordFile.exists()) {
|
||||
recordFile.mkdirs();
|
||||
}
|
||||
MergeOrCutTaskInfo mergeOrCutTaskInfo = new MergeOrCutTaskInfo();
|
||||
mergeOrCutTaskInfo.setId(taskId);
|
||||
mergeOrCutTaskInfo.setApp(app);
|
||||
mergeOrCutTaskInfo.setStream(stream);
|
||||
mergeOrCutTaskInfo.setCreateTime(simpleDateFormatForTime.format(System.currentTimeMillis()));
|
||||
if (startTime != null) {
|
||||
mergeOrCutTaskInfo.setStartTime(simpleDateFormatForTime.format(startTime));
|
||||
} else {
|
||||
String startTimeInFile = filesInTime.get(0).getParentFile().getName() + " "
|
||||
+ filesInTime.get(0).getName().split("-")[0];
|
||||
mergeOrCutTaskInfo.setStartTime(startTimeInFile);
|
||||
}
|
||||
if (endTime != null) {
|
||||
mergeOrCutTaskInfo.setEndTime(simpleDateFormatForTime.format(endTime));
|
||||
} else {
|
||||
String endTimeInFile = filesInTime.get(filesInTime.size() - 1).getParentFile().getName() + " "
|
||||
+ filesInTime.get(filesInTime.size() - 1).getName().split("-")[1];
|
||||
mergeOrCutTaskInfo.setEndTime(endTimeInFile);
|
||||
}
|
||||
if (filesInTime.size() == 1) {
|
||||
// 文件只有一个则不合并,直接复制过去
|
||||
mergeOrCutTaskInfo.setPercentage("1");
|
||||
// 处理文件路径
|
||||
String recordFileResultPath = recordFile.getAbsolutePath() + File.separator + stream + ".mp4";
|
||||
Path relativize = Paths.get(userSettings.getRecord()).relativize(Paths.get(recordFileResultPath));
|
||||
try {
|
||||
Files.copy(filesInTime.get(0).toPath(), Paths.get(recordFileResultPath));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
logger.info("[录像合并] 失败,APP:{}, STREAM: {}, 任务ID:{}", app, stream, taskId);
|
||||
return taskId;
|
||||
}
|
||||
mergeOrCutTaskInfo.setRecordFile(relativize.toString());
|
||||
if (remoteHost != null) {
|
||||
mergeOrCutTaskInfo.setDownloadFile(remoteHost + "/download.html?url=download/" + relativize);
|
||||
mergeOrCutTaskInfo.setPlayFile(remoteHost + "/download/" + relativize);
|
||||
}
|
||||
String key = String.format("%S_%S_%S_%S_%S", Constants.MERGEORCUT, userSettings.getId(), mergeOrCutTaskInfo.getApp(), mergeOrCutTaskInfo.getStream(), mergeOrCutTaskInfo.getId());
|
||||
redisUtil.set(key, mergeOrCutTaskInfo);
|
||||
logger.info("[录像合并] 合并完成,APP:{}, STREAM: {}, 任务ID:{}", app, stream, taskId);
|
||||
} else {
|
||||
ffmpegExecUtils.mergeOrCutFile(filesInTime, recordFile, stream, (status, percentage, result) -> {
|
||||
// 发出redis通知
|
||||
if (status.equals(Progress.Status.END.name())) {
|
||||
mergeOrCutTaskInfo.setPercentage("1");
|
||||
|
||||
// 处理文件路径
|
||||
Path relativize = Paths.get(userSettings.getRecord()).relativize(Paths.get(result));
|
||||
mergeOrCutTaskInfo.setRecordFile(relativize.toString());
|
||||
if (remoteHost != null) {
|
||||
mergeOrCutTaskInfo.setDownloadFile(remoteHost + "/download.html?url=" + relativize);
|
||||
mergeOrCutTaskInfo.setPlayFile(remoteHost + "/" + relativize);
|
||||
}
|
||||
logger.info("[录像合并] 合并完成,APP:{}, STREAM: {}, 任务ID:{}", app, stream, taskId);
|
||||
} else {
|
||||
mergeOrCutTaskInfo.setPercentage(percentage + "");
|
||||
}
|
||||
String key = String.format("%S_%S_%S_%S_%S", Constants.MERGEORCUT, userSettings.getId(), mergeOrCutTaskInfo.getApp(), mergeOrCutTaskInfo.getStream(), mergeOrCutTaskInfo.getId());
|
||||
redisUtil.set(key, mergeOrCutTaskInfo);
|
||||
});
|
||||
}
|
||||
|
||||
return taskId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定时间的日期文件夹
|
||||
*
|
||||
* @param app
|
||||
* @param stream
|
||||
* @param year
|
||||
* @param month
|
||||
* @return
|
||||
*/
|
||||
public List<File> getDateList(String app, String stream, Integer year, Integer month, Boolean sort) {
|
||||
File recordFile = new File(userSettings.getRecord());
|
||||
File streamFile = new File(recordFile.getAbsolutePath() + File.separator + app + File.separator + stream);
|
||||
return getDateList(streamFile, year, month, sort);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
});
|
||||
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(Boolean idEnd, String app, String stream, String taskId) {
|
||||
ArrayList<MergeOrCutTaskInfo> result = new ArrayList<>();
|
||||
if (app == null) {
|
||||
app = "*";
|
||||
}
|
||||
if (stream == null) {
|
||||
stream = "*";
|
||||
}
|
||||
if (taskId == null) {
|
||||
taskId = "*";
|
||||
}
|
||||
List<Object> taskCatch = redisUtil.scan(String.format("%S_%S_%S_%S_%S", Constants.MERGEORCUT,
|
||||
userSettings.getId(), app, stream, taskId));
|
||||
for (int i = 0; i < taskCatch.size(); i++) {
|
||||
String keyItem = taskCatch.get(i).toString();
|
||||
MergeOrCutTaskInfo mergeOrCutTaskInfo = (MergeOrCutTaskInfo) redisUtil.get(keyItem);
|
||||
if (mergeOrCutTaskInfo != null && mergeOrCutTaskInfo.getPercentage() != null) {
|
||||
if (idEnd != null) {
|
||||
if (idEnd) {
|
||||
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.getStartTime()).compareTo(simpleDateFormatForTime.parse(m2.getStartTime()));
|
||||
}
|
||||
if (sortResult == 0) {
|
||||
sortResult = simpleDateFormatForTime.parse(m1.getEndTime()).compareTo(simpleDateFormatForTime.parse(m2.getEndTime()));
|
||||
}
|
||||
} catch (ParseException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return sortResult * -1;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean stopTask(String taskId) {
|
||||
// Runnable task = taskList.get(taskId);
|
||||
// boolean result = false;
|
||||
// if (task != null) {
|
||||
// processThreadPool.remove(task);
|
||||
// taskList.remove(taskId);
|
||||
// List<Object> taskCatch = redisUtil.scan(String.format("%S_*_*_%S", keyStr, taskId));
|
||||
// if (taskCatch.size() == 1) {
|
||||
// redisUtil.del((String) taskCatch.get(0));
|
||||
// result = true;
|
||||
// }
|
||||
// }
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean collection(String app, String stream, String type) {
|
||||
File streamFile = new File(userSettings.getRecord() + 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.getRecord() + 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;
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
package com.fastbee.record.utils;
|
||||
|
||||
public class Constants {
|
||||
public final static String STREAM_CALL_INFO = "STREAM_CALL_INFO_";
|
||||
public final static String MERGEORCUT = "MERGEORCUT_";
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package com.fastbee.record.utils;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Date;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
package com.fastbee.record.utils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class PageInfo<T> {
|
||||
//当前页
|
||||
private int pageNum;
|
||||
//每页的数量
|
||||
private int pageSize;
|
||||
//当前页的数量
|
||||
private int size;
|
||||
//总页数
|
||||
private int pages;
|
||||
//总数
|
||||
private int total;
|
||||
|
||||
private List<T> resultData;
|
||||
|
||||
private List<T> list;
|
||||
|
||||
public PageInfo(List<T> resultData) {
|
||||
this.resultData = resultData;
|
||||
}
|
||||
|
||||
public void startPage(int page, int count) {
|
||||
if (count <= 0) count = 10;
|
||||
if (page <= 0) page = 1;
|
||||
this.pageNum = page;
|
||||
this.pageSize = count;
|
||||
this.total = resultData.size();
|
||||
|
||||
this.pages = total%count == 0 ? total/count : total/count + 1;
|
||||
int fromIndx = (page - 1) * count;
|
||||
if ( fromIndx > this.total - 1) {
|
||||
this.list = new ArrayList<>();
|
||||
this.size = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
int toIndx = page * count;
|
||||
if (toIndx > this.total) {
|
||||
toIndx = this.total;
|
||||
}
|
||||
this.list = this.resultData.subList(fromIndx, toIndx);
|
||||
this.size = this.list.size();
|
||||
}
|
||||
|
||||
public int getPageNum() {
|
||||
return pageNum;
|
||||
}
|
||||
|
||||
public void setPageNum(int pageNum) {
|
||||
this.pageNum = pageNum;
|
||||
}
|
||||
|
||||
public int getPageSize() {
|
||||
return pageSize;
|
||||
}
|
||||
|
||||
public void setPageSize(int pageSize) {
|
||||
this.pageSize = pageSize;
|
||||
}
|
||||
|
||||
public int getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public void setSize(int size) {
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public int getPages() {
|
||||
return pages;
|
||||
}
|
||||
|
||||
public void setPages(int pages) {
|
||||
this.pages = pages;
|
||||
}
|
||||
|
||||
public int getTotal() {
|
||||
return total;
|
||||
}
|
||||
|
||||
public void setTotal(int total) {
|
||||
this.total = total;
|
||||
}
|
||||
|
||||
public List<T> getList() {
|
||||
return list;
|
||||
}
|
||||
|
||||
public void setList(List<T> list) {
|
||||
this.list = list;
|
||||
}
|
||||
}
|
@ -0,0 +1,698 @@
|
||||
package com.fastbee.record.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);
|
||||
}
|
||||
/**
|
||||
* 查询集合中指定顺序的值和score,0, -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) {
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
59
fastbee-record/src/main/resources/all-application.yml
Normal file
59
fastbee-record/src/main/resources/all-application.yml
Normal file
@ -0,0 +1,59 @@
|
||||
spring:
|
||||
# REDIS数据库配置
|
||||
redis:
|
||||
# [必须修改] Redis服务器IP, REDIS安装在本机的,使用127.0.0.1
|
||||
host: 127.0.0.1
|
||||
# [必须修改] 端口号
|
||||
port: 6379
|
||||
# [可选] 数据库 DB
|
||||
database: 8
|
||||
# [可选] 访问密码,若你的redis服务器没有设置密码,就不需要用密码去连接
|
||||
password:
|
||||
# [可选] 超时时间
|
||||
timeout: 10000
|
||||
|
||||
# [可选] 监听的HTTP端口, 网页和接口调用都是这个端口
|
||||
# 您需要使用实际的证书名称替换domain_name.jks
|
||||
# 证书获取参考文档:https://help.aliyun.com/zh/ssl-certificate/user-guide/enable-https-on-spring-boot
|
||||
server:
|
||||
port: 18081
|
||||
# HTTPS配置, 默认不开启
|
||||
ssl:
|
||||
# 是否开启HTTPS访问 默认关闭
|
||||
enabled: false
|
||||
# enabled: true
|
||||
# 证书文件路径,您需要使用实际的证书名称替换domain_name.jks。
|
||||
key-store: classpath:fastbee.online.jks
|
||||
# 证书密码 修改为对应密码
|
||||
key-store-password: fastbee
|
||||
# 证书类型, 默认为jks,根据实际修改
|
||||
key-store-type: JKS
|
||||
|
||||
# [根据业务需求配置]
|
||||
user-settings:
|
||||
# [可选 ] zlm配置的录像路径,不配置则使用当前目录下的record目录 即: ./record
|
||||
record: /opt/media/bin/www
|
||||
# [可选 ] 录像保存时长(单位: 天)每天晚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
|
||||
|
||||
|
||||
# [可选] 日志配置, 一般不需要改
|
||||
logging:
|
||||
file:
|
||||
name: logs/record.log
|
||||
max-history: 30
|
||||
max-size: 10MB
|
||||
total-size-cap: 300MB
|
||||
level:
|
||||
root: WARN
|
||||
top:
|
||||
panll:
|
||||
assist: info
|
60
fastbee-record/src/main/resources/application-dev.yml
Normal file
60
fastbee-record/src/main/resources/application-dev.yml
Normal file
@ -0,0 +1,60 @@
|
||||
spring:
|
||||
# REDIS数据库配置
|
||||
redis:
|
||||
# [必须修改] Redis服务器IP, REDIS安装在本机的,使用127.0.0.1
|
||||
host: 127.0.0.1
|
||||
# [必须修改] 端口号
|
||||
port: 6379
|
||||
# [可选] 数据库 DB
|
||||
database: 8
|
||||
# [可选] 访问密码,若你的redis服务器没有设置密码,就不需要用密码去连接
|
||||
password:
|
||||
# [可选] 超时时间
|
||||
timeout: 10000
|
||||
|
||||
# [可选] 监听的HTTP端口, 网页和接口调用都是这个端口
|
||||
# 您需要使用实际的证书名称替换domain_name.jks
|
||||
# 证书获取参考文档:https://help.aliyun.com/zh/ssl-certificate/user-guide/enable-https-on-spring-boot
|
||||
server:
|
||||
port: 18081
|
||||
# HTTPS配置, 默认不开启
|
||||
ssl:
|
||||
# 是否开启HTTPS访问 默认关闭
|
||||
enabled: false
|
||||
# enabled: true
|
||||
# 证书文件路径,您需要使用实际的证书名称替换domain_name.jks。
|
||||
key-store: classpath:fastbee.online.jks
|
||||
# 证书密码 修改为对应密码
|
||||
key-store-password: fastbee
|
||||
# 证书类型, 默认为jks,根据实际修改
|
||||
key-store-type: JKS
|
||||
|
||||
# [根据业务需求配置]
|
||||
userSettings:
|
||||
# [可选 ] zlm配置的录像路径,
|
||||
record: /opt/media/bin/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/record.log
|
||||
max-history: 30
|
||||
max-size: 10MB
|
||||
total-size-cap: 300MB
|
||||
level:
|
||||
root: WARN
|
||||
com:
|
||||
fastbee:
|
||||
record: info
|
75
fastbee-record/src/main/resources/application-prod.yml
Normal file
75
fastbee-record/src/main/resources/application-prod.yml
Normal file
@ -0,0 +1,75 @@
|
||||
spring:
|
||||
# REDIS数据库配置
|
||||
redis:
|
||||
# [可选] 超时时间
|
||||
timeout: 10000
|
||||
# 以下为单机配置
|
||||
# [必须修改] Redis服务器IP, REDIS安装在本机的,使用127.0.0.1
|
||||
host: redis
|
||||
# [必须修改] 端口号
|
||||
port: 6379
|
||||
# [可选] 数据库 DB
|
||||
database: 0
|
||||
# [可选] 访问密码,若你的redis服务器没有设置密码,就不需要用密码去连接
|
||||
password: fastbee
|
||||
# 以下为集群配置
|
||||
# cluster:
|
||||
# nodes: 192.168.1.242:7001
|
||||
# password: 4767cb971b40a1300fa09b7f87b09d1c
|
||||
|
||||
# [可选] 监听的HTTP端口, 网页和接口调用都是这个端口
|
||||
# 您需要使用实际的证书名称替换domain_name.jks
|
||||
# 证书获取参考文档:https://help.aliyun.com/zh/ssl-certificate/user-guide/enable-https-on-spring-boot
|
||||
server:
|
||||
port: 18081
|
||||
servlet:
|
||||
context-path: / # 应用的访问路径
|
||||
# HTTPS配置, 默认不开启
|
||||
ssl:
|
||||
# 是否开启HTTPS访问 默认关闭
|
||||
enabled: false
|
||||
# enabled: true
|
||||
# 证书文件路径,您需要使用实际的证书名称替换domain_name.jks。
|
||||
key-store: classpath:fastbee.online.jks
|
||||
# 证书密码 修改为对应密码
|
||||
key-store-password: fastbee
|
||||
# 证书类型, 默认为jks,根据实际修改
|
||||
key-store-type: JKS
|
||||
|
||||
# [根据业务需求配置]
|
||||
userSettings:
|
||||
# [必选 ] 服务ID
|
||||
id: 334533
|
||||
# [必选 ] zlm配置的录像路径
|
||||
record: /opt/media/bin/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
|
||||
|
||||
# MyBatis配置
|
||||
mybatis:
|
||||
typeAliasesPackage: com.fastbee.oss.domain # 搜索指定包别名
|
||||
mapperLocations: classpath*:mapper/**/*Mapper.xml # 配置mapper的扫描,找到所有的mapper.xml映射文件
|
||||
configLocation: classpath:mybatis/mybatis-config.xml # 加载全局的配置文件
|
||||
|
||||
swagger-ui:
|
||||
|
||||
# [可选] 日志配置, 一般不需要改
|
||||
logging:
|
||||
file:
|
||||
name: logs/record.log
|
||||
max-history: 30
|
||||
max-size: 10MB
|
||||
total-size-cap: 300MB
|
||||
level:
|
||||
root: WARN
|
||||
com:
|
||||
fastbee:
|
||||
record: info
|
3
fastbee-record/src/main/resources/application.yml
Normal file
3
fastbee-record/src/main/resources/application.yml
Normal file
@ -0,0 +1,3 @@
|
||||
spring:
|
||||
profiles:
|
||||
active: prod
|
BIN
fastbee-record/src/main/resources/fastbee.online.jks
Normal file
BIN
fastbee-record/src/main/resources/fastbee.online.jks
Normal file
Binary file not shown.
20
fastbee-record/src/main/resources/mybatis/mybatis-config.xml
Normal file
20
fastbee-record/src/main/resources/mybatis/mybatis-config.xml
Normal file
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE configuration
|
||||
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-config.dtd">
|
||||
<configuration>
|
||||
<!-- 全局参数 -->
|
||||
<settings>
|
||||
<!-- 使全局的映射器启用或禁用缓存 -->
|
||||
<setting name="cacheEnabled" value="true" />
|
||||
<!-- 允许JDBC 支持自动生成主键 -->
|
||||
<setting name="useGeneratedKeys" value="true" />
|
||||
<!-- 配置默认的执行器.SIMPLE就是普通执行器;REUSE执行器会重用预处理语句(prepared statements);BATCH执行器将重用语句并执行批量更新 -->
|
||||
<setting name="defaultExecutorType" value="SIMPLE" />
|
||||
<!-- 指定 MyBatis 所用日志的具体实现 -->
|
||||
<setting name="logImpl" value="SLF4J" />
|
||||
<!-- 使用驼峰命名法转换字段 -->
|
||||
<!-- <setting name="mapUnderscoreToCamelCase" value="true"/> -->
|
||||
</settings>
|
||||
|
||||
</configuration>
|
25
fastbee-record/src/main/resources/static/download.html
Normal file
25
fastbee-record/src/main/resources/static/download.html
Normal 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>
|
Reference in New Issue
Block a user