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

Android Studio 環境配置

  • Android 6.0
  • minSdkVersion 21
  • targetSdkVersion 27
  • Language:Kotlin
  • 第三方使用套件:RxJava2

程度需求

  • 對 kotlin 有一定程度了解
  • 對 多執行緒 與 執行緒安全 有基礎概念
  • 對 物件導向的繼承、抽象、封裝有基本觀念
  • 熟悉 RxJava2
  • 難易度:★★★☆☆

之所以列出程度需求,並不是排擠新手或是不願意講解所運用到的觀念和關鍵技術,而是如果每個地方都必須講解的很徹底的話,那可能會偏離Camera2 開發過程的重點解說,先跟觀看此文的大家說聲抱歉抱歉!

因此有些基本觀念如你看不太懂,可以對該概念或技術先自我研習一下,養成好習慣,會對學習越來越有幫助的!

有些基本的程式觀念,日後我也會另開文章來說明,因為是開發歷程,有些Camera2的其他設定我也並沒有特別的深入研究,如你任何文章中看到錯誤,都可以提出指正哦!

目前可以達到的功能是:開啟相機前鏡頭後預覽 > 錄影成影片

前導:介紹與架構圖

難度標上三顆星,其實是因為當中我用了比較多的 Rx 操作以及 Camera2 Api 相較於 Camera Api對開發者真的是比較複雜了點,簡單的說,Camera2 Api 使用上沒有那麼直覺。

而且,Google 官方對 Camera2 的解說真的頗少,對於一開始就增加了像我這種英文不好的人來理解整個API結構的難度,所以我只好從官方的文章:

Camer2 Link

搭配 Source Code : Camera2Basic , Camera2Video 兩個範例去理解。

最大的架構原則,在我理解上,是使用了指令驅動和管線流模式。

  1. 指令驅動我定義為:每個需使用的功能必須經過下達指令要求後才能啟用,如沒有下達某要求,每個功能依照自己預設定義的方式開啟。

  2. 管線流模式我定義為:有一角色為中介控制者,從資料提供端提取資料或資料自行控制流入到一個輸入流,再將資料輸出到輸出流

指令驅動用個簡單的例子說明:我有一台電視,電視有一個功能叫做場景模式,比如足球模式、劇院模式、一般模式等等,可以改變電視的聲音等化效果、影像色彩之類的,但是我們一般不會去啟用這些模式,那麼電視就會以一般模式運行。

當我使用遙控器輸入劇院模式之後,電視就會被我驅動,改變成劇院模式。

管線流模式用個簡單的例子說明:我有一台筆記型電腦,開機之後,銀幕就會一直輸出影像。當我想要第二個輸出的時候(一般來說就是投影),我用另外一個銀幕接上電腦,那麼影像就也會輸出到另外一個銀幕上。

其實科技跟大眾的生活息息相關,很多我們在學習新技術的時候,身邊不乏一堆例子,都可以從中體會這些新技術的概念。

回到一開始我提到的最大架構原則,請注意我標註藍色的專有名詞,這些名詞會跟上方架構圖非常有關係。

先說明一下圖中的虛線部分,虛線代表流程線;大型箭頭代表管線流,分別有輸入跟輸出。這裡可能比較難以理解的是虛線 Create 和 大箭頭 Input 的地方。

兩個是擺在同一方向,但並不代表他們是一同運作的。什麼意思呢? 因為Camera2 API 利用 CameraDevice 產生 Request 和 Session 兩大重要物件,所以我將他分在Session之上;而 Input 是因為 Session 開啟相機的資料是來自 CameraDevice,所以我也將他分在Session之上。

CameraDevice產生兩大物件之後,在需求的時機點上,用Session去開啟相機輸入流讓資料輸入進來,換句話說,產生和輸入是分開的時機點,但一定要先產生才會有 Session可以開啟輸入流。

再來就是整體運作流程了,CameraManager 管理手機的相機,但不進行拍照、錄影、輸入輸出控制之類的實際硬體功能,我將 CameraManager 定義叫做入口點,他管理相機硬體上的資訊、硬體上的特徵描述以及打開相機

也因此 CameraManager 提供了一系列的 CameraCameraCharacteristics(相機特徵檔)供開發者取用,例如:相機預覽的大小、相機可支援錄製的大小、是否支援曝光模式、是否支援閃光模式,很多特徵描述,都可以到官方API文件上查閱。

接著看一下以下的流程描述:

  1. CameraManager 取得 CameraCameraCharacteristics,利用這些特徵描述先得知相關訊息,比如我想要先知道相機可以預覽的大小有多少,然後選出最適合的大小輸出到手機銀幕上的 Size
  2. 用 CameraManager openCamera,開啟成功之後,會得到 CameraDevice
  3. 用 CameraDevice 產生 CaptureRequest , CameraCaptureSession
  4. 用 CameraCaptureSession 搭配指令要求,請求相機開始輸入資料並將資料輸出到對應輸出上

