大疆文档(6)-Android教程-相机应用程序

本节全篇为大疆 Mobile SDK 安卓教程 部分,ios教程参见 IOS教程 .

相机应用程序

本教程旨在让您对DJI Mobile SDK有一个基本的了解。它将实现FPV视图和两个基本的相机功能:拍摄照片录制视频

您可以从此 Github Page 下载教程的最终示例项目。

在本教程中,我们将使用Android Studio 3.3。

激活应用程序和在中国的飞机绑定

对于在中国使用的DJI SDK移动应用程序,需要激活应用程序并将飞机绑定到用户的DJI帐户。

如果未激活应用程序,未使用飞机(如果需要)或使用旧版SDK(<4.1),则将禁用所有 摄像头实时流 ,并且飞行将限制在100米直径和30米高度的区域内,以确保飞机保持在视线范围内。

要了解如何实现此功能,请查看前面的教程 Application Activation and Aircraft Binding .

实现应用程序UI

导入maven依赖

  • 创建名为 FPVDemo 的新项目
  • 包名 com.dji.FPVDemo
  • 最低版本 API 19: Android 4.4 (KitKat)
  • 选择 "Empty Activity" 然后其他默认

在之前的教程中 Importing and Activating DJI SDK in Android Studio Project 已经学了如何导入Android SDK Maven依赖,并激活应用程序。如果你没有读之前的,就回去看一下,看完了,在继续实现下一个功能。

构建活动布局

1. 创建 MApplication 类

com.dji.FPVDemo 下创建 MApplication 类,并替换内容如下:

package com.dji.FPVDemo;

import android.app.Application;
import android.content.Context;

import com.secneo.sdk.Helper;

public class MApplication extends Application {

    private FPVDemoApplication fpvDemoApplication;
    @Override
    protected void attachBaseContext(Context paramContext) {
        super.attachBaseContext(paramContext);
        Helper.install(MApplication.this);
        if (fpvDemoApplication == null) {
            fpvDemoApplication = new FPVDemoApplication();
            fpvDemoApplication.setContext(this);
        }
    }

    @Override
    public void onCreate() {
        super.onCreate();
        fpvDemoApplication.onCreate();
    }

}

这里我们首先重写 attachBaseContext() 方法,以在使用任何SDK功能之前调用Helper 类的 install() 方法来加载SDK类。如果不这样做将导致意外崩溃。接下来,重写 onCreate() 方法以调用 FPVDemoApplicationonCreate() 方法。

2. 创建 FPVDemoApplication 类

com.dji.FPVDemo 下创建 FPVDemoApplication 类,并替换内容如下:

package com.dji.FPVDemo;
import android.app.Application;

public class FPVDemoApplication extends Application{

    @Override
    public void onCreate() {
        super.onCreate();
    }
}

这里,我们重写onCreate()方法。当应用程序被创建的时候,我们可以做一些设置。

3. 实现 MainActivity 类

MainActivity.java 文件由Android Studio默认创建。替换代码如下:

public class MainActivity extends Activity implements TextureView.SurfaceTextureListener, View.OnClickListener {

    protected TextureView mVideoSurface = null;
    private Button mCaptureBtn, mShootPhotoModeBtn, mRecordVideoModeBtn;
    private ToggleButton mRecordBtn;
    private TextView recordingTime;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        setContentView(R.layout.activity_main);
        initUI();
    }

    @Override
    public void onResume() {
        super.onResume();
    }

    @Override
    public void onPause() {
        super.onPause();
    }

    @Override
    public void onStop() {
        super.onStop();
    }

    public void onReturn(View view){
        this.finish();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
    }

    @Override
    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
    }

    @Override
    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
    }

    @Override
    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
        return false;
    }

    @Override
    public void onSurfaceTextureUpdated(SurfaceTexture surface) {
    }

    private void initUI() {
        // init mVideoSurface
        mVideoSurface = (TextureView)findViewById(R.id.video_previewer_surface);

        recordingTime = (TextView) findViewById(R.id.timer);
        mCaptureBtn = (Button) findViewById(R.id.btn_capture);
        mRecordBtn = (ToggleButton) findViewById(R.id.btn_record);
        mShootPhotoModeBtn = (Button) findViewById(R.id.btn_shoot_photo_mode);
        mRecordVideoModeBtn = (Button) findViewById(R.id.btn_record_video_mode);
        
        if (null != mVideoSurface) {
            mVideoSurface.setSurfaceTextureListener(this);
        }
        
        mCaptureBtn.setOnClickListener(this);
        mRecordBtn.setOnClickListener(this);
        mShootPhotoModeBtn.setOnClickListener(this);
        mRecordVideoModeBtn.setOnClickListener(this);

        recordingTime.setVisibility(View.INVISIBLE);

        mRecordBtn.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
            
           }
        });
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btn_capture:{
                break;
            }
            case R.id.btn_shoot_photo_mode:{
                break;
            }
            case R.id.btn_record_video_mode:{
                break;
            }
            default:
                break;
        }
    }
    
}

以上代码实现了以下功能:

1. 创建布局UI元素变量,包括一个 TextureView mVideoSurface,三个按钮mCaptureBtnmShootPhotoModeBtnmRecordVideoModeBtn,一个ToggleButtonmRecordBtn和一个TextView recordingTime

2. 然后调用该initUI()方法初始化UI变量。并为所有按钮实现按钮的setOnClickListener()方法。还为 ToggleButton 实现了setOnCheckedChangeListener() 方法。

3. 重写onClick()方法以实现三个按钮的单击操作。

4. 实现 MainActivity 布局

打开 activity_main.xml 布局文件,并使用以下代码替换代码:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    
    <TextureView
        android:id="@+id/video_previewer_surface"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="center"
        android:layout_centerHorizontal="true"
        android:layout_above="@+id/linearLayout" />
    
     <LinearLayout 
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:orientation="horizontal"
      android:layout_alignParentBottom="true"
         android:id="@+id/linearLayout">
    <Button
        android:id="@+id/btn_capture"
        android:layout_width="0dp"
        android:layout_weight="1"
        android:layout_gravity="center_vertical"
        android:layout_height="wrap_content"
        android:text="Capture" 
        android:textSize="12sp"/>

    <ToggleButton
        android:id="@+id/btn_record"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="Start Record"
        android:textOff="Start Record"
        android:textOn="Stop Record"
        android:layout_weight="1"
        android:layout_gravity="center_vertical"
        android:textSize="12dp"
        android:checked="false" />
     
    <Button
        android:id="@+id/btn_shoot_photo_mode"
        android:layout_width="0dp"
        android:layout_weight="1"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:text="Shoot Photo Mode"
        android:textSize="12sp"/>

    <Button
        android:id="@+id/btn_record_video_mode"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="Record Video Mode"
        android:layout_weight="1"
        android:layout_gravity="center_vertical" />

     </LinearLayout>

    <TextView
        android:id="@+id/timer"
        android:layout_width="150dp"
        android:layout_weight="1"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:layout_marginTop="23dp"
        android:gravity="center"
        android:textColor="#ffffff"
        android:layout_alignTop="@+id/video_previewer_surface"
        android:layout_centerHorizontal="true" />

</RelativeLayout>

在xml文件中,我们创建一个 TextureView(id: video_previewer_surface) 元素来显示来自摄像头的实时视频流。此外,我们实现了一个 LinearLayout 元素创建的3个button和一个toggleButton:

  • "Capture" Button(id: btn_capture)
  • "Shoot Photo Mode" Button(id: btn_shoot_photo_mode)
  • "Record Video Mode" Button(id: btn_record_video_mode)
  • "Record" ToggleButton(id: btn_record)

最后,我们创建一个 TextView(id: timer) 元素来显示记录视频时间。

5. 实现 ConnectionActivity 类

为了改善用户体验,我们最好创建一个 activity 来显示DJI产品和SDK之间的连接状态,一旦连接,用户可以按 OPEN 按钮进入 MainActivity

现在让我们在 com.dji.FPVDemo 下创建一个名为 "ConnectionActivity" 的Activity,并替换代码如下:

public class ConnectionActivity extends Activity implements View.OnClickListener {

    private static final String TAG = ConnectionActivity.class.getName();

    private TextView mTextConnectionStatus;
    private TextView mTextProduct;
    private TextView mVersionTv;
    private Button mBtnOpen;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        setContentView(R.layout.activity_connection);
        initUI();
    }

    @Override
    public void onResume() {
        Log.e(TAG, "onResume");
        super.onResume();
    }

    @Override
    public void onPause() {
        Log.e(TAG, "onPause");
        super.onPause();
    }

    @Override
    public void onStop() {
        Log.e(TAG, "onStop");
        super.onStop();
    }

    public void onReturn(View view){
        Log.e(TAG, "onReturn");
        this.finish();
    }

    @Override
    protected void onDestroy() {
        Log.e(TAG, "onDestroy");
        super.onDestroy();
    }

    private void initUI() {
        mTextConnectionStatus = (TextView) findViewById(R.id.text_connection_status);
        mTextProduct = (TextView) findViewById(R.id.text_product_info);

        mVersionTv = (TextView) findViewById(R.id.textView2);
        mVersionTv.setText(getResources().getString(R.string.sdk_version, DJISDKManager.getInstance().getSDKVersion()));

        mBtnOpen = (Button) findViewById(R.id.btn_open);
        mBtnOpen.setOnClickListener(this);
        mBtnOpen.setEnabled(false);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btn_open: {
                break;
            }
            default:
                break;
        }
    }

}

在上面显示的代码中,我们实现了以下功能:

  1. 创建布局的UI元素的变量,包括3个 TextView mTextConnectionStatus, mTextProduct, mVersionTv 和一个 Button mBtnOpen.
  2. onCreate() 方法中,我们调用 initUI() 方法去初始化UI元素。
  3. 接下来,实现 initUI() 方法以初始化三个 TextView 和那个 Button 。然后调用 mBtnOpensetOnClickListener() 方法mBtnOpen并把 this 作为参数传入 。
  4. 最后,重写 onClick() 方法以实现Button的单击操作。

