學成在線筆記十:媒資管理

視頻處理

需求分析

原始視頻通常需要經過編碼處理,生成m3u8和ts文件方可基於HLS協議播放視頻。通常用戶上傳原始視頻,系統自動處理成標準格式,系統對用戶上傳的視頻自動編碼、轉換,最終生成m3u8文件和ts文件,處理流程如下:

  1. 用戶上傳視頻成功。
  2. 系統對上傳成功的視頻自動開始編碼處理。
  3. 用戶查看視頻處理結果,沒有處理成功的視頻用戶可在管理界面再次觸發處理。
  4. 視頻處理完成將視頻地址及處理結果保存到數據庫。

視頻處理流程如下:

視頻處理進程的任務是接收視頻處理消息進行視頻處理,業務流程如下:

  1. 監聽MQ,接收視頻處理消息。
  2. 進行視頻處理。
  3. 向數據庫寫入視頻處理結果

視頻處理消費方

導入工程(省略)

application.yml

server:
  port: 31450
spring:
  application:
    name: xc-service-manage-media-processor
  data:
    mongodb:
      uri:  mongodb://root:123@localhost:27017
      database: xc_media
#rabbitmq配置
  rabbitmq:
    host: 192.168.136.110
    port: 5672
    username: xcEdu
    password: 123456
    virtual-host: /
xc-service-manage-media:
  mq:
    queue-media-video-processor: queue_media_video_processor
    routingkey-media-video: routingkey_media_video
  video-location: ${MEDIA_FILE_LOCATION:E:/nginx/xcEdu/video/}
  ffmpeg-path: F:/ffmpeg-20180227-fa0c9d6-win64-static/bin/ffmpeg.exe

RabbitMQConfig

package com.xuecheng.manage_media_process.config;

import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.amqp.SimpleRabbitListenerContainerFactoryConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author Administrator
 * @version 1.0
 * @create 2018-07-12 9:04
 **/
@Configuration
public class RabbitMQConfig {

    public static final String EX_MEDIA_PROCESSTASK = "ex_media_processor";

    //視頻處理隊列
    @Value("${xc-service-manage-media.mq.queue-media-video-processor}")
    public String queue_media_video_processtask;

    //視頻處理路由
    @Value("${xc-service-manage-media.mq.routingkey-media-video}")
    public String routingkey_media_video;

    //消費者併發數量
    public static final int DEFAULT_CONCURRENT = 10;


    /**
     * 交換機配置
     *
     * @return the exchange
     */
    @Bean(EX_MEDIA_PROCESSTASK)
    public Exchange EX_MEDIA_VIDEOTASK() {
        return ExchangeBuilder.directExchange(EX_MEDIA_PROCESSTASK).durable(true).build();
    }

    //聲明隊列
    @Bean("queue_media_video_processtask")
    public Queue QUEUE_PROCESSTASK() {
        Queue queue = new Queue(queue_media_video_processtask, true, false, true);
        return queue;
    }

    /**
     * 綁定隊列到交換機 .
     *
     * @param queue    the queue
     * @param exchange the exchange
     * @return the binding
     */
    @Bean
    public Binding binding_queue_media_processtask(@Qualifier("queue_media_video_processtask") Queue queue, @Qualifier(EX_MEDIA_PROCESSTASK) Exchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with(routingkey_media_video).noargs();
    }


    @Bean("customContainerFactory")
    public SimpleRabbitListenerContainerFactory containerFactory(SimpleRabbitListenerContainerFactoryConfigurer configurer,
                                                                 ConnectionFactory connectionFactory) {
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConcurrentConsumers(DEFAULT_CONCURRENT);
        factory.setMaxConcurrentConsumers(DEFAULT_CONCURRENT);
        configurer.configure(factory, connectionFactory);
        return factory;
    }

}

MediaProcessTask

package com.xuecheng.manage_media_process.mq;

import com.alibaba.fastjson.JSON;
import com.xuecheng.framework.domain.media.MediaFile;
import com.xuecheng.framework.domain.media.MediaFileProcess_m3u8;
import com.xuecheng.framework.domain.media.response.MediaCode;
import com.xuecheng.framework.exception.ExceptionCast;
import com.xuecheng.framework.utils.HlsVideoUtil;
import com.xuecheng.framework.utils.Mp4VideoUtil;
import com.xuecheng.manage_media_process.dao.MediaFileRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Map;

@Slf4j
@Component
public class MediaProcessTask {


    //ffmpeg絕對路徑
    @Value("${xc‐service‐manage‐media.ffmpeg‐path}")
    String ffmpeg_path;

    //上傳文件根目錄
    @Value("${xc‐service‐manage‐media.video‐location}")
    String serverPath;

    @Autowired
    MediaFileRepository mediaFileRepository;


    /**
     * 接收視頻處理消息並處理對應視頻格式
     *
     * @param msg 消息
     */
    @RabbitListener(queues = "${xc‐service‐manage‐media.mq.queue-media-video-processor}", containerFactory = "customContainerFactory")
    public void receiveMediaProcessTask(String msg) {
        Map<String, String> msgMap = JSON.parseObject(msg, Map.class);
        log.info("[視頻處理] 收到視頻處理消息, msg = {}", msgMap.toString());

        String mediaId = msgMap.get("mediaId");

        // 獲取文件
        MediaFile mediaFile = mediaFileRepository.findById(mediaId).orElse(null);
        if (mediaFile == null) {
            ExceptionCast.cast(MediaCode.MEDIA_FILE_NOT_EXIST);
        }
        // 判斷文件類型
        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 mp4_name = mediaFile.getFileId() + ".mp4";
        if (!buildMp4(mediaFile)) {
            ExceptionCast.cast(MediaCode.MEDIA_BUILD_MP4_FAIL);
        }

        // 生成m3u8
        if (!buildM3u8(mediaFile, mp4_name)) {
            ExceptionCast.cast(MediaCode.MEDIA_BUILD_M3U8_FAIL);
        }

        log.info("[視頻處理] 視頻處理完成, mediaId = [{}]", mediaId);

    }

    /**
     * 使用MP4文件生成m3u8文件並保存信息到數據庫
     *
     * @param mediaFile 源文件
     * @param mp4Name   mp4文件名
     * @return boolean
     */
    private boolean buildM3u8(MediaFile mediaFile, String mp4Name) {
        // mp4 url
        String video_path = serverPath + mediaFile.getFilePath() + mp4Name;
        // 生成後文件的存放位置
        String m3u8_name = mediaFile.getFileId() + ".m3u8";
        String m3u8folder_path = serverPath + mediaFile.getFilePath() + "hls/";
        // 生成m3u8文件
        HlsVideoUtil hlsVideoUtil = new HlsVideoUtil(ffmpeg_path, video_path, m3u8_name, m3u8folder_path);
        String result = hlsVideoUtil.generateM3u8();
        if (result == null || !result.equals("success")) {
            //操作失敗寫入處理日誌
            processFail(result, mediaFile);
            return false;
        }
        //獲取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);

        return true;
    }

    /**
     * 生成MP4文件
     *
     * @param mediaFile 源文件
     * @return boolean
     */
    private boolean buildMp4(MediaFile mediaFile) {
        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")) {
            //操作失敗寫入處理日誌
            processFail(result, mediaFile);
            return false;
        }

        return true;
    }

    /**
     * 操作失敗
     *
     * @param result    操作結果
     * @param mediaFile 文件
     */
    private void processFail(String result, MediaFile mediaFile) {
        mediaFile.setProcessStatus("303003");//處理狀態爲處理失敗
        MediaFileProcess_m3u8 mediaFileProcess_m3u8 = new MediaFileProcess_m3u8();
        mediaFileProcess_m3u8.setErrormsg(result);
        mediaFile.setMediaFileProcess_m3u8(mediaFileProcess_m3u8);
        mediaFileRepository.save(mediaFile);
    }


}

視頻處理髮送方

修改xc-service-manage-media相關代碼

application.yml

新增配置

spring:
  rabbitmq:
    host: 192.168.136.110
    port: 5672
    username: xcEdu
    password: 123456
    virtual-host: /
