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

1. 前言

这段时间,在使用 natario1/CameraView 来实现带滤镜的预览拍照录像功能。
由于CameraView封装的比较到位,在项目前期,的确为我们节省了不少时间。
但随着项目持续深入,对于CameraView的使用进入深水区,逐渐出现满足不了我们需求的情况。
特别是对于使用MultiFilter,叠加2个滤镜拍照是正常的,叠加2个以上滤镜拍照,预览时正常,拍出的照片就会全黑。
Github中的issues中,也有不少提这个BUG的,但是作者一直没有修复该问题。

在这里插入图片描述

上篇文章,我们已经对带滤镜拍照的整个流程有了大概的了解,这篇文章,我们重点来看takeFrame方法,这是带滤镜拍照的核心代码。

接下来我们就来解析takeFrame的源码

2. 创建EGL窗口

首先,会创建EGL窗口,这里创建了一个假的,前台不可见的一个EGL窗口,专门用来保存图片

// 0. EGL window will need an output.
// We create a fake one as explained in javadocs.
final int fakeOutputTextureId = 9999;
SurfaceTexture fakeOutputSurface = new SurfaceTexture(fakeOutputTextureId);
fakeOutputSurface.setDefaultBufferSize(mResult.size.getWidth(), mResult.size.getHeight());

3. 创建EGL Surface

接着,来创建EglSurface

// 1. Create an EGL surface
final EglCore core = new EglCore(eglContext, EglCore.FLAG_RECORDABLE);
final EglSurface eglSurface = new EglWindowSurface(core, fakeOutputSurface);
eglSurface.makeCurrent();

3.1 EglSurface

其中,这个com.otaliastudios.opengl.EglSurface是作者自己创建的,继承自EglNativeSurface

public open class EglNativeSurface internal constructor(internal var eglCore: EglCore,internal var eglSurface: EglSurface) {private var width = -1private var height = -1/*** Can be called by subclasses whose width is guaranteed to never change,* so we can cache this value. For window surfaces, this should not be called.*/@Suppress("unused")protected fun setWidth(width: Int) {this.width = width}/*** Can be called by subclasses whose height is guaranteed to never change,* so we can cache this value. For window surfaces, this should not be called.*/@Suppress("unused")protected fun setHeight(height: Int) {this.height = height}/*** Returns the surface's width, in pixels.** If this is called on a window surface, and the underlying surface is in the process* of changing size, we may not see the new size right away (e.g. in the "surfaceChanged"* callback).  The size should match after the next buffer swap.*/@Suppress("MemberVisibilityCanBePrivate")public fun getWidth(): Int {return if (width < 0) {eglCore.querySurface(eglSurface, EGL_WIDTH)} else {width}}/*** Returns the surface's height, in pixels.*/@Suppress("MemberVisibilityCanBePrivate")public fun getHeight(): Int {return if (height < 0) {eglCore.querySurface(eglSurface, EGL_HEIGHT)} else {height}}/*** Release the EGL surface.*/public open fun release() {eglCore.releaseSurface(eglSurface)eglSurface = EGL_NO_SURFACEheight = -1width = -1}/*** Whether this surface is current on the* attached [EglCore].*/@Suppress("MemberVisibilityCanBePrivate")public fun isCurrent(): Boolean {return eglCore.isSurfaceCurrent(eglSurface)}/*** Makes our EGL context and surface current.*/@Suppress("unused")public fun makeCurrent() {eglCore.makeSurfaceCurrent(eglSurface)}/*** Makes no surface current for the attached [eglCore].*/@Suppress("unused")public fun makeNothingCurrent() {eglCore.makeCurrent()}/*** Sends the presentation time stamp to EGL.* [nsecs] is the timestamp in nanoseconds.*/@Suppress("unused")public fun setPresentationTime(nsecs: Long) {eglCore.setSurfacePresentationTime(eglSurface, nsecs)}
}

3.2 EglCore

