學成在線(第13天)

 在線學習需求分析

學成在線做爲在線教育網站,提供多種學習形式,包括:錄播、直播、圖文、社羣等,學生登陸進入學習中心便可
在線學習,本章節將開發錄播課程的在線學習功能,需求以下:
一、學生能夠在windows瀏覽器上在線觀看視頻。
二、播放器具備快進、快退、暫停等基本功能。
三、學生能夠方便切換章節進行學習。css

 流媒體

流媒體就是將視頻文件分紅許多小塊兒,將這些小塊兒做爲數據包經過網絡發送出去,實現一邊傳輸視
頻 數據 包一邊觀看視頻。html

流式傳輸
在網絡上傳輸音、視頻信息有兩個方式:下載和流式傳輸。
下載:就是把音、視頻文件徹底下載到本機後開始播放,它的特色是必須等到視頻文件下載完成方可播放,
播放等待時間較長,沒法去播放還未下載的部分視頻。
流式傳輸:就是客戶端經過連接視頻服務器實時傳輸音、視頻信息,實現「邊下載邊播放」。
流式傳輸包括以下兩種方式:
1) 順序流式傳輸
即順序下載音、視頻文件,能夠實現邊下載邊播放,不過,用戶只能觀看已下載的視頻內容,沒法快進到未
下載的視頻部分,順序流式傳輸可使用Http服務器來實現,好比Nginx、Apache等。
2)實時流式傳輸
實時流式傳輸能夠解決順序流式傳輸沒法快進的問題,它與Http流式傳輸不一樣,它必須使用流媒體服務器並
且使用流媒體協議來傳輸視頻,它比Http流式傳輸複雜。常見的實時流式傳輸協議有RTSP、RTMP、RSVP
等。前端

流媒體系統的概要結構
經過流媒體系統的概要結構學習流媒體系統的基本業務流程。vue

一、將原始的視頻文件經過編碼器轉換爲適合網絡傳輸的流格式,編碼後的視頻直接輸送給媒體服務器。
原始的視頻文件一般是事先錄製好的視頻,好比經過攝像機、攝像頭等錄像、錄音設備採集到的音視頻文
件,體積較大,要想在網絡上傳輸須要通過壓縮處理,即經過編碼器進行編碼 。
二、媒體服務獲取到編碼好的視頻文件,對外提供流媒體數據傳輸接口,接口協議包括 :HTTP、RTSP、
RTMP等 。
三、播放器經過流媒體協議與媒體服務器通訊,獲取視頻數據,播放視頻。nginx

HLS是什麼?

HLS的工做方式是:將視頻拆分紅若干ts格式的小文件,經過m3u8格式的索引文件對這些ts小文件創建索引。通常
10秒一個ts文件,播放器鏈接m3u8文件播放,當快進時經過m3u8便可找到對應的索引文件,並去下載對應的ts文
件,從而實現快進、快退以近實時 的方式播放視頻。
IOS、Android設備、及各大瀏覽器都支持HLS協議。git

採用 HLS方案便可實現邊下載邊播放,並可不用使用rtmp等流媒體協議,不用構建專用的媒體服務器,節省成本。
本項目點播方案肯定爲方案3。github

FFmpeg  的基本使用

咱們將視頻錄製完成後,使用視頻編碼軟件對視頻進行編碼,本項目 使用FFmpeg對視頻進行編碼 。web

下載 :ffmpeg-20180227-fa0c9d6-win64-static.zip,並解壓,本教程將ffmpeg解壓到了
F:\devenv\edusoft\ffmpeg-20180227-fa0c9d6-win64-static\ffmpeg-20180227-fa0c9d6-win64-static下。
將F:\devenv\edusoft\ffmpeg-20180227-fa0c9d6-win64-static\ffmpeg-20180227-fa0c9d6-win64-static\bin目
錄配置在path環境變量中。
檢測是否安裝成功:數據庫

  生成m3u8/ts文件windows

使用ffmpeg生成 m3u8的步驟以下:
第一步:先將avi視頻轉成mp4

