視訊採集:Android平臺基於Camera 1的實現
前言
這篇文章簡單介紹下移動端Android系統下利用Camera1
進行視訊資料採集的方法。
按照慣例先上一份原始碼ofollow,noindex">AndroidVideo 。
Camera1呼叫攝像頭採集視訊的核心實現在CameraCapture.java 。
許可權配置
使用Android平臺提供的攝像頭,首先必須在配置檔案中新增如下許可權配置:
<uses-permission android:name="android.permission.CAMERA"/>
開啟攝像頭
1、首先我們需要獲取當前裝置的攝像頭數量:
int cameraNum = Camera.getNumberOfCameras();
2、一般業務上面都會指定是開啟前置攝像頭還是後置攝像頭:
//獲取對應攝像頭資訊 for (int id = 0; id < cameraNum; id++) { Camera.getCameraInfo(id, info); if (info.facing == cameraId) { //TODO } }
判斷info.facing
的值,他的值有如下幾種:
Camera.CameraInfo.CAMERA_FACING_FRONT Camera.CameraInfo.CAMERA_FACING_BACK
public static Camera open(int cameraId)
需要傳入的是攝像頭的ID;一般手機發展來說,都是先有後置攝像頭,然後才發展前置攝像頭,所以攝像頭的ID排列是後置是0,前置是1,其他攝像頭再遞增。
但是我們最好通過CAMERA_FACING_FRONT
和CAMERA_FACING_BACK
比對才比較靠譜。
open()
返回攝像頭例項,如果返回NULL或者拋異常,請檢查cameraId
傳入是否有誤或者許可權申請被禁止情況。
配置攝像頭引數
一般來說,我們需要關注攝像頭預覽格式、幀率和寬高尺寸等配置。
獲取引數集合
//獲取攝像頭引數設定集合 Camera.Parameters parms = mCamera.getParameters(); //進行引數設定 //必須setParameters後,更新的屬性才會生效 mCamera.setParameters(parms);
設定預覽格式
一般最通用的就是ImageFormat.NV21
格式,其實也就是YUV420SP
格式,對於YUV的具體格式這裡不做擴充套件分析,其最重要一點就是YUV420SP
的UV是交錯存放在一個平面的。
我們需要呼叫parms.getSupportedPreviewFormats()
返回一個支援格式列表,然後判斷其中是否包含我們所需要的格式。
//獲取支援的預覽格式集合 List<Integer> supportedPreviewFormat = parms.getSupportedPreviewFormats(); //一般來說,ImageFormat.NV21通用適配絕大部分手機 if (supportedPreviewFormat.contains(mConfig.mFormat)) { parms.setPreviewFormat(mConfig.mFormat); } else { //格式不相容,採用預設格式 or 返回客戶端處理錯誤 }
設定預覽寬高
通過呼叫parms.getSupportedPreviewSizes()
得出支援的size列表,然後對比我們需要的szie,查詢列表中是否存在。
如果不存在,建議取一個相對靠近的支援的size進行設定。
int weight; int lastWeight = Integer.MAX_VALUE; int curWidth = 0, curHeight = 0; //獲取支援的預覽size列表 List<Camera.Size> sizes = parms.getSupportedPreviewSizes(); for (Camera.Size size : sizes) { //如果height和width都一致,直接設定 if (size.height == mConfig.mHeight && size.width == mConfig.mWidth) { curWidth = size.width; curHeight = size.height; break; } //計算權重,這裡採用差值平方來做比較,也可以採用其他方式計算 weight = (size.width - mConfig.mWidth) * (size.width - mConfig.mWidth) + (size.height - mConfig.mHeight) * (size.height - mConfig.mHeight); if (weight < lastWeight) { curWidth = size.width; curHeight = size.height; } } //設定預覽的size尺寸 parms.setPreviewSize(curWidth, curHeight);
設定預覽的幀率
設定原始碼:
int weight; int lastWeight = Integer.MAX_VALUE; int curRange[] = new int[2]; //獲取支援的幀率上下限列表 List<int[]> ranges = parms.getSupportedPreviewFpsRange(); for (int[] range : ranges) { //如果幀率在支援的範圍之間,直接設定 if (mConfig.mMinFps >= range[0] && mConfig.mMaxFps <= range[1]) { curRange[0] = mConfig.mMinFps; curRange[1] = mConfig.mMaxFps; break; } //計算權重,這裡採用差值平方來做比較,也可以採用其他方式計算 weight = (range[0] - mConfig.mMinFps) * (range[0] - mConfig.mMinFps) + (range[1] - mConfig.mMaxFps) * (range[1] - mConfig.mMaxFps); if (weight < lastWeight) { curRange[0] = Math.max(range[0], mConfig.mMinFps); curRange[1] = Math.min(range[1], mConfig.mMaxFps); } } //設定幀率數值 parms.setPreviewFpsRange(curRange[0], curRange[1]);
注意的是,這裡的幀率範圍是需要乘以1000的,也就是說,如果你的一秒是15幀到30幀的話,那麼幀率範圍應該是[1500, 3000]。
PS:有點需要注意的是,需要設定15幀,而支援列表只有[1500, 2000],在設定setPreviewFpsRange(1500,1500)
發生異常了,那麼你需要呼叫setPreviewFpsRange(1500,2000)
來進行幀率的設定。
攝像頭旋轉問題
在Camare1的api中,你會發現size的設定是這樣的:
寬的值是1280(或者是640),而對應高的值是720(或者是480)
因為攝像頭預設採集出來的視訊畫面是橫版的,那麼我們需要獲取攝像頭的選擇角度進行校對視訊方向。
int degrees; Camera.CameraInfo info = new Camera.CameraInfo(); Camera.getCameraInfo(cameraId, info); if (mConfig.isFront) { degrees = info.orientation % 360; } else { degrees = (info.orientation + 360) % 360; } mCamera.setDisplayOrientation(degrees);
根據對應的cameraId取到Camera.CameraInfo
,而CameraInfo
的orientation
變數代表的就是該攝像頭採集到畫面的選擇角度。
我們還需要接下來進行處理:
前置攝像頭直接對info.orientation
進行360取模。
後置攝像頭需要對info.orientation
先加上360在進行360取模。
最後呼叫mCamera.setDisplayOrientation()
設定旋轉角度。
攝像頭預覽
我們採集到畫面後,一般需要提供給使用者渲染介面。
根據業務的需求,我們有多種方式可以選擇:
SurfaceView/">SurfaceView
比較簡單的一種方式,一般我們在佈局檔案裡面新增一個SurfaceView
,如下:
<SurfaceView android:id="@+id/surface_view" android:layout_width="720px" android:layout_height="1280px" />
在Java程式碼監聽SurfaceHolder.Callback
回撥:
SurfaceView surfaceView = findViewById(R.id.surface_view); surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() { @Override public void surfaceCreated(SurfaceHolder holder) { //SurfaceView建立成功 mCamera.setPreviewDisplay(holder); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { //SurfaceView的尺寸發生改變 } @Override public void surfaceDestroyed(SurfaceHolder holder) { //SurfaceView開始銷燬 } });
一般我們在surfaceCreated()
中呼叫了Camera.setPreviewDisplay(holder)
就完成了預覽介面的設定。
TextureView
這個也是一種比較簡單的預覽方式,一般我們在佈局檔案裡面新增一個TextureView
,如下:
<TextureView android:id="@+id/texture_view" android:layout_width="720px" android:layout_height="1280px" />
回到Java中,需要如下邏輯:
TextureView textureView = findViewById(R.id.texture_view); textureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() { @Override public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { //SurfaceTexture初始化完畢 mCamera.setPreviewTexture(surface); } @Override public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { } @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { return false; } @Override public void onSurfaceTextureUpdated(SurfaceTexture surface) { } });
只需要在onSurfaceTextureAvailable()
回撥中呼叫Camera.setPreviewTexture(surface)
即可。
SurfaceTexure
如果我們並不需要預覽介面,而是需要獲取到採集畫面進行預處理(例如美顏、人臉識別),然後在進行預覽的話。
那就需要用到靜默渲染的實現,也就是使用SurfaceTexure
來渲染採集畫面。
//textId,是申請的一個紋理ID,屬於OpenGL範疇,這裡不展開講解 SurfaceTexture texture = new SurfaceTexture(texId); mCamera.setPreviewTexture(texture); texture.setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() { @Override public void onFrameAvailable(SurfaceTexture surfaceTexture) { //每幀回撥,這裡我們可以利用紋理ID去獲取採集畫面 } });
GLSurfaceView
在SurfaceView
的基礎上封裝了OpenGL的一些通用性處理功能,提供一個較為簡單的OpenGL的使用環境。
GLSurfaceView
需要在佈局中才能生效:
<android.opengl.GLSurfaceView android:id="@+id/gl_view" android:layout_width="match_parent" android:layout_height="match_parent" />
java中我們需要對GLSurfaceView
設定一個Renderer
:
GLSurfaceView glSurfaceView = findViewById(R.id.gl_view); glSurfaceView.setRenderer(new GLSurfaceView.Renderer() { @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { //OpenGL紋理構造和其他初始化操作 } @Override public void onSurfaceChanged(GL10 gl, int width, int height) { //GLSurfaceView的尺寸發生改變 } @Override public void onDrawFrame(GL10 gl) { //每幀渲染,這裡利用OpenGL進行渲染 } });
GLSurfaceView
的相關原始碼解析可以看這篇部落格,由於OpenGL相關範疇比較大,所以本篇文章不對OpenGL的知識做講解。
視訊資料獲取
採集畫面獲取,一般來講有兩種主要方式。
原始資料bytes獲取
//setPreviewCallback 在啟動預覽後,每產生一幀都會回撥 //但是沒產生一幀都需要開闢一個新的buffer,GC頻繁,效率較低 mCamera.setPreviewCallback(new Camera.PreviewCallback() { @Override public void onPreviewFrame(byte[] data, Camera camera) { //data資料就是採集的畫面資料 } });
或者
//setPreviewCallbackWithBuffer 在啟動預覽後,需要手動呼叫Camera.addCallbackBuffer(data) //觸發回撥,byte[]資料需要根據一幀畫面的尺寸提前建立傳入 //例如NV21格式,size = width * height * 3 / 2; mCamera.setPreviewCallbackWithBuffer(new Camera.PreviewCallback() { @Override public void onPreviewFrame(byte[] data, Camera camera) { //data資料就是採集的畫面資料 //回收快取,下次仍然會使用,所以不需要再開闢新的快取,達到優化的目的 mCamera.addCallbackBuffer(data); } });
利用紋理ID進行靜默渲染
//textId,是申請的一個紋理ID,屬於OpenGL範疇,這裡不展開講解 SurfaceTexture texture = new SurfaceTexture(texId); mCamera.setPreviewTexture(texture); texture.setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() { @Override public void onFrameAvailable(SurfaceTexture surfaceTexture) { //每幀回撥,這裡我們可以利用紋理ID去獲取採集畫面 } });
採集執行緒
由於採集需要消耗一定的時間,所以我們建議Camera的呼叫需要在一個新的子執行緒進行呼叫,避免呼叫UI執行緒導致了ANR的發生。
比較推薦使用HandlerThread
建立一個子執行緒Looper
迴圈來處理Camera的相關業務。
結語
這篇文章簡單介紹了Android平臺基於Camera1的api進行攝像頭採集的功能。
需要注意的是谷歌已經將Camera1置為廢棄狀態了,轉而建議使用Camera2相關api進行採集,下一篇文章將會簡單介紹下怎麼利用Camera2相關api進行畫面採集。
Camera1雖然被廢棄,但是由於廠商相容性問題,Camera1的通用支援性還是比Camera2好不少,所以可以預知短時間內Camera1的採集框架還是會被主流採納使用。
End!