Android 多媒体通路简介
更新日期:
MediaPlayer
Sample Code
官方文档的状态机如下图:
按照官网状态机,一般 MediaPlayer 使用代码如下:
|
|
数据通路
官网的多媒体框架介绍(本文中的 android framework 代码基于 Android 10 其他版本可能会有一些差异):
MediaPlayer 数据通路时序图如下:
上面的数据通路可以分为6个部分:
应用层(App)
这部分主要是上层 apk 自己的代码。
Application Framework(Java)
这部分代码在:
framework/base/media/java/android/media/MediaPlayer.java
/system/framework/framework.jarframework.jar 中代码就是应用调用 sdk 里面的 android.jar。这部分的核心功能是调用 jni 实现的,jni 中有一个 native class 的 MediaPlayer 的。java 的 MediaPlayer 对象中有一个 long mNativeContext 的变量是保存 jni 中 native 对象 MediaPlayer 的指针的,用于 jni 调用;它会在 jni 的 native_init() 中初始化 java 层的 class 还有各种变量的引用,并且在 MediaPlayer.java 的 static 代码段中调用 System.loadLibrary(media_jni) 加载 jni so 和调用 native_init() 初始化。然后在 native_setup() 中将 new 出来的 native MediaPlayer 对象(指针)保存在 java 对象的 mNativeContext 字段中(这个变量是 long,指针本质就是一个地址)。基本上 Android 的多媒体的 api 框架都是这个套路,这个设计框架可以学习一下。
Application Framework(JNI)
这部分代码在:
framework/base/media/jni/android_media_MediaPlayer.cpp
/system/framework/lib/libmedia_jni.soMediaPlayer framework jni 中也没什么实际工作,主要就是 new 了 native MediaPlayer 对象,调用 native MediaPlayer 接口。
Binder IPC Proxy(Native)
这部分代码在:
frameworks/av/media/libmedia/mediaplayer.cpp
/system/framework/lib/libmedia.soav 下面这个 libmedia.so 会去和 MediaPlayerService 连接(创维 MediaPlayerService 的 Client),主要都是通过 Binder 去调用 MediaPlayerService 的接口,功能都在 MediaPlayerService 里面实现。这个 native 的类里面会持有 MediaPlayerService Client 的 Bp 端。所以这个 so 还是属于 App 进程空间的。主要功能实现都是调用 MediaPlayerService Client 的接口,转发到 MediaPlayerService 中调用。sdk 文档中的状态机是在这个类里维护的,所以状态仅仅在 App 端维护,Service 端没有这些状态机的概念。
MediaPlayerService(Native)
这部分代码在:
frameworks/av/media/libmediaplayerservice/MediaPlayerService.cpp
/system/framework/lib/libmediaplayerservice.soMediaPlayerService 是一个 native 的系统服务。它的服务的 bin 在 frameworks/av/media/mediaserver,进程是 mediaserver,服务接口是:media.player: [android.media.IMediaPlayerService]。它主要是维护一个 mClients 的列表(SortedVector)。当有客户端(上面 libmedia.so)连接的时候会创建 Client(Bn 端)对象,然后保存到列表里面,当客户端销毁的时候就会清掉这个 Client。当 App 调用 setDataSource 的时候,MediaPlayerService 会通过 MediaPlayerFactory 创建方案商自己的 Player(方案商的解码器是硬解,要是 AOSP 原生的就是软解),我们这边就是 AwPlayer。每一个 Client 中都会有一个 AwPlayer。App MediaPlayer 各种接口调用最后都会调用到解码器的同名接口。
AwPlayer(Native)
这部分代码在:
frameworks/av/media/libcedarx/android_adapter/awplayer/awplayer.cpp
/system/framework/lib/libawplayer.so其实这部分代码远不止这里列出来的这些。这部分就是方案商硬解的解码器的代码实现了。这里可以看到方案商的解码器需要适配 android 的 MediaPlayer 的接口。这里面有一堆解码器的库(例如上面列出来的 libcdx_ 开头的这些库),这部分就是多媒体那边负责了的,当然有兴趣的也可以看看代码,但是有一部分是不开源的。
MediaCodec
Sample Code
简单来说 MediaCodec 比 MediaPlayer 更加灵活,更加底层,这表明 MediaCodec 使用上会更加复杂。MediaPlayer 帮我们把视频解析、音视频解码以及同步、状态管理之类的都做了,开发者基本上只需要设置视频源,就可以开始播放了。而 MediaCodec 需要做大量的工作。当然好处就是实现一些视觉处理逻辑,例如图像识别啊、视觉算法之类的。下面的 sample code 只是为了简单说明 MediaCodec 的使用流程,省略了视频解析的流程,并且没有处理音频。android 上除了使用 MediaPlayer、MediaCodec 处理多媒体以外,还有一种更灵活的方式,那就是自己移植解码器,例如说 ffmpeg;当然这种就更加复杂,一般一些复杂的直播或者短视频软件才会这么干。
看看官方的框架介绍:
下面是 MediaCodec 的一般用法:
|
|
数据通路
MediaCodecService
MediaCodecService 启动时序图:
先来说一下 MediaCodecService 启动流程。MediaCodecService 和 MediaPlayerService 不一样,它属于 HIDL 服务,使用 Android 新的 HIDL 框架编写。一般 HIDL 的 Services 进程名字都叫 android.hardware.xx.xx@version-service。本来 MediaCodecService 的进程名也是叫 android.hardware.media.omx@1.0-service 的(编译出的bin文件都是 /vendor/bin/hw/android.hardware.media.omx@1.0-service),但是在 main 函数里面手动把进程名字改成 media.codec 了,所以 ps 看到的还是以前 android 版本的进程名字。media.codec 是硬解的 codec,另外一个 media.swcodec 是软解的 codec,这里暂时不分析系统自带的软解 codec。
上面可以看到 media.codec 的服务名字其实是叫 omx,而且它的组件也是 omx 之类的代码。MediaCodec 使用的是 OpenOmx(Android 上简称 omx) 的框架,OpenOmx 是由 Khronos Group 提出的一个开源跨平台多媒体框架,Android 主要使用 omx 来实现多媒体编解码硬件加速。omx 基于插件(Plugin)形式来实现编解码,芯片厂商只需要将自己的解码器包装成 omx 的 Plugin 就能实现解码硬件加速(硬解)。omx 用 node 代表一种解码器(例如说 h264、h265、mjpeg 等),node 的 instance 代表这种解码器的一个实例(例如说应用要创建一个 h264 的解码器,就需要实例化一个 h264 node 的 instance)。由 master 来管理 node。
- media.codec bin 的代码在 frameworks/av/services/mediacodec,IOmx 接口定义在 hardware/interfaces/media/omx/1.0/ 。media.codec 的 main 其实实现了 2个 hidl 接口,一个 IOmx,一个 IOmxStore。IOmx 就是 omx 实现,我们主要说这个,IOmxStore 暂时还没了解是干什么的,暂时先不说。main 函数整个流程很简单:new 了 Omx 之后,注册 hidl services 之后一直等待连接了。
- Omx.cpp 代码在 frameworks/av/media/libstagefright/omx/ (/system/lib/libstagefright_omx.so)。它的初始化流程也比较简单,new 了 一个 OMXMaster,然后在 xml 中加载了一些 Parser(parser 时序图里没画出来,这里略过)。主要初始化在 OMXMaster.cpp 里面(上面介绍了 master 主要是管理 omx 的 plugin 的)。
- OMXMaster 的初始化主要是加载 plugin。主要是函数 addVendorPlugin,这个函数是加载芯片原厂的解码器插件也就是硬解插件,还有一个 addPlatformPlugin 是加载 android 自带的软解的解码器插件,这里主要介绍硬解的插件加载过程,软解的先不说。插件加载是通过动态加载 libstagefrighthw.so 来实现的。通过查找并调用 so 中的 createOMXPlugin 函数来初始化硬解解码器插件。
- libstagefrighthw.so (/vendor/lib/libstagefrighthw.so) 相关源码在 frameworks/av/media/libcedarc/openomx/libstagefrighthw/ 。这个库里面有一个 c 函数,就是上面提到的 createOMXPlugin,它就 new 了一个 AwOMXPlugin 返回给 media.codec 服务,这是一个继承了 OMXPluginBase 的类,也是按照 android 定义的接口实现的。AwOMXPlugin 是在初始化函数里面初始化的,它也加载了另外一个库:libOmxCore.so 。然后 AddVDLib 中连续调用了 InitVDLIb 加载各个解码器模块(例如 libawh265.so、libawh264.so、libawmjpeg.so 等)。InitVDLib 里面也是 dlopen 打开 so,然后查找 一个叫 CedarPluginVDInit 的函数,并调用,就去初始化对应的解码器模块了。但是这些 libawxx.so 的库多媒体部门并不开源(这些 so 在 /frameworks/av/media/libcedarc/library/ 下面,分平台放置),所以我们无法看到具体的初始化代码。然后查找并调用了 libOmxCore.so 中的 OMX_Init 函数,不过这个函数目前是空函数,并没有时间处理逻辑。
- libOmxCore.so(/vendor/lib/libOmxCore.so) 相关源码在 frameworks/av/media/libcedarc/openomx/omxcore/。上面创建的 AwOMXPlugin 的对象之后,会接着调用 AwOMXPlugin 里面的 enumerateComponents 函数去枚举所有的插件里面所有的解码器组件(这是个 while 循环,当枚举函数返回 0 就表示组件已经加载完了)。AwOMXPlugin 会进一步调用到 aw_omx_core.c 里面的 OMX_ComponetNameEnum,而这个函数就是通过 index 在一个全局的数组 OmxCoreType core[] 里面获取编号 index 的解码器的名字。这个 OmxCoreType 是一个我们定义的结构体,里面有解码器的名字、so 的 fp handle、一些函数指针。这个数组的作用是定义所支持的解码器列表。这个变量定义在 aw_registry_table.c 文件中静态初始化(so 被 load 的时候就初始化了),aw_registry_table.c 是分平台放置的(有多份,不同平台支持的解码器不一样,分 IC),可以在这个文件里面看到平台支持的解码器情况。枚举完之后,会把初始化的解码器组件信息按名字保存在一个列表里面:mPluginByComponentName。这个列表在后面创建解码器(实例化解码器 Node 实例)的时候会用到。
- 上面能看到 OMX 的 Plugin 通过 so 的形式存在,通过定义好 函数接口,通过 dlopen、load、查找函数来实现 plugin 加载、交互。这个框架我们也可以学习一下。
接下来看看 MediaCodec 应用端这边的时序图:
可以看到 MediaCodec 比 MediaPlayer 要复杂很多(上面的时序已经简化过了)。
应用层(App)
这部分主要是上层 apk 自己的代码。
Application Framework(Java)
这部分代码在:
framework/base/media/java/android/media/MediaCodec.java
/system/framework/framework.jar这部分代码套路和 MediaPlayer 一样,不过多说明。
Application Framework(JNI)
这部分代码在:
framework/base/media/jni/android_media_MediaCodec.cpp
/system/framework/lib/libmedia_jni.so这部分代码套路也和 MediaPlayer 一样,不过多说明。
Stagefright Engine(Client)
这部分代码在:
frameworks/av/media/libstagefright/MediaCodec.cpp
frameworks/av/media/libstagefright/ACodec.cpp
/system/framework/lib/libstagefright.so这里主要是 media.codec 的 client 端,主要业务逻辑都是通过 media.codec 创建插件的解码器(node)的 instance,然后转由不同解码器的 instance 调用到解码器内部实现。MediaCodec 有2个静态创建 MediaCodec 的函数:一个 createByCodecName,一个 createDecoderByType。createByCodecName 传递的是解码器的名字,这个名字在 vendor 的 插件(我们这里就是前面说的那个 core[] 数组里面定义的)里面定义。当然 app 可以通过 MediaCodecList 罗列出所有支持指定类型的解码器的名字,例如说需要 “avc”(H264) 的解码器,一般返回的列表的第一个就是平台的硬解的解码器(例如说 SampleCode 里面 allwinner 开头的解码器)。当然你也可以根据平台自己写死解码器名字。第二个 createDecoderByType 传递的是需要解码器类型,例如说 “video/avc” ,内部也是通过 MediaCodecList 罗列出所有支持的解码器,然后取第一个。这个2个函数最终通路都是一样的,所以时序图只画了 createByCodecName,最后都是通过名字来创建解码器实例的。
MediaCodec 又包了一个 ACodec 的结构,真正处理逻辑的是 ACodec。ACodec 会连接 media.codec 服务,在服务端通过 omx master 创建对应 node(解码器)的 instance。
MediaCodec 和 ACodec 采用消息处理机制,分别有2个消息处理线程和队列。MediaCodec 的消息线程在 jni com_android_MediaCodec 的 JMediaCodec 构造函数里面创建,ACodec 则是在 MediaCodec 的 init 函数中创建。注意这里使用的 ALooper、AMessage 并不是 native ndk 那套 looper、message,而是自己 libstagefright 中 foundation 里面自己实现的一套(其实 android 里面也有一些重复的轮子)。一个 ALooper 都是一个 Thread 的封装,所以有处理消息的线程就有2个。
MediaCodec 和 ACodec 都采用状态机来处理逻辑,而且是各自独立管理的,也就是说有2套状态。MediaCodec 的状态官方用图表示出来了,就是前面贴的 MediaCodec 的框架图的那个状态图。而 ACodec 相当于是内部实现,官方并没有说明其状态变化,不过基本上套路也差不多。状态机基本上都是设置一个初始化状态,然后从一个状态迁合法的移到另外一个状态,如果当前状态不对的话,则会抛出错误。可以让复杂的逻辑有迹可循,可以借鉴学习一下。
MediaCodec 初始状态为 UNINITIALIZED, 创建 ACodec 对象(ACodec 对应初始状态为:UninitializedState) 。然后 MediaCodec 变成 INITIALIZING 调用 ACodec 的 initiateAllocateComponent。initiateAllocateComponent 会去连 meida.codec 创建对应的 node instance。这个时候 ACodec 会变成 Loaded 状态,然后通过回调通知 MediaCodec 变成 INITIALIZED 状态。
后续要调用 configure 进行配置,configure 套路和前面初始化基本一致,它主要是给 omx 配置 surface(buffer)。这里可以看到 omx 一般有2个端口,input port 和 output port,调用的时候需要指出是 input 还是 output,omx 的机制还不是很熟,这里不过多介绍。调用 configure 之后,MediaCodec 会从 INITIALIZED —> CONFIGURING —> CONFIGURED。ACodec 状态不变。
最后调用 start 让解码器开始工作。
Stagefright Engine(Service)
这部分代码在:
frameworks/av/media/libstagefright/omx/1.0/Omx.cpp
frameworks/av/media/libstagefright/omx/1.0/OMXMaster.cpp
frameworks/av/media/libstagefright/omx/1.0/OMXNodeInstance.cpp
/system/framework/lib/libstagefright_omx.so这部前面介绍过服务初始的流程。 承接前面,当 app 要创建指定的解码器,client 那边会调用 allocateNode 让服务创建指定解码器的 instance。service 这边会先 new 一个 OMXNodeInstance, 然后在前面加载好的mPluginByComponetName 列表中找到对应的 vendor plugin(我们这里是 AwOMXPlugin),然后调用 plugin 的 OMX_GetHandle 函数(这里就是 AwOmxComponentCreate)。这个函数里面会查找前面说的 core[] 这个数组,这个数组里面定义了解码器对应实现的 so:
|
|
例如说这个 H264 的解码器对应实现的 so 就是 libOmxVdec.so 。然后会在这个 so 中查找 OMXNode 的接口 函数,并调用。
- service 这边的逻辑就是上面那样,创建了 node 的对象(就是 node 的实例),然后调用 node 的方法。通过这里可以看到 omx 的结构是:master 管理(创建) plugin,一般 android 自带一个 soft plugin,vendor 带一个硬件 plugin;plugin 里面有一系列 node(一个 node 就是一个解码器),一个 node 可以创建多个实例(instance)。app 通过解码器的名字(node 的名字)创建 node 的 instance。当然了 vendor 的 plugin 和 node 都要实现 android 定义的 omx 相关的接口。
OMX Plugin
这部分代码在:
frameworks/av/media/libcederc/openomx/libstagefrighthw/AwOMXPlugin.cpp
/vendor/lib/libstagefrighthw.so这部分前面已经介绍过了。这里可以发现一个规律,android 框架代码的 so,都是在 /system/lib/ 下面的,vnedor 自己实现的 so 都是 /vendor/lib/ 下面的。
OMX IL Component
这部分代码在:
frameworks/av/media/libcederc/openomx/omxcore/aw_omx_core.c
frameworks/av/media/libcederc/openomx/omxcore/aw_omx_component.c
frameworks/av/media/libcederc/openomx/vdec/omx_vdec.c
/vendor/lib/libOmxCore.so
/vendor/lib/libOmxVdec.so这里接着前面 plugin 创建 node 那里。H264 实现的 so 是 libOmxVdec 。在 OMX_GerHandle 函数里面,回去 libOmxVdec.so 里面查找加载 node 的初始化函数 AwOmxComponentCreate(对应实现函数 __AwOmxComponentCreate),这个函数里面会先 new 一个 AwOmxVdec 的对象(这个应该是对应解码器的对象了,也可以说是 node 的对象),然后初始化 AwOmxVdec,把接口的函数指针都赋值。这样后续就能通过接口调用到解码器内部的函数了。至于这个 so 里面的实现目前还没细看(应该和 AwPlayer 一样,后面关键的代码都是二进制的 so 里面的)。
时序图可以看到创建 MediaCodec 是创建解码器对象,configure 是配置 surface(MediaCodec 的 configure 流程很复杂,有600多行,这里只选了我感兴趣的看),start 是申请了 buffer。start 之后解码器内部应该运转起来了。解码器内部可以通过 send event 的方式来通知 client 端的 ACodec(MediaCodec),运转起来之后应该状态会变成官网描述的 Flushed。
MediaCodec 还是比 MediaPlayer 复杂很多(当然 AwPlayer 的具体实现应该也挺复杂的),但是给予更多的灵活性。android 多媒体编解码的这2条通路的设计思路还是值得学习的。
MediaRecoder
Sample Code
待补充
数据通路
简单写一下代码位置:
java: android/frameworks/base/media/java/android/media/MediaRecorder.java
jni: android/frameworks/base/media/jni/android_media_MediaRecorder.cpp
native: android/frameworks/av/media/libmedia/mediarecorder.cpp
service: android/frameworks/av/media/libmediaplayerservice/StagefrightRecorder.cpp
未完待补充