xc-service-manage-media:
  mq:
    queue-media-video-processor: queue_media_video_processor
    routingkey-media-video: routingkey_media_video

RabbitMQConfig

package com.xuecheng.manage_media.config;

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author Administrator
 * @version 1.0
 * @create 2018-07-12 9:04
 **/
@Configuration
public class RabbitMQConfig {

    public static final String EX_MEDIA_PROCESSTASK = "ex_media_processor";

    //視頻處理隊列
    @Value("${xc-service-manage-media.mq.queue-media-video-processor}")
    public String queue_media_video_processtask;

    //視頻處理路由
    @Value("${xc-service-manage-media.mq.routingkey-media-video}")
    public String routingkey_media_video;

    //消費者併發數量
    public static final int DEFAULT_CONCURRENT = 10;


    /**
     * 交換機配置
     *
     * @return the exchange
     */
    @Bean(EX_MEDIA_PROCESSTASK)
    public Exchange EX_MEDIA_VIDEOTASK() {
        return ExchangeBuilder.directExchange(EX_MEDIA_PROCESSTASK).durable(true).build();
    }

    //聲明隊列
    @Bean("queue_media_video_processtask")
    public Queue QUEUE_PROCESSTASK() {
        Queue queue = new Queue(queue_media_video_processtask, true, false, true);
        return queue;
    }

    /**
     * 綁定隊列到交換機 .
     *
     * @param queue    the queue
     * @param exchange the exchange
     * @return the binding
     */
    @Bean
    public Binding binding_queue_media_processtask(@Qualifier("queue_media_video_processtask") Queue queue, @Qualifier(EX_MEDIA_PROCESSTASK) Exchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with(routingkey_media_video).noargs();
    }
}

MediaUploadService

新增消息發送方法並在mergeChunks方法的最後調用該方法完成視頻處理消息的發送。

    @Value("${xc-service-manage-media.mq.routingkey-media-video}")
    private String routingkey_media_video;

    @Autowired
    private RabbitTemplate rabbitTemplate;

	/**
     * 發送視頻處理消息
     *
     * @param mediaId 視頻id
     */
    public void sendProcessVideoMsg(String mediaId) {
        Optional<MediaFile> optional = mediaFileRepository.findById(mediaId);
        if (!optional.isPresent()) {
            ExceptionCast.cast(CommonCode.FAIL);
        }
        //發送視頻處理消息
        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);
            log.info("[發送視頻處理消息] 文件上傳完成, 發送視頻處理消息. msg = {}", msg);
        } catch (Exception e) {
            log.info("[發送視頻處理消息] 文件上傳完成, 發送視頻處理消息失敗. msg = {}", msg, e.getMessage());
            ExceptionCast.cast(CommonCode.FAIL);
        }

    }

測試

在課程管理前端選擇文件並上傳


查看控制檯,正在執行MP4文件生成

查看最終生成的m3u8文件列表

我的媒資

需求分析

通過我的媒資可以查詢本教育機構擁有的媒資文件,進行文件處理、刪除文件、修改文件信息等操作,具體需求如下:

1、分頁查詢我的媒資文件

2、刪除媒資文件

3、處理媒資文件

4、修改媒資文件信息

後端實現

MediaFileControllerApi

package com.xuecheng.api.media;

import com.xuecheng.framework.domain.media.request.QueryMediaFileRequest;
import com.xuecheng.framework.model.response.QueryResponseResult;
import com.xuecheng.framework.model.response.ResponseResult;
import io.swagger.annotations.Api;

@Api(value="媒資管理接口",description="提供媒資文件數據的增刪改查")
public interface MediaFileControllerApi {

    /**
     * 分頁查詢媒資文件列表
     *
     * @param page                  當前頁碼
     * @param size                  每頁記錄數
     * @param queryMediaFileRequest 查詢條件
     * @return QueryResponseResult
     */
    QueryResponseResult findList(int page, int size, QueryMediaFileRequest queryMediaFileRequest);

    /**
     * 刪除媒資文件
     *
     * @param id 媒資文件ID
     * @return ResponseResult
     */
    ResponseResult delete(String id);


}

