1. 程式人生 > >Android音視訊錄製(4)——變速錄製

Android音視訊錄製(4)——變速錄製

概述

在看本篇文章之前請務必先檢視這面三篇文章:

視訊變速是一個非常有趣的東西,在我們平時看電影的時候,導演對某些鏡頭進行快放(比如動作片的拳腳片段),某些鏡頭進行慢放(比如一些火山噴發之類的),從而造成非常震撼的影視效果。最近非常火的一些app,能讓普通群眾都能拍出很精彩的快速/慢速的視訊,而很多人對這種視訊效果都感覺很贊,下面我就來講述下視訊錄製過程中如何變速錄製。

下面我先說下視訊變速的原理:快速錄製就是“丟”幀,慢速錄製就是“加”幀,但幀率都保持不變,變的是時長。比如我4秒的視訊,幀率是20幀/秒,那一共是80幀,把每一幀都編碼0,1,2…,78,79,假設我定義的快速即為2倍變速,即4秒最後變成2秒的視訊,視訊幀的變化就是丟棄掉一半的幀,只取0, 2, 4…76, 78合成2秒的視訊,幀率依然是20幀/秒。慢速錄製也以1/2速度為例,不過慢速錄製相對複雜些,畢竟刪除總是比建立容易,4秒的視訊最終要變成8秒的視訊,幀率不變,所以肯定要“加”幀,其實就是複製幀,依然是0,1,2…78,79的視訊,對每一幀複製一遍,重新編碼,最後程式設計0,0A,1,1A….78,78A,79,79A一共160幀的8秒視訊。這其中最最核心的點在哪裡?三個字:時間戳。快速錄製的時候,你需要把正常第2n的時間戳設定為n, 慢速錄製的時候,需要把時間戳為n的幀變成2n。當然,talk is cheap, show me the code。下面我們看看如何實現。

程式碼的實現也是分兩部分,第一部分是,Surface變速錄製,第二部分是,Buffer變速錄製。快速變速以2倍速為例,慢速變速以1/2倍速為例

Surface變速錄製

Android音視訊錄製(1)——Surface錄製一文中並沒有說到任何關於時間戳的程式碼,其實因為surface錄製的時候egl預設給我們加上了時間戳,但是我們依然可以通過egl設定我們指定的時間戳,最終達到我們的目的。

首先定義幾種模式:

public enum  Speed{
        NORMAL,//正常速度
        SLOW,//慢速:0.5倍速
        FAST//快速:2倍速
    }

然後在VideoSurfaceEncoder中加入幾個變數:具體看註釋

    private Speed mSpeed;//模式:快速/慢速/常速
    private int mFrameIndex = 0;//實際編碼器渲染幀數
    private long mFirstTime;//第一幀渲染時間
    private long mCurrPTS;//當前正在渲染的幀的時間戳
    private int mDrainIndex = 0;//攝像頭傳遞過來幀數

