前幾天有同窗問了個問題:輝哥,咱們錄製視頻怎麼添加背景音樂?就在今天羣裏也有哥們在問:Android 上傳的視頻 iOS 無法播放,我怎麼轉換格式呢?令我很驚訝的是你們彷佛不會 FFmpeg 也沒有音視頻基礎,但你們又在作一些關於音視頻的功能。搞得咱們好像三言兩語施點法,就能幫你們解決問題似的。所以打算寫下此篇文章,但願能幫到有須要的同窗。 bash
視頻錄製涉及到知識點仍是挺多的,但若是你們不去細究原理與源碼,只是把效果作出來仍是挺簡單的,首先咱們來羅列一下大體的流程:ide
咱們須要用到 OpenGL 來渲染相機和採集數據,固然咱們也能夠直接用 SurfaceView 來預覽 Camera ,但直接用 SufaceView 並不方便美顏濾鏡和加水印貼圖,關於 OpenGL 的基礎知識你們能夠持續關注後期的文章。爲了方便共享渲染同一個紋理,咱們對 GLSurfaceView 的源碼進行修改,但前提是你們須要對 GLSurfaceView 的源碼以及渲染流程瞭如指掌,不然不建議你們直接去修改源碼,由於不一樣的版本不一樣機型,會給咱們形成不一樣的困擾。能在不修改源碼的狀況下能解決的問題,儘可能不要去動源碼,所以咱們儘可能用擴展的方式去實現。ui
/**
* 擴展 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 離屏渲染來繪製,由於有些實際開發場景須要加一些水印或者是貼紙等等。this
@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();
}
複製代碼
相機渲染顯示後,接下來咱們開一個線程去共享渲染相機的紋理,而且把數據繪製到 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;
}
}
複製代碼
目前已有兩個線程,一個線程是相機渲染到屏幕顯示,一個線程是共享相機渲染紋理繪製到 MediaCodec 的 InputSurface 上。那麼咱們還須要一個線程用 MediaCodec 編碼合成視頻文件。spa
/**
* 視頻的編碼線程
*/
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;
}
}
複製代碼
在不深究解編碼協議的前提下,只是把效果寫出來仍是很簡單的,但一出現問題每每就沒法下手了,所以仍是有必要去深究一些原理,瞭解一些最最基礎的東西,敬請期待!線程
視頻地址:pan.baidu.com/s/14EVKkIPk… 視頻密碼:jnbpcode