MediaFileController

package com.xuecheng.manage_media.controller;

import com.xuecheng.api.media.MediaFileControllerApi;
import com.xuecheng.framework.domain.media.request.QueryMediaFileRequest;
import com.xuecheng.framework.model.response.CommonCode;
import com.xuecheng.framework.model.response.QueryResponseResult;
import com.xuecheng.framework.model.response.ResponseResult;
import com.xuecheng.framework.web.BaseController;
import com.xuecheng.manage_media.service.MediaFileService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("media/file")
public class MediaFileController extends BaseController implements MediaFileControllerApi {


    @Autowired
    private MediaFileService mediaFileService;

    @Override
    @GetMapping("list/{page}/{size}")
    public QueryResponseResult findList(@PathVariable int page,
                                        @PathVariable int size, QueryMediaFileRequest queryMediaFileRequest) {
        return mediaFileService.findList(page, size, queryMediaFileRequest);
    }

    @Override
    @DeleteMapping("{id}")
    public ResponseResult delete(@PathVariable String id) {
        mediaFileService.delete(id);
        return new ResponseResult(CommonCode.SUCCESS);
    }
}

MediaFileService

package com.xuecheng.manage_media.service;

import com.xuecheng.framework.domain.media.MediaFile;
import com.xuecheng.framework.domain.media.request.QueryMediaFileRequest;
import com.xuecheng.framework.model.response.CommonCode;
import com.xuecheng.framework.model.response.QueryResponseResult;
import com.xuecheng.framework.model.response.QueryResult;
import com.xuecheng.framework.service.BaseService;
import com.xuecheng.manage_media.dao.MediaFileRepository;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class MediaFileService extends BaseService {

    @Autowired
    private MediaFileRepository mediaFileRepository;

    /**
     * 分頁查詢媒資文件列表
     *
     * @param page                  當前頁碼
     * @param size                  每頁記錄數
     * @param queryMediaFileRequest 查詢條件
     * @return QueryResponseResult
     */
    public QueryResponseResult findList(int page, int size, QueryMediaFileRequest queryMediaFileRequest) {
        // 處理分頁參數
        if (page <= 0) {
            page = 1;
        }
        page = page - 1;

        // 處理分頁查詢條件
        if (queryMediaFileRequest == null) {
            queryMediaFileRequest = new QueryMediaFileRequest();
        }
        MediaFile mediaFile = new MediaFile();
        if (StringUtils.isNotBlank(queryMediaFileRequest.getFileOriginalName())) {
            mediaFile.setFileOriginalName(queryMediaFileRequest.getFileOriginalName());
        }
        if (StringUtils.isNotBlank(queryMediaFileRequest.getProcessStatus())) {
            mediaFile.setProcessStatus(queryMediaFileRequest.getProcessStatus());
        }
        if (StringUtils.isNotBlank(queryMediaFileRequest.getTag())) {
            mediaFile.setTag(queryMediaFileRequest.getTag());
        }

        ExampleMatcher exampleMatcher = ExampleMatcher.matching()
                .withMatcher("fileOriginalName", ExampleMatcher.GenericPropertyMatchers.contains())
                .withMatcher("tag", ExampleMatcher.GenericPropertyMatchers.contains());

        Example<MediaFile> example = Example.of(mediaFile, exampleMatcher);

        // 查詢
        Page<MediaFile> mediaFiles = mediaFileRepository.findAll(example, PageRequest.of(page, size));

        QueryResult<MediaFile> queryResult = new QueryResult<>(mediaFiles.getContent(), mediaFiles.getTotalElements());
        return new QueryResponseResult(CommonCode.SUCCESS, queryResult);
    }

    /**
     * 刪除指定ID的mediaFile
     *
     * @param id 媒資文件ID
     */
    public void delete(String id) {
        mediaFileRepository.deleteById(id);
    }
}

前端實現

我這裏前端主要修改了處理狀態的下拉選擇框的數據從數據字典動態獲取。

修改media_list.vuemounted內容

