媒资管理
视频处理
需求分析
原始视频通常需要经过编码处理,生成m3u8和ts文件方可基于HLS协议播放视频。通常用户上传原始视频,系统自动处理成标准格式,系统对用户上传的视频自动编码、转换,最终生成m3u8文件和ts文件,处理流程如下:
- 用户上传视频成功。
- 系统对上传成功的视频自动开始编码处理。
- 用户查看视频处理结果,没有处理成功的视频用户可在管理界面再次触发处理。
- 视频处理完成将视频地址及处理结果保存到数据库。
视频处理流程如下:
视频处理进程的任务是接收视频处理消息进行视频处理,业务流程如下:
- 监听MQ,接收视频处理消息。
- 进行视频处理。
- 向数据库写入视频处理结果
视频处理消费方
导入工程(省略)
application.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| server: port: 31450 spring: application: name: xc-service-manage-media-processor data: mongodb: uri: mongodb://root:123@localhost:27017 database: xc_media
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
| 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;
@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;
@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; }
@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; }
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151
| 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 {
@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}", 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")) { mediaFile.setProcessStatus("303004"); mediaFileRepository.save(mediaFile); return; } else { mediaFile.setProcessStatus("303001"); mediaFileRepository.save(mediaFile); }
String mp4_name = mediaFile.getFileId() + ".mp4"; if (!buildMp4(mediaFile)) { ExceptionCast.cast(MediaCode.MEDIA_BUILD_MP4_FAIL); }
if (!buildM3u8(mediaFile, mp4_name)) { ExceptionCast.cast(MediaCode.MEDIA_BUILD_M3U8_FAIL); }
log.info("[视频处理] 视频处理完成, mediaId = [{}]", mediaId);
}
private boolean buildM3u8(MediaFile mediaFile, String mp4Name) { String video_path = serverPath + mediaFile.getFilePath() + mp4Name; String m3u8_name = mediaFile.getFileId() + ".m3u8"; String m3u8folder_path = serverPath + mediaFile.getFilePath() + "hls/"; 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; } 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); mediaFile.setFileUrl(mediaFile.getFilePath() + "hls/" + m3u8_name); mediaFileRepository.save(mediaFile);
return true; }
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; }
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
新增配置
1 2 3 4 5 6 7 8 9 10 11
| 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| 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;
@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;
@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; }
@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(); } }
|
新增消息发送方法并在mergeChunks
方法的最后调用该方法完成视频处理消息的发送。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| @Value("${xc-service-manage-media.mq.routingkey-media-video}") private String routingkey_media_video;
@Autowired private RabbitTemplate rabbitTemplate;
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、修改媒资文件信息
后端实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| 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 {
QueryResponseResult findList(int page, int size, QueryMediaFileRequest queryMediaFileRequest);
ResponseResult delete(String id);
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| 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); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| 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;
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); }
public void delete(String id) { mediaFileRepository.deleteById(id); } }
|
前端实现
我这里前端主要修改了处理状态
的下拉选择框的数据从数据字典动态获取。
修改media_list.vue
的mounted
内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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定义
1 2
| @ApiOperation("保存媒资信息") ResponseResult saveMedia(TeachplanMedia teachplanMedia);
|
CoursePlanController
新增接口实现
1 2 3 4 5 6 7
| @Override @PostMapping("savemedia") public ResponseResult saveMedia(@RequestBody TeachplanMedia teachplanMedia) { TeachplanMedia saveMedia = courseService.saveMedia(teachplanMedia); isNullOrEmpty(saveMedia, CommonCode.SERVER_ERROR); return ResponseResult.SUCCESS(); }
|
CourseService
新增方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| @Autowired private TeachplanMediaRepository teachplanMediaRepository;
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); }
|
1 2 3 4 5 6 7
| 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
。
1 2 3 4
| export const savemedia = teachplanMedia => { return http.requestPost(apiUrl+'/course/teachplan/savemedia',teachplanMedia); }
|
视频信息回显(省略)
注意:
因为数据库中的courseId的确切字段名为:courseid
而不是courseId
,需要在实体类上加入如下代码:
1 2
| @Column(name="courseid") private String courseId;
|
代码获取
代码获取