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>
|
</value>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</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" />
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectType">
|
<component name="ProjectType">
|
||||||
|
@ -3,6 +3,11 @@ apply plugin: 'com.android.application'
|
|||||||
android {
|
android {
|
||||||
compileSdkVersion 28
|
compileSdkVersion 28
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility 1.8
|
||||||
|
targetCompatibility 1.8
|
||||||
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "org.communiquons.android.comunic.client"
|
applicationId "org.communiquons.android.comunic.client"
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
@ -59,6 +64,8 @@ dependencies {
|
|||||||
implementation 'com.android.support:design:28.0.0-rc02'
|
implementation 'com.android.support:design:28.0.0-rc02'
|
||||||
implementation 'com.android.support:preference-v7: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.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'
|
testImplementation 'junit:junit:4.12'
|
||||||
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
||||||
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.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.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<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
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:fullBackupContent="@xml/backup_scheme"
|
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.JSONArray;
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
import org.webrtc.PeerConnection;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls helper
|
* Calls helper
|
||||||
@ -90,6 +93,30 @@ public class CallsHelper extends BaseHelper {
|
|||||||
return mCallsConfiguration != null && mCallsConfiguration.isEnabled();
|
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
|
* 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
|
* Try to get and return call information
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package org.communiquons.android.comunic.client.data.models;
|
package org.communiquons.android.comunic.client.data.models;
|
||||||
|
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
||||||
@ -75,6 +76,36 @@ public class CallInformation {
|
|||||||
this.members = members;
|
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() {
|
public String getCallName() {
|
||||||
return callName;
|
return callName;
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,7 @@ public class CallsConfiguration {
|
|||||||
this.signalServerPort = signalServerPort;
|
this.signalServerPort = signalServerPort;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isSignalSererSecure() {
|
public boolean isSignalServerSecure() {
|
||||||
return isSignalSererSecure;
|
return isSignalSererSecure;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,46 @@
|
|||||||
package org.communiquons.android.comunic.client.ui.activities;
|
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.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.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.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;
|
import java.util.Objects;
|
||||||
|
|
||||||
@ -13,21 +49,89 @@ import java.util.Objects;
|
|||||||
*
|
*
|
||||||
* @author Pierre HUBERT
|
* @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
|
* Mandatory argument that includes call id
|
||||||
*/
|
*/
|
||||||
public static final String ARGUMENT_CALL_ID = "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
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_call);
|
setContentView(R.layout.activity_call);
|
||||||
|
|
||||||
//Hide call bar
|
mCallID = getIntent().getIntExtra(ARGUMENT_CALL_ID, 0);
|
||||||
Objects.requireNonNull(getSupportActionBar()).hide();
|
|
||||||
|
//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
|
@Override
|
||||||
@ -37,7 +141,507 @@ public class CallActivity extends BaseActivity {
|
|||||||
//Hide call notifications
|
//Hide call notifications
|
||||||
PendingCallsBroadcastReceiver.RemoveCallNotification(this);
|
PendingCallsBroadcastReceiver.RemoveCallNotification(this);
|
||||||
|
|
||||||
((TextView)findViewById(R.id.call_id)).setText(
|
//Make sure we have access to user camera and microphone
|
||||||
"Call " + getIntent().getExtras().getInt(ARGUMENT_CALL_ID));
|
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.BuildConfig;
|
||||||
import org.communiquons.android.comunic.client.R;
|
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.enums.VirtualDirectoryType;
|
||||||
import org.communiquons.android.comunic.client.data.helpers.APIRequestHelper;
|
import org.communiquons.android.comunic.client.data.helpers.APIRequestHelper;
|
||||||
import org.communiquons.android.comunic.client.data.helpers.AccountHelper;
|
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.content.Context;
|
||||||
import android.os.AsyncTask;
|
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"
|
android:layout_height="match_parent"
|
||||||
tools:context=".ui.activities.CallActivity">
|
tools:context=".ui.activities.CallActivity">
|
||||||
|
|
||||||
<TextView
|
<ProgressBar
|
||||||
android:id="@+id/call_id"
|
android:id="@+id/progressBar"
|
||||||
|
style="?android:attr/progressBarStyle"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="21dp"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="8dp"
|
android:layout_marginStart="8dp"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="8dp"
|
||||||
android:layout_marginBottom="8dp"
|
android:layout_marginBottom="8dp"
|
||||||
android:text="TextView"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
</android.support.constraint.ConstraintLayout>
|
</android.support.constraint.ConstraintLayout>
|
@ -329,4 +329,7 @@
|
|||||||
<string name="button_accept_call">Répondre</string>
|
<string name="button_accept_call">Répondre</string>
|
||||||
<string name="title_incoming_call">Appel entrant</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_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>
|
</resources>
|
@ -328,4 +328,7 @@
|
|||||||
<string name="button_accept_call">Accept call</string>
|
<string name="button_accept_call">Accept call</string>
|
||||||
<string name="title_incoming_call">Incoming 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_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>
|
</resources>
|
||||||
|
Loading…
Reference in New Issue
Block a user