创建航拍相机App

  • 下载SDK

  • 导入SDK

  • 实现FPV视图功能

  • 激活 SDK

  • 连接飞行器

  • 享受FPV视图

  • 实现拍照功能

  • 实现录像功能

    • 1. 切换相机模式Camera Mode

    • 2. 添加录像按钮

  • 总结

如果您在本教程中遇到任何错误或者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 开发者可使用)

  • API 说明文档

  • lib库文件

支持平台:支持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 文件 导入 DJISDKVideoPreviewer 头文件, 然后创建 DJIDroneDJICamera 实例变量,如下所示实现它们的委托方法:

#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 消息:

- (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:

- (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 方法:

#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协议:

@interface DJICameraViewController ()<DJICameraDelegate, DJIDroneDelegate,DJIAppManagerDelegate>
{
    DJIDrone *_drone;
    DJICamera* _camera;
}

然后创建一个新方法,命名为registerApp,并且在viewDidLoad方法中调用它,如下所示:

- (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 变量值来寻找原因:

-(void)appManagerDidRegisterWithError:(int)error;

APP KEY 激活失败码如下所示:

resultDescription

0

Check permission successful

-1

Cannot connect to Internet

-2

Invalid app key

-3

Get permission data timeout

-4

Device uuid not match

-5

Project package name does not match the app key's identification code

-6

App key is forbidden

-7

Activated device number is up to the maximum available one

-8

App key's platform is not correct

-9

App key does not exist

-10

App key has no permission

-11

Server parser failed

-12

Error in server obtaining uuid

-13

Server app package name abnormal

-14

Server parsing activation data failed

-15

AES 256 encryption unsupported

-16

AES 256 encryption failed

-17

Get device uuid failed

-18

Empty app key

-1000

Server error

2. 接着, 我们来实现 DJIAppManagerDelegate 方法:

#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 方法中:

- (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 有以下四种类型:

  /**
 *  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.hDJICameraSettingsDef.h 文件。


注意: 因为 DJICamera 有多个子类: DJIInspireCamera, DJIPhantom3AdvancedCamera, DJIPhantomCamera等等, 你需要找到对应的方法来设置拍照参数. 例如, Inspire 1支持CameraAEBCapture 模式, 所以你应该在DJIInspireCamera.h文件中寻找AEB的设置方法,而不是DJICamera.h.


这里我们设置拍照模式为 CameraSingleCapture. 你可以在block中的DJIError 变量检查拍照结果。

编译运行你的工程,尝试下拍照功能,如果屏幕在你点击拍照按钮后,闪烁了一下,表示拍照成功!到此,你已经完成了拍照功能。

实现录像功能

1. 切换相机模式Camera Mode

在实现录像功能之前,我们需要先切换相机模式。 我们先检查下DJICameraSettingsDef.h文件:

   /**
 *  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, 这里我们只使用 CameraWorkModeCaptureCameraWorkModeRecord 两种相机模式. 因为这里使用的是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;

我们可以用这个委托方法,在切换CameraWorkModeCaptureCameraWorkModeRecord 相机模式时,更新segmented control的状态。

-(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 方法:

- (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 的文本:

-(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” 的格式,如下所示:

- (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 方法中:

- (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类的startRecordstopRecord方法. 并在出现错误时弹框提醒用户.

现在, 我们可以编译运行工程,检查下刚做好的功能. 你可以尝试下 录像切换相机工作模式 功能, 如果一切顺利,你会看到以下App截屏画面:

恭喜你! 你的FPV航拍App已经大功告成,你现在可以用它来控制Inspire 1的相机了。

总结

你已经完成了整篇教程的学习: 学会如何使用DJI Mobile SDK来开发app,展示飞行器相机的FPV视图,控制DJI 飞行器的相机进行拍照录像操作,这两项功能经常被使用到,也是一款航拍app的基本功能点。但是,要开发一款很酷的航拍app,你还有很长的一段路要走。像预览SD卡中的照片和视频,展示飞机的OSD数据等等。请继续关注我们后续的教程,希望你喜欢!