Android OpenGL+Camera2渲染(1) —— OpenGL简单介绍
Android OpenGL+Camera2渲染(2) —— OpenGL实现Camera2图像预览
Android OpenGL+Camera2渲染(3) —— 大眼,贴纸功能实现
Android OpenGL+Camera2渲染(4) —— 美颜功能实现
Android OpenGL+Camera2渲染(5) —— 录制视频,实现快录慢录
这篇文章是在前面几篇的基础上实现的,把美颜,大眼,贴纸的效果,使用MediaCodec编码后录制成视频,支持快录和慢录的功能。
首先 我们进过处理之后的数据是存在纹理当中的,而MediaCodec 编码的输入队列支持把surface作为输入源,那么这两者如何关联呢? 方案是这样的。 自己配置一个EGL环境,把这个EGL环境中的EGLSurface和mediacodec的surface绑定,把当前的EGL环境和我们处理美颜滤镜的EGL绑定,这样就可以在当前的EGL环境中操作美颜滤镜的EGL环境中的纹理了。然后把数据输出的MediaCodec的surface中。
在开始录制的时候,取初始化编码器MediaCodec、MediaMuxer 和 配置EGL环境进行绑定。
public void start(float speed, String savePath) {
mSavePath = savePath;
mSpeed = speed;
try {
MediaFormat mediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, mWidth, mHeight);
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, mWidth * mHeight * fps / 5);
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, fps);
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, fps);
mediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
inputSurface = mediaCodec.createInputSurface();
mediaMuxer = new MediaMuxer(mSavePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
//-------------配置EGL环境----------------
HandlerThread handlerThread = new HandlerThread("elgCodec");
handlerThread.start();
mHandler = new Handler(handlerThread.getLooper());
mHandler.post(new Runnable() {
@Override
public void run() {
eglConfigBase = new EglConfigBase(mContext, mWidth, mHeight, inputSurface, eglContext);
mediaCodec.start();
isPlaying = true;
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
这里传入一个speed,这个就和快录慢录有关系了,这个之后在讲。这里创建MediaCodec,通过
mediaCodec.createInputSurface()拿到了一个输入的Surface,使用 HandlerThread 创建了一个在子线程中处理的Handler,在这个线程中创建EGL环境。
这里传入的 eglContext 其实就是美颜大眼的EGLContext,通过它产生关联。
/**
* @param context
* @param width
* @param height
* @param surface MediaCodec创建的surface 我们需要将其贴到我们的虚拟屏幕上去
* @param eglContext GLThread的EGL上下文
*/
public EglConfigBase(Context context, int width, int height, Surface surface, EGLContext eglContext) {
//配置EGL环境
createEGL(eglContext);
//把Surface贴到 mEglDisplay ,发生关系
int[] attrib_list = {
EGL14.EGL_NONE
};
// 绘制线程中的图像 就是往这个mEglSurface 上面去画
mEglSurface = EGL14.eglCreateWindowSurface(mEglDisplay, mEglConfig, surface, attrib_list, 0);
// 绑定当前线程的显示设备及上下文, 之后操作opengl,就是在这个虚拟显示上操作
if (!EGL14.eglMakeCurrent(mEglDisplay,mEglSurface,mEglSurface,mEglContext)) {
throw new RuntimeException("eglMakeCurrent 失败!");
}
//像虚拟屏幕画
mScreenFilter = new ScreenFilter(context);
mScreenFilter.onReady(width,height);
}
private void createEGL(EGLContext eglContext) {
//创建 虚拟显示器
mEglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
if (mEglDisplay == EGL14.EGL_NO_DISPLAY) {
throw new RuntimeException("eglGetDisplay failed");
}
//初始化显示器
int[] version = new int[2];
// 12.1020203
//major:主版本 记录在 version[0]
//minor : 子版本 记录在 version[1]
if (!EGL14.eglInitialize(mEglDisplay, version, 0, version, 1)) {
throw new RuntimeException("eglInitialize failed");
}
// egl 根据我们配置的属性 选择一个配置
int[] attrib_list = {
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, //egl版本 2
EGL14.EGL_NONE
};
EGLConfig[] configs = new EGLConfig[1];
int[] num_config = new int[1];
// attrib_list:属性列表+属性列表的第几个开始
// configs:获取的配置 (输出参数)
//num_config: 长度和 configs 一样就行了
if (!EGL14.eglChooseConfig(mEglDisplay, attrib_list, 0,
configs, 0, configs.length, num_config, 0)) {
throw new IllegalArgumentException("eglChooseConfig#2 failed");
}
mEglConfig = configs[0];
int[] ctx_attrib_list = {
EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, //egl版本 2
EGL14.EGL_NONE
};
//创建EGL上下文
// 3 share_context: 共享上下文 传绘制线程(GLThread)中的EGL上下文 达到共享资源的目的 发生关系
mEglContext = EGL14.eglCreateContext(mEglDisplay, mEglConfig, eglContext, ctx_attrib_list
, 0);
// 创建失败
if (mEglContext == EGL14.EGL_NO_CONTEXT) {
throw new RuntimeException("EGL Context Error.");
}
}
这里的代码可以参考GlSurfaceView中的配置去写,发生关系就是在
mEglSurface = EGL14.eglCreateWindowSurface(mEglDisplay, mEglConfig, surface, attrib_list, 0);
这句产生的关联。
这里还创建了 ScreenFilter,用它 把外部的纹理信息(美颜滤镜后的纹理)写入到MediaCodec的Surface中。
public void draw(int textureId, long timestamp) {
if (!EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, mCurrentEglContext)) {
throw new RuntimeException("eglMakeCurrent 失败!");
}
screenFilter.onDrawFrame(textureId);
EGLExt.eglPresentationTimeANDROID(eglDisplay, eglSurface, timestamp);
//交换数据,输出到mediacodec InputSurface中
EGL14.eglSwapBuffers(eglDisplay, eglSurface);
}
在写之前,需要绑定当前的EGL 环境,这里需要传入时间戳,在调用了screenFilter.onDrawFrame(textureId); 方法之后,需要刷新eglSurface的时间戳, eglSwapBuffers 交换数据, EGL的工作模式是双缓存模式, 内部有两个frame buffer (fb),当EGL将一个fb 显示屏幕上,另一个就在后台等待opengl进行交换。所以draw完之后,需要把后台的buffer显示到前台。
在 GlRenderWrapper 的 onDrawFrame
@Override
public void onDrawFrame(GL10 gl) {
int textureId;
// 配置屏幕
//清理屏幕 :告诉opengl 需要把屏幕清理成什么颜色
GLES20.glClearColor(0, 0, 0, 0);
//执行上一个:glClearColor配置的屏幕颜色
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
//更新获取一张图
mSurfaceTexture.updateTexImage();
mSurfaceTexture.getTransformMatrix(mtx);
//cameraFiler需要一个矩阵,是Surface和我们手机屏幕的一个坐标之间的关系
cameraFilter.setMatrix(mtx);
textureId = cameraFilter.onDrawFrame(mTextures[0]);
if (bigEyeEnable) {
bigeyeFilter.setFace(tracker.mFace);
textureId = bigeyeFilter.onDrawFrame(textureId);
}
if (beautyEnable) {
textureId = beaytyFilter.onDrawFrame(textureId);
}
if (stickEnable) {
stickerFilter.setFace(tracker.mFace);
textureId = stickerFilter.onDrawFrame(textureId);
}
int id = screenFilter.onDrawFrame(textureId);
//进行录制
avcRecorder.encodeFrame(id, mSurfaceTexture.getTimestamp());
}
最后就是把纹理ID,拿去录制。
public void encodeFrame(final int textureId, final long timeStamp) {
if (!isPlaying) return;
mHandler.post(new Runnable() {
@Override
public void run() {
eglConfigBase.draw(textureId, timeStamp);
getCodec(false);
}
});
}
这里的Handler 就是刚刚创建EGL环境的Handler,所有此EGL环境中的处理都需要在此Handler线程中进行。执行eglConfigBase.draw 去写入到MediaCodec的Surface中。
private void getCodec(boolean endOfStream) {
if (endOfStream) {
mediaCodec.signalEndOfInputStream();
}
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
int status = mediaCodec.dequeueOutputBuffer(bufferInfo, 10_000);
//表示请求超时,10_000毫秒内没有数据到来
if (status == MediaCodec.INFO_TRY_AGAIN_LATER) {
} else if (status == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
//编码格式改变 ,第一次start 都会调用一次
MediaFormat outputFormat = mediaCodec.getOutputFormat();
//设置mediaMuxer 的视频轨
avcIndex = mediaMuxer.addTrack(outputFormat);
mediaMuxer.start();
} else if (status == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
//Outputbuffer 改变了
} else {
ByteBuffer outputBuffer = mediaCodec.getOutputBuffer(status);
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
bufferInfo.size = 0;
}
if (bufferInfo.size > 0) {
bufferInfo.presentationTimeUs = (long) (bufferInfo.presentationTimeUs / mSpeed);
outputBuffer.position(bufferInfo.offset);
outputBuffer.limit(bufferInfo.size - bufferInfo.offset);
//交给mediaMuxer 保存
mediaMuxer.writeSampleData(avcIndex, outputBuffer, bufferInfo);
}
mediaCodec.releaseOutputBuffer(status, false);
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
}
}
}
在第28行, bufferInfo.presentationTimeUs = (long) (bufferInfo.presentationTimeUs / mSpeed); 把正常的pts时间戳做一个转换,这里的pts是一个时间段的相对时间戳,如果要实现快录,speed设置一个小于1的值,慢录就设置一个大于1的值。在这里上面提到startRecord的时候会传递进来一个mSpeed,是这样设置的。
public void startRecord() {
float speed = 1.f;
switch (mSpeed) {
case MODE_EXTRA_SLOW:
speed = 0.3f;
break;
case MODE_SLOW:
speed = 0.5f;
break;
case MODE_NORMAL:
speed = 1.f;
break;
case MODE_FAST:
speed = 1.5f;
break;
case MODE_EXTRA_FAST:
speed = 3.f;
break;
}
glRender.startRecord(speed, savePath);
}
public void stop() {
isPlaying = false;
mHandler.post(new Runnable() {
@Override
public void run() {
getCodec(true);
mediaCodec.stop();
mediaCodec.release();
mediaCodec = null;
mediaMuxer.stop();
mediaMuxer.release();
mediaMuxer = null;
eglConfigBase.release();
eglConfigBase = null;
inputSurface.release();
inputSurface = null;
mHandler.getLooper().quitSafely();
mHandler = null;
if (onRecordListener != null) {
onRecordListener.recordFinish(mSavePath);
}
}
});
}
github项目地址:https://github.com/wangchao0837/OpenGlCameraRender