基于 QPlay 的智能无线流媒体传输音箱的设计
基于 QPlay 的智能无线流媒体传输音箱的设计
系统总体架构
QPlay音箱设备主要工作流程如图所示。由于采用libupnp作为UPnP SDK进行开发,所以程序开始时需要初始化UPnP SDK。
程序主要分为设备初始化,事件循环,设备结束三个阶段。其中事件循环是程序的核心。
设备初始化阶段
设备初始化阶段需要完成:
- 初始化UPnP SDK
调用库函数UpnpInit()初始化UPnP协议栈。
◆ UpnpInit()方法:
/**
* 初始化UPnP SDK。确定IP地址和端口号,用于监听UPnP和HTTP请求
* @param HostIP
* 主机IP地址。如果为NULL,将自动获取一个IP地址
* @param DestPort
* 目的端口号。如果为0,将使用一个随机的端口号。
* @return 成功返回0(UPNP_E_SUCCESS),失败返回错误码
*/
int UpnpInit(const char *HostIP, unsigned short DestPort);
如果IP地址为NULL,端口号为0。SDK将会自动去获取一个可用的IP地址和端口号。可以使用库函数UpnpGetServerIpAddress()获得该IP地址(失败返回NULL),使用库函数UpnpGetServerPort()获取目的端口号。
- 设置WEB服务器的根目录
调用库函数UpnpSetWebServerRootDir()把一个本地目录设置为WEB服务器的根目录,为HTTP请求描述文件时提供准确路径。
◆ UpnpSetWebServerRootDir()方法:
/**
* 设置WEB服务器根目录。以构建描述文件正确路径
* @param rootDir
* 根目录路径。如果为NULL,以程序所在的目录为根目录
* @return 成功返回0(UPNP_E_SUCCESS),失败返回错误码
*/
int UpnpSetWebServerRootDir(const char *rootDir);
- 注册根设备
注册根设备需要设置描述文件和异步事件回调函数,该回调函数负责处理控制点发送的订阅请求、控制请求等。
调用库函数UpnpRegisterRootDevice()来完成根设备注册。
◆ UpnpRegisterRootDevice()方法:
/**
* 注册根设备
* @param DescUrl
* 描述文档URL
* @param Callback
* 收到异步事件请求后执行的回调函数
* @param Cookie
* 回调发生时传给回调函数的参数。可以为NULL
* @param Hnd
* 设备的句柄。通过该句柄可以访问设备
* @return 成功返回0(UPNP_E_SUCCESS),失败返回错误码
*/
int UpnpRegisterRootDevice(const char * DescUrl, Upnp_FunPtr Fun, const void * Cookie, UpnpDevice_Handle * Hnd);
- 其它相关初始化
设备相关信息初始化,如打开DSP文件(用于播放音频)、初始化播放列表容器(用于存储歌曲信息)、注册信号处理函数(绑定结束函数,程序退出时进行资源回收)以及相关服务的状态变量初始化等。
- 广播设备存在公告
设备初始化结束后,将广播设备存在信息,等待控制点的请求。
程序调用库函数UpnpSendAdvertisement()广播设备存在公告,之后设备必须进入循环,等待事件的到来(或等待程序结束信息)。
◆ UpnpSendAdvertisement()方法:
/**
* 广播设备存在公告
* @param Hnd
* 设备句柄
* @param Exp
* 公告生存时间。在设备生命周期中,SDK会自动在超时前重新广播设备存在公告
* @return 成功返回0(UPNP_E_SUCCESS),失败返回错误码
*/
int UpnpSendAdvertisement(UpnpDevice_Handle Hnd, int Exp);
事件循环阶段
设备广播存在公告后,将进入事件循环阶段。该阶段主要接收控制点发送过来的各种异步请求:订阅请求、动作请求、获取状态变量请求(QPlay框架并未提供该请求)。
UPnP SDK会将各种请求进行处理,创建线程,调用注册根设备时注册的回调函数(称为event_handler)进行处理。
该回调函数原型是:
◆ event_handler()方法:
/**
* 事件回调函数。处理接收到的所有事件
* @param EventType
* 事件类型
* @param Event
* 指向事件结构体的指针。由于不同事件使用的结构不一致,因此这里统一使用空指针,需要根据事件类型进行转换
* @param Cookie
* 指向注册根设备时传入的参数
* @return 成功返回0(UPNP_E_SUCCESS),失败返回错误码
*/
int event_handler(Upnp_EventType EventType, void *Event, void *Cookie);
UPnP SDK的事件类型(EventType)一共有14种:
◆ UPNP_CONTROL_ACTION_REQUEST
动作操作请求。由设备接收,需要返回动作执行的结果。
事件结构体:
struct Upnp_Action_Request
{
int ErrCode; // 错误码(成功时为0)
int Socket; // 请求方套接字标识符
char ErrStr[LINE_SIZE]; // 错误信息
char ActionName[NAME_SIZE]; // 动作名称
char DevUDN[NAME_SIZE]; // 设备UDN
char ServiceID[NAME_SIZE]; // 服务ID
IXML_Document * ActionRequest;// 指向动作的DOM描述文档的指针
IXML_Document * ActionResult; // 指向动作结果的DOM描述文档的指针
struct sockaddr_storage CtrlPtIPAddr; // 请求方IP地址信息
IXML_Document * SoapHeader; // 执行包含SOAP头信息的XML描述文档的指针
};
◆ UPNP_CONTROL_GET_VAR_REQUEST
获取状态变量请求。由设备接收,需要返回动作执行的结果。
事件结构体:
struct Upnp_State_Var_Request
{
int ErrCode; // 错误码(成功时为0)
int Socket; // 请求方套接字标识符
char ErrStr[LINE_SIZE]; // 错误信息
char DevUDN[NAME_SIZE]; // 设备UDN
char ServiceID[NAME_SIZE]; // 服务ID
char StateVarName[NAME_SIZE]; // 状态变量名
struct sockaddr_storage CtrlPtIPAddr; // 请求方IP地址信息
DOMString CurrentVal; // 状态变量的当前值
};
◆ UPNP_CONTROL_GET_VAR_COMPLETE
获取状态变量响应。调用UpnpGetServiceVarStatus()后返回的响应。
事件结构体:
struct Upnp_State_Var_Complete
{
int ErrCode; // 错误码(成功时为0)
char CtrlUrl[NAME_SIZE]; // 对应服务的控制URL
char StateVarName[NAME_SIZE]; // 状态变量名
DOMString CurrentVal; // 状态变量的当前值
};
◆ UPNP_DISCOVERY_ADVERTISEMENT_ALIVE
存在发现信息。由控制点接收,有新的设备或服务可用。
事件结构体:
struct Upnp_Discovery
{
int ErrCode; // 错误码(成功时为0)
int Expires; // 公告超时时间
char DeviceId[LINE_SIZE]; // 设备唯一ID
char DeviceType[LINE_SIZE]; // 设备类型
char ServiceType[LINE_SIZE]; // 服务类型
char ServiceVer[LINE_SIZE]; // 服务版本号
char Location[LINE_SIZE]; // 设备的描述文档URL地址
char Os[LINE_SIZE]; // 设备运行的系统信息
char Date[LINE_SIZE]; // 响应时间
char Ext[LINE_SIZE]; // 设备描述信息
struct sockaddr_storage DestAddr; // 目标对象IP地址信息
};
◆ UPNP_DISCOVERY_ADVERTISEMENT_BYEBYE
离线发现信息。由控制点接收,有设备或服务关闭。
事件结构体:struct Upnp_Discovery;
◆ UPNP_DISCOVERY_SEARCH_RESULT
超时发现信息。由控制点接收,没有搜索到匹配的设备或服务,搜索超时。
事件结构体:struct Upnp_Discovery;
◆ UPNP_DISCOVERY_SEARCH_TIMEOUT
离线发现信息。由控制点接收,有设备或服务关闭。
事件结构体:无
◆ UPNP_EVENT_SUBSCRIPTION_REQUEST
订阅事件请求。由设备接收,设备的事件被订阅。需要调用UpnpAcceptSubscription()确认订阅并传送初始的状态变量表。
事件结构体:
struct Upnp_Subscription_Request
{
char * ServiceId; // 订阅的服务ID
char * UDN; // 通用设备名称
Upnp_SID Sid; // 分配的订阅ID
};
◆ UPNP_EVENT_RECEIVED
接收事件信息。由控制点接收,收到订阅的事件信息。
事件结构体:
struct Upnp_Event
{
Upnp_SID Sid; // 此次订阅的订阅ID
int EventKey; // 时间***
IXML_Document * ChangedVariables; // 发生改变的状态变量值
};
◆ UPNP_EVENT_RENEWAL_COMPLETE
续订事件响应。调用UpnpRenewSubscribeAsync()后返回的响应。
事件结构体:
struct Upnp_Event_Subscribe
{
Upnp_SID Sid; // 此次订阅的订阅ID
int ErrCode; // 错误码(成功时为0)
char PublisherUrl[NAME_SIZE]; // 订阅或退订的事件URL
int TimeOut; // 订阅时间(只对订阅)
};
◆ UPNP_EVENT_SUBSCRIBE_COMPLETE
订阅事件响应。调用UpnpSubscribeAsync()后返回的响应。只有返回成功(UPNP_E_SUCCESS)时,Sid才是有效的。
事件结构体:struct Upnp_Event_Subscribe;
◆ UPNP_EVENT_UNSUBSCRIBE_COMPLETE
退订事件响应。调用UpnpUnSubscribeAsync()后返回的响应。Sid表示正在退订的事件ID。
事件结构体:struct Upnp_Event_Subscribe;
◆ UPNP_EVENT_AUTORENEWAL_FAILED
自动续订失败。客户端的自动续订失败,订阅失效。
事件结构体:struct Upnp_Event_Subscribe;
◆ UPNP_EVENT_SUBSCRIPTION_EXPIRED
订阅过期。客户端的订阅已经过期,订阅失效。
事件结构体:struct Upnp_Event_Subscribe;
上述结构体定义中,LINE_SIZE为180,NAME_SIZE为256。
对于本程序,只需要处理动作操作请求(UPNP_CONTROL_ACTION_REQUEST)和订阅事件请求(UPNP_CONTROL_SUBSCRIPTION_REQUEST)。
因此,在程序的事件循环阶段,主要处理订阅请求和动作请求。
- 处理订阅事件
设备收到控制点的事件请求,事件回调函数(event_handler)的事件类型(EventType)为UPNP_CONTROL_SUBSCRIPTION_REQUEST,进入订阅事件处理。
判断Upnp_Subscription_Request结构体的ServiceId标签,可以获悉是订阅哪一个服务。在本程序中,提供的四个服务ID为:“urn:upnp-org:serviceId:AVTransport”(音视频传输服务)、“urn:upnp-org:serviceId:RenderingControl”(播放控制服务)、“urn:upnp-org:serviceId:ConnectionManager”(连接管理服务)和“urn:tencent-com:serviceId:QPlay”(QPlay服务)。
如果服务ID存在,且可以订阅。需要按照UPnP规范把相应服务的状态变量表信息转换为XML描述的形式,并使用库函数UpnpAcceptSubscription()或UpnpAcceptSubscriptionExt()接受订阅后发送给控制点。
◆ UpnpAcceptSubscriptionExt()方法:
/**
* 接受订阅和发送订阅服务的状态变量当前值
* @param Hnd
* 设备句柄
* @param DevID
* 设备ID。可以使用Upnp_Subscription_Request.UDN
* @param ServID
* 服务ID。可以使用Upnp_Subscription_Request.ServiceId
* @param PropSet
* DOM文档属性集。符合UPnP设备架构的XML模式的文档,使用相应的函数把数据转换为IXML_Document类型
* @param SubsId
* 订阅ID。可以使用Upnp_Subscription_Request.Sid
* @return 成功返回0(UPNP_E_SUCCESS),失败返回错误码
*/
int UpnpAcceptSubscriptionExt( UpnpDevice_Handle Hnd, const char * DevID, const char * ServID, IXML_Document * PropSet, Upnp_SID SubsId);
UpnpAcceptSubscription()与UpnpAcceptSubscriptionExt()功能一样,只是需要的参数有所不同。
- 处理动作事件
动作事件处理是程序运行的重要部分,所有功能的控制都依赖动作事件处理。该动作事件处理包含四个服务的所有动作
设备收到控制点的事件请求,事件回调函数(event_handler)的事件类型(EventType)为UPNP_CONTROL_ACTION_REQUEST,进入动作事件处理。
判断Upnp_Action_Request结构体的ServiceId标签,判断是哪一个服务的动作事件。再判断Upnp_Action_Request结构体的ActionName标签,获悉其动作事件名,
调用相应的动作处理函数。动作处理结束后,需要将动作响应信息(订阅的状态变量值)返回。
可以使用库函数UpnpMakeActionResponse()生成动作响应DOM文档信息。
◆ UpnpMakeActionResponse()方法:
/**
* 生成动作响应的DOM文档信息
* @param ActionName
* 动作名。可以使用Upnp_Action_Request.ActionName
* @param ServType
* 服务类型。可以使用Upnp_Action_Request.ServiceID
* @param NumArg
* 参数组(状态变量名,状态变量值)的数量
* @param Arg
* 其它状态变量参数组
* @return 返回生成的DOM文档指针。可以使用Upnp_Action_Request.ActionResult接收返回值
*/
IXML_Document * UpnpMakeActionResponse(const char * ActionName, const char * ServType, int NumArg, const char * Arg, ...);
也可以使用库函数UpnpAddToActionResponse()往动作响应DOM文档添加状态变量信息。
◆ UpnpAddToActionResponse()方法:
/**
* 在动作响应的DOM文档加入一个状态变量信息
* @param ActionResponse
* 动作响应信息DOM文档的二级指针。可以使用&Upnp_Action_Request.ActionResult
* @param ActionName
* 动作名。可以使用Upnp_Action_Request.ActionName
* @param ServType
* 服务类型。可以使用Upnp_Action_Request.ServiceID
* @param ArgName
* 状态变量名
* @param ArgVal
* 状态变量值
* @return 成功返回0(UPNP_E_SUCCESS),失败返回错误码
*/
int UpnpAddToActionResponse(IXML_Document ** ActionResponse, const char * ActionName, const char * ServType, const char * ArgName, const char * ArgVal);
设备结束阶段
QPlay2.0规定,当设备切换网络、关机等情况下,需要发出设备离线公告通知在网的QQ音乐应用程序等控制点该设备不可用。因此,在触发设备切换网络、关机等事件时,程序进入结束阶段。
结束阶段需要执行注销根设备,广播设备离线信息,释放占用的系统资源等操作,最后退出程序。
调用库函数UpnpUnRegisterRootDevice()注销根设备,再使用库函数UpnpFinish()执行广播设备离线信息、关闭定时器线程、停止Mini Server、注销线程池等操作。UpnpFinish()必须是UPnP SDK最后调用的API。
◆ UpnpUnRegisterRootDevice()方法:
/**
* 注销根设备
* @param Hnd
* 设备句柄
* @return 成功返回0(UPNP_E_SUCCESS),失败返回错误码
*/
int UpnpUnRegisterRootDevice(UpnpDevice_Handle Hnd);
◆ UpnpFinish()方法:
/**
* 广播设备离线信息,注销线程池等
* @param 无
* @return 成功返回0(UPNP_E_SUCCESS),失败返回错误码
*/
int UpnpFinish(void);
除了UPnP SDK内部的资源回收等,还需要回收程序中其它申请的资源。