Android OpenGL运行环境--在金山云多媒体SDK中的项目实践

本文主要说明如何在Android平台上创建、管理OpenGL ES运行环境,如何将摄像头、播放器的画面内容上传到Texture, 如何将Texture内容绘制到屏幕、取出数据buffer、送进编码器等内容。基本涵盖了直播、短视频SDK中跟GPU相关的处理。

1. 使用GPU的必要性

Android平台多媒体处理使用GPU的必要性,主要在以下两方面:

  • 美颜及各种视频特效的需求
    随着移动视频直播及短视频的高速增长,用户对直播、短视频中的视频图像处理的多样性及复杂性提出了更高的要求,如果使用CPU来实现这些处理,效率及发热上对移动端都是不可接受的。而GPU的高并行特性在进行视频图像处理上具有先天的优势,目前绝大多数的直播、短视频app中的美颜、特效处理都是由GPU来实现的。

  • Android使用硬解编解码时YUV数据打包格式的碎片化问题
    Android平台的碎片化问题一直是困扰开发者的一个痛点,硬件编解码这块尤其严重。如果在编解码过程中直接使用YUV buffer数据,不同的硬件平台所支持的YUV数据的打包方式是不同的,需要针对各个硬件平台一一适配,非常繁琐。相反,我们可以选择使用Surface作为MediaCodec原始图像的载体,让图像从采集到编码都在GPU通路中进行,则可以完全规避这个问题。

2. OpenGL ESEGL

Android平台上,要进行GPU的开发,需要跟OpenGL ESEGL这两套API接口打交道。

OpenGL ES定义了渲染图形的API,但是没有定义窗口系统,作为补充,Android平台使用EGL接口定义来管理窗口系统。简单来说,要绘制Texture、多边形,需要调用GLES接口,而要将绘制的内容渲染出来,则需要使用EGL调用。

OpenGL ES/EGL之前的关系,可以类比于画家作画场景:

  • 画家:编程人员;
  • 画笔、颜料:OpenGL ES API
  • 画布:EGL创建的Surface

另外,GLES的相关接口都是单线程设计,并且该线程中需要有预先创建好的GL上下文,这个上下文在Android平台上包含EGLContextEGLSurface

3. GL运行环境

创建GL运行环境也就是创建一个带有初始化好GL上下文的线程,GLES的绘制操作都被限定在这个线程中进行。

3.1 创建GL运行环境

创建GL的方法大致有三种:

  1. 通过GLSurfaceView
    GLSurfaceView类已经对EGL的相关操作做了完整的封装,设置GLSurfaceView.Renderer,会在GL上下文创建好之后触发onSurfaceCreated调用,调用requestRender后,触发运行在GL线程的onDrawFrame回调。

  2. 自行创建一个线程,在这个线程中通过EGL接口创建EGLContext, 并跟SurfaceTexture/Surface创建的EGLSurface进行关联。
    使用TextureView进行预览就是通过其中的SurfaceTexture创建了一个EGLSurface,而软硬编模块的EGLSurface也是通过ImageReaderMediaCodec中的surface来创建的。

  3. 类似2中的做法,但EGLSurface使用离屏缓冲区(pbuffer)而不是Surface/SurfaceTexture初始化,这里称为离屏运行环境。

3.2 GL运行环境的生命周期

GL运行环境的生命周期主要由GLSurface的生命周期来决定。

  • 对于由GLSurfaceView以及SurfaceTextureSurface所创建的GLSurface,会在相应的实例销毁时失效,比如APP切到后台,预览View不可见,或MediaCodec实例被销毁。

  • 对于离屏运行环境,可以不受View显隐及前后台切换的影响,完全由应用自行控制。

3.3 共享EGLContext

对于直播场景,我们需要将处理过的图在屏幕上进行预览,同时也需要将其绘制到ImageReaderMediaCodecsurface上以用于编码。这种场景下需要创建多个GL运行环境,预览、编码分别拥有自己的GL环境,然后共享处理后的Texture

这里需要注意:

  1. 在多个GL运行环境间传递Texture,这些EGLContext必须是共享的(后创建的EGLContext需要在创建时传入先创建的EGLContext实例作为参数)。
  2. 在多个GL运行环境间传递Texture之前,必须执行glFinish, 否则可能造成后一个线程的渲染内容错乱(理论上只需要执行glFlush即可,但在很多Android真机上是无效的)。

3.4 GL运行环境在金山云SDK中的使用

在金山云直播SDK、短视频SDK中,我们将EGL的相关操作封装了起来,提供了一个同时可以兼容GLSurfaceView, TextureView以及离屏渲染的GLRender类,相关接口则类似于GLSurfaceView

在实际应用中,以直播场景为例,我们用到了三个GL环境:

  1. 离屏环境。
    GL环境是初始创建的环境,后续的两个GL环境均以该环境中的EGLContext实例为参数创建自己的共享EGLContext
    另外,对采集到的视频图像的处理均在该环境下进行。
  2. 以GLSurfaceView或TextureView承载的预览环境。
    GL环境用来将离屏环境中采集、处理后的视频图像显示在预览View上。
  3. 以Surface承载的编码环境。
    GL环境用来将上述处理后的视频图像传送给编码器,然后推流,最终可以让观众在直播播放端看到。

关于三个EGLContext的关系,可以参考下图:

Android OpenGL运行环境--在金山云多媒体SDK中的项目实践_第1张图片
SharedEGLContext

