Compose使用OpenGL+CameraX快速实现相机“拍视频实时滤镜“、”拍照+滤镜“

news2024/11/17 22:25:51

一、前言

短视频热潮还没有褪去,写这篇文章主要是帮助大部分人,能快速上手实现类似效果,实际上是: CameraX拿相机数据,OpenGL给CameraX提供一个Surface,数据放到OpenGL渲染的线程上去做图像相关操作

OpenGL滤镜和录制视频核心代码一部分来自
https://github.com/aserbao/AndroidCamera

其实它里面用了Googlegrafika里面的视频录制的核心代码
https://github.com/google/grafika ,感兴趣的小伙伴,可以细读哦。

注意:文章末尾会贴上本篇文章的最终源代码地址。


GIF压缩失真了,请前往文章末尾下载源码体验

网上有很多,将解OpenGL的文章内容,同样的也很少有文章教大家如何去快速集成实现,大多数文章,上来就是一大篇和别的文章雷同的基础知识点讲解,容易看睡着,对于没有这方面基础的同学很难啃。

二、CameraX拍照的问题

1. CameraX去拍照片:默认图片质量大(可调整),但是:拍照数据返回

2. 没有什么用的CameraX拓展:这玩意并不适合我们,没有这个拓展就用不了,那要这个拓展有什么用。

3. 拍视频并不能ImageAnalysis做滤镜,拍照片可以用ImageAnalysis做滤镜效果

三、自定义GLTextureView

我编写一个GLCameraView方便支持我们的Activity动画打开。

因为我们要给CameraX提供surfaceProvider,所以我们需要实现SurfaceProvider接口

class GLCameraView(context: Context, private val ratio: Float) : GLTextureView(context), SurfaceProvider {
    override fun onSurfaceRequested(request: SurfaceRequest) {
        val resetTexture = resetPreviewTexture(request.resolution) ?: return
        val surface = Surface(resetTexture)
        request.provideSurface(surface, executor) {
            surface.release()
            if(!fromCameraLensFacing) {
                // 注意:切换前置和后置摄像头的时候,不能release surfaceTexture
                surfaceTexture?.release()
            }
        }
    }
    // 其他细节,可以直接用我们文章末尾的源码
}

根据方法提供的参数设置SurfaceTexture#setDefaultBufferSize,然后用surfaceTexture创建一个Surface,将它提供给SurfaceRequest

后面我们结合CameraX的时候就可以把GLCameraView传给Preview#setSurfaceProvider

四、CameraX + GLCameraView预览

1. 提供一个getCameraProvider()将相机的生命周期绑定到 LifecycleOwner

private suspend fun Context.getCameraProvider(): ProcessCameraProvider =
    suspendCoroutine { continuation ->
        ProcessCameraProvider.getInstance(this).also { cameraProvider ->
            cameraProvider.addListener(
                {
                  continuation.resume(cameraProvider.get())
                }, ContextCompat.getMainExecutor(this)
            )
        }
    }

2. 配置相机视图

@Composable
fun CameraXView(
    modifier: Modifier,
    lensFacing: Int,
    content: @Composable (GLCameraView) -> Unit
) {
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current

    val glCameraPreview = remember {
        // 配置9:16
        GLCameraView(context, 9 / 16F)
    }
    val scope = remember {
        CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
    }
    LaunchedEffect(lensFacing) {
        val cameraProvider = context.getCameraProvider()
        // 解除生命周期绑定,并从 CameraX 中移除
        cameraProvider.unbindAll()

        val preview = Preview.Builder().apply {
            // 设置成9:16
            setTargetAspectRatio(AspectRatio.RATIO_16_9)
        }.build()

        // 需要在preview.setSurfaceProvider前面调用
        glCameraPreview.switchCameraLensFacing(true,lensFacing)
        preview.setSurfaceProvider(glCameraPreview)

        // 设置前置、后置摄像头切换
        val cameraSelector = CameraSelector.Builder()
            .requireLensFacing(lensFacing)
            .build()
            
        kotlin.runCatching {
            cameraProvider.bindToLifecycle(
                lifecycleOwner,
                cameraSelector,
                preview
            )
        }
    }
    Box(...) {
        DisposableEffect(
            AndroidView(
                factory = { glCameraPreview },
                modifier = Modifier.fillMaxSize()
            )
        ) {
            onDispose {
                scope.launch {
                   glCameraPreview.switchCameraLensFacing(false,lensFacing)
                   val cameraProvider = context.getCameraProvider()
                   // 解除生命周期绑定,并从 CameraX 中移除
                   cameraProvider.unbindAll()
                }
            }
        }
        content(glCameraPreview)
    }
}

