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

概述

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

第一篇: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();
        }
    }

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

你可能感兴趣的:(Android音视频录制(4)——变速录制)