(自定义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"; protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.continuous_record); sv = (SurfaceView) findViewById(R.id.continuousCapture_surfaceView); SurfaceHolder sh = sv.getHolder(); 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); public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) { public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
接下来我们就要思考,要在surface的三大回调中做点什么?回想一下前一篇文章分析到的GLSurfaceView,该是要创建我们自己的EGL,并把native的surface和EGL组合成EGLSurface保存下来。以上注释已经给出了伪代码的执行过程。在开始之前建议打开我前一篇文章,跟着最底下的红色字体部分流程来理解。下面我们就带大家撸出这个EglCore和EglSurface吧。
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() { 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; 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)) { 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); EGL14.EGL_CONTEXT_CLIENT_VERSION, 3, EGLContext context = EGL14.eglCreateContext(mEGLDisplay, config, sharedContext, attrib3_list, 0); if (EGL14.eglGetError() == EGL14.EGL_SUCCESS) { Log.d(TAG, "Got GLES 3 config"); if (mEGLContext == EGL14.EGL_NO_CONTEXT) { //如果只要求GLES版本2 又或者GLES3失败了。 Log.d(TAG, "Trying GLES 2"); EGLConfig config = getConfig(flags, 2); EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGLContext context = EGL14.eglCreateContext(mEGLDisplay, config, sharedContext, attrib2_list, 0); if (EGL14.eglGetError() == EGL14.EGL_SUCCESS) { Log.d(TAG, "Got GLES 2 config");
我们来看看模板代码,我们通过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; private EGLConfig getConfig(int flags, int version) { int renderableType = EGL14.EGL_OPENGL_ES2_BIT; renderableType |= EGLExt.EGL_OPENGL_ES3_BIT_KHR; //EGL14.EGL_DEPTH_SIZE, 16, //EGL14.EGL_STENCIL_SIZE, 8, EGL14.EGL_RENDERABLE_TYPE, renderableType, EGL14.EGL_NONE, 0, // placeholder for recordable [@-3] 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");
首先我们根据传入的版本号确定渲染模式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实例,直接上代码
public EGLSurface createWindowSurface(Object surface) { if (!(surface instanceof Surface) && !(surface instanceof SurfaceTexture)) { throw new RuntimeException("invalid surface: " + surface); // 创建EGLSurface, 绑定传入进来的surface EGLSurface eglSurface = EGL14.eglCreateWindowSurface(mEGLDisplay, mEGLConfig, surface, GlUtil.checkGlError("eglCreateWindowSurface"); if (eglSurface == null) { throw new RuntimeException("surface was null");
我们模仿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 mHeight = -1; protected EglSurfaceBase(EglCore eglCore) { * @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. return mEglCore.querySurface(mEGLSurface, EGL14.EGL_WIDTH); return mEglCore.querySurface(mEGLSurface, EGL14.EGL_HEIGHT); public int querySurface(EGLSurface eglSurface, int what) { int[] value = new int[1]; EGL14.eglQuerySurface(mEGLDisplay, eglSurface, what, 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 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); Log.d(TAG, "WARNING: swapBuffers() failed"); 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 { public void releaseEglSurface() { mEglCore.makeNothingCurrent(); mEglCore.releaseSurface(mEGLSurface); mEGLSurface = EGL14.EGL_NO_SURFACE; public void makeNothingCurrent() { if (!EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, throw new RuntimeException("eglMakeCurrent To EGL_NO_SURFACE failed"); public void releaseSurface(EGLSurface eglSurface) { EGL14.eglDestroySurface(mEGLDisplay, eglSurface);
然后就是最后的 10、删除EGLContext对象;11、终止与EGLDisplay之间的连接。这部分和EGLSurface没关系了,就是EglCore的部分。
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;
到此,我们就完整的自定义了一套EGL环境操作接口,并封装自己了属于的EglSurface。拿着这套代码,我们就可以嵌入Android任何的渲染载体(native的Surface/SurfaceTexture)开展我们的OpenGL之旅了!下一篇文章,我们就拿着这套代码去实际运用,完成项目需求。
后台有同学私信对那个离屏surface敢兴趣,其实就老版本的PBuffer,这里给出相关代码,详情请follow github:
//EglSurfaceBase 创建离屏的EGLSurface,不过FBO比它好用多了 public void createOffScreenSurface(int width, int height) { if (mEGLSurface != EGL14.EGL_NO_SURFACE) { throw new IllegalStateException("surface already created"); mEGLSurface = mEglCore.createOffscreenSurface(width, height); //EglCore 用旧版的Pbuffer,创建离屏的EGLSurface public EGLSurface createOffscreenSurface(int width, int height) { EGL14.EGL_HEIGHT, height, EGLSurface eglSurface = EGL14.eglCreatePbufferSurface(mEGLDisplay, mEGLConfig, GlUtil.checkGlError("eglCreatePbufferSurface"); if (eglSurface == null) { throw new RuntimeException("surface was null");
The End .
|