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

2024-03-03 02:20

本文主要是介绍从0到1实现Web端H.265播放器:YUV渲染篇,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

说明

本文转载自:从0到1实现Web端H.265播放器:YUV渲染篇

前言

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

在这里插入图片描述

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

什么是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等。

在这里插入图片描述

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

与RGB的区别

在这里插入图片描述

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表示法:

在这里插入图片描述

  • 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工作原理

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

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

在这里插入图片描述

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

在这里插入图片描述

在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个三角形拼接的矩形上。如下图所示:

在这里插入图片描述

纹理空间的坐标系称为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转换成RGBgl_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]

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

在这里插入图片描述

不进行坐标翻转渲染出了纹理的原图,但可以发现图片的方向反了,原因便是前面提到的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)

绑定了顶点数据之后,还需要绑定下纹理数据。

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数据分别填充到对应的纹理取样器中即可绘制出图像了

在这里插入图片描述

// 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播放器:YUV渲染篇的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!


原文地址:https://blog.csdn.net/kaimo313/article/details/117090289
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.chinasem.cn/article/768156

相关文章

Spring Boot 实现 IP 限流的原理、实践与利弊解析

《SpringBoot实现IP限流的原理、实践与利弊解析》在SpringBoot中实现IP限流是一种简单而有效的方式来保障系统的稳定性和可用性,本文给大家介绍SpringBoot实现IP限... 目录一、引言二、IP 限流原理2.1 令牌桶算法2.2 漏桶算法三、使用场景3.1 防止恶意攻击3.2 控制资源

springboot下载接口限速功能实现

《springboot下载接口限速功能实现》通过Redis统计并发数动态调整每个用户带宽,核心逻辑为每秒读取并发送限定数据量,防止单用户占用过多资源,确保整体下载均衡且高效,本文给大家介绍spring... 目录 一、整体目标 二、涉及的主要类/方法✅ 三、核心流程图解(简化) 四、关键代码详解1️⃣ 设置

Nginx 配置跨域的实现及常见问题解决

《Nginx配置跨域的实现及常见问题解决》本文主要介绍了Nginx配置跨域的实现及常见问题解决,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来... 目录1. 跨域1.1 同源策略1.2 跨域资源共享(CORS)2. Nginx 配置跨域的场景2.1

Python中提取文件名扩展名的多种方法实现

《Python中提取文件名扩展名的多种方法实现》在Python编程中,经常会遇到需要从文件名中提取扩展名的场景,Python提供了多种方法来实现这一功能,不同方法适用于不同的场景和需求,包括os.pa... 目录技术背景实现步骤方法一:使用os.path.splitext方法二:使用pathlib模块方法三

CSS实现元素撑满剩余空间的五种方法

《CSS实现元素撑满剩余空间的五种方法》在日常开发中,我们经常需要让某个元素占据容器的剩余空间,本文将介绍5种不同的方法来实现这个需求,并分析各种方法的优缺点,感兴趣的朋友一起看看吧... css实现元素撑满剩余空间的5种方法 在日常开发中,我们经常需要让某个元素占据容器的剩余空间。这是一个常见的布局需求

HTML5 getUserMedia API网页录音实现指南示例小结

《HTML5getUserMediaAPI网页录音实现指南示例小结》本教程将指导你如何利用这一API,结合WebAudioAPI,实现网页录音功能,从获取音频流到处理和保存录音,整个过程将逐步... 目录1. html5 getUserMedia API简介1.1 API概念与历史1.2 功能与优势1.3

Java实现删除文件中的指定内容

《Java实现删除文件中的指定内容》在日常开发中,经常需要对文本文件进行批量处理,其中,删除文件中指定内容是最常见的需求之一,下面我们就来看看如何使用java实现删除文件中的指定内容吧... 目录1. 项目背景详细介绍2. 项目需求详细介绍2.1 功能需求2.2 非功能需求3. 相关技术详细介绍3.1 Ja

使用Python和OpenCV库实现实时颜色识别系统

《使用Python和OpenCV库实现实时颜色识别系统》:本文主要介绍使用Python和OpenCV库实现的实时颜色识别系统,这个系统能够通过摄像头捕捉视频流,并在视频中指定区域内识别主要颜色(红... 目录一、引言二、系统概述三、代码解析1. 导入库2. 颜色识别函数3. 主程序循环四、HSV色彩空间详解

PostgreSQL中MVCC 机制的实现

《PostgreSQL中MVCC机制的实现》本文主要介绍了PostgreSQL中MVCC机制的实现,通过多版本数据存储、快照隔离和事务ID管理实现高并发读写,具有一定的参考价值,感兴趣的可以了解一下... 目录一 MVCC 基本原理python1.1 MVCC 核心概念1.2 与传统锁机制对比二 Postg

SpringBoot整合Flowable实现工作流的详细流程

《SpringBoot整合Flowable实现工作流的详细流程》Flowable是一个使用Java编写的轻量级业务流程引擎,Flowable流程引擎可用于部署BPMN2.0流程定义,创建这些流程定义的... 目录1、流程引擎介绍2、创建项目3、画流程图4、开发接口4.1 Java 类梳理4.2 查看流程图4