Android OpenGL ES視頻渲染(二)EGL+OpenGL

相關文章: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 具備以下功能:

  1. 和設備的本地窗口系統通信 ,Android上就是ANativeWindow
  2. 查詢繪圖表面的可用類型及配置
  3. 創建OpenGL ES可用的“繪圖表面”,Android上就是Surface
  4. 在OpenGL ES 或是其它圖形渲染 API 之間同步渲染
  5. 管理渲染資源,如紋理貼圖

簡單來說,就是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                    //紋理的數據
}

繪製每一層數據,

  1. 更新紋理。 需要注意的是YUV4420P中,UV數據是Y的1/4,一半寬,一半高。具體不贅述,瞭解YUV420的構成即可。
  2. glDrawArrays以三角形方式繪製。
  3. 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);
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章