??????? Android 視頻數據采集系列的最后一篇出爐了,和前兩篇文章想比,這篇文章從系統API層面進行一些探索,涉及到的細節更多。初次接觸 Camera2 API 會覺得它的使用有些繁瑣,涉及到的類有些多,不過就像第一次使用Activity, Fragment 的API 一樣,只要多加練習,熟練掌握這些 API 只是時間問題。
????????Andrid 系統最初提供的操控相機的 API android.hardware.camera 現已棄用,新的API android.hardware.camera2 在andrid L 上開始使用,這里只討論和學習 Camera2 的使用。根據谷歌官方的說法,重新設計 Camera API 的目的在于大幅提高應用對于 Android 設備上的相機子系統的控制能力,同時重新組織 API,提高其效率和可維護性。借助額外的控制能力,開發者可以更輕松地在 Android 設備上構建高品質的相機應用,這些應用可在多種產品上穩定運行,同時仍會盡可能使用設備專用算法來最大限度地提升質量和性能。
????????關于Camera API 和 Camere API2 的對比,官方介紹 Camera2 的視頻里沒有詳細說明,只是一筆帶過,提到了以下兩點:
????1. Camera API 不支持捕捉未被壓縮的畫面,或者不支持在新的硬件上運行,預覽效率被限定在一到三秒每幀;
????2. Camere2 API 拍照的時間間隔更短,支持在多臺相機上預覽,可以直接加特效或者濾膜。
??????? Camera2 API 把攝像頭設備建模為管道,該管道接收一個捕獲單個幀的請求做為輸入,輸出一個捕獲結果元數據包,以及該請求的一組輸出圖像緩沖區。這些請求包含有關幀的捕獲和處理的所有配置信息,其中包括分辨率和像素格式,手動傳感器、鏡頭和閃光燈控件,3A 操作模式,RAW 到 YUV 處理控件等。捕獲單個幀的請求按順序處理,并且多個請求可以同時進行,攝像頭設備處理數據需要經過多道工序的加工處理,在大多數Android 設備上需要有多個正在運行的請求才能保持完整的幀率。
????????完整的相機模型可以用下面一張圖表示:
????????如果想全面了解相機模型的細節,這張圖比較合適,可是這么多大大小小的框框和沒見過的類對于我這樣的小白不太友好,我們來看一張精簡的圖片:
????????第二張圖是相機核心操作的模型圖,其中出現的一些類 CameraDevice, CameraRequest等看起來有些陌生,不用著急待會兒給他們做一一介紹。
????????為了方便理解,我們可以把攝像頭設備看作一個工廠車間,捕獲一張圖片的過程看作車間里一道完整的流水線,把 Camera2 API 相關的類看作車間里的師傅們,這些師傅各司其職,協同工作完成捕獲圖像的任務。下面我們來看完成這個任務需要的師傅們。
??? ??? CameraManager,類似于LocationManager、ConnectivityManager ,是一個系統級別的服務管理器,負責枚舉、查詢和打開可用的相機設備。
??? ??? CameraDevice,是連接到 Android 設備的單個相機的表示形式,可以對以高幀速率捕獲圖像和后期處理進行細粒度控制。CameraDevice 描述了硬件設備以及該設備的可用設置和輸出參數。這些信息通過 CameraCharacteristics 對象提供。
????????CameraCharacteristics,用來描述 CameraDevice 的屬性,比如支持的 JPEG 縮略圖大小等, 這些屬性對于給定的 CameraDevice 是固定的,可以通過CameraManager.getCameraCharacteristics 查詢。
??? ??? CameraCaptureSession,是 CameraDevice 捕獲圖像的會話,用于捕獲來自攝像機的圖像或重新處理先前在同一會話中捕獲的圖像。創建 CameraCaptureSession 需要配置攝像頭設備的內部管道并分配用于將圖像發送到所需目標的內存緩沖區,是一項耗費資源的異步操作,可能需要幾百毫秒。
??? ??? CaptureRequest,是從攝像機設備捕獲單個圖像所需的一組不變的設置,包含捕獲硬件(傳感器,鏡頭,閃光燈),處理管線,控制算法和輸出緩沖區的配置。還包含捕獲后的圖像數據發送的目標 Surface 列表。
??? ??? CaptureResult,是 CameraDevice 處理 CaptureRequest之后產生的,從圖像傳感器捕獲的單個圖像結果的子集。包含捕獲硬件(傳感器,鏡頭,閃光燈),處理管線,控制算法和輸出緩沖區的最終配置的子集。
??? ??? Image,是與媒體源(例如MediaCodec或CameraDevice)一起使用的單個完整圖像緩沖區。Image 允許通過一個或多個 ByteBuffer 高效的直接訪問 Image 中的像素數據。每個緩沖區數據封裝在一個描述像素數據的 Plane中,由于這種直接訪問的方式,Image不能直接用作UI資源。
??????? Image 通常是由硬件組件直接生成或使用的,是整個系統共享的有限資源,應在不再需要時及時關閉。例如,當使用 ImageReader 類從各種媒體源中讀取圖像時,一旦達到 ImageReader.getMaxImages 的數量限制,不關閉舊的 Image 對象將阻止新圖像的可用性。
??? ??? ImageReader 人如其名,用來讀取 Image 數據,也可以做為 Image 的存儲緩沖區,允許應用程序直接訪問渲染到 Surface 中的圖像數據。CameraDevice 捕獲的圖像數據被封裝在 Image 對象中,Surface 使用 ImageReader讀取這些數據。使用 ImageReader 可以同時訪問多個Imagge對象,發送到 ImageReader 的圖像將排隊等待,ImageReader 的工作方式類似于生產者消費者模式,直到之前的圖像被訪問取走,新的圖像才能被存到隊列里。由于內存限制,如果 ImageReader 未能以等于生產速率的速率獲取和釋放圖像,則圖像源為了嘗試把圖像渲染到 Surface上,最終將停止發送或者丟掉一些圖像。
??? ??? SurfaceView,老熟人了,音視頻開發中出鏡率最高的 View 之一, 用于渲染 Camera 設備的預覽畫面和捕獲的圖像。
??? ??? DngCreator,用于將原始像素數據寫入 DNG 文件的類。通常與CameraDevice 可用的 ImageFormat.RAW_SENSOR 緩沖區一起使用,或與應用程序生成的 Bayer-type 原始像素數據一起使用。
??? ??? DNG(Digital Negative)文件格式是 Adobe 公司發表的一種跨平臺文件格式,旨在統一數碼相機廣泛使用的圖像文件格式“RAW”。DNG 文件允許在用戶定義的顏色空間中定義像素數據和關聯的元數據,該元數據允許在后期處理期間將該像素數據轉換為標準CIE XYZ顏色空間。
????????以上就是車間里參與捕獲圖像的主要的師傅們,等師傅們準備就緒后,就可以開始動工了。從準備到捕獲一張圖像一共需要經過五個流程:
準備渲染圖像的 SurfaceView
????????整個捕獲圖像的過程中,SurfaceView 負責相機畫面的渲染工作,它內部使用 Surface 來展示這些圖像。Surface 的創建是一個異步操作,等 Surface 創建完畢后就可以進行下一步操作。
打開攝像頭設備,初始化 CameraDevice
????????這一步需要參與的類有 CameraManager、CameraId, 以及 CameraHandler。打開攝像頭是一個耗時操作,為了不阻塞主線程,需要在新的線程里執行。告訴 CameraManager 要打開的攝像頭ID(前置或者后置攝像頭)以及執行該操作的handler, CameraManager 會通過接口回調通知你操作的結果,打開攝像頭成功,或者錯誤,或者攝像頭不可用。整個異步操作使用 kotlin 協程完成:
/**?Opens?the?camera?and?returns?the?opened?device?(as?the?result?of?the?suspend?coroutine)?*/@ExperimentalCoroutinesApi@SuppressLint("MissingPermission")private suspend fun openCamera( cameraManager: CameraManager, cameraId: String, cameraHandler: Handler): CameraDevice = suspendCancellableCoroutine { cont ->???? cameraManager.openCamera(cameraId,?object?:?CameraDevice.StateCallback()?{ ??????????override?fun?onOpened(camera:?CameraDevice)?=?cont.resume(camera)??????????override?fun?onDisconnected(camera:?CameraDevice)?{ Log.w(TAG, "Camera $cameraId has been disconnected") requireActivity().finish()??????????} override fun onError(camera: CameraDevice, error: Int) { val msg = when (error) { ERROR_CAMERA_DEVICE -> "Fatal device" ERROR_CAMERA_DISABLED -> "Device policy" ERROR_CAMERA_IN_USE -> "Camera in use" ERROR_CAMERA_SERVICE -> "Fatal service"???????????????????ERROR_MAX_CAMERAS_IN_USE?->?"Maximum?cameras?in?use"???????????????????else?->?"Unknown"??????????????? } val exception = RuntimeException("Camera $cameraId error:($error) $msg")???????????????Log.e(TAG,?exception.message,?exception)???????????????if?(cont.isActive)?cont.resumeWithException(exception)??????????}??????},?cameraHandler)?}
?3. 配置 CameraCaptureSession,開啟圖像預覽
????????攝像頭成功打開以后,就可以調用設備的硬件和軟件資源,創建圖像預覽了,這一步需要 CameraDevice、CameraSession、ImageReader 協作完成。ImageReader 被創建作為捕獲靜態圖像的緩存,使用 CameraDevice 創建 CameraSession時,把用于預覽圖像的 SurfaceView 中的 Surface 和 ImageReader 中的 Surface 作為圖像幀數據的接收者。調用 CameraSession.setRepeatingRequest()啟動攝像頭連續畫面預覽。
//?Initialize?an?image?reader?which?will?be?used?to?capture?still?photosval size = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!! .getOutputSizes(args.pixelFormat).maxBy { it.height * it.width }!!imageReader = ImageReader.newInstance(size.width, size.height, args.pixelFormat, IMAGE_BUFFER_SIZE)// Creates list of surfaces where the camera will output framesval targets = listOf(viewFinder.holder.surface, imageReader.surface)// Start a capture session using our open camera and list of surfaces where frames will gosession = createCaptureSession(camera, targets, cameraHandler)val captureRequest = camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW).apply { addTarget(viewFinder.holder.surface)}// Keep sending the capture request as frequently as possible until the//?session?is?torn?down?or?session.stopRepeating()?is?calledsession.setRepeatingRequest(captureRequest.build(), null, cameraHandler)
????????到這里,捕獲圖像前的準備工作已經完成,下面的操作就是等待用戶按下捕獲圖像的按鈕,拍攝圖像和保存圖像結果。
4. 捕獲圖像
????????捕獲圖像的操作由 CameraCaptureSession 完成,它使用保存的 CameraDevice 創建一個 CaptureRequest.Builder 用來設置捕獲圖像的參數以及展示圖像的 Surface, 把 CaptureRequest 和捕獲圖像后的回調函數 CameraCaptureSession.CaptureCallback 交給 CameraCaptureSession 后,它會通過 CaptureCallback 及時通知外界捕獲圖像的進度。這里使用 ImageReader 作為捕獲圖像的緩沖區,捕獲完成后,CameraCaptureSession 返回捕獲結果 TotalCaptureResult。捕獲圖像屬于IO 密集型操作,同樣需要異步實現:
/*** Helper function used to capture a still image using the [CameraDevice.TEMPLATE_STILL_CAPTURE]template.* It performs synchronization between the [CaptureResult] and the [Image] resulting* from the single capture, and outputs a [CombinedCaptureResult] object.*/private suspend fun takePhoto(): CombinedCaptureResult = suspendCoroutine { cont ->// Fulsh any images left in the image reader@Suppress("ControlFlowWithEmptyBody") while (imageReader.acquireNextImage() != null) {} // Start a new image queueval imageQueue = ArrayBlockingQueue(IMAGE_BUFFER_SIZE)imageReader.setOnImageAvailableListener({ reader ->??????val?image?=?reader.acquireNextImage()??????Log.d(TAG,?"Image?available?in?queue:${image.timestamp}")??????imageQueue.add(image)}, imageReaderHandler)val captureResult = session.device.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE) .apply { addTarget(imageReader.surface) }session.capture(captureResult.build(), object : CameraCaptureSession.CaptureCallback() { override fun onCaptureStarted( session: CameraCaptureSession, request: CaptureRequest, timestamp: Long, frameNumber: Long?????)?{?????????super.onCaptureStarted(session,?request,?timestamp,?frameNumber) // Start the animation to inform the user that capture begin viewFinder.post(animationTask) }???????override?fun?onCaptureCompleted( session: CameraCaptureSession, request: CaptureRequest,???????????result:?TotalCaptureResult ) { // Save capture result and other operations... }, cameraHandler)}
5. 保存圖像
????????拿到捕獲的圖像后,到了最后一道工序,保存捕獲的圖像。如果圖像的格式是 JPEG 或者 DEPTH_JPEG,直接保存圖像的字節流,如果是原始格式 RAW_SENSOR,需要使用 DngCreator 把圖像數據保存為跨平臺的 DNG 格式,方便以后使用,保存成功以后返回一個 File 文件:
/** Helper function used to save a [CombinedCaptureResult] into a [File] */private suspend fun saveResult(result: CombinedCaptureResult): File = suspendCoroutine { cont -> when (result.format) { // When the format is JPEG or DEPTH JPEG we can simply save the bytes as-is ImageFormat.JPEG, ImageFormat.DEPTH_JPEG -> { val buffer = result.image.planes[0].buffer?????????val?bytes?=?ByteArray(buffer.remaining()).apply?{?buffer.get(this)?}?????????try?{ val output = createFile(requireContext(), "jpg") FileOutputStream(output).use { it.write(bytes) } cont.resume(output)??????????????}?catch?(exc:?IOException)?{ Log.e(TAG, "Unable to write JPEG image to file", exc) cont.resumeWithException(exc) }????????} // When the format is RAW we use the DngCreator utility library ImageFormat.RAW_SENSOR -> {???????????val?dngCreator?=?DngCreator(characteristics,?result.metadata) try { val output = createFile(requireContext(), "dng") FileOutputStream(output).use { dngCreator.writeImage(it, result.image) } cont.resume(output) } catch (exc: IOException) { Log.e(TAG, "Unable to write DNG image to file", exc) cont.resumeWithException(exc) } } // No other formats are supported by this sample else -> {???????????val?exc?=?RuntimeException("Unknown?image?format:?${result.image.format}") Log.e(TAG, exc.message, exc) cont.resumeWithException(exc)???????}??? }?}
????????到這里就完成了使用 Camera2 API 捕獲一張圖像的任務,使用 Camera2 API 可以拿到圖像的原始數據用作后期各種處理,并且對捕獲圖像的過程進行更細粒度的控制,獲取完成的示例代碼請移步 https://github.com/android/camera-sample 或者 https://github.com/Hiwensen/StreamingTour
??????? Android 設備采集視頻數據系列也到此結束,三種方法:使用系統已安裝的相機應用,使用 Jetpack ?CameraX 庫或者使用 Camera2 API,各有優缺點和不同的適用場景,總有一種能滿足你的需求。文中只介紹了基本的捕獲圖像的用法,至于錄制視頻和更多功能,后期有更多時間了繼續探索。