开源播放器CicadaPlayer中组件穿透的实现

什么是组件穿透

一个应用程序实例完成一件事情,一般需要组合多个组件来完成,比如在一次网络视频播放过程中有个播放器实例需要使用DataSource demxuer decoder render等模块来完成。而每个模块会根据当前的视频创建不同的组件来完成相应的功能,如http的网络流需要启用http DataSource组件,本地文件的播放需要启用file DataSource组件。那么不同的组件可能会有不同的特性和可配置的参数,对于一个播放器来说,在某些情况下可能无法统一去配置和管理,但又要能让用户去配置和管理这些组件。那么我们提供一个App层和组件层直接通信的通道,Direct Component Access(简称DCA)来完成此功能。

CicadaPlayer引入DCA的原因

CicadaPlayer在架构设计的时候就把插件化,可扩展性考虑进来,每个模块都有可能被其他人去扩展,比如去扩展一个解码器,或者一个demuxer,而这些扩展的组件可能本身有的功能要比播放器需要的多,而在播放器整个流程里面并不涉及到这些多出来的功能,比如解码器可以根据视频的特性配置一些参数,可让解码流程变得更好,demuxer需要上报一些数据到某个地方等,这些功能播放器是无法预知的,更不可能为每个模块都添加一些特定的事件和命名,所以CicadaPlayer采用了DCA的方式,给每个模块*。

CicadaPlayer目前DCA的实现

DCA的原型

IDCA.h

// direct component access
namespace Cicada {
    class IDCAObserver {
    public:
        virtual void onEvent(int level, const std::string &content) = 0;
    };
    class IDCA {
    public:
        void setDCAObserver(IDCAObserver *observer)
        {
            mObserver = observer;
        }
        virtual int invoke(int cmd, const std::string &content) = 0;
        virtual ~IDCA() = default;

    protected:
        void sendEvent(int level, const std::string &content)
        {
            if (mObserver) {
                mObserver->onEvent(level, content);
            }
        };

    private:
        IDCAObserver *mObserver{nullptr};
    };
}// namespace Cicada

class IDCA 中对外有两个接口

  1. setDCAObserver,此接口是要向DCA中设置一个监听者,也就是component的上行通道,继承了IDCA的类就可以通过sendEvent向外面的监听者发送事件消息。
  2. invoke,此接口是App向component发送命名的下行通道,调用该接口可以向对应的component发送消息或者命令。

以上两个接口的参数都是字符串,这个是为了扩展,此字符串可以是任意格式的内容。

IDemuxer继承了IDCA

目前只有IDemuxer继承了IDCA,也就是说所有的demuxer组件都具备了DCA的能力。其他模块暂时没有继承,有需要的话后面可以去继承,目前其他模块暂时无需求。

SuperMediaPlayer中对DCA的管理

SuperMediaPlayer是整个播放器的编排层,需要管理所有组件的DCA逻辑,在播放器的代码中我们增加了一个友元类来单独管理。

SMP_DCAManager

    class mediaPlayerDCAObserverListener {
    public:
        virtual void onEvent(const std::string &content) = 0;
    };
    class SMP_DCAObserver : public IDCAObserver {
    public:
        explicit SMP_DCAObserver(std::string className, std::string compName, void *obj)
            : mClass(std::move(className)), mName(compName), mObj(obj)
        {}
        void setListener(mediaPlayerDCAObserverListener *listener);

        void hello();

    private:
        void onEvent(int level, const std::string &content) override;

    private:
        std::string mClass{};
        std::string mName{};
        void *mObj{nullptr};
        mediaPlayerDCAObserverListener *mListener{nullptr};
    };
    class SMP_DCAManager : public mediaPlayerDCAObserverListener {
    public:
        explicit SMP_DCAManager(SuperMediaPlayer &player) : mPlayer(player)
        {}

        void createObservers();

        int invoke(const std::string &content);

        std::string getEvent();

        void reset();

    private:
        void onEvent(const std::string &content) override;

    private:
        SuperMediaPlayer &mPlayer;
        std::unique_ptr<SMP_DCAObserver> mDemuxerObserver{nullptr};
        std::queue<std::string> mEventQue;
        std::mutex mMutex;
    };

mediaPlayerDCAObserverListener

定义了向外输出的event是一个字符串:
virtual void onEvent(const std::string &content) = 0;

SMP_DCAObserver

实现了一个IDCAObserver的接口,用于接收来自于组件的消息,
实现了
void onEvent(int level, const std::string &content) override;
此函数中对来自于组件的消息进行了打包,将一些必要的信息和来自组件的内容打包成了一个json字符串,具体的打包格式后面再去介绍。

SMP_DCAManager

使用SMP_DCAObserver 实现mediaPlayerDCAObserverListener接口,完成了对播放器中所有的组件的DCA的管理

