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;
}
調用這個方法需要注意:必須和渲染的線程在同一線程裏面,否則會沒有顯示,黑屏,