IVWEB 玩轉WASM 系列-WEBGL YUV渲染圖像實踐

blank

IVWEB 玩轉WASM 系列-WEBGL YUV渲染圖像實踐

最近ivweb團隊在用WASM + FFmpeg 打造一個WEB 播放器。我們是通過寫C 語言用FFmpeg 解碼視頻,通過編譯C 語言轉WASM 運行在瀏覽器上與JavaScript 進行通信。默認FFmpeg 去解碼出來的數據是yuv,而canvas 只支持渲染rgb,那麼此時我們有兩種方法處理這個yuv,第一個使用FFmpeg 暴露的方法將yuv 直接轉成rgb 然後給canvas 進行渲染,第二個使用webgl 將yuv 轉rgb ,在canvas 上渲染。第一個好處是寫法很簡單,只需FFmpeg 暴露的方法將yuv 直接轉成rgb ,缺點呢就是會耗費一定的cpu,第二個好處是會利用gpu 進行加速,缺點是寫法比較繁瑣,而且需要熟悉WEBGL 。考慮到為了減少cpu 的佔用,利用gpu 進行並行加速,我們採用了第二種方法。
在講YUV 之前,我們先來看下YUV 是怎麼獲取到的:

blank

由於我們是寫播放器,實現一個播放器的步驟必定會經過以下這幾個步驟:

  1. 將視頻的文件比如mp4,avi,flv等等,mp4,avi,flv 相當於是一個容器,裡麵包含一些訊息,比如壓縮的視頻,壓縮的音頻等等, 進行解復用,從容器裡面提取出壓縮的視頻以及音頻,壓縮的視頻一般是H265、H264 格式或者其他格式,壓縮的音頻一般是aac或者mp3。
  2. 分別在壓縮的視頻和壓縮的音頻進行解碼,得到原始的視頻和音頻,原始的音頻數據一般是pcm ,而原始的視頻數據一般是yuv 或者rgb。
  3. 然後進行音視頻的同步。
    可以看到解碼壓縮的視頻數據之後,一般就會得到yuv。

YUV

YUV 是什麼

對於前端開發者來說,YUV 其實有點陌生,對於搞過音視頻開發的一般會接觸到這個,簡單來說,YUV 和我們熟悉的RGB 差不多,都是顏色編碼方式,只不過它們的三個字母代表的意義與RGB 不同,YUV 的“Y” 表示明亮度(Luminance或Luma),也就是灰度值;而”U” 和”V” 表示的則是色度(Chrominance或Chroma),描述影像色彩及飽和度,用於指定像素的顏色。

為了讓大家對YUV 有更加直觀的感受,我們來看下,Y,U,V 單獨顯示分別是什麼樣子,這裡使用了FFmpeg 命令將一張火影忍者的宇智波鼬圖片轉成YUV420P:

ffmpeg -i frame.jpg -s 352x288 -pix_fmt yuv420p test.yuv

GLYUVPlay軟件上打開test.yuv ,顯示原圖:

blank
原圖

Y分量單獨顯示:

blank
Y

U分量單獨顯示:

blank
U

V 分量單獨顯示:

blank
V

由上面可以發現,Y 單獨顯示的時候是可以顯示完整的圖像的,只不過圖片是灰色的。而U,V則代表的是色度,一個偏藍,一個偏紅。

使用YUV 的好處

  1. 由剛才看到的那樣,Y 單獨顯示是黑白圖像,因此YUV格式由彩色轉黑白很簡單,可以兼容老式黑白電視,這一特性用在於電視信號上。
  2. YUV的數據尺寸一般都比RGB格式小,可以節約傳輸的帶寬。 (但如果用YUV444的話,和RGB24一樣都是24位)

YUV 採樣

常見的YUV的採樣有YUV444,YUV422,YUV420:

blank

