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進行視頻渲染,可以使用GPU加速,相對於SurfaceView使用畫布進行繪製,OpenGL的繪製關聯到GPU,效率更高。
- 可以定製render(渲染器),從而可以實現定製效果。
使用流程:
創建一個GLSurfaceView用來承載視頻
->設置render(實現OpenGL着色器代碼)
->創建SurfaceTexture,綁定的外部Texture
->將SurfaceTexture的surface設置給MediaPlayer,啓動播放
->在render的onDrawFrame中更新Texture,繪製新畫面。
其中,render是最核心部分。
1、創建GLSurfaceView
<android.opengl.GLSurfaceView
android: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();//創建renderer
glView.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
@Override
public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
initGLProgram();
Surface surface = crateSurface();
// mediaplayer play
try {
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
};
填充頂點座標及紋理座標
完成頂點着色器及片元着色器後,創建渲染程序,接下來我們要填充頂點信息:
頂點着色器中,aPosition表示物體位置座標,座標系中x軸從左到右是從-1到1變化的,y軸從下到上是從-1到1變化的,物體的中心點恰好是(0,0)的位置。
aTexCoord描述紋理座標(如上圖OpenGL二維紋理座標),我們現在要把紋理按照,左下->右下->左上->右上的順序,貼到物體上。所以對應的頂點座標及紋理座標數據爲:
//頂點着色器座標,z爲0
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,
};
//紋理座標,texture座標ST,需要根據圖像進行轉換
float[] txts = {
0.0f, 0.0f,
1.0f, 0.0f,
0.0f, 1.0f,
1.0f, 1.0f
};
通過 GLES20.glEnableVertexAttribArray及GLES20.glVertexAttribPointer兩個函數,完成頂點信息設置。
設置顏色效果
通過glGetUniformLocation獲取到uColorMatrix矩陣的句柄,將顏色矩陣設賦值給它就行。這樣就會在片元着色器中生效。
//設置顏色效果
int colorMatrixHandle = GLES20.glGetUniformLocation(programId, "uColorMatrix");
GLES20.glUniformMatrix4fv(colorMatrixHandle, 1, false, COLOR_MATRIX3, 0);
完整代碼:
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 MediaPlayer
int[] textures = new int[1];//just one texures,use external mode
GLES20.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 play
try {
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中獲取到。
繪製前先清除上一幀,
//clear
GLES20.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);
完整代碼如下:
@Override
public void onSurfaceChanged(GL10 gl10, int width, int height) {
screenWidth = width;
screenHeight = height;
}
@Override
public void onDrawFrame(GL10 gl10) {
synchronized (this){
if(isFrameUpdate){
mSurfaceTexture.updateTexImage();
mSurfaceTexture.getTransformMatrix(mSTMatrix);
isFrameUpdate = false;
}
}
//update width and height
GLES20.glViewport(0, 0, screenWidth, screenHeight);
//clear
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
//update st mat4
GLES20.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);
}
//draw
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
}
@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
isFrameUpdate = true;
}
總結
播放效果如下:
下一章會描述如何在native層使用EGL和OpenGL,這樣會對Android OpenGL ES視頻渲染有更深入的瞭解。