OpenGL.ES在Android上的简单实践:18-水印录制(自定义Android-EGL)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/a360940265a/article/details/80116437

OpenGL.ES在Android上的简单实践:18-水印录制

(自定义Android-EGL)

 

1、确定需求

这次的项目需求总结下来是这样的:一个摄像头预览界面,一个按钮触发屏幕录制,录制视频带上水印效果。

1. 摄像头预览
2. 屏幕录制
3. 录制视频在指定位置附带上水印

确定需求后,我们逐一分析模块组成并完成它。So,Talk is cheap,Let me show codes!

 

2、EGL+Surface=EGLSurface

要想预览的时候增加水印(滤镜)效果,必须有EGL环境+shader的滤镜特效。所以我们先从简单开始,第一步的需求就是创建EGL,并能正常显示手机摄像头。首先创建我们这次的测试Activity->ContinuousRecordActivity并读取布局界面的SurfaceView,使用SurfaceView是方便直接获取Surface渲染表面。

public class ContinuousRecordActivity extends Activity implements SurfaceHolder.Callback {

    public static final String TAG = "ContinuousRecord";

    SurfaceView sv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.continuous_record);

        sv = (SurfaceView) findViewById(R.id.continuousCapture_surfaceView);
        SurfaceHolder sh = sv.getHolder();
        sh.addCallback(this);
    }

    @Override
    public void surfaceCreated(SurfaceHolder surfaceHolder) {
        Log.d(TAG, "surfaceCreated holder=" + surfaceHolder);
        //首先我们描述一下在这里即将发生的:
        // surface创建回调给开发者我们,然后我们创建一个EGL上下文,组成一个我们需要的EGLSurface
        // EglCore = new EglCore();
        // 把Egl和native的surface组合成=EglSurface,并保存下来。
        // EglSurface = new EglSurface(EglCore,surface);
    }

    @Override
    public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder surfaceHolder) {

    }
}

接下来我们就要思考,要在surface的三大回调中做点什么?回想一下前一篇文章分析到的GLSurfaceView,该是要创建我们自己的EGL,并把native的surface和EGL组合成EGLSurface保存下来。以上注释已经给出了伪代码的执行过程。在开始之前建议打开我前一篇文章,跟着最底下的红色字体部分流程来理解。下面我们就带大家撸出这个EglCore和EglSurface吧。

public class EglCore {
    private static String TAG = "EglCore";

    public static final int FLAG_TRY_GLES2 = 0x02;
    public static final int FLAG_TRY_GLES3 = 0x04;

    private EGLDisplay mEGLDisplay = EGL14.EGL_NO_DISPLAY;
    private EGLContext mEGLContext = EGL14.EGL_NO_CONTEXT;
    private EGLConfig mEGLConfig = null;
    private int mGlVersion = -1;

    public int getGlVersion() {
        return mGlVersion;
    }

    public EglCore() {
        this(null, FLAG_TRY_GLES2);
    }
    public EglCore(EGLContext sharedContext, int flags) {
        ... ...
    }
}

首先我们看看EglCore的组成,我预先定义两个版本号2和3,还有当前版本值mGlVersion。然后其次就是EGLDisplay EGLContext EGLConfig 等相关EGL环境所绑定的变量。我们创建一个无参默认构造函数。和一个附带参数的构造函数。为什么我们要传一个EGLCotext进来呢?因为EGLContext是可以多个的!没错,你没看错,可以多个EGLContext,但是实际使用只能是当前唯一,哈哈!就是说EGLDisplay+EGLContext+EGLSurface只能是唯一对应,如果要替换别的EGLContext,那OPENGL的一系列设置(glEnable接口)都跟着上下文一并替换了。但正常情况我们也就一个EGLContext了,所以默认传null就好。   好了,废话了一段,我们继续~

