Camera2 的使用

之前也反反复复用了几次Camera2,但是间隔了一段时间不用后,再次使用的时候又回去看之前的代码,显得特别麻烦,可能是之前没有对Camara2了解清楚,今天就好好的总结一下。

为什么使用Camara2呢

Android Camera2 API 是 Android 5.0(Lollipop)引入的相机框架,它提供了更强大和灵活的相机功能,相较于旧版的 Camera API,Camera2 API 具有以下几个主要的改进:

  1. 更直观的相机控制:Camera2 API 提供了更直观和一致的相机控制接口,使开发者可以更精确地控制相机的各种参数和功能。

  2. 延迟更低的相机操作:Camera2 API 定义了一个更现代化和高性能的相机架构,允许开发者在处理图像数据时进行并行操作,减少了传统相机 API 中的延迟问题。

  3. 支持多摄像头和多通道捕获:Camera2 API 具备对多摄像头设备的完全支持,包括前后摄像头的切换和同时使用多个摄像头进行捕获。它还允许开发者同时设置多个目标 Surface 进行图像捕获,例如同时保存照片和显示预览。

  4. 更好的图像质量和后期处理:Camera2 API 提供了更广泛的配置选项,使开发者能够更好地控制图像处理和后期处理操作,以获得更好的图像质量和效果。

总体来说,Android Camera2 API 是为了满足手机拍摄和图像处理需求日益增长的趋势而推出的更先进和强大的相机框架。它为开发者提供了更多的控制权和灵活性,使他们能够利用设备的相机功能创造出更多创意和高质量的摄影和图像应用程序。

如何使用Camera2

在了解之后,我们一起看看Camera2 如何使用。下面分别从 预览、拍照、录制视频和编码四个方面来介绍Camera2的使用。

Camera2 预览

  1. 添加权限
ini 复制代码
<uses-permission android:name="android.permission.CAMERA" />

动态请求权限:

kotlin 复制代码
private const val PERMISSIONS_REQUEST_CODE = 10
private val PERMISSIONS_REQUIRED = arrayOf(Manifest.permission.CAMERA)

fun hasPermissions(context: Context) = PERMISSIONS_REQUIRED.all {
    ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
}
  1. 获取 CameraManager 对象:可以通过 getSystemService() 方法获取系统的 CameraManager 对象。
kotlin 复制代码
private val cameraManager: CameraManager by lazy {
    application.getSystemService(Context.CAMERA_SERVICE) as CameraManager
}
  1. 获取摄像头ID
ini 复制代码
private val cameraId: String by lazy {
    val cameraIds = cameraManager.cameraIdList
    var id = cameraIds[0]
    for (cameraId in cameraIds) {
        val characteristics = cameraManager.getCameraCharacteristics(cameraId)
        val cameraDirection = characteristics.get(CameraCharacteristics.LENS_FACING)
        if (cameraDirection == CameraCharacteristics.LENS_FACING_FRONT) {
            id = cameraId
            break
        }
    }
    id
}
  1. 创建一个Handler 对象,让接口回调的方法在交给handler 所在的线程执行
scss 复制代码
private val cameraThread = HandlerThread("CameraThread").apply { start() }
private val cameraHandler = Handler(cameraThread.looper)
  1. 打开相机得到 CameraDevice对象
kotlin 复制代码
camera = openCamera(cameraManager, cameraId, cameraHandler)

private suspend fun openCamera(
    manager: CameraManager,
    cameraId: String,
    handler: Handler? = null
): CameraDevice = suspendCancellableCoroutine { cont ->
    manager.openCamera(cameraId, object : CameraDevice.StateCallback() {
        override fun onOpened(device: CameraDevice) = cont.resume(device)

        override fun onDisconnected(device: CameraDevice) {
            Log.w(TAG, "Camera $cameraId has been disconnected")
        }

        override fun onError(device: 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 exc = RuntimeException("Camera $cameraId error: ($error) $msg")
            Log.e(TAG, exc.message, exc)
            if (cont.isActive) cont.resumeWithException(exc)
        }
    }, handler)
}
  1. 创建session,通过 CameraDevice对象和预览的 SurfaceView 的Surface 创建出session对象
kotlin 复制代码
session = createCaptureSession(camera, targets, cameraHandler)

private suspend fun createCaptureSession(
    device: CameraDevice,
    targets: List<Surface>,
    handler: Handler? = null
): CameraCaptureSession = suspendCoroutine { cont ->


    device.createCaptureSession(targets, object : CameraCaptureSession.StateCallback() {

        override fun onConfigured(session: CameraCaptureSession) = cont.resume(session)

        override fun onConfigureFailed(session: CameraCaptureSession) {
            val exc = RuntimeException("Camera ${device.id} session configuration failed")
            Log.e(TAG, exc.message, exc)
            cont.resumeWithException(exc)
        }
    }, handler)
}
  1. 创建一个预览请求
scss 复制代码
val captureRequest = camera.createCaptureRequest(
    CameraDevice.TEMPLATE_PREVIEW).apply { addTarget(surfaceView.holder.surface) 
  1. 用session 发送预览请求,这样数据就源源不断的输出到Surface,从而在SurfaceView展示出来
csharp 复制代码
session.setRepeatingRequest(captureRequest.build(), null, cameraHandler)
  1. 处理预览方向问题,按照上面的做法可以预览,但发现方向不对,需要处理一下方向
less 复制代码
relativeOrientation = OrientationLiveData(this, characteristics).apply {
    observe(this@Camara2TakePhotoActivity, Observer { orientation ->
        Log.d(TAG, "Orientation changed: $orientation")
    })
}
kotlin 复制代码
class OrientationLiveData(
        context: Context,
        characteristics: CameraCharacteristics
): LiveData<Int>() {

    private val listener = object : OrientationEventListener(context.applicationContext) {
        override fun onOrientationChanged(orientation: Int) {
            val rotation = when {
                orientation <= 45 -> Surface.ROTATION_0
                orientation <= 135 -> Surface.ROTATION_90
                orientation <= 225 -> Surface.ROTATION_180
                orientation <= 315 -> Surface.ROTATION_270
                else -> Surface.ROTATION_0
            }
            val relative = computeRelativeRotation(characteristics, rotation)
            if (relative != value) postValue(relative)
        }
    }

    override fun onActive() {
        super.onActive()
        listener.enable()
    }

    override fun onInactive() {
        super.onInactive()
        listener.disable()
    }

    companion object {

        /**
         * Computes rotation required to transform from the camera sensor orientation to the
         * device's current orientation in degrees.
         *
         * @param characteristics the [CameraCharacteristics] to query for the sensor orientation.
         * @param surfaceRotation the current device orientation as a Surface constant
         * @return the relative rotation from the camera sensor to the current device orientation.
         */
        @JvmStatic
        private fun computeRelativeRotation(
                characteristics: CameraCharacteristics,
                surfaceRotation: Int
        ): Int {
            val sensorOrientationDegrees =
                    characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)!!

            val deviceOrientationDegrees = when (surfaceRotation) {
                Surface.ROTATION_0 -> 0
                Surface.ROTATION_90 -> 90
                Surface.ROTATION_180 -> 180
                Surface.ROTATION_270 -> 270
                else -> 0
            }

            // Reverse device orientation for front-facing cameras
            val sign = if (characteristics.get(CameraCharacteristics.LENS_FACING) ==
                    CameraCharacteristics.LENS_FACING_FRONT) 1 else -1

            // Calculate desired JPEG orientation relative to camera orientation to make
            // the image upright relative to the device orientation
            return (sensorOrientationDegrees - (deviceOrientationDegrees * sign) + 360) % 360
        }
    }
}

在这里就帮我们处理好了方向问题。 到这里就把摄像头预览实现了,接下来看看如何实现拍照的?

Camera2 拍照

通过预览的实现,发现Camera2 是通过session发送请求拿到数据的,拍照的时候,是不是发送一个拍照的请求然后就可以拿到数据呢?

