Android 硬编码(MediaCodec)

作者: unique半山居士 | 来源:发表于2020-05-11 10:16 被阅读0次

    直播行业的风潮,短视频的爆红,各种现象使得视频成为了现在内容生产的重要载体。这种现象导致各位产品都想在自己的app里多多少少的加点视频相关的功能,而视频相关的功能一般都避不开编码、解码这个话题。

    MediaCodec 是android提供的用于进行硬件编解码的类,可以用于音频和视频,本文以视频的编码为主题。

    先简单的介绍一下它的使用流程:

    1、创建MediaCodec实体:

    有四种方法 分别是MediaCodec 的构造方法,和其内含的三个静态方法:createDecoderByType/createEncoderByType/createByCodecName。

    常用的是 createDecoderByType/createEncoderByType两个,顾名思义分别是创建用于解码和编码的实体,传入参数为编解码器的mime type名称。
    下面是一些mine type的名称。

    added in [API level 16](https://developer.android.com/guide/topics/manifest/uses-sdk-element.html#ApiLevels)
    
    Instantiate the preferred decoder supporting input data of the given mime type. 
    The following is a partial list of defined mime types and their semantics:
    
    *   "video/x-vnd.on2.vp8" - VP8 video (i.e. video in .webm)
    *   "video/x-vnd.on2.vp9" - VP9 video (i.e. video in .webm)
    *   "video/avc" - H.264/AVC video
    *   "video/hevc" - H.265/HEVC video
    *   "video/mp4v-es" - MPEG4 video
    *   "video/3gpp" - H.263 video
    *   "audio/3gpp" - AMR narrowband audio
    *   "audio/amr-wb" - AMR wideband audio
    *   "audio/mpeg" - MPEG1/2 audio layer III
    *   "audio/mp4a-latm" - AAC audio (note, this is raw AAC packets, not packaged in LATM!)
    *   "audio/vorbis" - vorbis audio
    *   "audio/g711-alaw" - G.711 alaw audio
    *   "audio/g711-mlaw" - G.711 ulaw audio
    

    最常用的当属 : "video/avc" - H.264/AVC video,现在网络上流传的短视频的视轨格式基本上都是H264。

    于是我们踏出了坚实的第一步:

    MediaCodec mediaCodec = MediaCodec.createEncoderByType(MIME);
    

    2、配置MediaCodec:

    就一件事:调用MediaCodec的configure方法:

    public void configure(
                MediaFormat format,
                Surface surface,
                MediaCrypto crypto,
                int flags
    );
    
    • Surface surface:指定surface,一般用于解码器,设置后解码的内容会被渲染到所指定的surface上。无需要则传null
    • MediaCrypto crypto:指定一个crypto对象,用于对媒体数据进行安全解密。对于非安全的编解码器,传null。
    • int flags:当组件是编码器时,flags指定为常量CONFIGURE_FLAG_ENCODE。
    • MediaFormat format:输入数据的格式(解码器)或输出数据的所需格式(编码器)。

    前三个基本都是固定的,并没有什么变数。主要说一下MediaFormat:
    解码的情况下,该实体大多通过MediaExtractor从视频中获取,这里(暂不展开谈MediaExtractor的内容)。
    编码的情况实体需要由我们来进行创建,常用的方法是它的静态方法:createVideoFormat/createAudioFormat,两个顾名思义的方法。
    以视频为例:

    MediaFormat format = MediaFormat.createVideoFormat(MIME,width, height);
    
    • MIME:第一步中的mine_type
    • width,height:视频的分辨率/高宽

    MediaFormat 可以为编码器设置一些特性参数,比如比特率,帧率,gop(关键帧 帧间 间隔)等等:

    format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); 
    format.setInteger(MediaFormat.KEY_BIT_RATE,2 * 1024 * 1024 ));
    format.setInteger(MediaFormat.KEY_FRAME_RATE, fps);
    format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, gop);
    

    需要特別说明的是MediaFormat.KEY_COLOR_FORMAT :该属性用于指明video编码器的颜色格式,具体选择哪种颜色格式与输入的视频数据源颜色格式有关。
    比如:Camera预览采集的图像流通常为NV21或YV12,那么编码器需要指定相应的颜色格式,否则编码得到的数据可能会出现花屏、叠影、颜色失真等现象。

    MediaCodecInfo.CodecCapabilities.存储了编码器所有支持的颜色格式,常见颜色格式映射如下:

    原始数据 编码器
    NV12(YUV420sp) ———> COLOR_FormatYUV420PackedSemiPlanar
    NV21 ———-> COLOR_FormatYUV420SemiPlanar
    YV12(I420) ———-> COLOR_FormatYUV420Planar 
    

    然而我们的例子并没有使用上面的标识,而是使用了COLOR_FormatSurface,这是指对一个surface上的图像进行mediaCodec编码。
    至于为什么不用上面的标识呢?当然是有坑啦,这个我们后面再进行详细的说明。

    完成了MediaFormat 的构建,我们就成功的踏出了第二步

    mMediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
    mMediaCodec.start();//启动!!
    

    3、 数据处理:


    大体步骤:
    1、使用者从MediaCodec请求一个空的输入buffer(ByteBuffer),填充满数据后将它传递给MediaCodec处理。

     int inputBufferIndex = mediaCodec.dequeueInputBuffer(0);//获取输入缓存区授权
    ByteBuffer inputBuffer = codec.getInputBuffer(…);//获取实际buffer
    // 填充
    。。。
    codec.queueInputBuffer(inputBufferId, …);//加入编码队列
    

    2、MediaCodec处理完这些数据并将处理结果输出至一个空的输出buffer(ByteBuffer)中。
    使用者从MediaCodec获取输出buffer的数据,消耗掉里面的数据,使用完输出buffer的数据之后,将其释放回编解码器。

     int encoderStatus = mMediaCodec.dequeueOutputBuffer(mBufferInfo, 0);//获取输出缓存区授权
     ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);//获取实际buffer取得编码后的数据
      。。。
     codec.releaseOutputBuffer(outputBufferId, …);//释放使缓冲buffer回到队列中
    

    dequeueOutputBuffer的 mBufferInfo 为入参(传入一个实体,函数内会为这个实体的字段赋值),其中有buffer的size,offsize信息,帧的时间戳信息和用于表示帧属性的flag字段(最常用的就是用来判断是否是关键帧)。

    3、循环上述两步,当编码完成时,在步骤1传入MediaCodec.BUFFER_FLAG_END_OF_STREAM标识,用以标志整个流程的结束。

    codec.queueInputBuffer(  videoInputBufferIndex,  0,  0, -1, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
    

    在步骤2中读取到MediaCodec.BUFFER_FLAG_END_OF_STREAM时就可以停止整个编码任务啦。

    就此我们完成了硬编码的最后一步。

    4、坑坑坑

    事情并没有那么简单,android硬编码中可谓是一步一坑。。。让我们从头开始迈步子:

    1、坚实的第一步

    ——是真的很坚实 ^ > ^

    2、第二步

    MediaFormat 可以为编码器设置一些特性参数,然而不同的手机可以设置的参数不同(例如实际开发中我们可能需要设置
    KEY_PROFILE,KEY_BITRATE_MODE等参数)如果设置了手机不支持的参数,在调用configure方法时可能会抛出异常,或者设置的参数不生效,导致编码出来的视频不能达到预期的效果等等情况。不过u1s1现在的手机对常用的参数的支持越来越好了。

    我的方案:
    • 是要对其进行适配,找寻手机适合的参数
    • 对参数进行分级,反复尝试调用configure,失败则降级。例:
    try{
          if (encodeLevel > 1) {
              format.setInteger(MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileHigh);
              format.setInteger("level", MediaCodecInfo.CodecProfileLevel.AVCLevel41);
          } else if (encodeLevel > 0) {
              format.setInteger(MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileMain);
              format.setInteger("level", MediaCodecInfo.CodecProfileLevel.AVCLevel32);
          }
          //encodeLevel=0的时候放弃该参数,不进行设置
          mMediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
    }catch (Exception e) {
          if (level >= 1) {
                encodeLevel --; // "level down"
                prepareEncoder(encodeLevel );//重试
          } 
    }
    

    3、第三步:这是真的坑

    看起来逻辑很顺,queueInputBuffer输入数据,然后再调用dequeueOutputBuffer获取输出。然而这个输入很成问题。
    现在很多视频录制都要经过滤镜、美颜等等模块对图像进行加工,而这些模块往往输出的是RGB数据,我们要输入编码器首先要对颜色格式进行转换,不仅麻烦而且这个转换往往比较耗时,较差的手机编码速度跟不上,为了用户体验,我们被迫要削减录制视频的帧率。 然而当你转换完成后还有第二个坑在等着你。。。
    还记得MediaFormat.KEY_COLOR_FORMAT 吗?这个就是要指定输入数据的颜色格式,上面的映射关系看着很美好, 实际上这个映射关系非常乱,同一个标识在不同手机上对应着不同的YUV格式,让人摸不着头脑,而且还没有什么好的判断方法。输入错误的yuv格式就会导致视频整体颜色错乱,你辛辛苦转换的颜色格式在一台手机上完美无缺,很有可能换台手机就乱套了。

    所以上面使用了COLOR_FormatSurface,这是MediaCodec提供的另一种输入数据的方法。首先通过MediaCodec获取一个suface实体

    Surface  srface = mMediaCodec.createInputSurface();
    

    然后通过Opengl ES将图像渲染至该Surface上(在下篇文章里再介绍Opengl ES相关api的使用),MediaCodec就会对图像进行编码,而我们就只需要调用dequeueOutputBuffer的部分获取编码后的数据,省去了耗时的颜色转换和queueInputBuffer的调用。需要注意的是这个解决方案是有版本限制的:Android 4.3, API18。

    5、硬编码该崛起了

    说了这么多坑,为什么还要说硬编码崛起呢?我们这就来说说硬编码的优点:

    1、 硬编码是使用GPU来进行编码的。

    这比起ffmpeg等软编码解决方案来说效率更高,而且可以腾出CPU的资源来供给给其他模块,例如ui、常常和视频关联的人脸识别模块等等。现在各种高清视频开始展露头角,例如4k,h265格式的视频,单位时间内编解码的运算量急剧上升,单靠cpu的软编码是很难维持良好的用户体验的。

    2、 硬编码为原生代码,并不需要添加额外的编解码库。

    对于轻量级app包的大小还是颇具影响力的,单单ffmpeg的so就不小了。虽然硬编码有版本限制(为了避坑在 API18 以上使用硬编码比较合适),然而时代在发展,18以下的手机的市场份额会越来越小,这个限制会渐渐的失去影响力。

    3、Surface的输入方式,绘制即编码,非常方便。

    如果你使用ffmpeg的软编码手段,对某视频进行加工,比如添加水印,预览的绘制流程使用opengl es, 编码的流程调用ffmpeg的api,都是加水印,两处代码两种形式,加大了代码整体的复杂度。而硬编码绘制渲染的代码可以同时用于预览和编码。

    相关文章

      网友评论

        本文标题:Android 硬编码(MediaCodec)

        本文链接:https://www.haomeiwen.com/subject/cqpfnhtx.html