public EglCore(EGLContext sharedContext, int flags) {
        if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) {
            throw new RuntimeException("EGL already set up");
        }
        if (sharedContext == null) {
            sharedContext = EGL14.EGL_NO_CONTEXT;
        }
        // 1、获取EGLDisplay对象
        mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
        if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) {
            throw new RuntimeException("unable to get EGL14 display");
        }
        // 2、初始化与EGLDisplay之间的关联。
        int[] version = new int[2];
        if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)) {
            mEGLDisplay = null;
            throw new RuntimeException("unable to initialize EGL14");
        }
        ... ...
}   

我们按照前篇文章结尾部分的 创建EGL过程的流程步骤 开始:1、获取EGLDisplay对象;2、初始化与EGLDisplay之间的关联。

接下来我们就要执行3、获取EGLConfig对象;4、创建EGLContext 实例,见如下代码

public EglCore(EGLContext sharedContext, int flags) {
        ... ...
        if ((flags & FLAG_TRY_GLES3) != 0) {
            EGLConfig config = getConfig(flags, 3);
            if (config != null) {
                int[] attrib3_list = {
                        EGL14.EGL_CONTEXT_CLIENT_VERSION, 3,
                        EGL14.EGL_NONE
                };
                EGLContext context = EGL14.eglCreateContext(mEGLDisplay, config, sharedContext, attrib3_list, 0);
                if (EGL14.eglGetError() == EGL14.EGL_SUCCESS) {
                    Log.d(TAG, "Got GLES 3 config");
                    mEGLConfig = config;
                    mEGLContext = context;
                    mGlVersion = 3;
                }
            }
        }
        if (mEGLContext == EGL14.EGL_NO_CONTEXT) {  //如果只要求GLES版本2  又或者GLES3失败了。
            Log.d(TAG, "Trying GLES 2");
            EGLConfig config = getConfig(flags, 2);
            if (config != null) {
                int[] attrib2_list = {
                        EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
                        EGL14.EGL_NONE
                };
                EGLContext context = EGL14.eglCreateContext(mEGLDisplay, config, sharedContext, attrib2_list, 0);
                if (EGL14.eglGetError() == EGL14.EGL_SUCCESS) {
                    Log.d(TAG, "Got GLES 2 config");
                    mEGLConfig = config;
                    mEGLContext = context;
                    mGlVersion = 2;
                }
            }
        }
    }

我们来看看模板代码,我们通过getConfig获取相应的EGLConfig对象,然后在通过EGL14.eglCreateContext,并传入EGLConfig和属性列表,创建出EGLContext;EGL相关的接口很多这种属性配置列表,都是一个int数组,然后按照key-value这样排列下去,这里创建EGLContext,我们只需要传入版本号的信息,然后以单独一个EGL_NONE为结束符标志。

接下来,我们看看getConfig是如何构建EGLConfig:

    public static final int FLAG_RECORDABLE = 0x01;
    public static final int FLAG_TRY_GLES2 = 0x02;
    public static final int FLAG_TRY_GLES3 = 0x04;
    ... ...
    /**
     * 从本地设备中寻找合适的 EGLConfig.
     */
    private EGLConfig getConfig(int flags, int version) {
        int renderableType = EGL14.EGL_OPENGL_ES2_BIT;
        if (version >= 3) {
            renderableType |= EGLExt.EGL_OPENGL_ES3_BIT_KHR;
        }

        int[] attribList = {
                EGL14.EGL_RED_SIZE, 8,
                EGL14.EGL_GREEN_SIZE, 8,
                EGL14.EGL_BLUE_SIZE, 8,
                EGL14.EGL_ALPHA_SIZE, 8,
                //EGL14.EGL_DEPTH_SIZE, 16,
                //EGL14.EGL_STENCIL_SIZE, 8,
                EGL14.EGL_RENDERABLE_TYPE, renderableType,
                EGL14.EGL_NONE, 0,      // placeholder for recordable [@-3]
                EGL14.EGL_NONE
        };
        if ((flags & FLAG_RECORDABLE) != 0) {
            attribList[attribList.length - 3] = EGL_RECORDABLE_ANDROID; 
            // EGLExt.EGL_RECORDABLE_ANDROID;0x3142(required 26)
            // 如果说希望保留自己的最低版本SDK,我们可以自己定义一个EGL_RECORDABLE_ANDROID=0x3142;
            attribList[attribList.length - 2] = 1;
        }
        EGLConfig[] configs = new EGLConfig[1];
        int[] numConfigs = new int[1];
        if (!EGL14.eglChooseConfig(mEGLDisplay, attribList, 0, configs, 0, configs.length, numConfigs, 0)) {
            Log.w(TAG, "unable to find RGBA8888 / " + version + " EGLConfig");
            return null;
        }
        return configs[0];
    }

