Android OpenGL ES视频渲染(一)GLSurfaceView
相關(guān)文章:Android OpenGL ES視頻渲染(二)EGL+OpenGL
Android中視頻渲染有幾種方式,之前的文章使用的是nativewindow(包括softwareRender)。今天介紹另一總視頻渲染的方式——OpenGL ES。
 閱讀本文之前需要對OpenGL有一定的了解,可以參考https://www.jianshu.com/p/99daa25b4573
在Android中使用OpenGL的方法有兩種,一種是在native層使用EGL+OpenGL來實(shí)現(xiàn),另一種則是GLSurfaceView。
 本文將使用GLSurfaceView+MediaPlayer實(shí)現(xiàn)播放,并通過OpenGL進(jìn)行簡單的濾鏡處理,以此來說明如何使用GLSurfaceView。
題外話:nativewindow和OpenGL渲染視頻的代碼,可以參考ijkplayer的實(shí)現(xiàn)。
OpenGL
OpenGL引擎渲染圖像的流程比較復(fù)雜,簡單來說是以下幾步。(引用自https://www.jianshu.com/p/99daa25b4573)
 但我們最主要先了解頂點(diǎn)處理階段及片元處理階段。
階段一:指定幾何對象
 所謂幾何對象,就是點(diǎn),直線,三角形,這里將根據(jù)具體執(zhí)行的指令繪制幾何圖元。比如,OpenGL提供給開發(fā)者的繪制方法glDrawArrays,這個(gè)方法里的第一個(gè)參數(shù)是mode,就是制定繪制方式,可選值有一下幾種。
GL_POINT:以點(diǎn)的形式進(jìn)行繪制,通常用在繪制粒子效果的場景中。
 GL_LINES:以線的形式進(jìn)行繪制,通常用在繪制直線的場景中。
 GL_TRIANGLE_STRIP:以三角形的形式進(jìn)行繪制,所有二維圖像的渲染都會(huì)使用這種方式。
階段二:頂點(diǎn)處理
 不論以上的幾何對象是如何指定的,所有的幾何數(shù)據(jù)都將會(huì)經(jīng)過這個(gè)階段。這個(gè)階段所做的操作就是,根據(jù)模型視圖和投影矩陣進(jìn)行變換來改變頂點(diǎn)的位置,根據(jù)紋理坐標(biāo)與紋理矩陣來改變紋理坐標(biāo)的位置,如果涉及三維的渲染,那么這里還要處理光照計(jì)算與法線變換。
 一般輸出是以gl_Position來表示具體的頂點(diǎn)位置的,如果是以點(diǎn)來繪制幾何圖元,那么還應(yīng)該輸出gl_PointSize。
階段三:圖元組裝
 在經(jīng)過階段二的頂點(diǎn)處理操作之后,還是紋理坐標(biāo)都是已經(jīng)確定好了的。在這個(gè)階段,頂點(diǎn)將會(huì)根據(jù)應(yīng)用程序送往圖元的規(guī)則(如GL_POINT、GL_TRIANGLE_STRIP),將紋理組裝成圖元。
階段四:柵格化操作
 由階段三傳遞過來的圖元數(shù)據(jù),在此將會(huì)分解成更小的單元并對應(yīng)于幀緩沖區(qū)的各個(gè)像素。這些單元稱為片元,一個(gè)片元可能包含窗口顏色、紋理坐標(biāo)等屬性。片元的屬性是根據(jù)頂點(diǎn)坐標(biāo)利用插值來確定的,這就是柵格化操作,也就是確認(rèn)好每一個(gè)片元是什么。
階段五:片元處理
 通過紋理坐標(biāo)取得紋理(texture)中相對應(yīng)的片元像素值(texel),根據(jù)自己的業(yè)務(wù)處理(比如提亮、飽和度調(diào)節(jié)、對比度調(diào)節(jié)、高斯模糊)來變換這個(gè)片元的顏色。這里的輸出是gl_FragColor,用于表示修改之后的像素的最終結(jié)果。
階段六:幀緩沖操作
 該階段主要執(zhí)行幀緩沖的寫入操作,這也是渲染管線的最后一步,負(fù)責(zé)將最終的像素值寫入到幀緩沖區(qū)中。
OpenGL ES提供了可編程的著色器來代替渲染管線的某個(gè)階段。
 Vertex Shader(頂點(diǎn)著色器)用來替代頂點(diǎn)處理階段。
 Fragment Shader(片元著色器,又稱為像素著色器)用來替換片元處理階段。
簡單來講就是OpenGL會(huì)在頂點(diǎn)著色器確定頂點(diǎn)的位置,然后這些頂點(diǎn)連起來就是我們想要的圖形。接著在片元著色器里面給這些圖形上色:
GLSurfaceView
GLSurfaceView看名字就是可以使用OpenGL的SurfaceView,也確實(shí)如此,它繼承自SurfaceView,具備SurfaceView的特性,并加入了EGL的管理,它自帶了一個(gè)GLThread繪制線程(EGLContext創(chuàng)建GL環(huán)境所在線程即為GL線程),繪制的工作直接通過OpenGL在繪制線程進(jìn)行,不會(huì)阻塞主線程,繪制的結(jié)果輸出到SurfaceView所提供的Surface上。
 所以為什么我們不直接用surfaceView來進(jìn)行播放呢?有以下兩個(gè)好處:
使用流程:
創(chuàng)建一個(gè)GLSurfaceView用來承載視頻
 ->設(shè)置render(實(shí)現(xiàn)OpenGL著色器代碼)
 ->創(chuàng)建SurfaceTexture,綁定的外部Texture
 ->將SurfaceTexture的surface設(shè)置給MediaPlayer,啟動(dòng)播放
 ->在render的onDrawFrame中更新Texture,繪制新畫面。
其中,render是最核心部分。
1、創(chuàng)建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();//創(chuàng)建rendererglView.setRenderer(glVideoRenderer);//設(shè)置renderer創(chuàng)建GLSurfaceView后,設(shè)置其OpenGL版本為2.0,然后設(shè)置render。下面介紹MyGLRender。
2、創(chuàng)建render
render需要實(shí)現(xiàn)GLSurfaceView.Renderer的三個(gè)接口:
public interface Renderer {void onSurfaceCreated(GL10 var1, EGLConfig var2);void onSurfaceChanged(GL10 var1, int var2, int var3);void onDrawFrame(GL10 var1);}onSurfaceCreated進(jìn)行渲染程序的初始化,創(chuàng)建Surface,啟動(dòng)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()中創(chuàng)建頂點(diǎn)著色器和片元著色器代碼,一步步看:
頂點(diǎn)著色器
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會(huì)將每個(gè)頂點(diǎn)的坐標(biāo)傳遞給頂點(diǎn)著色器,我們可以在這里改變頂點(diǎn)的位置。例如我們給每個(gè)頂點(diǎn)都加上一個(gè)偏移,就能實(shí)現(xiàn)整個(gè)圖形的移動(dòng)。
aPosition為頂點(diǎn)坐標(biāo),賦值給gl_Position ,表示物體位置,構(gòu)成圖元,可由外部傳入。
 aTexCoord為紋理坐標(biāo),紋理坐標(biāo)描述紋理該如何在圖元上貼圖,可由外部傳入。
 vTexCoord為最終要傳遞給片元著色器的紋理坐標(biāo),為什么要在aTexCoord的基礎(chǔ)上進(jìn)行矩陣轉(zhuǎn)換呢?這是因?yàn)橛?jì)算機(jī)圖像坐標(biāo)與紋理坐標(biāo)的表示是不一致的。如下圖:
 
 因?yàn)槲覀兪褂玫膖exture是從外部得到的,其對應(yīng)的是計(jì)算機(jī)坐標(biāo)系,所以需要矩陣轉(zhuǎn)換,這個(gè)矩陣可通過SurfaceTexture.getTransformMatrix函數(shù)獲取到。
