最近團隊在用 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)過以下這幾個步驟:
- 將視頻的文件比如 mp4豫尽,avi,flv等等顷帖,mp4美旧,avi,flv 相當于是一個容器贬墩,里面包含一些信息榴嗅,比如壓縮的視頻,壓縮的音頻等等陶舞, 進行解復用嗽测,從容器里面提取出壓縮的視頻以及音頻,壓縮的視頻一般是 H265肿孵、H264 格式或者其他格式唠粥,壓縮的音頻一般是 aac或者 mp3疏魏。
- 分別在壓縮的視頻和壓縮的音頻進行解碼,得到原始的視頻和音頻晤愧,原始的音頻數(shù)據(jù)一般是pcm 大莫,而原始的視頻數(shù)據(jù)一般是 yuv 或者 rgb。
- 然后進行音視頻的同步官份。
可以看到解碼壓縮的視頻數(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分量單獨顯示:
U分量單獨顯示:
V 分量單獨顯示:
由上面可以發(fā)現(xiàn)凌摄,Y 單獨顯示的時候是可以顯示完整的圖像的羡蛾,只不過圖片是灰色的。而U锨亏,V則代表的是色度痴怨,一個偏藍,一個偏紅器予。
使用YUV 的好處
- 由剛才看到的那樣浪藻,Y 單獨顯示是黑白圖像,因此YUV格式由彩色轉黑白很簡單劣摇,可以兼容老式黑白電視珠移,這一特性用在于電視信號上。
- YUV的數(shù)據(jù)尺寸一般都比RGB格式小,可以節(jié)約傳輸?shù)膸捑濉#ǖ绻肶UV444的話暇韧,和RGB24一樣都是24位)
YUV 采樣
常見的YUV的采樣有YUV444,YUV422浓瞪,YUV420:
注:黑點表示采樣該像素點的Y分量懈玻,空心圓圈表示采用該像素點的UV分量。
- YUV 4:4:4采樣乾颁,每一個Y對應一組UV分量涂乌。
- YUV 4:2:2采樣,每兩個Y共用一組UV分量英岭。
- 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
示绊,比如 YUV411
、YUV420
格式多見于數(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 世界中,能繪制的基本圖形元素只有點泼各、線鞍时、三角形,每個圖像都是由大大小小的三角形組成扣蜻,如下圖逆巍,無論是多么復雜的圖形,其基本組成部分都是由三角形組成莽使。
著色器
著色器是在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 繪制流程可以歸納為以下幾點:
- 提供頂點坐標(需要我們提供)
- 圖元裝配(按圖元類型組裝成圖形)
- 光柵化(將圖元裝配好的圖形,生成像素點)
- 提供顏色值(可以動態(tài)計算去枷,像素著色)
- 通過 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 - 博客園