可以看到EglNativeSurface内部其实基本上就是调用的EglCoreEglCore内部封装了EGL相关的方法。
这里的具体实现我们不需要细看,只需要知道EglSurface是作者自己实现的一个Surface就可以了,内部封装了EGL,可以实现和GlSurfaceView类似的一些功能,在这里使用的EglSurface是专门给拍照准备的。

这样做的好处在于拍照的时候,预览界面(GLSurfaceView)不会出现卡顿的现象,但是坏处也显而易见,就是可能会出现预览效果和拍照的实际效果不一致的情况。(也就是本文所遇到的BUG的情况)

OpenGL是一个跨平台的操作GPUAPIOpenGL需要本地视窗系统进行交互,就需要一个中间控制层。
EGL就是连接OpenGL ES和本地窗口系统的接口,引入EGL就是为了屏蔽不同平台上的区别。

public expect class EglCore : EglNativeCorepublic open class EglNativeCore internal constructor(sharedContext: EglContext = EGL_NO_CONTEXT, flags: Int = 0) {private var eglDisplay: EglDisplay = EGL_NO_DISPLAYprivate var eglContext: EglContext = EGL_NO_CONTEXTprivate var eglConfig: EglConfig? = nullprivate var glVersion = -1 // 2 or 3init {eglDisplay = eglGetDefaultDisplay()if (eglDisplay === EGL_NO_DISPLAY) {throw RuntimeException("unable to get EGL14 display")}if (!eglInitialize(eglDisplay, IntArray(1), IntArray(1))) {throw RuntimeException("unable to initialize EGL14")}// Try to get a GLES3 context, if requested.val chooser = EglNativeConfigChooser()val recordable = flags and FLAG_RECORDABLE != 0val tryGles3 = flags and FLAG_TRY_GLES3 != 0if (tryGles3) {val config = chooser.getConfig(eglDisplay, 3, recordable)if (config != null) {val attributes = intArrayOf(EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE)val context = eglCreateContext(eglDisplay, config, sharedContext, attributes)try {Egloo.checkEglError("eglCreateContext (3)")eglConfig = configeglContext = contextglVersion = 3} catch (e: Exception) {// Swallow, will try GLES2}}}// If GLES3 failed, go with GLES2.val tryGles2 = eglContext === EGL_NO_CONTEXTif (tryGles2) {val config = chooser.getConfig(eglDisplay, 2, recordable)if (config != null) {val attributes = intArrayOf(EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE)val context = eglCreateContext(eglDisplay, config, sharedContext, attributes)Egloo.checkEglError("eglCreateContext (2)")eglConfig = configeglContext = contextglVersion = 2} else {throw RuntimeException("Unable to find a suitable EGLConfig")}}}/*** Discards all resources held by this class, notably the EGL context.  This must be* called from the thread where the context was created.* On completion, no context will be current.*/internal open fun release() {if (eglDisplay !== EGL_NO_DISPLAY) {// Android is unusual in that it uses a reference-counted EGLDisplay.  So for// every eglInitialize() we need an eglTerminate().eglMakeCurrent(eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)eglDestroyContext(eglDisplay, eglContext)eglReleaseThread()eglTerminate(eglDisplay)}eglDisplay = EGL_NO_DISPLAYeglContext = EGL_NO_CONTEXTeglConfig = null}/*** Makes this context current, with no read / write surfaces.*/internal open fun makeCurrent() {if (!eglMakeCurrent(eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, eglContext)) {throw RuntimeException("eglMakeCurrent failed")}}/*** Destroys the specified surface.  Note the EGLSurface won't actually be destroyed if it's* still current in a context.*/internal fun releaseSurface(eglSurface: EglSurface) {eglDestroySurface(eglDisplay, eglSurface)}/*** Creates an EGL surface associated with a Surface.* If this is destined for MediaCodec, the EGLConfig should have the "recordable" attribute.*/internal fun createWindowSurface(surface: Any): EglSurface {// Create a window surface, and attach it to the Surface we received.val surfaceAttribs = intArrayOf(EGL_NONE)val eglSurface = eglCreateWindowSurface(eglDisplay, eglConfig!!, surface, surfaceAttribs)Egloo.checkEglError("eglCreateWindowSurface")if (eglSurface === EGL_NO_SURFACE) throw RuntimeException("surface was null")return eglSurface}/*** Creates an EGL surface associated with an offscreen buffer.*/internal fun createOffscreenSurface(width: Int, height: Int): EglSurface {val surfaceAttribs = intArrayOf(EGL_WIDTH, width, EGL_HEIGHT, height, EGL_NONE)val eglSurface = eglCreatePbufferSurface(eglDisplay, eglConfig!!, surfaceAttribs)Egloo.checkEglError("eglCreatePbufferSurface")if (eglSurface === EGL_NO_SURFACE) throw RuntimeException("surface was null")return eglSurface}/*** Makes our EGL context current, using the supplied surface for both "draw" and "read".*/internal fun makeSurfaceCurrent(eglSurface: EglSurface) {if (eglDisplay === EGL_NO_DISPLAY) logv("EglCore", "NOTE: makeSurfaceCurrent w/o display")if (!eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)) {throw RuntimeException("eglMakeCurrent failed")}}/*** Makes our EGL context current, using the supplied "draw" and "read" surfaces.*/internal fun makeSurfaceCurrent(drawSurface: EglSurface, readSurface: EglSurface) {if (eglDisplay === EGL_NO_DISPLAY) logv("EglCore", "NOTE: makeSurfaceCurrent w/o display")if (!eglMakeCurrent(eglDisplay, drawSurface, readSurface, eglContext)) {throw RuntimeException("eglMakeCurrent(draw,read) failed")}}/*** Calls eglSwapBuffers. Use this to "publish" the current frame.* @return false on failure*/internal fun swapSurfaceBuffers(eglSurface: EglSurface): Boolean {return eglSwapBuffers(eglDisplay, eglSurface)}/*** Sends the presentation time stamp to EGL.  Time is expressed in nanoseconds.*/internal fun setSurfacePresentationTime(eglSurface: EglSurface, nsecs: Long) {eglPresentationTime(eglDisplay, eglSurface, nsecs)}/*** Returns true if our context and the specified surface are current.*/internal fun isSurfaceCurrent(eglSurface: EglSurface): Boolean {return eglContext == eglGetCurrentContext()&& eglSurface == eglGetCurrentSurface(EGL_DRAW)}/*** Performs a simple surface query.*/internal fun querySurface(eglSurface: EglSurface, what: Int): Int {val value = IntArray(1)eglQuerySurface(eglDisplay, eglSurface, what, value)return value[0]}public companion object {/*** Constructor flag: surface must be recordable.  This discourages EGL from using a* pixel format that cannot be converted efficiently to something usable by the video* encoder.*/internal const val FLAG_RECORDABLE = 0x01/*** Constructor flag: ask for GLES3, fall back to GLES2 if not available.  Without this* flag, GLES2 is used.*/internal const val FLAG_TRY_GLES3 = 0x02}
}

