一、前言
短视频热潮还没有褪去,写这篇文章主要是帮助大部分人,能快速上手实现类似效果,实际上是: CameraX拿相机数据,OpenGL给CameraX提供一个Surface,数据放到OpenGL渲染的线程上去做图像相关操作
OpenGL滤镜和录制视频核心代码一部分来自:
https://github.com/aserbao/AndroidCamera
其实它里面用了Google的grafika里面的视频录制的核心代码
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)
}
}
从上面的代码片段中,我们可以看到以下内容:
- 初始化ProcessCameraProvider的实例,将相机的生命周期绑定到LifecycleOwner。
- LaunchedEffect(lensFacing) 只有切换前置/后置摄像头会触发里面的逻辑,需要调用cameraProvider.unbindAll() 解除生命周期绑定,并从 CameraX 中移除。
- 初始化
Preview
设置surfaceProvider
,这里需要注意的是调用切换摄像头方向的方法需要在setSurfaceProvider
前面调用。 - 通过CameraSelector构建切换的前置/后置摄像头。
- 调用
cameraProvider.bindToLifecycle
。 - 在
AndroidView
的factory
里面返回我们的glCameraPreview
。 - 在
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
当使用是媒体(如音乐或电影配乐)时使用的值
所以:只要我们的设备播放音乐或者视频能听到声音,那么我们这个快门播放也同样可以听到声音。
从上面的用法,我们可以看出:
- 可以在CameraXView的
content: @Composable (GLCameraView) -> Unit
这里面放我们相机页面的其他菜单和操作视图
。 - 拍照基本上是秒生成返回图片数据,比CameraX直接去拍照快的不止一点两点。
- 照片和视频都支持水平滑动切换滤镜,同样,外部可以通过当前拍摄视频状态,来控制“水平滑动切换滤镜”功能是否可用。
- 拍照完成有快门声音播放。
六、处理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.csdn.net/download/logicsboy/87328624
点赞❤️+关注❤️+收藏❤️+评论❤️,划走了可就找不到了哦