原始視頻一般須要通過編碼處理,生成m3u8和ts文件方可基於HLS協議播放視頻。一般用戶上傳原始視頻,系統
自動處理成標準格式,系統對用戶上傳的視頻自動編碼、轉換,最終生成m3u8文件和ts文件,處理流程以下:
一、用戶上傳視頻成功
二、系統對上傳成功的視頻自動開始編碼處理
三、用戶查看視頻處理結果,沒有處理成功的視頻用戶可在管理界面再次觸發處理
四、視頻處理完成將視頻地址及處理結果保存到數據庫前端
視頻處理流程以下:java
視頻處理進程的任務是接收視頻處理消息進行視頻處理,業務流程以下:
一、監聽MQ,接收視頻處理消息。
二、進行視頻處理。
三、向數據庫寫入視頻處理結果。sql
視頻處理進程屬於媒資管理系統的一部分,考慮提升系統的擴展性,將視頻處理單獨定義視頻處理工程。數據庫
1)接收視頻處理消息
2)判斷媒體文件是否須要處理(本視頻處理程序目前只接收avi視頻的處理)
當前只有avi文件須要處理,其它文件須要更新處理狀態爲「無需處理」。
3)處理前初始化處理狀態爲「未處理」
4)處理失敗須要在數據庫記錄處理日誌,及處理狀態爲「處理失敗」
5)處理成功記錄處理狀態爲「處理成功」app
在MediaFile類中添加mediaFileProcess_m3u8屬性記錄ts文件列表,代碼以下:dom
//處理狀態 private String processStatus; //hls處理 private MediaFileProcess_m3u8 mediaFileProcess_m3u8;
@Data @ToString public class MediaFileProcess_m3u8 extends MediaFileProcess { //ts列表 private List<String> tslist; }
一、建立Dao
視頻處理結果須要保存到媒資數據庫,建立dao以下:ide
public interface MediaFileRepository extends MongoRepository<MediaFile,String> { }
二、在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
三、處理任務類
在mq包下建立MediaProcessTask類,此類負責監聽視頻處理隊列,並進行視頻處理。
整個視頻處理內容較多,這裏分兩部分實現:生成Mp4和生成m3u8,下邊代碼實現了生成mp4。this
@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及生成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如何判斷轉換成功?
第1、根據視頻時長來判斷,同mp4轉換成功的判斷方法。
第2、最後還要判斷m3u8文件內容是否完整。
當視頻上傳成功後向 MQ 發送視頻 處理消息。
修改媒資管理服務的文件上傳代碼,當文件上傳成功向MQ發送視頻處理消息。
一、將media-processor工程下的RabbitmqConfig配置類拷貝到media工程下。
二、在media工程下配置mq隊列等信息
修改application.yml
xc‐service‐manage‐media:
mq:
queue‐media‐video‐processor: queue_media_video_processor
routingkey‐media‐video: routingkey_media_video
在文件合併方法中添加向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); ......
測試流程:
一、上傳avi文件
二、觀察日誌是否發送消息
三、觀察視頻處理進程是否接收到消息進行處理
四、觀察mp4文件是否生成
五、觀察m3u8及 ts文件是否生成
經過個人媒資能夠查詢本教育機構擁有的媒資文件,進行文件處理、刪除文件、修改文件信息等操做,具體需求如
下:
一、分頁查詢個人媒資文件
二、刪除媒資文件
三、處理媒資文件
四、修改媒資文件信息
@Api(value = "媒體文件管理",description = "媒體文件管理接口",tags = {"媒體文件管理接口"}) public interface MediaFileControllerApi { @ApiOperation("查詢文件列表") public QueryResponseResult findList(int page, int size, QueryMediaFileRequest queryMediaFileRequest) ; }
@Repository public interface MediaFileDao extends MongoRepository<MediaFile,String> { }
定義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); } }
@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); } }
操做的業務流程以下:
一、進入課程計劃修改頁面
二、選擇視頻
打開媒資文件查詢窗口,找到該課程章節的視頻,選擇此視頻。
點擊「選擇媒資文件」打開媒資文件列表
三、 選擇成功後,將在課程管理數據庫保存課程計劃對應在的課程視頻地址。
在課程管理數據庫建立表 teachplan_media 存儲課程計劃與媒資關聯信息
用戶進入課程計劃頁面,選擇視頻,將課程計劃與視頻信息保存在課程管理數據庫中。
用戶操做流程:
一、進入課程計劃,點擊」選擇視頻「,打開個人媒資查詢頁面
二、爲課程計劃選擇對應的視頻,選擇「選擇」
三、前端請求課程管理服務保存課程計劃與視頻信息。
建立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; }
此接口做爲前端請求課程管理服務保存課程計劃與視頻信息的接口:
在課程管理服務增長接口:
@ApiOperation("保存媒資信息") public ResponseResult savemedia(TeachplanMedia teachplanMedia);
建立 TeachplanMediaRepository用於對TeachplanMedia的操做。
public interface TeachplanMediaRepository extends JpaRepository<TeachplanMedia, String> { }
//保存媒資信息 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); }
@Override @PostMapping("/savemedia") public ResponseResult savemedia(@RequestBody TeachplanMedia teachplanMedia) { return courseService.savemedia(teachplanMedia); }
課程計劃的視頻信息保存後在頁面沒法查看,本節解決課程計劃頁面顯示相關聯的媒資信息。
解決方案:
在獲取課程計劃樹結點信息時將關聯的媒資信息一併查詢,並在前端顯示,下圖說明了課程計劃顯示的區域。
修改課程計劃查詢的Dao:
一、修改模型
在課程計劃結果信息中添加媒資信息
@Data @ToString public class TeachplanNode extends Teachplan { List<TeachplanNode> children; //媒資信息 private String mediaId; private String mediaFileOriginalName; }
二、修改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) } }) },