其实基本流程差不多,只要修改一些地方即可。

  1. 创建一个 ImageReader ,先简单的介绍一下这个类是什么 android.media.ImageReader 是 Android Camera2 API 中的一个类,用于从相机设备获取图像数据。它可以用于捕获相机预览帧、拍照或录制视频等操作。

ImageReader 类提供了以下功能:

  1. 获取图像数据:可以通过调用 acquireLatestImage()acquireNextImage() 方法来获取相机设备的最新图像数据。您可以使用这些数据进行处理、保存或显示。

  2. 图像数据监听:可以通过设置 OnImageAvailableListener 接口来监听图像数据的可用性,并在图像可用时执行自定义操作。

  3. 图像格式和尺寸配置:可以指定要接收的图像格式和尺寸。通常,您可以选择 JPEG、YUV 或 RAW 等格式,并根据设备支持的尺寸列表选择合适的尺寸。

下面是使用 ImageReader 的示例代码:

kotlin 复制代码
val imageWidth = 640 // 图像宽度
val imageHeight = 480 // 图像高度
val imageFormat = ImageFormat.JPEG // 图像格式

val maxImages = 2 // 最大获取图像的数量
val imageReader = ImageReader.newInstance(imageWidth, imageHeight, imageFormat, maxImages)

imageReader.setOnImageAvailableListener(
    object : ImageReader.OnImageAvailableListener {
        override fun onImageAvailable(reader: ImageReader) {
            val image = reader.acquireLatestImage()
            // 处理图像数据
            image.close()
        }
    },
    handler // 用于处理回调的 Handler
)

在上述示例中,我们使用 newInstance() 方法创建一个 ImageReader 实例,并指定图像的宽度、高度和格式。然后,通过设置 setOnImageAvailableListener() 方法注册一个监听器,当图像可用时调用回调方法进行处理。

需要注意的是,使用完 ImageReader 后,应调用 close() 方法关闭它以释放资源。

ImageReader 是一个强大且灵活的类,可以满足相机应用中对图像数据的不同需求。

  1. 在创建session 的时候,把 ImageReader 的Surface传进去
scss 复制代码
// 创建相机将输出帧的surface,这里有两个地方 预览页面和 imageReader
val targets = listOf(surfaceView.holder.surface, imageReader.surface)

// 创建 session,注意这时候 传入的是两个Surface 一个是预览的,一个是拍照
session = createCaptureSession(camera, targets, cameraHandler)
  1. 创建一个捕获数据的请求,然后在回调中拿到数据,处理方向,然后把数据传出去,最后保存图片
