Android OpenGL ES视频渲染(一)GLSurfaceView

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来进行播放呢?有以下两个好处:

  1. 通过GLSurfaceView进行视频渲染,可以使用GPU加速,相对于SurfaceView使用画布进行绘制,OpenGL的绘制关联到GPU,效率更高。
  2. 可以定制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视频渲染有更深入的了解。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章