1. 程式人生 > >Android Camera增加自定義影象處理並錄製MP4

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;
        }
    }
});