😎 知识点概览
为了方便后续回顾该项目时能够清晰的知道本章节讲了哪些内容,并且能够从该章节的笔记中得到一些帮助,所以在完成本章节的学习后在此对本章节所涉及到的知识点进行总结概述。
本章节为【学成在线】项目的 day14
的内容
- 视频上传成功后通过
RabbitMQ
进行消息发送,再通过视频处理服务
对视频进行格式转换,以及m3u8
视频文件的生成。 - 实现媒资信息的浏览
-
Vue
跨组件间的通讯实战,实现课程计划与已上传的媒资文件的关联
目录
知识点结合实战应用会更有意义,所以这里不再对单个知识点进行拆分成单个笔记,内容会比较多,可以根据目录进行按需查阅。
一、视频处理
0x01 需求分析
原始视频通常需要经过编码处理,生成 m3u8
和 ts
文件方可基于 HLS
协议播放视频。通常用户上传原始视频,系统自动处理成标准格式,系统对用户上传的视频自动编码、转换,最终生成m3u8
文件和 ts
文件,处理流程如下:
1、用户上传视频成功
2、系统对上传成功的视频自动开始编码处理
3、用户查看视频处理结果,没有处理成功的视频用户可在管理界面再次触发处理
4、视频处理完成将视频地址及处理结果保存到数据库
视频处理流程如下:
视频处理进程的任务是接收视频处理消息进行视频处理,业务流程如下:
1、监听 MQ
,接收视频处理消息。
2、进行视频处理。
3、向数据库写入视频处理结果。
视频处理进程属于媒资管理系统的一部分,考虑提高系统的扩展性,将视频处理单独定义视频处理工程。
0x02 视频处理开发
视频处理工程创建
1、导入“资料” 下的视频处理工程:xc-service-manage-media-processor
2、RabbitMQ
配置
使用 rabbitMQ
的 routing
交换机模式,视频处理程序监听视频处理队列,如下图:
RabbitMQ配置如下:
@Configuration
public class RabbitMQConfig {
public static final String EX_MEDIA_PROCESSTASK = "ex_media_processor";
//视频处理队列
@Value("${xc‐service‐manage‐media.mq.queue‐media‐video‐processor}")
public String queue_media_video_processtask;
//视频处理路由
@Value("${xc‐service‐manage‐media.mq.routingkey‐media‐video}")
public String routingkey_media_video;
/**
* 交换机配置
* @return the exchange
*/
@Bean(EX_MEDIA_PROCESSTASK)
public Exchange EX_MEDIA_VIDEOTASK() {
return ExchangeBuilder.directExchange(EX_MEDIA_PROCESSTASK).durable(true).build();
}
//声明队列
@Bean("queue_media_video_processtask")
public Queue QUEUE_PROCESSTASK() {
Queue queue = new Queue(queue_media_video_processtask,true,false,true);
return queue;
}
/**
* 绑定队列到交换机 .
* @param queue the queue
* @param exchange the exchange
* @return the binding
*/
@Bean
public Binding binding_queue_media_processtask(@Qualifier("queue_media_video_processtask")Queue queue, @Qualifier(EX_MEDIA_PROCESSTASK) Exchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with(routingkey_media_video).noargs();
}
}
在 application.yml
中配置队列名称及 routingkey
xc‐service‐manage‐media:
mq:
queue‐media‐video‐processor: queue_media_video_processor
routingkey‐media‐video: routingkey_media_video
视频处理技术方案
如何通过程序进行视频处理?
ffmpeg
是一个可行的视频处理程序,可以通过 Java
调用 ffmpeg.exe
完成视频处理。
在 java
中可以使用 Runtime
类和 Process Builder
类两种方式来执行外部程序,工作中至少掌握一种。
本项目使用 Process Builder
的方式来调用 ffmpeg
完成视频处理。
关于 Process Builder
的测试如下:
//测试ping命令
@Test
public void testProcessBuilder() throws IOException {
//创建ProcessBuilder对象
ProcessBuilder processBuilder = new ProcessBuilder();
//设置执行的第三方程序(命令)
List<String> cmds = new ArrayList<>();
cmds.add("ping");
cmds.add("127.0.0.1");
processBuilder.command(cmds);
//合并标准输入流和错误输出
processBuilder.redirectErrorStream(true);
Process start = processBuilder.start();
//获取输入流
InputStream inputStream = start.getInputStream();
//将输入流转换为字符输入流
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "gbk");
//获取流的数据
int len = -1;
//数据缓冲区
char[] cache = new char[1024];
StringBuffer stringBuffer = new StringBuffer();
while ((len = inputStreamReader.read(cache)) != -1) {
//获取缓冲区内的数据
String outStr = new String(cache, 0, len);
System.out.println(outStr);
stringBuffer.append(outStr);
}
inputStream.close();
}
//测试使用工具类将avi转成mp4
@Test
public void testProcessMp4() throws IOException {
ProcessBuilder processBuilder = new ProcessBuilder();
//定义命令内容
List<String> command = new ArrayList<>();
command.add("D:/soft/ffmpeg-20200315-c467328-win64-static/bin/ffmpeg.exe");
command.add("-i");
command.add("E:/temp/1.avi");
command.add("-y"); //覆盖输出文件
command.add("-c:v");
command.add("libx264");
command.add("-s");
command.add("1280x720");
command.add("-pix_fmt");
command.add("yuv420p");
command.add("-b:a");
command.add("63k");
command.add("-b:v");
command.add("753k");
command.add("-r");
command.add("18");
command.add("E:/temp/1.mp4");
processBuilder.command(command);
//将标准输入流和错误输入流合并,通过标准输入流读取信息
processBuilder.redirectErrorStream(true);
Process start = processBuilder.start();
InputStream inputStream = start.getInputStream();
InputStreamReader streamReader = new InputStreamReader(inputStream, "gbk");
//获取输入流数据
int len = -1;
//数据缓冲区
char[] cache = new char[1024];
StringBuffer stringBuffer = new StringBuffer();
while ((len=streamReader.read(cache)) != -1){
//从缓冲区获取数据
String out = new String(cache, 0, len);
System.out.println(out);
stringBuffer.append(out);
}
inputStream.close();
}
上边的代码已经封装成工具类,参见:
上边的工具类中:
Mp4VideoUtil.java
完成 avi
转 mp4
HlsVideoUtil.java
完成 mp4
转 hls
分别测试每个工具类的使用方法。
public static void main(String[] args) throws IOException {
String ffmpeg_path = "D:/soft/ffmpeg-20200315-c467328-win64-static/bin/ffmpeg.exe";//ffmpeg的安装位置
String video_path = "E:\\temp\\1.avi";
String mp4_name = "2.mp4";
String mp4_path = "E:\\temp\\";
Mp4VideoUtil videoUtil = new Mp4VideoUtil(ffmpeg_path,video_path,mp4_name,mp4_path);
String s = videoUtil.generateMp4();
System.out.println(s);
}
视频处理实现
1、确定消息格式
MQ
消息统一采用 json
格式,视频处理生产方会向 MQ
发送如下消息,视频处理消费方接收此消息后进行视频处
理:
{
"mediaId":XXX}
2、处理流程
1)接收视频处理消息
2)判断媒体文件是否需要处理(本视频处理程序目前只接收avi
视频的处理)当前只有 avi
文件需要处理,其它文件需要更新处理状态为 “无需处理
”。
3)处理前初始化处理状态为 “未处理
”
4)处理失败需要在数据库记录处理日志,及处理状态为 “处理失败
”
5)处理成功记录处理状态为 “处理成功
“
3、数据模型
在 MediaFile
类中添加 mediaFileProcess_m3u8
属性记录 ts
文件列表,代码如下 :
//处理状态
private String processStatus;
//hls处理
private MediaFileProcess_m3u8 mediaFileProcess_m3u8;
MediaFileProcess_m3u8
如下
@Data
@ToString
public class MediaFileProcess_m3u8 extends MediaFileProcess {
//ts列表
private List<String> tslist;
}
4、视频处理生成 MP4
1)创建 dao
视频处理结果需要保存到媒资数据库,创建 dao
如下:
public interface MediaFileRepository extends MongoRepository<MediaFile,String> {
}
2)在 application.yml
中配置 ffmpeg
的位置及视频目录的根目录
xc‐service‐manage‐media:
video‐location: F:/develop/video/
ffmpeg‐path: D:/Program Files/ffmpeg‐20180227‐fa0c9d6‐win64‐static/bin/ffmpeg.exe
3)处理任务类
在 mq
包下创建 MediaProcessTask
类,此类负责监听视频处理队列,并进行视频处理。
整个视频处理内容较多,这里分两部分实现:生成 Mp4
和生成 m3u8
,下边代码实现了生成 mp4
。
@Component
public class MediaProcessTask {
//日志对象
private static final Logger LOGGER = LoggerFactory.getLogger(MediaProcessTask.class);
//ffmpeg绝对路径
@Value("${xc-service-manage-media.ffmpeg-path}")
String ffmpeg_path;
//上传文件根目录
@Value("${xc-service-manage-media.video-location}")
String serverPath;
@Autowired
MediaFileRepository mediaFileRepository;
@RabbitListener(queues = "${xc-service-manage-media.mq.queue-media-video-processor}")
public void receiveMediaProcessTask(String msg){
//将接收到的消息转换为json数据
Map msgMap = JSON.parseObject(msg);
LOGGER.info("receive media process task msg :{} ",msgMap);
//解析消息
//媒资文件id
String mediaId = (String) msgMap.get("mediaId");
//获取媒资文件信息
Optional<MediaFile> byId = mediaFileRepository.findById(mediaId);
if(!byId.isPresent()){
return;
}
MediaFile mediaFile = byId.get();
//媒资文件类型
String fileType = mediaFile.getFileType();
//目前只处理avi文件
if(fileType == null || !fileType.equals("avi")){
mediaFile.setProcessStatus("303004"); // 处理状态为无需处理
mediaFileRepository.save(mediaFile);
}else{
mediaFile.setProcessStatus("303001"); //处理状态为未处理
}
//生成MP4
String videoPath = serverPath + mediaFile.getFilePath() + mediaFile.getFileName();
String mp4Name = mediaFile.getFileId() + ".mp4";
String mp4FloderPath = serverPath + mediaFile.getFilePath();
Mp4VideoUtil mp4VideoUtil = new Mp4VideoUtil(ffmpeg_path, videoPath, mp4Name, mp4FloderPath);
String result = mp4VideoUtil.generateMp4();
if(result == null || !result.equals("success")){
//操作失败写入处理日志
mediaFile.setProcessStatus("303003");//处理状态为处理失败
MediaFileProcess_m3u8 mediaFileProcess_m3u8 = new MediaFileProcess_m3u8();
mediaFileProcess_m3u8.setErrormsg(result);
mediaFile.setMediaFileProcess_m3u8(mediaFileProcess_m3u8);
mediaFileRepository.save(mediaFile);
return;
}
//生成m3u8...
}
}
说明:
1、原始视频转成 mp4
如何判断转换成功?
根据视频时长来判断,取原视频和转换成功视频的时长(时分秒),如果相等则相同。
5、视频处理生成 m3u8
下边是完整的视频处理任务类代码,包括了生成 m3u8
及生成 mp4
的代码
package com.xuecheng.manage_media_process.mq;
import com.alibaba.fastjson.JSON;
import com.xuecheng.framework.domain.media.MediaFile;
import com.xuecheng.framework.domain.media.MediaFileProcess_m3u8;
import com.xuecheng.framework.utils.HlsVideoUtil;
import com.xuecheng.framework.utils.Mp4VideoUtil;
import com.xuecheng.manage_media_process.dao.MediaFileRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@Component
public class MediaProcessTask {
//日志对象
private static final Logger LOGGER = LoggerFactory.getLogger(MediaProcessTask.class);
//ffmpeg绝对路径
@Value("${xc-service-manage-media.ffmpeg-path}")
String ffmpeg_path;
//上传文件根目录
@Value("${xc-service-manage-media.video-location}")
String serverPath;
@Autowired
MediaFileRepository mediaFileRepository;
@RabbitListener(queues = "${xc-service-manage-media.mq.queue-media-video-processor}")
public void receiveMediaProcessTask(String msg){
//将接收到的消息转换为json数据
Map msgMap = JSON.parseObject(msg);
LOGGER.info("receive media process task msg :{} ",msgMap);
//解析消息
//媒资文件id
String mediaId = (String) msgMap.get("mediaId");
//获取媒资文件信息
Optional<MediaFile> byId = mediaFileRepository.findById(mediaId);
if(!byId.isPresent()){
return;
}
MediaFile mediaFile = byId.get();
//媒资文件类型
String fileType = mediaFile.getFileType();
//目前只处理avi文件
if(fileType == null || !fileType.equals("avi")){
mediaFile.setProcessStatus("303004"); // 处理状态为无需处理
mediaFileRepository.save(mediaFile);
}else{
mediaFile.setProcessStatus("303001"); //处理状态为未处理
}
//生成MP4
String videoPath = serverPath + mediaFile.getFilePath() + mediaFile.getFileName();
String mp4Name = mediaFile.getFileId() + ".mp4";
String mp4FloderPath = serverPath + mediaFile.getFilePath();
Mp4VideoUtil mp4VideoUtil = new Mp4VideoUtil(ffmpeg_path, videoPath, mp4Name, mp4FloderPath);
String result = mp4VideoUtil.generateMp4();
if(result == null || !result.equals("success")){
//操作失败写入处理日志
mediaFile.setProcessStatus("303003");//处理状态为处理失败
MediaFileProcess_m3u8 mediaFileProcess_m3u8 = new MediaFileProcess_m3u8();
mediaFileProcess_m3u8.setErrormsg(result);
mediaFile.setMediaFileProcess_m3u8(mediaFileProcess_m3u8);
mediaFileRepository.save(mediaFile);
return;
}
//生成m3u8列表
//生成m3u8
String mp4VideoPath = serverPath + mediaFile.getFilePath()+ mp4Name;//此地址为mp4的地址
String m3u8Name = mediaFile.getFileId()+".m3u8";
String m3u8FolderPath = serverPath + mediaFile.getFilePath()+"hls/";
//调用工具类进行生成m3u8
HlsVideoUtil hlsVideoUtil = new HlsVideoUtil(ffmpeg_path, mp4VideoPath, m3u8Name, m3u8FolderPath);
String m3u8Result = hlsVideoUtil.generateM3u8();
if(m3u8Result==null || !m3u8Result.equals("success")){
//操作失败写入处理日志
mediaFile.setProcessStatus("303003");//处理状态为处理失败
MediaFileProcess_m3u8 mediaFileProcess_m3u8 = new MediaFileProcess_m3u8();
mediaFileProcess_m3u8.setErrormsg(m3u8Result);
mediaFile.setMediaFileProcess_m3u8(mediaFileProcess_m3u8);
mediaFileRepository.save(mediaFile);
return ;
}
//获取m3u8列表
List<String> ts_list = hlsVideoUtil.get_ts_list();
//更新处理状态为成功
mediaFile.setProcessStatus("303002");//处理状态为处理成功
MediaFileProcess_m3u8 mediaFileProcess_m3u8 = new MediaFileProcess_m3u8();
mediaFileProcess_m3u8.setTslist(ts_list);
mediaFile.setMediaFileProcess_m3u8(mediaFileProcess_m3u8);
//m3u8文件url
mediaFile.setFileUrl(mediaFile.getFilePath()+"hls/"+m3u8Name);
mediaFileRepository.save(mediaFile);
}
}
0x03 发送视频处理消息
当视频上传成功后向 MQ
发送视频 处理消息。
修改媒资管理服务的文件上传代码,当文件上传成功向 MQ
发送视频处理消息。
配置RabbitMQ
1、将media-processor
工程下的 RabbitmqConfig
配置类拷贝到 media
工程下。
2、在 media
工程下配置 mq
队列等信息
修改 application.yml
xc-service-manage-media:
mq:
queue-media-video-processor: queue_media_video_processor
routingkey-media-video: routingkey_media_video
配置Service
在文件合并方法中添加向 mq
发送视频处理消息的代码:
//视频处理路由
@Value("${xc-service-manage-media.mq.routingkey-media-video}")
public String routingkey_media_video;
@Autowired
RabbitTemplate rabbitTemplate;
//向MQ发送视频处理消息
private ResponseResult sendProcessVideoMsg(String mediaId){
Optional<MediaFile> optional = mediaFileRepository.findById(mediaId);
if(!optional.isPresent()){
return new ResponseResult(CommonCode.FAIL);
}
MediaFile mediaFile = optional.get();
//发送视频处理消息
Map<String,String> msgMap = new HashMap<>();
msgMap.put("mediaId",mediaId);
//发送的消息
String msg = JSON.toJSONString(msgMap);
try {
this.rabbitTemplate.convertAndSend(RabbitMQConfig.EX_MEDIA_PROCESSTASK,routingkey_media_video,msg);
LOGGER.info("send media process task msg:{}",msg);
}catch (Exception e){
e.printStackTrace();
LOGGER.info("send media process task error,msg is:{},error:{}",msg,e.getMessage());
return new ResponseResult(CommonCode.FAIL);
}
return new ResponseResult(CommonCode.SUCCESS)