片元著色器
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,因?yàn)槭褂玫氖峭獠考y理samplerExternalOES類型的紋理sTexture,所以需要加上。
 vTexCoord是從頂點(diǎn)著色器傳過來的紋理坐標(biāo)。
 texture2D函數(shù)可以從該坐標(biāo)獲取到對應(yīng)的顏色,這里我們加入了顏色轉(zhuǎn)換矩陣uColorMatrix,這樣就能進(jìn)行一些效果處理。最后將顏色賦值給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};創(chuàng)建渲染程序
 如何將兩個(gè)著色器代碼替換到渲染管線中呢,基本流程如下圖:
 
 編譯shader程序(compileShader代碼)
創(chuàng)建渲染程序(buildProgram代碼)
最后調(diào)用glUseProgram,傳入渲染程序的ID就可以了。
代碼如下:
//創(chuàng)建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;}//創(chuàng)建渲染程序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;}填充頂點(diǎn)坐標(biāo)及紋理坐標(biāo)
 完成頂點(diǎn)著色器及片元著色器后,創(chuàng)建渲染程序,接下來我們要填充頂點(diǎn)信息:
 頂點(diǎn)著色器中,aPosition表示物體位置坐標(biāo),坐標(biāo)系中x軸從左到右是從-1到1變化的,y軸從下到上是從-1到1變化的,物體的中心點(diǎn)恰好是(0,0)的位置。
 
 aTexCoord描述紋理坐標(biāo)(如上圖OpenGL二維紋理坐標(biāo)),我們現(xiàn)在要把紋理按照,左下->右下->左上->右上的順序,貼到物體上。所以對應(yīng)的頂點(diǎn)坐標(biāo)及紋理坐標(biāo)數(shù)據(jù)為:
通過 GLES20.glEnableVertexAttribArray及GLES20.glVertexAttribPointer兩個(gè)函數(shù),完成頂點(diǎn)信息設(shè)置。
設(shè)置顏色效果
 通過glGetUniformLocation獲取到uColorMatrix矩陣的句柄,將顏色矩陣設(shè)賦值給它就行。這樣就會(huì)在片元著色器中生效。
完整代碼:
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");//轉(zhuǎn)換矩陣//頂點(diǎn)著色器坐標(biāo)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);//紋理坐標(biāo),texture坐標(biāo)ST,需要根據(jù)圖像進(jìn)行轉(zhuǎn)換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);//設(shè)置頂點(diǎn)坐標(biāo)和紋理坐標(biāo)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);//設(shè)置顏色效果int colorMatrixHandle = GLES20.glGetUniformLocation(programId, "uColorMatrix");GLES20.glUniformMatrix4fv(colorMatrixHandle, 1, false, COLOR_MATRIX3, 0);}3、創(chuàng)建SurfaceTexture,綁定外部紋理
glGenTextures創(chuàng)建Texture,我們使用的是外部紋理,所以只需要一個(gè)即可。
 glBindTexture綁定紋理,要注意這里需要設(shè)置GL_TEXTURE_EXTERNAL_OES標(biāo)志。
 glTexParameterf設(shè)置一些屬性,這里設(shè)置的是縮放的算法。
 然后根據(jù)mTextureID創(chuàng)建SurfaceTexture,然后創(chuàng)建Surface,Surface就可以設(shè)置給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設(shè)置給MediaPlayer,啟動(dòng)播放
沒什么可以說道的,就是把上面創(chuàng)建的surface設(shè)置給播放器,同步的prepare,加上start。
// mediaplayer playtry {mPlayer.setSurface(surface);mPlayer.prepare();mPlayer.start();} catch (IOException e) {e.printStackTrace();}5、onDrawFrame中更新Texture,繪制新畫面
上面創(chuàng)SurfaceTexture時(shí)通過setOnFrameAvailableListener設(shè)置了監(jiān)聽器,監(jiān)聽紋理的更新,更新了,我們就設(shè)置isFrameUpdate為true。
 onDrawFrame是render進(jìn)行繪制時(shí)會(huì)調(diào)用,當(dāng)isFrameUpdate為true,意味著我們可以進(jìn)行繪制了。
先通過SurfaceTexture.updateTexImage()更新紋理,然后glViewport設(shè)置繪制的窗口大小。
OpenGL雖然是在Surface上繪制,但我們可以不鋪滿整個(gè)Surface,可以只在它的某部分繪制,例如我們可以用下面代碼只用TextureSurface的左下角的四分之一去顯示OpenGL的畫面:
//width、height是TextureView的寬高 GLES20.glViewport(0, 0, width/2, height/2);我們這里還是鋪滿整個(gè)View,寬高可以在onSurfaceChanged中獲取到。
繪制前先清除上一幀,
//clearGLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);當(dāng)然這里還可以再清空片元著色器的外部紋理。
設(shè)置紋理變換矩陣,矩陣在SurfaceTexture.getTransformMatrix獲取到
 激活綁定紋理,然后就可以繪制了。
 繪制采用的三角形方式GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
render缺省模式是 RENDERMODE_CONTINUOUSLY,就是說 surface繪制線程不停循環(huán)調(diào)用onDrawFrame。所以幀頻控制取決于每幀的繪制時(shí)間,通常都是在onDrawFrame里加延時(shí)來控制的。
 當(dāng)設(shè)置為RENDERMODE_WHEN_DIRTY時(shí),就是通常的事件驅(qū)動(dòng)模式來繪制。畫面重新顯示出來或 requestRender()時(shí)才會(huì)調(diào)用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;}總結(jié)
播放效果如下:
 
 下一章會(huì)描述如何在native層使用EGL和OpenGL,這樣會(huì)對Android OpenGL ES視頻渲染有更深入的了解。
總結(jié)
以上是生活随笔為你收集整理的Android OpenGL ES视频渲染(一)GLSurfaceView的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
                            
                        - 上一篇: Cocos2d-x 学习笔记(11.1)
 - 下一篇: C++Primer 第9章 顺序容器