天天看點

Webcodecs音視訊編解碼與封裝技術探索

作者:閃念基因

1.背景

在web端處理音視訊是一個複雜而又重要的課題,市場上主流的視訊編輯通常采用服務端進行渲染導出,因為專用的伺服器對音視訊的編解碼能力更強,是以服務端渲染導出的速度很不錯;

少數編輯器在浏覽器本地對視訊進行處理,一方面對伺服器成本非常友好,另一方面可以不需要注冊等流程,在小型視訊的渲染上使用者體驗更好。但是浏覽器本地渲染對使用者裝置有一定要求,對浏覽器的相容性等等也有要求。

而經典的在浏覽器本地處理視訊的方案是通過ffmpeg.wasm,近些年Webcodecs API的出現與普及逐漸改變了這一現象。

ffmpeg.wasm的底層webassembly對ffmpeg多線程處理視訊的相容很差,GPU調用效果也不盡人如意,導緻渲染視訊的速度非常不理想,并且還要額外下載下傳編解碼器,整體使用體驗存在很多不适。

而WebCodecs API可以利用浏覽器自帶的FFmpeg,而且可以充分利用GPU,是以其執行效率是遠高于webassembly的。

Webcodecs音視訊編解碼與封裝技術探索

(該圖取自Bilibili團隊實驗[1])

1.1功能對比

特性/功能ffmpeg.wasmWebCodecs目标在浏覽器中實作音視訊處理提供底層音視訊操作API技術基礎基于WebAssembly技術基于現代Web浏覽器API跨平台相容性跨平台相容性較好跨平台相容性一般應用場景實時視訊預覽、截圖、教育與教程、社交媒體、資料分析實時通信、視訊編輯、自适應流媒體、機器學習操作層級提供簡單的JavaScript API,友善內建到Web應用更深入地控制媒體流,實作高效、低延遲的實時通信或視訊編輯前端內建難度提供了簡單的JavaScript API,內建相對容易需要一定的音視訊處理知識,內建難度可能稍高

2.WebCodecs 介紹

如果要問WebCodecs是什麼,可以簡單的概括為JavaScript賦予了通過浏覽器底層對視訊流的單個幀和音頻資料塊的底層通路能力的一項web技術。

簡單地說,就是設定一個解碼器,将視訊編碼位元組塊處理為視訊幀/音頻資料,或者反之,設定一個編碼器,将視訊幀/音頻資料處理回編碼位元組塊。

上文所說的WebCodecs API的解碼器有:

名稱介紹AudioDecoder解碼 EncodedAudioChunk 對象VideoDecoder解碼 EncodedVideoChunk 對象

上表中EncodedAudioChunk和EncodedVideoChunk就是上文提到的編碼位元組塊。

上文所說的WebCodecs API的編碼器有:

名稱介紹AudioEncoder編碼 AudioData 對象VideoEncoder編碼 VideoFrame 對象

上表中AudioData和VideoFrame就是上文提到的視訊幀/音頻資料。

請注意,WebCodecs API并不提供對某一視訊類型具體的編解碼器,解碼視訊時,你需要自行将這個視訊轉為EncodedVideoChunk和EncodedAudioChunk,再交由WebCodecs API進行處理。渲染合成視訊同理。

常見的方案有Mp4Box.js。

3.WebCodecs 支援情況

WebCodecs在Chrome 94上得到支援,下面是一個可供參考的浏覽器支援表。

浏覽器支援情況釋出時間Chrome94+2021-09-21Edge94+2021-09-24Firefox不支援

Opera80+2021-10-05Safari16.4+2023-03-27360 浏覽器14+2022-11QQ 浏覽器12+大約 2023-09-05 之後2345 浏覽器12+未查到Chrome Android94+2021-09-21Firefox for Android不支援

Opera Android66+2021-12-15Safari on iOS16.4+2023-03-27

可以看到,不少浏覽器的在23年才提供支援。

可用如下代碼進行判定浏覽器是否支援:

if('VideoEncoder' in window){ 
    console.log("webcodecs is supported.")
 }
           

4.視訊播放原理

衆所周知,視訊由畫面和音頻構成。而畫面由一幀一幀的圖像組成,音頻由一段一段的聲波構成。按照某個頻率不斷地同步切換幀和聲波,就可以實作視訊的播放。

但是,視訊并不會完整的将每一幀以圖檔的形式進行儲存,而是通過一些複雜的結構,将視訊的畫面進行壓縮,并将時長等中繼資料整合到一起,形成一個完整地視訊檔案。

下面介紹下視訊檔案的結構。

5.視訊結構

HTML5提供了HTMLMediaElement,可以直接使用HTML标簽播放視訊音頻,而對于m3u8或Flash時代留存的大量Flv視訊,也有例如FLV.js等相應的庫,使其可以被HTMLMediaElement播放。