kotlin 复制代码
 private suspend fun takePhoto():
            CombinedCaptureResult = suspendCoroutine { cont ->

        // 刷新图像阅读器中剩余的所有图像
        @Suppress("ControlFlowWithEmptyBody")
        while (imageReader.acquireNextImage() != null) {
        }

        // 启动一个新的图像队列
        val imageQueue = ArrayBlockingQueue<Image>(IMAGE_BUFFER_SIZE)
        imageReader.setOnImageAvailableListener({ reader ->
            // 拿到数据放到队列里面
            val image = reader.acquireNextImage()
            Log.d(TAG, "Image available in queue: ${image.timestamp}")
            imageQueue.add(image)
        }, imageReaderHandler)
        // 创建一个拍照的请求
        val captureRequest = session.device.createCaptureRequest(
            CameraDevice.TEMPLATE_STILL_CAPTURE).apply { addTarget(imageReader.surface) }
        // 发送一个捕获请求
        session.capture(captureRequest.build(), object : CameraCaptureSession.CaptureCallback() {

            override fun onCaptureStarted(
                session: CameraCaptureSession,
                request: CaptureRequest,
                timestamp: Long,
                frameNumber: Long) {
                super.onCaptureStarted(session, request, timestamp, frameNumber)
//                fragmentCameraBinding.viewFinder.post(animationTask)
            }

            override fun onCaptureCompleted(
                session: CameraCaptureSession,
                request: CaptureRequest,
                result: TotalCaptureResult) {
                super.onCaptureCompleted(session, request, result)
                val resultTimestamp = result.get(CaptureResult.SENSOR_TIMESTAMP)
                Log.d(TAG, "Capture result received: $resultTimestamp")

                // 设置一个超时,以防从管道中删除捕获的图像
                val exc = TimeoutException("Image dequeuing took too long")
                val timeoutRunnable = Runnable { cont.resumeWithException(exc) }
                // 发送一个延迟消息,如果超时了就抛异常,这个很不错
                imageReaderHandler.postDelayed(timeoutRunnable, IMAGE_CAPTURE_TIMEOUT_MILLIS)

               
                @Suppress("BlockingMethodInNonBlockingContext")
                lifecycleScope.launch(cont.context) {
                    while (true) {
                        // 拿到数据
                        // Dequeue images while timestamps don't match
                        val image = imageQueue.take()
                        // TODO(owahltinez): b/142011420
                        // if (image.timestamp != resultTimestamp) continue
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
                            image.format != ImageFormat.DEPTH_JPEG &&
                            image.timestamp != resultTimestamp) continue
                        Log.d(TAG, "Matching image dequeued: ${image.timestamp}")

                        // Unset the image reader listener
                        imageReaderHandler.removeCallbacks(timeoutRunnable)
                        imageReader.setOnImageAvailableListener(null, null)

                        // Clear the queue of images, if there are left
                        while (imageQueue.size > 0) {
                            imageQueue.take().close()
                        }

                        // Compute EXIF orientation metadata
                        val rotation = relativeOrientation.value ?: 0
                        val mirrored = characteristics.get(CameraCharacteristics.LENS_FACING) ==
                                CameraCharacteristics.LENS_FACING_FRONT
                        // 处理方向
                        val exifOrientation = computeExifOrientation(rotation, mirrored)
                        // 把 CombinedCaptureResult 传出去
                        cont.resume(CombinedCaptureResult(
                            image, result, exifOrientation, imageReader.imageFormat))
                    }
                }
            }
        }, cameraHandler)
    }
  1. 拿到数据后,保存到文件
scss 复制代码
lifecycleScope.launch(Dispatchers.IO) {
        takePhoto().use { result ->
            Log.d(TAG, "Result received: $result")

            // Save the result to disk
            val output = saveResult(result)
            Log.d(TAG, "Image saved: ${output.absolutePath}")

            if (output.extension == "jpg") {
                val exif = ExifInterface(output.absolutePath)
                exif.setAttribute(
                    ExifInterface.TAG_ORIENTATION, result.orientation.toString())
                exif.saveAttributes()
                Log.d(TAG, "EXIF metadata saved: ${output.absolutePath}")
            }

        }

        // Re-enable click listener after photo is taken
        it.post { it.isEnabled = true }
    }
kotlin 复制代码
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(this, "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(this, "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)
        }
    }
}
kotlin 复制代码
private fun createFile(context: Context, extension: String): File {
    val sdf = SimpleDateFormat("yyyy_MM_dd_HH_mm_ss_SSS", Locale.US)
    return File(context.filesDir, "IMG_${sdf.format(Date())}.$extension")
}

到此拍照也完成了,逻辑不多,只是代码有点多。

Camera2 录制视频

录制使用的是 MediaRecorder,设置好了之后 拿到 MediaRecorder的Surface 然后就可以去录制了。

  1. 初始化 MediaRecorder
scss 复制代码
private fun initMediaRecorder() {
    val profile = CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH)
    mediaRecorder = MediaRecorder().apply {

        setVideoSource(MediaRecorder.VideoSource.SURFACE)
        setAudioSource(MediaRecorder.AudioSource.MIC)
        setOutputFormat(profile.fileFormat)
        setVideoFrameRate(profile.videoFrameRate)
        setVideoSize(profile.videoFrameWidth, profile.videoFrameHeight)
        setVideoEncodingBitRate(profile.videoBitRate)
        setVideoEncoder(profile.videoCodec)
        setAudioEncoder(profile.audioCodec)
        setOutputFile("$cacheDir/output_recode_2.mp4")
        //处理方向问题
        setOrientationHint(relativeOrientation.value?:0)
        try {
            prepare()
            Log.i(TAG,"surface-------------$surface")
        } catch (e: IOException) {
            e.printStackTrace()
        }
    }


}
  1. 就是录制的时候,先停止之前的预览,然后重新场景一个session 和一个request ,把 MediaRecorder 对象的Surface对象传过去