从上面的代码片段中,我们可以看到以下内容:

  1. 初始化ProcessCameraProvider的实例,将相机的生命周期绑定到LifecycleOwner
  2. LaunchedEffect(lensFacing) 只有切换前置/后置摄像头会触发里面的逻辑,需要调用cameraProvider.unbindAll() 解除生命周期绑定,并从 CameraX 中移除。
  3. 初始化Preview设置surfaceProvider,这里需要注意的是调用切换摄像头方向的方法需要在setSurfaceProvider前面调用。
  4. 通过CameraSelector构建切换的前置/后置摄像头。
  5. 调用cameraProvider.bindToLifecycle
  6. AndroidViewfactory里面返回我们的glCameraPreview
  7. onDispose里面我们需要调用cameraProvider.unbindAll()解除生命周期绑定,并从 CameraX 中移除。

五、CameraXView三方app如何使用

上面我们给出了CameraXView的实现,我们在content: @Composable (GLCameraView) -> Unit 这里面可以放我们的拍照/拍视频/其他菜单视图,如果是拍视频,需要控制slideGpuFilterGroupEnable,没有拍摄视频前,可以水平切换滤镜,拍摄中是不可以切换的。

1. 拍摄视频用法:

CameraXView(
    modifier = modifier,
    lensFacing = lensFacingProvider()
) { glCameraPreview ->
    LaunchedEffect(recordingStateProvider()) {
       if (null != recordingStateProvider()) {
          // 拍视频
          glCameraPreview.takeVideo(videoRecordingFileUri?.path ?: "")
       }
       // 为null的状态,才可以滑动切换滤镜
       glCameraPreview.slideGpuFilterGroupEnable = (recordingStateProvider() == null)
   }
   
   // TakeVideoOptionsBar里面显示的视频拍摄的时间
   val recordingTimeState = glCameraPreview.getRecordingTimeState().collectAsState()

   TakeVideoOptionsBar(
       // 用于控制里面按钮状态
       recordingState = recordingStateProvider(),
       // 视频拍摄时长(实时的,单位:分钟:秒:毫秒)
       recordingTime = recordingTimeState.value,
       onTakeVideo = {
           // 回调当前状态,再根据viewModel返回的state去做ui数据展示
           onTakeVideoClick.invoke(glCameraPreview.isRecording())
       },
       ....
    )
}

2. 拍照片用法:

CameraXView(
  modifier = modifier,
  lensFacing = lensFacingProvider()
) { glCameraPreview ->
    TakePhotoOptionsBar(
        onTakePhoto = {
            // OpenGL里面去取一帧图片数据,这个数据是秒生成的
            glCameraPreview.takePicture(takePictureFileUri?.path ?: "")
            // 直接通知ViewModel播放一个“快门声音”,并打开需要接收照片数据的页面
            currentOnTakePhotoClick.invoke()
        }
    )
}

这里再插一段,怎么播放快门声音,这里给大家介绍一下:
Ringtone
提供了一种快速播放铃声、通知或其他类似类型声音的方法,我们可以看到官方文档里面提示我们去看RingtoneManager,当然,你也可以用SoundPool

我们在RingtoneManager找到getRingtone可以根据音频文件的Uri返回一个Ringtone,然后可以拿Ringtone#play去播放一段音频,还没结束,我们还需要配置一下音频属性AudioAttributes:

AudioAttributes.Builder().setUsage(...).build()

我们可以看到AudioAttributes.Builder#setUsage,有下面这么多usage可配置

篇幅问题,更多细节,大家可以仔细查看官方的文档,这里我们用的是:AudioAttributes.USAGE_MEDIA

当使用是媒体(如音乐或电影配乐)时使用的值

所以:只要我们的设备播放音乐或者视频能听到声音,那么我们这个快门播放也同样可以听到声音。

从上面的用法,我们可以看出:

  1. 可以在CameraXViewcontent: @Composable (GLCameraView) -> Unit 这里面放我们相机页面的其他菜单和操作视图
  2. 拍照基本上是秒生成返回图片数据,比CameraX直接去拍照快的不止一点两点
  3. 照片和视频都支持水平滑动切换滤镜,同样,外部可以通过当前拍摄视频状态,来控制“水平滑动切换滤镜”功能是否可用
  4. 拍照完成有快门声音播放。

六、处理grafik视频保存问题

无论是AndroidCamera还是grafika它们两个用的都是同一套视频录制方法,同样的在源码中一开始注释的内容是:

// We don't know when these will actually finish (or even start). 
// We don't want to delay the UI thread though, so we return immediately.

很显然,它并不知道这些何时会真正完成,那么怎么去解决呢?

点击停止录制的时候,肯定需要找到stopRecording对吧,我们进这里面看看

这个骚操作,我们得治治,我们先调用:

mHandler.sendMessage(mHandler.obtainMessage(MSG_QUIT))

然后再直接调用handleStopRecording方法,这里需要注意

需要将耗时操作移动到gl线程外面,否则OpenGL ES所在的线程被阻塞或者被挂起,导致渲染设备上下文丢失(直接表现就是:画面不会再动,无法继续绘制)

切记切记!!

所以这里我们可以放在子线程里面去执行handleStopRecording方法。

我们稍微修改了一下handleStopRecording方法里面的内容

private fun handleStopRecording() {
        Log.d(TAG, "handleStopRecording")
        kotlin.runCatching {
            mVideoEncoder?.drainEncoder(true)
        }
        mVideoEncoder?.stopAudRecord()
        releaseEncoder()
}

我们可以看到多了一个try/catch,为什么?drainEncoder方法的注释中写到它:

从编码器中提取所有待处理的数据并将其转发到复用器

CameraDrawer#onDrawFrame,系统会在每次重新绘制GLTextureView时调用此方法。
然后会触发我们的TextureMovieEncoder#handleFrameAvailable

这里我们针对上面红色框区域的代码做调整:

强行停止结束,会导致drainEncoder异常终止,需要捕获一下异常,如果发现异常同样需要停止后面的代码执行

我们在TextureMovieEncoder里面看到fun handleFrameAvailable的调用上一级来自TextureMovieEncoder#frameAvailable

同样我们需要调整这个方法里面的代码,调整前:

mHandler.sendMessage(mHandler.obtainMessage(MSG_FRAME_AVAILABLE,
     (int) (timestamp >> 32), (int) timestamp, transform));

调整后:

if(isRecording){
    mHandler?.apply {
        sendMessage(
            obtainMessage(
                MSG_FRAME_AVAILABLE,
                (timestamp shr 32).toInt(),
                timestamp.toInt(),
                transform
            )
        )
    }
}

我们再看一下TextureMovieEncoder的源码,里面只有触发了Looper#quitisRecording才为false,因为我们上面修改了TextureMovieEncoder#stopRecording方法,提前调用了Looper#quit,所以这里需要同步停止发送MSG_FRAME_AVAILABLE消息。

核心解决处理的关键代码,全部讲完。

下面就来看看我们最终提供给大家的项目功能视频吧,拍照和拍视频都支持水平滑动切换滤镜功能


GIF压缩失真了,请下载源码体验

目前遗留有个问题是:切换前置摄像头和后置摄像头会有个颠倒的问题,除了这个问题,一切正常满足生产开发需求。

源码地址

如果你可以访问github网站,点击此链接:
https://github.com/TheMelody/ComposeCameraxOpenGL

如果 不能访问github ,可点击这里下载,我们建议到 github 上下载:
https://download.csdn.net/download/logicsboy/87328624

点赞❤️+关注❤️+收藏❤️+评论❤️,划走了可就找不到了哦

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/107451.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java项目:springboot+layui就业信息管理

作者主页:源码空间站2022 简介:Java领域优质创作者、Java项目、学习资料、技术互助 文末获取源码 项目介绍 该项目采用了spring boot,spring,spring mvc,mybatis作为后端技术框架,这些组合稳定抗打&#x…

Redis缓存穿透、击穿、雪崩到底是个啥?7张图告诉你

目录一、缓存是什么?二、缓存的作用和成本1、缓存的作用:2、缓存的成本:三、缓存作用模型1、根据id查询数据缓存流程四、缓存更新策略1、内存淘汰2、超时剔除3、主动更新五、缓存穿透解决方法:六、缓存雪崩七、缓存击穿1、通过互斥…

基于局部特征和引导形状变形的重叠子宫颈细胞自动分割技术

注:该文为Automatic Segmentation of Overlapping Cervical Smear Cells based on Local Distinctive Features and Guided Shape Deformation的相关阅读注释和翻译 基于局部特征和引导形状变形的重叠子宫颈细胞自动分割技术 概述 提出了一种基于独特的局部特征和…

SpringBoot:模块探究之spring-boot-dependencies

在 SpringBoot 开发时,我们常常会发现一个现象:即在 pom 文件中,加入一个新的依赖,往往不需要引入相应的版本号(如下代码块所示),就可以正常引入依赖,这其实是因为我们依赖了 spring…

mysql修改字段的长度是否会锁表

结论: 缩小字段长度不能使用inpalce,会锁表。 放大字段长度:取决于变化前和变化后是否跨越255这个长度。以UTF8编码为例,一个字符占3个字节。 字段变化1:varchar(50)--》varchar(80…

嵌入式:ARM多寄存器存取指令详解

文章目录多寄存器存取指令的二进制编码指令汇编格式举例注意事项多寄存器传送指令可以用一条指令将16个可见寄存器(R0~R15)的任意子集合(或全部)存储到存储器或从存储器中读取数据到该寄存器集合中。如:可将寄存器列表…

李书福旗下亿咖通纳斯达克上市:作价38亿美元 路演PPT曝光

雷递网 雷建平 12月21日亿咖通科技控股(简称“ECARX”、“亿咖通科技”)今日与特殊目的公司COVA Acquisition Corp.(简称“COVA”)完成合并,并在美国纳斯达克股票市场(Nasdaq Stock Market LLC)…

[UE5]在多个固定摄像机视角间切换,切换多个摄像机,显示不同摄像机所看内容

[UE5]在多个固定摄像机视角间切换,切换多个摄像机,显示不同摄像机所看内容1.写在前面01.作者碎碎念02.结果演示截图演示视频视频教程源码链接03.实现思路实现思路04.同步的博客CSDN掘金博客园知乎2.需要准备的软件3.步骤大神步骤:详细步骤&am…

再写java探针

大家好,我是烤鸭: 以前写过一篇全链路探针实现的文章,最近同事间搞技术分享,再整理一篇。可惜这两年没有继续搞这方面的技术,算是两年前的拓展篇吧。很多技术只放了图,文字就不写了,可以参考…

笔记本加固态小白怎么设置

​最近有用户说电脑硬盘空间不够用了,于是问笔记本加固态小白怎么设置,打算将系统安装在这上面。但由于原先的系统盘有很多重要的数据,该用户就问到有没有什么办法不需要重装,关于笔记本加固态小白操作方法。 工具/原料&#xff…

Redis集群的三种方式详解(附优缺点及原理区别)

Redis提供了三种集群方式,下面我重点详解Redis三种集群方式的原理及优缺点等区别mikechen 目录 Redis主从复制模式Redis哨兵模式Redis集群模式 Redis主从复制模式 1.Redis主从复制定义 主从模式是三种模式中最简单的,主从模式指的是使用一个Redis实例…

Redis入门及Redis基本数据类型的相关命令

1.1、Redis简介 Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache, and message broker,翻译为: Redis是一个开源的内存中的数据结构存储系统,它可以用作∶数据库、缓存和消息中间件。官网: https:…

2023,AIGC能赚到钱吗?

2022年,AIGC(生成式AI)是当之无愧的网红。AI作画在各大社交平台刷屏,ChatGPT火爆国内外出尽了风头,依靠AI生成语音和表情、动作的数字人也频频露脸。2022年12月,Science杂志发布了2022年度科学十大突破&…

vue-element-template模板

vue-element-template模板 vue-element-admin是一个非常全面的系统,提供了很多现成的方案,我们可以在写项目的时候过来参考,但是也有缺点:不太好进行二次开发,因为要删减的东西太多了。于是就有另外一个子系统vue-elem…

Linux 安装nginx, 搭建nginx文件服务器

在linux安装nginx web 服务器,安装后再修改配置文件,将该主机作为一个文件服务器,最终效果如下图: 安装nginx 安装依赖包 yum install -y gcc pcre-devel zlib-devel openssl openssl-devel (若安装的Redhat7&…

JVM数据结构一览及分析

JVM总体一张图: 整体上来看:类文件从类加载子系统,加载完成之后,主要存放在方法区(JRockit和H9没有方法区,这里指的是HotSpot)。运行时的数据主要是存放在运行时数据区,代码的解释编…

✿✿✿JavaScript --- 常见样式案例

目录 1.下拉框复选框单选框的事件样式 和 禁止喜欢中文字与鼠标右键 2.点击按钮后样式发生变化 3.表格追随鼠标更换颜色 4.点击更换背景 5.点击不同的选择栏,内容展示在一个板块内 6.排他思想,许多按钮,点击谁谁就有样式 7.按钮全选反…

微机原理接口

目录 接口的基本概念 什么是接口?(背会) 接口电路的功能?(背会2019年 ) 接口电路基本结构 什么是端口 (背) 接口的控制原理 数据的传送方式(知道并行串行的特点就可以) 并行传…

校园二手交易WEB

开发工具(eclipse/idea/vscode等): 数据库(sqlite/mysql/sqlserver等): 功能模块(请用文字描述,至少200字): 方达影院系统总共分两个模块,用户模块和管理员模块, 用户模块:登录和注册功能&#…

【Pytorch_Geometric】(GCN)基本使用:数据集与邻接矩阵格式,图形(点)的可视化展示

声明:仅学习使用~ 目录 过程记录1、Graph Neural Networks2、数据集描述3、edge_index3、模型定义与训练方法4、输出特征展示5、训练模型(半监督,semi-supervised)过程记录 1、Graph Neural Networks 致力于解决不规则数据结构(图像和文本相对格式都固定,但是社交网络与…