Android Camera增加自定義影象處理並錄製MP4
在我的一篇部落格Android Camera API/Camera2 API 相機預覽及濾鏡、貼紙等處理中,介紹瞭如何給相機增加濾鏡貼紙的方法,也就是自定義影象處理。而另外一篇部落格Android硬編碼——音訊編碼、視訊編碼及音視訊混合介紹了一種編碼錄製MP4的方法,雖然兩者結合就能實現Camera增加自定義影象處理並錄製MP4的功能,但是實際上如果自定義的處理稍微複雜一些,或者錄製720p或者1080p的大小的視訊,在幀率上往往無法達到要求,而且在部分手機上難以相容。本篇部落格提供的是一種更為高效、“相容一切正常Android手機”的MP4錄製方案。
總體方案分析
對於前言中的兩篇部落格結合起來作為錄製方案,主要存在兩個問題:
- 部分手機的相容問題.
- 錄製720P及以上的視訊,幀率難以達到要求。
對於第一個問題,手機相容問題在於不同Android手機硬編碼支援的顏色空間有所差異,雖然絕大多數手機都支援YUV420P或者YUV420SP的格式,但是依舊會存在有些奇葩手機只支援另外的格式,如OMX_QCOM_COLOR_FormatYUV420PackedSemiPlanar32m
格式。
對於第二個問題,在上面所介紹的錄製方案中存在資料匯出的問題,glReadPixel同步讀取的方式會打斷GPU的渲染流程,如果採用非同步匯出的方式,資料拷貝也會佔用較長的時間。所以當錄製視訊較大時,就算相機的採集幀率有25幀,錄製也很難達到25幀。
那麼新的方案主要就是需要解決這兩個問題,如果相機採集的資料無須匯入到CPU中,直接交由GPU處理,處理完畢之後,再直接交給MediaCodec進行編碼,那麼這兩個問題就都能夠避免了。
實際上,MediaMuxer是Android 4.3新增的API,也就是說我們需要用Android硬編碼錄製MP4,支援的最低版本就應該是Android4.3。而Android在3.0時增加了SurfaceTexture,支援相機錄製直接輸出到SurfaceTexture上。MediaCodec也能夠直接從Surface上取得影象作為視訊流的輸入,這樣無論Android實際上是怎樣實現的,至少在這個過程中,其對外的表現是沒有資料從CPU到GPU或者GPU到CPU的過程。實際上MediaCodec直接從Surface上錄製,是藉助Graphics Buffer實現的,在這個過程中,的確是避免了Android類似glReadPixels的操作。
這樣一來,新的處理及錄製方案就很明確了:
相機通過SurfaceTexture共享出從相機採集到的影象,然後利用OpenGLES 處理這個影象,處理後的結果一方面交給預覽的Surface呈現出來,一方面交給MediaCodec提供的Surface,進而作為錄製視訊流輸入。具體過程如下:
- 建立OpenGL執行緒。
- 在GL執行緒中建立SurfaceTexture用於共享採集的影象資料。
- 處理SurfaceTexture共享出的紋理,生成新的紋理。
- 將處理後的紋理,渲染到螢幕的Surface上,用於預覽。
- 當用戶開啟錄製時,將處理後的紋理,再渲染到由視訊編碼的MediaCodec提供的Surface上,用於視訊的錄製編碼。
- 伴隨視訊影象的錄製,音訊錄製同步進行,並進行音視訊混流。使用者停止錄製時,給編碼器傳送錄製結束的訊號,結束視訊錄製與編碼,生成MP4檔案。
具體程式碼實現
根據上面分析羅列的過程,程式碼的具體實現如下:
第一步,建立OpenGL執行緒
OpenGL執行緒的建立,可以捋順GLSurfaceView的原始碼,參看GLSurfaceView中GL執行緒的建立、維護及銷燬的過程。主要就是利用EGL創建出OpenGL環境,建立時所在的執行緒,就是OpenGL執行緒。EGL建立GL環境在之前的部落格Android OpenGLES2.0(十五)——利用EGL後臺處理影象就介紹了。不同的是此次利用的是EGL14來建立OpenGL環境,以便提供編碼需要的時間戳。一個簡單的工具類如下:
public class EGLHelper {
private EGLSurface mEGLSurface;
private EGLContext mEGLContext;
private EGLDisplay mEGLDisplay;
private EGLConfig mEGLConfig;
private EGLSurface mEGLCopySurface;
private EGLContext mShareEGLContext= EGL14.EGL_NO_CONTEXT;
private boolean isDebug=true;
private int mEglSurfaceType= EGL14.EGL_WINDOW_BIT;
private Object mSurface;
private Object mCopySurface;
/**
* @param type one of {@link EGL14#EGL_WINDOW_BIT}、{@link EGL14#EGL_PBUFFER_BIT}、{@link EGL14#EGL_PIXMAP_BIT}
*/
public void setEGLSurfaceType(int type){
this.mEglSurfaceType=type;
}
public void setSurface(Object surface){
this.mSurface=surface;
}
public void setCopySurface(Object surface){
this.mCopySurface=surface;
}
/**
* create the environment for OpenGLES
* @param eglWidth width
* @param eglHeight height
*/
public boolean createGLES(int eglWidth, int eglHeight){
int[] attributes = new int[] {
EGL14.EGL_SURFACE_TYPE, mEglSurfaceType, //渲染型別
EGL14.EGL_RED_SIZE, 8, //指定RGB中的R大小(bits)
EGL14.EGL_GREEN_SIZE, 8, //指定G大小
EGL14.EGL_BLUE_SIZE, 8, //指定B大小
EGL14.EGL_ALPHA_SIZE, 8, //指定Alpha大小,以上四項實際上指定了畫素格式
EGL14.EGL_DEPTH_SIZE, 16, //指定深度快取(Z Buffer)大小
EGL14.EGL_RENDERABLE_TYPE, 4, //指定渲染api類別, 如上一小節描述,這裡或者是硬編碼的4(EGL14.EGL_OPENGL_ES2_BIT)
EGL14.EGL_NONE }; //總是以EGL14.EGL_NONE結尾
int glAttrs[] = {
EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, //0x3098是EGL14.EGL_CONTEXT_CLIENT_VERSION,但是4.2以前沒有EGL14
EGL14.EGL_NONE
};
int bufferAttrs[]={
EGL14.EGL_WIDTH,eglWidth,
EGL14.EGL_HEIGHT,eglHeight,
EGL14.EGL_NONE
};
//獲取預設顯示裝置,一般為裝置主螢幕
mEGLDisplay= EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
//獲取版本號,[0]為版本號,[1]為子版本號
int[] versions=new int[2];
EGL14.eglInitialize(mEGLDisplay,versions,0,versions,1);
log(EGL14.eglQueryString(mEGLDisplay, EGL14.EGL_VENDOR));
log(EGL14.eglQueryString(mEGLDisplay, EGL14.EGL_VERSION));
log(EGL14.eglQueryString(mEGLDisplay, EGL14.EGL_EXTENSIONS));
//獲取EGL可用配置
EGLConfig[] configs = new EGLConfig[1];
int[] configNum = new int[1];
EGL14.eglChooseConfig(mEGLDisplay, attributes,0, configs,0, 1, configNum,0);
if(configs[0]==null){
log("eglChooseConfig Error:"+ EGL14.eglGetError());
return false;
}
mEGLConfig = configs[0];
//建立EGLContext
mEGLContext= EGL14.eglCreateContext(mEGLDisplay,mEGLConfig,mShareEGLContext, glAttrs,0);
if(mEGLContext== EGL14.EGL_NO_CONTEXT){
return false;
}
//獲取建立後臺繪製的Surface
switch (mEglSurfaceType){
case EGL14.EGL_WINDOW_BIT:
mEGLSurface= EGL14.eglCreateWindowSurface(mEGLDisplay,mEGLConfig,mSurface,new int[]{EGL14.EGL_NONE},0);
break;
case EGL14.EGL_PIXMAP_BIT:
break;
case EGL14.EGL_PBUFFER_BIT:
mEGLSurface= EGL14.eglCreatePbufferSurface(mEGLDisplay,mEGLConfig,bufferAttrs,0);
break;
}
if(mEGLSurface== EGL14.EGL_NO_SURFACE){
log("eglCreateSurface Error:"+ EGL14.eglGetError());
return false;
}
if(!EGL14.eglMakeCurrent(mEGLDisplay,mEGLSurface,mEGLSurface,mEGLContext)){
log("eglMakeCurrent Error:"+ EGL14.eglQueryString(mEGLDisplay, EGL14.eglGetError()));
return false;
}
log("gl environment create success");
return true;
}
public EGLSurface createEGLWindowSurface(Object object){
return EGL14.eglCreateWindowSurface(mEGLDisplay,mEGLConfig,object,new int[]{EGL14.EGL_NONE},0);
}
public void setShareEGLContext(EGLContext context){
this.mShareEGLContext=context;
}
public EGLContext getEGLContext(){
return mEGLContext;
}
public boolean makeCurrent(){
return EGL14.eglMakeCurrent(mEGLDisplay,mEGLSurface,mEGLSurface,mEGLContext);
}
public boolean makeCurrent(EGLSurface surface){
return EGL14.eglMakeCurrent(mEGLDisplay,surface,surface,mEGLContext);
}
public boolean destroyGLES(){
EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT);
EGL14.eglDestroySurface(mEGLDisplay,mEGLSurface);
EGL14.eglDestroyContext(mEGLDisplay,mEGLContext);
EGL14.eglTerminate(mEGLDisplay);
log("gl destroy gles");
return true;
}
public void setPresentationTime(long time){
EGLExt.eglPresentationTimeANDROID(mEGLDisplay,mEGLSurface,time);
}
public void setPresentationTime(EGLSurface surface,long time){
EGLExt.eglPresentationTimeANDROID(mEGLDisplay,surface,time);
}
public boolean swapBuffers(){
return EGL14.eglSwapBuffers(mEGLDisplay,mEGLSurface);
}
public boolean swapBuffers(EGLSurface surface){
return EGL14.eglSwapBuffers(mEGLDisplay,surface);
}
//建立視訊資料流的OES TEXTURE
public int createTextureID() {
int[] texture = new int[1];
GLES20.glGenTextures(1, texture, 0);
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texture[0]);
GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR);
GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE);
return texture[0];
}
private void log(String log){
if(isDebug){
Log.e("EGLHelper",log);
}
}
}
使用時,建立一個執行緒,然後線上程中呼叫建立方法即可:
EGLHelper mShowEGLHelper=new EGLHelper();
//設定渲染輸出用的Surface
mShowEGLHelper.setSurface(mOutputSurface);
//建立GLES環境,對於WindowSurface來說,這裡傳入的大小是無效的
boolean ret=mShowEGLHelper.createGLES(mPreviewWidth,mPreviewHeight);
第二步,在GL執行緒中建立SurfaceTexture用於共享採集的影象資料
建立GL環境之後,在同樣的執行緒中創建出一個SurfaceTexture設定給相機,用於採集的影象資料紋理的共享。
//這個紋理ID就是後續處理的輸入紋理
mInputTextureId=mShowEGLHelper.createTextureID();
//建立一個SurfaceTexture,設定給相機
mInputTexture=new SurfaceTexture(mInputTextureId);
//給這個SurfaceTexture設定監聽,獲得了Frame的實話,傳送一個訊號,在其他地方,請求這個訊號並做相關處理
//低版本的SurfaceTexture無法指定Frame響應執行緒,這樣是將響應放入主執行緒中,避免訊號的傳送與請求在同一個執行緒中
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
mInputTexture.setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() {
@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
mSem.release();
}
});
}
});
第三步,處理SurfaceTexture共享出的紋理,生成新的紋理
當相機採集到資料時,傳送了一個訊號,在GL執行緒中可以請求這個訊號,每當請求到這個訊號時,就可以處理輸入資料了:
//更新影象流
mInputTexture.updateTexImage();
//獲取影象的變換矩陣
mInputTexture.getTransformMatrix(mRenderer.getTextureMatrix());
//這個Render是由使用者提供的,如果使用者無須處理,直接返回mInputTextureId即可。處理也可直接使用類似於GPUImage的第三方GPU處理框架,outputTextureId即為處理後的紋理id
int outputTextureId=mRenderer.drawToTexture(mInputTextureId);
第四步,處理後的紋理,渲染到螢幕的Surface上
相機錄製時,我們上面處理後的影象主要用於兩個方面,第一為使用者預覽,第二為編碼。無論使用者編碼還是不編碼,預覽是一直存在的。程式碼如下:
//makeCurrent通常只需要設定一次,就可以了,後續的渲染目標都是這個Surface,但是如果在一個GL環境中需要使用到多個Surface,就需要利用makeCurrent來選擇目標Surface
mShowEGLHelper.makeCurrent();
GLES20.glViewport(0,0,mPreviewWidth,mPreviewHeight);
mShowFilter.draw(outputTextureId);
//將渲染的內容真正的呈現到Surface上
mShowEGLHelper.swapBuffers();
第五步,使用者開啟錄製時,處理後的紋理渲染到編碼的Surface上
當用戶開啟錄製時,除了預覽我們還需要將處理後的紋理也渲染到編碼器提供的Surface上。
//利用編碼器提供的Surface,建立EGLSurface
if(mEGLEncodeSurface==null{
mEGLEncodeSurface=mShowEGLHelper.createEGLWindowSurface(mEncodeSurface);
}
//選擇編碼用的EGLSurface
mShowEGLHelper.makeCurrent(mEGLEncodeSurface);
GLES20.glViewport(0,0,mConfig.getVideoFormat().getInteger(MediaFormat.KEY_WIDTH),
mConfig.getVideoFormat().getInteger(MediaFormat.KEY_HEIGHT));
mRecFilter.draw(outputTextureId);
//設定編碼的時間戳
mShowEGLHelper.setPresentationTime(mEGLEncodeSurface,time*1000);
//編碼
videoEncodeStep(false);
mShowEGLHelper.swapBuffers(mEGLEncodeSurface);
最後,音視訊錄製及混流
音訊的獲取與編碼、音視訊的混流和上一遍音視訊硬編碼的博文中是一致的,只是視訊的編碼稍有差別。
視訊編碼的MediaCodec,呼叫了createInputSurface,建立了Surface用來接受處理後的視訊影象,然後在每次渲染後,從MediaCodec中獲取outputbuffer,並寫入MediaMuxer即可。停止錄製時,呼叫signalEndOfInputStream
傳送結束訊號。
private boolean videoEncodeStep(boolean isEnd){
if(isEnd){
mVideoEncoder.signalEndOfInputStream();
}
while (true){
int outputIndex=mVideoEncoder.dequeueOutputBuffer(mVideoEncodeBufferInfo,TIME_OUT);
if(outputIndex>=0){
if(isMuxStarted&&mVideoEncodeBufferInfo.size>0
&&mVideoEncodeBufferInfo.presentationTimeUs>0){
mMuxer.writeSampleData(mVideoTrack,
getOutputBuffer(mVideoEncoder,outputIndex),mVideoEncodeBufferInfo);
}
mVideoEncoder.releaseOutputBuffer(outputIndex,false);
if(mVideoEncodeBufferInfo.flags==MediaCodec.BUFFER_FLAG_END_OF_STREAM){
Log.d(Aavt.debugTag,"CameraRecorder get video encode end of stream");
return true;
}
}else if(outputIndex==MediaCodec.INFO_TRY_AGAIN_LATER){
break;
}else if(outputIndex==MediaCodec.INFO_OUTPUT_FORMAT_CHANGED){
Log.e(Aavt.debugTag,"get video output format changed ->"+mVideoEncoder.getOutputFormat().toString());
mVideoTrack=mMuxer.addTrack(mVideoEncoder.getOutputFormat());
mMuxer.start();
isMuxStarted=true;
}
}
return false;
}
其他
原始碼在github上,有需要的朋友可自行下載,此專案旨在編寫一套小巧實用的Android平臺音訊、視訊(影象)的處理框架,如有幫助,歡迎start、fork和打賞。本篇部落格相關程式碼為CameraRecorder,可以直接鏈入此框架使用:
mCameraRecord=new CameraRecorder();
//設定輸出路徑
mCameraRecord.setOutputPath(Environment.getExternalStorageDirectory().getAbsolutePath()+"/temp_cam.mp4");
//SurfaceView提供Surface用於預覽
mSurfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
mCamera=Camera.open(1);
//設定輸出Surface
mCameraRecord.setOutputSurface(holder.getSurface());
//設定錄製大小
mCameraRecord.setOutputSize(480, 640);
//設定自定義處理
mCameraRecord.setRenderer(new Renderer(){
@Override
public void create() {
try {
//只能在Renderer中呼叫createInputSurfaceTexture,用來作為相機的輸入
mCamera.setPreviewTexture(mCameraRecord.createInputSurfaceTexture());
} catch (IOException e) {
e.printStackTrace();
}
Camera.Size mSize=mCamera.getParameters().getPreviewSize();
mCameraWidth=mSize.height;
mCameraHeight=mSize.width;
mCamera.startPreview();
}
//Renderer的其他方法省略,在draw方法中實現自定義處理
});
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
//設定預覽大小
mCameraRecord.setPreviewSize(width,height);
//開始預覽
mCameraRecord.startPreview();
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
try {
//停止預覽
mCameraRecord.stopPreview();
} catch (InterruptedException e) {
e.printStackTrace();
}
if(mCamera!=null){
mCamera.stopPreview();
mCamera.release();
mCamera=null;
}
}
});