scss 复制代码
private suspend fun startRecord() {
    initMediaRecorder()
    // 停止预览
    stopPreview()
    // 创建录制的session,这时候需要传入mediaRecorder.surface 接收数据
    mediaRecorder?.let { mediaRecorder ->
        val recordSurface = mediaRecorder.surface
        val targets = listOf(surfaceView.holder.surface, recordSurface)

        // 创建 session,注意这时候 传入的是两个Surface 一个是预览的,一个是录制的
        session = createCaptureSession(camera, targets, cameraHandler)
        //创建一个 预览请求,设置为录制模式,并且添加预览和录制的Surface
        val captureRequest = camera.createCaptureRequest(
            CameraDevice.TEMPLATE_RECORD
        ).apply {
            addTarget(surfaceView.holder.surface)
            addTarget(recordSurface)
            set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO)
        }

        // 发送请求
        session.setRepeatingRequest(captureRequest.build(), null, cameraHandler)
        // 开启录制
        mediaRecorder.start()
    }

}

这样录制就完成了,思路还是挺清晰的。

编码

目的:通过Camera2和MediaCodec 对预览画面进行编码,然后通过MediaMuxer写入MP4文件。

思路:我们可以通过创建session的添加一个ImageReader 的Surface,在录制的时候发送的请求也带上ImageReader 的Surface,我们就可以通过监听 ImageReader 解析数据,然后把YUV数据传给mediaCodec进行编码即可,最后把编码好的数据写入MP4文件中。

  1. 创建一个MediaCodec对象,进行编码
kotlin 复制代码
private fun initMediaCodec() {
    val mimeType = MediaFormat.MIMETYPE_VIDEO_AVC // 视频编码格式
    val videoFormat = MediaFormat.createVideoFormat(mimeType, VIDEO_WIDTH, VIDEO_HEIGHT)
    videoFormat.setInteger(
        MediaFormat.KEY_COLOR_FORMAT,
        MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible
    )
    videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, VIDEO_WIDTH * VIDEO_HEIGHT)
    videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30)
    videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2)

    mediaCodec = MediaCodec.createEncoderByType(mimeType)
    mediaCodec.configure(videoFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
    
}
  1. 创建一个MediaMuxer 对象,进行MP4文件的写入
kotlin 复制代码
private fun initMediaMuxer() {
    val filename =
        Environment.getExternalStorageDirectory().absolutePath + "/DCIM/Camera/" + getCurrentTime() + ".mp4"
    try {
        mediaMuxer = MediaMuxer(filename, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
        mediaMuxer.setOrientationHint(90)
    } catch (e: IOException) {
        throw java.lang.RuntimeException(e)
    }
}
  1. 创建 ImageReader对象
kotlin 复制代码
private fun initPreviewImageReader() {
    previewImageReader = ImageReader.newInstance(
        previewSize.width, previewSize.height, ImageFormat.YUV_420_888, 1
    )
    previewImageReader.setOnImageAvailableListener({ imageReader ->
        val image = imageReader.acquireNextImage()
        encodeVideo(ImageUtil.getBytesFromImageAsType(image, 1))
        image.close()
    }, cameraHandler)
}
  1. 在创建session的时候,传入 ImageReader对象的Surface,并设置监听拿到YUV数据
scss 复制代码
private suspend fun startPreviewSession() {
    // 创建相机将输出帧的surface,这里有两个地方 预览页面和 imageReader
    initPreviewImageReader()
    val targets = listOf(surfaceView.holder.surface, previewImageReader.surface)
    // 创建 session,注意这时候 传入的是两个Surface 一个是预览的,一个是编码
    session = createCaptureSession(camera, targets, cameraHandler)

    //创建一个 预览请求,看到这时候 传入的只有SurfaceView 的Surface对象,编码请求没有用到
    val captureRequest = camera.createCaptureRequest(
        CameraDevice.TEMPLATE_PREVIEW
    ).apply { addTarget(surfaceView.holder.surface) }

    // 这将保持尽可能频繁地发送捕获请求,直到会话中断或调用session.stoprepeat ()
    session.setRepeatingRequest(captureRequest.build(), null, cameraHandler)
}
  1. 录制的时候,通过session 发送一个新的请求
scss 复制代码
val captureRequest = camera.createCaptureRequest(
    CameraDevice.TEMPLATE_RECORD
).apply {
    addTarget(surfaceView.holder.surface)
    addTarget(previewImageReader.surface)
    set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO)
}

