Android 解决CameraView叠加2个以上滤镜拍照黑屏的BUG(二) : 解决BUG

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 的初始化是在Camera2EngineonTakePictureSnapshot()方法中

@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中取出widthheight

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

Camera2EngineonTakePictureSnapshot()中,将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原始的代码,即方案一中修改的代码全部回滚,还是用的SnapshotGlPictureRecorderSnapshot2PictureRecorder

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博客

猜你喜欢

转载自blog.csdn.net/EthanCo/article/details/134310244
今日推荐