<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          看前端如何通過(guò)WebAssembly實(shí)現(xiàn)播放器預(yù)覽能力

          共 4142字,需瀏覽 9分鐘

           ·

          2021-01-15 09:26

          最近,團(tuán)隊(duì)小組內(nèi)部體驗(yàn)Web瀏覽器上課的音視頻播放功能,除了對(duì)比同行產(chǎn)品,也對(duì)比了主流視頻內(nèi)容的網(wǎng)站平臺(tái)。計(jì)劃補(bǔ)齊和增強(qiáng)與播放體驗(yàn)相關(guān)的能力。

          其中有一項(xiàng)能力在主流媒體視頻網(wǎng)站都支持的,那就是進(jìn)度條幀預(yù)覽:在鼠標(biāo)進(jìn)度條停留,不必跳轉(zhuǎn)進(jìn)度,即可展示所指畫(huà)面。

          在簡(jiǎn)單分析了B站、騰訊視頻后,發(fā)現(xiàn)都是采取在上架視頻時(shí),由后臺(tái)生成專門(mén)用來(lái)幀預(yù)覽的組合sprite圖,然后前端拉取后再計(jì)算進(jìn)度進(jìn)行展示。

          由于目前的我們后臺(tái)云點(diǎn)播錄制沒(méi)有生成幀預(yù)覽圖功能。另一方面,即便升級(jí)可能大量的存量存儲(chǔ)視頻無(wú)法幀預(yù)覽。于是我們決定嘗試前端實(shí)現(xiàn)動(dòng)態(tài)幀預(yù)覽的方案。

          先上效果圖

          瀏覽器獲取視頻畫(huà)面的方法:

          目前瀏覽器視頻幀提取的方案主要有:

          1. canvas + video方案:主要video在播通過(guò)canvas的drawImage提取視頻幀。但注意瀏覽器一般只能解析MP4/WebM的格式, H264/VP8編解碼的視頻。如果不是指定格式,要先解復(fù)用在利用MSE來(lái)實(shí)現(xiàn)。
          2. webassembly + ffmpeg方案:webassembly的出現(xiàn)為前端解碼視頻數(shù)據(jù)提供了可能,將ffmpeg編譯為wasm庫(kù),通過(guò)js調(diào)用并提取視頻幀數(shù)據(jù),再給到canvas繪制。

          第一種方案對(duì)于單個(gè)MP4文件還是合適的,但hls資源不是完整加載,并且瀏覽器不能直接復(fù)用ts格式,所以行不通。

          HLS動(dòng)態(tài)解密ts分片wasm ffmpeg獲取幀畫(huà)面的技術(shù)方案

          整體技術(shù)方案:?

          ①通過(guò)解析HLS masterPlayList 和 levelPlayList,拿到低清晰度的ts文件索引數(shù)組。

          ②支持區(qū)分判斷HLS加密,獲取解密秘鑰,AES 解密ts文件數(shù)據(jù)。

          ③ts文件arraybuffer數(shù)據(jù),申請(qǐng)內(nèi)存并寫(xiě)入wasm,調(diào)用wasm封裝截圖方法,返回RGB數(shù)據(jù)。

          ④將RGB數(shù)據(jù)轉(zhuǎn)為canvas imagedata,更新展示幀畫(huà)面,并緩存。監(jiān)聽(tīng)鼠標(biāo)事件定位幀緩存畫(huà)面,或加載新數(shù)據(jù)。

          FFmpeg編譯至WebAssembly

          前置準(zhǔn)備

          安裝emscripten的emsdk,實(shí)際上會(huì)遇到不少困難。按照emscripten官網(wǎng)的指示一步一步,遇到阻礙及時(shí)谷歌變更解決。安裝完成后執(zhí)行emcc -v 能查看版本,代表安裝成功。

          #?Get?the?emsdk?repo
          git?clone?https://github.com/emscripten-core/emsdk.git

          #?Enter?that?directory
          cd?emsdk

          #?Download?and?install?the?latest?SDK?tools.
          ./emsdk?install?latest

          #?Make?the?"latest"?SDK?"active"?for?the?current?user.?(writes?.emscripten?file)
          ./emsdk?activate?latest

          #?Activate?PATH?and?other?environment?variables?in?the?current?terminal
          source?./emsdk_env.sh

          FFmpeg編譯

          FFmpeg是個(gè)優(yōu)秀的音視頻處理庫(kù),包含了采集、格式轉(zhuǎn)化、編解碼、截圖、濾鏡等能力。我們需要禁用掉大部分能力,只編譯我們需要的部分,最后編譯產(chǎn)物是c依賴庫(kù)和相關(guān)頭文件。

          emconfigure?./configure?\
          ????--prefix=$WEB_CAPTURE_PATH/lib/ffmpeg-emcc?\
          ????--cc="emcc"?\
          ????--cxx="em++"?\
          ????--ar="emar"?\
          ????--cpu=generic?\
          ????--target-os=none?\
          ????--arch=x86_32?\
          ????--enable-gpl?\
          ????--enable-version3?\
          ????--enable-cross-compile?\
          ????--disable-ffmpeg?\
          ????--disable-ffplay?\
          ????--disable-ffprobe?\
          ????--disable-doc?\
          ????--disable-ffserver?\
          ????--disable-swresample?\
          ????--disable-postproc??\
          ????--disable-programs?\
          ????--disable-avfilter?\
          ????--disable-pthreads?\
          ????--disable-w32threads?\
          ????--disable-os2threads?\
          ????--disable-network?\
          ????--disable-logging?\
          ????--disable-everything?\
          ????--enable-protocol=file?\
          ????--enable-demuxer=mpegts?\
          ????--enable-decoder=h264?\
          ????--disable-asm?\
          ????--disable-debug?\

          分析ffmpeg提取幀流程

          視頻文件數(shù)據(jù)到幀的圖像數(shù)據(jù),按照流程:解格式封裝、視頻解碼,圖像數(shù)據(jù)轉(zhuǎn)換(YUV=>RGB)。則按照HLS分片提取圖像數(shù)據(jù)流程,需要涉及到以下ffmpeg中的庫(kù)。

          1. libavcodec:提供編解碼功能。這里我只是需要H264的視頻編解碼。
          2. libavformat:多路解復(fù)用(demux)和多路復(fù)用(mux)。這里我3需要解復(fù)用ts文件的格式、即mpegts。
          3. libswscale:圖像伸縮和像素格式轉(zhuǎn)化。
          4. libavutil:工具函數(shù)。

          編譯至Wasm

          最后需要通過(guò) emcc 來(lái)將demuxer和decoder和依賴的相關(guān)庫(kù)編譯為 wasm 然后提供瀏覽器使用javascript進(jìn)行調(diào)用。編譯選項(xiàng)如下:

          emcc?./getframe.c?./ffmpeg/lib/libavformat.a?./ffmpeg/lib/libavcodec.a?./ffmpeg/lib/libswscale.a?./ffmpeg/lib/libavutil.a?\
          ????-O3?\
          ????-I?"./ffmpeg/include"?\
          ????-s?WASM=1?\
          ????-s?TOTAL_MEMORY=33554432?\
          ????-s?EXPORTED_FUNCTIONS='["_main",?"_free",?"_getFrame",?"_setFile"]'?\
          ????-s?ASSERTIONS=1?\
          ????-s?ALLOW_MEMORY_GROWTH=1?\
          ????-s?MAXIMUM_MEMORY=4GB?\
          ????-o?getframe.js

          emcc編譯選項(xiàng),請(qǐng)參考: https://emscripten.org/docs/tools_reference/emcc.html?

          最后編譯wasm成功是一個(gè)wasm的二進(jìn)制文件,和一個(gè)膠水代碼js文件。

          Blockquote EXPORTED_FUNCTIONS: 參數(shù)告訴編譯器,代碼里面需要輸出的函數(shù)名。函數(shù)名前面要加下劃線.

          ASSERTIONS: ASSERTIONS=1 用于為內(nèi)存分配錯(cuò)誤啟用運(yùn)行時(shí)檢查(例如,寫(xiě)入比分配更多的內(nèi)存)。它還定義了Emscripten如何處理程序流中的錯(cuò)誤。可以將值設(shè)置為ASSERTIONS=2,以便運(yùn)行額外的測(cè)試。

          ALLOW_MEMORY_GROWTH: Emscripten堆一經(jīng)初始化,容量就固定了,無(wú)法再擴(kuò)容。而某些程序在運(yùn)行時(shí)需要的內(nèi)存容量在不同工況下可能有很大的波動(dòng)。為了滿足某些極端工況的需求而將TOTAL_MEMORY設(shè)置得非常高無(wú)疑是非常浪費(fèi)的,為此,Emscripten提供了可在運(yùn)行時(shí)擴(kuò)大內(nèi)存容量的模式,欲開(kāi)啟該模式,需要在編譯時(shí)增加-s ALLOW_MEMORY_GROWTH=1參數(shù)。

          封裝API

          這里參考了網(wǎng)上一些現(xiàn)成的做法,雖然可以生成ffmpeg.js和ffpmeg.wasm,并提供Module對(duì)象來(lái)操控,但是這樣JS的數(shù)據(jù)類型和C的數(shù)據(jù)類型差異比較多,頻繁地調(diào)C的API,讓數(shù)據(jù)傳來(lái)傳去比較麻煩。這里參考網(wǎng)上的教程、前置封裝ffmpeg的API,具體參考這里的實(shí)現(xiàn)和教程:https://github.com/liyincheng/ffmpeg-wasm-video-to-picture http://dranger.com/ffmpeg/tutorial01.html。ffempg可調(diào)用函數(shù):http://dranger.com/ffmpeg/functions.html 。

          1. 注冊(cè)所有可用的文件格式和編解碼器,后續(xù)打開(kāi)具有相應(yīng)格式/編解碼器的文件時(shí)就可使用,請(qǐng)注意,我們?cè)趍ain()中只需要調(diào)用一次av_register_all()即可。
          #include?
          #include?
          #include?
          #include?
          int?main(int?argc,?char?const?*argv[])?{
          ????av_register_all();
          ????return?0;
          }
          1. 打開(kāi)文件、檢索流信息、找視頻流的解碼器、復(fù)制上下文并打開(kāi)編解碼器。
          AVFormatContext?*pFormatCtx?=?NULL;
          //?Open?video?file
          if(avformat_open_input(&pFormatCtx,?argv[1],?NULL,?0,?NULL)!=0)
          return?-1;?//?Couldn't?open?file
          ??
          //?Retrieve?stream?information
          if(avformat_find_stream_info(pFormatCtx,?NULL)<0)
          ??return?-1;?//?Couldn'
          t?find?stream?information
          AVCodec?*pCodec?=?NULL;

          //?Find?the?decoder?for?the?video?stream
          pCodec=avcodec_find_decoder(pCodecCtx->codec_id);
          if(pCodec==NULL)?{
          ??fprintf(stderr,?"Unsupported?codec!\n");
          ??return?-1;?//?Codec?not?found
          }
          //?Copy?context
          pCodecCtx?=?avcodec_alloc_context3(pCodec);
          if(avcodec_copy_context(pCodecCtx,?pCodecCtxOrig)?!=?0)?{
          ??fprintf(stderr,?"Couldn't?copy?codec?context");
          ??return?-1;?//?Error?copying?codec?context
          }
          //?Open?codec
          if(avcodec_open2(pCodecCtx,?pCodec)<0)
          ??return?-1;?//?Could?not?open?codec

          其中主要步驟在于,讀取整個(gè)數(shù)據(jù)流,方法是讀取數(shù)據(jù)包,將其解碼為幀,一旦幀完成,我們將對(duì)其進(jìn)行轉(zhuǎn)換RGB(PIX_FMT_RGB24)并保存。

          struct?SwsContext?*sws_ctx?=?NULL;
          int?frameFinished;
          AVPacket?packet;
          //?initialize?SWS?context?for?software?scaling
          sws_ctx?=?sws_getContext(pCodecCtx->width,
          ????pCodecCtx->height,
          ????pCodecCtx->pix_fmt,
          ????pCodecCtx->width,
          ????pCodecCtx->height,
          ????PIX_FMT_RGB24,
          ????SWS_BILINEAR,
          ????NULL,
          ????NULL,
          ????NULL
          ????);

          i=0;
          while(av_read_frame(pFormatCtx,?&packet)>=0)?{
          ??//?Is?this?a?packet?from?the?video?stream?
          ??if(packet.stream_index==videoStream)?{
          ?//?Decode?video?frame
          ????avcodec_decode_video2(pCodecCtx,?pFrame,?&frameFinished,?&packet);
          ????
          ????//?Did?we?get?a?video?frame?
          ????if(frameFinished)?{
          ????//?Convert?the?image?from?its?native?format?to?RGB
          ????????sws_scale(sws_ctx,?(uint8_t?const?*?const?*)pFrame->data,
          ????pFrame->linesize,?0,?pCodecCtx->height,
          ????pFrameRGB->data,?pFrameRGB->linesize);
          ?
          ????????//?Save?the?frame?to?disk
          ????????if(++i<=5)
          ??????????SaveFrame(pFrameRGB,?pCodecCtx->width,?
          ????????????????????pCodecCtx->height,?i);
          ????}
          ??}
          ????
          ??//?Free?the?packet?that?was?allocated?by?av_read_frame
          ??av_free_packet(&packet);
          }

          javascript 調(diào)用 wasm

          https://www.cntofu.com/book/150/zh/ch2-c-js/ch2-01-js-call-c.md?

          javascript調(diào)用wasm,簡(jiǎn)單概括是就是內(nèi)存的寫(xiě)入與讀取的過(guò)程。理論上HLS文件拿到ts分片文件,將文件保存Unit8Array,并寫(xiě)入到wasm中。

          1. xhr 請(qǐng)求 tsFile 保存為 Uint8Array
          let?tsBuffer?=?new?Uint8Array(tsFileArrayBuffer);
          1. 申請(qǐng)內(nèi)存空間
          let?tsBufferPtr?=?Module._malloc(tsBuffer.length);
          1. 將buffer 寫(xiě)入 wasm 內(nèi)存
          Module.HEAP8.set(tsBuffer,?tsBufferPtr);
          1. 執(zhí)行封裝 _getFrame 函數(shù),傳入內(nèi)存指針,內(nèi)存大小,時(shí)間點(diǎn)。
          let?imgData?=?Module._getFrame(tsBufferPtr,?tsBuffer.length,?time)

          去的RGB數(shù)據(jù)在js層轉(zhuǎn)化canvas可用數(shù)據(jù)。

          ?const?canvas?=?document.createElement('canvas');
          ?const?ctx?=?canvas.getContext('2d');

          canvas.width?=?width;
          canvas.height?=?height;

          let?imageData?=?ctx.createImageData(width,?height);

          let?j?=?0;
          for?(let?i?=?0;?i?????if?(i?&&?i?%?3?==?0)?{
          ????????imageData.data[j]?=?255;
          ????????j?+=?1;
          ????}
          ????imageData.data[j]?=?imageBuffer[i];
          ????j?+=?1;
          }
          ctx.putImageData(imageData,?0,?0,?0,?0,?width,?height);
          const?finalData?=?canvas.toDataURL('image/jpeg');

          HLS動(dòng)態(tài)加載ts分片及解密

          HLS masterPlayList/ levelPalyList解析


          HLS點(diǎn)播資源并非單文件,而是一個(gè)m3u8協(xié)議的索引。要取拿到幀數(shù)據(jù),必須要加載ts分片文件數(shù)據(jù)。必須先HLS解析m3u8文件。由于我們?nèi)瑘D片拿來(lái)做預(yù)覽,并不需要很大的尺寸和清晰度。當(dāng)包含多個(gè)level(清晰度)的情況下,優(yōu)先選取最低清晰度的levelPlayList。


          MSE HLS解析:一般MSE HLS使用hls.js加載視頻播放,通過(guò)其創(chuàng)建實(shí)例(client),在onManifestParsed事件后通過(guò)client.levels可以讀取到到不同level的參數(shù)。


          Native HLS解析:對(duì)于移動(dòng)端瀏覽器,或者safari等瀏覽器,使用native播放m3u8的模式。我們可以自己解析m3u8的masterPlayList,然后自行解析。比如通過(guò)BANDWIDTH和RESOLUTION,取出最低清晰度,或者可以借助m3u8-parser進(jìn)行解析。

          #EXTM3U
          #EXT-X-VERSION:3
          #EXT-X-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=2099325,RESOLUTION=1920x1080
          v.f124099.m3u8
          #EXT-X-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=197642,RESOLUTION=1280x720
          v.f22239.m3u8
          #EXT-X-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=142162,RESOLUTION=960x540
          v.f22240.m3u8
          #EXT-X-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=95767,RESOLUTION=480x270
          v.f22241.m3u8

          獲取playList的segments映射時(shí)間段

          我們需要知道定位時(shí)間距離最近的segment,直接維護(hù)最低清晰度playlist數(shù)組,根據(jù)#EXTINF得知每個(gè)segment的duration時(shí)長(zhǎng),計(jì)算出總時(shí)長(zhǎng),及每個(gè)segment的開(kāi)始時(shí)間和結(jié)束時(shí)間。當(dāng)我們定位到指定時(shí)間,即可匹配到最近的ts文件作為被解析的數(shù)據(jù)。


          ......
          #EXTINF:10.000000,
          v.f22241.ts?start=260400&end=382047&type=mpegts
          #EXT-X-KEY:METHOD=AES-128,URI="http://getkeyurl",IV=0x00000000000000000000000000000000
          #EXTINF:10.000000,
          v.f22241.ts?start=382048&end=502943&type=mpegts
          #EXT-X-KEY:METHOD=AES-128,URI="http://getkeyurl",IV=0x00000000000000000000000000000000
          #EXTINF:10.000000,
          v.f22241.ts?start=502944&end=623455&type=mpegts
          #EXT-X-KEY:METHOD=AES-128,URI="http://getkeyurl",IV=0x00000000000000000000000000000000
          #EXTINF:10.000000,
          v.f22241.ts?start=623456&end=748303&type=mpegts
          .....

          AES解密ts文件

          獲取解密Key由于點(diǎn)播HLS資源已經(jīng)進(jìn)行了加密,ts文件數(shù)據(jù)無(wú)法直接給到wasm截取幀畫(huà)面。所以要對(duì)ts進(jìn)行解密。當(dāng)解析playlist時(shí)候匹配到#EXT-X-KEY:METHOD=AES-128則需要解密。KEY需要從URI屬性的地址請(qǐng)求獲取,一般具備登錄態(tài)的請(qǐng)求正確返回。IV數(shù)據(jù)直接取playlist上的IV即可。

          同樣的,在MSE HLS播放的,hls.js實(shí)例上能讀取到KEY和IV;對(duì)于native hls播放的,需要自己二次請(qǐng)求獲取。

          WebCrypto ASE解密參考hls.js源碼,將請(qǐng)求到的ts分片進(jìn)行解密。https://github.com/videojs/aes-decrypter

          ?let?decrypter?=?new?Decrypter();
          ?const?{?key,?iv?}?=?levelKey;
          ?decrypter.decrypt(data,?key,?iv,?(data)?=>?{
          ?????const?finalSegmentFile?=?new?Blob([data],?{
          ?????????type:?'video/mp2t',
          ?????});
          ?????//?給到wasm寫(xiě)入finalSegmentFile
          ?});

          點(diǎn)播進(jìn)度幀預(yù)覽邏輯及緩存策略

          動(dòng)態(tài)節(jié)流加載,并緩存至對(duì)應(yīng)時(shí)間區(qū)間:由于用戶的鼠標(biāo)在進(jìn)度條可能頻繁移動(dòng),這里設(shè)計(jì)應(yīng)該監(jiān)聽(tīng)mousemove但節(jié)流觸發(fā)。從解析playlist開(kāi)始,到ts文件加載與解密,wasm解碼獲取幀數(shù)據(jù)拿到imagedata,設(shè)置500ms觸發(fā)閾值,獲取幀圖像數(shù)據(jù)緩存到對(duì)應(yīng)時(shí)間區(qū)間。

          就近讀取緩存幀畫(huà)面:一般來(lái)說(shuō),相鄰進(jìn)度的幀畫(huà)面往往是相似,但加載到解幀的整個(gè)過(guò)程異步且存在一定耗時(shí),優(yōu)先展示相鄰分片區(qū)間的緩存幀圖像數(shù)據(jù),可以讓用戶第一時(shí)間感知,提升體驗(yàn)效果。

          問(wèn)題與小結(jié)

          用wasm做前端播放幀預(yù)覽的能力,已經(jīng)在業(yè)務(wù)側(cè)灰度上線。由于我們只需要解復(fù)用mpegts和h624decoder,編譯wasm大小2.6MB左右。主要受限于加載分片的網(wǎng)絡(luò)耗時(shí),從hover進(jìn)度條到預(yù)覽圖展示約在1.1秒左右,wasm解幀耗時(shí)60ms以內(nèi)。在支持wasm的PC瀏覽器上chrome、新版firefox和safari也都沒(méi)什么太大問(wèn)題。

          目前一個(gè)完整600M左右的高清回放資源,如果加載完整的資源用于幀預(yù)覽的消耗30-50MB流量,但實(shí)際情況下并不會(huì)完整的加載,一般都只在10M以內(nèi)。雖然這部分資源被http緩存,但是如果不是因?yàn)榫W(wǎng)絡(luò)差而走到低清晰度的情況下,這部分資源的流量多少還是有點(diǎn)被浪費(fèi)了。

          這幾年wasm從一些demo嘗試,到業(yè)務(wù)真正落地,越來(lái)越多場(chǎng)景中被得到應(yīng)用。以前一些客戶端app才有的功能,現(xiàn)在瀏覽器也并非不可想象。期待可以落地更多有趣和實(shí)用的功能。

          愛(ài)心三連擊

          1.看到這里了就點(diǎn)個(gè)在看支持下吧,你的在看是我創(chuàng)作的動(dòng)力。

          2.關(guān)注公眾號(hào)腦洞前端,獲取更多前端硬核文章!加個(gè)星標(biāo),不錯(cuò)過(guò)每一條成長(zhǎng)的機(jī)會(huì)。

          3.如果你覺(jué)得本文的內(nèi)容對(duì)你有幫助,就幫我轉(zhuǎn)發(fā)一下吧。





          瀏覽 53
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  大香蕉美女视频 | 欧美成人猛片AAAAAAA | 大鸡巴操小穴视频 | 久草在在线 | 大鸡八网站 |