4. 修改transform

这里的mTextureDrawerGlTextureDrawerGlTextureDrawer是一个绘制的管理类,无论是GlCameraPreview(预览)还是SnapshotGlPictureRecorder(带滤镜拍照),都是调用GlTextureDrawer.draw()来渲染openGL的。

public class GlTextureDrawer {//...省略了不重要的代码...private final GlTexture mTexture;private float[] mTextureTransform = Egloo.IDENTITY_MATRIX.clone();public void draw(final long timestampUs) {//...省略了不重要的代码...if (mProgramHandle == -1) {mProgramHandle = GlProgram.create(mFilter.getVertexShader(),mFilter.getFragmentShader());mFilter.onCreate(mProgramHandle);}GLES20.glUseProgram(mProgramHandle);mTexture.bind();mFilter.draw(timestampUs, mTextureTransform);mTexture.unbind();GLES20.glUseProgram(0);}public void release() {if (mProgramHandle == -1) return;mFilter.onDestroy();GLES20.glDeleteProgram(mProgramHandle);mProgramHandle = -1;}
}

transform ,也就是mTextureTransform,会传到Filter.draw()中,最终会改变OpenGL绘制的坐标矩阵,也就是GLSL中的uMVPMatrix变量。
而这边就是修改transform 的值,从而对图像进行镜像、旋转等操作。

