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

最近團隊在用 WASM + FFmpeg 打造一個 WEB 播放器。我們是通過寫 C 語言用 FFmpeg 解碼視頻,通過編譯 C 語言轉 WASM 運行在瀏覽器上與 JavaScript 進行通信。默認 FFmpeg 去解碼出來的數(shù)據(jù)是 yuv,而 canvas 只支持渲染 rgb当纱,那么此時我們有兩種方法處理這個yuv,第一個使用 FFmpeg 暴露的方法將 yuv 直接轉成 rgb 然后給 canvas 進行渲染踩窖,第二個使用 webgl 將 yuv 轉 rgb 坡氯,在 canvas 上渲染。第一個好處是寫法很簡單洋腮,只需 FFmpeg 暴露的方法將 yuv 直接轉成 rgb 箫柳,缺點呢就是會耗費一定的cpu,第二個好處是會利用 gpu 進行加速徐矩,缺點是寫法比較繁瑣滞时,而且需要熟悉 WEBGL 叁幢÷说疲考慮到為了減少 cpu 的占用,利用 gpu 進行并行加速曼玩,我們采用了第二種方法鳞骤。

在講 YUV 之前,我們先來看下 YUV 是怎么獲取到的:


實現(xiàn)播放器必定要經(jīng)過的步驟

由于我們是寫播放器黍判,實現(xiàn)一個播放器的步驟必定會經(jīng)過以下這幾個步驟:

  1. 將視頻的文件比如 mp4豫尽,avi,flv等等顷帖,mp4美旧,avi,flv 相當于是一個容器贬墩,里面包含一些信息榴嗅,比如壓縮的視頻,壓縮的音頻等等陶舞, 進行解復用嗽测,從容器里面提取出壓縮的視頻以及音頻,壓縮的視頻一般是 H265肿孵、H264 格式或者其他格式唠粥,壓縮的音頻一般是 aac或者 mp3疏魏。
  2. 分別在壓縮的視頻和壓縮的音頻進行解碼,得到原始的視頻和音頻晤愧,原始的音頻數(shù)據(jù)一般是pcm 大莫,而原始的視頻數(shù)據(jù)一般是 yuv 或者 rgb。
  3. 然后進行音視頻的同步官份。
    可以看到解碼壓縮的視頻數(shù)據(jù)之后葵硕,一般就會得到 yuv。

YUV

YUV 是什么

對于前端開發(fā)者來說贯吓,YUV 其實有點陌生懈凹,對于搞過音視頻開發(fā)的一般會接觸到這個,簡單來說悄谐,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,顯示原圖:

原圖

Y分量單獨顯示:
Y

U分量單獨顯示:
U

V 分量單獨顯示:
V

由上面可以發(fā)現(xiàn)凌摄,Y 單獨顯示的時候是可以顯示完整的圖像的羡蛾,只不過圖片是灰色的。而U锨亏,V則代表的是色度痴怨,一個偏藍,一個偏紅器予。

使用YUV 的好處

  1. 由剛才看到的那樣浪藻,Y 單獨顯示是黑白圖像,因此YUV格式由彩色轉黑白很簡單劣摇,可以兼容老式黑白電視珠移,這一特性用在于電視信號上。
  2. YUV的數(shù)據(jù)尺寸一般都比RGB格式小,可以節(jié)約傳輸?shù)膸捑濉#ǖ绻肶UV444的話暇韧,和RGB24一樣都是24位)

YUV 采樣

常見的YUV的采樣有YUV444,YUV422浓瞪,YUV420:


注:黑點表示采樣該像素點的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是連續(xù)交錯存儲的罚勾。
  • planar 的YUV格式,先連續(xù)存儲所有像素點的Y吭狡,緊接著存儲所有像素點的U尖殃,隨后是所有像素點的V。

