FFmpeg - 朋友圈錄製視頻添加背景音樂

前幾天有同學問了個問題:輝哥,我們錄製視頻怎麼添加背景音樂?就在今天羣裏也有哥們在問:Android 上傳的視頻 iOS 沒法播放,我怎麼轉換格式呢?令我很驚訝的是大家似乎不會 FFmpeg 也沒有音視頻基礎,但大家又在做一些關於音視頻的功能。搞得我們好像三言兩語施點法,就能幫大家解決問題似的。因此打算寫下此篇文章,希望能幫到有需要的同學。
 gif 錄製有點卡

視頻錄製涉及到知識點還是挺多的,但如果大家不去細究原理與源碼,只是把效果做出來還是挺簡單的,首先我們來羅列一下大致的流程:

  1. OpenGL 預覽相機
  2. MediaCodec 編碼相機數據
  3. MediaMuxer 合成輸出視頻文件

1. OpenGL 預覽相機

我們需要用到 OpenGL 來渲染相機和採集數據,當然我們也可以直接用 SurfaceView 來預覽 Camera ,但直接用 SufaceView 並不方便美顏濾鏡和加水印貼圖,關於 OpenGL 的基礎知識大家可以持續關注後期的文章。爲了方便共享渲染同一個紋理,我們對 GLSurfaceView 的源碼進行修改,但前提是大家需要對 GLSurfaceView 的源碼以及渲染流程瞭如指掌,否則不建議大家直接去修改源碼,因爲不同的版本不同機型,會給我們造成不同的困擾。能在不修改源碼的情況下能解決的問題,儘量不要去動源碼,因此我們儘量用擴展的方式去實現。

/**
 * 擴展 GLSurfaceView ,暴露 EGLContext
 */
public class BaseGLSurfaceView extends GLSurfaceView {
    /**
     * EGL環境上下文
     */
    protected EGLContext mEglContext;

    public BaseGLSurfaceView(Context context) {
        this(context, null);
    }

    public BaseGLSurfaceView(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 利用 setEGLContextFactory 這種擴展方式把 EGLContext 暴露出去
        setEGLContextFactory(new EGLContextFactory() {
            @Override
            public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig) {
                int[] attrib_list = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE};
                mEglContext = egl.eglCreateContext(display, eglConfig, EGL10.EGL_NO_CONTEXT, attrib_list);
                return mEglContext;
            }

            @Override
            public void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context) {
                if (!egl.eglDestroyContext(display, context)) {
                    Log.e("BaseGLSurfaceView", "display:" + display + " context: " + context);
                }
            }
        });
    }

    /**
     * 通過此方法可以獲取 EGL環境上下文,可用於共享渲染同一個紋理
     * @return EGLContext
     */
    public EGLContext getEglContext() {
        return mEglContext;
    }
}

順便提醒一下,我們需要用擴展紋理屬性,否則相機畫面無法渲染出來,同時採用 FBO 離屏渲染來繪製,因爲有些實際開發場景需要加一些水印或者是貼紙等等。

    @Override
    public void onDrawFrame(GL10 gl) {
        // 綁定 fbo
        mFboRender.onBindFbo();
        GLES20.glUseProgram(mProgram);
        mCameraSt.updateTexImage();

        // 設置正交投影參數
        GLES20.glUniformMatrix4fv(uMatrix, 1, false, matrix, 0);

        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVboId);
        /**
         * 設置座標
         * 2:2個爲一個點
         * GLES20.GL_FLOAT:float 類型
         * false:不做歸一化
         * 8:步長是 8
         */
        GLES20.glEnableVertexAttribArray(vPosition);
        GLES20.glVertexAttribPointer(vPosition, 2, GLES20.GL_FLOAT, false, 8,
                0);
        GLES20.glEnableVertexAttribArray(fPosition);
        GLES20.glVertexAttribPointer(fPosition, 2, GLES20.GL_FLOAT, false, 8,
                mVertexCoordinate.length * 4);

        // 繪製到 fbo
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
        // 解綁
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
        mFboRender.onUnbindFbo();
        // 再把 fbo 繪製到屏幕
        mFboRender.onDrawFrame();
    }

2. MediaCodec 編碼相機數據

