视频处理
需求分析
原始视频通常需要经过编码处理,生成m3u8和ts文件方可基于HLS协议播放视频。通常用户上传原始视频,系统自动处理成标准格式,系统对用户上传的视频自动编码、转换,最终生成m3u8文件和ts文件,处理流程如下:
- 用户上传视频成功。
- 系统对上传成功的视频自动开始编码处理。
- 用户查看视频处理结果,没有处理成功的视频用户可在管理界面再次触发处理。
- 视频处理完成将视频地址及处理结果保存到数据库。
视频处理流程如下:
视频处理进程的任务是接收视频处理消息进行视频处理,业务流程如下:
- 监听MQ,接收视频处理消息。
- 进行视频处理。
- 向数据库写入视频处理结果
视频处理消费方
导入工程(省略)
application.yml
server:
port: 31450
spring:
application:
name: xc-service-manage-media-processor
data:
mongodb:
uri: mongodb://root:123@localhost:27017
database: xc_media
#rabbitmq配置
rabbitmq:
host: 192.168.136.110
port: 5672
username: xcEdu
password: 123456
virtual-host: /
xc-service-manage-media:
mq:
queue-media-video-processor: queue_media_video_processor
routingkey-media-video: routingkey_media_video
video-location: ${MEDIA_FILE_LOCATION:E:/nginx/xcEdu/video/}
ffmpeg-path: F:/ffmpeg-20180227-fa0c9d6-win64-static/bin/ffmpeg.exe
RabbitMQConfig
package com.xuecheng.manage_media_process.config;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.amqp.SimpleRabbitListenerContainerFactoryConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author Administrator
* @version 1.0
* @create 2018-07-12 9:04
**/
@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;
//消费者并发数量
public static final int DEFAULT_CONCURRENT = 10;
/**
* 交换机配置
*
* @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();
}
@Bean("customContainerFactory")
public SimpleRabbitListenerContainerFactory containerFactory(SimpleRabbitListenerContainerFactoryConfigurer configurer,
ConnectionFactory connectionFactory) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConcurrentConsumers(DEFAULT_CONCURRENT);
factory.setMaxConcurrentConsumers(DEFAULT_CONCURRENT);
configurer.configure(factory, connectionFactory);
return factory;
}
}
MediaProcessTask
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.domain.media.response.MediaCode;
import com.xuecheng.framework.exception.ExceptionCast;
import com.xuecheng.framework.utils.HlsVideoUtil;
import com.xuecheng.framework.utils.Mp4VideoUtil;
import com.xuecheng.manage_media_process.dao.MediaFileRepository;
import lombok.extern.slf4j.Slf4j;
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;
@Slf4j
@Component
public class MediaProcessTask {
//ffmpeg绝对路径
@Value("${xc‐service‐manage‐media.ffmpeg‐path}")
String ffmpeg_path;
//上传文件根目录
@Value("${xc‐service‐manage‐media.video‐location}")
String serverPath;
@Autowired
MediaFileRepository mediaFileRepository;
/**
* 接收视频处理消息并处理对应视频格式
*
* @param msg 消息
*/
@RabbitListener(queues = "${xc‐service‐manage‐media.mq.queue-media-video-processor}", containerFactory = "customContainerFactory")
public void receiveMediaProcessTask(String msg) {
Map<String, String> msgMap = JSON.parseObject(msg, Map.class);
log.info("[视频处理] 收到视频处理消息, msg = {}", msgMap.toString());
String mediaId = msgMap.get("mediaId");
// 获取文件
MediaFile mediaFile = mediaFileRepository.findById(mediaId).orElse(null);
if (mediaFile == null) {
ExceptionCast.cast(MediaCode.MEDIA_FILE_NOT_EXIST);
}
// 判断文件类型
String fileType = mediaFile.getFileType();
if (fileType == null || !fileType.equals("avi")) {//目前只处理avi文件
mediaFile.setProcessStatus("303004");//处理状态为无需处理
mediaFileRepository.save(mediaFile);
return;
} else {
mediaFile.setProcessStatus("303001");//处理状态为未处理
mediaFileRepository.save(mediaFile);
}
// 生成mp4
String mp4_name = mediaFile.getFileId() + ".mp4";
if (!buildMp4(mediaFile)) {
ExceptionCast.cast(MediaCode.MEDIA_BUILD_MP4_FAIL);
}
// 生成m3u8
if (!buildM3u8(mediaFile, mp4_name)) {
ExceptionCast.cast(MediaCode.MEDIA_BUILD_M3U8_FAIL);
}
log.info("[视频处理] 视频处理完成, mediaId = [{}]", mediaId);
}
/**
* 使用MP4文件生成m3u8文件并保存信息到数据库
*
* @param mediaFile 源文件
* @param mp4Name mp4文件名
* @return boolean
*/
private boolean buildM3u8(MediaFile mediaFile, String mp4Name) {
// mp4 url
String video_path = serverPath + mediaFile.getFilePath() + mp4Name;
// 生成后文件的存放位置
String m3u8_name = mediaFile.getFileId() + ".m3u8";
String m3u8folder_path = serverPath + mediaFile.getFilePath() + "hls/";
// 生成m3u8文件
HlsVideoUtil hlsVideoUtil = new HlsVideoUtil(ffmpeg_path, video_path, m3u8_name, m3u8folder_path);
String result = hlsVideoUtil.generateM3u8();
if (result == null || !result.equals("success")) {
//操作失败写入处理日志
processFail(result, mediaFile);
return false;
}
//获取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/" + m3u8_name);
mediaFileRepository.save(mediaFile);
return true;
}
/**
* 生成MP4文件
*
* @param mediaFile 源文件
* @return boolean
*/
private boolean buildMp4(MediaFile mediaFile) {
String video_path = serverPath + mediaFile.getFilePath() + mediaFile.getFileName();
String mp4_name = mediaFile.getFileId() + ".mp4";
String mp4folder_path = serverPath + mediaFile.getFilePath();
Mp4VideoUtil videoUtil = new Mp4VideoUtil(ffmpeg_path, video_path, mp4_name, mp4folder_path);
String result = videoUtil.generateMp4();
if (result == null || !result.equals("success")) {
//操作失败写入处理日志
processFail(result, mediaFile);
return false;
}
return true;
}
/**
* 操作失败
*
* @param result 操作结果
* @param mediaFile 文件
*/
private void processFail(String result, MediaFile mediaFile) {
mediaFile.setProcessStatus("303003");//处理状态为处理失败
MediaFileProcess_m3u8 mediaFileProcess_m3u8 = new MediaFileProcess_m3u8();
mediaFileProcess_m3u8.setErrormsg(result);
mediaFile.setMediaFileProcess_m3u8(mediaFileProcess_m3u8);
mediaFileRepository.save(mediaFile);
}
}
视频处理发送方
修改xc-service-manage-media
相关代码
application.yml
新增配置
spring:
rabbitmq:
host: 192.168.136.110
port: 5672
username: xcEdu
password: 123456
virtual-host: /
xc-service-manage-media:
mq:
queue-media-video-processor: queue_media_video_processor
routingkey-media-video: routingkey_media_video
RabbitMQConfig
package com.xuecheng.manage_media.config;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author Administrator
* @version 1.0
* @create 2018-07-12 9:04
**/
@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;
//消费者并发数量
public static final int DEFAULT_CONCURRENT = 10;
/**
* 交换机配置
*
* @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();
}
}
MediaUploadService
新增消息发送方法并在mergeChunks
方法的最后调用该方法完成视频处理消息的发送。
@Value("${xc-service-manage-media.mq.routingkey-media-video}")
private String routingkey_media_video;
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 发送视频处理消息
*
* @param mediaId 视频id
*/
public void sendProcessVideoMsg(String mediaId) {
Optional<MediaFile> optional = mediaFileRepository.findById(mediaId);
if (!optional.isPresent()) {
ExceptionCast.cast(CommonCode.FAIL);
}
//发送视频处理消息
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);
log.info("[发送视频处理消息] 文件上传完成, 发送视频处理消息. msg = {}", msg);
} catch (Exception e) {
log.info("[发送视频处理消息] 文件上传完成, 发送视频处理消息失败. msg = {}", msg, e.getMessage());
ExceptionCast.cast(CommonCode.FAIL);
}
}
测试
在课程管理前端选择文件并上传
查看控制台,正在执行MP4文件生成
查看最终生成的m3u8文件列表
我的媒资
需求分析
通过我的媒资可以查询本教育机构拥有的媒资文件,进行文件处理、删除文件、修改文件信息等操作,具体需求如下:
1、分页查询我的媒资文件
2、删除媒资文件
3、处理媒资文件
4、修改媒资文件信息
后端实现
MediaFileControllerApi
package com.xuecheng.api.media;
import com.xuecheng.framework.domain.media.request.QueryMediaFileRequest;
import com.xuecheng.framework.model.response.QueryResponseResult;
import com.xuecheng.framework.model.response.ResponseResult;
import io.swagger.annotations.Api;
@Api(value="媒资管理接口",description="提供媒资文件数据的增删改查")
public interface MediaFileControllerApi {
/**
* 分页查询媒资文件列表
*
* @param page 当前页码
* @param size 每页记录数
* @param queryMediaFileRequest 查询条件
* @return QueryResponseResult
*/
QueryResponseResult findList(int page, int size, QueryMediaFileRequest queryMediaFileRequest);
/**
* 删除媒资文件
*
* @param id 媒资文件ID
* @return ResponseResult
*/
ResponseResult delete(String id);
}
MediaFileController
package com.xuecheng.manage_media.controller;
import com.xuecheng.api.media.MediaFileControllerApi;
import com.xuecheng.framework.domain.media.request.QueryMediaFileRequest;
import com.xuecheng.framework.model.response.CommonCode;
import com.xuecheng.framework.model.response.QueryResponseResult;
import com.xuecheng.framework.model.response.ResponseResult;
import com.xuecheng.framework.web.BaseController;
import com.xuecheng.manage_media.service.MediaFileService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("media/file")
public class MediaFileController extends BaseController implements MediaFileControllerApi {
@Autowired
private MediaFileService mediaFileService;
@Override
@GetMapping("list/{page}/{size}")
public QueryResponseResult findList(@PathVariable int page,
@PathVariable int size, QueryMediaFileRequest queryMediaFileRequest) {
return mediaFileService.findList(page, size, queryMediaFileRequest);
}
@Override
@DeleteMapping("{id}")
public ResponseResult delete(@PathVariable String id) {
mediaFileService.delete(id);
return new ResponseResult(CommonCode.SUCCESS);
}
}
MediaFileService
package com.xuecheng.manage_media.service;
import com.xuecheng.framework.domain.media.MediaFile;
import com.xuecheng.framework.domain.media.request.QueryMediaFileRequest;
import com.xuecheng.framework.model.response.CommonCode;
import com.xuecheng.framework.model.response.QueryResponseResult;
import com.xuecheng.framework.model.response.QueryResult;
import com.xuecheng.framework.service.BaseService;
import com.xuecheng.manage_media.dao.MediaFileRepository;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class MediaFileService extends BaseService {
@Autowired
private MediaFileRepository mediaFileRepository;
/**
* 分页查询媒资文件列表
*
* @param page 当前页码
* @param size 每页记录数
* @param queryMediaFileRequest 查询条件
* @return QueryResponseResult
*/
public QueryResponseResult findList(int page, int size, QueryMediaFileRequest queryMediaFileRequest) {
// 处理分页参数
if (page <= 0) {
page = 1;
}
page = page - 1;
// 处理分页查询条件
if (queryMediaFileRequest == null) {
queryMediaFileRequest = new QueryMediaFileRequest();
}
MediaFile mediaFile = new MediaFile();
if (StringUtils.isNotBlank(queryMediaFileRequest.getFileOriginalName())) {
mediaFile.setFileOriginalName(queryMediaFileRequest.getFileOriginalName());
}
if (StringUtils.isNotBlank(queryMediaFileRequest.getProcessStatus())) {
mediaFile.setProcessStatus(queryMediaFileRequest.getProcessStatus());
}
if (StringUtils.isNotBlank(queryMediaFileRequest.getTag())) {
mediaFile.setTag(queryMediaFileRequest.getTag());
}
ExampleMatcher exampleMatcher = ExampleMatcher.matching()
.withMatcher("fileOriginalName", ExampleMatcher.GenericPropertyMatchers.contains())
.withMatcher("tag", ExampleMatcher.GenericPropertyMatchers.contains());
Example<MediaFile> example = Example.of(mediaFile, exampleMatcher);
// 查询
Page<MediaFile> mediaFiles = mediaFileRepository.findAll(example, PageRequest.of(page, size));
QueryResult<MediaFile> queryResult = new QueryResult<>(mediaFiles.getContent(), mediaFiles.getTotalElements());
return new QueryResponseResult(CommonCode.SUCCESS, queryResult);
}
/**
* 删除指定ID的mediaFile
*
* @param id 媒资文件ID
*/
public void delete(String id) {
mediaFileRepository.deleteById(id);
}
}
前端实现
我这里前端主要修改了处理状态
的下拉选择框的数据从数据字典动态获取。
修改media_list.vue
的mounted
内容
mounted() {
//默认查询页面
this.query()
//初始化处理状态
//查询数据字典字典
this.processStatusList = [
{
id:'',
name:'全部'
}
]
// 查询数据库获取数据
systemApi.sys_getDictionary('303').then((res) => {
res.dvalue.forEach((element) => {
let data = {}
data.id = element.sdId
data.name = element.sdName
this.processStatusList.push(data)
})
})
}
注意:
还需要在前面引入查询数据字典的API定义:import * as systemApi from '../../../base/api/system'
在导入的数据库中会发现有两个数据字典的type
都为303
,查询会报错,所以我修改了另外一个的type
为403
媒资与课程计划关联
需求分析
- 进入课程计划修改页面。
- 选择视频。
- 选择成功后,将在课程管理数据库保存课程计划对应在的课程视频地址。
后端实现
修改xc-service-manage-course
中相关代码完成功能
CoursePlanControllerApi
修增API定义
@ApiOperation("保存媒资信息")
ResponseResult saveMedia(TeachplanMedia teachplanMedia);
CoursePlanController
新增接口实现
@Override
@PostMapping("savemedia")
public ResponseResult saveMedia(@RequestBody TeachplanMedia teachplanMedia) {
TeachplanMedia saveMedia = courseService.saveMedia(teachplanMedia);
isNullOrEmpty(saveMedia, CommonCode.SERVER_ERROR);
return ResponseResult.SUCCESS();
}
CourseService
新增方法
@Autowired
private TeachplanMediaRepository teachplanMediaRepository;
/**
* 保存课程计划关联媒资数据
*
* @param teachplanMedia 关联树数据
* @return TeachplanMedia
*/
public TeachplanMedia saveMedia(TeachplanMedia teachplanMedia) {
isNullOrEmpty(teachplanMedia, CommonCode.PARAMS_ERROR);
// 查询课程计划
Teachplan teachplan = coursePlanRepository.findById(teachplanMedia.getTeachplanId()).orElse(null);
isNullOrEmpty(teachplan, CourseCode.COURSE_MEDIS_TEACHPLAN_IS_NULL);
// 只允许叶子节点选择视频
String grade = teachplan.getGrade();
if (StringUtils.isEmpty(grade) || !grade.equals("3")) {
ExceptionCast.cast(CourseCode.COURSE_MEDIA_TEACHPLAN_GRADE_ERROR);
}
TeachplanMedia media;
Optional<TeachplanMedia> teachplanMediaOptional = teachplanMediaRepository.findById(teachplanMedia.getTeachplanId());
media = teachplanMediaOptional.orElseGet(TeachplanMedia::new);
//保存媒资信息与课程计划信息
media.setTeachplanId(teachplanMedia.getTeachplanId());
media.setCourseId(teachplanMedia.getCourseId());
media.setMediaFileOriginalName(teachplanMedia.getMediaFileOriginalName());
media.setMediaId(teachplanMedia.getMediaId());
media.setMediaUrl(teachplanMedia.getMediaUrl());
return teachplanMediaRepository.save(media);
}
TeachplanMediaRepository
package com.xuecheng.manage_course.dao;
import com.xuecheng.framework.domain.course.TeachplanMedia;
import org.springframework.data.jpa.repository.JpaRepository;
public interface TeachplanMediaRepository extends JpaRepository<TeachplanMedia, String> {
}
前端实现
前端基本上已经全部实现了,我这里有一点修改因为我自己太欠了,当时写CoursePlan
的时候用的根路径是是course/teachplan
。
我需要在调用的API地址前面加上teachplan
。
/*保存媒资信息*/
export const savemedia = teachplanMedia => {
return http.requestPost(apiUrl+'/course/teachplan/savemedia',teachplanMedia);
}
视频信息回显(省略)
注意:
因为数据库中的courseId的确切字段名为:courseid
而不是courseId
,需要在实体类上加入如下代码:
@Column(name="courseid")
private String courseId;