diff --git a/openvidu-android/.gitignore b/openvidu-android/.gitignore
new file mode 100644
index 00000000..603b1407
--- /dev/null
+++ b/openvidu-android/.gitignore
@@ -0,0 +1,14 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
diff --git a/openvidu-android/.idea/.name b/openvidu-android/.idea/.name
new file mode 100644
index 00000000..f2ca6493
--- /dev/null
+++ b/openvidu-android/.idea/.name
@@ -0,0 +1 @@
+OpenVidu Android
\ No newline at end of file
diff --git a/openvidu-android/.idea/codeStyles/Project.xml b/openvidu-android/.idea/codeStyles/Project.xml
new file mode 100644
index 00000000..681f41ae
--- /dev/null
+++ b/openvidu-android/.idea/codeStyles/Project.xml
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ xmlns:android
+
+ ^$
+
+
+
+
+
+
+
+
+ xmlns:.*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*:id
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:name
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ name
+
+ ^$
+
+
+
+
+
+
+
+
+ style
+
+ ^$
+
+
+
+
+
+
+
+
+ .*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*
+
+ http://schemas.android.com/apk/res/android
+
+
+ ANDROID_ATTRIBUTE_ORDER
+
+
+
+
+
+
+ .*
+
+ .*
+
+
+ BY_NAME
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/openvidu-android/.idea/gradle.xml b/openvidu-android/.idea/gradle.xml
new file mode 100644
index 00000000..d291b3d7
--- /dev/null
+++ b/openvidu-android/.idea/gradle.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/openvidu-android/.idea/misc.xml b/openvidu-android/.idea/misc.xml
new file mode 100644
index 00000000..7bfef59d
--- /dev/null
+++ b/openvidu-android/.idea/misc.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/openvidu-android/.idea/runConfigurations.xml b/openvidu-android/.idea/runConfigurations.xml
new file mode 100644
index 00000000..7f68460d
--- /dev/null
+++ b/openvidu-android/.idea/runConfigurations.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/openvidu-android/app/.gitignore b/openvidu-android/app/.gitignore
new file mode 100644
index 00000000..796b96d1
--- /dev/null
+++ b/openvidu-android/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/openvidu-android/app/build.gradle b/openvidu-android/app/build.gradle
new file mode 100644
index 00000000..ed16e954
--- /dev/null
+++ b/openvidu-android/app/build.gradle
@@ -0,0 +1,39 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 29
+ buildToolsVersion "29.0.1"
+ defaultConfig {
+ applicationId "com.example.openviduandroid"
+ minSdkVersion 21
+ targetSdkVersion 29
+ versionCode 1
+ versionName "1.0"
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ multiDexEnabled true
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+}
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+ implementation 'androidx.appcompat:appcompat:1.1.0'
+ implementation 'com.android.support.constraint:constraint-layout:1.1.3'
+ implementation 'com.jakewharton:butterknife:10.1.0'
+ implementation 'com.squareup.okhttp3:okhttp:4.1.1'
+ implementation 'com.neovisionaries:nv-websocket-client:2.9'
+ implementation 'org.webrtc:google-webrtc:1.0.28513'
+ annotationProcessor 'com.jakewharton:butterknife-compiler:10.1.0'
+ testImplementation 'junit:junit:4.12'
+ androidTestImplementation 'androidx.test:runner:1.1.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
+}
diff --git a/openvidu-android/app/proguard-rules.pro b/openvidu-android/app/proguard-rules.pro
new file mode 100644
index 00000000..f1b42451
--- /dev/null
+++ b/openvidu-android/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/openvidu-android/app/src/androidTest/java/com/example/openviduandroid/ExampleInstrumentedTest.java b/openvidu-android/app/src/androidTest/java/com/example/openviduandroid/ExampleInstrumentedTest.java
new file mode 100644
index 00000000..a4148420
--- /dev/null
+++ b/openvidu-android/app/src/androidTest/java/com/example/openviduandroid/ExampleInstrumentedTest.java
@@ -0,0 +1,27 @@
+package com.example.openviduandroid;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext() {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+ assertEquals("com.example.openviduandroid", appContext.getPackageName());
+ }
+}
diff --git a/openvidu-android/app/src/main/AndroidManifest.xml b/openvidu-android/app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..98d7bb1e
--- /dev/null
+++ b/openvidu-android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/openvidu-android/app/src/main/java/com/example/openviduandroid/activities/SessionActivity.java b/openvidu-android/app/src/main/java/com/example/openviduandroid/activities/SessionActivity.java
new file mode 100644
index 00000000..1522bd1f
--- /dev/null
+++ b/openvidu-android/app/src/main/java/com/example/openviduandroid/activities/SessionActivity.java
@@ -0,0 +1,299 @@
+package com.example.openviduandroid.activities;
+
+import android.Manifest;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.os.Handler;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+import androidx.fragment.app.DialogFragment;
+
+import com.example.openviduandroid.R;
+import com.example.openviduandroid.fragments.PermissionsDialogFragment;
+import com.example.openviduandroid.openvidu.LocalParticipant;
+import com.example.openviduandroid.openvidu.RemoteParticipant;
+import com.example.openviduandroid.openvidu.Session;
+import com.example.openviduandroid.utils.CustomHttpClient;
+import com.example.openviduandroid.websocket.CustomWebSocket;
+
+import org.jetbrains.annotations.NotNull;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.webrtc.EglBase;
+import org.webrtc.MediaStream;
+import org.webrtc.SurfaceViewRenderer;
+import org.webrtc.VideoTrack;
+
+import java.io.IOException;
+import java.util.Random;
+
+import butterknife.BindView;
+import butterknife.ButterKnife;
+import okhttp3.Call;
+import okhttp3.Callback;
+import okhttp3.MediaType;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+
+public class SessionActivity extends AppCompatActivity {
+
+ 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;
+ private final String TAG = "SessionActivity";
+ @BindView(R.id.views_container)
+ LinearLayout views_container;
+ @BindView(R.id.start_finish_call)
+ Button start_finish_call;
+ @BindView(R.id.session_name)
+ EditText session_name;
+ @BindView(R.id.participant_name)
+ EditText participant_name;
+ @BindView(R.id.openvidu_url)
+ EditText openvidu_url;
+ @BindView(R.id.openvidu_secret)
+ EditText openvidu_secret;
+ @BindView(R.id.local_gl_surface_view)
+ SurfaceViewRenderer localVideoView;
+ @BindView(R.id.main_participant)
+ TextView main_participant;
+ @BindView(R.id.peer_container)
+ FrameLayout peer_container;
+
+ private String OPENVIDU_URL;
+ private String OPENVIDU_SECRET;
+ private Session session;
+ private CustomHttpClient httpClient;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+ setContentView(R.layout.activity_main);
+ getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
+ askForPermissions();
+ ButterKnife.bind(this);
+ Random random = new Random();
+ int randomIndex = random.nextInt(100);
+ participant_name.setText(participant_name.getText().append(String.valueOf(randomIndex)));
+
+ OPENVIDU_URL = openvidu_url.getText().toString();
+ OPENVIDU_SECRET = openvidu_secret.getText().toString();
+ httpClient = new CustomHttpClient(OPENVIDU_URL, "Basic " + android.util.Base64.encodeToString(("OPENVIDUAPP:" + OPENVIDU_SECRET).getBytes(), android.util.Base64.DEFAULT).trim());
+ }
+
+ public 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);
+ }
+ }
+
+ public void joinSession(View view) {
+ if (arePermissionGranted()) {
+ if (start_finish_call.getText().equals(getResources().getString(R.string.hang_up))) {
+ // Already in a call
+ leaveSession();
+ return;
+ }
+ initViews();
+ start_finish_call.setText(getResources().getString(R.string.hang_up));
+ start_finish_call.setEnabled(false);
+ openvidu_url.setEnabled(false);
+ openvidu_url.setFocusable(false);
+ openvidu_secret.setEnabled(false);
+ openvidu_secret.setFocusable(false);
+ session_name.setEnabled(false);
+ session_name.setFocusable(false);
+ participant_name.setEnabled(false);
+ participant_name.setFocusable(false);
+
+ String sessionId = session_name.getText().toString();
+ getToken(sessionId);
+ } else {
+ DialogFragment permissionsFragment = new PermissionsDialogFragment();
+ permissionsFragment.show(getSupportFragmentManager(), "Permissions Fragment");
+ }
+ }
+
+ private void getToken(String sessionId) {
+ final SessionActivity thisActivity = this;
+ try {
+ // Session Request
+ RequestBody sessionBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), "{\"customSessionId\": \"" + sessionId + "\"}");
+ this.httpClient.httpCall("/api/sessions", "POST", "application/json", sessionBody, new Callback() {
+
+ @Override
+ public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
+ Log.d(TAG, "responseString: " + response.body().string());
+
+ // Token Request
+ RequestBody tokenBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), "{\"session\": \"" + sessionId + "\"}");
+ httpClient.httpCall("/api/tokens", "POST", "application/json", tokenBody, new Callback() {
+
+ @Override
+ public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
+ final String responseString = response.body().string();
+ Log.d(TAG, "responseString2: " + responseString);
+ JSONObject tokenJsonObject = null;
+ String token = null;
+ try {
+ tokenJsonObject = new JSONObject(responseString);
+ token = tokenJsonObject.getString("token");
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ String participantName = participant_name.getText().toString();
+ session = new Session(sessionId, token, views_container, thisActivity);
+ LocalParticipant localParticipant = new LocalParticipant(participantName, session, thisActivity.getApplicationContext(), localVideoView);
+ localParticipant.startCamera();
+
+ runOnUiThread(() -> {
+ main_participant.setText(participant_name.getText().toString());
+ main_participant.setPadding(20, 3, 20, 3);
+ });
+
+ CustomWebSocket webSocket = new CustomWebSocket(session, OPENVIDU_URL, thisActivity);
+ webSocket.execute();
+ session.setWebSocket(webSocket);
+ }
+
+ @Override
+ public void onFailure(@NotNull Call call, @NotNull IOException e) {
+ Log.e(TAG, "Error POST /api/tokens", e);
+ }
+ });
+ }
+
+ @Override
+ public void onFailure(@NotNull Call call, @NotNull IOException e) {
+ Log.e(TAG, "Error POST /api/sessions", e);
+ }
+ });
+ } catch (IOException e) {
+ Log.e("Error getting token", e.getMessage());
+ e.printStackTrace();
+ }
+ }
+
+ private void initViews() {
+ EglBase rootEglBase = EglBase.create();
+ localVideoView.init(rootEglBase.getEglBaseContext(), null);
+ localVideoView.setMirror(true);
+ localVideoView.setEnableHardwareScaler(true);
+ localVideoView.setZOrderMediaOverlay(true);
+ }
+
+ public void createRemoteParticipantVideo(final RemoteParticipant remoteParticipant) {
+ Handler mainHandler = new Handler(this.getMainLooper());
+ Runnable myRunnable = () -> {
+ View rowView = this.getLayoutInflater().inflate(R.layout.peer_video, null);
+ LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
+ lp.setMargins(0, 0, 0, 20);
+ rowView.setLayoutParams(lp);
+ int rowId = View.generateViewId();
+ rowView.setId(rowId);
+ views_container.addView(rowView);
+ SurfaceViewRenderer videoView = (SurfaceViewRenderer) ((ViewGroup) rowView).getChildAt(0);
+ remoteParticipant.setVideoView(videoView);
+ videoView.setMirror(false);
+ EglBase rootEglBase = EglBase.create();
+ videoView.init(rootEglBase.getEglBaseContext(), null);
+ videoView.setZOrderMediaOverlay(true);
+ View textView = ((ViewGroup) rowView).getChildAt(1);
+ remoteParticipant.setParticipantNameText((TextView) textView);
+ remoteParticipant.setView(rowView);
+
+ remoteParticipant.getParticipantNameText().setText(remoteParticipant.getParticipantName());
+ remoteParticipant.getParticipantNameText().setPadding(20, 3, 20, 3);
+ };
+ mainHandler.post(myRunnable);
+ }
+
+ public void setRemoteMediaStream(MediaStream stream, final RemoteParticipant remoteParticipant) {
+ final VideoTrack videoTrack = stream.videoTracks.get(0);
+ runOnUiThread(() -> {
+ remoteParticipant.getVideoView().setVisibility(View.VISIBLE);
+ videoTrack.addSink(remoteParticipant.getVideoView());
+ MediaStream mediaStream = session.getPeerConnectionFactory().createLocalMediaStream("105");
+ remoteParticipant.setMediaStream(mediaStream);
+ mediaStream.addTrack(session.getLocalParticipant().getAudioTrack());
+ mediaStream.addTrack(session.getLocalParticipant().getVideoTrack());
+ remoteParticipant.getPeerConnection().removeStream(mediaStream);
+ remoteParticipant.getPeerConnection().addStream(mediaStream);
+ });
+ }
+
+ public void enableLeaveButton() {
+ runOnUiThread(() -> {
+ start_finish_call.setEnabled(true);
+ });
+ }
+
+ public void leaveSession() {
+ this.session.leaveSession();
+ localVideoView.clearImage();
+ localVideoView.release();
+ start_finish_call.setText(getResources().getString(R.string.start_button));
+ openvidu_url.setEnabled(true);
+ openvidu_url.setFocusableInTouchMode(true);
+ openvidu_secret.setEnabled(true);
+ openvidu_secret.setFocusableInTouchMode(true);
+ session_name.setEnabled(true);
+ session_name.setFocusableInTouchMode(true);
+ participant_name.setEnabled(true);
+ participant_name.setFocusableInTouchMode(true);
+ main_participant.setText(null);
+ main_participant.setPadding(0, 0, 0, 0);
+ }
+
+ private boolean arePermissionGranted() {
+ return (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_DENIED) &&
+ (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_DENIED);
+ }
+
+ @Override
+ protected void onDestroy() {
+ leaveSession();
+ super.onDestroy();
+ }
+
+ @Override
+ public void onBackPressed() {
+ leaveSession();
+ super.onBackPressed();
+ }
+
+ @Override
+ protected void onStop() {
+ leaveSession();
+ super.onStop();
+ }
+
+}
diff --git a/openvidu-android/app/src/main/java/com/example/openviduandroid/constants/JsonConstants.java b/openvidu-android/app/src/main/java/com/example/openviduandroid/constants/JsonConstants.java
new file mode 100644
index 00000000..71cf868a
--- /dev/null
+++ b/openvidu-android/app/src/main/java/com/example/openviduandroid/constants/JsonConstants.java
@@ -0,0 +1,50 @@
+package com.example.openviduandroid.constants;
+
+public final class JsonConstants {
+
+ // RPC incoming methods
+ public static final String PARTICIPANT_JOINED = "participantJoined";
+ public static final String PARTICIPANT_PUBLISHED = "participantPublished";
+ public static final String PARTICIPANT_UNPUBLISHED = "participantUnpublished";
+ public static final String PARTICIPANT_LEFT = "participantLeft";
+ public static final String PARTICIPANT_EVICTED = "participantEvicted";
+ public static final String RECORDING_STARTED = "recordingStarted";
+ public static final String RECORDING_STOPPED = "recordingStopped";
+ public static final String SEND_MESSAGE = "sendMessage";
+ public static final String STREAM_PROPERTY_CHANGED = "streamPropertyChanged";
+ public static final String FILTER_EVENT_DISPATCHED = "filterEventDispatched";
+ public static final String ICE_CANDIDATE = "iceCandidate";
+ public static final String MEDIA_ERROR = "mediaError";
+
+ // RPC outgoing methods
+ public static final String JOINROOM_METHOD = "joinRoom";
+ public static final String LEAVEROOM_METHOD = "leaveRoom";
+ public static final String PUBLISHVIDEO_METHOD = "publishVideo";
+ public static final String ONICECANDIDATE_METHOD = "onIceCandidate";
+ public static final String RECEIVEVIDEO_METHOD = "receiveVideoFrom";
+ public static final String UNSUBSCRIBEFROMVIDEO_METHOD = "unsubscribeFromVideo";
+ public static final String SENDMESSAGE_ROOM_METHOD = "sendMessage";
+ public static final String UNPUBLISHVIDEO_METHOD = "unpublishVideo";
+ public static final String STREAMPROPERTYCHANGED_METHOD = "streamPropertyChanged";
+ public static final String FORCEDISCONNECT_METHOD = "forceDisconnect";
+ public static final String FORCEUNPUBLISH_METHOD = "forceUnpublish";
+ public static final String APPLYFILTER_METHOD = "applyFilter";
+ public static final String EXECFILTERMETHOD_METHOD = "execFilterMethod";
+ public static final String REMOVEFILTER_METHOD = "removeFilter";
+ public static final String ADDFILTEREVENTLISTENER_METHOD = "addFilterEventListener";
+ public static final String REMOVEFILTEREVENTLISTENER_METHOD = "removeFilterEventListener";
+ public static final String PING_METHOD = "ping";
+
+ public static final String JSON_RPCVERSION = "2.0";
+
+ public static final String VALUE = "value";
+ public static final String PARAMS = "params";
+ public static final String METHOD = "method";
+ public static final String ID = "id";
+ public static final String RESULT = "result";
+
+ public static final String SESSION_ID = "sessionId";
+ public static final String SDP_ANSWER = "sdpAnswer";
+ public static final String METADATA = "metadata";
+
+}
\ No newline at end of file
diff --git a/openvidu-android/app/src/main/java/com/example/openviduandroid/fragments/PermissionsDialogFragment.java b/openvidu-android/app/src/main/java/com/example/openviduandroid/fragments/PermissionsDialogFragment.java
new file mode 100644
index 00000000..b4787524
--- /dev/null
+++ b/openvidu-android/app/src/main/java/com/example/openviduandroid/fragments/PermissionsDialogFragment.java
@@ -0,0 +1,28 @@
+package com.example.openviduandroid.fragments;
+
+import android.app.Dialog;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.DialogFragment;
+
+import com.example.openviduandroid.activities.SessionActivity;
+import com.example.openviduandroid.R;
+
+public class PermissionsDialogFragment extends DialogFragment {
+
+ private static final String TAG = "PermissionsDialog";
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setTitle(R.string.permissions_dialog_title);
+ builder.setMessage(R.string.no_permissions_granted)
+ .setPositiveButton(R.string.accept_permissions_dialog, (dialog, id) -> ((SessionActivity) getActivity()).askForPermissions())
+ .setNegativeButton(R.string.cancel_dialog, (dialog, id) -> Log.i(TAG, "User cancelled Permissions Dialog"));
+ return builder.create();
+ }
+}
\ No newline at end of file
diff --git a/openvidu-android/app/src/main/java/com/example/openviduandroid/observers/CustomPeerConnectionObserver.java b/openvidu-android/app/src/main/java/com/example/openviduandroid/observers/CustomPeerConnectionObserver.java
new file mode 100644
index 00000000..7e5e56c6
--- /dev/null
+++ b/openvidu-android/app/src/main/java/com/example/openviduandroid/observers/CustomPeerConnectionObserver.java
@@ -0,0 +1,75 @@
+package com.example.openviduandroid.observers;
+
+import android.util.Log;
+
+import org.webrtc.DataChannel;
+import org.webrtc.IceCandidate;
+import org.webrtc.MediaStream;
+import org.webrtc.PeerConnection;
+import org.webrtc.RtpReceiver;
+
+import java.util.Arrays;
+
+public class CustomPeerConnectionObserver implements PeerConnection.Observer {
+
+ private String TAG = "PeerConnection";
+
+ public CustomPeerConnectionObserver(String id) {
+ this.TAG = this.TAG + "-" + id;
+ }
+
+ @Override
+ public void onSignalingChange(PeerConnection.SignalingState signalingState) {
+ Log.d(TAG, "onSignalingChange() called with: signalingState = [" + signalingState + "]");
+ }
+
+ @Override
+ public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {
+ Log.d(TAG, "onIceConnectionChange() called with: iceConnectionState = [" + iceConnectionState + "]");
+ }
+
+ @Override
+ public void onIceConnectionReceivingChange(boolean b) {
+ Log.d(TAG, "onIceConnectionReceivingChange() called with: b = [" + b + "]");
+ }
+
+ @Override
+ public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) {
+ Log.d(TAG, "onIceGatheringChange() called with: iceGatheringState = [" + iceGatheringState + "]");
+ }
+
+ @Override
+ public void onIceCandidate(IceCandidate iceCandidate) {
+ Log.d(TAG, "onIceCandidate() called with: iceCandidate = [" + iceCandidate + "]");
+ }
+
+ @Override
+ public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) {
+ Log.d(TAG, "onIceCandidatesRemoved() called with: iceCandidates = [" + Arrays.toString(iceCandidates) + "]");
+ }
+
+ @Override
+ public void onAddStream(MediaStream mediaStream) {
+ Log.d(TAG, "onAddStream() called with: mediaStream = [" + mediaStream + "]");
+ }
+
+ @Override
+ public void onRemoveStream(MediaStream mediaStream) {
+ Log.d(TAG, "onRemoveStream() called with: mediaStream = [" + mediaStream + "]");
+ }
+
+ @Override
+ public void onDataChannel(DataChannel dataChannel) {
+ Log.d(TAG, "onDataChannel() called with: dataChannel = [" + dataChannel + "]");
+ }
+
+ @Override
+ public void onRenegotiationNeeded() {
+ Log.d(TAG, "onRenegotiationNeeded() called");
+ }
+
+ @Override
+ public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) {
+ Log.d(TAG, "onAddTrack() called with: mediaStreams = [" + Arrays.toString(mediaStreams) + "]");
+ }
+}
diff --git a/openvidu-android/app/src/main/java/com/example/openviduandroid/observers/CustomSdpObserver.java b/openvidu-android/app/src/main/java/com/example/openviduandroid/observers/CustomSdpObserver.java
new file mode 100644
index 00000000..1bce70ce
--- /dev/null
+++ b/openvidu-android/app/src/main/java/com/example/openviduandroid/observers/CustomSdpObserver.java
@@ -0,0 +1,39 @@
+package com.example.openviduandroid.observers;
+
+import android.util.Log;
+
+import org.webrtc.SdpObserver;
+import org.webrtc.SessionDescription;
+
+public class CustomSdpObserver implements SdpObserver {
+
+ private String tag;
+
+ public CustomSdpObserver(String tag) {
+ this.tag = "SdpObserver-" + tag;
+ }
+
+ private void log(String s) {
+ Log.d(tag, s);
+ }
+
+ @Override
+ public void onCreateSuccess(SessionDescription sessionDescription) {
+ log("onCreateSuccess " + sessionDescription);
+ }
+
+ @Override
+ public void onSetSuccess() {
+ log("onSetSuccess ");
+ }
+
+ @Override
+ public void onCreateFailure(String s) {
+ log("onCreateFailure " + s);
+ }
+
+ @Override
+ public void onSetFailure(String s) {
+ log("onSetFailure " + s);
+ }
+}
diff --git a/openvidu-android/app/src/main/java/com/example/openviduandroid/openvidu/LocalParticipant.java b/openvidu-android/app/src/main/java/com/example/openviduandroid/openvidu/LocalParticipant.java
new file mode 100644
index 00000000..06504dc6
--- /dev/null
+++ b/openvidu-android/app/src/main/java/com/example/openviduandroid/openvidu/LocalParticipant.java
@@ -0,0 +1,116 @@
+package com.example.openviduandroid.openvidu;
+
+import android.content.Context;
+
+import org.webrtc.AudioSource;
+import org.webrtc.Camera1Enumerator;
+import org.webrtc.EglBase;
+import org.webrtc.IceCandidate;
+import org.webrtc.MediaConstraints;
+import org.webrtc.PeerConnectionFactory;
+import org.webrtc.SessionDescription;
+import org.webrtc.SurfaceTextureHelper;
+import org.webrtc.SurfaceViewRenderer;
+import org.webrtc.VideoCapturer;
+import org.webrtc.VideoSource;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+public class LocalParticipant extends Participant {
+
+ private Context context;
+ private SurfaceViewRenderer localVideoView;
+ private SurfaceTextureHelper surfaceTextureHelper;
+ private VideoCapturer videoCapturer;
+
+ private Collection localIceCandidates;
+ private SessionDescription localSessionDescription;
+
+ public LocalParticipant(String participantName, Session session, Context context, SurfaceViewRenderer localVideoView) {
+ super(participantName, session);
+ this.localVideoView = localVideoView;
+ this.localVideoView = localVideoView;
+ this.context = context;
+ this.participantName = participantName;
+ this.localIceCandidates = new ArrayList<>();
+ session.setLocalParticipant(this);
+ }
+
+ public void startCamera() {
+
+ final EglBase.Context eglBaseContext = EglBase.create().getEglBaseContext();
+ PeerConnectionFactory peerConnectionFactory = this.session.getPeerConnectionFactory();
+
+ // create AudioSource
+ AudioSource audioSource = peerConnectionFactory.createAudioSource(new MediaConstraints());
+ this.audioTrack = peerConnectionFactory.createAudioTrack("101", audioSource);
+
+ surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", eglBaseContext);
+ // create VideoCapturer
+ VideoCapturer videoCapturer = createCameraCapturer();
+ VideoSource videoSource = peerConnectionFactory.createVideoSource(videoCapturer.isScreencast());
+ videoCapturer.initialize(surfaceTextureHelper, context, videoSource.getCapturerObserver());
+ videoCapturer.startCapture(480, 640, 30);
+
+ // create VideoTrack
+ this.videoTrack = peerConnectionFactory.createVideoTrack("100", videoSource);
+ // display in localView
+ this.videoTrack.addSink(localVideoView);
+ }
+
+ private VideoCapturer createCameraCapturer() {
+ Camera1Enumerator enumerator = new Camera1Enumerator(false);
+ final String[] deviceNames = enumerator.getDeviceNames();
+
+ // Try to find front facing camera
+ for (String deviceName : deviceNames) {
+ if (enumerator.isFrontFacing(deviceName)) {
+ videoCapturer = enumerator.createCapturer(deviceName, null);
+ if (videoCapturer != null) {
+ return videoCapturer;
+ }
+ }
+ }
+ // Front facing camera not found, try something else
+ for (String deviceName : deviceNames) {
+ if (!enumerator.isFrontFacing(deviceName)) {
+ videoCapturer = enumerator.createCapturer(deviceName, null);
+ if (videoCapturer != null) {
+ return videoCapturer;
+ }
+ }
+ }
+ return null;
+ }
+
+ public void storeIceCandidate(IceCandidate iceCandidate) {
+ localIceCandidates.add(iceCandidate);
+ }
+
+ public Collection getLocalIceCandidates() {
+ return this.localIceCandidates;
+ }
+
+ public void storeLocalSessionDescription(SessionDescription sessionDescription) {
+ localSessionDescription = sessionDescription;
+ }
+
+ public SessionDescription getLocalSessionDescription() {
+ return this.localSessionDescription;
+ }
+
+ @Override
+ public void dispose() {
+ super.dispose();
+ if (videoTrack != null) {
+ videoTrack.removeSink(localVideoView);
+ videoCapturer.dispose();
+ videoCapturer = null;
+ }
+ if (surfaceTextureHelper != null) {
+ surfaceTextureHelper.dispose();
+ surfaceTextureHelper = null;
+ }
+ }
+}
diff --git a/openvidu-android/app/src/main/java/com/example/openviduandroid/openvidu/Participant.java b/openvidu-android/app/src/main/java/com/example/openviduandroid/openvidu/Participant.java
new file mode 100644
index 00000000..d21760dd
--- /dev/null
+++ b/openvidu-android/app/src/main/java/com/example/openviduandroid/openvidu/Participant.java
@@ -0,0 +1,93 @@
+package com.example.openviduandroid.openvidu;
+
+import android.util.Log;
+
+import org.webrtc.AudioTrack;
+import org.webrtc.IceCandidate;
+import org.webrtc.MediaStream;
+import org.webrtc.PeerConnection;
+import org.webrtc.VideoTrack;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public abstract class Participant {
+
+ protected String connectionId;
+ protected String participantName;
+ protected Session session;
+ protected List iceCandidateList = new ArrayList<>();
+ protected PeerConnection peerConnection;
+ protected AudioTrack audioTrack;
+ protected VideoTrack videoTrack;
+ protected MediaStream mediaStream;
+
+ public Participant(String participantName, Session session) {
+ this.participantName = participantName;
+ this.session = session;
+ }
+
+ public Participant(String connectionId, String participantName, Session session) {
+ this.connectionId = connectionId;
+ this.participantName = participantName;
+ this.session = session;
+ }
+
+ public String getConnectionId() {
+ return this.connectionId;
+ }
+
+ public void setConnectionId(String connectionId) {
+ this.connectionId = connectionId;
+ }
+
+ public String getParticipantName() {
+ return this.participantName;
+ }
+
+ public List getIceCandidateList() {
+ return this.iceCandidateList;
+ }
+
+ public PeerConnection getPeerConnection() {
+ return peerConnection;
+ }
+
+ public void setPeerConnection(PeerConnection peerConnection) {
+ this.peerConnection = peerConnection;
+ }
+
+ public AudioTrack getAudioTrack() {
+ return this.audioTrack;
+ }
+
+ public void setAudioTrack(AudioTrack audioTrack) {
+ this.audioTrack = audioTrack;
+ }
+
+ public VideoTrack getVideoTrack() {
+ return this.videoTrack;
+ }
+
+ public void setVideoTrack(VideoTrack videoTrack) {
+ this.videoTrack = videoTrack;
+ }
+
+ public MediaStream getMediaStream() {
+ return this.mediaStream;
+ }
+
+ public void setMediaStream(MediaStream mediaStream) {
+ this.mediaStream = mediaStream;
+ }
+
+ public void dispose() {
+ if (this.peerConnection != null) {
+ try {
+ this.peerConnection.dispose();
+ } catch (IllegalStateException e) {
+ Log.e("Dispose PeerConnection", e.getMessage());
+ }
+ }
+ }
+}
diff --git a/openvidu-android/app/src/main/java/com/example/openviduandroid/openvidu/RemoteParticipant.java b/openvidu-android/app/src/main/java/com/example/openviduandroid/openvidu/RemoteParticipant.java
new file mode 100644
index 00000000..e07b6ebc
--- /dev/null
+++ b/openvidu-android/app/src/main/java/com/example/openviduandroid/openvidu/RemoteParticipant.java
@@ -0,0 +1,47 @@
+package com.example.openviduandroid.openvidu;
+
+import android.view.View;
+import android.widget.TextView;
+
+import org.webrtc.SurfaceViewRenderer;
+
+public class RemoteParticipant extends Participant {
+
+ private View view;
+ private SurfaceViewRenderer videoView;
+ private TextView participantNameText;
+
+ public RemoteParticipant(String connectionId, String participantName, Session session) {
+ super(connectionId, participantName, session);
+ this.session.addRemoteParticipant(this);
+ }
+
+ public View getView() {
+ return this.view;
+ }
+
+ public void setView(View view) {
+ this.view = view;
+ }
+
+ public SurfaceViewRenderer getVideoView() {
+ return this.videoView;
+ }
+
+ public void setVideoView(SurfaceViewRenderer videoView) {
+ this.videoView = videoView;
+ }
+
+ public TextView getParticipantNameText() {
+ return this.participantNameText;
+ }
+
+ public void setParticipantNameText(TextView participantNameText) {
+ this.participantNameText = participantNameText;
+ }
+
+ @Override
+ public void dispose() {
+ super.dispose();
+ }
+}
diff --git a/openvidu-android/app/src/main/java/com/example/openviduandroid/openvidu/Session.java b/openvidu-android/app/src/main/java/com/example/openviduandroid/openvidu/Session.java
new file mode 100644
index 00000000..81c88be1
--- /dev/null
+++ b/openvidu-android/app/src/main/java/com/example/openviduandroid/openvidu/Session.java
@@ -0,0 +1,198 @@
+package com.example.openviduandroid.openvidu;
+
+import android.util.Log;
+import android.view.View;
+import android.widget.LinearLayout;
+
+import com.example.openviduandroid.activities.SessionActivity;
+import com.example.openviduandroid.constants.JsonConstants;
+import com.example.openviduandroid.observers.CustomPeerConnectionObserver;
+import com.example.openviduandroid.observers.CustomSdpObserver;
+import com.example.openviduandroid.websocket.CustomWebSocket;
+
+import org.webrtc.IceCandidate;
+import org.webrtc.MediaConstraints;
+import org.webrtc.MediaStream;
+import org.webrtc.PeerConnection;
+import org.webrtc.PeerConnectionFactory;
+import org.webrtc.SessionDescription;
+import org.webrtc.SoftwareVideoDecoderFactory;
+import org.webrtc.SoftwareVideoEncoderFactory;
+import org.webrtc.VideoDecoderFactory;
+import org.webrtc.VideoEncoderFactory;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+public class Session {
+
+ private LocalParticipant localParticipant;
+ private Map remoteParticipants = new HashMap<>();
+ private String id;
+ private String token;
+ private LinearLayout views_container;
+ private PeerConnectionFactory peerConnectionFactory;
+ private CustomWebSocket websocket;
+ private SessionActivity activity;
+
+ public Session(String id, String token, LinearLayout views_container, SessionActivity activity) {
+ this.id = id;
+ this.token = token;
+ this.views_container = views_container;
+ this.activity = activity;
+
+ PeerConnectionFactory.InitializationOptions.Builder optionsBuilder = PeerConnectionFactory.InitializationOptions.builder(activity.getApplicationContext());
+ optionsBuilder.setEnableInternalTracer(true);
+ PeerConnectionFactory.InitializationOptions opt = optionsBuilder.createInitializationOptions();
+ PeerConnectionFactory.initialize(opt);
+ PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
+
+ final VideoEncoderFactory encoderFactory;
+ final VideoDecoderFactory decoderFactory;
+ encoderFactory = new SoftwareVideoEncoderFactory();
+ decoderFactory = new SoftwareVideoDecoderFactory();
+
+ peerConnectionFactory = PeerConnectionFactory.builder()
+ .setVideoEncoderFactory(encoderFactory)
+ .setVideoDecoderFactory(decoderFactory)
+ .setOptions(options)
+ .createPeerConnectionFactory();
+ }
+
+ public void setWebSocket(CustomWebSocket websocket) {
+ this.websocket = websocket;
+ }
+
+ public PeerConnection createLocalPeerConnection() {
+ final List iceServers = new ArrayList<>();
+ PeerConnection.IceServer iceServer = PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer();
+ iceServers.add(iceServer);
+
+ PeerConnection peerConnection = peerConnectionFactory.createPeerConnection(iceServers, new CustomPeerConnectionObserver("local") {
+ @Override
+ public void onIceCandidate(IceCandidate iceCandidate) {
+ super.onIceCandidate(iceCandidate);
+ websocket.onIceCandidate(iceCandidate, localParticipant.getConnectionId());
+ }
+ });
+
+ return peerConnection;
+ }
+
+ public void createRemotePeerConnection(final String connectionId) {
+ final List iceServers = new ArrayList<>();
+ PeerConnection.IceServer iceServer = PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer();
+ iceServers.add(iceServer);
+
+ PeerConnection peerConnection = peerConnectionFactory.createPeerConnection(iceServers, new CustomPeerConnectionObserver("remotePeerCreation") {
+ @Override
+ public void onIceCandidate(IceCandidate iceCandidate) {
+ super.onIceCandidate(iceCandidate);
+ websocket.onIceCandidate(iceCandidate, connectionId);
+ }
+
+ @Override
+ public void onAddStream(MediaStream mediaStream) {
+ super.onAddStream(mediaStream);
+ activity.setRemoteMediaStream(mediaStream, remoteParticipants.get(connectionId));
+ }
+
+ @Override
+ public void onSignalingChange(PeerConnection.SignalingState signalingState) {
+ if (PeerConnection.SignalingState.STABLE.equals(signalingState)) {
+ final RemoteParticipant remoteParticipant = remoteParticipants.get(connectionId);
+ Iterator it = remoteParticipant.getIceCandidateList().iterator();
+ while (it.hasNext()) {
+ IceCandidate candidate = it.next();
+ remoteParticipant.getPeerConnection().addIceCandidate(candidate);
+ it.remove();
+ }
+ }
+ }
+ });
+
+ MediaStream mediaStream = peerConnectionFactory.createLocalMediaStream("105");
+ mediaStream.addTrack(localParticipant.getAudioTrack());
+ mediaStream.addTrack(localParticipant.getVideoTrack());
+ peerConnection.addStream(mediaStream);
+
+ this.remoteParticipants.get(connectionId).setPeerConnection(peerConnection);
+ }
+
+ public void createLocalOffer(MediaConstraints constraints) {
+ localParticipant.getPeerConnection().createOffer(new CustomSdpObserver("local offer sdp") {
+ @Override
+ public void onCreateSuccess(SessionDescription sessionDescription) {
+ super.onCreateSuccess(sessionDescription);
+ Log.i("createOffer SUCCESS", sessionDescription.toString());
+ localParticipant.getPeerConnection().setLocalDescription(new CustomSdpObserver("local set local"), sessionDescription);
+ websocket.publishVideo(sessionDescription);
+ }
+
+ @Override
+ public void onCreateFailure(String s) {
+ Log.e("createOffer ERROR", s);
+ }
+
+ }, constraints);
+ }
+
+ public String getId() {
+ return this.id;
+ }
+
+ public String getToken() {
+ return this.token;
+ }
+
+ public LocalParticipant getLocalParticipant() {
+ return this.localParticipant;
+ }
+
+ public void setLocalParticipant(LocalParticipant localParticipant) {
+ this.localParticipant = localParticipant;
+ }
+
+ public RemoteParticipant getRemoteParticipant(String id) {
+ return this.remoteParticipants.get(id);
+ }
+
+ public PeerConnectionFactory getPeerConnectionFactory() {
+ return this.peerConnectionFactory;
+ }
+
+ public void addRemoteParticipant(RemoteParticipant remoteParticipant) {
+ this.remoteParticipants.put(remoteParticipant.getConnectionId(), remoteParticipant);
+ }
+
+ public RemoteParticipant removeRemoteParticipant(String id) {
+ return this.remoteParticipants.remove(id);
+ }
+
+ public void leaveSession() {
+ websocket.setWebsocketCancelled(true);
+ if (websocket != null) {
+ websocket.leaveRoom();
+ websocket.disconnect();
+ }
+ this.localParticipant.dispose();
+ for (RemoteParticipant remoteParticipant : remoteParticipants.values()) {
+ if (remoteParticipant.getPeerConnection() != null) {
+ remoteParticipant.getPeerConnection().close();
+ }
+ views_container.removeView(remoteParticipant.getView());
+ }
+ if (peerConnectionFactory != null) {
+ peerConnectionFactory.dispose();
+ peerConnectionFactory = null;
+ }
+ }
+
+ public void removeView(View view) {
+ this.views_container.removeView(view);
+ }
+
+}
diff --git a/openvidu-android/app/src/main/java/com/example/openviduandroid/utils/CustomHttpClient.java b/openvidu-android/app/src/main/java/com/example/openviduandroid/utils/CustomHttpClient.java
new file mode 100644
index 00000000..74d4be36
--- /dev/null
+++ b/openvidu-android/app/src/main/java/com/example/openviduandroid/utils/CustomHttpClient.java
@@ -0,0 +1,92 @@
+package com.example.openviduandroid.utils;
+
+import java.io.IOException;
+import java.security.SecureRandom;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+import okhttp3.Call;
+import okhttp3.Callback;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+
+public class CustomHttpClient {
+
+ private OkHttpClient client;
+ private String baseUrl;
+ private String basicAuth;
+
+ public CustomHttpClient(String baseUrl, String basicAuth) {
+ this.baseUrl = baseUrl.endsWith("/") ? baseUrl : baseUrl + "/";
+ this.basicAuth = basicAuth;
+
+ try {
+ // Create a trust manager that does not validate certificate chains
+ final TrustManager[] trustAllCerts = new TrustManager[]{
+ new X509TrustManager() {
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+ }
+
+ @Override
+ public X509Certificate[] getAcceptedIssuers() {
+ return null;
+ }
+ }
+ };
+
+ // Install the all-trusting trust manager
+ final SSLContext sslContext = SSLContext.getInstance("SSL");
+ sslContext.init(null, trustAllCerts, new SecureRandom());
+ // Create an ssl socket factory with our all-trusting manager
+ final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
+
+ this.client = new OkHttpClient.Builder().sslSocketFactory(sslSocketFactory, new X509TrustManager() {
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+ }
+
+ @Override
+ public X509Certificate[] getAcceptedIssuers() {
+ return new java.security.cert.X509Certificate[]{};
+ }
+ }).hostnameVerifier(new HostnameVerifier() {
+ @Override
+ public boolean verify(String hostname, SSLSession session) {
+ return true;
+ }
+ }).build();
+
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void httpCall(String url, String method, String contentType, RequestBody body, Callback callback) throws IOException {
+ url = url.startsWith("/") ? url.substring(1) : url;
+ Request request = new Request.Builder()
+ .url(this.baseUrl + url)
+ .header("Authorization", this.basicAuth).header("Content-Type", contentType).method(method, body)
+ .build();
+ Call call = client.newCall(request);
+ call.enqueue(callback);
+ }
+
+}
diff --git a/openvidu-android/app/src/main/java/com/example/openviduandroid/websocket/CustomWebSocket.java b/openvidu-android/app/src/main/java/com/example/openviduandroid/websocket/CustomWebSocket.java
new file mode 100644
index 00000000..28392d7c
--- /dev/null
+++ b/openvidu-android/app/src/main/java/com/example/openviduandroid/websocket/CustomWebSocket.java
@@ -0,0 +1,559 @@
+package com.example.openviduandroid.websocket;
+
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.example.openviduandroid.activities.SessionActivity;
+import com.example.openviduandroid.constants.JsonConstants;
+import com.example.openviduandroid.observers.CustomSdpObserver;
+import com.example.openviduandroid.openvidu.LocalParticipant;
+import com.example.openviduandroid.openvidu.Participant;
+import com.example.openviduandroid.openvidu.RemoteParticipant;
+import com.example.openviduandroid.openvidu.Session;
+import com.neovisionaries.ws.client.ThreadType;
+import com.neovisionaries.ws.client.WebSocket;
+import com.neovisionaries.ws.client.WebSocketException;
+import com.neovisionaries.ws.client.WebSocketFactory;
+import com.neovisionaries.ws.client.WebSocketFrame;
+import com.neovisionaries.ws.client.WebSocketListener;
+import com.neovisionaries.ws.client.WebSocketState;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.webrtc.IceCandidate;
+import org.webrtc.MediaConstraints;
+import org.webrtc.MediaStream;
+import org.webrtc.PeerConnection;
+import org.webrtc.SessionDescription;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+public class CustomWebSocket extends AsyncTask implements WebSocketListener {
+
+ private final String TAG = "CustomWebSocketListener";
+ private final int PING_MESSAGE_INTERVAL = 5;
+ private final TrustManager[] trustManagers = new TrustManager[]{new X509TrustManager() {
+ @Override
+ public X509Certificate[] getAcceptedIssuers() {
+ return new X509Certificate[0];
+ }
+
+ @Override
+ public void checkServerTrusted(final X509Certificate[] chain, final String authType) throws CertificateException {
+ Log.i(TAG, ": authType: " + authType);
+ }
+
+ @Override
+ public void checkClientTrusted(final X509Certificate[] chain, final String authType) throws CertificateException {
+ Log.i(TAG, ": authType: " + authType);
+ }
+ }};
+ private AtomicInteger RPC_ID = new AtomicInteger(0);
+ private AtomicInteger ID_PING = new AtomicInteger(-1);
+ private AtomicInteger ID_JOINROOM = new AtomicInteger(-1);
+ private AtomicInteger ID_LEAVEROOM = new AtomicInteger(-1);
+ private AtomicInteger ID_PUBLISHVIDEO = new AtomicInteger(-1);
+ private Map IDS_RECEIVEVIDEO = new ConcurrentHashMap<>();
+ private Set IDS_ONICECANDIDATE = Collections.newSetFromMap(new ConcurrentHashMap<>());
+ private Session session;
+ private String openviduUrl;
+ private SessionActivity activity;
+ private WebSocket websocket;
+ private boolean websocketCancelled = false;
+
+ public CustomWebSocket(Session session, String openviduUrl, SessionActivity activity) {
+ this.session = session;
+ this.openviduUrl = openviduUrl;
+ this.activity = activity;
+ }
+
+ @Override
+ public void onTextMessage(WebSocket websocket, String text) throws Exception {
+ Log.i(TAG, "Text Message " + text);
+ JSONObject json = new JSONObject(text);
+ if (json.has(JsonConstants.RESULT)) {
+ handleServerResponse(json);
+ } else {
+ handleServerEvent(json);
+ }
+ }
+
+ private void handleServerResponse(JSONObject json) throws
+ JSONException {
+ final int rpcId = json.getInt(JsonConstants.ID);
+ JSONObject result = new JSONObject(json.getString(JsonConstants.RESULT));
+
+ if (result.has("value") && result.getString("value").equals("pong")) {
+ // Response to ping
+ Log.i(TAG, "pong");
+
+ } else if (rpcId == this.ID_JOINROOM.get()) {
+ // Response to joinRoom
+ activity.enableLeaveButton();
+
+ final LocalParticipant localParticipant = this.session.getLocalParticipant();
+ final String localConnectionId = result.getString(JsonConstants.ID);
+ localParticipant.setConnectionId(localConnectionId);
+
+ PeerConnection localPeerConnection = session.createLocalPeerConnection();
+ localParticipant.setPeerConnection(localPeerConnection);
+
+ MediaStream stream = this.session.getPeerConnectionFactory().createLocalMediaStream("102");
+ stream.addTrack(localParticipant.getAudioTrack());
+ stream.addTrack(localParticipant.getVideoTrack());
+ localParticipant.getPeerConnection().addStream(stream);
+
+ MediaConstraints sdpConstraints = new MediaConstraints();
+ sdpConstraints.mandatory.add(new MediaConstraints.KeyValuePair("offerToReceiveAudio", "true"));
+ sdpConstraints.mandatory.add(new MediaConstraints.KeyValuePair("offerToReceiveVideo", "true"));
+ session.createLocalOffer(sdpConstraints);
+
+ if (result.getJSONArray(JsonConstants.VALUE).length() > 0) {
+ // There were users already connected to the session
+ addRemoteParticipantsAlreadyInRoom(result);
+ }
+
+ } else if (rpcId == this.ID_LEAVEROOM.get()) {
+ // Response to leaveRoom
+ if (websocket.isOpen()) {
+ websocket.disconnect();
+ }
+
+ } else if (rpcId == this.ID_PUBLISHVIDEO.get()) {
+ // Response to publishVideo
+ SessionDescription sessionDescription = new SessionDescription(SessionDescription.Type.ANSWER, result.getString("sdpAnswer"));
+ this.session.getLocalParticipant().getPeerConnection().setRemoteDescription(new CustomSdpObserver("localSetRemoteDesc"), sessionDescription);
+
+ } else if (this.IDS_RECEIVEVIDEO.containsKey(rpcId)) {
+ // Response to receiveVideoFrom
+ SessionDescription sessionDescription = new SessionDescription(SessionDescription.Type.ANSWER, result.getString("sdpAnswer"));
+ session.getRemoteParticipant(IDS_RECEIVEVIDEO.remove(rpcId)).getPeerConnection().setRemoteDescription(new CustomSdpObserver("remoteSetRemoteDesc"), sessionDescription);
+
+ } else if (this.IDS_ONICECANDIDATE.contains(rpcId)) {
+ // Response to onIceCandidate
+ IDS_ONICECANDIDATE.remove(rpcId);
+
+ } else {
+ Log.e(TAG, "Unrecognized server response: " + result);
+ }
+ }
+
+ public void joinRoom() {
+ Map joinRoomParams = new HashMap<>();
+ joinRoomParams.put(JsonConstants.METADATA, "{\"clientData\": \"" + this.session.getLocalParticipant().getParticipantName() + "\"}");
+ joinRoomParams.put("secret", "");
+ joinRoomParams.put("session", this.session.getId());
+ joinRoomParams.put("platform", "Android " + android.os.Build.VERSION.SDK_INT);
+ joinRoomParams.put("token", this.session.getToken());
+ this.ID_JOINROOM.set(this.sendJson(JsonConstants.JOINROOM_METHOD, joinRoomParams));
+ }
+
+ public void leaveRoom() {
+ this.ID_LEAVEROOM.set(this.sendJson(JsonConstants.LEAVEROOM_METHOD));
+ }
+
+ public void publishVideo(SessionDescription sessionDescription) {
+ Map publishVideoParams = new HashMap<>();
+ publishVideoParams.put("audioActive", "true");
+ publishVideoParams.put("videoActive", "true");
+ publishVideoParams.put("doLoopback", "false");
+ publishVideoParams.put("frameRate", "30");
+ publishVideoParams.put("hasAudio", "true");
+ publishVideoParams.put("hasVideo", "true");
+ publishVideoParams.put("typeOfVideo", "CAMERA");
+ publishVideoParams.put("videoDimensions", "{\"width\":320, \"height\":240}");
+ publishVideoParams.put("sdpOffer", sessionDescription.description);
+ this.ID_PUBLISHVIDEO.set(this.sendJson(JsonConstants.PUBLISHVIDEO_METHOD, publishVideoParams));
+ }
+
+ public void receiveVideoFrom(SessionDescription sessionDescription, RemoteParticipant remoteParticipant, String streamId) {
+ Map receiveVideoFromParams = new HashMap<>();
+ receiveVideoFromParams.put("sdpOffer", sessionDescription.description);
+ receiveVideoFromParams.put("sender", streamId);
+ this.IDS_RECEIVEVIDEO.put(this.sendJson(JsonConstants.RECEIVEVIDEO_METHOD, receiveVideoFromParams), remoteParticipant.getConnectionId());
+ }
+
+ public void onIceCandidate(IceCandidate iceCandidate, String endpointName) {
+ Map onIceCandidateParams = new HashMap<>();
+ if (endpointName != null) {
+ onIceCandidateParams.put("endpointName", endpointName);
+ }
+ onIceCandidateParams.put("candidate", iceCandidate.sdp);
+ onIceCandidateParams.put("sdpMid", iceCandidate.sdpMid);
+ onIceCandidateParams.put("sdpMLineIndex", Integer.toString(iceCandidate.sdpMLineIndex));
+ this.IDS_ONICECANDIDATE.add(this.sendJson(JsonConstants.ONICECANDIDATE_METHOD, onIceCandidateParams));
+ }
+
+ private void handleServerEvent(JSONObject json) throws JSONException {
+ if (!json.has(JsonConstants.PARAMS)) {
+ Log.e(TAG, "No params " + json.toString());
+ } else {
+ final JSONObject params = new JSONObject(json.getString(JsonConstants.PARAMS));
+ String method = json.getString(JsonConstants.METHOD);
+ switch (method) {
+ case JsonConstants.ICE_CANDIDATE:
+ iceCandidateEvent(params);
+ break;
+ case JsonConstants.PARTICIPANT_JOINED:
+ participantJoinedEvent(params);
+ break;
+ case JsonConstants.PARTICIPANT_PUBLISHED:
+ participantPublishedEvent(params);
+ break;
+ case JsonConstants.PARTICIPANT_LEFT:
+ participantLeftEvent(params);
+ break;
+ default:
+ throw new JSONException("Unknown method: " + method);
+ }
+ }
+ }
+
+ public int sendJson(String method) {
+ return this.sendJson(method, new HashMap<>());
+ }
+
+ public synchronized int sendJson(String method, Map params) {
+ final int id = RPC_ID.get();
+ JSONObject jsonObject = new JSONObject();
+ try {
+ JSONObject paramsJson = new JSONObject();
+ for (Map.Entry param : params.entrySet()) {
+ paramsJson.put(param.getKey(), param.getValue());
+ }
+ jsonObject.put("jsonrpc", JsonConstants.JSON_RPCVERSION);
+ jsonObject.put("method", method);
+ jsonObject.put("id", id);
+ jsonObject.put("params", paramsJson);
+ } catch (JSONException e) {
+ Log.i(TAG, "JSONException raised on sendJson", e);
+ return -1;
+ }
+ this.websocket.sendText(jsonObject.toString());
+ RPC_ID.incrementAndGet();
+ return id;
+ }
+
+ private void addRemoteParticipantsAlreadyInRoom(JSONObject result) throws
+ JSONException {
+ for (int i = 0; i < result.getJSONArray(JsonConstants.VALUE).length(); i++) {
+ JSONObject participantJson = result.getJSONArray(JsonConstants.VALUE).getJSONObject(i);
+ RemoteParticipant remoteParticipant = this.newRemoteParticipantAux(participantJson);
+ JSONArray streams = participantJson.getJSONArray("streams");
+ for (int j = 0; j < streams.length(); j++) {
+ JSONObject stream = streams.getJSONObject(0);
+ String streamId = stream.getString("id");
+ this.subscribeAux(remoteParticipant, streamId);
+ }
+ }
+ }
+
+ private void iceCandidateEvent(JSONObject params) throws JSONException {
+ IceCandidate iceCandidate = new IceCandidate(params.getString("sdpMid"), params.getInt("sdpMLineIndex"), params.getString("candidate"));
+ final String connectionId = params.getString("senderConnectionId");
+ boolean isRemote = !session.getLocalParticipant().getConnectionId().equals(connectionId);
+ final Participant participant = isRemote ? session.getRemoteParticipant(connectionId) : session.getLocalParticipant();
+ final PeerConnection pc = participant.getPeerConnection();
+
+ switch (pc.signalingState()) {
+ case CLOSED:
+ Log.e("saveIceCandidate error", "PeerConnection object is closed");
+ break;
+ case STABLE:
+ if (pc.getRemoteDescription() != null) {
+ participant.getPeerConnection().addIceCandidate(iceCandidate);
+ } else {
+ participant.getIceCandidateList().add(iceCandidate);
+ }
+ break;
+ default:
+ participant.getIceCandidateList().add(iceCandidate);
+ }
+ }
+
+ private void participantJoinedEvent(JSONObject params) throws JSONException {
+ this.newRemoteParticipantAux(params);
+ }
+
+ private void participantPublishedEvent(JSONObject params) throws
+ JSONException {
+ String remoteParticipantId = params.getString(JsonConstants.ID);
+ final RemoteParticipant remoteParticipant = this.session.getRemoteParticipant(remoteParticipantId);
+ final String streamId = params.getJSONArray("streams").getJSONObject(0).getString("id");
+ this.subscribeAux(remoteParticipant, streamId);
+ }
+
+ private void participantLeftEvent(JSONObject params) throws JSONException {
+ final RemoteParticipant remoteParticipant = this.session.removeRemoteParticipant(params.getString("connectionId"));
+ remoteParticipant.dispose();
+ Handler mainHandler = new Handler(activity.getMainLooper());
+ Runnable myRunnable = () -> session.removeView(remoteParticipant.getView());
+ mainHandler.post(myRunnable);
+ }
+
+ private RemoteParticipant newRemoteParticipantAux(JSONObject participantJson) throws JSONException {
+ final String connectionId = participantJson.getString(JsonConstants.ID);
+ final String participantName = new JSONObject(participantJson.getString(JsonConstants.METADATA)).getString("clientData");
+ final RemoteParticipant remoteParticipant = new RemoteParticipant(connectionId, participantName, this.session);
+ this.activity.createRemoteParticipantVideo(remoteParticipant);
+ this.session.createRemotePeerConnection(remoteParticipant.getConnectionId());
+ return remoteParticipant;
+ }
+
+ private void subscribeAux(RemoteParticipant remoteParticipant, String streamId) {
+ remoteParticipant.getPeerConnection().createOffer(new CustomSdpObserver("remote offer sdp") {
+ @Override
+ public void onCreateSuccess(SessionDescription sessionDescription) {
+ super.onCreateSuccess(sessionDescription);
+ remoteParticipant.getPeerConnection().setLocalDescription(new CustomSdpObserver("remoteSetLocalDesc"), sessionDescription);
+ receiveVideoFrom(sessionDescription, remoteParticipant, streamId);
+ }
+
+ @Override
+ public void onCreateFailure(String s) {
+ Log.e("createOffer error", s);
+ }
+ }, new MediaConstraints());
+ }
+
+ public void setWebsocketCancelled(boolean websocketCancelled) {
+ this.websocketCancelled = websocketCancelled;
+ }
+
+ public void disconnect() {
+ this.websocket.disconnect();
+ }
+
+ @Override
+ public void onStateChanged(WebSocket websocket, WebSocketState newState) throws Exception {
+ Log.i(TAG, "State changed: " + newState.name());
+ }
+
+ @Override
+ public void onConnected(WebSocket ws, Map> headers) throws
+ Exception {
+ Log.i(TAG, "Connected");
+ pingMessageHandler();
+ this.joinRoom();
+ }
+
+ @Override
+ public void onConnectError(WebSocket websocket, WebSocketException cause) throws Exception {
+ Log.e(TAG, "Connect error: " + cause);
+ }
+
+ @Override
+ public void onDisconnected(WebSocket websocket, WebSocketFrame
+ serverCloseFrame, WebSocketFrame clientCloseFrame, boolean closedByServer) throws Exception {
+ Log.e(TAG, "Disconnected " + serverCloseFrame.getCloseReason() + " " + clientCloseFrame.getCloseReason() + " " + closedByServer);
+ }
+
+ @Override
+ public void onFrame(WebSocket websocket, WebSocketFrame frame) throws Exception {
+ Log.i(TAG, "Frame");
+ }
+
+ @Override
+ public void onContinuationFrame(WebSocket websocket, WebSocketFrame frame) throws Exception {
+ Log.i(TAG, "Continuation Frame");
+ }
+
+ @Override
+ public void onTextFrame(WebSocket websocket, WebSocketFrame frame) throws Exception {
+ Log.i(TAG, "Text Frame");
+ }
+
+ @Override
+ public void onBinaryFrame(WebSocket websocket, WebSocketFrame frame) throws Exception {
+ Log.i(TAG, "Binary Frame");
+ }
+
+ @Override
+ public void onCloseFrame(WebSocket websocket, WebSocketFrame frame) throws Exception {
+ Log.i(TAG, "Close Frame");
+ }
+
+ @Override
+ public void onPingFrame(WebSocket websocket, WebSocketFrame frame) throws Exception {
+ Log.i(TAG, "Ping Frame");
+ }
+
+ @Override
+ public void onPongFrame(WebSocket websocket, WebSocketFrame frame) throws Exception {
+ Log.i(TAG, "Pong Frame");
+ }
+
+ @Override
+ public void onTextMessage(WebSocket websocket, byte[] data) throws Exception {
+
+ }
+
+ @Override
+ public void onBinaryMessage(WebSocket websocket, byte[] binary) throws Exception {
+ Log.i(TAG, "Binary Message");
+ }
+
+ @Override
+ public void onSendingFrame(WebSocket websocket, WebSocketFrame frame) throws Exception {
+ Log.i(TAG, "Sending Frame");
+ }
+
+ @Override
+ public void onFrameSent(WebSocket websocket, WebSocketFrame frame) throws Exception {
+ Log.i(TAG, "Frame sent");
+ }
+
+ @Override
+ public void onFrameUnsent(WebSocket websocket, WebSocketFrame frame) throws Exception {
+ Log.i(TAG, "Frame unsent");
+ }
+
+ @Override
+ public void onThreadCreated(WebSocket websocket, ThreadType threadType, Thread thread) throws
+ Exception {
+ Log.i(TAG, "Thread created");
+ }
+
+ @Override
+ public void onThreadStarted(WebSocket websocket, ThreadType threadType, Thread thread) throws
+ Exception {
+ Log.i(TAG, "Thread started");
+ }
+
+ @Override
+ public void onThreadStopping(WebSocket websocket, ThreadType threadType, Thread thread) throws
+ Exception {
+ Log.i(TAG, "Thread stopping");
+ }
+
+ @Override
+ public void onError(WebSocket websocket, WebSocketException cause) throws Exception {
+ Log.i(TAG, "Error!");
+ }
+
+ @Override
+ public void onFrameError(WebSocket websocket, WebSocketException cause, WebSocketFrame
+ frame) throws Exception {
+ Log.i(TAG, "Frame error!");
+ }
+
+ @Override
+ public void onMessageError(WebSocket websocket, WebSocketException
+ cause, List frames) throws Exception {
+ Log.i(TAG, "Message error! " + cause);
+ }
+
+ @Override
+ public void onMessageDecompressionError(WebSocket websocket, WebSocketException cause,
+ byte[] compressed) throws Exception {
+ Log.i(TAG, "Message decompression error!");
+ }
+
+ @Override
+ public void onTextMessageError(WebSocket websocket, WebSocketException cause, byte[] data) throws
+ Exception {
+ Log.i(TAG, "Text message error! " + cause);
+ }
+
+ @Override
+ public void onSendError(WebSocket websocket, WebSocketException cause, WebSocketFrame frame) throws
+ Exception {
+ Log.i(TAG, "Send error! " + cause);
+ }
+
+ @Override
+ public void onUnexpectedError(WebSocket websocket, WebSocketException cause) throws
+ Exception {
+ Log.i(TAG, "Unexpected error! " + cause);
+ }
+
+ @Override
+ public void handleCallbackError(WebSocket websocket, Throwable cause) throws Exception {
+ Log.e(TAG, "Handle callback error! " + cause);
+ }
+
+ @Override
+ public void onSendingHandshake(WebSocket websocket, String requestLine, List
+ headers) throws Exception {
+ Log.i(TAG, "Sending Handshake! Hello!");
+ }
+
+ private void pingMessageHandler() {
+ long initialDelay = 0L;
+ ScheduledThreadPoolExecutor executor =
+ new ScheduledThreadPoolExecutor(1);
+ executor.scheduleWithFixedDelay(() -> {
+ Map pingParams = new HashMap<>();
+ if (ID_PING.get() == -1) {
+ // First ping call
+ pingParams.put("interval", "5000");
+ }
+ ID_PING.set(sendJson(JsonConstants.PING_METHOD, pingParams));
+ }, initialDelay, PING_MESSAGE_INTERVAL, TimeUnit.SECONDS);
+ }
+
+ private String getWebSocketAddress(String openviduUrl) {
+ try {
+ URL url = new URL(openviduUrl);
+ return "wss://" + url.getHost() + ":" + url.getPort() + "/openvidu";
+ } catch (MalformedURLException e) {
+ Log.e(TAG, "Wrong URL", e);
+ e.printStackTrace();
+ return "";
+ }
+ }
+
+ @Override
+ protected Void doInBackground(SessionActivity... sessionActivities) {
+ try {
+ WebSocketFactory factory = new WebSocketFactory();
+ SSLContext sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(null, trustManagers, new java.security.SecureRandom());
+ factory.setSSLContext(sslContext);
+ factory.setVerifyHostname(false);
+ websocket = factory.createSocket(getWebSocketAddress(openviduUrl));
+ websocket.addListener(this);
+ websocket.connect();
+ } catch (KeyManagementException | NoSuchAlgorithmException | IOException | WebSocketException e) {
+ Log.e("WebSocket error", e.getMessage());
+ Handler mainHandler = new Handler(activity.getMainLooper());
+ Runnable myRunnable = () -> {
+ Toast toast = Toast.makeText(activity, e.getMessage(), Toast.LENGTH_LONG);
+ toast.show();
+ activity.leaveSession();
+ };
+ mainHandler.post(myRunnable);
+ websocketCancelled = true;
+ }
+ return null;
+ }
+
+ @Override
+ protected void onProgressUpdate(Void... progress) {
+ Log.i(TAG, "PROGRESS " + Arrays.toString(progress));
+ }
+
+}
diff --git a/openvidu-android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/openvidu-android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 00000000..1f6bb290
--- /dev/null
+++ b/openvidu-android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/openvidu-android/app/src/main/res/drawable/ic_launcher_background.xml b/openvidu-android/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 00000000..0d025f9b
--- /dev/null
+++ b/openvidu-android/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/openvidu-android/app/src/main/res/layout/activity_main.xml b/openvidu-android/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 00000000..2fcf5600
--- /dev/null
+++ b/openvidu-android/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,137 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/openvidu-android/app/src/main/res/layout/peer_video.xml b/openvidu-android/app/src/main/res/layout/peer_video.xml
new file mode 100644
index 00000000..a07f5998
--- /dev/null
+++ b/openvidu-android/app/src/main/res/layout/peer_video.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/openvidu-android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/openvidu-android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 00000000..eca70cfe
--- /dev/null
+++ b/openvidu-android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/openvidu-android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/openvidu-android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 00000000..eca70cfe
--- /dev/null
+++ b/openvidu-android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/openvidu-android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/openvidu-android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000..898f3ed5
Binary files /dev/null and b/openvidu-android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/openvidu-android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/openvidu-android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 00000000..dffca360
Binary files /dev/null and b/openvidu-android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/openvidu-android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/openvidu-android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 00000000..64ba76f7
Binary files /dev/null and b/openvidu-android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/openvidu-android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/openvidu-android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 00000000..dae5e082
Binary files /dev/null and b/openvidu-android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/openvidu-android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/openvidu-android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000..e5ed4659
Binary files /dev/null and b/openvidu-android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/openvidu-android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/openvidu-android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..14ed0af3
Binary files /dev/null and b/openvidu-android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/openvidu-android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/openvidu-android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000..b0907cac
Binary files /dev/null and b/openvidu-android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/openvidu-android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/openvidu-android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..d8ae0315
Binary files /dev/null and b/openvidu-android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/openvidu-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/openvidu-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 00000000..2c18de9e
Binary files /dev/null and b/openvidu-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/openvidu-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/openvidu-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..beed3cdd
Binary files /dev/null and b/openvidu-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/openvidu-android/app/src/main/res/values/colors.xml b/openvidu-android/app/src/main/res/values/colors.xml
new file mode 100644
index 00000000..69b22338
--- /dev/null
+++ b/openvidu-android/app/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #008577
+ #00574B
+ #D81B60
+
diff --git a/openvidu-android/app/src/main/res/values/strings.xml b/openvidu-android/app/src/main/res/values/strings.xml
new file mode 100644
index 00000000..c9abac33
--- /dev/null
+++ b/openvidu-android/app/src/main/res/values/strings.xml
@@ -0,0 +1,19 @@
+
+ WebRTCExampleApp
+ Join
+ Session Name
+ TestSession
+ Participant Name
+ Participant
+ OPENVIDU URL
+ OPENVIDU SECRET
+ https://192.168.1.106:4443/
+ MY_SECRET
+ Leave session
+ COULD NOT ESTABLISH THE CONNECTION, TRY AGAIN
+ We can not give you service without your permission
+ We need your help
+ GIVE PERMISSIONS!
+ CANCEL
+ NO INTERNET CONNECTION, PLEASE CHECK YOUR CONNECTION
+
diff --git a/openvidu-android/app/src/main/res/values/styles.xml b/openvidu-android/app/src/main/res/values/styles.xml
new file mode 100644
index 00000000..5885930d
--- /dev/null
+++ b/openvidu-android/app/src/main/res/values/styles.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/openvidu-android/app/src/test/java/com/example/openviduandroid/ExampleUnitTest.java b/openvidu-android/app/src/test/java/com/example/openviduandroid/ExampleUnitTest.java
new file mode 100644
index 00000000..6b3a398c
--- /dev/null
+++ b/openvidu-android/app/src/test/java/com/example/openviduandroid/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package com.example.openviduandroid;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file
diff --git a/openvidu-android/build.gradle b/openvidu-android/build.gradle
new file mode 100644
index 00000000..f5fb2ccc
--- /dev/null
+++ b/openvidu-android/build.gradle
@@ -0,0 +1,27 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ repositories {
+ google()
+ jcenter()
+
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.5.0'
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/openvidu-android/gradle.properties b/openvidu-android/gradle.properties
new file mode 100644
index 00000000..199d16ed
--- /dev/null
+++ b/openvidu-android/gradle.properties
@@ -0,0 +1,20 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true
+
diff --git a/openvidu-android/gradle/wrapper/gradle-wrapper.jar b/openvidu-android/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..f6b961fd
Binary files /dev/null and b/openvidu-android/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/openvidu-android/gradle/wrapper/gradle-wrapper.properties b/openvidu-android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..9ac8b710
--- /dev/null
+++ b/openvidu-android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Tue Sep 10 10:08:16 CEST 2019
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
diff --git a/openvidu-android/gradlew b/openvidu-android/gradlew
new file mode 100755
index 00000000..cccdd3d5
--- /dev/null
+++ b/openvidu-android/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/openvidu-android/gradlew.bat b/openvidu-android/gradlew.bat
new file mode 100644
index 00000000..e95643d6
--- /dev/null
+++ b/openvidu-android/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/openvidu-android/settings.gradle b/openvidu-android/settings.gradle
new file mode 100644
index 00000000..b1a1253c
--- /dev/null
+++ b/openvidu-android/settings.gradle
@@ -0,0 +1,2 @@
+include ':app'
+rootProject.name='OpenVidu Android'