Android 音視訊 - EGL 原始碼解析以及 C++ 實現

聲網Agora發表於2021-10-28
OpenGL 是一個跨平臺的 API,而不同的作業系統(Windows,Android,IOS)各有自己的螢幕渲染實現。所以 OpenGL 定義了一箇中間介面層 EGL(Embedded Graphics Library)標準,具體實現交給各個作業系統本身

EGL

簡單來說 EGL 是一箇中間介面層,是一個規範,由於 OpenGL 的跨平臺性,所以說這個規範顯得尤其重要,不管各個作業系統如何蹦躂,都不能脫離我所定義的規範。

EGL 的一些基礎知識

  • EGLDisplay

EGL 定義的一個抽象的系統顯示類,用於操作裝置視窗。

  • EGLConfig

EGL 配置,如 rgba 位數

  • EGLSurface

渲染快取,一塊記憶體空間,所有要渲染到螢幕上的影像資料,都要先快取在 EGLSurface 上。

  • EGLContext

OpenGL 上下文,用於儲存 OpenGL 的繪製狀態資訊、資料。

初始化 EGL 的過程可以說是對上面幾個資訊進行配置的過程。

OpenGL ES 繪圖完整流程

我們在使用 Java GLSurfaceView 的時候其實只是自定義了 Render,該 Render 實現了 GLsurfaceView.Renderer 介面,然後自定義的 Render 中的 3 個方法就會得到回撥,Android 系統其實幫我省掉了其中的很多步驟。所以我們這裡來看一下完整流程(1). 獲取顯示裝置(對應於上面的 EGLDisplay)

/*
 * Get an EGL instance */
 mEgl = (EGL10) EGLContext.getEGL();
 
/*
 * Get to the default display. */
 mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);

(2). 初始化 EGL

int[] version = new int[2];
//初始化螢幕
if(!mEgl.eglInitialize(mEglDisplay, version)) {
    throw new RuntimeException("eglInitialize failed");
}

(3). 選擇 Config(用 EGLConfig 配置引數)

//這段程式碼的作用是選擇EGL配置, 即可以自己先設定好一個你希望的EGL配置,比如說RGB三種顏色各佔幾位,你可以隨便配,而EGL可能不能滿足你所有的要求,於是它會返回一些與你的要求最接近的配置供你選擇。
if (!egl.eglChooseConfig(display, mConfigSpec, configs, numConfigs,
 num_config)) {
    throw new IllegalArgumentException("eglChooseConfig#2 failed");
}

(4). 建立 EGLContext

//從上一步EGL返回的配置列表中選擇一種配置,用來建立EGL Context。
egl.eglCreateContext(display, config, EGL10.EGL_NO_CONTEXT,
 mEGLContextClientVersion != 0 ? attrib_list : null);

(5). 獲取 EGLSurface

//建立一個視窗Surface,可以看成螢幕所對應的記憶體
 egl.eglCreateWindowSurface(display, config, nativeWindow, null)
PS 這裡的 nativeWindow 是 GLSurfaceView 的 surfaceHolder

(6). 繫結渲染環境到當前執行緒

/*
 * Before we can issue GL commands, we need to make sure * the context is current and bound to a surface. */
 if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) {
    /*
    * Could not make the context current, probably because the underlying * SurfaceView surface has been destroyed. */ 
     logEglErrorAsWarning("EGLHelper", "eglMakeCurrent", mEgl.eglGetError());
     return false;
 }

迴圈繪製

loop:{
    //繪製中....
    //(7).交換緩衝區
    mEglHelper.swap();
}

public int swap() {
    if (! mEgl.eglSwapBuffers(mEglDisplay, mEglSurface)) {
        return mEgl.eglGetError();
    }
    return EGL10.EGL_SUCCESS;
}

Java - GLSurfaceView/GLTextureView

上面我們介紹了 EGL 的一些基礎知識,接著我們來看在 GLSurfaceView/GLTextureView 中 EGL 的具體實現,我們來從原始碼上剖析 Android 系統 EGL 及 GL 執行緒。

GLThread

我們來看一下 GLThread,GLThread 也是從普通的 Thread 類繼承而來,理論上就是一個普通的執行緒,為什麼它擁有 OpenGL 繪圖能力?繼續往下看,裡面最重要的部分就是 guardedRun()方法。

static class GLThread extends Thread {
    ...
    @Override
    public void run() {
      
        try {
                guardedRun();
         } catch (InterruptedException e) {
                // fall thru and exit normally
         } finally {
                sGLThreadManager.threadExiting(this);
         }
    }
}

讓我們來看一下 guardedRun()方法裡有什麼東西,guardedRun()裡大致做的事情:

private void guardedRun() throws InterruptedException {
    while(true){
        //if ready to draw
        ...
        mEglHelper.start();//對應於上面完整流程中的(1)(2)(3)(4)
        
        ...
        mEglHelper.createSurface()//對應於上面完整流程中的(5)(6)
        
        ...
        回撥GLSurfaceView.Renderer的onSurfaceCreated();
        ...
        回撥GLSurfaceView.Renderer的onSurfaceChanged();
        ...
        回撥GLSurfaceView.Renderer的onDrawFrame();
        
        ...
         mEglHelper.swap();//對應於上面完整流程中的(5)(7)
    }
}

從上面我們的分析得知 GLSurfaceView 中的 GLThread 就是一個普通的執行緒,只不過它按照了 OpenGL 繪圖的完整流程正確地操作了下來,因此它有 OpenGL 的繪圖能力。那麼,如果我們自己建立一個執行緒,也按這樣的操作方法,那我們也可以在自己建立的執行緒裡繪圖嗎?答案是肯定的(這不正是 EGL 的介面意義),下面我會給出 EGL 在 Native C/C++中的實現。

Native - EGL

Android Native 環境中並不存在現成的 EGL 環境,所以我們在進行 OpenGL 的 NDK 開發時就必須自己實現 EGL 環境,那麼如何實現呢,我們只需要參照 GLSurfaceView 中的 GLThread 的寫法就能實現 Native 中的 EGL。

PS

以下的內容可能需要你對 C/C++以及 NDK 有一定熟悉

第 1 步實現類似於 Java GLSurfaceView 中的 GLThread 的功能

gl_render.h

class GLRender {
    private:
         const char *TAG = "GLRender";
         //OpenGL渲染狀態
         enum STATE {
             NO_SURFACE, //沒有有效的surface
             FRESH_SURFACE, //持有一個為初始化的新的surface
             RENDERING, //初始化完畢,可以開始渲染
             SURFACE_DESTROY, //surface銷燬
             STOP //停止繪製
         };
         JNIEnv *m_env = NULL;
         //執行緒依附的jvm環境
         JavaVM *m_jvm_for_thread = NULL;
         //Surface引用,必須要使用引用,否則無法線上程中操作
         jobject m_surface_ref = NULL;
         //本地螢幕
         ANativeWindow *m_native_window = NULL;
         //EGL顯示錶面
         EglSurface *m_egl_surface = NULL;
         int m_window_width = 0;
         int m_window_height = 0;
         
         // 繪製代理器
         ImageRender *pImageRender;
         
         //OpenGL渲染狀態
         STATE m_state = NO_SURFACE;
         // 初始化相關的方法
         void InitRenderThread();
         bool InitEGL();
         void InitDspWindow(JNIEnv *env);
         // 建立/銷燬 Surface void CreateSurface();
         void DestroySurface();
         // 渲染方法
         void Render();
         void ReleaseSurface();
         void ReleaseWindow();
         // 渲染執行緒回撥方法
         static void sRenderThread(std::shared_ptr<GLRender> that);
    public:
         GLRender(JNIEnv *env);
         ~GLRender();
         //外部傳入Surface
         void SetSurface(jobject surface);
      