egl繪製的時候程式碼修改為如下,快速錄製即每兩次丟棄一次,慢速錄製則是每次繪製重複繪製多一次

 //egl 繪製
    public void render(float[] surfaceTextureMatrix, float
[] mvpMatrix) { if(mSpeed == Speed.NORMAL) {//常速錄製 draw(surfaceTextureMatrix, mvpMatrix); }else if(mSpeed == Speed.SLOW){//慢速錄製,則繪製兩次 mCurrPTS = getPTS(); draw(surfaceTextureMatrix, mvpMatrix); mCurrPTS = getPTS(); draw(surfaceTextureMatrix, mvpMatrix); }else if(mSpeed == Speed.FAST){ if(mDrainIndex % 2 == 0){//快速錄製 mCurrPTS = getPTS(); draw(surfaceTextureMatrix, mvpMatrix); } } mDrainIndex++; }

每次繪製,繪製幀數要加1:

 private void draw(float[] surfaceTextureMatrix, float[] mvpMatrix) {
        if(isAllKeyFrame()){
            requestKeyFrame();
        }
        mRenderer.draw(surfaceTextureMatrix, mvpMatrix);
        if(isAllKeyFrame()){
            requestKeyFrame();
        }
        mFrameIndex++;//繪製幀加1
    }

當然最重要的是時間戳的設定:常速的時候直接返回就好了,快速錄製就是根據第一幀的時間戳,得出當前幀對應的當前時間與第一幀時間差的一半,加上第一幀的時間戳,即為正確的時間戳。慢速錄製的時候時間戳就是第一幀時間戳,加上egl已經渲染的幀數乘上幀間隔即可。

private long getPTS() {
        long time = System.nanoTime();
        if(mFirstTime == -1){
            mFirstTime = time;
        }
        if(mSpeed == Speed.NORMAL){
            return time / 1000;
        }
        if(mSpeed == Speed.FAST){
            return mFirstTime + (time - mFirstTime) / 2;
        }
        if(mSpeed == Speed.SLOW){
            return mFirstTime + mFrameIndex * mFrameInterval;
        }
        return time / 1000;

    }

opengl繪製的時候設定時間戳:在SurfaceEncoderRenderer每次繪製完之後,設定時間戳,之後再進行swap操作,時間戳才能真正寫入到編碼器:

        while (mEncoder.isRecording()){
            mLock.lock();
            try {
                Log.d(TAG, "await~~~~");
                mDrawCondition.await();
                mEgl.makeCurrent();
                //makeCurrent表明opengl的操作是在egl環境下
                // clear screen with yellow color so that you can see rendering rectangle
                GLES20.glClearColor(1.0f, 1.0f, 0.0f, 1.0f);
                GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
                mDrawer.setMatrix(mMatrix, 16);
                mDrawer.draw(mTextureId, mMatrix);
                if(!mEncoder.isNormalSpeed()) {
                    mEgl.setPTS(mEncoder.getCurrPTS());//設定時間戳
                }
                mEgl.swapBuffers();
                mEncoder.singalOutput();//通知編碼器執行緒要輸出資料啦
                Log.d(TAG, "draw------------textureId=" + mTextureId);
            }finally {
                mLock.unlock();
            }

MEgl中設定時間戳:

/**
     *設定時間戳
     * @param pts 納秒
     */
    public void setPTS(long pts){
        EGLExt.eglPresentationTimeANDROID(mEglDisplay, mEGLSurface, pts);
    }

這樣surface變速錄製就已經完成。

Buffer 變速錄製

理解了surface的變速錄製,buffer錄製原理也一樣
VideoEncoder需要增加下面的變數:

    private Speed mSpeed = Speed.NORMAL;
    private int mFrameIndex = 0;
    private int mDrainIndex = 0;
    private long mFirstFramePTS = 0;

攝像頭提供幀資料:

 public void addFrame(byte[] data){
        Log.d(TAG, "drain frame-" + mDrainIndex + " frameIndex=" + mFrameIndex);
        if(mSpeed == Speed.FAST){
            if(mDrainIndex % 2 == 0){
                addFrame(data, getPTS());//快速錄製
            }
        }else if(mSpeed == Speed.SLOW){
            addFrame(data, getPTS());
            addFrame(data, getPTS());//慢速錄製
        }else{//normal
            addFrame(data, getPTS());//正常錄製
        }
        mDrainIndex++;
    }

獲取時間戳:這裡和surface錄製有區別,surface錄製時間戳是納秒,surface錄製的時間戳是微妙

public long getPTS(){
        if(mFrameIndex == 0){
            mFirstFramePTS = System.nanoTime() / 1000;
            return mFirstFramePTS;
        }
        long time = System.nanoTime() / 1000;
        if(mSpeed == Speed.FAST){
            return mFirstFramePTS + (time - mFirstFramePTS) / 2;//快速錄製
        }else if(mSpeed == Speed.NORMAL){//正常錄製
            return time;
        }else if(mSpeed == Speed.SLOW){//慢速錄製
            return mFirstFramePTS + mFrameIndex * mFrameInterval;
        }
        return System.nanoTime() / 1000;
    }

每次繪製的時候繪製幀都需要加1:

 public void addFrame(Frame frame){
        try {
            mLock.lock();
            mFrameList.add(frame);
            mFrameIndex++;//繪製幀+1
            Log.d(TAG, "add frame-" + frame.mTime + " frameIndex=" + mFrameIndex + " interval=" + mFrameInterval);
            mCondition.signal();

        }finally {
            mLock.unlock();
        }
    }

自此,變速錄製的就講解完了,各位小夥伴有什麼疑問的,歡迎反饋。