当前位置:   article > 正文

Android OpenGL+Camera2渲染(5) —— 录制视频,实现快录慢录_android egl surface mediacodec视频录制

android egl surface mediacodec视频录制

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环境进行绑定。

  1. public void start(float speed, String savePath) {
  2. mSavePath = savePath;
  3. mSpeed = speed;
  4. try {
  5. MediaFormat mediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, mWidth, mHeight);
  6. mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
  7. mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, mWidth * mHeight * fps / 5);
  8. mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, fps);
  9. mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, fps);
  10. mediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
  11. mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
  12. inputSurface = mediaCodec.createInputSurface();
  13. mediaMuxer = new MediaMuxer(mSavePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
  14. //-------------配置EGL环境----------------
  15. HandlerThread handlerThread = new HandlerThread("elgCodec");
  16. handlerThread.start();
  17. mHandler = new Handler(handlerThread.getLooper());
  18. mHandler.post(new Runnable() {
  19. @Override
  20. public void run() {
  21. eglConfigBase = new EglConfigBase(mContext, mWidth, mHeight, inputSurface, eglContext);
  22. mediaCodec.start();
  23. isPlaying = true;
  24. }
  25. });
  26. } catch (IOException e) {
  27. e.printStackTrace();
  28. }
  29. }

这里传入一个speed,这个就和快录慢录有关系了,这个之后在讲。这里创建MediaCodec,通过

mediaCodec.createInputSurface()拿到了一个输入的Surface,使用 HandlerThread 创建了一个在子线程中处理的Handler,在这个线程中创建EGL环境。

eglConfigBase = new EglConfigBase(mContext, mWidth, mHeight, inputSurface, eglContext);

这里传入的 eglContext 其实就是美颜大眼的EGLContext,通过它产生关联。

  1. /**
  2. * @param context
  3. * @param width
  4. * @param height
  5. * @param surface MediaCodec创建的surface 我们需要将其贴到我们的虚拟屏幕上去
  6. * @param eglContext GLThread的EGL上下文
  7. */
  8. public EglConfigBase(Context context, int width, int height, Surface surface, EGLContext eglContext) {
  9. //配置EGL环境
  10. createEGL(eglContext);
  11. //把Surface贴到 mEglDisplay ,发生关系
  12. int[] attrib_list = {
  13. EGL14.EGL_NONE
  14. };
  15. // 绘制线程中的图像 就是往这个mEglSurface 上面去画
  16. mEglSurface = EGL14.eglCreateWindowSurface(mEglDisplay, mEglConfig, surface, attrib_list, 0);
  17. // 绑定当前线程的显示设备及上下文, 之后操作opengl,就是在这个虚拟显示上操作
  18. if (!EGL14.eglMakeCurrent(mEglDisplay,mEglSurface,mEglSurface,mEglContext)) {
  19. throw new RuntimeException("eglMakeCurrent 失败!");
  20. }
  21. //像虚拟屏幕画
  22. mScreenFilter = new ScreenFilter(context);
  23. mScreenFilter.onReady(width,height);
  24. }
  1. private void createEGL(EGLContext eglContext) {
  2. //创建 虚拟显示器
  3. mEglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
  4. if (mEglDisplay == EGL14.EGL_NO_DISPLAY) {
  5. throw new RuntimeException("eglGetDisplay failed");
  6. }
  7. //初始化显示器
  8. int[] version = new int[2];
  9. // 12.1020203
  10. //major:主版本 记录在 version[0]
  11. //minor : 子版本 记录在 version[1]
  12. if (!EGL14.eglInitialize(mEglDisplay, version, 0, version, 1)) {
  13. throw new RuntimeException("eglInitialize failed");
  14. }
  15. // egl 根据我们配置的属性 选择一个配置
  16. int[] attrib_list = {
  17. EGL14.EGL_RED_SIZE, 8, // 缓冲区中 红分量 位数
  18. EGL14.EGL_GREEN_SIZE, 8,
  19. EGL14.EGL_BLUE_SIZE, 8,
  20. EGL14.EGL_ALPHA_SIZE, 8,
  21. EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, //egl版本 2
  22. EGL14.EGL_NONE
  23. };
  24. EGLConfig[] configs = new EGLConfig[1];
  25. int[] num_config = new int[1];
  26. // attrib_list:属性列表+属性列表的第几个开始
  27. // configs:获取的配置 (输出参数)
  28. //num_config: 长度和 configs 一样就行了
  29. if (!EGL14.eglChooseConfig(mEglDisplay, attrib_list, 0,
  30. configs, 0, configs.length, num_config, 0)) {
  31. throw new IllegalArgumentException("eglChooseConfig#2 failed");
  32. }
  33. mEglConfig = configs[0];
  34. int[] ctx_attrib_list = {
  35. EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, //egl版本 2
  36. EGL14.EGL_NONE
  37. };
  38. //创建EGL上下文
  39. // 3 share_context: 共享上下文 传绘制线程(GLThread)中的EGL上下文 达到共享资源的目的 发生关系
  40. mEglContext = EGL14.eglCreateContext(mEglDisplay, mEglConfig, eglContext, ctx_attrib_list
  41. , 0);
  42. // 创建失败
  43. if (mEglContext == EGL14.EGL_NO_CONTEXT) {
  44. throw new RuntimeException("EGL Context Error.");
  45. }
  46. }

