學成在線(第14天)

 視頻處理

 需求分析

原始視頻一般須要通過編碼處理,生成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;
}

 視頻處理生成Mp4

一、建立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...
    }
}
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如何判斷轉換成功?
第1、根據視頻時長來判斷,同mp4轉換成功的判斷方法。
第2、最後還要判斷m3u8文件內容是否完整。

發送視頻處理消息

當視頻上傳成功後向 MQ 發送視頻 處理消息。
修改媒資管理服務的文件上傳代碼,當文件上傳成功向MQ發送視頻處理消息。

RabbitMQ配置

一、將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

修改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);
......

 視頻處理測試

測試流程:
一、上傳avi文件
二、觀察日誌是否發送消息
三、觀察視頻處理進程是否接收到消息進行處理
四、觀察mp4文件是否生成
五、觀察m3u8及 ts文件是否生成

 

 個人媒資

經過個人媒資能夠查詢本教育機構擁有的媒資文件,進行文件處理、刪除文件、修改文件信息等操做,具體需求如
下:
一、分頁查詢個人媒資文件
二、刪除媒資文件
三、處理媒資文件
四、修改媒資文件信息

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

 

  媒資與課程計劃關聯

操做的業務流程以下:
一、進入課程計劃修改頁面
二、選擇視頻
打開媒資文件查詢窗口,找到該課程章節的視頻,選擇此視頻。
點擊「選擇媒資文件」打開媒資文件列表

三、 選擇成功後,將在課程管理數據庫保存課程計劃對應在的課程視頻地址。
在課程管理數據庫建立表 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;
}

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:
一、修改模型
在課程計劃結果信息中添加媒資信息

@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>
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)
      }
  })
},

相關文章
相關標籤/搜索