// 发送请求
session.setRepeatingRequest(captureRequest.build(), null, cameraHandler)
  1. 开启一个线程不断从解码器拿数据写入MediaMuxer
kotlin 复制代码
private fun startEncodeThread() {
    Thread {
       while (isRecord){
           encodeH264()
       }
        mediaCodec.stop()
        mediaCodec.release()
        mediaMuxer.stop()
        mediaMuxer.release()
    }.start()
}

private var mVideoTrackIndex = -1
private fun encodeH264() {
    val videoBufferInfo = MediaCodec.BufferInfo()
    var videobufferindex: Int = mediaCodec.dequeueOutputBuffer(videoBufferInfo, 0)
    if (videobufferindex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
        //添加轨道
        mVideoTrackIndex = mediaMuxer.addTrack(mediaCodec.outputFormat)
        mediaMuxer.start()
    } else {
            while (videobufferindex >= 0 && isRecord) {
                //获取输出数据成功
                val videoOutputBuffer: ByteBuffer =
                    mediaCodec.getOutputBuffer(videobufferindex)!!
                mediaMuxer.writeSampleData(mVideoTrackIndex, videoOutputBuffer, videoBufferInfo)
                mediaCodec.releaseOutputBuffer(videobufferindex, false)
                videobufferindex = mediaCodec.dequeueOutputBuffer(videoBufferInfo, 0)
            }
        }



}
  1. 从ImageReader 拿到的数据进行编码
kotlin 复制代码
private fun encodeVideo(nv21: ByteArray) {
    if (!isRecord) return
    val index: Int = mediaCodec.dequeueInputBuffer(WAIT_TIME)
    if (index >= 0) {
        val inputBuffer: ByteBuffer? = mediaCodec.getInputBuffer(index)
        inputBuffer?.clear()
        inputBuffer?.put(nv21, 0, nv21.size)
        mediaCodec.queueInputBuffer(
            index, 0, nv21.size, (System.nanoTime() - nanoTime) / 1000, 0
        )
    }
}
  1. 在编码之前,需要对YUV数据进行转换,因为Camera2 的YUV是YUV420_888 而 MediaCodec 需要的是 NV21,所以需要处理一下。在这里我在网上找到了一个YUV420_888转 YUV420P,YUV420SP,NV21的代码
ini 复制代码
public class ImageUtil {
    public static final int YUV420P = 0;
    public static final int YUV420SP = 1;// 就是NV12
    public static final int NV21 = 2;//NV21
    private static final String TAG = "ImageUtil";

