在我的一篇博客Android Camera API/Camera2 API 相机预览及滤镜、贴纸等处理中,介绍了如何给相机增加滤镜贴纸的方法,也就是自定义图像处理。而另外一篇博客Android硬编码——音频编码、视频编码及音视频混合介绍了一种编码录制MP4的方法,虽然两者结合就能实现Camera增加自定义图像处理并录制MP4的功能,但是实际上如果自定义的处理稍微复杂一些,或者录制720p或者1080p的大小的视频,在帧率上往往无法达到要求,而且在部分手机上难以兼容。本篇博客提供的是一种更为高效、“兼容一切正常Android手机”的MP4录制方案。
对于前言中的两篇博客结合起来作为录制方案,主要存在两个问题:
对于第一个问题,手机兼容问题在于不同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线程的创建,可以捋顺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设置给相机,用于采集的图像数据纹理的共享。
//这个纹理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();
}
});
}
});
当相机采集到数据时,发送了一个信号,在GL线程中可以请求这个信号,每当请求到这个信号时,就可以处理输入数据了:
//更新图像流
mInputTexture.updateTexImage();
//获取图像的变换矩阵
mInputTexture.getTransformMatrix(mRenderer.getTextureMatrix());
//这个Render是由使用者提供的,如果使用者无须处理,直接返回mInputTextureId即可。处理也可直接使用类似于GPUImage的第三方GPU处理框架,outputTextureId即为处理后的纹理id
int outputTextureId=mRenderer.drawToTexture(mInputTextureId);
相机录制时,我们上面处理后的图像主要用于两个方面,第一为用户预览,第二为编码。无论用户编码还是不编码,预览是一直存在的。代码如下:
//makeCurrent通常只需要设置一次,就可以了,后续的渲染目标都是这个Surface,但是如果在一个GL环境中需要使用到多个Surface,就需要利用makeCurrent来选择目标Surface
mShowEGLHelper.makeCurrent();
GLES20.glViewport(0,0,mPreviewWidth,mPreviewHeight);
mShowFilter.draw(outputTextureId);
//将渲染的内容真正的呈现到Surface上
mShowEGLHelper.swapBuffers();
当用户开启录制时,除了预览我们还需要将处理后的纹理也渲染到编码器提供的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]