这里的代码可以参考GlSurfaceView中的配置去写,发生关系就是在

mEglSurface = EGL14.eglCreateWindowSurface(mEglDisplay, mEglConfig, surface, attrib_list, 0);

这句产生的关联。

 

这里还创建了 ScreenFilter,用它 把外部的纹理信息(美颜滤镜后的纹理)写入到MediaCodec的Surface中。

看一下 EglConfigBase对外部提供的 draw 方法。

  1. public void draw(int textureId, long timestamp) {
  2. if (!EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, mCurrentEglContext)) {
  3. throw new RuntimeException("eglMakeCurrent 失败!");
  4. }
  5. screenFilter.onDrawFrame(textureId);
  6. EGLExt.eglPresentationTimeANDROID(eglDisplay, eglSurface, timestamp);
  7. //交换数据,输出到mediacodec InputSurface中
  8. EGL14.eglSwapBuffers(eglDisplay, eglSurface);
  9. }

 在写之前,需要绑定当前的EGL 环境,这里需要传入时间戳,在调用了screenFilter.onDrawFrame(textureId); 方法之后,需要刷新eglSurface的时间戳, eglSwapBuffers 交换数据, EGL的工作模式是双缓存模式, 内部有两个frame buffer (fb),当EGL将一个fb 显示屏幕上,另一个就在后台等待opengl进行交换。所以draw完之后,需要把后台的buffer显示到前台。

在 GlRenderWrapper 的 onDrawFrame

  1. @Override
  2. public void onDrawFrame(GL10 gl) {
  3. int textureId;
  4. // 配置屏幕
  5. //清理屏幕 :告诉opengl 需要把屏幕清理成什么颜色
  6. GLES20.glClearColor(0, 0, 0, 0);
  7. //执行上一个:glClearColor配置的屏幕颜色
  8. GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
  9. //更新获取一张图
  10. mSurfaceTexture.updateTexImage();
  11. mSurfaceTexture.getTransformMatrix(mtx);
  12. //cameraFiler需要一个矩阵,是Surface和我们手机屏幕的一个坐标之间的关系
  13. cameraFilter.setMatrix(mtx);
  14. textureId = cameraFilter.onDrawFrame(mTextures[0]);
  15. if (bigEyeEnable) {
  16. bigeyeFilter.setFace(tracker.mFace);
  17. textureId = bigeyeFilter.onDrawFrame(textureId);
  18. }
  19. if (beautyEnable) {
  20. textureId = beaytyFilter.onDrawFrame(textureId);
  21. }
  22. if (stickEnable) {
  23. stickerFilter.setFace(tracker.mFace);
  24. textureId = stickerFilter.onDrawFrame(textureId);
  25. }
  26. int id = screenFilter.onDrawFrame(textureId);
  27. //进行录制
  28. avcRecorder.encodeFrame(id, mSurfaceTexture.getTimestamp());
  29. }

最后就是把纹理ID,拿去录制。

encodeFrame

  1. public void encodeFrame(final int textureId, final long timeStamp) {
  2. if (!isPlaying) return;
  3. mHandler.post(new Runnable() {
  4. @Override
  5. public void run() {
  6. eglConfigBase.draw(textureId, timeStamp);
  7. getCodec(false);
  8. }
  9. });
  10. }

这里的Handler 就是刚刚创建EGL环境的Handler,所有此EGL环境中的处理都需要在此Handler线程中进行。执行eglConfigBase.draw 去写入到MediaCodec的Surface中。

getCodec 就是负责去从MediaCodec的输出队列中,拿编码完成的数据,交给MediaMuxer进行保存。

 

  1. private void getCodec(boolean endOfStream) {
  2. if (endOfStream) {
  3. mediaCodec.signalEndOfInputStream();
  4. }
  5. MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
  6. int status = mediaCodec.dequeueOutputBuffer(bufferInfo, 10_000);
  7. //表示请求超时,10_000毫秒内没有数据到来
  8. if (status == MediaCodec.INFO_TRY_AGAIN_LATER) {
  9. } else if (status == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
  10. //编码格式改变 ,第一次start 都会调用一次
  11. MediaFormat outputFormat = mediaCodec.getOutputFormat();
  12. //设置mediaMuxer 的视频轨
  13. avcIndex = mediaMuxer.addTrack(outputFormat);
  14. mediaMuxer.start();
  15. } else if (status == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
  16. //Outputbuffer 改变了
  17. } else {
  18. ByteBuffer outputBuffer = mediaCodec.getOutputBuffer(status);
  19. if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
  20. bufferInfo.size = 0;
  21. }
  22. if (bufferInfo.size > 0) {
  23. bufferInfo.presentationTimeUs = (long) (bufferInfo.presentationTimeUs / mSpeed);
  24. outputBuffer.position(bufferInfo.offset);
  25. outputBuffer.limit(bufferInfo.size - bufferInfo.offset);
  26. //交给mediaMuxer 保存
  27. mediaMuxer.writeSampleData(avcIndex, outputBuffer, bufferInfo);
  28. }
  29. mediaCodec.releaseOutputBuffer(status, false);
  30. if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
  31. }
  32. }
  33. }

在第28行, bufferInfo.presentationTimeUs = (long) (bufferInfo.presentationTimeUs / mSpeed); 把正常的pts时间戳做一个转换,这里的pts是一个时间段的相对时间戳,如果要实现快录,speed设置一个小于1的值,慢录就设置一个大于1的值。在这里上面提到startRecord的时候会传递进来一个mSpeed,是这样设置的。

 

  1. public void startRecord() {
  2. float speed = 1.f;
  3. switch (mSpeed) {
  4. case MODE_EXTRA_SLOW:
  5. speed = 0.3f;
  6. break;
  7. case MODE_SLOW:
  8. speed = 0.5f;
  9. break;
  10. case MODE_NORMAL:
  11. speed = 1.f;
  12. break;
  13. case MODE_FAST:
  14. speed = 1.5f;
  15. break;
  16. case MODE_EXTRA_FAST:
  17. speed = 3.f;
  18. break;
  19. }
  20. glRender.startRecord(speed, savePath);
  21. }

 

停止录制调用 stop()

  1. public void stop() {
  2. isPlaying = false;
  3. mHandler.post(new Runnable() {
  4. @Override
  5. public void run() {
  6. getCodec(true);
  7. mediaCodec.stop();
  8. mediaCodec.release();
  9. mediaCodec = null;
  10. mediaMuxer.stop();
  11. mediaMuxer.release();
  12. mediaMuxer = null;
  13. eglConfigBase.release();
  14. eglConfigBase = null;
  15. inputSurface.release();
  16. inputSurface = null;
  17. mHandler.getLooper().quitSafely();
  18. mHandler = null;
  19. if (onRecordListener != null) {
  20. onRecordListener.recordFinish(mSavePath);
  21. }
  22. }
  23. });
  24. }

 

github项目地址:https://github.com/wangchao0837/OpenGlCameraRender

声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号