Android Camera增加自定义图像处理并录制MP4

来源:互联网 发布:java 按键监听 编辑:程序博客网 时间:2024/05/21 13:54

在我的一篇博客Android Camera API/Camera2 API 相机预览及滤镜、贴纸等处理中,介绍了如何给相机增加滤镜贴纸的方法,也就是自定义图像处理。而另外一篇博客Android硬编码——音频编码、视频编码及音视频混合介绍了一种编码录制MP4的方法,虽然两者结合就能实现Camera增加自定义图像处理并录制MP4的功能,但是实际上如果自定义的处理稍微复杂一些,或者录制720p或者1080p的大小的视频,在帧率上往往无法达到要求,而且在部分手机上难以兼容。本篇博客提供的是一种更为高效、“兼容一切正常Android手机”的MP4录制方案。

总体方案分析

对于前言中的两篇博客结合起来作为录制方案,主要存在两个问题:

  1. 部分手机的兼容问题.
  2. 录制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,进而作为录制视频流输入。具体过程如下:

  1. 创建OpenGL线程。
  2. 在GL线程中创建SurfaceTexture用于共享采集的图像数据。
  3. 处理SurfaceTexture共享出的纹理,生成新的纹理。
  4. 将处理后的纹理,渲染到屏幕的Surface上,用于预览。
  5. 当用户开启录制时,将处理后的纹理,再渲染到由视频编码的MediaCodec提供的Surface上,用于视频的录制编码。
  6. 伴随视频图像的录制,音频录制同步进行,并进行音视频混流。用户停止录制时,给编码器发送录制结束的信号,结束视频录制与编码,生成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]


原创粉丝点击