Android MediaCodec编解码详解及demo

作者: sheepm | 来源:发表于2016-12-27 23:30 被阅读15364次

    原文地址

    Android MediaCodec stuff

    这篇文章是关于 MediaCodec 这一系列类,它主要是用来编码和解码音视频数据。并且包含了一些源码示例的集合以及常见问题的解答。
    在API23之后,官方的文档 official 就已经十分的详细了。这里的一些信息可以帮你了解一些编解码方面的知识,为了考虑兼容性,这里的代码大部分都是运行在API18及以上的环境中,当然如果你的目标是Lollipop 以上的用户,你可以有更多的选择,这些都没有在这里提及。

    概述

    MediaCodec 第一次可用是在 Android 4.1版本(API16 ),一开始是用来直接访问设备的媒体编解码器。它提供了一种极其原始的接口。MediaCodec类同时存在 Java和C++层中,但是只有前者是公共访问方法。
    在Android 4.3 (API18)中,MediaCodec被扩展为包含一种通过 Surface 提供输入的方法(通过 createInputSurface 方法),这允许输入来自于相机的预览或者是经过OpenGL ES呈现。而且Android4.3也是 MediaCodec 的第一个经过CTS测试(Compatibility Test Suite,CTS是google推出的一种设备兼容性测试规范,用来保证不同设备一致的用户体验,同时Google也提供了一份兼容性标准文档 CDD)的 release 版本。
    而且Android4.3还引入了 MediaMuxer,它允许将AVC编解码器(原始H.264基本流)的输出转换为.MP4​​格式,可以和音频流一起转码也可以单独转换。
    Android5.0(API21)引入了“异步模式”,它允许应用程序提供一个回调方法,在缓冲区可用时执行。但是整个文章链接里的代码都没有用到这个,因为兼容性保持到API 18+。

    基本使用

    所有的同步模式的 MediaCodec API都遵循一个模式:

    • 创建并配置一个 MediaCodec 对象
    • 循环直到完成:
      如果输入缓冲区就绪,读取一个输入块,并复制到输入缓冲区中
      如果输出缓冲区就绪,复制输出缓冲区的数据
    • 释放 MediaCodec 对象

    MediaCodec的一个实例会处理一种类型的数据,(比如,MP3音频或H.264视频),编码或是解码。它对原始数据操作,所有任何的文件头,比如ID3(一般是位于一个mp3文件的开头或末尾的若干字节内,附加了关于该mp3的歌手,标题,专辑名称,年代,风格等信息,该信息就被称为ID3信息)这些信息会被擦除。它不与任何高级的系统组件通信,也不会通过扬声器来播放音频,或是通过网络来获取视频流数据,它只是一个会从缓冲区取数据,并返回数据的中间层。
    一些编解码器对于它们的缓冲区是比较特殊的,它们可能需要一些特殊的内存对齐或是有特定的最小最大限制,为了适应广泛的可能性,buffer缓冲区分配是由编解码器自己实现的,而不是应用程序的层面。你并不需要一个带有数据的缓冲区给 MediaCodec,而是直接向它申请一个缓冲区,然后把你的数据拷贝进去。
    这看起来和“零拷贝”原则是相悖的,但大部分情况发生拷贝的几率是比较小的,因为编解码器并不需要复制或调整这些数据来满足要求,而且大多数我们可以直接使用缓冲区,比如直接从磁盘或网络读取数据到缓冲区中,不需要复制。
    MediaCodec的输入必须在“access units”中完成,在编码H.264视频时意味着一帧,在解码时意味着是一个NAL单元,然而,它看起来感觉更像是流,你不能提交一个单块,并期望不久后就出现,实际上,编解码器可能会在输出前入队好几个buffers。
    这里强烈建议直接从下面的示例代码中学习,而不是直接从官方文档上手。

    例子

    EncodeAndMuxTest.java (requires 4.3, API 18)
    使用OpenGL ES生成一个视频,通过 MediaCodec 使用H.264进行编码,而且通过 MediaMuxer 将流转换成一个.MP4文件,这里通过CTS 测试编写,也可以直接转成其他环境的代码。

    CameraToMpegTest.java (requires 4.3, API 18)
    通过相机预览录制视频并且编码成一个MP4文件,同样通过 MediaCodec 使用H.264进行编码,以及 MediaMuxer 将流转换成一个.MP4文件,作为一个扩展版,还通过GLES片段着色器在录制的时候改变视频,同样是一个CTS test,可以转换成其他环境的代码。

    Android Breakout game recorder patch (requires 4.3, API 18)
    这是 Android Breakout v1.0.2版本的一个补丁,添加了游戏录制功能,游戏是在60fps的全屏分辨率下,通过一个30fps 720p的配置使用AVC编解码器来录制视频,录制文件被保存在一个应用的私有空间,比如 ./data/data/com.faddensoft.breakout/files/video.mp4。这个本质上和 EncodeAndMuxTest.java 是一样的,不过这个是完全的真实环境不是CTS test,一个关键的区别在于EGL的创建,这里允许通过将显示和视频context以共享纹理的方式。

    EncodeDecodeTest.java (requires 4.3, API 18)
    CTS test,总共有三种test做着相同的事情,但是是不同的方式。每一个都是:
    生成video的帧,通过AVC进行编码,生成解码流,看看是否和原始数据一样
    上面的生成,编码,解码,检测基本是同时的,帧被生成后,传递给编码器,编码器拿到的数据会传递给解码器,然后进行校验,三种方式分别是
    Buffer到Buffer,buffers是软件生成的YUV帧数据,这种方式是最慢的,但是能够允许应用程序去检测和修改YUV数据。
    Buffer到Surface,编码是一样的,但是解码会在surface中,通过OpenGL ES的 getReadPixels()进行校验
    Surface到Surface,通过OpenGL ES生成帧并解码到Surface中,这是最快的方式,但是需要YUV和RGB数据的转换。

    DecodeEditEncodeTest.java (requires 4.3, API 18)
    CTS test,主要是生成一系列视频帧,通过AVC进行编码,编码数据流保存在内存中,使用 MediaCodec解码,通过OpenGL ES片段着色器编辑帧数据(交换绿/蓝颜色信道),解码编辑后的视频流,验证输出。

    ExtractMpegFramesTest.java (requires 4.1, API 16)
    ExtractMpegFramesTest.java (requires 4.2, API 17)
    提取一个.mp4视频文件的开始10帧,并保持成一个PNG文件到sd卡中,使用 MediaExtractor 提取 CSD 数据,并将单个 access units给 MediaCodec 解码器,帧被解码到一个SurfaceTexture的surface中,离屏渲染,并通过 glReadPixels() 拿到数据后使用 Bitmap#compress() 保存成一个PNG 文件。

    常见问题

    Q1:我怎么播放一个由MediaCodec创建的“video/avc”格式的视频流?
    A1.这个被创建的流是原始的H.264流数据,Linux的Totem Movie Player可以播放,但大部分其他的都播放不了,你可以使用 MediaMuxer 将其转换为MP4文件,看前面的EncodeAndMuxTest例子。

    Q2:当我创建一个编码器时,调用 MediaCodec的configure()方法会失败并抛出一个IllegalStateException异常?
    A2.这通常是因为你没有指定所有编码器需要的关键命令,可以看一个这个例子 this stackoverflow item

    Q3:我的视频解码器配置好了但是不接收数据,这是为什么?
    A3.一个比较常见的错误就是忽略设置Codec-Specific Data(CSD),这个在文档中简略的提到过,有两个key,“csd-0”,“csd-1”,这个相当于是一系列元数据的序列参数集合,我们只需要直到这个会在MediaCodec 编码的时候生成,并且在MediaCodec 解码的时候需要它。
    如果你直接把编码器输出传递给解码器,就会发现第一个包里面有BUFFER_FLAG_CODEC_CONFIG 的flag,这个参数需要确保传递给了解码器,这样解码器才会开始接收数据,或者你可以直接设置CSD数据给MediaFormat,通过 configure() 方法设置给解码器,这里可以参考 EncodeDecodeTest sample 这个例子。
    实在不行也可以使用 MediaExtractor ,它会帮你做好一切。

    Q4:我可以直接将流数据给解码器么?
    A4.不一定,解码器需要的是 "access units"格式的流,不一定是字节流。对于视频解码器,这意味着你需要保存通过编码器(比如H.264的NAL单元)创建的“包边界”,这里可以参考 DecodeEditEncodeTest sample 是如何操作的,一般不能读任意的块数据并传递给解码器。

    Q5:我在编码由相机预览拿到的YUV数据时,为什么看起来颜色有问题?
    A5.相机输出的颜色格式和MediaCodec 在编码时的输入格式是不一样的,相机支持YV12(平面 YUV 4:2:0) 以及 NV21 (半平面 YUV 4:2:0),MediaCodec支持以下一个或多个:
    .#19 COLOR_FormatYUV420Planar (I420)
    .#20 COLOR_FormatYUV420PackedPlanar (also I420)
    .#21 COLOR_FormatYUV420SemiPlanar (NV12)
    .#39 COLOR_FormatYUV420PackedSemiPlanar (also NV12)
    .#0x7f000100 COLOR_TI_FormatYUV420PackedSemiPlanar (also also NV12)
    I420的数据布局相当于YV12,但是Cr和Cb却是颠倒的,就像NV12和NV21一样。所以如果你想要去处理相机拿到的YV12数据,可能会看到一些奇怪的颜色干扰,比如这样 these images。直到Android4.4版本,依然没有统一的输入格式,比如Nexus 7(2012),Nexus 10使用的COLOR_FormatYUV420Planar,而Nexus 4, Nexus 5, and Nexus 7(2013)使用的是COLOR_FormatYUV420SemiPlanar,而Galaxy Nexus使用的COLOR_TI_FormatYUV420PackedSemiPlanar。
    一种可移植性更高,更有效率的方式就是使用API18 的Surface input API,这个在 CameraToMpegTest sample 中已经演示了,这样做的缺点就是你必须去操作RGB而不是YUV数据,这是一个图像处理的问题,如果你可以通过片段着色器来实现图像操作,可以利用GPU来处理这些转换和计算。

    Q6: EGL_RECORDABLE_ANDROID flag是用来干什么的?
    A6.这会告诉EGL,创建surface的行为必须是视频编解码器能兼容的,没有这个flag,EGL可能使用 MediaCodec 不能理解的格式来操作。

    Q7:我是不是必须要在编码时设置 presentation time stamp (pts)?
    A7.是的,一些设备如果没有设置合理的值,那么在编码的时候就会采取丢弃帧和低质量编码的方式。
    需要注意的一点就是MediaCodec所需要的time格式是微秒,大部分java代码中的都是毫秒或者纳秒。

    Q8:为什么有时输出混乱(比如都是零,或者太短等等)?
    A8.这常见的错误就是没有去适配ByteBuffer的position和limit,这些东西MediaCodec并没有自动的去做,
    我们需要手动的加上一些代码:

      int bufIndex = codec.dequeueOutputBuffer(info, TIMEOUT);
      ByteBuffer outputData = outputBuffers[bufIndex];
      if (info.size != 0) {
          outputData.position(info.offset);
          outputData.limit(info.offset + info.size);
      }
    

    在输入端,你需要在将数据复制到缓冲区之前调用 clear()

    Q9: 有时候会发现 storeMetaDataInBuffers 会打出一些错误log?
    A9.是的,比如在Nexus 5上,看起来是这样的

    E OMXNodeInstance: OMX_SetParameter() failed for StoreMetaDataInBuffers: 0x8000101a
    E ACodec  : [OMX.qcom.video.encoder.avc] storeMetaDataInBuffers (output) failed w/ err -2147483648
    

    不过可以忽略这些,不会出现什么问题。

    相关文章

      网友评论

      • 001annuo:java.lang.IllegalStateException: Failed to stop the muxer 视频合并的时候会报这个错误
      • keepKK:博主, EncodeDecodeTest.java这个编译不过,这个InputSurface找不到啊,不知道是怎么定义的,能否指点一下
        one1go:EncodeDecodeTest的链接失效了,还有有效链接吗
      • canlu:请教两个问题:
        1、在音频编码的时候,pcm->acc,出现输入倒序帧的问题,请问有遇到过吗?
        2、编码时输入到ByteBuffer的size是否需要根据采样率、通道数这些信息设置?
      • 小默森:请问大神,我最近在做一个录屏的功能,但是遇到一个问题,那就是横竖屏切换时会造成卡顿,目前觉得是,切换完成后,屏幕不动,回调数据就很少了,整个流程不知道哪儿出了问题.
        但是从安卓的角度来看,只要能在屏幕不动的情况下,还有回调数据,就可以了,这个有办法吗?
      • 899e2d74bdeb:请问 用mediacodec编码h264后 播放出来的视频速度很快,这是与pts有关的,请问如何设置pts,网上都方法都没有效果。
      • niuzhijun66:大神,用MediaCodec怎么裁剪视频尺寸呢?
      • LesChoristes:请问下硬解码的延迟大概有多大?
        大熊121:@kexinJiao 貌似不止吧,会存在帧间延迟吧
        kexinJiao:正常情况下在3ms-20ms,
      • a76355cca3cc:但是我播放帧率25 码流3000的就不会有延迟的
        a76355cca3cc: @sheepm 我发现好像是输出缓冲区的问题,但是没有找到它的借口
        sheepm:因为解码是有缓冲区的 你在码流和帧率较小的时候填充满缓冲区的时间比较长 造成延迟 你可以慢慢的增加 就会发现延迟越来越小
      • a76355cca3cc:我帧率15 码流1000时,视频会出现1-2s的延迟,请问您遇到过吗
        a76355cca3cc: @sheepm 直接用Android 硬解 h264数据流 会出现 延迟 码流不是1000是 300
        a76355cca3cc:@旋律並未伴隨故事響起 直接用Android 硬解 h264数据流 会出现 延迟 码流不是1000是 300
        sheepm:@旋律並未伴隨故事響起 是编码出来的视频延迟 还是什么
      • Andyzhao:大神

      本文标题:Android MediaCodec编解码详解及demo

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