当前位置:   article > 正文

OpenGL ES短视频开发(MediaCodec编码)

handlerthread("videocodec")

这一章节进行视频录制,选用MediaCodec, ffmpeg软编效率比较低,这里采用MediaCodec进行编码。

MediaCodec

MediaCodec是Android 4.1.2(API 16)提供的一套编解码API。它的使用非常简单,它存在一个输入缓冲区与一个输出缓冲区,在编码时我们将数据塞入输入缓冲区,然后从输出缓冲区取出编码完成后的数据就可以了。

除了直接操作输入缓冲区之外,还有另一种方式来告知MediaCodec需要编码的数据,那就是:

  1. public native final Surface createInputSurface();
  2. 复制代码

使用此接口创建一个Surface,然后我们在这个Surface中"作画",MediaCodec就能够自动的编码Surface中的“画作”,我们只需要从输出缓冲区取出编码完成之后的数据即可。

此前,我们使用OpenGL进行绘画显示在屏幕上,然而想要复制屏幕图像到cpu内存中却不是一件非常轻松的事情。所以我们可以直接将OpenGL显示到屏幕中的图像,同时绘制到MediaCodec#createInputSurface当中去。

PBO(Pixel Buffer Object,像素缓冲对象)通过直接的内存访问(Direct Memory Access,DMA)高速的复制屏幕图像像素数据到CPU内存,但这里我们直接使用createInputSurface更简单......

录制我们在另外一个线程中进行(录制现场),所以录制的EGL环境和显示的EGL环境(GLSurfaceView,显示线程)是两个独立的工作环境,他们又能够共享上下文资源:显示线程中使用的texture等,需要能够在录制线程中操作(通过录制线程中使用OpenGL绘制到MediaCodec的Surface)。

在这个线程中我们需要自己来:

1、配置录制使用的EGL环境(参照GLSurfaceView是怎么配置的)

2、完成将显示的图像绘制到MediaCodec的Surface中

3、编码(H.264)与复用(封装mp4)的工作

视频录制

处理录制Button的回调

  1. recordButton.setOnRecordListener(new RecordButton.OnRecordListener() {
  2. @Override
  3. public void onRecordStart() {
  4. douyinView.startRecord();
  5. }
  6. @Override
  7. public void onRecordStop() {
  8. douyinView.stopRecord();
  9. }
  10. });
  11. 复制代码

然后Douyinview通过 Render中来录制

  1. public void startRecord(float speed) {
  2. try {
  3. mMediaRecorder.start(speed);
  4. } catch (IOException e) {
  5. e.printStackTrace();
  6. }
  7. }
  8. public void stopRecord() {
  9. mMediaRecorder.stop();
  10. }
  11. 复制代码

因为在OpenGL显示到屏幕中的图像的同时绘制到MediaCodec#createInputSurface当中,而这里我们没有GLSurfaceView的EGL环境,所以我们需要自己创建一套EGL环境。

创建编码器MediaRecorder处理类,出入帧率,码率。

  1. /**
  2. * @param context 上下文
  3. * @param path 保存视频的地址
  4. * @param width 视频宽
  5. * @param height 视频高
  6. * 还可以让人家传递帧率 fps、码率等参数
  7. */
  8. public MediaRecorder(Context context, String path, int width, int height, EGLContext eglContext){
  9. mContext = context.getApplicationContext();
  10. mPath = path;
  11. mWidth = width;
  12. mHeight = height;
  13. mEglContext = eglContext;
  14. }
  15. 复制代码

给编码器传参:这里的码率、帧率直接写死的。

  1. /**
  2. * 开始录制视频
  3. */
  4. public void start(float speed) throws IOException{
  5. mSpeed = speed;
  6. /**
  7. * 配置MediaCodec 编码器
  8. */
  9. //视频格式
  10. // 类型(avc高级编码 h264) 编码出的宽、高
  11. MediaFormat mediaFormat = MediaFormat.createVideoFormat(
  12. MediaFormat.MIMETYPE_VIDEO_AVC, mWidth, mHeight);
  13. //参数配置
  14. // 1500kbs码率
  15. mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 1500_000);
  16. //帧率
  17. mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 20);
  18. //关键帧间隔
  19. mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 20);
  20. //颜色格式(RGB\YUV)
  21. //从surface当中回去
  22. mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.
  23. CodecCapabilities.COLOR_FormatSurface);
  24. //编码器
  25. mMediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
  26. //将参数配置给编码器
  27. mMediaCodec.configure(mediaFormat, null, null, MediaCodec.
  28. CONFIGURE_FLAG_ENCODE);
  29. //交给虚拟屏幕 通过opengl 将预览的纹理 绘制到这一个虚拟屏幕中
  30. //这样MediaCodec 就会自动编码 inputSurface 中的图像
  31. mInputSurface = mMediaCodec.createInputSurface();
  32. 。。。。。。。
  33. }
  34. 复制代码

这样就创建了InputSurface,Mediacodec往这里写数据。

