@(音視訊)[Audio|Video|MSE]
音視訊隨著網際網路的發展,對音視訊的需求越來越多,然而音視訊無亂是播放還是編解碼,封裝對效能要求都比較高,那現階段的前端再音視訊領域都能做些什麼呢。
[TOC]
音訊或視訊的播放
html5 audio
提起音視訊的播放,我萌首先想到的是HTMLMediaElement,video
播放視訊,audio
播放音訊。舉個栗子:
<audio controls autoplay loop="true" preload="auto" src="audio.mp3"></audio>
複製程式碼
controls
指定瀏覽器渲染成html5audio
.autoplay
屬性告訴瀏覽器,當載入完的時候,自動播放.loop
屬性迴圈播放.preload
當渲染到audio元素時,便載入音訊檔案.- 移動端的瀏覽器並不支援
autoplay
和preload
屬性,即不會自動載入音訊檔案,只有通過一些事件觸發,比如touch
、click
事件等觸發載入然後播放. - 媒體元素還有一些改變音量,某段音訊播放完成事件等,請閱讀HTMLMediaElement.
- 當然如果你的網頁是跑在WebView中,可以讓客戶端設定一些屬性實現預載入和自動播放。
AudioContext
雖然使用html5的audio
可以播放音訊,但是正如你看到存在很多問題,同時我萌不能對音訊的播放進行很好的控制,比如說從網路中獲取到音訊二進位制資料,有的時候我萌想順序播放多段音訊,對於使用audio元素也是力不從心,處理起來並不優雅。
舉個栗子:
function queuePlayAudio(sounds) {
let index = 0;
function recursivePlay(sounds, index) {
if(sounds.length == index) return;
sounds[index].play();
sounds[index].onended = recursivePlay.bind(this, sounds, ++index);
}
}
複製程式碼
監聽audio
元素的 onended
事件,順序播放。
為了更好的控制音訊播放,我萌需要AudioContext.
AudioContext介面表示由音訊模組連線而成的音訊處理圖,每個模組對應一個AudioNode。AudioContext可以控制它所包含的節點的建立,以及音訊處理、解碼操作的執行。做任何事情之前都要先建立AudioContext物件,因為一切都發生在這個環境之中。
可能理解起來比較晦澀,簡單的來說,AudioContext 像是一個工廠,對於一個音訊的播放,從音源到聲音控制,到連結播放硬體的實現播放,都是由各個模組負責處理,通過connect 實現流程的控制。
現在我萌便能實現音訊的播放控制,比如從網路中獲取。利用AJAX中獲取 arraybuffer型別資料,通過解碼,然後把音訊的二進位制資料傳給AudioContext建立的BufferSourceNode,最後通過連結 destination
模組實現音訊的播放。
export default class PlaySoundWithAudioContext {
constructor() {
if(PlaySoundWithAudioContext.isSupportAudioContext()) {
this.duration = 0;
this.currentTime = 0;
this.nextTime = 0;
this.pending = [];
this.mutex = false;
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
}
static isSupportAudioContext() {
return window.AudioContext || window.webkitAudioContext;
}
play(buffer) {
var source = this.audioContext.createBufferSource();
source.buffer = buffer;
source.connect(this.audioContext.destination);
source.start(this.nextTime);
this.nextTime += source.buffer.duration;
}
addChunks(buffer) {
this.pending.push(buffer);
let customer = () => {
if(!this.pending.length) return;
let buffer = this.pending.shift();
this.audioContext.decodeAudioData(buffer, buffer => {
this.play(buffer);
console.log(buffer)
if(this.pending.length) {
customer()
}
}, (err) => {
console.log('decode audio data error', err);
});
}
if(!this.mutex) {
this.mutex = true;
customer()
}
}
clearAll() {
this.duration = 0;
this.currentTime = 0;
this.nextTime = 0;
}
}
複製程式碼
AJAX呼叫
function xhr() {
var XHR = new XMLHttpRequest();
XHR.open('GET', '//example.com/audio.mp3');
XHR.responseType = 'arraybuffer';
XHR.onreadystatechange = function(e) {
if(XHR.readyState == 4) {
if(XHR.status == 200) {
playSoundWithAudioContext.addChunks(XHR.response);
}
}
}
XHR.send();
}
複製程式碼
使用Ajax播放對於小段的音訊檔案還行,但是一大段音訊檔案來說,等到下載完成才播放,不太現實,能否一邊下載一邊播放呢。這裡就要利用 fetch 實現載入stream
流。
fetch(url).then((res) => {
if(res.ok && (res.status >= 200 && res.status <= 299)) {
readData(res.body.getReader())
} else {
that.postMessage({type: constants.LOAD_ERROR})
}
})
function readData(reader) {
reader.read().then((result) => {
if(result.done) {
return;
}
console.log(result);
playSoundWithAudioContext.addChunks(result.value.buffer);
})
}
複製程式碼
簡單的來說,就是fetch
的response
返回一個readableStream介面,通過從中讀取流,不斷的餵給audioContext 實現播放,測試發現移動端不能順利實現播放,pc端瀏覽器可以。
PCM audio
實現audioContext播放時,我萌需要解碼,利用decodeAudioData
api實現解碼,我萌都知道,一般音訊都要壓縮成mp3,aac這樣的編碼格式,我萌需要先解碼成PCM資料才能播放,那PCM 又是什麼呢?我萌都知道,聲音都是由物體振動產生,但是這樣的聲波無法被計算機儲存計算,我萌需要使用某種方式去刻畫聲音,於是乎便有了PCM格式的資料,表示麥克風採集聲音的頻率,採集的位數以及聲道數,立體聲還是單聲道。
Media Source Extensions
Media Source Extensions可以動態的給Audio
和Video
建立stream流,實現播放,簡單的來說,可以很好的播放進行控制,比如再播放的時候實現 seek 功能什麼的,也可以在前端對某種格式進行轉換進行播放,並不是支援所有的格式的。
通過將資料append進SourceBuffer
中,MSE把這些資料存進緩衝區,解碼實現播放。這裡簡單的舉個使用MSE播放 audio
的栗子:
export default class PlaySoundWithMSE{
constructor(audio) {
this.audio = audio;
if(PlaySoundWithMSE.isSupportMSE()) {
this.pendingBuffer = [];
this._mediaSource = new MediaSource();
this.audio.src = URL.createObjectURL(this._mediaSource);
this._mediaSource.addEventListener('sourceopen', () => {
this.sourcebuffer = this._mediaSource.addSourceBuffer('audio/mpeg');
this.sourcebuffer.addEventListener('updateend',
this.handleSourceBufferUpdateEnd.bind(this));
})
}
}
addBuffer(buffer) {
this.pendingBuffer.push(buffer);
}
handleSourceBufferUpdateEnd() {
if(this.pendingBuffer.length) {
this.sourcebuffer.appendBuffer(this.pendingBuffer.shift());
} else {
this._mediaSource.endOfStream();
}
}
static isSupportMSE() {
return !!window.MediaSource;
}
}
複製程式碼
HTML5 播放器
談起html5播放器,你可能知道bilibili的flv.js,它便是依賴Media Source Extensions將flv編碼格式的視訊轉包裝成mp4格式,然後實現播放。
從流程圖中可以看到,IOController
實現對視訊流的載入,這裡支援fetch
的 stream能力,WebSocket
等,將得到的視訊流,這裡指的是flv格式的視訊流,將其轉封裝成MP4格式,最後將MP4格式的資料通過appendBuffer將資料餵給MSE,實現播放。
未來
上面談到的都是視訊的播放,你也看到,即使播放都存在很多限制,MSE的瀏覽器支援還不多,那在視訊的編碼解碼這些要求效能很高的領域,前端能否做一些事情呢? 前端效能不高有很多原因,在瀏覽器這樣的沙盒環境下,同時js這種動態語言,效能不高,所以有大佬提出把c++編譯成js ,然後提高效能,或許你已經知道我要說的是什麼了,它就是ASM.js,它是js的一種嚴格子集。我萌可以考慮將一些視訊編碼庫編譯成js去執行提高效能,其中就不得不提到的FFmpeg,可以考慮到將其編譯成asm,然後對視訊進行編解碼。
寫在最後
我萌可以看到,前端對音視訊的處理上由於諸多原因,可謂如履薄冰,但是在視訊播放上,隨著瀏覽器的支援,還是可以有所作為的。
招納賢士
今日頭條長期大量招聘前端工程師,可選北京、深圳、上海、廈門等城市。歡迎投遞簡歷到 tcscyl@gmail.com / yanglei.yl@bytedance.com