NDK系列之OpenGL仿抖音极快极慢录制特效视频,本节主要是在上一节OpenGL代码架构上增加极快极慢等特效的视频录制功能。
实现效果:
实现逻辑:
在上一节的特效效果的基础上,使用MediaCodec和自定义EGL,将效果视频录制保存到本地.mp4文件。
本节主要内容:
1.MediaCode介绍;
2.EGL的理解;
3.自定义MyMediaRecord;
4.自定义MyEGL;
源码:
NdkOpenGLPlay: NDK OpenGL渲染画面效果
一、MediaCode
MediaCodec是Android 4.1.2(API 16)提供的一套编解码API。
1)它的使用非常简单,它存在一个输入缓冲区与一个输出缓冲区,在编码时我们将数据塞入输入缓冲区,然后从输出缓冲区取出编码完成后的数据就可以了。
2)除了直接操作输入缓冲区之外,还有另一种方式来告知MediaCodec需要编码的输入数据,那就是:
public native final Surface createInputSurface();
以上接口创建一个Surface,然后我们在这个Surface中作画(输入源的直接绑定),MediaCodec就能够自动的编码Surface中的画作,我们只需要从输出缓冲区取出编码完成之后的数据。
3)录制我们在另外一个线程中进行(录制线程),所以录制的EGL环境和显示的EGL环境(GLSurfaceView,显示线程)是两个独立的工作环境,他们又能够共享上下文资源:显示线程中使用的texture等,需要能够在录制线程中操作 (通过录制线程中使用OpenGL绘制到MediaCodec的Surface)。
在这个线程中我们需要自己来:
1、配置录制使用的EGL环境(参照GLSurfaceView是怎么配置的)
2、将显示的图像绘制到MediaCodec的Surface中
3、编码(H.264)与复用(封装mp4)的工作
二、EGL
通俗上讲,OpenGL是一个操作GPU的API,它通过驱动向GPU发送相关指令,控制图形渲染管线状态机的运行状态。但OpenGL需要本地视窗系统进行交互,这就需要一个中间控制层,最好与平台无关。
EGL 因此被独立的设计出来,它作为OpenGL ES和本地窗口的桥梁。EGL 是 OpenGL ES(嵌入式)和底层 Native 平台视窗系统之间的接口。EGL API 是独立于OpenGL ES各版本标准的独立API ,其主要作用是为OpenGL指令创建 Context 、绘制目标Surface 、
配置Framebuffer属性、Swap提交绘制结果等。此外,EGL为GPU厂商和OS窗口系统之间提供了一个标准配置接口。
一般来说,OpenGL ES 图形管线的状态被存储于 EGL 管理的一个Context中。而Frame Buffers 和其他绘制 Surfaces 通过 EGL API进行创建、管理和销毁。EGL 同时也控制和提供了对设备显示和可能的设备渲染配置的访问。EGL标准是C的,在Android系统Java层封装了相关API。
三、MyMediaRecord
录制视频的工具类,其内部封装了MediaCodec;
1)当自定义渲染器MyGlRenderer,回调onSurfaceCreated()函数时,初始化录制工具类;
mMediaRecorder = new MyMediaRecorder(480, 800, FileUtil.getFilePath(), eglContext,
myGLSurfaceView.getContext());
MyMediaRecord.java
public MyMediaRecorder(int width, int height, String outputPath, EGLContext eglContext, Context context) {
mWidth = width;
mHeight = height;
mOutputPath = outputPath;
mEglContext = eglContext;
mContext = context;
}
2)当自定义渲染器MyGlRenderer,回调onDrawFrame()函数时,启动录制,真正执行编码的函数;
mMediaRecorder.encodeFrame(textureId, mSurfaceTexture.getTimestamp());
MyMediaRecord.java
public void encodeFrame(final int textureId, final long timestamp) {
// 当用户点击开始录制时才需要编码,预览时不需要
if (!isStart) {
return;
}
if (mHandler != null) {
mHandler.post(new Runnable() {
@Override
public void run() {
// 画到 虚拟屏幕上
if (null != mEGL) {
mEGL.draw(textureId, timestamp);
}
// 从编码器中去除数据 编码 封装成derry_xxxx.mp4文件成果
getEncodedData(false);
}
});
}
}
/**【MediaCodec输出缓冲区】
* 获取编码后的数据,写入封装成 derry_xxxx.mp4
* @param endOfStream 流结束的标记 true代表结束,false才继续工作
*/
private void getEncodedData(boolean endOfStream) {
if (endOfStream) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
mMediaCodec.signalEndOfInputStream(); // 让MediaCodec的输入流结束
}
}
// MediaCodec的输出缓冲区
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
while (true) {
// 使输出缓冲区出列,最多阻止“timeoutUs”微秒。 // 10ms
int status = mMediaCodec.dequeueOutputBuffer(bufferInfo, 10_000);
if(status == MediaCodec.INFO_TRY_AGAIN_LATER) { // 稍后再试的意思
// endOfStream = true, 要录制,继续循环,继续取新的编码数据
if(!endOfStream){
break;
}
} else if(status == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { // 输出格式已更改
MediaFormat outputFormat = mMediaCodec.getOutputFormat();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
index = mMediaMuxer.addTrack(outputFormat);
mMediaMuxer.start();// 启动封装器
}
} else if(status == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED){ // 输出缓冲区已更改
} else {
// 成功取到一个有效数据
ByteBuffer outputBuffer = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
outputBuffer = mMediaCodec.getOutputBuffer(status);
}
if (null == outputBuffer){
throw new RuntimeException("getOutputBuffer fail");
}
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0){
bufferInfo.size = 0; // 如果是配置信息
}
// TODO 下面开始写入操作
if(bufferInfo.size != 0){
// 除以大于1的 : 加速
// 小于1 的: 减速
bufferInfo.presentationTimeUs = (long)(bufferInfo.presentationTimeUs / mSpeed);
// 可能会出现类似:TimeUs < lastTimeUs xxxxxx for video Track
if(bufferInfo.presentationTimeUs <= lastTimeUs){
bufferInfo.presentationTimeUs = (long)(lastTimeUs + 1_000_000 /25/mSpeed);
}
lastTimeUs = bufferInfo.presentationTimeUs;
// 偏移位置
outputBuffer.position(bufferInfo.offset);
// 可读写的总长度
outputBuffer.limit(bufferInfo.offset + bufferInfo.size);
try{
// 写数据
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
mMediaMuxer.writeSampleData(index, outputBuffer, bufferInfo);
}
}catch (Exception e){
e.printStackTrace();
}
}
// 释放输出缓冲区
mMediaCodec.releaseOutputBuffer(status, false);
// 编码结束
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0){
break;
}
}
} // while end
}
}
3)用户按住拍Button时,事件分发到MyGlRenderer,调用MyMediaRecord开始录制
mMediaRecorder.start(speed);
MyMediaRecord.java
public void start(float speed) throws IOException {
mSpeed = speed;
/**
* 1.创建 MediaCodec 编码器
* type: 哪种类型的视频编码器
* MIMETYPE_VIDEO_AVC: H.264
*/
mMediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
/**
* 2.配置编码器参数
*/
// 视频格式
MediaFormat videoFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, mWidth, mHeight);
// 设置码率
videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, 1500_000);
// 帧率
videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 25);
// 颜色格式 (从Surface中自适应)
videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
// 关键帧间隔
videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 20);
// 配置编码器
mMediaCodec.configure(videoFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
/**
* 3.创建输入 Surface(虚拟屏幕)
*/
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
mInputSurface = mMediaCodec.createInputSurface();
}
/**
* 4, 创建封装器
*/
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
mMediaMuxer = new MediaMuxer(mOutputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
}
/**
* 5, 配置 EGL 环境
*/
HandlerThread handlerThread = new HandlerThread("MyMediaRecorder");
handlerThread.start();
Looper looper = handlerThread.getLooper();
mHandler = new Handler(looper);
mHandler.post(new Runnable() {
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
@Override
public void run() {
mEGL = new MyEGL(mEglContext, mInputSurface, mContext, mWidth, mHeight); // 在一个新的Thread中,初始化EGL环境
mMediaCodec.start(); // 启动编码器
isStart = true;
}
});
}
4)用户松开拍Button时,事件分发到MyGlRenderer,调用MyMediaRecord录制完成,获取编码后的数据,写入封装成.mp4文件
mMediaRecorder.stop();
MyMediaRecord.java
public void stop() {
isStart = false;
if (mHandler != null) {
mHandler.post(new Runnable() {
@Override
public void run() {
getEncodedData(true); // true代表:结束工作
if (mMediaCodec != null){ // MediaCodec 释放掉
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
mMediaCodec.stop();
mMediaCodec.release();
}
mMediaCodec = null;
}
if (mMediaMuxer != null) { // 封装器 释放掉
try{
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
mMediaMuxer.stop();
mMediaMuxer.release();
}
}catch (Exception e){
e.printStackTrace();
}
mMediaMuxer = null;
}
if (mInputSurface != null) { // MediaCodec的输入画布/虚拟屏幕 释放掉
mInputSurface.release();
mInputSurface = null;
}
mEGL.release(); // EGL中间件 释放掉
mEGL = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
mHandler.getLooper().quitSafely();
mHandler = null;
}
}
});
}
}
四、MyEGL
管理EGL环境的工具类
1)用户按住拍Button时,事件分发到MyMediaRecord,初始化EGL环境
mEGL = new MyEGL(mEglContext, mInputSurface, mContext, mWidth, mHeight);
MyEGL.java
public MyEGL(EGLContext eglContext, Surface surface, Context context, int width, int height) {
// 第一大步:创建EGL环境
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
createEGL(eglContext);
}
// 第二大步:创建窗口(画布),绘制线程中的图像,直接往这里创建的mEGLSurface上面画
int[] attrib_list = {EGL_NONE }; // 一定要有结尾符
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
mEGLSurface = eglCreateWindowSurface(mEGLDisplay, // EGL显示链接
mEGLConfig, // EGL最终选择配置的成果
surface, // MediaCodec的输入Surface画布
attrib_list, // 无任何配置,但也必须要传递 结尾符,否则人家没法玩
0 // attrib_list的零下标开始读取
); // 【关联的关键操作,关联(EGL显示链接)(EGL配置)(MediaCodec的输入Surface画布)】
}
// 第三大步:让 画布 盖住屏幕( 让 mEGLDisplay(EGL显示链接) 和 mEGLSurface(EGL的独有画布) 发生绑定关系)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
if(!eglMakeCurrent(mEGLDisplay, // EGL显示链接
mEGLSurface, // EGL的独有画布 用来画
mEGLSurface, // EGL的独有画布 用来读
mEGLContext // EGL的上下文
)){
throw new RuntimeException("eglMakeCurrent fail");
}
}
// 4,往虚拟屏幕上画画
mScreenFilter = new ScreenFilter(context);
mScreenFilter.onReady(width, height);
}
private void createEGL(EGLContext share_eglContext) {
// 1.获取EGL显示设备: EGL_DEFAULT_DISPLAY(代表 默认的设备 手机屏幕)
mEGLDisplay = eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
// 2.初始化设备
int[] version = new int[2]; // 主版本号,主版本号的位置下标,副版本号,副版本号的位置下标
if(!eglInitialize(mEGLDisplay, version, 0, version, 1)){
throw new RuntimeException("eglInitialize fail");
}
// 3.选择配置
int[] attrib_list = {
// key 像素格式 rgba
EGL_RED_SIZE, 8, // value 颜色深度都设置为八位
EGL_GREEN_SIZE, 8, // value 颜色深度都设置为八位
EGL_BLUE_SIZE, 8, // value 颜色深度都设置为八位
EGL_ALPHA_SIZE, 8, // value 颜色深度都设置为八位
// key 指定渲染api类型
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, // value EGL 2.0版本号
EGLExt.EGL_RECORDABLE_ANDROID, 1, // 告诉egl 以android兼容方式创建 surface
EGL_NONE // 一定要有结尾符
};
EGLConfig[] configs = new EGLConfig[1];
int[] num_config = new int[1];
if(!eglChooseConfig(
mEGLDisplay, // EGL显示链接
attrib_list, // 属性列表
0 , // attrib_list 从数组第零个下标开始找
configs, // 输出的配置选项成果
0, // configs 从数组第零个下标开始找
configs.length, // 配置的数量,只有一个
num_config, // 需要的配置int数组,他需要什么就给他什么
0 // num_config 从数组第零个下标开始找
)){
throw new RuntimeException("eglChooseConfig fail");
}
mEGLConfig = configs[0]; // 最终EGL选择配置的成果 保存起来
// 4.创建上下文
int[] ctx_attrib_list = {
EGL_CONTEXT_CLIENT_VERSION, 2, // EGL 上下文客户端版本 2.0
EGL_NONE // 一定要有结尾符
};
mEGLContext = eglCreateContext(
mEGLDisplay, // EGL显示链接
mEGLConfig, // EGL最终选择配置的成果
share_eglContext, // 共享上下文, 绘制线程 GLThread 中 EGL上下文,达到资源共享
ctx_attrib_list, // 传入上面的属性配置项
0);
if(null == mEGLContext || mEGLContext == EGL_NO_CONTEXT){
mEGLContext = null;
throw new RuntimeException("eglCreateContext fail");
}
}
2)当自定义渲染器MyGlRenderer,回调onDrawFrame()函数时,启动录制,调用EGL在虚拟屏幕上渲染,交换缓冲区数据
mEGL.draw(textureId, timestamp);
MyEGL.java
public void draw(int textureId, long timestamp){
// 在虚拟屏幕上渲染(为什么还要写一次同样的代码?答:这个是在EGL的专属线程中的)
mScreenFilter.onDrawFrame(textureId);
// 刷新时间戳(如果设置不合理,编码时会采取丢帧或降低视频质量方式进行编码)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
EGLExt.eglPresentationTimeANDROID(mEGLDisplay, mEGLSurface, timestamp);
}
// 交换缓冲区数据
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
eglSwapBuffers(mEGLDisplay, mEGLSurface); // 绘制操作
}
}
至此,OpenGL仿抖音极快极慢录制特效视频保存本地.mp4文件功能已完成。
源码:
NdkOpenGLPlay: NDK OpenGL渲染画面效果