基于 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内部的資源回收等,還需要回收程式中其它申請的資源。