         void Stop();
         void SetBitmapRender(ImageRender *bitmapRender);
        // 釋放資源相關方法
         void ReleaseRender();
         
         ImageRender *GetImageRender();
};

gl_render.cpp

//建構函式
GLRender::GLRender(JNIEnv *env) {
     this->m_env = env;
     //獲取JVM虛擬機器,為建立執行緒作準備
     env->GetJavaVM(&m_jvm_for_thread);
     InitRenderThread();
}
//解構函式
GLRender::~GLRender() {
    delete m_egl_surface;
}

//初始化渲染執行緒
void GLRender::InitRenderThread() {
    // 使用智慧指標,執行緒結束時,自動刪除本類指標
     std::shared_ptr<GLRender> that(this);
     std::thread t(sRenderThread, that);
     t.detach();
}

//執行緒回撥函式
void GLRender::sRenderThread(std::shared_ptr<GLRender> that) {
    JNIEnv *env;
     //(1) 將執行緒附加到虛擬機器,並獲取env
     if (that->m_jvm_for_thread->AttachCurrentThread(&env, NULL) != JNI_OK) {
            LOGE(that->TAG, "執行緒初始化異常");
            return; 
     }
     // (2) 初始化 EGL 
    if (!that->InitEGL()) {
         //解除執行緒和jvm關聯
         that->m_jvm_for_thread->DetachCurrentThread();
         return; 
     }
     
     //進入迴圈
    while (true) {
            //根據OpenGL渲染狀態進入不同的處理
            switch (that->m_state) {
                //重新整理Surface,從外面設定Surface後m_state置為該狀態,說明已經從外部(java層)獲得Surface的物件了
                case FRESH_SURFACE:
                     LOGI(that->TAG, "Loop Render FRESH_SURFACE")
                     // (3) 初始化Window
                     that->InitDspWindow(env);
                     // (4) 建立EglSurface
                     that->CreateSurface();
                     // m_state置為RENDERING狀態進入渲染
                     that->m_state = RENDERING;
                     break; 
                 case RENDERING:
                    LOGI(that->TAG, "Loop Render RENDERING")
                    // (5) 渲染
                    that->Render();
                    break; 
               
                 case STOP:
                    LOGI(that->TAG, "Loop Render STOP")
                    //(6) 解除執行緒和jvm關聯
                     that->ReleaseRender();
                     that->m_jvm_for_thread->DetachCurrentThread();
                     return; 
                case SURFACE_DESTROY:
                    LOGI(that->TAG, "Loop Render SURFACE_DESTROY")
                    //(7) 釋放資源
                    that->DestroySurface();
                    that->m_state = NO_SURFACE;
                    break; 
                case NO_SURFACE:
                default:
                    break;
     }
    usleep(20000);
 }
}

我們定義的 GLRender 各個流程程式碼裡已經標註了步驟,雖然程式碼量比較多,但是我們的 c++ class 分析也是跟 java 類似,

img

PS 上圖中的(3)(4)等步驟對應於程式碼中的步驟註釋

(1)將執行緒附加到虛擬機器,並獲取env

這一步簡單明瞭,我們往下看

EGL 封裝準備

我們在上一篇就知道了 EGL 的一些基礎知識,EGLDiaplay,EGLConfig,EGLSurface,EGLContext,我們需要把這些基礎類進行封裝,那麼如何進行封裝呢,我們先看一下對於我們上篇文章中自定義的 GLRender 類需要什麼 gl_render.h

//Surface引用,必須要使用引用,否則無法線上程中操作
jobject m_surface_ref = NULL;
//本地螢幕
ANativeWindow *m_native_window = NULL;
//EGL顯示錶面 注意這裡是我們自定義的EglSurface包裝類而不是系統提供的EGLSurface哦
EglSurface *m_egl_surface = NULL;

對於 gl_render 來說輸入的是外部的Surface物件,我們這裡的是jobject m_surface_ref,那麼輸出需要的是ANativeWindow,EglSurface

