Android Camera增加自定义图像处理并录制MP4
Posted 湖广午王
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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();
//设置渲染输出用的Surface
mShowEGLHelper.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即为处理后的纹理id
int outputTextureId=mRenderer.drawToTexture(mInputTextureId);
第四步,处理后的纹理,渲染到屏幕的Surface上
相机录制时,我们上面处理后的图像主要用于两个方面,第一为用户预览,第二为编码。无论用户编码还是不编码,预览是一直存在的。代码如下:
//makeCurrent通常只需要设置一次,就可以了,后续的渲染目标都是这个Surface,但是如果在一个GL环境中需要使用到多个Surface,就需要利用makeCurrent来选择目标Surface
mShowEGLHelper.makeCurrent();
GLES20.glViewport(0,0,mPreviewWidth,mPreviewHeight);
mShowFilter.draw(outputTextureId);
//将渲染的内容真正的呈现到Surface上
mShowEGLHelper.swapBuffers();
第五步,用户开启录制时,处理后的纹理渲染到编码的Surface上
当用户开启录制时,除了预览我们还需要将处理后的纹理也渲染到编码器提供的Surface上。
//利用编码器提供的Surface,创建EGLSurface
if(mEGLEncodeSurface==null
mEGLEncodeSurface=mShowEGLHelper.createEGLWindowSurface(mEncodeSurface);
//选择编码用的EGLSurface
mShowEGLHelper.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 Camera增加自定义图像处理并录制MP4的主要内容,如果未能解决你的问题,请参考以下文章
Android Camera API/Camera2 API 相机预览及滤镜贴纸等处理