相關文章:Android OpenGL ES視頻渲染(一)GLSurfaceView
上一篇講了如何在通過GLSurfaceView使用OpenGL進行視頻, 當時我們也講過,它是繼承自SurfaceView,具備SurfaceView的特性,並加入了EGL的管理。那麼本篇就介紹我們如何在native層使用EGL+OpenGL來完成視頻渲染的。
爲什麼要這麼麻煩呢,一是在native層使用能有更高的效率;二是在native層可以更好的結合OpenGL代碼,更好的把一些開源工具引進來,使用OpenGL也會更加靈活,因爲OpenGL本身就是底層代碼;三是這樣你會對OpenGL ES在Android上的使用有更深入的瞭解。
本文將使用FFmpeg解碼出YUV數據,並將YUV420P類型的視頻通過EGL+OpenGL渲染出來。
EGL簡介
前一章節我們有描述到OpenGL ES的一些使用,但OpenGL ES是 定義了一個渲染圖形的規範,但並沒有定義窗口系統。也就是說OpenGL中並沒有定義渲染的圖像是如何繪製到Android的窗口上的,這也是因爲OpenGL本身是跨平臺的,所以這部分需要各個平臺自己實現,那麼EGL就是連接OpenGL ES和本地窗口系統的接口,本地窗口相關的API提供了訪問本地窗口系統的接口,EGL提供了創建渲染表面,接下來OpenGL ES就可以在這個渲染表面上繪製,同時提供了圖形上下文,用來進行狀態管理。
EGL 具備以下功能:
- 和設備的本地窗口系統通信 ,Android上就是ANativeWindow
- 查詢繪圖表面的可用類型及配置
- 創建OpenGL ES可用的“繪圖表面”,Android上就是Surface
- 在OpenGL ES 或是其它圖形渲染 API 之間同步渲染
- 管理渲染資源,如紋理貼圖
簡單來說,就是EGL承擔了爲OpenGL ES提供上下文環境以及窗口管理的職責。
使用流程:
我們想實現一個視頻渲染的功能,把FFmpeg解碼出來的YUV420P數據顯示出來,那麼我們需要做以下事情:
1、OpenGL ES渲染程序的創建
2、EGL創建OpenGL ES上下文環境。
3、紋理更新並繪製。
1、OpenGL ES渲染程序的創建
這部分和在GLSurfaceView中使用時並沒太大區別:
編寫着色器代碼
->創建渲染程序
->填充頂點座標及紋理座標
頂點着色器
#define GET_STR(x) #x
static const char *verShader = GET_STR(
attribute vec4 aPosition; //頂點座標
attribute vec2 aTexCoord; //紋理座標
varying vec2 vTexCoord; //最終傳遞給片元着色器的座標
void main()
{
vTexCoord = aTexCoord;
gl_Position = aPosition;
}
);
頂點着色器紋理座標的計算略有不同,下面會說明。
片元着色器
static const char *fragShader_YUV420P = GET_STR(
precision mediump float; //精度
varying vec2 vTexCoord;//頂點着色器傳遞的紋理座標
uniform sampler2D yTexture; //第一層紋理Y
uniform sampler2D uTexture;//第二層紋理U
uniform sampler2D vTexture;//第三層紋理V
void main()
{
mediump vec3 yuv;
lowp vec3 rgb;
yuv.x= texture2D(yTexture, vTexCoord).r;
yuv.y = texture2D(uTexture, vTexCoord).r - 0.5;
yuv.z = texture2D(vTexture, vTexCoord).r - 0.5;
rgb = mat3(1.0, 1.0, 1.0,
0.0, -0.39465, 2.03211,
1.13983, -0.58060, 0.0) * yuv;
gl_FragColor = vec4(rgb, 1.0);
}
);
和GLSurfaceView中不同,片元着色器的代碼使用的是sampler2D 類型的紋理數據。
我們知道,GPU中用的都是RGBA顏色,所以要進行YUV420P轉RGB的計算,最後把RGB顏色設置給gl_FragColor,alpha設置爲1就行。
這裏簡單說下轉換過程,更詳細的請查看一些相關資料。
YUV420P是平面類型的數據,可以理解爲Y/U/V數據是分三層存放的,每一層都是單像素數據,所以我們分別從三層紋理中取出其第一個數據,例如yuv.x= texture2D(yTexture, vTexCoord).r;
即通過texture2D
函數從座標vTexCoord
取出yTexture
這一層紋理的像素,r是第一個數據,就是Y數據。
U/V同理,但U/V取出來的數據需要-0.5,因爲UV的默認值是127(OpenGL ES的Shader中會把內存中0~255的整數數值換算爲0.0~1.0的浮點數值)。
最後通過矩陣轉換YUV爲RGB,代碼是其中一種轉換方式,YUV轉RGB有很多不同的公式,對應不同的標準。
上面是一種標準,例如下面這種轉換標準也是可以的。
//yuv轉rgb
rgb.r = y + 1.4075 * v;
rgb.g = y - 0.3455 * u - 0.7169 * v;
rgb.b = y + 1.779 * u;
創建渲染程序
在《Android OpenGL ES視頻渲染(一)GLSurfaceView》中已詳細描述,不贅述,還是按照下面的流程。
填充頂點座標及紋理座標
在《Android OpenGL ES視頻渲染(一)GLSurfaceView》中已詳細描述。
這裏有個小區別,因爲沒有紋理座標轉換矩陣,我們就直接在填充階段,就將座標對應好。把紋理按照,左下->右下->左上->右上的順序,貼到物體上。對應代碼如下:
//頂點座標
static float vers[] =
{
-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 = (GLuint)glGetAttribLocation(program, "aPosition");
glEnableVertexAttribArray(apos);
glVertexAttribPointer(apos, 3, GL_FLOAT, GL_FALSE, 12, vers);
//紋理座標,注意計算機座標系和紋理座標系的轉換。這裏是轉換後的
static float txts[] =
{
0.0f, 1.0f,
1.0f, 1.0f,
0.0f, 0.0f,
1.0f, 0.0f
};
GLuint atex = (GLuint)glGetAttribLocation(program, "aTexCoord");
glEnableVertexAttribArray(atex);
glVertexAttribPointer(atex, 2, GL_FLOAT, GL_FALSE, 8, txts);
設置片元着色器各層紋理ID
glUniform1i(glGetUniformLocation(program, "yTexture"), 0); //Y 紋理index0
glUniform1i(glGetUniformLocation(program, "uTexture"), 1); //U 紋理index1
glUniform1i(glGetUniformLocation(program, "vTexture"), 2); //V 紋理index2
EGL創建OpenGL ES上下文環境
流程如下:
初始化EGLDisplay
->創建合適的繪製上下文EGLContext
->創建EGLSurface
->綁定線程
初始化EGLDisplay
EGLDisplay是一個封裝系統物理屏幕的數據類型(可以理解爲繪製目標的一個抽象),它將OpenGL ES的輸出和設備的屏幕橋接起來。
調用eglGetDisplay方法返回EGLDisplay來作爲OpenGL ES渲染的目標。傳入EGL_DEFAULT_DISPLAY,這樣會返回一個默認顯示設備。
eglInitialize初始化display,後兩個參數是Major和Minor的版本號,傳0即可。
//1 獲取EGLDisplay
EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
//2 初始化EGLDisplay
if(EGL_TRUE != eglInitialize(display, 0, 0))
{
LOGE("eglInitialize failed!");
return false;
}
創建合適的繪製上下文EGLContext
創建上下文需要指定一些配置項,類似於色彩格式、像素格式、RGBA的表示以及SurfaceType等,不
同的系統以及平臺使用的EGL標準是不同的。
一種方式是使用eglGetConfigs函數獲取底層窗口系統支持的所有EGL表面配置,然後再使用eglGetConfigAttrib依次查詢每個EGLConfig相關的信息,EGLConfig包含了渲染表面的所有信息,包括可用顏色、緩衝區等其他特性。
EGLBoolean eglGetConfigs(EGLDisplay display, EGLConfig *configs, EGLint maxReturnConfigs,EGLint *numConfigs);
EGLBoolean eglGetConfigAttrib(EGLDisplay display, EGLConfig config, EGLint attribute, EGLint *value)
我們常用的是另一種方式,調用eglChooseConfig讓EGL選擇一個合適的配置,參數如下:
EGLBoolean eglChooseConfig(EGLDisplay display,
const EGLint* attribs, // 想要的屬性事先定義到這個數組裏
EGLConfig* configs, // 圖形系統將返回若干滿足條件的配置到該數組
EGLint maxConfigs, // 上面數組的容量
EGLint* numConfigs); // 圖形系統返回的可用的配置個數
選擇好配置後,我們就可以創建上下文了。調用函數eglCreateContext。
EGLContext eglCreateContext
(
EGLDisplay display,
EGLConfig config, // 前面選好的可用EGLConfig
EGLContext shareContext, // 允許多個EGLContext共享特定類型的數據,傳遞EGL_NO_CONTEXT表示不與其他上下文共享資源
const EGLint* attribList // 指定操作的屬性列表,只能接受一個屬性EGL_CONTEXT_CLIENT_VERSION用來表示使用的OpenGL ES版本
);
完整代碼如下:
//3 配置
EGLint attribs[] =
{
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
EGL_NONE
};
EGLConfig config = 0;
EGLint numConfigs = 0;
if(EGL_TRUE != eglChooseConfig(display, attribs, &config, 1, &numConfigs))
{
LOGE("eglChooseConfig failed!");
return false;
}
//4 創建EGL context
const EGLint ctxAttr[] = { EGL_CONTEXT_CLIENT_VERSION , 2, EGL_NONE};
EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, ctxAttr);
創建EGLSurface
我們已經創建好上下文,那麼應該如何將OpenGL ES的輸出渲染到設備的屏幕上呢?需要將EGL和設備的屏幕連接起來,只有這樣EGL纔是一個“橋”的功能,從而使得OpenGL ES的輸出可以渲染到設備的屏上。所以接下來我們要創建EGLSurface ,EGLSurface回合原生窗口ANativeWindow 關聯起來,那麼渲染輸出就可以顯示到設備屏幕。
調用eglCreateWindowSurface創建EGLSurface,可以看到參數三,就是原生窗口,在Android裏爲ANativeWindow 。
EGLSurface eglCreateWindowSurface(EGLDisplay display,
EGLConfig config, // 前面選好的可用EGLConfig
EGLNatvieWindowType window, // 原生窗口
const EGLint *attribList) // 指定窗口屬性列表,可以爲null,一般指定渲染所用的緩衝區使用但緩衝或者後臺緩衝,默認爲後者。
ANativeWindow 可以從Java層獲取:Surface
SurfaceView mSurfaceView = (SurfaceView)findViewById(R.id.surfaceView1);
final SurfaceHolder sh = mSurfaceView.getHolder();
Surface surface = mSurfaceView.getSurface();
將這個surface傳入到JNI
native端:
ANativeWindow *win = ANativeWindow_fromSurface(env, jsurface);
其實Surface類型就是ANativeWindow的子類,所以也可以這樣獲取
sp<Surface> surface(android_view_Surface_getSurface(env, jsurface));
ANativeWindow *win = (ANativeWindow *) surface;
當然還有很多中方式,之前其他文章的SoftwareRenderer的介紹中也說過如何創建,不贅述。
完整代碼:
EGLSurface surface = eglCreateWindowSurface(display, config, win, NULL);
綁定線程
OpenGL ES的繪製都是需要在獨立的線程之中(GLSurfaceView中其實系統幫我們創建了GLThread),線程要綁定顯示設備(EGLSurface)與上下文環境(EGLContext),這樣纔可以執行OpenGL的指令。
調用eglMakeCurrent進行綁定。
EGLBoolean eglMakeCurrent(EGLDisplay display,
EGLSurface draw, // EGL繪圖表面
EGLSurface read, // EGL讀取表面
EGLContext context // 指定連接到該表面的渲染上下文
);
完整代碼:
if(EGL_TRUE != eglMakeCurrent(display, surface, surface, context))
{
LOGE("eglMakeCurrent failed!");
return false;
}
return true;
3、紋理更新並繪製
做好OpenGL及EGL的初始化工作後,我們在FFmpeg解碼出YUV數據線程中,將數據更新到紋理,然後交換緩衝swapbuf,這樣就會輸出到屏幕。爲什麼要swapbuf呢?因爲是雙緩衝繪製,繪製在後臺,顯示在前臺,交換緩衝,即將渲染的畫面輸出到前臺。
更新紋理代碼如下:
基本流程和之前一樣,調用glTexImage2D更新紋理數據,glTexImage2D的format參數需要根據數據類型來指定,因爲是YUV數據,所以format爲GL_LUMINANCE。其他參數比較好理解,參看代碼註釋即可。
void UpdateTexture(unsigned int index, int width, int height, unsigned char *buf)
{
unsigned int format = GL_LUMINANCE;
if(texts[index] == 0)
{
glGenTextures(1, &texts[index]);
glBindTexture(GL_TEXTURE_2D, texts[index]);
//設置縮放參數
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
}
//激活第N層紋理,綁定到創建的opengl紋理
glActiveTexture(GL_TEXTURE0 + index);
glBindTexture(GL_TEXTURE_2D, texts[index]);
//更新紋理
glTexImage2D(GL_TEXTURE_2D,
0, //細節基本 0默認
format,//gpu內部格式 亮度,灰度圖
width, height, //拉昇到全窗口
0, //邊框
format,//數據的像素格式,亮度,灰度圖 要與上面一致
GL_UNSIGNED_BYTE, //像素的數據類型
buf //紋理的數據
}
繪製每一層數據,
- 更新紋理。 需要注意的是YUV4420P中,UV數據是Y的1/4,一半寬,一半高。具體不贅述,瞭解YUV420的構成即可。
- glDrawArrays以三角形方式繪製。
- EGL eglSwapBuffers交換緩衝,顯示出來。
完整代碼如下:
void renderFrame(unsigned char *data[], int width, int height)
{
AutoMutex l(mux);
UpdateTexture(0, width, height, data[0]); // Y
UpdateTexture(1, width / 2, height / 2, data[1]); // U
UpdateTexture(2, width / 2, height / 2, data[2]); // V
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
eglSwapBuffers(display, surface);
}