OpenGL ES 与 GLSurfaceView 渲染视频帧

作者: sprint | 来源:发表于2019-06-14 19:39 被阅读3次

    01 前言

    大家好,本文是 iOS/Android 音视频专题 的第六篇,该专题中 AVPlayer 项目代码将在 Github 进行托管,你可在微信公众号(GeekDev)后台回复 资料 获取项目地址。

    在上篇文章 OpenGL ES for Android 世界 中我们已经对 OpenGL ES 有了大致的了解,在本篇文章中我们将使用 OpengGL ES 将解码后的视频进行播放。

    本期内容:

    • View 与 Surface 的渲染机制
    • SurfaceView/GLSurfaceView 与 Surface 的关系
    • GLSurfaceView 与 Renderer
    • SurfaceTexture 与 Surface
    • MediaCodec 解码视频并渲染
    • 结束语

    02 View 与 Surface 的渲染机制

    现在, 你已经对 OpenGLES 有所了解,但是在将视频渲染到视频屏幕之前,我们需要对 GLSurfaceView 与 Surface 有所了解。

    如果提到 Surface 可能大部分开发者接触的比较少,在 Android 绘制系统中,Surface 是一个非常重要的概念,它向 Applicaiton 提供了 Canvas,向 SufaceFlinger 提供了可供显示的图像缓存。在 Surface 内部维护了图像 buffer 对象,这个最终就会交由 SufaceFlinger 合成显示。

    在 Android 窗口中每个 Window 对象都会创建一个 Surface,这些窗口包括 Activity,Dialog,状态栏等,而我们使用的普通 View 与所属 Window 共享 Surface 实例,普通的 View 不会自己创建一个 Surface 对象,而是将内容绘制到所属 Window 中。之所以强调是普通 View,是由于 SurfaceView / GlSurafaceView 不会共享所属 Window 的 Surface,它会自己内部维护一个 Surface。

    当 Window 被创建时,Window Manger 为每个 Window 创建一个 Surface,当窗口需要重绘时,Window 调用 lockCanvas 方法锁定并返回 Canvas,Window 通过遍历 View 层级,并调用 View 的 OnDraw(Canvas canvas) 方法将 Canvas 传递给 View ,View 通过 Canvas 绘制任何内容。 这一系列操作完成后 Surface 将被 unlock ,由 SurfaceFlinger 合成到屏幕上。

    03 SurfaceView/GLSurfaceView 与 Surface 的关系

    SurfaceView 是 View 的子类,与普通 View 不同之处在于,它内部有自己专用的 Surface,与宿主 Window 不共享 Surface。由于,SurfaceView 与宿主 Window 的分离,对 SurfaceView 的渲染操作我们可以放到单独的线程,这样的设计是因为一些游戏,视频应用的渲染极其复杂,为了不影响对主线程事件的响应,需要将这些渲染任务独立于主线程。

    SurfaceView 的工作相对简单,最重要的任务仅仅是创建了 Surface,并在宿主窗口上打了一个洞,用以显示 Surface 的内容。 下面是部分源码:

    public class SurfaceView extends View implements ViewRootImpl.WindowStoppedCallback 
    {
    private static final String TAG = "SurfaceView";private static final boolean DEBUG = false;
    final ArrayList<SurfaceHolder.Callback> mCallbacks= new ArrayList<SurfaceHolder.Callback>();
    final Surface mSurface = new Surface(); // Current surface in use
    
    }
    

    04 GLSurfaceView 与 Renderer

    前面说了 SurfaceView ,而 GLSurfaceView 才是我们今天的重点,在上篇 《OpenGL ES for Android 世界》文章中,我们已经对 GLSurfaceView 有了初步的介绍,你可能还记得我们利用 GLSurfaceView 在屏幕上绘制了一个三角形。

    通过 GLSurfaceView 前缀我们大致可以猜到,它一定是和 OpenGL 相关的,正如你猜测的那样,GLSurfaceView 确实是封装了 GL 的相关内容,严格来说是使用 EGL 搭建了 GL 环境。 让我们可以通过 Render 接口,就可以直接渲染我们要显示的内容。

    GLSurfaceView 是对 SurfaceView 的扩展,不仅添加了 EGL 管理,而且为我们创建了一个 Renderer 线程,SurfaceView 的设计允许我们在主线程外执行渲染操作,而 GLSurfaceView 继承自 SurfaceView,并在内部创建了一个 GLThread,你的所有绘制任务,都将在 GLThread 线程中执行。

    GLSurfaceView 有一个 setRenderer(Renderer renderer) 方法,它允许我们实现自己的渲染逻辑,Renderer 接口的定义如下:

    public interface Renderer {    
       
        /** Surface 创建成功,GL 环境已经准备完成 */
        void onSurfaceCreated(GL10 gl, EGLConfig config);
            
         /** Surface 的宽高发生变化,一般在横竖屏或者 GLSurfaceView 宽高方式变化时 */      
        void onSurfaceChanged(GL10 gl, int width, int height);
         
        /** 在该方法内实现我们的 GL 渲染逻辑 */
        void onDrawFrame(GL10 gl);  
    }
    

    当我们解码完成拿到每帧的 Texture 时,我们将在 Renderer 的 OnDrawFrame 方法中将 Texture 绘制到屏幕上。

    05 SurfaceTexture 与 Surface

    SurfaceView 是 Surface + View 的结合体,而 SurfaceTexture 是 Surface + GL Texture 的结合体,SurfaceTexture 可以将 Surface 中最近的图像数据更新到 GL Texture 中。通过 GL Texture 我们就可以拿到视频帧,然后直接渲染到 GLSurfaceView 中。

    通过 setOnFrameAvailableListener(listener) 可以向 SurfaceTexture 注册监听事件,当 Surface 有新的图像可用时,调用 SurfaceTexture 的 updateTexImage() 方法将图像内容更新到 GL Texture 中,然后做绘制操作。

    下面是 SurfaceTexture 的基本创建流程:

    // step:1 创建绑定的纹理
    int textures[] = new int[1];
    GLES20.glGenTextures(1, textures, 0);
    int texId = textures[0];
    GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texId);
     
    // step:2 通过 texId 将与 SurfaceTexture 绑定
    SurfaceTexture surfaceTexture = new SurfaceTexture(texId);
    
    // step:3 注册监听事件
    surfaceTexture.setOnFrameAvailableListener(new surfaceTexture.OnFrameAvailableListener() {    
            @Override
             public void onFrameAvailable(SurfaceTexture surfaceTexture) {      
               // request render            
    } });
    

    06 MediaCodec 解码视频并渲染

    好了,上边我们说了那么多,都是解码视频帧的基础工作,现在,可以干点正事了。

    解码并渲染一个视频的标准流程如下:

    • 初始化 GLSurfaceView 设置,并制定 Renderer
    • 初始化 SurfaceTexture,并注册 onFrameAvaiableListener 监听
    • 初始化分离器,选择视频轨道
    • 初始化解码器,并配置 Surface
    • 实现 Renderer 接口,渲染视频纹理

    step1: 初始化 GLSurfaceView 设置,并制定 Renderer

    private void step1() {     mSurfaceView = findViewById(surfaceView);      // openGL ES 2.0      mSurfaceView.setEGLContextClientVersion(2);     mSurfaceView.setRenderer(mRenderer)      // 设置渲染模式  GLSurfaceView.RENDERMODE_WHEN_DIRTY 只有使用  requestRender() 是才会触发渲染     mSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
      }
    

    step2: 初始化 SurfaceTexture,并注册 onFrameAvaiableListener 监听

    private void step2() {
    
    ​mSurfaceTexture = new AVSurfaceTexture();
    mSurfaceTexture.getSurfaceTexture().setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() {
    
         // 当 MediaCodec 的 releaseOutputBuffer(idx,true) 调用后
         // OnFrame 被触发
    ​        @Override
    ​        public void onFrameAvailable(SurfaceTexture surfaceTexture) {
    ​            // 通知 Renderer
    ​            mSurfaceView.requestRender();
    ​         }
    ​    });
    
    }
    

    step3: 初始化分离器,选择视频轨道

    private void step3(){
    
    // step 1:创建一个媒体分离器 
    mMediaExtractor = new MediaExtractor();  
    // step 2:为媒体分离器装载媒体文件路径
    // 指定文件路径 
    Uri videoPathUri = Uri.parse("android.resource://" + getPackageName() + "/" + R.raw.demo_video); 
        
    try {                  
         mMediaExtractor.setDataSource(this, videoPathUri, null);        
    } catch (IOException e)
     {         
        e.printStackTrace();    
    }
              
    // step 3:获取并选中指定类型的轨道   
    // 媒体文件中的轨道数量 (一般有视频,音频,字幕等) 
    int trackCount = mMediaExtractor.getTrackCount();   
    
    // mime type 指示需要分离的轨道类型
    String extractMimeType = "video/";   
    MediaFormat trackFormat = null;
          
    // 记录轨道索引id,MediaExtractor 读取数据之前需要指定分离的轨道索引        int trackID = -1;   
    for (int i = 0; i < trackCount; i++)
    {       
           
          trackFormat = mMediaExtractor.getTrackFormat(i);         if(trackFormat.getString(MediaFormat.KEY_MIME).startsWith(extractMimeType)) 
            {  
               trackID = i; 
             break;   
          } 
     }      
               
    // 媒体文件中存在视频轨道      
    // step 4:选中指定类型的轨道  
                
        if (trackID != -1) 
              mMediaExtractor.selectTrack(trackID);
        
    }
    
    

    step4: 初始化解码器,并配置 Surface

    private void step4() {
    
    MediaFormat trackFormat = mMediaExtractor.getTrackFormat(mMediaExtractor.getSampleTrackIndex());
        
        try {    
          mMediaCodec = MediaCodec.createDecoderByType(trackFormat.getString(MediaFormat.KEY_MIME));
          
        /** configure 中指定 Surface */ 
        mMediaCodec.configure(trackFormat,mSurfaceTexture.getSurface(),null,0);        
        mMediaCodec.start(); 
                
        } catch (IOException e) 
         {            
            e.printStackTrace();       
         }
    }
    

    step5: 实现 Renderer 接口,渲染视频纹理

    private GLSurfaceView.Renderer mRenderer = new GLSurfaceView.Renderer() {
    
        @Override    
        public void onSurfaceCreated(GL10 gl, EGLConfig config) {            
    
        mProgram = new GPUTextureProgram(GPUTextureProgram.ProgramType.TEXTURE_EXT);        }
        
        @Override  
        public void onSurfaceChanged(GL10 gl, int width, int height) {           
            GLES20.glViewport(0,0 ,width,height);    
          }
          
         @Override   
         public void onDrawFrame(GL10 gl) {
              // 更新视频纹理
            mSurfaceTexture.updateTexImage();
            // 将纹理绘制到屏幕中
          mProgram.draw(mSurfaceTexture.getTextureID());
        }
    };
    

    该示例代码你可以在 AVPlayer 的 DemoMediaPlayer 中找到完整的代码示例。

    07 结束语

    现在, 我们初步实现了一个播放器雏形,没有暂停, 快慢速,更没有播放声音,也没有实现音视频同步,先不要着急,关注 GeekDev 公众号你将在第一时间获取最新内容。如果你想了解更多信息,可关注微信公众号 (GeekDev) 并回复 资料 获取。

    往期内容:

    iOS/Android 音视频开发专题介绍

    iOS/Android 音视频概念介绍

    MediaCodec/OpenMAX/StageFright 介绍

    使用 MediaExtractor 及 MediaCodec 解码音视频

    《OpenGL ES for Android 世界》

    下期预告:

    《 AVPlayer 添加音效 》

    相关文章

      网友评论

        本文标题:OpenGL ES 与 GLSurfaceView 渲染视频帧

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