Android NDK Camera2小結

1.概述

Android自帶camera API有兩種,Camera1和Camera2。其中,

Camera1始於Android最初版本,僅爲java層實現。

Camera2則有兩種實現。

Java層Camera2始於andorid 5.0。

Native Camera2則開始於android7.0(API level24)。

本文將對Native Camera2的使用(僅預覽功能)進行總結。總結將針對於如何使用和邏輯流程,不糾結於代碼,示例可參照google ndk samples(可從github查找)。

2.使用流程

通過Camera2實現預覽,需要三步。

第一步,開啓設備。

第二步,發送請求。

第三步,接收視頻數據,並轉換爲圖像格式顯示。

本文的講解基於UML Component圖,描述Camera2核心對象及API的調用生成關係。受版面所限,拆爲三圖。如縮略圖不清晰,可點擊放大。

Component圖中,模塊爲函數,節點爲對象。橫向(左或右)箭頭爲資源釋放操作調用。調用過程與順序/時機無關。

2.1開啓設備

該階段目的,在於獲取設備指針,以及獲取設備相應的信息,例如分別率,旋轉角度等。

通過系統所提供的ACameraManager_create方法,可獲得ACameraManager指針。

進而通過ACameraManager_getCameraIdList方法,可從ACameraManager中獲取所有Camera的ID。

獲取ID後,輔助以屬性TAG,可以通過ACameraMetadata_getConstEntry方法獲取對應Camera的屬性。

例如,傳入ACAMERA_SENSOR_ORIENTATION的TAG,可以獲取Camera的旋轉角度;ACAMERA_LENS_FACING,可以判斷是前置(ACAMERA_LENS_FACING_FRONT)或後置(ACAMERA_LENS_FACING_BACK)攝像頭;而ACAMERA_SCALER_AVAILABLE_STREAM_CONFIGURATIONS,可以獲得攝像頭的分辨率。

其它TAG,可參照NdkCameraMetadataTags.h。

此處需要注意的是,

(1)ACameraMetadata_getConstEntry方法返回的是一個ACameraMetadata_const_entry結構體,需根據type的值讀取data。

typedef struct ACameraMetadata_const_entry {
    /**
     * The tag identifying the entry.
     * See {@link NdkCameraMetadataTags.h} for more details. </p>
     */
    uint32_t tag;

    /**
     * The data type of this metadata entry.
     */
    uint8_t  type;

    /**
     * Count of elements (NOT count of bytes) in this metadata entry.
     */
    uint32_t count;

    /**
     * Pointer to the data held in this metadata entry.
     */
    union {
        const uint8_t *u8;
        const int32_t *i32;
        const float   *f;
        const int64_t *i64;
        const double  *d;
        const ACameraMetadata_rational* r;
    } data;
} ACameraMetadata_const_entry;

(2)不是所有API LEVEL>=24的android os都支持Camera2,所以有時通過ACameraManager_getCameraIdList方法獲取都ID數量可能爲0。

在獲取到Camera ID之後,通過ACameraManager_openCamera方法,可以開啓Camera,獲得一個ACameraDevice對象指針。

該指針將在發送請求階段使用。在調用ACameraManager_openCamera方法時,ACameraDevice_StateCallbacks可以爲null。

2.2發送請求

在獲取設備(指針)後,需要通過不斷的發送請求(ACaptureRequest),以得到圖像信息。沒有請求,便沒有數據。沒有數據,便無法生成之後的預覽圖像。因此,該步驟是最關鍵的一步,也是最爲複雜的一步。

對於整個發送請求(ACaptureRequest)過程,可以理解爲,

(1)生成一個向ACameraDevice發送的,類型爲ACameraDevice_request_template的ACaptureRequest。

(2)該ACaptureRequest被不斷重複發送。

(3)ACaptureRequest所返回的響應數據,由ACaptureSessionOutputContainer中的ACaptureSessionOutput接收。

(4)ACaptureRequest通過ACameraOutputTarget,將ACaptureSessionOutput與AImageReader綁定。

在預覽階段,AImageReader將用於數據的讀取。

不斷的發送請求,是如何完成的?

只需要調用ACameraCaptureSession_setRepeatingRequest方法即可。

camera_status_t ACameraCaptureSession_setRepeatingRequest(
        ACameraCaptureSession* session,
        /*optional*/ACameraCaptureSession_captureCallbacks* callbacks,
        int numRequests, ACaptureRequest** requests,
        /*optional*/int* captureSequenceId);

而當需要停止發送請求時,調用ACameraCaptureSession_stopRepeating方法即可。

camera_status_t ACameraCaptureSession_stopRepeating(ACameraCaptureSession* session);

題外話:預覽需要不斷的發送請求,獲取實時畫面。而拍照就不同了,僅需要發送單個(一次)請求即可。

