Can record video of video calls

This commit is contained in:
Pierre HUBERT 2019-03-01 16:36:20 +01:00
parent 7381091c08
commit 8f87b05fcc
9 changed files with 243 additions and 1 deletions

View File

@ -57,7 +57,7 @@ public class StringsUtils {
} }
/** /**
* Format timestamp to string * Format timestamp to string (date only)
* *
* @param time The time to format * @param time The time to format
* @return Generated string * @return Generated string
@ -68,6 +68,18 @@ public class StringsUtils {
return simpleDateFormat.format((long)1000*time); 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 * Convert an integer into a string, making sure that the generated string respects an minimum
* size * size

View File

@ -137,4 +137,21 @@ public final class Constants {
public static final String PREFERENCE_ACCELERATE_NOTIFICATIONS_REFRESH public static final String PREFERENCE_ACCELERATE_NOTIFICATIONS_REFRESH
= "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";
}
} }

View File

@ -5,10 +5,12 @@ import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.util.Log; import android.util.Log;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.PopupMenu;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.Toast; 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.CallResponse;
import org.communiquons.android.comunic.client.data.models.CallsConfiguration; 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.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.arrays.CallPeersConnectionsList;
import org.communiquons.android.comunic.client.ui.asynctasks.GetCallInformationTask; 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.HangUpCallTask;
import org.communiquons.android.comunic.client.ui.asynctasks.RespondToCallTask; 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.models.CallPeerConnection;
import org.communiquons.android.comunic.client.ui.receivers.PendingCallsBroadcastReceiver; 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.PermissionsUtils;
import org.communiquons.android.comunic.client.ui.utils.UiUtils; import org.communiquons.android.comunic.client.ui.utils.UiUtils;
import org.communiquons.signalexchangerclient.SignalExchangerCallback; import org.communiquons.signalexchangerclient.SignalExchangerCallback;
@ -43,11 +47,16 @@ import org.webrtc.SessionDescription;
import org.webrtc.StatsReport; import org.webrtc.StatsReport;
import org.webrtc.SurfaceViewRenderer; import org.webrtc.SurfaceViewRenderer;
import org.webrtc.VideoCapturer; import org.webrtc.VideoCapturer;
import org.webrtc.VideoFileRenderer;
import org.webrtc.VideoFrame; import org.webrtc.VideoFrame;
import org.webrtc.VideoSink; import org.webrtc.VideoSink;
import java.io.File;
import java.io.IOException;
import java.util.Objects; 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; 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 mIsCameraStopped = false;
private boolean mIsMicrophoneStopped = false; private boolean mIsMicrophoneStopped = false;
/**
* Specify whether we are recording video or not
*/
private boolean mIsRecordingVideo = false;
private VideoFileRenderer mVideoFileRenderer;
/** /**
* Connections list * Connections list
*/ */
@ -119,6 +135,7 @@ public class CallActivity extends BaseActivity implements SignalExchangerCallbac
private View mButtonsView; private View mButtonsView;
private ImageButton mStopCameraButton; private ImageButton mStopCameraButton;
private ImageButton mStopMicrophoneButton; private ImageButton mStopMicrophoneButton;
private ImageButton mMoreActionsButton;
@Override @Override
@ -203,6 +220,9 @@ public class CallActivity extends BaseActivity implements SignalExchangerCallbac
mStopMicrophoneButton = findViewById(R.id.stopMicrophoneButton); mStopMicrophoneButton = findViewById(R.id.stopMicrophoneButton);
mStopMicrophoneButton.setOnClickListener(v -> toggleStopMicrophone()); 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); mList.add(callPeer);
EglBase eglBase = EglBase.create(); EglBase eglBase = EglBase.create();
callPeer.setEglRenderer(eglBase);
//Create peer connection //Create peer connection
PeerConnectionClient peerConnectionClient = new PeerConnectionClient( PeerConnectionClient peerConnectionClient = new PeerConnectionClient(
@ -403,6 +424,12 @@ public class CallActivity extends BaseActivity implements SignalExchangerCallbac
callPeer.getRemoteSinks().add(remoteProxyRenderer); callPeer.getRemoteSinks().add(remoteProxyRenderer);
callPeer.setRemoteProxyRenderer(remoteProxyRenderer); callPeer.setRemoteProxyRenderer(remoteProxyRenderer);
ProxyVideoSink recordProxyVideoSink = new ProxyVideoSink();
callPeer.getRemoteSinks().add(recordProxyVideoSink);
callPeer.setRecordProxyRenderer(recordProxyVideoSink);
//Start connection //Start connection
peerConnectionClient.createPeerConnection( peerConnectionClient.createPeerConnection(
mLocalProxyVideoSink, mLocalProxyVideoSink,
@ -423,6 +450,9 @@ public class CallActivity extends BaseActivity implements SignalExchangerCallbac
mHangUpButton.setVisibility(View.GONE); mHangUpButton.setVisibility(View.GONE);
mStopped = true; mStopped = true;
//Stop recording video
stopVideoRecord();
if(mRefreshCallInformation != null) if(mRefreshCallInformation != null)
mRefreshCallInformation.interrupt(); mRefreshCallInformation.interrupt();
@ -449,6 +479,11 @@ public class CallActivity extends BaseActivity implements SignalExchangerCallbac
if(callPeer == null) if(callPeer == null)
return; return;
//Check if it is the first peer, if yes stop recording
if(mList.get(0) == callPeer)
stopVideoRecord();
((ProxyVideoSink)callPeer.getRemoteProxyRenderer()).setTarget(null); ((ProxyVideoSink)callPeer.getRemoteProxyRenderer()).setTarget(null);
callPeer.getPeerConnectionClient().close(); callPeer.getPeerConnectionClient().close();
@ -500,6 +535,87 @@ public class CallActivity extends BaseActivity implements SignalExchangerCallbac
mButtonsView.setVisibility(show ? View.VISIBLE : View.GONE); 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 //Based on https://github.com/vivek1794/webrtc-android-codelab
@Nullable @Nullable

View File

@ -2,6 +2,7 @@ package org.communiquons.android.comunic.client.ui.models;
import org.appspot.apprtc.PeerConnectionClient; import org.appspot.apprtc.PeerConnectionClient;
import org.communiquons.android.comunic.client.data.models.CallMember; import org.communiquons.android.comunic.client.data.models.CallMember;
import org.webrtc.EglBase;
import org.webrtc.SurfaceViewRenderer; import org.webrtc.SurfaceViewRenderer;
import org.webrtc.VideoSink; import org.webrtc.VideoSink;
@ -19,7 +20,9 @@ public class CallPeerConnection {
private PeerConnectionClient peerConnectionClient; private PeerConnectionClient peerConnectionClient;
private boolean connected = false; private boolean connected = false;
private VideoSink remoteProxyRenderer; private VideoSink remoteProxyRenderer;
private VideoSink recordProxyRenderer;
private ArrayList<VideoSink> remoteSinks = new ArrayList<>(); private ArrayList<VideoSink> remoteSinks = new ArrayList<>();
private EglBase eglRenderer;
//Views //Views
private SurfaceViewRenderer mRemoteViewView; private SurfaceViewRenderer mRemoteViewView;
@ -75,4 +78,20 @@ public class CallPeerConnection {
public void setRemoteProxyRenderer(VideoSink remoteProxyRenderer) { public void setRemoteProxyRenderer(VideoSink remoteProxyRenderer) {
this.remoteProxyRenderer = 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;
}
} }

View File

@ -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;
}
}
}

View File

@ -101,6 +101,18 @@
android:src="@drawable/ic_mic" android:src="@drawable/ic_mic"
android:tint="@android:color/white" /> android:tint="@android:color/white" />
<ImageButton
android:id="@+id/moreActionsButton"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:contentDescription="@string/action_more"
android:src="@drawable/ic_more"
android:tint="@android:color/white" />
</LinearLayout> </LinearLayout>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/action_record_video"
android:title="@string/action_record_video"/>
<item
android:id="@+id/action_stop_record_video"
android:title="@string/action_stop_record_video"/>
</menu>

View File

@ -339,4 +339,9 @@
<string name="err_save_image_in_gallery">Une erreur a survenue lors de l\'enregistrement de l\'image dans la gallerie !</string> <string name="err_save_image_in_gallery">Une erreur a survenue lors de l\'enregistrement de l\'image dans la gallerie !</string>
<string name="success_save_image_in_gallery">L\'image a bien été enregistrée dans la gallerie !</string> <string name="success_save_image_in_gallery">L\'image a bien été enregistrée dans la gallerie !</string>
<string name="err_missing_call_config">Configuration des appels vidéos manquante !</string> <string name="err_missing_call_config">Configuration des appels vidéos manquante !</string>
<string name="action_record_video">Enregistrer la vidéo</string>
<string name="action_stop_record_video">Arrêter l\'enregistrement vidéo</string>
<string name="err_call_no_peer_connected">Cet appel n\'est relié à aucun pair !</string>
<string name="err_can_not_create_record_file">Impossible de créer le fichier d\'enregistrement !</string>
<string name="err_initialize_video_call_recording">Une erreur a survenue lors de l\'initialisation de l\'enregistrement vidéo !</string>
</resources> </resources>

View File

@ -338,4 +338,9 @@
<string name="err_save_image_in_gallery">Could not save the image in the gallery!</string> <string name="err_save_image_in_gallery">Could not save the image in the gallery!</string>
<string name="success_save_image_in_gallery">Successfully saved the image into the gallery!</string> <string name="success_save_image_in_gallery">Successfully saved the image into the gallery!</string>
<string name="err_missing_call_config">Missing call configuration!</string> <string name="err_missing_call_config">Missing call configuration!</string>
<string name="action_record_video">Record video</string>
<string name="action_stop_record_video">Stop video record</string>
<string name="err_call_no_peer_connected">This call is not connected to any peer!</string>
<string name="err_can_not_create_record_file">Can not create record file!</string>
<string name="err_initialize_video_call_recording">Could not initialize video call recording!</string>
</resources> </resources>