乐趣区

关于音视频:Android-音视频采集那些事

音视频采集

在整个音视频解决的过程中,位于发送端的音视频采集工作无疑是整个音视频链路的开始。在 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。

退出移动版