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