在以往做Camera应用开发时,遇到一个问题,就是相机的预览如何做到在任意尺寸完全无变形的画面预览与视频录制。做过相机应用开发的朋友都知道,相机的预览尺寸并不是可以随意设置的,而是需要在支持的预览尺寸中选择一个,你的预览的view大小必须与选择的尺寸相匹配,才能保证画面不变形,但是这在实际开发中是无法应对各种各样的需求的,而且每个手机,支持的预览尺寸并不是完全一致的,而视频的大小往往是需要做到一致的,不然录制出来的视频,在其他手机上播放,就需要做大量的UI适配工作,也没有平台的统一性。

最初让我有这样的想法,是想做方形的视频,如最初Ins一样,那已经是7年前了,但是当时技术有限,并没有探索出最佳的方案,当时最多可以先录制再通过ffmpeg进行裁剪,这样效率太低了,用户是不能接受的,而且预览画面需要做遮罩,录制做不到所见即所得。

经过其他工作的积累,在OpenGL与Surface搭配工作这方便,点了新的技能点。终于探索出最佳的技术方案。

最终示例代码可以参考我的Github: iCamera

一、技术背景

在阅读接下来的内容前,你最好有以下技术储备:

  1. Camera1相关API的使用经验;
  2. 对Android的surface有一定了解;
  3. 对OpenGL最好有所了解;

当然,如果没有以上相关的知识储备,也不妨碍定性的理解整体流程。

二、基本思路

在一般的Camera应用开发时,通常需要有一个预览画面的控件,一般时SurfaceView或者TextureView,然后把Surface或者SurfaceTexture设置给Camera进行预览。

而在我们的方案中,这个过程,要增加一些步骤。具体的步骤如下:

  1. 从SurfaceView或者TextureView获得Surface对象,比如holder.surface或者Surface(surfaceTexture)
  2. 由此Surface对象,我们创建EGL环境,并重新创建一个SurfaceTexture对象,这个新的SurfaceTexture对象,最后需要设置给Camera进行预览;
  3. 监听步骤2中创建的SurfaceTexture对象的帧数据,通过OpenGL对画面消除形变并进行渲染。

以上就是基本思路,是不是还是一头雾水?没关系,下面进行具体实现的讲解。

三、具体实现

在上述的步骤中,第1步很简单,这里不再进行赘述,直接从步骤2开始。

3.1 通过Surface创建EGL环境

一般代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
private val display: EGLDisplay by lazy { EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY) }
private var eglSurface: EGLSurface? = null
var eglContext: EGLContext? = null
private set
private fun createEGL(surface: Surface) {
val version = IntArray(2)
EGL14.eglInitialize(display, version, 0, version, 1)

val attributes = intArrayOf(
EGL14.EGL_RED_SIZE, 8,
EGL14.EGL_GREEN_SIZE, 8,
EGL14.EGL_BLUE_SIZE, 8,
EGL14.EGL_ALPHA_SIZE, 8,
EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
EGL14.EGL_NONE, 0, // placeholder for recordable [@-3]
EGL14.EGL_NONE
)

val configs = arrayOfNulls<EGLConfig>(1)
val numConfigs = IntArray(1)
EGL14.eglChooseConfig(display, attributes, 0, configs, 0,
configs.size, numConfigs, 0)

val config = configs[0]

eglSurface = EGL14.eglCreateWindowSurface(
display, config, surface, intArrayOf(
EGL14.EGL_NONE
), 0
)

eglContext = EGL14.eglCreateContext(
display, config, sharedContext ?: EGL14.EGL_NO_CONTEXT, intArrayOf(
EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE
), 0
)
EGL14.eglMakeCurrent(display, eglSurface, eglSurface, eglContext)
}

这里需要注意的是,这个方法调用需要在一个独立的线程中,最后一句代码是与当前线程绑定,相关的OpenGL的操作,只能在对应的线程中进行。
有了这个OpenGL环境以后,就可以创建对应的SurfaceTexture了,简单的代码如下:

1
2
3
val textureIds = IntArray(1)
GLES20.glGenTextures(1, textureIds, 0)
val texture = SurfaceTexture(textureIds[0])

同样的,这里的代码也要运行在之前的创建EGL环境相同的线程之下,之后便可以将这里创建的texture设置给camera对象用于预览。

3.2 监听预览-消除形变-绘制画面

给上一步中创建的texture设置一个画面监听。

1
2
3
4
5
6
7
8
9
10
11
12
13
val matrix = FloatArray(16)
texture.setOnFrameAvailableListener {
queue {
texture.updateTexImage()
texture.getTransformMatrix(matrix)

drawFrame(textureIds[0], matrix)
}
}

fun drawFrame(textureId: Int, matrix: FloatArray) {
// ....
}

简单解释一下这里代码:

  1. 先创建一个浮点数组,用于接收预览画面的坐标数据,用于OpenGL绘制画面使用;
  2. 然后设置画面监听,这里在相机开启预览后会触发一次;
  3. 在画面监听回调中,切换到EGL线程中,调用updateTexImage,只有这样,画面监听回调才会持续的被执行;
  4. 最后调用getTransformMatrix获取画面坐标数据,为之后的画面绘制做准备。

接下来,便是执行画面的绘制,在画面绘制关键的一步就是消除形变,这里需要先获得camera对象的预览尺寸,从支持的预览尺寸中,挑选最佳匹配尺寸以及消除方形问题的相关代码,这里不再赘述,只提供消除形变的关键代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
val inputSize: Size // 相机输出的预览画面尺寸,因为要作为绘制的输入,所以称为inputSize,注意消除屏幕方向旋转的问题
val outputSize: Size // 输出画面的尺寸
val viewport: Rect // 计算出预览画面要消除形变,需要做的位置与尺寸变更

private fun calculateBestViewPort() {
if (inputSize.isEmpty || outputSize.isEmpty) {
return
}

val scale = max(outputSize.width.toFloat() / inputSize.width, outputSize.height.toFloat() / inputSize.height)
val srcWidth = (inputSize.width * scale).toInt()
val srcHeight = (inputSize.height * scale).toInt()

val left = (outputSize.width - srcWidth) / 2
val top = (outputSize.height - srcHeight) / 2
viewport.set(left, top, left + srcWidth, top + srcHeight)
}

在绘制画面前,执行OpenGL的glViewport方法,就可以消除形变了。

1
2
3
4
5
6
fun drawFrame(textureId: Int, texMatrix: FloatArray?) {
if (!viewport.isEmpty) {
GLES20.glViewport(viewport.left, viewport.top, viewport.width(), viewport.height())
}
// 绘制画面代码有大量的OpenGL操作,具体不再赘述
}

四、总结

到这里,主要的消除形变预览相机画面的主要逻辑流程,就基本结束了,重点是梳理思路,在具体的实践中,还是有很多细碎的问题的,就比如相机预览尺寸的选取与方向设置问题。

另外,为这里只是为了说明逻辑,临时写了代码逻辑部分,实际项目中,我直接使用了Grafika项目中的egl包下代码,这个项目给了我很多灵感,真心推荐给各位看一下这个项目中的代码。

也许同样做过消除形变的朋友,会认为为什么不直接使用TextureView,然后通过setTransform的方式消除形变呢?

实际上,如果只是为了消除预览的形变,这样做是没有问题的,也是非常便捷的,但是做相机应用的开发,难免会有拍照、录像、帧数据回调(主要是扫码)相关的功能性逻辑,在这类逻辑中,通过这种方式,就无法做到可见即所得式的效果了,就比如拍照,你必须在拍照后,对图片做一次按照预览框尺寸crop的操作了,如果只是拍照还比较简单,如果是录像的话,没有录像期间切换摄像头的需求,使用MediaRecorder或许可以,我没有试过,但是有这样的需求,MediaRecorder就无法满足需求了,面对这样的需求,我之前是通过获取帧数据,然后用libYuv缩放、剪裁反转旋转帧数据等操作,最后喂给MediaCodec的方式,我当前的这个方案,是可以通过共享OpenGL上下文的方式,实现所见即所得的录像,不需要繁琐的操作帧数据了,减少了额外库的引入。

对于上述录像相关的逻辑,我将用新的文章来阐述。

本文采用CC-BY-SA-3.0协议,转载请注明出处
Author: boybeak