如果您在本教程中遇到任何错误或者bug,请使用Github issue,在DJI论坛发帖或者在Gitbook中评论告知我们。您可以随时给我们发送Github pull request来帮助我们修复错误。关于文档的修改需要符合 格式标准
你可以从这里下载到本教程的Demo工程:https://github.com/DJI-Mobile-SDK/iOS-FPVDemo.git
下载SDK
你可以从这里下载到最新的SDK https://developer.dji.com/mobile-sdk/downloads
开发包内容包括:
相机、云台、地面站、操纵杆等各模块的demo程序(地面站和操纵杆仅Level 2 开发者可使用)
支持平台:支持iOS 6.1 及以上版本
导入SDK
1 . 先拷贝DJISDK.framework到您的工程目录下,再将DJISDK.framework拖到您工程”Frameworks”的目录下,如下图所示:
2 . 从Xcode的左侧导航栏选择当前工程,进入右侧的 Build Phases -> Link Binary With Libraries. 点击底部的 "+" 按钮添加 libstdc++.6.0.9.dylib 和 libz.dylib 到你的工程中. 编译SDK时需要用到这些动态库。
3 . 由于iOS SDK中使用了C++进行开发,故为了在工程中使用SDK,需要修改工程其中一个实现文件的后缀名为".mm ". 这里我们修改 "AppDelegate.m " 文件, 修改为 "AppDelegate.mm ".
4 . 重要提示 : APP如需支持Inspire 1/Phantom 3 Professional飞行器, 需要添加MFI通信支持。
添加方法:在工程的Supporting Files文件夹下的plist文件添加MFI协议名称,如下图所示:
实现FPV视图功能
1 . 我们使用 FFMPEG 解码库 (http://ffmpeg.org ) 对视频流进行解码. 你可以在下载好的SDK开发包中找到 VideoPreviewer 文件夹. 将它拷贝到 Xcode 工程的文件夹中, 然后像下图所示添加到工程导航栏的thirdParty 文件夹下:
2 . 接下来来到XCode -> Project -> Build Phases -> Link Binary With Libraries中, 点击“+ ” 添加libiconv.dylib动态库文件. 然后在Build Settings中的Header Search Paths 添加FFMPEG的 include 文件夹路径. 同时在Library Search Paths中添加 FFMPEG的 lib 文件夹的路径,如下图所示:
3 . 创建 DJICameraViewController 并在 Main.storyboard 中设置它为Root ViewController , 添加一个 UIView到该viewController中, 设置它的 IBOutlet 为fpvPreviewView , 然后在底部添加两个 UIButton 和一个 UISegmentedControl 控件, 设置好它们的IBOutlets 和 IBActions,如下图所示:
打开 DJICameraViewController.m 文件 导入 DJISDK 和 VideoPreviewer 头文件, 然后创建 DJIDrone 和 DJICamera 实例变量,如下所示实现它们的委托方法:
Copy #import <DJISDK/DJISDK.h>
#import "VideoPreviewer.h"
@interface DJICameraViewController ()<DJICameraDelegate, DJIDroneDelegate>
{
DJIDrone *_drone;
DJICamera* _camera;
}
4 . 在ViewDidLoad 函数中初始化 DJIDrone 实例变量 然后设置它的类型为 DJIDrone_Inspire (你可以根据你的飞机机型输入), 设置它的delegate 并启动 Video Previewer. 同时, 添加一个 NSNotificationCenter 的监听来检查 RegisterAppSuccess 消息:
Copy - (void)viewDidLoad {
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(registerAppSuccess:) name:@"RegisterAppSuccess" object:nil];
_drone = [[DJIDrone alloc] initWithType:DJIDrone_Inspire];
_drone.delegate = self;
_camera = _drone.camera;
_camera.delegate = self;
[[VideoPreviewer instance] start];
}
- (void)registerAppSuccess:(NSNotification *)notification
{
NSLog(@"registerAppSuccess");
[_drone connectToDrone];
[_camera startCameraSystemStateUpdates];
}
接着, 在viewWillAppear 方法中设置 fpvPreviewView 实例变量为 VideoPreviewer 的视图,以展示视频流画面,然后在viewWillDisappear 方法中重置为nil:
Copy - (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[_drone connectToDrone];
[_camera startCameraSystemStateUpdates];
[[VideoPreviewer instance] setView:self.fpvPreviewView];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[_camera stopCameraSystemStateUpdates];
[_drone disconnectToDrone];
[[VideoPreviewer instance] setView:nil];
}
最后, 实现 DJICameraDelegate 方法:
Copy #pragma mark - DJICameraDelegate
-(void) camera:(DJICamera*)camera didReceivedVideoData:(uint8_t*)videoBuffer length:(int)length
{
uint8_t* pBuffer = (uint8_t*)malloc(length);
memcpy(pBuffer, videoBuffer, length);
[[VideoPreviewer instance].dataQueue push:pBuffer length:length];
}
-(void) camera:(DJICamera*)camera didUpdateSystemState:(DJICameraSystemState*)systemState
{
if (!systemState.isTimeSynced) { //Only for Phantom 2 Vision/Phantom 2 Vision+ to check camera time
[_camera syncTime:nil];
}
if (systemState.isUSBMode) { //Only for Phantom 2 Vision/Phantom 2 Vision+ to keep cameraMode when systemState is under USBMode
[_camera setCamerMode:CameraCameraMode withResultBlock:Nil];
}
}
-(void) droneOnConnectionStatusChanged:(DJIConnectionStatus)status
{
if (status == ConnectionSucceeded) {
NSLog(@"Connection Succeeded");
}
else if(status == ConnectionStartConnect)
{
NSLog(@"Start Reconnect");
}
else if(status == ConnectionBroken)
{
NSLog(@"Connection Broken");
}
else if (status == ConnectionFailed)
{
NSLog(@"Connection Failed");
}
}
-(void) camera:(DJICamera)camera didReceivedVideoData:(uint8_t )videoBuffer length:(int)length 委托方法用来发送视频流信息给VideoPreviewer 进行解码.
-(void) camera:(DJICamera)camera didUpdateSystemState:(DJICameraSystemState )systemState 委托方法用来获取相机的状态信息, 它会被频繁调用, 所以你可以在这个委托方法中更新你的App界面状态和相机参数设置.
-(void) droneOnConnectionStatusChanged:(DJIConnectionStatus)status 委托方法用来检查飞行器的连接状态.
激活 SDK
1 . 在DJICameraViewController.m文件的类扩展部分实现DJIAppManagerDelegate 协议:
Copy @interface DJICameraViewController ()<DJICameraDelegate, DJIDroneDelegate,DJIAppManagerDelegate>
{
DJIDrone *_drone;
DJICamera* _camera;
}
然后创建一个新方法,命名为registerApp ,并且在viewDidLoad方法中调用它,如下所示:
Copy - (void)registerApp
{
NSString *appKey = @"Enter Your App Key Here";
[DJIAppManager registerApp:appKey withDelegate:self];
}
- (void)viewDidLoad {
[super viewDidLoad];
_drone = [[DJIDrone alloc] initWithType:DJIDrone_Inspire];
_drone.delegate = self;
_camera = _drone.camera;
_camera.delegate = self;
[self registerApp];
}
注意 : 你可以在SDK网站上创建属于你自己App的App Key : https://developer.dji.com/user/mobile-sdk/ , 如下图所示:
另外, App Key 是和工程的 Bundle Identifier 相关联的. 所以如果Bundle Identifier不正确的话,你就不能在多个不同工程里面使用同一个App Key。
如果注册App失败, 你可以通过检查以下委托方法的 error 变量值来寻找原因:
Copy -(void)appManagerDidRegisterWithError:(int)error;
APP KEY 激活失败码如下所示:
Check permission successful
Cannot connect to Internet
Get permission data timeout
Project package name does not match the app key's identification code
Activated device number is up to the maximum available one
App key's platform is not correct
App key has no permission
Error in server obtaining uuid
Server app package name abnormal
Server parsing activation data failed
AES 256 encryption unsupported
AES 256 encryption failed
2 . 接着, 我们来实现 DJIAppManagerDelegate 方法:
Copy #pragma mark DJIAppManagerDelegate Method
-(void)appManagerDidRegisterWithError:(int)error
{
NSString* message = @"Register App Successed!";
if (error != RegisterSuccess) {
message = @"Register App Failed! Please enter your App Key and check the network.";
}else
{
NSLog(@"registerAppSuccess");
[_drone connectToDrone];
[_camera startCameraSystemStateUpdates];
[[VideoPreviewer instance] start];
}
UIAlertView* alertView = [[UIAlertView alloc] initWithTitle:@"Register App" message:message delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
[alertView show];
}
在以上代码中, 当注册app成功后,我们调用了DJIDrone的connectToDrone 的方法, 启动DJIDrone和飞行器的连接, 然后调用DJICamera的startCameraSystemStateUpdates 方法来更新camera system state. 进一步的,调用VideoPreviewer 实例变量的start方法来开始视频流解码。 最后,我们创建一个UIAlertView来提醒用户注册app的状态.
3 . 现在运行你的Xcode工程, 如果一切顺利, 你可以看到 "Register App Successed!" 的提示!同时,如果你可以看到类似以下截屏画面, 那么你就可以准备启动你的航拍飞机,享受飞机摄像机上的FPV画面了!
连接飞行器
完成以上步骤后, 现在就可以连接你的移动设备到DJI飞行器上,检查是否获取到FPV画面,以下是连接指引:
连接 Inspire 1, Phantom 3 Professional or Phantom 3 Advanced:
1 . 首先启动遥控器电源, 再启动你的飞行器
2 . 使用苹果的数据线将移动设备连接到遥控器上
3 . 如果弹出“是否信任该设备?”对话框,选择信任
4 . 使用app来控制飞机的相机
连接 DJI Phantom 2 Vision+ or Phantom 2 Vision:
1 . 首先启动遥控器电源, 再启动你的飞行器
2 . 启动Wi-Fi中继器电源
3 . 在你的移动设备上启动 WIFI ,然后连接到名字类似Phantom-xxxxxx (xxxxxx是你的中继器SSID数字)的WIFI网络上
4 . 使用app来控制飞机的相机
享受FPV视图
如果你可以在app中看到飞机的视频流,那么恭喜,你已经完成了第一部分教程的内容了!下图是app的截屏:
实现拍照功能
添加以下代码到 captureAction 方法中:
Copy - (IBAction)captureAction:(id)sender {
[_camera startTakePhoto:CameraSingleCapture withResult:^(DJIError *error) {
if (error.errorCode != ERR_Succeeded) {
NSLog(@"Take Photo Error : %@", error.errorDescription);
}
}];
}
调用DJICamera 的以下方法:
-(void) startTakePhoto:(CameraCaptureMode)captureMode withResult:(DJIExecuteResultBlock)block;
CameraCaptureMode 有以下四种类型:
Copy /**
* Camera capture mode
*/
typedef NS_ENUM(uint8_t, CameraCaptureMode){
/**
* Single capture
*/
CameraSingleCapture,
/**
* Multiple capture
*/
CameraMultiCapture,
/**
* Continuous capture
*/
CameraContinousCapture,
/**
* AEB capture. Support in Inspire 1/Phantom3 professional/Phantom3 Advanced
*/
CameraAEBCapture,
};
它们提供了多种方式进行拍照, CameraSingleCapture 使用起来很简单, 你不需要在调用startTakePhoto 方法之前,设置其它参数. 对于 CameraMultiCapture , 你需要使用 DJICamera.h 的
-(void) setMultiCaptureCount:(CameraMultiCaptureCount)count withResultBlock:(DJIExecuteResultBlock)block 方法来设置多拍参数,并且在block中检查是否设置成功,然后才能调用startTakePhoto 方法。
更多信息, 请查看 DJICamera.h 和 DJICameraSettingsDef.h 文件。
注意: 因为 DJICamera 有多个子类: DJIInspireCamera, DJIPhantom3AdvancedCamera, DJIPhantomCamera等等, 你需要找到对应的方法来设置拍照参数. 例如, Inspire 1支持CameraAEBCapture 模式, 所以你应该在DJIInspireCamera.h文件中寻找AEB的设置方法,而不是DJICamera.h.
这里我们设置拍照模式为 CameraSingleCapture . 你可以在block中的DJIError 变量检查拍照结果。
编译运行你的工程,尝试下拍照功能,如果屏幕在你点击拍照 按钮后,闪烁了一下,表示拍照成功!到此,你已经完成了拍照功能。
实现录像功能
1. 切换相机模式Camera Mode
在实现录像功能之前,我们需要先切换相机模式。
我们先检查下DJICameraSettingsDef.h 文件:
Copy /**
* Camera work mode. Used in Inspire/Phantom3 professional/Phantom3 Advanced
*/
typedef NS_ENUM(uint8_t, CameraWorkMode){
/**
* Capture mode. In this mode, user could do capture action only.
*/
CameraWorkModeCapture = 0x00,
/**
* Record mode. In this mode, user could do record action only.
*/
CameraWorkModeRecord = 0x01,
/**
* Playback mode. In this mode, user could preview photos or videos and delete the file.
*/
CameraWorkModePlayback = 0x02,
/**
* Download mode. In this mode, user could download the selected file from SD card
*/
CameraWorkModeDownload = 0x03,
/**
* Unknown
*/
CameraWorkModeUnknown = 0xFF
};
如你所见, 有5种CameraWorkMode , 这里我们只使用 CameraWorkModeCapture 和 CameraWorkModeRecord 两种相机模式. 因为这里使用的是Inspire 1机型, 所以我们要用DJIInspireCamera.h 文件中的
-(void) setCameraWorkMode:(CameraWorkMode)mode withResult:(DJIExecuteResultBlock)block 方法来切换相机模式。
打开 Main.storyboard ,为UISegmented Control控件添加一个 IBOutlet, 命名为 "changeWorkModeSegmentControl". 还记得第一部分教程 中DJICameraDelegate 的一个委托方法吗?
-(void) camera:(DJICamera)camera didUpdateSystemState:(DJICameraSystemState )systemState;
我们可以用这个委托方法,在切换CameraWorkModeCapture 和 CameraWorkModeRecord 相机模式时,更新segmented control的状态。
Copy -(void) camera:(DJICamera*)camera didUpdateSystemState:(DJICameraSystemState*)systemState
{
if (_drone.droneType == DJIDrone_Inspire) {
//Update UISegmented Control's state
if (systemState.workMode == CameraWorkModeCapture) {
[self.changeWorkModeSegmentControl setSelectedSegmentIndex:0];
}else if (systemState.workMode == CameraWorkModeRecord){
[self.changeWorkModeSegmentControl setSelectedSegmentIndex:1];
}
}
}
然后我们按以下方式实现 changeWorkModeAction 方法:
Copy - (IBAction)changeWorkModeAction:(id)sender {
DJIInspireCamera* inspireCamera = (DJIInspireCamera*)_camera;
__weak DJICameraViewController *weakSelf = self;
UISegmentedControl *segmentControl = (UISegmentedControl *)sender;
if (segmentControl.selectedSegmentIndex == 0) { //CaptureMode
[inspireCamera setCameraWorkMode:CameraWorkModeCapture withResult:^(DJIError *error) {
if (error.errorCode != ERR_Succeeded) {
UIAlertView *errorAlert = [[UIAlertView alloc] initWithTitle:@"Set CameraWorkModeCapture Failed" message:error.errorDescription delegate:weakSelf cancelButtonTitle:@"OK" otherButtonTitles:nil];
[errorAlert show];
}
}];
}else if (segmentControl.selectedSegmentIndex == 1){ //RecordMode
[inspireCamera setCameraWorkMode:CameraWorkModeRecord withResult:^(DJIError *error) {
if (error.errorCode != ERR_Succeeded) {
UIAlertView *errorAlert = [[UIAlertView alloc] initWithTitle:@"Set CameraWorkModeRecord Failed" message:error.errorDescription delegate:weakSelf cancelButtonTitle:@"OK" otherButtonTitles:nil];
[errorAlert show];
}
}];
}
}
这里添加了两个UIAlertView, 在设置相机模式失败时提醒用户。
2. 添加录像按钮
首先, 我们需要一个布尔值来保存录像的状态,还需要一个UILabel来展示录像时间. 我们Main.storyboard , 然后拖动一个UILabel控件到屏幕的顶部,设置好它的Autolayout,并在DJICameraViewController.m 文件中创建一个名字为"currentRecordTimeLabel "的IBOutlet. 接着,为录像按钮创建一个名字为 "recordBtn " 的IBOutlet。
然后在DJICameraViewController 的类拓展中添加布尔参数 isRecording . 确保在viewDidLoad 方法中隐藏掉currentRecordTimeLabel . 我们可以在以下委托方法中更新 isRecording 的值 和 currentRecordTimeLabel 的文本:
Copy -(void) camera:(DJICamera*)camera didUpdateSystemState:(DJICameraSystemState*)systemState
{
if (_drone.droneType == DJIDrone_Inspire) {
self.isRecording = systemState.isRecording;
[self.currentRecordTimeLabel setHidden:!self.isRecording];
[self.currentRecordTimeLabel setText:[self formattingSeconds:systemState.currentRecordingTime]];
if (self.isRecording) {
[self.recordBtn setTitle:@"Stop Record" forState:UIControlStateNormal];
}else
{
[self.recordBtn setTitle:@"Start Record" forState:UIControlStateNormal];
}
//Update UISegmented Control's state
if (systemState.workMode == CameraWorkModeCapture) {
[self.changeWorkModeSegmentControl setSelectedSegmentIndex:0];
}else if (systemState.workMode == CameraWorkModeRecord){
[self.changeWorkModeSegmentControl setSelectedSegmentIndex:1];
}
}
}
因为 currentRecordingTime 的值是以秒为单位计算的, 所以我们需要将它转换成“mm:ss” 的格式,如下所示:
Copy - (NSString *)formattingSeconds:(int)seconds
{
NSDate *date = [NSDate dateWithTimeIntervalSince1970:seconds];
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:@"mm:ss"];
[formatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
NSString *formattedTimeString = [formatter stringFromDate:date];
return formattedTimeString;
}
接着, 添加以下代码到 recordAction 方法中:
Copy - (IBAction)recordAction:(id)sender {
__weak DJICameraViewController *weakSelf = self;
if (self.isRecording) {
[_camera stopRecord:^(DJIError *error) {
if (error.errorCode != ERR_Succeeded) {
UIAlertView *errorAlert = [[UIAlertView alloc] initWithTitle:@"Stop Record Error" message:error.errorDescription delegate:weakSelf cancelButtonTitle:@"OK" otherButtonTitles:nil];
[errorAlert show];
}
}];
}else
{
[_camera startRecord:^(DJIError *error) {
if (error.errorCode != ERR_Succeeded) {
UIAlertView *errorAlert = [[UIAlertView alloc] initWithTitle:@"Start Record Error" message:error.errorDescription delegate:weakSelf cancelButtonTitle:@"OK" otherButtonTitles:nil];
[errorAlert show];
}
}];
}
}
在上面的代码中, 我们通过判断isRecording 布尔值变量的值,实现了DJICamera 类的startRecord 和 stopRecord 方法. 并在出现错误时弹框提醒用户.
现在, 我们可以编译运行工程,检查下刚做好的功能. 你可以尝试下 录像 和 切换相机工作模式 功能, 如果一切顺利,你会看到以下App截屏画面:
恭喜你! 你的FPV航拍App已经大功告成,你现在可以用它来控制Inspire 1的相机了。
总结
你已经完成了整篇教程的学习: 学会如何使用DJI Mobile SDK来开发app,展示飞行器相机的FPV视图,控制DJI 飞行器的相机进行拍照 和录像 操作,这两项功能经常被使用到,也是一款航拍app的基本功能点。但是,要开发一款很酷的航拍app,你还有很长的一段路要走。像预览SD卡中的照片和视频,展示飞机的OSD数据等等。请继续关注我们后续的教程,希望你喜欢!