注意:SDK 团队目前已经把 DJIWidget 项目抽出来单独管理,项目地址。
在 DJI-Mobile-SDK 4.7.0 发布时,SDK 团队在其 Mobile-SDK-iOS
的仓库中同时发布了 DJIWidget
的代码。值得一提,DJIWidget
跟 DJI-UX-SDK
没有什么关系,虽然 DJI-UX-SDK
里面也有 Widget 的概念,不要混为一谈。
1 - DJIWidget 是什么
DJIWidget
是一个 framework,项目里面包含几个相对独立的功能模块。而此前作为独立项目存在的 VideoPreviewer
现在则更名为了 DJIVideoPreviewer
,被纳入到 DJIWidget
项目中。
目前 Mesh 中使用到的仅仅是 DJIVideoPreviewer
此模块,用于解码并渲染图传画面。而对于其他功能模块诸如 RTMPProcessor
以及 VideoLiveCamera
等,其作用从命名可以窥探一二,但现在官方文档缺少对它们职责的明确描述。
2 - 当前 DJIWidget 的使用低效在哪里
毫无疑问,它的低效在于缺乏对依赖管理工具的支持。这也就是我们说无法使用 CocoaPods 在项目中引入 DJIWidget
,而必须以子项目的形式集成到项目中(或者一顿操作生成一个 fat framework 文件丢到项目里面,更难受)。
依赖管理的重要性不言而喻,我想没有工程师能够忍受得了以 SubProject 的形式在自己的项目中集成第三方库。光是想象一下集成了 SubProject 之后 Xcode 中显示的项目结构,我都要吐。
版本控制?不存在的。想象一下,某天你发现 DJIWidget
的代码更新了,maybe 只是更新了某个 API 的命名,于是你把整个 Mobile-SDK-iOS
项目给 clone 下来,找到里面的 DJIWidget
项目,然后再用力把它拖到你的项目里面,接着提交你的项目代码。接着过两天,你发现工程师把 DJIWidget 里面的两个无用文件给删除了,而你的强迫症最终驱使你去更新它,于是你又重复了一遍上面的操作。如果多个项目想统一使用同一个版本的 DJIWidget
代码怎么办?呵呵,那就把项目 A 里面正在用的 DJIWidget
给拷贝出来然后用力拖到项目 B 里面吧😇。
什么?你问我为什么 DJI 的工程师不会对这样的方式感到恶心?我想他们内部的项目一定不是这样来做代码管理的😇。
2.1 - 上帝说,要有 Pod
Mao 说,自己动手,丰衣足食。那就用自己双手让 DJIWidget 支持 CocoaPods 吧。这也就是说我们需要自己维护一份 DJIWidget.podspec
。在 Kiwi 仓库的根目录下,可以看到这份 DJIWidget.podspec
文件,里面的内容一目了然。但这里有两个细节需要说说:
2.2 - Podfile 中 DJIWidget 的指定方式
由于在 pod trunk push
时 DJIWidget
编译检查一直通不过,所以笔者无法将它推到 master repo 或者是内部的私有 repo,因而项目里面暂时通过 git + tag 的方式来控制对 DJIWidget 的依赖:
pod 'DJIWidget', :git => 'git@github.com:gzkiwiinc/Mobile-SDK-iOS.git', :tag => 'w1.0'
pod 'DJIWidget/DJIVideoPreviewerExtension', :git => 'git@github.com:gzkiwiinc/Mobile-SDK-iOS.git', :tag => 'w1.0'
如果你只需要使用官方提供的 DJIWidget
功能,那么 pod 'DJIWidget'
就可以了;如果你也需要使用 DJIVideoPreviewer 中的 delegate,那么就加上 pod 'DJIWidget/DJIVideoPreviewerExtension'
方式来指定。
如果看到这篇文章的你有方法可以解决 lint 时发生的编译问题,请毫不犹豫地伸出你的援手。
2.3 - DJIWidget 的版本号
在上面你可能发现了所指定的 tag 有点奇怪:w1.0
。嘛,这是一个折衷的选择。前面说了,DJIWidget
不是一个单独的仓库,而是寄生在 Mobile-SDK-iOS
这个仓库里面,而本来 Mobile-SDK-iOS
自身也存在 tag 以标记对应的 SDK 版本:v4.7.0
,v4.7.1
等等。
为了避免日后同步原仓库时可能发生的冲突,所以使用形如 w1.0
的打 tag 方式来针对 DJIWidget
进行标记。其中 w
代表 widget。目前各个 tag DJIWidget
对应的最新可用 SDK 版本可以在这里查询到。
3 - 如何充分使用 DJIVideoPreviewer
DJIVideoPreviewer
用于显示图传数据,其内部会对 h264 码流进行解码然后渲染显示。
那么在 Mesh 里面,当两台 iOS 设备通过 Mesh 进行远程图传共享时,为了降低网络带宽的占用,提高共享图传的实时性,Mesh 会将图传数据重新编码成更低分辨率的 h264 码流。而这里的「重新编码」操作,需要解码后的图像数据作为输入。而复用 DJIVideoPreviewer
的解码结果无疑是最高效的做法。于是需要一种机制,来获取到 DJIVideoPreviewer
解码后的图像数据。
3.1 - 取回解码后的图像 buffer
既然 DJIVideoPreviewer
是开源的,直接改源码当然是一种思路。但这里为了降低维护成本,从解决冲突的痛苦中解脱出来,我们充分利用了 OC 的动态特性,使用 Runtime 进行代码注入,来为 DJIVideoPreviewer
添加了 delegate
属性。通过它,你就可以拿到解码后的图像 buffer:
DJIVideoPreviewer.instance().delegate = self
extension XXXViewController: DJIVideoPreviewerDelegate {
func djiVideoPreviewer(_ videoPreviewer: DJIVideoPreviewer, willProcessImageBuffer imageBuffer: CVImageBuffer) {
// Handle your imageBuffer
}
}
相关代码都在 Kiwi 仓库根目录下的 DJIVideoPreviewerExtension
文件夹中。它主要通过两个事情来完成任务:
通过 Category 以及 associated object 来为 DJIVideoPreviewer 增加 delegate,详见
DJIVideoPreviewer+Delegate
相关文件;通过 Category 以及 method swizzling 来 hook 住解码相关的关键函数,取出解码后 buffer,进行转换(buffer 的转换代码来自 DJIWidget),详见
DJIVideoPreviewer+RetrieveDeocdedBuffer
相关文件;
这么一来日后维护的成本大大降低,往后如果 DJIVideoPreviewer
内部的解码逻辑发生改变,那么重新找到解码渲染的关键函数来进行 hook 就好了。