音視訊採集
在整個音視訊處理的過程中,位於傳送端的音視訊採集工作無疑是整個音視訊鏈路的開始。在 Android 或者 IOS 上都有相關的硬體裝置——Camera 和麥克風作為輸入源。本章我們來分析如何在 Android 上通過 Camera 以及錄音裝置採集資料。本章可結合之前釋出的文章Android 音視訊 - MediaCodec 編解碼音視訊做一個完整的 Demo。
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 音訊資料。
小結
本章我們對音視訊的原生輸入 API 進行了詳細的介紹,這個也是我們後面部落格的基礎,有了 YUV 和 PCM 資料之後,就可以編碼了,下一篇我們再來分析 MediaCodec,用 MediaCodec 對原生音視訊資料進行硬編碼生成 Mp4。