视频录制可以使用android提供的api,如MediaRecorder,对视频的编码也有MediaCodec这样的api可以使用。
MediaCodec的使用,用到两个缓存队列,一个输入缓存队列,一个是输出缓存队列,只需要使用queueInputBuffer把要编码的数据byte数组提交到输入队列,就可以使用enqueueOutputbuffer从输出队列取出编码完成的数据。这个用法有一个前提,就是要拿到待编码的byte数据。
但是,在使用opengl es将摄像头数据绘制到屏幕时,数据的处理是在着色器中处理的。没有办法从着色器中直接拿到byte数组,那怎么完成视频的录制呢?
MediaCodec中有一个方法:public native final Surface createInputSurface();
这个方法会请求一个surface作为待编码数据的输入,这个Surface必须是被硬件加速层渲染的,比如Opengl es。也就是说,如果我们往Surface中绘制数据,那么MediaCodec就可以从Surface中拿数据来完成编码。
那么opengl 怎么往Surface中绘制数据呢?
在Android中Surface就是一块画布,在WindowManagerService中对应了一个窗口WindowState,在SurfaceFlinger中对应了一个Layer,在Surface内部有一块存储数据的内存空间。这些都是Android中的概念,但是Opengl它不认识Surface是什么。
我们接下来要解决的问题就是让opengl认识Surface。
怎么认识呢?Surface除了是一块画布,还有一个概念,就是它是面向应用程序的本地窗口ANativeWindow,openggl要完成绘制,一定是要和本地窗口建立关联的,也即是opengl环境的搭建,完成这个工作的是egl。
EGL是图形渲染api和本地窗口系统之间的一层接口,提供如下功能:
1,创建rendering surface,让应用程序可以在上面作图。
2,创建graphics context,因为opengl是一个状态机,是一个pipeline,所以它需要状态管理,这就是context的工作。
3,同步应用程序和本地平台渲染api。
4,提供对显示设备的访问。
参考GLSurfaceView的源码,在GLSurfaceView的GLThread运行起来后,会借助EglHelper来初始了EGL环境,这个过程中创建了EGLSurface,实现了opengl绘制数据到EGLSurface中,因为EGLSurface和Surface做了绑定,最终实现数据绘制到了Surface中。
EGLSurface是可以跟Surface产生关联的,这就可以实现使用opengl绘制的数据,通过EGLSurface,传递到Surface,进一步传给MediaCodec,完成编码。
最终实现的结构图是这样的:
首先,摄像头的数据到了GLSurfaceView中,GLSurfaceView搭建了EGL环境,在EGL环境中创建了EGLSurface,并与GLSurfaceView中的Surface做了绑定,所以opengl绘制数据到EGLSurface,实际就绘制到了Surface中。
然后,MediaCodec中也有一个surface,我们去搭建一个EGL环境,在我们搭建的EGL环境中创建一个EGLSurface与MediaCodec中的surface绑定起来,让opengl把数据也绘制一份到这个EGL环境中的EGLSurface中,这样Mediacodec就可以拿到要编码的数据了。
下面就看代码实现:
视频录制需要动态开启Camera,Storage的权限。
MediaCodec的使用,配置编码器,创建编码器,得到编码器的inputSurface,开启编码。
然后把编码后的数据通过MediaMuxer封装到一个容器中。
在编码器启动后,就要开始不断的获取输入数据了,所以在启动编码器后,会创建EGL环境,把MediaCodec中的surface绑定到EGL的eglSurface上。
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, 24);
//关键帧间隔
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();
//混合器(复用器),将编码的h264封装为mp4,
mMuxer = new MediaMuxer(mPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
//开始编码
mMediaCodec.start();
//创建opengl的环境,
HandlerThread gl_codec = new HandlerThread("gl_codec");
gl_codec.start();
mHandler = new Handler(gl_codec.getLooper());
mHandler.post(new Runnable() {
@Override
public void run() {
eglEnv = new EGLEnv(mContext, mGlContext, mSurface, mWidth, mHeight);
isStart = true;
}
});
}
然后看下EGL环境搭建的代码,在单独的类EGLEnv.java中完成:
EGL环境的搭建流程:
1,获得显示窗口,作为opengl的绘制目标,
2,初始化显示窗口
3,配置属性选项,
4,创建EGL上下文,
5,创建EGL surface,
6,选定当前的上下文,绑定当前线程的显示设备,
public EGLEnv(Context context, EGLContext mGlContext, Surface surface, int width, int height) {
//获得显示窗口,作为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.");
}
//配置属性选项,
int[] configAttribs = {
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[] eglConfig = new EGLConfig[1];
if (!EGL14.eglChooseConfig(mEglDisplay, configAttribs, 0,
eglConfig, 0, eglConfig.length,
numConfigs, 0)) {
throw new RuntimeException("eglChooseConfig,failed."+EGL14.eglGetError());
}
mEglConfig = eglConfig[0];
//创建EGL上下文,
int[] contex_attrib_list = {
EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
EGL14.EGL_NONE
};
//与GLSurfaceView中的EGLContext共享数据,只有这样才能拿到处理完之后显示的图像纹理,
mEglContext = EGL14.eglCreateContext(mEglDisplay, mEglConfig, mGlContext, contex_attrib_list,0);
if (mEglContext == EGL14.EGL_NO_CONTEXT) {
throw new RuntimeException("eglCreateContext,failed."+EGL14.eglGetError());
}
//创建EGL surface
int[] surface_attrib_list = {
EGL14.EGL_NONE
};
mEglSurface = EGL14.eglCreateWindowSurface(mEglDisplay, mEglConfig, surface, surface_attrib_list, 0);
if (mEglSurface == null) {
throw new RuntimeException("eglCreateWindowSurface,failed."+EGL14.eglGetError());
}
//选定当前的上下文,绑定当前线程的显示设备,一个进程中可能创建多个context,
// 必须选择其中一个作为当前的处理对象。这里选择的是跟GLSurfaceView共享context,
if (!EGL14.eglMakeCurrent(mEglDisplay,mEglSurface, mEglSurface, mEglContext)) {
throw new RuntimeException("eglMakeCurrent,failed."+EGL14.eglGetError());
}
recordFilter = new RecordFilter(context);
}
这里面那么多概念是什么样的关系呢?在理下,EGLContext会管理绘制过程,EGLDisplay是一个跟具体系统无关的显示设备,其中包含着一个EGLSurface,在EGLSurface创建时,关联了一个本地窗口Surface,这个Surface决定了绘制的数据是显示到屏幕,还是作为MediaCodec的输入。
还有一点需要 注意:
//与GLSurfaceView中的EGLContext共享数据,只有这样才能拿到处理完之后显示的图像纹理,
mEglContext = EGL14.eglCreateContext(mEglDisplay, mEglConfig, mGlContext, contex_attrib_list,0);
其中的参数mGlContext来自于EGL14.eglGetCurrentContext(),并且是在GLSurfaceView的render实现类中的onSurfaceCreated中赋值的。
原本GLSurfaceView中EGLSurface和我们自己创建的EGL环境中EGLSurface是没有关联的,但是我们用同一个EGLContext上下文,这就让他们之间产生了关联。也就实现了MediaCodec环境中需要的EGLSurface,能够使用到,共享到GLSurfaceView中纹理。
准备工作完成,下面开始绘制,录像。
在有一帧新的数据时,回调GLSurfaceView.Renderer的onDrawFrame
@Override
public void onDrawFrame(GL10 gl) {
//绘制与摄像头绑定的纹理,实际的绘制只是把参数传给着色器,
int id =cameraFilter.onDraw(texture[0]);
mRecorder.fireFrame(id, mCameraTexture.getTimestamp());
}
这里的纹理id,是代表绘制到屏幕上的纹理,我们要拿这个纹理id,用opengl绘制到mediacodec的EGLSurface上去。
在 int id =cameraFilter.onDraw(texture[0]);这之前,可能会有多个filter来处理数据,都会返回一个纹理id,录像这里需要的纹理id是最后一级需要绘制到屏幕上的纹理id,也就是包含了所有处理效果的数据,当然也可以中间部分的纹理id,比如说某些特效,只想预览,不想保存下来。
在MyMediaRecorder中,新启一个线程,处理绘制,编码:
public void fireFrame(final int textureId, final long timeStamp) {
if (!isStart) {
return;
}
//录制用的opengl已经和mHandler所在线程绑定,所以需要在这个线程中使用录制的opengl
mHandler.post(new Runnable() {
@Override
public void run() {
//绘制
eglEnv.draw(textureId, timeStamp);
//编码
surfaceCodec(false);
}
});
}
EGLEnv的绘制,
public void draw(int textureId, long timestamp) {
recordFilter.onDraw(textureId, mFilterChain);
//设置绘制时间
EGLExt.eglPresentationTimeANDROID(mEglDisplay, mEglSurface, timestamp);
//egl surface是双缓冲模式,绘制完成后,需要交换前后台buffer,才能将绘制数据显示到屏幕上。
EGL14.eglSwapBuffers(mEglDisplay, mEglSurface);
}
这里的RecordFilter的绘制流程,跟普通的opengl往屏幕绘制没有区别,直接使用了父类AbstractFilter的绘制操作;
public class RecordFilter extends AbstractFilter{}
看到这里,你可能会有疑问,既然跟opengl往屏幕绘制没有区别,那数据怎么绘制到mediacodec中的EGLSurface中的呢?
还记得在创建EGL环境时,最后一句调用
EGL14.eglMakeCurrent(mEglDisplay,mEglSurface, mEglSurface, mEglContext)
这句话的作用就是指定当前的上下文,设定opengl当前处理的显示器,当前处理的EGLSurface,这样数据就绘制到了指定的mEGLSurface中。
绘制完后,获取编码后的数据,封装成MP4文件,这块实现是MyMediaRecorder中代码:
private void surfaceCodec(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) {
//如果还没结束录像,就退出循环,等待下次拿到更多Camera数据完成编码。
if (!endOfStream) {
break;
}
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
//输出格式发生改变,开启混合器,第一次总会调用,
MediaFormat outputFormat = mMediaCodec.getOutputFormat();
track = mMuxer.addTrack(outputFormat);
mMuxer.start();
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
//忽略,不处理
} else {
//调整时间戳,实现快速,慢速录像,
bufferInfo.presentationTimeUs = (long)(bufferInfo.presentationTimeUs / mSpeed);
//bufferInfo.presentationTimeUs <= mLastTimeStamp可能会有异常。
if (bufferInfo.presentationTimeUs <= mLastTimeStamp) {
bufferInfo.presentationTimeUs = (long) (mLastTimeStamp + 1_000_000 / 24/ 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);
//如果给了结束信号,
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
break;
}
}
}
}
实现快速,慢速录制代码:
//调整时间戳,实现快速,慢速录像,
bufferInfo.presentationTimeUs = (long)(bufferInfo.presentationTimeUs / mSpeed);
//bufferInfo.presentationTimeUs <= mLastTimeStamp可能会有异常。
if (bufferInfo.presentationTimeUs <= mLastTimeStamp) {
bufferInfo.presentationTimeUs = (long) (mLastTimeStamp + 1_000_000 / 24/ mSpeed);
}
mLastTimeStamp = bufferInfo.presentationTimeUs;
就是通过调整mSpeed参数,大于1时表示快速,小于1表示慢速。实际调整的就是每一帧图像pts值,也就是视频显示的时间戳,这个时间戳是递增的。所以快速,慢速视频,对文件大小是没有影响的,