所有随风而逝的都是属于昨天的,所有历经风雨留下来的才是面向未来的
Android Camera系列(一):SurfaceView+Camera
Android Camera系列(二):TextureView+Camera
Android Camera系列(三):GLSurfaceView+Camera
Android Camera系列(四):TextureView+OpenGL ES+Camera
Android Camera系列(五):Camera2
Android Camera系列(六):MediaCodec视频编码上-编码YUV
Android Camera系列(七):MediaCodec视频编码中-OpenGL ES多线程渲染
Android Camera系列(八):MediaCodec视频编码下-OpenGL ES离屏渲染
本系列主要讲述Android开发中Camera的相关操作、预览方式、视频录制等。项目结构简单、代码耦合性低,旨在帮助大家能从中有所收获(方便copy :) ),对于个人来说也是一个总结的好机会。
引言
上一篇中我们采用了共享EGLContext
的方式,实现了将SurfaceTexture
纹理绘制到不同线程的Surface中。这种方式需要采用多线程,实现起来不够线性,能不能在单一线程中将纹理绘制到多个Surface
上呢?
还记得我们Camera2章节吗,Camera2是如何实现一份数据绘制到不同的Surface中的呢?查看源码我们可知,他是通过创建N个EGLSurface
,每个EGLSurface
绑定不同的Surface
,具体要绘制到哪个Surface
就通过makeCurrent
方法进行切换控制。
离屏渲染
要将渲染的结果输出到不同的目标,我们需要使用一种称为离屏渲染的技术。
什么是离屏渲染呢?顾名思意,就是让OpenGL不将渲染的结果直接输出到屏幕上,而是输出到一个中间缓冲区(一块GPU空间),然后再将中间缓冲区的内容输出到屏幕或编码器等目标上,这就称为离屏渲染。
在Android系统下可以使用三种方法实现同时将OpenGL ES的内容输出给多个目标(屏幕和编码器)。第一种方法是二次渲染法 ;第二种方法是FBO ;第三种是使用BlitFramebuffer;
一. 二次渲染
想通过二次渲染实现OpenGL ES将渲染结果送给屏幕和编码器,我们必须自定义EGL环境,创建屏幕预览EGLSurface和编码器EGLSurface。Android Camera系列(四):TextureView+OpenGL ES+Camera不熟悉自定义OpenGL ES环境,请查看该章节。我们在自己创建的OpenGL ES线程中使用EGL API ,通过多次渲染将结果输出给多个目标Surface来实现二次渲染,架构图如下:
上图我们看到有SurfaceView,MediaCodec,Camera,OpenGL/EGL等组件。
- SurfaceView用于展示OpenGL的渲染结果
- MediaCodec用于编码,关联的Surface用于接收需要编码的数据
- Camera用于采集视频数据,采集到数据后通知渲染线程,渲染线程通过SurfaceTexture从BufferQueue中取走数据交由OpenGL处理
- OpenGL/EGL 用于渲染,它收到数据后调用Shader程序进行渲染;将渲染结果输出到SurfaceView中显示到屏幕;然后我们需要切换当前渲染的EGLSurface,通过调用EGL的eglMakeCurrent方法,将默认SurfaceView的EGLSurface切换到MediaCodec的EGLSurface上,然后再次调用Shader程序进行渲染,并将渲染结果输出给MediaCodec的Surface进行编码。
通过上面的流程我们知道,二次渲染就是调用了两次Shader程序进行渲染,每次渲染后的结果输送给不同的Surface,因此称为二次渲染法。
渲染核心代码实现如下:
java
/**
* 二次渲染方式
*
* @param recordWindowSurface
* @return
*/
private boolean drawTwice(WindowSurface recordWindowSurface) {
boolean swapResult;
// 先绘制到屏幕上
mPreviewTexture.getTransformMatrix(mDisplayProjectionMatrix);
mCameraFilter.draw(mTextureId, mDisplayProjectionMatrix);
swapResult = mWindowSurface.swapBuffers();
// 再绘制到视频Surface中
mVideoEncoder.frameAvailable();
recordWindowSurface.makeCurrent();
GLES20.glViewport(0, 0,
mVideoEncoder.getVideoWidth(), mVideoEncoder.getVideoHeight());
mCameraFilter.draw(mTextureId, mDisplayProjectionMatrix);
recordWindowSurface.setPresentationTime(mPreviewTexture.getTimestamp());
recordWindowSurface.swapBuffers();
// Restore
GLES20.glViewport(0, 0, mWindowSurface.getWidth(), mWindowSurface.getHeight());
mWindowSurface.makeCurrent();
return swapResult;
}
代码中我们有两个WindowSurface,一个是预览的WindowSurface,一个是编码WindowSurface,代码实现和上面的流程完全一样。
WindowSurface 是我们在 Android Camera系列(四)中对EGL进行了封装,CameraFilter 我们在Android OpenGLES2.0开发(八)中定义的Shader程序。
二. FBO
上面的代码流程我们知道,CameraFilter
程序会调用两次draw
方法,将同一个纹理绘制到不同的Surface上,看起来貌似没有问题。如果CameraFilter中要对图像进行变换,如美颜、高斯模糊等操作,那么我们就要对同一个纹理进行两次计算,这无疑是对GPU的浪费,且效率低下。那么我们如何只计算一次,把结果输出给不同的Surface呢?
OpenGL ES为我们提供了一种高效的办法,即FBO(FrameBufferObject)。接下里我们看看FBO是如何将渲染结果输送给多个目标的
FBO法中我们操作步骤如下:
- 将渲染结果绘制到FBO中
- 将FBO数据输送到屏幕中
- 将FBO数据输送到编码器
FBO法中,我们不直接将OpenGL ES的渲染结果输送给不同的Surface,而是将结果输出到FBO中,FBO可以理解为一块显存区域,用于存放OpenGL ES的渲染结果。
我们知道CameraFilter着色器程序是将SurfaceTexture纹理渲染到EGLSurface中的,如何将纹理渲染到FBO帧缓冲区中呢,我们需要一个渲染到FBO中的着色器程序。
渲染结果输出到FBO后,我们可以将FBO结果分别输出给不同的目标,FBO->屏幕,FBO->MediaCodec。而FBO输出到不同的目标也需要一个新的着色器去绘制。
1. 创建FBO着色器
由上面的概念我们知道FBO就是一块缓冲区,那么我们就需要创建出这块缓冲区出来,我们需要对CameraFilter进行改造
java
/**
* 渲染Camera数据,可离屏渲染到FBO中
*/
public class CameraFilter implements AFilter {
//FBO id
protected int[] mFrameBuffers;
//fbo 纹理id
protected int[] mFrameBufferTextures;
// 是否使用离屏渲染
protected boolean isFBO;
public void setFBO(boolean FBO) {
isFBO = FBO;
}
/**
* 创建帧缓冲区(FBO)
*
* @param width
* @param height
*/
public void createFrameBuffers(int width, int height) {
if (mFrameBuffers != null) {
destroyFrameBuffers();
}
//fbo的创建 (缓存)
//1、创建fbo (离屏屏幕)
mFrameBuffers = new int[1];
// 1、创建几个fbo 2、保存fbo id的数据 3、从这个数组的第几个开始保存
GLES20.glGenFramebuffers(mFrameBuffers.length, mFrameBuffers, 0);
//2、创建属于fbo的纹理
mFrameBufferTextures = new int[1]; //用来记录纹理id
//创建纹理
int textureId = GLESUtils.create2DTexture();
mFrameBufferTextures[0] = textureId;
//让fbo与 纹理发生关系
//创建一个 2d的图像
// 目标 2d纹理+等级 + 格式 +宽、高+ 格式 + 数据类型(byte) + 像素数据
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mFrameBufferTextures[0]);
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height,
0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null);
// 让fbo与纹理绑定起来 , 后续的操作就是在操作fbo与这个纹理上了
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBuffers[0]);
GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
GLES20.GL_TEXTURE_2D, mFrameBufferTextures[0], 0);
//解绑
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
}
/**
* 销毁帧缓冲区(FBO)
*/
public void destroyFrameBuffers() {
//删除fbo的纹理
if (mFrameBufferTextures != null) {
GLES20.glDeleteTextures(1, mFrameBufferTextures, 0);
mFrameBufferTextures = null;
}
//删除fbo
if (mFrameBuffers != null) {
GLES20.glDeleteFramebuffers(1, mFrameBuffers, 0);
mFrameBuffers = null;
}
}
@Override
public void surfaceChanged(int width, int height) {
...
if (isFBO) {
createFrameBuffers(width, height);
}
}
@Override
public int draw(int textureId, float[] matrix) {
GLESUtils.checkGlError("draw start");
...
if (isFBO) {
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
//返回fbo的纹理id
return mFrameBufferTextures[0];
} else {
return textureId;
}
}
@Override
public void release() {
GLES20.glDeleteProgram(mProgram);
mProgram = -1;
destroyFrameBuffers();
}
}
我们对CameraFilter进行改造,现在他既能将纹理绘制到屏幕中,也能将纹理绘制到FBO中。
2. FBO渲染到Surface
FBO中的数据渲染到Surface又该用哪中着色器程序呢,实际上FBO中数据就是RGBA,可以理解为就是2D纹理。渲染2D纹理我们熟啊,Android OpenGLES2.0开发(七):纹理贴图之显示图片章节我们成功将Bitmap转化为2D纹理绘制到GLSurfaceView上。而现在FBO就可以获取2D纹理对象,所以我们需要一个绘制2D纹理的着色器程序,将Image
绘制图片着色器拷贝过来,重命名为Texture2DFilter将Bitmap转换为2D纹理代码删除即可。
java
/**
* 将离屏渲染的数据绘制到屏幕中
*/
public class Texture2DFilter implements AFilter {
/**
* 绘制的流程
* 1.顶点着色程序 - 用于渲染形状的顶点的 OpenGL ES 图形代码
* 2.片段着色器 - 用于渲染具有特定颜色或形状的 OpenGL ES 代码 纹理。
* 3.程序 - 包含您想要用于绘制的着色器的 OpenGL ES 对象 一个或多个形状
* <p>
* 您至少需要一个顶点着色器来绘制形状,以及一个 片段着色器来为该形状着色。
* 这些着色器必须经过编译,然后添加到 OpenGL ES 程序中,该程序随后用于绘制 形状。
*/
// 顶点着色器代码
private final String vertexShaderCode =
// This matrix member variable provides a hook to manipulate
// the coordinates of the objects that use this vertex shader
"attribute vec4 vPosition;\n" +
"attribute vec2 vTexCoordinate;\n" +
"varying vec2 aTexCoordinate;\n" +
"void main() {\n" +
// the matrix must be included as a modifier of gl_Position
// Note that the uMVPMatrix factor *must be first* in order
// for the matrix multiplication product to be correct.
" gl_Position = vPosition;\n" +
" aTexCoordinate = vTexCoordinate;\n" +
"}";
// 片段着色器代码
private final String fragmentShaderCode =
"precision mediump float;\n" +
"uniform sampler2D vTexture;\n" +
"varying vec2 aTexCoordinate;\n" +
"void main() {\n" +
" gl_FragColor = texture2D(vTexture, aTexCoordinate);\n" +
"}\n";
/**
* OpenGL程序句柄
*/
private int mProgram;
/**
* 顶点坐标缓冲区
*/
private FloatBuffer mVertexBuffer;
/**
* 纹理坐标缓冲区
*/
private FloatBuffer mTextureBuffer;
/**
* 此数组中每个顶点的坐标数
*/
static final int COORDS_PER_VERTEX = 2;
/**
* 顶点坐标数组
* 顶点坐标系中原点(0,0)在画布中心
* 向左为x轴正方向
* 向上为y轴正方向
* 画布四个角坐标如下:
* (-1, 1),(1, 1)
* (-1,-1),(1,-1)
*/
private float vertexCoords[] = {
-1.0f, 1.0f, // 左上
-1.0f, -1.0f, // 左下
1.0f, 1.0f, // 右上
1.0f, -1.0f // 右下
};
/**
* 纹理坐标数组
* 这里我们需要注意纹理坐标系,原点(0,0s)在画布左下角
* 向左为x轴正方向
* 向上为y轴正方向
* 画布四个角坐标如下:
* (0,1),(1,1)
* (0,0),(1,0)
*/
private float textureCoords[] = {
0.0f, 1.0f, // 左上
0.0f, 0.0f, // 左下
1.0f, 1.0f, // 右上
1.0f, 0.0f, // 右下
};
/**
* 顶点坐标句柄
*/
private int mPositionHandle;
/**
* 纹理坐标句柄
*/
private int mTexCoordinateHandle;
/**
* 纹理贴图句柄
*/
private int mTexHandle;
private final int vertexCount = vertexCoords.length / COORDS_PER_VERTEX;
private final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertex
public Texture2DFilter() {
// 初始化形状坐标的顶点字节缓冲区
mVertexBuffer = ByteBuffer.allocateDirect(vertexCoords.length * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(vertexCoords);
mVertexBuffer.position(0);
// 初始化纹理坐标顶点字节缓冲区
mTextureBuffer = ByteBuffer.allocateDirect(textureCoords.length * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(textureCoords);
mTextureBuffer.position(0);
}
@Override
public void surfaceCreated() {
// 创建OpenGLES程序
mProgram = GLESUtils.createProgram(vertexShaderCode, fragmentShaderCode);
// 获取顶点着色器vPosition成员的句柄
mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
// 获取顶点着色器中纹理坐标的句柄
mTexCoordinateHandle = GLES20.glGetAttribLocation(mProgram, "vTexCoordinate");
// 获取Texture句柄
mTexHandle = GLES20.glGetUniformLocation(mProgram, "vTexture");
}
@Override
public void surfaceChanged(int width, int height) {
GLES20.glViewport(0, 0, width, height);
}
@Override
public int draw(int textureId, float[] matrix) {
// 将程序添加到OpenGL ES环境
GLES20.glUseProgram(mProgram);
GLESUtils.checkGlError("glUseProgram");
// 为正方形顶点启用控制句柄
GLES20.glEnableVertexAttribArray(mPositionHandle);
GLESUtils.checkGlError("glEnableVertexAttribArray");
// 写入坐标数据
GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, vertexStride, mVertexBuffer);
GLESUtils.checkGlError("glVertexAttribPointer");
// 启用纹理坐标控制句柄
GLES20.glEnableVertexAttribArray(mTexCoordinateHandle);
GLESUtils.checkGlError("glEnableVertexAttribArray");
// 写入坐标数据
GLES20.glVertexAttribPointer(mTexCoordinateHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, vertexStride, mTextureBuffer);
GLESUtils.checkGlError("glVertexAttribPointer");
// 激活纹理编号0
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
// 绑定纹理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);
// 设置纹理采样器编号,该编号和glActiveTexture中设置的编号相同
GLES20.glUniform1i(mTexHandle, 0);
// 绘制
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
GLESUtils.checkGlError("glDrawArrays");
// 取消绑定纹理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
// 禁用顶点阵列
GLES20.glDisableVertexAttribArray(mPositionHandle);
GLES20.glDisableVertexAttribArray(mTexCoordinateHandle);
return textureId;
}
@Override
public void release() {
GLES20.glDeleteProgram(mProgram);
mProgram = -1;
}
}
3. FBO渲染流程
FBO核心渲染代码如下,drawFBO
中的代码和流程图描述一致
java
/**
* OpenGL ES渲染Camera预览数据的线程
*
* @author xiaozhi
* @since 2024/8/22
*/
public class RenderThread extends Thread
private CameraFilter mFBOFilter;
private Texture2DFilter mScreenFilter;
public RenderThread(Context context, TextureMovieEncoder2 textureMovieEncoder) {
...
mFBOFilter = new CameraFilter();
mFBOFilter.setFBO(true);
mScreenFilter = new Texture2DFilter();
}
/**
* 离屏渲染
*
* @param recordWindowSurface
* @return
*/
private boolean drawFBO(WindowSurface recordWindowSurface) {
boolean swapResult;
// 将数据绘制到FBO Buffer中
mPreviewTexture.getTransformMatrix(mDisplayProjectionMatrix);
int fboId = mFBOFilter.draw(mTextureId, mDisplayProjectionMatrix);
// 将离屏FrameBuffer绘制到视频Surface中
mVideoEncoder.frameAvailable();
recordWindowSurface.makeCurrent();
GLES20.glViewport(0, 0,
mVideoEncoder.getVideoWidth(), mVideoEncoder.getVideoHeight());
mScreenFilter.draw(fboId, mDisplayProjectionMatrix);
recordWindowSurface.setPresentationTime(mPreviewTexture.getTimestamp());
recordWindowSurface.swapBuffers();
// 将离屏FrameBuffer绘制到屏幕Surface中
mWindowSurface.makeCurrent();
GLES20.glViewport(0, 0, mWindowSurface.getWidth(), mWindowSurface.getHeight());
mScreenFilter.draw(fboId, mDisplayProjectionMatrix);
swapResult = mWindowSurface.swapBuffers();
return swapResult;
}
}
FBO高效的原理在于,纹理渲染到FBO中的前置操作。如果需要对纹理进行美颜、高斯模糊等复杂的计算,FBO确实能提高效率。如果没有复杂的计算,那么FBO和二次渲染法效率差不多。
三. BlitFramebuffer
上面的方式,相当于我们引入了一个中间区域用来中转,似乎显的复杂了一些。有没有更加简洁高效的方式,只渲染一次就能实现多次拷贝呢?OpenGL ES3.0出现了一种更高效的方法,即BlitFramebuffer。
来直接看流程图:
该方式不再使用FBO做缓存,而是像二次渲染法一样,先将渲染的内容输出到当前Surface中,但并不展示到屏幕上。我们来看下这种方式的流程:
- 先将渲染结果输送给
SurfaceView
的Surface
- 切换
Surface
为MediaCodec
的Surface
,此时当前Surface为MediaCodec的 - 利用OpenGL3.0提供的API
BlitFramebuffer
从原来的Surface拷贝数据到当前Surface中,再调用EGL的eglSwapBuffers
将Surface中的内容送编码器编码 - 最后将当前Surface切回原来的Surface,也就是SurfaceView的Surface,同样调用EGL的eglSwapBuffers方法,将其内容显示到屏幕上
配合代码享用更佳:
java
/**
* BlitFramebuffer方式
*
* @param recordWindowSurface
* @return
*/
private boolean drawBlitFrameBuffer(WindowSurface recordWindowSurface) {
boolean swapResult;
// 先绘制到屏幕上
mPreviewTexture.getTransformMatrix(mDisplayProjectionMatrix);
mCameraFilter.draw(mTextureId, mDisplayProjectionMatrix);
mVideoEncoder.frameAvailable();
// 把屏幕Surface渲染数据拷贝到视频Surface中
// 该方式的效率是最高的,一次渲染输出给多个目标,但是只有OpenGL3.0才有该方法
recordWindowSurface.makeCurrentReadFrom(mWindowSurface);
GLES30.glBlitFramebuffer(
0, 0, mWindowSurface.getWidth(), mWindowSurface.getHeight(),
0, 0, mVideoEncoder.getVideoWidth(), mVideoEncoder.getVideoHeight(),
GLES30.GL_COLOR_BUFFER_BIT, GLES30.GL_NEAREST);
int err;
if ((err = GLES30.glGetError()) != GLES30.GL_NO_ERROR) {
Log.w(TAG, "ERROR: glBlitFramebuffer failed: 0x" +
Integer.toHexString(err));
}
recordWindowSurface.setPresentationTime(mPreviewTexture.getTimestamp());
recordWindowSurface.swapBuffers();
// 切换为屏幕Surface
mWindowSurface.makeCurrent();
swapResult = mWindowSurface.swapBuffers();
return swapResult;
}
该方式的效率是最高的,一次渲染输出给多个目标,但是只有OpenGL3.0才有该方法
最后
MediaCodec硬编码总算是讲完了,我们用了3个章节由浅入深的介绍了Android下是如何硬编码的,对YUV数据进行编码,也能使用OpenGL ES渲染到Surface上进行编码。但这仅仅只是一个开始,还有更多的内容需要深入学习、探讨。
不知不觉Camera
和OpenGL ES
系列都已经写了八个章节了,不管有多少人看,首先写这些系列是对自己的一个交代,也是对过去的总结,更是对未来的憧憬。希望自己不断前行,变得更好,也希望这个世界变得更好。
lib-camera库包结构如下:
包 | 说明 |
---|---|
camera | camera相关操作功能包,包括Camera和Camera2。以及各种预览视图 |
encoder | MediaCdoec录制视频相关,包括对ByteBuffer和Surface的录制 |
gles | opengles操作相关 |
permission | 权限相关 |
util | 工具类 |
每个包都可独立使用做到最低的耦合,方便白嫖
github地址:https://github.com/xiaozhi003/AndroidCamera,如果对你有帮助可以star下,万分感谢^_^
参考: