使用 CameraX 在 Jetpack Compose 中构建相机 Android 应用程序

这篇具有很好参考价值的文章主要介绍了使用 CameraX 在 Jetpack Compose 中构建相机 Android 应用程序。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

使用 CameraX 在 Jetpack Compose 中构建相机 Android 应用程序

使用 CameraX 在 Jetpack Compose 中构建相机 Android 应用程序
CameraX 是一个 Jetpack 库,旨在帮助简化相机应用程序的开发。

[camerax官方文档] https://developer.android.com/training/camerax

CameraX的几个用例:

  • Image Capture
  • Video Capture
  • Preview
  • Image analyze
    具体如何使用相关用例,请查看上面的官方链接。
    下面仅就视频录制用例来叙述相关实现流程。

视频录制

使用 CameraX 在 Jetpack Compose 中构建相机 Android 应用程序

  1. 添加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 接口的实现,它允许我们开始录制。

recordingStartedaudioEnabled 是辅助变量,我们将在该屏幕上使用它们,它们的含义应该很明显。

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定义所需的质量设置。我们希望使用全高清(Full HD)质量,因此我们将传递 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) 返回一个 Future,我们需要等待它完成以获取实例。

接下来,我们需要将所有内容绑定到生命周期,并传递 lifecycleOwnercameraSelectorpreview videoCapture

现在是时候完成其余的 Compose 代码了,希望您还在我身边!

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)
}

这是一个简单的函数,它创建一个文件,准备录制并开始录制。如果启用了音频,我们还将启用音频录制。该函数返回的对象将用于停止录制。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 对象来改变预览显示的内容。
使用 CameraX 在 Jetpack Compose 中构建相机 Android 应用程序

这就是该屏幕的全部内容,但是现在我们希望能够查看我们录制的内容,对吗?当然了,为此,我们将创建另一个屏幕并使用ExoPlayer来显示视频。

首先,让我们在 consumer 回调函数中添加逻辑:

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")
    }
}

如果事件是 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

使用 CameraX 在 Jetpack Compose 中构建相机 Android 应用程序

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 是用于播放器媒体播放的高级视图。它在播放期间显示视频、字幕和专辑封面,并使用 StyledPlayerControlView 显示播放控件。
StyledPlayerView 可以通过设置属性(或调用相应的方法)或覆盖绘图进行自定义。
使用 CameraX 在 Jetpack Compose 中构建相机 Android 应用程序

源码地址

https://github.com/Giga99/CameraApp文章来源地址https://www.toymoban.com/news/detail-456474.html

到了这里,关于使用 CameraX 在 Jetpack Compose 中构建相机 Android 应用程序的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Android Jetpack Compose中使用字段验证的方法

    数据验证是创建健壮且用户友好的Android应用程序的关键部分。随着现代UI工具包Jetpack Compose的引入,处理字段验证变得更加高效和直观。在这篇文章中,我们将探讨如何在Android应用中使用Jetpack Compose进行字段验证。 字段验证是确保用户在各种输入字段中输入的数据符合特定

    2024年02月11日
    浏览(31)
  • Android 实现相机(CameraX)预览

    CameraX 是一个 Jetpack 库,旨在帮助您更轻松地开发相机应用。 对于新应用,我们建议从 CameraX 开始。它提供一致且易于使用的 API,适用于绝大多数 Android 设备,并向后兼容 Android 5.0(API 级别 21)。 CameraX 支持大多数常见的相机用例: 预览 :在屏幕上查看图片。 图片分析 :

    2024年02月15日
    浏览(28)
  • Android相机调用-CameraX【外接摄像头】【USB摄像头】

    Android相机调用有原生的Camera和Camera2,我觉得调用代码都太复杂了,CameraX调用代码简洁很多。 说明文档:https://developer.android.com/jetpack/androidx/releases/camera?hl=zh-cn 现有查到的调用资料都不够新,对于外接摄像头(USB摄像头)这类非前置也非后置摄像头的设备调用,都说是没有实

    2024年02月09日
    浏览(36)
  • Android Jetpack Compose 别裁

    一、简介 二、compose优缺点 三、compose好学吗 四、Android Jetpack Compose 跟 fluter 哪个更好 五、Android Jetpack Compose 跟 fluter 技能学习选择 之所以叫Android Jetpack Compose别裁是希望能取舍网上的对compose的资料,推出别出心裁的文章,文章结束都会有一个案例,通过实践学习,让学习的

    2024年02月03日
    浏览(45)
  • Jetpack Compose: Hello Android

    Jetpack Compose 是一个现代化的工具包,用于使用声明式方法构建原生 Android UI。在本博文中,我们将深入了解一个基本的 “Hello Android” 示例,以帮助您开始使用 Jetpack Compose。我们将探讨所提供代码片段中使用的函数和注解。 在深入代码之前,请确保您已经准备好使用 Jetpac

    2024年03月10日
    浏览(42)
  • 在基于 Android 相机预览的 CV 应用程序中使用 OpenCL

    组装和配置 Android OpenCL SDK。 示例的 JNI 部分依赖于标准 Khornos OpenCL 标头,以及 OpenCL 和 libOpenCL.so 的C++包装器。标准 OpenCL 标头可以从 OpenCV 存储库中的第三方目录或 Linux 发行版包中复制。C++包装器在Github上的官方Khronos存储库中可用。按以下方式将头文件复制到专用目录:

    2024年01月21日
    浏览(38)
  • Android Jetpack Compose — Slider滑动条

            在Android Jetpack Compose中,Slider(滑动条)是一个常用的用户界面控件,它允许通过滑动条来选择一个范围或数值。Slider控件非常适用于调整音量、亮度、进度等需要连续调整的场景。 一、Slider的属性         Slider是Android Jetpack Compose中的一个控件,用于实现滑动条

    2024年02月11日
    浏览(31)
  • 探索Android Jetpack Compose的Surface组件

    随着声明性 UI 框架 Jetpack Compose 的出现,Android 开发变得更加简洁和直观。在这篇博客中,我们将深入探讨其中的一项基本构建块 —— Surface 组件,了解它如何影响 UI 的显示和设计。 一、Jetpack Compose和Surface组件 二、Surface组件的基本使用 三、影响Surface的属性 一、Jetpack Co

    2024年02月11日
    浏览(38)
  • 现代化 Android 开发:Jetpack Compose 最佳实践

    作者:古哥E下 如果一直关注 Compose 的发展的话,可以明显感受到 2022 年和 2023 年的 Compose 使用讨论的声音已经完全不一样了, 2022 年还多是观望,2023 年就有很多团队开始采纳 Compose 来进行开发了。不过也有很多同学接触了下 Compose,然后就放弃了。要么使用起来贼特么不顺手

    2024年02月17日
    浏览(47)
  • 对于Android开发,我们为何要学Jetpack Compose?

    Jetpack Compose 是用于构建原生 Android 界面的新工具包。它可简化并加快 Android 上的界面开发,使用更少的代码、强大的工具和直观的 Kotlin API,快速让应用生动而精彩。Compose 使用全新的组件——可组合项 (Composable) 来布局界面,使用修饰符 (Modifier) 来配置可组合项。 为何Jetp

    2024年02月10日
    浏览(30)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包