Android(安卓)Camera增加自定义图像处理并录制MP4
在我的一篇博客Android Camera API/Camera2 API 相机预览及滤镜、贴纸等处理中,介绍了如何给相机增加滤镜贴纸的方法,也就是自定义图像处理。而另外一篇博客Android硬编码——音频编码、视频编码及音视频混合介绍了一种编码录制MP4的方法,虽然两者结合就能实现Camera增加自定义图像处理并录制MP4的功能,但是实际上如果自定义的处理稍微复杂一些,或者录制720p或者1080p的大小的视频,在帧率上往往无法达到要求,而且在部分手机上难以兼容。本篇博客提供的是一种更为高效、“兼容一切正常Android手机”的MP4录制方案。
总体方案分析
对于前言中的两篇博客结合起来作为录制方案,主要存在两个问题:
- 部分手机的兼容问题.
- 录制720P及以上的视频,帧率难以达到要求。
对于第一个问题,手机兼容问题在于不同Android手机硬编码支持的颜色空间有所差异,虽然绝大多数手机都支持YUV420P或者YUV420SP的格式,但是依旧会存在有些奇葩手机只支持另外的格式,如OMX_QCOM_COLOR_FormatYUV420PackedSemiPlanar32m
格式。
对于第二个问题,在上面所介绍的录制方案中存在数据导出的问题,glReadPixel同步读取的方式会打断GPU的渲染流程,如果采用异步导出的方式,数据拷贝也会占用较长的时间。所以当录制视频较大时,就算相机的采集帧率有25帧,录制也很难达到25帧。
那么新的方案主要就是需要解决这两个问题,如果相机采集的数据无须导入到CPU中,直接交由GPU处理,处理完毕之后,再直接交给MediaCodec进行编码,那么这两个问题就都能够避免了。
实际上,MediaMuxer是Android 4.3新增的API,也就是说我们需要用Android硬编码录制MP4,支持的最低版本就应该是Android4.3。而Android在3.0时增加了SurfaceTexture,支持相机录制直接输出到SurfaceTexture上。MediaCodec也能够直接从Surface上取得图像作为视频流的输入,这样无论Android实际上是怎样实现的,至少在这个过程中,其对外的表现是没有数据从CPU到GPU或者GPU到CPU的过程。实际上MediaCodec直接从Surface上录制,是借助Graphics Buffer实现的,在这个过程中,的确是避免了Android类似glReadPixels的操作。
这样一来,新的处理及录制方案就很明确了:
相机通过SurfaceTexture共享出从相机采集到的图像,然后利用OpenGLES 处理这个图像,处理后的结果一方面交给预览的Surface呈现出来,一方面交给MediaCodec提供的Surface,进而作为录制视频流输入。具体过程如下:
- 创建OpenGL线程。
- 在GL线程中创建SurfaceTexture用于共享采集的图像数据。
- 处理SurfaceTexture共享出的纹理,生成新的纹理。
- 将处理后的纹理,渲染到屏幕的Surface上,用于预览。
- 当用户开启录制时,将处理后的纹理,再渲染到由视频编码的MediaCodec提供的Surface上,用于视频的录制编码。
- 伴随视频图像的录制,音频录制同步进行,并进行音视频混流。用户停止录制时,给编码器发送录制结束的信号,结束视频录制与编码,生成MP4文件。
具体代码实现
根据上面分析罗列的过程,代码的具体实现如下:
第一步,创建OpenGL线程
OpenGL线程的创建,可以捋顺GLSurfaceView的源码,参看GLSurfaceView中GL线程的创建、维护及销毁的过程。主要就是利用EGL创建出OpenGL环境,创建时所在的线程,就是OpenGL线程。EGL创建GL环境在之前的博客Android OpenGLES2.0(十五)——利用EGL后台处理图像就介绍了。不同的是此次利用的是EGL14来创建OpenGL环境,以便提供编码需要的时间戳。一个简单的工具类如下:
public class EGLHelper { private EGLSurface mEGLSurface; private EGLContext mEGLContext; private EGLDisplay mEGLDisplay; private EGLConfig mEGLConfig; private EGLSurface mEGLCopySurface; private EGLContext mShareEGLContext= EGL14.EGL_NO_CONTEXT; private boolean isDebug=true; private int mEglSurfaceType= EGL14.EGL_WINDOW_BIT; private Object mSurface; private Object mCopySurface; /** * @param type one of {@link EGL14#EGL_WINDOW_BIT}、{@link EGL14#EGL_PBUFFER_BIT}、{@link EGL14#EGL_PIXMAP_BIT} */ public void setEGLSurfaceType(int type){ this.mEglSurfaceType=type; } public void setSurface(Object surface){ this.mSurface=surface; } public void setCopySurface(Object surface){ this.mCopySurface=surface; } /** * create the environment for OpenGLES * @param eglWidth width * @param eglHeight height */ public boolean createGLES(int eglWidth, int eglHeight){ int[] attributes = new int[] { EGL14.EGL_SURFACE_TYPE, mEglSurfaceType, //渲染类型 EGL14.EGL_RED_SIZE, 8, //指定RGB中的R大小(bits) EGL14.EGL_GREEN_SIZE, 8, //指定G大小 EGL14.EGL_BLUE_SIZE, 8, //指定B大小 EGL14.EGL_ALPHA_SIZE, 8, //指定Alpha大小,以上四项实际上指定了像素格式 EGL14.EGL_DEPTH_SIZE, 16, //指定深度缓存(Z Buffer)大小 EGL14.EGL_RENDERABLE_TYPE, 4, //指定渲染api类别, 如上一小节描述,这里或者是硬编码的4(EGL14.EGL_OPENGL_ES2_BIT) EGL14.EGL_NONE }; //总是以EGL14.EGL_NONE结尾 int glAttrs[] = { EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, //0x3098是EGL14.EGL_CONTEXT_CLIENT_VERSION,但是4.2以前没有EGL14 EGL14.EGL_NONE }; int bufferAttrs[]={ EGL14.EGL_WIDTH,eglWidth, EGL14.EGL_HEIGHT,eglHeight, EGL14.EGL_NONE }; //获取默认显示设备,一般为设备主屏幕 mEGLDisplay= EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); //获取版本号,[0]为版本号,[1]为子版本号 int[] versions=new int[2]; EGL14.eglInitialize(mEGLDisplay,versions,0,versions,1); log(EGL14.eglQueryString(mEGLDisplay, EGL14.EGL_VENDOR)); log(EGL14.eglQueryString(mEGLDisplay, EGL14.EGL_VERSION)); log(EGL14.eglQueryString(mEGLDisplay, EGL14.EGL_EXTENSIONS)); //获取EGL可用配置 EGLConfig[] configs = new EGLConfig[1]; int[] configNum = new int[1]; EGL14.eglChooseConfig(mEGLDisplay, attributes,0, configs,0, 1, configNum,0); if(configs[0]==null){ log("eglChooseConfig Error:"+ EGL14.eglGetError()); return false; } mEGLConfig = configs[0]; //创建EGLContext mEGLContext= EGL14.eglCreateContext(mEGLDisplay,mEGLConfig,mShareEGLContext, glAttrs,0); if(mEGLContext== EGL14.EGL_NO_CONTEXT){ return false; } //获取创建后台绘制的Surface switch (mEglSurfaceType){ case EGL14.EGL_WINDOW_BIT: mEGLSurface= EGL14.eglCreateWindowSurface(mEGLDisplay,mEGLConfig,mSurface,new int[]{EGL14.EGL_NONE},0); break; case EGL14.EGL_PIXMAP_BIT: break; case EGL14.EGL_PBUFFER_BIT: mEGLSurface= EGL14.eglCreatePbufferSurface(mEGLDisplay,mEGLConfig,bufferAttrs,0); break; } if(mEGLSurface== EGL14.EGL_NO_SURFACE){ log("eglCreateSurface Error:"+ EGL14.eglGetError()); return false; } if(!EGL14.eglMakeCurrent(mEGLDisplay,mEGLSurface,mEGLSurface,mEGLContext)){ log("eglMakeCurrent Error:"+ EGL14.eglQueryString(mEGLDisplay, EGL14.eglGetError())); return false; } log("gl environment create success"); return true; } public EGLSurface createEGLWindowSurface(Object object){ return EGL14.eglCreateWindowSurface(mEGLDisplay,mEGLConfig,object,new int[]{EGL14.EGL_NONE},0); } public void setShareEGLContext(EGLContext context){ this.mShareEGLContext=context; } public EGLContext getEGLContext(){ return mEGLContext; } public boolean makeCurrent(){ return EGL14.eglMakeCurrent(mEGLDisplay,mEGLSurface,mEGLSurface,mEGLContext); } public boolean makeCurrent(EGLSurface surface){ return EGL14.eglMakeCurrent(mEGLDisplay,surface,surface,mEGLContext); } public boolean destroyGLES(){ EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT); EGL14.eglDestroySurface(mEGLDisplay,mEGLSurface); EGL14.eglDestroyContext(mEGLDisplay,mEGLContext); EGL14.eglTerminate(mEGLDisplay); log("gl destroy gles"); return true; } public void setPresentationTime(long time){ EGLExt.eglPresentationTimeANDROID(mEGLDisplay,mEGLSurface,time); } public void setPresentationTime(EGLSurface surface,long time){ EGLExt.eglPresentationTimeANDROID(mEGLDisplay,surface,time); } public boolean swapBuffers(){ return EGL14.eglSwapBuffers(mEGLDisplay,mEGLSurface); } public boolean swapBuffers(EGLSurface surface){ return EGL14.eglSwapBuffers(mEGLDisplay,surface); } //创建视频数据流的OES TEXTURE public int createTextureID() { int[] texture = new int[1]; GLES20.glGenTextures(1, texture, 0); GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texture[0]); GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR); GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR); GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE); GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE); return texture[0]; } private void log(String log){ if(isDebug){ Log.e("EGLHelper",log); } }}
使用时,创建一个线程,然后在线程中调用创建方法即可:
EGLHelper mShowEGLHelper=new EGLHelper();//设置渲染输出用的SurfacemShowEGLHelper.setSurface(mOutputSurface);//创建GLES环境,对于WindowSurface来说,这里传入的大小是无效的boolean ret=mShowEGLHelper.createGLES(mPreviewWidth,mPreviewHeight);
第二步,在GL线程中创建SurfaceTexture用于共享采集的图像数据
创建GL环境之后,在同样的线程中创建出一个SurfaceTexture设置给相机,用于采集的图像数据纹理的共享。
//这个纹理ID就是后续处理的输入纹理mInputTextureId=mShowEGLHelper.createTextureID();//创建一个SurfaceTexture,设置给相机mInputTexture=new SurfaceTexture(mInputTextureId);//给这个SurfaceTexture设置监听,获得了Frame的实话,发送一个信号,在其他地方,请求这个信号并做相关处理//低版本的SurfaceTexture无法指定Frame响应线程,这样是将响应放入主线程中,避免信号的发送与请求在同一个线程中new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { mInputTexture.setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() { @Override public void onFrameAvailable(SurfaceTexture surfaceTexture) { mSem.release(); } }); }});
第三步,处理SurfaceTexture共享出的纹理,生成新的纹理
当相机采集到数据时,发送了一个信号,在GL线程中可以请求这个信号,每当请求到这个信号时,就可以处理输入数据了:
//更新图像流mInputTexture.updateTexImage();//获取图像的变换矩阵mInputTexture.getTransformMatrix(mRenderer.getTextureMatrix());//这个Render是由使用者提供的,如果使用者无须处理,直接返回mInputTextureId即可。处理也可直接使用类似于GPUImage的第三方GPU处理框架,outputTextureId即为处理后的纹理idint outputTextureId=mRenderer.drawToTexture(mInputTextureId);
第四步,处理后的纹理,渲染到屏幕的Surface上
相机录制时,我们上面处理后的图像主要用于两个方面,第一为用户预览,第二为编码。无论用户编码还是不编码,预览是一直存在的。代码如下:
//makeCurrent通常只需要设置一次,就可以了,后续的渲染目标都是这个Surface,但是如果在一个GL环境中需要使用到多个Surface,就需要利用makeCurrent来选择目标SurfacemShowEGLHelper.makeCurrent();GLES20.glViewport(0,0,mPreviewWidth,mPreviewHeight);mShowFilter.draw(outputTextureId);//将渲染的内容真正的呈现到Surface上mShowEGLHelper.swapBuffers();
第五步,用户开启录制时,处理后的纹理渲染到编码的Surface上
当用户开启录制时,除了预览我们还需要将处理后的纹理也渲染到编码器提供的Surface上。
//利用编码器提供的Surface,创建EGLSurfaceif(mEGLEncodeSurface==null{ mEGLEncodeSurface=mShowEGLHelper.createEGLWindowSurface(mEncodeSurface);}//选择编码用的EGLSurfacemShowEGLHelper.makeCurrent(mEGLEncodeSurface);GLES20.glViewport(0,0,mConfig.getVideoFormat().getInteger(MediaFormat.KEY_WIDTH), mConfig.getVideoFormat().getInteger(MediaFormat.KEY_HEIGHT));mRecFilter.draw(outputTextureId);//设置编码的时间戳mShowEGLHelper.setPresentationTime(mEGLEncodeSurface,time*1000);//编码videoEncodeStep(false);mShowEGLHelper.swapBuffers(mEGLEncodeSurface);
最后,音视频录制及混流
音频的获取与编码、音视频的混流和上一遍音视频硬编码的博文中是一致的,只是视频的编码稍有差别。
视频编码的MediaCodec,调用了createInputSurface,创建了Surface用来接受处理后的视频图像,然后在每次渲染后,从MediaCodec中获取outputbuffer,并写入MediaMuxer即可。停止录制时,调用signalEndOfInputStream
发送结束信号。
private boolean videoEncodeStep(boolean isEnd){ if(isEnd){ mVideoEncoder.signalEndOfInputStream(); } while (true){ int outputIndex=mVideoEncoder.dequeueOutputBuffer(mVideoEncodeBufferInfo,TIME_OUT); if(outputIndex>=0){ if(isMuxStarted&&mVideoEncodeBufferInfo.size>0 &&mVideoEncodeBufferInfo.presentationTimeUs>0){ mMuxer.writeSampleData(mVideoTrack, getOutputBuffer(mVideoEncoder,outputIndex),mVideoEncodeBufferInfo); } mVideoEncoder.releaseOutputBuffer(outputIndex,false); if(mVideoEncodeBufferInfo.flags==MediaCodec.BUFFER_FLAG_END_OF_STREAM){ Log.d(Aavt.debugTag,"CameraRecorder get video encode end of stream"); return true; } }else if(outputIndex==MediaCodec.INFO_TRY_AGAIN_LATER){ break; }else if(outputIndex==MediaCodec.INFO_OUTPUT_FORMAT_CHANGED){ Log.e(Aavt.debugTag,"get video output format changed ->"+mVideoEncoder.getOutputFormat().toString()); mVideoTrack=mMuxer.addTrack(mVideoEncoder.getOutputFormat()); mMuxer.start(); isMuxStarted=true; } } return false;}
其他
源码在github上,有需要的朋友可自行下载,此项目旨在编写一套小巧实用的Android平台音频、视频(图像)的处理框架,如有帮助,欢迎start、fork和打赏。本篇博客相关代码为CameraRecorder,可以直接链入此框架使用:
mCameraRecord=new CameraRecorder(); //设置输出路径mCameraRecord.setOutputPath(Environment.getExternalStorageDirectory().getAbsolutePath()+"/temp_cam.mp4");//SurfaceView提供Surface用于预览mSurfaceView.getHolder().addCallback(new SurfaceHolder.Callback() { @Override public void surfaceCreated(SurfaceHolder holder) { mCamera=Camera.open(1); //设置输出Surface mCameraRecord.setOutputSurface(holder.getSurface()); //设置录制大小 mCameraRecord.setOutputSize(480, 640); //设置自定义处理 mCameraRecord.setRenderer(new Renderer(){ @Override public void create() { try { //只能在Renderer中调用createInputSurfaceTexture,用来作为相机的输入 mCamera.setPreviewTexture(mCameraRecord.createInputSurfaceTexture()); } catch (IOException e) { e.printStackTrace(); } Camera.Size mSize=mCamera.getParameters().getPreviewSize(); mCameraWidth=mSize.height; mCameraHeight=mSize.width; mCamera.startPreview(); } //Renderer的其他方法省略,在draw方法中实现自定义处理 }); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { //设置预览大小 mCameraRecord.setPreviewSize(width,height); //开始预览 mCameraRecord.startPreview(); } @Override public void surfaceDestroyed(SurfaceHolder holder) { try { //停止预览 mCameraRecord.stopPreview(); } catch (InterruptedException e) { e.printStackTrace(); } if(mCamera!=null){ mCamera.stopPreview(); mCamera.release(); mCamera=null; } }});
欢迎转载,转载请保留文章出处。湖广午王的博客[http://blog.csdn.net/junzia/article/details/78154648]
更多相关文章
- android上gl纹理资源路径的问题
- Android(安卓)OpenGL 纹理绘制图像---总结
- android 3D 游戏实现之人物行走(MD2)
- 使用MediaCodec实现H264编码「第四章,Android音视频编码那点破事
- 丢掉龟速的java媒体库,通过Lame实现Android录音同时转换为mp3格式
- 关于Android与pc通信时中文乱码的分析和解决
- Android(安卓)OpenGL ES学习笔记之添加纹理
- Android实验5---通讯录(解决ListView刷新问题及一些编码规范的总
- android视频录制与滤镜(二)——google官方硬编demo: