百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

CameraX 在 Jetpack Compose 中构建相机 Android 应用_camera camera.

toyiye 2024-04-03 23:10 51 浏览 0 评论

Jetpack Compose + CameraX

考虑创建相机应用程序还是需要在应用程序中录制视频? 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/小程序/网站源码资源!

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码