Android OpenGL ES(五)-结合相机进行预览/录制
上文中我们已经实现了在纹理上添加滤镜的效果。这编文章就是将OpenGl和相机结合到一起。
预览与拍照
整体流程理解
预览的整体流程.png- 将
Camera
中得到的ImageStream
由SurfaceTexture
接受,并转换成OpenGL ES
纹理。 - 创建
GLSurfaceView
。在OpenGL
环境下,用GLSurfaceView.Render
将这个纹理绘制出来。 - 整体的
ImageStream
的流向就是
Camera ==>SurfaceTexture==>texture(samplerExternalOES) ==>draw to GLSurfaceView
各个部分详解
Camra Api
首先是相机的Api的书写。
Camera Interface
为我们相机的操作定义一个接口。因为我们的相机Api。有Camera2和Camera的区别。这里还是简单的使用Camera来完成。
/**
* 定义个相机的功能接口
*/
public interface ICamera {
boolean open(int cameraId);
/**
* 设置画面的比例
*/
void setAspectRatio(AspectRatio aspectRatio);
/**
* 开启预览
*/
boolean preview();
/**
* 关闭相机
*
* @return
*/
boolean close();
/**
* 使用SurfaceTexture 来作为预览的画面
*
* @param surfaceTexture
*/
void setPreviewTexture(SurfaceTexture surfaceTexture);
CameraSize getPreviewSize();
CameraSize getPictureSize();
}
定义一个相机的接口。我们知道。我们需要相机做的几个通常的操作。
CameraApi14
/**
* for api 14
* <p>
* Camera主要涉及参数
* 1. 预览画面的大小
* 2. pic图片的大小
* 3. 对焦模式
* 4. 闪光灯模式
*/
public class CameraApi14 implements ICamera {
/*
当前的相机Id
*/
private int mCameraId;
/*
当前的相机对象
*/
private Camera mCamera;
/*
当前的相机参数
*/
private Camera.Parameters mCameraParameters;
//想要的尺寸。
private int mDesiredHeight = 1920;
private int mDesiredWidth = 1080;
private boolean mAutoFocus;
public CameraSize mPreviewSize;
public CameraSize mPicSize;
/*
* 当前相机的高宽比
*/
private AspectRatio mDesiredAspectRatio;
public CameraApi14() {
mDesiredHeight = 1920;
mDesiredWidth = 1080;
//创建默认的比例.因为后置摄像头的比例,默认的情况下,都是旋转了270
mDesiredAspectRatio = AspectRatio.of(mDesiredWidth, mDesiredHeight).inverse();
}
@Override
public boolean open(int cameraId) {
/*
预览的尺寸和照片的尺寸
*/
final CameraSize.ISizeMap mPreviewSizes = new CameraSize.ISizeMap();
final CameraSize.ISizeMap mPictureSizes = new CameraSize.ISizeMap();
if (mCamera != null) {
releaseCamera();
}
mCameraId = cameraId;
mCamera = Camera.open(mCameraId);
if (mCamera != null) {
mCameraParameters = mCamera.getParameters();
mPreviewSizes.clear();
//先收集参数.因为每个手机能够得到的摄像头参数都不一致。所以将可能的尺寸都得到。
for (Camera.Size size : mCameraParameters.getSupportedPreviewSizes()) {
mPreviewSizes.add(new CameraSize(size.width, size.height));
}
mPictureSizes.clear();
for (Camera.Size size : mCameraParameters.getSupportedPictureSizes()) {
mPictureSizes.add(new CameraSize(size.width, size.height));
}
//挑选出最需要的参数
adJustParametersByAspectRatio2(mPreviewSizes, mPictureSizes);
return true;
}
return false;
}
private void adJustParametersByAspectRatio(CameraSize.ISizeMap previewSizes, CameraSize.ISizeMap pictureSizes) {
//得到当前预期比例的size
SortedSet<CameraSize> sizes = previewSizes.sizes(mDesiredAspectRatio);
if (sizes == null) { //表示不支持.
// TODO: 2018/9/14 这里应该抛出异常?
return;
}
//当前先不考虑Orientation
CameraSize previewSize;
mPreviewSize = new CameraSize(mDesiredWidth, mDesiredHeight);
if (mCameraId == Camera.CameraInfo.CAMERA_FACING_BACK) {
mPreviewSize = new CameraSize(mDesiredHeight, mDesiredWidth);
mCameraParameters.setRotation(90);
} else {
// previewSize = mPreviewSize;
}
//默认去取最大的尺寸
mPicSize = pictureSizes.sizes(mDesiredAspectRatio).first();
mCameraParameters.setPreviewSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
mCameraParameters.setPictureSize(mPicSize.getWidth(), mPicSize.getHeight());
//设置对角和闪光灯
setAutoFocusInternal(mAutoFocus);
//先不设置闪光灯
// mCameraParameters.setFlashMode("FLASH_MODE_OFF");
//设置到camera中
// mCameraParameters.setRotation(90);
mCamera.setParameters(mCameraParameters);
// mCamera.setDisplayOrientation(90);
// setCameraDisplayOrientation();
}
private void adJustParametersByAspectRatio2(CameraSize.ISizeMap previewSizes, CameraSize.ISizeMap pictureSizes) {
//得到当前预期比例的size
SortedSet<CameraSize> sizes = previewSizes.sizes(mDesiredAspectRatio);
if (sizes == null) { //表示不支持.
// TODO: 2018/9/14 这里应该抛出异常?
return;
}
//当前先不考虑Orientation
mPreviewSize = sizes.first();
//默认去取最大的尺寸
mPicSize = pictureSizes.sizes(mDesiredAspectRatio).first();
mCameraParameters.setPreviewSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
mCameraParameters.setPictureSize(mPicSize.getWidth(), mPicSize.getHeight());
mPreviewSize = mPreviewSize.inverse();
mPicSize = mPicSize.inverse();
//设置对角和闪光灯
setAutoFocusInternal(mAutoFocus);
//先不设置闪光灯
// mCameraParameters.setFlashMode("FLASH_MODE_OFF");
//设置到camera中
// mCameraParameters.setRotation(90);
mCamera.setParameters(mCameraParameters);
// mCamera.setDisplayOrientation(90);
// setCameraDisplayOrientation();
}
private boolean setAutoFocusInternal(boolean autoFocus) {
mAutoFocus = autoFocus;
// if (isCameraOpened()) {
final List<String> modes = mCameraParameters.getSupportedFocusModes();
if (autoFocus && modes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
mCameraParameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
} else if (modes.contains(Camera.Parameters.FOCUS_MODE_FIXED)) {
mCameraParameters.setFocusMode(Camera.Parameters.FOCUS_MODE_FIXED);
} else if (modes.contains(Camera.Parameters.FOCUS_MODE_INFINITY)) {
mCameraParameters.setFocusMode(Camera.Parameters.FOCUS_MODE_INFINITY);
} else {
mCameraParameters.setFocusMode(modes.get(0));
}
return true;
// } else {
// return false;
// }
}
private void releaseCamera() {
if (mCamera != null) {
mCamera.release();
mCamera = null;
}
}
@Override
public void setAspectRatio(AspectRatio aspectRatio) {
this.mDesiredAspectRatio = aspectRatio;
}
@Override
public boolean preview() {
if (mCamera != null) {
mCamera.startPreview();
return true;
}
return false;
}
@Override
public boolean close() {
if (mCamera != null) {
try {
//stop preview时,可能爆出异常
mCamera.stopPreview();
mCamera.release();
mCamera = null;
return true;
} catch (Exception e) {
e.printStackTrace();
}
}
return false;
}
@Override
public void setPreviewTexture(SurfaceTexture surfaceTexture) {
if (mCamera != null) {
try {
mCamera.setPreviewTexture(surfaceTexture);
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Override
public CameraSize getPreviewSize() {
return mPreviewSize;
}
@Override
public CameraSize getPictureSize() {
return mPicSize;
}
}
这里的代码就是使用Camera
来实现上面的功能。
- 因为使用我们期望将Camera中得到的数据传递到纹理上,所以需要
setPreviewTexture(SurfaceTexture texture)
。让这个SurfaceTexture
来承载。
@Override
public void setPreviewTexture(SurfaceTexture surfaceTexture) {
if (mCamera != null) {
try {
mCamera.setPreviewTexture(surfaceTexture);
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 选择相机的预览的尺寸和旋转的角度
相机的parameter
的选择,只要选对了对应的想要的比例就行了。没有其他需要的。
设备坐标和纹理坐标之间的方向不同问题,由后面纹理的矩阵来控制就好了。
SurfaceTexture
可以从图像流中捕获帧作为OpenGL ES
纹理。
- 直接使用创建的纹理,来创建SurfaceTexture就可以了。
mSurfaceTexture = new SurfaceTexture(mTextureId);
- 然后再将其设置给Camera.同时每次SurfaceTexture刷新的时候,都必须刷新GLSurfaceView。
mCameraApi.setPreviewTexture(mCameraDrawer.getSurfaceTexture());
//默认使用的GLThread.每次刷新的时候,都强制要求是刷新这个GLSurfaceView
mCameraDrawer.getSurfaceTexture().setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() {
@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
requestRender();
}
});
注意事项
使用时必须要注意的是
-
纹理对象使用
GL_TEXTURE_EXTERNAL_OES
纹理目标,该目标由GL_OES_EGL_image_externalOpenGL ES扩展定义。
每次绑定纹理时,它必须绑定到GL_TEXTURE_EXTERNAL_OES
目标而不是GL_TEXTURE_2D
目标。在OpenGL ES 2.0着色器必须使用
#extension GL_OES_EGL_image_external:require
- 着色器还必须使用samplerExternalOES GLSL采样器类型访问纹理。
uniform samplerExternalOES uTexture;
GLSurfaceView.Render
GLSL部分
- oes_base_vertex.glsl
attribute vec4 aPosition;
attribute vec2 aCoordinate;
uniform mat4 uMatrix;
uniform mat4 uCoordinateMatrix;
varying vec2 vTextureCoordinate;
void main(){
gl_Position = uMatrix*aPosition;
vTextureCoordinate = (uCoordinateMatrix*vec4(aCoordinate,0.1,0.1)).xy;
}
顶点着色器对比相对简单。这里需要注意的就是矩阵相乘的顺序问题。
//这个是正确的顺序。相称的顺序相反,图像是反的!!!
gl_Position = uMatrix*aPosition;
-
oes_base_fragment.glsl
这里就是如上面注意事项中说的。必须使用samplerExternalOES
来采样。
#extension GL_OES_EGL_image_external : require
precision mediump float;
varying vec2 vTextureCoordinate;
uniform samplerExternalOES uTexture;
void main() {
gl_FragColor = texture2D(uTexture,vTextureCoordinate);
}
其他的部分
其他的部分和前几编文章中提到的相差不多。
0. 生成纹理
这里就是上面所说的。只能用GLES11Ext.GL_TEXTURE_EXTERNAL_OES
这种纹理。
private int genOesTextureId() {
int[] textureObjectId = new int[1];
GLES20.glGenTextures(1, textureObjectId, 0);
//绑定纹理
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureObjectId[0]);
//设置放大缩小。设置边缘测量
GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR);
GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE);
return textureObjectId[0];
}
1. 视图矩阵。
因为设备坐标和纹理的坐标不同。而前置摄像头和后置摄像头的翻转的方向也不同。所以要做下面的处理
//计算需要变化的矩阵
private void calculateMatrix() {
//得到通用的显示的matrix
Gl2Utils.getShowMatrix(mModelMatrix, mPreviewWidth, mPreviewHeight, this.mSurfaceWidth, this.mSurfaceHeight);
if (mCameraId == Camera.CameraInfo.CAMERA_FACING_FRONT) { //前置摄像头
Gl2Utils.flip(mModelMatrix, true, false);
Gl2Utils.rotate(mModelMatrix, 90);
} else { //后置摄像头
int rotateAngle = 270;
Gl2Utils.rotate(mModelMatrix, rotateAngle);
}
mOesFilter.setMatrix(mModelMatrix);
}
- 得到标准的透视视图矩阵
public static void getShowMatrix(float[] matrix,int imgWidth,int imgHeight,int viewWidth,int
viewHeight){
if(imgHeight>0&&imgWidth>0&&viewWidth>0&&viewHeight>0){
float sWhView=(float)viewWidth/viewHeight;
float sWhImg=(float)imgWidth/imgHeight;
float[] projection=new float[16];
float[] camera=new float[16];
if(sWhImg>sWhView){
Matrix.orthoM(projection,0,-sWhView/sWhImg,sWhView/sWhImg,-1,1,1,3);
}else{
Matrix.orthoM(projection,0,-1,1,-sWhImg/sWhView,sWhImg/sWhView,1,3);
}
Matrix.setLookAtM(camera,0,0,0,1,0,0,0,0,1,0);
Matrix.multiplyMM(matrix,0,projection,0,camera,0);
}
}
这部分就是标准的处理方式了。谁的比例大,用谁的。
- 处理不同摄像头的旋转
如果是前置摄像头的话,需要进行左右的翻转。然后旋转90度。
后置摄像头的话,只需要旋转270度就可以了。
2. 绘制图形
重温一下绘制整体的流程
//draw step
public void draw() {
//step0 clear
onClear();
//step1 use program
onUseProgram();
//step2 active and bind custom data
onSetExpandData();
//step3 bind texture
onBindTexture();
//step4 normal draw
onDraw();
}
onClear
GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
这里有一个疑问。这里其实不用每次都清除的。只要初始化清除就可以了吧?
-
onUseProgram
这一步,就是使用我们之前已经创建和link
好的program
private void onUseProgram() {
GLES20.glUseProgram(mProgram);
}
-
onSetExpandData
这里做的其实就是我们额外给glsl
添加的属性。应用上面我们变化的矩阵。
private void onSetExpandData() {
GLES20.glUniformMatrix4fv(mUMatrix, 1, false, matrix, 0);
GLES20.glUniformMatrix4fv(mUCoordMatrix, 1, false, mCoordMatrix, 0);
}
-
onBindTexture
接着就是激活和绑定纹理数据.
private void onBindTexture() {
GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + getTextureType());
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, getTextureId());
GLES20.glUniform1i(mUTexture, getTextureType());
}
我们的纹理都挂载在GLES20.GL_TEXTURE0 + getTextureType()
上。同时一定要注意的是,相机这里需要的是GLES11Ext.GL_TEXTURE_EXTERNAL_OES
这种拓展类型的纹理采样。
-
onDraw
绘制图像的话,同之前相同,只需要绘制一个长方形就可以了。
private void onDraw() {
//设置定点数据
GLES20.glEnableVertexAttribArray(mAPosition);
GLES20.glVertexAttribPointer(
mAPosition,
2,
GLES20.GL_FLOAT,
false,
0,
mVerBuffer);
//
GLES20.glEnableVertexAttribArray(mACoord);
GLES20.glVertexAttribPointer(
mACoord,
2,
GLES20.GL_FLOAT,
false,
0,
mTextureCoordinate);
//绘制三角形带
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
GLES20.glDisableVertexAttribArray(mAPosition);
GLES20.glDisableVertexAttribArray(mACoord);
}
GLSurfaceView
最后在GLSurfaceView的对应的生命周期内调用方法就可以了~~
录制
整体流程理解
录制的整体流程.png在原来预览的基础上,我们需要加入MediaCodec
进行视频编码。
-
图中的
EglCore
着保存EglContext
。EglSurface
和EglConfig
的配置。WindowSurface
就是将EglContext
和Surface
相互关联的帮助类。 -
Encoder
是在EncoderThread
中进行。两个线程(和原来的GLTHread
)需要共享EGLContext
。 -
使用
MediaCodec
进行视频编码,只要通过它的InputSurface
,将数据输入就可以。所以通过共享的EGLContext
,来创建一个WindowSurface
。然后再通过在该线程内GL的draw
方法,就可以将EGLContext
中的Oes纹理,绘制到Surface
上。这样MediaCodec
就可以得到数据。 -
整体流向
当我们接受到frame时,我们需要
- 在
GLSurfaceView
的渲染线程,将数据渲染到SurfaceView
- 在
Encoder
的线程,将frame
渲染到Mediacodec
的InputSurface
中
Camera==>
SurfaceTexture.onFrameAvailable==>
GLSurfaceView.requestRender ==>
{
//通知更新下一帧
mSurfaceTexture.updateTexImage()
//在`Encoder`的线程,将`frame`渲染到`Mediacodec`的`InputSurface`中。通知编码器线程绘制并编码
mVideoEncoder.frameAvailable(mSurfaceTexture) ==>
{
//通知编码器进行编码
mVideoEncoder.drainEncoder(false);
//刷入数据
mFullScreen.drawFrame(mTextureId, transform);
//给InputWindow设置时间戳
mInputWindowSurface.setPresentationTime(timestampNanos);
//刷新之后,编码器得到数据?
mInputWindowSurface.swapBuffers();
}
//同时Render绘制到屏幕上。在`GLSurfaceView`的渲染线程,将数据渲染到`SurfaceView`
mOesFilter.draw();
}
各个部分详解
TextureMovieEncoder
主要还是添加了一个这个类。
理想状态下,我们创建Video Encoder,然后为它创建EGLContext,然后将这个context传入GLSurfaceView来共享。 但是这里的Api做不到这样,所以我们只能反着来。当GLSurfaceView torn down时,(可能时我们旋转了屏幕),EGLContext也会同样被抛弃。这样意味这当它回来的时候,我们就需要重新为Video encoder创建EGLContext.(而且,"preserve EGLContext on pause" 这样的功能,也不启作用。就是上一个暂停状态的EGLContext,在这里也不能用)我们可以通过使用TextureView 来替代GLSurfaceView来做一些简化。但是这样会由一点性能的问题。
创建EGL环境
获取EGLContext
可以直接在GLThread
中通过EGL14.eglGetCurrentContext()
,就可以得到和线程绑定的EGLContext
(EGLContext
其实也是存在于ThreadLocal
当中)
private void startRecord() {
mOutputFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), "camera-test" + System.currentTimeMillis() + ".mp4");
Log.d(TAG, "file path = " + mOutputFile.getAbsolutePath());
// start recording
mVideoEncoder.startRecording(new TextureMovieEncoder.EncoderConfig(
mOutputFile, mPreviewHeight, mPreviewWidth, 1000000, EGL14.eglGetCurrentContext()));
mRecordingStatus = RECORDING_ON;
}
线程的通信是通过Handler
来完成。
public void startRecording(EncoderConfig config) {
Log.d(TAG, "Encoder: startRecording()");
//mReadyFence 这个锁是来锁这个线程的所有操作的。包括开始。停止。绘制。
synchronized (mReadyFence) {
if (mRunning) {
Log.w(TAG, "Encoder thread already running");
return;
}
mRunning = true;
new Thread(this, "TextureMovieEncoder").start();
while (!mReady) {
try {
mReadyFence.wait();
} catch (InterruptedException ie) {
// ignore
}
}
}
mHandler.sendMessage(mHandler.obtainMessage(MSG_START_RECORDING, config));
}
创建WindowSurface
。将EGLContext
和Encoder.InputSurface
关联在一起
private void prepareEncoder(EGLContext sharedContext, int width, int height, int bitRate,
File outputFile) {
try {
//这个就算MediaCodec的封装。包括MediaCodec进行编码。MediaMuxer进行视频封装
mVideoEncoder = new VideoEncoderCore(width, height, bitRate, outputFile);
} catch (IOException ioe) {
throw new RuntimeException(ioe);
}
//通过EglContext创建EglCore
mEglCore = new EglCore(sharedContext, EglCore.FLAG_RECORDABLE);
//创建inputWindowSurface
mInputWindowSurface = new WindowSurface(mEglCore, mVideoEncoder.getInputSurface(), true);
//在完成EGL的初始化之后,需要通过eglMakeCurrent()函数来将当前的上下文切换,这样opengl的函数才能启动作用。
mInputWindowSurface.makeCurrent();
mFullScreen = new FullFrameRect(
new Texture2dProgram(Texture2dProgram.ProgramType.TEXTURE_EXT));
}
这里进行了一系列的初始化工作。
- 初始化了
VideoEncoderCore
。它是MediaCodec
和MediaMuxer
的封装。
public VideoEncoderCore(int width, int height, int bitRate, File outputFile)
throws IOException {
//MediaCodec的BufferInfo的缓存。通过这个BufferInfo不断的运输数据。(原始=>编码后的)
mBufferInfo = new MediaCodec.BufferInfo();
//创建MediaFormat MIME_TYPE = "video/avc"
MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, width, height);
//设置我们想要的参数。如果参数不合法的话,在configure时,就会报错的
//这个ColorFormat很重要,这里一定要设置COLOR_FormatSurface
format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
//这里的设置5 seconds 在相邻的 I-frames,why?
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL);
if (VERBOSE) Log.d(TAG, "format: " + format);
//创建编码器
mEncoder = MediaCodec.createEncoderByType(MIME_TYPE);
mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
//得到对应InputSureface
mInputSurface = mEncoder.createInputSurface();
//启动
mEncoder.start();
//创建MediaMuxer。我们不能直接在这里开始muxer.因为MediaFormat 还没得到输入。必须要在编码器得到输入之后,才能添加。这里先不添加音频。
mMuxer = new MediaMuxer(outputFile.toString(),
MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
mTrackIndex = -1;
mMuxerStarted = false;
}
- 初始化了
EglCore
。主要是管理EGL的state,包括 (display, context, config)。
//主要是初始化display 和EglContext
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;
}
//先创建一个默认的Display
mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) {
throw new RuntimeException("unable to get EGL14 display");
}
//检查是否创建成功
int[] version = new int[2];
if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)) {
mEGLDisplay = null;
throw new RuntimeException("unable to initialize EGL14");
}
// Try to get a GLES3 context, if requested.
if ((flags & FLAG_TRY_GLES3) != 0) {
//Log.d(TAG, "Trying GLES 3");
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 only, or GLES 3 attempt failed
//Log.d(TAG, "Trying GLES 2");
//获取GL的Config
EGLConfig config = getConfig(flags, 2);
if (config == null) {
throw new RuntimeException("Unable to find a suitable EGLConfig");
}
int[] attrib2_list = {
EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
EGL14.EGL_NONE
};
//获取EGLContext关键方法就是它,EGL14.eglCreateContext
EGLContext context = EGL14.eglCreateContext(mEGLDisplay, config, sharedContext,
attrib2_list, 0);
checkEglError("eglCreateContext");
mEGLConfig = config;
mEGLContext = context;
mGlVersion = 2;
}
// Confirm with query.
int[] values = new int[1];
EGL14.eglQueryContext(mEGLDisplay, mEGLContext, EGL14.EGL_CONTEXT_CLIENT_VERSION,
values, 0);
Log.d(TAG, "EGLContext created, client version " + values[0]);
}
//EGL的配置。类似键值对的数组。
private EGLConfig getConfig(int flags, int version) {
int renderableType = EGL14.EGL_OPENGL_ES2_BIT;
if (version >= 3) {
renderableType |= EGLExt.EGL_OPENGL_ES3_BIT_KHR;
}
// The actual surface is generally RGBA or RGBX, so situationally omitting alpha
// doesn't really help. It can also lead to a huge performance hit on glReadPixels()
// when reading into a GL_RGBA buffer.
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;
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 RGB8888 / " + version + " EGLConfig");
return null;
}
return configs[0];
}
- 初始化了
WindowSurface
。并通过eglMakeCurrent()函数,切换到当前的上下文。
/**
* 将EGL和原生的window surface关联在一起
* 如果传入releaseSurface为true的话,当你调用release方法时,这个Surface就会自动被release。
* 但时如果是使用了SurfaceView的Surface等,Android框架创建的Surface时需要注意,
* 它会干涉原生框架的调用,比如上述的SurfaceView的Surface,release之后,surfaceDestroyed()回调将不会再收到
*/
public WindowSurface(EglCore eglCore, Surface surface, boolean releaseSurface) {
super(eglCore);
createWindowSurface(surface);
mSurface = surface;
mReleaseSurface = releaseSurface;
}
/**
* 创建 window surface.我们的之前的信息提前就保存再EglCore内了
* <p>
* @param surface May be a Surface or SurfaceTexture.
*/
public void createWindowSurface(Object surface) {
if (mEGLSurface != EGL14.EGL_NO_SURFACE) {
throw new IllegalStateException("surface already created");
}
mEGLSurface = mEglCore.createWindowSurface(surface);
}
/**
* 如果我们是为了MediaCodec创建,那么EGLConfig需要有"recordable"的attribute.这个部分,在上面初始化EglCore时,已经完成了EGLConfig和EGLDisplay的配置
*/
public EGLSurface createWindowSurface(Object surface) {
if (!(surface instanceof Surface) && !(surface instanceof SurfaceTexture)) {
throw new RuntimeException("invalid surface: " + surface);
}
// Create a window surface, and attach it to the Surface we received.
int[] surfaceAttribs = {
EGL14.EGL_NONE
};
//这就是我们想要的EGLSurface的创建方式 EGL14.eglCreateWindowSurface
EGLSurface eglSurface = EGL14.eglCreateWindowSurface(mEGLDisplay, mEGLConfig, surface,
surfaceAttribs, 0);
checkEglError("eglCreateWindowSurface");
if (eglSurface == null) {
throw new RuntimeException("surface was null");
}
return eglSurface;
}
- 初始化
FullFrameRect
。它是OpenGl绘制命令等的封装。
frameAvailable
在接受到Frame
时,进行编码
mVideoEncoder.frameAvailable(mSurfaceTexture);
- 时间戳和tranfrom矩阵
float[] transform = new float[16]; // TODO - avoid alloc every frame
st.getTransformMatrix(transform);
long timestamp = st.getTimestamp();
if (timestamp == 0) {
// Seeing this after device is toggled off/on with power button. The
// first frame back has a zero timestamp.
//
// MPEG4Writer thinks this is cause to abort() in native code, so it's very
// important that we just ignore the frame.
Log.w(TAG, "HEY: got SurfaceTexture with timestamp of zero");
return;
}
mHandler.sendMessage(mHandler.obtainMessage(MSG_FRAME_AVAILABLE,
(int) (timestamp >> 32), (int) timestamp, transform));
- 编码
private void handleFrameAvailable(float[] transform, long timestampNanos) {
if (VERBOSE) Log.d(TAG, "handleFrameAvailable tr=" + transform);
//视频编码
mVideoEncoder.drainEncoder(false);
mFullScreen.drawFrame(mTextureId, transform);
//设置时间戳
mInputWindowSurface.setPresentationTime(timestampNanos);
mInputWindowSurface.swapBuffers();
}
/**
* 从encoder中得到数据,再写入到muxer中。
* 下面这段代码就是通用的编码的代码了
*/
public void drainEncoder(boolean endOfStream) {
final int TIMEOUT_USEC = 10000;
if (VERBOSE) Log.d(TAG, "drainEncoder(" + endOfStream + ")");
//如果通知编码器结束,就会signalEndOfInputStream
if (endOfStream) {
if (VERBOSE) Log.d(TAG, "sending EOS to encoder");
mEncoder.signalEndOfInputStream();
}
//得到outputBuffer
ByteBuffer[] encoderOutputBuffers = mEncoder.getOutputBuffers();
//不断循环,当读取所有数据时
while (true) {
//上面换成的BufferInfo,送入到Encoder中,去查询状态
int encoderStatus = mEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
//如果时继续等待,就暂时不用处理。大多数情况,都是从这儿跳出循环
if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
// no output available yet
if (!endOfStream) {
break; // out of while
} else {
if (VERBOSE) Log.d(TAG, "no output available, spinning to await EOS");
}
//outputBuffer发生变化了。就重新去获取
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
// not expected for an encoder
encoderOutputBuffers = mEncoder.getOutputBuffers();
//格式发生变化。这个第一次configure之后也会调用一次。在这里进行muxer的初始化
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// should happen before receiving buffers, and should only happen once
if (mMuxerStarted) {
throw new RuntimeException("format changed twice");
}
MediaFormat newFormat = mEncoder.getOutputFormat();
Log.d(TAG, "encoder output format changed: " + newFormat);
// now that we have the Magic Goodies, start the muxer
mTrackIndex = mMuxer.addTrack(newFormat);
mMuxer.start();
mMuxerStarted = true;
} else if (encoderStatus < 0) {
Log.w(TAG, "unexpected result from encoder.dequeueOutputBuffer: " +
encoderStatus);
// let's ignore it
} else {
//写入数据
ByteBuffer encodedData = encoderOutputBuffers[encoderStatus];
if (encodedData == null) {
throw new RuntimeException("encoderOutputBuffer " + encoderStatus +
" was null");
}
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
// The codec config data was pulled out and fed to the muxer when we got
// the INFO_OUTPUT_FORMAT_CHANGED status. Ignore it.
if (VERBOSE) Log.d(TAG, "ignoring BUFFER_FLAG_CODEC_CONFIG");
mBufferInfo.size = 0;
}
if (mBufferInfo.size != 0) {
if (!mMuxerStarted) {
throw new RuntimeException("muxer hasn't started");
}
//切到对应的位置,进行书写
// adjust the ByteBuffer values to match BufferInfo (not needed?)
encodedData.position(mBufferInfo.offset);
encodedData.limit(mBufferInfo.offset + mBufferInfo.size);
//写入
mMuxer.writeSampleData(mTrackIndex, encodedData, mBufferInfo);
if (VERBOSE) {
Log.d(TAG, "sent " + mBufferInfo.size + " bytes to muxer, ts=" +
mBufferInfo.presentationTimeUs);
}
}
//重新释放,为了下一次的输入
mEncoder.releaseOutputBuffer(encoderStatus, false);
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
//到达最后了,就跳出循环
if (!endOfStream) {
Log.w(TAG, "reached end of stream unexpectedly");
} else {
if (VERBOSE) Log.d(TAG, "end of stream reached");
}
break; // out of while
}
}
}
}
添加滤镜
整体流程理解
添加滤镜后的整体流程.png上面,我们是直接绘制OES的纹理。这里,因为要添加滤镜的效果。所以我们需要将纹理进行处理。
离屏绘制
离屏绘制.png先将
OES
纹理,绑定到FrameBuffer
上。同时会在FrameBuffer
上绑定一个新的textureId
(这里命名为OffscreenTextureId
)。然后调用绘制OES
纹理的方法,数据就会传递到FBO
上。而我们可以通过绑定在其上的OffscreenTextureId
得到其数据。通常情况下,我们把绑定FrameBuffer
和绘制这个新的OffscreenTextureId
代表的纹理的过程,称为离屏绘制。
绑定和生成FrameBuffer
的时机
创建FrameBuffer
。因为RenderBuffer的存储大小要和当前的显示的宽和高相关。所以会在onSurfaceChanged
生命周期方法时候调用。
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
//在这里监听到尺寸的改变。做出对应的变化
prepareFramebuffer(width, height);
//...
}
//生成frameBuffer的时机
private void prepareFramebuffer(int width, int height) {
int[] values = new int[1];
//申请一个与FrameBuffer绑定的textureId
GLES20.glGenTextures(1, values, 0);
mOffscreenTextureId = values[0];
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mOffscreenTextureId);
// Create texture storage.
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height, 0,
GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null);
// Set parameters. We're probably using non-power-of-two dimensions, so
// some values may not be available for use.
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER,
GLES20.GL_NEAREST);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER,
GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S,
GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T,
GLES20.GL_CLAMP_TO_EDGE);
//创建FrameBuffer Object并且绑定它
GLES20.glGenFramebuffers(1, values, 0);
mFrameBuffer = values[0];
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBuffer);
// 创建RenderBuffer Object并且绑定它
GLES20.glGenRenderbuffers(1, values, 0);
mRenderBuffer = values[0];
GLES20.glBindRenderbuffer(GLES20.GL_RENDERBUFFER, mRenderBuffer);
//为我们的RenderBuffer申请存储空间
GLES20.glRenderbufferStorage(GLES20.GL_RENDERBUFFER, GLES20.GL_DEPTH_COMPONENT16, width, height);
// 将renderBuffer挂载到frameBuffer的depth attachment 上。就上面申请了OffScreenId和FrameBuffer相关联
GLES20.glFramebufferRenderbuffer(GLES20.GL_FRAMEBUFFER, GLES20.GL_DEPTH_ATTACHMENT, GLES20.GL_RENDERBUFFER, mRenderBuffer);
// 将text2d挂载到frameBuffer的color attachment上
GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, mOffscreenTextureId, 0);
// See if GLES is happy with all this.
int status = GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER);
if (status != GLES20.GL_FRAMEBUFFER_COMPLETE) {
throw new RuntimeException("Framebuffer not complete, status=" + status);
}
// 先不使用FrameBuffer,将其切换掉。到开始绘制的时候,在绑定回来
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
}
//在onDrawFrame中添加代码
@Override
public void onDrawFrame(GL10 gl) {
//...省略
//重新切换到FrameBuffer上。
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBuffer);
//这里的绘制,就会将数据挂载到FrameBuffer上了。
mOesFilter.draw();
//解除绑定,结束FrameBuffer部分的数据写入
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
//....省略
}
-
FrameBuffer
帧缓冲对象
openGL绘制流程.png
我们自己创建的FrameBuffer其实只是一个容器。所以我们要将数据挂载上去,它才算是完整。
FrameBuffer.png所以,我们可以看到申请FrameBuffer需要进行下面的三步
- 生成一个FrameBuffer
- 申请一个RenderBuffer,并且挂载
GL_DEPTH_ATTACHMENT
上。
RenderBuffer
也是一个渲染缓冲区对象。RenderBuffer
对象是新引入的用于离屏渲染。它允许将场景直接渲染到Renderbuffer
对象,而不是渲染到纹理对象。
Renderbuffe
r只是一个包含可渲染内部格式的单个映像的数据存储对象。它用于存储没有相应纹理格式的OpenGL
逻辑缓冲区,如模板或深度缓冲区。
- 申请一个
textureId
,挂载到GL_COLOR_ATTACHMENT0
上。 - 重新切换到FrameBuffer上(绑定),然后绘制。
我们就可以通过这个纹理,得到保存在FBO
上的数据了
添加滤镜的绘制
添加滤镜.png我们可以通过FBO
,进行滤镜处理。我们将得到的数据,再次进行绘制,在这次的绘制中,我们就可以添加上我们想要的滤镜处理了。
但是这里不仅仅是要绘制到屏幕上,同时要在开启录制的时候,输入给Encoder进行视频的编码和封装。
所以我们需要将数据再写写入一个新的FrameBuffer中,然后再其输出的outputTexture中,就可以得到应用了纹理的数据了。
- 滤镜处理
就算将上面的OffscreenTextureId作为这里滤镜的输入Id.
@Override
public void onDrawFrame(GL10 gl) {
//...省略
//经过路径处理
mColorFilter.setTextureId(mOffscreenTextureId);
mColorFilter.onDrawFrame();
int outputTextureId = mColorFilter.getOutputTextureId();
//...省略
}
同时滤镜内,也按照上面的FrameBuffer
的处理流程。将数据挂载到FrameBuffer
上。得到挂载在FrameBuffer
上的outputTextureId
代码同上,省略
将应用了滤镜的纹理分别绘制到GLView
和Encoder
当中
image.png
@Override
public void onDrawFrame(GL10 gl) {
//省略...
//经过滤镜处理
mColorFilter.setTextureId(mOffscreenTextureId);
mColorFilter.onDrawFrame();
int outputTextureId = mColorFilter.getOutputTextureId();
//将得到的outputTextureId,输入encoder,进行编码
mVideoEncoder.setTextureId(outputTextureId);
mVideoEncoder.frameAvailable(mSurfaceTexture);
//将得到的outputTextureId,再次Draw,因为没有FrameBuffer,所以这次Draw的数据,就直接到了Surface上了。
mShowFilter.setTextureId(outputTextureId);
mShowFilter.onDrawFrame();
}
- 将得到的
outputTextureId
,输入Encoder
的InputSurface
中,通知内部进行draw
和进行编码。 - 将得到的
outputTextureId
,再次Draw,因为没有FrameBuffer
,所以这次draw
的数据,就直接到了Surface
上了。也就是直接绘制到了我们的GLSurfaceView
上了。
小结
- 对比之前绘制流程。上文是直接将纹理绘制到了
GLView
上显示,而这里是将纹理绘制到绑定的FrameBuffer
中,而且
绘制的结果不直接显示出来。所以可以形象的理解离屏绘制,就是将绘制的结果保存在与FrameBuffer
绑定的一个新的textureId
(OffscreenTextureId
)中,不直接绘制到屏幕上。 - 把握好整体流程之后,这部分的处理也会变得简单起来。后面就可以如何添加更加炫酷的滤镜和玩法了。
最后
整编文章就重要的部分还是在理解整个纹理中数据传递的路线。
后续,会对这里的相机的预览添加其他的滤镜。
Demo位置
https://github.com/deepsadness/ZeroToOpenGL
系列文章地址
Android OpenGL ES(一)-开始描绘一个平面三角形
Android OpenGL ES(二)-正交投影
Android OpenGL ES(三)-平面图形
Android OpenGL ES(四)-为平面图添加滤镜
Android OpenGL ES(五)-结合相机进行预览/录制及添加滤镜
Android OpenGL ES(六) - 将输入源换成视频
Android OpenGL ES(七) - 生成抖音照片电影