本节全篇为大疆 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()
方法以调用 FPVDemoApplication
的 onCreate()
方法。
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
,三个按钮mCaptureBtn
,mShootPhotoModeBtn
,mRecordVideoModeBtn
,一个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;
}
}
}
在上面显示的代码中,我们实现了以下功能:
- 创建布局的UI元素的变量,包括3个 TextView
mTextConnectionStatus
,mTextProduct
,mVersionTv
和一个 ButtonmBtnOpen
. - 在
onCreate()
方法中,我们调用initUI()
方法去初始化UI元素。 - 接下来,实现
initUI()
方法以初始化三个 TextView 和那个 Button 。然后调用mBtnOpen
的setOnClickListener()
方法mBtnOpen
并把this
作为参数传入 。 - 最后,重写
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 文件夹中把文件都复制到你的项目中。
此外,打开 “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 选项卡,您应该会看到 MainActivity 和 ConnectionActivity 的预览屏幕截图,如下所示:
- ConnectionActivity
- MainActivity
有关更多详细信息,请查看本教程的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();
}
}
在这里,我们实现了几个功能:
- 我们重写了
onCreate()
方法来初始化mHandler
和mDJISDKManagerCallback
实例变量并实现它们的回调方法。 - 对于
SDKManagerCallback
的四个接口方法。我们使用onRegister()
方法去检查应用程序注册状态并在此处显示文本消息。连接或断开产品时,将调用onProductConnect()
andonProductDisconnect()
方法。此外,我们使用onComponentChange()
方法来检查组件变更并调用notifyStatusChange()
方法来通知变更。 - 检查
WRITE_EXTERNAL_STORAGE
和READ_PHONE_STATE
权限,然后调用DJISDKManager
的registerApp()
方法去注册该应用程序。
现在让我们构建并运行项目并将其安装到您的Android设备上。如果一切顺利,当您成功注册应用程序时,您应该看到如下图所示的 "Register Success" 的文本提示。
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));
}
});
}
});
}
}
在上面显示的代码中,我们实现了以下功能:
- 在该
onCreate()
方法中,我们调用checkAndRequestPermissions()
方法来检查是否存在任何缺失的权限,并在需要时请求运行时权限。然后调用initUI()
方法初始化UI元素。 - 接下来,重写
onRequestPermissionsResult()
方法以检查运行时权限请求结果。然后调用startSDKRegistration()
方法来注册应用程序。 - 此外,实现
startSDKRegistration()
方法并调用DJISDKManager
的registerApp()
方法去注册应用程序。如果注册成功,在onRegister()
回调方法内调用DJISDKManager
的startConnectionToProduct()
方法来启动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);
}
}
在上面的代码中,我们实现了以下功能:
- 创建 "BroadcastReceiver" 并重写
onReceive()
方法以调用refreshSDKRelativeUI()
方法来刷新UI元素。 - 我们重写
onDestroy()
方法并通过传递mReceiver
变量来调用unregisterReceiver()
方法,从而注销广播接收者。 - 在
refreshSDKRelativeUI()
方法中,我们通过调用isConnected()
方法去检查产品的连接状态。如果产品已连接,我们启用mBtnOpen
按钮,更新mTextConnectionStatus
的文本内容,并用产品名称更新mTextProduct
的内容。否则,如果产品断开连接,我们将禁用mBtnOpen
按钮,并更新mTextProduct
andmTextConnectionStatus
的内容。
最后,让我们来实现 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" 文件并声明 TAG
和 mReceivedVideoDataListener
变量如下:
// 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产品的相机查看实时视频流!
享受第一人称视角
如果您可以在应用程序中看到实时视频流,恭喜!让我们前进吧。
实现拍照功能
现在,让我们重写 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()
方法,以控制相机拍照。在这里,我们调用 Handler
的 postDelayed()
方法来延迟方法执行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 mRecordBtn
的 setOnCheckedChangeListener()
方法并重写它的 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动画来演示这三个功能:
现在你可以用这个app去控制你的DJI产品的相机了。
摘要
在本教程中,您已经学会了如何使用DJI Mobile SDK从飞机的摄像头显示FPV视图,并控制DJI飞机的摄像头拍摄照片和录制视频。这些是典型的无人机移动应用程序中最基本和最常见的功能:拍照 和 录像。然而,如果你想创建一个更加华丽的无人机应用程序,你还有很长的路要走。应该实现更高级的功能,包括预览SD卡中的照片和视频,显示飞机的OSD数据等。希望您喜欢本教程,并继续关注我们的下一个!