OpenGL+MediaCodec视频录制

通过OpenGL渲染摄像头数据到屏幕时,使用GLSurfaceView就可以完成,如果要做中间层处理,比如美颜等,通过着色器处理之后写入帧缓冲(FBO)来完成的。那么如何从着色器拿到数据然后编码生成视频呢?一般来说,使用软编码或者MediaCodec来进行编码,如果图像的byte[] 拿到了,编码就简单了,但是现在时在着色器中处理的,没有byte[],怎么将我们处理之后的图像变成一个Mp4文件呢?

MediaCodec

MediaCodec时AndroidSDK中为我们提供编解码的API:

MediaCodec内部有两个缓存队列,分别是:输入缓冲队列与输出缓冲队列。我们只需要使用 queueInputBuffer 将需要的 byte 数据提交到输入队列,使用 equeueOutputBuffer 从输出队列去除编码完成后的数据即编码后的数据。

但是没法得到原始图像的 byte 数据,怎么办呢?

1Surface 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共享数据,只有这样才能拿到处理完之后显示的图像纹理,拿到这个纹理才能进行编码:

 1public class EGLEnv {
 2    private final EGLContext mEglContext;
 3    private final RecordFilter recordFilter;
 4
 5    private final EGLSurface mEglSurface; // 画布
 6    private final EGLDisplay mEglDisplay; // 窗口
 7
 8    /**
 9     * @param surface MediaRecorder的surface
10     */
11    public EGLEnv(Context context,EGLContext mGlContext, Surface surface,int width,int height) {
12        // 1、创建Display,获得显示窗口,作为OpenGL的绘制目标
13        mEglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
14        if (mEglDisplay == EGL14.EGL_NO_DISPLAY) {
15            throw new RuntimeException("eglGetDisplay failed");
16        }
17
18        // 初始化显示窗口
19        int[] version = new int[2];
20        if(!EGL14.eglInitialize(mEglDisplay, version,0,version,1)) {
21            throw new RuntimeException("eglInitialize failed");
22        }
23
24        // 2、配置Display,属性选项(RGBA)
25        int[] configAttributes = {
26                EGL14.EGL_RED_SIZE, 8,
27                EGL14.EGL_GREEN_SIZE, 8,
28                EGL14.EGL_BLUE_SIZE, 8,
29                EGL14.EGL_ALPHA_SIZE, 8,
30                EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, // OpenGL ES 2.0
31                EGL14.EGL_NONE // 参数占位符
32        };
33        int[] numConfigs = new int[1];
34        EGLConfig[] configs = new EGLConfig[1];
35        // EGL根据属性选择一个配置
36        if (!EGL14.eglChooseConfig(mEglDisplay, configAttributes, 0, configs, 0, configs.length,
37                numConfigs, 0)) {
38            throw new RuntimeException("EGL error " + EGL14.eglGetError());
39        }
40
41        EGLConfig mEglConfig = configs[0];
42
43        // EGL上下文参数
44        int[] context_attribute_list = {
45                EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
46                EGL14.EGL_NONE
47        };
48
49        // 让编码的OpenGLContext与GLSurfaceView的OpenGLContext绑定,与GLSurfaceView中的EGLContext共享数据
50        // 只有这样才能拿到处理完之后显示的图像纹理,拿到这个纹理才能进行编码
51        mEglContext = EGL14.eglCreateContext(mEglDisplay, mEglConfig,
52                mGlContext, context_attribute_list, 0);
53
54        if (mEglContext == EGL14.EGL_NO_CONTEXT){
55            throw new RuntimeException("EGL error " + EGL14.eglGetError());
56        }
57
58        // 创建EGLSurface (画布)
59        int[] surface_attribute_list = {
60                EGL14.EGL_NONE
61        };
62        mEglSurface = EGL14.eglCreateWindowSurface(mEglDisplay, mEglConfig, surface, surface_attribute_list, 0);
63        if (mEglSurface == null){
64            throw new RuntimeException("EGL error " + EGL14.eglGetError());
65        }
66
67        // 绑定当前线程的显示器(display)
68        if (!EGL14.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)){
69            throw new RuntimeException("EGL error " + EGL14.eglGetError());
70        }
71
72        recordFilter = new RecordFilter(context);
73        recordFilter.setSize(width,height);
74    }
75
76    public void draw(int textureId, long timestamp) {
77        recordFilter.onDraw(textureId);
78        EGLExt.eglPresentationTimeANDROID(mEglDisplay, mEglSurface, timestamp);
79        // EGLSurface是双缓冲模式
80        EGL14.eglSwapBuffers(mEglDisplay, mEglSurface);
81    }
82
83    public void release(){
84        EGL14.eglDestroySurface(mEglDisplay,mEglSurface);
85        EGL14.eglMakeCurrent(mEglDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE,
86                EGL14.EGL_NO_CONTEXT);
87        EGL14.eglDestroyContext(mEglDisplay, mEglContext);
88        EGL14.eglReleaseThread();
89        EGL14.eglTerminate(mEglDisplay);
90        recordFilter.release();
91    }
92}

