參考
w3c media-source
Media Source 系列 - 使用 Media Source Extensions 播放視頻
全面進(jìn)階 H5 直播
無(wú) Flash 時(shí)代,讓直播擁抱 H5(MSE篇)
使用 MediaSource 搭建流式播放器
一统屈、MSE 意義
1.粗識(shí) HTML5 video 標(biāo)簽和MSE媒體源擴(kuò)展
當(dāng)前網(wǎng)頁(yè)上能夠搜到的HTML5和MSE相關(guān)的內(nèi)容一抓一大把,本文的目的是盡量用較短的篇幅框杜,簡(jiǎn)述瀏覽器為何要使用HTML5的MSE擴(kuò)展庆聘。這也是在我最開(kāi)始接觸有關(guān)內(nèi)容時(shí)的最大的疑惑题禀。
以往用戶在瀏覽網(wǎng)頁(yè)內(nèi)容尤其是視頻內(nèi)容時(shí)馍资,需要使用像Adobe Flash或是微軟的Silverlight這樣的插件竭沫,播放視音頻內(nèi)容即使是電腦小白也知道燥翅,需要媒體播放器的支持,前面提到的插件就是起到媒體播放器的作用蜕提。但是使用插件這樣的方式是很不便捷且很不安全的森书,一些不法分子會(huì)在這些插件上動(dòng)手腳。因此W3C的最新的HTML5標(biāo)準(zhǔn)中谎势,定義了一系列新的元素來(lái)避免使用插件凛膏,其中就包含了<video>標(biāo)簽這一大名鼎鼎的元素。
正是使用了<video>標(biāo)簽脏榆,支持HTML5的瀏覽器得以實(shí)現(xiàn)無(wú)插件就原生支持播放媒體內(nèi)容猖毫,但是對(duì)媒體內(nèi)容的格式有所限制。說(shuō)到媒體內(nèi)容须喂,就自然地需要談到媒體的封裝格式和編碼格式吁断,這里總結(jié)一下,原視頻文件通過(guò)編碼來(lái)壓縮文件大小坞生,再通過(guò)封裝將壓縮視音頻仔役、字幕組合到一個(gè)容器內(nèi),具體內(nèi)容請(qǐng)大家自行查閱是己。
我們可以把<video>標(biāo)簽看做擁有解封裝和解碼功能的瀏覽器自帶播放器又兵。隨著視頻點(diǎn)播柳击、直播等視頻業(yè)務(wù)的發(fā)展晚吞,視頻通過(guò)流媒體傳輸協(xié)議(目前常用的有兩種兄淫,MPEG-DASH和Apple的HLS)從服務(wù)器端分發(fā)給客戶端崖疤,媒體內(nèi)容進(jìn)一步包含在一層傳輸協(xié)議中坦喘,這樣<video>就無(wú)法識(shí)別了原押。以HLS為例它呀,將源文件內(nèi)容分散地封裝到了一個(gè)個(gè)TS文件中海诲。
僅靠<video>標(biāo)簽無(wú)法識(shí)別這樣的TS文件级野,那么就引入了MSE拓展來(lái)幫助瀏覽器識(shí)別并處理TS文件页屠,將其變回原來(lái)可識(shí)別的媒體容器格式,這樣<video>就可以識(shí)別并播放原來(lái)的文件了蓖柔。那么支持HTML5的瀏覽器就相當(dāng)于內(nèi)置了一個(gè)能夠解析流協(xié)議的播放器辰企。
比如在hls.js 源碼解讀【1】中,介紹的hls.js
hls實(shí)際會(huì)先通過(guò) ajax(loader 是可以完成自定義的) 請(qǐng)求 m3u8文件况鸣,然后會(huì)讀取到文件的分片列表牢贸,以及視頻的編碼格式,時(shí)長(zhǎng)等镐捧。隨后會(huì)按照順序(非 seek )去對(duì)分片進(jìn)行請(qǐng)求潜索,這些也是通過(guò) ajax 請(qǐng)求二進(jìn)制的文件臭增,然后借助 Media Source Extensions 將 buffer 內(nèi)容進(jìn)行合流,然后組成一個(gè)可播的媒體資源文件竹习。
2.為什么國(guó)內(nèi)大部分視頻廠商不對(duì)PC開(kāi)放HTML5?
視頻源存在兼容性問(wèn)題誊抛。原生的 HTML5 <video> 元素在 Windows PC 上僅支持 mp4 (H.264 編碼)、webm整陌、ogg 等格式視頻的播放拗窃。而由于歷史遺留問(wèn)題(HTML5 視頻標(biāo)準(zhǔn)最終被廣泛支持以前,F(xiàn)lash 在 Web 視頻播放方面有著統(tǒng)治地位)泌辫,視頻網(wǎng)站的視頻源和轉(zhuǎn)碼設(shè)置随夸,很多都高清源都是適用于 Flash 播放的 FLV 格式,只有少量低清晰度視頻是 mp4 格式震放,webm 和 ogg 更是聽(tīng)都沒(méi)聽(tīng)說(shuō)過(guò)宾毒。比如優(yōu)酷只有高清和標(biāo)清才有 MP4 源,超清殿遂、1080P 等诈铛,基本都是 FLV 和 HLS(M3U8)的視頻源(在 Windows PC 上支持 M3U8 比支持 FLV 更復(fù)雜,我們不做過(guò)多贅述)勉躺。而騰訊視頻癌瘾,因?yàn)檗D(zhuǎn)型 MP4 比較早,視頻源幾乎全部都是 MP4 和 HLS饵溅,所以現(xiàn)在可以在 Mac OS X 上率先支持 PC Web 端的 HTML5 播放器(Safari 下 HLS妨退、Chrome 下 MP4)。
但是 HTML5 是不是就真的沒(méi)辦法播放 FLV 等格式視頻了呢蜕企?不是咬荷。解決方案是 MSE,Media Source Extensions轻掩,就是說(shuō)幸乒,HTML5 <video> 不僅可以直接播放上面支持的 mp4、m3u8唇牧、webm罕扎、ogg 格式,還可以支持由 JS 處理過(guò)后的視頻流丐重,這樣我們就可以用 JS 把一些不支持的視頻流格式腔召,轉(zhuǎn)化為支持的格式(如 H.264 的 mp4)。B 站開(kāi)源的 flv.js 就是這個(gè)技術(shù)的一個(gè)典型實(shí)現(xiàn)扮惦。B 站的 PC HTML5 播放器臀蛛,就是用 MSE 技術(shù),將 FLV 源用 JS 實(shí)時(shí)轉(zhuǎn)碼成 HTML5 支持的視頻流編碼格式(其實(shí)就一個(gè)文件頭的差異(這里文件頭改成容器。感謝評(píng)論區(qū)謙謙的指教浊仆,是容器的差異客峭,容器不只是文件頭)),提供給 HTML5 播放器播放抡柿。
一些人問(wèn)我為什么不直接采用 MP4 格式舔琅,并表示對(duì) FLV 格式的厭惡。這個(gè)問(wèn)題一方面是歷史遺留問(wèn)題沙绝,由于視頻網(wǎng)站前期完全依賴 Flash 播放而選擇 FLV 格式搏明;另一方面鼠锈,如果仔細(xì)研究過(guò) FLV/MP4 封裝格式闪檬,你會(huì)發(fā)現(xiàn) FLV 格式非常簡(jiǎn)潔,而 MP4 內(nèi)部 box 種類繁雜购笆,結(jié)構(gòu)復(fù)雜固實(shí)而又有太多冗余數(shù)據(jù)粗悯。FLV 天生具備流式特征適合網(wǎng)絡(luò)流傳輸,而 MP4 這種使用最廣泛的存儲(chǔ)格式同欠,設(shè)計(jì)卻并不一定優(yōu)雅样傍。
3.Media Source Extensions
我們已經(jīng)可以在 Web 應(yīng)用程序上無(wú)插件地播放視頻和音頻了。但是铺遂,現(xiàn)有架構(gòu)過(guò)于簡(jiǎn)單衫哥,只能滿足一次播放整個(gè)曲目的需要,無(wú)法實(shí)現(xiàn)拆分/合并數(shù)個(gè)緩沖文件襟锐。流媒體直到現(xiàn)在還在使用 Flash 進(jìn)行服務(wù)撤逢,以及通過(guò) RTMP 協(xié)議進(jìn)行視頻串流的 Flash 媒體服務(wù)器。
MSE 使我們可以把通常的單個(gè)媒體文件的 src 值替換成引用 MediaSource 對(duì)象(一個(gè)包含即將播放的媒體文件的準(zhǔn)備狀態(tài)等信息的容器)粮坞,以及引用多個(gè) SourceBuffer 對(duì)象(代表多個(gè)組成整個(gè)串流的不同媒體塊)的元素蚊荣。MSE 讓我們能夠根據(jù)內(nèi)容獲取的大小和頻率,或是內(nèi)存占用詳情(例如什么時(shí)候緩存被回收)莫杈,進(jìn)行更加精準(zhǔn)地控制互例。 它是基于它可擴(kuò)展的 API 建立自適應(yīng)比特率流客戶端(例如DASH 或 HLS 的客戶端)的基礎(chǔ)。
Download ---》 Response.arrayBuffer(適用fetch/xhr等異步獲取流媒體數(shù)據(jù)) ---》 SourceBuffer(添加到MediaSource的buffer中) ---》 <vedio/> or <autio/>
二筝闹、運(yùn)行DEMO
參考MDN在線DEMO bufferAll媳叨,將HTML代碼及所用的文件frag_bunny.mp4下載到本地,即可運(yùn)行
<html><head>
<meta charset="utf-8">
</head>
<body>
<video controls=""></video>
<script>
var video = document.querySelector('video');
var assetURL = 'frag_bunny.mp4';
// Need to be specific for Blink regarding codecs
// ./mp4info frag_bunny.mp4 | grep Codec
var mimeCodec = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"';
if ('MediaSource' in window &&
MediaSource.isTypeSupported(mimeCodec)) {
var mediaSource = new MediaSource;
//console.log(mediaSource.readyState); // closed
video.src = URL.createObjectURL(mediaSource);
mediaSource.addEventListener('sourceopen', sourceOpen);
} else {
console.error('Unsupported MIME type or codec: ', mimeCodec);
}
function sourceOpen (e) {
//console.log(this.readyState); // open
var mediaSource = e.target;
var sourceBuffer = mediaSource.addSourceBuffer(mimeCodec);
fetchAB(assetURL, function (buf) {
sourceBuffer.addEventListener('updateend', function (_) {
mediaSource.endOfStream();
video.play();
//console.log(mediaSource.readyState); // ended
});
console.log("buf",buf);
sourceBuffer.appendBuffer(buf);
});
};
function fetchAB (url, cb) {
console.log(url);
var xhr = new XMLHttpRequest;
xhr.open('get', url);
xhr.responseType = 'arraybuffer';
xhr.onload = function () {
cb(xhr.response);
};
xhr.send();
};
</script>
</body></html>
1.參考MSE(Media Source Extensions)的一點(diǎn)嘗試
遇到的坑是:一開(kāi)始用的是自己本地隨便找的一個(gè)視頻文件关顷,結(jié)果報(bào)錯(cuò):Uncaught DOMException: Failed to execute ‘endOfStream’ on ‘MediaSource’: The MediaSource’s readyState is not ‘open’.原因是該MP4文件不是 framented mp4糊秆,不支持這種MSE的播放形式。這里也提供一個(gè)轉(zhuǎn)換的工具解寝,支持將普通MP4轉(zhuǎn)為 framented mp4:Bento4 MP4 & DASH Class Library, SDK and Tools
2.參考mp4文件格式之fragment mp4
對(duì)于普通 MP4 文件扩然,整個(gè)mp4文件的的meta數(shù)據(jù)都在文件頭,所有媒體數(shù)據(jù)為整體一塊聋伦。當(dāng)文件比較大的時(shí)候夫偶,meta數(shù)據(jù)就比較大界睁。這樣對(duì)mp4文件的本地播放是沒(méi)有問(wèn)題。但對(duì)于一些視頻播放網(wǎng)站而言兵拢,用戶的播放器必須下載全meta數(shù)據(jù)才能開(kāi)始播放翻斟,這就意味著用戶的緩沖時(shí)間將因?yàn)閙p4文件的存儲(chǔ)結(jié)構(gòu)而延長(zhǎng)。目前一種解決方法是將大的mp4文件切成物理分離的多段说铃,使得每段的meta都比較小访惜,從而在一定程度上減少緩沖時(shí)間。
對(duì)于fragment mp4腻扇,mp4文件被分成多個(gè)frag分片债热,而原來(lái)的meta數(shù)據(jù)大大變小,且沒(méi)個(gè)frag都可以單獨(dú)索引幼苛、傳輸和播放窒篱,這樣就可以解決mp4不能流式傳輸播放的問(wèn)題。對(duì)用戶體驗(yàn)比較好舶沿。然而目前這種格式并不被多數(shù)解碼器完整支持墙杯,部分播放器加載文件時(shí)間過(guò)長(zhǎng),而且瀏覽器內(nèi)嵌播放器也可能不支持播放括荡。
3.參考WebSocket+MSE——HTML5 直播技術(shù)解析
我們可以看到 non-fragment mp4 的最頂層 box 類型非常少高镐,而 fragment mp4 是由一段一段的 moof+mdat 組成的,它們已經(jīng)包含了足夠的 metadata 信息與數(shù)據(jù), 可以直接 seek 到這個(gè)位置開(kāi)始播放畸冲。也就是說(shuō) fMP4 是一個(gè)流式的封裝格式嫉髓,這樣更適合在網(wǎng)絡(luò)中進(jìn)行流式傳輸,而不需要依賴文件頭的metadata召夹。
Apple在WWDC 2016 大會(huì)上宣布會(huì)在 iOS 10岩喷、tvOS、macO S的 HLS 中支持 fMP4监憎,可見(jiàn)fMP4 的前景非常的好纱意。
值得一提的是,fMP4鲸阔、CMAF偷霉、ISOBMFF 其實(shí)都是類似的東西。
把一個(gè) non-fragment MP4 轉(zhuǎn)換成 fragment MP4褐筛±嗌伲可以使用 FFmpeg 的 -movflags 來(lái)轉(zhuǎn)換。
對(duì)于原始文件為非 MP4 文件:ffmpeg -i trailer_1080p.mov -c:v copy -c:a copy -movflags frag_keyframe+empty_moov bunny_fragmented.mp4
對(duì)于原始文件已經(jīng)是 MP4 文件:ffmpeg -i non_fragmented.mp4 -movflags frag_keyframe+empty_moov fragmented.mp4
或者使用 mp4fragment:mp4fragment input.mp4 output.mp4
三渔扎、URL.createObjectURL
在H5直播系列一 Blob File FileReader URL曾經(jīng)介紹過(guò)URL.createObjectURL方法硫狞。
//blob參數(shù)是一個(gè)File對(duì)象或者Blob對(duì)象.
var objecturl = window.URL.createObjectURL(blob);
上面的代碼會(huì)對(duì)二進(jìn)制數(shù)據(jù)生成一個(gè) URL,這個(gè) URL 可以放置于任何通常可以放置 URL 的地方残吩,比如 img 標(biāo)簽的 src 屬性财忽。需要注意的是,即使是同樣的二進(jìn)制數(shù)據(jù)泣侮,每調(diào)用一次 URL.createObjectURL 方法即彪,就會(huì)得到一個(gè)不一樣的 URL。這個(gè) URL 的存在時(shí)間活尊,等同于網(wǎng)頁(yè)的存在時(shí)間隶校,一旦網(wǎng)頁(yè)刷新或卸載,這個(gè) URL 就失效蛹锰。(File 和 Blob 又何嘗不是這樣呢)除此之外深胳,也可以手動(dòng)調(diào)用 URL.revokeObjectURL 方法,使 URL 失效宁仔。
window.URL.revokeObjectURL(objectURL);
舉個(gè)簡(jiǎn)單的例子稠屠。
var blob = new Blob(["Hello hanzichi"]);
var a = document.createElement("a");
a.href = window.URL.createObjectURL(blob);
a.download = "a.txt";
a.textContent = "Download";
document.body.appendChild(a);
頁(yè)面上生成了一個(gè)超鏈接,點(diǎn)擊它就能下載一個(gè)名為 a.txt 的文件翎苫,里面的內(nèi)容是 Hello hanzichi。
四榨了、使用createObjectURL將MediaSource和video標(biāo)簽連接起來(lái)
var mediaSource = new MediaSource;
//console.log(mediaSource.readyState); // closed
video.src = URL.createObjectURL(mediaSource);
mediaSource.addEventListener('sourceopen', sourceOpen);
這里傳入createObjectURL的不是File或Blob了煎谍,而是MediaSource。MS 的實(shí)例通過(guò) URL.createObjectURL() 創(chuàng)建的 url 并不會(huì)同步連接到 video.src龙屉。換句話說(shuō)呐粘,URL.createObjectURL() 只是底層流(MS)和 video.src 的連接中間者,一旦兩者連接到一起之后转捕,該對(duì)象就沒(méi)用了作岖。那么什么時(shí)候 MS 才會(huì)和 video.src 連接到一起呢?創(chuàng)建實(shí)例都是同步的五芝,但是底層流和 video.src 的連接是異步的痘儡。MS 提供了一個(gè) sourceopen 事件給我們進(jìn)行這項(xiàng)異步處理。一旦連接到一起之后枢步,該 URL object 就沒(méi)用了沉删,處于內(nèi)存節(jié)省的目的,可以使用 URL.revokeObjectURL(vidElement.src) 銷(xiāo)毀指定的 URL object醉途。
mediaSource.addEventListener('sourceopen', sourceOpen);
function sourceOpen(){
URL.revokeObjectURL(vidElement.src)
}
MSE 支持具體的事件
- sourceopen 綁定到媒體元素后開(kāi)始觸發(fā)
- sourceclosed 未綁定到媒體元素后開(kāi)始觸發(fā)
- sourceended 所有數(shù)據(jù)接收完成后觸發(fā)
對(duì)應(yīng)的屬性mediaSource.readyState
- open MSE 實(shí)例矾瑰,已經(jīng)綁定到了媒體元素上,等待接受數(shù)據(jù)或者正在接受數(shù)據(jù)
- closed MSE 實(shí)例未綁定到了媒體元素上隘擎。MS剛創(chuàng)建時(shí)就是該狀態(tài)殴穴。
- ended MSE 實(shí)例,已經(jīng)綁定到了媒體元素上, 并且所有數(shù)據(jù)都已經(jīng)接受到了。當(dāng)endOfStream()執(zhí)行完成采幌,會(huì)變?yōu)樵摖顟B(tài)恍涂。
五、設(shè)置編碼類型mime 字符串
function sourceOpen(e) {
URL.revokeObjectURL(videoMp4.src);
var mime = 'video/webm; codecs="opus, vp9"';
// e.target refers to the mediaSource instance.
// Store it in a variable so it can be used in a closure.
var mediaSource = e.target;
var sourceBuffer = mediaSource.addSourceBuffer(mime);
// Fetch and process the video.
}
var mime = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"'
首先植榕,前面的 video/mp4 代表這是一段 mp4 格式封裝的視頻再沧,同理也存在類似 video/webm、audio/mpeg尊残、audio/mp4 這樣的 mime 格式炒瘸。一般情況下,可以通過(guò) canPlayType 這個(gè)方法來(lái)判斷瀏覽器是否支持當(dāng)前格式寝衫。
后面的這一段 codecs="...." 比較特別顷扩,以逗號(hào)相隔,分為兩段:
第一段慰毅,'avc1.42E01E'隘截,即它用于告訴瀏覽器關(guān)于視頻編解碼的一些重要信息,諸如編碼方式汹胃、分辨率婶芭、幀率、碼率以及對(duì)解碼器解碼能力的要求着饥。
在這個(gè)例子中犀农,**'avc1' **代表視頻采用 H.264 編碼,隨后是一個(gè)分隔點(diǎn)宰掉,之后是 3 個(gè)兩位的十六進(jìn)制的數(shù)呵哨,這 3 個(gè)十六進(jìn)制數(shù)分別代表:
- AVCProfileIndication(42)
- profile_compability(E0)
- AVCLevelIndication(1E)
第一個(gè)用于標(biāo)識(shí) H.264 的 profile,后兩個(gè)用于標(biāo)識(shí)視頻對(duì)于解碼器的要求轨奄。
對(duì)于一個(gè) mp4 視頻孟害,可以使用 mp4file 這樣的命令行工具:
mp4file --dump xxx.mp4
找到 avcC Box 后,就可以看到這三個(gè)值:
mp4file --dump movie.mp4
...
type avcC (moov.trak.mdia.minf.stbl.stsd.avc1.avcC) // avc1
configurationVersion = 1 (0x01)
AVCProfileIndication = 66 (0x42) // 42
profile_compatibility = 224 (0xe0) // E0
AVCLevelIndication = 30 (0x1e) // 1E
...
有一處要注意挪拟,后面兩個(gè)值(profile_compability挨务、AVCLevelIndication)只是瀏覽器用于判斷自身的解碼能力能否滿足需求,所以不需要和視頻完全對(duì)應(yīng)舞丛,更高也是可以的耘子。
下面來(lái)看 codecs 的第二段 'mp4a.40.2',這一段信息是關(guān)于音頻部分的球切,代表視頻的音頻部分采用了 AAC LC 標(biāo)準(zhǔn):'mp4a' 代表此視頻的音頻部分采用 MPEG-4 壓縮編碼谷誓。隨后是一個(gè)分隔點(diǎn),和一個(gè)十六進(jìn)制數(shù)(40)吨凑,這是 ObjectTypeIndication捍歪,40 對(duì)應(yīng)的是 Audio ISO/IEC 14496-3 標(biāo)準(zhǔn)户辱。(不同的值具有不同的含義,詳細(xì)可以參考官方文檔)
然后又是一個(gè)分隔點(diǎn)糙臼,和一個(gè)十進(jìn)制數(shù)(2)庐镐,這是 MPEG-4 Audio Object Type,維基百科中的解釋是 "MPEG-4 AAC LC Audio Object Type is based on the MPEG-2 Part 7 Low Complexity profile (LC) combined with Perceptual Noise Substitution (PNS) (defined in MPEG-4 Part 3 Subpart 4)"变逃,具體是什么意思就不翻譯了必逆,其實(shí)就是一種 H.264 視頻中常用的音頻編碼規(guī)范。
這一整段 codecs 都有完善的官方文檔揽乱,可以參考:The 'Codecs' and 'Profiles' Parameters for "Bucket" Media Types
六名眉、請(qǐng)求資源
sourceBuffer對(duì)象提供了一系列接口,這里用到的是 appendBuffer 方法凰棉,可以動(dòng)態(tài)地向 MediaSource 中添加視頻/音頻片段(對(duì)于一個(gè) MediaSource损拢,可以同時(shí)存在多個(gè) SourceBuffer)
如果視頻很長(zhǎng),存在多個(gè)chunk 的話撒犀,就需要不停地向 SourceBuffer 中加入新的 chunk福压。這里就需要注意一個(gè)問(wèn)題了,即 appendBuffer 是異步執(zhí)行的或舞,在完成前荆姆,不能 append 新的 chunk:
sourceBuffer.appendBuffer(buffer1)
sourceBuffer.appendBuffer(buffer2)
// Uncaught DOMException:
Failed to set the 'timestampOffset' property on 'SourceBuffer':
This SourceBuffer is still processing
an 'appendBuffer' or 'remove' operation.
而是應(yīng)該監(jiān)聽(tīng) SourceBuffer 上的 updateend 事件,確定空閑后嚷那,再加入新的 chunk:
sourceBuffer.addEventListener('updateend', () => {
// 這個(gè)時(shí)候才能加入新 chunk
// 先設(shè)定新chunk加入的位置胞枕,比如第20秒處
sourceBuffer.timestampOffset = 20
// 然后加入
sourceBuffer.append(newBuffer)
}
七、SourceBuffer簡(jiǎn)介
SourceBuffer 是由 mediaSource 創(chuàng)建魏宽,并直接和 HTMLMediaElement 接觸。簡(jiǎn)單來(lái)說(shuō)决乎,它就是一個(gè)流的容器队询,里面提供的 append(),remove() 來(lái)進(jìn)行流的操作构诚,它可以包含一個(gè)或者多個(gè) media segments蚌斩。
interface SourceBuffer : EventTarget {
attribute AppendMode mode;
readonly attribute boolean updating;
readonly attribute TimeRanges buffered;
attribute double timestampOffset;
readonly attribute AudioTrackList audioTracks;
readonly attribute VideoTrackList videoTracks;
readonly attribute TextTrackList textTracks;
attribute double appendWindowStart;
attribute unrestricted double appendWindowEnd;
attribute EventHandler onupdatestart;
attribute EventHandler onupdate;
attribute EventHandler onupdateend;
attribute EventHandler onerror;
attribute EventHandler onabort;
void appendBuffer(BufferSource data);
void abort();
void remove(double start, unrestricted double end);
};
1.mode
上面說(shuō)過(guò),SB(SourceBuffer) 里面存儲(chǔ)的是 media segments(就是你每次通過(guò) append 添加進(jìn)去的流片段)范嘱。SB.mode 有兩種格式:
- segments: 亂序排放送膳。通過(guò) timestamps 來(lái)標(biāo)識(shí)其具體播放的順序。比如:20s的 buffer丑蛤,30s 的 buffer 等叠聋。
- sequence: 按序排放。通過(guò) appendBuffer 的順序來(lái)決定每個(gè) mode 添加的順序受裹。timestamps 根據(jù) sequence 自動(dòng)產(chǎn)生碌补。
那么上面兩個(gè)哪個(gè)是默認(rèn)值呢虏束?看情況,講真厦章,沒(méi)騙你镇匀。當(dāng) media segments 天生自帶 timestamps,那么 mode 就為 segments 袜啃,否則為 sequence汗侵。所以,一般情況下群发,我們是不用管它的值晰韵。不過(guò),你可以在后面也物,將 segments 設(shè)置為 sequence 這個(gè)是沒(méi)毛病的宫屠。反之,將 sequence 設(shè)置為 segments 就有問(wèn)題了滑蚯。
var bufferMode = sourceBuffer.mode;
if (bufferMode == 'segments') {
sourceBuffer.mode = 'sequence';
}
segments 表示 A/V 的播放時(shí)根據(jù)你視頻播放流中的 pts 來(lái)決定浪蹂,該模式也是最常使用的。因?yàn)橐粢曨l播放中告材,最重要的就是 pts 的排序坤次。因?yàn)椋琾ts 可以決定播放的時(shí)長(zhǎng)和順序斥赋,如果一旦 A/V 的 pts 錯(cuò)開(kāi)缰猴,有可能就會(huì)造成 A/V sync drift。
sequence 則是根據(jù)空間上來(lái)進(jìn)行播放的疤剑。每次通過(guò) appendBuffer 來(lái)添加指定的 Buffer 的時(shí)候滑绒,實(shí)際上就是添加一段 A/V segment。此時(shí)隘膘,播放器會(huì)根據(jù)其添加的位置疑故,來(lái)決定播放順序。還需要注意弯菊,在播放的同時(shí)纵势,你需要告訴 SB,這段 segment 有多長(zhǎng)管钳,也就是該段 Buffer 的實(shí)際偏移量钦铁。而該段偏移量就是由 timestampOffset 決定的。整個(gè)過(guò)程用代碼描述一下就是:
sb.appendBuffer(media.segment);
sb.timestampOffset += media.duration;
另外才漆,如果你想手動(dòng)更改 mode 也是可以的牛曹,不過(guò)需要注意幾個(gè)先決條件:
- 對(duì)應(yīng)的 SB.updating 必須為 false.
- 如果該 parent MS 處于 ended 狀態(tài),則會(huì)手動(dòng)將 MS readyState 變?yōu)?open 的狀態(tài)栽烂。
2.buffered
返回一個(gè) timeRange 對(duì)象躏仇。用來(lái)表示當(dāng)前被存儲(chǔ)在 SB 中的 buffer恋脚。
- updating
返回 Boolean,表示當(dāng)前 SB 是否正在被更新焰手。例如: SourceBuffer.appendBuffer(), SourceBuffer.appendStream(), SourceBuffer.remove() 調(diào)用時(shí)糟描。
- true:當(dāng)前 SB 正在處理添加或者移除的 segment
- false:當(dāng)前 SB 處于空閑狀態(tài)。當(dāng)且僅當(dāng) updating = false 的時(shí)候书妻,才可以對(duì) SB 進(jìn)行額外的操作船响。
SB 內(nèi)部的 buffer 管理主要是通過(guò) appendBuffer(BufferSource data) 和 remote() 兩個(gè)方法來(lái)實(shí)現(xiàn)的。當(dāng)然躲履,并不是所有的 Buffer 都能隨便添加給指定的 SB见间,這里面是需要條件和相關(guān)順序的。
- 該 buffer工猜,必須滿足 MIME 限定的類型
- 該 buffer米诉,必須包含 initialization segments(IS) 和 media segments(MS)
下圖是相關(guān)的支持 MIME:
這里需要提醒大家一點(diǎn),MSE 只支持 fmp4 的格式篷帅。具體內(nèi)容可以參考: 學(xué)好 MP4史侣,讓直播更給力。上面提到的 IS 和 MS 實(shí)際上就是 FMP4 中不同盒子的集合而已魏身。
4.事件
在 SB 中惊橱,相關(guān)事件觸發(fā)包括:
- updatestart: 當(dāng) updating 由 false 變?yōu)?true。
- update:當(dāng) append()/remove() 方法被成功調(diào)用完成時(shí)箭昵,updating 由 true 變?yōu)?false税朴。
- updateend: append()/remove() 已經(jīng)結(jié)束
- error: 在 append() 過(guò)程中發(fā)生錯(cuò)誤,updating 由 true 變?yōu)?false家制。
- abort: 當(dāng) append()/remove() 過(guò)程中正林,使用 abort() 方法廢棄時(shí),會(huì)觸發(fā)颤殴。此時(shí)卓囚,updating 由 true 變?yōu)?false。
注意上面有兩個(gè)事件比較類似:update 和 updateend诅病。都是表示處理的結(jié)束,不同的是粥烁,update 比 updateend 先觸發(fā)贤笆。
sourceBuffer.addEventListener('updateend', function (e) {
// 當(dāng)指定的 buffer 加載完后,就可以開(kāi)始播放
mediaSource.endOfStream();
video.play();
});
5.添加/移除 buffer
在添加 Buffer 的時(shí)候讨阻,你需要了解你所采用的 mode 是哪種類型芥永,sequence 或者 segments。這兩種是完全兩種不同的添加方式钝吮。
(1)segments
這種方式是直接根據(jù) MP4 文件中的 pts 來(lái)決定播放的位置和順序埋涧,它的添加方式極其簡(jiǎn)單板辽,只需要判斷 updating === false,然后棘催,直接通過(guò) appendBuffer 添加即可劲弦。
if (!sb.updating) {
let MS = this._mergeBuffer(media.tmpBuffer);
sb.appendBuffer(MS); // ****
media.duration += lib.duration;
media.tmpBuffer = [];
}
(2)sequence
如果你是采用這種方式進(jìn)行添加 Buffer 進(jìn)行播放的話,那么你也就沒(méi)必要了解 FMP4 格式醇坝,而是了解 MP4 格式邑跪。因?yàn)椋撃J较潞糁恚琒B 是根據(jù)具體添加的位置來(lái)進(jìn)行播放的画畅。所以,如果你是 FMP4 的話宋距,有可能就有點(diǎn)不適合了轴踱。針對(duì) sequence 來(lái)說(shuō),每段 buffer 都必須有自己本身的指定時(shí)長(zhǎng)谚赎,每段 buffer 不需要參考的 baseDts淫僻,即,他們直接可以毫無(wú)關(guān)聯(lián)沸版。那 sequence 具體怎么操作呢嘁傀?
簡(jiǎn)單來(lái)說(shuō),在每一次添加過(guò)后视粮,都需要根據(jù)指定 SB 上的 timestampOffset细办。該屬性,是用來(lái)控制具體 Buffer 的播放時(shí)長(zhǎng)和位置的蕾殴。
if (!sb.updating) {
let MS = this._mergeBuffer(media.tmpBuffer);
sb.appendBuffer(MS); // ****
sb.timestampOffset += lib.duration; // ****
media.tmpBuffer = [];
}
上面兩端打 * 號(hào)的就是重點(diǎn)內(nèi)容笑撞。該方式比較容易用來(lái)直接控制 buffer 片段的添加,而不用過(guò)度關(guān)注相對(duì) baseDTS 的值钓觉。
6.控制播放片段
如果要在 video 標(biāo)簽中控制指定片段的播放茴肥,一般是不可能的。因?yàn)榈丛郑诩虞d整個(gè)視頻 buffer 的時(shí)候瓤狐,視頻長(zhǎng)度就已經(jīng)固定的,剩下的只是你如果在 video 標(biāo)簽中控制播放速度和音量大小批幌。而在 MSE 中础锐,如何在已獲得整個(gè)視頻流 Buffer 的前提下,完成底層視頻 Buffer 的切割和指定時(shí)間段播放呢荧缘?
這里皆警,需要利用 SB 下的 appendWindowStart 和 appendWindowEnd 這兩個(gè)屬性。
他們兩個(gè)屬性主要是為了設(shè)置截粗,當(dāng)有視頻 Buffer 添加時(shí)信姓,只有符合在 [start,end] 之間的 media frame 才能 append鸵隧,否則,無(wú)法 append意推。例如:
sourceBuffer.appendWindowStart = 2.0;
sourceBuffer.appendWindowEnd = 5.0;
設(shè)置添加 Buffer 的時(shí)間戳為 [2s,5s] 之間豆瘫。appendWindowStart 和 appendWindowEnd 的基準(zhǔn)單位為 s。該屬性值左痢,通常在添加 Buffer 之前設(shè)置靡羡。
6.SB 內(nèi)存釋放
SB 內(nèi)存釋放其實(shí)就和在 JS 中,將一個(gè)變量指向 null 一樣的過(guò)程俊性。
var a = new ArrayBuffer(1024 * 1000);
a = null; // start garbage collection
在 SB 中略步,簡(jiǎn)單的來(lái)說(shuō),就是移除指定的 time ranges’ buffer定页。需要用到的 API 為:
remove(double start, unrestricted double end);
具體的步驟為:
- 找到具體需要移除的 segment趟薄。
- 得到其開(kāi)始(start)的時(shí)間戳(以 s 為單位)
- 得到其結(jié)束(end)的時(shí)間戳(以 s 為單位)
- 此時(shí),updating 為 true典徊,表明正在移除
- 完成之后杭煎,出發(fā) updateend 事件
如果,你想直接清空 Buffer 重新添加的話卒落,可以直接利用 abort() API 來(lái)做羡铲。它的工作是清空當(dāng)前 SB 中所有的 segment,使用方法也很簡(jiǎn)單儡毕,不過(guò)就是需要注意不要和 remove 操作一起執(zhí)行也切。更保險(xiǎn)的做法就是直接,通過(guò) updating===false 來(lái)完成:
if(sb.updating===false){
sb.abort();
}
這時(shí)候腰湾,abort 的主要流程為:
- 確保 MS.readyState===“open”
- 將 appendWindowStart 設(shè)置為 pts 原始值雷恃,比如,0
- 將 appendWindowEnd 設(shè)置為正無(wú)限大费坊,即倒槐,Infinity。
abort(): 用來(lái)放棄當(dāng)前 append 流的操作附井。不過(guò)讨越,該方法的業(yè)務(wù)場(chǎng)景也比較有限。它只能用在當(dāng) SB 正在更新流的時(shí)候永毅。即谎痢,此時(shí)通過(guò) fetch,已經(jīng)接受到新流,并且使用 appendBuffer 添加卷雕,此為開(kāi)始的時(shí)間。然后到 updateend 事件觸發(fā)之前票从,這段時(shí)間之內(nèi)調(diào)用 abort()
漫雕。有一個(gè)業(yè)務(wù)場(chǎng)景是滨嘱,當(dāng)用戶移動(dòng)進(jìn)度條,而此時(shí) fetch 已經(jīng)獲取前一次的 media segments浸间,那么可以使用 abort
放棄該操作太雨,轉(zhuǎn)而請(qǐng)求新的 media segments。具體可以參考:abort 使用
7.appendBuffer(ArrayBuffer)
用來(lái)添加 ArrayBuffer魁蒜。該 ArrayBuffer 一般是通過(guò) fetch 的 response.arrayBuffer();
來(lái)獲取的囊扳。在使用 addSourceBuffer 創(chuàng)建之前,還需要保證當(dāng)前瀏覽器是否支持該編碼格式兜看。當(dāng)然锥咸,不支持也行,頂多是當(dāng)前 MS 報(bào)錯(cuò)细移,斷掉當(dāng)前 JS 線程搏予。
八、MediaSource簡(jiǎn)介
[Constructor]
interface MediaSource : EventTarget {
readonly attribute SourceBufferList sourceBuffers;
readonly attribute SourceBufferList activeSourceBuffers;
readonly attribute ReadyState readyState;
attribute unrestricted double duration;
attribute EventHandler onsourceopen;
attribute EventHandler onsourceended;
attribute EventHandler onsourceclose;
SourceBuffer addSourceBuffer(DOMString type);
void removeSourceBuffer(SourceBuffer sourceBuffer);
void endOfStream(optional EndOfStreamError error);
void setLiveSeekableRange(double start, double end);
void clearLiveSeekableRange();
static boolean isTypeSupported(DOMString type);
};
1.isTypeSupported
isTypeSupported 主要是用來(lái)檢測(cè) MS 是否支持某個(gè)特定的編碼和容器盒子弧轧。例如:
MediaSource.isTypeSupported('video/mp4; codecs="avc1.42E01E, mp4a.40.2"')
這里有一份具體的 mimeType 參考列表雪侥。
2.addSourceBuffer
用來(lái)返回一個(gè)具體的視頻流 SB,接受一個(gè) mimeType 表示該流的編碼格式精绎。例如:
var mimeType = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"';
var sourceBuffer = mediaSource.addSourceBuffer(mimeType);
3.removeSourceBuffer
用來(lái)移除某個(gè) sourceBuffer速缨。比如當(dāng)前流已經(jīng)結(jié)束,那么你就沒(méi)必要再保留當(dāng)前 SB 來(lái)占用空間代乃,可以直接移除旬牲。具體格式為:
mediaSource.removeSourceBuffer(sourceBuffer);
4.endOfStream()
用來(lái)表示接受的視頻流的停止,注意襟己,這里并不是斷開(kāi)引谜,相當(dāng)于只是下好了一部分視頻,然后你可以進(jìn)行播放擎浴。此時(shí)员咽,MS 的狀態(tài)變?yōu)椋篹nded。例如:
var mediaSource = this;
var sourceBuffer = mediaSource.addSourceBuffer(mimeCodec);
fetchAB(assetURL, function (buf) {
sourceBuffer.addEventListener('updateend', function (_) {
mediaSource.endOfStream(); // 結(jié)束當(dāng)前的接受
video.play(); // 可以播放當(dāng)前獲得的流
});
sourceBuffer.appendBuffer(buf);
});
5.sourceBuffers
sourceBuffers 是 MS 實(shí)例上的一個(gè)屬性贮预,它返回的是一個(gè) SourceBufferList 的對(duì)象贝室,里面可以獲取當(dāng)前 MS 上掛載的所有 SB。不過(guò)仿吞,只有當(dāng) MS 為 open 狀態(tài)的時(shí)候滑频,它才可以訪問(wèn)。具體使用為:
let SBs = mediaSource.sourceBuffers;
那我們?cè)趺传@取到具體的 SB 對(duì)象呢唤冈?因?yàn)橄棵裕浞祷刂凳?SourceBufferList 對(duì)象,具體格式為:
interface SourceBufferList : EventTarget {
readonly attribute unsigned long length;
attribute EventHandler onaddsourcebuffer;
attribute EventHandler onremovesourcebuffer;
getter SourceBuffer (unsigned long index);
};
簡(jiǎn)單來(lái)說(shuō),你可以直接通過(guò) index 來(lái)訪問(wèn)具體的某個(gè) SB:
let SBs = mediaSource.sourceBuffers;
let SB1 = SBs[0];
SBL 對(duì)象還提供了 addsourcebuffer 和 removesourcebuffer 事件绘搞,如果你想監(jiān)聽(tīng) SB 的變化彤避,可以直接通過(guò) SBL 來(lái)做。這也是為什么 MS 沒(méi)有提供監(jiān)聽(tīng)事件的一個(gè)原因夯辖。所以琉预,刪除某一個(gè) SB 就可以通過(guò) SBL 查找,然后蒿褂,利用 remove 方法移除即可:
let SBs = mediaSource.sourceBuffers;
let SB1 = SBs[0];
mediaSource.removeSourceBuffer(SB1);
6.activeSourceBuffers
activeSourceBuffers 實(shí)際上是 sourceBuffers 的子集圆米,返回的同樣也是 SBL 對(duì)象。為什么說(shuō)也是子集呢啄栓?
因?yàn)?ASBs 包含的是當(dāng)前正在使用的 SB娄帖。因?yàn)榍懊嬲f(shuō)了,每個(gè) SB 實(shí)際上都可以具體代表一個(gè) track谴供,比如块茁,video track,audio track桂肌,text track 等等数焊,這些都算。那怎么標(biāo)識(shí)正在使用的 SB 呢崎场?很簡(jiǎn)單佩耳,不用標(biāo)識(shí)啊,因?yàn)榭刂颇囊粋€(gè) SB 正在使用是你來(lái)決定的谭跨。如果非要標(biāo)識(shí)干厚,就需要使用到 HTML 中的 video 和 audio 節(jié)點(diǎn)。通過(guò)
audioTrack = media.audioTracks[index]
videoTrack = media.videoTracks[index]
// media 為具體的 video/audio 的節(jié)點(diǎn)
// 返回值就是 video/audio 的底層 tracks
audioTrack = media.audioTracks.getTrackById( id )
videoTrack = media.videoTracks.getTrackById( id )
videoTrack.selected // 返回 boolean 值螃宙,標(biāo)識(shí)是否正在被使用
上面的代碼只是告訴你客叉,正在使用 的含義是什么灶体。對(duì)于我們實(shí)際編碼的 SB 來(lái)說(shuō)纳胧,并沒(méi)有太多關(guān)系浙于,了解就好。上面說(shuō)了 ASBs 返回值也是一個(gè) SBL堂湖。所以闲先,使用方式可以直接參考 SBL 即可。
7.狀態(tài)切換
要說(shuō)道狀態(tài)切換无蜂,我們得先知道 MS 一共有幾個(gè)狀態(tài)值伺糠。MS 本身狀態(tài)并不復(fù)雜,一共只有三個(gè)狀態(tài)值:
enum ReadyState {
"closed",
"open",
"ended"
};
- closed: 當(dāng)前的 MS 并沒(méi)有和 HTMLMedia 元素連接
- open: MS 已經(jīng)和 HTMLMedia 連接斥季,并且等待新的數(shù)據(jù)被添加到 SB 中去训桶。
- ended: 當(dāng)調(diào)用 endOfStream 方法時(shí)會(huì)觸發(fā),并且此時(shí)依然和 HTMLMedia 元素連接。
記住渊迁,closed 和 ended 到的區(qū)別關(guān)鍵點(diǎn)在于有沒(méi)有和 HTMLMedia 元素連接慰照。
其對(duì)應(yīng)的還有三個(gè)監(jiān)聽(tīng)事件:
- sourceopen: 當(dāng)狀態(tài)變?yōu)?open 時(shí)觸發(fā)。常常在 MS 和 HTMLMedia 綁定時(shí)觸發(fā)琉朽。
- sourceended: 當(dāng)狀態(tài)變?yōu)?ended 時(shí)觸發(fā)。
- sourceclose: 當(dāng)狀態(tài)變?yōu)?closed 時(shí)觸發(fā)稚铣。
那哪種條件下會(huì)觸發(fā)呢箱叁?
(1)sourceopen 觸發(fā)
sourceopen 事件相同于是一個(gè)總領(lǐng)事件,只有當(dāng) sourceopen 時(shí)間觸發(fā)后惕医,后續(xù)對(duì)于 MS 來(lái)說(shuō)耕漱,才是一個(gè)可操作的對(duì)象。通常來(lái)說(shuō)抬伺,只有當(dāng) MS 和 video 元素成功綁定時(shí)螟够,才會(huì)正常觸發(fā):
let mediaSource = new MediaSource();
vidElement.src = URL.createObjectURL(mediaSource);
其實(shí)這簡(jiǎn)單的來(lái)說(shuō),就是給 MS 添加 HTML media 元素峡钓。其整個(gè)過(guò)程為:
- 先延時(shí) media 元素的 load 事件妓笙,將 delaying-the-load-event-flag 設(shè)置為 false
- 將 readyState 設(shè)置為 open。
- 觸發(fā) MS 的 sourceopen 事件
(2)sourceended 觸發(fā)
sourceended 的觸發(fā)條件其實(shí)很簡(jiǎn)單能岩,只有當(dāng)你調(diào)用 endOfStream 的時(shí)候寞宫,會(huì)進(jìn)行相關(guān)的觸發(fā)。mediaSource.endOfStream();
這個(gè)就沒(méi)啥需要過(guò)多講的了拉鹃。
(3)sourceclose 的觸發(fā)
sourceclose 是在 media 元素和 MS 斷開(kāi)的時(shí)候辈赋,才會(huì)觸發(fā)。那這個(gè)怎么斷開(kāi)呢膏燕?難道直接將 media 的元素的 src 直接設(shè)置為 null 就 OK 了嗎钥屈?要是這樣,我就日了狗了坝辫。MS 會(huì)這么簡(jiǎn)單么篷就?實(shí)際上并不,如果要手動(dòng)觸發(fā) sourceclose 事件的話阀溶,則需要下列步驟:
- 將 readyState 設(shè)置為 closed
- 將 MS.duration 設(shè)置為 NaN
- 移除 activeSourceBuffers 上的所有 Buffer
- 觸發(fā) activeSourceBuffers 的 removesourcebuffer 事件
- 移除 sourceBuffers 上的 SourceBuffer腻脏。
- 觸發(fā) sourceBuffers 的 removesourcebuffer 事件
- 觸發(fā) MediaSource 的 sourceclose 事件
到這里,三個(gè)狀態(tài)事件基本就介紹完了银锻。不過(guò)永品,感覺(jué)只有 sourceopen 才是最有用的一個(gè)。
8.track 的切換
track 這個(gè)概念其實(shí)是音視頻播放的軌道击纬,它和 MS 沒(méi)有太大的關(guān)系鼎姐。不過(guò),和 SB 還是有一點(diǎn)關(guān)系的。因?yàn)榭唤埃硞€(gè)一個(gè) SB 里面可能會(huì)包含一個(gè) track 或者說(shuō)是幾個(gè) track饭尝。所以,推薦某一個(gè) SB 最好包含一個(gè)值包含一個(gè) track献宫,這樣钥平,后面的 track 也方便更換。在 track 中的替換里姊途,有三種類型涉瘾,audio,video捷兰,text 軌道立叛。
(1)video 切換
切換的含義有兩種,一種是移除原有的贡茅,一種是添加新的秘蛇。這里,我們需要分兩部分來(lái)講解顶考。
(a)移除原有不需要 track
- 從 activeSourceBuffers 移除與當(dāng)前 track 相關(guān)的 SB
- 觸發(fā) activeSourceBuffers 的 removesourcebuffer 事件
(b)添加指定的 track
- 從 activeSourceBuffers 添加指定的 SourceBuffer
- 觸發(fā) activeSourceBuffers 的 addsourcebuffer 事件
(2)audio 切換
audio 的切換和 video 的過(guò)程一模一樣赁还。這里我就不過(guò)多贅述了。
9.MS duration 修正機(jī)制
MS 的 duration 實(shí)際上就是 media 中播放的時(shí)延村怪。通常來(lái)說(shuō)秽浇,A/V track 實(shí)際上是兩個(gè)獨(dú)立的播放流,這中間必定會(huì)存在先關(guān)的差異時(shí)間甚负。但是柬焕,media 播放機(jī)制永遠(yuǎn)會(huì)以最長(zhǎng)的 duration 為準(zhǔn)。這種情況對(duì)于 live stream 的播放梭域,特別適合斑举。因?yàn)?liveStream 是不斷動(dòng)態(tài)添加 buffer,但是 buffer 內(nèi)部會(huì)有一定的時(shí)長(zhǎng)的病涨,而 MS 就需要針對(duì)這個(gè) buffer 進(jìn)行動(dòng)態(tài)更新富玷。整個(gè)更新機(jī)制為:
- 當(dāng)前 MS.duration 更新為 new duration。
- 如果 new duration 比 sourceBuffers 中的最大的 pts 小既穆,這時(shí)候就會(huì)報(bào)錯(cuò)赎懦。
- 讓最后一個(gè)的 sample 的 end time 為所后 timeRanges 的 end time。
- 將 new duration 設(shè)置為當(dāng)前 SourceBuffer 中最大的 endTime幻工。
- 將 video/audio 的播放時(shí)長(zhǎng)(duration) 設(shè)置為最新的 new duration励两。
10.如何界定 track
這里先聲明一下,track 和 SB 并不是一一對(duì)應(yīng)的關(guān)系囊颅。他們的關(guān)系只能是 SB : track = 1: 1 or 2 or 3当悔。即傅瞻,一個(gè) SB可能包含,一個(gè) A/V track(1)盲憎,或者嗅骄,一個(gè) Video track ,一個(gè)Audio track(2)饼疙,或者 再額外加一個(gè) text track(3)溺森。
上面也說(shuō)過(guò),推薦將 track 和 SB 設(shè)置為一一對(duì)應(yīng)的關(guān)系窑眯,應(yīng)該這樣比較好控制儿惫,比如,移除或者同步等操作伸但。具體編碼細(xì)節(jié)我們有空再說(shuō),這里先來(lái)說(shuō)一下留搔,SB 里面怎么決定 track 的播放更胖。
track 最重要的特性就是 pts ,duration隔显,access point flag却妨。track 中 最基本的單位叫做 Coded Frame,表示具體能夠播放的音視頻數(shù)據(jù)括眠。它本身其實(shí)就是一些列的 media data彪标,并且這些 media data 里面必須包含 pts,dts掷豺,sampleDuration 的相關(guān)信息捞烟。在 SB 中,有幾個(gè)基本內(nèi)部屬性是用來(lái)標(biāo)識(shí)前面兩個(gè)字段的当船。
- last decode timestamp: 用來(lái)表示最新一個(gè) frame 的編碼時(shí)間(pts)题画。默認(rèn)為 null 表示里面沒(méi)有任何數(shù)據(jù)
- last frame duration: 表示 coded frame group 里面最新的 frame 時(shí)長(zhǎng)。
- highest end timestamp: 相當(dāng)于就是最后一個(gè) frame 的 pts + duration
- need random access point flag: 這個(gè)就相當(dāng)于是同步幀的意思德频。主要設(shè)置是根據(jù)音視頻流 里面具體字段決定的苍息,和前端這邊編碼沒(méi)關(guān)系。
- track buffer ranges: 該字段表示的是 coded frame group 里面壹置,每一幀對(duì)應(yīng)存儲(chǔ)的 pts 范圍竞思。
這里需要特別說(shuō)一下 last frame duration 的概念,其實(shí)也就是 Coded Frame Duration 的內(nèi)容钞护。Coded Frame Duration 針對(duì)不同的 track 有兩種不同的含義盖喷。一種是針對(duì) video/text 的 track,一種是針對(duì) audio 的 track:
- video/text: 其播放時(shí)長(zhǎng)(duration)直接是根據(jù) pts 直接的差值來(lái)決定患亿,和你具體播放的 samplerate 沒(méi)啥關(guān)系传蹈。雖然押逼,官方也有一個(gè)計(jì)算 refsampelDuration 的公式:duration = timescale / fps,不過(guò)惦界,由于視頻的幀率是動(dòng)態(tài)變化的挑格,沒(méi)什么太大的作用。
- audio: audio 的播放時(shí)長(zhǎng)必須是嚴(yán)格根據(jù)采樣頻率來(lái)的沾歪,即漂彤,其播放時(shí)間必須和你自己定制的 timescale 以及 sampleRate 一致才行。針對(duì)于 AAC灾搏,因?yàn)槠洳蓸宇l率常為 44100Hz挫望,其固定播放時(shí)長(zhǎng)則為:duration = 1024 / sampleRate * timescale
所以,如果你在針對(duì) unstable stream 做同步的話狂窑,一定需要注意這個(gè)坑媳板。有時(shí)候,dts 不同步泉哈,有可能才是真正的同步蛉幸。
我們?cè)倩氐缴厦娴淖?title 上-- 如果界定 track。一個(gè) SB 里面是否擁有一個(gè)或者多個(gè) track丛晦,主要是根據(jù)里面的視頻格式來(lái)決定的奕纫。打個(gè)比方,比如烫沙,你是在編碼 MP4 的流文件匹层。它里面的 track 內(nèi)容,則是根據(jù) moov box 中的 trak box 來(lái)判斷的锌蓄。即升筏,如果你的 MP4 文件只包含一個(gè),那么煤率,里面的 track 也有只有一個(gè)仰冠。
九、MSE兼容性 caniuse
1.iOS Safari 不支持 Media Source Extensions 因此無(wú)法使用 flv.js
以下摘自HTML5 媒體源擴(kuò)展(MSE):把影視制作級(jí)別的視頻格式帶入 Web
要覆蓋99%的用戶蝶糯,我們需要做一個(gè)視頻流兼容設(shè)置洋只,這樣也可以讓那些不支持MSE的瀏覽器也能順利播放,比如一些舊版本的瀏覽器昼捍,和iOS上的Safari识虚。老的瀏覽器可以使用Flash播放器來(lái)提供服務(wù),F(xiàn)lash播放器是可以直接播放MSE的MPEG-DASH格式內(nèi)容的妒茬,如Bitdash player播放器担锤。為了支持iOS設(shè)備,我們必須要使用Apple的HLS流媒體格式乍钻,這是蘋(píng)果在HTML5中強(qiáng)推的另一種方式肛循。Apple并不喜歡支持開(kāi)放標(biāo)準(zhǔn)(如MSE)铭腕,不過(guò)Mac OSX上的Safari還是支持MSE的。
2.以下摘自X5內(nèi)核視頻之問(wèn)答匯總--本帖最后由 YongLing 于 2018-07-05 14:45:51 編輯
Q:X5內(nèi)核支持MSE嗎多糠?
A:X5內(nèi)核MSE正在支持中累舷,預(yù)計(jì)TBS44200版本及以后,QQ瀏覽器8.6版本及以后支持夹孔。