选择离屏环境作为主EGL环境的一些考虑:

  1. 可以实现无预览推流;
  2. 应用切后台,预览环境被销毁后依然可以正常进行视频直播,这对于主播短时间切出直播App的场景是比较有意义的;
  3. 大大降低了直播过程中切换预览View时的处理复杂度,实际应用场景可以参考金山云直播Demo中的悬浮窗推流功能;
  4. 可以方便的接入多个预览View,实现各种滤镜的同屏效果对比等一些功能。

4. 上传视频图像到Texture

Android系统中,主要有两种方式将图像以Texture的形式上传到GPU

  1. 直接绘制到用Texture创建的SurfaceTexture上;
  2. 通过GLES调用,上传图像的buffer数据。

4.1 Camera, MediaCodec输出图像到Texture

直播、短视频场景下,我们需要的画面内容一般包括手机摄像头采集的画面,以及待编辑视频解码后的输出画面。而系统的Camera采集接口以及MediaCodec解码接口提供了将采集画面绘制到surface的能力,因此我们可以通过这些接口高效的将摄像头及源视频解码后的图像上传到GPUTexture上。

这里有两个注意点:

  1. 输出的Texture格式为Android平台特定的EXTERNAL_OES格式,不同于通常的SAMPLE2D格式,使用时需要留意。
  2. 输出的每一帧Texture是包含一个4x4的转换矩阵的,这个矩阵通过SurfaceTexturegetTransformMatrix方法来获取,进行后续的渲染和处理时,需要在顶点shader中计算坐标时乘上这个矩阵才能得到正确的结果。

部分关键调用示例:

// 创建OES格式的Texture
int[] textures = new int[1];
GLES20.glGenTextures(1, textures, 0);
int textureId = textures[0];
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texId);
GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER,
        GLES20.GL_LINEAR);
GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER,
        GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S,
        GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T,
        GLES20.GL_CLAMP_TO_EDGE);

// 由Texture来创建SurfaceTexture
mSurfaceTexture = new SurfaceTexture(mTextureId);

// 设置为Camera的预览
mCamera.setPreviewTexture(mSurfaceTexture);

// MediaCodec只支持预览到Surface上,所以需要再由SurfaceTexture来创建一个Surface,
// 然后用这个Surface来初始化MediaCodec.
mSurface = new Surface(mSurfaceTexture);
mMediaCodec.configure(mediaFormat, mSurface, null, 0);

4.2 RGBA/YUV数据到Texture

对于软件解码器输出的buffer数据,以及作为背景图、水印等功能的图片内容要上传到Texture,需要用GLES的相关API来实现。

参考接口:

// 上传ByteBuffer到Texture
GLES20.glTexImage2D(int target, int level, int internalformat, int width, int height, int border, int format, int type, Buffer pixels);
GLES20.glTexSubImage2D(int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, Buffer pixels);

// 上传Bitmap到Texture
GLUtils.texImage2D(int target, int level, Bitmap bitmap, int border);
GLUtils.texSubImage2D(int target, int level, int xoffset, int yoffset, Bitmap bitmap);

以上两个接口可以方便的上传RGB/RGBA格式的图像buffer数据,而对于软件解码器输出的YUV(I420)格式的数据,可以将三个色彩分量分开上传,然后在Shader中通过转换矩阵转换为RGBA格式再进行后续处理。

5. 渲染Texture画面

  • 当在GLSurfaceView提供的EGLContext中进行渲染时,GLSurfaceView已经做好了针对EGL相关接口的封装,调用完GLESglDrawArray方法后,调用栈返回,即可将相应的内容绘制到屏幕上了。
  • 当在Surface/SurfaceTexture初始化的EGLContext中进行渲染时,还需要自行调用EGL14.eglSwapBuffers方法切换已绘制缓冲区进行显示,或者输出到MediaCodec编码器。

6. 从Texture中高效获取图像Buffer

在直播及短视频应用场景中,我们会有将GPU处理后的图像取出来存放到CPU Buffer中的需求,例如画面截图、使用软件编码器进行编码等。

Android平台上从GPU中获取Buffer数据有以下几种方案:

  1. 使用OpenGL ES 2.0通用的glReadPixels方法。
    该方法在大多数Android平台上的效率都非常低下,获取一帧720p的图像往往需要100ms以上,不具备实用性。
  2. 使用OpenGL ES 3.0引入的PBO(Pixel Buffer Object)来读取。
    这种方法获取到图像Buffer数据的速度非常快(<5ms),但是获取后再用CPU访问这块Buffer的效率则非常低下,也不具备实用性。
  3. 使用Android 4.4引入的ImageReader类。
    该方法的思路是,将GPU处理后的Texture绘制到ImageReader提供的Surface上,然后ImageReader会在OnImageAvailableListener回调中将返回读取出来的图像Buffer数据。
    ImageReaderAndroid系统提供的API,从GPU中读取Buffer数据非常高效,完全可以满足需求。
    至于ImageReader的兼容性问题,我们目前只遇到了在三星S4上不兼容的情况,而这种情况也可以通过异常捕捉来规避。

7. 结束语

简述了一下金山云多媒体SDK在使用Android OpenGL中遇到的问题,欢迎大家指正。

也欢迎大家使用直播和短视频SDK:

  • https://github.com/ksvc/KSYLive_Android
  • https://github.com/ksvc/KSYMediaEditorKit_Android

转载请注明:
作者金山视频云,首发 Jianshu.com


金山云SDK仓库地址:
https://github.com/ksvc

金山云SDK相关的QQ交流群:

  • 视频云技术交流群:574179720
  • 视频云Android技术交流:6200036233

你可能感兴趣的:(Android OpenGL运行环境--在金山云多媒体SDK中的项目实践)