云端录制直播流视频,上传云盘

前言

哪一天我心血来潮,想把我儿子学校的摄像头视频流录制下来,并保存到云盘上,这样我就可以在有空的时候看看我儿子在学校干嘛。想到么就干,当时花了一些时间开发了一个后端服务,通过数据库配置录制参数,以后的设想是能够通过页面去配置,能够自动捕获直播视频流,这还得要求自己先学会vue,所以还得缓缓。

实现

技术栈:Spring Boot、Webflux、r2dbc、javacv

架构图:

流程很简单,主要还是要用到JavaCV从视频流里捕获视频,先报错到本地,然后有一个定时任务会定时去检测目录内是否有新生成的文件,有就上传到配置的云盘(百度云)。

1、创建pom

代码语言:javascript
复制
<?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>
	&lt;dependency&gt;
		&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
		&lt;artifactId&gt;spring-boot-starter-webflux&lt;/artifactId&gt;
	&lt;/dependency&gt;

	&lt;dependency&gt;
		&lt;groupId&gt;org.projectlombok&lt;/groupId&gt;
		&lt;artifactId&gt;lombok&lt;/artifactId&gt;
		&lt;optional&gt;true&lt;/optional&gt;
	&lt;/dependency&gt;

	&lt;dependency&gt;
		&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
		&lt;artifactId&gt;spring-boot-starter-data-r2dbc&lt;/artifactId&gt;
	&lt;/dependency&gt;

	&lt;dependency&gt;
		&lt;groupId&gt;dev.miku&lt;/groupId&gt;
		&lt;artifactId&gt;r2dbc-mysql&lt;/artifactId&gt;
	&lt;/dependency&gt;

	&lt;dependency&gt;
		&lt;groupId&gt;cn.hutool&lt;/groupId&gt;
		&lt;artifactId&gt;hutool-all&lt;/artifactId&gt;
		&lt;version&gt;5.7.22&lt;/version&gt;
	&lt;/dependency&gt;

	&lt;dependency&gt;
		&lt;groupId&gt;org.bytedeco&lt;/groupId&gt;
		&lt;artifactId&gt;javacv-platform&lt;/artifactId&gt;
		&lt;version&gt;1.4.4&lt;/version&gt;
	&lt;/dependency&gt;
	
	 &lt;dependency&gt;
        &lt;groupId&gt;org.apache.httpcomponents&lt;/groupId&gt;
        &lt;artifactId&gt;httpcore&lt;/artifactId&gt;
        &lt;version&gt;4.4.10&lt;/version&gt;
    &lt;/dependency&gt;
    
    &lt;dependency&gt;
        &lt;groupId&gt;org.apache.httpcomponents&lt;/groupId&gt;
        &lt;artifactId&gt;httpclient&lt;/artifactId&gt;
        &lt;version&gt;4.5.6&lt;/version&gt;
    &lt;/dependency&gt;

	&lt;dependency&gt;
		&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
		&lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;
		&lt;scope&gt;test&lt;/scope&gt;
	&lt;/dependency&gt;
	&lt;dependency&gt;
		&lt;groupId&gt;io.projectreactor&lt;/groupId&gt;
		&lt;artifactId&gt;reactor-test&lt;/artifactId&gt;
		&lt;scope&gt;test&lt;/scope&gt;
	&lt;/dependency&gt;
&lt;/dependencies&gt;

&lt;build&gt;
	&lt;finalName&gt;video-cloud-record&lt;/finalName&gt;
	&lt;plugins&gt;
		&lt;plugin&gt;
			&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
			&lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;
			&lt;configuration&gt;
				&lt;excludes&gt;
					&lt;exclude&gt;
						&lt;groupId&gt;org.projectlombok&lt;/groupId&gt;
						&lt;artifactId&gt;lombok&lt;/artifactId&gt;
					&lt;/exclude&gt;
				&lt;/excludes&gt;
			&lt;/configuration&gt;
		&lt;/plugin&gt;
	&lt;/plugins&gt;
&lt;/build&gt;

</project>

2、定时异常信息

代码语言:javascript
复制
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、统一结果集

代码语言:javascript
复制
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 &lt;T&gt; Result&lt;T&gt; ok(T data) {
	return new Result&lt;T&gt;(&#34;0&#34;, data, &#34;&#34;);
}

public static &lt;T&gt; Result&lt;T&gt; error(String code, String msg) {
	code = StrUtil.isEmpty(code)? &#34;500&#34; : code;
	return new Result&lt;T&gt;(code, null, msg);
}

}

4、定义两个Model

TaskList 用来保存用户相关的录制任务

代码语言:javascript
复制
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 定义用户信息,保存了用过相关的录制参数

代码语言:javascript
复制
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&lt;Integer, User&gt; userMap = new ConcurrentHashMap&lt;Integer, User&gt;();

@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 任务请求参数

代码语言:javascript
复制
package net.video.record.entity.vo;

import lombok.Data;
import lombok.experimental.Accessors;

@Data
@Accessors(chain = true)
public class TaskReq {

private Integer taskId;

}

UserReq

代码语言:javascript
复制
package net.video.record.entity.vo;

import lombok.Data;

@Data
public class UserReq {

private String userName;

private String password;

}

UserRes

代码语言:javascript
复制
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 = &#34;yyyy-MM-dd HH:mm:ss&#34;)
private LocalDateTime createTime;

@JsonFormat(pattern = &#34;yyyy-MM-dd HH:mm:ss&#34;)
private LocalDateTime modifyTime;

}

6、把网盘接口封装一下

我封装的是百度网盘,可以去网盘开放平台查看文档,这里贴出主要的上传代码。

代码语言:javascript
复制
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&lt;String&gt; fileMd5 = Arrays.asList(SecureUtil.md5(file));
	PreCreateReq preCreateReq = new PreCreateReq().setAccess_token(req.getAccess_token())
			.setAutoinit(1).setIsdir(0).setRtype(1)
			.setPath(&#34;/apps/直播云存储/&#34; + task.getId() + &#34;/&#34; + DateUtil.today() + &#34;/&#34; + file.getName())
			.setSize(String.valueOf(file.length()))
			.setBlock_list(JSONUtil.toJsonStr(fileMd5));
	PreCreateRes preCreate = preCreate(preCreateReq);
	
	for (int i = 0; i &lt; 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(&#34;segment&#34;);
	//生成模式 实时
	recorder.setOption(&#34;segment_list_flags&#34;, &#34;live&#34;);
	//分片时长 60s
	recorder.setOption(&#34;segment_time&#34;, &#34;60&#34;);
	//锁定分片时长
	recorder.setOption(&#34;segment_atclocktime&#34;, &#34;1&#34;);
	//用来严格控制分片时长
	recorder.setOption(&#34;break_non_keyframes&#34;, &#34;1&#34;);
	//设置日志级别
	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 
				&amp;&amp; DateUtil.between(startDate, new Date(), DateUnit.SECOND) &lt;= 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>