void createObservers();此函数需要在SuperMediaPlayer中去调用,在合适的地方去调用该函数,该函数将创建所有需要DCA的模块的的监听者,可以接收所有组件的消息,目前只创建了Demuxer的监听者,另外创建完监听者后,这里自动发了一个hello的消息出去,通知上层自己的存在,这样,上层就可以根据这个hello的消息向该组件发命名。

int invoke(const std::string &content);invoke的内容是一个json串,要合乎定义才能被正确执行,主要是对命令进行分发,后面一起介绍封装格式。

void onEvent(const std::string &content) override;将封装好的消息放到队列中。

std::string getEvent();提供给播放器的编排层SuperMediaPlayer去使用,将消息队列里面的小取出,发送出去,所以这里的消息是异步的。

SuperMediaPlayer中使用DCA

    void SuperMediaPlayer::ProcessPrepareMsg()
    {
        ...
        if (mDemuxerService->getDemuxerHandle()) {
        ...
            mDcaManager.createObservers();
        }

        //step2: Demuxer init and getstream index
        ...
   }

在创建完demuxer后调用mDcaManager.createObservers();

   int SuperMediaPlayer::mainService()
    {
       ...
        string event = mDcaManager.getEvent();
        while (!event.empty()) {
            mPNotifier->NotifyEvent(MEDIA_PLAYER_EVENT_DIRECT_COMPONENT_MSG, event.c_str());
            event = mDcaManager.getEvent();
        }
        ...

在编排主线程中从DcaManager中去读取组件的消息,并向外notify一个MEDIA_PLAYER_EVENT_DIRECT_COMPONENT_MSG类型的event,这个消息是可以在App层直接收到的。

    int SuperMediaPlayer::invokeComponent(std::string content)
    {
        return mDcaManager.invoke(content);
    }

通过此接口同步下传命令到组件里面。

DCA消息的格式

发送出来消息

void SMP_DCAObserver::onEvent(int level, const string &content)
{
    CicadaJSONItem item;
    item.addValue("class", mClass);//组件的类型,如demxuer
    item.addValue("obj", to_string((uint64_t) mObj));//组件对象的地址
    item.addValue("name", mName);// 组件本身的名字
    item.addValue("level", level);//消息级别,可以不用
    item.addValue("content", content);//具体消息的的内容
    if (mListener) {
        mListener->onEvent(item.printJSON());
    }
}

其中
接收的消息class obj name字段都由SMP_DCAObserver类自动完成填充。其中name是IDemuxer中的接口,如果具体的实现中没有重写此属性,则此值是"IDemuxer"。

int SMP_DCAManager::invoke(const string &content)
{
    CicadaJSONItem item(content);
    string ClassName = item.getString("class");
    if (ClassName == "demuxer" && mDemuxerObserver != nullptr) {
        if ((void *) atoll(item.getString("obj").c_str()) == (void *) mPlayer.mDemuxerService) {
            assert(mPlayer.mDemuxerService->getDemuxerHandle());
            if (mPlayer.mDemuxerService->getDemuxerHandle()->getName() == item.getString("name")) {
                return mPlayer.mDemuxerService->getDemuxerHandle()->invoke(item.getInt("cmd", -1), item.getString("content"));
            }
        }
    }
    // TODO: error code
    return 0;
}

接收的消息和发送的消息非常类似,只是将原来的level字段替换成了cmd字段,此字段也是可选使用。

通过此消息可以精确的定位到消息是谁发来的,将要把命令发给谁,支持多实例。

CicadaPlayer中一个收发消息的例子

cicadaPlayer.cpp

static void onEvent(int64_t errorCode, const void *errorMsg, void *userData)
{
    ...

    switch (errorCode) {
    ...
        case MediaPlayerEventType::MEDIA_PLAYER_EVENT_DIRECT_COMPONENT_MSG: {
            AF_LOGI("get a dca message %s\n", errorMsg);
            CicadaJSONItem msg((char *) errorMsg);
            if (msg.getString("content", "") == "hello") {
                msg.deleteItem("content");
                msg.addValue("content", "hi");
                msg.addValue("cmd", 0);
                cont->player->InvokeComponent(msg.printJSON());
            }
            break;
        }

        default:
            break;
    }
}

在播放器的onEvent回调里面收到MEDIA_PLAYER_EVENT_DIRECT_COMPONENT_MSG的消息,然后解析是一个打招呼的消息,将消息的内容content替换成hi,增加一个0的cmd,将消息回给组件。

在具体的使用中,可以在hello消息中判断组件的名字是否是你所关心的,如果关心的,则将此消息保存下来,把此消息中的内容替换下就可以向组件发送命令了。这里的来来回回是需要App层和组件之间自己去定义的。

上一篇:OpenType字体与TrueType字体的区别


下一篇:Linux下搭建MySQL集群