mounted() {
      //默認查詢頁面
      this.query()
      //初始化處理狀態
      //查詢數據字典字典
      this.processStatusList = [
        {
          id:'',
          name:'全部'
        }
      ]
      // 查詢數據庫獲取數據
      systemApi.sys_getDictionary('303').then((res) => {
        res.dvalue.forEach((element) => {
          let data = {}
          data.id = element.sdId
          data.name = element.sdName
          this.processStatusList.push(data)
        })
      })
      
    }

注意:

還需要在前面引入查詢數據字典的API定義:import * as systemApi from '../../../base/api/system'

在導入的數據庫中會發現有兩個數據字典的type都爲303,查詢會報錯,所以我修改了另外一個的type403

媒資與課程計劃關聯

需求分析

  1. 進入課程計劃修改頁面。
  2. 選擇視頻。
  3. 選擇成功後,將在課程管理數據庫保存課程計劃對應在的課程視頻地址。

後端實現

修改xc-service-manage-course中相關代碼完成功能

CoursePlanControllerApi

修增API定義

    @ApiOperation("保存媒資信息")
    ResponseResult saveMedia(TeachplanMedia teachplanMedia);

CoursePlanController

新增接口實現

    @Override
    @PostMapping("savemedia")
    public ResponseResult saveMedia(@RequestBody TeachplanMedia teachplanMedia) {
        TeachplanMedia saveMedia = courseService.saveMedia(teachplanMedia);
        isNullOrEmpty(saveMedia, CommonCode.SERVER_ERROR);
        return ResponseResult.SUCCESS();
    }

CourseService

新增方法

    @Autowired
    private TeachplanMediaRepository teachplanMediaRepository;

	/**
     * 保存課程計劃關聯媒資數據
     *
     * @param teachplanMedia 關聯樹數據
     * @return TeachplanMedia
     */
    public TeachplanMedia saveMedia(TeachplanMedia teachplanMedia) {
        isNullOrEmpty(teachplanMedia, CommonCode.PARAMS_ERROR);
        // 查詢課程計劃
        Teachplan teachplan = coursePlanRepository.findById(teachplanMedia.getTeachplanId()).orElse(null);
        isNullOrEmpty(teachplan, CourseCode.COURSE_MEDIS_TEACHPLAN_IS_NULL);

        // 只允許葉子節點選擇視頻
        String grade = teachplan.getGrade();
        if (StringUtils.isEmpty(grade) || !grade.equals("3")) {
            ExceptionCast.cast(CourseCode.COURSE_MEDIA_TEACHPLAN_GRADE_ERROR);
        }
        TeachplanMedia media;

        Optional<TeachplanMedia> teachplanMediaOptional = teachplanMediaRepository.findById(teachplanMedia.getTeachplanId());
        media = teachplanMediaOptional.orElseGet(TeachplanMedia::new);

        //保存媒資信息與課程計劃信息
        media.setTeachplanId(teachplanMedia.getTeachplanId());
        media.setCourseId(teachplanMedia.getCourseId());
        media.setMediaFileOriginalName(teachplanMedia.getMediaFileOriginalName());
        media.setMediaId(teachplanMedia.getMediaId());
        media.setMediaUrl(teachplanMedia.getMediaUrl());

        return teachplanMediaRepository.save(media);
    }

TeachplanMediaRepository

package com.xuecheng.manage_course.dao;

import com.xuecheng.framework.domain.course.TeachplanMedia;
import org.springframework.data.jpa.repository.JpaRepository;

public interface TeachplanMediaRepository extends JpaRepository<TeachplanMedia, String> {
}

前端實現

前端基本上已經全部實現了,我這裏有一點修改因爲我自己太欠了,當時寫CoursePlan的時候用的根路徑是是course/teachplan

我需要在調用的API地址前面加上teachplan

/*保存媒資信息*/
export const savemedia = teachplanMedia => {
  return http.requestPost(apiUrl+'/course/teachplan/savemedia',teachplanMedia);
}

視頻信息回顯(省略)

注意:

因爲數據庫中的courseId的確切字段名爲:courseid而不是courseId,需要在實體類上加入如下代碼:

    @Column(name="courseid")
    private String courseId;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章