Tim's Note

试问Coding应不好,却道:此心安处是吾乡

0%

OpenGL+MediaCodec 视频录制

通过 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
public class EGLEnv {
private final EGLContext mEglContext;
private final RecordFilter recordFilter;

private final EGLSurface mEglSurface; // 画布
private final EGLDisplay mEglDisplay; // 窗口

/**
* @param surface MediaRecorder 的 surface
*/
public EGLEnv(Context context,EGLContext mGlContext, Surface surface,int width,int height) {
// 1、创建 Display,获得显示窗口,作为 OpenGL 的绘制目标
mEglDisplay = EGL14.eglGetDisplay (EGL14.EGL_DEFAULT_DISPLAY);
if (mEglDisplay == EGL14.EGL_NO_DISPLAY) {
throw new RuntimeException("eglGetDisplay failed");
}

// 初始化显示窗口
int[] version = new int[2];
if(!EGL14.eglInitialize (mEglDisplay, version,0,version,1)) {
throw new RuntimeException("eglInitialize failed");
}

// 2、配置 Display,属性选项(RGBA)
int[] configAttributes = {
EGL14.EGL_RED_SIZE, 8,
EGL14.EGL_GREEN_SIZE, 8,
EGL14.EGL_BLUE_SIZE, 8,
EGL14.EGL_ALPHA_SIZE, 8,
EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, // OpenGL ES 2.0
EGL14.EGL_NONE // 参数占位符
};
int[] numConfigs = new int[1];
EGLConfig [] configs = new EGLConfig[1];
// EGL 根据属性选择一个配置
if (!EGL14.eglChooseConfig (mEglDisplay, configAttributes, 0, configs, 0, configs.length,
numConfigs, 0)) {
throw new RuntimeException("EGL error " + EGL14.eglGetError ());
}

EGLConfig mEglConfig = configs [0];

// EGL 上下文参数
int[] context_attribute_list = {
EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
EGL14.EGL_NONE
};

// 让编码的 OpenGLContext 与 GLSurfaceView 的 OpenGLContext 绑定,与 GLSurfaceView 中的 EGLContext 共享数据
// 只有这样才能拿到处理完之后显示的图像纹理,拿到这个纹理才能进行编码
mEglContext = EGL14.eglCreateContext (mEglDisplay, mEglConfig,
mGlContext, context_attribute_list, 0);

if (mEglContext == EGL14.EGL_NO_CONTEXT){
throw new RuntimeException("EGL error " + EGL14.eglGetError ());
}

// 创建 EGLSurface (画布)
int[] surface_attribute_list = {
EGL14.EGL_NONE
};
mEglSurface = EGL14.eglCreateWindowSurface (mEglDisplay, mEglConfig, surface, surface_attribute_list, 0);
if (mEglSurface == null){
throw new RuntimeException("EGL error " + EGL14.eglGetError ());
}

// 绑定当前线程的显示器(display)
if (!EGL14.eglMakeCurrent (mEglDisplay, mEglSurface, mEglSurface, mEglContext)){
throw new RuntimeException("EGL error " + EGL14.eglGetError ());
}

recordFilter = new RecordFilter(context);
recordFilter.setSize (width,height);
}

public void draw(int textureId, long timestamp) {
recordFilter.onDraw (textureId);
EGLExt.eglPresentationTimeANDROID (mEglDisplay, mEglSurface, timestamp);
// EGLSurface 是双缓冲模式
EGL14.eglSwapBuffers (mEglDisplay, mEglSurface);
}

public void release(){
EGL14.eglDestroySurface (mEglDisplay,mEglSurface);
EGL14.eglMakeCurrent (mEglDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE,
EGL14.EGL_NO_CONTEXT);
EGL14.eglDestroyContext (mEglDisplay, mEglContext);
EGL14.eglReleaseThread ();
EGL14.eglTerminate (mEglDisplay);
recordFilter.release ();
}
}

