mirror of
https://github.com/pierre42100/ComunicAndroid
synced 2024-11-23 13:59:29 +00:00
First call on Android device
This commit is contained in:
parent
797b0ae09b
commit
f08f1940fc
@ -29,7 +29,7 @@
|
||||
</value>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
|
@ -3,6 +3,11 @@ apply plugin: 'com.android.application'
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility 1.8
|
||||
targetCompatibility 1.8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "org.communiquons.android.comunic.client"
|
||||
minSdkVersion 21
|
||||
@ -59,6 +64,8 @@ dependencies {
|
||||
implementation 'com.android.support:design:28.0.0-rc02'
|
||||
implementation 'com.android.support:preference-v7:28.0.0-rc02'
|
||||
implementation 'com.android.support:support-v4:28.0.0-rc02'
|
||||
implementation 'com.squareup.okhttp3:okhttp:3.12.1'
|
||||
implementation 'org.whispersystems:webrtc-android:M71'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
||||
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
|
||||
|
@ -7,6 +7,12 @@
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
|
||||
<!-- Video calls require camera and microphone -->
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="@xml/backup_scheme"
|
||||
|
137
app/src/main/java/org/appspot/apprtc/AppRTCClient.java
Normal file
137
app/src/main/java/org/appspot/apprtc/AppRTCClient.java
Normal file
@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright 2013 The WebRTC Project Authors. All rights reserved.
|
||||
*
|
||||
* Use of this source code is governed by a BSD-style license
|
||||
* that can be found in the LICENSE file in the root of the source
|
||||
* tree. An additional intellectual property rights grant can be found
|
||||
* in the file PATENTS. All contributing project authors may
|
||||
* be found in the AUTHORS file in the root of the source tree.
|
||||
*/
|
||||
|
||||
package org.appspot.apprtc;
|
||||
|
||||
import org.webrtc.IceCandidate;
|
||||
import org.webrtc.PeerConnection;
|
||||
import org.webrtc.SessionDescription;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* AppRTCClient is the interface representing an AppRTC client.
|
||||
*/
|
||||
public interface AppRTCClient {
|
||||
/**
|
||||
* Struct holding the connection parameters of an AppRTC room.
|
||||
*/
|
||||
class RoomConnectionParameters {
|
||||
public final String roomUrl;
|
||||
public final String roomId;
|
||||
public final boolean loopback;
|
||||
public final String urlParameters;
|
||||
public RoomConnectionParameters(
|
||||
String roomUrl, String roomId, boolean loopback, String urlParameters) {
|
||||
this.roomUrl = roomUrl;
|
||||
this.roomId = roomId;
|
||||
this.loopback = loopback;
|
||||
this.urlParameters = urlParameters;
|
||||
}
|
||||
public RoomConnectionParameters(String roomUrl, String roomId, boolean loopback) {
|
||||
this(roomUrl, roomId, loopback, null /* urlParameters */);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously connect to an AppRTC room URL using supplied connection
|
||||
* parameters. Once connection is established onConnectedToRoom()
|
||||
* callback with room parameters is invoked.
|
||||
*/
|
||||
void connectToRoom(RoomConnectionParameters connectionParameters);
|
||||
|
||||
/**
|
||||
* Send offer SDP to the other participant.
|
||||
*/
|
||||
void sendOfferSdp(final SessionDescription sdp);
|
||||
|
||||
/**
|
||||
* Send answer SDP to the other participant.
|
||||
*/
|
||||
void sendAnswerSdp(final SessionDescription sdp);
|
||||
|
||||
/**
|
||||
* Send Ice candidate to the other participant.
|
||||
*/
|
||||
void sendLocalIceCandidate(final IceCandidate candidate);
|
||||
|
||||
/**
|
||||
* Send removed ICE candidates to the other participant.
|
||||
*/
|
||||
void sendLocalIceCandidateRemovals(final IceCandidate[] candidates);
|
||||
|
||||
/**
|
||||
* Disconnect from room.
|
||||
*/
|
||||
void disconnectFromRoom();
|
||||
|
||||
/**
|
||||
* Struct holding the signaling parameters of an AppRTC room.
|
||||
*/
|
||||
class SignalingParameters {
|
||||
public final List<PeerConnection.IceServer> iceServers;
|
||||
public final boolean initiator;
|
||||
public final String clientId;
|
||||
public final String wssUrl;
|
||||
public final String wssPostUrl;
|
||||
public final SessionDescription offerSdp;
|
||||
public final List<IceCandidate> iceCandidates;
|
||||
|
||||
public SignalingParameters(List<PeerConnection.IceServer> iceServers, boolean initiator,
|
||||
String clientId, String wssUrl, String wssPostUrl, SessionDescription offerSdp,
|
||||
List<IceCandidate> iceCandidates) {
|
||||
this.iceServers = iceServers;
|
||||
this.initiator = initiator;
|
||||
this.clientId = clientId;
|
||||
this.wssUrl = wssUrl;
|
||||
this.wssPostUrl = wssPostUrl;
|
||||
this.offerSdp = offerSdp;
|
||||
this.iceCandidates = iceCandidates;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback interface for messages delivered on signaling channel.
|
||||
*
|
||||
* <p>Methods are guaranteed to be invoked on the UI thread of |activity|.
|
||||
*/
|
||||
interface SignalingEvents {
|
||||
/**
|
||||
* Callback fired once the room's signaling parameters
|
||||
* SignalingParameters are extracted.
|
||||
*/
|
||||
void onConnectedToRoom(final SignalingParameters params);
|
||||
|
||||
/**
|
||||
* Callback fired once remote SDP is received.
|
||||
*/
|
||||
void onRemoteDescription(final SessionDescription sdp);
|
||||
|
||||
/**
|
||||
* Callback fired once remote Ice candidate is received.
|
||||
*/
|
||||
void onRemoteIceCandidate(final IceCandidate candidate);
|
||||
|
||||
/**
|
||||
* Callback fired once remote Ice candidate removals are received.
|
||||
*/
|
||||
void onRemoteIceCandidatesRemoved(final IceCandidate[] candidates);
|
||||
|
||||
/**
|
||||
* Callback fired once channel is closed.
|
||||
*/
|
||||
void onChannelClose();
|
||||
|
||||
/**
|
||||
* Callback fired once channel error happened.
|
||||
*/
|
||||
void onChannelError(final String description);
|
||||
}
|
||||
}
|
1428
app/src/main/java/org/appspot/apprtc/PeerConnectionClient.java
Normal file
1428
app/src/main/java/org/appspot/apprtc/PeerConnectionClient.java
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,153 @@
|
||||
/*
|
||||
* Copyright 2018 The WebRTC project authors. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by a BSD-style license
|
||||
* that can be found in the LICENSE file in the root of the source
|
||||
* tree. An additional intellectual property rights grant can be found
|
||||
* in the file PATENTS. All contributing project authors may
|
||||
* be found in the AUTHORS file in the root of the source tree.
|
||||
*/
|
||||
|
||||
package org.appspot.apprtc;
|
||||
|
||||
import android.media.AudioFormat;
|
||||
import android.os.Environment;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import org.webrtc.audio.JavaAudioDeviceModule;
|
||||
import org.webrtc.audio.JavaAudioDeviceModule.SamplesReadyCallback;
|
||||
import org.webrtc.voiceengine.WebRtcAudioRecord;
|
||||
import org.webrtc.voiceengine.WebRtcAudioRecord.WebRtcAudioRecordSamplesReadyCallback;
|
||||
|
||||
/**
|
||||
* Implements the AudioRecordSamplesReadyCallback interface and writes
|
||||
* recorded raw audio samples to an output file.
|
||||
*/
|
||||
public class RecordedAudioToFileController
|
||||
implements SamplesReadyCallback, WebRtcAudioRecordSamplesReadyCallback {
|
||||
private static final String TAG = "RecordedAudioToFile";
|
||||
private static final long MAX_FILE_SIZE_IN_BYTES = 58348800L;
|
||||
|
||||
private final Object lock = new Object();
|
||||
private final ExecutorService executor;
|
||||
@Nullable private OutputStream rawAudioFileOutputStream;
|
||||
private boolean isRunning;
|
||||
private long fileSizeInBytes;
|
||||
|
||||
public RecordedAudioToFileController(ExecutorService executor) {
|
||||
Log.d(TAG, "ctor");
|
||||
this.executor = executor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called on the same executor thread as the one provided at
|
||||
* construction.
|
||||
*/
|
||||
public boolean start() {
|
||||
Log.d(TAG, "start");
|
||||
if (!isExternalStorageWritable()) {
|
||||
Log.e(TAG, "Writing to external media is not possible");
|
||||
return false;
|
||||
}
|
||||
synchronized (lock) {
|
||||
isRunning = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called on the same executor thread as the one provided at
|
||||
* construction.
|
||||
*/
|
||||
public void stop() {
|
||||
Log.d(TAG, "stop");
|
||||
synchronized (lock) {
|
||||
isRunning = false;
|
||||
if (rawAudioFileOutputStream != null) {
|
||||
try {
|
||||
rawAudioFileOutputStream.close();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to close file with saved input audio: " + e);
|
||||
}
|
||||
rawAudioFileOutputStream = null;
|
||||
}
|
||||
fileSizeInBytes = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Checks if external storage is available for read and write.
|
||||
private boolean isExternalStorageWritable() {
|
||||
String state = Environment.getExternalStorageState();
|
||||
if (Environment.MEDIA_MOUNTED.equals(state)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Utilizes audio parameters to create a file name which contains sufficient
|
||||
// information so that the file can be played using an external file player.
|
||||
// Example: /sdcard/recorded_audio_16bits_48000Hz_mono.pcm.
|
||||
private void openRawAudioOutputFile(int sampleRate, int channelCount) {
|
||||
final String fileName = Environment.getExternalStorageDirectory().getPath() + File.separator
|
||||
+ "recorded_audio_16bits_" + String.valueOf(sampleRate) + "Hz"
|
||||
+ ((channelCount == 1) ? "_mono" : "_stereo") + ".pcm";
|
||||
final File outputFile = new File(fileName);
|
||||
try {
|
||||
rawAudioFileOutputStream = new FileOutputStream(outputFile);
|
||||
} catch (FileNotFoundException e) {
|
||||
Log.e(TAG, "Failed to open audio output file: " + e.getMessage());
|
||||
}
|
||||
Log.d(TAG, "Opened file for recording: " + fileName);
|
||||
}
|
||||
|
||||
// Called when new audio samples are ready.
|
||||
@Override
|
||||
public void onWebRtcAudioRecordSamplesReady(WebRtcAudioRecord.AudioSamples samples) {
|
||||
onWebRtcAudioRecordSamplesReady(new JavaAudioDeviceModule.AudioSamples(samples.getAudioFormat(),
|
||||
samples.getChannelCount(), samples.getSampleRate(), samples.getData()));
|
||||
}
|
||||
|
||||
// Called when new audio samples are ready.
|
||||
@Override
|
||||
public void onWebRtcAudioRecordSamplesReady(JavaAudioDeviceModule.AudioSamples samples) {
|
||||
// The native audio layer on Android should use 16-bit PCM format.
|
||||
if (samples.getAudioFormat() != AudioFormat.ENCODING_PCM_16BIT) {
|
||||
Log.e(TAG, "Invalid audio format");
|
||||
return;
|
||||
}
|
||||
synchronized (lock) {
|
||||
// Abort early if stop() has been called.
|
||||
if (!isRunning) {
|
||||
return;
|
||||
}
|
||||
// Open a new file for the first callback only since it allows us to add audio parameters to
|
||||
// the file name.
|
||||
if (rawAudioFileOutputStream == null) {
|
||||
openRawAudioOutputFile(samples.getSampleRate(), samples.getChannelCount());
|
||||
fileSizeInBytes = 0;
|
||||
}
|
||||
}
|
||||
// Append the recorded 16-bit audio samples to the open output file.
|
||||
executor.execute(() -> {
|
||||
if (rawAudioFileOutputStream != null) {
|
||||
try {
|
||||
// Set a limit on max file size. 58348800 bytes corresponds to
|
||||
// approximately 10 minutes of recording in mono at 48kHz.
|
||||
if (fileSizeInBytes < MAX_FILE_SIZE_IN_BYTES) {
|
||||
// Writes samples.getData().length bytes to output stream.
|
||||
rawAudioFileOutputStream.write(samples.getData());
|
||||
fileSizeInBytes += samples.getData().length;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to write audio to file: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
74
app/src/main/java/org/appspot/apprtc/RtcEventLog.java
Normal file
74
app/src/main/java/org/appspot/apprtc/RtcEventLog.java
Normal file
@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2018 The WebRTC project authors. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by a BSD-style license
|
||||
* that can be found in the LICENSE file in the root of the source
|
||||
* tree. An additional intellectual property rights grant can be found
|
||||
* in the file PATENTS. All contributing project authors may
|
||||
* be found in the AUTHORS file in the root of the source tree.
|
||||
*/
|
||||
|
||||
package org.appspot.apprtc;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.util.Log;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import org.webrtc.PeerConnection;
|
||||
|
||||
public class RtcEventLog {
|
||||
private static final String TAG = "RtcEventLog";
|
||||
private static final int OUTPUT_FILE_MAX_BYTES = 10_000_000;
|
||||
private final PeerConnection peerConnection;
|
||||
private RtcEventLogState state = RtcEventLogState.INACTIVE;
|
||||
|
||||
enum RtcEventLogState {
|
||||
INACTIVE,
|
||||
STARTED,
|
||||
STOPPED,
|
||||
}
|
||||
|
||||
public RtcEventLog(PeerConnection peerConnection) {
|
||||
if (peerConnection == null) {
|
||||
throw new NullPointerException("The peer connection is null.");
|
||||
}
|
||||
this.peerConnection = peerConnection;
|
||||
}
|
||||
|
||||
public void start(final File outputFile) {
|
||||
if (state == RtcEventLogState.STARTED) {
|
||||
Log.e(TAG, "RtcEventLog has already started.");
|
||||
return;
|
||||
}
|
||||
final ParcelFileDescriptor fileDescriptor;
|
||||
try {
|
||||
fileDescriptor = ParcelFileDescriptor.open(outputFile,
|
||||
ParcelFileDescriptor.MODE_READ_WRITE | ParcelFileDescriptor.MODE_CREATE
|
||||
| ParcelFileDescriptor.MODE_TRUNCATE);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to create a new file", e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Passes ownership of the file to WebRTC.
|
||||
boolean success =
|
||||
peerConnection.startRtcEventLog(fileDescriptor.detachFd(), OUTPUT_FILE_MAX_BYTES);
|
||||
if (!success) {
|
||||
Log.e(TAG, "Failed to start RTC event log.");
|
||||
return;
|
||||
}
|
||||
state = RtcEventLogState.STARTED;
|
||||
Log.d(TAG, "RtcEventLog started.");
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (state != RtcEventLogState.STARTED) {
|
||||
Log.e(TAG, "RtcEventLog was not started.");
|
||||
return;
|
||||
}
|
||||
peerConnection.stopRtcEventLog();
|
||||
state = RtcEventLogState.STOPPED;
|
||||
Log.d(TAG, "RtcEventLog stopped.");
|
||||
}
|
||||
}
|
@ -15,6 +15,9 @@ import org.communiquons.android.comunic.client.data.models.NextPendingCallInform
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.webrtc.PeerConnection;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Calls helper
|
||||
@ -90,6 +93,30 @@ public class CallsHelper extends BaseHelper {
|
||||
return mCallsConfiguration != null && mCallsConfiguration.isEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of STUN and TURN servers
|
||||
*
|
||||
* @return List of STUN and TURN servers available for Comunic
|
||||
*/
|
||||
public static ArrayList<PeerConnection.IceServer> GetPeerServers(){
|
||||
|
||||
ArrayList<PeerConnection.IceServer> servers = new ArrayList<>();
|
||||
|
||||
//Stun server
|
||||
servers.add(PeerConnection.IceServer.builder(
|
||||
mCallsConfiguration.getStunServer()).createIceServer());
|
||||
|
||||
//TURN server
|
||||
servers.add(PeerConnection.IceServer
|
||||
.builder(mCallsConfiguration.getTurnServer())
|
||||
.setUsername(mCallsConfiguration.getTurnUsername())
|
||||
.setPassword(mCallsConfiguration.getTurnPassword())
|
||||
.createIceServer()
|
||||
);
|
||||
|
||||
return servers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a call for a conversation, returns information about this call then
|
||||
*
|
||||
@ -151,6 +178,26 @@ public class CallsHelper extends BaseHelper {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information about a call
|
||||
*
|
||||
* @param callID Target call ID
|
||||
* @return Information about the call / null in case of failure
|
||||
*/
|
||||
public CallInformation getInfo(int callID){
|
||||
|
||||
APIRequest request = new APIRequest(getContext(), "calls/getInfo");
|
||||
request.addInt("call_id", callID);
|
||||
|
||||
try {
|
||||
return JSONObjectToCallInformation(request.exec().getJSONObject(), null);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Try to get and return call information
|
||||
|
@ -1,6 +1,7 @@
|
||||
package org.communiquons.android.comunic.client.data.models;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
@ -75,6 +76,36 @@ public class CallInformation {
|
||||
this.members = members;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a member by user ID
|
||||
*
|
||||
* @param userID The ID of the user to search
|
||||
* @return Information about the target user
|
||||
*/
|
||||
public CallMember findMember(int userID){
|
||||
for(CallMember member : members)
|
||||
if(member.getUserID() == userID)
|
||||
return member;
|
||||
|
||||
throw new RuntimeException("Specified user was not found in the conversation!");
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a member by call ID
|
||||
*
|
||||
* @param userCallID The ID of the target user
|
||||
* @return Information about the user / null in case of failure
|
||||
*/
|
||||
@Nullable
|
||||
public CallMember findMember(String userCallID){
|
||||
for(CallMember member : members)
|
||||
if(member.getUserCallID().equals(userCallID))
|
||||
return member;
|
||||
|
||||
throw new RuntimeException("Specified user was not found in the conversation!");
|
||||
}
|
||||
|
||||
|
||||
public String getCallName() {
|
||||
return callName;
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ public class CallsConfiguration {
|
||||
this.signalServerPort = signalServerPort;
|
||||
}
|
||||
|
||||
public boolean isSignalSererSecure() {
|
||||
public boolean isSignalServerSecure() {
|
||||
return isSignalSererSecure;
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,46 @@
|
||||
package org.communiquons.android.comunic.client.ui.activities;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.widget.TextView;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.util.Log;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.appspot.apprtc.AppRTCClient;
|
||||
import org.appspot.apprtc.PeerConnectionClient;
|
||||
import org.communiquons.android.comunic.client.R;
|
||||
import org.communiquons.android.comunic.client.data.enums.MemberCallStatus;
|
||||
import org.communiquons.android.comunic.client.data.helpers.CallsHelper;
|
||||
import org.communiquons.android.comunic.client.data.models.CallInformation;
|
||||
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.ui.arrays.CallPeersConnectionsList;
|
||||
import org.communiquons.android.comunic.client.ui.asynctasks.GetCallInformationTask;
|
||||
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.signalexchangerclient.SignalExchangerCallback;
|
||||
import org.communiquons.signalexchangerclient.SignalExchangerClient;
|
||||
import org.communiquons.signalexchangerclient.SignalExchangerInitConfig;
|
||||
import org.webrtc.Camera1Enumerator;
|
||||
import org.webrtc.CameraEnumerator;
|
||||
import org.webrtc.EglBase;
|
||||
import org.webrtc.IceCandidate;
|
||||
import org.webrtc.Logging;
|
||||
import org.webrtc.PeerConnectionFactory;
|
||||
import org.webrtc.SessionDescription;
|
||||
import org.webrtc.StatsReport;
|
||||
import org.webrtc.SurfaceViewRenderer;
|
||||
import org.webrtc.VideoCapturer;
|
||||
import org.webrtc.VideoFrame;
|
||||
import org.webrtc.VideoSink;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@ -13,21 +49,89 @@ import java.util.Objects;
|
||||
*
|
||||
* @author Pierre HUBERT
|
||||
*/
|
||||
public class CallActivity extends BaseActivity {
|
||||
public class CallActivity extends BaseActivity implements SignalExchangerCallback {
|
||||
|
||||
/**
|
||||
* Debug tag
|
||||
*/
|
||||
private static final String TAG = CallActivity.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* Mandatory argument that includes call id
|
||||
*/
|
||||
public static final String ARGUMENT_CALL_ID = "call_id";
|
||||
|
||||
|
||||
/**
|
||||
* Permissions requests codes
|
||||
*/
|
||||
private static final int MY_PERMISSIONS_REQUEST_CAMERA = 100;
|
||||
private static final int MY_PERMISSIONS_REQUEST_RECORD_AUDIO = 101;
|
||||
private static final int MY_PERMISSIONS_REQUEST = 102;
|
||||
|
||||
/**
|
||||
* Refresh call information thread
|
||||
*/
|
||||
private RefreshCallInformation mRefreshCallInformation = null;
|
||||
|
||||
/**
|
||||
* Current call ID and information
|
||||
*/
|
||||
private int mCallID = -1;
|
||||
private CallInformation mCallInformation = null;
|
||||
|
||||
/**
|
||||
* Signal exchanger client
|
||||
*/
|
||||
private SignalExchangerClient mSignalExchangerClient = null;
|
||||
|
||||
|
||||
/**
|
||||
* Connections list
|
||||
*/
|
||||
private CallPeersConnectionsList mList = new CallPeersConnectionsList();
|
||||
|
||||
|
||||
/**
|
||||
* WebRTC attributes
|
||||
*/
|
||||
private EglBase rootEglBase;
|
||||
|
||||
|
||||
/**
|
||||
* Views
|
||||
*/
|
||||
private ProgressBar mProgressBar;
|
||||
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_call);
|
||||
|
||||
//Hide call bar
|
||||
Objects.requireNonNull(getSupportActionBar()).hide();
|
||||
mCallID = getIntent().getIntExtra(ARGUMENT_CALL_ID, 0);
|
||||
|
||||
//Get views
|
||||
initViews();
|
||||
initVideos();
|
||||
|
||||
//Mark the call as accepted
|
||||
RespondToCallTask respondToCallTask = new RespondToCallTask(this);
|
||||
respondToCallTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
|
||||
new CallResponse(mCallID, true));
|
||||
getTasksManager().addTask(respondToCallTask);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
|
||||
//Refresh at a regular interval information about the call
|
||||
mRefreshCallInformation = new RefreshCallInformation();
|
||||
mRefreshCallInformation.start();
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -37,7 +141,507 @@ public class CallActivity extends BaseActivity {
|
||||
//Hide call notifications
|
||||
PendingCallsBroadcastReceiver.RemoveCallNotification(this);
|
||||
|
||||
((TextView)findViewById(R.id.call_id)).setText(
|
||||
"Call " + getIntent().getExtras().getInt(ARGUMENT_CALL_ID));
|
||||
//Make sure we have access to user camera and microphone
|
||||
askForPermissions();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
mRefreshCallInformation.interrupt();
|
||||
}
|
||||
|
||||
/**
|
||||
* Request access to user camera and microphone devices
|
||||
*
|
||||
* Based on https://github.com/sergiopaniego/WebRTCAndroidExample
|
||||
*/
|
||||
private void askForPermissions() {
|
||||
if ((ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
|
||||
!= PackageManager.PERMISSION_GRANTED) &&
|
||||
(ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
|
||||
!= PackageManager.PERMISSION_GRANTED)) {
|
||||
ActivityCompat.requestPermissions(this,
|
||||
new String[]{Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO},
|
||||
MY_PERMISSIONS_REQUEST);
|
||||
} else if (ContextCompat.checkSelfPermission(this,
|
||||
Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(this,
|
||||
new String[]{Manifest.permission.RECORD_AUDIO},
|
||||
MY_PERMISSIONS_REQUEST_RECORD_AUDIO);
|
||||
|
||||
} else if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(this,
|
||||
new String[]{Manifest.permission.CAMERA},
|
||||
MY_PERMISSIONS_REQUEST_CAMERA);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get views
|
||||
*/
|
||||
private void initViews(){
|
||||
|
||||
mProgressBar = findViewById(R.id.progressBar);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
private void initVideos(){
|
||||
|
||||
rootEglBase = EglBase.create();
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Refresh call information
|
||||
*/
|
||||
private void getCallInformation(){
|
||||
|
||||
GetCallInformationTask getCallInformationTask = new GetCallInformationTask(this);
|
||||
getCallInformationTask.setOnPostExecuteListener(this::onGotCallInformation);
|
||||
getCallInformationTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, mCallID);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Once we have got information about the call
|
||||
*
|
||||
* @param info Information about the call
|
||||
*/
|
||||
private void onGotCallInformation(@Nullable CallInformation info){
|
||||
|
||||
if(info == null){
|
||||
Toast.makeText(this, R.string.err_get_call_info, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
setTitle(info.getCallName());
|
||||
mCallInformation = info;
|
||||
|
||||
//Check if everyone left the conversation
|
||||
if(mCallInformation.hasAllMembersLeftCallExcept(AccountUtils.getID(this))){
|
||||
Toast.makeText(this, R.string.notice_call_terminated, Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
//Connect to signaling server
|
||||
if(mSignalExchangerClient == null){
|
||||
initializeSignalClient();
|
||||
return;
|
||||
}
|
||||
|
||||
//Check connection is establish
|
||||
if(!mSignalExchangerClient.isConnected())
|
||||
return;
|
||||
|
||||
processClientsConnections();
|
||||
}
|
||||
|
||||
|
||||
private void initializeSignalClient(){
|
||||
|
||||
CallsConfiguration callsConfiguration = CallsHelper.GetCallsConfiguration();
|
||||
|
||||
assert callsConfiguration != null;
|
||||
mSignalExchangerClient = new SignalExchangerClient(new SignalExchangerInitConfig(
|
||||
callsConfiguration.getSignalServerName(),
|
||||
callsConfiguration.getSignalServerPort(),
|
||||
mCallInformation.findMember(AccountUtils.getID(this)).getUserCallID(),
|
||||
callsConfiguration.isSignalServerSecure()
|
||||
), this);
|
||||
}
|
||||
|
||||
|
||||
private void processClientsConnections(){
|
||||
//Process each peer connection
|
||||
for(CallMember member : mCallInformation.getMembers())
|
||||
processClientConnection(member);
|
||||
}
|
||||
|
||||
private void processClientConnection(CallMember member){
|
||||
|
||||
//Skip current user
|
||||
if(member.getUserID() == AccountUtils.getID(this))
|
||||
return;
|
||||
|
||||
//Check if the member left the conversation
|
||||
if(member.getStatus() != MemberCallStatus.ACCEPTED){
|
||||
disconnectFromPeer(member);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if(mList.find(member) == null && member.getUserID() > AccountUtils.getID(this)) {
|
||||
createPeerConnection(member, false);
|
||||
mSignalExchangerClient.sendReadyMessage(member.getUserCallID());
|
||||
}
|
||||
|
||||
|
||||
if(mList.find(member) != null)
|
||||
Objects.requireNonNull(mList.find(member)).setMember(member);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the peer connection for a specific call member
|
||||
*
|
||||
* @param member Target member
|
||||
* @param isInitiator Specify whether if we should send the offer or not to this user
|
||||
*/
|
||||
private void createPeerConnection(CallMember member, boolean isInitiator){
|
||||
|
||||
Log.v(TAG, "Create peer connection for connection with user " + member.getUserID());
|
||||
|
||||
CallPeerConnection callPeer = new CallPeerConnection(member);
|
||||
mList.add(callPeer);
|
||||
|
||||
//Create peer connection
|
||||
PeerConnectionClient peerConnectionClient = new PeerConnectionClient(
|
||||
getApplicationContext(),
|
||||
rootEglBase,
|
||||
new PeerConnectionClient.PeerConnectionParameters(
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
"",
|
||||
true,
|
||||
false,
|
||||
0,
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null
|
||||
),
|
||||
new PeerConnectionEvents(callPeer)
|
||||
);
|
||||
callPeer.setPeerConnectionClient(peerConnectionClient);
|
||||
|
||||
PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
|
||||
peerConnectionClient.createPeerConnectionFactory(options);
|
||||
|
||||
|
||||
//Signaling parameters
|
||||
AppRTCClient.SignalingParameters parameters = new AppRTCClient.SignalingParameters(
|
||||
CallsHelper.GetPeerServers(), isInitiator, null,
|
||||
null, null, null, null
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
//Initialize video view
|
||||
SurfaceViewRenderer localView = new SurfaceViewRenderer(this);
|
||||
localView.init(rootEglBase.getEglBaseContext(), null);
|
||||
localView.setZOrderMediaOverlay(true);
|
||||
callPeer.setLocalVideoView(localView);
|
||||
|
||||
SurfaceViewRenderer remoteView = new SurfaceViewRenderer(this);
|
||||
remoteView.init(rootEglBase.getEglBaseContext(), null);
|
||||
remoteView.setZOrderMediaOverlay(false);
|
||||
callPeer.setRemoteViewView(remoteView);
|
||||
|
||||
|
||||
ProxyVideoSink localProxyVideoSink = new ProxyVideoSink();
|
||||
localProxyVideoSink.setTarget(callPeer.getLocalVideoView());
|
||||
|
||||
ProxyVideoSink remoteProxyRenderer = new ProxyVideoSink();
|
||||
remoteProxyRenderer.setTarget(callPeer.getRemoteViewView());
|
||||
callPeer.getRemoteSinks().add(remoteProxyRenderer);
|
||||
|
||||
//Start connection
|
||||
peerConnectionClient.createPeerConnection(
|
||||
localProxyVideoSink,
|
||||
callPeer.getRemoteSinks(),
|
||||
createCameraCapturer(new Camera1Enumerator(false)),
|
||||
parameters
|
||||
);
|
||||
|
||||
if(isInitiator)
|
||||
peerConnectionClient.createOffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from a specific peer
|
||||
*
|
||||
* @param member Information about related call member
|
||||
*/
|
||||
private void disconnectFromPeer(CallMember member){
|
||||
|
||||
CallPeerConnection callPeer = mList.find(member);
|
||||
if(callPeer == null)
|
||||
return;
|
||||
|
||||
callPeer.getPeerConnectionClient().close();
|
||||
|
||||
mList.remove(callPeer);
|
||||
}
|
||||
|
||||
//Based on https://github.com/vivek1794/webrtc-android-codelab
|
||||
@Nullable
|
||||
private VideoCapturer createCameraCapturer(CameraEnumerator enumerator){
|
||||
final String[] deviceNames = enumerator.getDeviceNames();
|
||||
|
||||
// First, try to find front facing camera
|
||||
Logging.d(TAG, "Looking for front facing cameras.");
|
||||
for (String deviceName : deviceNames) {
|
||||
if (enumerator.isFrontFacing(deviceName)) {
|
||||
Logging.d(TAG, "Creating front facing camera capturer.");
|
||||
VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null);
|
||||
|
||||
if (videoCapturer != null) {
|
||||
return videoCapturer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Front facing camera not found, try something else
|
||||
Logging.d(TAG, "Looking for other cameras.");
|
||||
for (String deviceName : deviceNames) {
|
||||
if (!enumerator.isFrontFacing(deviceName)) {
|
||||
Logging.d(TAG, "Creating other camera capturer.");
|
||||
VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null);
|
||||
|
||||
if (videoCapturer != null) {
|
||||
return videoCapturer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onSignalServerError(String msg, @Nullable Throwable t) {
|
||||
runOnUiThread(() -> Toast.makeText(this,
|
||||
R.string.err_connect_signaling_server, Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectedToSignalingServer() {
|
||||
runOnUiThread(this::processClientsConnections);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReadyMessageCallback(String target_id, int number_targets) {
|
||||
Log.e(TAG, "Send ready message callback");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReadyMessage(String source_id) {
|
||||
|
||||
runOnUiThread(() -> {
|
||||
|
||||
//Ignore message if a connection has already been established
|
||||
if (mList.findByCallID(source_id) != null) {
|
||||
Log.e(TAG, "Ignored ready message from " + source_id + " because a connection has already be made!");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
CallMember member = mCallInformation.findMember(source_id);
|
||||
|
||||
if (member == null) {
|
||||
Log.e(TAG, source_id + " sent a ready message but it does not belong to the conversation!");
|
||||
return;
|
||||
}
|
||||
|
||||
Log.v(TAG, source_id + " informed it is ready to establish connection.");
|
||||
createPeerConnection(member, true);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSignal(String source_id, String signal) {
|
||||
Log.e(TAG, "Received new signal from " + source_id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendSignalCallback(int number_targets) {
|
||||
Log.e(TAG, "Send signal callback, number of targets: " + number_targets);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void gotRemoteIceCandidate(String source_id, IceCandidate iceCandidate) {
|
||||
|
||||
runOnUiThread(() -> {
|
||||
|
||||
CallPeerConnection connection = mList.findByCallID(source_id);
|
||||
if(connection == null) {
|
||||
Log.e(TAG, "Dropped ICE candidate from " + source_id + " no peer connection was ready to receive it!");
|
||||
return;
|
||||
}
|
||||
|
||||
connection.getPeerConnectionClient().addRemoteIceCandidate(iceCandidate);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void gotRemoteSessionDescription(String source_id, SessionDescription sessionDescription) {
|
||||
|
||||
runOnUiThread(() -> {
|
||||
|
||||
CallPeerConnection connection = mList.findByCallID(source_id);
|
||||
if(connection == null) {
|
||||
Log.e(TAG, "Dropped session description from " + source_id + " no peer connection was ready to receive it!");
|
||||
return;
|
||||
}
|
||||
|
||||
connection.getPeerConnectionClient().setRemoteDescription(sessionDescription);
|
||||
connection.getPeerConnectionClient().createAnswer();
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Class used to received events that comes from a connection
|
||||
*/
|
||||
private class PeerConnectionEvents implements PeerConnectionClient.PeerConnectionEvents {
|
||||
|
||||
private CallPeerConnection connection;
|
||||
|
||||
PeerConnectionEvents(CallPeerConnection connection) {
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLocalDescription(SessionDescription sdp) {
|
||||
Log.v(TAG, "Got a new local description");
|
||||
runOnUiThread(() ->
|
||||
mSignalExchangerClient.sendSessionDescription(
|
||||
connection.getMember().getUserCallID(), sdp));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIceCandidate(IceCandidate candidate) {
|
||||
Log.v(TAG, "Got a new ICE candidate");
|
||||
runOnUiThread(() -> mSignalExchangerClient.sendIceCandidate(
|
||||
connection.getMember().getUserCallID(),
|
||||
candidate));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIceCandidatesRemoved(IceCandidate[] candidates) {
|
||||
Log.v(TAG, "Some ice candidates removed with peer " +
|
||||
connection.getMember().getUserID());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIceConnected() {
|
||||
Log.v(TAG, "Ice connected with peer " +
|
||||
connection.getMember().getUserID());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIceDisconnected() {
|
||||
Log.v(TAG, "Ice disconnected from peer " +
|
||||
connection.getMember().getUserID());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnected() {
|
||||
Log.v(TAG, "Connected to peer " +
|
||||
connection.getMember().getUserID());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnected() {
|
||||
Log.v(TAG, "Disconnected from peer " +
|
||||
connection.getMember().getUserID());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPeerConnectionClosed() {
|
||||
Log.v(TAG, "Connection close from user " +
|
||||
connection.getMember().getUserID());
|
||||
runOnUiThread(() -> disconnectFromPeer(connection.getMember()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPeerConnectionStatsReady(StatsReport[] reports) {
|
||||
Log.v(TAG, "Stats ready for peer connection with " +
|
||||
connection.getMember().getUserID());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPeerConnectionError(String description) {
|
||||
Log.e(TAG, "Peer connection error with " +
|
||||
connection.getMember().getUserID() + " " + description);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh call information thread
|
||||
*/
|
||||
private class RefreshCallInformation extends Thread {
|
||||
|
||||
private final Object o = new Object();
|
||||
private boolean stop = false;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
super.run();
|
||||
|
||||
synchronized (o){
|
||||
|
||||
while(!stop) {
|
||||
|
||||
runOnUiThread(CallActivity.this::getCallInformation);
|
||||
|
||||
|
||||
try {
|
||||
o.wait((long) (1.5 * 1000));
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public void interrupt(){
|
||||
stop = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* I don't know why, but this is an absolute requirement ! (to show videos)
|
||||
*/
|
||||
private static class ProxyVideoSink implements VideoSink {
|
||||
private VideoSink target;
|
||||
|
||||
@Override
|
||||
synchronized public void onFrame(VideoFrame frame) {
|
||||
if (target == null) {
|
||||
Logging.d(TAG, "Dropping frame in proxy because target is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
target.onFrame(frame);
|
||||
}
|
||||
|
||||
synchronized public void setTarget(VideoSink target) {
|
||||
this.target = target;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ import android.widget.Toast;
|
||||
|
||||
import org.communiquons.android.comunic.client.BuildConfig;
|
||||
import org.communiquons.android.comunic.client.R;
|
||||
import org.communiquons.android.comunic.client.crashreporter.CrashReporter;
|
||||
import org.communiquons.crashreporter.CrashReporter;
|
||||
import org.communiquons.android.comunic.client.data.enums.VirtualDirectoryType;
|
||||
import org.communiquons.android.comunic.client.data.helpers.APIRequestHelper;
|
||||
import org.communiquons.android.comunic.client.data.helpers.AccountHelper;
|
||||
|
@ -0,0 +1,48 @@
|
||||
package org.communiquons.android.comunic.client.ui.arrays;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.communiquons.android.comunic.client.data.models.CallMember;
|
||||
import org.communiquons.android.comunic.client.ui.models.CallPeerConnection;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* List of clients connections
|
||||
*
|
||||
* @author Pierre HUBERT
|
||||
*/
|
||||
public class CallPeersConnectionsList extends ArrayList<CallPeerConnection> {
|
||||
|
||||
/**
|
||||
* Find the connection matching a specific call member
|
||||
*
|
||||
*
|
||||
* @param member Information about the target member
|
||||
* @return Full client connection
|
||||
*/
|
||||
@Nullable
|
||||
public CallPeerConnection find(CallMember member){
|
||||
for(CallPeerConnection connection : this)
|
||||
if(connection.getMember().getUserID() == member.getUserID())
|
||||
return connection;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Try to find a peer connection using call ID
|
||||
*
|
||||
* @param id The ID of the user call ID
|
||||
* @return Information about the peer connection / null object in case of failure
|
||||
*/
|
||||
@Nullable
|
||||
public CallPeerConnection findByCallID(String id){
|
||||
for(CallPeerConnection connection : this)
|
||||
if(connection.getMember().getUserCallID().equals(id))
|
||||
return connection;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package org.communiquons.android.comunic.client.ui.asynctasks;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.communiquons.android.comunic.client.data.helpers.CallsHelper;
|
||||
import org.communiquons.android.comunic.client.data.models.CallInformation;
|
||||
|
||||
/**
|
||||
* Task to get information about a call
|
||||
*
|
||||
* @author Pierre HUBERT
|
||||
*/
|
||||
public class GetCallInformationTask extends SafeAsyncTask<Integer, Void, CallInformation> {
|
||||
|
||||
public GetCallInformationTask(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CallInformation doInBackground(Integer... integers) {
|
||||
|
||||
CallsHelper callsHelper = new CallsHelper(getContext());
|
||||
|
||||
CallInformation callInformation = callsHelper.getInfo(integers[0]);
|
||||
|
||||
//Try to get call name
|
||||
if(callInformation == null || callsHelper.getCallName(callInformation) == null)
|
||||
return null;
|
||||
|
||||
return callInformation;
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
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.SurfaceViewRenderer;
|
||||
import org.webrtc.VideoSink;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Single remote connection information
|
||||
*
|
||||
* @author Pierre HUBERT
|
||||
*/
|
||||
public class CallPeerConnection {
|
||||
|
||||
//Private fields
|
||||
private CallMember member;
|
||||
private PeerConnectionClient peerConnectionClient;
|
||||
private ArrayList<VideoSink> remoteSinks = new ArrayList<>();
|
||||
|
||||
//Views
|
||||
private SurfaceViewRenderer mLocalVideoView;
|
||||
private SurfaceViewRenderer mRemoteViewView;
|
||||
|
||||
public CallPeerConnection(CallMember member) {
|
||||
this.member = member;
|
||||
}
|
||||
|
||||
public CallMember getMember() {
|
||||
return member;
|
||||
}
|
||||
|
||||
public void setMember(CallMember member) {
|
||||
this.member = member;
|
||||
}
|
||||
|
||||
public PeerConnectionClient getPeerConnectionClient() {
|
||||
return peerConnectionClient;
|
||||
}
|
||||
|
||||
public void setPeerConnectionClient(PeerConnectionClient peerConnectionClient) {
|
||||
this.peerConnectionClient = peerConnectionClient;
|
||||
}
|
||||
|
||||
public ArrayList<VideoSink> getRemoteSinks() {
|
||||
return remoteSinks;
|
||||
}
|
||||
|
||||
public void setRemoteSinks(ArrayList<VideoSink> remoteSinks) {
|
||||
this.remoteSinks = remoteSinks;
|
||||
}
|
||||
|
||||
public SurfaceViewRenderer getRemoteViewView() {
|
||||
return mRemoteViewView;
|
||||
}
|
||||
|
||||
public void setRemoteViewView(SurfaceViewRenderer mRemoteViewView) {
|
||||
this.mRemoteViewView = mRemoteViewView;
|
||||
}
|
||||
|
||||
public SurfaceViewRenderer getLocalVideoView() {
|
||||
|
||||
return mLocalVideoView;
|
||||
}
|
||||
|
||||
public void setLocalVideoView(SurfaceViewRenderer mLocalVideoView) {
|
||||
this.mLocalVideoView = mLocalVideoView;
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package org.communiquons.android.comunic.client.crashreporter;
|
||||
package org.communiquons.crashreporter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.AsyncTask;
|
@ -0,0 +1,84 @@
|
||||
package org.communiquons.signalexchangerclient;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/**
|
||||
* Signal exchanger client request
|
||||
*
|
||||
* @author Pierre HUBERT
|
||||
*/
|
||||
class ClientRequest {
|
||||
|
||||
/**
|
||||
* Contains request information
|
||||
*/
|
||||
private JSONObject mList;
|
||||
|
||||
/**
|
||||
* Initialize object
|
||||
*/
|
||||
ClientRequest(){
|
||||
this.mList = new JSONObject();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a string to the request
|
||||
*
|
||||
* @param name The name of the string to add
|
||||
* @param value The value of the string to add
|
||||
* @return This object to help to concatenate requests
|
||||
*/
|
||||
ClientRequest addString(String name, String value){
|
||||
try {
|
||||
mList.put(name, value);
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException("Could not add a string to a JSON object!");
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a boolean to the request
|
||||
*
|
||||
* @param name The name of the string to add
|
||||
* @param value Boolean value
|
||||
* @return This object
|
||||
*/
|
||||
ClientRequest addBoolean(String name, boolean value){
|
||||
try {
|
||||
mList.put(name, value);
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a JSON object to the request
|
||||
*
|
||||
* @param name The name of the field to add
|
||||
* @param value The object
|
||||
* @return This object
|
||||
*/
|
||||
ClientRequest addJSONObject(String name, JSONObject value){
|
||||
try {
|
||||
mList.put(name, value);
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get resulting JSON object
|
||||
*
|
||||
* @return Get the resulting JSON object
|
||||
*/
|
||||
JSONObject get(){
|
||||
return mList;
|
||||
}
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
package org.communiquons.signalexchangerclient;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.webrtc.IceCandidate;
|
||||
import org.webrtc.SessionDescription;
|
||||
|
||||
/**
|
||||
* This interface should be implemented by the classes
|
||||
* that makes use of the {@link SignalExchangerClient}
|
||||
* in order to get updated about new information
|
||||
* availability
|
||||
*
|
||||
* @author Pierre HUBERT
|
||||
*/
|
||||
public interface SignalExchangerCallback {
|
||||
|
||||
/**
|
||||
* Method called when an error occur
|
||||
*
|
||||
* @param msg Message associated to the error
|
||||
* @param t Optional associated throwable
|
||||
*/
|
||||
void onSignalServerError(String msg, @Nullable Throwable t);
|
||||
|
||||
/**
|
||||
* Method called once we are connected to the server
|
||||
*/
|
||||
void onConnectedToSignalingServer();
|
||||
|
||||
/**
|
||||
* Method called on ready message callback
|
||||
*
|
||||
* @param target_id The ID of the target
|
||||
* @param number_targets The number of peers who received the message
|
||||
*/
|
||||
void onReadyMessageCallback(String target_id, int number_targets);
|
||||
|
||||
/**
|
||||
* Method called when this client receive a new ready message signal
|
||||
*
|
||||
* @param source_id The source of the message
|
||||
*/
|
||||
void onReadyMessage(String source_id);
|
||||
|
||||
/**
|
||||
* Method called when the client received a signal
|
||||
*
|
||||
* @param source_id The source of the signal
|
||||
* @param signal The signal
|
||||
*/
|
||||
void onSignal(String source_id, String signal);
|
||||
|
||||
/**
|
||||
* Send signals callback
|
||||
*
|
||||
* @param number_targets The number of targets for the signal
|
||||
*/
|
||||
void onSendSignalCallback(int number_targets);
|
||||
|
||||
/**
|
||||
* This method is called once we received a remote Ice Candidate
|
||||
*
|
||||
* @param source_id The source of the signal
|
||||
* @param iceCandidate The candidate itself
|
||||
*/
|
||||
void gotRemoteIceCandidate(String source_id, IceCandidate iceCandidate);
|
||||
|
||||
/**
|
||||
* This method is called when we have got a new remote session description
|
||||
*
|
||||
* @param source_id The source of the signal
|
||||
* @param sessionDescription The session description
|
||||
*/
|
||||
void gotRemoteSessionDescription(String source_id, SessionDescription sessionDescription);
|
||||
}
|
@ -0,0 +1,333 @@
|
||||
package org.communiquons.signalexchangerclient;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.webrtc.IceCandidate;
|
||||
import org.webrtc.SessionDescription;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.WebSocket;
|
||||
import okhttp3.WebSocketListener;
|
||||
|
||||
/**
|
||||
* Signal exchanger client
|
||||
*
|
||||
* @author Pierre HUBERT
|
||||
*/
|
||||
public class SignalExchangerClient extends WebSocketListener {
|
||||
|
||||
/**
|
||||
* Debug log
|
||||
*/
|
||||
private static final String TAG = SignalExchangerClient.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* Instance configuration
|
||||
*/
|
||||
private SignalExchangerInitConfig mConfig;
|
||||
|
||||
/**
|
||||
* Signal exchanger callback
|
||||
*/
|
||||
@Nullable
|
||||
private SignalExchangerCallback mCallback;
|
||||
|
||||
/**
|
||||
* Http Client
|
||||
*/
|
||||
private OkHttpClient mClient;
|
||||
|
||||
/**
|
||||
* Current WebSocket connection
|
||||
*/
|
||||
private WebSocket mWebSocket;
|
||||
|
||||
/**
|
||||
* Initialize a SignalExchanger client
|
||||
*
|
||||
* @param config Configuration of the client
|
||||
* @param cb Callback function to call when we got information update
|
||||
*/
|
||||
public SignalExchangerClient(@NonNull SignalExchangerInitConfig config,
|
||||
@Nullable SignalExchangerCallback cb){
|
||||
|
||||
//Save configuration
|
||||
this.mConfig = config;
|
||||
this.mCallback = cb;
|
||||
|
||||
//Connect to the WebSocket
|
||||
String url = (config.isSecure() ? "wss" : "ws")
|
||||
+ "://" + config.getDomain() + ":" + config.getPort() + "/socket";
|
||||
|
||||
mClient = new OkHttpClient();
|
||||
Request request = new Request.Builder().url(url).build();
|
||||
|
||||
mWebSocket = mClient.newWebSocket(request, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current client configuration
|
||||
*
|
||||
* @return Configuration of the client
|
||||
*/
|
||||
public SignalExchangerInitConfig getConfig() {
|
||||
return mConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the callback to use on new updates
|
||||
*
|
||||
* @param mCallback Callback to use
|
||||
*/
|
||||
public void setCallback(@Nullable SignalExchangerCallback mCallback) {
|
||||
this.mCallback = mCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check out whether the current client is connected to a server or not
|
||||
*
|
||||
* @return true if the client is connected to a server / false else
|
||||
*/
|
||||
public boolean isConnected(){
|
||||
return mWebSocket != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send ready message to a client
|
||||
*
|
||||
* @param target_client_id The ID of the target client
|
||||
*/
|
||||
public void sendReadyMessage(String target_client_id){
|
||||
sendData(new ClientRequest()
|
||||
.addBoolean("ready_msg", true)
|
||||
.addString("target_id", target_client_id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a signal to a target
|
||||
*
|
||||
* @param target_id The ID of the target
|
||||
* @param signal The signal to send
|
||||
*/
|
||||
public void sendSignal(String target_id, String signal){
|
||||
sendData(new ClientRequest()
|
||||
.addString("target_id", target_id)
|
||||
.addString("signal", signal));
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a session description to a target
|
||||
*
|
||||
* @param target_id The ID of the target
|
||||
* @param description The description
|
||||
*/
|
||||
public void sendSessionDescription(String target_id, SessionDescription description){
|
||||
try {
|
||||
JSONObject object = new JSONObject();
|
||||
object.put("type", description.type.canonicalForm());
|
||||
object.put("sdp", description.description);
|
||||
sendSignal(target_id, object.toString());
|
||||
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an Ice Candidate to a remote peer
|
||||
*
|
||||
* @param target_id The ID of the target
|
||||
* @param candidate The candidate to send
|
||||
*/
|
||||
public void sendIceCandidate(String target_id, IceCandidate candidate){
|
||||
try {
|
||||
JSONObject candidateObj = new JSONObject();
|
||||
candidateObj.put("sdpMid", candidate.sdpMid);
|
||||
candidateObj.put("sdpMLineIndex", candidate.sdpMLineIndex);
|
||||
candidateObj.put("candidate", candidate.sdp);
|
||||
|
||||
JSONObject object = new JSONObject();
|
||||
object.put("candidate", candidateObj);
|
||||
sendSignal(target_id, object.toString());
|
||||
|
||||
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send data to the server
|
||||
*
|
||||
* @param request The data to send to the server
|
||||
*/
|
||||
private void sendData(@NonNull ClientRequest request){
|
||||
|
||||
//Continues only in case of active connection
|
||||
if(!isConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Send data to the server
|
||||
Log.v(TAG, "Sending " + request.get().toString());
|
||||
mWebSocket.send(request.get().toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a web socket has been accepted by the remote peer and may begin transmitting
|
||||
* messages.
|
||||
*/
|
||||
public void onOpen(WebSocket webSocket, Response response) {
|
||||
|
||||
//Save WebSocket object
|
||||
this.mWebSocket = webSocket;
|
||||
|
||||
//Send the ID of current client to the server
|
||||
sendData(new ClientRequest()
|
||||
.addString("client_id", mConfig.getClientID()));
|
||||
|
||||
//Inform we are connected
|
||||
if(mCallback != null)
|
||||
mCallback.onConnectedToSignalingServer();
|
||||
|
||||
}
|
||||
|
||||
/** Invoked when a text (type {@code 0x1}) message has been received. */
|
||||
@Override
|
||||
public void onMessage(WebSocket webSocket, String text) {
|
||||
Log.v(TAG, "Received new message from server: " + text);
|
||||
|
||||
//Decode message
|
||||
try {
|
||||
JSONObject message = new JSONObject(text);
|
||||
|
||||
//Ready message callback
|
||||
if(message.has("ready_message_sent")){
|
||||
|
||||
if(mCallback != null)
|
||||
mCallback.onReadyMessageCallback(
|
||||
message.getString("target_id"),
|
||||
message.getInt("number_of_targets")
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
//Ready message
|
||||
else if(message.has("ready_msg")){
|
||||
|
||||
if(mCallback != null)
|
||||
mCallback.onReadyMessage(
|
||||
message.getString("source_id")
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
//Signal
|
||||
else if(message.has("signal")) {
|
||||
|
||||
if(mCallback != null)
|
||||
mCallback.onSignal(
|
||||
message.getString("source_id"),
|
||||
message.getString("signal")
|
||||
);
|
||||
|
||||
processReceivedSignal(message.getString("source_id"),
|
||||
message.getString("signal"));
|
||||
}
|
||||
|
||||
//Send signal callback
|
||||
else if(message.has("signal_sent")){
|
||||
|
||||
if(mCallback != null)
|
||||
mCallback.onSendSignalCallback(
|
||||
message.getInt("number_of_targets")
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
//Success message
|
||||
else if(message.has("success"))
|
||||
Log.v(TAG, "Success: " + message.getString("success"));
|
||||
|
||||
//Unrecognized message
|
||||
else
|
||||
Log.e(TAG, "Message from server not understood!");
|
||||
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
|
||||
if(mCallback != null)
|
||||
mCallback.onSignalServerError("Could not parse response from server!", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a received signal
|
||||
*
|
||||
* @param source_id The source of the signal
|
||||
* @param signal The signal to process
|
||||
*/
|
||||
private void processReceivedSignal(String source_id, String signal) throws JSONException {
|
||||
|
||||
JSONObject object = new JSONObject(signal);
|
||||
|
||||
//Ice candidate
|
||||
if(object.has("candidate")) {
|
||||
|
||||
JSONObject candidate = object.getJSONObject("candidate");
|
||||
|
||||
if (mCallback != null)
|
||||
mCallback.gotRemoteIceCandidate(
|
||||
source_id, new IceCandidate(
|
||||
candidate.getString("sdpMid"),
|
||||
candidate.getInt("sdpMLineIndex"),
|
||||
candidate.getString("candidate")
|
||||
)
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
//Sdp signal
|
||||
else if(object.has("sdp") && object.has("type")){
|
||||
|
||||
SessionDescription.Type type = SessionDescription.Type.fromCanonicalForm(
|
||||
object.getString("type"));
|
||||
String sdp = object.getString("sdp");
|
||||
|
||||
if(mCallback != null)
|
||||
mCallback.gotRemoteSessionDescription(source_id,
|
||||
new SessionDescription(type, sdp));
|
||||
|
||||
}
|
||||
|
||||
else
|
||||
Log.e(TAG, "Could not understand received signal!");
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when both peers have indicated that no more messages will be transmitted and the
|
||||
* connection has been successfully released. No further calls to this listener will be made.
|
||||
*/
|
||||
public void onClosed(WebSocket webSocket, int code, String reason) {
|
||||
mWebSocket = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a web socket has been closed due to an error reading from or writing to the
|
||||
* network. Both outgoing and incoming messages may have been lost. No further calls to this
|
||||
* listener will be made.
|
||||
*/
|
||||
public void onFailure(WebSocket webSocket, Throwable t, @Nullable Response response) {
|
||||
|
||||
if(mCallback != null)
|
||||
mCallback.onSignalServerError(t.getMessage(), t);
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
package org.communiquons.signalexchangerclient;
|
||||
|
||||
/**
|
||||
* Signal exchanger configuration intialization
|
||||
*
|
||||
* @author Pierre HUBERT
|
||||
*/
|
||||
public class SignalExchangerInitConfig {
|
||||
|
||||
//Private fields
|
||||
private String domain;
|
||||
private int port;
|
||||
private String clientID;
|
||||
private boolean isSecure;
|
||||
|
||||
public SignalExchangerInitConfig() {
|
||||
|
||||
}
|
||||
|
||||
public SignalExchangerInitConfig(String domain, int port, String clientID, boolean isSecure) {
|
||||
this.domain = domain;
|
||||
this.port = port;
|
||||
this.clientID = clientID;
|
||||
this.isSecure = isSecure;
|
||||
}
|
||||
|
||||
public String getDomain() {
|
||||
return domain;
|
||||
}
|
||||
|
||||
public void setDomain(String domain) {
|
||||
this.domain = domain;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
public void setPort(int port) {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public String getClientID() {
|
||||
return clientID;
|
||||
}
|
||||
|
||||
public void setClientID(String clientID) {
|
||||
this.clientID = clientID;
|
||||
}
|
||||
|
||||
public boolean isSecure() {
|
||||
return isSecure;
|
||||
}
|
||||
|
||||
public void setSecure(boolean secure) {
|
||||
isSecure = secure;
|
||||
}
|
||||
}
|
@ -6,17 +6,18 @@
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".ui.activities.CallActivity">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/call_id"
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="21dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="TextView"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</android.support.constraint.ConstraintLayout>
|
@ -329,4 +329,7 @@
|
||||
<string name="button_accept_call">Répondre</string>
|
||||
<string name="title_incoming_call">Appel entrant</string>
|
||||
<string name="err_get_next_pending_call_info">Impossible de récupérer les informations de l\'appel en cours !</string>
|
||||
<string name="err_get_call_info">Impossible de récupérer les infoamtions sur l\'appel !</string>
|
||||
<string name="err_connect_signaling_server">Impossible de connecter au signaling server !</string>
|
||||
<string name="notice_call_terminated">Appel terminé.</string>
|
||||
</resources>
|
@ -328,4 +328,7 @@
|
||||
<string name="button_accept_call">Accept call</string>
|
||||
<string name="title_incoming_call">Incoming call</string>
|
||||
<string name="err_get_next_pending_call_info">Could not get pending call information!</string>
|
||||
<string name="err_get_call_info">Could not get call information!</string>
|
||||
<string name="err_connect_signaling_server">Could not connect to signaling server!</string>
|
||||
<string name="notice_call_terminated">Call terminated</string>
|
||||
</resources>
|
||||
|
Loading…
Reference in New Issue
Block a user