mirror of
				https://github.com/pierre42100/ComunicAndroid
				synced 2025-11-04 03:24:04 +00:00 
			
		
		
		
	Can record video of video calls
This commit is contained in:
		@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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";
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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<VideoSink> 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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -101,6 +101,18 @@
 | 
			
		||||
            android:src="@drawable/ic_mic"
 | 
			
		||||
            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>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										12
									
								
								app/src/main/res/menu/menu_call_more_actions.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								app/src/main/res/menu/menu_call_more_actions.xml
									
									
									
									
									
										Normal 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>
 | 
			
		||||
@@ -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="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="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>
 | 
			
		||||
@@ -338,4 +338,9 @@
 | 
			
		||||
    <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="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>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user