Android 13 - Media框架(21)- ACodec(三)

这一节我们一起来了解 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;
  1. 判断当前的组件是 encoder 还是 decoder;
  2. 判断组件是 audio 还是 video;
  3. 初始化 input 和 ouput format 以及 port mode;

在这里 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(&param);
            param.bEnabled = OMX_FALSE;
            if (mOMXNode->getParameter(index, &param, 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(&param);
            param.nPortIndex = kPortIndexOutput;

            status_t err = mOMXNode->getParameter(
                    (OMX_INDEXTYPE)OMX_IndexParamVideoAndroidRequiresSwRenderer,
                    &param, 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

    • non-secure:kPortModePresetByteBuffer,上层可以直接读写buffer;
    • secure:kPortModePresetSecureBuffer,上层需要用特殊的方式读写buffer;
  • out Port

    • has native window
      • tunnel mode:上层不需要接收 output buffer;
      • non tunnel mode:kPortModeDynamicANWBuffer,使用动态的native window;
    • no native window
      • non-secure: kPortModePresetByteBuffer,上层可以直接读写buffer;
      • secure:error,不允许这种情况。

完成上述一系列操作后,还需要再调用 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> &params) {
    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 方法就算阅读完成了,我们下节再见。

你可能感兴趣的:(Android,Media,android,Framework,Media,多媒体,C++)