MediaRecorder.java 负责视频的编码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
public class MediaRecorder {
private final int mWidth;
private final int mHeight;
private final String mPath;
private final Context mContext;

private MediaCodec mMediaCodec;
private Surface mSurface;
private EGLContext mGlContext;
private MediaMuxer mMuxer;
private Handler mHandler;
private boolean isStart;
private int track;
private float mSpeed;
private long mLastTimeStamp;
private EGLEnv eglEnv;

public MediaRecorder(Context context, String path, EGLContext glContext, int width, int
height) {
mContext = context.getApplicationContext ();
mPath = path;
mWidth = width;
mHeight = height;
mGlContext = glContext;
}

public void start(float speed) throws IOException {
mSpeed = speed;
// 设置编码格式
MediaFormat format = MediaFormat.createVideoFormat (MediaFormat.MIMETYPE_VIDEO_AVC,
mWidth, mHeight);
// 颜色空间 从 surface 当中获得
format.setInteger (MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities
.COLOR_FormatSurface);
// 码率
format.setInteger (MediaFormat.KEY_BIT_RATE, 1500_000);
// 帧率
format.setInteger (MediaFormat.KEY_FRAME_RATE, 25);
// 关键帧间隔
format.setInteger (MediaFormat.KEY_I_FRAME_INTERVAL, 10);
// 创建编码器
mMediaCodec = MediaCodec.createEncoderByType (MediaFormat.MIMETYPE_VIDEO_AVC);
// 配置编码器
mMediaCodec.configure (format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
// 这个 surface 显示的内容就是要编码的画面
mSurface = mMediaCodec.createInputSurface ();

// 混合器 (复用器) 将编码的 h.264 封装为 mp4
mMuxer = new MediaMuxer(mPath,
MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);

// 开启编码
mMediaCodec.start ();

// 创建 OpenGL 环境
HandlerThread handlerThread = new HandlerThread("codec-gl");
handlerThread.start ();
mHandler = new Handler(handlerThread.getLooper ());

mHandler.post (() -> {
// 创建 EGL 环境
eglEnv = new EGLEnv(mContext, mGlContext, mSurface, mWidth, mHeight);
isStart = true;
});
}


public void fireFrame(final int textureId, final long timestamp) {
if (!isStart) {
return;
}
// 录制用的 OpenGL 已经和 Handler 的线程绑定了 ,所以需要在这个线程中使用录制的 OpenGL
mHandler.post (() -> {
// 画画
eglEnv.draw (textureId, timestamp);
codec (false);
});
}


private void codec(boolean endOfStream) {
// 给个结束信号
if (endOfStream) {
mMediaCodec.signalEndOfInputStream ();
}
while (true) {
// 获得输出缓冲区 (编码后的数据从输出缓冲区获得)
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo ();
int encoderStatus = mMediaCodec.dequeueOutputBuffer (bufferInfo, 10_000);
// 需要更多数据
if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
// 如果是结束那直接退出,否则继续循环
if (!endOfStream) {
break;
}
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// 输出格式发生改变 第一次总会调用所以在这里开启混合器
MediaFormat newFormat = mMediaCodec.getOutputFormat ();
track = mMuxer.addTrack (newFormat);
mMuxer.start ();
} else {
// 调整时间戳
bufferInfo.presentationTimeUs = (long) (bufferInfo.presentationTimeUs/mSpeed);
// 有时候会出现异常 : timestampUs xxx < lastTimestampUs yyy for Video track
if (bufferInfo.presentationTimeUs <= mLastTimeStamp) {
bufferInfo.presentationTimeUs = (long) (mLastTimeStamp + 1_000_000 / 25 /mSpeed);
}
mLastTimeStamp = bufferInfo.presentationTimeUs;

// 正常则 encoderStatus 获得缓冲区下标
ByteBuffer encodedData = mMediaCodec.getOutputBuffer (encoderStatus);
// 如果当前的 buffer 是配置信息,不管它 不用写出去
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
bufferInfo.size = 0;
}
if (bufferInfo.size != 0) {
// 设置从哪里开始读数据 (读出来就是编码后的数据)
encodedData.position (bufferInfo.offset);
// 设置能读数据的总长度
encodedData.limit (bufferInfo.offset + bufferInfo.size);
// 写出为 mp4
mMuxer.writeSampleData (track, encodedData, bufferInfo);
}
// 释放这个缓冲区,后续可以存放新的编码后的数据啦
mMediaCodec.releaseOutputBuffer (encoderStatus, false);
// 如果给了结束信号 signalEndOfInputStream
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
break;
}
}
}
}


public void stop() {
// 释放
isStart = false;
mHandler.post (() -> {
codec (true);
mMediaCodec.stop ();
mMediaCodec.release ();
mMediaCodec = null;
mMuxer.stop ();
mMuxer.release ();
eglEnv.release ();
eglEnv = null;
mMuxer = null;
mSurface = null;
mHandler.getLooper ().quitSafely ();
mHandler = null;
});
}
}

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

欢迎关注我的其它发布渠道