前言
哪一天我心血来潮,想把我儿子学校的摄像头视频流录制下来,并保存到云盘上,这样我就可以在有空的时候看看我儿子在学校干嘛。想到么就干,当时花了一些时间开发了一个后端服务,通过数据库配置录制参数,以后的设想是能够通过页面去配置,能够自动捕获直播视频流,这还得要求自己先学会vue,所以还得缓缓。
实现
技术栈:Spring Boot、Webflux、r2dbc、javacv
架构图:
流程很简单,主要还是要用到JavaCV从视频流里捕获视频,先报错到本地,然后有一个定时任务会定时去检测目录内是否有新生成的文件,有就上传到配置的云盘(百度云)。
1、创建pom
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.6.4</version> <relativePath /> <!-- lookup parent from repository --> </parent> <groupId>net.178le</groupId> <artifactId>video-cloud-record</artifactId> <version>0.0.1-SNAPSHOT</version> <name>video-cloud-record</name> <description>视频云录制</description> <properties> <java.version>1.8</java.version> </properties> <dependencies>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-r2dbc</artifactId> </dependency> <dependency> <groupId>dev.miku</groupId> <artifactId>r2dbc-mysql</artifactId> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.7.22</version> </dependency> <dependency> <groupId>org.bytedeco</groupId> <artifactId>javacv-platform</artifactId> <version>1.4.4</version> </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpcore</artifactId> <version>4.4.10</version> </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.6</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>io.projectreactor</groupId> <artifactId>reactor-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <finalName>video-cloud-record</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build>
</project>
2、定时异常信息
package net.video.record.config;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;import lombok.extern.slf4j.Slf4j;
/**
@desc 全局异常捕捉并转换异常
*/
@Slf4j
@RestControllerAdvice(basePackages = "net.video.record")
public class GlobalExceptionHandler {@ExceptionHandler(Exception.class)
public Result<String> handleException(Exception e) {
log.error("{}", e);
return Result.error("", e.getMessage());
}
}
3、统一结果集
package net.video.record.config;
import cn.hutool.core.util.StrUtil;
import lombok.AllArgsConstructor;
import lombok.Data;@Data
@AllArgsConstructor
public class Result<T> {private String code; private T data; private String msg; public static <T> Result<T> ok(T data) { return new Result<T>("0", data, ""); } public static <T> Result<T> error(String code, String msg) { code = StrUtil.isEmpty(code)? "500" : code; return new Result<T>(code, null, msg); }
}
4、定义两个Model
TaskList 用来保存用户相关的录制任务
package net.video.record.entity.model;
import java.time.LocalDateTime;
import java.util.Date;import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Table;import lombok.Data;
@Data
@Table("task_list")
public class TaskList {@Id private Integer id; private String name; private String streamUrl; private Integer userId; private Integer status; private Integer delFlag; private LocalDateTime createTime; private LocalDateTime modifyTime; private String runRule; private LocalDateTime lastRunTime; private Integer recordTime; private Integer segTime;
}
User 定义用户信息,保存了用过相关的录制参数
package net.video.record.entity.model;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Table;import lombok.Data;
import lombok.experimental.Accessors;@Data
@Accessors(chain = true)
@Table("user")
public class User {public static Map<Integer, User> userMap = new ConcurrentHashMap<Integer, User>(); @Id private Integer id; private String userName; private String password; private String bdAccessToken; private String bdRefreshToken; private LocalDateTime createTime; private LocalDateTime modifyTime;
}
5、几个VO
TaskReq 任务请求参数
package net.video.record.entity.vo;
import lombok.Data;
import lombok.experimental.Accessors;@Data
@Accessors(chain = true)
public class TaskReq {private Integer taskId;
}
UserReq
package net.video.record.entity.vo;
import lombok.Data;
@Data
public class UserReq {private String userName; private String password;
}
UserRes
package net.video.record.entity.vo;
import java.time.LocalDateTime;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
@Data
public class UserRes {private Integer id; private String userName; private String password; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createTime; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime modifyTime;
}
6、把网盘接口封装一下
我封装的是百度网盘,可以去网盘开放平台查看文档,这里贴出主要的上传代码。
public String upload(BdFileUpload req, TaskList task) {
User user = User.userMap.get(task.getUserId());
if (user == null) {
throw new RuntimeException("用户信息不存在");
}
//大于4m的话分片,这里先不处理分片
File file = req.getFile();
req.setAccess_token(user.getBdAccessToken());
List<String> fileMd5 = Arrays.asList(SecureUtil.md5(file));
PreCreateReq preCreateReq = new PreCreateReq().setAccess_token(req.getAccess_token())
.setAutoinit(1).setIsdir(0).setRtype(1)
.setPath("/apps/直播云存储/" + task.getId() + "/" + DateUtil.today() + "/" + file.getName())
.setSize(String.valueOf(file.length()))
.setBlock_list(JSONUtil.toJsonStr(fileMd5));
PreCreateRes preCreate = preCreate(preCreateReq);
for (int i = 0; i < fileMd5.size(); i++) {
SegUploadReq segUploadReq = new SegUploadReq()
.setAccess_token(req.getAccess_token())
.setPath(preCreate.getPath())
.setUploadid(preCreate.getUploadid())
.setPartseq(i)
.setFile(req.getFile());
SegUploadRes segUploadRes = SegUpload(segUploadReq);
}
CreateFileReq createFileReq = new CreateFileReq().setAccess_token(req.getAccess_token())
.setBlock_list(JSONUtil.toJsonStr(fileMd5))
.setPath(preCreateReq.getPath())
.setSize(preCreateReq.getSize())
.setIsdir(preCreateReq.getIsdir())
.setRtype(preCreateReq.getRtype())
.setUploadid(preCreate.getUploadid());
CreateFileRes createFile = createFile(createFileReq);
return createFile.getServer_filename();
}</code></pre></div></div><h4 id="bk7ig" name="7%E3%80%81%E8%A7%86%E9%A2%91%E6%B5%81%E5%BD%95%E5%88%B6%E9%83%A8%E5%88%86">7、视频流录制部分</h4><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>javascript</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-javascript"><code class="language-javascript" style="margin-left:0">/**
* 录制视频
* @param inputFile 该地址可以是网络直播/录播地址,也可以是远程/本地文件路径
* @param outputFile 该地址只能是文件地址,如果使用该方法推送流媒体服务器会报错,原因是没有设置编码格式
* @param audioChannel 是否录制音频 1录制
* @param time 录制时间
* @throws Exception
* @throws org.bytedeco.javacv.FrameRecorder.Exception
*/
public void frameRecord(String inputFile, String outputFile, int audioChannel, int time)
throws Exception, org.bytedeco.javacv.FrameRecorder.Exception {
// 获取视频源
FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(inputFile);
// 流媒体输出地址,分辨率(长,高),是否录制音频(0:不录制/1:录制)
FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(outputFile, 1280, 720, audioChannel);
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC);
//设置分片
recorder.setFormat("segment");
//生成模式 实时
recorder.setOption("segment_list_flags", "live");
//分片时长 60s
recorder.setOption("segment_time", "60");
//锁定分片时长
recorder.setOption("segment_atclocktime", "1");
//用来严格控制分片时长
recorder.setOption("break_non_keyframes", "1");
//设置日志级别
avutil.av_log_set_level(avutil.AV_LOG_ERROR);
// 开始取视频源
try {
grabber.start();
recorder.start();
Frame frame = null;
Date startDate = new Date();
while ((frame = grabber.grabFrame()) != null
&& DateUtil.between(startDate, new Date(), DateUnit.SECOND) <= time * 60) {
recorder.record(frame);
}
recorder.stop();
grabber.stop();
} finally {
if (grabber != null) {
grabber.stop();
}
}
}</code></pre></div></div><h3 id="7rvc2" name="%E6%80%BB%E7%BB%93">总结</h3><p>这里我只贴出了部分代码,如果有想要了解具体实现的,也可以留言跟我交流。这个系统我也只是快速实现了一下,只达到能用的程度,其中对javacv、webflux进行了一定学习研究,后续的完善,还要看我哪天再次心血来</p>