Android Camera2 API 錄影開發歷程 (三)

重點整理

  • 打開相機,完成 full screen 預覽

第一步

最先開始,我們要先對整個螢幕設置全畫面顯示,並隱藏status bar 和 navgation(有的手機是用虛擬導航按鍵,那就直接隱藏)

navgation 用在實體的導航按鍵當然是不起作用的,不過多設這個flag對實體沒影響,主要是通吃所有手機全螢幕顯示

在 onResume 下多加這行:

1
2
3
4
5
window.decorView.systemUiVisibility =
View.SYSTEM_UI_FLAG_FULLSCREEN or
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN

接著我們要先做一件重要的事情,資源有開就有關,所以要在生命週期的部分去控制這個行為,記得之前已經有在 onResume 的地方打開了相機吧! 這時侯要在 onPause 的時候關閉,無論有無授權都關閉!

1
2
3
4
5
override fun onPause() {
super.onPause()

cameraCore.close()
}

close 都還沒有任何程式碼實作,之後再做就可以了

第二步

開始準備預覽畫面,首先要先在Activity中宣告出兩個變數,一個是 textureView 用 findViewById 存取,一個是 surfaceListener,textureView的各種狀態call back,使用匿名類別直接實作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private lateinit var textureView: TextureView

private val surfaceListener = object : TextureView.SurfaceTextureListener {
override fun onSurfaceTextureSizeChanged(p0: SurfaceTexture?, p1: Int, p2: Int) {

}

override fun onSurfaceTextureUpdated(p0: SurfaceTexture?) {
}

override fun onSurfaceTextureDestroyed(p0: SurfaceTexture?): Boolean {
return true
}

override fun onSurfaceTextureAvailable(surface: SurfaceTexture?, width: Int, height: Int) {
preview()
}

}

然後在onSurfaceTextureAvailable呼叫 preview() 的 private 方法,稍後會實作。

  • 在onCreate 加上:
1
textureView = findViewById(R.id.surface)
  • onResume 原本直接openCamera的呼叫,改寫為:
1
2
3
4
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
//有授權就打開
preview()
}

之後統一打開相機接著打開預覽的步驟,都在prview直接動作。

  • preview()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private fun preview() {
if (textureView.isAvailable) {
cameraCore.openCamera().flatMap {
if (!it) {
return@flatMap Observable.just(false)
}

textureView.surfaceTexture.setDefaultBufferSize(cameraCore.previewSize.width,cameraCore.previewSize.height)
return@flatMap cameraCore.startPreview(arrayListOf(Surface(textureView.surfaceTexture)))
}.subscribe {

Log.d("preview", " Preview :$it")
}
} else {
textureView.surfaceTextureListener = surfaceListener
}

}
  • 另外要特別注意的是,如果這台手機支援的最大preview size之長或寬小於 screen 的時候,即時用 1080x1920 對 Texture BufferSize設定,比例跟720x1280一樣的情況之下,還是一樣會造成圖像拉伸問題,所以在挑選合適的大小的時候,盡量preview長寬小於Screen,並符合 Screen ratio

程式碼解說:
onSurfaceTextureAvailable , onResume 中都直接呼叫了 preview(),onSurfaceTextureAvailable 照字面上顯示就是 TextureView 可使用的時候,開啟camera preview的時候,也是必須要在 Texture 可用之後,才比較不會有問題。

而第一行 textureView.isAvailable 的判斷,簡單做兩件事情:

  1. 可用的時候,直接preview
  2. 不可用的時候,先用 listener 去監聽,得到 callback之後在 preivew

所以在onSurfaceTextureAvailable call back時,preview()呼叫後就會進入 textureView.isAvailable == true 的判斷,然後開啟相機並預覽。

這邊我直接使用了 flatMap,串連兩個 Observable ,在 flatMap 區塊中,如果接到 false 代表開啟失敗,那麼就直接回傳了 Observable.just(false),後面也都不用持續在做。

operator of flatMap 在 Rx 的用意,是一種Observale的轉換方式,其轉換結果也要符合是 Observale 的 class,而 operator of map 在 Rx 的意思則是轉換資料,並不用轉換成整個 Observale

這個方式用在一步串一步的流程中,滿好使用與理解的!

第三步

Activity

如果Activity的程式碼實作都沒問題之後,就切回 CameraCore 寫有關預覽的部分程式碼。這次我們直接把重點擺在startPreview。

