注意:SDK 团队目前已经把 DJIWidget 项目抽出来单独管理,项目地址

在 DJI-Mobile-SDK 4.7.0 发布时,SDK 团队在其 Mobile-SDK-iOS仓库中同时发布了 DJIWidget 的代码。值得一提,DJIWidgetDJI-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 的工程师不会对这样的方式感到恶心?我想他们内部的项目一定不是这样来做代码管理的😇。

如何高效并充分使用 DJIWidget - 图1

2.1 - 上帝说,要有 Pod

Mao 说,自己动手,丰衣足食。那就用自己双手让 DJIWidget 支持 CocoaPods 吧。这也就是说我们需要自己维护一份 DJIWidget.podspec。在 Kiwi 仓库的根目录下,可以看到这份 DJIWidget.podspec 文件,里面的内容一目了然。但这里有两个细节需要说说:

2.2 - Podfile 中 DJIWidget 的指定方式

由于在 pod trunk pushDJIWidget 编译检查一直通不过,所以笔者无法将它推到 master repo 或者是内部的私有 repo,因而项目里面暂时通过 git + tag 的方式来控制对 DJIWidget 的依赖:

  1. pod 'DJIWidget', :git => 'git@github.com:gzkiwiinc/Mobile-SDK-iOS.git', :tag => 'w1.0'
  2. 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.0v4.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:

  1. DJIVideoPreviewer.instance().delegate = self
  2. extension XXXViewController: DJIVideoPreviewerDelegate {
  3. func djiVideoPreviewer(_ videoPreviewer: DJIVideoPreviewer, willProcessImageBuffer imageBuffer: CVImageBuffer) {
  4. // Handle your imageBuffer
  5. }
  6. }

相关代码都在 Kiwi 仓库根目录下的 DJIVideoPreviewerExtension 文件夹中。它主要通过两个事情来完成任务:

  • 通过 Category 以及 associated object 来为 DJIVideoPreviewer 增加 delegate,详见 DJIVideoPreviewer+Delegate 相关文件;

  • 通过 Category 以及 method swizzling 来 hook 住解码相关的关键函数,取出解码后 buffer,进行转换(buffer 的转换代码来自 DJIWidget),详见 DJIVideoPreviewer+RetrieveDeocdedBuffer 相关文件;

这么一来日后维护的成本大大降低,往后如果 DJIVideoPreviewer 内部的解码逻辑发生改变,那么重新找到解码渲染的关键函数来进行 hook 就好了。