首先我们根据传入的版本号确定渲染模式renderableType ,然后我们开始构建EGLConfig的属性列表了。(这里又出现属性列表了)我们解读这个属性列表:我们请求RGBA四通道,每个通道都是8个字节。然后注释的两个字段,一个是深度测试,一个是模板缓冲测试的,以后我们再来讨论这部分,现在我们用不着这些所以就注释掉吧;紧接着就是渲染模式renderableType ;

好,来干货了。这里出现一个占位的概念,我们顺着代码继续,判断flags是否附带FLAG_RECORDABLE,如果是我们把占位填充成EGLExt.EGL_RECORDABLE_ANDROID = 1;声明当前EGL是可录屏的。这个是Android平台特意有的标志位。需要版本26的SDK,如果说希望保留自己的最低版本SDK,我们可以自己定义一个EGL_RECORDABLE_ANDROID=0x3142。我们回溯代码,这个标志位flags(FLAG_RECORDABLE=0x01)是EglCore初始化传进来的,是我们自己定义的专门用以判断是否开启录制。如果希望EGL+Surface带录制属性则EglCore(null,FLAG_RECORDABLE);否则EglCore(null,0)即可。到此我们就创建好EGLConfig 和 EGLContext。

接下来就是第五步5、创建EGLSurface实例,直接上代码

    /**
     * 创建一个 EGL+Surface
     * @param surface
     * @return
     */
    public EGLSurface createWindowSurface(Object surface) {
        if (!(surface instanceof Surface) && !(surface instanceof SurfaceTexture)) {
            throw new RuntimeException("invalid surface: " + surface);
        }
        // 创建EGLSurface, 绑定传入进来的surface
        int[] surfaceAttribs = {
                EGL14.EGL_NONE
        };
        EGLSurface eglSurface = EGL14.eglCreateWindowSurface(mEGLDisplay, mEGLConfig, surface,
                surfaceAttribs, 0);
        GlUtil.checkGlError("eglCreateWindowSurface");
        if (eglSurface == null) {
            throw new RuntimeException("surface was null");
        }
        return eglSurface;
    }

我们模仿Android源代码的写法,因为系统的surface和SurfaceTexture都可以充当EGLSurface的载体,所以我们把参数定义为Object,在函数开始就先判断是否合法。然后就是调用eglCreateWindowSurface根据我们之前的mEGLDisplay, mEGLConfig,创建出EGLSurface,然后就大功告成了 ... ... 吗?

回溯到我们一开始给出的伪代码,类EglCore用以表示EGL环境,还有一个类用以表示EglSurface。为啥要这样划分?因为很多的时候,我们都是要直接或间接操作EglSurface的。我们创建EglSurfaceBase,用以表示我们自己的EglSurface

public class EglSurfaceBase {
    private static final String TAG = GlUtil.TAG;

    protected EglCore mEglCore;
    private EGLSurface mEGLSurface = EGL14.EGL_NO_SURFACE;
    private int mWidth = -1;
    private int mHeight = -1;

    protected EglSurfaceBase(EglCore eglCore) {
        mEglCore = eglCore;
    }

