1. 前言
这段时间,在使用 natario1/CameraView 来实现带滤镜的预览
、拍照
、录像
功能。
由于CameraView
封装的比较到位,在项目前期,的确为我们节省了不少时间。
但随着项目持续深入,对于CameraView
的使用进入深水区,逐渐出现满足不了我们需求的情况。
特别是对于使用MultiFilter
,叠加2
个滤镜拍照是正常的,叠加2
个以上滤镜拍照,预览时正常,拍出的照片就会全黑。
Github
中的issues
中,也有不少提这个BUG
的,但是作者一直没有修复该问题。
上一篇文章,我们已经复现出了这个BUG
,
还有另外一篇文章已经说明了为什么CameraView
预览和拍照的效果为什么会不一致。
而这篇文章我们正式来解决这个BUG
。
2. 方案一
由于CameraView
预览和拍照的效果不一致,首先想到的是,自己重新去实现拍照相关的逻辑,从而避免eglSurface
的各种问题。
2.1 源码说明
首先我们知道滤镜拍照的处理是在SnapshotGlPictureRecorder.take()
方法中,而Snapshot2PictureRecorder
作为Camera2
的实现,是继承自SnapshotGlPictureRecorder
的。
public class Snapshot2PictureRecorder extends SnapshotGlPictureRecorder {
//...省略了代码...
}
Snapshot2PictureRecorder
的初始化是在Camera2Engine
的onTakePictureSnapshot()
方法中
@EngineThread
@Override
protected void onTakePictureSnapshot(@NonNull final PictureResult.Stub stub,
@NonNull final AspectRatio outputRatio,
boolean doMetering) {
stub.size = getUncroppedSnapshotSize(Reference.OUTPUT);
stub.rotation = getAngles().offset(Reference.VIEW, Reference.OUTPUT, Axis.ABSOLUTE);
mPictureRecorder = new Snapshot2PictureRecorder(stub, this,
(RendererCameraPreview) mPreview, outputRatio);
mPictureRecorder.take();
}
所以我们想替换带滤镜拍照的逻辑,那么就替换这两个类就可以了。
2.2 替换SnapshotGlPictureRecorder
我们新建一个MySnapshotGlPictureRecorder
类,用来替换SnapshotGlPictureRecorder
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
public class MySnapshotGlPictureRecorder extends SnapshotPictureRecorder {
private RendererCameraPreview mPreview;
private AspectRatio mOutputRatio;
private Overlay mOverlay;
private boolean mHasOverlay;
public MySnapshotGlPictureRecorder(
@NonNull PictureResult.Stub stub,
@Nullable PictureResultListener listener,
@NonNull RendererCameraPreview preview,
@NonNull AspectRatio outputRatio,
@Nullable Overlay overlay) {
super(stub, listener);
mPreview = preview;
mOutputRatio = outputRatio;
mOverlay = overlay;
mHasOverlay = mOverlay != null && mOverlay.drawsOn(Overlay.Target.PICTURE_SNAPSHOT);
}
@Override
public void take() {
mPreview.addRendererFrameCallback(new RendererFrameCallback() {
@RendererThread
public void onRendererTextureCreated(int textureId) {
MySnapshotGlPictureRecorder.this.onRendererTextureCreated(textureId);
}
@RendererThread
@Override
public void onRendererFilterChanged(@NonNull Filter filter) {
MySnapshotGlPictureRecorder.this.onRendererFilterChanged(filter);
}
@RendererThread
@Override
public void onRendererFrame(@NonNull SurfaceTexture surfaceTexture,
int rotation, float scaleX, float scaleY) {
mPreview.removeRendererFrameCallback(this);
MySnapshotGlPictureRecorder.this.onRendererFrame(surfaceTexture,
rotation, scaleX, scaleY);
}
});
}
private void onRendererTextureCreated(int textureId) {
Rect crop = CropHelper.computeCrop(mResult.size, mOutputRatio);
mResult.size = new Size(crop.width(), crop.height());
/*if (CameraViewConstants.custom) {
// 如果自定义标志位生效,那么使用1920*1080,此处
mResult.size = new Size(1920, 1080);
}*/
}
private void onRendererFilterChanged(Filter filter) {
}
private void onRendererFrame(SurfaceTexture surfaceTexture, int rotation, float scaleX, float scaleY) {
//待实现...
}
}
这里onRendererTextureCreated
中,会确定图像的尺寸。接着在onRendererFrame
中,就要进行拍照时候的滤镜处理了。
2.2.1 获取图像的尺寸
从刚才赋值的mResult.size
中取出width
和height
int width = mResult.size.getWidth();
int height = mResult.size.getHeight();
2.2.2 读取图像数据
接着调用GLES20.glReadPixels()
从GPU
帧缓冲区中取出已经过OpenGL
处理的像素数据,并保存在buffer
中。
由于是RGBA
格式的,所以capacity
传的是width * height * 4
。
ByteBuffer buffer = ByteBuffer.allocateDirect(width * height * 4);
buffer.order(ByteOrder.nativeOrder());
GLES20.glReadPixels(
0,
0,
width,
height,
GLES20.GL_RGBA,
GLES20.GL_UNSIGNED_BYTE,
buffer
);
2.2.3 创建Bitamp
将buffer
转化为Bitmap
buffer.rewind();
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
bitmap.copyPixelsFromBuffer(buffer);
2.2.4 使用矩阵进行旋转处理
使用Matrix
矩阵可以对Bitmap
做镜像、旋转等处理,这里根据实际的摄像头硬件方向进行相应的处理即可。
Matrix matrix = new Matrix();
matrix.preRotate(180,width/2F,height/2F);
Bitmap newBitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, false);
2.2.5 将Bitmap转为Byte数组
将Bitmap
转为Byte
数组
ByteArrayOutputStream stream = new ByteArrayOutputStream();
newBitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream);
byte[] byteArray = stream.toByteArray();
try {
stream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
bitmap.recycle();
newBitmap.recycle();
2.2.6 分发数据
将Byte
数组赋值给data
,并调用dispatchResult
进行分发,最终分发到CameraView
中的mListeners
回调列表。
mResult.data = byteArray;
dispatchResult();
我们设置addCameraListener
回调,就可以在调用takePictureSnapshot()
拍照后,取到相关数据了。
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)
}
}
})
2.3 新建MySnapshot2PictureRecorder
新建MySnapshot2PictureRecorder
继承自MySnapshotGlPictureRecorder
public class MySnapshot2PictureRecorder extends MySnapshotGlPictureRecorder {
public MySnapshot2PictureRecorder(@NonNull PictureResult.Stub stub,
@NonNull Camera2Engine engine,
@NonNull RendererCameraPreview preview,
@NonNull AspectRatio outputRatio) {
super(stub, engine, preview, outputRatio, engine.getOverlay());
}
@Override
public void take() {
super.take();
}
@Override
protected void dispatchResult() {
super.dispatchResult();
}
}
2.4 替换为MySnapshot2PictureRecorder
在Camera2Engine
的onTakePictureSnapshot()
中,将Snapshot2PictureRecorder
的初始化替换为MySnapshot2PictureRecorder
@EngineThread
@Override
protected void onTakePictureSnapshot(@NonNull final PictureResult.Stub stub,
@NonNull final AspectRatio outputRatio,
boolean doMetering) {
stub.size = getUncroppedSnapshotSize(Reference.OUTPUT);
stub.rotation = getAngles().offset(Reference.VIEW, Reference.OUTPUT, Axis.ABSOLUTE);
//Snapshot2PictureRecorder 替换为了MySnapshot2PictureRecorder
mPictureRecorder = new MySnapshot2PictureRecorder(stub, this,
(RendererCameraPreview) mPreview, outputRatio);
mPictureRecorder.take();
}
2.5 运行程序
重新运行程序,调用带滤镜拍照,可以发现CameraView叠加2个以上滤镜拍照黑屏的BUG
已经被解决了。
正当我高兴的时候,却发现还有一个问题 : 每次重新赋值fitler之后,预览都会黑屏闪一下,才回复正常
binding.cameraView.filter = multiFilter
这让我十分郁闷,这也太坑了吧 ? 一时间也没啥思路,梳理了多次流程后,想想还是只能从MultiFilter
入手。
很幸运的是,也不知道是灵光一现,还是歪打正着,让我找到了第二种解决方案
3. 方案二
我们把代码回滚到修改了方案一之前的状态,再来分析下源码。
3.1 源码分析
MultiFilter
中的onCreate()
方法是空的,却有这么一行注释 :
我们将在draw()操作中创建子元素,因为其中一些可能是在调用onCreate()之后添加的。
@Override
public void onCreate(int programHandle) {
// We'll create children during the draw() op, since some of them
// might have been added after this onCreate() is called.
}
再来看draw()
方法,这里遍历了滤镜列表filters
,并对每个filters
进行初始化,然后再进行绘制,下一个滤镜在上一个滤镜的基础上进行绘制,从而达到滤镜叠加的效果。
这样就有个问题了,意味着每次draw
的时候,都回重新去初始化fitler
,那么我们可以推测出,filter
的初始化原本应该是放在onCreate()
中的。
@Override
public void draw(long timestampUs, @NonNull float[] transformMatrix) {
synchronized (lock) {
for (int i = 0; i < filters.size(); i++) {
boolean isFirst = i == 0;
boolean isLast = i == filters.size() - 1;
Filter filter = filters.get(i);
State state = states.get(filter);
maybeSetSize(filter);
maybeCreateProgram(filter, isFirst, isLast);
maybeCreateFramebuffer(filter, isFirst, isLast);
GLES20.glUseProgram(state.programHandle);
if (!isLast) {
state.outputFramebuffer.bind();
GLES20.glClearColor(0, 0, 0, 0);
} else {
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
}
if (isFirst) {
filter.draw(timestampUs, transformMatrix);
} else {
filter.draw(timestampUs, Egloo.IDENTITY_MATRIX);
}
if (!isLast) {
state.outputTexture.bind();
} else {
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
}
GLES20.glUseProgram(0);
}
}
}
3.2 方案实现
将ondraw
中的maybeSetSize()
、maybeCreateProgram()
、maybeCreateFramebuffer()
移动到onCreate()
中
public void onCreate(int programHandle) {
synchronized (lock) {
for (int i = 0; i < filters.size(); i++) {
boolean isFirst = i == 0;
boolean isLast = i == filters.size() - 1;
Filter filter = filters.get(i);
State state = states.get(filter);
maybeSetSize(filter);
maybeCreateProgram(filter, isFirst, isLast);
maybeCreateFramebuffer(filter, isFirst, isLast);
}
}
}
除此之外其他的代码还是用的
CameraView
原始的代码,即方案一中修改的代码全部回滚,还是用的SnapshotGlPictureRecorder
和Snapshot2PictureRecorder
3.3. 运行程序
重新运行程序,可以发现CameraView叠加2个以上滤镜拍照黑屏的BUG
和每次重新赋值fitler之后,预览都会黑屏闪一下,才回复正常
这两个BUG,都已经解决了 !
4. 其他
4.1 CameraView源码解析系列
Android 相机库CameraView源码解析 (一) : 预览-CSDN博客
Android 相机库CameraView源码解析 (二) : 拍照-CSDN博客
Android 相机库CameraView源码解析 (三) : 滤镜相关类说明-CSDN博客
Android 相机库CameraView源码解析 (四) : 带滤镜拍照-CSDN博客
Android 相机库CameraView源码解析 (五) : 保存滤镜效果-CSDN博客
4.2 解决CameraViewBUG
Android 解决CameraView叠加2个以上滤镜拍照黑屏的BUG (一) : 复现BUG-CSDN博客
Android 解决CameraView叠加2个以上滤镜拍照黑屏的BUG(二) : 解决BUG-CSDN博客
为什么相机库CameraView预览和拍照的效果不一致 ?-CSDN博客