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/codeStyles/codeStyleConfig.xml b/openvidu-androidx/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 000000000..a55e7a179 --- /dev/null +++ b/openvidu-androidx/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ 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..5cd135a06 --- /dev/null +++ b/openvidu-androidx/.idea/gradle.xml @@ -0,0 +1,20 @@ + + + + + + + \ No newline at end of file diff --git a/openvidu-androidx/.idea/jarRepositories.xml b/openvidu-androidx/.idea/jarRepositories.xml new file mode 100644 index 000000000..a5f05cd8c --- /dev/null +++ b/openvidu-androidx/.idea/jarRepositories.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + \ 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..b6ea2b11a --- /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..052099c90 --- /dev/null +++ b/openvidu-androidx/app/build.gradle @@ -0,0 +1,45 @@ +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 + } + buildFeatures { + viewBinding 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.squareup.okhttp3:okhttp:4.2.0' + implementation 'com.neovisionaries:nv-websocket-client:2.9' + implementation 'org.webrtc:google-webrtc:1.0.30039' + 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..b8bc14dcc --- /dev/null +++ b/openvidu-androidx/app/src/main/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + 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 000000000..08fc776e6 Binary files /dev/null and b/openvidu-androidx/app/src/main/ic_launcher-web.png differ 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..84c45b632 --- /dev/null +++ b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/activities/SessionActivity.java @@ -0,0 +1,582 @@ +package io.openvidu.openvidu_android.activities; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.os.Handler; +import android.text.Editable; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +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.Objects; +import java.util.Random; + +import io.openvidu.openvidu_android.R; +import io.openvidu.openvidu_android.databinding.ActivityMainBinding; +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.utils.OpenViduUtils; +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 { + + /** Android */ + 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 SharedPreferences sharedPreferences = null; // initialized in onCreate + + /** Logging */ + private final String TAG = "SessionActivity"; + + /** Layout */ + // View Binding for layout activity_main.xml; (initialized in onCreate) + private ActivityMainBinding activityMainBinding = null; + + /** HTTP */ + static final String METHOD_POST = "POST"; + + /** OpenVidu */ + static final String CONFIG_URL = "/config"; + static final String SESSION_URL = "/api/sessions"; + static final String TOKEN_URL = "/api/tokens"; + + private String OPENVIDU_URL; + private String OPENVIDU_SECRET; // WARNING: For example only; use login and user tokens in production + private Session session; + private CustomHttpClient httpClient; + private static boolean bAuthenticated; + + @Override + protected void onCreate(Bundle savedInstanceState) { + Log.i(TAG, "LIFECYCLE: onCreate"); + super.onCreate(savedInstanceState); + + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + + loadSharedPreferences(); + + // Initialize View Binding for activity_main and set content View + // The class name "ActivityMainBinding" is automatically generated from the layout filename activity_main + // https://developer.android.com/topic/libraries/view-binding + activityMainBinding = ActivityMainBinding.inflate(getLayoutInflater()); + View rootView = activityMainBinding.getRoot(); + setContentView( rootView ); + + getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN); + + askForPermissions(); + + generateRandomName(); + } + + private void loadSharedPreferences(){ + final String sharedPreferencesName = getString(R.string.main_shared_preferences); + sharedPreferences = getSharedPreferences(sharedPreferencesName, Context.MODE_PRIVATE); + } + + /** + * + */ + @Override + protected void onStart() { + Log.i(TAG, "LIFECYCLE: onStart"); + super.onStart(); + } + + /** + * Restore state + */ + @Override + protected void onResume() { + Log.i(TAG, "LIFECYCLE: onResume"); + super.onResume(); + + if( bAuthenticated ) { + viewToLoggedInState(); + initHttpClientUnsecure(); + }else{ + viewToLoggedOutState(); + } + + String urlString = sharedPreferences.getString(getString(R.string.sp_key_openvidu_url), getString(R.string.default_openvidu_url)); + activityMainBinding.openviduUrl.setText(urlString); + } + + /** + * Save state + */ + @Override + protected void onPause() { + Log.i(TAG, "LIFECYCLE: onPause"); + super.onPause(); + + SharedPreferences.Editor spEditor = sharedPreferences.edit(); + spEditor.putString(getSharedPrefKey(R.string.sp_openvidu_url), activityMainBinding.openviduUrl.getText().toString()); + spEditor.apply(); + } + + /** + * + */ + @Override + protected void onStop() { + Log.i(TAG, "LIFECYCLE: onStop"); + leaveSession(); + super.onStop(); + } + + /** + * + */ + @Override + protected void onDestroy() { + Log.i(TAG, "LIFECYCLE: onDestroy"); + leaveSession(); + super.onDestroy(); + } + + + /** + * Ask for permissions CAMERA and/or RECORD_AUDIO + */ + public void askForPermissions() { + boolean bCameraPermission + = ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED; + boolean bRecordAudioPermission + = ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED; + + if( !bCameraPermission && !bRecordAudioPermission ) { + ActivityCompat.requestPermissions(this, + new String[]{Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO}, MY_PERMISSIONS_REQUEST); + } else if ( !bRecordAudioPermission ){ + ActivityCompat.requestPermissions(this, + new String[]{Manifest.permission.RECORD_AUDIO}, MY_PERMISSIONS_REQUEST_RECORD_AUDIO); + } else if ( !bCameraPermission ) { + ActivityCompat.requestPermissions(this, + new String[]{Manifest.permission.CAMERA}, MY_PERMISSIONS_REQUEST_CAMERA); + } + } + + /** + * This is a pseudo-login that performs an API request to see if it was successful. + * + * @param view + */ + public void onClickLogin(View view) { + initHttpClientUnsecure(); + verifyOpenViduAuthentication(); + } + + /** + * Join or leave a session depending on the text value of id/start_finish_call. + * + * @param view + */ + public void onClickStartFinishCall(View view) { + try { + boolean bInSession = activityMainBinding.startFinishCall.getText().equals("Leave session"); + if (bInSession) { + leaveSession(); + return; + } + if (arePermissionGranted()) { + initViews(); + viewToConnectingState(); + + String sessionId = activityMainBinding.sessionName.getText().toString(); + joinOpenViduSession(sessionId); + } else { + DialogFragment permissionsFragment = new PermissionsDialogFragment(); + permissionsFragment.show(getSupportFragmentManager(), "Permissions Fragment"); + } + }catch(Exception e){ + Log.e(TAG, e.toString()); + e.printStackTrace(); + } + } + + /** + * Initialize httpClient using the server's url and secret. This is + * not a secure way to perform operations from the client-side -- convert to + * login and user tokens in production. + */ + private void initHttpClientUnsecure(){ + if( this.httpClient != null){ + this.httpClient.dispose(); + } + OPENVIDU_URL = activityMainBinding.openviduUrl.getText().toString(); + OPENVIDU_SECRET = activityMainBinding.openviduSecret.getText().toString(); + httpClient = new CustomHttpClient(OPENVIDU_URL, OpenViduUtils.getBasicAuthString(OPENVIDU_SECRET)); + } + + /** + * If authentication is successful, enable the ability to join sessions. + */ + private void verifyOpenViduAuthentication(){ + try { + httpClient.httpGet(CONFIG_URL, new Callback() { + @SuppressLint("DefaultLocale") + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException + { + if( !response.isSuccessful() ){ + Log.e(TAG, String.format("verifyOpenViduAuthentication(%d) - Response not successful", response.code())); + runOnUiThread( () -> { + Toast.makeText( getApplicationContext(), + String.format("verifyOpenViduAuthentication(%d) - Response not successful", response.code()), + Toast.LENGTH_SHORT).show(); + }); + return; + } + runOnUiThread( () -> { + Toast.makeText(getApplicationContext(), "Authentication Successful", Toast.LENGTH_SHORT).show(); + viewToLoggedInState(); + }); + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + runOnUiThread( () -> { + Toast.makeText(getApplicationContext(), "Authentication Failed", Toast.LENGTH_LONG).show(); + }); + } + + }); + }catch( IOException e ){ + e.printStackTrace(); + } + } + + /** + * Create and/or join sessionId + * + * @param sessionId The customSessionId of the session. + */ + private void joinOpenViduSession(String sessionId) { + try { + // HTTP POST sessionId to SESSION_URL + MediaType jsonMediaType = MediaType.parse("application/json; charset=utf-8"); + String contentString = String.format("{\"customSessionId\": \"%s\"}", sessionId); + RequestBody sessionBody = RequestBody.create(contentString, jsonMediaType); + this.httpClient.httpCall(SESSION_URL, METHOD_POST, "application/json", sessionBody, new Callback(){ + /** + * WARNING: response.body().string() != response.body().toString(), e.g. + * toString() = okhttp3.internal.http.RealResponseBody@fe18513 + * string() = {"id":"SessionA","createdAt":1593059100440} + * + * @param call + * @param response + * @throws IOException + */ + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException + { + Log.d(TAG, String.format("joinOpenViduSession.httpCall(SESSION_URL).onResponse(): code = %d, body = %s", response.code(), response.body().string())); + + // For example, + // If no session exists, code = 200 and response.body().string() = {"id":"SessionA","createdAt":1593059100440} + // If session already exists, code = 409 and response.body().string() is empty + if( response.code() != 200 && response.code() != 409 ) { + Log.e(TAG, "JoinOpenViduSession.httpCall(SESSION_URL) - unhandled error code"); + return; + } + + final String tokenRequestContent = "{\"session\": \"" + sessionId + "\"}"; + final RequestBody tokenRequestBody = RequestBody.create(tokenRequestContent, jsonMediaType); + final String tokenContentType = "application/json"; + httpClient.httpCall(TOKEN_URL, METHOD_POST, tokenContentType, tokenRequestBody, new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) { + if( !response.isSuccessful() ){ + Log.e(TAG, "joinOpenViduSession().httpCall(TOKEN_URL).onResponse() - Failed to get token"); + return; + } + + try { + // @requireNonNull - Throws NullPointerException with message + String jsonBodyString = Objects.requireNonNull(response.body(), "response body empty").string(); + Log.d(TAG, String.format("joinOpenViduSession().httpCall(TOKEN_URL): jsonBody = %s", jsonBodyString)); + + if( jsonBodyString.isEmpty() ){ + Log.e(TAG, "JoinOpenViduSession().httpCall(TOKEN_URL): response empty"); + return; + } + try { + JSONObject tokenJsonObject = new JSONObject(jsonBodyString); + String tokenString = tokenJsonObject.getString("token"); + onSessionTokenReceived(tokenString, sessionId); + } catch (JSONException e) { + Log.e(TAG, String.valueOf(e)); + e.printStackTrace(); + } + }catch(NullPointerException | IOException e){ + Log.e(TAG, String.valueOf(e)); + e.printStackTrace(); + } + } + + @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 onSessionTokenReceived(String token, String sessionId) { + session = new Session(sessionId, token, activityMainBinding.viewsContainer, this); + + // Initialize our local participant and start local camera + String participantName = activityMainBinding.participantName.getText().toString(); + LocalParticipant localParticipant = new LocalParticipant( + participantName, + session, + this.getApplicationContext(), + activityMainBinding.localGlSurfaceView); + localParticipant.startCamera(); + + // Update local participant view + runOnUiThread(() -> { + String participantNameString = activityMainBinding.participantName.getText().toString(); + activityMainBinding.mainParticipant.setText(participantNameString); + activityMainBinding.mainParticipant.setPadding(20, 3, 20, 3); + }); + + // Initialize and connect the websocket to OpenVidu Server + startWebSocket(); + } + + /** + * Start custom OpenVidu web socket + */ + private void startWebSocket() { + CustomWebSocket webSocket = new CustomWebSocket(session, OPENVIDU_URL, this); + webSocket.execute(); + session.setWebSocket(webSocket); + } + + /** + * On connection error, display a toast and transition the UI to disconnected state. + */ + 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 viewToLoggedOutState(){ + bAuthenticated = false; + activityMainBinding.sessionName.setEnabled(false); + activityMainBinding.participantName.setEnabled(false); + activityMainBinding.startFinishCall.setEnabled(false); + } + + /** + * + */ + private void viewToLoggedInState(){ + bAuthenticated = true; + activityMainBinding.sessionName.setEnabled(true); + activityMainBinding.participantName.setEnabled(true); + activityMainBinding.startFinishCall.setEnabled(true); + } + + public void viewToDisconnectedState() { + runOnUiThread(() -> { + activityMainBinding.localGlSurfaceView.clearImage(); + activityMainBinding.localGlSurfaceView.release(); + activityMainBinding.startFinishCall.setText(getResources().getString(R.string.start_button)); + activityMainBinding.startFinishCall.setEnabled(true); + activityMainBinding.openviduUrl.setEnabled(true); + activityMainBinding.openviduUrl.setFocusableInTouchMode(true); + activityMainBinding.openviduSecret.setEnabled(true); + activityMainBinding.openviduSecret.setFocusableInTouchMode(true); + activityMainBinding.sessionName.setEnabled(true); + activityMainBinding.sessionName.setFocusableInTouchMode(true); + activityMainBinding.participantName.setEnabled(true); + activityMainBinding.participantName.setFocusableInTouchMode(true); + activityMainBinding.mainParticipant.setText(null); + activityMainBinding.mainParticipant.setPadding(0, 0, 0, 0); + }); + } + + /** + * + */ + public void viewToConnectingState() { + runOnUiThread(() -> { + activityMainBinding.startFinishCall.setEnabled(false); + activityMainBinding.openviduUrl.setEnabled(false); + activityMainBinding.openviduUrl.setFocusable(false); + activityMainBinding.openviduSecret.setEnabled(false); + activityMainBinding.openviduSecret.setFocusable(false); + activityMainBinding.sessionName.setEnabled(false); + activityMainBinding.sessionName.setFocusable(false); + activityMainBinding.participantName.setEnabled(false); + activityMainBinding.participantName.setFocusable(false); + }); + } + + /** + * + */ + public void viewToConnectedState() { + runOnUiThread(() -> { + activityMainBinding.startFinishCall.setText(getResources().getString(R.string.hang_up)); + activityMainBinding.startFinishCall.setEnabled(true); + }); + } + + /** + * + * @param remoteParticipant + */ + 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); + activityMainBinding.viewsContainer.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); + } + + /** + * + * @param stream + * @param remoteParticipant + */ + 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() { + try { + this.session.leaveSession(); + this.httpClient.dispose(); + viewToDisconnectedState(); + }catch(Exception e){ + e.printStackTrace(); + } + } + + /** + * + * @return + */ + private boolean arePermissionGranted() { + return (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_DENIED) && + (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_DENIED); + } + + /** + * + */ + @Override + public void onBackPressed() { + leaveSession(); + super.onBackPressed(); + } + + /** + * Initializes the surface view for the local participant. + */ + private void initViews() { + EglBase.Context rootEglBaseContext = EglBase.create().getEglBaseContext(); + activityMainBinding.localGlSurfaceView.init(rootEglBaseContext, null); + activityMainBinding.localGlSurfaceView.setMirror(true); + activityMainBinding.localGlSurfaceView.setEnableHardwareScaler(true); + activityMainBinding.localGlSurfaceView.setZOrderMediaOverlay(true); + } + + /** + * + */ + private void generateRandomName(){ + Random random = new Random(); + int randomIndex = random.nextInt(100); + Editable randomName = activityMainBinding.participantName.getText().append(String.valueOf(randomIndex)); + activityMainBinding.participantName.setText( randomName ); + } + + private String getSharedPrefKey(int stringId) { return getSharedPrefString(stringId); } + private String getString(int stringId){ return getResources().getString(stringId); } +} 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..9f9189801 --- /dev/null +++ b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/fragments/PermissionsDialogFragment.java @@ -0,0 +1,32 @@ +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; + +/** + * This fragment display a permission request message to the user before + * invoking SessionActivity.askForPermissions + */ +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..6978c844f --- /dev/null +++ b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/utils/CustomHttpClient.java @@ -0,0 +1,105 @@ +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 httpGet(String url, Callback callback) throws IOException{ + url = url.startsWith("/") ? url.substring(1) : url; + Request request = new Request.Builder() + .url(this.baseUrl + url) + .header("Authorization", this.basicAuth) + .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/utils/OpenViduUtils.java b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/utils/OpenViduUtils.java new file mode 100644 index 000000000..bb5e7684e --- /dev/null +++ b/openvidu-androidx/app/src/main/java/io/openvidu/openvidu_android/utils/OpenViduUtils.java @@ -0,0 +1,22 @@ +package io.openvidu.openvidu_android.utils; + +import android.text.Editable; +import android.util.Base64; + +import java.util.Random; + +/** + * Contains static methods + */ +public class OpenViduUtils { + + /** + * + * @param secret + * @return + */ + public static String getBasicAuthString(String secret){ + String encodedSecretString = android.util.Base64.encodeToString(("OPENVIDUAPP:" + secret).getBytes(), Base64.DEFAULT); + return String.format("Basic %s", encodedSecretString.trim()); + } +} 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..811c8980c --- /dev/null +++ b/openvidu-androidx/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + +