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

用CameraX实现抖音式特效相机:美颜+滤镜+实时分析的完整代码实现 本文详细介绍了如何使用CameraX构建具备商业化水准的特效相机,涵盖从基础架构到高级功能的完整技术方案。通过实时美颜引擎、动态滤镜系统和性能调优等实践,帮助开发者实现抖音式特效相机的完整代码实现,提升短视频应用的用户体验。 阅读详情

一、前言

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

OpenGL滤镜来自aserbao_AndroidCamera

视频录制核心代码参考改造自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. 在AndroidView的factory里面返回我们的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. 可以在CameraXView的content: @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#quit,isRecording才为false,因为我们上面修改了TextureMovieEncoder#stopRecording方法,提前调用了Looper#quit,所以这里需要同步停止发送MSG_FRAME_AVAILABLE消息。

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

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


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

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

源码地址:

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

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

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

Android Compose 调用系统相机拍照 这段代码首先定义了一个CameraApp的Composable函数,该函数包含了一个按钮,用户点击按钮时会启动系统相机应用。请注意,实际应用中,还需要处理权限请求等更多复杂的情况。此外,由于不同Android版本的相机API可能有所不同,你可能需要根据你的目标Android版本来调整代码。在Android中,使用Compose调用系统相机拍照功能涉及到多个组件和步骤。在Compose中,你可以创建一个RememberLauncher来处理这个任务。首先,确保你的Android项目已经添加了相机相关的权限。 阅读详情

相关推荐

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

资源代码对应的文章地址:https://blog.youkuaiyun.com/logicsboy/article/details/128404161 使用Jetpack Compose UI开发框架,快速简单易上手,使用CameraX + OpenGL,实现更快更高效的拍照片+录视频。 为什么要用OpenGL呢?因为我们需要用到“滤镜”,同样的,我们:把数据放到OpenGL渲染的线程上去做图像相关操作,能实现更快的数据保存,基本上是毫秒级别的,而且图片质量不失真,速度快,如果只用CameraX去拍照片,你会发现CameraX图片质量大,而且返回数据结果慢,感觉臃肿的样子。 同学们,可以下载我们的源码和快手APP的视频拍照和视频录制进行对比一下,我们的速度和它的是一样的,而且图片质量也不失真。

Android使用OpenGL实现相机滤镜

在onSurfaceCreated方法中通过setOnFrameAvailableListener方法监听相机数据的回调,相机数据回调时,调用requestRender()方法,触发onDrawFrame方法。继承GLSurfaceView用于展示渲染的画面,并实现GLSurfaceView.Renderer接口。在onDrawFrame函数中对画面进行渲染,渲染的逻辑在CameraDrawer中,基本都是些固定代码。而渲染的glsl代码我放在了,raw文件夹下。版本是早期的版本,可以只获取相机数据。

工宗浩生财指南针 1104

GLCamera:基于 OpenGL ES 开发,支持实时滤镜、相机录制、视频播放功能

GLCamera 多种滤镜 后期仍会扩展 支持相机录制

Jetpack Compose 中使用 CameraX 拍照和录制视频

在 Android 开发的历史中,Camera 的 API 是一直受人诟病的,直观的感觉就是配置复杂、臃肿、难用、不易理解,官方也在尝试着不断改进开发者关于Camera的使用体验,Camera 的 API 截止目前经历了 Camera(已废弃)、Camera2、CameraX 三个版本。 初代 Camera API从 5.0 开始已经宣布废弃,而 Camera2 的 API 特别难用,所以就有了 CameraX ,本文主要探索如何在 Jetpack Compose 中使用 CameraX。

川峰的博客 4838

android 摄像头实时滤镜

这个是一个可以实时滤镜的录制视频,对照片进行滤镜相框处理的代码

Android_CameraX_+_OpenGL。虽然_CameraX_已经封装了大部分对相机操作,_GLCameraX.zip

Android_CameraX_+_OpenGL。虽然_CameraX_已经封装了大部分对相机操作,_GLCameraX

gl-Camera

OpenGL的Camera的原理详细,和原理的推导过程。

Stream__的博客 1370

2023东华大学OJ机试题22-70

东华大学计算机考研复试机试22 约瑟夫环223、整除的尾数24、回文质数25、汽水瓶26、阶乘最后的非0位27、算菜价28、 水果价格29、求奇数的乘积30、求最晚和最早日期31、素数32、计算e33 数字之和34 繁殖问题35 奇妙的数字36 整除的尾数37 黑色星期五38 树39 约瑟夫环40 最大与最小41 环42 求数列项43 最高频率44 三艘船45 回文数46 特殊四位数47 最大值48 数列149 修理牛棚50 按要求输出序列51 部落人乘法52 序列53 双重回文数54 等差数列55 人见人

m0_49263811的博客 2597

Android笔记(十三):结合JetPack Compose和CameraX实现视频的录制和存储

结合Jetpack Compose和CameraX框架实现视频的录制以及将录制的时间保存到媒体库中。

userhu2012的博客 2212

电网加密芯片全解析:分类与功能​

在电网数字化转型的浪潮中,数据安全和设备的身份认证变得愈发重要。加密芯片作为保障电网信息安全的核心部件,凭借独特的技术特性,在不同应用场景下发挥着关键作用,有力推动着智能电网的安全稳定发展。接下来,我们将对电网中常见的加密芯片展开分类介绍,并探讨其对应的功能。​。

1959

MacBook用户必看:用Homebrew一键搞定frp内网穿透(含阿里云配置模板)

本文为MacBook用户提供基于Homebrew的frp内网穿透实战指南,详细介绍了从环境准备到阿里云服务器配置的全过程。通过Homebrew一键安装frp客户端,结合阿里云配置模板,实现高效稳定的内网穿透,适用于远程开发、IoT设备调试等多种场景。

weixin_28692817的博客 321

音视频系列--OpenGL纹理+CameraX滤镜渲染

上一篇写了OpenGL基础理论 最近趁着空余时间把OpenGL纹理学习了,然后结合CameraX做了下滤镜效果,还是打算记录下学习效果。 关于纹理的介绍可以学习这篇文章 一、CameraRender 数据源使用CameraX,没有Camera2代码的复杂,CameraX的使用比较简单,之前写的这篇文章有过CameraX的介绍使用,这里不做过多赘述了。 public class CameraRender implements GLSurfaceView.Renderer, Preview.OnPreview

夏的专栏 1630

PCL 点云拟合 最小二乘法拟合平面

在点云处理领域,最小二乘法(Least Squares)拟合是一种常用的数学方法,用于估计数据点的最佳拟合曲线或平面。平面拟合广泛应用于点云数据处理中的地面检测、物体表面分析等任务。 本文将介绍如何使用PCL库实现基于最小二乘法的平面拟合算法,通过拟合点云数据中的平面,来提取出数据的平面部分。

纵马踏花向自由 1026

compose中实现拍照和选取相册功能兼容android 13+

拍照和选取图片的intent在Android上是通用的,后面写的compose组合方法才是compose里面特有的,所以这里也可以拿来放到非compose里面使用。//图片结果//定义单例的原因是因为拍照返回的时候页面会重新实例takePhoto,导致拍照的uri始终为空 val instance get() = Helper . obj } private object Helper {. let {

Tobey_r1的博客 3690

5、Raspberry Pi CPU架构与多线程编程详解

本文详细介绍了Raspberry Pi的CPU架构(包括ARMv6Z、ARMv7-A和ARMv8-A),通过/proc/cpuinfo解析硬件信息,并深入探讨了多线程编程的核心技术。内容涵盖pthread库的使用,如线程创建、属性设置、等待与分离、线程同步机制(互斥锁),以及实际应用场景如多媒体处理和网络服务。同时强调了线程安全、死锁预防和资源管理等关键注意事项,帮助开发者在Raspberry Pi上构建高效稳定的多线程应用。

sre5engineer的博客 93

171、运动控制中的标定:摩擦力与死区补偿标定

摘要(149字) 运动控制系统中的摩擦力与死区补偿是提升精度的关键。摩擦力分为静摩擦、库仑摩擦和粘滞摩擦,其中Stribeck效应是低速抖动的主因;死区则表现为控制信号的无效区间。有效标定方法包括恒速测试(摩擦力)和斜坡响应法(死区)。补偿策略需采用分段前馈和动态调整,避免过度补偿导致振荡。实践中要注意机械状态检查、补偿量限幅,并定期重新标定。硬件升级配合算法补偿才能实现最优控制,标定工作应优先于PID参数调整。

m0_50546716的博客 226

jetpack Compose 使用 GLSurfaceView的例子

页面中包含两个按钮, 点击按钮1 ,GL中新增纹理,实现了一个 compose 测试 GL纹理的例子;点击按钮2 ,GL中释放纹理。

GYBIN02的专栏 853

snmp MIB详解

每个 MIB 对象都由一个对象标识符(OID)和对象类型组成。OID 是一个由多个数字组成的路径(类似于 IP 地址),它标识了 MIB 中对象的层次结构。

dlz0836的博客 3717

录制视频,实现特效与滤镜的几种方式(二)

本文并非专业音视频领域的文章,只不过是其在 Android 方向的 Camera 硬件下结合一些常用的应用场景而已。所以本文并不涉及到太专业的音视频知识,你只需要稍微了解一些以下知识点即可流畅阅读。Android 三种 Camera 分别如何预览,有什么区别?三种 Camera 回调的数据 byte[] 格式有什么区别?如何转换如何旋转?录制视频中常用的 NV21,I420,Surface 三种输入格式对哪一种COLOR_FORMAT完成编码?

chuyouyinghe的专栏 1282

Origin教程004:Origin图形导出

需要编辑的话,双击图形,会自动用origin软件打开图形,这种方式是最开始导出图形的功能,方式一和二都是后面增加的。如果导出的图片边距较大的时候,会影响图片的美观,可以通过。也可以设置在PPT中直接编辑,找到。解决方法是,选择一条线,

WwLK123的博客 6327

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

Android笔记(八):基于CameraX库结合Compose和传统视图组件PreviewView实现照相机画面预览和照相功能

CameraX是JetPack库之一,通过CameraX可以向应用增加相机的功能。在下列内容中,将介绍一个结合CameraX实现一个简单的拍照应用。本应用必须采用Android SDK 34。

userhu2012的博客 3633

温度PID控制PLC程序

温度PID稳定控制程序,适用于欧姆龙PLC,对其他PLC也具有很大参考价值

CameraX 1.3 Beta 版发布 | 为 Android 应用带来更强大的相机功能

作者 / Donovan McMurray,Camera Developer Relations EngineerCameraX 是 Android Jetpack 的相机库,可帮助您为不同的 Android 版本和设备打造一致的出色体验,随着 CameraX 1.3 版本的推出,这个库的实用性也更上一层楼。CameraX 已用于越来越多的 Android 应用中,拥有非常广泛的应用场景,包括简单...

Android开发者 1361

Devexpress16.2.6破解工具

Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

在 Jetpack Compose 中使用 CameraX示例

在使用Jetpack Compose开发安卓应用,当在学习使用CameraX组件时发现官方提供的教程不是Compose的。与是我就记录一下,简单的示例。

lxyoucan的博客 850

CameraX使用

添加依赖 def camerax_version = "1.1.0-beta03" // CameraX core library implementation "androidx.camera:camera-core:$camerax_version" // CameraX Camera2 extensions[可选]拓展库可实现人像、HDR、夜间和美颜、滤镜但依赖于OEM implementation "androidx.camera:camera-camera2:$camerax_version" /

Alvin's Planets 1402

CameraX和OpenGL的融合(cameraX预览数据openGL渲染)

上一篇实现了rc版本的CameraX的一些基本能力 但是那只是基本,因为很多时候我们想要在拿到预览的YUV之前,就做一些事情,那只能通过拿到SurfaceTexture交给OpenGL去渲染,这篇文章就是主要说这个的,相关文章或者视频再网上挺少的,所以花费了比较多时间,后面细说。 前置知识 OpenGL(Open Graphics Library)是开放图形库。是用于渲染2D、3D矢量图形的跨语言、跨平台的应用程序编程接口API。简单来说就是一套画图的API。 OpenGL ES (OpenGL fo

重学Android,保持学习 3208
上一篇: OpenHarmony 物联网设备开发环境搭建
下一篇: 使用DevEco Device Tool编译并烧录全部步骤和过程详解
Halifax ‎
博客等级 码龄14年 4272粉丝 43原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Halifax ‎

交个朋友

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值