diff --git a/app/src/main/java/org/communiquons/android/comunic/client/data/utils/StringsUtils.java b/app/src/main/java/org/communiquons/android/comunic/client/data/utils/StringsUtils.java index 86a0749..447de22 100644 --- a/app/src/main/java/org/communiquons/android/comunic/client/data/utils/StringsUtils.java +++ b/app/src/main/java/org/communiquons/android/comunic/client/data/utils/StringsUtils.java @@ -57,7 +57,7 @@ public class StringsUtils { } /** - * Format timestamp to string + * Format timestamp to string (date only) * * @param time The time to format * @return Generated string @@ -68,6 +68,18 @@ public class StringsUtils { return simpleDateFormat.format((long)1000*time); } + /** + * Format timestamp to string (date + time) + * + * @param time The time to format + * @return Generated string + */ + public static String FormatDateTime(int time){ + SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", + Locale.getDefault()); + return simpleDateFormat.format((long)1000*time); + } + /** * Convert an integer into a string, making sure that the generated string respects an minimum * size diff --git a/app/src/main/java/org/communiquons/android/comunic/client/ui/Constants.java b/app/src/main/java/org/communiquons/android/comunic/client/ui/Constants.java index 81df000..e28ad97 100644 --- a/app/src/main/java/org/communiquons/android/comunic/client/ui/Constants.java +++ b/app/src/main/java/org/communiquons/android/comunic/client/ui/Constants.java @@ -137,4 +137,21 @@ public final class Constants { public static final String PREFERENCE_ACCELERATE_NOTIFICATIONS_REFRESH = "accelerate_notifications_refresh"; } + + + /** + * External storage directory + */ + public final class EXTERNAL_STORAGE { + + /** + * Main storage directory + */ + public static final String MAIN_DIRECTORY_NAME = "Comunic"; + + /** + * Video calls directory + */ + public static final String VIDEO_CALLS_STORAGE_DIRECTORY = "VideoCalls"; + } } diff --git a/app/src/main/java/org/communiquons/android/comunic/client/ui/activities/CallActivity.java b/app/src/main/java/org/communiquons/android/comunic/client/ui/activities/CallActivity.java index 691c60e..b8b91f2 100644 --- a/app/src/main/java/org/communiquons/android/comunic/client/ui/activities/CallActivity.java +++ b/app/src/main/java/org/communiquons/android/comunic/client/ui/activities/CallActivity.java @@ -5,10 +5,12 @@ import android.os.AsyncTask; import android.os.Bundle; import android.support.annotation.Nullable; import android.util.Log; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.LinearLayout; +import android.widget.PopupMenu; import android.widget.ProgressBar; import android.widget.Toast; @@ -22,12 +24,14 @@ import org.communiquons.android.comunic.client.data.models.CallMember; import org.communiquons.android.comunic.client.data.models.CallResponse; import org.communiquons.android.comunic.client.data.models.CallsConfiguration; import org.communiquons.android.comunic.client.data.utils.AccountUtils; +import org.communiquons.android.comunic.client.data.utils.StringsUtils; import org.communiquons.android.comunic.client.ui.arrays.CallPeersConnectionsList; import org.communiquons.android.comunic.client.ui.asynctasks.GetCallInformationTask; import org.communiquons.android.comunic.client.ui.asynctasks.HangUpCallTask; import org.communiquons.android.comunic.client.ui.asynctasks.RespondToCallTask; import org.communiquons.android.comunic.client.ui.models.CallPeerConnection; import org.communiquons.android.comunic.client.ui.receivers.PendingCallsBroadcastReceiver; +import org.communiquons.android.comunic.client.ui.utils.FilesUtils; import org.communiquons.android.comunic.client.ui.utils.PermissionsUtils; import org.communiquons.android.comunic.client.ui.utils.UiUtils; import org.communiquons.signalexchangerclient.SignalExchangerCallback; @@ -43,11 +47,16 @@ import org.webrtc.SessionDescription; import org.webrtc.StatsReport; import org.webrtc.SurfaceViewRenderer; import org.webrtc.VideoCapturer; +import org.webrtc.VideoFileRenderer; import org.webrtc.VideoFrame; import org.webrtc.VideoSink; +import java.io.File; +import java.io.IOException; import java.util.Objects; +import static org.communiquons.android.comunic.client.data.utils.TimeUtils.time; +import static org.communiquons.android.comunic.client.ui.Constants.EXTERNAL_STORAGE.VIDEO_CALLS_STORAGE_DIRECTORY; import static org.webrtc.RendererCommon.ScalingType.SCALE_ASPECT_FILL; /** @@ -96,6 +105,13 @@ public class CallActivity extends BaseActivity implements SignalExchangerCallbac private boolean mIsCameraStopped = false; private boolean mIsMicrophoneStopped = false; + + /** + * Specify whether we are recording video or not + */ + private boolean mIsRecordingVideo = false; + private VideoFileRenderer mVideoFileRenderer; + /** * Connections list */ @@ -119,6 +135,7 @@ public class CallActivity extends BaseActivity implements SignalExchangerCallbac private View mButtonsView; private ImageButton mStopCameraButton; private ImageButton mStopMicrophoneButton; + private ImageButton mMoreActionsButton; @Override @@ -203,6 +220,9 @@ public class CallActivity extends BaseActivity implements SignalExchangerCallbac mStopMicrophoneButton = findViewById(R.id.stopMicrophoneButton); mStopMicrophoneButton.setOnClickListener(v -> toggleStopMicrophone()); + + mMoreActionsButton = findViewById(R.id.moreActionsButton); + mMoreActionsButton.setOnClickListener(v -> showMoreActions()); } @@ -337,6 +357,7 @@ public class CallActivity extends BaseActivity implements SignalExchangerCallbac mList.add(callPeer); EglBase eglBase = EglBase.create(); + callPeer.setEglRenderer(eglBase); //Create peer connection PeerConnectionClient peerConnectionClient = new PeerConnectionClient( @@ -403,6 +424,12 @@ public class CallActivity extends BaseActivity implements SignalExchangerCallbac callPeer.getRemoteSinks().add(remoteProxyRenderer); callPeer.setRemoteProxyRenderer(remoteProxyRenderer); + + ProxyVideoSink recordProxyVideoSink = new ProxyVideoSink(); + callPeer.getRemoteSinks().add(recordProxyVideoSink); + callPeer.setRecordProxyRenderer(recordProxyVideoSink); + + //Start connection peerConnectionClient.createPeerConnection( mLocalProxyVideoSink, @@ -423,6 +450,9 @@ public class CallActivity extends BaseActivity implements SignalExchangerCallbac mHangUpButton.setVisibility(View.GONE); mStopped = true; + //Stop recording video + stopVideoRecord(); + if(mRefreshCallInformation != null) mRefreshCallInformation.interrupt(); @@ -449,6 +479,11 @@ public class CallActivity extends BaseActivity implements SignalExchangerCallbac if(callPeer == null) return; + //Check if it is the first peer, if yes stop recording + if(mList.get(0) == callPeer) + stopVideoRecord(); + + ((ProxyVideoSink)callPeer.getRemoteProxyRenderer()).setTarget(null); callPeer.getPeerConnectionClient().close(); @@ -500,6 +535,87 @@ public class CallActivity extends BaseActivity implements SignalExchangerCallbac mButtonsView.setVisibility(show ? View.VISIBLE : View.GONE); } + private void showMoreActions(){ + + PopupMenu popupMenu = new PopupMenu(this, mMoreActionsButton); + popupMenu.inflate(R.menu.menu_call_more_actions); + + popupMenu.getMenu().findItem( + mIsRecordingVideo ? R.id.action_record_video : R.id.action_stop_record_video + ).setVisible(false); + + popupMenu.setOnMenuItemClickListener(this::onChooseAction); + popupMenu.show(); + } + + private boolean onChooseAction(MenuItem item){ + + if(item.getItemId() == R.id.action_record_video){ + startVideoRecord(); + return true; + } + + if(item.getItemId() == R.id.action_stop_record_video){ + stopVideoRecord(); + return true; + } + + return false; + } + + /** + * Start to record video + * + * Warning ! There is currently a technical limitation : only the first connection will + * be recorded ! And as soon as a connection is closed, the record is stopped + */ + private void startVideoRecord(){ + + if(mList.size() == 0){ + Toast.makeText(this, R.string.err_call_no_peer_connected, Toast.LENGTH_SHORT).show(); + return; + } + + CallPeerConnection callPeer = mList.get(0); + + //Create target file + String filename = StringsUtils.FormatDateTime(time()).replace(":", "-") + ".mp4"; + File file = FilesUtils.GetExternalStorageFile(VIDEO_CALLS_STORAGE_DIRECTORY, filename); + + if(file == null || file.exists()){ + Toast.makeText(this, R.string.err_can_not_create_record_file, Toast.LENGTH_SHORT).show(); + return; + } + + //Create video file renderer + try { + mVideoFileRenderer = new VideoFileRenderer(file.getAbsolutePath(), + 640, 480, + callPeer.getEglRenderer().getEglBaseContext()); + + + ((ProxyVideoSink)callPeer.getRecordProxyRenderer()). + setTarget(mVideoFileRenderer); + + } catch (IOException e) { + e.printStackTrace(); + Toast.makeText(this, R.string.err_initialize_video_call_recording, Toast.LENGTH_SHORT).show(); + } + + mIsRecordingVideo = true; + } + + private void stopVideoRecord(){ + + if(!mIsRecordingVideo) + return; + + ProxyVideoSink recordVideoSink = + (ProxyVideoSink) mList.get(0).getRecordProxyRenderer(); + mIsRecordingVideo = false; + recordVideoSink.setTarget(null); + mVideoFileRenderer.release(); + } //Based on https://github.com/vivek1794/webrtc-android-codelab @Nullable diff --git a/app/src/main/java/org/communiquons/android/comunic/client/ui/models/CallPeerConnection.java b/app/src/main/java/org/communiquons/android/comunic/client/ui/models/CallPeerConnection.java index 52c5334..25bb2f3 100644 --- a/app/src/main/java/org/communiquons/android/comunic/client/ui/models/CallPeerConnection.java +++ b/app/src/main/java/org/communiquons/android/comunic/client/ui/models/CallPeerConnection.java @@ -2,6 +2,7 @@ package org.communiquons.android.comunic.client.ui.models; import org.appspot.apprtc.PeerConnectionClient; import org.communiquons.android.comunic.client.data.models.CallMember; +import org.webrtc.EglBase; import org.webrtc.SurfaceViewRenderer; import org.webrtc.VideoSink; @@ -19,7 +20,9 @@ public class CallPeerConnection { private PeerConnectionClient peerConnectionClient; private boolean connected = false; private VideoSink remoteProxyRenderer; + private VideoSink recordProxyRenderer; private ArrayList remoteSinks = new ArrayList<>(); + private EglBase eglRenderer; //Views private SurfaceViewRenderer mRemoteViewView; @@ -75,4 +78,20 @@ public class CallPeerConnection { public void setRemoteProxyRenderer(VideoSink remoteProxyRenderer) { this.remoteProxyRenderer = remoteProxyRenderer; } + + public EglBase getEglRenderer() { + return eglRenderer; + } + + public void setEglRenderer(EglBase eglRenderer) { + this.eglRenderer = eglRenderer; + } + + public VideoSink getRecordProxyRenderer() { + return recordProxyRenderer; + } + + public void setRecordProxyRenderer(VideoSink recordProxyRenderer) { + this.recordProxyRenderer = recordProxyRenderer; + } } diff --git a/app/src/main/java/org/communiquons/android/comunic/client/ui/utils/FilesUtils.java b/app/src/main/java/org/communiquons/android/comunic/client/ui/utils/FilesUtils.java new file mode 100644 index 0000000..3a57e1d --- /dev/null +++ b/app/src/main/java/org/communiquons/android/comunic/client/ui/utils/FilesUtils.java @@ -0,0 +1,44 @@ +package org.communiquons.android.comunic.client.ui.utils; + +import android.os.Environment; +import android.support.annotation.Nullable; + +import java.io.File; + +import static org.communiquons.android.comunic.client.ui.Constants.EXTERNAL_STORAGE.MAIN_DIRECTORY_NAME; + +/** + * Files utilities + * + * @author Pierre HUBERT + */ +public class FilesUtils { + + /** + * Get a {@link File} object for a file present outside of the application directories + * + * @param subdirectory Comunic storage subdirectory + * @param filename The name of the target file + * @return File pointer / null in case of failure + */ + @Nullable + public static File GetExternalStorageFile(String subdirectory, String filename){ + + try { + File container = new File( + Environment.getExternalStorageDirectory() + "/" + MAIN_DIRECTORY_NAME, + subdirectory); + + if (!container.exists()) + if (!container.mkdirs()) + return null; + + return new File(container, filename); + + } catch (Exception e){ + e.printStackTrace(); + return null; + } + } + +} diff --git a/app/src/main/res/layout/activity_call.xml b/app/src/main/res/layout/activity_call.xml index c711ff9..7741900 100644 --- a/app/src/main/res/layout/activity_call.xml +++ b/app/src/main/res/layout/activity_call.xml @@ -101,6 +101,18 @@ android:src="@drawable/ic_mic" android:tint="@android:color/white" /> + + diff --git a/app/src/main/res/menu/menu_call_more_actions.xml b/app/src/main/res/menu/menu_call_more_actions.xml new file mode 100644 index 0000000..5df35a9 --- /dev/null +++ b/app/src/main/res/menu/menu_call_more_actions.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 1ecdeba..e327413 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -339,4 +339,9 @@ Une erreur a survenue lors de l\'enregistrement de l\'image dans la gallerie ! L\'image a bien été enregistrée dans la gallerie ! Configuration des appels vidéos manquante ! + Enregistrer la vidéo + Arrêter l\'enregistrement vidéo + Cet appel n\'est relié à aucun pair ! + Impossible de créer le fichier d\'enregistrement ! + Une erreur a survenue lors de l\'initialisation de l\'enregistrement vidéo ! \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5c4a91d..c20c141 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -338,4 +338,9 @@ Could not save the image in the gallery! Successfully saved the image into the gallery! Missing call configuration! + Record video + Stop video record + This call is not connected to any peer! + Can not create record file! + Could not initialize video call recording!