最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 从0到1实现Web端H.265播放器:YUV渲染篇

    正文概述 掘金(淘系前端团队)   2021-05-14   929

    前言

    上一篇文章《视频解码篇》主要介绍了原始HEVC码流如何解码成YUV数据(通常视频采用的都是YUV格式),本章主要介绍如何将解码的YUV数据渲染成图像。在此之前我们先回顾一下DEMO架构

    从0到1实现Web端H.265播放器:YUV渲染篇

    上图中可以看到,我们接收到YUV数据后需要使用WebGL对YUV处理转换成RGB数据然后进行渲染。那么为什么要转换成RGB呢,首先我们先了解下什么是YUV,以及YUV和RGB的区别。

    什么是YUV

    从0到1实现Web端H.265播放器:YUV渲染篇 (从上至下分别是原图,Y分量,U分量,V分量)

    节选一段维基百科的描述:

    YUV是编译true-color颜色空间的种类,Y'UV, YUV, YCbCr,YPbPr等专有名词都可以称为YUV,彼此有重叠。“Y”表示明亮度(Luminance、Luma),“U”和“V”则是色度、浓度(Chrominance、Chroma)。通俗讲就是Y可以用来渲染黑白图像,而UV用来上色。

    YUV Formats分成两个格式:

    • 紧缩格式(packed formats):将Y、U、V值存储成Macro Pixels数组,和RGB的存放方式类似。

    • 平面格式(planar formats):将Y、U、V的三个分量分别存放在不同的矩阵中。

    紧缩格式中的YUV是混合在一起的,对于YUV4:4:4格式而言,用紧缩格式很合适的,因此就有了UYVY、YUYV等。平面格式是指每Y分量,U分量和V分量都是以独立的平面组织的,也就是说所有的U分量必须在Y分量后面,而V分量在所有的U分量后面,此一格式适用于采样。平面格式有I420(4:2:0)、YV12、IYUV等。

    从0到1实现Web端H.265播放器:YUV渲染篇

    本文用例中的视频为420p采样,故后续代码均以YUV-420p采样为准

    与RGB的区别

    从0到1实现Web端H.265播放器:YUV渲染篇

    RGB,三原色光模式,又称RGB颜色模型或红绿蓝颜色模型,是一种加色模型,将红(Red)、绿(Green)、蓝(Blue)三原色的色光以不同的比例相加,以合成产生各种色彩光。

    至今为止,所有的彩色显示屏都是使用三原色光加色技术,以RGB三原色作为子像素构成一像素,由多个像素构成整个画面,通过发射出三种不同强度的电子束,使屏幕内侧覆盖的红、绿、蓝磷光材料发光而产生色彩。包括如今的液晶显示屏(LCD)。

    RGB诉求于人眼对色彩的感应,YUV则着重于视觉对于亮度的敏感程度。因为人眼相比色度,对亮度更敏感。所以YUV对亮度的完全采样,色度的选择采样。即可在人眼察觉不到的范围内最大限度的压缩图像。色度抽样

    为节省带宽起见,大多数YUV格式平均使用的每像素位数都少于24位。主要的抽样(subsample)格式有YCbCr 4:2:0、YCbCr 4:2:2、YCbCr 4:1:1和YCbCr 4:4:4。YUV的表示法称为A:B:C表示法:

    从0到1实现Web端H.265播放器:YUV渲染篇

    • 4:4:4表示完全取样。

    • 4:2:2表示2:1的水平取样,垂直完全采样。

    • 4:2:0表示2:1的水平取样,垂直2:1采样。

    • 4:1:1表示4:1的水平取样,垂直完全采样。

    由于YUV占用较少的带宽,而显示器又是使用RGB发光,所以一般都是采用YUV传输,然后转换成RGB渲染到显示器上。

    WebGL-YUV渲染

    目前在Web上高性能渲染YUV数据需要借助WebGL的能力,将YUV转RGB的计算过程放在shader里可以获得硬件加速。GPU对浮点数运算要快于CPU。

    在此推荐一个入门学习网站WebGL Fundamentals,可多语言切换(含中文)。

    鉴于部分读者可能没时间翻阅,下面我也将简单介绍下WebGL是如何工作的

    WebGL工作原理

    WebGL脱胎于OpenGL,Web开发者可通过HTML5Canvas获取gl对象从而使用WebGL能力为图像绘制提供硬件加速。

    大家学过几何的应该都知道点线面概念,而WebGL可以通过对应方法绘制点(Point)、线(Line)、三角(TRIANGLES),其它图形则是通过拼凑三角而成,比如矩形就是两个三角。绘制图形需要用到着色器,主要分为顶点着色器(vertex shader)和片段着色器(fragment shader),这两者一般成对出现。着色器有着C Like语法的强类型脚本语言GLSL,使用该语言进行函数计算。每一对组合关联在一起就是一个program(着色程序)。
    从0到1实现Web端H.265播放器:YUV渲染篇

    如上图所示,vertex array指的是模型数据,主要分为VBO和IBO,前者是顶点数据,后者是顶点索引。输入到vertex shader确定顶点坐标,通过IBO确定哪几个VBO连接成三角形,再将这些三角形进行光栅化(通俗讲就是矢量图转像素图)。fragment shader接收到光栅化后的像素面进行着色。

    从0到1实现Web端H.265播放器:YUV渲染篇

    在JS中创建着色器并关联program的步骤如下:

    const gl = canvas.getContext('webgl')
    // 创建着色器
    const vertexShader = gl.createShader(gl.VERTEX_SHADER)
    const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)
    const program = gl.createProgram()
    if (!(vertexShader && fragmentShader && program)) {
        console.warn('shaders create failed')
    }
    // vertexShaderScript420 为 yuv420p 顶点着色器脚本内容,后文再介绍
    gl.shaderSource(vertexShader, vertexShaderScript420)
    gl.compileShader(vertexShader)
    if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
        console.warn('Vertex shader failed to compile: ', gl.getShaderInfoLog(vertexShader))
    }
    // fragmentShaderScript420 为 yuv420p 片段着色器脚本内容
    gl.shaderSource(fragmentShader, fragmentShaderScript420)
    gl.compileShader(fragmentShader)
    if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
        console.log('Fragment shader failed to compile: ', gl.getShaderInfoLog(fragmentShader))
    }
    // 关联并使用此着色程序
    gl.attachShader(program, vertexShader)
    gl.attachShader(program, fragmentShader)
    gl.linkProgram(program)
    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
        console.log('Program failed to compile: ', gl.getProgramInfoLog(program))
    }
    gl.useProgram(program)
    

    GLSL脚本

    基础概念

    前段代码中提到的vertexShaderScript420fragmentShaderScript420都是对应着色器的脚本代码内容,基于GLSL脚本语言。下面将简单介绍下GLSL中的概念和语法:

    • 属性(Attributes)和缓冲(WebGLBuffer)

    • 缓冲(WebGLBuffer)用来发送到GPU的数据队列,你可以用来存储位置、法向量等任何数据。

    • 属性(Attributes)用来指明怎么从缓冲中获取所需数据并将它提供给顶点着色器。

    • 全局变量(Uniforms)

    • 全局变量在着色程序运行前赋值,在运行过程中全局有效。

    • 纹理(Textures)

    • 纹理是一个数据序列,可以在着色程序运行中随意读取其中的数据。 大多数情况存放的是图像数据。

    • 可变量(Varyings)

    • 可变量是一种顶点着色器给片断着色器传值的方式。即可以在片段着色器代码中访问顶点着色器的varying可变量

    代码实例

    WebGL绘制只关心两件事:裁剪空间中的坐标值和颜色值。顶点着色器提供裁剪空间坐标值,片断着色器提供颜色值。

    那我们要怎么绘制视频图像数据呢,大家玩过3D游戏的游戏都有听说过贴图这个说法吧,在WebGL里这个技术叫做纹理映射。把纹理空间的像素映射到几何物体的表面。FFmpeg产生的每一帧YUV数据都可以当作是一个纹理图案,映射到2个三角形拼接的矩形上。如下图所示:

    从0到1实现Web端H.265播放器:YUV渲染篇

    纹理空间的坐标系称为UV(ST)坐标,分别表示显示器水平、垂直方向的坐标。一般取值范围为0-1。由于Canvas坐标系Y轴朝下,与纹理坐标对比相当于Y轴翻转。所以要么使用GL的方法gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1),要么针对顶点坐标作特殊处理。

    首先我们先编写着色器脚本程序,代码如下:

    • vertexShaderScript420

      attribute vec4 vertexPos; // 顶点坐标 attribute vec2 texturePos; // 纹理坐标 varying vec2 textureCoord; // 传递纹理坐标

      void main() { gl_Position = vertexPos; // 设置顶点坐标 textureCoord = texturePos; // 设置纹理坐标 }

    • fragmentShaderScript420

      // 片断着色器没有默认精度,所以我们需要设置一个精度 // 这里选择高精度 precision highp float; varying highp vec2 textureCoord; // 接收纹理坐标 uniform sampler2D ySampler; // y图片纹理数据取样器 uniform sampler2D uSampler; // u... uniform sampler2D vSampler; // v... const mat4 YUV2RGB = mat4( 1.1643828125, 0, 1.59602734375, -.87078515625, 1.1643828125, -.39176171875, -.81296875, .52959375, 1.1643828125, 2.017234375, 0, -1.081390625, 0, 0, 0, 1 ); // YUV 转 RGB 的数学计算公式。

      void main(void) { highp float y = texture2D(ySampler, textureCoord).r; // .r等同于.x、.s、[0] highp float u = texture2D(uSampler, textureCoord).r; highp float v = texture2D(vSampler, textureCoord).r; // gl_FragColor是一个片断着色器主要设置的变量,后面则是矩阵运算,将YUV转换成RGB gl_FragColor = vec4(y, u, v, 1) * YUV2RGB; }

    vertexShaderScript420代码负责接收设置顶点坐标、接收并传递纹理坐标。fragmentShaderScript420代码负责接收yuv纹理贴图数据并通过转换公式(GLSL支持矩阵向量乘法运算)将YUV转换成RGB。

    纹理映射

    创建了着色器的实例对象以及着色器的内部计算逻辑后,需要填充顶点数据,告诉着色器要绘制几个顶点,以及纹理与几何面的关系。

    • 顶点坐标取值范围为-1到1,我们渲染平面图,所以只提供x,y坐标即可。总共两个三角片,顶点每三个连接在一起即[1, 1, -1, 1, 1, -1, 1, -1, -1, 1, -1, -1]

    • 纹理坐标取值范围为0到1,因为canvas和uv坐标为y轴翻转关系,正常来说我们需要对顶点也做翻转处理。即[1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0]

    为了让各位更清晰的了解纹理映射的关系,我们把顶点坐标固定(代表三角形也是固定的),纹理坐标则罗列三种情况如下所示:

    从0到1实现Web端H.265播放器:YUV渲染篇

    • 不进行坐标翻转渲染出了纹理的原图,但可以发现图片的方向反了,原因便是前面提到的Canvas Y轴朝下的原因。需要额外调用gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1)即可正常渲染

    • A三角坐标翻转,同时B坐标映射乱序一下,会发现A是正常的,但B却是旋转了45度的翻转图。

    • 对A、B都做正常顺序的翻转映射,不需要调用额外的API也可正常渲染

    这里我们选择第三种情况的坐标映射关系,具体代码如下:

    // 创建缓冲并存入相关顶点数据
    gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer())
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([1, 1, -1, 1, 1, -1, 1, -1, -1, 1, -1, -1]), gl.STATIC_DRAW)
    // 找到顶点坐标属性(Attribute)的地址
    const vertexPos = gl.getAttribLocation(program, 'vertexPos')
    // 告诉WebGL怎么从缓冲中获取数据传递给属性
    gl.enableVertexAttribArray(vertexPos)
    gl.vertexAttribPointer(vertexPos, 2, gl.FLOAT, false, 0, 0) // (属性地址, 坐标数, 32位浮点数, 不标准化, stride, offset)
    
    gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer())
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0]), gl.STATIC_DRAW)
    
    const texturePos = gl.getAttribLocation(program, 'texturePos')
    gl.enableVertexAttribArray(texturePos)
    gl.vertexAttribPointer(texturePos, 2, gl.FLOAT, false, 0, 0)
    

    绑定了顶点数据之后,还需要绑定下纹理数据。WebGL使用多个纹理单元

    function createTexture(gl: WebGL2RenderingContext) {
        const texture = gl.createTexture()
        gl.bindTexture(gl.TEXTURE_2D, texture)
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)    // 当放大时选择4个像素混合
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)    // 当缩小时选择4个像素混合
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) // 表示U方向不需要重复贴图
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) // 表示V方向不需要重复贴图
        gl.bindTexture(gl.TEXTURE_2D, null)
        return texture
    }
    // 创建y纹理对象
    const yTexture = createTexture(gl)
    // 找到ySampler地址,并告诉sampler取样器使用第0个纹理单元,即gl.TEXTURE0
    const ySampler = gl.getUniformLocation(program, 'ySampler')
    gl.uniform1i(ySampler, 0)
    
    const uTexture = createTexture(gl)
    const uSampler = gl.getUniformLocation(program, 'uSampler')
    gl.uniform1i(uSampler, 1)
    
    const vTexture = createTexture(gl)
    const vSampler = gl.getUniformLocation(program, 'vSampler')
    gl.uniform1i(vSampler, 2)
    

    绘制YUV数据

    在《视频解码篇》中,我们通过FFmpeg解码得到了每一帧的YUV数据,且采用了yuv420p排列,所以平铺模式下y数据在前,u数据紧跟,v数据最后,将yuv数据分别填充到对应的纹理取样器中即可绘制出图像了
    从0到1实现Web端H.265播放器:YUV渲染篇

    // buffer 即为解码后的帧数据,videoWidth、videoHeight分别为视频画面的宽和高
    
    const size = videoWidth * videoHeight
    gl.viewport(0, 0, videoWidth, videoHeight)
    
    // 根据前面YUV的说明已经清楚,有多少个像素就有多少y分量,所以y分量数据长度=宽*高
    const yLen = size
    const yData = buffer.subarray(0, yLen)
    gl.activeTexture(gl.TEXTURE0)
    gl.bindTexture(gl.TEXTURE_2D, yTexture)
    // 指明纹理的具体属性
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, videoWidth, videoHeight, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, yData)
    
    // 420模式下u和v都为y分量的1/4.
    const uLen = size / 4
    const uData = buffer.subarray(yLen, yLen + uLen)
    gl.activeTexture(gl.TEXTURE1)
    gl.bindTexture(gl.TEXTURE_2D, uTexture)
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, videoWidth / 2, videoHeight / 2, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, uData)
    
    const vLen = uLen
    const vData = buffer.subarray(yLen + uLen, yLen + uLen + vLen)
    gl.activeTexture(gl.TEXTURE2)
    gl.bindTexture(gl.TEXTURE_2D, vTexture)
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, videoWidth / 2, videoHeight / 2, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, vData)
    
    // 按照多个三角形的方式绘制,从顶点0开始绘制,总计6个顶点
    gl.drawArrays(gl.TRIANGLES, 0, 6)
    

    结语

    《视频解码篇》中通过FFmpeg解码出的帧数据即可通过以上步骤渲染到Canvas中。以上内容是我在H265播放器应用中的WebGL实践总结,WebGL的世界很大,本人也尚在学习中,此文如有错误之处欢迎指出。

    尽请期待后续的系列文章:
    《从0到1实现Web端H.265播放器:MP4/fMP4 解封装篇》


    起源地下载网 » 从0到1实现Web端H.265播放器:YUV渲染篇

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元