播放的时候我们的顺序是 解封装 ——>解码——>渲染, 所以我们编码完成后,还需要处理对应的封装操作:在GLThread线程中把数据交给我们的虚拟屏幕环境,这里我们通过 HandlerThread拿去Looper给到Handler进行

GLThread跟我们创建的这个子线程之间的通信。

  1. /**
  2. * 开始录制视频
  3. */
  4. public void start(float speed) throws IOException{
  5. // H.264
  6. // 播放:
  7. // MP4 -> 解复用 (解封装) -> 解码 -> 绘制
  8. //封装器 复用器
  9. // 一个 mp4 的封装器 将h.264 通过它写出到文件就可以了
  10. mMediaMuxer = new MediaMuxer(mPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
  11. /**
  12. * 配置EGL环境,需要在一个线程中处理,线程间通信
  13. * Handler
  14. * Handler: 子线程通知主线程
  15. * Looper.loop()
  16. */
  17. HandlerThread handlerThread = new HandlerThread("VideoCodec");
  18. handlerThread.start();
  19. Looper looper = handlerThread.getLooper();
  20. //用于其他线程 通知子线程
  21. mHandler = new Handler(looper);
  22. //子线程:EGL的绑定线程,对我们自己创建的opengl操作都在这个线程当中执行
  23. mHandler.post(new Runnable() {
  24. @Override
  25. public void run() {
  26. //创建我们的子线程,用于
  27. mEglBase = new EGLBase(mContext, mWidth, mHeight, mInputSurface, mEglContext);
  28. //启动编码器
  29. mMediaCodec.start();
  30. isStart = true;
  31. }
  32. });
  33. }
  34. 复制代码
创建EGL工作环境
  • 创建EGLContext
  • 创建用于绘制的mEglSurface
  • 双缓冲进行绘画 mEglSurface + mEglDisplay进行交替绘制

创建EGLBase来录制Opengl操作需要的EGL环境配置,传入宽、高,surface,参考GLSurfaceView的配置过程。

如代码中所示,创建mEglContext需要传入mEglDisplay、mEglConfig, mEglContext = EGL14.eglCreateContext(mEglDisplay, mEglConfig, eglContext, ctx_attrib_list, 0);

  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, 0);
  41. // 创建失败
  42. if (mEglContext == EGL14.EGL_NO_CONTEXT) {
  43. throw new RuntimeException("EGL Context Error.");
  44. }
  45. }
  46. 复制代码

创建完 EglContext,需要将surface传递到 EglDisplay中去,创建

  1. // 绘制线程中的图像 就是往这个mEglSurface 上面去画
  2. mEglSurface = EGL14.eglCreateWindowSurface(mEglDisplay, mEglConfig, surface, attrib_list, 0);
  3. 复制代码

绑定并向虚拟屏幕上画:

  1. // 绑定当前线程的显示设备及上下文, 之后操作opengl,就是在这个虚拟显示上操作
  2. if (!EGL14.eglMakeCurrent(mEglDisplay,mEglSurface,mEglSurface,mEglContext)) {
  3. throw new RuntimeException("eglMakeCurrent 失败!");
  4. }
  5. //向虚拟屏幕画
  6. mScreenFilter = new ScreenFiliter(context);
  7. mScreenFilter.onReady(width,height);
  8. 复制代码

双缓存画画:mEglSurface + mEglDisplay进行交替绘制。

  1. public void draw(int textureId, long timestamp){
  2. // 绑定当前线程的显示设备及上下文, 之后操作opengl,就是在这个虚拟显示上操作
  3. if (!EGL14.eglMakeCurrent(mEglDisplay,mEglSurface,mEglSurface,mEglContext)) {
  4. throw new RuntimeException("eglMakeCurrent 失败!");
  5. }
  6. //画画
  7. mScreenFilter.onDrawFrame(textureId);
  8. //刷新eglsurface的时间戳
  9. EGLExt.eglPresentationTimeANDROID(mEglDisplay, mEglSurface, timestamp);
  10. //交换数据
  11. //EGL的工作模式是双缓存模式,内部有两个frame buffer(fb)
  12. //当EGL将一个fb显示到屏幕上,另一个就在后台等待opengl进行交换
  13. EGL14.eglSwapBuffers(mEglDisplay, mEglSurface);
  14. }
  15. 复制代码

添加共享的EGLContext, 在创建的EGL环境下的子线程下进行编码,接受传入的视频宽、高,以及Surface,这里直接把渲染线程中的EGLContext给自定义的绘制EGL,作为share_context.

  1. mHandler.post(new Runnable() {
  2. @Override
  3. public void run() {
  4. //创建我们的子线程EGL环境
  5. mEglBase = new EGLBase(mContext, mWidth, mHeight, mInputSurface, mEglContext);
  6. //启动编码器
  7. mMediaCodec.start();
  8. isStart = true;
  9. }
  10. });
  11. /**
  12. * 创建好渲染器
  13. * @param gl
  14. * @param config
  15. */
  16. @Override
  17. public void onSurfaceCreated(GL10 gl, EGLConfig config) {
  18. 。。。。。。。。
  19. //注意,必须在Gl线程中创建文件
  20. mCameraFiliter = new CameraFilter(mDouyinView.getContext());
  21. mScreenFiliter = new ScreenFiliter(mDouyinView.getContext());
  22. //渲染线程的上下文,需要给到自己的EGL环境下作为share_context
  23. EGLContext eglContext = EGL14.eglGetCurrentContext();
  24. mMediaRecorder = new MediaRecorder(mDouyinView.getContext(), "/sdcard/a.mp4", CameraHelper.HEIGHT,CameraHelper.WIDTH, eglContext);
  25. }
  26. 复制代码
