大數據

開源播放器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層和組件之間自己去定義的。

Leave a Reply

Your email address will not be published. Required fields are marked *