在整個音視頻處理的過程中,位於發送端的音視頻采集工作無疑是整個音視頻鏈路的開始。在 Android 或者 IOS 上都有相關的硬件設備——Camera 和麥克風作為輸入源。本章我們來分析如何在 Android 上通過 Camera 以及錄音設備采集數據。本章可結合之前發布的文章
Camera
在 Android 上的圖片/視頻采集設備無疑就是 Camera 了,在 Android SDK API21 之前的版本只能使用 Camera1 ,在 API 21 之后 Camera1 已經被標記為 Deprecated ,Google 推薦使用 Camera2,下面我們來分別看一下。
Camera1
我們先來看一下 Camera1 體系的部分類圖。
Camera 類是 Camera1 體系的核心類,該類還有好多內部類,如上圖:
Camera.CameraInfo 類表達 Camera 的前后(facing)和旋轉(orientation)等 Camera 相關的信息。
Camera.Parameters 類是 Camera 相關的參數設置比如設置預覽 Size 以及設置旋轉角度等。
Camera 類擁有打開 Camera、設置參數、設置預覽等 API,下面我們來看使用 Camera API 打開系統照相機的流程。
1.在開啟 Camera 之前先釋放 Camera,這一步的目的是重置 Camera 的狀態重置 Camera 的 previewCallback 為 null
調用 Camera 的 release 釋放
把 Camera 對象設置為 null
/** *釋放Camera */ private fun releaseCamera() { //重置previewCallback為空 cameraInstance!!.setPreviewCallback(null) cameraInstance!!.release() cameraInstance = null }
2.獲取 Camera 的 Id
/** *獲取Camera Id */ private fun getCurrentCameraId(): Int { val cameraInfo = Camera.CameraInfo() //遍歷所有的Camera id,比較CameraInfo facing for (id in 0 until Camera.getNumberOfCameras()) { Camera.getCameraInfo(id, cameraInfo) if (cameraInfo.facing == cameraFacing) { return id } } return 0 }
3.打開 Camera 獲取 Camera 對象
/** *獲取Camera 實例 */ private fun getCameraInstance(id: Int): Camera { return try { //調用Camera的open函數獲取Camera的實例 Camera.open(id) } catch (e: Exception) { throw IllegalAccessError("Camera not found") } }
4.設置 Camera 的相關參數
//[3]設置參數 val parameters = cameraInstance!!.parameters if (parameters.supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) { parameters.focusMode = Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE } cameraInstance!!.parameters = parameters
5.設置 previewDisplay
//【4】 調用Camera API 設置預覽Surface surfaceHolder?.let { cameraInstance!!.setPreviewDisplay(it) }
6.設置預覽回調
//【5】 調用Camera API設置預覽回調 cameraInstance!!.setPreviewCallback { data, camera -> if (data == null || camera == null) { return@setPreviewCallback } val size = camera.parameters.previewSize onPreviewFrame?.invoke(data, size.width, size.height) }
7.開啟預覽
//【6】 調用Camera API開啟預覽 cameraInstance!!.startPreview()
上面代碼中的【3】【4】【5】【6】都是調用 Camera 類的 API 來完成,
經過上面的流程之后,Camera 的預覽會顯示在傳入的 Surface 上,並且在 Camera 停止前會一直回調函數onPreviewFrame(byte[] data,Camera camera)
,其中 byte[] data 中存儲的就是實時的 YUV 圖像數據。byte[] data 的格式是 YUV 格式中的 NV21。
YUV 圖像格式
色彩空間
這里我們只講常用到的兩種色彩空間。
RGBRGB 的顏色模式應該是我們最熟悉的一種,在現在的電子設備中應用廣泛。通過 R G B 三種基礎色,可以混合出所有的顏色。
YUV 這里着重講一下 YUV,這種色彩空間並不是我們熟悉的。這是一種亮度與色度分離的色彩格式。
早期的電視都是黑白的,即只有亮度值,即 Y。有了彩色電視以后,加入了 UV 兩種色度,形成現在的 YUV,也叫 YCbCr。
Y:亮度,就是灰度值。除了表示亮度信號外,還含有較多的綠色通道量。
U:藍色通道與亮度的差值。
V:紅色通道與亮度的差值。
采用 YUV 有什么優勢呢?
人眼對亮度敏感,對色度不敏感,因此減少部分 UV 的數據量,人眼卻無法感知出來,這樣可以通過壓縮 UV 的分辨率,在不影響觀感的前提下,減小視頻的體積。
RGB 和 YUV 的換算
Y = 0.299R + 0.587G + 0.114B
U = -0.147R - 0.289G + 0.436B
V = 0.615R - 0.515G - 0.100B
——————————————————
R = Y + 1.14V
G = Y - 0.39U - 0.58V
B = Y + 2.03U
YUV 格式
YUV 存儲方式分為兩大類:planar 和 packed。
-
planar:先存儲所有 Y,緊接着存儲所有 U,最后是 V;
-
packed:每個像素點的 Y、U、V 連續交叉存儲。
pakced 存儲方式已經非常少用,大部分視頻都是采用 planar 存儲方式。
對於 planar 存儲方式,通過省略一些色度信息,即亮度共用一些色度信息,進而節省存儲空間。因此,planar 又區分了以下幾種格式: YUV444、 YUV422、YUV420。
YUV 4:4:4 采樣,每一個 Y 對應一組 UV 分量。
YUV 4:2:2 采樣,每兩個 Y 共用一組 UV 分量。
YUV 4:2:0 采樣,每四個 Y 共用一組 UV 分量。
其中,最常用的就是 YUV420。
YUV420 格式存儲方式又分兩種類型
-
YUV420P:三平面存儲。數據組成為 YYYYYYYYUUVV(如 I420)或 YYYYYYYYVVUU(如 YV12)。
-
YUV420SP:兩平面存儲。分為兩種類型 YYYYYYYYUVUV(如 NV12)或 YYYYYYYYVUVU(如 NV21)
Camera2
在 Andorid SDK API 21 之后呢,Google 就推薦使用 Camera2 體系來管理設備,Camera2 還是與 Camera1 有很大的不同的。一樣的,我們先來看一下 Camera2 體系的部分類圖
Camera2 要比 Camera1 復雜的多,CameraManager CameraCaptureSession 是 Camera2 體系的核心類,CameraManager 用來管理攝像頭的打開和關閉 Camera2 引入了 CameraCaptureSession 來管理拍攝會話。
我們下面來看一下更詳細的流程圖。
1.在開啟 Camera 之前先釋放 Camera,這一步的目的是重置 Camera 的狀態。
private fun releaseCamera() { imageReader?.close() cameraInstance?.close() captureSession?.close() imageReader = null cameraInstance = null captureSession = null }
2.獲取 Camera 的 Id
/** *【1】 獲取Camera Id */ private fun getCameraId(facing: Int): String? { return cameraManager.cameraIdList.find { id -> cameraManager.getCameraCharacteristics(id).get(CameraCharacteristics.LENS_FACING) == facing } }
3.打開 Camera
try { //【2】打開Camera,傳入的 CameraDeviceCallback()是攝像機設備狀態回調 cameraManager.openCamera(cameraId, CameraDeviceCallback(), null) } catch (e: CameraAccessException) { Log.e(TAG, "Opening camera (ID: $cameraId) failed.") } //設備狀態回調 private inner class CameraDeviceCallback : CameraDevice.StateCallback() { override fun onOpened(camera: CameraDevice) { cameraInstance = camera //【3】開啟拍攝會話 startCaptureSession() } override fun onDisconnected(camera: CameraDevice) { camera.close() cameraInstance = null } override fun onError(camera: CameraDevice, error: Int) { camera.close() cameraInstance = null } }
4.開啟拍攝會話
//【3】開啟拍攝會話 private fun startCaptureSession() { val size = chooseOptimalSize() //創建ImageRender並設置回調 imageReader = ImageReader.newInstance(size.width, size.height, ImageFormat.YUV_420_888, 2).apply { setOnImageAvailableListener({ reader -> val image = reader?.acquireNextImage() ?: return@setOnImageAvailableListener onPreviewFrame?.invoke(image.generateNV21Data(), image.width, image.height) image.close() }, null) } try { if (surfaceHolder == null) { //設置ImageRender的surface給cameraInstance,以便后面預覽的時候數據呈現到ImageRender的surface,從而觸發ImageRender的回調 cameraInstance?.createCaptureSession( listOf(imageReader!!.surface), //【4】CaptureStateCallback是CameraCaptureSession的內部類,是攝像機會話狀態的回調 CaptureStateCallback(), null ) } else { cameraInstance?.createCaptureSession( listOf(imageReader!!.surface, surfaceHolder!!.surface), CaptureStateCallback(), null ) } } catch (e: CameraAccessException) { Log.e(TAG, "Failed to start camera session") } } //攝像機會話狀態的回調 private inner class CaptureStateCallback : CameraCaptureSession.StateCallback() { override fun onConfigureFailed(session: CameraCaptureSession) { Log.e(TAG, "Failed to configure capture session.") } //攝像機配置完成 override fun onConfigured(session: CameraCaptureSession) { cameraInstance ?: return captureSession = session //設置預覽CaptureRequest.Builder val builder = cameraInstance!!.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW) builder.addTarget(imageReader!!.surface) surfaceHolder?.let { builder.addTarget(it.surface) } try { //開啟會話 session.setRepeatingRequest(builder.build(), null, null) } catch (e: CameraAccessException) { Log.e(TAG, "Failed to start camera preview because it couldn't access camera", e) } catch (e: IllegalStateException) { Log.e(TAG, "Failed to start camera preview.", e) } } }
PS
ImageRender 可以直接訪問呈現在 Surface 上得圖像數據,ImageRender 的工作原理是創建實例並設置回調,這個回調會在 ImageRender 所關聯的 Surface 上的圖像可用時調用
我們分析了上面的 Camera 采集數據,完整的代碼請看文末的 Github 地址
AudioRecord
上面分析完了視頻,我們接着來看音頻,錄音 API 我們使用 AudioRecord,錄音的流程相對於視頻而言要簡單許多,一樣的,我們先來看一下簡單類圖
就一個類,API 也簡單明了,我們來看一下流程
下面上代碼
public void startRecord() { //開啟錄音 mAudioRecord.startRecording(); mIsRecording = true; //開啟新線程輪詢 ExecutorService executorService = Executors.newSingleThreadExecutor(); executorService.execute(new Runnable() { @Override public void run() { byte[] buffer = new byte[DEFAULT_BUFFER_SIZE_IN_BYTES]; while (mIsRecording) { int len = mAudioRecord.read(buffer, 0, DEFAULT_BUFFER_SIZE_IN_BYTES); if (len > 0) { byte[] data = new byte[len]; System.arraycopy(buffer, 0, data, 0, len); //處理data } } } }); } public void stopRecord() { mIsRecording = false; mAACMediaCodecEncoder.stopEncoder(); mAudioRecord.stop(); }
AudioRecord 生成的 byte[] data 即 PCM 音頻數據
小結