上一篇讲了用OpenGL纹理渲染摄像头数据,这一篇打算利用上一篇的开发成果,录制视频。
一、FBO
前面讲的利用OpenGL纹理渲染的Camera数据是直接显示到屏幕的,但是在直播推流时候是不能这么做的,这时候得把用OpenGL渲染出来的数据放到一个容器中,然后用这个容器的数据去显示到屏幕,或者录制,或者去直播推流就行了。这个容器就是这里要介绍的FBO(Frame Buffer object)
,离屏缓存。
我们需要对纹理进行多次渲染采样时,而这些渲染采样是不需要展示给用户看的,所以我们就可以用一个单独的缓冲对象(离屏渲染)来存储我们的这几次渲染采样的结果,等处理完后才显示到窗口上。文章最后参考部分,有关于FBO的文章,可以参考学习下。
1.1、创建FBO
@Override
public void setSize(int width, int height) {
super.setSize(width, height);
releaseFrame();
//frameBuffer 是图层的一个引用
frameBuffer = new int[1];
// 实例化fbo,让摄像头的数据先渲染到fbo
GLES20.glGenFramebuffers(1, frameBuffer, 0);
// 生成一个纹理,相当于图层
frameTextures = new int[1];
GLES20.glGenTextures(frameTextures.length, frameTextures, 0);
// 配置纹理
for (int i = 0; i < frameTextures.length; i++) {
//绑定纹理,后续配置纹理,开始操作纹理,后续操作是原子操作
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, frameTextures[i]);
// GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER,
// GLES20.GL_NEAREST);//放大过滤
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER,
GLES20.GL_LINEAR);//缩小过滤,一般建议这种
//告诉,gpu操作完了
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
}
// 开始做绑定操作
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, frameTextures[0]);
/**
* 指定一个二维的纹理图片显示方式
* level
* 指定细节级别,0级表示基本图像,n级则表示Mipmap缩小n级之后的图像(缩小2^n)
* internalformat
* 指定纹理内部格式,必须是下列符号常量之一:GL_ALPHA,GL_LUMINANCE,GL_LUMINANCE_ALPHA,GL_RGB,GL_RGBA。
* width height
* 指定纹理图像的宽高,所有实现都支持宽高至少为64 纹素的2D纹理图像和宽高至少为16 纹素的立方体贴图纹理图像 。
* border
* 指定边框的宽度。必须为0。
* format
* 指定纹理数据的格式。必须匹配internalformat。下面的符号值被接受:GL_ALPHA,GL_RGB,GL_RGBA,GL_LUMINANCE,和GL_LUMINANCE_ALPHA。
* type
* 指定纹理数据的数据类型。下面的符号值被接受:GL_UNSIGNED_BYTE,GL_UNSIGNED_SHORT_5_6_5,GL_UNSIGNED_SHORT_4_4_4_4,和GL_UNSIGNED_SHORT_5_5_5_1。
* data
* 指定一个指向内存中图像数据的指针。
*/
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height, 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE,
null);
//要开始使用 gpu的fbo,数据区域gpu
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, frameBuffer[0]); //綁定FBO
//真正发生绑定fbo和纹理(图层)
GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, frameTextures[0], 0);
//释放纹理图层
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
//释放fbo
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
}
1.2、绘制到FBO
@Override
public int onDraw(int texture) {
// 数据渲染到fbo中,输出设备就是fbo
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, frameBuffer[0]);
super.onDraw(texture);
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
return frameTextures[0] ;
}
在这个部分绘制的屏幕数据就是到了FBO所在图层。拿到绘制到FBO的图层索引frameTextures[0],然后不加glBindFramebuffer
绑定,就可以绘制到屏幕上。
二、自定义EGL线程
上面数据渲染到了FBO
,接着就可以从FBO
拿到数据,然后录制成视频。因为需要调用OpenGL相关操作,所以这里需要自定义EGL
线程,GLSurfaceView里面默认有一个GLThread线程,已经绑定了EGL。
OpenGL 是跨平台的、专业的图形编程接口,而接口的实现是由厂商来完成的。而当我们使用这组接口完成绘制之后,要把结果显示在屏幕上,就要用到 EGL 来完成这个转换工作。EGL是OpenGL ES 渲染API和本地窗口系统(native platform window system)之间的一个中间接口层,它也主要由厂商来实现。
关于EGL的相关使用,可以参考最后的文章。
public class EGLEnv {
private EGLDisplay mEglDisplay;
// 调用 oepngl 的函数
private EGLContext mEglContext;
private final EGLConfig mEglConfig;
private final EGLSurface mEglSurface;
private ScreenFilter screenFilter;
//mediacodec提供的场地,变成egl线程
public EGLEnv(Context context, EGLContext mGlContext, Surface surface, int width, int height) {
// 获得显示窗口,作为OpenGL的绘制目标
mEglDisplay= EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
if (mEglDisplay == EGL14.EGL_NO_DISPLAY) {
throw new RuntimeException("eglGetDisplay failed");
}
// 100% 固定代码
// 初始化展示窗口
int[] version = new int[2];
if(!EGL14.eglInitialize(mEglDisplay, version,0,version,1)) {
throw new RuntimeException("eglInitialize failed");
}
// 配置 属性选项
int[] configAttribs = {
EGL14.EGL_RED_SIZE, 8, //颜色缓冲区中红色位数
EGL14.EGL_GREEN_SIZE, 8,//颜色缓冲区中绿色位数
EGL14.EGL_BLUE_SIZE, 8, //
EGL14.EGL_ALPHA_SIZE, 8,//
EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, //opengl es 2.0
EGL14.EGL_NONE
};
int[] numConfigs = new int[1];
EGLConfig[] configs = new EGLConfig[1];
//EGL 根据属性选择一个配置
if (!EGL14.eglChooseConfig(mEglDisplay, configAttribs, 0, configs, 0, configs.length,
numConfigs, 0)) {
throw new RuntimeException("EGL error " + EGL14.eglGetError());
}
mEglConfig = configs[0];
int[] context_attrib_list = {
EGL14.EGL_CONTEXT_CLIENT_VERSION,2,
EGL14.EGL_NONE
};
// 创建好了之后,生成上下文 你是可以读取到数据
mEglContext=EGL14.eglCreateContext(mEglDisplay, mEglConfig, mGlContext, context_attrib_list,0);
if (mEglContext == EGL14.EGL_NO_CONTEXT){
throw new RuntimeException("EGL error " + EGL14.eglGetError());
}
/**
* 创建EGLSurface
*/
int[] surface_attrib_list = {
EGL14.EGL_NONE
};
// 录屏推流
mEglSurface = EGL14.eglCreateWindowSurface(mEglDisplay, mEglConfig, surface, surface_attrib_list, 0);
// mEglSurface == null
if (mEglSurface == null){
throw new RuntimeException("EGL error " + EGL14.eglGetError());
}
/**
* 绑定当前线程的显示器display mEglDisplay 虚拟, 不是物理设备
*/
if (!EGL14.eglMakeCurrent(mEglDisplay,mEglSurface,mEglSurface,mEglContext)){
throw new RuntimeException("EGL error " + EGL14.eglGetError());
}
screenFilter = new ScreenFilter(context);
screenFilter.setSize(width,height);
}
public void draw(int textureId, long timestamp) {
screenFilter.onDraw(textureId);
// 给帧缓冲 时间戳
EGLExt.eglPresentationTimeANDROID(mEglDisplay,mEglSurface,timestamp);
//EGLSurface是双缓冲模式,交换缓冲区
EGL14.eglSwapBuffers(mEglDisplay,mEglSurface);
}
public void release(){
EGL14.eglDestroySurface(mEglDisplay,mEglSurface);
EGL14.eglMakeCurrent(mEglDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE,
EGL14.EGL_NO_CONTEXT);
EGL14.eglDestroyContext(mEglDisplay, mEglContext);
EGL14.eglReleaseThread();
EGL14.eglTerminate(mEglDisplay);
screenFilter.release();
}
}
后面在录制视频时候会向EGL提供一个Surface,当把FBO数据渲染到自定义的EGL显示设备时,后面就可以直接通过MediaCodec编码渲染数据。
三、录制视频
private void codec(boolean endOfStream) {
//endOfStream为true录制完成
if(endOfStream){
mMediaCodec.signalEndOfInputStream();
return;
}
// 编码
while (true) {
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
int index = mMediaCodec.dequeueOutputBuffer(bufferInfo, 10_000);
// 编码的地方
//需要更多数据
if (index == MediaCodec.INFO_TRY_AGAIN_LATER) {
//如果是结束那直接退出,否则继续循环
if (!endOfStream) {
break;
}
} else if (index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
//输出格式发生改变 第一次总会调用所以在这里开启混合器
MediaFormat newFormat = mMediaCodec.getOutputFormat();
track = mMuxer.addTrack(newFormat);
mMuxer.start();
} else if (index == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
//可以忽略
} else {
//调整时间戳
bufferInfo.presentationTimeUs = (long) (bufferInfo.presentationTimeUs / mSpeed);
//有时候会出现异常 : timestampUs xxx < lastTimestampUs yyy for Video track
if (bufferInfo.presentationTimeUs <= mLastTimeStamp) {
bufferInfo.presentationTimeUs = (long) (mLastTimeStamp + 1_000_000 / 25 / mSpeed);
}
mLastTimeStamp = bufferInfo.presentationTimeUs;
//正常则 index 获得缓冲区下标
ByteBuffer encodedData = mMediaCodec.getOutputBuffer(index);
//如果当前的buffer是配置信息,不管它 不用写出去
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
bufferInfo.size = 0;
}
if (bufferInfo.size != 0) {
//设置从哪里开始读数据(读出来就是编码后的数据)
encodedData.position(bufferInfo.offset);
//设置能读数据的总长度
encodedData.limit(bufferInfo.offset + bufferInfo.size);
//写出为mp4
mMuxer.writeSampleData(track, encodedData, bufferInfo);
}
// 释放这个缓冲区,后续可以存放新的编码后的数据啦
mMediaCodec.releaseOutputBuffer(index, false);
// 如果给了结束信号 signalEndOfInputStream
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
break;
}
}
}
}