ffmpeg.exe -i  lucene.avi -c:v libx264 -s 1280x720 -pix_fmt yuv420p -b:a 63k -b:v 753k -r 18 lucene.mp4

第二步:將mp4生成m3u8

ffmpeg -i  lucene.mp4   -hls_time 10 -hls_list_size 0  -hls_segment_filename ./hls/lucene_%05d.ts ./hls/lucene.m3u8

-hls_time 設置每片的長度,單位爲秒
-hls_list_size n: 保存的分片的數量,設置爲0表示保存全部分片
-hls_segment_filename :段文件的名稱,%05d表示5位數字
生成的效果是:將lucene.mp4視頻文件每10秒生成一個ts文件,最後生成一個m3u8文件,m3u8文件是ts的索引
文件。

 播放器

視頻編碼後要使用播放器對其進行解碼、播放視頻內容。在web應用中經常使用的播放器有flash播放器、H5播放器或
瀏覽器插件播放器,其中以flash和H5播放器最多見。
flash播放器:缺點是須要在客戶機安裝Adobe Flash Player播放器,優勢是flash播放器已經很成熟了,而且瀏覽
器對flash支持也很好。
H5播放器:基於h5自帶video標籤進行構建,優勢是大部分瀏覽器支持H5,不用再安裝第三方的flash播放器,並
且隨着前端技術的發展,h5技術會愈來愈成熟。
本項目採用H5播放器,使用Video.js開源播放器。
Video.js是一款基於HTML5世界的網絡視頻播放器。它支持HTML5和Flash視頻,它支持在臺式機和移動設備上播
放視頻。這個項目於2010年中開始,目前已在40萬網站使用。

Nginx媒體服務器

HLS協議基於Http協議,本項目使用Nginx做爲視頻服務器。下圖是Nginx媒體服務器的配置流程圖:

1.用戶打開www.xuecheng.com上邊的 video.html網頁 

2.video.xuecheng.com進行負載均衡處理,將視頻請求轉發到媒體服務器

根據上邊的流程,咱們在媒體服務器上安裝Nginx,並配置以下:

#學成網媒體服務
server {
listen       90;    
server_name  localhost;    
#視頻目錄    
location /video/ {    
alias   F:/develop/video/;        
}    
}

媒體服務器代理

媒體服務器不止一臺,經過代理實現負載均衡功能,使用Nginx做爲媒體服務器的代理,此代理服務器做爲
video.xuecheng.com域名服務器。
配置video.xuecheng.com虛擬主機:
注意:開發中代理服務器和媒體服務器在同一臺服務器,使用同一個Nginx。

學成網媒體服務代理
map $http_origin $origin_list{
    default http://www.xuecheng.com;
    "~http://www.xuecheng.com" http://www.xuecheng.com;
    "~http://ucenter.xuecheng.com" http://ucenter.xuecheng.com;
}
#學成網媒體服務代理
server {
listen       80;    
server_name video.xuecheng.com;    
   
location /video {      
proxy_pass http://video_server_pool;          
add_header Access‐Control‐Allow‐Origin $origin_list;        
#add_header Access‐Control‐Allow‐Origin *;        
add_header Access‐Control‐Allow‐Credentials true;          
add_header Access‐Control‐Allow‐Methods GET;        
}     
   
}

video_server_pool的配置以下:

#媒體服務
    upstream video_server_pool{
     server 127.0.0.1:90 weight=10;    
    } 

 測試video.js

一、編寫測試頁面video.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta http‐equiv="content‐type" content="text/html; charset=utf‐8" />
    <title>視頻播放</title>
    <link href="/plugins/videojs/video‐js.css" rel="stylesheet">
</head>
<body>
<video id=example‐video width=800 height=600 class="video‐js vjs‐default‐skin vjs‐big‐play‐
centered" controls poster="http://127.0.0.1:90/video/add.jpg">
    <source
            src="http://video.xuecheng.com/video/hls/lucene.m3u8"
            type="application/x‐mpegURL">
</video>
<input type="button" onClick="switchvideo()" value="switch"/>
<script src="/plugins/videojs/video.js"></script>
<script src="/plugins/videojs/videojs‐contrib‐hls.js"></script>
<script>
    var player = videojs('example‐video');
    //player.play();