相機渲染顯示後,接下來我們開一個線程去共享渲染相機的紋理,並且把數據繪製到 MediaCodec 的 InputSurface 上。

    /**
     * 視頻錄製的渲染線程
     */
    public static final class VideoRenderThread extends Thread {
        private WeakReference<BaseVideoRecorder> mVideoRecorderWr;
        private boolean mShouldExit = false;
        private boolean mHashCreateContext = false;
        private boolean mHashSurfaceChanged = false;
        private boolean mHashSurfaceCreated = false;
        private EglHelper mEGlHelper;
        private int mWidth;
        private int mHeight;

        public VideoRenderThread(WeakReference<BaseVideoRecorder> videoRecorderWr) {
            this.mVideoRecorderWr = videoRecorderWr;
            mEGlHelper = new EglHelper();
        }

        public void setSize(int width, int height) {
            this.mWidth = width;
            this.mHeight = height;
        }

        @Override
        public void run() {
            while (true) {
                if (mShouldExit) {
                    onDestroy();
                    return;
                }

                BaseVideoRecorder videoRecorder = mVideoRecorderWr.get();
                if (videoRecorder == null) {
                    mShouldExit = true;
                    continue;
                }

                if (!mHashCreateContext) {
                    // 初始化創建 EGL 環境
                    mEGlHelper.initCreateEgl(videoRecorder.mSurface, videoRecorder.mEglContext);
                    mHashCreateContext = true;
                }

                GL10 gl = (GL10) mEGlHelper.getEglContext().getGL();

                if (!mHashSurfaceCreated) {
                    // 回調 onSurfaceCreated
                    videoRecorder.mRenderer.onSurfaceCreated(gl, mEGlHelper.getEGLConfig());
                    mHashSurfaceCreated = true;
                }

                if (!mHashSurfaceChanged) {
                    // 回調 onSurfaceChanged
                    videoRecorder.mRenderer.onSurfaceChanged(gl, mWidth, mHeight);
                    mHashSurfaceChanged = true;
                }

                // 回調 onDrawFrame
                videoRecorder.mRenderer.onDrawFrame(gl);

                // 繪製到 MediaCodec 的 Surface 上面去
                mEGlHelper.swapBuffers();

                try {
                    // 60 fps
                    Thread.sleep(16 / 1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        private void onDestroy() {
            mEGlHelper.destroy();
        }

        public void requestExit() {
            mShouldExit = true;
        }
    }

3. MediaMuxer 合成輸出視頻文件

目前已有兩個線程,一個線程是相機渲染到屏幕顯示,一個線程是共享相機渲染紋理繪製到 MediaCodec 的 InputSurface 上。那麼我們還需要一個線程用 MediaCodec 編碼合成視頻文件。

    /**
     * 視頻的編碼線程
     */
    public static final class VideoEncoderThread extends Thread {
        private WeakReference<BaseVideoRecorder> mVideoRecorderWr;

        private volatile boolean mShouldExit;

        private MediaCodec mVideoCodec;
        private MediaCodec.BufferInfo mBufferInfo;
        private MediaMuxer mMediaMuxer;

        /**
         * 視頻軌道
         */
        private int mVideoTrackIndex = -1;

        private long mVideoPts = 0;

        public VideoEncoderThread(WeakReference<BaseVideoRecorder> videoRecorderWr) {
            this.mVideoRecorderWr = videoRecorderWr;
            mVideoCodec = videoRecorderWr.get().mVideoCodec;
            mBufferInfo = new MediaCodec.BufferInfo();
            mMediaMuxer = videoRecorderWr.get().mMediaMuxer;
        }

        @Override
        public void run() {
            mShouldExit = false;
            mVideoCodec.start();

            while (true) {
                if (mShouldExit) {
                    onDestroy();
                    return;
                }

                int outputBufferIndex = mVideoCodec.dequeueOutputBuffer(mBufferInfo, 0);

                if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                    mVideoTrackIndex = mMediaMuxer.addTrack(mVideoCodec.getOutputFormat());
                    mMediaMuxer.start();
                } else {
                    while (outputBufferIndex >= 0) {
                        // 獲取數據
                        ByteBuffer outBuffer = mVideoCodec.getOutputBuffers()[outputBufferIndex];
                        outBuffer.position(mBufferInfo.offset);
                        outBuffer.limit(mBufferInfo.offset + mBufferInfo.size);

                        // 修改視頻的 pts
                        if (mVideoPts == 0) {
                            mVideoPts = mBufferInfo.presentationTimeUs;
                        }
                        mBufferInfo.presentationTimeUs -= mVideoPts;

                        // 寫入數據
                        mMediaMuxer.writeSampleData(mVideoTrackIndex, outBuffer, mBufferInfo);

                        // 回調當前錄製時間
                        if (mVideoRecorderWr.get().mRecordInfoListener != null) {
                            mVideoRecorderWr.get().mRecordInfoListener.onTime(mBufferInfo.presentationTimeUs / 1000);
                        }

                        // 釋放 OutputBuffer
                        mVideoCodec.releaseOutputBuffer(outputBufferIndex, false);
                        outputBufferIndex = mVideoCodec.dequeueOutputBuffer(mBufferInfo, 0);
                    }
                }
            }
        }

        private void onDestroy() {
            // 先釋放 MediaCodec
            mVideoCodec.stop();
            mVideoCodec.release();
            // 後釋放 MediaMuxer
            mMediaMuxer.stop();
            mMediaMuxer.release();
        }

        public void requestExit() {
            mShouldExit = true;
        }
    }

在不深究解編碼協議的前提下,只是把效果寫出來還是很簡單的,但一出現問題往往就無法下手了,因此還是有必要去深究一些原理,瞭解一些最最基礎的東西,敬請期待!

視頻地址:https://pan.baidu.com/s/14EVKkIPkRbu8idb-1N-9jw
視頻密碼:jnbp

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章