教你用OpenGL,實現微信雙視頻

碼個蛋(codeegg)第 776 次推文

原文: https://blog.csdn.net/a296777513/article/details/70495534

前言

想做的有很多,奈何能力實在有限,所以只能一步一步來,將自己做出來的儘量用簡單易懂的語言描述出來,希望自己總結的對閱讀這篇文章的同學有所幫助。

在上一篇文章 [Android OpenGL 使用 GLSurfaceView 預覽視頻](https://blog.csdn.net/a296777513/article/details/63685658)中講述了怎樣在 GLSurfaceView 上預覽 Camera 的視頻數據,在本章中打算實現一個類似微信視頻通話的效果,微信視頻通話主要有大小兩個視頻數據渲染(自己的視頻和對端的視頻),手指點擊小視頻,可以切換大視頻和小視頻的位置,可以拖動小視頻。

第一章 渲染多個視頻流數據

第一次看到這個功能,大部分人的第一個解決方案,就是創建多個 View,每個 View 渲染一條視頻數據,這樣是可行的,但是如果是多人視頻呢?20 個人就需要創建 20 個 GLSurfaceView,這樣顯然是不可行的,所以最好的辦法就是將所有的數據流都繪製在同一個 GLSurfaceView 上,這樣只需要控制 OpenGL 來控制視頻繪製的大小的位置就可以解決,這樣很大程度上節省了內存,提升了效率。

首先看一下實際的效果圖:

教你用OpenGL,实现微信双视频
教你用OpenGL,实现微信双视频

可以看到小視頻是疊加在大視頻上面的,雖然看上去好像是兩個 view,但是其實所有的圖像都是繪製在同一個 GLSurfaceView 上的,我們需要做的就是計算出每個小圖縮放的比例,然後計算出每個小圖擺放的位置,視頻 OpengGL 的一些方法將視頻渲染的位置繪製到相應的位置上。

首先,在函數 onDrawFrame 中繪製出所需要繪製的視頻數據。

<code> @Override/<code><code> public void onDrawFrame(GL10 gl) {/<code><code> // TODO Auto-generated method stub/<code><code> LOG.logI("onDrawFrame...");/<code><code> // 設置白色為清屏/<code><code> GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);/<code><code> // 清除屏幕和深度緩存/<code><code> GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);/<code><code> // 更新紋理/<code><code> mSurface.updateTexImage;/<code>
<code> // mDirectDrawers中有兩個對象,一個是繪製Camera傳遞過來的數據,一個是繪製由bitmap轉換成的紋理/<code><code> for (int i = 0; i < mDirectDrawers.size; i++) {/<code><code> DirectDrawer directDrawer = mDirectDrawers.get(i);/<code><code> if (i == 0) {/<code><code> directDrawer.resetMatrix;/<code><code> } else {/<code><code> directDrawer.calculateMatrix(mThumbnailRect, mScreenWidth, mScreenHeight);/<code><code> }/<code><code> directDrawer.draw;/<code><code> }/<code><code> }/<code>

從上面的代碼可以看出,directDrawer.draw 調用了兩次,也就是說 OpenGL 在這個 GLSurfaceView 上繪製了兩次,但是如果不做處理的話,第二個視頻渲染會覆蓋第一個效果。這裡我們需要對第二個視頻流做一些處理:

縮小:如下面的代碼所示,我們將視頻轉換的矩陣存儲在一個 16 位的數組中,即 mMVP,我們需要在每次計算之前調用 setIdentityM,這行代碼的意思是將數據初始化到開始的位置和大小,因為每次縮小都是相對於初始的狀態,接下來我們計算 x 軸和 y 軸的縮小比例,這裡我定義的是縮小 1/4,然後調用 scaleM 就可以得到縮小後的比例,大概的過程如下圖所示:

<code>Matrix.setIdentityM(mMVP, 0);/<code><code>float scaleX = 1f / 4f;/<code><code>float scaleY = 1f / 4f;/<code><code>Matrix.scaleM(mMVP, 0, scaleX, scaleY, 0);/<code>
教你用OpenGL,实现微信双视频

可以從上圖中看到,虛線為原來圖像的大小,經過縮小後變為實線矩形的大小

移動:小視頻的初始化位置是左下方,所以需要將縮小後的視頻移動到左下方,代碼如下:

<code>float ratioX = (rectF.left - .5f * (1 - scaleX) * screenWidth) / rectF.width;/<code><code>float ratioY = (rectF.top - .5f * (1 + scaleY) * screenHeight) / rectF.height;/<code><code>Matrix.translateM(mMVP, 0, ratioX * 2, ratioY * 2, 0f);/<code>

大致的過程如下圖:

教你用OpenGL,实现微信双视频

至此,在同一個 GLSurfaceView 上繪製兩個視頻數據,並且將第二個視頻縮小和移動的過程就敘述完了,由於上面是將縮小和移動分開來講,其實縮小和移動的代碼是在一起的:

<code>public void calculateMatrix(RectF rectF, float screenWidth, float screenHeight) {/<code><code> Matrix.setIdentityM(mMVP, 0);/<code><code> float scaleX = 1f / 4f;/<code><code> float scaleY = 1f / 4f;/<code><code> float ratioX = (rectF.left - .5f * (1 - scaleX) * screenWidth) / rectF.width;/<code><code> float ratioY = (rectF.top - .5f * (1 + scaleY) * screenHeight) / rectF.height;/<code><code> Matrix.scaleM(mMVP, 0, scaleX, scaleY, 0);/<code><code> Matrix.translateM(mMVP, 0, ratioX * 2, ratioY * 2, 0f);/<code><code> }/<code>

第二章 滑動視頻

移動小視頻還是比較簡單的,上一章節已經敘述了根據小視頻的位置 (Rect),來對小視頻進行縮小和移動,所以我們只需要根據手機滑動來改變小視頻的位置即可。下面給出移動視頻的主要代碼。

<code>@Override/<code><code>public boolean onTouchEvent(MotionEvent event)/<code><code> switch (event.getAction) {/<code><code> case MotionEvent.ACTION_DOWN:/<code><code> mDownX = event.getX;/<code><code> mDownY = event.getY;/<code><code> if (mDownX > mThumbnailRect.left && mDownX < mThumbnailRect.right/<code><code> && mDownY > mThumbnailRect.bottom && mDownY < mThumbnailRect.top) {/<code><code> mTouchThumbnail = true;/<code><code> mLastYLength = 0;/<code><code> mLastXLength = 0;/<code><code> return true;/<code><code> } else {/<code><code> mTouchThumbnail = false;/<code><code> }/<code>
<code> break;/<code><code> case MotionEvent.ACTION_MOVE:/<code><code> float moveX = event.getX;/<code><code> float moveY = event.getY;/<code><code> if (mTouchThumbnail) {/<code><code> float lengthX = Math.abs(mDownX - moveX);/<code><code> float lengthY = Math.abs(mDownY - moveY);/<code><code> float length = (float) Math.sqrt(Math.pow(lengthX, 2) + Math.pow(lengthY, 2));/<code><code> if (length > mTouchSlop) {/<code><code> moveView(mThumbnailRect, mDownY - moveY, moveX - mDownX);/<code><code> isMoveThumbnail = true;/<code><code> } else {/<code><code> isMoveThumbnail = false;/<code><code> }/<code><code> return true;/<code><code> }/<code><code> break;/<code><code> case MotionEvent.ACTION_UP:/<code><code> if (mTouchThumbnail) {/<code><code> mLastYLength = 0;/<code><code> mLastXLength = 0;/<code><code> //抬起手指時,如果不是移動小視頻,那麼就是點擊小視頻/<code><code> if (!isMoveThumbnail) {/<code><code> changeThumbnailPosition;/<code><code> }/<code><code> return true;/<code><code> }/<code><code> break;/<code><code> }/<code><code> return super.onTouchEvent(event);/<code><code> }/<code>
  1. 判斷手指按下的位置是否在小視頻的區域中,如果在,則記錄按下的 X 和 Y 的座標值,然後將 mTouchThumbnail 置為 true。

  2. 如果手指移動的距離超過 Android 定義的最小移動距離,則開始改變小視頻的位置,否則判斷這次觸摸事件為點擊小視頻。

  3. 根據 X 軸移動的距離和 Y 軸移動的距離改變小視頻的位置,然後在 OpenGL 繪製過程中移動小視頻。

第三章 創建紋理

因為這裡需要實現兩個視頻數據的渲染,由於現在只能獲取攝像頭的數據,另一個為了更加直觀的顯示出效果,這裡用一個 bitmap 的紋理來代替,以後有了其他視頻的數據,用相應的紋理代替即可。

<code>public static int loadTexture(Bitmap bitmap) {/<code><code> if (bitmap ==  || bitmap.isRecycled) {/<code><code> return 0;/<code><code> }/<code>
<code> int texture = new int[1];/<code>
<code> glGenTextures(1, texture, 0);/<code>
<code> if (texture[0] == 0) {/<code><code> return 0;/<code><code> }/<code>

<code> // Bind to the texture in OpenGL/<code><code> GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture[0]);/<code><code> // Configure min/mag filtering, i.e. what scaling method do we use if what we're rendering/<code><code> // is smaller or larger than the source image./<code><code> GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,/<code><code> GLES20.GL_TEXTURE_MIN_FILTER,GLES20.GL_LINEAR);/<code><code> GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,/<code><code> GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);/<code><code> GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,/<code><code> GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);/<code><code> GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,/<code><code> GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);/<code><code> // Load the bitmap into the bound texture./<code><code> texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);/<code>
<code> return texture[0];/<code><code> }/<code>

glTexParameterf(int target, int pname, float param) 函數用來確定如何把圖象從紋理圖象空間映射到幀緩衝圖象空間 (如:映射時為了避免多邊形上的圖像失真,而重新構造紋理圖像等)。

target

目標紋理 (target),必須為 GL_TEXTURE_1D 或 GL_TEXTURE_2D 或這是 GL_TEXTURE_3D;

pname

  • 過濾器 (pname):GL_TEXTURE_MAG_FILTER(紋理放大時), GL_TEXTURE_MIN_FILTER(紋理縮小時)

  • 環繞方向 (pname):GL_TEXTURE_WRAP_S, GL_TEXTURE_WRAP_T, GL_TEXTURE_WRAP_R 分別為 x,y,z 方向。

param

pname 為過濾器時的參數:GL_NEARST(最鄰近的像素),GL_LINEAR(線性插值)

pname 為環繞方向時的參數:(以下,n 為紋理方向上的紋理的長度)

  • GL_REPEAT:相當於忽略掉紋理座標的整數部分。濾鏡為線性時,處於 [1/2n,1] 與第一個紋理像素融合。處於 [0,1/2n] 與最後一個像素融合。

  • GL_MIRRORED_REPEAT:相當於將紋理座標 1.1 變成 0.9,達到鏡像反射的效果。

  • GL_CLAMP:截取紋理座標到 [0,1] 。將導致紋理座標處於 [1-1/2n, 1] 的像素,在紋理濾鏡為線性濾鏡時,與 border 融合,最終紋理座標為 1 的像素,將為 border 和邊界像素的中值。

  • GL_CLAMP_TO_EDGE:截取紋理座標到 [1/2n,1-1/2n]。將導致永遠不會與 border 融合。

  • GL_CLAMP_TO_BORDER:截取紋理座標到 [-1/2n,1+1/2n]。將導致紋理座標處於 [1-1/2n,1+1/2n] 範圍內的像素,在紋理濾鏡為線性濾鏡時,與 border 融合,最終紋理座標為 1+1/2n 的像素將於 border 同色。

總結

這篇文章講了實現類似微信視頻聊天的三個功能的大體實現思路,和一些基本的知識點的介紹。

代碼奉上:https://github.com/296777513/AndroidOpenGL

今日問題:

大家用OpenGL做過什麼功能?


分享到:


相關文章: