1. 前言
这段时间,在使用 natario1/CameraView 来实现带滤镜的预览
、拍照
、录像
功能。
由于CameraView
封装的比较到位,在项目前期,的确为我们节省了不少时间。
但随着项目持续深入,对于CameraView
的使用进入深水区,逐渐出现满足不了我们需求的情况。
Github
中的issues
中,有些BUG
作者一直没有修复。
那要怎么办呢 ? 项目迫切地需要实现相关功能,只能自己硬着头皮去看它的源码,去解决这些问题。
上篇文章,我们对带滤镜拍照的整个流程有了大致的了解,这篇文章,我们重点来看如何保存滤镜的效果。
以下源码解析基于CameraView 2.7.2
implementation("com.otaliastudios:cameraview:2.7.2")
为了在博客上更好的展示,本文贴出的代码进行了部分精简
绘制并保存是在SnapshotGlPictureRecorder
类中takeFrame()
方法的第5部分。
// 5. Draw and save
long timestampUs = surfaceTexture.getTimestamp() / 1000L;
LOG.i("takeFrame:", "timestampUs:", timestampUs);
mTextureDrawer.draw(timestampUs);
if (mHasOverlay) mOverlayDrawer.render(timestampUs);
mResult.data = eglSurface.toByteArray(Bitmap.CompressFormat.JPEG);
这部分代码做了两件事 :
mTextureDrawer.draw
: 绘制滤镜eglSurface.toByteArray
: 转化为JPEG
格式的Byte
数组
2. 绘制滤镜
首先来看mTextureDrawer.draw()
,mTextureDrawer
上篇文章我们已经介绍过了,通过它,我们最终会调用到mFilter.draw()
public void draw(final long timestampUs) {
if (mPendingFilter != null) {
release();
mFilter = mPendingFilter;
mPendingFilter = null;
}
if (mProgramHandle == -1) {
mProgramHandle = GlProgram.create(
mFilter.getVertexShader(),
mFilter.getFragmentShader());
mFilter.onCreate(mProgramHandle);
Egloo.checkGlError("program creation");
}
GLES20.glUseProgram(mProgramHandle);
Egloo.checkGlError("glUseProgram(handle)");
mTexture.bind();
mFilter.draw(timestampUs, mTextureTransform);
mTexture.unbind();
GLES20.glUseProgram(0);
Egloo.checkGlError("glUseProgram(0)");
}
可以看到,mTextureDrawer.draw()
里,调用顺序如下
- 调用
GlProgram.create()
创建一个OpenGL Program
- 调用
Filter
里的onCreate()
GLES20.glUseProgram()
,启用这个Program
- 调用
mTexture.bind()
,mTexture
是GlTexture
,这个主要是绑定Texture
- 然后调用
Filter
的onDraw
方法 - 最后,调用
mTexture.unbind
解除绑定
这里我们重点来看mFilter.onDraw
,也就是上文说的Filter
接口中的onDraw
。
所以拍照的绘制,就是在这一块执行的。
3. 转化为JPEG
格式的Byte
数组
当使用OpenGL
绘制到滤镜后,来看接下来的eglSurface.toByteArray()
这里的eglSurface
是EglSurface
,可以看到其内部调用了toOutputStream
,并最终将ByteArray
返回。
public fun toByteArray(format: Bitmap.CompressFormat = Bitmap.CompressFormat.PNG): ByteArray {
val stream = ByteArrayOutputStream()
stream.use {
toOutputStream(it, format)
return it.toByteArray()
}
}
toOutputStream
中调用了GLES20.glReadPixels
,作用是从GPU
帧缓冲区中读取像素数据。
具体来说,这个函数可以读取当前帧缓冲区或纹理映射到帧缓冲区上的像素数据,并将这些像素数据写入到内存缓冲区中。这是OpenGL提供的用于从帧缓冲区中读取像素数据的函数。在使用glReadPixels()函数捕获屏幕截图时,一般需要先创建一个大小等同于屏幕分辨率的缓冲区对象,并将其与PBO相关联。然后,通过调用glReadPixels()函数来读取帧缓冲区中的像素数据,并将其存储到PBO中。最后,可以使用标准C/C++语法将PBO中的像素数据保存为图片文件,或者进行其他处理和分析。
public fun toOutputStream(stream: OutputStream, format: Bitmap.CompressFormat = Bitmap.CompressFormat.PNG) {
if (!isCurrent()) throw RuntimeException("Expected EGL context/surface is not current")
val width = getWidth()
val height = getHeight()
val buf = ByteBuffer.allocateDirect(width * height * 4)
buf.order(ByteOrder.LITTLE_ENDIAN)
GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buf)
Egloo.checkGlError("glReadPixels")
buf.rewind()
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
bitmap.copyPixelsFromBuffer(buf)
bitmap.compress(format, 90, stream)
bitmap.recycle()
}
调用了GLES20.glReadPixels
后,像素数据会被存储在buf
中,接着通过buf
创建Bitmap
,并将Bitmap
返回。
4. 分发回调
最后,调用dispatchResult
分发回调
protected void dispatchResult() {
if (mListener != null) {
mListener.onPictureResult(mResult, mError);
mListener = null;
mResult = null;
}
}
实现了PictureResultListener
接口的是CameraBaseEngine
public void onPictureResult(PictureResult.Stub result, Exception error) {
mPictureRecorder = null;
if (result != null) {
getCallback().dispatchOnPictureTaken(result);
} else {
getCallback().dispatchError(new CameraException(error,
CameraException.REASON_PICTURE_FAILED));
}
}
可以看到这里调用了getCallback().dispatchOnPictureTaken()
,最终会调用到CameraView.dispatchOnPictureTaken()
@Override
public void dispatchOnPictureTaken(final PictureResult.Stub stub) {
LOG.i("dispatchOnPictureTaken", stub);
mUiHandler.post(new Runnable() {
@Override
public void run() {
PictureResult result = new PictureResult(stub);
for (CameraListener listener : mListeners) {
listener.onPictureTaken(result);
}
}
});
}
这里会遍历mListeners
,然后调用onPictureTaken
方法。
而mListeners
什么时候被添加呢 ? CameraView
中有一个addCameraListener
方法,专门用来添加回调。
public void addCameraListener(CameraListener cameraListener) {
mListeners.add(cameraListener);
}
5. 设置回调
所以我们只要添加了这个回调,并实现onPictureTaken方法,就可以在onPictureTaken()中获取到拍照后的图像信息了。
binding.cameraView.addCameraListener(object : CameraListener() {
override fun onPictureTaken(result: PictureResult) {
super.onPictureTaken(result)
//拍照回调
val bitmap = BitmapFactory.decodeByteArray(result.data, 0, result.data.size)
bitmap?.also {
Toast.makeText(this@Test2Activity, "拍照成功", Toast.LENGTH_SHORT).show()
//将Bitmap设置到ImageView上
binding.img.setImageBitmap(it)
val file = getNewImageFile()
//保存图片到指定目录
ImageUtils.save(it, file, Bitmap.CompressFormat.JPEG)
}
}
})
6. 其他
6.1 CameraView源码解析系列
Android 相机库CameraView源码解析 (一) : 预览-CSDN博客
Android 相机库CameraView源码解析 (二) : 拍照-CSDN博客
Android 相机库CameraView源码解析 (三) : 滤镜相关类说明-CSDN博客
Android 相机库CameraView源码解析 (四) : 带滤镜拍照-CSDN博客
Android 相机库CameraView源码解析 (五) : 保存滤镜效果-CSDN博客