这几天公司的应用(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文件进行编译库和相关开发学习的。
那么,第一步就是,拉下项目进行编译和运行示例了。
首先,使用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和示例运行程序了。
选择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
了。
你可以直接把这两个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"
。
这样,你就可以使用自己编译好的framework了。
首先是对三方库的使用,来理解接口调用方式。还好库里提供了几个例子程序,先慢慢看了三天。移植了其中一个关于媒体控制器的示例到项目中,仅仅实现了查找附近设备的功能。但这是个好的开头,对我来说有相当的鼓励作用。开发过程中主要是参照MicroMediaController
的代码进行的。
我发现对于优质C++库的学习,真是一种赏心悦目的体验,当然看懂C++的细节还是相当痛苦的。
这个Platinum库应该是遵循dlna协议编写的,相关的文档很少,项目属于自注释型的,也就是说,代码中的注释就是文档的大部分,不过要学习这个库,dlan协议还是有必要详细看看的,否则即使通过修改程序,达到了最初设定的功能目标,想要扩展一些功能,却是会边参数都不会传递的,因为这些参数是在协议是规定的。
在接口封闭的过程中,发现参考别人的封闭方法实在能够学习很多,比例我在用Objective-C封C++接口的过程中,就参考了Platinum项目目录下Platinum/Source/Extras/ObjectiveC/
对MediaServer
的封装方法。
未完,待续…
在识别小米盒子的时候,总是识别不到,修改了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;
}
个人发现小米盒子对于dlan协议实现部分的静音控制命令似乎有些出入,我使用示例程序发送静音命令到小米盒子,发现只能使设备静音,却不能使设备恢复声音,这个问题有待进一步确认。