什么是组件穿透
一个应用程序实例完成一件事情,一般需要组合多个组件来完成,比如在一次网络视频播放过程中有个播放器实例需要使用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层和组件之间自己去定义的。