注:黑點表示採樣該像素點的Y分量,空心圓圈表示採用該像素點的UV分量。

  1. YUV 4:4:4採樣,每一個Y對應一組UV分量。
  2. YUV 4:2:2採樣,每兩個Y共用一組UV分量。
  3. YUV 4:2:0採樣,每四個Y共用一組UV分量。

YUV 存儲方式

YUV的存儲格式有兩類:packed(打包)和planar(平面):

  • packed 的YUV格式,每個像素點的Y,U,V是連續交錯存儲的。
  • planar 的YUV格式,先連續存儲所有像素點的Y,緊接著存儲所有像素點的U,隨後是所有像素點的V。

舉個例子,對於planar 模式,YUV 可以這麼存YYYYUUVV,對於packed 模式,YUV 可以這麼存YUYVYUYV。

YUV 格式一般有多種,YUV420SP、YUV420P、YUV422P,YUV422SP等,我們來看下比較常見的格式:

  • YUV420P(每四個Y 會共用一組UV 分量):
blank
  • YUV420SP(packed,每四個Y 會共用一組UV 分量,和YUV420P不同的是,YUV420SP存儲的時候U,V 是交錯存儲):
blank
  • YUV422P(planar,每兩個Y 共用一組UV 分量,所以U和V 會比YUV420P U 和V 各多加一行):
blank
  • YUV422SP(packed,每兩個Y 共用一組UV 分量):
blank

其中YUV420P和YUV420SP根據U、V的順序,又可分出2種格式:

  • YUV420P :U前V後即YUV420P ,也叫I420 ,V前U後,叫YV12
  • YUV420SP :U前V後叫NV12 ,V前U後叫NV21

數據排列如下:

I420:YYYYYYYYUUVV=>YUV420PYV12:YYYYYYYYVVUU=>YUV420PNV12:YYYYYYYYUVUV=>YUV420SPNV21:YYYYYYYYVUVU=>YUV420SP

至於為啥會有這麼多格式,經過大量搜索發現原因是為了適配不同的電視廣播制式和設備系統,比如ios下只有這一種模式NV12 ,安卓的模式是NV21 ,比如YUV411YUV420格式多見於數碼攝像機數據中,前者用於NTSC制,後者用於PAL制。至於電視廣播制式的介紹我們可以看下這篇文章:

YUV 計算方法

以YUV420P存儲一張1080 x 1280圖片為例子,其存儲大小為((1080 x 1280 x 3) >> 1)個字節,這個是怎麼算出來的?我們來看下面這張圖:

blank

以Y420P存儲那麼Y佔的大小為W x H = 1080x1280 ,U為(W/2) * (H/2)= (W*H)/4 = (1080x1280)/4 ,同理V為

(W*H)/4 = (1080x1280)/4 ,因此一張圖為Y+U+V = (1080x1280)*3/2

由於三個部分內部均是行優先存儲,三個部分之間是Y,U,V 順序存儲,那麼YUV的存儲位置如下(PS:後面會用到):

Y01080*1280U1080*1280(1080*1280)*5/4V(1080*1280)*5/4(1080*1280)*3/2

WEBGL

WEBGL 是什麼

簡單來說,WebGL是一項用來在網頁上繪製和渲染複雜3D圖形,並允許用戶與之交互的技術。

WEBGL 組成

在webgl 世界中,能繪製的基本圖形元素只有點、線、三角形,每個圖像都是由大大小小的三角形組成,如下圖,無論是多麼複雜的圖形,其基本組成部分都是由三角形組成。

blank
圖來源於網絡

著色器

著色器是在GPU上運行的程序,是用OpenGL ES著色語言編寫的,有點類似c 語言:

blank

具體的語法可以參考著色器語言GLSL (opengl-shader-language)入門大全,這裡不在多加贅述。

在WEBGL 中想要繪製圖形就必須要有兩個著色器:

  • 頂點著色器
  • 片元著色器

其中頂點著色器的主要功能就是用來處理頂點的,而片元著色器則是用來處理由光柵化階段生成的每個片元(PS:片元可以理解為像素),最後計算出每個像素的顏色。