final float[] transform = mTextureDrawer.getTextureTransform();// 2. Apply preview transformations
surfaceTexture.getTransformMatrix(transform);
float scaleTranslX = (1F - scaleX) / 2F;
float scaleTranslY = (1F - scaleY) / 2F;
Matrix.translateM(transform, 0, scaleTranslX, scaleTranslY, 0);
Matrix.scaleM(transform, 0, scaleX, scaleY, 1);// 3. Apply rotation and flip// If this doesn't work, rotate "rotation" before scaling, like GlCameraPreview does.Matrix.translateM(transform, 0, 0.5F, 0.5F, 0); // Go back to 0,0Matrix.rotateM(transform, 0, rotation + mResult.rotation, 0, 0, 1); // Rotate to OUTPUTMatrix.scaleM(transform, 0, 1, -1, 1); // Vertical flip because we'll use glReadPixelsMatrix.translateM(transform, 0, -0.5F, -0.5F, 0); // Go back to old position

5. 绘制Overlay

这个没有研究过,似乎是用来绘制覆盖层。这不重要,这里跳过,一般也不会进入这个逻辑。

// 4. Do pretty much the same for overlays
if (mHasOverlay) {// 1. First we must draw on the texture and get latest imagemOverlayDrawer.draw(Overlay.Target.PICTURE_SNAPSHOT);// 2. Then we can apply the transformationsMatrix.translateM(mOverlayDrawer.getTransform(), 0, 0.5F, 0.5F, 0);Matrix.rotateM(mOverlayDrawer.getTransform(), 0, mResult.rotation, 0, 0, 1);Matrix.scaleM(mOverlayDrawer.getTransform(), 0, 1, -1, 1); // Vertical flip because we'll use glReadPixelsMatrix.translateM(mOverlayDrawer.getTransform(), 0, -0.5F, -0.5F, 0);
}
mResult.rotation = 0;

6. 绘制并保存

这里就是带滤镜拍照部分,核心中的核心代码了。
这里主要分为两步

  • mTextureDrawer.draw : 绘制滤镜
  • eglSurface.toByteArray : 将画面保存为JPEG格式的Byte数组
// 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);

这部分具体的代码具体详见下篇文章

7. 释放资源

// 6. Cleanup
eglSurface.release();
mTextureDrawer.release();
fakeOutputSurface.release();
if (mHasOverlay) mOverlayDrawer.release();
core.release();
dispatchResult();

8. 其他

8.1 解决CameraView滤镜黑屏系列

Android 解决CameraView叠加2个以上滤镜拍照黑屏的BUG (一)_氦客的博客-CSDN博客
Android 解决CameraView叠加2个以上滤镜拍照黑屏的BUG (二)_氦客的博客-CSDN博客
Android 解决CameraView叠加2个以上滤镜拍照黑屏的BUG (三)_氦客的博客-CSDN博客

8.2 Android Camera2 系列

更多Camera2相关文章,请看
十分钟实现 Android Camera2 相机预览_氦客的博客-CSDN博客
十分钟实现 Android Camera2 相机拍照_氦客的博客-CSDN博客
十分钟实现 Android Camera2 视频录制_氦客的博客-CSDN博客

8.3 Android 相机相关文章

Android 使用CameraX实现预览/拍照/录制视频/图片分析/对焦/缩放/切换摄像头等操作_氦客的博客-CSDN博客
Android 从零开发一个简易的相机App_android开发简易app_氦客的博客-CSDN博客
Android 使用Camera1实现相机预览、拍照、录像_android 相机预览_氦客的博客-CSDN博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/198789.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Semi-Supervised Multi-Modal Learning with Balanced Spectral Decomposition

