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

前幾天有同學(xué)問了個問題:輝哥饰抒,我們錄制視頻怎么添加背景音樂?就在今天群里也有哥們在問:Android 上傳的視頻 iOS 沒法播放旅敷,我怎么轉(zhuǎn)換格式呢癣亚?令我很驚訝的是大家似乎不會 FFmpeg 也沒有音視頻基礎(chǔ)寺鸥,但大家又在做一些關(guān)于音視頻的功能。搞得我們好像三言兩語施點法品山,就能幫大家解決問題似的胆建。因此打算寫下此篇文章,希望能幫到有需要的同學(xué)肘交。


gif 錄制有點卡

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

  1. OpenGL 預(yù)覽相機
  2. MediaCodec 編碼相機數(shù)據(jù)
  3. MediaMuxer 合成輸出視頻文件

1. OpenGL 預(yù)覽相機

我們需要用到 OpenGL 來渲染相機和采集數(shù)據(jù)凉驻,當(dāng)然我們也可以直接用 SurfaceView 來預(yù)覽 Camera ,但直接用 SufaceView 并不方便美顏濾鏡和加水印貼圖复罐,關(guān)于 OpenGL 的基礎(chǔ)知識大家可以持續(xù)關(guān)注后期的文章沿侈。為了方便共享渲染同一個紋理,我們對 GLSurfaceView 的源碼進行修改市栗,但前提是大家需要對 GLSurfaceView 的源碼以及渲染流程了如指掌,否則不建議大家直接去修改源碼咳短,因為不同的版本不同機型填帽,會給我們造成不同的困擾。能在不修改源碼的情況下能解決的問題咙好,盡量不要去動源碼篡腌,因此我們盡量用擴展的方式去實現(xiàn)。

/**
 * 擴展 GLSurfaceView 勾效,暴露 EGLContext
 */
public class BaseGLSurfaceView extends GLSurfaceView {
    /**
     * EGL環(huán)境上下文
     */
    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環(huán)境上下文嘹悼,可用于共享渲染同一個紋理
     * @return EGLContext
     */
    public EGLContext getEglContext() {
        return mEglContext;
    }
}

順便提醒一下,我們需要用擴展紋理屬性层宫,否則相機畫面無法渲染出來杨伙,同時采用 FBO 離屏渲染來繪制,因為有些實際開發(fā)場景需要加一些水印或者是貼紙等等萌腿。

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

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

        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVboId);
        /**
         * 設(shè)置坐標(biāo)
         * 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 編碼相機數(shù)據(jù)

相機渲染顯示后限匣,接下來我們開一個線程去共享渲染相機的紋理,并且把數(shù)據(jù)繪制到 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) {
                    // 初始化創(chuàng)建 EGL 環(huán)境
                    mEGlHelper.initCreateEgl(videoRecorder.mSurface, videoRecorder.mEglContext);
                    mHashCreateContext = true;
                }

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

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

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

                // 回調(diào) 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) {
                        // 獲取數(shù)據(jù)
                        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;

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

                        // 回調(diào)當(dāng)前錄制時間
                        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;
        }
    }

在不深究解編碼協(xié)議的前提下,只是把效果寫出來還是很簡單的窗慎,但一出現(xiàn)問題往往就無法下手了物喷,因此還是有必要去深究一些原理,了解一些最最基礎(chǔ)的東西,敬請期待脯丝!

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末商膊,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子宠进,更是在濱河造成了極大的恐慌晕拆,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,084評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件材蹬,死亡現(xiàn)場離奇詭異实幕,居然都是意外死亡,警方通過查閱死者的電腦和手機堤器,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,623評論 3 392
  • 文/潘曉璐 我一進店門昆庇,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人闸溃,你說我怎么就攤上這事整吆。” “怎么了辉川?”我有些...
    開封第一講書人閱讀 163,450評論 0 353
  • 文/不壞的土叔 我叫張陵表蝙,是天一觀的道長。 經(jīng)常有香客問我乓旗,道長府蛇,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,322評論 1 293
  • 正文 為了忘掉前任屿愚,我火速辦了婚禮汇跨,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘妆距。我一直安慰自己穷遂,他們只是感情好娱据,可當(dāng)我...
    茶點故事閱讀 67,370評論 6 390
  • 文/花漫 我一把揭開白布塞颁。 她就那樣靜靜地躺著,像睡著了一般吸耿。 火紅的嫁衣襯著肌膚如雪祠锣。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,274評論 1 300
  • 那天咽安,我揣著相機與錄音伴网,去河邊找鬼。 笑死妆棒,一個胖子當(dāng)著我的面吹牛澡腾,可吹牛的內(nèi)容都是我干的沸伏。 我是一名探鬼主播,決...
    沈念sama閱讀 40,126評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼动分,長吁一口氣:“原來是場噩夢啊……” “哼毅糟!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起澜公,我...
    開封第一講書人閱讀 38,980評論 0 275
  • 序言:老撾萬榮一對情侶失蹤姆另,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后坟乾,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體迹辐,經(jīng)...
    沈念sama閱讀 45,414評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,599評論 3 334
  • 正文 我和宋清朗相戀三年甚侣,在試婚紗的時候發(fā)現(xiàn)自己被綠了明吩。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,773評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡殷费,死狀恐怖印荔,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情详羡,我是刑警寧澤躏鱼,帶...
    沈念sama閱讀 35,470評論 5 344
  • 正文 年R本政府宣布,位于F島的核電站殷绍,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏鹊漠。R本人自食惡果不足惜主到,卻給世界環(huán)境...
    茶點故事閱讀 41,080評論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望躯概。 院中可真熱鬧登钥,春花似錦、人聲如沸娶靡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,713評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽姿锭。三九已至塔鳍,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間呻此,已是汗流浹背轮纫。 一陣腳步聲響...
    開封第一講書人閱讀 32,852評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留焚鲜,地道東北人掌唾。 一個月前我還...
    沈念sama閱讀 47,865評論 2 370
  • 正文 我出身青樓放前,卻偏偏與公主長得像,于是被迫代替她去往敵國和親糯彬。 傳聞我的和親對象是個殘疾皇子凭语,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,689評論 2 354

推薦閱讀更多精彩內(nèi)容