返回顶部

学成在线(第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)
      }
  })
},

posted @ 2020-02-10 23:22  桃花换了酒钱  阅读(474)  评论(0编辑  收藏  举报
/* * * * 烟花 */