绘制、编码、读取output

在子线程中启动编码

  1. //交给虚拟屏幕 通过opengl 将预览的纹理 绘制到这一个虚拟屏幕中
  2. //这样MediaCodec 就会自动编码 inputSurface 中的图像
  3. mInputSurface = mMediaCodec.createInputSurface();
  4. 。。。。。。
  5. mHandler.post(new Runnable() {
  6. @Override
  7. public void run() {
  8. //创建我们的子线程,用于把预览的图像存储到虚拟Diaplay中去。
  9. mEglBase = new EGLBase(mContext, mWidth, mHeight, mInputSurface, mEglContext);
  10. //启动编码器
  11. mMediaCodec.start();
  12. isStart = true;
  13. }
  14. });
  15. 复制代码

上边的mMediaCodec.start()之后会从mInputSurface获取data, 而mEglBase会在draw方法里向mInputSurface写入data图像。

  1. public void encodeFrame(final int textureId,final long timestamp) {
  2. if (!isStart){
  3. return;
  4. }
  5. mHandler.post(new Runnable() {
  6. @Override
  7. public void run() {
  8. //把图像画到虚拟屏幕
  9. mEglBase.draw(textureId, timestamp);
  10. //从编码器的输出缓冲区获取编码后的数据就ok了
  11. getCodec(false);
  12. }
  13. });
  14. }
  15. 复制代码

最后在看从输出缓冲区拿取编码后的数据通过mMediaMuxer进行封装,生成path路径对应的MP4文件。

  1. /**
  2. * 获取编码后 的数据
  3. *
  4. * @param endOfStream 标记是否结束录制
  5. */
  6. private void getCodec(boolean endOfStream) {
  7. //不录了, 给mediacodec一个标记
  8. if (endOfStream) {
  9. mMediaCodec.signalEndOfInputStream();
  10. }
  11. //输出缓冲区
  12. MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
  13. // 希望将已经编码完的数据都 获取到 然后写出到mp4文件
  14. while (true) {
  15. //等待10 ms
  16. int status = mMediaCodec.dequeueOutputBuffer(bufferInfo, 10_000);
  17. //让我们重试 1、需要更多数据 2、可能还没编码为完(需要更多时间)
  18. if (status == MediaCodec.INFO_TRY_AGAIN_LATER) {
  19. // 如果是停止 我继续循环
  20. // 继续循环 就表示不会接收到新的等待编码的图像
  21. // 相当于保证mediacodec中所有的待编码的数据都编码完成了,不断地重试 取出编码器中的编码好的数据
  22. // 标记不是停止 ,我们退出 ,下一轮接收到更多数据再来取输出编码后的数据
  23. if (!endOfStream) {
  24. //不写这个 会卡太久了,没有必要 你还是在继续录制的,还能调用这个方法的!
  25. break;
  26. }
  27. //否则继续
  28. } else if (status == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
  29. //开始编码 就会调用一次
  30. MediaFormat outputFormat = mMediaCodec.getOutputFormat();
  31. //配置封装器
  32. // 增加一路指定格式的媒体流 视频
  33. index = mMediaMuxer.addTrack(outputFormat);
  34. mMediaMuxer.start();
  35. } else if (status == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
  36. //忽略
  37. } else {
  38. //成功 取出一个有效的输出
  39. ByteBuffer outputBuffer = mMediaCodec.getOutputBuffer(status);
  40. //如果获取的ByteBuffer 是配置信息 ,不需要写出到mp4
  41. if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
  42. bufferInfo.size = 0;
  43. }
  44. if (bufferInfo.size != 0) {
  45. bufferInfo.presentationTimeUs = (long) (bufferInfo.presentationTimeUs / mSpeed);
  46. //写到mp4
  47. //根据偏移定位
  48. outputBuffer.position(bufferInfo.offset);
  49. //ByteBuffer 可读写总长度
  50. outputBuffer.limit(bufferInfo.offset + bufferInfo.size);
  51. //写出
  52. mMediaMuxer.writeSampleData(index, outputBuffer, bufferInfo);
  53. }
  54. //输出缓冲区 我们就使用完了,可以回收了,让mediacodec继续使用
  55. mMediaCodec.releaseOutputBuffer(status, false);
  56. //结束
  57. if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
  58. break;
  59. }
  60. }
  61. }
  62. }
  63. 复制代码
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/575326
推荐阅读
相关标签
  

闽ICP备14008679号