舉個例子划煮,對于 planar 模式送丰,YUV 可以這么存 YYYYUUVV,對于 packed 模式弛秋,YUV 可以這么存YUYVYUYV器躏。

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

  • YUV420P(每四個 Y 會共用一組 UV 分量):


  • YUV420SP(packed,每四個 Y 會共用一組 UV 分量贼急,和YUV420P不同的是茅茂,YUV420SP存儲的時候 U,V 是交錯存儲):


  • YUV422P(planar太抓,每兩個 Y 共用一組 UV 分量空闲,所以 U和 V 會比 YUV420P U 和 V 各多加一行):


  • YUV422SP(packed,每兩個 Y 共用一組 UV 分量):


其中YUV420P和YUV420SP根據(jù)U走敌、V的順序碴倾,又可分出2種格式:

  • YUV420P:U前V后即YUV420P,也叫I420,V前U后跌榔,叫YV12异雁。

  • YUV420SP:U前V后叫NV12,V前U后叫NV21僧须。

數(shù)據(jù)排列如下:

I420: YYYYYYYY UU VV =>YUV420P

YV12: YYYYYYYY VV UU =>YUV420P

NV12: YYYYYYYY UV UV =>YUV420SP

NV21: YYYYYYYY VU VU =>YUV420SP

至于為啥會有這么多格式纲刀,經(jīng)過大量搜索發(fā)現(xiàn)原因是為了適配不同的電視廣播制式和設備系統(tǒng),比如 ios 下只有這一種模式NV12担平,安卓的模式是 NV21示绊,比如 YUV411YUV420格式多見于數(shù)碼攝像機數(shù)據(jù)中暂论,前者用于NTSC制面褐,后者用于 PAL制。至于電視廣播制式的介紹我們可以看下這篇文章【標準】NTSC取胎、PAL盆耽、SECAM三大制式簡介

YUV 計算方法

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


以 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
由于三個部分內(nèi)部均是行優(yōu)先存儲秧饮,三個部分之間是Y,U,V 順序存儲映挂,那么YUV的存儲位置如下(PS:后面會用到):

Y:0 到 1080*1280
U:1080*1280 到 (1080*1280)*5/4
V:(1080*1280)*5/4 到 (1080*1280)*3/2

WEBGL

WEBGL 是什么

簡單來說,WebGL是一項用來在網(wǎng)頁上繪制和渲染復雜3D圖形盗尸,并允許用戶與之交互的技術柑船。

WEBGL 組成

在 webgl 世界中,能繪制的基本圖形元素只有點泼各、線鞍时、三角形,每個圖像都是由大大小小的三角形組成扣蜻,如下圖逆巍,無論是多么復雜的圖形,其基本組成部分都是由三角形組成莽使。

圖來源于網(wǎng)絡

著色器

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


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

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

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

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

WEBGL 繪制流程

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


在這個圖中,我們把頂點寫入到緩沖區(qū)里揍异,緩沖區(qū)對象是WebGL系統(tǒng)中的一塊內(nèi)存區(qū)域全陨,我們可以一次性地向緩沖區(qū)對象中填充大量的頂點數(shù)據(jù),然后將這些數(shù)據(jù)保存在其中衷掷,供頂點著色器使用辱姨。接著我們創(chuàng)建并編譯頂點著色器和片元著色器,并用 program 連接兩個著色器戚嗅,并使用雨涛。舉個例子簡單理解下為什么要這樣做,我們可以理解成創(chuàng)建Fragment 元素: let f = document.createDocumentFragment()懦胞,
所有的著色器創(chuàng)建并編譯后會處在一種游離的狀態(tài)替久,我們需要將他們聯(lián)系起來,并使用(可以理解成 document.body.appendChild(f)躏尉,添加到 body蚯根,dom 元素才能被看到,也就是聯(lián)系并使用)胀糜。
接著我們還需要將緩沖區(qū)與頂點著色器進行連接颅拦,這樣才能生效。

二教藻、圖元裝配
我們提供頂點之后距帅,GPU根據(jù)我們提供的頂點數(shù)量,會挨個執(zhí)行頂點著色器程序括堤,生成頂點最終的坐標碌秸,將圖形裝配起來∪簦可以理解成制作風箏哮肚,就需要將風箏骨架先搭建起來,圖元裝配就是在這一階段广匙。

