本项目主要是集成大疆SDK,并通过大疆API来获取无人机实时传递回来的h264数据,以及通过编解码API拿到YUV数据;
该项目里面封装了一些基础的API调用,方便后续集成调用。
1. 无人机接入准备
2. 编译环境准备
3. 关键代码
4. 项目地址
1. 本项目测试机型为大疆的御pro 2,遥控器为不带屏遥控器。(带屏遥控器是无法使用大疆SDK的)
2. 集成使用要点:
* 需先用测试手机下载大疆专用APP (DJI GO 4)软件进行无人机与遥控器之间的对频操作,此时应该注意,每次将手机和无人机进行USB连接时,会弹出默认启动应用弹框,应选择仅次一次;不然后续装上自己的应用后,连上无人机是无法拉起我们自己的应用的。
* 当连接成功,可以正常起飞后,此时在手机端装入自己的应用,启动并执行 DroneModel中的 startRegisterSDK 方法,执行注册大疆SDK。
* 注册成功后,会弹出登录弹框,提示登录大疆账号,此时应填入自己的大疆账号(国内每三个月登录一次,国外不受此限制)
* 接口 setDjiProductStateCallBack 是回调产品数据,当存在产品时,会执行onAirPlaneStateChange(bool)方法,传入参数为true
* 接口 setDroneH264VideoDataCallBack 回调,来获取无人机回传的H264数据,获取H264数据
* 接口 DroneYUVDataCallBack回调,来获取无人机回传的数据,通过CodeManager解码后获取YUV数据
* 方法 videoPreview方法,传入surfaceView,来执行无人机摄像头数据预览
* 方法 setAirPlaneCameraParam方法,来设置无人机摄像头参数,具体参数请查看DJICameraParams类,其中有详细的说明
* DroneModel是单例方法,直接通过getInstance拿到单例对象即可
* FileUtils工具类提供了写入H264流到文件,写出的文件可以直接通过暴风影音来进行播放,查看H264是否正常
1. 当前编译环境:Android studio 3.2 gradle版本为:3.2.1 gradle-wrapper版本为:4.6
2. 大疆SDK导入 :在当前项目build.gradle 下导入
1. build.gradle 配置
android {
defaultConfig {
……
ndk {
// 这里只做了armeabi-v7a架构的支持,大疆SDK不支持armeabi架构
abiFilters 'armeabi-v7a'
}
}
dexOptions {
javaMaxHeapSize "4g"
}
packagingOptions {
doNotStrip "*/*/libdjivideo.so"
doNotStrip "*/*/libSDKRelativeJNI.so"
doNotStrip "*/*/libFlyForbid.so"
doNotStrip "*/*/libduml_vision_bokeh.so"
doNotStrip "*/*/libyuv2.so"
doNotStrip "*/*/libGroudStation.so"
doNotStrip "*/*/libFRCorkscrew.so"
doNotStrip "*/*/libUpgradeVerify.so"
doNotStrip "*/*/libFR.so"
doNotStrip "*/*/libDJIFlySafeCore.so"
doNotStrip "*/*/libdjifs_jni.so"
doNotStrip "*/*/libsfjni.so"
exclude 'META-INF/rxjava.properties'
}
dependencies {
implementation 'com.android.support:multidex:1.0.2'
implementation('com.dji:dji-sdk:4.9', {
//if your app needn't support Mavic 2 Pro and Mavic 2 Zoom, pls exclude the library-anti-distortion module
//like following
//exclude module: 'library-anti-distortion'
//Uncomment the following line if your APP does not need network RTK.
//For Phantom 4 RTK and Matrice 210 RTK V2. Network RTK can be used as a virtual reference station to provide
//cm level accurate position.
//exclude module: 'library-anti-distortion'
})
compileOnly 'com.dji:dji-sdk-provided:4.9'
}
}
2. AndroidManifest配置
//必须加入这两个feature
// application中需要填入apikey,这个key主要用于注册大疆SDK,需要向大疆开发者官网进行申请key,根据对应的包名申请
// 对应接入大疆遥控器时,需要拉起的页面,这个页面主要旨在监听USB,所以需要配置过滤器以及meta-data
----------------------------------------------------------------------------------------------------
/**
* @author 李文烙
* @date 2019/10/26
* @Desc 核心Application类,这里要执行加载大疆SDK
*/
public class MainApplication extends Application {
private static BaseProduct djiProduct;
@Override
public void onCreate() {
super.onCreate();
}
public static synchronized BaseProduct getProductInstance() {
if (null == djiProduct) {
djiProduct = DJISDKManager.getInstance().getProduct();
}
return djiProduct;
}
public static synchronized void updateProduct(BaseProduct product) {
djiProduct = product;
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
com.secneo.sdk.Helper.install(MainApplication.this);
}
}
----------------------------------------------------------------------------------------------------
/**
* @author 李文烙
* @date 2019/10/26
* @Desc 无人机Api调用模块类
*/
public class DroneModel {
private static final String TAG = "DJIModel";
private Context context;
private long lastUpdate;
private AtomicBoolean isRegistrationInProgress = new AtomicBoolean(false);
private Camera airPlaneCamera;
private DJIProductStateCallBack djiProductStateCallBack;
private VideoFeeder.VideoDataListener receviedVideoDataListener = null;
private DroneH264VideoDataCallBack droneH264VideoDataCallBack;
private DroneYUVDataCallBack droneYUVDataCallBack;
private static DroneModel djiInstance;
private SurfaceHolder.Callback surfaceCallback;
private DJICodecManager codecManager;
private DroneModel(Context context) {
this.context = context;
}
/**
* 单例模式获取实例
*
* @param context 上下文
* @return 单例模式
*/
public static DroneModel getInstance(Context context) {
if (djiInstance == null) {
synchronized (DroneModel.class) {
if (djiInstance == null) {
djiInstance = new DroneModel(context);
}
}
}
return djiInstance;
}
public void setDjiProductStateCallBack(DJIProductStateCallBack djiProductStateCallBack) {
this.djiProductStateCallBack = djiProductStateCallBack;
}
public void setDjiVideoDataCallBack(DroneH264VideoDataCallBack djiVideoDataCallBack) {
this.droneH264VideoDataCallBack = djiVideoDataCallBack;
}
public void setDroneYUVDataCallBack(DroneYUVDataCallBack droneYUVDataCallBack) {
this.droneYUVDataCallBack = droneYUVDataCallBack;
}
/**
* 设置无人机摄像头参数
*
* @param djiCameraParams 参数列表,仅支持分辨率、帧率等调节,具体请查看实现类
* @param callback 设置回调
*/
public void setAirPlaneCameraParam(DJICameraParams djiCameraParams, CommonCallbacks.CompletionCallback callback) {
final BaseProduct product = MainApplication.getProductInstance();
if (airPlaneCamera == null && product == null && product.getCamera() == null) {
return;
}
airPlaneCamera.setVideoResolutionAndFrameRate(djiCameraParams.getResolutionAndFrameRate(), callback);
airPlaneCamera.setVideoStandard(djiCameraParams.getVideoStandard(), callback);
airPlaneCamera.setFocusMode(djiCameraParams.getFocusMode(), callback);
airPlaneCamera.setMode(djiCameraParams.getCameraMode(), callback);
airPlaneCamera.setOrientation(djiCameraParams.getCameraOrientation(), callback);
}
/**
* 预览视频
*
* @param surfaceView 需要预览的视图
*/
public void videoPreview(SurfaceView surfaceView) {
if (codecManager != null) {
codecManager.cleanSurface();
codecManager.destroyCodec();
codecManager = null;
}
SurfaceHolder surfaceHolder = surfaceView.getHolder();
surfaceCallback = new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
Log.d(TAG, "real onSurfaceTextureAvailable");
if (codecManager == null) {
codecManager = new DJICodecManager(context, holder, surfaceView.getWidth(),
surfaceView.getHeight());
}
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (codecManager != null) {
codecManager.cleanSurface();
codecManager.destroyCodec();
codecManager = null;
}
if (airPlaneCamera != null && VideoFeeder.getInstance().provideTranscodedVideoFeed() != null) {
VideoFeeder.getInstance().provideTranscodedVideoFeed().removeVideoDataListener(receviedVideoDataListener);
}
}
};
surfaceHolder.addCallback(surfaceCallback);
}
/**
* 设置是否接收YUV数据,YUV数据是通过大疆编解码后拿到的数据,所以需要调用codecManager来打开相应的开关
* 这里要注意的是,必须先执行videoPreview进行预览后,才能调用YUV数据,不然codecManger未初始化
* @param {Object} boolean isEnable
*/
public void setReceviedYuvData(boolean isEnable) {
if (codecManager != null) {
if (isEnable) {
codecManager.enabledYuvData(true);
codecManager.setYuvDataCallback((final ByteBuffer yuvFrame, int dataSize, final int width, final
int height) -> {
if (droneYUVDataCallBack != null) {
droneYUVDataCallBack.onReceviedYUVData(yuvFrame, dataSize, width, height);
}
});
} else {
codecManager.enabledYuvData(false);
codecManager.setYuvDataCallback(null);
}
}
}
/**
* 注册大疆SDK,如果不执行注册,则无法使用大疆的任何接口数据
*/
public void startSDKRegistration() {
if (isRegistrationInProgress.compareAndSet(false, true)) {
AsyncTask.execute(() ->
DJISDKManager.getInstance().registerApp(context, new DJISDKManager.SDKManagerCallback() {
@Override
public void onRegister(DJIError djiError) {
if (djiError == DJISDKError.REGISTRATION_SUCCESS) {
Logger.error(TAG, DJISDKError.REGISTRATION_SUCCESS.getDescription());
DJISDKManager.getInstance().startConnectionToProduct();
HandlerUtils.postToMain(() -> loginDJIUserAccount());
Logger.info(TAG, "Register SDK Success");
} else {
Logger.info(TAG, "Register sdk fails, check network is available");
}
Logger.info(TAG, djiError.getDescription());
isRegistrationInProgress.set(false);
}
@Override
public void onProductDisconnect() {
Logger.info(TAG, "Product Disconnected");
notifyStatusChange(false);
}
@Override
public void onProductConnect(BaseProduct baseProduct) {
notifyStatusChange(true);
isRegistrationInProgress.set(false);
if (baseProduct != null) {
MainApplication.updateProduct(baseProduct);
}
}
@Override
public void onComponentChange(BaseProduct.ComponentKey componentKey, BaseComponent oldComponent,
BaseComponent newComponent) {
if (newComponent != null && oldComponent == null) {
Logger.info(TAG, componentKey.name() + ",index:" + newComponent.getIndex());
}
if (newComponent != null) {
newComponent.setComponentListener(isConnect -> {
Logger.info(TAG, " Component " + (isConnect ? "connected" : "disconnected"));
notifyStatusChange(isConnect);
});
}
}
}));
}
}
/**
* 登录大疆账号,国内必须执行登录账号后,方可使用无人机,每3个月登录一次
*/
private void loginDJIUserAccount() {
UserAccountManager.getInstance().logIntoDJIUserAccount(context, new CommonCallbacks
.CompletionCallbackWith() {
@Override
public void onSuccess(final UserAccountState userAccountState) {
Logger.info(TAG, "login success! Account state is:" + userAccountState.name());
}
@Override
public void onFailure(DJIError error) {
Logger.info(TAG, "login Fail! Error info:" + error.getDescription());
}
});
}
/**
* 状态变更方法,根据无人机不同的产品状态来回调给使用者
*
* @param isConnect 是否连接
*/
private void notifyStatusChange(Boolean isConnect) {
if (djiProductStateCallBack != null) {
djiProductStateCallBack.onAirPlaneStateChange(isConnect);
}
registerDJIVideoDataListener();
if (isConnect) {
if (VideoFeeder.getInstance().getPrimaryVideoFeed() != null) {
VideoFeeder.getInstance().provideTranscodedVideoFeed().addVideoDataListener(receviedVideoDataListener);
}
}
}
/**
* 注册大疆无人机视频数据监听事件
*/
public void registerDJIVideoDataListener() {
final BaseProduct product = MainApplication.getProductInstance();
receviedVideoDataListener = (videoBuffer, size) -> {
Log.d(TAG, "camera recv video data size: " + size);
if (droneH264VideoDataCallBack != null) {
droneH264VideoDataCallBack.onReceviedVideoData(videoBuffer, size);
}
};
if (null == product || !product.isConnected()) {
airPlaneCamera = null;
} else {
if (!product.getModel().equals(Model.UNKNOWN_AIRCRAFT)) {
airPlaneCamera = product.getCamera();
airPlaneCamera.setMode(SettingsDefinitions.CameraMode.RECORD_VIDEO, djiError -> {
if (djiError != null) {
Logger.info(TAG, "can't change mode of cam era, error:" + djiError.getDescription());
}
});
airPlaneCamera.setVideoResolutionAndFrameRate(new ResolutionAndFrameRate(
SettingsDefinitions.VideoResolution.RESOLUTION_1920x1080, SettingsDefinitions.VideoFrameRate.FRAME_RATE_30_FPS), new CommonCallbacks.CompletionCallback() {
@Override
public void onResult(DJIError djiError) {
if (djiError != null) {
Logger.info(TAG, "can't change mode of cam era, error:" + djiError.getDescription());
}
}
});
/**
* 这里面需要调用该接口才能正常拿到H264流的回调
*/
VideoFeeder.getInstance().provideTranscodedVideoFeed().addVideoDataListener(receviedVideoDataListener);
//VideoFeeder.getInstance().getPrimaryVideoFeed().addVideoDataListener(receviedVideoDataListener);
}
}
}
/**
* 关闭时,应该反注销下视频流的获取
*/
public void unRegisterDJIVideoDataListener() {
if (VideoFeeder.getInstance().provideTranscodedVideoFeed() != null) {
VideoFeeder.getInstance().provideTranscodedVideoFeed().removeVideoDataListener(receviedVideoDataListener);
}
}
/**
* 大疆产品状态回调
*/
public interface DJIProductStateCallBack {
void onAirPlaneStateChange(Boolean isConnect);
}
/**
* 大疆无人机H264视频数据回调
*/
public interface DroneH264VideoDataCallBack {
void onReceviedVideoData(byte[] videoBuffer, int size);
}
public interface DroneYUVDataCallBack {
void onReceviedYUVData(ByteBuffer yuvFrame, int dataSize, int width, int height);
}
}
本项目地址(https://github.com/919232176/DroneMSDK),有需要的同学可以根据实际情况去更改哈