package org.droidplanner.services.android.impl.utils.video; import android.content.Context; import android.os.Bundle; import android.os.Handler; import android.os.RemoteException; import android.text.TextUtils; import android.util.Log; import android.view.Surface; import com.o3dr.android.client.utils.connection.AbstractIpConnection; import com.o3dr.android.client.utils.connection.IpConnectionListener; import com.o3dr.android.client.utils.connection.UdpConnection; import com.o3dr.android.client.utils.video.DecoderListener; import com.o3dr.android.client.utils.video.MediaCodecManager; import com.o3dr.services.android.lib.drone.action.CameraActions; import com.o3dr.services.android.lib.drone.attribute.error.CommandExecutionError; import com.o3dr.services.android.lib.model.ICommandListener; import org.droidplanner.services.android.impl.communication.model.DataLink; import java.io.IOException; import java.nio.ByteBuffer; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import timber.log.Timber; /** * Handles the video stream from artoo. */ public class VideoManager implements IpConnectionListener { private static final String TAG = VideoManager.class.getSimpleName(); private static final SimpleDateFormat FILE_DATE_FORMAT = new SimpleDateFormat("yyyy_MM_dd_HH_mm_ss", Locale.US); private static final String NO_VIDEO_OWNER = "no_video_owner"; protected static final long RECONNECT_COUNTDOWN = 1000l; //ms public static final int ARTOO_UDP_PORT = 5600; private static final int UDP_BUFFER_SIZE = 1500; private final AtomicBoolean videoStreamObserverUsed = new AtomicBoolean(false); private final DataLink.DataLinkProvider linkProvider; public interface LinkListener { void onLinkConnected(); void onLinkDisconnected(); } private final Runnable reconnectTask = new Runnable() { @Override public void run() { handler.removeCallbacks(reconnectTask); if(linkConn != null) linkConn.connect(linkProvider.getConnectionExtras()); } }; private LinkListener linkListener; protected final Handler handler; private final AtomicBoolean isStarted = new AtomicBoolean(false); private final AtomicBoolean wasConnected = new AtomicBoolean(false); private final AtomicReference<String> videoOwnerId = new AtomicReference<>(NO_VIDEO_OWNER); private final AtomicReference<String> videoTagRef = new AtomicReference<>(""); protected UdpConnection linkConn; private final MediaCodecManager mediaCodecManager; private final StreamRecorder streamRecorder; private int linkPort = -1; public VideoManager(Context context, Handler handler, DataLink.DataLinkProvider linkProvider) { this.streamRecorder = new StreamRecorder(context); this.handler = handler; this.mediaCodecManager = new MediaCodecManager(handler); this.mediaCodecManager.setNaluChunkListener(streamRecorder); this.linkProvider = linkProvider; } private void enableLocalRecording(String filename) { streamRecorder.enableRecording(filename); } private void disableLocalRecording() { streamRecorder.disableRecording(); } public void startDecoding(final int udpPort, final Surface surface, final DecoderListener listener) { start(udpPort, null); final Surface currentSurface = mediaCodecManager.getSurface(); if (surface == currentSurface) { if (listener != null) listener.onDecodingStarted(); return; } // Stop any in progress decoding. Log.i(TAG, "Setting up video stream decoding."); mediaCodecManager.stopDecoding(new DecoderListener() { @Override public void onDecodingStarted() { } @Override public void onDecodingError() { } @Override public void onDecodingEnded() { try { Log.i(TAG, "Video decoding set up complete. Starting..."); mediaCodecManager.startDecoding(surface, listener); } catch (IOException | IllegalStateException e) { Log.e(TAG, "Unable to create media codec.", e); if (listener != null) listener.onDecodingError(); } } }); } public void reset() { Timber.d("Resetting video tag (%s) and owner id (%s)", videoTagRef.get(), videoOwnerId.get()); videoTagRef.set(""); videoOwnerId.set(NO_VIDEO_OWNER); disableLocalRecording(); stopDecoding(null); } public void stopDecoding(DecoderListener listener) { Log.i(TAG, "Aborting video decoding process."); mediaCodecManager.stopDecoding(listener); stop(); } public boolean isLinkConnected() { return this.linkConn != null && this.linkConn.getConnectionStatus() == AbstractIpConnection.STATE_CONNECTED; } private void start(int udpPort, LinkListener listener) { if (this.linkConn == null || udpPort != this.linkPort){ if (isStarted.get()){ stop(); } this.linkConn = new UdpConnection(handler, udpPort, UDP_BUFFER_SIZE, true, 42); this.linkConn.setIpConnectionListener(this); this.linkPort = udpPort; } Log.d(TAG, "Starting video manager"); handler.removeCallbacks(reconnectTask); isStarted.set(true); this.streamRecorder.startConverterThread(); this.linkConn.connect(linkProvider.getConnectionExtras()); this.linkListener = listener; } private void stop() { Log.d(TAG, "Stopping video manager"); handler.removeCallbacks(reconnectTask); isStarted.set(false); if(this.linkConn != null) { //Break the link this.linkConn.disconnect(); this.linkConn = null; } this.linkPort = -1; this.streamRecorder.stopConverterThread(); } @Override public void onIpConnected() { Log.d(TAG, "Connected to video stream"); handler.removeCallbacks(reconnectTask); wasConnected.set(true); if (linkListener != null) linkListener.onLinkConnected(); } @Override public void onIpDisconnected() { Log.d(TAG, "Video stream disconnected"); if (isStarted.get()) { if (shouldReconnect()) { //Try to reconnect handler.postDelayed(reconnectTask, RECONNECT_COUNTDOWN); } if (linkListener != null && wasConnected.get()) linkListener.onLinkDisconnected(); wasConnected.set(false); } } @Override public void onPacketReceived(ByteBuffer packetBuffer) { if (!videoStreamObserverUsed.get()) { // Feed this data stream to the decoder. mediaCodecManager.onInputDataReceived(packetBuffer.array(), packetBuffer.limit()); } } protected void postSuccessEvent(final ICommandListener listener) { if (handler != null && listener != null) { handler.post(new Runnable() { @Override public void run() { try { listener.onSuccess(); } catch (RemoteException e) { Log.e(TAG, e.getMessage(), e); } } }); } } protected void postTimeoutEvent(final ICommandListener listener) { if (handler != null && listener != null) { handler.post(new Runnable() { @Override public void run() { try { listener.onTimeout(); } catch (RemoteException e) { Log.e(TAG, e.getMessage(), e); } } }); } } protected void postErrorEvent(final int error, final ICommandListener listener) { if (handler != null && listener != null) { handler.post(new Runnable() { @Override public void run() { try { listener.onError(error); } catch (RemoteException e) { Log.e(TAG, e.getMessage(), e); } } }); } } protected boolean shouldReconnect() { return true; } private void checkForLocalRecording(String appId, Bundle videoProps){ if (TextUtils.isEmpty(appId)) return; final boolean isLocalRecordingEnabled = videoProps.getBoolean(CameraActions.EXTRA_VIDEO_ENABLE_LOCAL_RECORDING); if (isLocalRecordingEnabled) { String localRecordingFilename = videoProps.getString(CameraActions.EXTRA_VIDEO_LOCAL_RECORDING_FILENAME); if(TextUtils.isEmpty(localRecordingFilename)){ localRecordingFilename = appId + "." + FILE_DATE_FORMAT.format(new Date()); } if(!localRecordingFilename.equalsIgnoreCase(streamRecorder.getRecordingFilename())){ if(streamRecorder.isRecordingEnabled()){ disableLocalRecording(); } enableLocalRecording(localRecordingFilename); } } else { disableLocalRecording(); } } public void startVideoStream(Bundle videoProps, String appId, String newVideoTag, Surface videoSurface, final ICommandListener listener) { Timber.d("Video stream start request from %s. Video owner is %s.", appId, videoOwnerId.get()); if (!isAppIdValid(appId, listener)) { return; } final int udpPort = videoProps.getInt(CameraActions.EXTRA_VIDEO_PROPS_UDP_PORT, -1); if (videoSurface == null || udpPort == -1){ postErrorEvent(CommandExecutionError.COMMAND_FAILED, listener); return; } if (newVideoTag == null) newVideoTag = ""; if (appId.equals(videoOwnerId.get())) { String currentVideoTag = videoTagRef.get(); if (currentVideoTag == null) currentVideoTag = ""; if (newVideoTag.equals(currentVideoTag)) { // Check if the local recording state needs to be updated. checkForLocalRecording(appId, videoProps); postSuccessEvent(listener); return; } } if (videoOwnerId.compareAndSet(NO_VIDEO_OWNER, appId)) { videoTagRef.set(newVideoTag); checkForLocalRecording(appId, videoProps); Timber.i("Starting video decoding."); startDecoding(udpPort, videoSurface, new DecoderListener() { @Override public void onDecodingStarted() { Timber.i("Video decoding started."); postSuccessEvent(listener); } @Override public void onDecodingError() { Timber.i("Video decoding failed."); postErrorEvent(CommandExecutionError.COMMAND_FAILED, listener); reset(); } @Override public void onDecodingEnded() { Timber.i("Video decoding ended successfully."); reset(); } }); } else { postErrorEvent(CommandExecutionError.COMMAND_DENIED, listener); } } public void startVideoStreamForObserver(String appId, String newVideoTag, final ICommandListener listener) { Timber.d("Video stream start request from %s. Video owner is %s.", appId, videoOwnerId.get()); if (!isAppIdValid(appId, listener)) { return; } if (newVideoTag == null) newVideoTag = ""; if (appId.equals(videoOwnerId.get())) { String currentVideoTag = videoTagRef.get(); if (currentVideoTag == null) currentVideoTag = ""; if (newVideoTag.equals(currentVideoTag)){ postSuccessEvent(listener); return; } } if (videoOwnerId.compareAndSet(NO_VIDEO_OWNER, appId)) { videoTagRef.set(newVideoTag); Timber.i("Successful lock obtained for app with id %s.", appId); videoStreamObserverUsed.set(true); postSuccessEvent(listener); } else { postErrorEvent(CommandExecutionError.COMMAND_DENIED, listener); } } public void stopVideoStream(String appId, String currentVideoTag, final ICommandListener listener) { Timber.d("Video stream stop request from %s. Video owner is %s.", appId, videoOwnerId.get()); if (!isAppIdValid(appId, listener)) { return; } final String currentVideoOwner = videoOwnerId.get(); if (NO_VIDEO_OWNER.equals(currentVideoOwner)) { Timber.d("No video owner set. Nothing to do."); disableLocalRecording(); postSuccessEvent(listener); return; } if (currentVideoTag == null) currentVideoTag = ""; if (appId.equals(currentVideoOwner) && currentVideoTag.equals(videoTagRef.get()) && videoOwnerId.compareAndSet(currentVideoOwner, NO_VIDEO_OWNER)){ videoTagRef.set(""); disableLocalRecording(); Timber.d("Stopping video decoding. Current owner is %s.", currentVideoOwner); Timber.i("Stopping video decoding."); stopDecoding(new DecoderListener() { @Override public void onDecodingStarted() { } @Override public void onDecodingError() { postSuccessEvent(listener); } @Override public void onDecodingEnded() { postSuccessEvent(listener); } }); } else { postErrorEvent(CommandExecutionError.COMMAND_DENIED, listener); } } public void stopVideoStreamForObserver(String appId, String currentVideoTag, final ICommandListener listener) { Timber.d("Video stream stop request from %s. Video owner is %s.", appId, videoOwnerId.get()); if (!isAppIdValid(appId, listener)) { return; } final String currentVideoOwner = videoOwnerId.get(); if (NO_VIDEO_OWNER.equals(currentVideoOwner)) { Timber.d("No video owner set. Nothing to do."); postSuccessEvent(listener); return; } if (currentVideoTag == null) currentVideoTag = ""; if (appId.equals(currentVideoOwner) && currentVideoTag.equals(videoTagRef.get()) && videoOwnerId.compareAndSet(currentVideoOwner, NO_VIDEO_OWNER)){ videoTagRef.set(""); Timber.d("Stopping video decoding. Current owner is %s.", currentVideoOwner); Timber.i("Stop using video observer..."); videoStreamObserverUsed.set(false); postSuccessEvent(listener); } else { postErrorEvent(CommandExecutionError.COMMAND_DENIED, listener); } } public void tryStoppingVideoStream(String parentId) { if (TextUtils.isEmpty(parentId)) return; final String videoOwner = videoOwnerId.get(); if (NO_VIDEO_OWNER.equals(videoOwner)) return; if (videoOwner.equals(parentId)){ Timber.d("Stopping video owned by %s", parentId); if(videoStreamObserverUsed.get()){ stopVideoStreamForObserver(parentId, videoTagRef.get(), null); } else { stopVideoStream(parentId, videoTagRef.get(), null); } } } private boolean isAppIdValid(String appId, ICommandListener listener) { if (TextUtils.isEmpty(appId)) { Timber.w("Owner id is empty."); postErrorEvent(CommandExecutionError.COMMAND_DENIED, listener); return false; } return true; } }