    /**
     * 创建要使用的渲染表面EGLSurface
     * @param Surface or SurfaceTexture.
     */
    public void createWindowSurface(Object surface) {
        if (mEGLSurface != EGL14.EGL_NO_SURFACE) {
            throw new IllegalStateException("surface already created");
        }
        mEGLSurface = mEglCore.createWindowSurface(surface);
        // 不用急着在这里创建width/height, 因为surface的大小,不同情况下都会改变。
        //mWidth = mEglCore.querySurface(mEGLSurface, EGL14.EGL_WIDTH);
        //mHeight = mEglCore.querySurface(mEGLSurface, EGL14.EGL_HEIGHT);
    }

    /**
     * 返回surface的width长度, 单位是pixels.
     */
    public int getWidth() {
        if (mWidth < 0) {
            return mEglCore.querySurface(mEGLSurface, EGL14.EGL_WIDTH);
        } else {
            return mWidth;
        }
    }
    public int getHeight() {
        if (mHeight < 0) {
            return mEglCore.querySurface(mEGLSurface, EGL14.EGL_HEIGHT);
        } else {
            return mHeight;
        }
    }
    ... ... ...
}

public class EglCore {
    ... ... ...
    // 查询当前surface的状态值。
    public int querySurface(EGLSurface eglSurface, int what) {
        int[] value = new int[1];
        EGL14.eglQuerySurface(mEGLDisplay, eglSurface, what, value, 0);
        return value[0];
    } 
    ... ... ...
}

看看以上代码,我们这个EglSurfaceBase扩展功能,拥有了长宽值,这长宽值是直接通过查询当前EGLSurface的状态值返回的,并没有赋值到变量width/height,保证其准确性。那我们什么时候才赋值呢?当我们是创建离屏渲染的EGLSurface的时候。这个以后才讨论。到此我才算初步的完成第5、创建EGLSurface实例。

紧接着就是第6、连接EGLContext和EGLSurface。那是怎么连接呢?还记得源码分析的makeCurrent吗?是的就是它。让我们来继续封装它。

(开发的时候多数是通过EglSurfce,间接操作的EglCore)
public class EglSurfaceBase {
    ... ... ...
    // 连接 EGL context 和当前 eglsurface 
    public void makeCurrent() {
        mEglCore.makeCurrent(mEGLSurface);
    }
    public void makeCurrentReadFrom(EglSurfaceBase readSurface) {
        mEglCore.makeCurrent(mEGLSurface, readSurface.mEGLSurface);
    }
}

public class EglCore{
    ... ... ...
    public void makeCurrent(EGLSurface eglSurface) {
        if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) {
            Log.d(TAG, "NOTE: makeCurrent w/r display");
        }
        if (!EGL14.eglMakeCurrent(mEGLDisplay, eglSurface, eglSurface, mEGLContext)) {
            throw new RuntimeException("eglMakeCurrent failed");
        }
    }

    public void makeCurrent(EGLSurface drawSurface, EGLSurface readSurface) {
        if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) {
            Log.d(TAG, "NOTE: makeCurrent w/o display");
        }
        if (!EGL14.eglMakeCurrent(mEGLDisplay, drawSurface, readSurface, mEGLContext)) {
            throw new RuntimeException("eglMakeCurrent(draw,read) failed");
        }
    }
}

我暴露了两种参数方法,看参数名字应该都能搞清楚了。不知道大家还记不记得,OpenGL是使用双缓冲机制的渲染面的,所以我们makeCurrent之后,还需要每帧的交替(swapBuffers)读写的surface。我们赶紧也把此接口封装。

(开发的时候多数是通过EglSurfce,间接操作的EglCore)
public class EglSurfaceBase {   
    public boolean swapBuffers() {
        boolean result = mEglCore.swapBuffers(mEGLSurface);
        if (!result) {
            Log.d(TAG, "WARNING: swapBuffers() failed");
        }
        return result;
    }
}

