在Compose中使用camerax进行拍照和录视频

ModelEngine·创作计划征文活动 10w+人浏览 498人参与

camerax中提供了camera-compose库,用于在compose中使用camerax来拍照和录视频。该库提供了一个CameraXViewfinder组件,这是一个可组合的适配器,它通过完成提供的SurfaceRequest来显示来自CameraX的帧。它是一个Viewfinder的封装器,它会在内部将CameraX SurfaceRequest转换为ViewfinderSurfaceRequest。此外,所有通常通过ViewfinderSurfaceRequest处理的交互都将从SurfaceRequest派生而来。

配置相关依赖

在gradle中配置camerax相关的依赖

dependencies {
  implementation("androidx.camera:camera-core:1.5.1")
  implementation("androidx.camera:camera-camera2:1.5.1")
  implementation("androidx.camera:camera-compose:1.5.1")
  implementation("androidx.camera:camera-lifecycle:1.5.1")
  implementation("androidx.camera:camera-video:1.5.1")
}

core库提供了camerax的核心,camera2是camerax的具体实现,compose库提供了CameraXViewfinder组件,lifecycle用于管理生命周期,video用于录制视频。

权限申请

拍摄照片需要相机权限,如果是录视频,还需要录音权限。先在AndroidManifest中申明,然后在使用的地方动态申请。

<uses-permission android:name="android.permission.CAMERA" />
  <uses-permission android:name="android.permission.RECORD_AUDIO" />

动态申请权限

val launcher = rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {}
launcher.launch(arrayOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO))

CameraXViewfinder绑定相机并预览画面

CameraXViewfinder的使用非常简单,只需要传入一个SurfaceRequest参数就可以了。

@Composable
fun cameraLayout() {
  CameraXViewfinder(
    surfaceRequest = surfaceRequest,
    modifier = Modifier.fillMaxSize()
  )
}

surfaceRequest的获取需要绑定相机,并在预览中获取。首先使用ProcessCameraProvider将摄像头的生命周期绑定到应用程序进程内的任何LifecycleOwner上,一个进程中只能存在一个进程摄像头提供。程序重量级资源(例如已打开并正在运行的摄像头设备)的作用域将限定在bindToLifecycle提供的生命周期内。其他轻量级资源(例如静态摄像头特性)可以在首次使用getInstance检索此提供程序时被检索并缓存,并在进程的整个生命周期内保持有效。示例如下

fun cameraLayout() {
  val context = LocalContext.current
  val lifecycleOwner = LocalLifecycleOwner.current
  var surfaceRequest by remember { mutableStateOf<SurfaceRequest?>(null) }
  LaunchedEffect(Unit) {
    val group = UseCaseGroup.Builder().addUseCase(Preview.Builder().build().apply {
      setSurfaceProvider {
        surfaceRequest = it
      }
    }).build()
    val processCameraProvider = ProcessCameraProvider.awaitInstance(context)
    processCameraProvider.bindToLifecycle(lifecycleOwner, CameraSelector.DEFAULT_BACK_CAMERA, group).also {
      try {
        awaitCancellation()
      } finally {
        processCameraProvider.unbindAll()
      }
    }
  }
  surfaceRequest?.also {
    CameraXViewfinder(
      surfaceRequest = it,
      modifier = Modifier.fillMaxSize()
    )
  }
}

UseCaseGroup用于管理一组用例集合,当用例组绑定到生命周期时,它会将所有用例绑定到同一个生命周期。用例组内的用例通常共享一些公共属性,例如由视口定义的视野范围。camerax使用不同的用例来管理相机,例如提供相机预览能力的Preview用例,用于拍照输出的ImageCapture用例,提供视图窗口的ViewPort用例,以及提供录制视频能力的VideoCapture用例。最后processCameraProvider调用bindToLifecycle绑定生命周期,这里使用CameraSelector.DEFAULT_BACK_CAMERA后置摄像头进行预览。

使用ImageCapture进行拍照

要进行拍照,首先需要在UseCaseGroup中添加ImageCapture用例,然后使用ImageCapture进行拍照,示例如下

class CameraViewModel(application: Application) : AndroidViewModel(application) {
  private var imageCapture: ImageCapture? = null
  var cameraControl: CameraControl? = null
    private set