Y是所有模态的表征矩阵&#xff0c; ∑ i 1 d h ( λ i ) \sum_{i1}^dh(\lambda_i) ∑i1d​h(λi​) is the proposed eigenvalue-based objective function,the final similarity matrix W for the multimodal data as a block matrix 辅助信息 作者未提供代码

腾讯云轻量数据库是什么?性能如何?费用价格说明

腾讯云轻量数据库测评&#xff0c;轻量数据库100%兼容MySQL 5.7和8.0&#xff0c;腾讯云提供1C1G20GB、1C1G40GB、1C2G80GB、2C4G120GB、2C8G240GB五种规格轻量数据库&#xff0c;腾讯云百科txybk.com分享腾讯云轻量数据库测评、轻量数据库详细介绍、特性、配置价格和常见问题解…

滚珠螺杆在航天工业领域中的重要性

滚珠螺杆是重要的航天工业配件之一&#xff0c;在航天工业领域中具有非常重要的地位和作用。 首先&#xff0c;滚珠螺杆作为一种高精度、高刚度的传动元件&#xff0c;能够提供准确的传动和定位精度&#xff0c;从而保证航天器的可靠性和性能。航天器在飞行过程中需要精确控制其…

NX二次开发UF_CAM_ask_f_s_db_object 函数介绍

文章作者&#xff1a;里海 来源网站&#xff1a;里海NX二次开发3000例专栏 UF_CAM_ask_f_s_db_object Defined in: uf_cam.h int UF_CAM_ask_f_s_db_object(UF_CAM_db_object_t * db_obj ) overview 概述 This function provides the database object which is currently u…

软文推广如何实现效果?媒介盒子为你支招

当今数字化时代&#xff0c;软文已经成为各个品牌推广的常用方式&#xff0c;它通过优质内容输出和有效传播渠道的搭建&#xff0c;实现企业品牌、服务、产品的传播&#xff0c;在消费者心中构建起鲜明的传播形象&#xff0c;但有许多企业在进行推广时都会存在瓶颈&#xff0c;…

centos虚拟机无法接受消息(防火墙)

1.利用wireshark抓包&#xff0c; 发现发送信息后&#xff0c; 虚拟机返回 :host administratively prohibited 2.发现是centos虚拟机未关闭防火墙 &#xff08;关闭后可正常接收消息&#xff09;

瑞吉外卖Day06

