Android OpenGL ES视频渲染(一)GLSurfaceView
相關文章:Android OpenGL ES視頻渲染(二)EGL+OpenGL
Android中視頻渲染有幾種方式,之前的文章使用的是nativewindow(包括softwareRender)。今天介紹另一總視頻渲染的方式——OpenGL ES。
閱讀本文之前需要對OpenGL有一定的了解,可以參考https://www.jianshu.com/p/99daa25b4573
在Android中使用OpenGL的方法有兩種,一種是在native層使用EGL+OpenGL來實現,另一種則是GLSurfaceView。
本文將使用GLSurfaceView+MediaPlayer實現播放,并通過OpenGL進行簡單的濾鏡處理,以此來說明如何使用GLSurfaceView。
題外話:nativewindow和OpenGL渲染視頻的代碼,可以參考ijkplayer的實現。
OpenGL
OpenGL引擎渲染圖像的流程比較復雜,簡單來說是以下幾步。(引用自https://www.jianshu.com/p/99daa25b4573)
但我們最主要先了解頂點處理階段及片元處理階段。
階段一:指定幾何對象
所謂幾何對象,就是點,直線,三角形,這里將根據具體執行的指令繪制幾何圖元。比如,OpenGL提供給開發者的繪制方法glDrawArrays,這個方法里的第一個參數是mode,就是制定繪制方式,可選值有一下幾種。
GL_POINT:以點的形式進行繪制,通常用在繪制粒子效果的場景中。
GL_LINES:以線的形式進行繪制,通常用在繪制直線的場景中。
GL_TRIANGLE_STRIP:以三角形的形式進行繪制,所有二維圖像的渲染都會使用這種方式。
階段二:頂點處理
不論以上的幾何對象是如何指定的,所有的幾何數據都將會經過這個階段。這個階段所做的操作就是,根據模型視圖和投影矩陣進行變換來改變頂點的位置,根據紋理坐標與紋理矩陣來改變紋理坐標的位置,如果涉及三維的渲染,那么這里還要處理光照計算與法線變換。
一般輸出是以gl_Position來表示具體的頂點位置的,如果是以點來繪制幾何圖元,那么還應該輸出gl_PointSize。
階段三:圖元組裝
在經過階段二的頂點處理操作之后,還是紋理坐標都是已經確定好了的。在這個階段,頂點將會根據應用程序送往圖元的規則(如GL_POINT、GL_TRIANGLE_STRIP),將紋理組裝成圖元。
階段四:柵格化操作
由階段三傳遞過來的圖元數據,在此將會分解成更小的單元并對應于幀緩沖區的各個像素。這些單元稱為片元,一個片元可能包含窗口顏色、紋理坐標等屬性。片元的屬性是根據頂點坐標利用插值來確定的,這就是柵格化操作,也就是確認好每一個片元是什么。
階段五:片元處理
通過紋理坐標取得紋理(texture)中相對應的片元像素值(texel),根據自己的業務處理(比如提亮、飽和度調節、對比度調節、高斯模糊)來變換這個片元的顏色。這里的輸出是gl_FragColor,用于表示修改之后的像素的最終結果。
階段六:幀緩沖操作
該階段主要執行幀緩沖的寫入操作,這也是渲染管線的最后一步,負責將最終的像素值寫入到幀緩沖區中。
OpenGL ES提供了可編程的著色器來代替渲染管線的某個階段。
Vertex Shader(頂點著色器)用來替代頂點處理階段。
Fragment Shader(片元著色器,又稱為像素著色器)用來替換片元處理階段。
簡單來講就是OpenGL會在頂點著色器確定頂點的位置,然后這些頂點連起來就是我們想要的圖形。接著在片元著色器里面給這些圖形上色:
GLSurfaceView
GLSurfaceView看名字就是可以使用OpenGL的SurfaceView,也確實如此,它繼承自SurfaceView,具備SurfaceView的特性,并加入了EGL的管理,它自帶了一個GLThread繪制線程(EGLContext創建GL環境所在線程即為GL線程),繪制的工作直接通過OpenGL在繪制線程進行,不會阻塞主線程,繪制的結果輸出到SurfaceView所提供的Surface上。
所以為什么我們不直接用surfaceView來進行播放呢?有以下兩個好處:
使用流程:
創建一個GLSurfaceView用來承載視頻
->設置render(實現OpenGL著色器代碼)
->創建SurfaceTexture,綁定的外部Texture
->將SurfaceTexture的surface設置給MediaPlayer,啟動播放
->在render的onDrawFrame中更新Texture,繪制新畫面。
其中,render是最核心部分。
1、創建GLSurfaceView
<android.opengl.GLSurfaceViewandroid:id="@+id/surface_view"android:layout_width="match_parent"android:layout_height="match_parent" /> glView = findViewById(R.id.surface_view);glView.setEGLContextClientVersion(2);MyGLRender glVideoRenderer = new MyGLRender();//創建rendererglView.setRenderer(glVideoRenderer);//設置renderer創建GLSurfaceView后,設置其OpenGL版本為2.0,然后設置render。下面介紹MyGLRender。
2、創建render
render需要實現GLSurfaceView.Renderer的三個接口:
public interface Renderer {void onSurfaceCreated(GL10 var1, EGLConfig var2);void onSurfaceChanged(GL10 var1, int var2, int var3);void onDrawFrame(GL10 var1);}onSurfaceCreated進行渲染程序的初始化,創建Surface,啟動MediaPlayer
@Overridepublic void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {initGLProgram();Surface surface = crateSurface();// mediaplayer playtry {mPlayer.setSurface(surface);mPlayer.prepare();mPlayer.start();} catch (IOException e) {e.printStackTrace();}}渲染程序的初始化
initGLProgram()中創建頂點著色器和片元著色器代碼,一步步看:
頂點著色器
private final String VSH_CODE = "uniform mat4 uSTMatrix;\n"+"attribute vec4 aPosition;\n"+"attribute vec4 aTexCoord;\n"+"varying vec2 vTexCoord;\n"+"void main(){\n"+"vTexCoord = (uSTMatrix*aTexCoord).xy;\n"+"gl_Position = aPosition;\n"+"}";OpenGL會將每個頂點的坐標傳遞給頂點著色器,我們可以在這里改變頂點的位置。例如我們給每個頂點都加上一個偏移,就能實現整個圖形的移動。
aPosition為頂點坐標,賦值給gl_Position ,表示物體位置,構成圖元,可由外部傳入。
aTexCoord為紋理坐標,紋理坐標描述紋理該如何在圖元上貼圖,可由外部傳入。
vTexCoord為最終要傳遞給片元著色器的紋理坐標,為什么要在aTexCoord的基礎上進行矩陣轉換呢?這是因為計算機圖像坐標與紋理坐標的表示是不一致的。如下圖:
因為我們使用的texture是從外部得到的,其對應的是計算機坐標系,所以需要矩陣轉換,這個矩陣可通過SurfaceTexture.getTransformMatrix函數獲取到。
片元著色器
private final String FSH_CODE = "#extension GL_OES_EGL_image_external : require\n"+"precision mediump float;\n"+"varying vec2 vTexCoord;\n"+"uniform mat4 uColorMatrix;\n"+"uniform samplerExternalOES sTexture;\n"+"void main() {\n"+"gl_FragColor=uColorMatrix*texture2D(sTexture, vTexCoord).rgba;\n"+//"gl_FragColor = texture2D(sTexture, vTexCoord);\n"+"}";片元著色器要注意的是#extension GL_OES_EGL_image_external : require,因為使用的是外部紋理samplerExternalOES類型的紋理sTexture,所以需要加上。
vTexCoord是從頂點著色器傳過來的紋理坐標。
texture2D函數可以從該坐標獲取到對應的顏色,這里我們加入了顏色轉換矩陣uColorMatrix,這樣就能進行一些效果處理。最后將顏色賦值給gl_FragColor。
顏色效果矩陣如下:
private static float[] COLOR_MATRIX3 = {// 懷舊效果矩陣0.393f,0.349f, 0.272f,0.0f ,0.769f,0.686f,0.534f,0.0f,0.189f,0.168f,0.131f,0.0f,0.0f,0.0f,0.0f,1.0f};創建渲染程序
如何將兩個著色器代碼替換到渲染管線中呢,基本流程如下圖:
編譯shader程序(compileShader代碼)
創建渲染程序(buildProgram代碼)
最后調用glUseProgram,傳入渲染程序的ID就可以了。
代碼如下:
//創建shaderprivate int compileShader(int type, String code){int shaderObjectId = GLES20.glCreateShader(type);if (shaderObjectId == 0){Log.d(TAG, "compileShader: glCreateShader err");return 0;}GLES20.glShaderSource(shaderObjectId, code);GLES20.glCompileShader(shaderObjectId);int[] compileStatus = new int[1];GLES20.glGetShaderiv(shaderObjectId, GLES20.GL_COMPILE_STATUS, compileStatus, 0);if (compileStatus[0] == 0){// if it failed, delete the shader objectLog.d(TAG, "compileShader: glCompileShader err");GLES20.glDeleteShader(shaderObjectId);return 0;}Log.d(TAG, "compileShader: success: "+shaderObjectId);return shaderObjectId;}//創建渲染程序private int buildProgram(int vertexShaderId, int fragmentShaderId){int programObjectId = GLES20.glCreateProgram();if(programObjectId == 0){Log.d(TAG, "buildProgram: glCreateProgram err");return 0;}GLES20.glAttachShader(programObjectId, vertexShaderId);GLES20.glAttachShader(programObjectId, fragmentShaderId);GLES20.glLinkProgram(programObjectId);int[] linkStatus = new int[1];GLES20.glGetProgramiv(programObjectId, GLES20.GL_LINK_STATUS, linkStatus, 0);if (linkStatus[0] == 0){// if it failed, delete the shader objectGLES20.glDeleteProgram(programObjectId);Log.d(TAG, "buildProgram: glLinkProgram err");return 0;}Log.d(TAG, "buildProgram: success: "+programObjectId);return programObjectId;}填充頂點坐標及紋理坐標
完成頂點著色器及片元著色器后,創建渲染程序,接下來我們要填充頂點信息:
頂點著色器中,aPosition表示物體位置坐標,坐標系中x軸從左到右是從-1到1變化的,y軸從下到上是從-1到1變化的,物體的中心點恰好是(0,0)的位置。
aTexCoord描述紋理坐標(如上圖OpenGL二維紋理坐標),我們現在要把紋理按照,左下->右下->左上->右上的順序,貼到物體上。所以對應的頂點坐標及紋理坐標數據為:
通過 GLES20.glEnableVertexAttribArray及GLES20.glVertexAttribPointer兩個函數,完成頂點信息設置。
設置顏色效果
通過glGetUniformLocation獲取到uColorMatrix矩陣的句柄,將顏色矩陣設賦值給它就行。這樣就會在片元著色器中生效。
完整代碼:
private void initGLProgram(){int vertexShader = compileShader(GLES20.GL_VERTEX_SHADER, VSH_CODE);int fragmentShader = compileShader(GLES20.GL_FRAGMENT_SHADER, FSH_CODE);int programId = buildProgram(vertexShader, fragmentShader);if(programId == 0)return;GLES20.glUseProgram(programId);mSTMatrixHandle = GLES20.glGetUniformLocation(programId, "uSTMatrix");//轉換矩陣//頂點著色器坐標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,};FloatBuffer vertexBuffer = ByteBuffer.allocateDirect(vers.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer().put(vers);vertexBuffer.position(0);//紋理坐標,texture坐標ST,需要根據圖像進行轉換float[] txts = {0.0f, 0.0f,1.0f, 0.0f,0.0f, 1.0f,1.0f, 1.0f};FloatBuffer textureVertexBuffer = ByteBuffer.allocateDirect(txts.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer().put(txts);textureVertexBuffer.position(0);//設置頂點坐標和紋理坐標int apos = GLES20.glGetAttribLocation(programId, "aPosition");GLES20.glEnableVertexAttribArray(apos);GLES20.glVertexAttribPointer(apos, 3, GLES20.GL_FLOAT, false, 12, vertexBuffer);int atex = GLES20.glGetAttribLocation(programId, "aTexCoord");GLES20.glEnableVertexAttribArray(atex);GLES20.glVertexAttribPointer(atex, 2, GLES20.GL_FLOAT, false, 8, textureVertexBuffer);//設置顏色效果int colorMatrixHandle = GLES20.glGetUniformLocation(programId, "uColorMatrix");GLES20.glUniformMatrix4fv(colorMatrixHandle, 1, false, COLOR_MATRIX3, 0);}3、創建SurfaceTexture,綁定外部紋理
glGenTextures創建Texture,我們使用的是外部紋理,所以只需要一個即可。
glBindTexture綁定紋理,要注意這里需要設置GL_TEXTURE_EXTERNAL_OES標志。
glTexParameterf設置一些屬性,這里設置的是縮放的算法。
然后根據mTextureID創建SurfaceTexture,然后創建Surface,Surface就可以設置給MeidaPlayer。
完整代碼:
private Surface crateSurface(){// Create SurfaceTexture that will feed this textureId and pass to MediaPlayerint[] textures = new int[1];//just one texures,use external modeGLES20.glGenTextures(1, textures, 0);mTextureID = textures[0];GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureID);GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);mSurfaceTexture = new SurfaceTexture(mTextureID);mSurfaceTexture.setOnFrameAvailableListener(this);Surface surface = new Surface(mSurfaceTexture);return surface;}4、Surface設置給MediaPlayer,啟動播放
沒什么可以說道的,就是把上面創建的surface設置給播放器,同步的prepare,加上start。
// mediaplayer playtry {mPlayer.setSurface(surface);mPlayer.prepare();mPlayer.start();} catch (IOException e) {e.printStackTrace();}5、onDrawFrame中更新Texture,繪制新畫面
上面創SurfaceTexture時通過setOnFrameAvailableListener設置了監聽器,監聽紋理的更新,更新了,我們就設置isFrameUpdate為true。
onDrawFrame是render進行繪制時會調用,當isFrameUpdate為true,意味著我們可以進行繪制了。
先通過SurfaceTexture.updateTexImage()更新紋理,然后glViewport設置繪制的窗口大小。
OpenGL雖然是在Surface上繪制,但我們可以不鋪滿整個Surface,可以只在它的某部分繪制,例如我們可以用下面代碼只用TextureSurface的左下角的四分之一去顯示OpenGL的畫面:
//width、height是TextureView的寬高 GLES20.glViewport(0, 0, width/2, height/2);我們這里還是鋪滿整個View,寬高可以在onSurfaceChanged中獲取到。
繪制前先清除上一幀,
//clearGLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);當然這里還可以再清空片元著色器的外部紋理。
設置紋理變換矩陣,矩陣在SurfaceTexture.getTransformMatrix獲取到
激活綁定紋理,然后就可以繪制了。
繪制采用的三角形方式GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
render缺省模式是 RENDERMODE_CONTINUOUSLY,就是說 surface繪制線程不停循環調用onDrawFrame。所以幀頻控制取決于每幀的繪制時間,通常都是在onDrawFrame里加延時來控制的。
當設置為RENDERMODE_WHEN_DIRTY時,就是通常的事件驅動模式來繪制。畫面重新顯示出來或 requestRender()時才會調用onDrawFrame.
完整代碼如下:
@Overridepublic void onSurfaceChanged(GL10 gl10, int width, int height) {screenWidth = width;screenHeight = height;}@Overridepublic void onDrawFrame(GL10 gl10) {synchronized (this){if(isFrameUpdate){mSurfaceTexture.updateTexImage();mSurfaceTexture.getTransformMatrix(mSTMatrix);isFrameUpdate = false;}}//update width and heightGLES20.glViewport(0, 0, screenWidth, screenHeight);//clearGLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);//update st mat4GLES20.glUniformMatrix4fv(mSTMatrixHandle, 1, false, mSTMatrix, 0);//bind and active, juest one time{GLES20.glActiveTexture(GLES20.GL_TEXTURE0);GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureID);}//drawGLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);}@Overridepublic void onFrameAvailable(SurfaceTexture surfaceTexture) {isFrameUpdate = true;}總結
播放效果如下:
下一章會描述如何在native層使用EGL和OpenGL,這樣會對Android OpenGL ES視頻渲染有更深入的了解。
總結
以上是生活随笔為你收集整理的Android OpenGL ES视频渲染(一)GLSurfaceView的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Cocos2d-x 学习笔记(11.1)
- 下一篇: (已解决)Android Studio