用華為CameraKit實現預覽和拍照

語言: CN / TW / HK

theme: cyanosis highlight: rainbow


前言

前面研究了一下如何在Android手機上獲取超廣角鏡頭一些獲取您的Android裝置超廣角能力的思路 - 掘金 (juejin.cn)。發現HUAWEI官方有推出過一個相機庫CameraKit,就想著自己接入一下看看效果,順便記錄一些遇到的坑。

流程

使用Gradle整合比較常規,看文件即可:

CameraKit - 相機能力接入準備

官方提供的整合流程如下圖: image.png

CameraKit提供了一個Mode類作為一次拍照流程的相關抽象,可理解為一個Session。

CameraKit的生命週期: - 模式建立:CameraKit提供了多種相機的模式,譬如:普通拍照、人像、夜景等,當然還有錄影相關的。 詳情可參考文件:Mode.Type - 模式配置:主要是配置預覽解析度、拍照解析度等,還有關於在該模式下的一些操作事件的回撥、資料的回撥。 - 基於模式的操作:比較好理解的是利用Mode類進行預覽、拍照、縮放等。 - 操作回撥:每當觸發一個操作後,會通過在模式配置下注冊的回撥中回撥相關事件或資料。 - 模式釋放:不需要時釋放資源。

接入

在使用CameraKit時,一切的前提是需要例項化出CameraKit物件,它是一個餓漢式的單例,在例項化前會判斷一些約束條件,符合條件後才會建立。 java CameraKit cameraKit = CameraKit.getInstance(getApplicationContext());

模型建立

在預覽的檢視準備好之後,就可以開始建立模式了,譬如在TextureView#onSurfaceTextureAvailable後。在建立前還需要新建一個HandlerThread作為整個相機運作的執行緒

```java private final ModeStateCallback mModeStateCallback = new ModeStateCallback() { @Override public void onCreated(Mode mode) { super.onCreated(mode); mMode = mode; configMode(); // Mode建立成功,可以開始進行模式配置 }

@Override
public void onCreateFailed(String cameraId, int modeType, int errorCode) {
    super.onCreateFailed(cameraId, modeType, errorCode);
}

@Override
public void onConfigured(Mode mode) {
    super.onConfigured(mode);
    mMode.startPreview();  // Mode配置成功,可以開始預覽
}

@Override
public void onConfigureFailed(Mode mode, int errorCode) {
    super.onConfigureFailed(mode, errorCode);
}

@Override
public void onReleased(Mode mode) {
    super.onReleased(mode);
}

@Override
public void onFatalError(Mode mode, int errorCode) {
    super.onFatalError(mode, errorCode);
}

};

cameraKit.createMode(CameraInfo.FacingType.CAMERA_FACING_BACK, Mode.Type.NORMAL_MODE, mModeStateCallback, mHandler); `` -CameraKit中有**對於手機物理攝像頭進行抽象**,在應用層只會提供前置/後置兩個列舉。這裡使用後置攝像頭CameraInfo.FacingType.CAMERA_FACING_BACK。 -Mode.Type.NORMAL_MODE為普通拍照模式,如果有拍攝人像、夜景等其他需求,可對應傳入。 -ModeStateCallback`用於監聽Mode物件的事件。 - 最後還需要一個屬於HandlerThread的Handler,用於訊息分發。

模式配置

從上述程式碼中ModeStateCallback#onCreated的回撥可以看到,在成功建立模式後就可以開始配置了。

比較重要的是預覽解析度和拍照解析度,可通過以下程式碼獲取裝置支援的 ```java // 預覽解析度 List supportedPreviewSizes = mMode.getModeCharacteristics() .getSupportedPreviewSizes(SurfaceTexture.class);

// 拍照解析度
List supportedCaptureSizes = mMode.getModeCharacteristics() .getSupportedCaptureSizes(ImageFormat.JPEG); 因為用的是`TextureView`,所以傳入`SurfaceTexture.class`。預覽解析度還需要設定回TextureView中**保證預覽畫面正常**。ps:解析度的篩選邏輯比較常規就不多贅述了,這裡選一個**最大的3:4比例**。java textureView.getSurfaceTexture() .setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight()); ```

