渲染YUV数据的OpenGL实现
本文已参与[新人创作礼]活动,一起开启掘金创作之路
本文主要介绍使用OpenGL ES来渲染I420(YUV420P)
, NV12(YUV420SP)
的方法。
OpenGL ES是OpenGL的精简版本,主要针对于手机、游戏主机等嵌入式设备,它提供了一套设备图形硬件的软件接口,通过直接操作图形硬件,使我们能够高效地绘制图形。OpenGL在iOS架构中属于媒体层,与quartz(core graphics)类似,是相对底层的技术,可以控制每一帧的图形绘制。由于图形渲染是通过图形硬件(GPU)来完成的,相对于使用CPU,能够获得更高的帧率同时不会因为负载过大而造成卡顿。
编辑
OpenGL处于绘制接口的底层
创建GLView
我们需要创建一个用来展示OpenGL绘制内容的View,只需要将UIView的根图层(underlying layer)替换成CAEAGLLayer实例即可。通过覆盖UIView
的类方法+(Class)layerClass
,可以实现这一点,CAEAGLLayer默认是透明的,这会影响性能,所以将它设为不透明。
+ (class)layerClass {
return [CAEAGLLayer class];
}
- (void)setupLayer {
_eaglLayer = (CAEAGLLayer*) self.layer;
_eaglLayer.opaque = YES;
}
创建EAGLContext
EAGLContext对象管理OpenGL绘制所需要的所有信息,和Quartz 2D所使用的CGContext类似。
- (void)setupContext {
//创建一个OpenGLES 2.0接口的context
EAGLRenderingAPI api = kEAGLRenderingAPIOpenGLES2;
_context = [[EAGLContext alloc] initWithAPI:api];
if (!_context) {
NSLog(@"Failed to initialize OpenGLES 2.0 context");
exit(1);
}
//将其设置为current context
if (![EAGLContext setCurrentContext:_context]) {
NSLog(@"Failed to set current OpenGL context");
exit(1);
}
}
创建render buffer
render buffer用来存储将要绘制到屏幕上图像。OpenGL中的对象都需要创建、绑定,并且都是ID引用的。
- (void)setupRenderBuffer {
//创建一个render buffer对象,并绑定到GL_RENDERBUFFER目标上
glGenRenderbuffers(1, &_renderBuffer);
glBindRenderbuffer(GL_RENDERBUFFER, _renderBuffer);
//为render buffer分配存储空间
[_context renderbufferStorage:GL_RENDERBUFFER fromDrawable:_eaglLayer];
}
创建frame buffer
一个frame buffer对象包括render buffer, depth buffer, stencil buffer等,拥有OpenGL绘制时需要的信息。
- (void)setupFrameBuffer {
GLuint framebuffer;
glGenFramebuffers(1, &framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
//将之前创建的render buffer附着到frame buffer作为其logical buffer
//GL_COLOR_ATTACHMENT0指定第一个颜色缓冲区附着点
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
GL_RENDERBUFFER, _renderBuffer);
}
glFramebufferRenderbuffer
调用后,render buffer通过GL_COLOR_ATTACHMENT0
引用使用render buffer
渲染
- (void)render {
//设置用来清除屏幕的颜色,类似于quartz中设置CGcontext画笔的颜色
glClearColor(0, 0, 0, 1.0);
//执行清除操作,设置render buffer中的像素颜色为上一步指定的颜色
glClear(GL_COLOR_BUFFER_BIT);
//渲染render buffer中的图像到GLView的CAEAGLLayer
[_context presentRenderbuffer:GL_RENDERBUFFER];
}
shader(着色器)
shader是上是在GPU上执行的程序,保存在.glsl文件中或以字符串形式写在OpenGL代码里,使用GLSL(OpenGL shading language)语言编写,shader在运行时编译,链接,最终在GPU上执行采样操作。
OpenGL中有两种shader:
- vertex shader(顶点着色器):vertex shader在每个顶点上都执行执行一次,通过不同世界的坐标系转化定位顶点的最终位置。它可以数据给fragment shader,如纹理坐标、顶点坐标,变换矩阵等。
- fragment shader(片段着色器):fragment shader在每个像素上都会执行一次,通过插值确定像素的最终显示颜色。
创建shader
以下是两个简单的shader,用来说明GLSL的语法特点。
vertex shader:
//attribute 关键字用来描述传入shader的变量
attribute vec4 vertexPosition; // 需要从外部获取的4分量vector
attribute vec4 pixelColor;
//varying 关键字用来描述从vertex shader传递给fragment shader的变量
//精度修饰符分为三种:highp, mediump, lowp
varying mediump vec4 finalPixelColor; //mediumP修饰代表中等精度,提高效率。
void main(void) {
finalPixelColor = pixelColor; // 将pixelColor的值通过finalPixelColor传递给fragment shader
gl_Position = vertexPosition; // gl_Position是vertex shader的内建变量,gl_Position中的顶点值最终输出到渲染管线中
}
fragment shader:
varying mediump vec4 finalPixelColor;
void main(void) {
gl_FragColor = finalPixelColor; // gl_FragColor是fragment shader的内建变量,gl_FragColor中的像素值最终输出到渲染管线中
}
}
使用shader
shader在运行时完成编译、链接,是在GPU上执行的小程序,以下是shader编译、链接的过程,为了阅读方便,省略了调试异常情况的判断和调试log输出。
//编译shader函数
- (GLuint)compileShader:(NSString*)shaderName withType:(GLenum)shaderType {
// 读取shader文件的内容为字符串
NSString* shaderPath = [[NSBundle mainBundle] pathForResource:shaderName
ofType:@"glsl"];
NSError* error;
NSString* shaderString = [NSString stringWithContentsOfFile:shaderPath encoding:NSUTF8StringEncoding error:&error];
if (!shaderString) {
NSLog(@"Error loading shader: %@", error.localizedDescription);
exit(1);
}
// 创建shader对象,返回其引用
GLuint shaderHandle = glCreateShader(shaderType);
// 获取C字符串,作为源代码传给OpenGL
const char * shaderStringUTF8 = [shaderString UTF8String];
int shaderStringLength = [shaderString length];
glShaderSource(shaderHandle, 1, &shaderStringUTF8, &shaderStringLength);
// 运行时编译shader
glCompileShader(shaderHandle);
return shaderHandle;
}
//编译、链接shader
-(void)configuerShader{
// 创建并编译shader
GLuint vertexShader = [self compileShader:@"vertexShader" withType:GL_VERTEX_SHADER];
GLuint fragmentShader = [self compileShader:@"fragmentShader" withType:GL_FRAGMENT_SHADER];
//创建一个程序对象,返回其引用
_programHandle = glCreateProgram();
//将两个shader绑定到程序对象, 不需要时可以使用glDetachShader解绑
glAttachShader(programHandle, vertexShader);
glAttachShader(programHandle, fragmentShader);
//链接两个shader
glLinkProgram(_programHandle);
//选择创建的程序对象为当前使用的程序,类似setCurrentContext, 不需要时使用glDeleteProgram删除
glUseProgram(_programHandle);
}
-(void)configuerSlot{
//获取shader中attribute变量的引用
_vertexPosition = glGetAttribLocation(programHandle, "vertexPosition");
_pixelColor = glGetAttribLocation(programHandle, "pixelColor");
//启用attribute变量,使其对GPU可见,默认为关闭
glEnableVertexAttribArray(_vertexPosition);
glEnableVertexAttribArray(_pixelColor);
}
-(void)initOpenGl{
[self configuerShader];
[self coniigureSlot];
}
使用OpenGL绘制一个简单的矩形
以上内容介绍了OpenGL的基本数据结构,现在先来绘制一个简单的矩形
初始化
现在需要给OpenGL提供attribute变量值与顶点数据。顶点数据用来提供绘制时的几何信息。OpenGL中只能绘制三角形,三角形保证了其内部像素都在同一个平面。要绘制复杂的几何图形,可以用三角形组合的方式实现。
顶点数据使用VBO(vertex buffer object)来传递给GPU。
初始化VBO
OpenGL需要有两种VBO来确定几何图形,vertex VBO提供顶点本身,index VBO提供三角形所使用的顶点的index序列。这样保证了显存中存储的顶点数据是唯一的,不会浪费资源。VBO中存储着CPU传给GPU的数据,存储在显存里,在执行大量重复的绘制操作时,可以提高效率。
初始化attribute变量
之前创建的shader中,有两个attribute变量,需要使用glVertexAttribPointer
输入给shader。
//我们需要在一个矩形中绘制图像,需要两个三角形模拟,所以需要四个顶点,索引数组说明了两个三角形顶点组成。
//默认情况下,OpenGL 的Viewport左下角顶点为(-1,-1),右上角顶点为(1,1)。
const float vertices[] = {
1, -1, 0,//index 0
1, 1, 0,//index 1
-1, 1, 0,//index 2
-1, -1, 0 //index 3
}
const GLubyte Indices[] = {
0, 1, 2,
2, 3, 0
};
- (void)setupVBOs {
//顶点VBO
GLuint vertexBuffer;
glGenBuffers(1, &vertexBuffer);
glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
//将顶点坐标写入顶点VBO
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), Vertices, GL_STATIC_DRAW);
//索引VBO
GLuint indexBuffer;
glGenBuffers(1, &indexBuffer);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
//将顶点索引数据写入索引VBO
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(Indices), Indices, GL_STATIC_DRAW);
}
-(void)feedAttributeSlot{
//由于使用了VBO,所以最后一个参数传数据在VBO中的偏移量,这点需要注意
glVertexAttribPointer(_vertexPosition, 3, GL_FLOAT, GL_FALSE, sizeof(float)*3, 0);
//没有使用VBO,直接传指针给函数,
float blueColor[] = {0, 0, 1, 0};
glVertexAttribPointer(_pixelColor, 4, GL_FLOAT, GL_FALSE, 0, blueColor);
}
注意此时写入VBO的只是一些二进制的数据,需要在读取数据是,给出数据类型才能正确读取。
绘制
- (void)render {
//绘制黑色背景
glClearColor(0, 0, 0, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
//创建一个OpenGL绘制的窗口
glViewport(0, 0, self.frame.size.width, self.frame.size.height);
//使用顶点索引,绘制图形。调用函数后,vertex shader会在每个顶点执行一遍,确定顶点信息。fragment shader会在每个像素执行一遍,确定像素颜色。
//在使用VBO的情况下,最后一个参数传0
glDrawElements(GL_TRIANGLES, sizeof(Indices)/sizeof(Indices[0]),
GL_UNSIGNED_BYTE, 0);
//EACAGLContext 渲染OpenGL绘制好的图像到EACAGLLayer
[_context presentRenderbuffer:GL_RENDERBUFFER];
}
由于shader中pixelColor变量类型是varying类型,在处理未知值是,会自动插值,默认为线性插值。如果对shader的GLSL语法不熟悉,可以看这篇文章。
所以,当顶点之间的颜色不同是,fragment shader在处理图形内部的像素后会返回一个根据顶点插值后的数值,整个图形是渐变色的。
这里因为四个顶点都设置为了蓝色,所以绘制出来是一个蓝色的矩形。
(添加图)
使用OpenGL绘制YUV数据
以上内容简单介绍了如何使用OpenGL绘制,现在重点如何使用OpenGL绘制YUV数据。
YUV是一种颜色编码格式,常用的格式有YUV444
,YUV422P
,YUV420P
,YUV420SP
等。
本文主要研究YUV420P
的I420
与YUV420SP
的NV12
。
纹理
我们需要将YUV数据纹理的方式加载到OpenGL,再将纹理贴到之前创建矩形上,完成绘制。
将每个顶点赋予一个纹理坐标,OpenGL会根据纹理坐标插值得到图形内部的像素值。OpenGL的纹理坐标系是归一化的,取值范围是0 - 1,左下角是原点。
编辑
三角形贴上纹理需要的纹理坐标
纹理目标、纹理对象、纹理单元
- 纹理目标是显卡的软件接口中定义的句柄,指向要进行当前操作的显存。
- 纹理对象是我们创建的用来存储纹理的显存,在实际使用过程中使用的是创建后返回的ID。
- 纹理单元是显卡中所有的可用于在shader中进行纹理采样的显存,数量与显卡类型相关,至少16个。在激活某个纹理单元后,纹理目标就该纹理单元,默认激活的是GL_TEXTURE0。
可以这么想象,纹理目标是转轮手枪正对弹膛的单孔,纹理对象就是子弹,纹理单元是手枪的六个弹孔。下面用代码说明它们之间的关系。
//创建一个纹理对象数组,数组里是纹理对象的ID
GLuint texture[3];
//创建纹理对象,第一个参数是要创建的数量,第二个参数是数组的基址
glGenTextures(3, &texture);
//激活GL_TEXTURE0这个纹理单元,用于之后的纹理采样
glActiveTexture(GL_TEXTURE0);
//绑定纹理对象texture[0]到纹理目标GL_TEXTURE_2D,接下来对纹理目标的操作都发生在此对象上
glBindTexture(GL_TEXTURE_2D, texture[0]);
//创建图像,采样工作在GL_TEXTURE0中完成,图像数据存储在GL_TEXTURE_2D绑定的对象,即texture[0]中。
glTexImage(GL_TEXTURE_2D, ...);
//解除绑定,此时再对GL_TEXTURE_2D不会影响到texture[0],texture[0]的内存不会回收。
glBindTexture(GL_TEXTURE_2D, 0);
//可以不断创建新的纹理对象,直到显存耗净
修改shader
之前创建的简单shader现在要修改代码,实现对YUV数据的绘制。如果对GLSL语法与YUV不熟悉,可以看OpenGL的着色语言:GLSL和YUV颜色编码解析。
//vertex shader
ttribute vec4 position;
attribute mediump vec2 textureCoordinate;//要获取的纹理坐标
varying mediump vec2 coordinate;//传递给fragm shader的纹理坐标,会自动插值
void main(void) {
gl_Position = vertexPosition;
coordinate = textureCoordinate;
}
//fragment shader
precision mediump float;
uniform sampler2D SamplerY;//sample2D的常量,用来获取I420数据的Y平面数据
uniform sampler2D SamplerU;//U平面
uniform sampler2D SamplerV;//V平面
uniform sampler2D SamplerNV12_Y;//NV12数据的Y平面
uniform sampler2D SamplerNV12_UV;//NV12数据的UV平面
varying highp vec2 coordinate;//纹理坐标
uniform int yuvType;//0 代表 I420, 1 代表 NV12
//用来做YUV --> RGB 的变换矩阵
const vec3 delyuv = vec3(-0.0/255.0,-128.0/255.0,-128.0/255.0);
const vec3 matYUVRGB1 = vec3(1.0,0.0,1.402);
const vec3 matYUVRGB2 = vec3(1.0,-0.344,-0.714);
const vec3 matYUVRGB3 = vec3(1.0,1.772,0.0);
void main()
{
vec3 CurResult;
highp vec3 yuv;
if (yuvType == 0){
yuv.x = texture2D(SamplerY, coordinate).r;//因为是YUV的一个平面,所以采样后的r,g,b,a这四个参数的数值是一样的
yuv.y = texture2D(SamplerU, coordinate).r;
yuv.z = texture2D(SamplerV, coordinate).r;
}
else{
yuv.x = texture2D(SamplerY, coordinate).r;
yuv.y = texture2D(SamplerUV, coordinate).r;//因为NV12是2平面的,对于UV平面,在加载纹理时,会指定格式,让U值存在r,g,b中,V值存在a中。
yuv.z = texture2D(SamplerUV, coordinate).a;//这里会在下面解释
}
yuv += delyuv;//读取值得范围是0-255,读取时要-128回归原值
//用数量积来模拟矩阵变换,转换成RGB值
CurResult.x = dot(yuv,matYUVRGB1);
CurResult.y = dot(yuv,matYUVRGB2);
CurResult.z = dot(yuv,matYUVRGB3);
//输出像素值给光栅器
gl_FragColor = vec4(CurResult.rgb, 1);
}
加载YUV数据到纹理对象
现在有了可以处理YUV数据的shader,我们需要加载YUV数据,来让OpenGL完成绘制。
//创建纹理对象,需要3个纹理对象来获取不同平面的数据
-(void)setupTexture{
_planarTextureHandles = (GLuint *)malloc(3*sizeof(GLuint));
glGenTextures(3, _planarTextureHandles);
}
-(void)feedTextureWithImageData:(Byte*)imageData imageSize:(CGSize)imageSize type:(NSInteger)type{
//根据YUV编码的特点,获得不同平面的基址
Byte * yPlane = imageData;
Byte * uPlane = imageData + imageSize.width*imageSize.height;
Byte * vPlane = imageData + imageSize.width*imageSize.height * 5 / 4;
if (type == 0) {
[self textureYUV:yPlane widthType:imageSize.width heightType:imageSize.height index:0];
[self textureYUV:uPlane widthType:imageSize.width/2 heightType:imageSize.height/2 index:1];
[self textureYUV:vPlane widthType:imageSize.width/2 heightType:imageSize.height/2 index:2];
}else{
[self textureYUV:yPlane widthType:imageSize.width heightType:imageSize.height index:0];
[self textureNV12:uPlane widthType:imageSize.width/2 heightType:imageSize.height/2 index:1];
}
}
- (void) textureYUV: (Byte*)imageData widthType: (int) width heightType: (int) height index: (int) index
{
//将纹理对象绑定到纹理目标
glBindTexture(GL_TEXTURE_2D, _planarTextureHandles[index]);
//设置放大和缩小时,纹理的过滤选项为:线性过滤
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
//设置纹理X,Y轴的纹理环绕选项为:边缘像素延伸
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
//加载图像数据到纹理,GL_LUMINANCE指明了图像数据的像素格式为只有亮度,虽然第三个和第七个参数都使用了GL_LUMINANCE,
//但意义是不一样的,前者指明了纹理对象的颜色分量成分,后者指明了图像数据的像素格式
//获得纹理对象后,其每个像素的r,g,b,a值都为相同,为加载图像的像素亮度,在这里就是YUV某一平面的分量值
glTexImage2D( GL_TEXTURE_2D, 0, GL_LUMINANCE, width, height, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, imageData );
//解绑
glBindTexture(GL_TEXTURE_2D, 0);
}
CADisplayLink定时绘制
现在已经能够将YUV数据加载到纹理对象了,下一步来改造render方法,将其绘制到屏幕上。可以用CADisplayLink定时调用render方法,可以根据屏幕刷新频率来控制YUV视频流的帧率。
- (void)render {
//绘制黑色背景
glClearColor(0, 0, 0, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
//获取平面的scale
CGFloat scale = [[UIScreen mainScreen] scale];
CGFloat width = _frame.size.width*scale;
CGFloat height = _frame.size.height*scale;
//创建一个OpenGL绘制的窗口
glViewport(0, 0,width,height);
[self drawTexture];
//EACAGLContext 渲染OpenGL绘制好的图像到EACAGLLayer
[_context presentRenderbuffer:GL_RENDERBUFFER];
}
//fragment shader的sample数组
GLint sampleHandle[3];
//绘制纹理
- (void) drawTexture{
//传纹理坐标给fragment shader
glVertexAttribPointer([AVGLShareInstance shareInstance].texCoordAttributeLocation, 2, GL_FLOAT, GL_FALSE,
sizeof(Vertex), (void*)offsetof(Vertex, TexCoord));
glEnableVertexAttribArray([AVGLShareInstance shareInstance].texCoordAttributeLocation);
//传纹理的像素格式给fragment shader
GLint yuvType = glGetUniformLocation(_programHandle, "yuvType");
glUniform1i([AVGLShareInstance shareInstance].drawTypeUniform, yuvType);
//type: 0是I420, 1是NV12
int planarCount = 0;
if (type == 0) {
planarCount = 3;//I420有3个平面
sampleHandle[1] = glGetUniformLocation(_programHandle, "samplerY");
sampleHandle[2] = glGetUniformLocation(_programHandle, "samplerU");
sampleHandle[3] = glGetUniformLocation(_programHandle, "samplerV");
}else{
planarCount = 2;//NV12有两个平面
sampleHandle[1] = glGetUniformLocation(_programHandle, "SamplerNV12_Y");
sampleHandle[2] = glGetUniformLocation(_programHandle, "SamplerNV12_UV");
}
for (int i=0; i<planarCount; i++){
glActiveTexture(GL_TEXTURE0+i);
glBindTexture(GL_TEXTURE_2D, _planarTextureHandles[i]);
glUniform1i(sampleHandle[i], i);
}
//绘制函数,使用三角形作为图元构造要绘制的几何图形,由于顶点的indexs使用了VBO,所以最后一个参数传0
//调用这个函数后,vertex shader先在每个顶点执行一次,之后fragment shader在每个像素执行一次,绘制后的图像存储在render buffer中。
glDrawElements(GL_TRIANGLES, 6,GL_UNSIGNED_BYTE, 0);
}
可以想象的应用场景
使用OpenGL绘制视频,是实现简单AR最简单的方式,也可以根据业务来对视频播放做进一步的个性定制,比如动态打码,贴纸等。
再附加一篇android:
1、渲染YUV数据原理
1.1、为什么用OpenGL来处理YUVP颜色格式视频?
OpenGL中是不能直接渲染YUV数据的,但是我们可以用3个纹理来
分别获取Y、U和V的值,然后根据公式:
r = y + 1.403 * v;
g = y - 0.344 * u - 0.714 * v;
b = y + 1.770 * u;
转为rgb颜色格式显示出来。这个转换过程是在GPU中完成的,计算
效率比在CPU中计算高很多倍!
1.2、Shader编写
vertex_shader.glsl
attribute vec4 av_Position;
attribute vec2 af_Position;
varying vec2 v_texPosition;
void main() {
v_texPosition = af_Position;
gl_Position = av_Position;
}
注: attribute 只能在vertex中使用
varying 用于vertex和fragment之间传递值
fragment_shader.glsl
precision mediump float;
varying vec2 v_texPosition;
uniform sampler2D sampler_y;
uniform sampler2D sampler_u;
uniform sampler2D sampler_v;
void main() {
float y,u,v;
y = texture2D(sampler_y,v_texPosition).r;
u = texture2D(sampler_u,v_texPosition).r- 0.5;
v = texture2D(sampler_v,v_texPosition).r- 0.5;
vec3 rgb;
rgb.r = y + 1.403 * v;
rgb.g = y - 0.344 * u - 0.714 * v;
rgb.b = y + 1.770 * u;
gl_FragColor = vec4(rgb,1);
}
注: uniform 用于在application中向vertex和fragment中传递值。
2、OpenGL ES渲染YUV数据
2.1、实现OpenGL ES
实现OpenGL ES的工具类-JfShaderUtil.java
package com.example.myplayer.opengl;
import android.content.Context;
import android.opengl.GLES20;
import com.example.myplayer.log.JfLog;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
public class JfShaderUtil {
public static String readRawTxt(Context context, int rawId) {
InputStream inputStream = context.getResources().openRawResource(rawId);
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
StringBuffer sb = new StringBuffer();
String line;
try
{
while((line = reader.readLine()) != null)
{
sb.append(line).append("\n");
}
reader.close();
}
catch (Exception e)
{
e.printStackTrace();
}
return sb.toString();
}
public static int loadShader(int shaderType, String source)
{
int shader = GLES20.glCreateShader(shaderType);
if(shader != 0)
{
GLES20.glShaderSource(shader, source);
GLES20.glCompileShader(shader);
int[] compile = new int[1];
GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compile, 0);
if(compile[0] != GLES20.GL_TRUE)
{
JfLog.e("shader compile error");
GLES20.glDeleteShader(shader);
shader = 0;
}
}
return shader;
}
public static int createProgram(String vertexSource, String fragmentSource)
{
int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);
if(vertexShader == 0)
{
return 0;
}
int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);
if(fragmentShader == 0)
{
return 0;
}
int program = GLES20.glCreateProgram();
if(program != 0)
{
GLES20.glAttachShader(program, vertexShader);
GLES20.glAttachShader(program, fragmentShader);
GLES20.glLinkProgram(program);
int[] linsStatus = new int[1];
GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linsStatus, 0);
if(linsStatus[0] != GLES20.GL_TRUE)
{
JfLog.e( "link program error");
GLES20.glDeleteProgram(program);
program = 0;
}
}
return program;
}
}
实现JfRender
package com.example.myplayer.opengl;
import android.content.Context;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import com.example.myplayer.R;
import com.example.myplayer.log.JfLog;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
public class JfRender implements GLSurfaceView.Renderer {
private Context context;
private final float[] vertexData ={//顶点坐标
-1f, -1f,
1f, -1f,
-1f, 1f,
1f, 1f
};
private final float[] textureData ={//纹理坐标
0f,1f,
1f, 1f,
0f, 0f,
1f, 0f
};
private FloatBuffer vertexBuffer;
private FloatBuffer textureBuffer;
private int program_yuv;
private int avPosition_yuv;
private int afPosition_yuv;
private int sampler_y;
private int sampler_u;
private int sampler_v;
private int[] textureId_yuv;
//渲染用
private int width_yuv;
private int height_yuv;
private ByteBuffer y;
private ByteBuffer u;
private ByteBuffer v;
public JfRender(Context context){
this.context = context;
//存储顶点坐标数据
vertexBuffer = ByteBuffer.allocateDirect(vertexData.length * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(vertexData);
vertexBuffer.position(0);
//存储纹理坐标
textureBuffer = ByteBuffer.allocateDirect(textureData.length * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(textureData);
textureBuffer.position(0);
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
initRenderYUV();
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
GLES20.glViewport(0, 0, width, height);
}
@Override
public void onDrawFrame(GL10 gl) {
//用黑色清屏
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
renderYUV();
}
/**
* 初始化
*/
private void initRenderYUV(){
String vertexSource = JfShaderUtil.readRawTxt(context, R.raw.vertex_shader);
String fragmentSource = JfShaderUtil.readRawTxt(context,R.raw.fragment_shader);
//创建一个渲染程序
program_yuv = JfShaderUtil.createProgram(vertexSource,fragmentSource);
//得到着色器中的属性
avPosition_yuv = GLES20.glGetAttribLocation(program_yuv,"av_Position");
afPosition_yuv = GLES20.glGetAttribLocation(program_yuv,"af_Position");
sampler_y = GLES20.glGetUniformLocation(program_yuv, "sampler_y");
sampler_u = GLES20.glGetUniformLocation(program_yuv, "sampler_u");
sampler_v = GLES20.glGetUniformLocation(program_yuv, "sampler_v");
//创建纹理
textureId_yuv = new int[3];
GLES20.glGenTextures(3, textureId_yuv, 0);
for(int i = 0; i < 3; i++)
{
//绑定纹理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId_yuv[i]);
//设置环绕和过滤方式
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
}
JfLog.d("initRenderYUV");
}
public void setYUVRenderData(int width, int height, byte[] y, byte[] u, byte[] v)
{
this.width_yuv = width;
this.height_yuv = height;
this.y = ByteBuffer.wrap(y);
this.u = ByteBuffer.wrap(u);
this.v = ByteBuffer.wrap(v);
}
/**
* 渲染
*/
private void renderYUV(){
JfLog.d("渲染中");
if(width_yuv > 0 && height_yuv > 0 && y != null && u != null && v != null){
GLES20.glUseProgram(program_yuv);//使用源程序
GLES20.glEnableVertexAttribArray(avPosition_yuv);//使顶点属性数组有效
GLES20.glVertexAttribPointer(avPosition_yuv, 2, GLES20.GL_FLOAT, false, 8, vertexBuffer);//为顶点属性赋值
GLES20.glEnableVertexAttribArray(afPosition_yuv);
GLES20.glVertexAttribPointer(afPosition_yuv, 2, GLES20.GL_FLOAT, false, 8, textureBuffer);
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);//激活纹理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId_yuv[0]);//绑定纹理
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE, width_yuv, height_yuv, 0, GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, y);//
GLES20.glActiveTexture(GLES20.GL_TEXTURE1);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId_yuv[1]);
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE, width_yuv / 2, height_yuv / 2, 0, GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, u);
GLES20.glActiveTexture(GLES20.GL_TEXTURE2);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId_yuv[2]);
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE, width_yuv / 2, height_yuv / 2, 0, GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, v);
GLES20.glUniform1i(sampler_y, 0);
GLES20.glUniform1i(sampler_u, 1);
GLES20.glUniform1i(sampler_v, 2);
y.clear();
u.clear();
v.clear();
y = null;
u = null;
v = null;
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
}
}
}
实现JfGLSurfaceView
package com.example.myplayer.opengl;
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.util.AttributeSet;
public class JfGLSurfaceView extends GLSurfaceView {
private JfRender jfRender;
public JfGLSurfaceView(Context context) {
this(context,null);
}
public JfGLSurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
setEGLContextClientVersion(2);
jfRender = new JfRender(context);
setRenderer(jfRender);
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);//requestRender()时不会重复渲染
}
public void setYUVData(int width,int height,byte[] y,byte[] u,byte[] v){
if (jfRender != null) {
jfRender.setYUVRenderData(width, height, y, u, v);
requestRender();
}
}
}
JfPlayer.java中调用
private JfGLSurfaceView jfGLSurfaceView;
public void setJfGLSurfaceView(JfGLSurfaceView jfGLSurfaceView) {
this.jfGLSurfaceView = jfGLSurfaceView;
}
/**
*
* @param width
* @param height
* @param y
* @param u
* @param v
*/
public void onCallRenderYUV(int width,int height,byte[] y,byte[] u,byte[] v){
JfLog.d("获取到视频的数据");
if (jfGLSurfaceView != null) {
jfGLSurfaceView.setYUVData(width, height, y, u, v);
}
}
MainActivity中设置WlGLSurfaceView:
private WlGLSurfaceView wlGLSurfaceView;
jfGLSurfaceView = (JfGLSurfaceView) findViewById(R.id.jfglsfv);
jfPlayer.setJfGLSurfaceView(jfGLSurfaceView);
问题
播放的时候会闪黑屏!
因为在JfRender.java中的private void renderYUV() 方法中有一个判断语句,
if(width_yuv > 0 && height_yuv > 0 && y != null && u != null && v != null){}
有可能视频中的某些AVFrame可能不满足这些条件,比如y、u、v数据某一个为空,
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
导致这句不执行,所以没有画出来矩形;
@Override
public void onDrawFrame(GL10 gl) {
//用黑色清屏
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
renderYUV();
}
在onDrawFrame回调方法中就用黑色清了一次屏,所以会闪黑屏!
如果改为
@Override
public void onDrawFrame(GL10 gl) {
//用黑色清屏
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
GLES20.glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
renderYUV();
}
会很明显的闪红屏!
解决方法:
不管有没有渲染都把矩形画出来!
@Override
public void onDrawFrame(GL10 gl) {
//用黑色清屏
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
renderYUV();
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
}
FFmpeg
推荐阅读
-
二、代码实现YUV420图像的水平拼接
-
解密Android Bitmap转I420的难题,附图文详解YUV420数据格式
-
如何实现neon优化的yuv420转rgb24汇编代码,iOS/Android可用的具体操作步骤
-
YUV420和YUV422格式的数据采集和存储方法
-
将YUV420图像转换为BGR图像的方法使用OpenCvSharp实现
-
重新渲染YUV420P格式的OpenGL
-
Qt QML VideoOutput 显示自定义的 YUV420P 数据流
-
使用OpenGL渲染YUV数据的方法
-
Opencv实现yuv420P格式的读取
-
两种方法将YUV420P文件转换为PNG图像格式的Python实现