ACameraCaptureSession_setRepeatingRequest方法中核心的傳參有三個,ACameraCaptureSession,ACaptureRequest和numRequests。接下來,開始詳細講述發送請求的過程。

2.2.1創建請求

requests,是一個ACaptureRequest列表,numRequests當然指的是ACaptureRequest的數量了。可見,可以同時發送多個請求。但對於單一的預覽,一個請求足已。

那麼,ACaptureRequest是向誰發出的請求?

camera_status_t ACameraDevice_createCaptureRequest(
        const ACameraDevice* device, ACameraDevice_request_template templateId,
        /*out*/ACaptureRequest** request);

ACaptureRequest由ACameraDevice生成,因此,是向ACameraDevice發送的請求。ACameraDevice在開啓設備階段已被獲取。

請求的類型,由枚舉ACameraDevice_request_template決定。參見NdkCameraDevice.h。對於預覽,需選擇TEMPLATE_PREVIEW。

因此,創建請求的過程,可以理解爲:

生成一個向ACameraDevice發送的,類型爲ACameraDevice_request_template的請求。

2.2.2建立會話

ACameraCaptureSession是一個會話。它也是由ACameraDevice生成,同時還綁定來一個ACaptureSessionOutputContainer。

camera_status_t ACameraDevice_createCaptureSession(
        ACameraDevice* device,
        const ACaptureSessionOutputContainer*       outputs,
        const ACameraCaptureSession_stateCallbacks* callbacks,
        /*out*/ACameraCaptureSession** session);

ACaptureSessionOutputContainer,通過ACaptureSessionOutputContainer_create方法創建。

camera_status_t ACaptureSessionOutputContainer_create(
        /*out*/ACaptureSessionOutputContainer** container);

ACaptureSessionOutputContainer是一個可以容納多個ACaptureSessionOutput的集合。

ACaptureSessionOutput是一個輸出流,用於接收預覽所用的圖像數據。該對象由AImageReader間接創建。關於AImageReader,見圖片讀取章節。間接創建步驟如下

(1)AImageReader通過AImageReader_getWindow方法,獲得一個ANativeWindow對象(注意,該ANativeWindow對象與此後預覽圖像章節所述的ANativeWindow對象,並非同一對象)。

(2)ANativeWindow對象通過ACaptureSessionOutput_create方法,獲得一個ACaptureSessionOutput對象。

因此,一個ACameraCaptureSession包含了以下信息:

(1)與哪個ACameraDevice進行會話。

(2)用哪些ACaptureSessionOutput來接收數據。

而ACameraCaptureSession又通過ACameraCaptureSession_setRepeatingRequest方法,與ACaptureRequest建立關聯。

因此,ACaptureRequest與ACaptureSessionOutput,ACameraDevice都是有關聯的。

那麼ACaptureRequest如何確定數據格式,大小?AImageReader即爲關鍵。

ACaptureRequest需要與AImageReader建立關聯。

2.2.3接收圖片數據

在創建AImageReader時,需要提供四個參數:圖像寬度,高度,圖像格式,最大圖片存儲量。

media_status_t AImageReader_new(
        int32_t width, int32_t height, int32_t format, int32_t maxImages,
        /*out*/AImageReader** reader);

其中,圖像寬度和高度的單位爲像素。

圖像格式,可參見枚舉類型AIMAGE_FORMATS(NdkImage.h)。由於預覽時,前端爲視頻格式,此處應使用AIMAGE_FORMAT_YUV_420_888。

最大圖片存儲量maxImages,需要特別注意。若數量少(例如1,2),則會在預覽時,產生卡頓。而數量多,則會引起延時。例如 數量40,寬高爲640*480時,將引起3至4秒的延時。

AImageReader如何與ACaptureRequest建立關聯呢?也是通過間接的途徑。

(1)AImageReader通過AImageReader_getWindow方法,獲得一個ANativeWindow對象(注意,該ANativeWindow對象與此後預覽圖像章節所述的ANativeWindow對象,並非同一對象)。

(2)ANativeWindow對象通過ACameraOutputTarget_create方法,創建一個ACameraOutputTarget對象。

(3)通過ACaptureRequest_addTarget方法,將ACameraOutputTarget與AImageReader建立關聯。

至此,ACaptureRequest可以通過AImageReader確定數據格式,高寬,及最大數量,並將圖像數據寫入AImageReader所對應的ACaptureSessionOutput中。

接下來,只剩下如何顯示的問題了。

2.3預覽圖像

2.3.1關鍵因素

AImageReader,ANativeWindow和ANativeWindow_Buffer是預覽階段的關鍵因素。

其中AImageReader起到一個承上啓下的作用。

在發送請求階段,綁定請求(ACaptureRequest)和輸出(ACaptureSessionOutput),以獲取實時視頻數據。

在預覽階段,作爲數據源,用於數據轉換(YUV數據轉ARGB等圖像數據類型)。