ps:mMode.getModeCharacteristics()可以獲取到在該模式下一些支援的引數,除上述的解析度外還有支援的對焦型別、縮放範圍等。可參考:ModeCharacteristics

java mMode.getModeConfigBuilder() .addPreviewSurface(new Surface(textureView.getSurfaceTexture())) .addCaptureImage(pictureSize, ImageFormat.JPEG); mMode.getModeConfigBuilder().setDataCallback(mActionDataCallback, mHandler); mMode.getModeConfigBuilder().setStateCallback(mActionStateCallback, mHandler); mMode.configure(); 配置時還需要傳入ActionStateCallbackActionDataCallback物件,用於在該模式下的一些操作事件的回撥、資料的回撥 ```java private final ActionStateCallback mActionStateCallback = new ActionStateCallback() { @Override public void onPreview(Mode mode, int state, @Nullable PreviewResult result) { super.onPreview(mode, state, result); // 預覽事件回撥 }

@Override
public void onTakePicture(Mode mode, int state, @Nullable TakePictureResult result) {
    super.onTakePicture(mode, state, result);
    // 拍照事件回撥
}

@Override
public void onFocus(Mode mode, int state, @Nullable FocusResult result) {
    super.onFocus(mode, state, result);
    // 對焦事件回撥
}

};

private final ActionDataCallback mActionDataCallback = new ActionDataCallback() { @Override public void onImageAvailable(Mode mode, int type, Image image) { super.onImageAvailable(mode, type, image); // 拍照資料回撥 }

@Override
public void onThumbnailAvailable(Mode mode, int type, android.util.Size size, byte[] data) {
    super.onThumbnailAvailable(mode, type, size, data);
}

}; ```

開始預覽

ModeStateCallback#onConfigured回撥後即可呼叫Mode#startPreview開啟預覽。 java // ModeStateCallback @Override public void onConfigured(Mode mode) { super.onConfigured(mode); mMode.startPreview(); // Mode配置成功,可以開始預覽 } 這時您的介面上應該就能看到預覽畫面了。

拍照

Mode#takePicture觸發拍照 java mMode.takePicture();

ActionStateCallback#onTakePicture會回撥拍照相關的事件,包括錯誤事件 java // ActionStateCallback @Override public void onTakePicture(Mode mode, int state, @Nullable TakePictureResult result) { super.onTakePicture(mode, state, result); if (state == TakePictureResult.State.CAPTURE_COMPLETED) { // 拍照完成 } else if (state == TakePictureResult.State.ERROR_CAPTURE_NOT_READY || state == TakePictureResult.State.ERROR_FILE_IO || state == TakePictureResult.State.ERROR_UNKNOWN || state == TakePictureResult.State.ERROR_UNSUPPORTED_OPERATION) { // 拍照出錯 } } 可參考:ActionStateCallback.TakePictureResult.State

拍照成功後在ActionDataCallback#onImageAvailable回撥原圖的Image物件,資料格式為jpg java // ActionDataCallback @Override public void onImageAvailable(Mode mode, int type, Image image) { super.onImageAvailable(mode, type, image); if (type == Type.TAKE_PICTURE) { ByteBuffer buffer = image.getPlanes()[0].getBuffer(); byte[] bytes = new byte[buffer.remaining()]; buffer.get(bytes); // 轉成Bitmap? image.close(); } } 這樣就會拿到jpg的byte陣列了。

資源釋放

在結束時,需要釋放相關資源,當然還包括建立的HandlerThread,避免記憶體洩漏java if (mMode != null) { mMode.release(); }

超廣角能力

由於筆者最初是想研究超廣角的,所以也來看一下 java float[] zooms = mMode.getModeCharacteristics().getSupportedZoom(); mMode.setZoom(zooms[0]); 由於CameraKit已經幫我們抽象了物理攝像頭,對於後置攝像頭當然也包括那顆超廣角攝像頭。使用以上程式碼可以獲取到當前模式下所支援的縮放範圍,一般是一個長度為2的陣列。在華為P40上zooms[0] = 0.6f。設定後即可獲得超廣角的預覽。

一些疑難雜症

支援的解析度較少

在華為P40上,通過CameraKit獲取支援的拍照解析度極少

  • 通過CameraKit獲取

    image.png

  • 通過原生Camera2獲取

    image.png

  • 即使是超廣角鏡頭,通過原生Camera2獲取

    image.png

