javaandroidrtmplive-streamingdji-sdk

Using DJI Android SDK LiveStreamManager to stream drone camera live has a huge delay. Using the SampleCode it doesn't, what am I missing?


I have created an android application using the DJI SDK. I have followed the instruction, and basically copied the code from the DJI Sample Code (https://github.com/dji-sdk/Mobile-SDK-Android/blob/master/Sample%20Code/app/src/main/java/com/dji/sdk/sample/demo/camera/LiveStreamView.java), since it was working properly.

After launching a Connectivity activity, which registers the SDK and connects to the Mavic 2 Zoom drone, another activity comes, which handles live streaming to a RTMP server. When using the sample code, streaming to the same RTMP server, it has no delay, but when using my app, it has a good 15 second delay. I can't figure out why, I'm using the same components. The only difference is that I'm setting the camera focus to the max, but I did the same in the Sample Code, so it shouldn't cause any problems. Also using the same VideoFeedView as in the Sample.

public class MainActivity extends Activity implements View.OnClickListener {

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

    private String liveShowUrl = "rtmp://192.168.00.00/live";

    private VideoFeedView primaryVideoFeedView;
    private VideoFeedView fpvVideoFeedView;
    private EditText showUrlInputEdit;

    private Button startLiveShowBtn;
    private Button enableVideoEncodingBtn;
    private Button disableVideoEncodingBtn;
    private Button stopLiveShowBtn;
    private Button soundOnBtn;
    private Button soundOffBtn;
    private Button isLiveShowOnBtn;
    private Button showInfoBtn;
    private Button showLiveStartTimeBtn;
    private Button showCurrentVideoSourceBtn;
    private Button changeVideoSourceBtn;
    private Camera camera;

    private LiveStreamManager.OnLiveChangeListener listener;
    private LiveStreamManager.LiveStreamVideoSource currentVideoSource = LiveStreamManager.LiveStreamVideoSource.Primary;

    private CommonCallbacks.CompletionCallback focusSetCompletionCallback = new CommonCallbacks.CompletionCallback() {
        @Override
        public void onResult(DJIError djiError) {
            Log.d(TAG, "Camera focus is set to manual");
            Toast.makeText(getApplicationContext(), "camera focus set to manual", Toast.LENGTH_SHORT).show();
        }
    };

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

        camera = DronifyApplication.getCameraInstance();
        camera.getFocusRingValueUpperBound(new CommonCallbacks.CompletionCallbackWith<Integer>() {
            @Override
            public void onSuccess(Integer integer) {
                Toast.makeText(getApplicationContext(), "UPPER IS: " + integer.toString(),Toast.LENGTH_LONG).show();
                Log.d(TAG, "UPPER IS: " + integer.toString());
            }

            @Override
            public void onFailure(DJIError djiError) {
                Toast.makeText(getApplicationContext(), "UPPER IS NOT SUPPORTED", Toast.LENGTH_LONG).show();

            }
        });
        camera.setFocusMode(SettingsDefinitions.FocusMode.MANUAL, focusSetCompletionCallback);
        if (camera.isAdjustableFocalPointSupported()) {
            camera.setFocusRingValue(65, new CommonCallbacks.CompletionCallback() {
                @Override
                public void onResult(DJIError djiError) {
                    Log.i(TAG, "set focus ring value to max");
                    Toast.makeText(getApplicationContext(), "set focus ring value to max", Toast.LENGTH_SHORT).show();
                }
            });
        }

        Intent intent = new Intent(getApplication(), TCPService.class);
        getApplication().startService(intent);
    }

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

    public static boolean isMultiStreamPlatform() {
        if (DJISDKManager.getInstance() == null){
            return false;
        }
        Model model = DJISDKManager.getInstance().getProduct().getModel();
        return model != null && (model == Model.INSPIRE_2
                || model == Model.MATRICE_200
                || model == Model.MATRICE_210
                || model == Model.MATRICE_210_RTK
                || model == Model.MATRICE_600
                || model == Model.MATRICE_600_PRO
                || model == Model.A3
                || model == Model.N3);
    }

    private void initUI() {
        primaryVideoFeedView = (VideoFeedView) findViewById(R.id.video_view_primary_video_feed);
        primaryVideoFeedView.registerLiveVideo(VideoFeeder.getInstance().getPrimaryVideoFeed(), true);

        fpvVideoFeedView = (VideoFeedView) findViewById(R.id.video_view_fpv_video_feed);
        fpvVideoFeedView.registerLiveVideo(VideoFeeder.getInstance().getSecondaryVideoFeed(), false);
        if (isMultiStreamPlatform()){
            fpvVideoFeedView.setVisibility(View.VISIBLE);
        }

        showUrlInputEdit = (EditText) findViewById(R.id.edit_live_show_url_input);
        showUrlInputEdit.setText(liveShowUrl);

        startLiveShowBtn = (Button) findViewById(R.id.btn_start_live_show);
        enableVideoEncodingBtn = (Button) findViewById(R.id.btn_enable_video_encode);
        disableVideoEncodingBtn = (Button) findViewById(R.id.btn_disable_video_encode);
        stopLiveShowBtn = (Button) findViewById(R.id.btn_stop_live_show);
        soundOnBtn = (Button) findViewById(R.id.btn_sound_on);
        soundOffBtn = (Button) findViewById(R.id.btn_sound_off);
        isLiveShowOnBtn = (Button) findViewById(R.id.btn_is_live_show_on);
        showInfoBtn = (Button) findViewById(R.id.btn_show_info);
        showLiveStartTimeBtn = (Button) findViewById(R.id.btn_show_live_start_time);
        showCurrentVideoSourceBtn = (Button) findViewById(R.id.btn_show_current_video_source);
        changeVideoSourceBtn = (Button) findViewById(R.id.btn_change_video_source);

        startLiveShowBtn.setOnClickListener(this);
        enableVideoEncodingBtn.setOnClickListener(this);
        disableVideoEncodingBtn.setOnClickListener(this);
        stopLiveShowBtn.setOnClickListener(this);
        soundOnBtn.setOnClickListener(this);
        soundOffBtn.setOnClickListener(this);
        isLiveShowOnBtn.setOnClickListener(this);
        showInfoBtn.setOnClickListener(this);
        showLiveStartTimeBtn.setOnClickListener(this);
        showCurrentVideoSourceBtn.setOnClickListener(this);
        changeVideoSourceBtn.setOnClickListener(this);
    }

    private void initListener() {
        showUrlInputEdit.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {

            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
                liveShowUrl = s.toString();
            }

            @Override
            public void afterTextChanged(Editable s) {

            }
        });
        listener = new LiveStreamManager.OnLiveChangeListener() {
            @Override
            public void onStatusChanged(int i) {
                //Toast.makeText(getApplicationContext(), "status changed : " + i, Toast.LENGTH_SHORT).show();
            }
        };
    }



    @Override
    public void onAttachedToWindow() {
        super.onAttachedToWindow();
        BaseProduct product = DronifyApplication.getProductInstance();
        if (product == null || !product.isConnected()) {
            //Toast.makeText(getApplicationContext(), "disconnected", Toast.LENGTH_SHORT).show();
            return;
        }
        if (isLiveStreamManagerOn()){
            DJISDKManager.getInstance().getLiveStreamManager().registerListener(listener);
        }
    }

    @Override
    public void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        if (isLiveStreamManagerOn()){
            DJISDKManager.getInstance().getLiveStreamManager().unregisterListener(listener);
        }
    }

    private boolean isLiveStreamManagerOn() {
        if (DJISDKManager.getInstance().getLiveStreamManager() == null) {
            //Toast.makeText(getApplicationContext(), "no liveStream manager", Toast.LENGTH_SHORT).show();
            return false;
        }
        return true;
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btn_start_live_show:
                startLiveShow();
                break;
            case R.id.btn_enable_video_encode:
                enableReEncoder();
                break;
            case R.id.btn_disable_video_encode:
                disableReEncoder();
                break;
            case R.id.btn_stop_live_show:
                stopLiveShow();
                break;
            case R.id.btn_sound_on:
                soundOn();
                break;
            case R.id.btn_sound_off:
                soundOff();
                break;
            case R.id.btn_is_live_show_on:
                isLiveShowOn();
                break;
            case R.id.btn_show_info:
                showInfo();
                break;
            case R.id.btn_show_live_start_time:
                showLiveStartTime();
                break;
            case R.id.btn_show_current_video_source:
                showCurrentVideoSource();
                break;
            case R.id.btn_change_video_source:
                changeVideoSource();
                break;
            default:
                break;
        }
    }



    private void enableReEncoder() {
        if (!isLiveStreamManagerOn()) {
            return;
        }
        DJISDKManager.getInstance().getLiveStreamManager().setVideoEncodingEnabled(true);
        Toast.makeText(getApplicationContext(), "Force Re-Encoder Enabled!", Toast.LENGTH_SHORT).show();
    }

    private void disableReEncoder() {
        if (!isLiveStreamManagerOn()) {
            return;
        }
        DJISDKManager.getInstance().getLiveStreamManager().setVideoEncodingEnabled(false);
        Toast.makeText(getApplicationContext(), "Disable Force Re-Encoder!", Toast.LENGTH_SHORT).show();
    }


    private void soundOn() {
        if (!isLiveStreamManagerOn()) {
            return;
        }
        DJISDKManager.getInstance().getLiveStreamManager().setAudioMuted(false);
        Toast.makeText(getApplicationContext(), "Sound ON", Toast.LENGTH_SHORT).show();
    }

    private void soundOff() {
        if (!isLiveStreamManagerOn()) {
            return;
        }
        DJISDKManager.getInstance().getLiveStreamManager().setAudioMuted(true);
        Toast.makeText(getApplicationContext(), "Sound OFF", Toast.LENGTH_SHORT).show();
    }

    private void isLiveShowOn() {
        if (!isLiveStreamManagerOn()) {
            return;
        }
       Toast.makeText(getApplicationContext(), "Is Live Show On:" + DJISDKManager.getInstance().getLiveStreamManager().isStreaming(), Toast.LENGTH_SHORT).show();
    }


    private void showLiveStartTime() {
        if (!isLiveStreamManagerOn()) {
            return;
        }
        if (!DJISDKManager.getInstance().getLiveStreamManager().isStreaming()){
            Toast.makeText(getApplicationContext(), "Please Start Live First", Toast.LENGTH_SHORT).show();
            return;
        }
        long startTime = DJISDKManager.getInstance().getLiveStreamManager().getStartTime();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
        String sd = sdf.format(new Date(Long.parseLong(String.valueOf(startTime))));
        Toast.makeText(getApplicationContext(), "Live Start Time: " + sd, Toast.LENGTH_SHORT).show();
    }

    private void changeVideoSource() {
        if (!isLiveStreamManagerOn()) {
            return;
        }
        if (!isSupportSecondaryVideo()) {
            return;
        }
        if (DJISDKManager.getInstance().getLiveStreamManager().isStreaming()) {
            Toast.makeText(getApplicationContext(), "Before change live source, you should stop live stream!", Toast.LENGTH_SHORT).show();
            return;
        }
        currentVideoSource = (currentVideoSource == LiveStreamManager.LiveStreamVideoSource.Primary) ?
                LiveStreamManager.LiveStreamVideoSource.Secoundary :
                LiveStreamManager.LiveStreamVideoSource.Primary;
        DJISDKManager.getInstance().getLiveStreamManager().setVideoSource(currentVideoSource);
        Toast.makeText(getApplicationContext(), "Change Success ! Video Source : " + currentVideoSource.name(), Toast.LENGTH_SHORT).show();
    }

    private void showCurrentVideoSource(){
        Toast.makeText(getApplicationContext(), "Video Source : " + currentVideoSource.name(), Toast.LENGTH_SHORT).show();
    }

    private boolean isSupportSecondaryVideo(){
        if (isMultiStreamPlatform()) {
            Toast.makeText(getApplicationContext(), "No secondary video!", Toast.LENGTH_SHORT).show();
            return false;
        }
        return true;
    }

    private void showInfo() {
        StringBuilder sb = new StringBuilder();
        sb.append("Video BitRate:").append(DJISDKManager.getInstance().getLiveStreamManager().getLiveVideoBitRate()).append(" kpbs\n");
        sb.append("Audio BitRate:").append(DJISDKManager.getInstance().getLiveStreamManager().getLiveAudioBitRate()).append(" kpbs\n");
        sb.append("Video FPS:").append(DJISDKManager.getInstance().getLiveStreamManager().getLiveVideoFps()).append("\n");
        sb.append("Video Cache size:").append(DJISDKManager.getInstance().getLiveStreamManager().getLiveVideoCacheSize()).append(" frame");
        Toast.makeText(getApplicationContext(), sb.toString(), Toast.LENGTH_LONG).show();
    }

    void startLiveShow() {
        Toast.makeText(getApplicationContext(), "start live show: " + isLiveStreamManagerOn(), Toast.LENGTH_SHORT).show();
        if (!isLiveStreamManagerOn()) {
            Toast.makeText(getApplicationContext(), "1. return", Toast.LENGTH_SHORT).show();
            return;
        }
        if (DJISDKManager.getInstance().getLiveStreamManager().isStreaming()) {
            Toast.makeText(getApplicationContext(), "live show already started", Toast.LENGTH_SHORT).show();
            return;
        }

     new Thread() {
            @Override
            public void run() {
                DJISDKManager.getInstance().getLiveStreamManager().setLiveUrl(liveShowUrl);
                DJISDKManager.getInstance().getLiveStreamManager().setVideoEncodingEnabled(true);
                DJISDKManager.getInstance().getLiveStreamManager().setAudioMuted(false);
                final int result = DJISDKManager.getInstance().getLiveStreamManager().startStream();
                DJISDKManager.getInstance().getLiveStreamManager().setStartTime();
                runOnUiThread(new Runnable() {
                public void run() {
                    Toast.makeText(getApplication(), "RESULT: " + result, Toast.LENGTH_SHORT).show();
                }
                });
            }
        }.start();
    }

    private void stopLiveShow() {
        if (!isLiveStreamManagerOn()) {
            return;
        }
        DJISDKManager.getInstance().getLiveStreamManager().stopStream();
        Toast.makeText(getApplicationContext(), "stop live show", Toast.LENGTH_SHORT).show();
    }
}

Any idea why? I have tested it on Google Pixel 2, and Huawei Mate 10. The sample has no problem on both devices, my app has the delay. Thanks!


Solution

  • Answering my own question, the only difference I noticed was that the SampleCode asked for 4 permission, and all the projects I've tried or copied the permissions, always just 3 permissions.

    So Manifest:

    < uses-permission android:name="android.permission.RECORD_AUDIO" />

    your runtime permissions:

    Manifest.permission.RECORD_AUDIO

    and the delay is gone, all works fine. Still don't know why :)