Android MediaCodec
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android MediaCodec相关的知识,希望对你有一定的参考价值。
参考技术A MediaCodec 类为开发者提供了能访问到android底层媒体 Codec (Encoder/Decoder)的能力,它是Android底层多媒体基础架构的一部分(通常和MediaExtractor、MediaSync、MediaMuxer、MediaCrypto、MediaDrm、Image、Surface、AudioTrack一起使用)。
Codec 对三种类型类型的数据起作用: 编码后的压缩数据 , 原始视频数据 , 原始音频数据 。这三种类型的数据都可以通过 ByteBuffer 来传递给 Codec ,但是对于 原始视频数据 我们建议使用 Surface 来传递,这样可以提高 Codec 的性能, Surface 使用的是 native video buffer ,不用映射或者拷贝成 ByteBuffer ,因此这样的方式更高效。当你使用 Surface 来传递 原始视频数据 时,也就无法获取到了 原始视频数据 ,Android 提供了 ImageReader 帮助你获取到解码后的 原始视频数据 。这种方式可能仍然有要比 ByteBuffer 的方式更加高效,因为某些 native video buffer 会直接映射成 byteBuffer 。当然如果你 ByteBuffer 的模式,你可以使用 Image 类提供的 getInput/OutputImage(int) 来获取 原始视频数据 。
给 Decoder 输入的 InputBuffer 或者 Encoder 输出的 outputBuffer 包含的都是编码后的压缩数据,数据的压缩类型由 MediaFormat#KEY_MIME 指明。对于视频类型而言,这个数据通常是一个压缩后的视频帧。对于音频数据而言,通常是一个访问单元(一个编码的音频段,通常包含几毫秒的音频数据,数据类型format type 指定),有时候,一个音频单元对于一个 buffer 而言可能有点宽松,所以一个 buffer 里可能包含多个编码后的音频数据单元。无论 Buffer 包含的是视频数据还是音频数据, Buffer 都不会再任意字节边界上开始或者结束,而是在帧(视频)或者单元(音频)的边界上开始或者结束。除非它们被BUFFER_FLAG_PARTIAL_FRAME标记。
原始音频Buffer包含PCM音频数据的整个帧,是每一个通道按着通道顺序的采样数据。每一个采样按16Bit量化。
在 ByteBuffer 模式下,视频数据的排布由 MediaFormat#KEY_COLOR_FORMAT 指定,我们可以通过 getCodecInfo().MediaCodecInfo#getCapabilitiesForType.CodecCapabilities#colorFormats 获取到一个设备支持的 color format 数组。视频 Codec 可能支持三种类型的Color Format:
从 Build.VERSION_CODES.LOLLIPOP_MR1 开始所有的视频 Codec 都支持 flexible YUV 4:2:0
对于 Build.VERSION_CODES.LOLLIPOP 之前并且支持 Image 类时,我们需要使用 MediaFormat#KEY_STRIDE 和 MediaFormat#KEY_SLICE_HEIGHT 的值去理解输出的原始视频数据的布局。
键值 MediaFormat#KEY_WIDTH 和 MediaFormat#KEY_HEIGHT 指明了视频Frame的size。然而,对于大多数用于编码的视频图像,他们只占用了video Frame的一部分。这部分用一个 \'crop rectangle 来表示。
我们需要用下面的一些 keys 从获取原始视频数据的 crop rectangle ,如果 out format 中没有包含这些 keys ,则表示视频占据了整个 video Frame ,这个 crop rectangle 的解释应该立足于应用任何 MediaFormat#KEY_ROTATION 之前。
下面是在旋转之前计算视频的尺寸的案例:
从概念上讲Codec的声明周期存在三种状态: Stoped , Executing , Released 。 Stoped 状态是一个集合状态,它聚合了三种状态: Uninitialized , Configured ,和 Error ,同时 Executing 状态的处理也是通过三个子状态来完成: Flushed , Running , End-of-Stream 。
Executing 状态有三个子状态:Flushed,Running,和End-of-Stream,当我们调用玩 Start() 函数后, Codec 就立刻进入 Flushed 子状态,这个状态下,它持有全部的buffer,只要第一个Input buffer被dequeued,Codec就转变成 Running 子状态,这个状态占据了 Codec 的生命周期的绝大部分。当入队一个带有 end-of-stream标志的InputBuffer后, Codec 将转换成 End of Stream 子状态,在这个状态下, Codec 将不会再接收任何输入的数据,但是仍然会产生output buffer ,直到end-of-Stream标记的buffer被输出。我们可以在 Executing 状态的任何时候,使用 flush() 函数,将 Codec 切换成 Flushed 状态。
调用 stop() 函数会将 Codec 返回到 Uninitialized 状态,这样我们就可以对 Codec 进行重新配置,当你用完了 Codec 后,你必须要调用 release() 函数去释放这个 Codec 。
在极少数情况下, Codec 可能也会遇到错误,此时 Codec 将会切换到 Error 状态,我们可以通过queuing操作获取到一个无效的返回值,或者有时会通过异常来的得知 Codec 发生了错误。通过调用 reset() 函数,将 Codec 进行重置,这样 Codec 将切换成 Uninitalized 状态,我们可以在任何状态下调用 rest() 函数将Codec 将切换成 Uninitalized`状态。
使用 MediaCodecList 创建一个指定 MediaFormat 的MediaCodec。当我们解码一个文件或者一个流时,我们可以通过 MediaExtractor#getTrackFormat 获取期望的Fromat,同时我们可以通过 MediaFormat#setFeatureEnabled 为 Codec 注入任何我们想要的特性。然后调用 MediaCodecList#findDecoderForFormat 获取能够处理对应format数据 Codec 的name,最后我们使用 createByCodecName(String) 创建出这个 Codec 。
我们也可以使用 createDecoder/EncoderByType(java.lang.String) 函数来创建指定的 MIME 类型的 Codec ,但是这样我们无法向其中注入一些指定的特性,这样创建的 Codec 可能不能处理我们期望的媒体类型数据。
使用 MediaCodec 和 Surface 进行 Android 编码
【中文标题】使用 MediaCodec 和 Surface 进行 Android 编码【英文标题】:Android encoding using MediaCodec and a Surface 【发布时间】:2015-12-03 02:02:33 【问题描述】:我一直在通过 MediaCodec 将视频直接渲染到从我的 UI 中的 SurfaceView 获取的 Surface。这很好用。
我现在正尝试使用 MediaCodec 作为编码器。作为测试,我想渲染到 Surface(如上)并通过配置为编码器的不同 MediaCodec 实例环回。
我看到了编码器的 createInputSurface() 方法。我想我希望编码器创建这个表面,然后让解码器 MediaCodec 使用它作为要绘制的表面。首先,这可能吗?
其次,我不确定如何从编码器创建的 Surface 创建 SurfaceView。我只从 SurfaceView 中提取了一个 Surface,但我没有从文档中看到如何反向执行此操作。
【问题讨论】:
【参考方案1】:表面是生产者-消费者安排的“生产者”一侧。一般来说,API 以消费者为中心,消费者创建两端,然后将生产者接口(Surface)交还给您。
因此,对于 SurfaceView 或 MediaCodec 编码器,您可以创建对象并获取其 Surface。然后使用 Canvas、OpenGL ES 或 MediaCodec 解码器向它们发送图形数据缓冲区。
没有办法获取编码器的输入 Surface 并将其用作 SurfaceView 的显示 Surface——它们是两个不同的管道。 SurfaceView 的消费者位于系统合成器 (SurfaceFlinger) 中,这就是为什么您必须等待“表面创建”回调触发的原因。 MediaCodec编码器的消费者在mediaserver进程中,虽然异步性比较隐蔽。
将 MediaCodec 解码器输出发送到 SurfaceView 很简单,就像将输出发送到 MediaCodec 编码器一样。正如您所猜测的,只需将编码器的输入 Surface 传递给解码器。生活变得有趣的地方是你想同时做这两件事。
Surface 的底层代码(称为 BufferQueue)应该能够(如 Lollipop)进行多路复用,但我不知道 Lollipop 中的 API 会将这种能力暴露给应用程序。这意味着您在做事时遇到困难。
困难的方法是创建一个 SurfaceTexture(a/k/a GLConsumer),它是管道的消费者端。您可以从中创建一个 Surface,使用 sole constructor。你把它交给 MediaCodec 解码器。现在出现的每一帧都会被 SurfaceTexture 转换为 GLES 纹理。您可以将它们渲染到 SurfaceView 和编码器的输入 Surface。
您可以在Grafika 中找到各种示例,在graphics architecture doc 中可以找到更详细的机制说明。
【讨论】:
我现在正在使用 MediaCodec 将流解码到 SurfaceView。这很好用。使用 MediaCodec 作为编码器让我感到困惑的部分是我需要向编解码器询问表面(createSurface)。我是否使用编码器创建表面并将该表面引用提供给要使用的解码器?还是我必须在解码 Surface 像素和编码器创建的像素之间执行复制? 是... 将解码器的输出直接发送到编码器,首先使用createInputSurface()
创建编码器的Surface,然后在配置时将其交给解码器。不需要手动复制。
如果我想在表面上查看解码器实例的输出,这是否意味着我必须使用另一个 SurfaceView/SurfaceTexture 实例,因为编码器会为其功能创建不同的表面?
您可以将解码器的输出发送到单个 Surface。这意味着您可以查看它或对其进行编码,但不能同时查看它,除非您做额外的工作(例如,使用答案中提到的 SurfaceTexture)。
嗯。如果我使用编码器创建的 Surface,我的解码器实例会在 dequeueOutputBuffer 处引发 IllegalStateException。如果我在从 SurfaceView 派生的 Surface 中换回,我不会收到此异常并且它按预期工作。所有的事情都和我上一个一样。在解码端工作的代码版本。以上是关于Android MediaCodec的主要内容,如果未能解决你的问题,请参考以下文章
Android 逆向Android 权限 ( Android 逆向中使用的 android.permission 权限 | Android 系统中的 Linux 用户权限 )