6. 实现 ConnectionActivity 布局

打开 activity_connection.xml 布局文件并使用以下代码替换代码:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:id="@+id/text_connection_status"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="Status: No Product Connected"
        android:textColor="@android:color/black"
        android:textSize="20dp"
        android:textStyle="bold"
        android:layout_alignBottom="@+id/text_product_info"
        android:layout_centerHorizontal="true"
        android:layout_marginBottom="89dp" />

    <TextView
        android:id="@+id/text_product_info"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="270dp"
        android:text="@string/product_information"
        android:textColor="@android:color/black"
        android:textSize="20dp"
        android:gravity="center"
        android:textStyle="bold"
        />

    <Button
        android:id="@+id/btn_open"
        android:layout_width="150dp"
        android:layout_height="55dp"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="350dp"
        android:background="@drawable/round_btn"
        android:text="Open"
        android:textColor="@color/colorWhite"
        android:textSize="20dp"
        />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="430dp"
        android:text="@string/sdk_version"
        android:textSize="15dp"
        android:id="@+id/textView2" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceSmall"
        android:text="DJIFPVDemo"
        android:id="@+id/textView"
        android:layout_marginTop="58dp"
        android:textStyle="bold"
        android:textSize="20dp"
        android:textColor="@color/colorBlack"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true" />

</RelativeLayout>

在xml文件中,我们在 RelativeLayout 中创建了四个 TextView 和一个 Button 。我们用 TextView(id: text_connection_status) 来显示产品连接状态,并用TextView(id:text_product_info) 显示连接的产品名称。 Button(id: btn_open) 用于打开 MainActivity

7. 配置res的xml资源文件

完成上述步骤后,从Github示例项目的 drawable 文件夹中把文件都复制到你的项目中。

imageFiles

此外,打开 “colors.xml” 文件并更新内容如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorWhite">#FFFFFF</color>
    <color name="colorBlack">#000000</color>
    
    <!-- your local color can remain the same -->
    <color name="colorPrimary">#3F51B5</color>
    <color name="colorPrimaryDark">#303F9F</color>
    <color name="colorAccent">#FF4081</color>
</resources>

此外,打开 "strings.xml" 文件并将内容替换为以下内容:

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <string name="app_name">DJI FPV Demo</string>
    <string name="action_settings">Settings</string>   
    <string name="disconnected">Disconnected</string>
    <string name="product_information">Product Information</string>
    <string name="connection_loose">Status: No Product Connected</string>
    <string name="sdk_version">DJI SDK Version: %1$s</string>

</resources>

最后,打开 "styles.xml" 文件,如果不是以下内容,择替换为以下内容:

<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

</resources>

现在,如果您打开 activity_main.xml 文件,然后单击左下角的 Design 选项卡,您应该会看到 MainActivityConnectionActivity 的预览屏幕截图,如下所示:

  • ConnectionActivity
img
  • MainActivity
img

有关更多详细信息,请查看本教程的Github源代码。

注册应用程序

完成上述步骤后,让我们用你从 DJI Developer 网站申请的 App Key 来注册应用程序。如果你不熟悉 App Key, 请查看 Get Started .

1. 让我们打开 AndroidManifest.xml 文件,并在 application 元素顶部添加以下代码:

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />

<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-feature
    android:name="android.hardware.usb.host"
    android:required="false" />
<uses-feature
    android:name="android.hardware.usb.accessory"
    android:required="true" />

在这里,我们请求必须授予应用程序的权限才能正确注册DJI SDK。此外,我们还声明了应用程序使用的 camera 和 USB hardwares。

接下来,在 application 元素开头添加 android:name=".MApplication"

<application
    android:name=".MApplication"
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">

此外,让我们在 ConnectionActivity activity元素的顶部添加以下元素作为元素的子元素:

<!-- DJI SDK -->
<uses-library android:name="com.android.future.usb.accessory" />
<meta-data
    android:name="com.dji.sdk.API_KEY"
    android:value="Please enter your APP Key here." />

<activity
    android:name="dji.sdk.sdkmanager.DJIAoaControllerActivity"
    android:theme="@android:style/Theme.Translucent" >
    <intent-filter>
        <action android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED" />
    </intent-filter>

    <meta-data
        android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED"
        android:resource="@xml/accessory_filter" />
</activity>
<service android:name="dji.sdk.sdkmanager.DJIGlobalService" >
</service>
<!-- DJI SDK -->

在上面的代码中,您应该将 "Please enter your App Key here." 替换为你的 App Key

最后,更新 "MainActivity" and "ConnectionActivity" 两个activity元素如下:

<activity android:name=".ConnectionActivity"
    android:configChanges="orientation"
    android:screenOrientation="portrait">

    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>
<activity android:name=".MainActivity"
    android:screenOrientation="landscape"></activity>

在上面的代码中,我们添加 "android:screenOrientation" 的属性来将 "ConnectionActivity" 设置为portrait 并将 "MainActivity" 设置为 landscape

2. 完成上述步骤后,打开 "FPVDemoApplication.java" 文件并将代码替换为Github源代码中的相同文件,这里我们将解释它的重要部分:

@Override
public void onCreate() {
    super.onCreate();
    mHandler = new Handler(Looper.getMainLooper());

    /**
     * When starting SDK services, an instance of interface DJISDKManager.DJISDKManagerCallback will be used to listen to
     * the SDK Registration result and the product changing.
     */
    mDJISDKManagerCallback = new DJISDKManager.SDKManagerCallback() {

        //Listens to the SDK registration result
        @Override
        public void onRegister(DJIError error) {

            if(error == DJISDKError.REGISTRATION_SUCCESS) {

                Handler handler = new Handler(Looper.getMainLooper());
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(getApplicationContext(), "Register Success", Toast.LENGTH_LONG).show();
                    }
                });

                DJISDKManager.getInstance().startConnectionToProduct();

            } else {

                Handler handler = new Handler(Looper.getMainLooper());
                handler.post(new Runnable() {

                    @Override
                    public void run() {
                        Toast.makeText(getApplicationContext(), "Register sdk fails, check network is available", Toast.LENGTH_LONG).show();
                    }
                });

            }
            Log.e("TAG", error.toString());
        }

        @Override
        public void onProductDisconnect() {
            Log.d("TAG", "onProductDisconnect");
            notifyStatusChange();
        }
        @Override
        public void onProductConnect(BaseProduct baseProduct) {
            Log.d("TAG", String.format("onProductConnect newProduct:%s", baseProduct));
            notifyStatusChange();

        }
        @Override
        public void onComponentChange(BaseProduct.ComponentKey componentKey, BaseComponent oldComponent,
                                      BaseComponent newComponent) {
            if (newComponent != null) {
                newComponent.setComponentListener(new BaseComponent.ComponentListener() {

                    @Override
                    public void onConnectivityChange(boolean isConnected) {
                        Log.d("TAG", "onComponentConnectivityChanged: " + isConnected);
                        notifyStatusChange();
                    }
                });
            }

            Log.d("TAG",
                    String.format("onComponentChange key:%s, oldComponent:%s, newComponent:%s",
                            componentKey,
                            oldComponent,
                            newComponent));

        }
    };

    //Check the permissions before registering the application for android system 6.0 above.
    int permissionCheck = ContextCompat.checkSelfPermission(getApplicationContext(), android.Manifest.permission.WRITE_EXTERNAL_STORAGE);
    int permissionCheck2 = ContextCompat.checkSelfPermission(getApplicationContext(), android.Manifest.permission.READ_PHONE_STATE);
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || (permissionCheck == 0 && permissionCheck2 == 0)) {
        //This is used to start SDK services and initiate SDK.
        DJISDKManager.getInstance().registerApp(getApplicationContext(), mDJISDKManagerCallback);
        Toast.makeText(getApplicationContext(), "registering, pls wait...", Toast.LENGTH_LONG).show();

    } else {
        Toast.makeText(getApplicationContext(), "Please check if the permission is granted.", Toast.LENGTH_LONG).show();
    }
}

在这里,我们实现了几个功能:

  1. 我们重写了 onCreate() 方法来初始化 mHandlermDJISDKManagerCallback 实例变量并实现它们的回调方法。
  2. 对于 SDKManagerCallback 的四个接口方法。我们使用 onRegister() 方法去检查应用程序注册状态并在此处显示文本消息。连接或断开产品时,将调用 onProductConnect() and onProductDisconnect() 方法。此外,我们使用 onComponentChange() 方法来检查组件变更并调用 notifyStatusChange() 方法来通知变更。
  3. 检查 WRITE_EXTERNAL_STORAGEREAD_PHONE_STATE 权限,然后调用 DJISDKManagerregisterApp() 方法去注册该应用程序。

现在让我们构建并运行项目并将其安装到您的Android设备上。如果一切顺利,当您成功注册应用程序时,您应该看到如下图所示的 "Register Success" 的文本提示。

registerSuccess

Important: 在加载SDK类之后,请在 onCreate() 方法内初始化DJI Android SDK类对象,否则将导致意外崩溃。

有关注册应用程序的更多详细信息,请查看本教程: Importing and Activating DJI SDK in Android Studio Project.

使用 ConnectionActivity

完成上述步骤后,让我们打开 "ConnectionActivity.java" 文件并在 onCreate() 方法上面创建几个用以检查权限和注册的变量:

private static final String[] REQUIRED_PERMISSION_LIST = new String[]{
        Manifest.permission.VIBRATE,
        Manifest.permission.INTERNET,
        Manifest.permission.ACCESS_WIFI_STATE,
        Manifest.permission.WAKE_LOCK,
        Manifest.permission.ACCESS_COARSE_LOCATION,
        Manifest.permission.ACCESS_NETWORK_STATE,
        Manifest.permission.ACCESS_FINE_LOCATION,
        Manifest.permission.CHANGE_WIFI_STATE,
        Manifest.permission.WRITE_EXTERNAL_STORAGE,
        Manifest.permission.BLUETOOTH,
        Manifest.permission.BLUETOOTH_ADMIN,
        Manifest.permission.READ_EXTERNAL_STORAGE,
        Manifest.permission.READ_PHONE_STATE,
};
private List<String> missingPermission = new ArrayList<>();
private AtomicBoolean isRegistrationInProgress = new AtomicBoolean(false);
private static final int REQUEST_PERMISSION_CODE = 12345;

接下来,在 onCreate()方法中调用 checkAndRequestPermissions() 方法,并实现以下方法:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    checkAndRequestPermissions();
    setContentView(R.layout.activity_connection);
    initUI();
}

/**
 * Checks if there is any missing permissions, and
 * requests runtime permission if needed.
 */
private void checkAndRequestPermissions() {
    // Check for permissions
    for (String eachPermission : REQUIRED_PERMISSION_LIST) {
        if (ContextCompat.checkSelfPermission(this, eachPermission) != PackageManager.PERMISSION_GRANTED) {
            missingPermission.add(eachPermission);
        }
    }
    // Request for missing permissions
    if (!missingPermission.isEmpty() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        ActivityCompat.requestPermissions(this,
                missingPermission.toArray(new String[missingPermission.size()]),
                REQUEST_PERMISSION_CODE);
    }

}

/**
 * Result of runtime permission request
 */
@Override
public void onRequestPermissionsResult(int requestCode,
                                       @NonNull String[] permissions,
                                       @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    // Check for granted permission and remove from missing list
    if (requestCode == REQUEST_PERMISSION_CODE) {
        for (int i = grantResults.length - 1; i >= 0; i--) {
            if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
                missingPermission.remove(permissions[i]);
            }
        }
    }
    // If there is enough permission, we will start the registration
    if (missingPermission.isEmpty()) {
        startSDKRegistration();
    } else {
        showToast("Missing permissions!!!");
    }
}

private void startSDKRegistration() {
    if (isRegistrationInProgress.compareAndSet(false, true)) {
        AsyncTask.execute(new Runnable() {
            @Override
            public void run() {
                showToast( "registering, pls wait...");
                DJISDKManager.getInstance().registerApp(getApplicationContext(), new DJISDKManager.SDKManagerCallback() {
                    @Override
                    public void onRegister(DJIError djiError) {
                        if (djiError == DJISDKError.REGISTRATION_SUCCESS) {
                            DJILog.e("App registration", DJISDKError.REGISTRATION_SUCCESS.getDescription());
                            DJISDKManager.getInstance().startConnectionToProduct();
                            showToast("Register Success");
                        } else {
                            showToast( "Register sdk fails, check network is available");
                        }
                        Log.v(TAG, djiError.getDescription());
                    }

                    @Override
                    public void onProductDisconnect() {
                        Log.d(TAG, "onProductDisconnect");
                        showToast("Product Disconnected");

                    }
                    @Override
                    public void onProductConnect(BaseProduct baseProduct) {
                        Log.d(TAG, String.format("onProductConnect newProduct:%s", baseProduct));
                        showToast("Product Connected");

                    }
                    @Override
                    public void onComponentChange(BaseProduct.ComponentKey componentKey, BaseComponent oldComponent,
                                                  BaseComponent newComponent) {

                        if (newComponent != null) {
                            newComponent.setComponentListener(new BaseComponent.ComponentListener() {

                                @Override
                                public void onConnectivityChange(boolean isConnected) {
                                    Log.d(TAG, "onComponentConnectivityChanged: " + isConnected);
                                }
                            });
                        }
                        Log.d(TAG,
                                String.format("onComponentChange key:%s, oldComponent:%s, newComponent:%s",
                                        componentKey,
                                        oldComponent,
                                        newComponent));

                    }
                });
            }
        });
    }
}

在上面显示的代码中,我们实现了以下功能:

  1. 在该 onCreate() 方法中,我们调用 checkAndRequestPermissions() 方法来检查是否存在任何缺失的权限,并在需要时请求运行时权限。然后调用 initUI() 方法初始化UI元素。
  2. 接下来,重写 onRequestPermissionsResult() 方法以检查运行时权限请求结果。然后调用 startSDKRegistration() 方法来注册应用程序。
  3. 此外,实现 startSDKRegistration() 方法并调用 DJISDKManagerregisterApp() 方法去注册应用程序。如果注册成功,在 onRegister()回调方法内调用 DJISDKManagerstartConnectionToProduct() 方法来启动SDK和DJI产品之间的连接。

完成上述步骤后,继续在onCreate()方法底部添加代码:

// Register the broadcast receiver for receiving the device connection's changes.
IntentFilter filter = new IntentFilter();
filter.addAction(FPVDemoApplication.FLAG_CONNECTION_CHANGE);
registerReceiver(mReceiver, filter);

在这里,我们注册广播接收者以接收设备连接的变化。

接下来,在 initUI() 方法下面添加以下代码:

protected BroadcastReceiver mReceiver = new BroadcastReceiver() {

    @Override
    public void onReceive(Context context, Intent intent) {
        refreshSDKRelativeUI();
    }
};

@Override
protected void onDestroy() {
    Log.e(TAG, "onDestroy");
    unregisterReceiver(mReceiver);
    super.onDestroy();
}

private void refreshSDKRelativeUI() {
    BaseProduct mProduct = FPVDemoApplication.getProductInstance();

    if (null != mProduct && mProduct.isConnected()) {
        Log.v(TAG, "refreshSDK: True");
        mBtnOpen.setEnabled(true);

        String str = mProduct instanceof Aircraft ? "DJIAircraft" : "DJIHandHeld";
        mTextConnectionStatus.setText("Status: " + str + " connected");

        if (null != mProduct.getModel()) {
            mTextProduct.setText("" + mProduct.getModel().getDisplayName());
        } else {
            mTextProduct.setText(R.string.product_information);
        }

    } else {
        Log.v(TAG, "refreshSDK: False");
        mBtnOpen.setEnabled(false);

        mTextProduct.setText(R.string.product_information);
        mTextConnectionStatus.setText(R.string.connection_loose);
    }
}

在上面的代码中,我们实现了以下功能:

  1. 创建 "BroadcastReceiver" 并重写 onReceive() 方法以调用 refreshSDKRelativeUI() 方法来刷新UI元素。
  2. 我们重写 onDestroy() 方法并通过传递 mReceiver 变量来调用 unregisterReceiver() 方法,从而注销广播接收者。
  3. refreshSDKRelativeUI() 方法中,我们通过调用 isConnected() 方法去检查产品的连接状态。如果产品已连接,我们启用 mBtnOpen 按钮,更新 mTextConnectionStatus 的文本内容,并用产品名称更新 mTextProduct 的内容。否则,如果产品断开连接,我们将禁用 mBtnOpen 按钮,并更新 mTextProduct and mTextConnectionStatus 的内容。

最后,让我们来实现 mBtnOpen 按钮的 onClick() 的方法和showToast()方法,如下图所示:

@Override
public void onClick(View v) {
    switch (v.getId()) {

        case R.id.btn_open: {
            Intent intent = new Intent(this, MainActivity.class);
            startActivity(intent);
            break;
        }
        default:
            break;
    }
}

private void showToast(final String toastMsg) {
    runOnUiThread(new Runnable() {
        @Override
        public void run() {
            Toast.makeText(getApplicationContext(), toastMsg, Toast.LENGTH_LONG).show();

        }
    });
}

在这里,我们使用 MainActivity 类创建一个Intent对象,并通过传递 intent 对象来调用 startActivity() 方法,去启动MainActivity。

实现第一人称视角

现在,让我们打开 "MainActivity.java" 文件并声明 TAGmReceivedVideoDataListener 变量如下:

// Codec for video live view
protected DJICodecManager mCodecManager = null;
private static final String TAG = MainActivity.class.getName();
protected VideoFeeder.VideoDataListener mReceivedVideoDataListener = null;

然后更新 onCreate() 方法如下:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    initUI();

    // The callback for receiving the raw H264 video data for camera live view
    mReceivedVideoDataListener = new VideoFeeder.VideoDataListener() {

        @Override
        public void onReceive(byte[] videoBuffer, int size) {
            if (mCodecManager != null) {
                mCodecManager.sendDataToDecoder(videoBuffer, size);
            }
        }
    };
}

在上面的代码中,我们使用VideoFeeder的 VideoDataListener()初始化 mReceivedVideoDataListener 。在回调中,我们重写 onReceive() 方法以获取 raw H264 视频数据并发送到 mCodecManager 进行解码。

接下来,让我们实现 onProductChange() 方法,并在 onResume() 方法中调用它,如下所示:

protected void onProductChange() {
    initPreviewer();
}

@Override
public void onResume() {
    Log.e(TAG, "onResume");
    super.onResume();
    initPreviewer();
    onProductChange();

    if(mVideoSurface == null) {
        Log.e(TAG, "mVideoSurface is null");
    }
}

此外,让我们实现两个重要的方法, 在 mVideoSurface 上来显示和重置实时视频流:

private void initPreviewer() {

    BaseProduct product = FPVDemoApplication.getProductInstance();

    if (product == null || !product.isConnected()) {
        showToast(getString(R.string.disconnected));
    } else {
        if (null != mVideoSurface) {
            mVideoSurface.setSurfaceTextureListener(this);
        }
        if (!product.getModel().equals(Model.UNKNOWN_AIRCRAFT)) {
            VideoFeeder.getInstance().getPrimaryVideoFeed().addVideoDataListener(mReceivedVideoDataListener);
        }
    }
}

private void uninitPreviewer() {
    Camera camera = FPVDemoApplication.getCameraInstance();
    if (camera != null){
        // Reset the callback
            VideoFeeder.getInstance().getPrimaryVideoFeed().addVideoDataListener(null);
    }
}

initPreviewer() 方法中,首先,我们检查产品连接状态并调用 TextureView 的 setSurfaceTextureListener() 方法,将texture监听器设置为MainActivity。然后检查 VideoFeeder 是否有视频信息流并且视频信息流的大小>0,并将 mReceivedVideoDataListener 设置为它的“监听器”。因此,一旦相机连接并接收视频数据,它将显示在 mVideoSurface TextureView上。

此外,我们实现了 uninitPreviewer() 方法去把相机的 "VideoDataListener" 重置为null。

现在,让我们重写4个SurfaceTextureListener的接口方法,如下所示:

@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
    Log.e(TAG, "onSurfaceTextureAvailable");
    if (mCodecManager == null) {
        mCodecManager = new DJICodecManager(this, surface, width, height);
    }
}

@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
    Log.e(TAG, "onSurfaceTextureSizeChanged");
}

@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
    Log.e(TAG,"onSurfaceTextureDestroyed");
    if (mCodecManager != null) {
        mCodecManager.cleanSurface();
        mCodecManager = null;
    }

    return false;
}

@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
}

我们在onSurfaceTextureAvailable()方法中初始化 mCodecManager 变量,然后重置 mCodecManager 并调用其 cleanSurface() 方法来重置 surface 数据。

有关更多详细实现,请查看本教程的Github源代码。

连接到飞机或手持设备

完成上述步骤后,请检查这个 Connect Mobile Device and Run Application 指南,以运行应用程序,并根据我们目前完成的应用程序,从您的DJI产品的相机查看实时视频流!

享受第一人称视角

如果您可以在应用程序中看到实时视频流,恭喜!让我们前进吧。

fpv

实现拍照功能

现在,让我们重写 onClick() 方法实现拍照按钮的点击操作:

@Override
public void onClick(View v) {

    switch (v.getId()) {
        case R.id.btn_capture:{
            captureAction();
            break;
        }
        default:
            break;
    }
}

然后声明一个 handler 变量并在onCreate()方法中初始化,如下所示:

private Handler handler;
handler = new Handler();

接下来,实现 captureAction() 方法如下:

// Method for taking photo
private void captureAction(){

    final Camera camera = FPVDemoApplication.getCameraInstance();
    if (camera != null) {

        SettingsDefinitions.ShootPhotoMode photoMode = SettingsDefinitions.ShootPhotoMode.SINGLE; // Set the camera capture mode as Single mode
        camera.setShootPhotoMode(photoMode, new CommonCallbacks.CompletionCallback(){
                @Override
                public void onResult(DJIError djiError) {
                    if (null == djiError) {
                        handler.postDelayed(new Runnable() {
                            @Override
                            public void run() {
                                camera.startShootPhoto(new CommonCallbacks.CompletionCallback() {
                                    @Override
                                    public void onResult(DJIError djiError) {
                                        if (djiError == null) {
                                            showToast("take photo: success");
                                        } else {
                                            showToast(djiError.getDescription());
                                        }
                                    }
                                });
                            }
                        }, 2000);
                    }
                }
        });
    }
}

在上面的代码中,首先,我们创建一个 "ShootPhotoMode" 变量并设置为 "ShootPhotoMode.SINGLE" 模式 。然后调用 Camera 对象的 setShootPhotoMode() 方法来设置拍摄照片模式。相机拍照模式在其定义中有几种模式。您可以为 "ShootPhotoMode" 使用“AEB”,“BURST”,“HDR”等,有关详细信息,请查看 SettingsDefinitions.ShootPhotoMode

接下来,在 setShootPhotoMode 方法的完成回调函数中实现 Camera 的 startShootPhoto() 方法,以控制相机拍照。在这里,我们调用 HandlerpostDelayed() 方法来延迟方法执行2000毫秒,因为相机需要时间去执行 setShootPhotoMode 命令。

最后,我们覆盖其获取结果的onResult()方法startShootPhoto()并向用户显示相关文本。我们重写 startShootPhoto()onResult() 方法去获取结果并向用户展示相关文本。

构建并运行您的项目,然后尝试拍摄照片功能。如果在按下 Capture 按钮后屏幕闪烁,则捕获功能现在可以正常工作。

实现录像功能

切换相机模式

在我们继续实现录像操作方法之前,让我们实现切换相机模式功能。改善 onClick() 方法,通过为mShootPhotoModeBtn and mRecordVideoModeBtn 添加按钮的点击操作,如下所示:

@Override
public void onClick(View v) {

    switch (v.getId()) {
        case R.id.btn_capture:{
            captureAction();
            break;
        }
        case R.id.btn_shoot_photo_mode:{
            switchCameraMode(SettingsDefinitions.CameraMode.SHOOT_PHOTO);
            break;
        }
        case R.id.btn_record_video_mode:{
            switchCameraMode(SettingsDefinitions.CameraMode.RECORD_VIDEO);
            break;
        }
        default:
            break;
    }
}

接下来,实现 switchCameraMode() 方法:

private void switchCameraMode(SettingsDefinitions.CameraMode cameraMode){

    Camera camera = FPVDemoApplication.getCameraInstance();
    if (camera != null) {
        camera.setMode(cameraMode, new CommonCallbacks.CompletionCallback() {
            @Override
            public void onResult(DJIError error) {

                if (error == null) {
                    showToast("Switch Camera Mode Succeeded");
                } else {
                    showToast(error.getDescription());
                }
            }
        });
        }
}

在上面的代码中,我们调用 Camera 对象的 setMode() 方法并为其分配 cameraMode 参数。然后重写 onResult() 方法以向用户显示更改相机模式结果。

实现录像功能

完成切换相机模式功能后,我们现在可以实现录像功能了。让我们通过在底部添加以下代码来改进一下 initUI() 方法:

mRecordBtn.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
    @Override
    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
        if (isChecked) {
            recordingTime.setVisibility(View.VISIBLE);
            startRecord();

        } else {
            recordingTime.setVisibility(View.INVISIBLE);
            stopRecord();
        }
    }
});

在这里,我们实现 ToggleButton mRecordBtnsetOnCheckedChangeListener() 方法并重写它的 onCheckedChanged()方法来检查 isChecked 变量值,它代表了按钮的切换状态,并对应的调用 startRecord()stopRecord() 方法。

接下来,实现 startRecord()stopRecord() 方法如下所示:

// Method for starting recording
private void startRecord(){

    final Camera camera = FPVDemoApplication.getCameraInstance();
    if (camera != null) {
        camera.startRecordVideo(new CommonCallbacks.CompletionCallback(){
            @Override
            public void onResult(DJIError djiError)
            {
                if (djiError == null) {
                    showToast("Record video: success");
                }else {
                    showToast(djiError.getDescription());
                }
            }
        }); // Execute the startRecordVideo API
    }
}

// Method for stopping recording
private void stopRecord(){

    Camera camera = FPVDemoApplication.getCameraInstance();
    if (camera != null) {
        camera.stopRecordVideo(new CommonCallbacks.CompletionCallback(){

            @Override
            public void onResult(DJIError djiError)
            {
                if(djiError == null) {
                    showToast("Stop recording: success");
                }else {
                    showToast(djiError.getDescription());
                }
            }
        }); // Execute the stopRecordVideo API
    }

}

在上面的代码中,我们调用了Camera 的 startRecordVideo() and stopRecordVideo() 方法来实现 开始录像 和 停止录像 的功能。并通过重写 onResult() 方法向我们的用户显示结果消息。

最后,当视频开始录制时,我们应该向用户显示录制时间信息。因此,让我们将以下代码添加到onCreate()方法的底部,如下所示:

Camera camera = FPVDemoApplication.getCameraInstance();

if (camera != null) {

    camera.setSystemStateCallback(new SystemState.Callback() {
        @Override
        public void onUpdate(SystemState cameraSystemState) {
            if (null != cameraSystemState) {

                int recordTime = cameraSystemState.getCurrentVideoRecordingTimeInSeconds();
                int minutes = (recordTime % 3600) / 60;
                int seconds = recordTime % 60;

                final String timeString = String.format("%02d:%02d", minutes, seconds);
                final boolean isVideoRecording = cameraSystemState.isRecording();

                MainActivity.this.runOnUiThread(new Runnable() {

                    @Override
                    public void run() {

                        recordingTime.setText(timeString);

                        /*
                         * Update recordingTime TextView visibility and mRecordBtn's check state
                         */
                        if (isVideoRecording){
                            recordingTime.setVisibility(View.VISIBLE);
                        }else
                        {
                            recordingTime.setVisibility(View.INVISIBLE);
                        }
                    }
                });
            }
        }
    });

}

在这里,我们实现了 Camera 的 setSystemStateCallback() 并重写了 onUpdate() 方法,以获取当前相机系统状态,我们调用 SystemState 对象的 getCurrentVideoRecordingTimeInSeconds() 方法来获取录像时间信息。在我们向用户显示录像时间信息之前,我们应该将它从秒转换为 "00:00" 格式,包括分钟和秒。最后,我们使用最新的录像时间信息更新TextView recordingTime 变量的文本值,并更新 recordingTime TextView 在UI线程中的可见性。

有关更多详细信息,请查看本教程的Github源代码。

现在,让我们构建并运行项目并检查功能。这里我们以Mavic Pro为例。您可以尝试使用 Capture, Record and Switch Camera WorkMode 功能,这里有一个gif动画来演示这三个功能:

demoAni

现在你可以用这个app去控制你的DJI产品的相机了。

摘要

在本教程中,您已经学会了如何使用DJI Mobile SDK从飞机的摄像头显示FPV视图,并控制DJI飞机的摄像头拍摄照片和录制视频。这些是典型的无人机移动应用程序中最基本和最常见的功能:拍照录像。然而,如果你想创建一个更加华丽的无人机应用程序,你还有很长的路要走。应该实现更高级的功能,包括预览SD卡中的照片和视频,显示飞机的OSD数据等。希望您喜欢本教程,并继续关注我们的下一个!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容