学成在线(第14天)媒资管理
视频处理
需求分析
原始视频通常需要经过编码处理,生成m3u8和ts文件方可基于HLS协议播放视频。通常用户上传原始视频,系统
自动处理成标准格式,系统对用户上传的视频自动编码、转换,最终生成m3u8文件和ts文件,处理流程如下:
1、用户上传视频成功
2、系统对上传成功的视频自动开始编码处理
3、用户查看视频处理结果,没有处理成功的视频用户可在管理界面再次触发处理
4、视频处理完成将视频地址及处理结果保存到数据库
视频处理流程如下:
视频处理进程的任务是接收视频处理消息进行视频处理,业务流程如下:
1、监听MQ,接收视频处理消息。
2、进行视频处理。
3、向数据库写入视频处理结果。
视频处理进程属于媒资管理系统的一部分,考虑提高系统的扩展性,将视频处理单独定义视频处理工程。
视频处理实现
处理流程
1)接收视频处理消息
2)判断媒体文件是否需要处理(本视频处理程序目前只接收avi视频的处理)
当前只有avi文件需要处理,其它文件需要更新处理状态为“无需处理”。
3)处理前初始化处理状态为“未处理”
4)处理失败需要在数据库记录处理日志,及处理状态为“处理失败”
5)处理成功记录处理状态为“处理成功”
数据模型
在MediaFile类中添加mediaFileProcess_m3u8属性记录ts文件列表,代码如下:
//处理状态 private String processStatus; //hls处理 private MediaFileProcess_m3u8 mediaFileProcess_m3u8;
@Data @ToString public class MediaFileProcess_m3u8 extends MediaFileProcess { //ts列表 private List<String> tslist; }
视频处理生成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.upload‐location}") String serverPath; @Autowired MediaFileRepository mediaFileRepository; @RabbitListener(queues = "${xc‐service‐manage‐media.mq.queue‐media‐processtask}") public void receiveMediaProcessTask(String msg) throws IOException { Map msgMap = JSON.parseObject(msg, Map.class); LOGGER.info("receive media process task msg :{} ",msgMap); //解析消息 //媒资文件id String mediaId = (String) msgMap.get("mediaId"); //获取媒资文件信息 Optional<MediaFile> optional = mediaFileRepository.findById(fileMd5); if(!optional.isPresent()){ return ; } MediaFile mediaFile = optional.get(); //媒资文件类型 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 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")){ //操作失败写入处理日志 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
下边是完整的视频处理任务类代码,包括了生成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.upload‐location}") String serverPath; @Autowired MediaFileRepository mediaFileRepository; @RabbitListener(queues = "${xc‐service‐manage‐media.mq.queue‐media‐processtask}") public void receiveMediaProcessTask(String msg) throws IOException { Map msgMap = JSON.parseObject(msg, Map.class); LOGGER.info("receive media process task msg :{} ",msgMap); //解析消息 //媒资文件id String mediaId = (String) msgMap.get("mediaId"); //获取媒资文件信息 Optional<MediaFile> optional = mediaFileRepository.findById(fileMd5); if(!optional.isPresent()){ return ; } MediaFile mediaFile = optional.get(); //媒资文件类型 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 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")){ //操作失败写入处理日志 mediaFile.setProcessStatus("303003");//处理状态为处理失败 MediaFileProcess_m3u8 mediaFileProcess_m3u8 = new MediaFileProcess_m3u8(); mediaFileProcess_m3u8.setErrormsg(result); mediaFile.setMediaFileProcess_m3u8(mediaFileProcess_m3u8); mediaFileRepository.save(mediaFile); return ; } //生成m3u8 video_path = serverPath + mediaFile.getFilePath()+mp4_name;//此地址为mp4的地址 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); result = hlsVideoUtil.generateM3u8(); 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列表 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); } }
说明:
mp4转成m3u8如何判断转换成功?
第一、根据视频时长来判断,同mp4转换成功的判断方法。
第二、最后还要判断m3u8文件内容是否完整。
发送视频处理消息
当视频上传成功后向 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发送视频处理消息的代码:
//向MQ发送视频处理消息 public ResponseResult sendProcessVideoMsg(String mediaId){ Optional<MediaFile> optional = mediaFileRepository.findById(fileMd5); 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); }
在mergechunks方法最后调用sendProcessVideo方法。
...... //状态为上传成功 mediaFile.setFileStatus("301002"); mediaFileRepository.save(mediaFile); String mediaId = mediaFile.getFileId(); //向MQ发送视频处理消息 sendProcessVideoMsg(mediaId); ......
视频处理测试
测试流程:
1、上传avi文件
2、观察日志是否发送消息
3、观察视频处理进程是否接收到消息进行处理
4、观察mp4文件是否生成
5、观察m3u8及 ts文件是否生成
我的媒资
通过我的媒资可以查询本教育机构拥有的媒资文件,进行文件处理、删除文件、修改文件信息等操作,具体需求如
下:
1、分页查询我的媒资文件
2、删除媒资文件
3、处理媒资文件
4、修改媒资文件信息
API
@Api(value = "媒体文件管理",description = "媒体文件管理接口",tags = {"媒体文件管理接口"}) public interface MediaFileControllerApi { @ApiOperation("查询文件列表") public QueryResponseResult findList(int page, int size, QueryMediaFileRequest queryMediaFileRequest) ; }
服务端开发
Dao
@Repository public interface MediaFileDao extends MongoRepository<MediaFile,String> { }
Service
定义findList方法实现媒资文件查询列表。
@Service public class MediaFileService { private static Logger logger = LoggerFactory.getLogger(MediaFileService.class); @Autowired MediaFileRepository mediaFileRepository; //文件列表分页查询 public QueryResponseResult findList(int page,int size,QueryMediaFileRequest queryMediaFileRequest){ //查询条件 MediaFile mediaFile = new MediaFile(); if(queryMediaFileRequest == null){ queryMediaFileRequest = new QueryMediaFileRequest(); } //查询条件匹配器 ExampleMatcher matcher = ExampleMatcher.matching() .withMatcher("tag", ExampleMatcher.GenericPropertyMatchers.contains())//tag字段 模糊匹配 .withMatcher("fileOriginalName", ExampleMatcher.GenericPropertyMatchers.contains())//文件原始名称模糊匹配 .withMatcher("processStatus", ExampleMatcher.GenericPropertyMatchers.exact());// 处理状态精确匹配(默认) //查询条件对象 if(StringUtils.isNotEmpty(queryMediaFileRequest.getTag())){ mediaFile.setTag(queryMediaFileRequest.getTag()); } if(StringUtils.isNotEmpty(queryMediaFileRequest.getFileOriginalName())){ mediaFile.setFileOriginalName(queryMediaFileRequest.getFileOriginalName()); } if(StringUtils.isNotEmpty(queryMediaFileRequest.getProcessStatus())){ mediaFile.setProcessStatus(queryMediaFileRequest.getProcessStatus()); } //定义example实例 Example<MediaFile> ex = Example.of(mediaFile, matcher); page = page‐1; //分页参数 Pageable pageable = new PageRequest(page, size); //分页查询 Page<MediaFile> all = mediaFileRepository.findAll(ex,pageable); QueryResult<MediaFile> mediaFileQueryResult = new QueryResult<MediaFile>(); mediaFileQueryResult.setList(all.getContent()); mediaFileQueryResult.setTotal(all.getTotalElements()); return new QueryResponseResult(CommonCode.SUCCESS,mediaFileQueryResult); } }
Controller
@RestController @RequestMapping("/media/file") public class MediaFileController implements MediaFileControllerApi { @Autowired MediaFileService mediaFileService; @Autowired MediaUploadService mediaUploadService; @Override @GetMapping("/list/{page}/{size}") public QueryResponseResult findList(@PathVariable("page") int page, @PathVariable("size") int size, QueryMediaFileRequest queryMediaFileRequest) { //媒资文件查询 return mediaFileService.findList(page,size,queryMediaFileRequest); } }
媒资与课程计划关联
操作的业务流程如下:
1、进入课程计划修改页面
2、选择视频
打开媒资文件查询窗口,找到该课程章节的视频,选择此视频。
点击“选择媒资文件”打开媒资文件列表
3、 选择成功后,将在课程管理数据库保存课程计划对应在的课程视频地址。
在课程管理数据库创建表 teachplan_media 存储课程计划与媒资关联信息
保存视频信息
需求分析
用户进入课程计划页面,选择视频,将课程计划与视频信息保存在课程管理数据库中。
用户操作流程:
1、进入课程计划,点击”选择视频“,打开我的媒资查询页面
2、为课程计划选择对应的视频,选择“选择”
3、前端请求课程管理服务保存课程计划与视频信息。
数据模型
创建teachplanMedia 模型类:
@Data @ToString @Entity @Table(name="teachplan_media") @GenericGenerator(name = "jpa‐assigned", strategy = "assigned") public class TeachplanMedia implements Serializable { private static final long serialVersionUID = ‐916357110051689485L; @Id @GeneratedValue(generator = "jpa‐assigned") @Column(name="teachplan_id") private String teachplanId; @Column(name="media_id") private String mediaId; @Column(name="media_fileoriginalname") private String mediaFileOriginalName; @Column(name="media_url") private String mediaUrl; @Column(name="courseid") private String courseId; }
API接口
此接口作为前端请求课程管理服务保存课程计划与视频信息的接口:
在课程管理服务增加接口:
@ApiOperation("保存媒资信息") public ResponseResult savemedia(TeachplanMedia teachplanMedia);
服务端开发
DAO
创建 TeachplanMediaRepository用于对TeachplanMedia的操作。
public interface TeachplanMediaRepository extends JpaRepository<TeachplanMedia, String> { }
Service
//保存媒资信息 public ResponseResult savemedia(TeachplanMedia teachplanMedia) { if(teachplanMedia == null){ ExceptionCast.cast(CommonCode.INVALIDPARAM); } //课程计划 String teachplanId = teachplanMedia.getTeachplanId(); //查询课程计划 Optional<Teachplan> optional = teachplanRepository.findById(teachplanId); if(!optional.isPresent()){ ExceptionCast.cast(CourseCode.COURSE_MEDIA_TEACHPLAN_ISNULL); } Teachplan teachplan = optional.get(); //只允许为叶子结点课程计划选择视频 String grade = teachplan.getGrade(); if(StringUtils.isEmpty(grade) || !grade.equals("3")){ ExceptionCast.cast(CourseCode.COURSE_MEDIA_TEACHPLAN_GRADEERROR); } TeachplanMedia one = null; Optional<TeachplanMedia> teachplanMediaOptional = teachplanMediaRepository.findById(teachplanId); if(!teachplanMediaOptional.isPresent()){ one = new TeachplanMedia(); }else{ one = teachplanMediaOptional.get(); } //保存媒资信息与课程计划信息 one.setTeachplanId(teachplanId); one.setCourseId(teachplanMedia.getCourseId()); one.setMediaFileOriginalName(teachplanMedia.getMediaFileOriginalName()); one.setMediaId(teachplanMedia.getMediaId()); one.setMediaUrl(teachplanMedia.getMediaUrl()); teachplanMediaRepository.save(one); return new ResponseResult(CommonCode.SUCCESS); }
Controller
@Override @PostMapping("/savemedia") public ResponseResult savemedia(@RequestBody TeachplanMedia teachplanMedia) { return courseService.savemedia(teachplanMedia); }
查询视频信息
需求分析
课程计划的视频信息保存后在页面无法查看,本节解决课程计划页面显示相关联的媒资信息。
解决方案:
在获取课程计划树结点信息时将关联的媒资信息一并查询,并在前端显示,下图说明了课程计划显示的区域。
Dao
修改课程计划查询的Dao:
1、修改模型
在课程计划结果信息中添加媒资信息
@Data @ToString public class TeachplanNode extends Teachplan { List<TeachplanNode> children; //媒资信息 private String mediaId; private String mediaFileOriginalName; }
2、修改sql语句,添加关联查询媒资信息
添加mediaId、mediaFileOriginalName
<resultMap type="com.xuecheng.framework.domain.course.ext.TeachplanNode" id="teachplanMap" > <id property="id" column="one_id"/> <result property="pname" column="one_name"/> <result property="grade" column="one_grade"/> <collection property="children" ofType="com.xuecheng.framework.domain.course.ext.TeachplanNode"> <id property="id" column="two_id"/> <result property="pname" column="two_name"/> <result property="grade" column="two_grade"/> <collection property="children" ofType="com.xuecheng.framework.domain.course.ext.TeachplanNode"> <id property="id" column="three_id"/> <result property="pname" column="three_name"/> <result property="grade" column="three_grade"/> <result property="mediaId" column="mediaId"/> <result property="mediaFileOriginalName" column="mediaFileOriginalName"/> </collection> </collection> </resultMap> <select id="selectList" resultMap="teachplanMap" parameterType="java.lang.String" > SELECT a.id one_id, a.pname one_name, a.grade one_grade, a.orderby one_orderby, b.id two_id, b.pname two_name, b.grade two_grade, b.orderby two_orderby, c.id three_id, c.pname three_name, c.grade three_grade, c.orderby three_orderby, media.media_id mediaId, media.media_fileoriginalname mediaFileOriginalName FROM teachplan a LEFT JOIN teachplan b ON a.id = b.parentid LEFT JOIN teachplan c ON b.id = c.parentid LEFT JOIN teachplan_media media ON c.id = media.teachplan_id WHERE a.parentid = '0' <if test="_parameter!=null and _parameter!=''"> and a.courseid=#{courseId} </if> ORDER BY a.orderby, b.orderby, c.orderby </select>
页面查询视频
<el‐button style="font‐size: 12px;" type="text" on‐click={ () => this.querymedia(data.id) }> {data.mediaFileOriginalName} 选择视频</el‐button>
选择视频后立即刷新课程计划树,在提交成功后,添加查询课程计划代码:this.findTeachplan(),完整代码如下:
choosemedia(mediaId,fileOriginalName,mediaUrl){ this.mediaFormVisible = false; //保存课程计划与视频对应关系 let teachplanMedia = {}; teachplanMedia.teachplanId = this.activeTeachplanId; teachplanMedia.mediaId = mediaId; teachplanMedia.mediaFileOriginalName = fileOriginalName; teachplanMedia.mediaUrl = mediaUrl; teachplanMedia.courseId = this.courseid; //保存媒资信息到课程数据库 courseApi.savemedia(teachplanMedia).then(res=>{ if(res.success){ this.$message.success("选择视频成功") //查询课程计划 this.findTeachplan() }else{ this.$message.error(res.message) } }) },