public class EglCore{
    public boolean swapBuffers(EGLSurface eglSurface) {
        return EGL14.eglSwapBuffers(mEGLDisplay, eglSurface);
    }
}

搞定。之后就是opengl业务开发的相关draw接口的绘制工作了。(呼~先休息休息     你看我不到你看我不到o(*////▽////*)q )

 

跳过第7、使用GL指令绘制图形 的过程之后,我们就要开始回收拾EGL了。即就是执行8~9~10~11的操作了。

我们先来 8、断开并释放与EGLSurface关联的EGLContext对象; 9、删除EGLSurface对象

(开发的时候多数是通过EglSurfce,间接操作的EglCore)
public class EglSurfaceBase {  
    // 释放 EGL surface.
    public void releaseEglSurface() {
        mEglCore.makeNothingCurrent();
        mEglCore.releaseSurface(mEGLSurface);
        mEGLSurface = EGL14.EGL_NO_SURFACE;  
        mWidth = mHeight = -1;
    }
    ... ... ...
}

public class EglCore{
    ... ...
    public void makeNothingCurrent() {
        if (!EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE,
                EGL14.EGL_NO_CONTEXT)) {
            throw new RuntimeException("eglMakeCurrent To EGL_NO_SURFACE failed");
        }
    }
    public void releaseSurface(EGLSurface eglSurface) {
        EGL14.eglDestroySurface(mEGLDisplay, eglSurface);
    }
}

然后就是最后的 10、删除EGLContext对象;11、终止与EGLDisplay之间的连接。这部分和EGLSurface没关系了,就是EglCore的部分。

public class EglCore {
    ... ... ...
    // 释放EGL资源
    public void release() {
        if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) {
            // Android 使用一个引用计数EGLDisplay。
            // 因此,对于每个eglInitialize,我们需要一个eglTerminate。
            EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE,
                    EGL14.EGL_NO_CONTEXT); // 确保EglSurface和EGLContext已经分离
            EGL14.eglDestroyContext(mEGLDisplay, mEGLContext);
            EGL14.eglReleaseThread();
            EGL14.eglTerminate(mEGLDisplay);
        }
        mEGLDisplay = EGL14.EGL_NO_DISPLAY;
        mEGLContext = EGL14.EGL_NO_CONTEXT;
        mEGLConfig = null;
    }
}

到此,我们就完整的自定义了一套EGL环境操作接口,并封装自己了属于的EglSurface。拿着这套代码,我们就可以嵌入Android任何的渲染载体(native的Surface/SurfaceTexture)开展我们的OpenGL之旅了!下一篇文章,我们就拿着这套代码去实际运用,完成项目需求。


 

后台有同学私信对那个离屏surface敢兴趣,其实就老版本的PBuffer,这里给出相关代码,详情请follow github:

    //EglSurfaceBase 创建离屏的EGLSurface,不过FBO比它好用多了
    @Deprecated
    public void createOffScreenSurface(int width, int height) {
        if (mEGLSurface != EGL14.EGL_NO_SURFACE) {
            throw new IllegalStateException("surface already created");
        }
        mEGLSurface = mEglCore.createOffscreenSurface(width, height);
        mWidth = width;
        mHeight = height;
    }

    //EglCore 用旧版的Pbuffer,创建离屏的EGLSurface
    public EGLSurface createOffscreenSurface(int width, int height) {
        int[] surfaceAttribs = {
                EGL14.EGL_WIDTH, width,
                EGL14.EGL_HEIGHT, height,
                EGL14.EGL_NONE
        };
        EGLSurface eglSurface = EGL14.eglCreatePbufferSurface(mEGLDisplay, mEGLConfig,
                surfaceAttribs, 0);
        GlUtil.checkGlError("eglCreatePbufferSurface");
        if (eglSurface == null) {
            throw new RuntimeException("surface was null");
        }
        return eglSurface;
    }

The  End .

没有更多推荐了,返回首页