WEBGL 繪製流程

一、提供頂點坐標<br>因為程序很傻,不知道圖形的各個頂點,需要我們自己去提供,頂點坐標可以是自己手動寫或者是由軟件導出:

blank

在這個圖中,我們把頂點寫入到緩衝區裡,緩衝區對像是WebGL系統中的一塊內存區域,我們可以一次性地向緩衝區對像中填充大量的頂點數據,然後將這些數據保存在其中,供頂點著色器使用。接著我們創建並編譯頂點著色器和片元著色器,並用program 連接兩個著色器,並使用。舉個例子簡單理解下為什麼要這樣做,我們可以理解成創建Fragment元素: let f = document.createDocumentFragment()

所有的著色器創建並編譯後會處在一種游離的狀態,我們需要將他們聯繫起來,並使用(可以理解成document.body.appendChild(f) ,添加到body,dom元素才能被看到,也就是聯繫並使用)。

接著我們還需要將緩衝區與頂點著色器進行連接,這樣才能生效。

二、圖元裝配<br>我們提供頂點之後,GPU根據我們提供的頂點數量,會挨個執行頂點著色器程序,生成頂點最終的坐標,將圖形裝配起來。可以理解成製作風箏,就需要將風箏骨架先搭建起來,圖元裝配就是在這一階段。

三、光柵化<br>這一階段就好比是製作風箏,搭建好風箏骨架後,但是此時卻不能飛起來,因為裡面都是空的,需要為骨架添加布料。而光柵化就是在這一階段,將圖元裝配好的幾何圖形轉成片元(PS: 片元可以理解成像素)。

blank

四、著色與渲染

blank

著色這一階段就好比風箏布料搭建完成,但是此時並沒有什麼圖案,需要繪製圖案,讓風箏更加好看,也就是光柵化後的圖形此時並沒有顏色,需要經過片元著色器處理,逐片元進行上色並寫到顏色緩衝區裡,最後在瀏覽器才能顯示有圖像的幾何圖形。

總結
WEBGL 繪製流程可以歸納為以下幾點:

  1. 提供頂點坐標(需要我們提供)
  2. 圖元裝配(按圖元類型組裝成圖形)
  3. 光柵化(將圖元裝配好的圖形,生成像素點)
  4. 提供顏色值(可以動態計算,像素著色)
  5. 通過canvas 繪製在瀏覽器上。
blank

WEBGL YUV 繪製圖像思路

由於每個視頻幀的圖像都不太一樣,我們肯定不可能知道那麼多頂點,那麼我們怎麼將視頻幀的圖像用webgl 畫出來呢?這裡使用了一個技巧—紋理映射。簡單來說就是將一張圖像貼在一個幾何圖形表面,使幾何圖形看起來像是有圖像的幾何圖形,也就是將紋理坐標和webgl 系統坐標進行一一對應:

blank

如上圖,上面那個是紋理坐標,分為s 和t 坐標(或者叫uv 坐標),值的範圍在【0,1】之間,值和圖像大小、分辨率無關。下面那張圖是webgl坐標系統,是一個三維的坐標系統,這裡聲明了四個頂點,用兩個三角形組裝成一個長方形,然後將紋理坐標的頂點與webgl 坐標系進行一一對應,最終傳給片元著色器,片元著色器提取圖片的一個個紋素顏色,輸出在顏色緩衝區裡,最終繪製在瀏覽器裡(PS:紋素你可以理解為組成紋理圖像的像素)。但是如果按圖上進行一一對應的話,成像會是反的,因為canvas 的圖像坐標,默認(0,0)是在左上角:

blank

而紋理坐標則是在左下角,所以繪製時成像就會倒立,解決方法有兩種:

  • 對紋理圖像進行Y 軸翻轉,webgl 提供了api:
// 1代表对纹理图像进行y轴反转
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL,1);
  • 紋理坐標和webgl坐標映射進行倒轉,舉個栗子,如上圖所示,本來的紋理坐標(0.0,1.0)對應的是webgl坐標(-1.0,1.0,0.0)(0.0,0.0)對應的是(-1.0,-1.0,0.0) ,那麼我們倒轉過來, (0.0,1.0)對應的是(-1.0,-1.0,0.0) ,而(0.0,0.0)對應的是(-1.0,1.0,0.0) ,這樣在瀏覽器成像就不會是反的。

詳細步驟

  • 著色器部分
//頂點著色器vertexShaderattributelowpvec4a_vertexPosition;//通過js傳遞頂點坐標attributevec2a_texturePosition;//通過js傳遞紋理坐標varyingvec2v_texCoord;//傳遞紋理坐標給片元著色器voidmain(){gl_Position=a_vertexPosition;//設置頂點坐標v_texCoord=a_texturePosition;//設置紋理坐標}//片元著色器fragmentShaderprecisionlowpfloat;// lowp代表計算精度,考慮節約性能使用了最低精度uniformsampler2DsamplerY;// sampler2D是取樣器類型,圖片紋理最終存儲在該類型對像中uniformsampler2DsamplerU;// sampler2D是取樣器類型,圖片紋理最終存儲在該類型對像中uniformsampler2DsamplerV;// sampler2D是取樣器類型,圖片紋理最終存儲在該類型對像中varyingvec2v_texCoord;//接受頂點著色器傳來的紋理坐標voidmain(){floatr,g,b,y,u,v,fYmul;y=texture2D(samplerY,v_texCoord).r;u=texture2D(samplerU,v_texCoord).r;v=texture2D(samplerV,v_texCoord).r;// YUV420P轉RGBfYmul=y*1.1643828125;r=fYmul+1.59602734375*v-0.870787598;g=fYmul-0.39176171875*u-0.81296875*v+0.52959375;b=fYmul+2.01723046875*u-1.081389160375;gl_FragColor=vec4(r,g,b,1.0);}
  • 創建並編譯著色器,將頂點著色器和片段著色器連接到program,並使用:
letvertexShader=this._compileShader(vertexShaderSource,gl.VERTEX_SHADER);// 创建并编译顶点着色器
letfragmentShader=this._compileShader(fragmentShaderSource,gl.FRAGMENT_SHADER);// 创建并编译片元着色器
letprogram=this._createProgram(vertexShader,fragmentShader);// 创建program并连接着色器
  • 創建緩衝區,存頂點和紋理坐標(PS:緩衝區對像是WebGL系統中的一塊內存區域,我們可以一次性地向緩衝區對像中填充大量的頂點數據,然後將這些數據保存在其中,供頂點著色器使用)。
letvertexBuffer=gl.createBuffer();letvertexRectangle=newFloat32Array([1.0,1.0,0.0,-1.0,1.0,0.0,1.0,-1.0,0.0,-1.0,-1.0,0.0]);gl.bindBuffer(gl.ARRAY_BUFFER,vertexBuffer);//向緩衝區寫入數據gl.bufferData(gl.ARRAY_BUFFER,vertexRectangle,gl.STATIC_DRAW);//找到頂點的位置letvertexPositionAttribute=gl.getAttribLocation(program,'a_vertexPosition');//告訴顯卡從當前綁定的緩衝區中讀取頂點數據gl.vertexAttribPointer(vertexPositionAttribute,3,gl.FLOAT,false,0,0);//連接vertexPosition變量與分配給它的緩衝區對象gl.enableVertexAttribArray(vertexPositionAttribute);//聲明紋理坐標lettextureRectangle=newFloat32Array([1.0,0.0,0.0,0.0,1.0,1.0,0.0,1.0]);lettextureBuffer=gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER,textureBuffer);gl.bufferData(gl.ARRAY_BUFFER,textureRectangle,gl.STATIC_DRAW);lettextureCoord=gl.getAttribLocation(program,'a_texturePosition');gl.vertexAttribPointer(textureCoord,2,gl.FLOAT,false,0,0);gl.enableVertexAttribArray(textureCoord);
  • 初始化並激活紋理單元(YUV)
