Android音视频录制(4)——变速录制

来源:互联网 发布:淘宝投诉失败 编辑:程序博客网 时间:2024/05/18 12:04

概述

在看本篇文章之前请务必先查看这面三篇文章:

第一篇:Android音视频录制概述
第二篇Android音视频录制(1)——Surface录制
第三篇Android音视频录制(2)——Buffer录制

视频变速是一个非常有趣的东西,在我们平时看电影的时候,导演对某些镜头进行快放(比如动作片的拳脚片段),某些镜头进行慢放(比如一些火山喷发之类的),从而造成非常震撼的影视效果。最近非常火的一些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();        }    }

自此,变速录制的就讲解完了,各位小伙伴有什么疑问的,欢迎反馈。

原创粉丝点击