通过 OpenGL 渲染摄像头数据到屏幕时,使用 GLSurfaceView 就可以完成,如果要做中间层处理,比如美颜等,通过着色器处理之后写入帧缓冲 (FBO) 来完成的。那么如何从着色器拿到数据然后编码生成视频呢?一般来说,使用软编码或者 MediaCodec 来进行编码,如果图像的 byte []
拿到了,编码就简单了,但是现在时在着色器中处理的,没有 byte []
,怎么将我们处理之后的图像变成一个 Mp4 文件呢?
MediaCodec
MediaCodec 时 AndroidSDK 中为我们提供编解码的 API:
MediaCodec
内部有两个缓存队列,分别是:输入缓冲队列与输出缓冲队列。我们只需要使用 queueInputBuffer
将需要的 byte 数据提交到输入队列,使用 equeueOutputBuffer
从输出队列去除编码完成后的数据即编码后的数据。
但是没法得到原始图像的 byte 数据,怎么办呢?
1 | Surface surface = mMediaCodec.createInputSurface (); |
这与录屏直播的问题是一样的,当我们用上述方法得到一个 Surface,这个 Surface 由编码器创建,如果向这个 Surface 画画,那么画在上面的图像就会自动进行编码。所以现在问题变成了,如何将 OpenGL 处理之后的图像再画到这个 Surface 中去? 简单来说就是 OpenGL 怎么朝指定的 Surface 中画画!!
EGL 环境
OpenGL ES 只是图形 API,不负责管理(显示)窗口,窗口的管理交由各个设备自己来完成。OpenGL ES 调用用于渲染纹理多边形,而 EGL 调用用于将渲染放到屏幕上,所以 EGL 可以看作为 OpenGL ES 与设备的桥梁。
Android 使用 EGL 库创建 OpenGL ES 上下文并为 OpenGL ES 渲染提供窗口系统。调用任何 OpenGL 函数前,必须已经创建了 OpenGL 上下文。OpenGL ES 操作适用于当前上下文,渲染代码应该在当前 GLES 线程上执行。
上下文:在渲染过程中需要将顶点信息(形状)、纹理信息(图像)等渲染状态信息存储起来,而存储这些信息的数据结构就可以看作 OpenGL 的上下文。
使用 GLSurfaceView 时,我们无需关系搭建 OpenGL 上下文环境,也无需关心创建 OpenGL ES 的显示设备。但是使用 GLSurfaceView 不够灵活,比如共享 OpenGL 上下文来达到多线程共同操作一份纹理等操作都不能直接使用。
本次的 OpenGL 上下文环境依旧使用 Java 层 EGL 的 API 来完成,因为仅仅只是视频录制,不涉及复杂的场景,所以暂时先使用 Java 层的 EGL API 来完成。但是对于稍微复杂点的场景(比如人脸识别),则需要到 C++ 层来实施,其实本质都是一样的。
解决方案
所以到目前思路很清晰了,直接利用 EGL 来建立 OpenGL 与设备的联系,让 OpenGL 将纹理一边用绘制到屏幕,一边用于编码,这样就完成了视频录制功能了:
EGLSurface 可以是由 EGL 分配的离屏缓冲区,也可以是由操作系统分配的窗口。能够与 Surface 绑定,让 OpenGL 在 EGLSurface 绘制,相当于在 Surface 绘制。
需要在一个单独的线程中搭建一个 EGL 环境,创建 EGLSurface 与 MediaCodec 的 Surface 绑定,来绘制图像到需要编码的 Surface 中去。
代码实现
EGLEnv.java,此类负责通过 EGL 创建 OpenGL 上下文环境。以及让编码的 OpenGLContext 与 GLSurfaceView 的 OpenGLContext 绑定,与 GLSurfaceView 中的 EGLContext 共享数据,只有这样才能拿到处理完之后显示的图像纹理,拿到这个纹理才能进行编码:
1 | public class EGLEnv { |
MediaRecorder.java 负责视频的编码:
1 | public class MediaRecorder { |
代码其实不难理解,完整的工程请见:https://github.com/zouchanglin/render-camera