美文网首页开发程序员OpenGL
TextureView+SurfaceTexture+OpenG

TextureView+SurfaceTexture+OpenG

作者: 子雷 | 来源:发表于2015-09-16 00:26 被阅读65510次

    做好的Demo截图


    opengl-video
    前言

    讲了这么多,可能有人要问了,播放视频用个android封装的VideoView或者用MediaPlayer+SurfaceView来进行播放视频不就得了吗,干嘛还要整这么麻烦。OK,为了回答这个问题,我们先看看OpenGL ES干什么的,它是OpenGL三维图形API的子集,图形硬件的一种软件接口,针对手机、PDA和游戏主机等嵌入式设备而设计。我想如果是做游戏类开发的肯定对一些图形库不会陌生,其实很多游戏引擎内部都是封装的像OpenGL,DirectX 3D之类的图形库,然后开发者就可以通过这些图形库,来开发很多好玩的东西,如游戏,动画等等,那么我现在用Opengl es来绘制视频是不是也可以定制很多有意思的东西,比如开发一个左右分屏视频播放器,然后在虚拟现实(VR)头盔上来观看2d的视频,如果用opengl去绘制,那简直分分中搞定,因为这里,每一帧的视频在opengl 看来只是一张纹理贴图而已,那么我想把这个贴图贴在哪里就贴在哪里。总之,用opengl可以开发出很多有意思的二维,三维的图形应用出来。

    正文

    说了这么多,咱们开始吧,

    /***
     * 在这个类里面对视频纹理进行绘制工作,继承了 {@link TextureSurfaceRenderer},
     * 并实现了{@link SurfaceTexture.OnFrameAvailableListener}
     */
    public class VideoTextureSurfaceRenderer extends TextureSurfaceRenderer implements
            SurfaceTexture.OnFrameAvailableListener
    {
    
        public static final String TAG = VideoTextureSurfaceRenderer.class.getSimpleName();
        /**绘制的区域尺寸*/
        private static float squareSize = 1.0f;
        private static float squareCoords[] = {
                -squareSize,  squareSize, 0.0f,   // top left
                -squareSize, -squareSize, 0.0f,   // bottom left
                squareSize, -squareSize, 0.0f,    // bottom right
                squareSize,  squareSize, 0.0f     // top right
        };
        /**绘制次序*/
        private static short drawOrder[] = {
             0, 1, 2, 
             0, 2, 3
         };
        /**
         * 用来缓存纹理坐标,因为纹理都是要在后台被绘制好,然
         * 后不断的替换最前面显示的纹理图像
         */
        private FloatBuffer textureBuffer;
        /**纹理坐标*/
        private float textureCoords[] = {
                0.0f, 1.0f, 0.0f, 1.0f,
                0.0f, 0.0f, 0.0f, 1.0f,
                1.0f, 0.0f, 0.0f, 1.0f,
                1.0f, 1.0f, 0.0f, 1.0f
        };
        /**生成的真实纹理数组*/
        private int[] textures = new int[1];
        /**着色器脚本程序的handle(句柄)*/
        private int shaderProgram;
        /**squareCoords的的顶点缓存*/
        private FloatBuffer vertexBuffer;
        /**绘制次序的缓存*/
        private ShortBuffer drawOrderBuffer;
        
        /**矩阵来变换纹理坐标,(具体含义下面再解释)*/
        private float[] videoTextureTransform;
        /**当前的视频帧是否可以得到*/
        private boolean frameAvailable = false;
        private Context context;
        private SurfaceTexture videoTexture;    // 从视频流捕获帧作为Opengl ES 的Texture
        
        /**
         * @param texture 从TextureView获取到的SurfaceTexture,目的是为了配置EGL 的native window
        */
        public VideoTextureSurfaceRenderer(Context context, SurfaceTexture texture, int width, int height)
        {
            super(texture, width, height);  //先调用父类去做EGL初始化工作
            this.context = context;
            videoTextureTransform = new float[16];      
        }
       
        // 代码略
    }
    

    对上面的代码再稍稍解释一下吧,在绘制之前,我们首先选定一块区域来让图像就在这块区域来绘制,则有如下定义:

    private static float squareSize = 1.0f;
        private static float squareCoords[] = {
                -squareSize,  squareSize, 0.0f,   // top left
                -squareSize, -squareSize, 0.0f,   // bottom left
                squareSize, -squareSize, 0.0f,    // bottom right
                squareSize,  squareSize, 0.0f     // top right
        };
    

    上幅图来解释解释:

    square
    squareSize=1.0时,square的面积就是整个手机屏幕,若squareSize=0.5f则每个边长都为屏幕的一半。数组的坐标顺序为:左上->左下->右下->右上。

    然后接着定义一个纹理坐标数组:

     private float textureCoords[] = {
                0.0f, 1.0f, 0.0f, 1.0f,  //左上
                0.0f, 0.0f, 0.0f, 1.0f,  //左下
                1.0f, 0.0f, 0.0f, 1.0f,  //右下
                1.0f, 1.0f, 0.0f, 1.0f   //右上
        };
       
    

    接着上图:


    texCoord

    如上图,这就是2D OpenGL ES纹理坐标,它由四个列向量(s, t, 0, 1)组成,其中s, t ∈[0, 1]。
    不知道大家有没有发现,绘制区域(quareCoords[])的顶点数组坐标顺序跟纹理数组坐标的顺序都是从 左上方开始->到右上方结束,为什么要这样做呢? 其实目的就是为了统一方向,因为我们知道手机屏幕的坐标是左上角为原点(0.0, 0.0),所以为了以后不必要的转换工作,最好将绘制的顺序的都统一起来。

    然后则看看纹理的绘制次序drawOrder[] = {0,1,2, 0, 2, 3} ; 这又是个什么次序呢? 好,再来个图看看

    drawOrder
    其实,opengl es 在绘制一个多边形时,都是用一个基本的图元即三角形拼凑出来的,比如一个矩形它可以用(0->1->2)和(0->2->3)的次序,通过两个三角形给拼凑出来的,当然也可是其它的组合比如(1,3,2)(1,3,0)等等,总之得用两个三角形拼凑成一个矩形,不过建议还是都按找同一钟次序,比如都是按照顺时针或都按照逆时针来拼凑。

    OK,接下来再贴出省略的代码:

        /**
        * 重写父类方法,初始化组件
        */
        @Override
        protected void initGLComponents()
        {
            setupVertexBuffer();
            setupTexture();
            loadShaders();
        }
    
    /***
    * 设置顶点缓存
    */
     private void setupVertexBuffer()
        {
            /** Draw Order buffer*/
            ByteBuffer orderByteBuffer = ByteBuffer.allocateDirect(drawOrder. length * 2); 
            orderByteBuffer.order(ByteOrder.nativeOrder());  //Modifies this buffer's byte order
            drawOrderBuffer = orderByteBuffer.asShortBuffer();  //创建此缓冲区的视图,作为一个short缓冲区.
            drawOrderBuffer.put(drawOrder);
            drawOrderBuffer.position(0); //下一个要被读或写的元素的索引,从0 开始
    
            // Initialize the texture holder
            ByteBuffer bb = ByteBuffer.allocateDirect(squareCoords.length * 4);
            bb.order(ByteOrder.nativeOrder());
            vertexBuffer = bb.asFloatBuffer();
            vertexBuffer.put(squareCoords);
            vertexBuffer.position(0);
        }
    

    ByteBuffer.allocateDirect(drawOrder. length * 2)表示的是直接从系统中分配大小为 (drawOrder. length * 2)的内存,2 代表一个short型占两个字节,(int占4个字节)。

    /**接着初始化纹理*/
    private void setupTexture()
        {
            ByteBuffer texturebb = ByteBuffer.allocateDirect(textureCoords.length * 4);
            texturebb.order(ByteOrder.nativeOrder());
    
            textureBuffer = texturebb.asFloatBuffer();
            textureBuffer.put(textureCoords);
            textureBuffer.position(0);
    
            // 启用纹理
            GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
            //1表示只需要一个纹理索引
            GLES20.glGenTextures(1, textures, 0);         
           GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textures[0]);
    
          videoTexture = new SurfaceTexture(textures[0]);
          videoTexture.setOnFrameAvailableListener(this);
        }
    

    关于glBindTexure(int target, int texture) 中的参数target,表示指定这是一张什么类型的纹理,在此是GLES11Ext.GL_BLEND_EQUATION_RGB_OES,也可以是常用的2D纹理如GLES20.GL_TEXTURE_2D等;第二个参数texture,在程序第一次使用这个参数时,这个函数会创建一个新的对象,并把这个对象分配给它,之后这个texture就成了一个活动的纹理对象。如果texture=0 则OpenGL就停止使用纹理对象,并返回到初始的默认纹理。

    /**加载顶点与片段着色器*/
     private void loadShaders()
        {
            final String vertexShader = RawResourceReader.readTextFileFromRawResource(context, R.raw.vetext_sharder);
            final String fragmentShader = RawResourceReader.readTextFileFromRawResource(context, R.raw.fragment_sharder);
    
            final int vertexShaderHandle = ShaderHelper.compileShader(GLES20.GL_VERTEX_SHADER, vertexShader);
            final int fragmentShaderHandle = ShaderHelper.compileShader(GLES20.GL_FRAGMENT_SHADER, fragmentShader);
            shaderProgram = ShaderHelper.createAndLinkProgram(vertexShaderHandle, fragmentShaderHandle,
                    new String[]{"texture","vPosition","vTexCoordinate","textureTransform"});
        }
    

    关于可编程着色器在计算机图形领域本身就是个很大的主题,已超出了本文的范围。那么就稍稍解释下,OpenGL着色语言(OpenGL shading Language),它是一种编程语言,用于创建可编程的着色器。在opengl es 2.0以前使用的是一种“固定功能管线“,而2.0以后就是使用的着这钟可“编程的管线“,这种管线又分为顶点处理管线与片段处理管线(涉及到OpenGL的渲染管线的一些机制,大家自行的查吧)。
    我现在就说说这个GLSL着色器是怎么使用的吧:有关使用GLSL创建着色器流程如下图(参照OpenGL 编程指南):


    shaderCreate

    大家可以参照这幅图来看看程序中shader的使用。
    在本项目的res目录下新建一个raw文件夹,然后分别创建vertext_shader.glsl和fragment_sharder.glsl文件,代码如下:
    vertext_sharder:

    attribute vec4 vPosition;    //顶点着色器输入变量由attribute来声明
    attribute vec4 vTexCoordinate;
    //uniform表示一个变量的值由应用程序在着色器执行之前指定,
    //并且在图元处理过程中不会发生任何变化。mat4表示一个4x4矩阵
    uniform mat4 textureTransform; 
    varying vec2 v_TexCoordinate;   //片段着色器输入变量用arying来声明
    
    void main () {
        v_TexCoordinate = (textureTransform * vTexCoordinate).xy;
        gl_Position = vPosition;
    }
    

    fragment_shader

    /**使用GL_OES_EGL_image_external扩展处理,来增强GLSL*/
    #extension GL_OES_EGL_image_external : require
    precision mediump float;
    uniform samplerExternalOES texture; //定义扩展的的纹理取样器amplerExternalOES
    varying vec2 v_TexCoordinate;
    
    void main () {
        vec4 color = texture2D(texture, v_TexCoordinate);
        gl_FragColor = color;
    }
    

    着色器语言非常的类似C语言,也是从main函数开始执行的。其中的很多语法,变量等等,还是大家自行的查查,这不是几句能说明白的。着色器的创建及编译过程的代码都在项目里的一个util包下的三个工具类,RawRourceReader,ShaderHelper,TextureHelper类中,我就不再贴出来了,有兴趣大家可以fork或clone下来看看。

    ok,终于初始化完了,太不容易了,冏。
    好吧,绘制工作开始跑起来。

       @Override
        protected boolean draw()
        {
            synchronized (this)
            {
                if (frameAvailable)
                {
                    videoTexture .updateTexImage();
                    videoTexture .getTransformMatrix(videoTextureTransform);
                    frameAvailable = false;
                }
                else
                {
                    return false;
                }
    
            }
            GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
            GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
            GLES20.glViewport(0, 0, width, height);
            this.drawTexture();
    
            return true;
        }
    

    当视频的帧可得到时,调用videoTexture .updateTexImage()方法,这个方法的官网解释如下:
    <font color=#008000 >Update the texture image to the most recent frame from the image stream. This may only be called while the OpenGL ES context that owns the texture is current on the calling thread. It will implicitly bind its texture to the GL_TEXTURE_EXTERNAL_OES texture target.</font>
    大意是,从的图像流中更新纹理图像到最近的帧中。这个函数仅仅当拥有这个纹理的Opengl ES上下文当前正处在绘制线程时被调用。它将隐式的绑定到这个扩展的GL_TEXTURE_EXTERNAL_OES 目标纹理(为什么上面的片段着色代码中要扩展这个OES,可能就是应为这个吧)。
    videoTexture .getTransformMatrix(videoTextureTransform)这又是什么意思呢?当对纹理用amplerExternalOES采样器采样时,应该首先使用getTransformMatrix(float[])查询得到的矩阵来变换纹理坐标,每次调用updateTexImage()的时候,可能会导致变换矩阵发生变化,因此在纹理图像更新时需要重新查询,该矩阵将传统的2D OpenGL ES纹理坐标列向量(s,t,0,1),其中s,t∈[0,1],变换为纹理中对应的采样位置。该变换补偿了图像流中任何可能导致与传统OpenGL ES纹理有差异的属性。例如,从图像的左下角开始采样,可以通过使用查询得到的矩阵来变换列向量(0,0,0,1),而从右上角采样可以通过变换(1,1,0,1)来得到。

    关于 GLES20.glViewport(int x, int y, int width, int height) ,也是个比较重要的函数,这个函数大家网上查查。
    ok,接下来到了真正的绘制纹理的时候了。代码如下:

     private void drawTexture() {
            // Draw texture
            GLES20.glUseProgram(shaderProgram); //绘制时使用着色程序
            int textureParamHandle = GLES20.glGetUniformLocation(shaderProgram, "texture"); //返回一个于着色器程序中变量名为"texture"相关联的索引
            int textureCoordinateHandle = GLES20.glGetAttribLocation(shaderProgram, "vTexCoordinate");
            int positionHandle = GLES20.glGetAttribLocation(shaderProgram, "vPosition");
            int textureTransformHandle = GLES20.glGetUniformLocation(shaderProgram, "textureTransform");
            //在用VertexAttribArray前必须先激活它    
            GLES20.glEnableVertexAttribArray(positionHandle);
             //指定positionHandle的数据值可以在什么地方访问。 vertexBuffer在内部(NDK)是个指针,指向数组的第一组值的内存
            GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 0, vertexBuffer);               
            GLES20.glBindTexture(GLES20.GL_TEXTURE0, textures[0]);
            GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
            //指定一个当前的textureParamHandle对象为一个全局的uniform 变量
            GLES20.glUniform1i(textureParamHandle, 0);
    
            GLES20.glEnableVertexAttribArray(textureCoordinateHandle);
            GLES20.glVertexAttribPointer(textureCoordinateHandle, 4, GLES20.GL_FLOAT, false, 0, textureBuffer);
    
            GLES20.glUniformMatrix4fv(textureTransformHandle, 1, false, videoTextureTransform, 0);
           //GLES20.GL_TRIANGLES(以无数小三角行的模式)去绘制出这个纹理图像
            GLES20.glDrawElements(GLES20.GL_TRIANGLES, drawOrder.length, GLES20.GL_UNSIGNED_SHORT, drawOrderBuffer);
            GLES20.glDisableVertexAttribArray(positionHandle);
            GLES20.glDisableVertexAttribArray(textureCoordinateHandle);
        }
    

    好了所有代码就分析到这里了,前两篇链接TextureView+SurfaceTexture+OpenGL ES来播放视频(一) TextureView+SurfaceTexture+OpenGL ES来播放视频(二) , 项目地址在here

    注: 当然了,其实也没必要配置EGL环境这么麻烦,android的GLSurfaceView就已经在底层就配置好了EGL环境,且自带绘制线程

    相关文章

      网友评论

      • 小白学AI:不错,讲解的很详细。但是关于视频是如何加载到纹理中的,我还是有点不清楚。根据目前理解,我认为videoTexture是根据视频获取到的纹理,但是视频数据是怎么进入到纹理中的呢?(图片加载到纹理中,就可以显式的看到图像,返回一个纹理ID)。难道是这句: videoTexture = new SurfaceTexture(textures[0]);
        videoTexture.setOnFrameAvailableListener(this);

        另外我现在有个疑惑,希望楼主给个解答:我想加载视频到纹理中,不需要显示,后续做纹理合成。我之前加载图片到纹理中是采用如下的代码:
        public static int loadTexture(final Bitmap img, final int usedTexId, final boolean recycle) {
        int textures[] = new int[1];
        if (usedTexId == NO_TEXTURE) {
        GLES20.glGenTextures(1, textures, 0);
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]);
        GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
        GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
        GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
        GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
        GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
        GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
        GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
        GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);

        GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, img, 0);
        } else {
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, usedTexId);
        GLUtils.texSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0, img);
        textures[0] = usedTexId;
        }
        if (recycle) {
        img.recycle();
        }
        return textures[0];
        }
        我自己尝试了下,一直有问题,因为我不知道如何把视频和纹理建立起联系。还请楼主不吝赐教。
        丿灬序曲:个人理解,创建一个GLSurfaceView,设置Renderer,Renderer中写实现接口,接口中初始化mediaplayer, mediaplayer设置surface surface创建需要传入SurfaceTexture,SurfaceTexture创建需要关联初始化的纹理id,就是你代码中的纹理id,然后整个流程就关联起来了,你还可以在Renderer的接口中重写绘制很多东西,mediaplayer设置surace的时候就是关联起来,让视频的每一帧都经过一次处理,视频是在GLSurfaceView中播放
        子雷:我的理解是这样的,MediaPlayer解码出来的视频raw数据不断的交给 Surface, 然后SurfaceTexture不断的消费Surface中 视频的帧数据Buffer,也就是 Opengl 通过texId指向的这个 SurfaceTexture,来将 视频帧数据 交给GPU来处理
        子雷:@营养快线_3ab0 你可以参考 http://hukai.me/android-deeper-graphics-architecture/
      • GreenStone:厉害啊
      • maimingliang:请问一下,如果使用OpenGL调整视频画幅?
      • 洞幺长老:对比了下 textureview 播放CPU占用特别高是什么原因
      • 7d6c93e8426b:你好作者我要截图怎么办呢,我现在用 “ glReadPixels ” 截图出来的是黑色的 :kissing_closed_eyes: :flushed: :flushed:
      • 7d6c93e8426b:你好作者我想截图怎么办呢
      • aafcdeed2af0:你好,我在编译通过项目后,运行NativeMediaActivity时,有声音无画面,请问你哪里这是这样么?
        7d6c93e8426b:你好作者 我想截个图怎么办呢 求救
        子雷:@小诸葛_4d98 已经更新了代码~
        小诸葛_4d98:我这里也是这样
      • 狗三四:videoTexture .getTransformMatrix(videoTextureTransform)这里救了我啊,虽然还是不懂什么原理,不过把问题解决了
        某些芯片不支持非32倍数宽高的视频,解码出来的图像需要这个矩阵来做补偿,不然会有一些奇怪问题
      • hewking:你好 想问下你在哪家 虚拟现实公司工作,最近挺火的 我也挺感兴趣的
      • zjujunge:为什么这里要new一个SurfaceTexture?不可以使用传进来的SurfaceTexture参数??
        子雷:@zjujunge 两个SurfaceTexture的作用是不一样的。 通过TextureView获取到的surfaceTexture是为了创建一个EGL native窗口(当然用SurfaceHolder或Surface也是可以最为eglCreateWindowSurface的native_window参数),你可以把这个窗口看成手机显示屏,,,而 new 的这个surfaceTexture才是作为Mediaplayer不断生产的视频流的载体,在内部将视频流转换为一帧一帧的纹理texture,然后被绘制,最终显示在那个窗口上
      • Vic橙:您好作者

        public VideoTextureSurfaceRenderer(Context context, SurfaceTexture texture, int width, int height)
        {
        super(texture, width, height); //先调用父类去做EGL初始化工作
        this.context = context;
        videoTextureTransform = new float[16];
        surfaceTexture = new SurfaceTexture(textures[0]);
        surfaceTexture.setOnFrameAvailableListener(this); //注册视频帧是否可得到的监听器
        }
        为什么这里要new一个SurfaceTexture?不可以使用传进来的SurfaceTexture参数??
        子雷:@Vic橙 两个SurfaceTexture的作用是不一样的。 通过TextureView获取到的surfaceTexture是为了创建一个EGL native窗口(当然用SurfaceHolder或Surface也是可以最为eglCreateWindowSurface的native_window参数),你可以把这个窗口看成手机显示屏,,,而 new 的这个surfaceTexture才是作为Mediaplayer不断生产的视频流的载体,在内部将视频流转换为一帧一帧的纹理texture,然后被绘制,最终显示在那个窗口上
      • 挂挂牛:RawRourceReader,ShaderHelper,TextureHelper这三个工具类是已经有的吗?还是要自己写,编译说找不到
      • b537cbe46aee:感谢分享
      • 齐丹汉姆:关注下
      • 深深浅浅的刀割:不错,我厂代理的视频会议解决方案也是相似原理😂😂😂😂😂

      本文标题:TextureView+SurfaceTexture+OpenG

      本文链接:https://www.haomeiwen.com/subject/xhjmcttx.html