zoukankan      html  css  js  c++  java
  • 学成在线(第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...
        }
    }
    View Code

    视频处理生成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);
        }
    }
    View Code

    说明:
    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);
        }
    }
    View Code

    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);
    }
    View Code

    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>
    View Code

    页面查询视频

    <el‐button style="font‐size: 12px;" type="text" on‐click={ () => this.querymedia(data.id) }>
    {data.mediaFileOriginalName}&nbsp;&nbsp;&nbsp;&nbsp;选择视频</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)
          }
      })
    },

  • 相关阅读:
    mac下远程win8.1时提示"桌面连接无法验证您希望连接的计算机的身份"的解决办法
    在sublime text3下,用快捷键把文件打开到浏览器中
    实现多行文字对齐的原理
    js中===、==、!=、!===的区别
    sublime格式化HTML+CSS插件--HTML-CSS-JS Prettify
    实现多行文字居中方法(兼容IE6)
    telnet localhost 8089 ==》》命令使用
    在Tomcat下部属项目三种方式:
    java服务器
    可变参数
  • 原文地址:https://www.cnblogs.com/anan-java/p/12293413.html
Copyright © 2011-2022 走看看