三、光柵化
這一階段就好比是制作風箏恼策,搭建好風箏骨架后鸦致,但是此時卻不能飛起來潮剪,因為里面都是空的,需要為骨架添加布料分唾。而光柵化就是在這一階段抗碰,將圖元裝配好的幾何圖形轉成片元(PS: 片元可以理解成像素)。

四绽乔、著色與渲染


著色這一階段就好比風箏布料搭建完成榜旦,但是此時并沒有什么圖案店读,需要繪制圖案,讓風箏更加好看,也就是光柵化后的圖形此時并沒有顏色室埋,需要經(jīng)過片元著色器處理,逐片元進行上色并寫到顏色緩沖區(qū)里锈锤,最后在瀏覽器才能顯示有圖像的幾何圖形撵孤。

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

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

WEBGL YUV 繪制圖像思路

由于每個視頻幀的圖像都不太一樣,我們肯定不可能知道那么多頂點删顶,那么我們怎么將視頻幀的圖像用 webgl 畫出來呢竖螃?這里使用了一個技巧—紋理映射。簡單來說就是將一張圖像貼在一個幾何圖形表面逗余,使幾何圖形看起來像是有圖像的幾何圖形特咆,也就是將紋理坐標和 webgl 系統(tǒng)坐標進行一一對應:


如上圖,上面那個是紋理坐標猎荠,分為 s 和 t 坐標(或者叫 uv 坐標)坚弱,值的范圍在【0,1】之間关摇,值和圖像大小荒叶、分辨率無關。下面那張圖是webgl坐標系統(tǒng)输虱,是一個三維的坐標系統(tǒng)些楣,這里聲明了四個頂點,用兩個三角形組裝成一個長方形宪睹,然后將紋理坐標的頂點與 webgl 坐標系進行一一對應愁茁,最終傳給片元著色器,片元著色器提取圖片的一個個紋素顏色亭病,輸出在顏色緩沖區(qū)里鹅很,最終繪制在瀏覽器里(PS:紋素你可以理解為組成紋理圖像的像素)。但是如果按圖上進行一一對應的話罪帖,成像會是反的促煮,因為 canvas 的圖像坐標邮屁,默認(0,0)是在左上角:


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

  • 對紋理圖像進行 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)帮掉,這樣在瀏覽器成像就不會是反的。

詳細步驟

  • 著色器部分
// 頂點著色器vertexShader
attribute lowp vec4 a_vertexPosition; // 通過 js 傳遞頂點坐標
attribute vec2 a_texturePosition; // 通過 js 傳遞紋理坐標
varying vec2 v_texCoord; // 傳遞紋理坐標給片元著色器
void main(){
    gl_Position=a_vertexPosition;// 設置頂點坐標
    v_texCoord=a_texturePosition;// 設置紋理坐標
}


// 片元著色器fragmentShader
precision lowp float;// lowp代表計算精度窒典,考慮節(jié)約性能使用了最低精度
uniform sampler2D samplerY;// sampler2D是取樣器類型蟆炊,圖片紋理最終存儲在該類型對象中
uniform sampler2D samplerU;// sampler2D是取樣器類型,圖片紋理最終存儲在該類型對象中
uniform sampler2D samplerV;// sampler2D是取樣器類型瀑志,圖片紋理最終存儲在該類型對象中
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;
    
    // YUV420P 轉 RGB    
  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);
}
  • 創(chuàng)建并編譯著色器涩搓,將頂點著色器和片段著色器連接到 program,并使用:
let vertexShader=this._compileShader(vertexShaderSource,gl.VERTEX_SHADER);// 創(chuàng)建并編譯頂點著色器
let fragmentShader=this._compileShader(fragmentShaderSource,gl.FRAGMENT_SHADER);// 創(chuàng)建并編譯片元著色器

