一.前言
- 在學習音視頻的過程中,實現視頻渲染是非常常見的,而渲染的方式也挺多,可以使用Java層的OpenGL ES進行圖形渲染,也可以使用Ffmpeg來顯示,還有就是通過C++層的OpenGL ES來進行渲染。
- OpenGL ES是OpenGL三維圖形API的子集,本文針對通過C++層實現OpenGL ES來進行渲染視頻做一下記錄。
- 整體效果:
二.使用OpenGL ES實現視頻渲染功能
- 若想在NDK代碼中使用OpenGL,還得借助EGL這座橋梁才行。對于Android系統而言,EGL(Enterprise Generation Language,企業生成語言)是OpenGL ES與原生窗口之間的接口層。
- 使用OpenGL ES實現視頻渲染功能的大體步驟,可以劃分為4個步驟:
- 初始化EGL & 讓EGL接管原生窗口ANativeWindow
- 初始化OpenGL ES
- 該部分是跟著色器和紋理有關的
- 通過OpenGL ES渲染視頻畫面
- 釋放EGL資源
1.準備工作
- 創建支持native的Android項目,以及相關的so文件和yuv文件(這部分copy步驟三提供的項目中的即可)。
- 對cmake的知識需要有基礎的掌握,這部分知識可以查看Android音視頻探索之旅 | CMake基礎語法 && 創建支持Ffmpeg的Android項目。
2.代碼環節
2.1.自定義一個GLSurfaceView實現類
package com.jack.ffmpeg_simple02;import android.content.Context;
import android.opengl.GLSurfaceView;
import android.util.AttributeSet;
import android.view.SurfaceHolder;
import androidx.annotation.NonNull;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;/*** @創建者 Jack* @創建時間 2025-07-12 12:40* @描述*/
public class PlayView extends GLSurfaceView implements SurfaceHolder.Callback, GLSurfaceView.Renderer,Runnable{public PlayView(Context context, AttributeSet attrs) {super(context, attrs);//適配Android8.0及以上 無法正常渲染視頻的問題setRenderer( this );}@Overridepublic void surfaceChanged(@NonNull SurfaceHolder surfaceHolder, int i, int i1, int i2) {}@Overridepublic void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) {new Thread( this ).start();}@Overridepublic void surfaceDestroyed(@NonNull SurfaceHolder surfaceHolder) {}@Overridepublic void onDrawFrame(GL10 gl10) {}@Overridepublic void onSurfaceChanged(GL10 gl10, int i, int i1) {}@Overridepublic void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {}public native void PlayYuv(String url,Object surface);@Overridepublic void run() {//注意:需要手動開啟存儲權限PlayYuv("/sdcard/out.yuv",getHolder().getSurface());}
}
- 并在MainActivity中引入使用
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity"><com.jack.ffmpeg_simple02.PlayViewandroid:layout_width="match_parent"android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
2.2.C++層代碼
#include <jni.h>
#include <string>
#include <android/log.h>
#include <android/native_window_jni.h>
#include <EGL/egl.h>
#include <GLES2/gl2.h>
#include <string.h>#define LOGD(...) __android_log_print(ANDROID_LOG_WARN,"Test OpenGL ES ",__VA_ARGS__)//著色器之間不能直接通信,每個著色器都是獨立的小程序,它們唯一的交流信息就是輸入和輸出參數
//著色器的小程序采用GLSL語言(OpenGL Shader Language)編寫//常用的限定符主要有attribute、varying、in、out、uniform五個,分別說明如下。
// attribute:表示該變量是輸入參數。GLSL 2.0使用。
// varying:表示該變量是輸出參數。GLSL 2.0使用。
// in:表示該變量是輸入參數。GLSL 3.0使用。
// out:表示該變量是輸出參數。GLSL 3.0使用。
// uniform:表示該變量是全局參數。// **頂點著色器** glsl 確定位置
//以下場景,頂點著色器 的模板幾乎是固定的
//? 在屏幕上渲染一張2D紋理(如圖片/視頻)。
//? 處理坐標系差異(如YUV數據)。
//? 簡單的全屏繪制。
#define GET_STR(x) #x
static const char *vertexShader = GET_STR(attribute vec4 aPosition; //頂點坐標attribute vec2 aTexCoord; //材質頂點坐標varying vec2 vTexCoord; //輸出的材質坐標(處理后的紋理坐標(傳遞給片元著色器))//聲明完數據變量之后,即可編寫形如void main() { /*里面是具體的實現代碼*/ }的小程序代碼。void main() {// 翻轉Y坐標 翻轉的原因:確保圖像方向正確。vTexCoord = vec2(aTexCoord.x, 1.0 - aTexCoord.y);//簡單投影:代碼中頂點坐標已經是裁剪空間坐標(范圍[-1,1]),直接賦值即可全屏渲染。例如:頂點坐標(1.0, -1.0, 0.0)對應屏幕右下角。gl_Position = aPosition;//給內置的位置變量賦值}
);//片元著色器,軟解碼和部分x86硬解碼
//這段片元著色器代碼是專門為YUV420P格式視頻數據轉RGB渲染設計的(細節部分暫時沒有必要關注)
static const char *fragYUV420P = GET_STR(precision mediump float; //中等精度浮點數 作用:平衡性能和精度,適合移動端GPU(OpenGL ES要求必須聲明精度)。varying vec2 vTexCoord; //頂點著色器傳遞的坐標//YUV420P數據存儲為三個獨立平面(Y全分辨率,U/V半分辨率),需分別采樣。uniform sampler2D yTexture; //輸入的材質(不透明灰度,單像素)uniform sampler2D uTexture;uniform sampler2D vTexture;void main() {vec3 yuv;vec3 rgb;yuv.r = texture2D(yTexture, vTexCoord).r;yuv.g = texture2D(uTexture, vTexCoord).r - 0.5;yuv.b = 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);}
);GLint InitShader(const char *code, GLint type) {//8.創建指定類型的著色器并返回著色器的編號。輸入參數填著色器的類型,其中 GL_VERTEX_SHADER 表示頂點著色器,GL_FRAGMENT_SHADER 表示片段(元)著色器。GLint sh = glCreateShader(type);if (sh == 0) {LOGD("glCreateShader %d failed!", type);return 0;}//9.指定著色器的程序內容。 第一個參數填著色器編號,第二個參數填1,表示1個著色器, 第三個參數填著色器的代碼字符串glShaderSource(sh,1, //shader數量&code, //shader代碼0); //代碼長度//10.編譯著色器的程序代碼。輸入參數填著色器編號glCompileShader(sh);//11.獲取編譯情況GLint status;glGetShaderiv(sh, GL_COMPILE_STATUS, &status);if (status == 0) {LOGD("glCompileShader failed!");return 0;}LOGD("glCompileShader success!");return sh;
}extern "C"
JNIEXPORT void JNICALL
Java_com_jack_ffmpeg_1simple02_PlayView_PlayYuv(JNIEnv *env, jobject thiz, jstring url_,jobject surface) {const char *url = env->GetStringUTFChars(url_, 0);LOGD("open url is %s", url);FILE *fp = fopen(url, "rb");if (!fp) {LOGD("open file %s failed!", url);return;}//1-7:可以看成 ***** 步驟一,初始化EGL & 讓EGL接管原生窗口ANativeWindow *****//盡管EGL本身屬于接口層,但EGL的表面對象不是憑空產生的,而是從原生窗口接管而來的。//要引入ANativeWindow,從原生窗口接管表面對象,然后才能創建用于OpenGL ES的EGL環境//1.獲取原始窗口---從Surface獲取原生窗口ANativeWindow *nwin = ANativeWindow_fromSurface(env, surface);//2.獲取EGL顯示器EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);if (display == EGL_NO_DISPLAY) {LOGD("eglGetDisplay failed!");return;}if (EGL_TRUE != eglInitialize(display, 0, 0)) {LOGD("eglInitialize failed!");return;}//3.輸出配置// EGL配置EGLConfig config;// 配置數量EGLint configNum;//配置規格,涉及RGB顏色空間EGLint configSpec[] = {EGL_RED_SIZE, 8,EGL_GREEN_SIZE, 8,EGL_BLUE_SIZE, 8,EGL_SURFACE_TYPE, EGL_WINDOW_BIT, EGL_NONE};//4.eglChooseConfig:選擇配置。給EGL顯示器選擇最佳配置。第一個參數為EGLDisplay類型的顯示器變量,第二個參數為指定了RGB顏色空間的配置規格,第三個參數為EGLConfig類型的配置變量。if (EGL_TRUE != eglChooseConfig(display, configSpec, &config, 1, &configNum)) {LOGD("eglChooseConfig failed!");return;}//5.創建EGL表面。這里EGL接管了原生窗口的表面對象EGLSurface winsurface = eglCreateWindowSurface(display, config, nwin, 0);if (winsurface == EGL_NO_SURFACE) {LOGD("eglCreateWindowSurface failed!");return;}const EGLint ctxAttr[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};//6.結合 EGL顯示器 與 EGL配置創建EGL 實例EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, ctxAttr);if (context == EGL_NO_CONTEXT) {LOGD("eglCreateContext failed!");return;}//7.創建EGL環境,之后即可執行OpenGL的相關操作。第一個參數為EGLDisplay類型的顯示器變量,第二個參數為繪制需要的EGL表面變量,第三個參數為讀取需要的EGL表面變量if (EGL_TRUE != eglMakeCurrent(display, winsurface, winsurface, context)) {LOGD("eglMakeCurrent failed!");return;}LOGD("EGL Init Success!");//8-24 可以看成 ***** 步驟二,初始化OpenGL ES *****//該部分是跟著色器紋理有關的,這三個部分可以劃分為3個小一點的步驟//8-16,劃分到步驟01中. ***** 分別依據對應小程序,初始化頂點著色器和片段著色器,并獲取著色器鏈接后的小程序編號. *****//17-19,劃分到步驟02中,***** 根據小程序編號設置 頂點坐標 和 材質坐標 *****//20-24,劃分到步驟03中,***** 分別創建Y、U、V三個分量的紋理,并分別設置三個紋理分量的規格與材質 *****//8-11,頂點和片元shader初始化//頂點shader初始化GLint vsh = InitShader(vertexShader, GL_VERTEX_SHADER);//片元yuv420 shader初始化GLint fsh = InitShader(fragYUV420P, GL_FRAGMENT_SHADER);//12.創建小程序,并返回小程序的編號GLint program = glCreateProgram();if (program == 0) {LOGD("glCreateProgram failed!");return;}//13.將著色器的編譯結果添加至小程序。第一個參數填小程序編號,第二個參數填著色器編號glAttachShader(program, vsh);glAttachShader(program, fsh);//14.鏈接著色器的小程序。輸入參數填小程序編號glLinkProgram(program);GLint status = 0;//15.檢查著色器鏈接是否成功。 第一個參數填小程序編號, 第二個參數填 GL_LINK_STATUS ,第三個參數填待返回的狀態變量。 狀態值為GL_TRUE表示成功,其他表示失敗。glGetProgramiv(program, GL_LINK_STATUS, &status);if (status != GL_TRUE) {LOGD("glLinkProgram failed!");return;}//16.使用小程序。輸入參數填小程序編號glUseProgram(program);LOGD("glLinkProgram success!");//加入三維頂點數據 兩個三角形組成正方形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,};//17.從小程序獲取屬性變量的位置索引。第一個參數填小程序編號,第二個參數填屬性變量的名稱。輸出參數為屬性變量的位置索引GLuint apos = (GLuint) glGetAttribLocation(program, "aPosition");//18.啟用頂點屬性數組。輸入參數為屬性變量的位置索引glEnableVertexAttribArray(apos);//19.指定頂點屬性數組的位置索引及其數據格式。第一個參數填屬性變量的位置索引;第二個參數填屬性的長度,對于三維空間填3,因為三維空間有x、y、z三個方向,對于二維空間填2,因為二維空間只有x、y兩個方向glVertexAttribPointer(apos, 3, GL_FLOAT, GL_FALSE, 12, vers);// 上面的三個函數要分別調用兩輪,其中第一輪的 glGetAttribLocation → glEnableVertexAttribArray → glVertexAttribPointer 設置頂點坐標,// 第二輪的glGetAttribLocation→glEnableVertexAttribArray→glVertexAttribPointer設置材質坐標。// 之所以設置完 頂點坐標 還要設置 材質坐標 ,是因為后面要往頂點組成的輪廓粘貼圖像紋理,才能呈現最終的空間景象。這里的圖像紋理就來自視頻幀的YUV圖像。//故:下方的就不做過多注釋//加入材質坐標數據static float txts[] = {1.0f, 0.0f, //右下0.0f, 0.0f,1.0f, 1.0f,0.0, 1.0};GLuint atex = (GLuint) glGetAttribLocation(program, "aTexCoord");glEnableVertexAttribArray(atex);glVertexAttribPointer(atex, 2, GL_FLOAT, GL_FALSE, 8, txts);//注意:測試,寫定的數據(要跟YUV中的寬高保持一致,否則畫面顯示會存在異常)int width = 424;int height = 240;//20.設置紋理層//glGetUniformLocation:獲取紋理在小程序中的位置。第一個參數填小程序編號,第二個參數填紋理的名稱。輸出參數為紋理在小程序中的位置//glUniform1i:設置紋理層。第一個參數為紋理在小程序中的位置,第二個參數為紋理序號glUniform1i(glGetUniformLocation(program, "yTexture"), 0); //對于紋理第1層glUniform1i(glGetUniformLocation(program, "uTexture"), 1); //對于紋理第2層glUniform1i(glGetUniformLocation(program, "vTexture"), 2); //對于紋理第3層//21.創建opengl紋理GLuint texts[3] = {0};//glGenTextures:創建紋理數組。第一個參數為數組長度,填3表示有三個色彩分量;第二個參數填待創建的紋理數組。glGenTextures(3, texts);//22.綁定指定紋理(設置紋理屬性)。第一個參數填GL_TEXTURE_2D,第二個參數填具體紋理,比如下標為0的紋理數組元素表示采用第一個分量的紋理。glBindTexture(GL_TEXTURE_2D, texts[0]);//23.設置紋理的過濾器glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);//24.設置紋理的規格與材質。glTexImage2D:對于Y分量來說,其寬高就是視頻的寬高;對U分量和V分量來說,其寬高各為視頻寬高的一半。最后一個參數填當前分量的緩存數據。// ***** 注意事項 *****//調用 glTexImage2D 函數時,最后一個參數非空表示直接渲染紋理。// 對于YUV空間來說,每個視頻幀對三個分量各自調用 glUniform1i → glActiveTexture → glBindTexture → glTexParameteri → glTexImage2D ,// 最后只要調用一次 glDrawArrays 函數即可完成該幀圖像的繪制操作。//調用 glTexImage2D 函數時,最后一個參數為空表示要分兩步渲染紋理。// 第一步,對三個分量各自調用 glUniform1i→glBindTexture → glTexParameteri → glTexImage2D,表示先占個位;// 第二步,每個視頻幀對三個分量各自調用 glActiveTexture → glBindTexture → glTexSubImage2D,表示替換當前分量的緩存數據,// 最后只要調用一次 glDrawArrays 函數即可完成該幀圖像的繪制操作。// ***** 注意事項 *****glTexImage2D(GL_TEXTURE_2D,0, //細節基本 0默認GL_LUMINANCE, //gpu內部格式 亮度,灰度圖width, height, //拉升到全屏0, //邊框GL_LUMINANCE, //數據的像素格式 亮度,灰度圖 要與上面一致GL_UNSIGNED_BYTE, //像素的數據類型NULL //紋理的數據);glBindTexture(GL_TEXTURE_2D, texts[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,GL_LUMINANCE,width / 2, height / 2,0,GL_LUMINANCE,GL_UNSIGNED_BYTE,NULL);glBindTexture(GL_TEXTURE_2D, texts[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,GL_LUMINANCE,width / 2, height / 2,0,GL_LUMINANCE,GL_UNSIGNED_BYTE,NULL);// 25-29 可以看成 ***** 步驟三 通過OpenGL ES渲染視頻畫面,輪詢每個視頻幀的時候,需要把視頻幀的YUV數據寫入對應的ES紋理 (for循環已經做了該部分) *****//輪詢每個視頻幀的時候,需要把視頻幀的YUV數據寫入對應的ES紋理//將file讀取的內容,Y U V分別使用三個數組來臨時存儲 實際項目不要這么來寫,需要通過一定的封裝unsigned char *buf[3] = {0};buf[0] = new unsigned char[width * height];buf[1] = new unsigned char[width * height / 4];buf[2] = new unsigned char[width * height / 4];//模擬操作,實際項目不要這么寫for (int i = 0; i < 10000; i++) {if (feof(fp) == 0) {fread(buf[0], 1, width * height, fp);fread(buf[1], 1, width * height / 4, fp);fread(buf[2], 1, width * height / 4, fp);}//激活第1層紋理,綁定到創建的opengl紋理//25.激活紋理glActiveTexture(GL_TEXTURE0);//26.綁定指定紋理glBindTexture(GL_TEXTURE_2D, texts[0]);//27.替換紋理內容。最后一個參數填當前分量的緩存數據glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_LUMINANCE, GL_UNSIGNED_BYTE,buf[0]);//第2層紋理 同理glActiveTexture(GL_TEXTURE0 + 1);glBindTexture(GL_TEXTURE_2D, texts[1]);glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width / 2, height / 2, GL_LUMINANCE,GL_UNSIGNED_BYTE, buf[1]);//第3層紋理同理glActiveTexture(GL_TEXTURE0 + 2);glBindTexture(GL_TEXTURE_2D, texts[2]);glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width / 2, height / 2, GL_LUMINANCE,GL_UNSIGNED_BYTE, buf[2]);//28.采用頂點的坐標數組方式繪制圖形glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);//29.將OpenGL的紋理緩存顯示到屏幕上。第一個參數為EGLDisplay類型的顯示器變量,第二個參數為EGLSurface類型的EGL表面變量。//把OpenGL ES的紋理緩存顯示到屏幕上:在調用OpenGL ES的繪制函數之后,還要調用EGL的eglSwapBuffers函數,才能把OpenGL ES的紋理緩存顯示到屏幕上eglSwapBuffers(display, winsurface);}// ***** 步驟四:釋放EGL資源 *****//視頻遍歷結束,除釋放FFmpeg相關的實例資源外,還要釋放EGL的表面和實例資源,包括EGL用到的原生窗口也要釋放。// 釋放原生窗口// 銷毀EGL表面// 銷毀EGL實例env->ReleaseStringUTFChars(url_, url);
}
- 這部分的注釋寫的非常詳細,當熟練了這塊代碼之后,可以對其進行封裝、改善,效果更好。重點重復一下著色器相關的流程,大體劃分為3步:
-
1.分別依據對應小程序,初始化頂點著色器和片段著色器,并獲取著色器鏈接后的小程序編號
- 1.InitShader(返回著色器的編號):初始化頂點著色器和片段著色器;
- 2.創建小程序,并返回小程序的編號;
- 3.鏈接著色器的小程序。輸入參數填小程序編號;
- 4.檢查著色器鏈接是否成功;
- 5.使用小程序。輸入參數填小程序編號;
-
2.根據小程序編號設置頂點坐標和材質坐標
- 1.從小程序獲取屬性變量的位置索引;
- 2.啟用頂點屬性數組;
- 3.指定頂點屬性數組的位置索引及其數據格式;
-
3.分別創建Y、U、V三個分量的紋理,并分別設置三個紋理分量的規格與材質
- 1.設置紋理層;
- 2.創建紋理數組;
- 3.綁定指定紋理;
- 4.設置紋理的過濾器;
- 5.設置紋理的規格與材質;
- 6.替換紋理內容;
- 7.采用頂點的坐標數組方式繪制圖形;
-
三.總結
- 項目代碼可以在碼云上面進行下載,6.0以上的設備需要手動開啟動態權限,這部分代碼沒有寫在項目里面。另外一個細節就是8.0及以上的設備需要做適配處理,詳細的注釋在代碼中有提及。
- 通過OpenGL ES來渲染視頻是實現Android視頻播放器的基礎,這塊的知識很有必要熟練掌握。