基於 QPlay 的智能無線流媒體傳輸音箱的設計

基於 QPlay 的智能無線流媒體傳輸音箱的設計

系統總體架構

基於QPlay的DMR主要工作流程圖

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. 處理動作事件

動作事件處理是程序運行的重要部分,所有功能的控制都依賴動作事件處理。該動作事件處理包含四個服務的所有動作

各服務的動作事件

設備收到控制點的事件請求,事件回調函數(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內部的資源回收等,還需要回收程序中其它申請的資源。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章