在我的一篇博客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]


更多相关文章

  1. android上gl纹理资源路径的问题
  2. Android(安卓)OpenGL 纹理绘制图像---总结
  3. android 3D 游戏实现之人物行走(MD2)
  4. 使用MediaCodec实现H264编码「第四章,Android音视频编码那点破事
  5. 丢掉龟速的java媒体库,通过Lame实现Android录音同时转换为mp3格式
  6. 关于Android与pc通信时中文乱码的分析和解决
  7. Android(安卓)OpenGL ES学习笔记之添加纹理
  8. Android实验5---通讯录(解决ListView刷新问题及一些编码规范的总
  9. android视频录制与滤镜(二)——google官方硬编demo:

随机推荐

  1. MySQL简单了解“order by”是怎么工作的
  2. Windows环境下的MYSQL5.7配置文件定位图
  3. mysql 8.0.16 winx64.zip安装配置方法图
  4. mysql 8.0.16 压缩包安装配置方法图文教
  5. 你需要理解的关于MySQL的锁知识
  6. win10下mysql 8.0.16 winx64安装图文最新
  7. mysql installer community 8.0.16.0安装
  8. Windows10下mysql 8.0.16 安装配置方法图
  9. win10下mysql 8.0.16 winx64安装配置方法
  10. Windows10 mysql 8.0.12 非安装版配置启