    /***
     * 此方法内注释以640*480为例
     * 未考虑CropRect的
     */
    public static byte[] getBytesFromImageAsType(Image image, int type) {
        try {
            //获取源数据,如果是YUV格式的数据planes.length = 3
            //plane[i]里面的实际数据可能存在byte[].length <= capacity (缓冲区总大小)
            final Image.Plane[] planes = image.getPlanes();

            //数据有效宽度,一般的,图片width <= rowStride,这也是导致byte[].length <= capacity的原因
            // 所以我们只取width部分
            int width = image.getWidth();
            int height = image.getHeight();

            //此处用来装填最终的YUV数据,需要1.5倍的图片大小,因为Y U V 比例为 4:1:1
            byte[] yuvBytes = new byte[width * height * ImageFormat.getBitsPerPixel(ImageFormat.YUV_420_888) / 8];
            //目标数组的装填到的位置
            int dstIndex = 0;

            //临时存储uv数据的
            byte uBytes[] = new byte[width * height / 4];
            byte vBytes[] = new byte[width * height / 4];
            int uIndex = 0;
            int vIndex = 0;

            int pixelsStride, rowStride;
            for (int i = 0; i < planes.length; i++) {
                pixelsStride = planes[i].getPixelStride();
                rowStride = planes[i].getRowStride();

                ByteBuffer buffer = planes[i].getBuffer();

                //如果pixelsStride==2,一般的Y的buffer长度=640*480,UV的长度=640*480/2-1
                //源数据的索引,y的数据是byte中连续的,u的数据是v向左移以为生成的,两者都是偶数位为有效数据
                byte[] bytes = new byte[buffer.capacity()];
                buffer.get(bytes);

                int srcIndex = 0;
                if (i == 0) {
                    //直接取出来所有Y的有效区域,也可以存储成一个临时的bytes,到下一步再copy
                    for (int j = 0; j < height; j++) {
                        System.arraycopy(bytes, srcIndex, yuvBytes, dstIndex, width);
                        srcIndex += rowStride;
                        dstIndex += width;
                    }
                } else if (i == 1) {
                    //根据pixelsStride取相应的数据
                    for (int j = 0; j < height / 2; j++) {
                        for (int k = 0; k < width / 2; k++) {
                            uBytes[uIndex++] = bytes[srcIndex];
                            srcIndex += pixelsStride;
                        }
                        if (pixelsStride == 2) {
                            srcIndex += rowStride - width;
                        } else if (pixelsStride == 1) {
                            srcIndex += rowStride - width / 2;
                        }
                    }
                } else if (i == 2) {
                    //根据pixelsStride取相应的数据
                    for (int j = 0; j < height / 2; j++) {
                        for (int k = 0; k < width / 2; k++) {
                            vBytes[vIndex++] = bytes[srcIndex];
                            srcIndex += pixelsStride;
                        }
                        if (pixelsStride == 2) {
                            srcIndex += rowStride - width;
                        } else if (pixelsStride == 1) {
                            srcIndex += rowStride - width / 2;
                        }
                    }
                }
            }

            image.close();

            //根据要求的结果类型进行填充
            switch (type) {
                case YUV420P:
                    System.arraycopy(uBytes, 0, yuvBytes, dstIndex, uBytes.length);
                    System.arraycopy(vBytes, 0, yuvBytes, dstIndex + uBytes.length, vBytes.length);
                    break;
                case YUV420SP:
                    for (int i = 0; i < vBytes.length; i++) {
                        yuvBytes[dstIndex++] = uBytes[i];
                        yuvBytes[dstIndex++] = vBytes[i];
                    }
                    break;
                case NV21:
                    for (int i = 0; i < vBytes.length; i++) {
                        yuvBytes[dstIndex++] = vBytes[i];
                        yuvBytes[dstIndex++] = uBytes[i];
                    }
                    break;
            }
            return yuvBytes;
        } catch (final Exception e) {
            if (image != null) {
                image.close();
            }
            Log.i(TAG, e.toString());
        }
        return null;
    }
}

在代码中我本来是想转NV21的,结果发现颜色不对,试着转 YUV420SP(也就是NV12)结果正常了,我试着找了很多资料,结果都没有找到,都说 当MediaCodec 指定为 MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible 支持的是NV21 ,这个问题先留着,有知道的小伙伴可以在评论区告诉我一下,谢谢了。

YUV的一点相关知识

首先了解YUV是什么? YUV 是一种常用的颜色编码系统,主要用于图像和视频处理。它将颜色信息分成亮度(Y)和色度(U、V)两个分量,以提高压缩效率和减少数据传输量。