let program=this._createProgram(vertexShader,fragmentShader);// 創(chuàng)建program并連接著色器

  • 創(chuàng)建緩沖區(qū)劈猪,存頂點和紋理坐標(PS:緩沖區(qū)對象是WebGL系統(tǒng)中的一塊內(nèi)存區(qū)域昧甘,我們可以一次性地向緩沖區(qū)對象中填充大量的頂點數(shù)據(jù),然后將這些數(shù)據(jù)保存在其中战得,供頂點著色器使用)充边。
let vertexBuffer = gl.createBuffer();
let vertexRectangle = new Float32Array([
    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);
// 向緩沖區(qū)寫入數(shù)據(jù)
gl.bufferData(gl.ARRAY_BUFFER, vertexRectangle, gl.STATIC_DRAW);
// 找到頂點的位置
let vertexPositionAttribute = gl.getAttribLocation(program, 'a_vertexPosition');
// 告訴顯卡從當前綁定的緩沖區(qū)中讀取頂點數(shù)據(jù)
gl.vertexAttribPointer(vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0);
// 連接vertexPosition 變量與分配給它的緩沖區(qū)對象
gl.enableVertexAttribArray(vertexPositionAttribute);

// 聲明紋理坐標
let textureRectangle = new Float32Array([1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0]);
let textureBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, textureBuffer);
gl.bufferData(gl.ARRAY_BUFFER, textureRectangle, gl.STATIC_DRAW);
let textureCoord = 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(); // 創(chuàng)建紋理
gl.uniform1i(gl.getUniformLocation(program,'samplerY'),0);//獲取samplerY變量的存儲位置,指定紋理單元編號0將紋理對象傳遞給samplerY

gl.activeTexture(gl.TEXTURE1);
gl.u=this._createTexture();
gl.uniform1i(gl.getUniformLocation(program,'samplerU'),1);//獲取samplerU變量的存儲位置常侦,指定紋理單元編號1將紋理對象傳遞給samplerU

gl.activeTexture(gl.TEXTURE2);
gl.v=this._createTexture();
gl.uniform1i(gl.getUniformLocation(program,'samplerV'),2);//獲取samplerV變量的存儲位置痛黎,指定紋理單元編號2將紋理對象傳遞給samplerV
  • 渲染繪制(PS:由于我們獲取到的數(shù)據(jù)是YUV420P予弧,那么計算方法可以參考剛才說的計算方式)刮吧。
 // 設置清空顏色緩沖時的顏色值
 gl.clearColor(0, 0, 0, 0);
 // 清空緩沖
 gl.clear(gl.COLOR_BUFFER_BIT);

let uOffset = width * height;
let vOffset = (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); // 繪制四個點,也就是長方形

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


完整代碼:

export default class WebglScreen {
    constructor(canvas) {
        this.canvas = canvas;
        this.gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
        this._init();
    }

    _init() {
        let gl = this.gl;
        if (!gl) {
            console.log('gl not support致讥!');
            return;
        }
        // 圖像預處理
        gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
        // GLSL 格式的頂點著色器代碼
        let vertexShaderSource = `
            attribute lowp vec4 a_vertexPosition;
            attribute vec2 a_texturePosition;
            varying vec2 v_texCoord;
            void main() {
                gl_Position = a_vertexPosition;
                v_texCoord = a_texturePosition;
            }
        `;

        let fragmentShaderSource = `
            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);
            }
        `;

        let vertexShader = this._compileShader(vertexShaderSource, gl.VERTEX_SHADER);
        let fragmentShader = this._compileShader(fragmentShaderSource, gl.FRAGMENT_SHADER);

        let program = 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) {
        let gl = this.gl;
        let vertexBuffer = gl.createBuffer();
        let vertexRectangle = new Float32Array([
            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);
        // 向緩沖區(qū)寫入數(shù)據(jù)
        gl.bufferData(gl.ARRAY_BUFFER, vertexRectangle, gl.STATIC_DRAW);
        // 找到頂點的位置
        let vertexPositionAttribute = gl.getAttribLocation(program, 'a_vertexPosition');
        // 告訴顯卡從當前綁定的緩沖區(qū)中讀取頂點數(shù)據(jù)
        gl.vertexAttribPointer(vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0);
        // 連接vertexPosition 變量與分配給它的緩沖區(qū)對象
        gl.enableVertexAttribArray(vertexPositionAttribute);

        let textureRectangle = new Float32Array([1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0]);
        let textureBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, textureBuffer);
        gl.bufferData(gl.ARRAY_BUFFER, textureRectangle, gl.STATIC_DRAW);
        let textureCoord = gl.getAttribLocation(program, 'a_texturePosition');
        gl.vertexAttribPointer(textureCoord, 2, gl.FLOAT, false, 0, 0);
        gl.enableVertexAttribArray(textureCoord);
    }

