Android音视频开发之openGL视频录制

Posted 初一十五啊

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android音视频开发之openGL视频录制相关的知识,希望对你有一定的参考价值。

前言

周五了,说实话根本不想动,今天就来的时间就来水一下度过八。

新增:Flutter番外篇:Flutter面试-项目实战-电子书

关注公众号:初一十五a
解锁 《Android十一大板块文档》
音视频大合集,从初中高到面试应有尽有;让学习更贴近未来实战。已形成PDF版

十一个模块内容如下

1.2022最新Android11位大厂面试专题,128道附答案
2.音视频大合集,从初中高到面试应有尽有
3.Android车载应用大合集,从零开始一起学
4.性能优化大合集,告别优化烦恼
5.Framework大合集,从里到外分析的明明白白
6.Flutter大合集,进阶Flutter高级工程师
7.compose大合集,拥抱新技术
8.Jetpack大合集,全家桶一次吃个够
9.架构大合集,轻松应对工作需求
10.Android基础篇大合集,根基稳固高楼平地起
11.Flutter番外篇:Flutter面试+项目实战+电子书

整理不易,关注一下吧。开始进入正题,ღ( ´・ᴗ・` ) 🤔

在仿抖音的第一步中封装了ScreenFilter类来实现渲染屏幕的操作,我们都知道在抖音的视频录制过程中,可以添加很多的效果进行显示,比如说磨皮、美颜、大眼以及滤镜等效果,如果把这些效果都放在ScreenFilter中,就需要使用很多的if else来进行判断是否开启效果,显而易见,这样的会显得项目结构不是很美好,我们可以将每种效果都写成一个Filter,并且在ScreenFilter之前的效果,都可以不用显示
到屏幕当中去,所以可以使用FBO来实现这个需求。

但是这里有一个问题,就是在摄像头画面经过FBO缓冲,我们再从FBO中绘制到屏幕上去,这里的ScreenFilter获取的纹理是来自于FBO中的纹理,也就是OpenGL ES中的,所以不再需要额外扩展的纹理类型了,可以直接使用sampler2D类型,也就意味着ScrennFilter,

\\1. 开启效果:使用sampler2D
\\2. 未开启效果:使用samplerExternalOES

那么就需要ScreenFilter使用if else去判断,很麻烦,所以我们可以不管摄像头是否开启效果都先将摄像头数据写到FBO中,这样的话,ScreenFilter的采样数据始终都可以是sampler2D了。也就是下面这种结构:

需求:
长按按钮进行视频的录制,视频有5种速度的录制,极慢、慢、正常、快、以及极快,抬起手指时候停止录制,并将视频保存以MP4格式保存在sdcard中。(抖音的视频录制在录制完成以后显示的时候都是正常速度,这里我为了看到效果,保存下来的时候是用当前选择的速度进行显示的)。

分析需求:
想要录制视频,就需要对视频进行编码,摄像头采集到的视频数据一般为AVC格式的,这里我们需要将AVC格式的数据,编码成h.264的,然后再封装为MP4格式的数据。对于速度的控制,可以在写出到MP4文件格式之前,修改它的时间戳,就可以了。

实现需求
MediaCodec
MediaCodec是android4.1.2(API 16)提供的一套编解码的API,之前试过使用FFmpeg来进行编码,效果不如这个,这个也比较简单,这次视频录制就使用它来进行编码。MediaCodec使用很简单,它存在一个输入缓冲区和一个输出缓冲区,我们把要编码的数据塞到输入缓冲区,它就可以进行编码了,然后从输出缓冲区取编码后的数据就可以了。

还有一种方式可以告知MediaCodec需要编码的数据,

  /**
      *Requests a surface to use as the input to an encoder, in place of input buffers,需要一个 Surface作为接口
      */
      @NonNull
      public native final surface createInputsurface();

这个接口是用来创建一个Surface的,Surface是用来干啥的呢,就是用来"画画"的,也就是说我们只要在这个Surface上画出我们需要的图像,MediaCodec就会自动帮我们编码这个Surface上面的图像数据,我们可以直接从输出缓冲区中获取到编码后的数据。之前的时候我们是使用OpenGL绘画显示到屏幕上去,我们可以同时将这个画面绘制到MediaCodec#createInputSurface() 中去,这样就可以了。

那怎么样才能绘制到MediaCodec的Surface当中去呢,我们知道录制视频是在一个线程中,显示图(GLSurfaceView)是在另一个GLThread线程中进行的,所以这两者的EGL环境也不同,但是两者又共享上下文资源,录制现场中画面的绘制需要用到显示线程中的texture等,那么这个线程就需要我们做这些:

1.配置录制使用的EGL环境(可以参照GLSurfaceView怎么配置的)
2.将显示的图像绘制到MediaCodec中的Surface中> 3. 编码(h.264)与复用(mp4)的工作

代码实现

MediaRecorder.java

视频编码类

/**
 *开始录制视频
 *@param speed
 */
public void start (float speed) throws IOException 
    mSpeed = speed;
    /**
     * 配置MediaCodec 编辑器
     */
    //视频格式
   // 类型(avc高级编码 H264) 编辑出的宽丶高
   MediaFormat mediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC,mWidtl,mHeight);
   //参数配置
   //1500kbs码率
   mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, value:1500_00);
   //帧率
   mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, value:20);
   //关键帧间隔
   mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, value:20);
   //颜色格式
   //从Surface当中获取的
   mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
   //编码器
   mMediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
   //将参数配置给编码器
   mMediaCodec.configure(mediaFormat, surfacenull; null, crypto;null,MediaCodec.CONFIGURE_FLAG_ENCODE);

   //交给虚拟屏幕 通过OpenGL 将预览的纹理 会知道这一个虚拟屏幕中
   //这样MediaCodec就会自动编码InputSurface中的图像了
   mInputSurface = mMediaCodec.createInputSurface();

   //  H.264
   //  播放
   //  MP4 -> 解复用(解封装) -> 解码 -> 绘制
   //封装器 复用器
   // 一个 mp4 的封装器 将h.264 通过它写出到文件就可以了
   mMediaMuxer = new MediaMuxer(mpath,MediaMuxer.outputFormat,MUXER_OUTPUT_MPEG_4);

   /**
    *配置EGL 环境
    */
    //Handler : 线程通信
    // Handler : 子线程通知主线程
      Looper.loop();
   HandlerThread handlerThread = new HandlerThread( name;''VideoCoder'');
   handlerThread.start();
   Looper looper = handlerThread.getLooper();
   // 用于其他线程 通知子线程
   mHandler = new Handler(looer);
   //子线程: EGL的绑定线程,对我们自己创建的EGL环境的opengl操作都在这个线程中执行
   mHandler.post() 
            //创建我们的EGL环境(虚拟设备丶EGL上下文等)
            //mEglBase = new EGLBase(mcontext,mWidth,nHeight,mInputsurface,mEglcontext);
            //启动编辑器
            mMeddiacodec.starat();
            isStart = true;
    

/**
 * 传递 纹理进来   
 * 相当于调用一次就有一个新的图像
 */
public void encodeFrame(final int textureId, final long timestamp) 
    if (!isStart) 
        ruturn
    
    mHandler.post() 
             //把图像画到虚拟屏幕
             mEglBase.draw(textureId,timestamp):
             //从编码器的输出缓冲区获取编码后的数据就OK了
             getCodec(endofstream;false);
    

 * 获取编码后的数据
 *
 * @param endofstream 标记是否结束录制
 */
private void getCodec(boolean endofstream) 
    //不录了,给mediacodec一个标记
    if(endofstream)
        mMediaCodec.signalEndofInputstream();
    
    //输出缓冲区
    MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
    // 希望将已经编码完的数据都获取到然后写到mp4文件
    while(true) 
        //等待10ms
        int status = mMediaCodec.dequeueoutputBuffer(bufferInfo,timeoutUs;10_000);
        //让我们重试 1丶需要更多数据 2丶可能还没编码完(需要更多时间)
        if (status == MediaCodec.INFO_TRY_AGAIN_LATER) 
            // 如果是停止 我继续循环
            // 继续循环 就表现不会接收到新的等待编码的图像
            // 相当于保证mediacodec中所有的待编码的数据都编码完成了,不断地重试 取出编码器中编码好的数据
            // 标记不是停止,我们退出,下一轮接收到更多数据再来取输出编码后的数据
            if(!endofstream)
                //不写这个 会卡太久了,没有必要 你还是继续录制的,还能调用这个方法的!
                break; 
            
            //否则继续
         else if (status == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) 
            //开始编码 就会调用一次
            MediaFormat outputFormat = mMediaCdec.getoutputFormat();
            //配置封装器
            // 增加一路指定格式的媒体流 视频
            index = mMediaMuxer.addTrack(outputFormat);
            mMediaMuxer.start();

这里的status==MediaCodec.INFO_TRY_AGAIN_LATER可以看下图理解

 else if (status = = MediaCodec,INFO_OUTPUT_BUFFERS_CHANGED) 
   //忽略
 else if 
   //成功 取出一个有效的输出
   ByteBuffer outputBuffer = mMediaCodec.getoutputBuffer(status);
   //如果获取的ByteBuffer是配置信息,不需要写到mp4
   if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) 
       bufferInfo.size = 0
   

   if (bufferInfo.size != 0) 
       bufferInfo.presentationTimeUs =(long) (bufferInfo.presentationTimeUs / mSpeed);
       //写到mp4
       //根据偏移定位
       outputBuffer.position(bufferInfo.offset);
       //ByteBuffer 可读写总长度
       outputBuffer.limit(bufferInfo.offset +bufferInfo.size);
       //写出
       mMediaMuxer.writeSampleData(index,outputBuffer,bufferInfo);
    
    //输出缓冲区 我们就使用完了,可以回收了,让mediacodec继续使用
    //结束
    if((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) 
       break
    

下文讲:openSL ES

关注公众号:初一十五a
解锁 《Android十一大板块文档》
音视频大合集,从初中高到面试应有尽有;让学习更贴近未来实战。已形成PDF版

新增:Flutter番外篇:Flutter面试-项目实战-电子书

十一个模块内容如下

1.2022最新Android11位大厂面试专题,128道附答案
2.音视频大合集,从初中高到面试应有尽有
3.Android车载应用大合集,从零开始一起学
4.性能优化大合集,告别优化烦恼
5.Framework大合集,从里到外分析的明明白白
6.Flutter大合集,进阶Flutter高级工程师
7.compose大合集,拥抱新技术
8.Jetpack大合集,全家桶一次吃个够
9.架构大合集,轻松应对工作需求
10.Android基础篇大合集,根基稳固高楼平地起
11.Flutter番外篇:Flutter面试+项目实战+电子书

整理不易,关注一下吧。ღ( ´・ᴗ・` ) 🤔

以上是关于Android音视频开发之openGL视频录制的主要内容,如果未能解决你的问题,请参考以下文章

OpenglEs之EGL环境搭建

Android Studio App开发之使用摄像机录制视频和从视频库中选取视频的讲解及实战(附源码)

Android录制视频并添加水印

Android开发之MediaRecorder类详解

android 视频录制 混淆打包 之native层 异常的解决

Android 音视频之openGL特效