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 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 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 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 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

你可能感兴趣的:(FFmpeg - 朋友圈录制视频添加背景音乐)