    /**
     * 創(chuàng)建并編譯一個著色器
     * @param {string} shaderSource GLSL 格式的著色器代碼
     * @param {number} shaderType 著色器類型, VERTEX_SHADER 或 FRAGMENT_SHADER仅仆。
     * @return {glShader} 著色器。
     */
    _compileShader(shaderSource, shaderType) {
        // 創(chuàng)建著色器程序
        let shader = this.gl.createShader(shaderType);
        // 設置著色器的源碼
        this.gl.shaderSource(shader, shaderSource);
        // 編譯著色器
        this.gl.compileShader(shader);
        const success = this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS);
        if (!success) {
            let err = this.gl.getShaderInfoLog(shader);
            this.gl.deleteShader(shader);
            console.error('could not compile shader', err);
            return;
        }

        return shader;
    }

    /**
     * 從 2 個著色器中創(chuàng)建一個程序
     * @param {glShader} vertexShader 頂點著色器垢袱。
     * @param {glShader} fragmentShader 片斷著色器墓拜。
     * @return {glProgram} 程序
     */
    _createProgram(vertexShader, fragmentShader) {
        const gl = this.gl;
        let program = gl.createProgram();

        // 附上著色器
        gl.attachShader(program, vertexShader);
        gl.attachShader(program, fragmentShader);

        gl.linkProgram(program);
        // 將 WebGLProgram 對象添加到當前的渲染狀態(tài)中
        gl.useProgram(program);
        const success = this.gl.getProgramParameter(program, this.gl.LINK_STATUS);

        if (!success) {
            console.err('program fail to link' + this.gl.getShaderInfoLog(program));
            return;
        }

        return program;
    }

    /**
     * 設置紋理
     */
    _createTexture(filter = this.gl.LINEAR) {
        let gl = this.gl;
        let t = gl.createTexture();
        // 將給定的 glTexture 綁定到目標(綁定點
        gl.bindTexture(gl.TEXTURE_2D, t);
        // 紋理包裝 參考https://github.com/fem-d/webGL/blob/master/blog/WebGL基礎學習篇(Lesson%207).md -> Texture wrapping
        gl.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);
        return t;
    }

    /**
     * 渲染圖片出來
     * @param {number} width 寬度
     * @param {number} height 高度
     */
    renderImg(width, height, data) {
        let gl = 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);

        let uOffset = width * height;
        let vOffset = (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);
    }

    /**
     * 根據(jù)重新設置 canvas 大小
     * @param {number} width 寬度
     * @param {number} height 高度
     * @param {number} maxWidth 最大寬度
     */
    setSize(width, height, maxWidth) {
        let canvasWidth = 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);
    }
}

最后我們來看下效果圖:


遇到的問題

在實際開發(fā)過程中咳榜,我們測試一些直播流,有時候渲染的時候圖像顯示是正常的爽锥,但是顏色會偏綠涌韩,經(jīng)研究發(fā)現(xiàn),直播流的不同主播的視頻寬度是會不一樣氯夷,比如在主播在 pk 的時候寬度368臣樱,熱門主播寬度會到 720,小主播寬度是 540腮考,而寬度為 540 的會顯示偏綠雇毫,具體原因是 webgl 會經(jīng)過預處理,默認會將以下值設置為 4:

// 圖像預處理
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 4);

這樣默認設置會每行 4 個字節(jié) 4 個字節(jié)處理踩蔚,而 Y分量每行的寬度是 540棚放,是 4 的倍數(shù),字節(jié)對齊了寂纪,所以圖像能夠正常顯示席吴,而 U,V 分量寬度是 540 / 2 = 270捞蛋,270 不是4 的倍數(shù)孝冒,字節(jié)非對齊,因此色素就會顯示偏綠拟杉。目前有兩種方法可以解決這個問題:

  • 第一個是直接讓 webgl 每行 1 個字節(jié) 1 個字節(jié)處理(對性能有影響):
// 圖像預處理
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
  • 第二個是讓獲取到的圖像的寬度是 8 的倍數(shù)庄涡,這樣就能做到 YUV 字節(jié)對齊,就不會顯示綠屏搬设,但是不建議這樣做穴店, 轉的時候CPU占用極大撕捍,建議采取第一個方案。

參考文章

圖像視頻編碼和FFmpeg(2)——YUV格式介紹和應用 - eustoma - 博客園
YUV pixel formats
https://wiki.videolan.org/YUV/
使用 8 位 YUV 格式的視頻呈現(xiàn) | Microsoft Docs
IOS 視頻格式之YUV - 簡書
圖解WebGL&Three.js工作原理 - cnwander - 博客園

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末泣洞,一起剝皮案震驚了整個濱河市忧风,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌球凰,老刑警劉巖狮腿,帶你破解...
    沈念sama閱讀 222,865評論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異呕诉,居然都是意外死亡缘厢,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,296評論 3 399
  • 文/潘曉璐 我一進店門甩挫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來贴硫,“玉大人,你說我怎么就攤上這事伊者∮⒃猓” “怎么了?”我有些...
    開封第一講書人閱讀 169,631評論 0 364
  • 文/不壞的土叔 我叫張陵删壮,是天一觀的道長贪绘。 經(jīng)常有香客問我,道長央碟,這世上最難降的妖魔是什么税灌? 我笑而不...
    開封第一講書人閱讀 60,199評論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮亿虽,結果婚禮上菱涤,老公的妹妹穿的比我還像新娘。我一直安慰自己洛勉,他們只是感情好粘秆,可當我...
    茶點故事閱讀 69,196評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著收毫,像睡著了一般攻走。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上此再,一...
    開封第一講書人閱讀 52,793評論 1 314
  • 那天昔搂,我揣著相機與錄音,去河邊找鬼输拇。 笑死摘符,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播逛裤,決...
    沈念sama閱讀 41,221評論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼瘩绒,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了带族?” 一聲冷哼從身側響起锁荔,我...
    開封第一講書人閱讀 40,174評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎炉菲,沒想到半個月后堕战,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,699評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡拍霜,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,770評論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了薪介。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片祠饺。...
    茶點故事閱讀 40,918評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖汁政,靈堂內(nèi)的尸體忽然破棺而出道偷,到底是詐尸還是另有隱情,我是刑警寧澤记劈,帶...
    沈念sama閱讀 36,573評論 5 351
  • 正文 年R本政府宣布勺鸦,位于F島的核電站,受9級特大地震影響目木,放射性物質發(fā)生泄漏换途。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,255評論 3 336
  • 文/蒙蒙 一刽射、第九天 我趴在偏房一處隱蔽的房頂上張望军拟。 院中可真熱鬧,春花似錦誓禁、人聲如沸懈息。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,749評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽辫继。三九已至,卻和暖如春俗慈,著一層夾襖步出監(jiān)牢的瞬間姑宽,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,862評論 1 274
  • 我被黑心中介騙來泰國打工姜盈, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留低千,地道東北人。 一個月前我還...
    沈念sama閱讀 49,364評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像示血,于是被迫代替她去往敵國和親棋傍。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,926評論 2 361