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

码个蛋(codeegg)第 776 次推文

作者: a296777513

原文: 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 来控制视频绘制的大小的位置就可以解决,这样很大程度上节省了内存,提升了效率。

首先看一下实际的效果图:

可以看到小视频是叠加在大视频上面的,虽然看上去好像是两个 view,但是其实所有的图像都是绘制在同一个 GLSurfaceView 上的,我们需要做的就是计算出每个小图缩放的比例,然后计算出每个小图摆放的位置,视频 OpengGL 的一些方法将视频渲染的位置绘制到相应的位置上。

首先,在函数 onDrawFrame 中绘制出所需要绘制的视频数据。

 @Override
    public void onDrawFrame(GL10 gl) {
        // TODO Auto-generated method stub
        LOG.logI("onDrawFrame...");
        // 设置白色为清屏
        GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
        // 清除屏幕和深度缓存
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
        // 更新纹理
        mSurface.updateTexImage();


        // mDirectDrawers中有两个对象,一个是绘制Camera传递过来的数据,一个是绘制由bitmap转换成的纹理
        for (int i = 0; i < mDirectDrawers.size(); i++) {
            DirectDrawer directDrawer = mDirectDrawers.get(i);
            if (i == 0) {
                directDrawer.resetMatrix();
            } else {
                directDrawer.calculateMatrix(mThumbnailRect, mScreenWidth, mScreenHeight);
            }
            directDrawer.draw();
        }
    }

从上面的代码可以看出,directDrawer.draw() 调用了两次,也就是说 OpenGL 在这个 GLSurfaceView 上绘制了两次,但是如果不做处理的话,第二个视频渲染会覆盖第一个效果。这里我们需要对第二个视频流做一些处理:

缩小:如下面的代码所示,我们将视频转换的矩阵存储在一个 16 位的数组中,即 mMVP,我们需要在每次计算之前调用 setIdentityM(),这行代码的意思是将数据初始化到开始的位置和大小,因为每次缩小都是相对于初始的状态,接下来我们计算 x 轴和 y 轴的缩小比例,这里我定义的是缩小 1/4,然后调用 scaleM 就可以得到缩小后的比例,大概的过程如下图所示:

Matrix.setIdentityM(mMVP, 0);
float scaleX = 1f / 4f;
float scaleY = 1f / 4f;
Matrix.scaleM(mMVP, 0, scaleX, scaleY, 0);

可以从上图中看到,虚线为原来图像的大小,经过缩小后变为实线矩形的大小

移动:小视频的初始化位置是左下方,所以需要将缩小后的视频移动到左下方,代码如下:

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

大致的过程如下图:

至此,在同一个 GLSurfaceView 上绘制两个视频数据,并且将第二个视频缩小和移动的过程就叙述完了,由于上面是将缩小和移动分开来讲,其实缩小和移动的代码是在一起的:

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

第二章 滑动视频

移动小视频还是比较简单的,上一章节已经叙述了根据小视频的位置 (Rect),来对小视频进行缩小和移动,所以我们只需要根据手机滑动来改变小视频的位置即可。下面给出移动视频的主要代码。

@Override
public boolean onTouchEvent(MotionEvent event)
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mDownX = event.getX();
                mDownY = event.getY();
                if (mDownX > mThumbnailRect.left && mDownX < mThumbnailRect.right
                        && mDownY > mThumbnailRect.bottom && mDownY < mThumbnailRect.top) {
                    mTouchThumbnail = true;
                    mLastYLength = 0;
                    mLastXLength = 0;
                    return true;
                } else {
                    mTouchThumbnail = false;
                }


                break;
            case MotionEvent.ACTION_MOVE:
                float moveX = event.getX();
                float moveY = event.getY();
                if (mTouchThumbnail) {
                    float lengthX = Math.abs(mDownX - moveX);
                    float lengthY = Math.abs(mDownY - moveY);
                    float length = (float) Math.sqrt(Math.pow(lengthX, 2) + Math.pow(lengthY, 2));
                    if (length > mTouchSlop) {
                        moveView(mThumbnailRect, mDownY - moveY, moveX - mDownX);
                        isMoveThumbnail = true;
                    } else {
                        isMoveThumbnail = false;
                    }
                    return true;
                }
                break;
            case MotionEvent.ACTION_UP:
                if (mTouchThumbnail) {
                    mLastYLength = 0;
                    mLastXLength = 0;
                    //抬起手指时,如果不是移动小视频,那么就是点击小视频
                    if (!isMoveThumbnail) {
                        changeThumbnailPosition();
                    }
                    return true;
                }
                break;
        }
        return super.onTouchEvent(event);
    }
  1. 判断手指按下的位置是否在小视频的区域中,如果在,则记录按下的 X 和 Y 的坐标值,然后将 mTouchThumbnail 置为 true。

  2. 如果手指移动的距离超过 Android 定义的最小移动距离,则开始改变小视频的位置,否则判断这次触摸事件为点击小视频。

  3. 根据 X 轴移动的距离和 Y 轴移动的距离改变小视频的位置,然后在 OpenGL 绘制过程中移动小视频。

第三章 创建纹理

因为这里需要实现两个视频数据的渲染,由于现在只能获取摄像头的数据,另一个为了更加直观的显示出效果,这里用一个 bitmap 的纹理来代替,以后有了其他视频的数据,用相应的纹理代替即可。

public static int loadTexture(Bitmap bitmap) {
        if (bitmap == null || bitmap.isRecycled()) {
            return 0;
        }


        int[] texture = new int[1];


        glGenTextures(1, texture, 0);


        if (texture[0] == 0) {
            return 0;
        }


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


        return texture[0];
    }

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做过什么功能?

专属升级社区:《这件事情,我终于想明白了》 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值