MediaCodec 是 Android 平台上音视频编解码的标准接口,无论是使用软解还是硬解都要通过调用 MediaCodec来完成,是学习 Android 音视频不可跳过的重要部分。MediaCodec 部分的代码有几千行,光是头文件就有几百行,对于我这样的新手来说,简直就劝退了,又或者是硬着头皮往下看,一行一行阅读,看到里面的各种状态各种变量,很容易就晕了。我们这一篇笔记旨在从设计思路的角度了解 MediaCodec,不仅仅是粘贴代码流程,力求更好地帮助理解 MediaCodec 的原理。
ps:由于本人水平有限,MediaCodec 中的部分内容也没有理解,如果有错误还恳请指正。
1、准备工作
MediaCodec 中有一些类我们可以不用看,例如 ResourceManagerServiceProxy,大概是用于资源管理的,我们碰到可以跳过;还有一个类 Histogram,应该是做 decoder 性能统计用的,阅读过程中碰到同样跳过。
目前我只阅读了 ACodec,所以文中设计 CCodec 的部分暂时跳过,另外关于 ACodec 我们只要了解有什么接口就行,不必深入了解它的内部实现。
MediaCodec 使用异步消息处理机制(AMessage/ALooper/AHandler),不是很了解的同学可以阅读之前的文章 Android 13 - Media框架 - 异步消息机制。
观察 MediaCodec 对外开放的接口可以发现,很多接口使用的是消息的同步处理方法,例如 configure、setCallback、queueInputBuffer、dequeueInputBuffer 等等都是调用的 AMessage 的 postAndAwaitResponse 方法,为什么这里不直接在函数体中实现功能,非要使用消息同步消息机制处理呢?原因我们在之前的笔记中已经提到过了,异步消息处理机制可以帮我们实现线程同步,避免异常状态的出现。ACodec 会频繁向 MediaCodec 上抛消息,如果接口调用过程中有一些事件发送,很容易就出现异常了,使用异步消息机制将上层命令和底层回调放到同一个线程中处理,就会井井有条了。
可能有人还会问,为什么MediaCodec 部分对外接口不用异步处理呢?有些接口不太适宜异步调用,比如上面提到的queueInputBuffer 等,设计为异步的会增加外层的设计难度;其他的我也不是很了解,但是我觉得 MediaCodec 的外层 NuPlayerDecoder 已经使用了异步处理,如果 MediaCodec 也使用异步,异步套异步的情况下需要考虑的状态反而会变的更多,可能会得不偿失。
接下来我们就正式去看 MediaCodec 的实现。
2、MediaCodec的创建
cpp
static sp<MediaCodec> CreateByType(
const sp<ALooper> &looper, const AString &mime, bool encoder, status_t *err = NULL,
pid_t pid = kNoPid, uid_t uid = kNoUid);
static sp<MediaCodec> CreateByType(
const sp<ALooper> &looper, const AString &mime, bool encoder, status_t *err,
pid_t pid, uid_t uid, sp<AMessage> format);
static sp<MediaCodec> CreateByComponentName(
const sp<ALooper> &looper, const AString &name, status_t *err = NULL,
pid_t pid = kNoPid, uid_t uid = kNoUid);
MediaCodec 隐藏了自己的构造函数,对外提供了三个静态函数用于创建自身实例,ps:这算不算建造者模式?。
- CreateByType 会根据媒体类型(mime type)来选择合适的编解码组件(component),使用时需要指定创建解码器(decoder)还是编码器(encoder);要注意的是第二个CreateByType会传递 format 参数,这里传递的参数并不会用于 configure,仅仅用于编解码组件的选择;
- CreateByComponentName 是由使用者指定要创建的编解码组件;
这三个方法最终会调用 MediaCodec 构造函数,并且调用 init 方法将 component name 传递给 MediaCodec。
MediaCodec 构造函数主要用于初始化成员变量,需要重点关注的是两个成员 mGetCodecBase
和 mGetCodecInfo
,这是两个函数指针:
- mGetCodecBase:根据 component name 创建对应的 CodecBase 对象(ACodec/CCodec);
- mGetCodecInfo:获取 component 所支援的媒体类型信息(MediaCodecInfo);
如果我们没有指定这两个函数,MediaCodec 为我们提供了默认实现,我们这里暂时先不了解。init 方法主要完成了 MediaCodec 的初始化工作,内容如下:
cpp
status_t MediaCodec::init(const AString &name) {
// save init parameters for reset
mInitName = name;
mCodecInfo.clear();
bool secureCodec = false;
const char *owner = "";
if (!name.startsWith("android.filter.")) {
// 1、根据 component 获取 codecInfo
err = mGetCodecInfo(name, &mCodecInfo);
// 2、这里会做 double check
if (err != OK) {
mCodec = NULL; // remove the codec.
return err;
}
if (mCodecInfo == nullptr) {
ALOGE("Getting codec info with name '%s' failed", name.c_str());
return NAME_NOT_FOUND;
}
// 3、判断选择的是否是 secure component
secureCodec = name.endsWith(".secure");
Vector<AString> mediaTypes;
// 4、获取当前的 codecInfo 中的 mediatype,判断创建的是什么组件
mCodecInfo->getSupportedMediaTypes(&mediaTypes);
for (size_t i = 0; i < mediaTypes.size(); ++i) {
if (mediaTypes[i].startsWith("video/")) {
mDomain = DOMAIN_VIDEO;
break;
} else if (mediaTypes[i].startsWith("audio/")) {
mDomain = DOMAIN_AUDIO;
break;
} else if (mediaTypes[i].startsWith("image/")) {
mDomain = DOMAIN_IMAGE;
break;
}
}
// 5、获取 component 隶属的架构
owner = mCodecInfo->getOwnerName();
}
// 6、根据架构名和组件名创建CodecBase
mCodec = mGetCodecBase(name, owner);
// 7、如果是 video,则需要使用单独的 Looper
if (mDomain == DOMAIN_VIDEO) {
// video codec needs dedicated looper
if (mCodecLooper == NULL) {
status_t err = OK;
mCodecLooper = new ALooper;
mCodecLooper->setName("CodecLooper");
err = mCodecLooper->start(false, false, ANDROID_PRIORITY_AUDIO);
}
mCodecLooper->registerHandler(mCodec);
} else {
mLooper->registerHandler(mCodec);
}
mLooper->registerHandler(this);
// 8、创建 CodecCallback 和 BufferCallback
mCodec->setCallback(
std::unique_ptr<CodecBase::CodecCallback>(
new CodecCallback(new AMessage(kWhatCodecNotify, this))));
mBufferChannel = mCodec->getBufferChannel();
mBufferChannel->setCallback(
std::unique_ptr<CodecBase::BufferCallback>(
new BufferCallback(new AMessage(kWhatCodecNotify, this))));
// 9、涉及到 CodecBase 状态的调用,用消息处理
sp<AMessage> msg = new AMessage(kWhatInit, this);
if (mCodecInfo) {
msg->setObject("codecInfo", mCodecInfo);
}
msg->setString("name", name);
sp<AMessage> response;
err = PostAndAwaitResponse(msg, &response);
return err;
}
- 根据传入参数 component name 获取对应的 codec Info,再反向获取 component 所支持的 mime type;可能会有人问,我上面明明已经传入 mime type了,为什么这里还要费力再反向获取呢?我觉得这是为了 CreateByComponentName 服务的,有的时候我们指定了组件名称,但是 MediaCodec 并不知道它是 audio 组件还是 video 组件,也不知道该组件系统是否支持,所以这里做了一次判断,这个步骤对于 CreateByType 是多余的,因为调用 init 之前已经通过 MediaCodecList 获取了支持当前 mime 的组件明;
- 获取 component 隶属的架构,MediaCodec 当前串接有两个编解码框架,一个是老的 ACodec/OMX 架构,另一个是新的 CCodec 架构,owner name 就是用来区分 component 是哪个架构下的,之后会根据这个 name 创建对应的 CCodec;
- 调用 mGetCodecBase 函数,利用 owner name 和 component name 创建 CodecBase 对象,我们暂时只看
ACodec
; - 将 AHandler 注册到 ALooper 中,注意的是 Video Codec 将独享一个 looper,而 MediaCodec、Audio Codec 会共享上层传递的 looper;
- 给 ACodec 和 BufferChannel 注册回调消息;
- 发送消息,到 looper 线程中处理 kWhatInit 方法,调用 ACodec 的 initiateAllocateComponent 方法,这里我们要注意的是,发送消息调用的方法是 PostAndAwaitResponse,它内部封装的是 AMessage.postAndAwaitResponse,是==阻塞处理消息==。
接下来我们一起来看 kWhatInit 是如何处理的:
cpp
case kWhatInit:
{
// 1、检查状态
if (mState != UNINITIALIZED) {
PostReplyWithError(msg, INVALID_OPERATION);
break;
}
// 2、判断是否正在等待某个方法返回
if (mReplyID) {
// 如果是就将消息先加入到容器中等待处理
mDeferredMessages.push_back(msg);
break;
}
sp<AReplyToken> replyID;
CHECK(msg->senderAwaitsResponse(&replyID));
mReplyID = replyID;
// 3、设置新状态
setState(INITIALIZING);
sp<RefBase> codecInfo;
(void)msg->findObject("codecInfo", &codecInfo);
AString name;
CHECK(msg->findString("name", &name));
sp<AMessage> format = new AMessage;
if (codecInfo) {
format->setObject("codecInfo", codecInfo);
}
format->setString("componentName", name);
// 4、调用 CodecBase 方法
mCodec->initiateAllocateComponent(format);
break;
}
- 检查当前 MediaCodec 状态;我觉得 MediaCodec 的状态有两种作用,一个是检查当前的函数是否在合法状态下调用,另一个是当有事件需要处理时,根据当前的状态做出不同的反应,例如 callback 到达时根据当前状态做不同处理,又比如 stop 和 release 方法,同样需要根据当前状态处理不同的事务。因为 MediaCodec 以阻塞处理为主,所以状态是否合法不会有什么问题,我们在后面的代码中会做忽略。
- MediaCodec 会有一些中间状态,例如这里的
INITIALIZING
,表示正在处理 init 的过程中。这里有个==DeferredMessages==用于存储即将延时处理的消息,这个情况什么时候会出现?比如说我们当前正在处理上层调用的 flush 方法,调用 ACodec 的异步方法后会等待消息返回,这期间收到了 BufferChannel 发过来的消息,消息会进入到 looper 线程处理,但是我们要先等待 ACodec flush 处理完成,再去处理 BufferChannel 的消息,因为 flush 之后所有的 buffer 将被刷新。我觉得 DeferredMessages 是用来处理消息优先级的,如果当前有上层函数调用(命令),将会优先等待这些消息处理完成。 - 调用 setState 设置当前状态,我们要了解的是出了设置状态外,里面还会重置 flag 等内容,flag 具体有什么作用我们后面再看;
- 调用 CodecBase 的 initiateAllocateComponent 方法,从这里我们大致可以了解到,创建 Component 只需要传递 component name 一个参数即可。