//切換視頻    
    function switchvideo(){
        player.src({
            src: 'http://video.xuecheng.com/video/hls/lucene.m3u8',
            type: 'application/x‐mpegURL',
            withCredentials: true
});
        player.play();
    }
</script>
</body>
</html>
View Code

二、測試
配置hosts文件,本教程開發環境使用Window10,修改C:\Windows\System32\drivers\etc\hosts文件

127.0.0.1 video.xuecheng.com

 搭建學習中心前端

學成網學習中心提供學生在線學習的各各模塊,上一章節測試的點播學習功能也屬於學習中心的一部分,本章節將
實現學習中心點播學習的前端部分。之因此先實現前端部分,主要是由於要將video.js+vue.js集成,一部分精力還
是要放在技術研究。

先看一下界面原型,以下圖,最終的目標是在此頁面使用video.js播放視頻。

 配置域名

學習中心的二級域名爲ucenter.xuecheng.com,咱們在nginx中配置ucenter虛擬主機。

#學成網用戶中心
server {
listen       80;    
server_name ucenter.xuecheng.com;    
   
#我的中心    
location / {      
proxy_pass http://ucenter_server_pool;          
}     
}
#前端ucenter
upstream ucenter_server_pool{
  #server 127.0.0.1:7081 weight=10;
  server 127.0.0.1:13000 weight=10;
}

調試視頻播放頁面

使用vue-video-player組件將video.js集成到vue.js中,本項目使用vue-video-player實現video.js播放。
組件地址:https://github.com/surmon-china/vue-video-player
上面的 xc-ui-pc-learning工程已經添加vue-video-player組件,咱們在vue頁面直接使用便可。
前邊咱們已經測試經過 video.js,下面咱們直接在vue頁面中使用vue-video-player完成視頻播放。
導入learning_video.vue頁面到course 模塊下。
配置路由:

import learning_video from '@/module/course/page/learning_video.vue';
  {
    path: '/learning/:courseId/:chapter',
    component: learning_video,
    name: '錄播視頻學習',
    hidden: false,
    iconCls: 'el‐icon‐document'
  }

預覽效果:
請求:http://ucenter.xuecheng.com/#/learning/1/2
第一個參數: courseId,課程id,這裏是測試頁面效果隨便輸入一個ID便可,這裏輸入1
第二個參數:chapter,課程計劃id,這裏是測試頁面效果隨便輸入一個ID便可,這裏輸入2

 

 媒資管理

每一個教學機構均可以在媒資系統管理本身的教學資源,包括:視頻、教案等文件。
目前媒資管理的主要管理對象是課程錄播視頻,包括:媒資文件的查詢、視頻上傳、視頻刪除、視頻處理等。
媒資查詢:教學機構查詢本身所擁有的媒體文件。
視頻上傳:將用戶線下錄製的教學視頻上傳到媒資系統。
視頻處理:視頻上傳成功,系統自動對視頻進行編碼處理。
視頻刪除 :若是該視頻已再也不使用,能夠從媒資系統刪除。

下邊是媒資系統與其它系統的交互狀況:

一、上傳媒資文件
前端/客戶端請求媒資系統上傳文件。
文件上傳成功將文件存儲到媒資服務器,將文件信息存儲到數據庫。
二、使用媒資
課程管理請求媒資系統查詢媒資信息,將課程計劃與媒資信息對應、存儲。
三、視頻播放
用戶進入學習中心請求學習服務學習在線播放視頻。
學習服務校驗用戶資格經過後請求媒資系統獲取視頻地址。

業務流程

服務端須要實現以下功能:
一、上傳前檢查上傳環境
檢查文件是否上傳,已上傳則直接返回。
檢查文件上傳路徑是否存在,不存在則建立。
二、分塊檢查
檢查分塊文件是否上傳,已上傳則返回true。
未上傳則檢查上傳路徑是否存在,不存在則建立。
三、分塊上傳
將分塊文件上傳到指定的路徑。
四、合併分塊
將全部分塊文件合併爲一個文件。
在數據庫記錄文件信息。