  suspend fun bindToCamera(lifecycleOwner: LifecycleOwner, surface: Preview.SurfaceProvider) {
    val processCameraProvider = ProcessCameraProvider.awaitInstance(application)
    val group = UseCaseGroup.Builder().addUseCase(Preview.Builder().build().apply {
      surfaceProvider = surface
    }).addUseCase(ImageCapture.Builder().build().also { imageCapture = it }).build()
    processCameraProvider.bindToLifecycle(lifecycleOwner, CameraSelector.DEFAULT_BACK_CAMERA, group).also {
      cameraControl = it.cameraControl
      try {
        awaitCancellation()
      } finally {
        processCameraProvider.unbindAll()
        cameraControl = null
      }
    }
  }

  fun takePhoto() {
    val imageCapture = imageCapture ?: return
    val name = SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault()).format(System.currentTimeMillis())
    val dir = File(application.cacheDir, Environment.DIRECTORY_PICTURES)
    dir.takeUnless { it.exists() }?.mkdirs()
    val options = ImageCapture.OutputFileOptions.Builder(File(dir, "DIC_${name}.jpg"))
      .setMetadata(ImageCapture.Metadata()).build()
    imageCapture.takePicture(options, CameraXExecutors.ioExecutor(), object : ImageCapture.OnImageSavedCallback {
        override fun onImageSaved(output: ImageCapture.OutputFileResults) {
          val path = output.savedUri?.toString()
          Log.i("camera","照片路径:$path")
        }
    })
  }
}

就这么几行代码,传入照片路径,就能进行拍照了。在options中,我们可以通过setMetadata给照片添加无数据,例如设置位置,是否水平或垂直翻转等。

拍照时设置闪光灯缩放和聚焦

设置闪光灯比较简单,在前面绑定生命周期后,可以获取到CameraControl对象,直接通过CameraControl对象的flashMode属性就可以设置闪光灯模式,例如开启闪光灯

//关闭闪光灯:ImageCapture.FLASH_MODE_OFF
//开启闪光灯:ImageCapture.FLASH_MODE_ON
//设置自动模式:ImageCapture.FLASH_MODE_AUTO
imageCapture?.flashMode = ImageCapture.FLASH_MODE_ON

缩放和聚焦涉及到CameraXViewfinder的手势处理,获取点击位置进行聚焦,以及双手捏合进行缩放处理。

@Composable
fun cameraLayout() {
  var cameraZoom by remember { mutableStateOf(1F) }
  var surfaceRequest by remember { mutableStateOf<SurfaceRequest?>(null) }
  val coordinateTransformer = remember { MutableCoordinateTransformer() }
  surfaceRequest?.also { request ->
    CameraXViewfinder(
      surfaceRequest = request,
      modifier = Modifier.fillMaxSize()
        .pointerInput(Unit) {
          detectTapGestures {
            with(coordinateTransformer) {
              it.transform()
            }.let {
              SurfaceOrientedMeteringPointFactory(
                request.resolution.width.toFloat(),
                request.resolution.height.toFloat()
              ).createPoint(it.x, it.y)
            }.also {
              val action = FocusMeteringAction.Builder(it, FocusMeteringAction.FLAG_AF.or(FocusMeteringAction.FLAG_AE))
                .setAutoCancelDuration(3, TimeUnit.SECONDS).build()
              viewModel.cameraControl?.startFocusAndMetering(action)?.addListener({ }, CameraXExecutors.ioExecutor())
            }
          }
        }
        .pointerInput(Unit) {
          detectTransformGestures { _, _, zoom, _ ->
            cameraZoom *= zoom
            viewModel.cameraControl?.setZoomRatio(cameraZoom)
          }
        },
      coordinateTransformer = coordinateTransformer
    )
  }
}

处理照片与预览范围不一致

当我们浏览本地拍摄的照片时,会发现照片的实现范围可能会比预览时的范围大,如果没有特殊要求,这个差别可能不会有什么影响,但如果我们需要对拍摄的照片进行处理,比如加水印,或进行裁剪,影响就比较大了,会出现水印位置不对,裁剪位置不对等问题。其实处理这些问题非常简单,根本原因是拍摄的宽度比与预览的宽高比不一致导致的。我们可以在UseCaseGroup中添加视图窗口,告诉相机我们的预览尺寸是多大,这样拍摄出来的照片,会根据我们提供的视图窗口比例进行裁剪。