CameraKit的例項約束條件

這個其實不太算是問題,只是限制罷了。CameraKit例項化前會判斷 - app是否已經獲取了拍照許可權(ps:個人認為這個這個判斷應該交給呼叫方判斷的。。。) - 裝置是否支援,支援的範圍如下圖:

![image.png](http://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/12af1112ce3347feb60b77ea7757e7ea~tplv-k3u1fbpfcp-watermark.image?)
筆者有一臺華為MatePad11,是高通晶片的,實測發現並不支援,所以該庫支援的範圍還是比較窄的。

拍照輸出的時間很慢

一般使用Camera2拍照平均在500ms可以輸出,使用CameraKit最快也要2s。如果使用一些更為專業的功能可能會更長,這個沒有細測。 在HUAWEI的社群中也有人提問:CameraKit中拍照速度慢的情況下要5、6秒,太慢了,請問如何優化-華為開發者論壇

筆者的猜測是,從CameraKit匯入的一些類來看,其依賴的還是Camera2。推測是在輸出到呼叫方之前,CameraKit會呼叫一些系統的服務對影象進行處理,就比如超廣角的輸出是處理過畸變的。還有就是上述說的解析度支援極少,所能選用的3:4解析度已經到4096 * 3072,導致這些處理比較耗時。

無法獲取預覽幀

接入時發現該庫並沒有很好的提供獲取預覽幀的方法,只能通過在配置Mode時新增多一個Surface。這裡使用ImageReader實現,具體可參考Camera2的做法,大同小異。 ```java previewImageReader = ImageReader.newInstance( previewSize.getWidth(), previewSize.getHeight(), ImageFormat.YUV_420_888, 2); previewImageReader.setOnImageAvailableListener(this, mHandler);

mMode.getModeConfigBuilder() .addPreviewSurface(new Surface(textureView.getSurfaceTexture())) .addPreviewSurface(previewImageReader.getSurface()) .addCaptureImage(pictureSize, ImageFormat.JPEG); mMode.getModeConfigBuilder().setDataCallback(mActionDataCallback, mHandler); mMode.getModeConfigBuilder().setStateCallback(mActionStateCallback, mHandler); mMode.configure(); `` 這裡需要使用YUV_420_888`,提高輸出效率。還需要注意的是輸出的Image轉成byte陣列的問題。

還有一點,根據社群的一些反饋,並不是所有裝置都支援這樣同時註冊兩個預覽流,可通過以下方式獲取最大的支援數 java mMode.getModeCharacteristics().getMaxPreviewSurfaceNumber()

該方法雖然可以穩定獲取到預覽幀,但是隨之而來的是加大了拍照輸出的時間。嚴重的可達到5、6s以上。

另一種思路

這個又有另外一個思路去解決:上述程式碼只註冊一個ImageReader用於獲取YUV_420_888的預覽幀,再通過GLSurfaceView繪製到檢視上,同時將Image轉成byte陣列作為資料層的回撥。具體的實現這裡不細說了,推薦一個OPPO的Demo,裡面有YUV_420_888通過OpenGL繪製的邏輯,可以參考一下:oppo/CameraUnit

這裡需要注意的是: - ImageReader#OnImageAvailableListener輸出和GLSurfaceView.Renderer#onDrawFrame的繪製在兩個執行緒,所以需要保證同步。 - 由於Image轉成byte陣列的過程可能存在耗時,這一塊主要來源於大記憶體的申請和gc,可採用全域性變數避免頻繁的記憶體申請。但由於採用了全域性變數,但又不能因為這個轉換導致繪製的掉幀情況,所以需要一些原子性的變數加以輔助,適當做一些丟幀操作。

以上只是筆者的設想,裡面也有更好的優化空間。

最後

以上就是筆者關於華為CameraKit的研究記錄。其實都2023年了,在一臺鴻蒙手機上做一些Android開發確實有些不太靠譜。在官方文件中最近一次更新是在2021年,目前HUAWEI還是把焦點放在鴻蒙的更新上,華為社群關於該庫的問題看上去也沒有得到很好的解決。所以以上提到的那些問題可能不會得到很好的解決了。這篇文章也可以當是一篇冷知識看看吧。

image.png