天天看点

基于 QPlay 的智能无线流媒体传输音箱的设计基于 QPlay 的智能无线流媒体传输音箱的设计

基于 QPlay 的智能无线流媒体传输音箱的设计

系统总体架构

基于 QPlay 的智能无线流媒体传输音箱的设计基于 QPlay 的智能无线流媒体传输音箱的设计

QPlay音箱设备主要工作流程如图所示。由于采用libupnp作为UPnP SDK进行开发,所以程序开始时需要初始化UPnP SDK。

程序主要分为设备初始化,事件循环,设备结束三个阶段。其中事件循环是程序的核心。

设备初始化阶段

设备初始化阶段需要完成:

  1. 初始化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()获取目的端口号。

  1. 设置WEB服务器的根目录

调用库函数UpnpSetWebServerRootDir()把一个本地目录设置为WEB服务器的根目录,为HTTP请求描述文件时提供准确路径。

◆ UpnpSetWebServerRootDir()方法:

/**
 * 设置WEB服务器根目录。以构建描述文件正确路径
 * @param	rootDir
 * 			根目录路径。如果为NULL,以程序所在的目录为根目录
 * @return	成功返回0(UPNP_E_SUCCESS),失败返回错误码
 */
int UpnpSetWebServerRootDir(const char *rootDir);
           
  1. 注册根设备

注册根设备需要设置描述文件和异步事件回调函数,该回调函数负责处理控制点发送的订阅请求、控制请求等。

调用库函数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);
           
  1. 其它相关初始化

设备相关信息初始化,如打开DSP文件(用于播放音频)、初始化播放列表容器(用于存储歌曲信息)、注册信号处理函数(绑定结束函数,程序退出时进行资源回收)以及相关服务的状态变量初始化等。

  1. 广播设备存在公告

设备初始化结束后,将广播设备存在信息,等待控制点的请求。

程序调用库函数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)。

因此,在程序的事件循环阶段,主要处理订阅请求和动作请求。

  1. 处理订阅事件

设备收到控制点的事件请求,事件回调函数(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()功能一样,只是需要的参数有所不同。

  1. 处理动作事件

动作事件处理是程序运行的重要部分,所有功能的控制都依赖动作事件处理。该动作事件处理包含四个服务的所有动作

基于 QPlay 的智能无线流媒体传输音箱的设计基于 QPlay 的智能无线流媒体传输音箱的设计

设备收到控制点的事件请求,事件回调函数(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内部的资源回收等,还需要回收程序中其它申请的资源。