1.用户地址 1.1实体类 /*** 地址簿*/ Data public class AddressBook implements Serializable {private static final long serialVersionUID 1L;private Long id;//用户idprivate Long userId;//收货人private String consignee;//手机号private String phone;//性别 0 女…

centos7 探测某个tcp端口是否在监听

脚本 nc -vz 192.168.3.128 60001 if [ $? -eq 0 ]; thenecho "tcp succeed" elseecho "tcp failed" fi nc -vz 192.168.3.128 60001 探测192.168.3.128服务器上60001 tcp端口, -vz说明是探测TCP的 端口开启的情况 执行脚本 端口禁用情况 执行脚本

“腾易视连”构建汽车生态新格局 星选计划赋能创作者价值提升

11月16日&#xff0c;在2023年广州国际车展前夕&#xff0c;以“腾易视连&#xff0c;入局视频号抓住增长新机会”为主题的腾易创作者大会在广州隆重举办。此次大会&#xff0c;邀请行业嘉宾、媒体伙伴、生态伙伴、视频号汽车领域原生达人等共济一堂&#xff0c;结合汽车行业数…

Typescript 的 class 类

介绍 1. 类介绍 传统的JavaScript通过函数和基于原型的继承来创建可重用的组件&#xff0c;从ES6开始&#xff0c;JavaScript程序员也可以使用面向对象的方法来创建对象。例如&#xff0c;下列通过class关键词&#xff0c;来声明了一个类&#xff1a;Greeter class Greeter …

所有做海外营销的,都应该知道什么是SocialSelling

#01 社媒社交营销&#xff0c;早已不是选择题 当下&#xff0c;一个共同的认识已越来越深刻&#xff0c;不管是跨境电商还是外贸&#xff0c;又或者是海外实体店、APP出海、SaaS出海、金融出海等不同业态&#xff0c;社媒和社交营销已是所有企业的共识。 社媒社交营销&#xf…

使用drawio的图层构建更强大的图表

drawio中使用图层 drawio是一款强大的图表绘制软件&#xff0c;支持在线云端版本以及windows, macOS, linux安装版。 如果想在线直接使用&#xff0c;则直接输入网址draw.io或者使用drawon(桌案), drawon.cnhttps://www.drawon.cn?useSourcecsdn内部完整的集成了drawio的所有功…

信息中心网络提出的背景、研究现状及研究内容

信息中心网络什么时候提出的&#xff1f;未来发展前景&#xff1f;有什么著名实验室在做&#xff1f; 1、提出背景&#xff1a; 互联网产生于上世纪60年代&#xff1a; &#xff08;1&#xff09;网络设备数量呈指数性增长 截至2022年底全球范围内预计将有超过280亿台终端设…

「项目阅读系列」go-gin-example star 6.5k!(1)

文章目录 准备工作适宜人群项目信息 项目结构代码阅读主要模块代码主函数模块router 路由模块auth 授权模块数据库 修改文章请求分析其他依赖 总结 准备工作 适宜人群 初学 go 语法&#xff0c;希望了解 go 项目的构建过程和方式。 项目信息 go-gin-example 项目是使用 gin…

JUnit 单元自动化

一、Junit 是什么&#xff1f; Junit 是 Java 中用于单元测试的框架。使用 Junit 能让我们快速高效的完成单元测试。 自动化测试&#xff1a;JUnit提供了自动化测试的能力&#xff0c;开发人员可以编写一次测试用例&#xff0c;然后通过简单的命令或集成到持续集成工具中进行…

100套Axure RP大数据可视化大屏模板及通用组件库

106套Axure RP大数据可视化大屏模板包括了多种实用美观的可视化组件库及行业模板库&#xff0c;行业模板涵盖&#xff1a;金融、教育、医疗、政府、交通、制造等多个行业提供设计参考。 随着大数据的发展&#xff0c;可视化大屏在各行各业得到越来越广泛的应用。可视化大屏不再…

移动端表格分页uni-app

使用uni-app提供的uni-table表格 网址&#xff1a;https://uniapp.dcloud.net.cn/component/uniui/uni-table.html#%E4%BB%8B%E7%BB%8D <uni-table ref"table" :loading"loading" border stripe type"selection" emptyText"暂无更多数据…

分享职业技术培训类型

职业技术培训类型包括&#xff1a;Python技术应用、人工智能应机器学习、大数据分析、机器学习。 一、“Python技术应用工程师” “Python技术应用工程师”职业技术认证是由工业和信息化部教育与考试中心推出一套专业化、科学化、系统化的人才考核标准&#xff0c;涉及在互…

剪辑视频怎么把说话声音转成文字?

短视频已然成为了一种生活潮流&#xff0c;我们每天都在浏览各种短视频&#xff0c;或者用视频的形式记录生活&#xff0c;在制作视频的时候&#xff0c;字幕是一个很大的问题&#xff0c;给视频添加字幕可以更直观、更方便浏览。手动添加太费时间&#xff0c;下面就给大家分享…

设计模式(5)-使用设计模式实现简易版springIoc

自定义简易版springIoc 1 spring使用回顾 自定义spring框架前&#xff0c;先回顾一下spring框架的使用&#xff0c;从而分析spring的核心&#xff0c;并对核心功能进行模拟。 数据访问层。定义UserDao接口及其子实现类 public interface UserDao {public void add(); }public…