ANativeWindow做爲畫布(由外部傳入,並非由AImageReader,與發送請求階段的ANativeWindow對象不同),用於顯示預覽圖像。

ANativeWindow_Buffer則可以看作是畫布(ANativeWindow)的顯存。當數據以某一圖片格式寫入ANativeWindow_Buffer時,圖像便呈現在畫布(ANativeWindow)上。

2.3.2獲取圖片

在ACaptureRequest被髮出後,將會有AImage不斷的存入AImageReader對應的緩衝區。緩衝區內存儲的圖片數量上限,在創建AImageReader時已被設定。

題外話:

對於性能較低的設備,當緩衝區內的圖片上限過大,則會出現預覽延時。因爲有太多的舊圖片在排隊等待處理。

而緩衝區內圖片上限過小,不論設備的性能好壞,則都會出現預覽卡頓,因爲沒有連續的圖片可以被連續的畫到畫布上。

AImageReader通過AImageReader_acquireNextImage方法,獲得最早進入緩衝區的AImage。

在獲取到AImage後,可以通過AImage_getPlaneRowStride和AImage_getPlaneData方法,獲取YUV格式的視頻數據。

關於YUV格式可參照之前的博文:YUV簡介,這裏不在詳述。

2.3.3初始化ANativeWindow_Buffer

在獲取ANativeWindow_Buffer前,需要通過ANativeWindow_setBuffersGeometry方法,對其屬性進行初始化。

int32_t ANativeWindow_setBuffersGeometry(ANativeWindow* window,
        int32_t width, int32_t height, int32_t format);

其中,width和height爲ANativeWindow_Buffer的寬高。

ANativeWindow_Buffer的寬高不同於畫布View的寬高。例如畫布View寬高爲1080*2018,ANativeWindow_Buffer寬高爲480*640,ANativeWindow_Buffer會自動被映射到畫布View上,由硬件或底層完成,無需干預。

ANativeWindow_Buffer的寬高也不同於AImage的寬高。AImage帶有ACameraDevice的偏轉角度,需要調整爲自然0度(會造成寬高的數值交換)後,再做格式轉換。

format爲YUV轉換後的圖片格式(例如WINDOW_FORMAT_RGBA_8888等,可參見Native_Window.h)。

2.3.4獲取ANativeWindow_Buffer

獲取過程相對程序化。需要依次調用ANativeWindow_acquire和ANativeWindow_lock方法。ANativeWindow_acquire及其對應的ANativeWindow_release方法,可以理解爲安全策略,爲技術層次,無業務邏輯解釋。可參見其官方註解。

/**
 * Acquire a reference on the given {@link ANativeWindow} object. This prevents the object
 * from being deleted until the reference is removed.
 */
void ANativeWindow_acquire(ANativeWindow* window);
/**
 * Remove a reference that was previously acquired with {@link ANativeWindow_acquire()}.
 */
void ANativeWindow_release(ANativeWindow* window);

ANativeWindow_lock方法將返回一個ANativeWindow_Buffer。

該ANativeWindow_Buffer,即爲ANativeWindow需要顯示的下一個畫面所對應的顯存。

2.3.5寫入ANativeWindow_Buffer

通過實現自定義方法TODO_YUV_2_ImageFormat,將數據進行轉換,並映射到上一步所獲得的ANativeWindow_Buffer中。

TODO_YUV_2_ImageFormat方法,至少應包含三個功能,

(1)角度的修正。將圖像由翻轉角度,矯正爲自然0度。

(2)格式轉換。YUV轉ImageFormat。其中ImageFormat爲ANativeWindow_Buffer初始化時,所設置的格式。例如ARGB_8888。

(3)座標映射。將寬高爲A*B的AImage,映射到寬高爲C*D的ANativeWindow_Buffer中。

2.3.6釋放ANativeWindow_Buffer

在每次寫入完成後,需要釋放ANativeWindow_Buffer。步驟十分簡單,與獲取時相反,依次調用

(1)ANativeWindow_unlockAndPost

(2)ANativeWindow_release

在釋放完ANativeWindow_Buffer後,一定要通過AImage_delete方法,將AImage移除緩衝區。

至此,整個預覽完成。

2.4關閉Camera

關閉Camera,有一點需要強調。

ACameraDevice_close較爲耗時,在MI 8和Samsung SM G9500上測試,需要約200ms左右。而Activity onPause/onStop/onDestory的單個超時上限爲500ms。若有其它耗時操作與ACameraDevice_close一起進行,將有可能引起Timeout。

其它不再詳細描述,只需要調用相關的free,delete,close方法即可。

3.結束語

相對於Camera1而言,NDK Camera2的邏輯較爲複雜。但隨着API Level的提升,Camera1終將被替換。Google最新提出的CameraX其實也是對Camera2的封裝。因此,Camera2在未來還會存在一段很長的時間。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章