MediaRecorder.java负责视频的编码:

  1public class MediaRecorder {
  2    private final int mWidth;
  3    private final int mHeight;
  4    private final String mPath;
  5    private final Context mContext;
  6
  7    private MediaCodec mMediaCodec;
  8    private Surface mSurface;
  9    private EGLContext mGlContext;
 10    private MediaMuxer mMuxer;
 11    private Handler mHandler;
 12    private boolean isStart;
 13    private int track;
 14    private float mSpeed;
 15    private long mLastTimeStamp;
 16    private EGLEnv eglEnv;
 17
 18    public MediaRecorder(Context context, String path, EGLContext glContext, int width, int
 19            height) {
 20        mContext = context.getApplicationContext();
 21        mPath = path;
 22        mWidth = width;
 23        mHeight = height;
 24        mGlContext = glContext;
 25    }
 26
 27    public void start(float speed) throws IOException {
 28        mSpeed = speed;
 29        // 设置编码格式
 30        MediaFormat format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC,
 31                mWidth, mHeight);
 32        // 颜色空间 从 surface当中获得
 33        format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities
 34                .COLOR_FormatSurface);
 35        // 码率
 36        format.setInteger(MediaFormat.KEY_BIT_RATE, 1500_000);
 37        // 帧率
 38        format.setInteger(MediaFormat.KEY_FRAME_RATE, 25);
 39        // 关键帧间隔
 40        format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 10);
 41        // 创建编码器
 42        mMediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
 43        // 配置编码器
 44        mMediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
 45        // 这个surface显示的内容就是要编码的画面
 46        mSurface = mMediaCodec.createInputSurface();
 47
 48        // 混合器 (复用器) 将编码的h.264封装为mp4
 49        mMuxer = new MediaMuxer(mPath,
 50                MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
 51
 52        // 开启编码
 53        mMediaCodec.start();
 54
 55        // 创建OpenGL环境
 56        HandlerThread handlerThread = new HandlerThread("codec-gl");
 57        handlerThread.start();
 58        mHandler = new Handler(handlerThread.getLooper());
 59
 60        mHandler.post(() -> {
 61            // 创建EGL环境
 62            eglEnv = new EGLEnv(mContext, mGlContext, mSurface, mWidth, mHeight);
 63            isStart = true;
 64        });
 65    }
 66
 67
 68    public void fireFrame(final int textureId, final long timestamp) {
 69        if (!isStart) {
 70            return;
 71        }
 72        //录制用的OpenGL已经和Handler的线程绑定了 ,所以需要在这个线程中使用录制的OpenGL
 73        mHandler.post(() -> {
 74            //画画
 75            eglEnv.draw(textureId, timestamp);
 76            codec(false);
 77        });
 78    }
 79
 80
 81    private void codec(boolean endOfStream) {
 82        //给个结束信号
 83        if (endOfStream) {
 84            mMediaCodec.signalEndOfInputStream();
 85        }
 86        while (true) {
 87            //获得输出缓冲区 (编码后的数据从输出缓冲区获得)
 88            MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
 89            int encoderStatus = mMediaCodec.dequeueOutputBuffer(bufferInfo, 10_000);
 90            //需要更多数据
 91            if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
 92                //如果是结束那直接退出,否则继续循环
 93                if (!endOfStream) {
 94                    break;
 95                }
 96            } else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
 97                //输出格式发生改变  第一次总会调用所以在这里开启混合器
 98                MediaFormat newFormat = mMediaCodec.getOutputFormat();
 99                track = mMuxer.addTrack(newFormat);
100                mMuxer.start();
101            } else {
102                //调整时间戳
103                bufferInfo.presentationTimeUs = (long) (bufferInfo.presentationTimeUs / mSpeed);
104                //有时候会出现异常 : timestampUs xxx < lastTimestampUs yyy for Video track
105                if (bufferInfo.presentationTimeUs <= mLastTimeStamp) {
106                    bufferInfo.presentationTimeUs = (long) (mLastTimeStamp + 1_000_000 / 25 / mSpeed);
107                }
108                mLastTimeStamp = bufferInfo.presentationTimeUs;
109
110                //正常则 encoderStatus 获得缓冲区下标
111                ByteBuffer encodedData = mMediaCodec.getOutputBuffer(encoderStatus);
112                //如果当前的buffer是配置信息,不管它 不用写出去
113                if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
114                    bufferInfo.size = 0;
115                }
116                if (bufferInfo.size != 0) {
117                    //设置从哪里开始读数据(读出来就是编码后的数据)
118                    encodedData.position(bufferInfo.offset);
119                    //设置能读数据的总长度
120                    encodedData.limit(bufferInfo.offset + bufferInfo.size);
121                    //写出为mp4
122                    mMuxer.writeSampleData(track, encodedData, bufferInfo);
123                }
124                // 释放这个缓冲区,后续可以存放新的编码后的数据啦
125                mMediaCodec.releaseOutputBuffer(encoderStatus, false);
126                // 如果给了结束信号 signalEndOfInputStream
127                if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
128                    break;
129                }
130            }
131        }
132    }
133
134
135    public void stop() {
136        // 释放
137        isStart = false;
138        mHandler.post(() -> {
139            codec(true);
140            mMediaCodec.stop();
141            mMediaCodec.release();
142            mMediaCodec = null;
143            mMuxer.stop();
144            mMuxer.release();
145            eglEnv.release();
146            eglEnv = null;
147            mMuxer = null;
148            mSurface = null;
149            mHandler.getLooper().quitSafely();
150            mHandler = null;
151        });
152    }
153}

代码其实不难理解,完整的工程请见: https://github.com/zouchanglin/render-camera