//激活指定的紋理單元gl.activeTexture(gl.TEXTURE0);gl.y=this._createTexture();//創建紋理gl.uniform1i(gl.getUniformLocation(program,'samplerY'),0);//獲取samplerY變量的存儲位置,指定紋理單元編號0將紋理對像傳遞給samplerYgl.activeTexture(gl.TEXTURE1);gl.u=this._createTexture();gl.uniform1i(gl.getUniformLocation(program,'samplerU'),1);//獲取samplerU變量的存儲位置,指定紋理單元編號1將紋理對像傳遞給samplerUgl.activeTexture(gl.TEXTURE2);gl.v=this._createTexture();gl.uniform1i(gl.getUniformLocation(program,'samplerV'),2);//獲取samplerV變量的存儲位置,指定紋理單元編號2將紋理對像傳遞給samplerV
  • 渲染繪製(PS:由於我們獲取到的數據是YUV420P,那麼計算方法可以參考剛才說的計算方式)。
//設置清空顏色緩衝時的顏色值gl.clearColor(0,0,0,0);//清空緩衝gl.clear(gl.COLOR_BUFFER_BIT);letuOffset=width*height;letvOffset=(width>>1)*(height>>1);gl.bindTexture(gl.TEXTURE_2D,gl.y);//填充Y紋理,Y的寬度和高度就是width,和height,存儲的位置就是data.subarray(0, width * height)gl.texImage2D(gl.TEXTURE_2D,0,gl.LUMINANCE,width,height,0,gl.LUMINANCE,gl.UNSIGNED_BYTE,data.subarray(0,uOffset));gl.bindTexture(gl.TEXTURE_2D,gl.u);//填充U紋理,Y的寬度和高度就是width/2和height/2,存儲的位置就是data.subarray(width * height, width/2 * height/2 + width * height)gl.texImage2D(gl.TEXTURE_2D,0,gl.LUMINANCE,width>>1,height>>1,0,gl.LUMINANCE,gl.UNSIGNED_BYTE,data.subarray(uOffset,uOffset+vOffset));gl.bindTexture(gl.TEXTURE_2D,gl.v);//填充U紋理,Y的寬度和高度就是width/2和height/2,存儲的位置就是data.subarray(width/2 * height/2 + width * height, data.length)gl.texImage2D(gl.TEXTURE_2D,0,gl.LUMINANCE,width>>1,height>>1,0,gl.LUMINANCE,gl.UNSIGNED_BYTE,data.subarray(uOffset+vOffset,data.length));gl.drawArrays(gl.TRIANGLE_STRIP,0,4);//繪製四個點,也就是長方形

上述那些步驟最終可以繪製成這張圖:

blank

完整代碼:

exportdefaultclassWebglScreen{constructor(canvas){this.canvas=canvas;this.gl=canvas.getContext('webgl')||canvas.getContext('experimental-webgl');this._init();}_init(){letgl=this.gl;if(!gl){console.log('gl not support! ');return;}//圖像預處理gl.pixelStorei(gl.UNPACK_ALIGNMENT,1);// GLSL格式的頂點著色器代碼letvertexShaderSource=`attribute lowp vec4 a_vertexPosition;attribute vec2 a_texturePosition;varying vec2 v_texCoord;void main() {gl_Position = a_vertexPosition;v_texCoord = a_texturePosition;}`;letfragmentShaderSource=`precision lowp float;uniform sampler2D samplerY;uniform sampler2D samplerU;uniform sampler2D samplerV;varying vec2 v_texCoord;void main() {float r,g,b,y,u,v,fYmul;y = texture2D(samplerY, v_texCoord).r;u = texture2D(samplerU, v_texCoord).r;v = texture2D(samplerV, v_texCoord).r;fYmul = y * 1.1643828125;r = fYmul + 1.59602734375 * v - 0.870787598;g = fYmul - 0.39176171875 * u - 0.81296875 * v + 0.52959375;b = fYmul + 2.01723046875 * u - 1.081389160375;gl_FragColor = vec4(r, g, b, 1.0);}`;letvertexShader=this._compileShader(vertexShaderSource,gl.VERTEX_SHADER);letfragmentShader=this._compileShader(fragmentShaderSource,gl.FRAGMENT_SHADER);letprogram=this._createProgram(vertexShader,fragmentShader);this._initVertexBuffers(program);//激活指定的紋理單元gl.activeTexture(gl.TEXTURE0);gl.y=this._createTexture();gl.uniform1i(gl.getUniformLocation(program,'samplerY'),0);gl.activeTexture(gl.TEXTURE1);gl.u=this._createTexture();gl.uniform1i(gl.getUniformLocation(program,'samplerU'),1);gl.activeTexture(gl.TEXTURE2);gl.v=this._createTexture();gl.uniform1i(gl.getUniformLocation(program,'samplerV'),2);}/***初始化頂點buffer* @param {glProgram} program程序*/_initVertexBuffers(program){letgl=this.gl;letvertexBuffer=gl.createBuffer();letvertexRectangle=newFloat32Array([1.0,1.0,0.0,-1.0,1.0,0.0,1.0,-1.0,0.0,-1.0,-1.0,0.0]);gl.bindBuffer(gl.ARRAY_BUFFER,vertexBuffer);//向緩衝區寫入數據gl.bufferData(gl.ARRAY_BUFFER,vertexRectangle,gl.STATIC_DRAW);//找到頂點的位置letvertexPositionAttribute=gl.getAttribLocation(program,'a_vertexPosition');//告訴顯卡從當前綁定的緩衝區中讀取頂點數據gl.vertexAttribPointer(vertexPositionAttribute,3,gl.FLOAT,false,0,0);//連接vertexPosition變量與分配給它的緩衝區對象gl.enableVertexAttribArray(vertexPositionAttribute);lettextureRectangle=newFloat32Array([1.0,0.0,0.0,0.0,1.0,1.0,0.0,1.0]);lettextureBuffer=gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER,textureBuffer);gl.bufferData(gl.ARRAY_BUFFER,textureRectangle,gl.STATIC_DRAW);lettextureCoord=gl.getAttribLocation(program,'a_texturePosition');gl.vertexAttribPointer(textureCoord,2,gl.FLOAT,false,0,0);gl.enableVertexAttribArray(textureCoord);}/***創建並編譯一個著色器* @param {string} shaderSource GLSL格式的著色器代碼* @param {number} shaderType著色器類型, VERTEX_SHADER或FRAGMENT_SHADER。* @return {glShader}著色器。*/_compileShader(shaderSource,shaderType){//創建著色器程序letshader=this.gl.createShader(shaderType);//設置著色器的源碼this.gl.shaderSource(shader,shaderSource);//編譯著色器this.gl.compileShader(shader);constsuccess=this.gl.getShaderParameter(shader,this.gl.COMPILE_STATUS);if(!success){leterr=this.gl.getShaderInfoLog(shader);this.gl.deleteShader(shader);console.error('could not compile shader',err);return;}returnshader;}/***從2個著色器中創建一個程序* @param {glShader} vertexShader頂點著色器。* @param {glShader} fragmentShader片斷著色器。* @return {glProgram}程序*/_createProgram(vertexShader,fragmentShader){constgl=this.gl;letprogram=gl.createProgram();//附上著色器gl.attachShader(program,vertexShader);gl.attachShader(program,fragmentShader);gl.linkProgram(program);//將WebGLProgram對象添加到當前的渲染狀態中gl.useProgram(program);constsuccess=this.gl.getProgramParameter(program,this.gl.LINK_STATUS);if(!success){console.err('program fail to link'+this.gl.getShaderInfoLog(program));return;}returnprogram;}/***設置紋理*/_createTexture(filter=this.gl.LINEAR){letgl=this.gl;lett=gl.createTexture();//將給定的glTexture綁定到目標(綁定點gl.bindTexture(gl.TEXTURE_2D,t);//紋理包裝參考https://github.com/fem-d/webGL/blob/master/blog/WebGL基礎學習篇(Lesson%207).md -> Texture wrappinggl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_S,gl.CLAMP_TO_EDGE);gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_T,gl.CLAMP_TO_EDGE);//設置紋理過濾方式gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,filter);gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_FILTER,filter);returnt;}/***渲染圖片出來* @param {number} width寬度* @param {number} height高度*/renderImg(width,height,data){letgl=this.gl;//設置視口,即指定從標准設備到窗口坐標的x、y仿射變換gl.viewport(0,0,gl.canvas.width,gl.canvas.height);//設置清空顏色緩衝時的顏色值gl.clearColor(0,0,0,0);//清空緩衝gl.clear(gl.COLOR_BUFFER_BIT);letuOffset=width*height;letvOffset=(width>>1)*(height>>1);gl.bindTexture(gl.TEXTURE_2D,gl.y);//填充紋理gl.texImage2D(gl.TEXTURE_2D,0,gl.LUMINANCE,width,height,0,gl.LUMINANCE,gl.UNSIGNED_BYTE,data.subarray(0,uOffset));gl.bindTexture(gl.TEXTURE_2D,gl.u);gl.texImage2D(gl.TEXTURE_2D,0,gl.LUMINANCE,width>>1,height>>1,0,gl.LUMINANCE,gl.UNSIGNED_BYTE,data.subarray(uOffset,uOffset+vOffset));gl.bindTexture(gl.TEXTURE_2D,gl.v);gl.texImage2D(gl.TEXTURE_2D,0,gl.LUMINANCE,width>>1,height>>1,0,gl.LUMINANCE,gl.UNSIGNED_BYTE,data.subarray(uOffset+vOffset,data.length));gl.drawArrays(gl.TRIANGLE_STRIP,0,4);}/***根據重新設置canvas大小* @param {number} width寬度* @param {number} height高度* @param {number} maxWidth最大寬度*/setSize(width,height,maxWidth){letcanvasWidth=Math.min(maxWidth,width);this.canvas.width=canvasWidth;this.canvas.height=canvasWidth*height/width;}destroy(){const{gl}=this;gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT|gl.STENCIL_BUFFER_BIT);}}

最後我們來看下效果圖:

blank

遇到的問題

在實際開發過程中,我們測試一些直播流,有時候渲染的時候圖像顯示是正常的,但是顏色會偏綠,經研究發現,直播流的不同主播的視頻寬度是會不一樣,比如在主播在pk 的時候寬度368,熱門主播寬度會到720,小主播寬度是540,而寬度為540 的會顯示偏綠,具體原因是webgl 會經過預處理,默認會將以下值設置為4:

// 图像预处理
gl.pixelStorei(gl.UNPACK_ALIGNMENT,4);

這樣默認設置會每行4個字節4個字節處理,而Y分量每行的寬度是540,是4的倍數,字節對齊了,所以圖像能夠正常顯示,而U,V分量寬度是540 / 2 = 270 ,270不是4的倍數,字節非對齊,因此色素就會顯示偏綠。目前有兩種方法可以解決這個問題:

  • 第一個是直接讓webgl 每行1 個字節1 個字節處理(對性能有影響):
// 图像预处理
gl.pixelStorei(gl.UNPACK_ALIGNMENT,1);
  • 第二個是讓獲取到的圖像的寬度是8 的倍數,這樣就能做到YUV 字節對齊,就不會顯示綠屏,但是不建議這樣做, 轉的時候CPU佔用極大,建議採取第一個方案。

參考文章

What do you think?

Written by marketer

blank

圖表製作可以很簡單- 圖表魔方ChartCube

blank

微前端的核心價值