美文网首页
IVWEB 玩转 WASM 系列-WEBGL YUV渲染图像实践

IVWEB 玩转 WASM 系列-WEBGL YUV渲染图像实践

作者: Moorez | 来源:发表于2019-12-01 00:41 被阅读0次

    最近团队在用 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 是怎么获取到的:


    实现播放器必定要经过的步骤

    由于我们是写播放器,实现一个播放器的步骤必定会经过以下这几个步骤:

    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,显示原图:

    原图
    Y分量单独显示:
    Y
    U分量单独显示:
    U
    V 分量单独显示:
    V
    由上面可以发现,Y 单独显示的时候是可以显示完整的图像的,只不过图片是灰色的。而U,V则代表的是色度,一个偏蓝,一个偏红。

    使用YUV 的好处

    1. 由刚才看到的那样,Y 单独显示是黑白图像,因此YUV格式由彩色转黑白很简单,可以兼容老式黑白电视,这一特性用在于电视信号上。
    2. YUV的数据尺寸一般都比RGB格式小,可以节约传输的带宽。(但如果用YUV444的话,和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是连续交错存储的。
    • planar 的YUV格式,先连续存储所有像素点的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根据U、V的顺序,又可分出2种格式:

    • YUV420P:U前V后即YUV420P,也叫I420,V前U后,叫YV12

    • YUV420SP:U前V后叫NV12,V前U后叫NV21

    数据排列如下:

    I420: YYYYYYYY UU VV =>YUV420P
    
    YV12: YYYYYYYY VV UU =>YUV420P
    
    NV12: YYYYYYYY UV UV =>YUV420SP
    
    NV21: YYYYYYYY VU VU =>YUV420SP
    

    至于为啥会有这么多格式,经过大量搜索发现原因是为了适配不同的电视广播制式和设备系统,比如 ios 下只有这一种模式NV12,安卓的模式是 NV21,比如 YUV411YUV420格式多见于数码摄像机数据中,前者用于NTSC制,后者用于 PAL制。至于电视广播制式的介绍我们可以看下这篇文章【标准】NTSC、PAL、SECAM三大制式简介

    YUV 计算方法

    以YUV420P存储一张1080 x 1280图片为例子,其存储大小为 ((1080 x 1280 x 3) >> 1) 个字节,这个是怎么算出来的?我们来看下面这张图:


    以 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:后面会用到):
    Y:0 到 1080*1280
    U:1080*1280 到 (1080*1280)*5/4
    V:(1080*1280)*5/4 到 (1080*1280)*3/2
    

    WEBGL

    WEBGL 是什么

    简单来说,WebGL是一项用来在网页上绘制和渲染复杂3D图形,并允许用户与之交互的技术。

    WEBGL 组成

    在 webgl 世界中,能绘制的基本图形元素只有点、线、三角形,每个图像都是由大大小小的三角形组成,如下图,无论是多么复杂的图形,其基本组成部分都是由三角形组成。

    图来源于网络

    着色器

    着色器是在GPU上运行的程序,是用OpenGL ES着色语言编写的,有点类似 c 语言:


    具体的语法可以参考着色器语言 GLSL (opengl-shader-language)入门大全,这里不在多加赘述。

    在 WEBGL 中想要绘制图形就必须要有两个着色器:

    • 顶点着色器
    • 片元着色器

    其中顶点着色器的主要功能就是用来处理顶点的,而片元着色器则是用来处理由光栅化阶段生成的每个片元(PS:片元可以理解为像素),最后计算出每个像素的颜色。

    WEBGL 绘制流程

    一、提供顶点坐标
    因为程序很傻,不知道图形的各个顶点,需要我们自己去提供,顶点坐标可以是自己手动写或者是由软件导出:


    在这个图中,我们把顶点写入到缓冲区里,缓冲区对象是WebGL系统中的一块内存区域,我们可以一次性地向缓冲区对象中填充大量的顶点数据,然后将这些数据保存在其中,供顶点着色器使用。接着我们创建并编译顶点着色器和片元着色器,并用 program 连接两个着色器,并使用。举个例子简单理解下为什么要这样做,我们可以理解成创建Fragment 元素: let f = document.createDocumentFragment()
    所有的着色器创建并编译后会处在一种游离的状态,我们需要将他们联系起来,并使用(可以理解成 document.body.appendChild(f),添加到 body,dom 元素才能被看到,也就是联系并使用)。
    接着我们还需要将缓冲区与顶点着色器进行连接,这样才能生效。

    二、图元装配
    我们提供顶点之后,GPU根据我们提供的顶点数量,会挨个执行顶点着色器程序,生成顶点最终的坐标,将图形装配起来。可以理解成制作风筝,就需要将风筝骨架先搭建起来,图元装配就是在这一阶段。

    三、光栅化
    这一阶段就好比是制作风筝,搭建好风筝骨架后,但是此时却不能飞起来,因为里面都是空的,需要为骨架添加布料。而光栅化就是在这一阶段,将图元装配好的几何图形转成片元(PS: 片元可以理解成像素)。

    四、着色与渲染


    着色这一阶段就好比风筝布料搭建完成,但是此时并没有什么图案,需要绘制图案,让风筝更加好看,也就是光栅化后的图形此时并没有颜色,需要经过片元着色器处理,逐片元进行上色并写到颜色缓冲区里,最后在浏览器才能显示有图像的几何图形。

    总结
    WEBGL 绘制流程可以归纳为以下几点:

    1. 提供顶点坐标(需要我们提供)
    2. 图元装配(按图元类型组装成图形)
    3. 光栅化(将图元装配好的图形,生成像素点)
    4. 提供颜色值(可以动态计算,像素着色)
    5. 通过 canvas 绘制在浏览器上。

    WEBGL YUV 绘制图像思路

    由于每个视频帧的图像都不太一样,我们肯定不可能知道那么多顶点,那么我们怎么将视频帧的图像用 webgl 画出来呢?这里使用了一个技巧—纹理映射。简单来说就是将一张图像贴在一个几何图形表面,使几何图形看起来像是有图像的几何图形,也就是将纹理坐标和 webgl 系统坐标进行一一对应:


    如上图,上面那个是纹理坐标,分为 s 和 t 坐标(或者叫 uv 坐标),值的范围在【0,1】之间,值和图像大小、分辨率无关。下面那张图是webgl坐标系统,是一个三维的坐标系统,这里声明了四个顶点,用两个三角形组装成一个长方形,然后将纹理坐标的顶点与 webgl 坐标系进行一一对应,最终传给片元着色器,片元着色器提取图片的一个个纹素颜色,输出在颜色缓冲区里,最终绘制在浏览器里(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代表计算精度,考虑节约性能使用了最低精度
    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);
    }
    
    • 创建并编译着色器,将顶点着色器和片段着色器连接到 program,并使用:
    let vertexShader=this._compileShader(vertexShaderSource,gl.VERTEX_SHADER);// 创建并编译顶点着色器
    let fragmentShader=this._compileShader(fragmentShaderSource,gl.FRAGMENT_SHADER);// 创建并编译片元着色器
    
    let program=this._createProgram(vertexShader,fragmentShader);// 创建program并连接着色器
    
    
    • 创建缓冲区,存顶点和纹理坐标(PS:缓冲区对象是WebGL系统中的一块内存区域,我们可以一次性地向缓冲区对象中填充大量的顶点数据,然后将这些数据保存在其中,供顶点着色器使用)。
    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);
    // 向缓冲区写入数据
    gl.bufferData(gl.ARRAY_BUFFER, vertexRectangle, gl.STATIC_DRAW);
    // 找到顶点的位置
    let vertexPositionAttribute = gl.getAttribLocation(program, 'a_vertexPosition');
    // 告诉显卡从当前绑定的缓冲区中读取顶点数据
    gl.vertexAttribPointer(vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0);
    // 连接vertexPosition 变量与分配给它的缓冲区对象
    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(); // 创建纹理
    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:由于我们获取到的数据是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);
            // 向缓冲区写入数据
            gl.bufferData(gl.ARRAY_BUFFER, vertexRectangle, gl.STATIC_DRAW);
            // 找到顶点的位置
            let vertexPositionAttribute = gl.getAttribLocation(program, 'a_vertexPosition');
            // 告诉显卡从当前绑定的缓冲区中读取顶点数据
            gl.vertexAttribPointer(vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0);
            // 连接vertexPosition 变量与分配给它的缓冲区对象
            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);
        }
    
        /**
         * 创建并编译一个着色器
         * @param {string} shaderSource GLSL 格式的着色器代码
         * @param {number} shaderType 着色器类型, VERTEX_SHADER 或 FRAGMENT_SHADER。
         * @return {glShader} 着色器。
         */
        _compileShader(shaderSource, shaderType) {
            // 创建着色器程序
            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 个着色器中创建一个程序
         * @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 对象添加到当前的渲染状态中
            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);
        }
    
        /**
         * 根据重新设置 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);
        }
    }
    

    最后我们来看下效果图:


    遇到的问题

    在实际开发过程中,我们测试一些直播流,有时候渲染的时候图像显示是正常的,但是颜色会偏绿,经研究发现,直播流的不同主播的视频宽度是会不一样,比如在主播在 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占用极大,建议采取第一个方案。

    参考文章

    图像视频编码和FFmpeg(2)——YUV格式介绍和应用 - eustoma - 博客园
    YUV pixel formats
    https://wiki.videolan.org/YUV/
    使用 8 位 YUV 格式的视频呈现 | Microsoft Docs
    IOS 视频格式之YUV - 简书
    图解WebGL&Three.js工作原理 - cnwander - 博客园

    相关文章

      网友评论

          本文标题:IVWEB 玩转 WASM 系列-WEBGL YUV渲染图像实践

          本文链接:https://www.haomeiwen.com/subject/ytgawctx.html