05
2017
10

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

在我的一篇博客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();
//设置渲染输出用的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小例子--实现微信界面 下一篇:快速实现自定义控件开关按钮