關於ANativeWindow可以檢視官方文件ANativeWindow

那麼EglSurface呢,

egl_surface.h

class EglSurface {
private:
    const char *TAG = "EglSurface";
    //本地螢幕
     ANativeWindow *m_native_window = NULL;
     //封裝了EGLDisplay EGLConfig EGLContext的自定義類
     EglCore *m_core;
     //EGL API提供的 EGLSurface
     EGLSurface m_surface;
}
可以看到我們上面的定義的思想也是 V(View)和 C(Controller)進行了分離。

egl_core.h

class EglCore {
private:
    const char *TAG = "EglCore";
     //EGL顯示視窗
     EGLDisplay m_egl_dsp = EGL_NO_DISPLAY;
     //EGL上下文
     EGLContext m_egl_context = EGL_NO_CONTEXT;
     //EGL配置
     EGLConfig m_egl_config;
}

有了上面的準備工作後,我們就跟著流程圖的步驟來一步步走。

(2)初始化 EGL

gl_render.cpp

bool GLRender::InitEGL() {
    //建立EglSurface物件
    m_egl_surface = new EglSurface();
    //呼叫EglSurface的init方法
    return m_egl_surface->Init();
}

egl_surface.cpp

PS 我們上面也說了 EGL 的初始化主要是對 EGLDisplay EGLConfig EGLContext 的操作,所以現在是對 EGLCore 的操作。
EglSurface::EglSurface() {
    //建立EGLCore
    m_core = new EglCore();
}

bool EglSurface::Init() {
    //呼叫EGLCore的init方法
    return m_core->Init(NULL);
}

egl_core.cpp

EglCore::EglCore() {
}


bool EglCore::Init(EGLContext share_ctx) {
    if (m_egl_dsp != EGL_NO_DISPLAY) {
        LOGE(TAG, "EGL already set up")
        return true;
     }
    if (share_ctx == NULL) {
            share_ctx = EGL_NO_CONTEXT;
     }
     //獲取Dispaly
    m_egl_dsp = eglGetDisplay(EGL_DEFAULT_DISPLAY);
     if (m_egl_dsp == EGL_NO_DISPLAY || eglGetError() != EGL_SUCCESS) {
            LOGE(TAG, "EGL init display fail")
            return false;
     }
        EGLint major_ver, minor_ver;
     //初始化egl
     EGLBoolean success = eglInitialize(m_egl_dsp, &major_ver, &minor_ver);
     if (success != EGL_TRUE || eglGetError() != EGL_SUCCESS) {
            LOGE(TAG, "EGL init fail")
            return false;
     }
        LOGI(TAG, "EGL version: %d.%d", major_ver, minor_ver)
     //獲取EGLConfig   
     m_egl_config = GetEGLConfig();
     const EGLint attr[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};
     //建立EGLContext
     m_egl_context = eglCreateContext(m_egl_dsp, m_egl_config, share_ctx, attr);
     if (m_egl_context == EGL_NO_CONTEXT) {
            LOGE(TAG, "EGL create fail, error is %x", eglGetError());
     return false; }
        EGLint egl_format;
     success = eglGetConfigAttrib(m_egl_dsp, m_egl_config, EGL_NATIVE_VISUAL_ID, &egl_format);
     if (success != EGL_TRUE || eglGetError() != EGL_SUCCESS) {
            LOGE(TAG, "EGL get config fail, error is %x", eglGetError())
            return false;
     }
    LOGI(TAG, "EGL init success")
    return true;
}

