Android 平台 不能直接渲染YUV格式视频,需要做YUV 转换 RGB,再去渲染,转换的方式有两种:
1、使用ffmpeg的转换api,将解码后的原数据转换为RGB格式数据,再渲染
2、使用opengl GPU 做转换 渲染
本文章主要介绍第二种方式基于NDK来做渲染(使用GPU做渲染,可以提高效率)
使用GPU 来做渲染 就需要 接触到 EGL 和 OPENGL 这两个词,这里摘录一些介绍和使用:
通俗上讲,OpenGL是一个操作GPU的API,它通过驱动向GPU发送相关指令,控制图形渲染管线状态机的运行状态。但OpenGL需要本地视窗系统进行交互,这就需要一个中间控制层,最好与平台无关。
EGL——因此被独立的设计出来,它作为OpenGL ES和本地窗口的桥梁。
EGL 是 OpenGL ES(嵌入式)和底层 Native 平台视窗系统之间的接口。EGL API 是独立于OpenGL ES各版本标准的独立API,其主要作用是为OpenGL指令创建 Context 、绘制目标Surface 、配置Framebuffer属性、Swap提交绘制结果等。此外,EGL为GPU厂商和OS窗口系统之间提供了一个标准配置接口。
一般来说,OpenGL ES 图形管线的状态被存储于 EGL 管理的一个Context中。而Frame Buffers 和其他绘制 Surfaces 通过 EGL API进行创建、管理和销毁。 EGL 同时也控制和提供了对设备显示和可能的设备渲染配置的访问。
EGL标准是C的,在Android系统Java层封装了相关API。
摘录自https://gameinstitute.qq.com/community/detail/103360
了解了EGL 和 OPENGL 就需要知道怎使用了,上面的连接已经 详细介绍了,API的调用流程,这里直接跳出代码
int openEglYuv(ANativeWindow *window) {
EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
if (display == EGL_NO_DISPLAY) {
LOGE("YUV : get Display failure")
return -1;
}
EGLint major;
EGLint minor;
EGLBoolean success = eglInitialize(display, &major, &minor);
if (!success) {
eglTerminate(display);
LOGE("YUV : init Display failure")
return -1;
}
global_Context.eglDisplay = display;
EGLConfig config;
EGLint configNum;
EGLint configSpec[] = {
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
// EGL_SURFACE_TYPE, EGL_PBUFFER_BIT,
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
EGL_STENCIL_SIZE, 8,
EGL_NONE
};
success = eglChooseConfig(display, configSpec, &config, 1, &configNum);
if (!success) {
eglTerminate(display);
LOGE("YUV : ChooseConfig failure")
return -1;
}
EGLSurface surface = eglCreateWindowSurface(display, config, window, 0);
if (surface == EGL_NO_SURFACE) {
LOGE("YUV : CreateWindowSurface failure")
eglTerminate(display);
return -1;
}
global_Context.eglSurface = surface;
EGLSurface psurface = eglCreatePbufferSurface(display, config, 0);
if (psurface == EGL_NO_SURFACE) {
LOGE("YUV : CreateWindowSurface failure")
eglTerminate(display);
return -1;
}
global_Context.eglPsurface = psurface;
EGLint ctxAttr[] = {
EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE
};
EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, ctxAttr);
if (context == EGL_NO_CONTEXT) {
eglDestroySurface(display, psurface);
eglTerminate(display);
LOGE("YUV : CreateContext failure")
return -1;
}
global_Context.eglContext = context;
return 0;
}
这一段代码只是做了相关的初始化工作,现在来编写 渲染相关的逻辑,这里写下步骤:
1、编写 顶点,片元 着色器 字符串。
2、加载 顶点,片元着色器
3、创建 渲染工程,管理着色器
4、提取着色器 参数,初始化 纹理(配置渲染逻辑)
5、渲染YUV
顶点 ,片元 着色器的语法,没怎么接触,不做详细分析,这里贴出代码:
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;
}
);
const char *fragYUV420P_ = GET_STR(
precision
mediump float;
varying
vec2 vTextCoord;
//输入的yuv三个纹理
uniform
sampler2D yTexture;//采样器
uniform
sampler2D uTexture;//采样器
uniform
sampler2D vTexture;//采样器
void main() {
vec3 yuv;
vec3 rgb;
//分别取yuv各个分量的采样纹理(r表示?)
//
yuv.x = texture2D(yTexture, vTextCoord).g;
yuv.y = texture2D(uTexture, vTextCoord).g - 0.5;
yuv.z = texture2D(vTexture, vTextCoord).g - 0.5;
rgb = mat3(
1.0, 1.0, 1.0,
0.0, -0.39465, 2.03211,
1.13983, -0.5806, 0.0
) * yuv;
//gl_FragColor是OpenGL内置的
gl_FragColor = vec4(rgb, 1.0);
}
);
2,装载着色器:
/**
* 加载 着色器
* @param type 着色器类型
* @param shaderSrc 着色源码
* @return
*/
GLuint LoadShader(GLenum type, const char *shaderSrc) {
GLuint shader;
GLint compiled;
shader = glCreateShader(type); // 创建 着色器 句柄
if (shader == 0) {
LOGE("create shader error");
return 0;
}
// 装载 着色器源码
glShaderSource(shader, 1, &shaderSrc, nullptr);
// 编译着色器
glCompileShader(shader);
// 检测编译状态
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (!compiled) {
GLint infoLen = 0;
// 获取日志长度
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLen);
if (infoLen > 1) {
GLchar *infoLog = static_cast<GLchar *>(malloc(sizeof(GLchar) * infoLen));
// 获取日志 信息
glGetShaderInfoLog(shader, infoLen, nullptr, infoLog);
LOGE("%s", infoLog);
free(infoLog);
}
glDeleteShader(shader);
return 0;
}
return shader;
}
3,创建 渲染工程
GLuint LoadProgramYUV( ){
GLint vertexShader = LoadShader(GL_VERTEX_SHADER,vertexShader_);
GLint fragShader = LoadShader(GL_FRAGMENT_SHADER,fragYUV420P_);
// 创建渲染程序
GLint program = glCreateProgram();
if (program == 0){
LOGE("YUV : CreateProgram failure")
glDeleteShader(vertexShader);
glDeleteShader(fragShader);
return 0;
}
// 先渲染程序中 加入着色器
glAttachShader(program,vertexShader);
glAttachShader(program,fragShader);
// 链接 程序
glLinkProgram(program);
GLint status = 0;
glGetProgramiv(program,GL_LINK_STATUS,&status);
if (status == 0){
LOGE("glLinkProgram failure")
glDeleteProgram(program);
return 0;
}
global_Context.glProgram = program;
glUseProgram(program);
return 1;
}
4、配置渲染逻辑
void RenderYuvConfig(uint width, uint height) {
/* 加入三维顶点数据*/
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
};
//
GLuint apos = static_cast<GLuint>(glGetAttribLocation(global_Context.glProgram, "aPosition"));
glEnableVertexAttribArray(apos);
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
};
GLuint aTex = static_cast<GLuint>(glGetAttribLocation(global_Context.glProgram, "aTextCoord"));
glEnableVertexAttribArray(aTex);
glVertexAttribPointer(aTex, 2, GL_FLOAT, GL_FALSE, 0, fragment);
//纹理初始化
//设置纹理层对应的对应采样器?
/**
* //获取一致变量的存储位置
GLint textureUniformY = glGetUniformLocation(program, "SamplerY");
GLint textureUniformU = glGetUniformLocation(program, "SamplerU");
GLint textureUniformV = glGetUniformLocation(program, "SamplerV");
//对几个纹理采样器变量进行设置
glUniform1i(textureUniformY, 0);
glUniform1i(textureUniformU, 1);
glUniform1i(textureUniformV, 2);
*/
// //对sampler变量,使用函数glUniform1i和glUniform1iv进行设置
glUniform1i(glGetUniformLocation(global_Context.glProgram, "yTexture"), 0);
glUniform1i(glGetUniformLocation(global_Context.glProgram, "uTexture"), 1);
glUniform1i(glGetUniformLocation(global_Context.glProgram, "vTexture"), 2);
//纹理ID
// GLuint texts[3] = {0};
// //创建若干个纹理对象,并且得到纹理ID
glGenTextures(3, global_Context.mTextures);
//
// //绑定纹理。后面的的设置和加载全部作用于当前绑定的纹理对象
// //GL_TEXTURE0、GL_TEXTURE1、GL_TEXTURE2 的就是纹理单元,GL_TEXTURE_1D、GL_TEXTURE_2D、CUBE_MAP为纹理目标
// //通过 glBindTexture 函数将纹理目标和纹理绑定后,对纹理目标所进行的操作都反映到对纹理上
glBindTexture(GL_TEXTURE_2D, global_Context.mTextures[0]);
// //缩小的过滤器
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
// //放大的过滤器
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// //设置纹理的格式和大小
// // 加载纹理到 OpenGL,读入 buffer 定义的位图数据,并把它复制到当前绑定的纹理对象
// // 当前绑定的纹理对象就会被附加上纹理图像。
// //width,height表示每几个像素公用一个yuv元素?比如width / 2表示横向每两个像素使用一个元素?
glTexImage2D(GL_TEXTURE_2D,
0,//细节基本 默认0
GL_LUMINANCE,//gpu内部格式 亮度,灰度图(这里就是只取一个亮度的颜色通道的意思)
width,//加载的纹理宽度。最好为2的次幂(这里对y分量数据当做指定尺寸算,但显示尺寸会拉伸到全屏?)
height,//加载的纹理高度。最好为2的次幂
0,//纹理边框
GL_LUMINANCE,//数据的像素格式 亮度,灰度图
GL_UNSIGNED_BYTE,//像素点存储的数据类型
NULL //纹理的数据(先不传)
);
//
// //绑定纹理
glBindTexture(GL_TEXTURE_2D, global_Context.mTextures[1]);
// //缩小的过滤器
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,//细节基本 默认0
GL_LUMINANCE,//gpu内部格式 亮度,灰度图(这里就是只取一个颜色通道的意思)
width / 2,//u数据数量为屏幕的4分之1
height / 2,
0,//边框
GL_LUMINANCE,//数据的像素格式 亮度,灰度图
GL_UNSIGNED_BYTE,//像素点存储的数据类型
NULL //纹理的数据(先不传)
);
//
// //绑定纹理
glBindTexture(GL_TEXTURE_2D, global_Context.mTextures[2]);
// //缩小的过滤器
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,//细节基本 默认0
GL_LUMINANCE,//gpu内部格式 亮度,灰度图(这里就是只取一个颜色通道的意思)
width / 2,
height / 2,//v数据数量为屏幕的4分之1
0,//边框
GL_LUMINANCE,//数据的像素格式 亮度,灰度图
GL_UNSIGNED_BYTE,//像素点存储的数据类型
NULL //纹理的数据(先不传)
);
}
5,、渲染
void RenderYUV(GLuint width, GLuint height, AVFrame * frame) {
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, global_Context.mTextures[0]);
glTexSubImage2D(GL_TEXTURE_2D, 0,
0, 0,
width, height,
GL_LUMINANCE, GL_UNSIGNED_BYTE,
frame->data[0]);
// 数据 U
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, global_Context.mTextures[1]);
glTexSubImage2D(GL_TEXTURE_2D, 0,
0, 0,
width / 2, height / 2,
GL_LUMINANCE, GL_UNSIGNED_BYTE,
frame->data[1]);
// 数据 V
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, global_Context.mTextures[2]);
glTexSubImage2D(GL_TEXTURE_2D, 0,
0, 0,
width / 2, height / 2,
GL_LUMINANCE, GL_UNSIGNED_BYTE,
frame->data[2]);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
eglSwapBuffers(global_Context.eglDisplay, global_Context.eglSurface);
}
上面的代码实现了YUV420P 格式的视频,渲染NV21 的 格式视频,的着色器 和 配置是有所不同的这里也贴出来:
渲染 NV21 着色器:
const char *vertexNV21Shader_ = GET_STR(
attribute
vec4 aPosition;//输入的顶点坐标,会在程序指定将数据输入到该字段
attribute
vec2 aTextCoord;//输入的纹理坐标,会在程序指定将数据输入到该字段
varying
vec2 vTextCoord;//输出的纹理坐标
void main() {
//这里其实是将上下翻转过来(因为安卓图片会自动上下翻转,所以转回来)
vTextCoord = vec2(aTextCoord.x, 1.0 - aTextCoord.y);
// vTextCoord = aTextCoord;
//直接把传入的坐标值作为传入渲染管线。gl_Position是OpenGL内置的
gl_Position = aPosition;
}
);
const char *fragYUV420SP_ = GET_STR(
precision
mediump float;
varying
vec2 vTextCoord;
//输入的yuv三个纹理
uniform
sampler2D yTexture;//采样器
uniform
sampler2D uvTexture;//采样器
const mat3 converMat = mat3(
1.0, 1.0, 1.0,
0.0, -0.39456, 2.03211,
1.13983, -0.58060, 0.0
);
void main() {
vec3 yuv;
vec3 rgb;
//分别取yuv各个分量的采样纹理(r表示?)
//
yuv.x = texture2D(yTexture, vTextCoord).r;
yuv.y = texture2D(uvTexture, vTextCoord).r - 0.5;
yuv.z = texture2D(uvTexture, vTextCoord).a - 0.5;
//gl_FragColor是OpenGL内置的
gl_FragColor = vec4(converMat * yuv, 1.0);
}
);
渲染NV21 配置:
void RenderYuvNV21Config(uint width, uint height) {
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
};
/* 加入三维顶点数据*/
//
GLuint apos = static_cast<GLuint>(glGetAttribLocation(global_Context.glProgram, "aPosition"));
glEnableVertexAttribArray(apos);
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
};
GLuint aTex = static_cast<GLuint>(glGetAttribLocation(global_Context.glProgram, "aTextCoord"));
glEnableVertexAttribArray(aTex);
glVertexAttribPointer(aTex, 2, GL_FLOAT, GL_FALSE, 0, fragment);
//纹理初始化
//设置纹理层对应的对应采样器?
/**
* //获取一致变量的存储位置
GLint textureUniformY = glGetUniformLocation(program, "SamplerY");
GLint textureUniformU = glGetUniformLocation(program, "SamplerU");
GLint textureUniformV = glGetUniformLocation(program, "SamplerV");
//对几个纹理采样器变量进行设置
glUniform1i(textureUniformY, 0);
glUniform1i(textureUniformU, 1);
glUniform1i(textureUniformV, 2);
*/
// //对sampler变量,使用函数glUniform1i和glUniform1iv进行设置
glUniform1i(glGetUniformLocation(global_Context.glProgram, "yTexture"), 0);
glUniform1i(glGetUniformLocation(global_Context.glProgram, "uvTexture"), 1);
//纹理ID
// GLuint texts[3] = {0};
// //创建若干个纹理对象,并且得到纹理ID
glGenTextures(3, global_Context.mTextures);
//
// //绑定纹理。后面的的设置和加载全部作用于当前绑定的纹理对象
// //GL_TEXTURE0、GL_TEXTURE1、GL_TEXTURE2 的就是纹理单元,GL_TEXTURE_1D、GL_TEXTURE_2D、CUBE_MAP为纹理目标
// //通过 glBindTexture 函数将纹理目标和纹理绑定后,对纹理目标所进行的操作都反映到对纹理上
glBindTexture(GL_TEXTURE_2D, global_Context.mTextures[0]);
// //缩小的过滤器
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
// //放大的过滤器
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// //设置纹理的格式和大小
// // 加载纹理到 OpenGL,读入 buffer 定义的位图数据,并把它复制到当前绑定的纹理对象
// // 当前绑定的纹理对象就会被附加上纹理图像。
// //width,height表示每几个像素公用一个yuv元素?比如width / 2表示横向每两个像素使用一个元素?
glTexImage2D(GL_TEXTURE_2D,
0,//细节基本 默认0
GL_LUMINANCE,//gpu内部格式 亮度,灰度图(这里就是只取一个亮度的颜色通道的意思)
width,//加载的纹理宽度。最好为2的次幂(这里对y分量数据当做指定尺寸算,但显示尺寸会拉伸到全屏?)
height,//加载的纹理高度。最好为2的次幂
0,//纹理边框
GL_LUMINANCE,//数据的像素格式 亮度,灰度图
GL_UNSIGNED_BYTE,//像素点存储的数据类型
NULL //纹理的数据(先不传)
);
//
// //绑定纹理
glBindTexture(GL_TEXTURE_2D, global_Context.mTextures[1]);
// //缩小的过滤器
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,//细节基本 默认0
GL_LUMINANCE_ALPHA,//gpu内部格式 亮度,灰度图(这里就是只取一个颜色通道的意思)
width / 2,//u数据数量为屏幕的4分之1
height / 2,
0,//边框
GL_LUMINANCE_ALPHA,//数据的像素格式 亮度,灰度图
GL_UNSIGNED_BYTE,//像素点存储的数据类型
NULL //纹理的数据(先不传)
);
}
这里 其实只需要两个纹理,我这偷懒,没有去掉
渲染 NV21:
void RenderYUVNV21(GLuint width, GLuint height, AVFrame *frame) {
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, global_Context.mTextures[0]);
glTexSubImage2D(GL_TEXTURE_2D, 0,
0, 0,
width, height,
GL_LUMINANCE,
GL_UNSIGNED_BYTE,
frame->data[0]);
// 数据 U
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, global_Context.mTextures[1]);
glTexSubImage2D(GL_TEXTURE_2D, 0,
0, 0,
width / 2, height / 2,
GL_LUMINANCE_ALPHA,
GL_UNSIGNED_BYTE,
frame->data[1]);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
eglSwapBuffers(global_Context.eglDisplay, global_Context.eglSurface);
}
绑定山下文环境:
int makeContext(uint width,uint height,int type) {
EGLBoolean success = eglMakeCurrent(global_Context.eglDisplay, global_Context.eglSurface,
global_Context.eglPsurface, global_Context.eglContext);
if (!success) {
eglDestroySurface(global_Context.eglDisplay, global_Context.eglSurface);
eglDestroyContext(global_Context.eglDisplay, global_Context.eglContext);
eglTerminate(global_Context.eglDisplay);
LOGE("YUV : MakeCurrent failure")
return -1;
}
if (type == 12) { //yuvj420
LoadProgramYUV();
RenderYuvConfig(width,height);
} else if (type == 23) {
LoadProgramYUVnv21(); // nv21
RenderYuvNV21Config(width,height);
}
return 0;
}
调用这个方法需要注意:必须和渲染的线程在同一线程里面,否则会没有显示,黑屏,