這裏先說明,我獨立CameraCore出來的意義:

  • 封裝 Camera2 使用
  • 跟 Activity 程式碼分開
  • 跟 View 的邏輯分開

這樣做的好處很多,最大的益處就是邏輯切割和重複使用,比較不會因為需要改view而整個Activity大改程式碼與架構。

CameraCore

  • thread lock

請先在class 內宣告一個執行緒鎖,這裏運用了 Semaphore。

1
private val lock = Semaphore(1)

Semaphore 建構子的 permits 填入1,代表一次執行一次就上鎖,其他 thread 等釋放。

  • startPreview(surface: List)

先把原本的回傳值改為 create ,就跟 openCamera 一樣:

1
2
3
4
5
fun startPreview(surface: List<Surface>): Observable<Boolean> {
return Observable.create {

}
}

開始寫主要的核心程式碼的時候,要先稍微提一下 Semaphore 用在這 class 中的意義:

Semaphore的使用是 acquire + release,具體運作流程不多說。主要在CameraCore中的作用是上鎖 preview 和 close 兩方法之間不要互搶資源。

是一種保證一定會依照以下流程的運作的手法:

  1. preview -> close
  2. close -> preivew

當你在背景執行緒開啟預覽、關閉相機的時候尤其更為明顯;甚至如果更安全,連 openCamera 都可以加上去,保證流程順暢。

所以一開始在 create 區塊中第一行要寫的就是上鎖:

1
2
3
4
5
6
7
return Observable.create {
if (!lock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
it.onNext(false)
it.onComplete()
return@create
}
}

tryAcquire 跟 acquire 不一樣的地方在,tryAcquire 是有試著獲取的意味,我試著獲取一段時間之內,如果沒有拿到鑰匙就會回傳 false,這時候我們就要做相對應的處理。

第四步

接著最後的流程就是建立兩個主要物件:CaptureRequest、CameraCaptureSession,它們的建立方式要使用 CameraDevice 來建立,就用我們開啟相機時得到的相機實例來建立吧!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
return Observable.create {

if (!lock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
it.onNext(false)
it.onComplete()
return@create
}

if (cameraDevice == null || surface.isEmpty()) {
it.onNext(false)
it.onComplete()
lock.release()
} else {
requestBuilder = cameraDevice?.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)

//必要步驟,target 至少一定一個
requestBuilder?.addTarget(surface[0])

cameraDevice?.createCaptureSession(surface, object : CameraCaptureSession.StateCallback() {
override fun onConfigureFailed(session: CameraCaptureSession?) {
lock.release()

it.onNext(false)
it.onComplete()
}

override fun onConfigured(session: CameraCaptureSession?) {
this@CameraCore.captureSession = session
lock.release()

previewNow()

it.onNext(true)
it.onComplete()
}

}, null)
}
}

1.一開始先判斷 device 是不是 null ? or 有沒有 surface?

2.createCaptureRequest,這裡要傳入告知TEMPLATE 型態

TEMPLATE 有分 PREVIEW , RECORD etc,很多,我也沒看完就不多做解釋。

建立出Builder後,把他指定給先前我們寫好的全域變數

3.對這個Buidler addTarget,至少要有一個,而且一定要是整個陣列中的其中之一,這是官方文件上寫的,實際測試的時候,如果沒有增加任何一個 target ,確實會出錯

4.最後 createCaptureSession,把要 output 的資源指定上去,並且監聽 callback

onConfigureFailed,onNext(失敗),並且釋放鎖

onConfigured,將 session 保存起來,然後釋放鎖、onNext(成功)

進行 previewNow(),要使用 session開始preview,一定要在onConfigured成功後執行。

兩個call back 記得最後都要 it.onComplete

previewNow()

當完成上面的程式碼之後,previewNow 還沒做所以一定是有錯誤,接著把它完全:

1
2
3
4
5
//啟動 session 預覽
private fun previewNow() {
requestBuilder?.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO)
captureSession?.setRepeatingRequest(requestBuilder?.build(), null, null)
}

1.先設定 Request 的指令,這裡我設定了 MODE_AUTO,也就是 3A 自動模式

2.setRepeatingRequest,帶入三個參數,Request 實例、callback介面、Handler loop

Request 實例來自Builder.build()。這是一種 build 的架構設計模式,可自行google參考

callback介面,這裡不傳入
Handler loop,給 null 用 main loop執行

執行 APP,看看畫面有沒有出現吧!