這幾天公司的應用(iOS端)上要加一個dlna
的功能,就是局域網內設備投屏控制的一個功能,並提供移動端控制。因爲三方庫Platinum是使用C++寫的,所以我被分配去做庫的Objective-C封裝的工作。第一次接手這種事,對一個非計算機專業的學生來說還是蠻有挑戰性的。組長說要先寫一個接口設計文檔來描述將要封裝的接口和調用方式。只能網上查看各種資料嘍!
這裏是我一頓狂搜、看各種博客後蒐集到的有用資料,列表如下:
鏈接 | 描述 |
---|---|
Open Connectivity Foundation (OCF)官網 | 這裏有UPnP相關的文檔和各家公司開發的SDK,例如:Plutinosoft開發的Platinum庫也可以從這裏了順藤摸瓜找到 |
dlna官網 | 這裏有對dlna協議描述的文檔下載,可以說要全面的學習dlna,這裏的文檔是不可或缺的,當然,實際中我們也沒有必要學太深,不過知道這個資源,學習時就有底了;-) |
一個關於dlan介紹的博客 | 上面兩個網站就是通過閱讀這個博客《DLNA&UPnP開發筆記》系列共四篇文章後找到的,值的閱讀 |
好了,有了以上的幾個資源,我們就可以開動了。我工作中使用了Platinum庫進行dlna的媒體控制器(DMC
)開發,所以也沒有對dlna有太全面的瞭解,一切是從對Platinum庫所提供的示例程序和項目README文件進行編譯庫和相關開發學習的。
那麼,第一步就是,拉下項目進行編譯和運行示例了。
編譯Platinum庫
首先,使用git拉下項目最近一次的提交
git clone --depth=1 https://github.com/plutinosoft/Platinum.git
因爲Platinum的編譯需要依賴一個名爲Neptune
的C++跨平臺運行環境,當然這個項目裏已經有解決方法,不需要我們另外下載或編譯Neptune
庫,我們只需要通過Carthage
工具,將Neptune
的framework下載到Platinum的項目目錄下。具體過程如下:
你要先進行Platinum項目目錄下,發現其中有着名爲Cartfile
或Cartfile.resolved
這樣的文件,文件中指明瞭Carthage
這個工具軟件所要下載的framework名稱和版本,其實carthage
這個工具類似於CocoaPods
這個工具。不過你的Mac上很可能並沒有安裝carthage
,所以你其實可以通過先在Mac上安裝homebrew這個工具,然後使用homebrew
來安裝carghage
。如下:
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" # 安裝homebrew
brew install carthage # homebrew 安裝成功後,安裝carthage
cd Platinum/ # 進入Platinum項目目錄下
carthage update # 運行carthage 讓其下載 Carthfile文件中指定的framework
當以上過程完成後,你會發現在Platinum/Carthage
這個目錄下面,已經存在Neptune
這個C++跨平臺運行環境的分別針對Mac和iOS的framework了,你只要確保以上的命令運行成功,並最終得到Neptune的FrameWork就可以編譯Platinum的針對iOS的項目和示例程序了。
使用XCode打開/Platinum/Build/Targets/universal-apple-macosx/Platinum.xcodeproj
項目文件,然後分別運行各Target
,就可以生成相關的framework和示例運行程序了。
生成同時支持真機和模擬機的framework動態鏈接庫
選擇Platinum-iOS
編譯方案(Scheme),在編輯方案(Edit Scheme…)對話框,運行(Run)分類中的信息(Info)選項卡下,選擇編譯配置(Build Configuration)爲Release
。設置好編譯方案後,選擇任一模擬器(e.g: iphone SE)編譯一次,再選擇通用iOS設備(Generic iOS Device)編譯一次,這兩次編譯,分別得到對應於i386 x86_64
架構的模擬器framework和對應於真機armv7 arm64
架構的framework,我們把這兩個對應於不同架構的framework合併成一個framework就完成了同時滿足真機調試和模擬器調試的framework了。
生成的兩個針對不同架構的framework所在的目錄如下,你也可以通過右鍵Show in Finder
的方式找到它們的位置, 這兩個目錄路徑動態變化,不完全與我的一致。
#這個目錄對應於真機的 armv7 arm64 架構
/Users/JokerAtBaoFeng/Library/Developer/Xcode/DerivedData/Platinum-bawuiqxkhqixgybjjufqgvmduavh/Build/Products/Release-iphoneos
#這個目錄對應於模擬器的 i386 x86_64 架構
/Users/JokerAtBaoFeng/Library/Developer/Xcode/DerivedData/Platinum-bawuiqxkhqixgybjjufqgvmduavh/Build/Products/Release-iphonesimulator
你可以分別使用下面的命令來查看framework中的文件支持的架構,如下:
lipo -info Release-iphoneos/Platinum.framework/Platinum
輸出:
Architectures in the fat file: Release-iphoneos/Platinum.framework/Platinum are: armv7 arm64
lipo -info Release-iphonesimulator/Platinum.framework/Platinum
輸出:
Architectures in the fat file: Release-iphonesimulator/Platinum.framework/Platinum are: i386 x86_64
利用下面的命令將兩個framework合併, 並替換Release-iphoneos/Platinum.framework/Platinum
文件,這個文件就是合併之後的文件:
lipo -create Release-iphoneos/Platinum.framework/Platinum Release-iphonesimulator/Platinum.framework/Platinum -output Release-iphoneos/Platinum.framework/Platinum
這時你就可以使用Release-iphoneos/Platinum.framework
導入你要用到的項目中去了。哦,對了,由於Platinum.framework運行需要依賴Neptune.framework
,所以導入自己的項目時,記得把carthage下載的Neptune.framework
一併導入。
再次查看合併後的framework所支持的架構:
lipo -info Release-iphoneos/Platinum.framework/Platinum
輸出:
Architectures in the fat file: Release-iphoneos/Platinum.framework/Platinum are: i386 x86_64 armv7 arm64
可以看到,它已經同時支持i386 x86_64 armv7 arm64
了。
Platinum.framework和Neptune.framework的使用
你可以直接把這兩個framework文件直接拖入項目中,並在提示時選擇Copy Items if needed
,然後點擊finished完成導入。
然後到項目對應Target下的Build Phases
|Link Binary With Libraries
下確保Platinum
和Neptune
兩個framework都在列表中。
由於iOS新版本支持了動態鏈接庫,而我們上述過程默認生成的也是動態的framework, 所以還需要在Target的General
| Embedded Binaries
中同樣的添加上述的兩個framework,以使我們在安裝應用的同時,也將對應的動態庫拷貝到機器中去,否則會由於機器上缺少對應framework而報錯。
在項目中使用framework的頭文件,需要使用尖括號<header.h>
而非雙引號"header.h"
。
這樣,你就可以使用自己編譯好的framework了。
對庫的熟悉過程
首先是對三方庫的使用,來理解接口調用方式。還好庫裏提供了幾個例子程序,先慢慢看了三天。移植了其中一個關於媒體控制器的示例到項目中,僅僅實現了查找附近設備的功能。但這是個好的開頭,對我來說有相當的鼓勵作用。開發過程中主要是參照MicroMediaController
的代碼進行的。
我發現對於優質C++庫的學習,真是一種賞心悅目的體驗,當然看懂C++的細節還是相當痛苦的。
這個Platinum庫應該是遵循dlna協議編寫的,相關的文檔很少,項目屬於自注釋型的,也就是說,代碼中的註釋就是文檔的大部分,不過要學習這個庫,dlan協議還是有必要詳細看看的,否則即使通過修改程序,達到了最初設定的功能目標,想要擴展一些功能,卻是會邊參數都不會傳遞的,因爲這些參數是在協議是規定的。
在接口封閉的過程中,發現參考別人的封閉方法實在能夠學習很多,比例我在用Objective-C封C++接口的過程中,就參考了Platinum項目目錄下Platinum/Source/Extras/ObjectiveC/
對MediaServer
的封裝方法。
未完,待續…
#issue1
在識別小米盒子的時候,總是識別不到,修改了Platinum中的部分代碼,並重新編譯後導入項目,得以正常識別:
修改前
PltCtrlPoint.cpp
class PLT_DeviceReadyIterator
{
public:
PLT_DeviceReadyIterator() {}
NPT_Result operator()(PLT_DeviceDataReference& device) const {
NPT_Result res = device->m_Services.ApplyUntil(
PLT_ServiceReadyIterator(),
NPT_UntilResultNotEquals(NPT_SUCCESS));
if (NPT_FAILED(res)) return res;
res = device->m_EmbeddedDevices.ApplyUntil(
PLT_DeviceReadyIterator(),
NPT_UntilResultNotEquals(NPT_SUCCESS));
if (NPT_FAILED(res)) return res;
// a device must have at least one service or embedded device
// otherwise it's not ready
if (device->m_Services.GetItemCount() == 0 &&
device->m_EmbeddedDevices.GetItemCount() == 0) {
return NPT_FAILURE;
}
return NPT_SUCCESS;
}
};
修改後
PltCtrlPoint.cpp
class PLT_DeviceReadyIterator
{
public:
PLT_DeviceReadyIterator() {}
NPT_Result operator()(PLT_DeviceDataReference& device) const {
NPT_Result res = device->m_Services.ApplyUntil(
PLT_ServiceReadyIterator(),
NPT_UntilResultNotEquals(NPT_SUCCESS));
// if (NPT_FAILED(res)) return res;
res = device->m_EmbeddedDevices.ApplyUntil(
PLT_DeviceReadyIterator(),
NPT_UntilResultNotEquals(NPT_SUCCESS));
// if (NPT_FAILED(res)) return res;
if(NPT_FAILED(res) && NPT_FAILED(res)) return res;
// a device must have at least one service or embedded device
// otherwise it's not ready
if (device->m_Services.GetItemCount() == 0 &&
device->m_EmbeddedDevices.GetItemCount() == 0) {
return NPT_FAILURE;
}
return NPT_SUCCESS;
}
};
修改原因
我發現對於搜索到的小米盒子,代碼過不了下面這個函數的第55
行:
PltCtrlPoint.cpp
/*----------------------------------------------------------------------
| PLT_CtrlPoint::ProcessGetSCPDResponse
+---------------------------------------------------------------------*/
NPT_Result
PLT_CtrlPoint::ProcessGetSCPDResponse(NPT_Result res,
const NPT_HttpRequest& request,
const NPT_HttpRequestContext& context,
NPT_HttpResponse* response,
PLT_DeviceDataReference& device)
{
NPT_COMPILER_UNUSED(context);
NPT_AutoLock lock(m_Lock);
PLT_DeviceReadyIterator device_tester;
NPT_String scpd;
PLT_DeviceDataReference root_device;
PLT_Service* service;
NPT_String prefix = NPT_String::Format("PLT_CtrlPoint::ProcessGetSCPDResponse for a service of device \"%s\" @ %s (result = %d, status = %d)",
(const char*)device->GetFriendlyName(),
(const char*)request.GetUrl().ToString(),
res,
response?response->GetStatusCode():0);
// verify response was ok
NPT_CHECK_LABEL_FATAL(res, bad_response);
NPT_CHECK_POINTER_LABEL_FATAL(response, bad_response);
PLT_LOG_HTTP_RESPONSE(NPT_LOG_LEVEL_FINER, prefix, response);
// make sure root device hasn't disappeared
NPT_CHECK_LABEL_WARNING(FindDevice(device->GetUUID(), root_device, true),
bad_response);
res = device->FindServiceBySCPDURL(request.GetUrl().ToRequestString(), service);
NPT_CHECK_LABEL_SEVERE(res, bad_response);
// get response body
res = PLT_HttpHelper::GetBody(*response, scpd);
NPT_CHECK_LABEL_FATAL(res, bad_response);
// DIAL support
if (root_device->GetType().Compare("urn:dial-multiscreen-org:device:dial:1") == 0) {
AddDevice(root_device);
return NPT_SUCCESS;
}
// set the service scpd
res = service->SetSCPDXML(scpd);
NPT_CHECK_LABEL_SEVERE(res, bad_response);
// if root device is ready, notify listeners about it and embedded devices
if (NPT_SUCCEEDED(device_tester(root_device))) {
AddDevice(root_device);
}
return NPT_SUCCESS;
bad_response:
NPT_LOG_SEVERE_2("Bad SCPD response for device \"%s\":%s",
(const char*)device->GetFriendlyName(),
(const char*)scpd);
if (!root_device.IsNull()) RemoveDevice(root_device);
return res;
}
#issue2
個人發現小米盒子對於dlan協議實現部分的靜音控制命令似乎有些出入,我使用示例程序發送靜音命令到小米盒子,發現只能使設備靜音,卻不能使設備恢復聲音,這個問題有待進一步確認。