这一节我们一起来了解 ACodec 是如何通过 configureCodec 方法配置 OMX 组件的,因为 configureCodec 代码比较长,所以我们会把代码进行拆分来了解。
ps:这部分的代码我们先跳过 encoder 的流程。
先来看函数入参,第一个参数 mime,第二个参数为 AMessage,不过看 onConfigureComponent 代码我们就可以知道 mime 是来自于 msg 的,所以这里边的信息会有重复。
status_t ACodec::configureCodec(const char *mime, const sp<AMessage> &msg)
进入函数体内,首先干了3件事情:
int32_t encoder;
if (!msg->findInt32("encoder", &encoder)) {
encoder = false;
}
// 创建空白的input / output format message
sp<AMessage> inputFormat = new AMessage;
sp<AMessage> outputFormat = new AMessage;
mConfigFormat = msg;
mIsEncoder = encoder;
mIsVideo = !strncasecmp(mime, "video/", 6);
mIsImage = !strncasecmp(mime, "image/", 6);
// 初始化 port mode
mPortMode[kPortIndexInput] = IOMX::kPortModePresetByteBuffer;
mPortMode[kPortIndexOutput] = IOMX::kPortModePresetByteBuffer;
在这里 input format 和传入参数 msg 中的内容有一些区别,msg 中的信息都是由 extractor 解析出来的,input format 中除了有这些信息外,还存储有用于配置 OMX 组件的一些信息;output format 中存储的是 OMX 组件回传给上层的输出格式信息。
接下来有一个很重要的内容就是 Port Mode,每个组件会有两个端口(input / output Port),这里的 mode 指的就是对这两个端口的定义,影响的是在端口中传输的 buffer 的类型。
PortMode
定义位于 frameworks/av/media/libmedia/include/media/IOMX.h
之所以把 PortMode 放在这个文件中,是因为它只用于 ACodec 和 OMXNode 当中,OMXNode 收到 ACodec 设定下来的 mode 之后会做相关处理之后再传送给 OMX 组件。
PortMode 分为两组:
enum PortMode {
kPortModePresetStart = 0,
kPortModePresetByteBuffer,
kPortModePresetANWBuffer,
kPortModePresetSecureBuffer,
kPortModePresetEnd,
kPortModeDynamicStart = 100,
kPortModeDynamicANWBuffer, // uses metadata mode kMetadataBufferTypeANWBuffer
// or kMetadataBufferTypeGrallocSource
kPortModeDynamicNativeHandle, // uses metadata mode kMetadataBufferTypeNativeHandleSource
kPortModeDynamicEnd,
};
一组以 Preset 前缀,另一组以 Dynamic 作为前缀。他们两个的区别在于,Preset 表示端口内的 buffer 在启动前已经被预先设定好了,在编解码组件运行过程中这些 buffer 不会发生变化
;Dynamic 则表示动态,意思就是组件运行过程中,我们使用的 buffer 可能会发生动态变化,有一些 buffer 可能被弃用,也有一些 buffer 会被新加入使用;这里对部分 PortMode 类型进行描述:
kPortModePresetByteBuffer
:使用最普通的 buffer,我们可以很轻松访问到 buffer 中的内容;kPortModePresetANWBuffer
:使用预先设定的 Native Window Buffer,我们将无法直接访问这块 buffer 中的数据;kPortModePresetSecureBuffer
:使用预先设定的 Secure Buffer,我们无法直接访问 buffer 中的内容;kPortModeDynamicANWBuffer
:使用动态的 Native Window Buffer,我们将无法直接访问这块 buffer 中的数据;kPortModeDynamicNativeHandle
:使用动态的 Native Buffer Handle,buffer 以 handle 的形式回传上来,我们将无法直接访问 buffer 中的数据;具体这些 Mode 会在什么情况下使用,我们在后面会看到。
status_t ACodec::setComponentRole(
bool isEncoder, const char *mime) {
const char *role = GetComponentRole(isEncoder, mime);
if (role == NULL) {
return BAD_VALUE;
}
status_t err = SetComponentRole(mOMXNode, role);
if (err != OK) {
ALOGW("[%s] Failed to set standard component role '%s'.",
mComponentName.c_str(), role);
}
return err;
}
接下来会调用 setComponentRole 方法,首先来讲我理解的为什么要调用这个方法:我们实现的 OMX 组件可能共享的是一套流程,也就是各个组件 lib 可能是链接到同一个lib当中,那这里就会有一个问题,每当我们调用 getHandle 创建一个句柄时,组件并不知道我们要对什么格式的数据进行处理,也不知道是做编码还是做解码,所以上层需要设定相关参数通知 OMX 组件它需要走什么流程。
这个方法还有一个作用,它内部有个 GetComponentRole 方法,会根据传进来的 mime type 获取对应的 Role,如果这里没有找到对应的 role,则会发生 error,因此如果要支持某个格式的播放,则必须要修改 GetComponentRole 中使用到的一个数组 kMimeToRole
,我们这里就不再展开了。
这里 SetComponentRole 是如何将参数设定下去的也不做过多的解释,主要流程是创建一个参数,初始化参数,设定参数内容,调用 setParameter 传递参数。
status_t SetComponentRole(const sp<IOMXNode> &omxNode, const char *role) {
OMX_PARAM_COMPONENTROLETYPE roleParams;
InitOMXParams(&roleParams);
strncpy((char *)roleParams.cRole,
role, OMX_MAX_STRINGNAME_SIZE - 1);
roleParams.cRole[OMX_MAX_STRINGNAME_SIZE - 1] = '\0';
return omxNode->setParameter(
OMX_IndexParamStandardComponentRole,
&roleParams, sizeof(roleParams));
}
我们这里先跳过 encoder 的流程,所以接下来会跳过一部分代码。
int32_t lowLatency = 0;
if (msg->findInt32("low-latency", &lowLatency)) {
err = setLowLatency(lowLatency);
if (err != OK) {
return err;
}
}
判断并设定 OMX 组件是否打开低延迟的模式,我理解的 low-latency
可能是 OMX 组件低缓冲快速解码。
// 查找是否有 native window(surface)设定下来
sp<RefBase> obj;
bool haveNativeWindow = msg->findObject("native-window", &obj)
&& obj != NULL && mIsVideo && !encoder;
mUsingNativeWindow = haveNativeWindow;
if (mIsVideo && !encoder) {
inputFormat->setInt32("adaptive-playback", false);
int32_t usageProtected;
// 如果 format 中有设定 protect 信息
if (msg->findInt32("protected", &usageProtected) && usageProtected) {
if (!haveNativeWindow) {
ALOGE("protected output buffers must be sent to an ANativeWindow");
return PERMISSION_DENIED;
}
// 设定相关flag
mFlags |= kFlagIsGrallocUsageProtected;
mFlags |= kFlagPushBlankBuffersToNativeWindowOnShutdown;
}
}
// 如果是 secure 组件
if (mFlags & kFlagIsSecure) {
// use native_handles for secure input buffers
err = setPortMode(kPortIndexInput, IOMX::kPortModePresetSecureBuffer);
if (err != OK) {
ALOGI("falling back to non-native_handles");
setPortMode(kPortIndexInput, IOMX::kPortModePresetByteBuffer);
err = OK; // ignore error for now
}
OMX_INDEXTYPE index;
if (mOMXNode->getExtensionIndex(
"OMX.google.android.index.preregisterMetadataBuffers", &index) == OK) {
OMX_CONFIG_BOOLEANTYPE param;
InitOMXParams(¶m);
param.bEnabled = OMX_FALSE;
if (mOMXNode->getParameter(index, ¶m, sizeof(param)) == OK) {
if (param.bEnabled == OMX_TRUE) {
mFlags |= kFlagPreregisterMetadataBuffers;
}
}
}
}
以上这段代码就开始使用到我们上文讲到的 PortMode 了。如果使用的是 secure 组件,那么 input port mode 会被设定为 kPortModePresetSecureBuffer,这种情况下,input buffer将会使用预设的 secure buffer,我们在向 input buffer 写入数据的时候会与普通的memcpy有一些不同,这一点我们后续再看;如果port mode 设定失败,那么将尝试使用 kPortModePresetByteBuffer mode,也就是所谓的普通 buffer。
if (haveNativeWindow) {
sp<ANativeWindow> nativeWindow =
static_cast<ANativeWindow *>(static_cast<Surface *>(obj.get()));
int32_t tunneled;
if (msg->findInt32("feature-tunneled-playback", &tunneled) &&
tunneled != 0) {
ALOGI("Configuring TUNNELED video playback.");
mTunneled = true;
int32_t audioHwSync = 0;
if (!msg->findInt32("audio-hw-sync", &audioHwSync)) {
ALOGW("No Audio HW Sync provided for video tunnel");
}
err = configureTunneledVideoPlayback(audioHwSync, nativeWindow);
if (err != OK) {
ALOGE("configureTunneledVideoPlayback(%d,%p) failed!",
audioHwSync, nativeWindow.get());
return err;
}
int32_t maxWidth = 0, maxHeight = 0;
if (msg->findInt32("max-width", &maxWidth) &&
msg->findInt32("max-height", &maxHeight)) {
err = mOMXNode->prepareForAdaptivePlayback(
kPortIndexOutput, OMX_TRUE, maxWidth, maxHeight);
if (err != OK) {
ALOGW("[%s] prepareForAdaptivePlayback failed w/ err %d",
mComponentName.c_str(), err);
// allow failure
err = OK;
} else {
inputFormat->setInt32("max-width", maxWidth);
inputFormat->setInt32("max-height", maxHeight);
inputFormat->setInt32("adaptive-playback", true);
}
}
}
视频解码完成,如果要显示(渲染)就需要一个 Native Window,可以理解为一个窗口。渲染有两种AvSync 方式,一种是利用硬件同步,另一种是软件同步。硬件同步也就是上面代码中的 tunnel mode,需要从 Audio HAL 获取一个 hw sync id,然后把该 id 传递给 OMX 组件,创建出一个 sideband Handle,最后将该 handle 与 native window绑定,这样就完成了硬件同步的设定,具体硬件同步是如何实现的,暂时还没有做了解。
上面所说的绑定是通过调用 configureTunneledVideoPlayback 来完成,里面有两步内容,一是创建 handle,而是将handle 与 native window 绑定,这里不做展开。
这里有一点内容我们需要提前做了解,由于 Tunnel Mode 的 AvSync 不需要上层再做介入,所以 output buffer 将不再会回流到上层。这种模式下 ouput port mode将没有意义。
else {
ALOGV("Configuring CPU controlled video playback.");
mTunneled = false;
// Explicity reset the sideband handle of the window for
// non-tunneled video in case the window was previously used
// for a tunneled video playback.
err = native_window_set_sideband_stream(nativeWindow.get(), NULL);
if (err != OK) {
ALOGE("set_sideband_stream(NULL) failed! (err %d).", err);
return err;
}
err = setPortMode(kPortIndexOutput, IOMX::kPortModeDynamicANWBuffer);
if (err != OK) {
// if adaptive playback has been requested, try JB fallback
// NOTE: THIS FALLBACK MECHANISM WILL BE REMOVED DUE TO ITS
// LARGE MEMORY REQUIREMENT
// we will not do adaptive playback on software accessed
// surfaces as they never had to respond to changes in the
// crop window, and we don't trust that they will be able to.
int usageBits = 0;
bool canDoAdaptivePlayback;
if (nativeWindow->query(
nativeWindow.get(),
NATIVE_WINDOW_CONSUMER_USAGE_BITS,
&usageBits) != OK) {
canDoAdaptivePlayback = false;
} else {
canDoAdaptivePlayback =
(usageBits &
(GRALLOC_USAGE_SW_READ_MASK |
GRALLOC_USAGE_SW_WRITE_MASK)) == 0;
}
int32_t maxWidth = 0, maxHeight = 0;
if (canDoAdaptivePlayback &&
msg->findInt32("max-width", &maxWidth) &&
msg->findInt32("max-height", &maxHeight)) {
ALOGV("[%s] prepareForAdaptivePlayback(%dx%d)",
mComponentName.c_str(), maxWidth, maxHeight);
err = mOMXNode->prepareForAdaptivePlayback(
kPortIndexOutput, OMX_TRUE, maxWidth, maxHeight);
ALOGW_IF(err != OK,
"[%s] prepareForAdaptivePlayback failed w/ err %d",
mComponentName.c_str(), err);
if (err == OK) {
inputFormat->setInt32("max-width", maxWidth);
inputFormat->setInt32("max-height", maxHeight);
inputFormat->setInt32("adaptive-playback", true);
}
}
// allow failure
err = OK;
}
还有一种渲染方式被称为软件同步,指的是应用内控制 output buffer 的渲染时机,上层需要拿到 output buffer,根据对应的pts决定什么时候要渲染,这种情况下上层就需要拿到 output buffer 了。这时候的 output port mode 会被设定为 kPortModeDynamicANWBuffer
,为什么是这个呢?buffer 的渲染其实就是把来自于 native window 的 buffer 填充满数据再返还回去,所以 buffer 的类型是 ANWBuffer;那还有一个问题,为什么前缀用的是 Dynamic 呢?这是因为 native window 的 buffer queue 中的 buffer 可能会发生变化,我们返还回去再拿回来的 buffer 并不一定是一开始存到 Port 中的 buffer。
if (mIsVideo || mIsImage) {
// determine need for software renderer
bool usingSwRenderer = false;
if (haveNativeWindow) {
bool requiresSwRenderer = false;
OMX_PARAM_U32TYPE param;
InitOMXParams(¶m);
param.nPortIndex = kPortIndexOutput;
status_t err = mOMXNode->getParameter(
(OMX_INDEXTYPE)OMX_IndexParamVideoAndroidRequiresSwRenderer,
¶m, sizeof(param));
if (err == OK && param.nU32 == 1) {
requiresSwRenderer = true;
}
if (mComponentName.startsWith("OMX.google.") || requiresSwRenderer) {
usingSwRenderer = true;
haveNativeWindow = false;
(void)setPortMode(kPortIndexOutput, IOMX::kPortModePresetByteBuffer);
} else if (!storingMetadataInDecodedBuffers()) {
err = setPortMode(kPortIndexOutput, IOMX::kPortModePresetANWBuffer);
if (err != OK) {
return err;
}
}
}
如果使用的是软件解码器,或者需要软件渲染,由于需要获取到 output buffer 中的内容(读取写入),所以要把 output mode 设定为 kPortModePresetByteBuffer,这种情况用的是比较少的。
这里还有了解一个方法 storingMetadataInDecodedBuffers
:
inline bool storingMetadataInDecodedBuffers() {
return (mPortMode[kPortIndexOutput] == IOMX::kPortModeDynamicANWBuffer) && !mIsEncoder;
}
当解码器的 output port mode 为 kPortModeDynamicANWBuffer时,这时候用的就是 MetaData 模式,这里的 meta data 指的,回传给上层的 buffer 中的数据不是真正的yuv数据(raw data),数据中存储的时一个地址或者一个句柄,这样可以减少 buffer 的拷贝。
那如果没有 Native Window 呢?ouput port mode应该如何设定呢?应该是保持默认的状态kPortModePresetByteBuffer,上层可以获取到output buffer中的内容。但是有一个特例,如果使用的是 secure 组件,那么output buffer 必须要送到 native window,否则将会发生错误,这里可以理解为,secure 组件需要保护输出。
看起来 port mode 情况比较多,但是其实并不复杂,我们再做一次整理:
in Port
out Port
完成上述一系列操作后,还需要再调用 setupVideoDecoder
对解码器进行配置,这里不对所有内容做展开,只对一个内容做了解:
status_t ACodec::setupVideoDecoder(
const char *mime, const sp<AMessage> &msg, bool haveNativeWindow,
bool usingSwRenderer, sp<AMessage> &outputFormat) {
status_t err = GetVideoCodingTypeFromMime(mime, &compressionFormat);
if (err != OK) {
return err;
}
}
static const struct VideoCodingMapEntry {
const char *mMime;
OMX_VIDEO_CODINGTYPE mVideoCodingType;
} kVideoCodingMapEntry[] = {
{ MEDIA_MIMETYPE_VIDEO_AVC, OMX_VIDEO_CodingAVC },
{ MEDIA_MIMETYPE_VIDEO_HEVC, OMX_VIDEO_CodingHEVC },
{ MEDIA_MIMETYPE_VIDEO_MPEG4, OMX_VIDEO_CodingMPEG4 },
{ MEDIA_MIMETYPE_VIDEO_H263, OMX_VIDEO_CodingH263 },
{ MEDIA_MIMETYPE_VIDEO_MPEG2, OMX_VIDEO_CodingMPEG2 },
{ MEDIA_MIMETYPE_VIDEO_VP8, OMX_VIDEO_CodingVP8 },
{ MEDIA_MIMETYPE_VIDEO_VP9, OMX_VIDEO_CodingVP9 },
{ MEDIA_MIMETYPE_VIDEO_DOLBY_VISION, OMX_VIDEO_CodingDolbyVision },
{ MEDIA_MIMETYPE_IMAGE_ANDROID_HEIC, OMX_VIDEO_CodingImageHEIC },
{ MEDIA_MIMETYPE_VIDEO_AV1, OMX_VIDEO_CodingAV1 },
};
setupVideoDecoder 中调用了一个 GetVideoCodingTypeFromMime 方法,获取 mime 对应的编码格式,如果获取不到,那么将会认为当前系统不支持该格式的编解码。上文中我们对组件设定了 component role,让组件走指定格式的流程,这里我们还要再获取编码格式,感觉是有点重复判断了。后期如果要支援更多的编解码类型,这里一定要记得添加相关的内容。
setupVideoDecoder 调用完成后会有一个 output format 回传,这里的 format 并不准确,可能是 omx 组件的一些默认参数和格式,当正式开始解码后,会有正确的 output format 再回传。
以上是 Video 相关的配置,接下来我们要大致了解下 Audio 的配置,不同的 Audio 需要不同的 input format,所以需要对格式进行判断,并且对格式中的内容进行解析,再分别将参数设定给 OMX 组件,这部分可能是不够统一。
以上配置都是设置的Android为我们定义好的参数,有时候厂商会有一些自定义参数,这时候我们可以调用setVendorParameters
设定这些自定义参数。
status_t ACodec::setVendorParameters(const sp<AMessage> ¶ms) {
std::map<std::string, std::string> vendorKeys; // maps reduced name to actual name
constexpr char prefix[] = "vendor.";
constexpr size_t prefixLength = sizeof(prefix) - 1;
// longest possible vendor param name
char reducedKey[OMX_MAX_STRINGNAME_SIZE + OMX_MAX_STRINGVALUE_SIZE];
...
}
vendor 参数需要以 vendor.
开头,其他的内容等具体使用到了我们再研究。
到这里为止 configureCodec 方法就算阅读完成了,我们下节再见。