From fab4e3c7bc03843146276724e6a3fab4be62c83c Mon Sep 17 00:00:00 2001 From: Michael Peacock Date: Wed, 24 Jun 2020 16:21:28 -0500 Subject: [PATCH 1/3] copied project openvidu-android to openvidu-androidx --- openvidu-androidx/.gitignore | 14 + openvidu-androidx/.idea/.name | 1 + .../.idea/codeStyles/Project.xml | 116 ++++ openvidu-androidx/.idea/gradle.xml | 17 + openvidu-androidx/.idea/misc.xml | 9 + openvidu-androidx/.idea/runConfigurations.xml | 12 + openvidu-androidx/.idea/vcs.xml | 6 + openvidu-androidx/app/.gitignore | 1 + openvidu-androidx/app/build.gradle | 44 ++ openvidu-androidx/app/proguard-rules.pro | 21 + .../ExampleInstrumentedTest.java | 27 + .../app/src/main/AndroidManifest.xml | 26 + .../app/src/main/ic_launcher-web.png | Bin 0 -> 19643 bytes .../activities/SessionActivity.java | 335 ++++++++++ .../constants/JsonConstants.java | 50 ++ .../fragments/PermissionsDialogFragment.java | 28 + .../CustomPeerConnectionObserver.java | 75 +++ .../observers/CustomSdpObserver.java | 39 ++ .../openvidu/LocalParticipant.java | 124 ++++ .../openvidu/Participant.java | 93 +++ .../openvidu/RemoteParticipant.java | 47 ++ .../openvidu_android/openvidu/Session.java | 220 +++++++ .../utils/CustomHttpClient.java | 95 +++ .../websocket/CustomWebSocket.java | 588 ++++++++++++++++++ .../drawable-v24/ic_launcher_foreground.xml | 34 + .../res/drawable/ic_launcher_background.xml | 170 +++++ .../app/src/main/res/layout/activity_main.xml | 137 ++++ .../app/src/main/res/layout/peer_video.xml | 22 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 1589 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 1907 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 3505 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1175 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 1177 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2232 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 2220 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 2746 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 4977 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 3447 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 4898 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 7902 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 5053 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 7571 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 11613 bytes .../app/src/main/res/values/colors.xml | 6 + .../res/values/ic_launcher_background.xml | 4 + .../app/src/main/res/values/strings.xml | 19 + .../app/src/main/res/values/styles.xml | 11 + .../openvidu_android/ExampleUnitTest.java | 17 + openvidu-androidx/build.gradle | 27 + openvidu-androidx/gradle.properties | 20 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + openvidu-androidx/gradlew | 172 +++++ openvidu-androidx/gradlew.bat | 84 +++ openvidu-androidx/settings.gradle | 2 + 57 files changed, 2729 insertions(+) create mode 100644 openvidu-androidx/.gitignore create mode 100644 openvidu-androidx/.idea/.name create mode 100644 openvidu-androidx/.idea/codeStyles/Project.xml create mode 100644 openvidu-androidx/.idea/gradle.xml create mode 100644 openvidu-androidx/.idea/misc.xml create mode 100644 openvidu-androidx/.idea/runConfigurations.xml create mode 100644 openvidu-androidx/.idea/vcs.xml create mode 100644 openvidu-androidx/app/.gitignore create mode 100644 openvidu-androidx/app/build.gradle create mode 100644 openvidu-androidx/app/proguard-rules.pro create mode 100644 openvidu-androidx/app/src/androidTest/java/io/openvidu/openvidu_android/ExampleInstrumentedTest.java create mode 100644 openvidu-androidx/app/src/main/AndroidManifest.xml create mode 100644 openvidu-androidx/app/src/main/ic_launcher-web.png create mode 100644 openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/activities/SessionActivity.java create mode 100644 openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/constants/JsonConstants.java create mode 100644 openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/fragments/PermissionsDialogFragment.java create mode 100644 openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/observers/CustomPeerConnectionObserver.java create mode 100644 openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/observers/CustomSdpObserver.java create mode 100644 openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/openvidu/LocalParticipant.java create mode 100644 openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/openvidu/Participant.java create mode 100644 openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/openvidu/RemoteParticipant.java create mode 100644 openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/openvidu/Session.java create mode 100644 openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/utils/CustomHttpClient.java create mode 100644 openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/websocket/CustomWebSocket.java create mode 100644 openvidu-androidx/app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 openvidu-androidx/app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 openvidu-androidx/app/src/main/res/layout/activity_main.xml create mode 100644 openvidu-androidx/app/src/main/res/layout/peer_video.xml create mode 100644 openvidu-androidx/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 openvidu-androidx/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 openvidu-androidx/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 openvidu-androidx/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 openvidu-androidx/app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 openvidu-androidx/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 openvidu-androidx/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 openvidu-androidx/app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 openvidu-androidx/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 openvidu-androidx/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 openvidu-androidx/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 openvidu-androidx/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 openvidu-androidx/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 openvidu-androidx/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 openvidu-androidx/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 openvidu-androidx/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 openvidu-androidx/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 openvidu-androidx/app/src/main/res/values/colors.xml create mode 100644 openvidu-androidx/app/src/main/res/values/ic_launcher_background.xml create mode 100644 openvidu-androidx/app/src/main/res/values/strings.xml create mode 100644 openvidu-androidx/app/src/main/res/values/styles.xml create mode 100644 openvidu-androidx/app/src/test/java/io/openvidu/openvidu_android/ExampleUnitTest.java create mode 100644 openvidu-androidx/build.gradle create mode 100644 openvidu-androidx/gradle.properties create mode 100644 openvidu-androidx/gradle/wrapper/gradle-wrapper.jar create mode 100644 openvidu-androidx/gradle/wrapper/gradle-wrapper.properties create mode 100644 openvidu-androidx/gradlew create mode 100644 openvidu-androidx/gradlew.bat create mode 100644 openvidu-androidx/settings.gradle diff --git a/openvidu-androidx/.gitignore b/openvidu-androidx/.gitignore new file mode 100644 index 000000000..603b14077 --- /dev/null +++ b/openvidu-androidx/.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-androidx/.idea/.name b/openvidu-androidx/.idea/.name new file mode 100644 index 000000000..f2ca64933 --- /dev/null +++ b/openvidu-androidx/.idea/.name @@ -0,0 +1 @@ +OpenVidu Android \ No newline at end of file diff --git a/openvidu-androidx/.idea/codeStyles/Project.xml b/openvidu-androidx/.idea/codeStyles/Project.xml new file mode 100644 index 000000000..681f41ae2 --- /dev/null +++ b/openvidu-androidx/.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-androidx/.idea/gradle.xml b/openvidu-androidx/.idea/gradle.xml new file mode 100644 index 000000000..3f5f1e0b1 --- /dev/null +++ b/openvidu-androidx/.idea/gradle.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/openvidu-androidx/.idea/misc.xml b/openvidu-androidx/.idea/misc.xml new file mode 100644 index 000000000..7bfef59df --- /dev/null +++ b/openvidu-androidx/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/openvidu-androidx/.idea/runConfigurations.xml b/openvidu-androidx/.idea/runConfigurations.xml new file mode 100644 index 000000000..7f68460d8 --- /dev/null +++ b/openvidu-androidx/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/openvidu-androidx/.idea/vcs.xml b/openvidu-androidx/.idea/vcs.xml new file mode 100644 index 000000000..6c0b86358 --- /dev/null +++ b/openvidu-androidx/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/openvidu-androidx/app/.gitignore b/openvidu-androidx/app/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/openvidu-androidx/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/openvidu-androidx/app/build.gradle b/openvidu-androidx/app/build.gradle new file mode 100644 index 000000000..389285188 --- /dev/null +++ b/openvidu-androidx/app/build.gradle @@ -0,0 +1,44 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 29 + buildToolsVersion "29.0.1" + defaultConfig { + applicationId "io.openvidu.openvidu_android" + 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' + } + debug { + debuggable true + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt') + } + } + 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.2.0' + implementation 'com.squareup.okhttp3:okhttp:4.2.0' + implementation 'com.neovisionaries:nv-websocket-client:2.9' + implementation 'org.webrtc:google-webrtc:1.0.28513' + annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.0' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' +} diff --git a/openvidu-androidx/app/proguard-rules.pro b/openvidu-androidx/app/proguard-rules.pro new file mode 100644 index 000000000..f1b424510 --- /dev/null +++ b/openvidu-androidx/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-androidx/app/src/androidTest/java/io/openvidu/openvidu_android/ExampleInstrumentedTest.java b/openvidu-androidx/app/src/androidTest/java/io/openvidu/openvidu_android/ExampleInstrumentedTest.java new file mode 100644 index 000000000..247ac721a --- /dev/null +++ b/openvidu-androidx/app/src/androidTest/java/io/openvidu/openvidu_android/ExampleInstrumentedTest.java @@ -0,0 +1,27 @@ +package io.openvidu.openvidu_android; + +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("io.openvidu.openvidu_android", appContext.getPackageName()); + } +} diff --git a/openvidu-androidx/app/src/main/AndroidManifest.xml b/openvidu-androidx/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..e752cb569 --- /dev/null +++ b/openvidu-androidx/app/src/main/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + diff --git a/openvidu-androidx/app/src/main/ic_launcher-web.png b/openvidu-androidx/app/src/main/ic_launcher-web.png new file mode 100644 index 0000000000000000000000000000000000000000..08fc776e61861e568429f22503359696d18a23fb GIT binary patch literal 19643 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 II;Vst0BJ~E9{>OV literal 0 HcmV?d00001 diff --git a/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/activities/SessionActivity.java b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/activities/SessionActivity.java new file mode 100644 index 000000000..d78755462 --- /dev/null +++ b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/activities/SessionActivity.java @@ -0,0 +1,335 @@ +package io.openvidu.openvidu_android.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 android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.DialogFragment; + +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 io.openvidu.openvidu_android.R; +import io.openvidu.openvidu_android.fragments.PermissionsDialogFragment; +import io.openvidu.openvidu_android.openvidu.LocalParticipant; +import io.openvidu.openvidu_android.openvidu.RemoteParticipant; +import io.openvidu.openvidu_android.openvidu.Session; +import io.openvidu.openvidu_android.utils.CustomHttpClient; +import io.openvidu.openvidu_android.websocket.CustomWebSocket; +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))); + } + + 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 buttonPressed(View view) { + if (start_finish_call.getText().equals(getResources().getString(R.string.hang_up))) { + // Already connected to a session + leaveSession(); + return; + } + if (arePermissionGranted()) { + initViews(); + viewToConnectingState(); + + 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()); + + String sessionId = session_name.getText().toString(); + getToken(sessionId); + } else { + DialogFragment permissionsFragment = new PermissionsDialogFragment(); + permissionsFragment.show(getSupportFragmentManager(), "Permissions Fragment"); + } + } + + private void getToken(String sessionId) { + 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) { + String responseString = null; + try { + responseString = response.body().string(); + } catch (IOException e) { + Log.e(TAG, "Error getting body", e); + } + 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(); + } + getTokenSuccess(token, sessionId); + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + Log.e(TAG, "Error POST /api/tokens", e); + connectionError(); + } + }); + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + Log.e(TAG, "Error POST /api/sessions", e); + connectionError(); + } + }); + } catch (IOException e) { + Log.e(TAG, "Error getting token", e); + e.printStackTrace(); + connectionError(); + } + } + + private void getTokenSuccess(String token, String sessionId) { + // Initialize our session + session = new Session(sessionId, token, views_container, this); + + // Initialize our local participant and start local camera + String participantName = participant_name.getText().toString(); + LocalParticipant localParticipant = new LocalParticipant(participantName, session, this.getApplicationContext(), localVideoView); + localParticipant.startCamera(); + runOnUiThread(() -> { + // Update local participant view + main_participant.setText(participant_name.getText().toString()); + main_participant.setPadding(20, 3, 20, 3); + }); + + // Initialize and connect the websocket to OpenVidu Server + startWebSocket(); + } + + private void startWebSocket() { + CustomWebSocket webSocket = new CustomWebSocket(session, OPENVIDU_URL, this); + webSocket.execute(); + session.setWebSocket(webSocket); + } + + private void connectionError() { + Runnable myRunnable = () -> { + Toast toast = Toast.makeText(this, "Error connecting to " + OPENVIDU_URL, Toast.LENGTH_LONG); + toast.show(); + viewToDisconnectedState(); + }; + new Handler(this.getMainLooper()).post(myRunnable); + } + + private void initViews() { + EglBase rootEglBase = EglBase.create(); + localVideoView.init(rootEglBase.getEglBaseContext(), null); + localVideoView.setMirror(true); + localVideoView.setEnableHardwareScaler(true); + localVideoView.setZOrderMediaOverlay(true); + } + + public void viewToDisconnectedState() { + runOnUiThread(() -> { + localVideoView.clearImage(); + localVideoView.release(); + start_finish_call.setText(getResources().getString(R.string.start_button)); + start_finish_call.setEnabled(true); + 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); + }); + } + + public void viewToConnectingState() { + runOnUiThread(() -> { + 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); + }); + } + + public void viewToConnectedState() { + runOnUiThread(() -> { + start_finish_call.setText(getResources().getString(R.string.hang_up)); + start_finish_call.setEnabled(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); + videoTrack.addSink(remoteParticipant.getVideoView()); + runOnUiThread(() -> { + remoteParticipant.getVideoView().setVisibility(View.VISIBLE); + }); + } + + public void leaveSession() { + this.session.leaveSession(); + this.httpClient.dispose(); + viewToDisconnectedState(); + } + + 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-androidx/app/src/main/java/io/openvidu/openvidu_android/constants/JsonConstants.java b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/constants/JsonConstants.java new file mode 100644 index 000000000..6d0ac2ac6 --- /dev/null +++ b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/constants/JsonConstants.java @@ -0,0 +1,50 @@ +package io.openvidu.openvidu_android.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-androidx/app/src/main/java/io/openvidu/openvidu_android/fragments/PermissionsDialogFragment.java b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/fragments/PermissionsDialogFragment.java new file mode 100644 index 000000000..f88472711 --- /dev/null +++ b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/fragments/PermissionsDialogFragment.java @@ -0,0 +1,28 @@ +package io.openvidu.openvidu_android.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 io.openvidu.openvidu_android.R; +import io.openvidu.openvidu_android.activities.SessionActivity; + +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-androidx/app/src/main/java/io/openvidu/openvidu_android/observers/CustomPeerConnectionObserver.java b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/observers/CustomPeerConnectionObserver.java new file mode 100644 index 000000000..26482628d --- /dev/null +++ b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/observers/CustomPeerConnectionObserver.java @@ -0,0 +1,75 @@ +package io.openvidu.openvidu_android.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-androidx/app/src/main/java/io/openvidu/openvidu_android/observers/CustomSdpObserver.java b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/observers/CustomSdpObserver.java new file mode 100644 index 000000000..c6824a3c9 --- /dev/null +++ b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/observers/CustomSdpObserver.java @@ -0,0 +1,39 @@ +package io.openvidu.openvidu_android.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-androidx/app/src/main/java/io/openvidu/openvidu_android/openvidu/LocalParticipant.java b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/openvidu/LocalParticipant.java new file mode 100644 index 000000000..1dc363373 --- /dev/null +++ b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/openvidu/LocalParticipant.java @@ -0,0 +1,124 @@ +package io.openvidu.openvidu_android.openvidu; + +import android.content.Context; +import android.os.Build; + +import org.webrtc.AudioSource; +import org.webrtc.Camera1Enumerator; +import org.webrtc.Camera2Enumerator; +import org.webrtc.CameraEnumerator; +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() { + CameraEnumerator enumerator; + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) { + enumerator = new Camera2Enumerator(this.context); + } else { + 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-androidx/app/src/main/java/io/openvidu/openvidu_android/openvidu/Participant.java b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/openvidu/Participant.java new file mode 100644 index 000000000..704ab032a --- /dev/null +++ b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/openvidu/Participant.java @@ -0,0 +1,93 @@ +package io.openvidu.openvidu_android.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.close(); + } catch (IllegalStateException e) { + Log.e("Dispose PeerConnection", e.getMessage()); + } + } + } +} diff --git a/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/openvidu/RemoteParticipant.java b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/openvidu/RemoteParticipant.java new file mode 100644 index 000000000..2ce739b52 --- /dev/null +++ b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/openvidu/RemoteParticipant.java @@ -0,0 +1,47 @@ +package io.openvidu.openvidu_android.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-androidx/app/src/main/java/io/openvidu/openvidu_android/openvidu/Session.java b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/openvidu/Session.java new file mode 100644 index 000000000..b71797db0 --- /dev/null +++ b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/openvidu/Session.java @@ -0,0 +1,220 @@ +package io.openvidu.openvidu_android.openvidu; + +import android.util.Log; +import android.view.View; +import android.widget.LinearLayout; + +import io.openvidu.openvidu_android.activities.SessionActivity; +import io.openvidu.openvidu_android.observers.CustomPeerConnectionObserver; +import io.openvidu.openvidu_android.observers.CustomSdpObserver; +import io.openvidu.openvidu_android.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.RtpReceiver; +import org.webrtc.RtpTransceiver; +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.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(iceServers); + rtcConfig.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.ENABLED; + rtcConfig.bundlePolicy = PeerConnection.BundlePolicy.MAXBUNDLE; + rtcConfig.rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.NEGOTIATE; + rtcConfig.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY; + rtcConfig.keyType = PeerConnection.KeyType.ECDSA; + rtcConfig.enableDtlsSrtp = true; + rtcConfig.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN; + + PeerConnection peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, 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.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(iceServers); + rtcConfig.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.ENABLED; + rtcConfig.bundlePolicy = PeerConnection.BundlePolicy.MAXBUNDLE; + rtcConfig.rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.NEGOTIATE; + rtcConfig.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY; + rtcConfig.keyType = PeerConnection.KeyType.ECDSA; + rtcConfig.enableDtlsSrtp = true; + rtcConfig.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN; + + PeerConnection peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, new CustomPeerConnectionObserver("remotePeerCreation") { + @Override + public void onIceCandidate(IceCandidate iceCandidate) { + super.onIceCandidate(iceCandidate); + websocket.onIceCandidate(iceCandidate, connectionId); + } + + @Override + public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) { + super.onAddTrack(rtpReceiver, mediaStreams); + activity.setRemoteMediaStream(mediaStreams[0], 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(); + } + } + } + }); + + peerConnection.addTrack(localParticipant.getAudioTrack());//Add audio track to create transReceiver + peerConnection.addTrack(localParticipant.getVideoTrack());//Add video track to create transReceiver + + for (RtpTransceiver transceiver : peerConnection.getTransceivers()) { + //We set both audio and video in receive only mode + transceiver.setDirection(RtpTransceiver.RtpTransceiverDirection.RECV_ONLY); + } + + 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-androidx/app/src/main/java/io/openvidu/openvidu_android/utils/CustomHttpClient.java b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/utils/CustomHttpClient.java new file mode 100644 index 000000000..a3cf387d5 --- /dev/null +++ b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/utils/CustomHttpClient.java @@ -0,0 +1,95 @@ +package io.openvidu.openvidu_android.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; + +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); + } + + public void dispose() { + this.client.dispatcher().executorService().shutdown(); + } + +} diff --git a/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/websocket/CustomWebSocket.java b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/websocket/CustomWebSocket.java new file mode 100644 index 000000000..80cb022a1 --- /dev/null +++ b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/websocket/CustomWebSocket.java @@ -0,0 +1,588 @@ +package io.openvidu.openvidu_android.websocket; + +import android.os.AsyncTask; +import android.os.Handler; +import android.util.Log; +import android.widget.Toast; + +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.RtpTransceiver; +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; + +import io.openvidu.openvidu_android.activities.SessionActivity; +import io.openvidu.openvidu_android.constants.JsonConstants; +import io.openvidu.openvidu_android.observers.CustomSdpObserver; +import io.openvidu.openvidu_android.openvidu.LocalParticipant; +import io.openvidu.openvidu_android.openvidu.Participant; +import io.openvidu.openvidu_android.openvidu.RemoteParticipant; +import io.openvidu.openvidu_android.openvidu.Session; + +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.viewToConnectedState(); + + final LocalParticipant localParticipant = this.session.getLocalParticipant(); + final String localConnectionId = result.getString(JsonConstants.ID); + localParticipant.setConnectionId(localConnectionId); + + PeerConnection localPeerConnection = session.createLocalPeerConnection(); + + localPeerConnection.addTrack(localParticipant.getAudioTrack()); + localPeerConnection.addTrack(localParticipant.getVideoTrack()); + + for (RtpTransceiver transceiver : localPeerConnection.getTransceivers()) { + transceiver.setDirection(RtpTransceiver.RtpTransceiverDirection.SEND_ONLY); + } + + localParticipant.setPeerConnection(localPeerConnection); + + 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); + try { + 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); + } + } catch (Exception e) { + //Sometimes when we enter in room the other participants have no stream + //We catch that in this way the iteration of participants doesn't stop + Log.e(TAG, "Error in addRemoteParticipantsAlreadyInRoom: " + e.getLocalizedMessage()); + } + } + } + + 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); + String participantName = ""; + if (participantJson.getString(JsonConstants.METADATA) != null) { + String jsonStringified = participantJson.getString(JsonConstants.METADATA); + try { + JSONObject json = new JSONObject(jsonStringified); + String clientData = json.getString("clientData"); + if (clientData != null) { + participantName = clientData; + } + } catch(JSONException e) { + participantName = jsonStringified; + } + } + 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) { + MediaConstraints sdpConstraints = new MediaConstraints(); + sdpConstraints.mandatory.add(new MediaConstraints.KeyValuePair("offerToReceiveAudio", "true")); + sdpConstraints.mandatory.add(new MediaConstraints.KeyValuePair("offerToReceiveVideo", "true")); + + 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); + } + }, sdpConstraints); + } + + 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); + if (url.getPort() > -1) + return "wss://" + url.getHost() + ":" + url.getPort() + "/openvidu"; + return "wss://" + url.getHost() + "/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-androidx/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/openvidu-androidx/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 000000000..1f6bb2906 --- /dev/null +++ b/openvidu-androidx/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/openvidu-androidx/app/src/main/res/drawable/ic_launcher_background.xml b/openvidu-androidx/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..0d025f9bf --- /dev/null +++ b/openvidu-androidx/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/openvidu-androidx/app/src/main/res/layout/activity_main.xml b/openvidu-androidx/app/src/main/res/layout/activity_main.xml new file mode 100644 index 000000000..a881ed5e8 --- /dev/null +++ b/openvidu-androidx/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + + +