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

前言

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

实现

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

架构图:

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

1、创建pom

xml 复制代码
<?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、定时异常信息

java 复制代码
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、统一结果集

java 复制代码
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 用来保存用户相关的录制任务

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

java 复制代码
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 任务请求参数

java 复制代码
package net.video.record.entity.vo;

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

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

	private Integer taskId;
}

UserReq

java 复制代码
package net.video.record.entity.vo;

import lombok.Data;

@Data
public class UserReq {

	private String userName;
	
	private String password;
}

UserRes

java 复制代码
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、把网盘接口封装一下

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

java 复制代码
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();
	}

7、视频流录制部分

java 复制代码
/**
	 * 录制视频
	 * @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 系列文章

  1. Spring Boot 源码阅读初始化环境搭建
  2. Spring Boot 框架整体启动流程详解
  3. Spring Boot 系统初始化器详解
  4. Spring Boot 监听器详解
  5. Spring Boot banner详解
  6. Spring Boot 属性配置解析
  7. Spring Boot 属性加载原理解析
  8. Spring Boot 异常报告器解析
  9. Spring Boot 3.x 自动配置详解
相关推荐
川石课堂软件测试41 分钟前
性能测试|docker容器下搭建JMeter+Grafana+Influxdb监控可视化平台
运维·javascript·深度学习·jmeter·docker·容器·grafana
昌sit!7 小时前
K8S node节点没有相应的pod镜像运行故障处理办法
云原生·容器·kubernetes
茶馆大橘10 小时前
微服务系列五:避免雪崩问题的限流、隔离、熔断措施
java·jmeter·spring cloud·微服务·云原生·架构·sentinel
北漂IT民工_程序员_ZG10 小时前
k8s集群安装(minikube)
云原生·容器·kubernetes
coding侠客11 小时前
揭秘!微服务架构下,Apollo 配置中心凭啥扮演关键角色?
微服务·云原生·架构
2301_8061313617 小时前
Kubernetes的基本构建块和最小可调度单元pod-0
云原生·容器·kubernetes
Walden-202019 小时前
构建基于 DCGM-Exporter, Node exporter,PROMETHEUS 和 GRAFANA 构建算力监控系统
docker·容器·grafana·prometheus
licy__21 小时前
Docker 基础命令简介
docker·云原生·eureka
0_1_bits1 天前
【系统设计】高效的分布式系统:使用 Spring Boot 和 Kafka 实现 Saga 模式
spring boot·后端·云原生·架构·kafka·linq
探索云原生1 天前
GPU 环境搭建指南:如何在裸机、Docker、K8s 等环境中使用 GPU
ai·云原生·kubernetes·go·gpu