這些高度封裝的庫也使得我們對視訊檔案的結構比較陌生,這裡以最常見的MP4格式簡單介紹一下。

5.1視訊的編碼

視訊編碼是将原始視訊資料轉換為壓縮格式的過程,以減小檔案大小并提高傳輸效率。

編碼的目的是為了壓縮,不同的編碼格式則對應不同的壓縮算法。

MP4檔案常用的編碼格式有H.264(即AVC)、H.265(HEVC)、VP8、VP9等。

H.265在市場上有很高的占有量,但因為其高昂的授權費用,免費的AV1編碼正逐漸被市場接納。

5.2視訊的封裝

視訊編碼後,将其和檔案的中繼資料封裝到容器格式中,以建立完整的視訊檔案。

壓縮後的原始資料,需要有中繼資料的配合才能被解析播放;

常見的中繼資料包括:時間資訊,編碼格式,分辨率,作者,标題等等。

5.3動态補償與幀間壓縮

對視訊進行二次壓縮,無需掌握具體算法。

動态補償指的是,連續的兩幀之間有相同的部分,隻是位置發生了變化,是以第二幀可以隻儲存偏移量

幀間壓縮是對兩幀之間進行diff,第二幀隻儲存diff運算出的不同的那一部分

5.4幀的類型

根據上面的過程,幀之間互相可能并不獨立,于是産生了三種幀類型

I幀:也就是關鍵幀,保留完整的畫面資訊,沒有被二次壓縮,可以被獨立還原為圖像

P幀:依賴前一幀的解碼結果才能還原為圖像

B幀:依賴前一幀與後一幀的解碼結果才能還原為圖像,但占用空間一般最少

6.Demo

前面介紹了非常多的Webcodecs和視訊相關的概念,我們來做一個小的demo,利用MP4Box.js作為編解碼器,嘗試解析一個視訊。

先放一個Demo 位址:https://codesandbox.io/p/devbox/nifty-dawn-gghryr?embed=1&file=%2Findex.js%3A111%2C1

(代碼基于張鑫旭blob修改[2])

6.1解析部分

我們先建立一個Mp4box執行個體:

const mp4box = MP4Box.createFile();
           

Webcodecs基于Stream的思想,是以我們需要用Stream去提供資料。比較簡單的方法是用fetch去請求:

fetch(mp4url)
  .then((res) => res.arrayBuffer())
  .then((buffer) => {
    state.innerHTML = "開始解碼視訊";
    buffer.fileStart = 0;
    mp4box.appendBuffer(buffer);
    mp4box.flush();
  });

           

請注意,mp4box.appendBuffer接受ArrayBuffer類型的資料。

