快捷搜索:  汽车  科技

camera开源项目(七爪源码使用CameraX)

camera开源项目(七爪源码使用CameraX)val permissionState = rememberMultiplePermissionsState( permissions = listOf( Manifest.permission.CAMERA Manifest.permission.RECORD_AUDIO ) ) LaunchedEffect(Unit) { permissionState.launchMultiplePermissionRequest() } PermissionsRequired( multiplePermissionsState = permissionState permissionsNotGrantedContent = { /* ... */ } permissionsNotAvailableConten

Jetpack Compose CameraX

camera开源项目(七爪源码使用CameraX)(1)

考虑创建相机应用程序还是需要在应用程序中录制视频? CameraX 库是一个很好的方法。 今天,我将向您解释如何使用 Google 推荐的 CameraX 库创建相机应用程序。

“CameraX 是一个 Jetpack 库,旨在帮助简化相机应用程序的开发。”

您可以在以下几个用例中使用 CameraX:

  • 图像捕获 - 保存图像
  • 视频捕获 - 保存视频和音频
  • 预览 - 在显示器上查看图像
  • 图像分析 - 无缝访问缓冲区以用于您的算法

在本文中,我们将介绍视频捕获,因为它不是那么受欢迎的主题。

视频截取

首先,让我们添加一些依赖项:

// CameraX cameraxVersion = '1.2.0-beta01' implementation "androidx.camera:camera-lifecycle:$cameraxVersion" implementation "androidx.camera:camera-video:$cameraxVersion" implementation "androidx.camera:camera-view:$cameraxVersion" implementation "androidx.camera:camera-extensions:$cameraxVersion" // Accompanist accompanistPermissionsVersion = '0.23.1' implementation "com.google.accompanist:accompanist-permissions:$accompanistPermissionsVersion"

现在,我们的主屏幕将录制视频,但首先,我们需要请求摄像头和音频权限。 正如我在之前的一篇文章中已经解释的那样,不会详细介绍,如果您需要更多解释,请查看。

val permissionState = rememberMultiplePermissionsState( permissions = listOf( Manifest.permission.CAMERA Manifest.permission.RECORD_AUDIO ) ) LaunchedEffect(Unit) { permissionState.launchMultiplePermissionRequest() } PermissionsRequired( multiplePermissionsState = permissionState permissionsNotGrantedContent = { /* ... */ } permissionsNotAvailableContent = { /* ... */ } ) { // Rest of the compose code will be here }

现在我们将创建几个录制视频所需的对象。

val context = LocalContext.current val lifecycleOwner = LocalLifecycleOwner.current var recording: Recording? = remember { null } val previewView: PreviewView = remember { PreviewView(context) } val videoCapture: MutableState<VideoCapture<Recorder>?> = remember { mutableStateOf(null) } val recordingStarted: MutableState<Boolean> = remember { mutableStateOf(false) } val audioEnabled: MutableState<Boolean> = remember { mutableStateOf(false) } val cameraSelector: MutableState<CameraSelector> = remember { mutableStateOf(CameraSelector.DEFAULT_BACK_CAMERA) } LaunchedEffect(previewView) { videoCapture.value = context.createVideoCaptureUseCase( lifecycleOwner = lifecycleOwner cameraSelector = cameraSelector.value previewView = previewView ) }

Recording 是一个允许我们控制当前活动记录的对象。它将允许我们停止、暂停和恢复当前录制。我们在开始录制时创建该对象。

PreviewView 是一个自定义视图,将显示相机源。我们将它绑定到生命周期,将它添加到 AndroidView 中,它会向我们展示我们当前正在录制的内容。

VideoCapture 是一个通用类,它提供适用于视频应用程序的摄像头流。这里我们传递了 Recorder 类,它是 VideoOutput 接口的实现,它允许我们开始录制。

recordingStarted 和 audioEnabled 是我们将在此屏幕中使用的辅助变量,我认为它们几乎是不言自明的。

CameraSelector 是一组要求和优先级,用于选择相机或返回一组过滤的相机。在这里,我们将只使用默认的前后摄像头。

在 LaunchedEffect 中,我们调用了一个函数,该函数将为我们创建一个视频捕获用例。该函数如下所示:

suspend fun Context.createVideoCaptureUseCase( lifecycleOwner: LifecycleOwner cameraSelector: CameraSelector previewView: PreviewView ): VideoCapture<Recorder> { val preview = Preview.Builder() .build() .apply { setSurfaceProvider(previewView.surfaceProvider) } val qualitySelector = QualitySelector.from( Quality.FHD FallbackStrategy.lowerQualityOrHigherThan(Quality.FHD) ) val recorder = Recorder.Builder() .setexecutor(mainExecutor) .setQualitySelector(qualitySelector) .build() val videoCapture = VideoCapture.withOutput(recorder) val cameraProvider = getCameraProvider() cameraProvider.unbindAll() cameraProvider.bindToLifecycle( lifecycleOwner cameraSelector preview videoCapture ) return videoCapture }

首先,我们创建一个 Preview,它是一个提供用于在屏幕上显示的相机预览流的用例。我们可以在这里设置多个东西,比如纵横比、捕获处理器、图像信息处理器等等。我们不需要它们,所以我们创建普通的 Preview 对象。

接下来是选择我们视频的质量。为此,我们使用 QualitySelector 来定义所需的质量设置。我们想要全高清质量,所以我们将通过 Quality.FHD。有些手机的质量可能不理想,因此您应该始终有一个备份计划,就像我们在这里通过 FallbackStrategy 所做的那样。有几个策略:

  • HigherQualityOrLowerThan — 选择最接近并高于输入质量的质量。如果这不能产生受支持的质量,请选择最接近并低于输入质量的质量
  • HigherQualityThan - 选择最接近并高于输入质量的质量
  • lowerQualityOrHigherThan — 选择最接近并低于输入质量的质量。如果这不能产生受支持的质量,请选择最接近并高于输入质量的质量
  • lowerQualityThan - 选择最接近并高于输入质量的质量

另一种方法是通过 Quality.LOWEST 或 Quality.HIGHEST,这可能是更简单的方法,但我也想展示这个。

现在我们创建一个 Recorder 并使用它通过调用 VideoCapture.withOutput(recorder) 来获取 VideoCapture 对象。

相机提供者是 ProcessCameraProvider 单例的对象,它允许我们将相机的生命周期绑定到应用程序进程中的任何 LifecycleOwner。我们用来获取相机提供程序的函数是:

suspend fun Context.getCameraProvider(): ProcessCameraProvider = suspendCoroutine { continuation -> ProcessCameraProvider.getInstance(this).also { future -> future.addListener( { continuation.resume(future.get()) } mainExecutor ) } }

ProcessCameraProvider.getInstance(this) 正在返回我们需要等待完成以获取实例的未来。

接下来,我们需要将所有内容绑定到生命周期,并传递lifecycleOwner、cameraSelector、preview 和videoCapture。

现在是时候完成剩下的撰写代码了,我希望你还在我身边!

在 PermissionsRequired 内容块中,我们添加了 AndroidView 和用于录制的按钮。 像这样:

AndroidView( factory = { previewView } modifier = Modifier.fillMaxSize() ) IconButton( onClick = { if (!recordingStarted.value) { videoCapture.value?.let { videoCapture -> recordingStarted.value = true val mediaDir = context.externalCacheDirs.firstOrNull()?.let { File(it context.getString(R.string.app_name)).apply { mkdirs() } } recording = startRecordingVideo( context = context filenameFormat = "yyyy-MM-dd-HH-mm-ss-SSS" videoCapture = videoCapture outputDirectory = if (mediaDir != null && mediaDir.exists()) mediaDir else context.filesDir executor = context.mainExecutor audioEnabled = audioEnabled.value ) { event -> // Process events that we get while recording } } } else { recordingStarted.value = false recording?.stop() } } modifier = Modifier .align(Alignment.BottomCenter) .padding(bottom = 32.dp) ) { Icon( painter = painterResource(if (recordingStarted.value) R.drawable.ic_stop else R.drawable.ic_record) contentDescription = "" modifier = Modifier.size(64.dp) ) }

AndroidView 将显示我们的预览。

至于按钮,我们将使用它来开始和停止录制。 当我们要开始录制时,我们首先获取将放置视频的媒体目录,如果该目录不存在,我们只需创建它。 接下来是调用 startRecordingVideo 函数,如下所示:

fun startRecordingVideo( context: Context filenameFormat: String videoCapture: VideoCapture<Recorder> outputDirectory: File executor: Executor audioEnabled: Boolean consumer: Consumer<VideoRecordEvent> ): Recording { val videoFile = File( outputDirectory SimpleDateFormat(filenameFormat Locale.US).format(System.currentTimeMillis()) ".mp4" ) val outputOptions = FileOutputOptions.Builder(videoFile).build() return videoCapture.output .prepareRecording(context outputOptions) .apply { if (audioEnabled) withAudioEnabled() } .start(executor consumer) }

一个创建文件、准备录音并启动它的简单函数。 如果启用了音频,我们还将在启用音频的情况下开始录制。 此函数返回的对象,我们将使用它来停止录制。 消费者参数是一个回调,将在每个事件上调用。 您可以在视频录制完成后使用它来获取文件的 URI。

让我们添加音频和相机选择器的逻辑。

if (!recordingStarted.value) { IconButton( onClick = { audioEnabled.value = !audioEnabled.value } modifier = Modifier .align(Alignment.BottomStart) .padding(bottom = 32.dp) ) { Icon( painter = painterResource(if (audioEnabled.value) R.drawable.ic_mic_on else R.drawable.ic_mic_off) contentDescription = "" modifier = Modifier.size(64.dp) ) } } if (!recordingStarted.value) { IconButton( onClick = { cameraSelector.value = if (cameraSelector.value == CameraSelector.DEFAULT_BACK_CAMERA) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA lifecycleOwner.lifecycleScope.launch { videoCapture.value = context.createVideoCaptureUseCase( lifecycleOwner = lifecycleOwner cameraSelector = cameraSelector.value previewView = previewView ) } } modifier = Modifier .align(Alignment.BottomEnd) .padding(bottom = 32.dp) ) { Icon( painter = painterResource(R.drawable.ic_switch_camera) contentDescription = "" modifier = Modifier.size(64.dp) ) } }

它们是两个按钮,可以启用-禁用音频并在前后摄像头之间切换。 当我们在摄像机之间切换时,我们需要创建一个新的 videoCapture 对象来更改预览显示的内容。

这就是这个屏幕的内容,但现在很高兴看到我们录制的内容对不对? 当然,为此,我们将创建另一个屏幕并使用 ExoPlayer 显示视频。

让我们首先在我们的消费者回调中添加逻辑:

if (event is VideoRecordEvent.Finalize) { val uri = event.outputResults.outputUri if (uri != Uri.EMPTY) { val uriEncoded = URLEncoder.encode( uri.toString() StandardCharsets.UTF_8.toString() ) navController.navigate("${Route.VIDEO_PREVIEW}/$uriEncoded") } }

如果 event 是 VideoRecordEvent.Finalize,则表示录制完成,我们可以获取视频的 URI。 有几个视频记录事件,您可以使用其中任何一个,但在这里我们只需要 Finalize:

  • Start
  • Finalize
  • Status
  • Pause
  • Resume

如果视频太短,URI 可以为空,比如不到半秒或类似的东西,这就是我们需要 if 语句的原因。

应该对 URI 进行编码以将其作为导航参数传递。

这个屏幕的最终代码如下所示:

@OptIn(ExperimentalPermissionsApi::class) @Composable fun VideoCaptureScreen( navController: NavController ) { val context = LocalContext.current val lifecycleOwner = LocalLifecycleOwner.current val permissionState = rememberMultiplePermissionsState( permissions = listOf( Manifest.permission.CAMERA Manifest.permission.RECORD_AUDIO ) ) var recording: Recording? = remember { null } val previewView: PreviewView = remember { PreviewView(context) } val videoCapture: MutableState<VideoCapture<Recorder>?> = remember { mutableStateOf(null) } val recordingStarted: MutableState<Boolean> = remember { mutableStateOf(false) } val audioEnabled: MutableState<Boolean> = remember { mutableStateOf(false) } val cameraSelector: MutableState<CameraSelector> = remember { mutableStateOf(CameraSelector.DEFAULT_BACK_CAMERA) } LaunchedEffect(Unit) { permissionState.launchMultiplePermissionRequest() } LaunchedEffect(previewView) { videoCapture.value = context.createVideoCaptureUseCase( lifecycleOwner = lifecycleOwner cameraSelector = cameraSelector.value previewView = previewView ) } PermissionsRequired( multiplePermissionsState = permissionState permissionsNotGrantedContent = { /* ... */ } permissionsNotAvailableContent = { /* ... */ } ) { Box( modifier = Modifier.fillMaxSize() ) { AndroidView( factory = { previewView } modifier = Modifier.fillMaxSize() ) IconButton( onClick = { if (!recordingStarted.value) { videoCapture.value?.let { videoCapture -> recordingStarted.value = true val mediaDir = context.externalCacheDirs.firstOrNull()?.let { File(it context.getString(R.string.app_name)).apply { mkdirs() } } recording = startRecordingVideo( context = context filenameFormat = "yyyy-MM-dd-HH-mm-ss-SSS" videoCapture = videoCapture outputDirectory = if (mediaDir != null && mediaDir.exists()) mediaDir else context.filesDir executor = context.mainExecutor audioEnabled = audioEnabled.value ) { event -> if (event is VideoRecordEvent.Finalize) { val uri = event.outputResults.outputUri if (uri != Uri.EMPTY) { val uriEncoded = URLEncoder.encode( uri.toString() StandardCharsets.UTF_8.toString() ) navController.navigate("${Route.VIDEO_PREVIEW}/$uriEncoded") } } } } } else { recordingStarted.value = false recording?.stop() } } modifier = Modifier .align(Alignment.BottomCenter) .padding(bottom = 32.dp) ) { Icon( painter = painterResource(if (recordingStarted.value) R.drawable.ic_stop else R.drawable.ic_record) contentDescription = "" modifier = Modifier.size(64.dp) ) } if (!recordingStarted.value) { IconButton( onClick = { audioEnabled.value = !audioEnabled.value } modifier = Modifier .align(Alignment.BottomStart) .padding(bottom = 32.dp) ) { Icon( painter = painterResource(if (audioEnabled.value) R.drawable.ic_mic_on else R.drawable.ic_mic_off) contentDescription = "" modifier = Modifier.size(64.dp) ) } } if (!recordingStarted.value) { IconButton( onClick = { cameraSelector.value = if (cameraSelector.value == CameraSelector.DEFAULT_BACK_CAMERA) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA lifecycleOwner.lifecycleScope.launch { videoCapture.value = context.createVideoCaptureUseCase( lifecycleOwner = lifecycleOwner cameraSelector = cameraSelector.value previewView = previewView ) } } modifier = Modifier .align(Alignment.BottomEnd) .padding(bottom = 32.dp) ) { Icon( painter = painterResource(R.drawable.ic_switch_camera) contentDescription = "" modifier = Modifier.size(64.dp) ) } } } } }

ExoPlayer

ExoPlayer 是 Android 的 MediaPlayer API 的替代品,用于在本地和互联网上播放音频和视频。 它更易于使用并提供更多功能。 此外,它很容易定制和扩展。

现在,当我们知道什么是 ExoPlayer 后,让我们创建下一个屏幕。 添加依赖:

//ExoPlayer Library exoPlayerVersion = '2.18.1' implementation "com.google.android.exoplayer:exoplayer:$exoPlayerVersion"

我们的屏幕应该是这样的:

@Composable fun VideoPreviewScreen( uri: String ) { val context = LocalContext.current val exoPlayer = remember(context) { ExoPlayer.Builder(context).build().apply { setMediaItem(MediaItem.fromUri(uri)) prepare() } } DisposableEffect( Box( modifier = Modifier.fillMaxSize() ) { AndroidView( factory = { context -> StyledPlayerView(context).apply { player = exoPlayer } } modifier = Modifier.fillMaxSize() ) } ) { onDispose { exoPlayer.release() } } }

我们将使用构建器创建 ExoPlayer,设置将要加载的视频的 URI,然后准备播放器。

我们使用 AndroidView 来显示我们的视频,并将 StyledPlayerView 附加到它。

StyledPlayerView 是 Player 媒体播放的高级视图。 它在播放期间显示视频、字幕和专辑封面,并使用 StyledPlayerControlView 显示播放控件。

StyledPlayerView 可以通过设置属性(或者调用相应的方法),或者覆盖drawable来自定义。

这就是我们的录像机,我希望你在这篇文章中学到了一些新的东西并且你喜欢它。

关注七爪网,获取更多APP/小程序/网站源码资源!

猜您喜欢: