視頻處理
需求分析
原始視頻通常需要經過編碼處理,生成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;