最近項目中需要前端播放 .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頁面中,Issue1441 和 Issue4297 中茫因,面對videojs能否直接播放 .ts 的疑問蚪拦,開發(fā)團(tuán)隊都表示雖然庫本身沒有直接的相關(guān)實現(xiàn),但可以利用相關(guān)的邏輯自行實現(xiàn)冻押。最重要的是都指出了mux.js這一工具驰贷。根據(jù)實測,只用這一個庫即可在web端直接播放 .ts 視頻洛巢,如下是它的轉(zhuǎn)化流程括袒。
代碼示例
示例中是以 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é)为肮。