FFmpeg|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; } }

在不深究解编码协议的前提下,只是把效果写出来还是很简单的,但一出现问题往往就无法下手了,因此还是有必要去深究一些原理,了解一些最最基础的东西,敬请期待!
【FFmpeg|FFmpeg - 朋友圈录制视频添加背景音乐】视频地址:https://pan.baidu.com/s/14EVKkIPkRbu8idb-1N-9jw
视频密码:jnbp

    推荐阅读