上傳註冊

一、配置
application.yml配置上傳文件的路徑:

xc‐service‐manage‐media:
  upload‐location: F:/develop/video/

二、定義Dao
媒資文件管理Dao

public interface MediaFileRepository extends MongoRepository<MediaFile,String> {
}

三、Service
功能:
1)檢查上傳文件是否存在
2)建立文件目錄

@Service
public class MediaUploadService {
 private final static Logger LOGGER = LoggerFactory.getLogger(MediaUploadController.class);
    @Autowired
    MediaFileRepository mediaFileRepository;
    //上傳文件根目錄
    @Value("${xc‐service‐manage‐media.upload‐location}")
    String uploadPath;
    /**
     * 根據文件md5獲得文件路徑
     * 規則:
     * 一級目錄:md5的第一個字符
     * 二級目錄:md5的第二個字符
     * 三級目錄:md5
     * 文件名:md5+文件擴展名
     * @param fileMd5 文件md5值
     * @param fileExt 文件擴展名
     * @return 文件路徑
     */
    private String getFilePath(String fileMd5,String fileExt){
        String filePath = uploadPath+fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) +
"/" + fileMd5 + "/" + fileMd5 + "." + fileExt;
        return filePath;
    }
    //獲得文件目錄相對路徑,路徑中去掉根目錄
    private String getFileFolderRelativePath(String fileMd5,String fileExt){
        String filePath = fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" +
fileMd5 + "/";
        return filePath;
    }
    //獲得文件所在目錄
    private String getFileFolderPath(String fileMd5){
        String fileFolderPath = uploadPath+ fileMd5.substring(0, 1) + "/" + fileMd5.substring(1,
2) + "/" + fileMd5 + "/" ;
        return fileFolderPath;
    }
    //建立文件目錄
    private boolean createFileFold(String fileMd5){
        //建立上傳文件目錄
        String fileFolderPath = getFileFolderPath(fileMd5);
        File fileFolder new File(fileFolderPath);
        if (!fileFolder.exists()) {
            //建立文件夾
            boolean mkdirs = fileFolder.mkdirs();
            return mkdirs;
        }
        return true;
    }
 //文件上傳註冊
    public ResponseResult register(String fileMd5, String fileName, String fileSize, String
mimetype, String fileExt) {
        //檢查文件是否上傳
        //一、獲得文件的路徑
        String filePath = getFilePath(fileMd5, fileExt);
        File file new File(filePath);
        //二、查詢數據庫文件是否存在
        Optional<MediaFile> optional = mediaFileRepository.findById(fileMd5);
        //文件存在直接返回
        if(file.exists() && optional.isPresent()){
            ExceptionCast.cast(MediaCode.UPLOAD_FILE_REGISTER_EXIST);
        }
        boolean fileFold = createFileFold(fileMd5);
        if(!fileFold){
            //上傳文件目錄建立失敗
            ExceptionCast.cast(MediaCode.UPLOAD_FILE_REGISTER_CREATEFOLDER_FAIL);
        }
        return new ResponseResult(CommonCode.SUCCESS);
    }
  
   
}
View Code

分塊檢查

在Service 中定義分塊檢查方法:

//獲得塊文件所在目錄
private String getChunkFileFolderPath(String fileMd5){
String fileChunkFolderPath = getFileFolderPath(fileMd5) +"/" + "chunks" + "/";    
return fileChunkFolderPath;    
}
//檢查塊文件
public CheckChunkResult checkchunk(String fileMd5, String chunk, String chunkSize) {
    //獲得塊文件所在路徑
    String chunkfileFolderPath = getChunkFileFolderPath(fileMd5);
    //塊文件的文件名稱以1,2,3..序號命名,沒有擴展名
    File chunkFile = new File(chunkfileFolderPath+chunk);
    if(chunkFile.exists()){
        return new CheckChunkResult(MediaCode.CHUNK_FILE_EXIST_CHECK,true);
    }else{
        return new CheckChunkResult(MediaCode.CHUNK_FILE_EXIST_CHECK,false);
    }
}

