赞
踩
哪一天我心血来潮,想把我儿子学校的摄像头视频流录制下来,并保存到云盘上,这样我就可以在有空的时候看看我儿子在学校干嘛。想到么就干,当时花了一些时间开发了一个后端服务,通过数据库配置录制参数,以后的设想是能够通过页面去配置,能够自动捕获直播视频流,这还得要求自己先学会vue,所以还得缓缓。
技术栈:Spring Boot、Webflux、r2dbc、javacv
架构图:
流程很简单,主要还是要用到JavaCV从视频流里捕获视频,先报错到本地,然后有一个定时任务会定时去检测目录内是否有新生成的文件,有就上传到配置的云盘(百度云)。
<?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>
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()); } }
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); } }
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; }
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; }
我封装的是百度网盘,可以去网盘开放平台查看文档,这里贴出主要的上传代码。
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(); }
/** * 录制视频 * @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(); } } }
这里我只贴出了部分代码,如果有想要了解具体实现的,也可以留言跟我交流。这个系统我也只是快速实现了一下,只达到能用的程度,其中对javacv、webflux进行了一定学习研究,后续的完善,还要看我哪天再次心血来潮。
作者其他文章推荐:
基于Spring Boot 3.1.0 系列文章
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。