EGLConfig EglCore::GetEGLConfig() {
    EGLint numConfigs;
    EGLConfig config;

  //希望的最小配置,
    static const EGLint CONFIG_ATTRIBS[] = {
            EGL_BUFFER_SIZE, EGL_DONT_CARE,
            EGL_RED_SIZE, 8,//R 位數
            EGL_GREEN_SIZE, 8,//G 位數
            EGL_BLUE_SIZE, 8,//B 位數
            EGL_ALPHA_SIZE, 8,//A 位數
            EGL_DEPTH_SIZE, 16,//深度
            EGL_STENCIL_SIZE, EGL_DONT_CARE,
            EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
            EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
            EGL_NONE // the end 結束標誌
    };
  //根據你所設定的最小配置系統會選擇一個滿足你最低要求的配置,這個真正的配置往往要比你期望的屬性更多
    EGLBoolean success = eglChooseConfig(m_egl_dsp, CONFIG_ATTRIBS, &config, 1, &numConfigs);
    if (!success || eglGetError() != EGL_SUCCESS) {
        LOGE(TAG, "EGL config fail")
        return NULL;
    }
    return config;
}

(3)建立 Window

gl_render.cpp

void GLRender::InitDspWindow(JNIEnv *env) {
  //傳進來的Surface物件的引用
    if (m_surface_ref != NULL) {
        // 初始化視窗
        m_native_window = ANativeWindow_fromSurface(env, m_surface_ref);

        // 繪製區域的寬高
        m_window_width = ANativeWindow_getWidth(m_native_window);
        m_window_height = ANativeWindow_getHeight(m_native_window);

        //設定寬高限制緩衝區中的畫素數量
        ANativeWindow_setBuffersGeometry(m_native_window, m_window_width,
                                         m_window_height, WINDOW_FORMAT_RGBA_8888);

        LOGD(TAG, "View Port width: %d, height: %d", m_window_width, m_window_height)
    }
}

(4)建立 EglSurface 並繫結到執行緒

gl_render.cpp

void GLRender::CreateSurface() {
    m_egl_surface->CreateEglSurface(m_native_window, m_window_width, m_window_height);
    glViewport(0, 0, m_window_width, m_window_height);
}

egl_surface.cpp

/**
 * 
 * @param native_window 傳入上一步建立的ANativeWindow
 * @param width 
 * @param height 
 */
void EglSurface::CreateEglSurface(ANativeWindow *native_window, int width, int height) {
    if (native_window != NULL) {
        this->m_native_window = native_window;
        m_surface = m_core->CreateWindSurface(m_native_window);
    } else {
        m_surface = m_core->CreateOffScreenSurface(width, height);
    }
    if (m_surface == NULL) {
        LOGE(TAG, "EGL create window surface fail")
        Release();
    }
    MakeCurrent();
}

void EglSurface::MakeCurrent() {
    m_core->MakeCurrent(m_surface);
}

egl_core.cpp

EGLSurface EglCore::CreateWindSurface(ANativeWindow *window) {
  //呼叫EGL Native API建立Window Surface
    EGLSurface surface = eglCreateWindowSurface(m_egl_dsp, m_egl_config, window, 0);
    if (eglGetError() != EGL_SUCCESS) {
        LOGI(TAG, "EGL create window surface fail")
        return NULL;
    }
    return surface;
}

void EglCore::MakeCurrent(EGLSurface egl_surface) {
  //呼叫EGL Native API 繫結渲染環境到當前執行緒
    if (!eglMakeCurrent(m_egl_dsp, egl_surface, egl_surface, m_egl_context)) {
        LOGE(TAG, "EGL make current fail");
    }
}

(5)渲染

gl_render.cpp

void GLRender::Render() {
    if (RENDERING == m_state) {
        pImageRender->DoDraw();//畫畫畫....
        m_egl_surface->SwapBuffers();
    }
}

egl_surface.cpp

void EglSurface::SwapBuffers() {
    m_core->SwapBuffer(m_surface);
}

egl_core.cpp

void EglCore::SwapBuffer(EGLSurface egl_surface) {
  //呼叫EGL Native API
    eglSwapBuffers(m_egl_dsp, egl_surface);
}

後面的停止與銷燬就交給讀者自行研究了。

程式碼

EGLDemoActivity.java

EGL Native

相關文章