这次的项目需求总结下来是这样的:一个摄像头预览界面,一个按钮触发屏幕录制,录制视频带上水印效果。
1. 摄像头预览
2. 屏幕录制
3. 录制视频在指定位置附带上水印
确定需求后,我们逐一分析模块组成并完成它。So,Talk is cheap,Let me show codes!
要想预览的时候增加水印(滤镜)效果,必须有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 .