Android音視訊學習——Camera2官方demo解析(2)
本篇主要就幾個關鍵的類進行解釋,並且對需要注意的點註釋,此外再總結一下如何使用Camera2進行拍照和預覽的流程。附上官方demo。
上面是Camera2的流程示意圖,由於我喜歡從整體思路上分析程式碼,所以下面先就整個呆萌的思路拓展一下。
首先肯定是解決相機的問題啦,畢竟是主角嘛,但是相機是底層的,Android為我們抽象了CameraDevice
類,用來表示各個相機。我們又知道,裝置的相機通常有好幾個,前後的特點都不一樣,比如映象問題,單單一個CameraDevice
無法表示全。這時候可能有人會想把CameraDevice
做成抽象類,每個相機作為子類各自配置自己的屬性,反正開啟相機的操作不依賴相機本身,只需要相機的標識就行,開啟相機以後再將該相機的例項物件返回給呼叫者就ok了,沒錯,呆萌也是這樣想的:
@Override
public void onOpened(@NonNull CameraDevice cameraDevice) {
mCameraDevice = cameraDevice;
}
相機裝置的問題解決了,但是誰來拍照啊,開啟相機不可能讓CameraDevice
自己開啟自己吧,所以需要一個CameraManager
來管理相機的開啟,相機是系統級別的,所以返回的Manager
都是一樣的也無妨,就將其上升到服務的層次,只要有上下文就能獲取,通過下面的方法獲取並進行拍照。
CameraManager manager = (CameraManager) activity.getSystemService (Context.CAMERA_SERVICE);
manager.openCamera(mCameraId, mStateCallback, mBackgroundHandler);
呆萌又想,如果都交給CameraManager
來管理,它還是要管理好多東西哦,比如相機的取景方向,這是個大問題,見解析(1),為了滿足單一職責原則和開閉原則,不如將相機裝置的屬性封裝成一個類,這樣CameraDevice
只需要專注於建立對話Session
就行了, 於是CameraCharacteristics
應運而生,其封裝了指定ID的相機中的各種屬性,例如想要獲取指定ID相機的取景方向:
CameraCharacteristics characteristics = manager.getCameraCharacteristics (cameraId);
mSensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
我們知道,相機裝置是嵌入在Android裝置當中的,兩者之間的通訊需要底層的知識,但是我們不需要知道,所以Android為我們提供了CameraCaptureSession
這個上層類,翻譯為相機捕捉的對話,顯然這個對話為我們預覽和拍攝做了封裝,其作用相當於圖中的pipeline
。
開啟相機後就可以通過管道進行配置了,而相機的各種配置很多,使用者的需求也是不一樣的,如果在對話管道CameraCaptureSession
中封裝了對相機的配置,顯然很臃腫,所以此處抽象出來一個CaptureRequest
類,並且對其的配置使用了Builder模式,這個模式適合的場景就是Director
主動去配置物件的屬性,提供了預設配置,而不需要在建構函式中將屬性一次性配置完成。建議如果程式碼中需要配置較多的屬性,並且這些屬性都有預設值的話,使用Builder模式更佳。呆萌是這樣體現的:
//建立Builder
mPreviewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
//設定持續的自動對焦
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
//建立CaptureRequest
mPreviewRequest = mPreviewRequestBuilder.build();
配置完成後就可以進行預覽和拍照了,返回的結果被封裝成為CameraMetadata
,這是一個控制相機和儲存相機資訊的基類,它描述了查詢資訊,拍照結果等返回的key/value
組成的map
,有個很重要的問題是返回的結果以什麼形式呈現,我們首先想到的肯定是以方法的返回值返回,但是這裡涉及到何時拍照完成的問題,以及拍照中對焦距、閃光燈等配置的調節,都需要立即呈現給Android系統,顯然觀察者模式是不錯的選擇,通過設定回撥來立即接受到配置的變化以及完成與否,呆萌是這樣做的:
/* process(CaptureResult)是針對不同的結果和當前的狀態對相機進行設定和操縱的方法 */
private CameraCaptureSession.CaptureCallback mCaptureCallback = new CameraCaptureSession.CaptureCallback() {
@Override
public void onCaptureProgressed(@NonNull CameraCaptureSession session,
@NonNull CaptureRequest request,
@NonNull CaptureResult partialResult) {
process(partialResult);
}
@Override
public void onCaptureCompleted(@NonNull CameraCaptureSession session,
@NonNull CaptureRequest request,
@NonNull TotalCaptureResult result) {
process(result);
}
}
有了上面這幾個概念,整個流程也就差不多走通了,下面梳理一下:
- 首先獲取
CameraManager
,呼叫其getCameraIdList()
獲取所有的CameraId,從中選擇需要的,根據ID找到對應的CameraDevice
- 獲取到該相機的
CameraCharacteristics
,獲取各屬性(相機取景方向,可支援的輸出影象大小),根據屬性配置顯示的大小,並根據當前螢幕旋轉位置和相機位置調整預覽和拍照的方向 - 建立後臺執行緒和
Handler
物件,提供給Camera
用於後臺操作 - 建立
CameraDevice.StateCallback
物件,在開啟和關閉的回撥中啟動和釋放資源,並建立預覽 - 開啟相機前動態檢查相機許可權,獲得許可權後通過
manager
的open
方法開啟相機 - 建立
CameraCaptureSession.CaptureCallback
物件,在回撥中根據返回結果進行相應操作 - 利用
CameraDevice
建立CaptureRequest.builder
物件進行配置,並建立Session
對話,在onConfigured
的回撥中啟動預覽 - 拍照時與⑦基本相同,只是
request
的目標Surface
改變,並且呼叫的是Session
的capture()
方法
以上就是用Camera2
API拍照的全部流程,其中有不少細節需要注意,在下面將一一註釋,如果真的需要用到此API,建議直接修改官方呆萌就夠了。
是否支援
具體見Android裝置對新Camera2 API的支援問題:以華為M2為例,現在很多推出的裝置雖然都是Android5.0以上的,也就是支援Camera2
的API,但是雖然它們實現了這些API,但是實際上可配置的引數比Camera
少,甚至效能都比Camera
差,為此廠商在系統中寫明瞭對Camera2
的支援程度,通過:
CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId);
characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
可以獲得支援的程度,得到的值中有這三種:LIMITED值為0,FULL值為1,LEGACY值為2,其支援程度為FULL > LIMITED > LEGACY。關於LEGACY是這樣說的:
A LEGACY device does not support per-frame control, manual sensor control, manual post-processing, arbitrary cropping regions, and has relaxed performance constraints.
翻譯一下:遺留的裝置不支援每幀控制、手動感測器控制、手動後處理、任意裁剪區域,並且具有寬鬆的效能約束。可以說相容性很差了,我在自己手機上驗證了下,發現LEGACY,屬於遺留機!可以預見很多手機上並不完全支援Camera2
API,所以如果手機並不是FULL支援的話,建議還是使用Camera
。
Semaphore
官方呆萌中使用了Semaphore
防止在相機未關閉前就退出了App,因為這樣會導致無法正常關閉相機,釋放資源,相機服務是系統級別的,App如果一直持有其引用就無法被gc清理,造成洩露,同時也防止併發狀態下多個應用頻繁啟動關閉相機。Semaphore
簡單來說就是食堂阿姨,初始化的時候給阿姨指定量的卡布奇諾,呼叫acquire
的時候拿走拿走一杯,呼叫release
阿姨就再多放一杯,如果卡布奇諾暫時賣完了,Semaphore
值為0,acquire
就阻塞,等待release
釋放一個,還可以設定阻塞時限,如果超時就返回false
等等。在呆萌中這樣寫:
private final CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice cameraDevice) {
// This method is called when the camera is opened. We start camera preview here.
mCameraOpenCloseLock.release();
mCameraDevice = cameraDevice;
createCameraPreviewSession();
}
@Override
public void onDisconnected(@NonNull CameraDevice cameraDevice) {
mCameraOpenCloseLock.release();
cameraDevice.close();
mCameraDevice = null;
}
@Override
public void onError(@NonNull CameraDevice cameraDevice, int error) {
mCameraOpenCloseLock.release();
cameraDevice.close();
mCameraDevice = null;
Activity activity = getActivity();
if (null != activity) {
activity.finish();
}
}
}
@Override
public void onResume() {
super.onResume();
startBackgroundThread();
if (mTextureView.isAvailable()) {
openCamera(mTextureView.getWidth(), mTextureView.getHeight());
} else {
mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
}
}
private void openCamera(int width, int height){
if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
throw new RuntimeException("Time out waiting to lock camera opening.");
}
manager.openCamera(mCameraId, mStateCallback, mBackgroundHandler);
}
private void closeCamera(){
try {
mCameraOpenCloseLock.acquire();
...
} finally {
mCameraOpenCloseLock.release();
}
}
首先我們需要直到onDisconnected
方法在退出Activity後並不會被呼叫,然後我們假設一個場景,第一次開啟Activity,此時Semaphore
為1,onResume
回撥,mTextureView
不可用,呼叫setSurfaceTextureListener
方法,最終呼叫openCamera
,其中嘗試獲取許可,獲得後Semaphore
為0,並開啟相機,相機的onOpened
回撥,release()
方法被呼叫,釋放一個許可,Semaphore
為1,隨後退出App,在OnPause
回撥中關閉相機,首先呼叫acquire()
獲取許可,並呼叫close()
方法關閉相機,注意關閉相機中並未退出Activity
,此時如果其他應用呼叫openCamera
方法,會因為Semaphore
為0而處於阻塞狀態,等到相機關閉後finally
語句塊呼叫,釋放許可,允許其他應用訪問相機。
輸出影象大小
在建立CameraCaptureSession
的時候需要傳入一個回撥物件,其中有一個回撥方法是這樣的:
@Override
public void onConfigureFailed(
@NonNull CameraCaptureSession cameraCaptureSession) {
showToast("Failed");
}
很多時候我們會遇到這個回撥,特別是沒有配置好的時候,這個回調發生的時機在官方文件中是這樣的:
This can happen if the set of requested outputs contains unsupported sizes, or too many outputs are requested at once.
不支援的大小?沒錯,無論是Camera
還是Camera2
都有著支援的解析度大小,如果請求輸出的Surface
包括了不支援的解析度,就會回撥這個方法,並且無法正確開啟相機,呆萌中的幾個方法都是在設定合適的解析度大小:
private void setUpCameraOutputs(int width, int height){}
private static Size chooseOptimalSize(Size[] choices, int textureViewWidth, int textureViewHeight, int maxWidth, int maxHeight, Size aspectRatio){}
具體的篩選見呆萌,就是在最大寬高範圍內,根據textureView
的寬高,分為大的Size和不大的Size,如果有大的,在大的中選最小的,反之在小中選最大的。然後根據螢幕旋轉和相機的方向,以及當前螢幕的Orientation
進行調整。總之輸出的Surface
的大小也是很講究的。
剩下的難點其實就是CaptureResult
中各個配置常量的含義,這點太多了,就不一一列舉了,大家用到的時候自己去發掘吧。