Android利用MediaCodec和GLSurfaceView录制视频
目录
一,目标
二,核心流程
三,设计到的技术点
4,MediaCodec编码原理
5,核心代码实现
5.1 自定义GLSurfaceView
5.2 设置录制特效模式
5.3 自定义渲染模式GLSurfaceView.Renderer
5.4 自定义视频编码器MediaRecorder
一,目标
使用Android的相机拍摄视频,在相机预览的同时,可以做一些极快,极慢,大眼,瘦脸的效果,需要在录制过程中高性能,如果直接使用 SufaceView 并不方便美颜滤镜和加水印贴图,以及对录制过程中视频操作
二,核心流程
三,设计到的技术点
3.1 OpenGL ES 预览相机(Fbo)
Frame Buffer Object
帧缓冲对象:FBO。默认情况下,我们在`GLSurfaceView`中绘制的结果是显示到屏幕上,然而实际中有很多情况并不需要渲染到屏幕上,这个时候使用FBO就可以很方便的实现这类需求。FBO可以让我们的渲染不渲染到屏幕上,而是渲染到离屏Buffer中。
3.2 MediaCodec 编码相机数据
MediaCodec是Android 4.1.2(API 16)提供的一套编解码API。它的使用非常简单,它存在一个输入缓冲区与一个输出缓冲区,在编码时我们将数据塞入输入缓冲区,然后从输出缓冲区取出编码完成后的数据就可以了
3.3 MediaMuxer 合成输出视频文件
4,MediaCodec编码原理
4.1
除了直接操作输入缓冲区之外,还有另一种方式来告知MediaCodec需要编码的数据,那就是:
public native final Surface createInputSurface();
使用此接口创建一个`Surface`,然后我们在这个`Surface`中"作画",`MediaCodec`就能够自动的编码`Surface`中的“画作”,我们只需要从输出缓冲区取出编码完成之后的数据即可。
此外,我们使用OpenGL进行绘画显示在屏幕上,然而想要复制屏幕图像到内存中却不是一件非常轻松的事情。但是现在我们可以直接将OpenGL显示到屏幕中的图像,同时绘制到`MediaCodec#createInputSurface`当中去,这样子操作就会更加简单高效。
4.2
录制我们在另外一个线程中进行(**录制现场**),所以录制的EGL环境和显示的EGL环境(`GLSurfaceView`,**显示线程**)是两个独立的工作环境,他们又能够共享上下文资源:显示线程中使用的texture等,需要能够在录制线程中操作(通过录制线程中使用OpenGL绘制到MediaCodec的Surface)。
4.3
在这个线程中我们需要自己来:
1)、配置录制使用的EGL环境(参照GLSurfaceView是怎么配置的)
2)、完成将显示的图像绘制到MediaCodec的Surface中
3)、编码(H.264)与复用(封装mp4)的工作
5,核心代码实现
5.1 自定义GLSurfaceView
配置egl版本和设置自定义渲染器,以及渲染模式
public TigerView(Context context, AttributeSet attrs) {super(context, attrs);/*** 设置egl版本*/setEGLContextClientVersion(2);/*** 设置渲染器*/mTigerRender = new TigerRender(this);setRenderer(mTigerRender);/*** 设置按需渲染,当我们调用requestRender()的时候就会调用GlThread回调一次onDrawFrame()*/setRenderMode(RENDERMODE_WHEN_DIRTY);}
5.2 设置录制特效模式
public void startRecord() {float speed = 1.f;switch (mSpeed) {case MODE_EXTRA_SLOW:speed = 0.3f;break;case MODE_SLOW:speed = 0.5f;break;case MODE_NORMAL:speed = 1.f;break;case MODE_FAST:speed = 1.5f;break;case MODE_EXTRA_FAST:speed = 3.f;break;}mTigerRender.startRecord(speed);}
5.3 自定义渲染模式GLSurfaceView.Renderer
public void onSurfaceCreated(GL10 gl, EGLConfig config) {mCameraHelper = new CameraHelper(Camera.CameraInfo.CAMERA_FACING_BACK);//准备好摄像头绘制的画布//通过open gl创建一个纹理idmTextures = new int[1];GLES20.glGenTextures(mTextures.length, mTextures, 0);mSurfaceTexture = new SurfaceTexture(mTextures[0]);//设置有一帧新的数据到来的时候,回调监听mSurfaceTexture.setOnFrameAvailableListener(this);//必须要在GlThread里面创建着色器程序mCameraFilter = new CameraFilter(mView.getContext());mScreeFilter = new ScreeFilter(mView.getContext());EGLContext eglContext = EGL14.eglGetCurrentContext();mMediaRecorder = new MediaRecorder(mView.getContext(), "/mnt/sdcard/test.mp4", CameraHelper.HEIGHT, CameraHelper.WIDTH, eglContext);}/*** 画布发生改变** @param gl the GL interface. Use <code>instanceof</code> to* test if the interface supports GL11 or higher interfaces.* @param width* @param height*/@Overridepublic void onSurfaceChanged(GL10 gl, int width, int height) {mCameraHelper.startPreview(mSurfaceTexture);mCameraFilter.onReady(width, height);mScreeFilter.onReady(width, height);}/*** 画画** @param gl the GL interface. Use <code>instanceof</code> to* test if the interface supports GL11 or higher interfaces.*/@Overridepublic void onDrawFrame(GL10 gl) {//告诉open gl需要把屏幕清理成 什么样子的颜色GLES20.glClearColor(0, 0, 0, 0);//开始真正的屏幕颜色清理,也就是上一次设置的屏幕颜色GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);//把摄像头采集的数据输出出来//更新纹理,然后我们才可以使用opengl从SurfaceTexure当中获取数据,进行渲染mSurfaceTexture.updateTexImage();//mSurfaceTexture比较特殊,在设置坐标的时候,需要一个变换矩阵,使用的是特殊的采样器samplerExternalOES//这种采样器,正常的是sample2DmSurfaceTexture.getTransformMatrix(mtx);mCameraFilter.setMatrix(mtx);int id = mCameraFilter.onDrawFrame(mTextures[0]);//在这里添加各种效果,相当于责任链//开始画画mScreeFilter.onDrawFrame(id);mMediaRecorder.encodeFrame(id, mSurfaceTexture.getTimestamp());}
1)通过在onSurfaceCreated设置相机录制功能,同时准备好相机预览的画布,通过open gl创建一个纹理id,给后续预览各种特效使用
2)给SurfaceTexture设置设置有一帧新的数据到来的时候,回调监听,mSurfaceTexture.setOnFrameAvailableListener(this)
3)onSurfaceChanged里面进行画布大小的设定和纹理绑定
4)onDrawFrame里面进行绘画,同时使用MediaCodc在单独的进程里面进行视频编码encodeFrame
5)在onFrameAvailable中
有新的一帧数据过来以后,有一帧新的图像,就开始调用GLSurfaceView的onDrawFrame进行绘制
5.4 自定义视频编码器MediaRecorder
public MediaRecorder(Context context, String path, int width, int height, EGLContext eglContext) {mContext = context.getApplicationContext();mPath = path;mWidth = width;mHeight = height;mEglContext = eglContext;}/*** 开始录制视频*/public void start(float speed) throws IOException {/*** 配置MediaCodec编码器,视频编码的宽,高,帧率,码率* 录制成mp4格式,视频编码格式是h264 MIMETYPE_VIDEO_AVC 高级编码*/mSpeed = speed;MediaFormat videoFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, mWidth, mHeight);//配置码率 1500kbsvideoFormat.setInteger(MediaFormat.KEY_BIT_RATE, 1500_000);//帧率videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 20);//关键字间隔videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 20);//创建视频高级编码器mMediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);//因为是从Surface中读取的,所以不需要设置这个颜色格式videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);//配置编码器,CONFIGURE_FLAG_ENCODE,mMediaCodec.configure(videoFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);//交给虚拟屏幕,通过opengl 将预览的纹理绘制到这一个虚拟屏幕中,这样子Mediacodc 就会自动编码这一帧图像mInputSurface = mMediaCodec.createInputSurface();//mp4 播放流程 解复用--》解码 》绘制//mp4 编码流程 封装器--》编码//MUXER_OUTPUT_MPEG_4 MP4格式封装器,将h.264通过他写出到文件就可以了mMediaMuxer = new MediaMuxer(mPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);/*** 配置EGL环境,也就是配置我们的虚拟屏幕环境*/HandlerThread handlerThread = new HandlerThread("ViewoCodec");handlerThread.start();Looper looper = handlerThread.getLooper();//子线程和子线程之间的通信mHandler = new Handler(looper);//EGl的绑定线程,对我们自己创建的EGl环境,都是在这个线程里面进行mHandler.post(() -> {//创建我们的EGL环境(虚拟设备,EGL上下文 )mEglBase = new EGLBase(mContext, mWidth, mHeight, mInputSurface, mEglContext);//启动编码器mMediaCodec.start();isStart = true;});}/*** textureId 纹理id* 调用一次,就有一个新的图片需要编码*/public void encodeFrame(int textureId, long timesnap) {if (!isStart) {return;}//切换到子线程中编码mHandler.post(() -> {//把图像纹理画到虚拟屏幕里面mEglBase.draw(textureId, timesnap);//此时我们需要从编码器里面的输出缓冲区获取编码以后的数据就可以了,getCodec(false);});}private void getCodec(boolean endOfStream) {if (endOfStream) {//表示停止录制,此时我们不录制了,需要给mediacoic 通知mMediaCodec.signalEndOfInputStream();}MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();//希望将已经编码完成的数据都获取到,然后写出到mp4文件中while (true) {//并不是传给mediacodec一帧数据就表示可以编码出一帧数据,有可能需要好多帧数据才可以同时编码出数据出来//输出缓冲区//传递-1表示一直等到输出缓冲区有一个编码好的有效的数据以后才会继续向下走,不然就会一直卡在127行,//10_000超时时间int status = mMediaCodec.dequeueOutputBuffer(bufferInfo, 10_000);if (status == MediaCodec.INFO_TRY_AGAIN_LATER) {//如果是停止,就继续循环,// 继续循环就表示不会接收到新的等待编码的图像了//相当于保证mediacodic中所有待编码的数据都编码完成// 标记不是停止,我们退出,下一轮接收到更多的数据才来输出编码以后的数据,我们就让继续走// 表示需要更多数据才可以编码出图像 false是继续录制,未来还有机会在调用getCodecif (!endOfStream) {//结束录制了break;}//否则继续} else if (status == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {//开始编码,就会调用一次MediaFormat outputFormat = mMediaCodec.getOutputFormat();//配置封装器,增加一路指定格式的媒体流index = mMediaMuxer.addTrack(outputFormat);//启动封装器mMediaMuxer.start();} else if (status == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {//忽略} else {//成功取出一个有效的输出ByteBuffer outputBuffer = mMediaCodec.getOutputBuffer(status);if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {//如果获取的ByteBuffer是配置信息,那么就不需要写出到mp4文件中bufferInfo.size = 0;}if (bufferInfo.size != 0) {bufferInfo.presentationTimeUs = (long) (bufferInfo.presentationTimeUs / mSpeed);//写出到 mp4文件中//根据偏移定位去获取数据,而不是从0开始outputBuffer.position(bufferInfo.offset);//设置可读可写的总长度outputBuffer.limit(bufferInfo.offset + bufferInfo.size);mMediaMuxer.writeSampleData(index, outputBuffer, bufferInfo);}//输出缓冲区使用完毕了, 此时就可以回收了,让mediacodec继续使用mMediaCodec.releaseOutputBuffer(status, false);if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {//结束了,break;}}}}public void stop() {isStart = false;mHandler.post(() -> {getCodec(true);mMediaCodec.stop();mMediaCodec.release();mMediaCodec = null;mMediaMuxer.stop();mMediaMuxer.release();mMediaMuxer = null;mEglBase.release();mEglBase = null;mInputSurface = null;mHandler.getLooper().quitSafely();mHandler = null;});}
}