Web端直接播放 .ts 視頻

最近項目中需要前端播放 .ts 格式視頻,搗鼓了幾天學習到很多知識,也發掘了一種優秀的解決方案,分享給有同樣需求的同學。

常見方案

在網上查找的大部分解決方案都是用諸如videojs等網頁播放器,接收 .m3u8索引文件的方式來播放ts切片。這種方案的缺點是需要後端對原始ts切片做處理,生成 .m3u8索引文件

ffmpeg -i source.ts -c copy -map 0 -f segment -segment_list playlist.m3u8 -segment_time 10 output%03d.ts

項目中已存儲的 .ts 切片數量衆多,已經佔用了NAS服務器絕大部分的資源,生成的 .m3u8 索引雖然非常小,但會生成一堆切片後的新 .ts 視頻,例如上述指令將會生成一堆 10s 長度的 ts新切片。出於各種考慮後端的同學拒絕了這種重新生成新切片加索引的方案。

邪道方案

在我們的項目中,每一個ts切片已經就是一個獨立內容的視頻了,時長在20s以內,因此其實不用切割,只需要生成一個索引文件就可以了, .m3u8格式如下:

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-ALLOW-CACHE:YES
#EXT-X-TARGETDURATION:93
#EXTINF:92.008578,
test.ts
#EXT-X-ENDLIST

定義好的時長並不影響最終網頁播放器計算出的時長,因此可以取一個統一的極大值,整體上就只有倒數第二行的ts文件名需要根據不同 ts 視頻修改,可以用腳本統一生成所有ts文件的索引文件。這個方案極其low,當然也被後端同學拒絕了。

插件方案

VLC Web Plugin,一個需要VLC播放器以及瀏覽器插件的方案,並且不支持Chrome,使用複雜,感興趣的同學可以自行嘗試。

優雅方案

在中文互聯網搜索無果後,果斷轉向了Google,然而也未果,正當我絕望地準備調整心態,接受下載後VLC播放的保底方案時,終於發現了一絲線索,在vediojs的Github頁面中,Issue1441Issue4297 中,面對videojs能否直接播放 .ts 的疑問,開發團隊都表示雖然庫本身沒有直接的相關實現,但可以利用相關的邏輯自行實現。最重要的是都指出了mux.js這一工具。根據實測,只用這一個庫即可在web端直接播放 .ts 視頻,如下是它的轉化流程。
Transfer Diagram

代碼示例

示例中是以 ajax 的方式接收 .ts 二進制數據,mux.js引入方式可以直接標籤引入,也可以npm install mux.js後 import進頁面。

var $ = document.querySelector.bind(document);
    var vjsParsed,
        video, 
        mediaSource;
    // 定義通用的事件回調處理函數,只做打印事件類型
    function logevent (event) {
      console.log(event);
    }
    
    // ajax
    let xhr = new XMLHttpRequest();
    xhr.open('GET', "./test.ts");
    // 接收的是 video/mp2t 二進制數據,Blob類型也可以,但arraybuffer類型方便後續直接處理 
    xhr.responseType = "arraybuffer";
    xhr.send();
    xhr.onreadystatechange = function () {
      if (xhr.readyState ==4) {
        if (xhr.status == 200) {
          transferFormat(xhr.response);
        } else {
          console.log('error');
        }
      }
    }
    
    function transferFormat (data) {
      // 將源數據從ArrayBuffer格式保存爲可操作的Uint8Array格式
      // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer
      var segment = new Uint8Array(data); 
      var combined = false;
      // 接收無音頻ts文件,OutputType設置爲'video',帶音頻ts設置爲'combined'
      var outputType = 'video';
      var remuxedSegments = [];
      var remuxedBytesLength = 0;
      var remuxedInitSegment = null;

      // remux選項默認爲true,將源數據的音頻視頻混合爲mp4,設爲false則不混合
      var transmuxer = new muxjs.mp4.Transmuxer({remux: false});
      
      // 監聽data事件,開始轉換流
      transmuxer.on('data', function(event) {
        console.log(event);
        if (event.type === outputType) {
          remuxedSegments.push(event);
          remuxedBytesLength += event.data.byteLength;
          remuxedInitSegment = event.initSegment;
        }
      });
      // 監聽轉換完成事件,拼接最後結果並傳入MediaSource
      transmuxer.on('done', function () {
        var offset = 0;
        var bytes = new Uint8Array(remuxedInitSegment.byteLength + remuxedBytesLength)
        bytes.set(remuxedInitSegment, offset);
        offset += remuxedInitSegment.byteLength;

        for (var j = 0, i = offset; j < remuxedSegments.length; j++) {
          bytes.set(remuxedSegments[j].data, i);
          i += remuxedSegments[j].byteLength;
        }
        remuxedSegments = [];
        remuxedBytesLength = 0;
        // 解析出轉換後的mp4相關信息,與最終轉換結果無關
        vjsParsed = muxjs.mp4.tools.inspect(bytes);
        console.log('transmuxed', vjsParsed);

        prepareSourceBuffer(combined, outputType, bytes);
      });
      // push方法可能會觸發'data'事件,因此要在事件註冊完成後調用
      transmuxer.push(segment); // 傳入源二進制數據,分割爲m2ts包,依次調用上圖中的流程
      // flush的調用會直接觸發'done'事件,因此要事件註冊完成後調用
      transmuxer.flush(); // 將所有數據從緩存區清出來
    }

    function prepareSourceBuffer (combined, outputType, bytes) {
      var buffer;
      video = document.createElement('video');
      video.controls = true;
      // MediaSource Web API: https://developer.mozilla.org/zh-CN/docs/Web/API/MediaSource
      mediaSource = new MediaSource(); 
      video.src = URL.createObjectURL(mediaSource);
    
      $('#video-wrapper').appendChild(video); // 將H5 video元素添加到對應DOM節點下
    
      // 轉換後mp4的音頻格式 視頻格式
      var codecsArray = ["avc1.64001f", "mp4a.40.5"];
    
      mediaSource.addEventListener('sourceopen', function () {
        // MediaSource 實例默認的duration屬性爲NaN
        mediaSource.duration = 0;
        // 轉換爲帶音頻、視頻的mp4
        if (combined) {
          buffer = mediaSource.addSourceBuffer('video/mp4;codecs="' + 'avc1.64001f,mp4a.40.5' + '"');
        } else if (outputType === 'video') {
          // 轉換爲只含視頻的mp4
          buffer = mediaSource.addSourceBuffer('video/mp4;codecs="' + codecsArray[0] + '"');
        } else if (outputType === 'audio') {
          // 轉換爲只含音頻的mp4
          buffer = mediaSource.addSourceBuffer('audio/mp4;codecs="' + (codecsArray[1] ||codecsArray[0]) + '"');
        }
    
        buffer.addEventListener('updatestart', logevent);
        buffer.addEventListener('updateend', logevent);
        buffer.addEventListener('error', logevent);
        video.addEventListener('error', logevent);
        // mp4 buffer 準備完畢,傳入轉換後的數據
        // 將 bytes 放入 MediaSource 創建的sourceBuffer中
        // https://developer.mozilla.org/en-US/docs/Web/API/SourceBuffer/appendBuffer
        buffer.appendBuffer(bytes);
        // 自動播放
        // video.play();
      });
    };

IE8及以上 、 IE Edge 、Chrome 、 Firefox 瀏覽器下均能正常播放。希望本文能幫到各位開發同學。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章