Web端 Html5 直接播放 .ts 視頻

最近項目中需要前端播放 .ts 格式視頻,搗鼓了幾天學(xué)習(xí)到很多知識绪励,也發(fā)掘了一種未曾見過的優(yōu)秀解決方案,分享給有同樣需求的同學(xué)沪摄。

常見方案

在網(wǎng)上查找的大部分解決方案都是用諸如videojs等網(wǎng)頁播放器实辑,接收 .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 切片數(shù)量眾多匹舞,已經(jīng)占用了NAS服務(wù)器絕大部分的資源排宰,生成的 .m3u8 索引雖然非常小似芝,但會生成一堆切片后的新 .ts 視頻,例如上述指令將會生成一堆 10s 長度的 ts新切片板甘。出于各種考慮后端的同學(xué)拒絕了這種重新生成新切片加索引的方案党瓮。

邪道方案

在我們的項目中,每一個ts切片已經(jīng)就是一個獨立內(nèi)容的視頻了盐类,時長在20s以內(nèi)寞奸,因此其實不用切割,只需要生成一個索引文件就可以了在跳, .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

定義好的時長并不影響最終網(wǎng)頁播放器計算出的時長枪萄,因此可以取一個統(tǒng)一的極大值,整體上就只有倒數(shù)第二行的ts文件名需要根據(jù)不同 ts 視頻修改猫妙,可以用腳本統(tǒng)一生成所有ts文件的索引文件瓷翻。這個方案極其low,當(dāng)然也被后端同學(xué)拒絕了。

插件方案

VLC Web Plugin齐帚,一個需要VLC播放器以及瀏覽器插件的方案元践,并且不支持Chrome,使用復(fù)雜童谒,感興趣的同學(xué)可以自行嘗試单旁。

優(yōu)雅方案

在中文互聯(lián)網(wǎng)搜索無果后,果斷轉(zhuǎn)向了Google饥伊,然而也未果象浑,正當(dāng)我絕望地準(zhǔn)備調(diào)整心態(tài),接受下載后VLC播放的保底方案時琅豆,終于發(fā)現(xiàn)了一絲線索愉豺,在vediojs的Github頁面中,Issue1441Issue4297 中茫因,面對videojs能否直接播放 .ts 的疑問蚪拦,開發(fā)團(tuán)隊都表示雖然庫本身沒有直接的相關(guān)實現(xiàn),但可以利用相關(guān)的邏輯自行實現(xiàn)冻押。最重要的是都指出了mux.js這一工具驰贷。根據(jù)實測,只用這一個庫即可在web端直接播放 .ts 視頻洛巢,如下是它的轉(zhuǎn)化流程括袒。

Transfer Diagram

代碼示例

示例中是以 ajax 的方式接收 .ts 二進(jìn)制數(shù)據(jù),mux.js引入方式可以直接標(biāo)簽引入稿茉,也可以npm install mux.js后 import進(jìn)頁面锹锰。

var $ = document.querySelector.bind(document);
    var vjsParsed,
        video, 
        mediaSource;
    // 定義通用的事件回調(diào)處理函數(shù),只做打印事件類型
    function logevent (event) {
      console.log(event);
    }
    
    // ajax
    let xhr = new XMLHttpRequest();
    xhr.open('GET', "./test.ts");
    // 接收的是 video/mp2t 二進(jìn)制數(shù)據(jù)漓库,并且arraybuffer類型方便后續(xù)直接處理 
    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) {
      // 將源數(shù)據(jù)從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設(shè)置為'video',帶音頻ts設(shè)置為'combined'
      var outputType = 'video';
      var remuxedSegments = [];
      var remuxedBytesLength = 0;
      var remuxedInitSegment = null;

      // remux選項默認(rèn)為true渺蒿,將源數(shù)據(jù)的音頻視頻混合為mp4痢士,設(shè)為false則不混合
      var transmuxer = new muxjs.mp4.Transmuxer({remux: false});
      
      // 監(jiān)聽data事件,開始轉(zhuǎn)換流
      transmuxer.on('data', function(event) {
        console.log(event);
        if (event.type === outputType) {
          remuxedSegments.push(event);
          remuxedBytesLength += event.data.byteLength;
          remuxedInitSegment = event.initSegment;
        }
      });
      // 監(jiān)聽轉(zhuǎn)換完成事件蘸嘶,拼接最后結(jié)果并傳入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;
        // 解析出轉(zhuǎn)換后的mp4相關(guān)信息良瞧,與最終轉(zhuǎn)換結(jié)果無關(guān)
        vjsParsed = muxjs.mp4.tools.inspect(bytes);
        console.log('transmuxed', vjsParsed);

        prepareSourceBuffer(combined, outputType, bytes);
      });
      // push方法可能會觸發(fā)'data'事件陪汽,因此要在事件注冊完成后調(diào)用
      transmuxer.push(segment); // 傳入源二進(jìn)制數(shù)據(jù)训唱,分割為m2ts包,依次調(diào)用上圖中的流程
      // flush的調(diào)用會直接觸發(fā)'done'事件挚冤,因此要事件注冊完成后調(diào)用
      transmuxer.flush(); // 將所有數(shù)據(jù)從緩存區(qū)清出來
    }

    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元素添加到對應(yīng)DOM節(jié)點下
    
      // 轉(zhuǎn)換后mp4的音頻格式 視頻格式
      var codecsArray = ["avc1.64001f", "mp4a.40.5"];
    
      mediaSource.addEventListener('sourceopen', function () {
        // MediaSource 實例默認(rèn)的duration屬性為NaN
        mediaSource.duration = 0;
        // 轉(zhuǎn)換為帶音頻况增、視頻的mp4
        if (combined) {
          buffer = mediaSource.addSourceBuffer('video/mp4;codecs="' + 'avc1.64001f,mp4a.40.5' + '"');
        } else if (outputType === 'video') {
          // 轉(zhuǎn)換為只含視頻的mp4
          buffer = mediaSource.addSourceBuffer('video/mp4;codecs="' + codecsArray[0] + '"');
        } else if (outputType === 'audio') {
          // 轉(zhuǎn)換為只含音頻的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 準(zhǔn)備完畢,傳入轉(zhuǎn)換后的數(shù)據(jù)
        // 將 bytes 放入 MediaSource 創(chuàng)建的sourceBuffer中
        // https://developer.mozilla.org/en-US/docs/Web/API/SourceBuffer/appendBuffer
        buffer.appendBuffer(bytes);
        // 自動播放
        // video.play();
      });
    };

IE8及以上 训挡、 IE Edge 澳骤、Chrome 歧强、 Firefox 瀏覽器下均能正常播放。希望本文能幫到各位開發(fā)同學(xué)为肮。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末摊册,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子颊艳,更是在濱河造成了極大的恐慌茅特,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件棋枕,死亡現(xiàn)場離奇詭異白修,居然都是意外死亡,警方通過查閱死者的電腦和手機重斑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進(jìn)店門兵睛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人窥浪,你說我怎么就攤上這事祖很。” “怎么了漾脂?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵突琳,是天一觀的道長。 經(jīng)常有香客問我符相,道長拆融,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任啊终,我火速辦了婚禮镜豹,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蓝牲。我一直安慰自己趟脂,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布例衍。 她就那樣靜靜地躺著昔期,像睡著了一般。 火紅的嫁衣襯著肌膚如雪佛玄。 梳的紋絲不亂的頭發(fā)上硼一,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天,我揣著相機與錄音梦抢,去河邊找鬼般贼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的哼蛆。 我是一名探鬼主播蕊梧,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼腮介!你這毒婦竟也來了肥矢?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤叠洗,失蹤者是張志新(化名)和其女友劉穎橄抹,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體惕味,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡楼誓,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了名挥。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片疟羹。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖禀倔,靈堂內(nèi)的尸體忽然破棺而出榄融,到底是詐尸還是另有隱情,我是刑警寧澤救湖,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布愧杯,位于F島的核電站,受9級特大地震影響鞋既,放射性物質(zhì)發(fā)生泄漏力九。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一邑闺、第九天 我趴在偏房一處隱蔽的房頂上張望跌前。 院中可真熱鬧,春花似錦陡舅、人聲如沸抵乓。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽灾炭。三九已至,卻和暖如春颅眶,著一層夾襖步出監(jiān)牢的瞬間蜈出,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工帚呼, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留掏缎,地道東北人。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓煤杀,卻偏偏與公主長得像眷蜈,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子沈自,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,440評論 2 348

推薦閱讀更多精彩內(nèi)容