Android 音視訊 - EGL 原始碼解析以及 C++ 實現
OpenGL 是一個跨平臺的 API,而不同的作業系統(Windows,Android,IOS)各有自己的螢幕渲染實現。所以 OpenGL 定義了一箇中間介面層 EGL(Embedded Graphics Library)標準,具體實現交給各個作業系統本身
簡單來說 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 類似,
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); }
後面的停止與銷燬就交給讀者自行研究了。
- 音訊技術的下一個“熱點”,會出現在哪個領域?丨一期一會 • 音訊工程師專場
- 桌面軟體開發框架大賞
- 即時通訊場景下安全合規的實踐和經驗
- 大家談的視訊體驗指標,都有哪些?如何測定?
- 從開源模型、框架到自研,聲網 Web 端虛擬背景演算法正式釋出
- 聲網的混沌工程實踐
- 聲網崩潰資料的自動化閉環處理
- 視訊影象色彩增強的主要方法與落地實踐
- 聲網AI降噪測評系統初探
- Agora Flat:線上教室的開源初體驗
- 三步開啟你的網路服務全球動態加速之旅
- 如何基於 React Native 快速實現一個視訊通話應用
- 小談音視訊質量檢測
- Android 音視訊 - MediaCodec 編解碼音視訊
- 【AI 全棧 SOTA 綜述 】這些你都不知道,怎麼敢說會 AI?【語音識別原理 實戰】
- Android 音視訊 - EGL 原始碼解析以及 C 實現
- Android 音視訊採集那些事
- 別再傻傻分不清 AVSx H.26x MPEG-x 了
- 音視訊編解碼 -- 編碼引數 CRF
- 【音視訊專題】音訊質量評估方法那些事