什麼是組件穿透
一個應用程序實例完成一件事情,一般需要組合多個組件來完成,比如在一次網絡視頻播放過程中有個播放器實例需要使用DataSource demxuer decoder render等模塊來完成。而每個模塊會根據當前的視頻創建不同的組件來完成相應的功能,如http的網絡流需要啟用http DataSource組件,本地文件的播放需要啟用file DataSource組件。那麼不同的組件可能會有不同的特性和可配置的參數,對於一個播放器來說,在某些情況下可能無法統一去配置和管理,但又要能讓用戶去配置和管理這些組件。那麼我們提供一個App層和組件層直接通信的通道,Direct Component Access(簡稱DCA)來完成此功能。
CicadaPlayer引入DCA的原因
CicadaPlayer在架構設計的時候就把插件化,可擴展性考慮進來,每個模塊都有可能被其他人去擴展,比如去擴展一個解碼器,或者一個demuxer,而這些擴展的組件可能本身有的功能要比播放器需要的多,而在播放器整個流程裡面並不涉及到這些多出來的功能,比如解碼器可以根據視頻的特性配置一些參數,可讓解碼流程變得更好,demuxer需要上報一些數據到某個地方等,這些功能播放器是無法預知的,更不可能為每個模塊都添加一些特定的事件和命名,所以CicadaPlayer採用了DCA的方式,給每個模塊自由。
CicadaPlayer目前DCA的實現
DCA的原型
// 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 中對外有兩個接口
- setDCAObserver,此接口是要向DCA中設置一個監聽者,也就是component的上行通道,繼承了IDCA的類就可以通過sendEvent向外面的監聽者發送事件消息。
- invoke,此接口是App向component發送命名的下行通道,調用該接口可以向對應的component發送消息或者命令。
以上兩個接口的參數都是字符串,這個是為了擴展,此字符串可以是任意格式的內容。
IDemuxer繼承了IDCA
目前只有IDemuxer繼承了IDCA,也就是說所有的demuxer組件都具備了DCA的能力。其他模塊暫時沒有繼承,有需要的話後面可以去繼承,目前其他模塊暫時無需求。
SuperMediaPlayer中對DCA的管理
SuperMediaPlayer是整個播放器的編排層,需要管理所有組件的DCA邏輯,在播放器的代碼中我們增加了一個友元類來單獨管理。
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中一個收發消息的例子
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層和組件之間自己去定義的。