suspend fun bindToCamera(lifecycleOwner: LifecycleOwner, viewSize: Size, surface: Preview.SurfaceProvider) {
  val processCameraProvider = ProcessCameraProvider.awaitInstance(application)
  val group = UseCaseGroup.Builder().addUseCase(Preview.Builder().build().apply {
    setSurfaceProvider{}
    surfaceProvider = surface
  }).addUseCase(ImageCapture.Builder().setFlashMode(flashMode).build().also { imageCapture = it })
    .setViewPort(ViewPort.Builder(Rational(viewSize.width.toInt(), viewSize.height.toInt()), Surface.ROTATION_0).build()
    ).build()
  processCameraProvider.bindToLifecycle(lifecycleOwner, CameraSelector.DEFAULT_BACK_CAMERA, group).also {
    cameraControl = it.cameraControl
    try {
      awaitCancellation()
    } finally {
      processCameraProvider.unbindAll()
      cameraControl = null
    }
  }
}

使用VideoCapture录制视频

在camerax中录制视频也非常简单,首先在UseCaseGroup中添加VideoCapture用例,然后用VideoCapture进行录制。需要注意的是录制视频不仅需要相机权限,还需要录音权限。示例如下

class CameraViewModel(application: Application) : AndroidViewModel(application) {
  private var videoCapture: VideoCapture<Recorder>? = null
  private val tag = javaClass.simpleName
  
  suspend fun bindToCamera(lifecycleOwner: LifecycleOwner, viewSize: Size, surface: Preview.SurfaceProvider) {
    val processCameraProvider = ProcessCameraProvider.awaitInstance(application)
    val group = UseCaseGroup.Builder().addUseCase(Preview.Builder().build().apply {
      setSurfaceProvider{}
      surfaceProvider = surface
    }).addUseCase(ImageCapture.Builder().setFlashMode(flashMode).build().also { imageCapture = it })
      .addUseCase(VideoCapture.withOutput(Recorder.Builder().setQualitySelector(QualitySelector.from(Quality.FHD)).build()).also {
        videoCapture = it
      })
      .setViewPort(ViewPort.Builder(Rational(viewSize.width.toInt(), viewSize.height.toInt()), Surface.ROTATION_0).build()
      ).build()
    processCameraProvider.bindToLifecycle(lifecycleOwner, CameraSelector.DEFAULT_BACK_CAMERA, group).also {
      cameraControl = it.cameraControl
      try {
        awaitCancellation()
      } finally {
        processCameraProvider.unbindAll()
        cameraControl = null
      }
    }
  }
  fun startOrStopRecordVideo() {
    if (recording == null) {
      recording = videoCapture?.takeIf { checkPermission(Manifest.permission.RECORD_AUDIO) }?.let { capture ->
        val name = SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault()).format(System.currentTimeMillis())
        val dir = File(application.cacheDir, Environment.DIRECTORY_MOVIES).also { it.takeUnless { it.exists() }?.mkdirs() }
        capture.output.prepareRecording(application, FileOutputOptions.Builder(File(dir, "${name}.mp4")).build()).withAudioEnabled()
          .start(CameraXExecutors.ioExecutor()) {
            when (it) {
              is VideoRecordEvent.Start -> Log.w(tag, "开始录制")
              is VideoRecordEvent.Finalize -> {
                if (it.hasError()) {
                  Log.w(tag, "录制失败: ${it.cause?.message}")
                } else {
                  Log.i(tag, "结束录制: ${it.outputResults.outputUri}")
                }
              }
              is VideoRecordEvent.Status -> {
                Log.w(tag, "正在录制")
              }
            }
          }
      }
    } else {
      recording?.stop()
      recording = null
    }
  }
}

我们可以通过VideoRecordEvent来获取视频的录制信息,例如当状态为VideoRecordEvent.Status正在录制时,获取录制的时间,以及录制的文件大小

//录制的时间,单位毫秒
val second = it.recordingStats.recordedDurationNanos / 1000000
//视频文件大小,单位MB
val size = it.recordingStats.numBytesRecorded / 1024 / 1024

在这里插入图片描述
在这里插入图片描述
完整源码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值