NDK OpenGL仿抖音极快极慢录制特效视频

NDK​系列之OpenGL仿抖音极快极慢录制特效视频,本节主要是在上一节OpenGL代码架构上增加极快极慢等特效的视频录制功能。

实现效果:

NDK OpenGL仿抖音极快极慢录制特效视频_第1张图片

实现逻辑:

NDK OpenGL仿抖音极快极慢录制特效视频_第2张图片

在上一节的特效效果的基础上,使用MediaCodec和自定义EGL,将效果视频录制保存到本地.mp4文件。

本节主要内容:

1.MediaCode介绍;

2.EGL的理解;

3.自定义MyMediaRecord;

4.自定义MyEGL;

源码:

NdkOpenGLPlay: NDK OpenGL渲染画面效果

一、MediaCode

MediaCodec是Android 4.1.2(API 16)提供的一套编解码API。

1)它的使用非常简单,它存在一个输入缓冲区与一个输出缓冲区,在编码时我们将数据塞入输入缓冲区,然后从输出缓冲区取出编码完成后的数据就可以了。

NDK OpenGL仿抖音极快极慢录制特效视频_第3张图片

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渲染画面效果

你可能感兴趣的:(NDK,音视频,OpenGL,NDK)