上傳分塊

在Service 中定義分塊上傳分塊方法:

//塊文件上傳
public ResponseResult uploadchunk(MultipartFile file, String fileMd5, String chunk) {
    if(file == null){
        ExceptionCast.cast(MediaCode.UPLOAD_FILE_REGISTER_ISNULL);
    }
    //建立塊文件目錄
    boolean fileFold = createChunkFileFolder(fileMd5);
    //塊文件
    File chunkfile = new File(getChunkFileFolderPath(fileMd5) + chunk);
    //上傳的塊文件
    InputStream inputStream= null;
    FileOutputStream outputStream null;
    try {
        inputStream = file.getInputStream();
        outputStream new FileOutputStream(chunkfile);
        IOUtils.copy(inputStream,outputStream);
    } catch (Exception e) {
        e.printStackTrace();
        LOGGER.error("upload chunk file fail:{}",e.getMessage());
        ExceptionCast.cast(MediaCode.CHUNK_FILE_UPLOAD_FAIL);
    }finally {
        try {
            inputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            outputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    return new ResponseResult(CommonCode.SUCCESS);
}
    //建立塊文件目錄
    private boolean createChunkFileFolder(String fileMd5){
        //建立上傳文件目錄
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
        File chunkFileFolder new File(chunkFileFolderPath);
        if (!chunkFileFolder.exists()) {
            //建立文件夾
            boolean mkdirs = chunkFileFolder.mkdirs();
            return mkdirs;
        }
        return true;
    }
View Code

合併分塊

在Service 中定義分塊合併分塊方法,功能以下:
1)將塊文件合併

2 )校驗文件md5是否正確
3)向Mongodb寫入文件信息

public ResponseResult mergechunks(String fileMd5, String fileName, Long fileSize, String
mimetype, String fileExt) {
    //獲取塊文件的路徑
    String chunkfileFolderPath = getChunkFileFolderPath(fileMd5);
    File chunkfileFolder new File(chunkfileFolderPath);
    if(!chunkfileFolder.exists()){
        chunkfileFolder.mkdirs();
    }
    //合併文件路徑
    File mergeFile = new File(getFilePath(fileMd5,fileExt));
    //建立合併文件
    //合併文件存在先刪除再建立
    if(mergeFile.exists()){
        mergeFile.delete();
    }
    boolean newFile = false;
    try {
        newFile = mergeFile.createNewFile();
    } catch (IOException e) {
        e.printStackTrace();
        LOGGER.error("mergechunks..create mergeFile fail:{}",e.getMessage());
    }
    if(!newFile){
        ExceptionCast.cast(MediaCode.MERGE_FILE_CREATEFAIL);
    }
    //獲取塊文件,此列表是已經排好序的列表
    List<File> chunkFiles = getChunkFiles(chunkfileFolder);
    //合併文件
    mergeFile = mergeFile(mergeFile, chunkFiles);
    if(mergeFile == null){
        ExceptionCast.cast(MediaCode.MERGE_FILE_FAIL);
    }
    //校驗文件
    boolean checkResult = this.checkFileMd5(mergeFile, fileMd5);
    if(!checkResult){
        ExceptionCast.cast(MediaCode.MERGE_FILE_CHECKFAIL);
    }
    //將文件信息保存到數據庫
    MediaFile mediaFile = new MediaFile();
    mediaFile.setFileId(fileMd5);
    mediaFile.setFileName(fileMd5+"."+fileExt);
    mediaFile.setFileOriginalName(fileName);
    //文件路徑保存相對路徑
    mediaFile.setFilePath(getFileFolderRelativePath(fileMd5,fileExt));
    mediaFile.setFileSize(fileSize);
    mediaFile.setUploadTime(new Date());
    mediaFile.setMimeType(mimetype);
    mediaFile.setFileType(fileExt);
//狀態爲上傳成功
    mediaFile.setFileStatus("301002");
    MediaFile save = mediaFileDao.save(mediaFile);
    return new ResponseResult(CommonCode.SUCCESS);
}
 //校驗文件的md5值
    private boolean checkFileMd5(File mergeFile,String md5){
        if(mergeFile == null || StringUtils.isEmpty(md5)){
            return false;
        }
        //進行md5校驗
        FileInputStream mergeFileInputstream = null;
        try {
            mergeFileInputstream new FileInputStream(mergeFile);
            //獲得文件的md5
            String  mergeFileMd5 = DigestUtils.md5Hex(mergeFileInputstream);
            //比較md5
            if(md5.equalsIgnoreCase(mergeFileMd5)){
               return true;
            }
        } catch (Exception e) {
            e.printStackTrace();
            LOGGER.error("checkFileMd5 error,file is:{},md5 is:
{}",mergeFile.getAbsoluteFile(),md5);
        }finally{
            try {
                mergeFileInputstream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return false;
    }
//獲取全部塊文件
    private List<File> getChunkFiles(File chunkfileFolder){
        //獲取路徑下的全部塊文件
        File[] chunkFiles = chunkfileFolder.listFiles();
        //將文件數組轉成list,並排序
        List<File> chunkFileList = new ArrayList<File>();
        chunkFileList.addAll(Arrays.asList(chunkFiles));
        //排序
        Collections.sort(chunkFileList, new Comparator<File>() {
            @Override
            public int compare(File o1, File o2) {
                if(Integer.parseInt(o1.getName())>Integer.parseInt(o2.getName())){
                    return 1;
                }
                return ‐1;
  }
        });
        return chunkFileList;
    }
    //合併文件
    private File mergeFile(File mergeFile,List<File> chunkFiles){
        try {
            //建立寫文件對象
            RandomAccessFile raf_write = new RandomAccessFile(mergeFile,"rw");
            //遍歷分塊文件開始合併
            //讀取文件緩衝區
            byte[] b = new byte[1024];
            for(File chunkFile:chunkFiles){
                RandomAccessFile raf_read new RandomAccessFile(chunkFile,"r");
                int len = ‐1;
                //讀取分塊文件
                while((len = raf_read.read(b))!=‐1){
                    //向合併文件中寫數據
                    raf_write.write(b,0,len);
                }
                raf_read.close();
            }
            raf_write.close();
        } catch (Exception e) {
            e.printStackTrace();
            LOGGER.error("merge file error:{}",e.getMessage());
            return null;
        }
        return mergeFile;
    }
View Code

Controller

@RestController
@RequestMapping("/media/upload")
public class MediaUploadController implements MediaUploadControllerApi {
    @Autowired
    MediaUploadService mediaUploadService;
    @Override
     @PostMapping("/register")
    public ResponseResult register(@RequestParam("fileMd5") String fileMd5,
@RequestParam("fileName") String fileName, @RequestParam("fileSize") Long fileSize,
@RequestParam("mimetype") String mimetype, @RequestParam("fileExt") String fileExt) {
        return mediaUploadService.register(fileMd5,fileName,fileSize,mimetype,fileExt);
    }
    @Override
    @PostMapping("/checkchunk")
    public CheckChunkResult checkchunk(@RequestParam("fileMd5") String fileMd5,@RequestParam("chunk") Integer chunk, @RequestParam("chunkSize") Integer chunkSize) {
        return mediaUploadService.checkchunk(fileMd5,chunk,chunkSize);
    }
    @Override
     @PostMapping("/uploadchunk")
    public ResponseResult uploadchunk(@RequestParam("file") MultipartFile file,
@RequestParam("fileMd5") String fileMd5, @RequestParam("chunk") Integer chunk) {
        return mediaUploadService.uploadchunk(file,fileMd5,chunk);
    }
    @Override
    @PostMapping("/mergechunks")
    public ResponseResult mergechunks(@RequestParam("fileMd5") String fileMd5,
@RequestParam("fileName") String fileName, @RequestParam("fileSize") Long fileSize,
@RequestParam("mimetype") String mimetype, @RequestParam("fileExt") String fileExt) {
        return mediaUploadService.mergechunks(fileMd5,fileName,fileSize,mimetype,fileExt);
    }
}
View Code

 

 

相關文章
相關標籤/搜索