加載小型視訊時,可以直接用上面的代碼。但若是視訊較大,上面的代碼效率就不太夠看。可以用reader.read().then(({ done, value })替代,但是要注意,這樣擷取的data是Unit8Array類型,需要手動轉為ArrayBuffer,并且要修改buffer.fileStart為這一段data的起點。

然後,我們對mp4box進行監聽,當檔案開始解碼會首先觸發onMoovStart(Demo中未用到),這裡的Moov可能不好了解,他指的是"Movie Box",也被稱為 "moov atom",包含了視訊檔案的關鍵資訊,如視訊和音頻的媒體資料、時長、軌道資訊等。

當moov解析完成,會觸發onReady,onReady會将視訊的詳細資訊也就是moov傳給回調函數的第一個參數。詳細的資料結構可以參考Mp4Box.js官方文檔:位址

我們姑且叫這個資訊為info,這裡面我們在意的參數是軌道info.videoTracks。他是一個數組,包含了這個軌道的采樣率、編碼方法等等資訊,一般長度是2,第0個是視訊軌道,第1個是音頻軌道。(不過例如專業電影等更複雜的視訊可能會有更多軌道,這裡不做考慮)

我們将軌道拉出來,扔到下面的萃取環節中。

6.2萃取部分

這是一個非常形象的名稱,在官方文檔裡叫做Extraction,它用來提取軌道并進行采樣。

我們在onReady過程中,設定了Extraction的參數:

mp4box.setExtractionOptions(videoTrack.id, "video", {
      nbSamples: 100,
    });
           

第二個參數指的是user,指的是此軌道的分段調用方,将會被傳到後面介紹的onSamples中,可以是任意字元串,表示唯一辨別

第三個參數中nbSamples表示每次回調調用的樣本數。如果收到的資料不足以提取樣本數量,則保留迄今為止收到的樣本。如果未提供,則預設值為 1000。越大擷取的幀數越多。

當一組樣本準備就緒時,将根據 setExtractionOptions 中傳遞的選項,啟動onSamples的回調函數。

mp4box.onSamples = function (trackId, ref, samples) {
//......
}
           

onSamples會給回掉傳入三個參數:trackId, ref, samples分别代表軌道id,user,上一步采樣的樣品數組。

通過周遊這個數組,将樣品編碼成EncodedVideoChunk資料:

for (const sample of samples) {
      const type = sample.is_sync ? "key" : "delta";

      const chunk = new EncodedVideoChunk({
        type,
        timestamp: sample.cts,
        duration: sample.duration,
        data: sample.data,
      });

      videoDecoder.decode(chunk);
    }
           

其中sample.is_sync為true,則為關鍵字。然後将EncodedVideoChunk送入videoDecoder.decode進行解碼,進而擷取幀資料。

videoDecoder是在onReady中建立的:

videoDecoder = new VideoDecoder({
    output: (videoFrame) => {
      createImageBitmap(videoFrame).then((img) => {
        videoFrames.push({
          img,
          duration: videoFrame.duration,
          timestamp: videoFrame.timestamp,
        });
        state.innerHTML = "已擷取幀數:" + videoFrames.length;
        videoFrame.close();
      });
    },
    error: (err) => {
      console.error("videoDecoder錯誤:", err);
    },
  });
           

其本質還是使用Webcodecs API的VideoDecoder,在接受onSamples送來的資料後,解碼為videoFrame資料。此時的操作可根據業務來,Demo中将他送到createImageBitmap轉為位圖,然後推入videoFrames中。

在Demo的控制台中列印videoFrames,即可直接看到幀的數組。

Webcodecs音視訊編解碼與封裝技術探索

image.png

我們可以在頁面上再建立一個canvas,然後ctx.drawImage(videoFrames[0].img,0,0)即可将任意一幀繪制到畫面上。(Demo裡沒有加canvas,大家可以在控制台自己加)

Webcodecs音視訊編解碼與封裝技術探索

image.png

6.3音頻

音頻的操作與視訊類似,onReady中的info也有audioTracks屬性,從裡面取出來并配置Extraction:

if (audioTrack) {
    mp4box.setExtractionOptions(audioTrack.id, 'audio', {
            nbSamples: 100000
        })
    }
           

配置音頻解碼器AudioDecoder:

audioDecoder = new AudioDecoder({
    output: (audioFrame) => {
        console.log('audioFrame:', audioFrame);
    },
    error: (err) => {
        console.error('audioDecoder錯誤:', err);
    }
})
const config = {
    codec: audioTrack.codec,
    sampleRate: audioTrack.audio.sample_rate,
    numberOfChannels: audioTrack.audio.channel_count,
}

audioDecoder.configure(config);
           

其他操作不再贅述,可以在Demo的AudioTest.html中檢視。

可以發現,最後擷取到的資料與上文,視訊幀的結構非常類似,是AudioData資料結構,可以将他轉換為Float32Array,就可以進行對音頻的各種操作了。

視訊的機關是幀,音頻的機關可以說是波,任意一個波可以用若幹個三角函數sin、cos之和表示。

根據高中實體,波可以相加。我們可以将兩個Float32Array每一項相加,實作兩個聲波的混流:

function mixAudioBuffers(buffer1, buffer2) {  
    if (buffer1.length !== buffer2.length) {  
        return
    } 
    const mixedBuffer = new Float32Array(buffer1.length);  
   
    for (let i = 0; i < buffer1.length; i++) {  
        const mixedSample = buffer1[i] + buffer2[i];  
        mixedBuffer[i] = Math.min(1, Math.max(-1, mixedSample));  
    }  
    return mixedBuffer;  
}  
           

上述代碼進行了歸一化,避免求和的值超過1,而這裡的波的振幅範圍是[-1,1]。更常見的做法是提前對波進行縮放。

此外,我們可以通過改變波的振幅來修改音量,隻需要把Float32Array的每一項*2即可放大兩倍音量:

function increaseVolume(audioBuffer, volumeFactor) {   
    const adjustedBuffer = new Float32Array(audioBuffer.length);    
    for (let i = 0; i < audioBuffer.length; i++) {  
        const adjustedSample = audioBuffer[i] * volumeFactor;  
        adjustedBuffer[i] = Math.min(1, Math.max(-1, adjustedSample));  
    } 
    return adjustedBuffer;  
}  
           

可以參考:AudioData 文檔,摸索更多有意思的操作。

7.可能的應用場景

  1. 在上傳視訊的場景截取視訊封面
  2. 輕量級視訊剪輯
  3. 封裝不易被爬蟲的視訊播放器

8.部分參考資料

  1. https://www.bilibili.com/read/cv30358687/
  2. https://www.zhangxinxu.com/study/202311/js-mp4-parse-effect-pixi-demo.php
  3. https://developer.mozilla.org/en-US/docs/Web/API/AudioData
  4. https://zhuanlan.zhihu.com/p/648657440

作者:zhouzijian

來源-微信公衆号:大轉轉FE

出處:https://mp.weixin.qq.com/s/RiUz-l3Hzn0Xrq4vrEWgNQ

繼續閱讀