YUV 是一种通过对 RGB(红绿蓝)颜色模型进行转换得到的颜色空间。其中,Y 分量表示图像的亮度(明亮度)信息,用于描述灰度级别。而 U 和 V 分量则表示颜色差信息,用于描述色彩信息。

  • Y 分量:亮度分量,表示像素的明亮度。在黑白图像中,只有 Y 分量,每个像素都有一个与之对应的 Y 值来表示其亮度。
  • U 和 V 分量:色度分量,用于表示像素的颜色信息。通过 U 和 V 分量的组合,可以对 RGB 颜色空间进行编码。

YUV 色彩空间的优点之一是可以有效地压缩色彩信息。由于人眼对亮度更为敏感,而对色彩变化不太敏感,因此在视频编码和传输中,可以对亮度分量进行高质量的传输,而对色度分量进行较低质量的传输。这样就可以实现对图像和视频进行高效的压缩存储和传输,同时保持较好的视觉质量。

需要注意的是,YUV 并不是一种具体的图像格式,而是一种颜色空间。在实际应用中,常见的 YUV 图像格式包括 YUV420、YUV422、YUV444 等,它们具体描述了 Y、U、V 分量的采样率和排列方式。

我们用得最多的就是YUV420 了,而YUV420 又分为 YUV420P 和YUV420SP,他们的区别也很简单,首先前面的是Y,后面是UV,UV不同的排列就出现了以下几种类型:

  • 先排U再排V,就是 YUV420P 中的YU12
  • 先排V再排U,就是YUV420P中的YV12
  • UV交替排序,就是YUV420sp中的NV12
  • VU交替排序,就是YUV420sp中的NV21

使用Camera2遇到的一些问题

出现绿色滤镜或蓝色滤镜或是花屏等,一般都是宽高的问题,这时候需要调整一下 imageReader 和解码器指定的宽高才可以。

要是出现播放画面是正常的,只是颜色不对,一般就是YUV转换的问题,Camera2生产出来的数据是我们构建ImageReader 的时候指定的。

arduino 复制代码
previewImageReader = ImageReader.newInstance(
    previewSize.width, previewSize.height, ImageFormat.YUV_420_888, 1
)

有很多种类型,但开始我想着用NV21 结果直接闪退了。 我们在创建编码器的时候,也要指明类型:

markdown 复制代码
videoFormat.setInteger(
    MediaFormat.KEY_COLOR_FORMAT,
    MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible
)

也有好多类型,但一般我们选择的是MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible

总结

  • 本章简单的介绍了Camera2 的简单使用,实现 预览、拍照、录制视频、视频编码,希望可以帮助到小伙伴们快速上手Camera2。
  • 有段时间没有写博客了,结构上可能还有乱,章节也有点长,之后再改进一下。
  • 做音视频开发也有一段时间了,接下来也慢慢的把音视频相关的知识点慢慢的写成博客,可以和小伙伴一起学习一起进步
相关推荐
在狂风暴雨中奔跑7 天前
Android+FFmpeg+x264重编码压缩你的视频
音视频开发
音视频牛哥12 天前
[2015~2024]SmartMediaKit音视频直播技术演进之路
音视频开发·视频编码·直播
音视频牛哥14 天前
Windows平台Unity3D下RTMP播放器低延迟设计探讨
音视频开发·视频编码·直播
音视频牛哥14 天前
Windows平台Unity3D下如何低延迟低资源占用播放RTMP或RTSP流?
音视频开发·视频编码·直播
音视频牛哥14 天前
Android平台GB28181设备接入模块动态文字图片水印技术探究
音视频开发·视频编码·直播
陈年15 天前
纯前端视频剪辑
音视频开发
声知视界16 天前
音视频基础能力之 Android 音频篇 (三):高性能音频采集
android·音视频开发
音视频牛哥19 天前
RTSP摄像头8K超高清使用场景探究和播放器要求
音视频开发·视频编码·直播
音视频牛哥19 天前
RTMP如何实现毫秒级延迟体验?
音视频开发·视频编码·直播
哔哩哔哩技术21 天前
WASM 助力 WebCodecs:填补解封装能力的空白
音视频开发