使用Platium庫開發dlna投屏功能

這幾天公司的應用(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項目目錄下,發現其中有着名爲CartfileCartfile.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下確保PlatinumNeptune兩個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協議實現部分的靜音控制命令似乎有些出入,我使用示例程序發送靜音命令到小米盒子,發現只能使設備靜音,卻不能使設備恢復聲音,這個問題有待進一步確認。

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