以上四點流程,其中還有很多細節,之後會一一說明。

其中 CameraCaptureSession 所代表的就是中介者,輸入流來自相機,輸出流有很多,可能是 SurfaceView,TextureView,MediaRecorder,ImageReader etc.

其中 CaptureRequest 所代表的就是指令驅動,這裡你可以下達我要開啟3A自動模式還是手動控制模式,我要讓相機集中放大哪一區塊顯示,或是讓拍照的結果解析度控制在哪個解析度上。

當然,Camera1 API 就讓人熟知的就是相機成像的旋轉方向和銀幕方向是彼此相差90度的這個問題,想當然Camera2 應該也會知道這個問題,但是Camera2 不再讓開發者直接進行物理性旋轉。

簡單的說就是,setDisplayOrientation 在 Camera2 上沒有相等的設置了。

那怎麼辦,其實我研究了一兩個禮拜,得到的結果是:當 Session 渲染資料到Surface上的時候,如果你的銀幕比例剛好等於(也可以不用差太多)你的預覽和錄影結果比例就並不會因為你手持手機旋轉到各個角度之後產生拉伸的問題。

不過以上測驗,在Android 5 的手機,沒有用! 如果你真的很想要相容 Android 5 ,用 Camera 和 Camera2混合使用,才能真正的治本。

旋轉角度

在動手實作之前,我們一樣要先來探討一下,相機物理角度和銀幕角度是什麼樣的關係。

兩個名詞在英文上定義:

  1. 相機物理角度 = Sensor Orientation
  2. 銀幕角度 = Screen Orientation

首先,先定義旋轉方向是順時針,從0開始到360度:

圖中小黑圓點就是代表0點像素成像的方式,通過圖例就可以發現,相機角度跟銀幕角度總是相差90度,這是因為目前市面上幾乎所有手機都將相機硬體位置和銀幕位置設計成90度相差。

不過這個相差90度,不論是否相機在硬體上被固定的角度在90度角或是270度角,都是成立的,所以在Camera1 的時代,我們都會直接設定 setDisplayOrientation(90)的原因就在這裡。

因為相機成像的結果必然會和銀幕差了那90度角,你可以將左右兩圖重疊來看就會明白為什麼了。

題外話

接著來講個額外的說明好了,為什麼相機角度總是跟銀幕角度差了90度? 據我開始研究相機歷經Camera1 到 Camera2的,到了最近我去翻了一些有關影片拍攝、相機相關的文章來研究的時候才理解,其實這跟人在觀看影片和照片的習慣有關。

在沒有智慧型手機的年代,大概是八九年前吧,相機拍攝照片,DV拍攝影片,有沒有發現成像出來的結果都依循著一定的寬高比,像是 4:3 , 16:9,也就是寬比高大的成像結果。

我們都暸解 4:3,16:9 成像,像是電影、電視銀幕、專業的相機都會以寬比高大的方式被製作,之所以這些從以前一直到現在都沒改變的成像結果,就是因為寬比高大的成像能夠拍攝到的環境和表現的結果都能讓人觀看時比較舒服,而且來的有質感。

試著想像一下,如果你今天看一部電影,是高比寬的成像方式拍攝的,也就是用 9:16 的方式拍攝,我相信誰都無法接受這樣子的電影吧,感覺就真的非常奇怪。

那麼來到智慧型手機爆炸的年代,人們被改變了使用習慣,手機拿著的方式是直立銀幕的,因為比較好握和處理事情,以至於大多數包括我的開發者一開始會困惑為什麼相機總是跟銀幕差90度,好麻煩啊!

然後漸漸的,我們開始快速的操作相機來拍照或錄影,就並不會特別將手機橫著(90度 or 270度)拍攝,而是直立著拍攝,所以這個相差結果的原因就是如此。

這時候,如果你想要拍攝更多東西進來的時候,你會將手機橫著拿。

這時候無論你是270度還是90度,你都會符合正常拍攝方式,也是因為這樣硬體上才”正常”的設計成跟螢幕成90度差。

而正常上,相機的成像角度其實前後鏡頭會不一樣,有的手機後鏡頭是90前鏡頭是270,而有的手機卻是後鏡頭270前鏡頭90。

但是無論如何,不管是90或是270,都是寬比高長的比例尺,只是我們開發者要特別注意一件事情:

相機物理角度會影響拍攝結果,錄影或拍照都是!我們會預期直著拍,但是為什麼影片卻是直著拍沒錯,但是整個畫面被翻轉成90 or 270?

所以這時候,我們就要根據相機物理角度搭配螢幕旋轉角度,計算之後再輸出成:我們是怎麼拿著拍,就該怎麼呈現的方向!就像是以下這種讓我們疑惑的結果:

那要怎麼做以及詳細計算之後在錄影的地方就會提到!

第二章開始,就要正式用程式碼來帶範例囉!