/ 今日科技快讯 /
近日,快手短视频携手春晚签约“品牌强国工程”强国品牌服务项目。快手成为中央广播电视总台2020年《春节联欢晚会》独家互动合作伙伴,开展今年的春晚红包互动,在除夕为全国人民送上祝福。快手因此成为首家参与春晚红包活动的短视频平台。 / 作者简介 / 本篇文章来自易水南风的投稿,分享了android开发中如何基于O penGL 来渲染yuv视频的内容,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。 易水南风的博客地址:https://blog.csdn.net/sinat_23092639/ 前言 / 本篇博文涉及的知识点主要有三个:
yuv的概念
基于ndk进行C++程序的基本编写
OpenGL纹理的绘制
4:2:2
表示2:1水平下采样,没有垂直下采样。每条扫描线包含四个Y样本对应两个U或V样本。也就是水平方向按照y:uv使用2:1进行采样,垂直方向全采样的方式:4:2:0
表示2:1水平下采样,2:1垂直下采样。也就是水平方向按照y:uv使用2:1进行采样,垂直方向按照y:uv使用2:1的方式: 注意这里4:2:0并不代表y:u:v = 4:2:0,这里指的是在每一行扫描时,只扫描一种色度分量(U 或者 V),和 Y 分量按照 2 : 1 的方式采样。比如,第一行扫描时,YU 按照 2 : 1 的方式采样,那么第二行扫描时,YV 分量按照 2:1 的方式采样。所以y和u或者v的比都是2:1。 4:1:1 表示4:1水平下采样,没有垂直下采样。每条扫描线包含四个Y样本对应于每一个U或V样本。 4:1:1抽样比其他格式更少见,本文不详细讨论。 yuv存储格式 YUV存储格式有两大类:planar 和 packed。 packed:Y、U和V组件存储在一个数组中。每个像素点的Y,U,V是连续交错存储的。和RGB的存储格式类似。planar :Y、U和V组件存储为三个独立的数组中。y、u、v每个采样点使用8bit存储。接下来详细讲下集中常见的yuv格式存储方式。4:2:2格式主要有两种具体格式: YUY2 属于packed类型,YUY2格式,数据可视为unsigned char数组。第一个字节包含第一个Y样本,第二个字节包含第一U (Cb)样本,第三字节包含第二Y样本,第四个字节包含第V (Cr)样本,以此类推,如图: 可以看到,Y0 和 Y1 公用 U0 V0 分量,Y2 和 Y3 公用 U1 V1 分量,以此类推。UYVY
也是属于属于packed类型的,和YUY2和类似,只是存储方向是相反的: 4:2:0格式又包含多种存储方式,这里重点将以下几种: YUV 420P 和 YUV 420SP 都是基于 Planar 平面模式 进行存储的,先存储所有的 Y 分量后, YUV420P 类型就会先存储所有的 U 分量或者 V 分量,而 YUV420SP 则是按照 UV 或者 VU 的交替顺序进行存储了,具体查看看下图YUV420P
(这里需要敲黑板,因为本文播放的yuv就是YUV420P格式,熟悉它的存储格式才可以理解代码中读取视频帧数据的逻辑) 正是因为 YUV420P是2:1水平下采样,2:1垂直下采样,所以y分量数量等于视频宽高,u和v分量都是视频宽乘以高/4。YUV420SP
4:2:0格式还有YV12、YU12、NV12 、NV21等存储格式,这里因为篇幅关系就不做细谈。yuv转RGB
目前一般解码后的视频格式为yuv,但是一般显卡渲染的格式是RGB,所以需要把yuv转化为RGB。关于yuv转RGB这里有个公式可以自己使用: 或者直接用yuv的矩阵乘以以下矩阵得到对应的RGB矩阵: yuv就先介绍到这里,熟悉yuv对于后面yuv视频播放至关重要。 / 谈谈OpenGL / OpenGL是行业领域中最为广泛接纳的 2D/3D 图形 API。OpenGL是一个跨平台的软件接口语言,用于调用硬件的2D、3D图形处理器。由于只是软件接口,所以具体底层实现依赖硬件设备制造商。关于OpenGL的知识,可能写20篇博文也介绍不完,这里只介绍和当前播放yuv相关的,不会很详细,详细教程可以看这个网站:
https://www.zhihu.com/question/19913939安卓使用的是OpenGL ES版本,即OpenGL的一个子集,裁剪了一些功能,专门使用在嵌入式设备。 OpenGL图形渲染管线 首先要解释的是OpenGL的图形渲染管线:指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程。分为两个主要部分:第一部分把你的3D坐标转换为2D坐标,第二部分是把2D坐标转变为实际的有颜色的像素。 图形渲染管线接受一组3D坐标,然后把它们转变为你屏幕上的有色2D像素输出。图形渲染管线可以被划分为几个阶段,每个阶段将会把前一个阶段的输出作为输入。 当今大多数显卡都有成千上万的小处理核心,它们在GPU上为每一个(渲染管线)阶段运行各自的相互独立的并行处理小程序,从而在图形渲染管线中快速处理你的数据。这些小程序叫做着色器(Shader),因为它们运行在GPU中,所以解放了CPU的省生产力。 图形渲染管线的每个阶段的展示: 图形渲染管线的第一个部分是顶点着色器(Vertex Shader),它把一个单独的顶点作为输入。顶点着色器主要的目的是把3D坐标转为另一种3D坐标(坐标系统的转化),同时顶点着色器允许我们对顶点属性进行一些基本处理。顶点着色器代码是每个顶点执行一次。 图元装配(Primitive Assembly)阶段将顶点着色器输出的所有顶点作为输入(如果是GL_POINTS,那么就是一个顶点),并所有的点装配成指定图元的形状。比如将顶点装配为三角形或者矩形。 几何着色器的输出会被传入光栅化阶段(Rasterization Stage),这里它会把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment,OpenGL渲染一个像素所需的所有数据)。 片段着色器的主要目的是计算一个像素的最终颜色,这也是所有OpenGL高级效果产生的地方。片段着色器是每个片段(像素)执行一次。 而我们要处理的,主要就是顶点着色器和片段着色器的代码逻辑,着色器是用叫GLSL的类C语言写成的,它包含一些针对向量和矩阵操作的有用特性。
OpenGL坐标系
要写顶点着色器代码,首先就要知道OpenGL顶点坐标系: 按照惯例,OpenGL是一个右手坐标系。简单来说,就是正x轴在你的右手边,正y轴朝上,而正z轴是朝向后方的。想象你的屏幕处于三个轴的中心,则正z轴穿过你的屏幕朝向你: (这里要提的一点事,OpenGL在执行顶点着色器之后,会像流水线一样将坐标进行5个步骤的变换:局部坐标–世界坐标–观察坐标–裁剪坐标–屏幕坐标,这里因为实例是2D的,暂时还不需要关心这些) 现在需要记得的是,OpenGL仅当3D坐标在3个轴(x、y和z)上都为-1.0到1.0的范围内时才处理它。所有在所谓的标准化设备坐标(Normalized Device Coordinates)范围内的坐标才会最终呈现在屏幕上(在这个范围以外的坐标都不会显示)。 2D情况下,既不考虑z轴,则一般来说顶点坐标系如下所示: OpenGL纹理绘制 通过顶点着色器和片段着色器,我们可以指定要绘制的物体形状大小以及颜色,但是如果我们要做类似将一张图片绘制上去,该如何做呢? OpenGL提供了纹理这个概念,让你可以将一张图片“贴”到你想要的位置。 那么纹理是如何“贴”到图形上去的呢?其实就是对图片进行采样,再将采样到的颜色数据绘制到图形相应的位置。 为了能够把纹理映射(Map)到我们的图形上,我们需要指定图形的每个顶点各自对应纹理的哪个部分。所以图形的每个顶点都会关联一个纹理的坐标,用来标明该从纹理图像的哪个部分采样。 通俗来说,就是比方你顶点坐标提供的是一个矩形,现在要将一张图片(纹理)“贴”到矩形上,那么需要指定一个纹理坐标,告诉OpenGL矩形光栅化处理后的每个片段对应图片的哪个像素的颜色。纹理坐标,简单来说就是以一张纹理图片的某个点作为原点的坐标系。类似下图所示: 由上图可以看到纹理坐标系的模样了,不过在Android平台,纹理坐标如下: 即以图片的左上角为原点的坐标系。 所以在提供了顶点坐标和纹理坐标之后,OpenGL就知道如何通过采样纹理上的像素的颜色数据,将颜色绘制到顶点坐标所表达的图形上的对应位置。 / 程序实例分析 / 所谓工欲善其事必先利其器,基础知识讲得差不多了,那么又要进入最重要的将代码环节了,这里使用的yuv格式为yuv420p。 这里使用cmake进行构建,native-lib为项目自定义的动态库名称,其余需要链接的动态库如下配置:find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log )
target_link_libraries( # Specifies the target library.native-lib
GLESv2
EGL
android
# Links the target library to the log library
# included in the NDK.
${log-lib} )
Java层首先创建一个集成GLSurfaceView的类:
public class YuvPlayer extends GLSurfaceView implements Runnable, SurfaceHolder.Callback, GLSurfaceView.Renderer {//这里将yuv视频文件放在sdcard目录中private final static String PATH = "/sdcard/sintel_640_360.yuv";public YuvPlayer(Context context, AttributeSet attrs) {super(context, attrs);
setRenderer(this);
}@Overridepublic void surfaceCreated(SurfaceHolder holder) {new Thread(this).start();
}@Overridepublic void surfaceDestroyed(SurfaceHolder holder) {
}@Overridepublic void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
}@Overridepublic void run() {
loadYuv(PATH,getHolder().getSurface());
}//定义一个native方法加载yuv视频文件public native void loadYuv(String url, Object surface);@Overridepublic void onSurfaceCreated(GL10 gl, EGLConfig config) {
}@Overridepublic void onSurfaceChanged(GL10 gl, int width, int height) {
}@Overridepublic void onDrawFrame(GL10 gl) {
}
}
进入native层的loadYuv方法:
Java_com_example_yuvopengldemo_YuvPlayer_loadYuv(JNIEnv *env, jobject thiz, jstring jUrl,
jobject surface) {const char *url = env->GetStringUTFChars(jUrl, 0);//打开yuv视频文件
FILE *fp = fopen(url, "rb");if (!fp) {//打Log方法
LOGD("oepn file %s fail", url);return;
}
LOGD("open ulr is %s", url);
首先是从Java层传入的jstring变量转为char*,然后打开yuv视频文件。接下来是初始化EGL。这里简单解释下EGL是什么。
EGL是Khronos呈现api(如OpenGL ES或OpenVG)与底层本机平台窗口系统之间的接口。它处理图形上下文管理、表面/缓冲区绑定和呈现同步,并使用其他Khronos api支持高性能、加速、混合模式的2D和3D呈现。EGL还提供了Khronos之间的互操作能力,以支持在api之间高效地传输数据——例如在运行OpenMAX AL的视频子系统和运行OpenGL ES的GPU之间。
通俗来讲就是,EGL是渲染API(如OpenGL, OpenGL ES, OpenVG)和本地窗口系统之间的接口。EGL可以理解为OpenGL ES ES和设备之间的桥梁,EGL是为OpenGL提供绘制表面的。因为OpenGL是跨平台的,当它访问不同平台的设备的时候需要EGL作为中间的适配器。
EGL的使用步骤:
具体的代码:
//1.获取原始窗口
ANativeWindow *nwin = ANativeWindow_fromSurface(env, surface);//获取OpenGl ES的渲染目标。Display(EGLDisplay) 是对实际显示设备的抽象。
EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);if (display == EGL_NO_DISPLAY) {
LOGD("egl display failed");return;
}//2.初始化egl与 EGLDisplay 之间的连接,后两个参数为主次版本号if (EGL_TRUE != eglInitialize(display, 0, 0)) {
LOGD("eglInitialize failed");return;
}//创建渲染用的surface//2.1 surface配置
EGLConfig eglConfig;
EGLint configNum;
EGLint configSpec[] = {
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
EGL_NONE
};if (EGL_TRUE != eglChooseConfig(display, configSpec, &eglConfig, 1, &configNum)) {
LOGD("eglChooseConfig failed");return;
}//2.2创建surface(将egl和NativeWindow进行关联,即将EGl和设备屏幕连接起来。最后一个参数为属性信息,0表示默认版本)。Surface(EGLSurface)是对用来存储图像的内存区FrameBuffer 的抽象。这就是我们要渲染的Surface
EGLSurface winSurface = eglCreateWindowSurface(display, eglConfig, nwin, 0);if (winSurface == EGL_NO_SURFACE) {
LOGD("eglCreateWindowSurface failed");return;
}//3 创建关联上下文const EGLint ctxAttr[] = {
EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE
};//创建egl关联OpenGl的上下文环境 EGLContext 实例。EGL_NO_CONTEXT表示不需要多个设备共享上下文。Context (EGLContext) 存储 OpenGL ES绘图的一些状态信息。上面的代码只是egl和设备窗口的关联,这里是和OpenGl的关联
EGLContext context = eglCreateContext(display, eglConfig, EGL_NO_CONTEXT, ctxAttr);if (context == EGL_NO_CONTEXT) {
LOGD("eglCreateContext failed");return;
}//将EGLContext和opengl真正关联起来。绑定该线程的显示设备及上下文//两个surface一个读一个写。if (EGL_TRUE != eglMakeCurrent(display, winSurface, winSurface, context)) {
LOGD("eglMakeCurrent failed");return;
}
创建初始化EGL,接下来就是真正的OpenGL绘制代码。先看下着色器代码。看着色器代码之前,先了解下GLSL一些基础:常见的变量类型:
attritude:一般用于各个顶点各不相同的量。如顶点位置、纹理坐标、法向量、颜色等等。
uniform:一般用于对于物体中所有顶点或者所有的片段都相同的量。比如光源位置、统一变换矩阵、颜色等。
varying:表示易变量,一般用于顶点着色器传递到片段着色器的量。
vec2:包含了2个浮点数的向量
vec3:包含了3个浮点数的向量
vec4:包含了4个浮点数的向量
sampler1D:1D纹理着色器
sampler2D:2D纹理着色器
sampler3D:3D纹理着色器
//顶点着色器,每个顶点执行一次,可以并行执行
#define GET_STR(x) #xstatic const char *vertexShader = GET_STR(
attribute vec4 aPosition;//输入的顶点坐标,会在程序指定将数据输入到该字段
attribute vec2 aTextCoord;//输入的纹理坐标,会在程序指定将数据输入到该字段
varying vec2 vTextCoord;//输出的纹理坐标,输入到片段着色器void main() {//这里其实是将上下翻转过来(因为安卓图片会自动上下翻转,所以转回来。也可以在顶点坐标中就上下翻转)
vTextCoord = vec2(aTextCoord.x, 1.0 - aTextCoord.y);//直接把传入的坐标值作为传入渲染管线。gl_Position是OpenGL内置的
gl_Position = aPosition;
}
);
这里逻辑很简单。使用两个attribute变量,一个接受顶点坐标,一个接收纹理坐标,这里以标准的OpenGL的纹理坐标为标准,即和安卓平台是上下翻转关系的(在本文OpenGL纹理绘制一节有说到),所以对传进来的纹理坐标在0.0~1。0之间进行上下翻转,再赋值给varying类型变量vTextCoord,vTextCoord将通过渲染管线传给片段着色器。最后将传进来的顶点坐标赋值给gl_Position ,gl_Position 是OpenGL内置的表示顶点坐标的变量。gl_Position 被赋值之后,将通过渲染管线传给后面的阶段,在图元装配的时候,将顶点连接起来。在光栅化图元的时候,将两个顶点之间的线段分解成大量的小片段,varying数据在这个过程中计算生成,记录在每个片段中,之后传递给片段着色器。
然后编写片段着色器代码:
//图元被光栅化为多少片段,就被调用多少次static const char *fragYUV420P = GET_STR(
precision mediump float;//接收从顶点着色器、光栅化处理传来的纹理坐标数据
varying vec2 vTextCoord;//输入的yuv三个纹理
uniform sampler2D yTexture;//y分量纹理
uniform sampler2D uTexture;//u分量纹理
uniform sampler2D vTexture;//v分量纹理void main() {//存放采样之后的yuv数据
vec3 yuv;//存放yuv数据转化后的rgb数据
vec3 rgb;//对yuv各个分量对应vTextCoord的像素进行采样。这里texture2D得到的结果是一个vec4变量,它的r、g、b、a的值都为采样到的那个分量的值//将采样到的y、u、v分量的数据分别保存在vec3 yuv的r、g、b(或者x、y、z)分量
yuv.r = texture2D(yTexture, vTextCoord).g;
yuv.g = texture2D(uTexture, vTextCoord).g - 0.5;
yuv.b = texture2D(vTexture, vTextCoord).g - 0.5;//这里必须把yuv转化为RGB
rgb = mat3(1.0, 1.0, 1.0,0.0, -0.39465, 2.03211,1.13983, -0.5806, 0.0
) * yuv;//gl_FragColor是OpenGL内置的,将rgb数据赋值给gl_FragColor,传到渲染管线的下一阶段 ,gl_FragColor 表示正在呈现的像素的 R、G、B、A 值。
gl_FragColor = vec4(rgb, 1.0);
}
);
这里要将yuv三个分量分别用三层纹理来渲染,然后将多层纹理混合一起显示。代码中三个sampler2D类型变量就是纹理图片,需要从外部程序传入。然后通过texture2D方法采样得到对应纹理坐标位置的颜色数据,将yuv三个分量的采样值放入vec3 类型变量yuv的三个分量中,因为OpenGL只支持RGB的渲染,所以需要将vec3类型的 yuv通过公式转为一个rgb 的vec3 类型变量。最后将rgb 变量构建一个vec4变量,作为最终颜色赋值给gl_FragColor 。
着色器代码定义完,接下来就是渲染逻辑部分。
首先是将前面的定义的着色器加载、编译以及创建、链接、激活着色器程序:
GLint vsh = initShader(vertexShader, GL_VERTEX_SHADER);
GLint fsh = initShader(fragYUV420P, GL_FRAGMENT_SHADER);//创建渲染程序
GLint program = glCreateProgram();if (program == 0) {
LOGD("glCreateProgram failed");return;
}//向渲染程序中加入着色器
glAttachShader(program, vsh);
glAttachShader(program, fsh);//链接程序
glLinkProgram(program);
GLint status = 0;
glGetProgramiv(program, GL_LINK_STATUS, &status);if (status == 0) {
LOGD("glLinkProgram failed");return;
}
LOGD("glLinkProgram success");//激活渲染程序
glUseProgram(program);
其中initShader函数:
GLint initShader(const char *source, GLint type) {//创建shader
GLint sh = glCreateShader(type);if (sh == 0) {
LOGD("glCreateShader %d failed", type);return 0;
}//加载shader
glShaderSource(sh,1,//shader数量
&source,0);//代码长度,传0则读到字符串结尾//编译shader
glCompileShader(sh);
GLint status;
glGetShaderiv(sh, GL_COMPILE_STATUS, &status);if (status == 0) {
LOGD("glCompileShader %d failed", type);
LOGD("source %s", source);return 0;
}
LOGD("glCompileShader %d success", type);return sh;
}
传入顶点坐标数组给顶点着色器:
//加入三维顶点数据。这里就是整个屏幕的矩形。static float ver[] = {1.0f, -1.0f, 0.0f,
-1.0f, -1.0f, 0.0f,1.0f, 1.0f, 0.0f,
-1.0f, 1.0f, 0.0f
};//获取顶点着色器的aPosition属性引用
GLuint apos = static_cast(glGetAttribLocation(program, "aPosition"));
glEnableVertexAttribArray(apos);//将顶点坐标传入顶点着色器的aPosition属性//各个参数意义:apos:顶点着色器中aPosition变量的引用。3表示数组中三个数字表示一个顶点。GL_FLOAT表示数据类型是浮点数。//GL_FALSE表示不进行归一化。0表示stride(跨距),在数组表示多种属性的时候使用到,这里因为这有一个属性,设置为0即可。ver表示所传入的顶点数组地址
glVertexAttribPointer(apos, 3, GL_FLOAT, GL_FALSE, 0, ver);
传入纹理坐标数组给顶点着色器:
//加入纹理坐标数据,这里是整个纹理。static float fragment[] = {1.0f, 0.0f,0.0f, 0.0f,1.0f, 1.0f,0.0f, 1.0f
};将纹理坐标数组传入顶点着色器的aTextCoord属性
GLuint aTex = static_cast(glGetAttribLocation(program, "aTextCoord"));
glEnableVertexAttribArray(aTex);//各个参数意义:aTex :顶点着色器中aTextCoord变量的引用。2表示数组中三个数字表示一个顶点。GL_FLOAT表示数据类型是浮点数。//GL_FALSE表示不进行归一化。表示stride(跨距),在数组表示多种属性的时候使用到,这里因为这有一个属性,设置为0即可。fragment表示所传入的顶点数组地址
glVertexAttribPointer(aTex, 2, GL_FLOAT, GL_FALSE, 0, fragment);
如果能把传入顶点坐标数组给顶点着色器理解,这一段就没有什么难度了。
接着是纹理对象的处理:
这里要讲一下几个概念:纹理对象、纹理目标、纹理单元。
纹理对象是我们创建的用来存储纹理的显存,在实际使用过程中使用的是创建后返回的纹理ID。纹理目标可以简单理解为纹理的类型,比如指定是渲染2D还是3D等。纹理单元:纹理的操作容器,有GL_TEXTURE0、GL_TEXTURE1、GL_TEXTURE2等,纹理单元的数量是有限的,最多16个。所以在最多只能同时操作16个纹理。可以简单理解为第几层纹理。
创建纹理对象:
//指定纹理变量在哪一层纹理单元渲染
glUniform1i(glGetUniformLocation(program, "yTexture"), GL_TEXTURE0);
glUniform1i(glGetUniformLocation(program, "uTexture"), GL_TEXTURE1);
glUniform1i(glGetUniformLocation(program, "vTexture"), GL_TEXTURE2);//纹理ID
GLuint texts[3] = {0};//创建3个纹理对象,并且得到各自的纹理ID。之后对纹理的操作就可以通过该纹理ID进行。
glGenTextures(3, texts);
将纹理对象和相应的纹理目标进行绑定:
//yuv视频宽高int width = 640;int height = 360;//通过 glBindTexture 函数将纹理目标和以texts[0]为ID的纹理对象绑定后,对纹理目标所进行的操作都反映到该纹理对象上
glBindTexture(GL_TEXTURE_2D, texts[0]);//缩小的过滤器(关于过滤详细可见 [纹理](https://learnopengl-cn.github.io/01%20Getting%20started/06%20Textures/))
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);//放大的过滤器
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);//设置纹理的格式和大小// 当前绑定的纹理对象就会被渲染上纹理
glTexImage2D(GL_TEXTURE_2D,0,//指定要Mipmap的等级
GL_LUMINANCE,//gpu内部格式,告诉OpenGL内部用什么格式存储和使用这个纹理数据。 亮度,灰度图(这里就是只取一个亮度的颜色通道的意思,因这里只取yuv其中一个分量)
width,//加载的纹理宽度。最好为2的次幂
height,//加载的纹理高度。最好为2的次幂0,//纹理边框
GL_LUMINANCE,//数据的像素格式 亮度,灰度图
GL_UNSIGNED_BYTE,//一个像素点存储的数据类型
NULL //纹理的数据(先不传,等后面每一帧刷新的时候传)
);
这里要注意视频的宽高一定设置正确,不然渲染的数据就都是错误的。
这里要说明下glTexImage2D第三个参数,告诉OpenGL内部用什么格式存储和使用这个纹理数据(一个像素包含多少个颜色成分,是否压缩)。常用的常量如下:
这里yuv三个分量的代码都是一样的,只是传入的宽高不同,对于u和v来说,宽高各位视频宽高的二分之一:
//设置纹理的格式和大小
glTexImage2D(GL_TEXTURE_2D,0,//细节基本 默认0
GL_LUMINANCE,//gpu内部格式 亮度,灰度图(这里就是只取一个颜色通道的意思)
width / 2,
height / 2,//v数据数量为屏幕的4分之10,//边框
GL_LUMINANCE,//数据的像素格式 亮度,灰度图
GL_UNSIGNED_BYTE,//像素点存储的数据类型
NULL //纹理的数据(先不传)
);
为什么是width / 2,height / 2呢?还记得上文说过的yuv420p的采样和存储格式么?YUV420P是2:1水平下采样,2:1垂直下采样,所以y分量数量等于视频宽乘以高,u和v分量都是视频宽/2乘以高/2。
从视频文件中读取yuv数据到内存中:
unsigned char *buf[3] = {0};
buf[0] = new unsigned char[width * height];//y
buf[1] = new unsigned char[width * height / 4];//u
buf[2] = new unsigned char[width * height / 4];//v//循环读出每一帧for (int i = 0; i 10000; ++i) {//读一帧yuv420p数据if (feof(fp) == 0) {//读取y数据
fread(buf[0], 1, width * height, fp);//读取u数据
fread(buf[1], 1, width * height / 4, fp);//读取v数据
fread(buf[2], 1, width * height / 4, fp);
}
还是回顾刚才敲黑板的地方,由图可得yuv420p中,是先存储视频宽高个y元素,再存储视频宽乘以高/4个u,再存储视频宽乘以高/4个v,所以for循环中读取一帧才按照yuv的顺序和数量依次读到内存的数组中。
在读出一帧后,更新数据到纹理对象上。buf[0]即y分量的数据渲染到纹理上:
//激活第一层纹理,绑定到创建的纹理
glActiveTexture(GL_TEXTURE0);//绑定y对应的纹理
glBindTexture(GL_TEXTURE_2D, texts[0]);//替换纹理,比重新使用glTexImage2D性能高多
glTexSubImage2D(GL_TEXTURE_2D, 0,0, 0,//相对原来的纹理的offset
width, height,//加载的纹理宽度、高度。最好为2的次幂
GL_LUMINANCE, GL_UNSIGNED_BYTE,
buf[0]);
u和v也是一样,只是宽高换为width / 2, height / 2。最后将画面显示出来:
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);//窗口显示,交换双缓冲区
eglSwapBuffers(display, winSurface);
如此循环,就将每一帧渲染出来,也就播放了yuv视频。这里我使用ffmpeg命令将《龙猫》中截取10秒的视频转化为yuv,录屏的gif不知为何总是上传不了,所以这里只上传了一张截图 = = 。
虽然只是10秒的视频,但是已经超过github的最大上传量,所以视频没有上传。各位如果需要可以自己用ffmpeg命令转换任何一个格式支持视频文件为yuv420p格式来运行。
接触音视频开发领域时间不长,如有错误疏漏,请各位指正~项目地址:
https://github.com/yishuinanfeng/YuvVideoPlayerDemo推荐阅读: 使用HashMap不如使用SparseArray? 网络请求只会用Retrofit?外国人已经在用Graphql了 给TextView文本加标签,小功能大秘密 欢迎关注我的公众号 学习技术或投稿 长按上图,识别图中二维码即可关注