diff --git a/openvidu-android/.gitignore b/openvidu-android/.gitignore new file mode 100644 index 00000000..603b1407 --- /dev/null +++ b/openvidu-android/.gitignore @@ -0,0 +1,14 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx diff --git a/openvidu-android/.idea/.name b/openvidu-android/.idea/.name new file mode 100644 index 00000000..f2ca6493 --- /dev/null +++ b/openvidu-android/.idea/.name @@ -0,0 +1 @@ +OpenVidu Android \ No newline at end of file diff --git a/openvidu-android/.idea/codeStyles/Project.xml b/openvidu-android/.idea/codeStyles/Project.xml new file mode 100644 index 00000000..681f41ae --- /dev/null +++ b/openvidu-android/.idea/codeStyles/Project.xml @@ -0,0 +1,116 @@ + + + + + + + +
+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:name + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + name + + ^$ + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .* + + http://schemas.android.com/apk/res/android + + + ANDROID_ATTRIBUTE_ORDER + +
+
+ + + + .* + + .* + + + BY_NAME + +
+
+
+
+
+
\ No newline at end of file diff --git a/openvidu-android/.idea/gradle.xml b/openvidu-android/.idea/gradle.xml new file mode 100644 index 00000000..d291b3d7 --- /dev/null +++ b/openvidu-android/.idea/gradle.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/openvidu-android/.idea/misc.xml b/openvidu-android/.idea/misc.xml new file mode 100644 index 00000000..7bfef59d --- /dev/null +++ b/openvidu-android/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/openvidu-android/.idea/runConfigurations.xml b/openvidu-android/.idea/runConfigurations.xml new file mode 100644 index 00000000..7f68460d --- /dev/null +++ b/openvidu-android/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/openvidu-android/app/.gitignore b/openvidu-android/app/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/openvidu-android/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/openvidu-android/app/build.gradle b/openvidu-android/app/build.gradle new file mode 100644 index 00000000..ed16e954 --- /dev/null +++ b/openvidu-android/app/build.gradle @@ -0,0 +1,39 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 29 + buildToolsVersion "29.0.1" + defaultConfig { + applicationId "com.example.openviduandroid" + minSdkVersion 21 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + multiDexEnabled true + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'com.android.support.constraint:constraint-layout:1.1.3' + implementation 'com.jakewharton:butterknife:10.1.0' + implementation 'com.squareup.okhttp3:okhttp:4.1.1' + implementation 'com.neovisionaries:nv-websocket-client:2.9' + implementation 'org.webrtc:google-webrtc:1.0.28513' + annotationProcessor 'com.jakewharton:butterknife-compiler:10.1.0' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test:runner:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' +} diff --git a/openvidu-android/app/proguard-rules.pro b/openvidu-android/app/proguard-rules.pro new file mode 100644 index 00000000..f1b42451 --- /dev/null +++ b/openvidu-android/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/openvidu-android/app/src/androidTest/java/com/example/openviduandroid/ExampleInstrumentedTest.java b/openvidu-android/app/src/androidTest/java/com/example/openviduandroid/ExampleInstrumentedTest.java new file mode 100644 index 00000000..a4148420 --- /dev/null +++ b/openvidu-android/app/src/androidTest/java/com/example/openviduandroid/ExampleInstrumentedTest.java @@ -0,0 +1,27 @@ +package com.example.openviduandroid; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + + assertEquals("com.example.openviduandroid", appContext.getPackageName()); + } +} diff --git a/openvidu-android/app/src/main/AndroidManifest.xml b/openvidu-android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..98d7bb1e --- /dev/null +++ b/openvidu-android/app/src/main/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + diff --git a/openvidu-android/app/src/main/java/com/example/openviduandroid/activities/SessionActivity.java b/openvidu-android/app/src/main/java/com/example/openviduandroid/activities/SessionActivity.java new file mode 100644 index 00000000..1522bd1f --- /dev/null +++ b/openvidu-android/app/src/main/java/com/example/openviduandroid/activities/SessionActivity.java @@ -0,0 +1,299 @@ +package com.example.openviduandroid.activities; + +import android.Manifest; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.os.Handler; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.DialogFragment; + +import com.example.openviduandroid.R; +import com.example.openviduandroid.fragments.PermissionsDialogFragment; +import com.example.openviduandroid.openvidu.LocalParticipant; +import com.example.openviduandroid.openvidu.RemoteParticipant; +import com.example.openviduandroid.openvidu.Session; +import com.example.openviduandroid.utils.CustomHttpClient; +import com.example.openviduandroid.websocket.CustomWebSocket; + +import org.jetbrains.annotations.NotNull; +import org.json.JSONException; +import org.json.JSONObject; +import org.webrtc.EglBase; +import org.webrtc.MediaStream; +import org.webrtc.SurfaceViewRenderer; +import org.webrtc.VideoTrack; + +import java.io.IOException; +import java.util.Random; + +import butterknife.BindView; +import butterknife.ButterKnife; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.MediaType; +import okhttp3.RequestBody; +import okhttp3.Response; + +public class SessionActivity extends AppCompatActivity { + + private static final int MY_PERMISSIONS_REQUEST_CAMERA = 100; + private static final int MY_PERMISSIONS_REQUEST_RECORD_AUDIO = 101; + private static final int MY_PERMISSIONS_REQUEST = 102; + private final String TAG = "SessionActivity"; + @BindView(R.id.views_container) + LinearLayout views_container; + @BindView(R.id.start_finish_call) + Button start_finish_call; + @BindView(R.id.session_name) + EditText session_name; + @BindView(R.id.participant_name) + EditText participant_name; + @BindView(R.id.openvidu_url) + EditText openvidu_url; + @BindView(R.id.openvidu_secret) + EditText openvidu_secret; + @BindView(R.id.local_gl_surface_view) + SurfaceViewRenderer localVideoView; + @BindView(R.id.main_participant) + TextView main_participant; + @BindView(R.id.peer_container) + FrameLayout peer_container; + + private String OPENVIDU_URL; + private String OPENVIDU_SECRET; + private Session session; + private CustomHttpClient httpClient; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + setContentView(R.layout.activity_main); + getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN); + askForPermissions(); + ButterKnife.bind(this); + Random random = new Random(); + int randomIndex = random.nextInt(100); + participant_name.setText(participant_name.getText().append(String.valueOf(randomIndex))); + + OPENVIDU_URL = openvidu_url.getText().toString(); + OPENVIDU_SECRET = openvidu_secret.getText().toString(); + httpClient = new CustomHttpClient(OPENVIDU_URL, "Basic " + android.util.Base64.encodeToString(("OPENVIDUAPP:" + OPENVIDU_SECRET).getBytes(), android.util.Base64.DEFAULT).trim()); + } + + public void askForPermissions() { + if ((ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) + != PackageManager.PERMISSION_GRANTED) && + (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) + != PackageManager.PERMISSION_GRANTED)) { + ActivityCompat.requestPermissions(this, + new String[]{Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO}, + MY_PERMISSIONS_REQUEST); + } else if (ContextCompat.checkSelfPermission(this, + Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this, + new String[]{Manifest.permission.RECORD_AUDIO}, + MY_PERMISSIONS_REQUEST_RECORD_AUDIO); + } else if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) + != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this, + new String[]{Manifest.permission.CAMERA}, + MY_PERMISSIONS_REQUEST_CAMERA); + } + } + + public void joinSession(View view) { + if (arePermissionGranted()) { + if (start_finish_call.getText().equals(getResources().getString(R.string.hang_up))) { + // Already in a call + leaveSession(); + return; + } + initViews(); + start_finish_call.setText(getResources().getString(R.string.hang_up)); + start_finish_call.setEnabled(false); + openvidu_url.setEnabled(false); + openvidu_url.setFocusable(false); + openvidu_secret.setEnabled(false); + openvidu_secret.setFocusable(false); + session_name.setEnabled(false); + session_name.setFocusable(false); + participant_name.setEnabled(false); + participant_name.setFocusable(false); + + String sessionId = session_name.getText().toString(); + getToken(sessionId); + } else { + DialogFragment permissionsFragment = new PermissionsDialogFragment(); + permissionsFragment.show(getSupportFragmentManager(), "Permissions Fragment"); + } + } + + private void getToken(String sessionId) { + final SessionActivity thisActivity = this; + try { + // Session Request + RequestBody sessionBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), "{\"customSessionId\": \"" + sessionId + "\"}"); + this.httpClient.httpCall("/api/sessions", "POST", "application/json", sessionBody, new Callback() { + + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + Log.d(TAG, "responseString: " + response.body().string()); + + // Token Request + RequestBody tokenBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), "{\"session\": \"" + sessionId + "\"}"); + httpClient.httpCall("/api/tokens", "POST", "application/json", tokenBody, new Callback() { + + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + final String responseString = response.body().string(); + Log.d(TAG, "responseString2: " + responseString); + JSONObject tokenJsonObject = null; + String token = null; + try { + tokenJsonObject = new JSONObject(responseString); + token = tokenJsonObject.getString("token"); + } catch (JSONException e) { + e.printStackTrace(); + } + String participantName = participant_name.getText().toString(); + session = new Session(sessionId, token, views_container, thisActivity); + LocalParticipant localParticipant = new LocalParticipant(participantName, session, thisActivity.getApplicationContext(), localVideoView); + localParticipant.startCamera(); + + runOnUiThread(() -> { + main_participant.setText(participant_name.getText().toString()); + main_participant.setPadding(20, 3, 20, 3); + }); + + CustomWebSocket webSocket = new CustomWebSocket(session, OPENVIDU_URL, thisActivity); + webSocket.execute(); + session.setWebSocket(webSocket); + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + Log.e(TAG, "Error POST /api/tokens", e); + } + }); + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + Log.e(TAG, "Error POST /api/sessions", e); + } + }); + } catch (IOException e) { + Log.e("Error getting token", e.getMessage()); + e.printStackTrace(); + } + } + + private void initViews() { + EglBase rootEglBase = EglBase.create(); + localVideoView.init(rootEglBase.getEglBaseContext(), null); + localVideoView.setMirror(true); + localVideoView.setEnableHardwareScaler(true); + localVideoView.setZOrderMediaOverlay(true); + } + + public void createRemoteParticipantVideo(final RemoteParticipant remoteParticipant) { + Handler mainHandler = new Handler(this.getMainLooper()); + Runnable myRunnable = () -> { + View rowView = this.getLayoutInflater().inflate(R.layout.peer_video, null); + LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); + lp.setMargins(0, 0, 0, 20); + rowView.setLayoutParams(lp); + int rowId = View.generateViewId(); + rowView.setId(rowId); + views_container.addView(rowView); + SurfaceViewRenderer videoView = (SurfaceViewRenderer) ((ViewGroup) rowView).getChildAt(0); + remoteParticipant.setVideoView(videoView); + videoView.setMirror(false); + EglBase rootEglBase = EglBase.create(); + videoView.init(rootEglBase.getEglBaseContext(), null); + videoView.setZOrderMediaOverlay(true); + View textView = ((ViewGroup) rowView).getChildAt(1); + remoteParticipant.setParticipantNameText((TextView) textView); + remoteParticipant.setView(rowView); + + remoteParticipant.getParticipantNameText().setText(remoteParticipant.getParticipantName()); + remoteParticipant.getParticipantNameText().setPadding(20, 3, 20, 3); + }; + mainHandler.post(myRunnable); + } + + public void setRemoteMediaStream(MediaStream stream, final RemoteParticipant remoteParticipant) { + final VideoTrack videoTrack = stream.videoTracks.get(0); + runOnUiThread(() -> { + remoteParticipant.getVideoView().setVisibility(View.VISIBLE); + videoTrack.addSink(remoteParticipant.getVideoView()); + MediaStream mediaStream = session.getPeerConnectionFactory().createLocalMediaStream("105"); + remoteParticipant.setMediaStream(mediaStream); + mediaStream.addTrack(session.getLocalParticipant().getAudioTrack()); + mediaStream.addTrack(session.getLocalParticipant().getVideoTrack()); + remoteParticipant.getPeerConnection().removeStream(mediaStream); + remoteParticipant.getPeerConnection().addStream(mediaStream); + }); + } + + public void enableLeaveButton() { + runOnUiThread(() -> { + start_finish_call.setEnabled(true); + }); + } + + public void leaveSession() { + this.session.leaveSession(); + localVideoView.clearImage(); + localVideoView.release(); + start_finish_call.setText(getResources().getString(R.string.start_button)); + openvidu_url.setEnabled(true); + openvidu_url.setFocusableInTouchMode(true); + openvidu_secret.setEnabled(true); + openvidu_secret.setFocusableInTouchMode(true); + session_name.setEnabled(true); + session_name.setFocusableInTouchMode(true); + participant_name.setEnabled(true); + participant_name.setFocusableInTouchMode(true); + main_participant.setText(null); + main_participant.setPadding(0, 0, 0, 0); + } + + private boolean arePermissionGranted() { + return (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_DENIED) && + (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_DENIED); + } + + @Override + protected void onDestroy() { + leaveSession(); + super.onDestroy(); + } + + @Override + public void onBackPressed() { + leaveSession(); + super.onBackPressed(); + } + + @Override + protected void onStop() { + leaveSession(); + super.onStop(); + } + +} diff --git a/openvidu-android/app/src/main/java/com/example/openviduandroid/constants/JsonConstants.java b/openvidu-android/app/src/main/java/com/example/openviduandroid/constants/JsonConstants.java new file mode 100644 index 00000000..71cf868a --- /dev/null +++ b/openvidu-android/app/src/main/java/com/example/openviduandroid/constants/JsonConstants.java @@ -0,0 +1,50 @@ +package com.example.openviduandroid.constants; + +public final class JsonConstants { + + // RPC incoming methods + public static final String PARTICIPANT_JOINED = "participantJoined"; + public static final String PARTICIPANT_PUBLISHED = "participantPublished"; + public static final String PARTICIPANT_UNPUBLISHED = "participantUnpublished"; + public static final String PARTICIPANT_LEFT = "participantLeft"; + public static final String PARTICIPANT_EVICTED = "participantEvicted"; + public static final String RECORDING_STARTED = "recordingStarted"; + public static final String RECORDING_STOPPED = "recordingStopped"; + public static final String SEND_MESSAGE = "sendMessage"; + public static final String STREAM_PROPERTY_CHANGED = "streamPropertyChanged"; + public static final String FILTER_EVENT_DISPATCHED = "filterEventDispatched"; + public static final String ICE_CANDIDATE = "iceCandidate"; + public static final String MEDIA_ERROR = "mediaError"; + + // RPC outgoing methods + public static final String JOINROOM_METHOD = "joinRoom"; + public static final String LEAVEROOM_METHOD = "leaveRoom"; + public static final String PUBLISHVIDEO_METHOD = "publishVideo"; + public static final String ONICECANDIDATE_METHOD = "onIceCandidate"; + public static final String RECEIVEVIDEO_METHOD = "receiveVideoFrom"; + public static final String UNSUBSCRIBEFROMVIDEO_METHOD = "unsubscribeFromVideo"; + public static final String SENDMESSAGE_ROOM_METHOD = "sendMessage"; + public static final String UNPUBLISHVIDEO_METHOD = "unpublishVideo"; + public static final String STREAMPROPERTYCHANGED_METHOD = "streamPropertyChanged"; + public static final String FORCEDISCONNECT_METHOD = "forceDisconnect"; + public static final String FORCEUNPUBLISH_METHOD = "forceUnpublish"; + public static final String APPLYFILTER_METHOD = "applyFilter"; + public static final String EXECFILTERMETHOD_METHOD = "execFilterMethod"; + public static final String REMOVEFILTER_METHOD = "removeFilter"; + public static final String ADDFILTEREVENTLISTENER_METHOD = "addFilterEventListener"; + public static final String REMOVEFILTEREVENTLISTENER_METHOD = "removeFilterEventListener"; + public static final String PING_METHOD = "ping"; + + public static final String JSON_RPCVERSION = "2.0"; + + public static final String VALUE = "value"; + public static final String PARAMS = "params"; + public static final String METHOD = "method"; + public static final String ID = "id"; + public static final String RESULT = "result"; + + public static final String SESSION_ID = "sessionId"; + public static final String SDP_ANSWER = "sdpAnswer"; + public static final String METADATA = "metadata"; + +} \ No newline at end of file diff --git a/openvidu-android/app/src/main/java/com/example/openviduandroid/fragments/PermissionsDialogFragment.java b/openvidu-android/app/src/main/java/com/example/openviduandroid/fragments/PermissionsDialogFragment.java new file mode 100644 index 00000000..b4787524 --- /dev/null +++ b/openvidu-android/app/src/main/java/com/example/openviduandroid/fragments/PermissionsDialogFragment.java @@ -0,0 +1,28 @@ +package com.example.openviduandroid.fragments; + +import android.app.Dialog; +import android.os.Bundle; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; + +import com.example.openviduandroid.activities.SessionActivity; +import com.example.openviduandroid.R; + +public class PermissionsDialogFragment extends DialogFragment { + + private static final String TAG = "PermissionsDialog"; + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setTitle(R.string.permissions_dialog_title); + builder.setMessage(R.string.no_permissions_granted) + .setPositiveButton(R.string.accept_permissions_dialog, (dialog, id) -> ((SessionActivity) getActivity()).askForPermissions()) + .setNegativeButton(R.string.cancel_dialog, (dialog, id) -> Log.i(TAG, "User cancelled Permissions Dialog")); + return builder.create(); + } +} \ No newline at end of file diff --git a/openvidu-android/app/src/main/java/com/example/openviduandroid/observers/CustomPeerConnectionObserver.java b/openvidu-android/app/src/main/java/com/example/openviduandroid/observers/CustomPeerConnectionObserver.java new file mode 100644 index 00000000..7e5e56c6 --- /dev/null +++ b/openvidu-android/app/src/main/java/com/example/openviduandroid/observers/CustomPeerConnectionObserver.java @@ -0,0 +1,75 @@ +package com.example.openviduandroid.observers; + +import android.util.Log; + +import org.webrtc.DataChannel; +import org.webrtc.IceCandidate; +import org.webrtc.MediaStream; +import org.webrtc.PeerConnection; +import org.webrtc.RtpReceiver; + +import java.util.Arrays; + +public class CustomPeerConnectionObserver implements PeerConnection.Observer { + + private String TAG = "PeerConnection"; + + public CustomPeerConnectionObserver(String id) { + this.TAG = this.TAG + "-" + id; + } + + @Override + public void onSignalingChange(PeerConnection.SignalingState signalingState) { + Log.d(TAG, "onSignalingChange() called with: signalingState = [" + signalingState + "]"); + } + + @Override + public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) { + Log.d(TAG, "onIceConnectionChange() called with: iceConnectionState = [" + iceConnectionState + "]"); + } + + @Override + public void onIceConnectionReceivingChange(boolean b) { + Log.d(TAG, "onIceConnectionReceivingChange() called with: b = [" + b + "]"); + } + + @Override + public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) { + Log.d(TAG, "onIceGatheringChange() called with: iceGatheringState = [" + iceGatheringState + "]"); + } + + @Override + public void onIceCandidate(IceCandidate iceCandidate) { + Log.d(TAG, "onIceCandidate() called with: iceCandidate = [" + iceCandidate + "]"); + } + + @Override + public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) { + Log.d(TAG, "onIceCandidatesRemoved() called with: iceCandidates = [" + Arrays.toString(iceCandidates) + "]"); + } + + @Override + public void onAddStream(MediaStream mediaStream) { + Log.d(TAG, "onAddStream() called with: mediaStream = [" + mediaStream + "]"); + } + + @Override + public void onRemoveStream(MediaStream mediaStream) { + Log.d(TAG, "onRemoveStream() called with: mediaStream = [" + mediaStream + "]"); + } + + @Override + public void onDataChannel(DataChannel dataChannel) { + Log.d(TAG, "onDataChannel() called with: dataChannel = [" + dataChannel + "]"); + } + + @Override + public void onRenegotiationNeeded() { + Log.d(TAG, "onRenegotiationNeeded() called"); + } + + @Override + public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) { + Log.d(TAG, "onAddTrack() called with: mediaStreams = [" + Arrays.toString(mediaStreams) + "]"); + } +} diff --git a/openvidu-android/app/src/main/java/com/example/openviduandroid/observers/CustomSdpObserver.java b/openvidu-android/app/src/main/java/com/example/openviduandroid/observers/CustomSdpObserver.java new file mode 100644 index 00000000..1bce70ce --- /dev/null +++ b/openvidu-android/app/src/main/java/com/example/openviduandroid/observers/CustomSdpObserver.java @@ -0,0 +1,39 @@ +package com.example.openviduandroid.observers; + +import android.util.Log; + +import org.webrtc.SdpObserver; +import org.webrtc.SessionDescription; + +public class CustomSdpObserver implements SdpObserver { + + private String tag; + + public CustomSdpObserver(String tag) { + this.tag = "SdpObserver-" + tag; + } + + private void log(String s) { + Log.d(tag, s); + } + + @Override + public void onCreateSuccess(SessionDescription sessionDescription) { + log("onCreateSuccess " + sessionDescription); + } + + @Override + public void onSetSuccess() { + log("onSetSuccess "); + } + + @Override + public void onCreateFailure(String s) { + log("onCreateFailure " + s); + } + + @Override + public void onSetFailure(String s) { + log("onSetFailure " + s); + } +} diff --git a/openvidu-android/app/src/main/java/com/example/openviduandroid/openvidu/LocalParticipant.java b/openvidu-android/app/src/main/java/com/example/openviduandroid/openvidu/LocalParticipant.java new file mode 100644 index 00000000..06504dc6 --- /dev/null +++ b/openvidu-android/app/src/main/java/com/example/openviduandroid/openvidu/LocalParticipant.java @@ -0,0 +1,116 @@ +package com.example.openviduandroid.openvidu; + +import android.content.Context; + +import org.webrtc.AudioSource; +import org.webrtc.Camera1Enumerator; +import org.webrtc.EglBase; +import org.webrtc.IceCandidate; +import org.webrtc.MediaConstraints; +import org.webrtc.PeerConnectionFactory; +import org.webrtc.SessionDescription; +import org.webrtc.SurfaceTextureHelper; +import org.webrtc.SurfaceViewRenderer; +import org.webrtc.VideoCapturer; +import org.webrtc.VideoSource; + +import java.util.ArrayList; +import java.util.Collection; + +public class LocalParticipant extends Participant { + + private Context context; + private SurfaceViewRenderer localVideoView; + private SurfaceTextureHelper surfaceTextureHelper; + private VideoCapturer videoCapturer; + + private Collection localIceCandidates; + private SessionDescription localSessionDescription; + + public LocalParticipant(String participantName, Session session, Context context, SurfaceViewRenderer localVideoView) { + super(participantName, session); + this.localVideoView = localVideoView; + this.localVideoView = localVideoView; + this.context = context; + this.participantName = participantName; + this.localIceCandidates = new ArrayList<>(); + session.setLocalParticipant(this); + } + + public void startCamera() { + + final EglBase.Context eglBaseContext = EglBase.create().getEglBaseContext(); + PeerConnectionFactory peerConnectionFactory = this.session.getPeerConnectionFactory(); + + // create AudioSource + AudioSource audioSource = peerConnectionFactory.createAudioSource(new MediaConstraints()); + this.audioTrack = peerConnectionFactory.createAudioTrack("101", audioSource); + + surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", eglBaseContext); + // create VideoCapturer + VideoCapturer videoCapturer = createCameraCapturer(); + VideoSource videoSource = peerConnectionFactory.createVideoSource(videoCapturer.isScreencast()); + videoCapturer.initialize(surfaceTextureHelper, context, videoSource.getCapturerObserver()); + videoCapturer.startCapture(480, 640, 30); + + // create VideoTrack + this.videoTrack = peerConnectionFactory.createVideoTrack("100", videoSource); + // display in localView + this.videoTrack.addSink(localVideoView); + } + + private VideoCapturer createCameraCapturer() { + Camera1Enumerator enumerator = new Camera1Enumerator(false); + final String[] deviceNames = enumerator.getDeviceNames(); + + // Try to find front facing camera + for (String deviceName : deviceNames) { + if (enumerator.isFrontFacing(deviceName)) { + videoCapturer = enumerator.createCapturer(deviceName, null); + if (videoCapturer != null) { + return videoCapturer; + } + } + } + // Front facing camera not found, try something else + for (String deviceName : deviceNames) { + if (!enumerator.isFrontFacing(deviceName)) { + videoCapturer = enumerator.createCapturer(deviceName, null); + if (videoCapturer != null) { + return videoCapturer; + } + } + } + return null; + } + + public void storeIceCandidate(IceCandidate iceCandidate) { + localIceCandidates.add(iceCandidate); + } + + public Collection getLocalIceCandidates() { + return this.localIceCandidates; + } + + public void storeLocalSessionDescription(SessionDescription sessionDescription) { + localSessionDescription = sessionDescription; + } + + public SessionDescription getLocalSessionDescription() { + return this.localSessionDescription; + } + + @Override + public void dispose() { + super.dispose(); + if (videoTrack != null) { + videoTrack.removeSink(localVideoView); + videoCapturer.dispose(); + videoCapturer = null; + } + if (surfaceTextureHelper != null) { + surfaceTextureHelper.dispose(); + surfaceTextureHelper = null; + } + } +} diff --git a/openvidu-android/app/src/main/java/com/example/openviduandroid/openvidu/Participant.java b/openvidu-android/app/src/main/java/com/example/openviduandroid/openvidu/Participant.java new file mode 100644 index 00000000..d21760dd --- /dev/null +++ b/openvidu-android/app/src/main/java/com/example/openviduandroid/openvidu/Participant.java @@ -0,0 +1,93 @@ +package com.example.openviduandroid.openvidu; + +import android.util.Log; + +import org.webrtc.AudioTrack; +import org.webrtc.IceCandidate; +import org.webrtc.MediaStream; +import org.webrtc.PeerConnection; +import org.webrtc.VideoTrack; + +import java.util.ArrayList; +import java.util.List; + +public abstract class Participant { + + protected String connectionId; + protected String participantName; + protected Session session; + protected List iceCandidateList = new ArrayList<>(); + protected PeerConnection peerConnection; + protected AudioTrack audioTrack; + protected VideoTrack videoTrack; + protected MediaStream mediaStream; + + public Participant(String participantName, Session session) { + this.participantName = participantName; + this.session = session; + } + + public Participant(String connectionId, String participantName, Session session) { + this.connectionId = connectionId; + this.participantName = participantName; + this.session = session; + } + + public String getConnectionId() { + return this.connectionId; + } + + public void setConnectionId(String connectionId) { + this.connectionId = connectionId; + } + + public String getParticipantName() { + return this.participantName; + } + + public List getIceCandidateList() { + return this.iceCandidateList; + } + + public PeerConnection getPeerConnection() { + return peerConnection; + } + + public void setPeerConnection(PeerConnection peerConnection) { + this.peerConnection = peerConnection; + } + + public AudioTrack getAudioTrack() { + return this.audioTrack; + } + + public void setAudioTrack(AudioTrack audioTrack) { + this.audioTrack = audioTrack; + } + + public VideoTrack getVideoTrack() { + return this.videoTrack; + } + + public void setVideoTrack(VideoTrack videoTrack) { + this.videoTrack = videoTrack; + } + + public MediaStream getMediaStream() { + return this.mediaStream; + } + + public void setMediaStream(MediaStream mediaStream) { + this.mediaStream = mediaStream; + } + + public void dispose() { + if (this.peerConnection != null) { + try { + this.peerConnection.dispose(); + } catch (IllegalStateException e) { + Log.e("Dispose PeerConnection", e.getMessage()); + } + } + } +} diff --git a/openvidu-android/app/src/main/java/com/example/openviduandroid/openvidu/RemoteParticipant.java b/openvidu-android/app/src/main/java/com/example/openviduandroid/openvidu/RemoteParticipant.java new file mode 100644 index 00000000..e07b6ebc --- /dev/null +++ b/openvidu-android/app/src/main/java/com/example/openviduandroid/openvidu/RemoteParticipant.java @@ -0,0 +1,47 @@ +package com.example.openviduandroid.openvidu; + +import android.view.View; +import android.widget.TextView; + +import org.webrtc.SurfaceViewRenderer; + +public class RemoteParticipant extends Participant { + + private View view; + private SurfaceViewRenderer videoView; + private TextView participantNameText; + + public RemoteParticipant(String connectionId, String participantName, Session session) { + super(connectionId, participantName, session); + this.session.addRemoteParticipant(this); + } + + public View getView() { + return this.view; + } + + public void setView(View view) { + this.view = view; + } + + public SurfaceViewRenderer getVideoView() { + return this.videoView; + } + + public void setVideoView(SurfaceViewRenderer videoView) { + this.videoView = videoView; + } + + public TextView getParticipantNameText() { + return this.participantNameText; + } + + public void setParticipantNameText(TextView participantNameText) { + this.participantNameText = participantNameText; + } + + @Override + public void dispose() { + super.dispose(); + } +} diff --git a/openvidu-android/app/src/main/java/com/example/openviduandroid/openvidu/Session.java b/openvidu-android/app/src/main/java/com/example/openviduandroid/openvidu/Session.java new file mode 100644 index 00000000..81c88be1 --- /dev/null +++ b/openvidu-android/app/src/main/java/com/example/openviduandroid/openvidu/Session.java @@ -0,0 +1,198 @@ +package com.example.openviduandroid.openvidu; + +import android.util.Log; +import android.view.View; +import android.widget.LinearLayout; + +import com.example.openviduandroid.activities.SessionActivity; +import com.example.openviduandroid.constants.JsonConstants; +import com.example.openviduandroid.observers.CustomPeerConnectionObserver; +import com.example.openviduandroid.observers.CustomSdpObserver; +import com.example.openviduandroid.websocket.CustomWebSocket; + +import org.webrtc.IceCandidate; +import org.webrtc.MediaConstraints; +import org.webrtc.MediaStream; +import org.webrtc.PeerConnection; +import org.webrtc.PeerConnectionFactory; +import org.webrtc.SessionDescription; +import org.webrtc.SoftwareVideoDecoderFactory; +import org.webrtc.SoftwareVideoEncoderFactory; +import org.webrtc.VideoDecoderFactory; +import org.webrtc.VideoEncoderFactory; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +public class Session { + + private LocalParticipant localParticipant; + private Map remoteParticipants = new HashMap<>(); + private String id; + private String token; + private LinearLayout views_container; + private PeerConnectionFactory peerConnectionFactory; + private CustomWebSocket websocket; + private SessionActivity activity; + + public Session(String id, String token, LinearLayout views_container, SessionActivity activity) { + this.id = id; + this.token = token; + this.views_container = views_container; + this.activity = activity; + + PeerConnectionFactory.InitializationOptions.Builder optionsBuilder = PeerConnectionFactory.InitializationOptions.builder(activity.getApplicationContext()); + optionsBuilder.setEnableInternalTracer(true); + PeerConnectionFactory.InitializationOptions opt = optionsBuilder.createInitializationOptions(); + PeerConnectionFactory.initialize(opt); + PeerConnectionFactory.Options options = new PeerConnectionFactory.Options(); + + final VideoEncoderFactory encoderFactory; + final VideoDecoderFactory decoderFactory; + encoderFactory = new SoftwareVideoEncoderFactory(); + decoderFactory = new SoftwareVideoDecoderFactory(); + + peerConnectionFactory = PeerConnectionFactory.builder() + .setVideoEncoderFactory(encoderFactory) + .setVideoDecoderFactory(decoderFactory) + .setOptions(options) + .createPeerConnectionFactory(); + } + + public void setWebSocket(CustomWebSocket websocket) { + this.websocket = websocket; + } + + public PeerConnection createLocalPeerConnection() { + final List iceServers = new ArrayList<>(); + PeerConnection.IceServer iceServer = PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer(); + iceServers.add(iceServer); + + PeerConnection peerConnection = peerConnectionFactory.createPeerConnection(iceServers, new CustomPeerConnectionObserver("local") { + @Override + public void onIceCandidate(IceCandidate iceCandidate) { + super.onIceCandidate(iceCandidate); + websocket.onIceCandidate(iceCandidate, localParticipant.getConnectionId()); + } + }); + + return peerConnection; + } + + public void createRemotePeerConnection(final String connectionId) { + final List iceServers = new ArrayList<>(); + PeerConnection.IceServer iceServer = PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer(); + iceServers.add(iceServer); + + PeerConnection peerConnection = peerConnectionFactory.createPeerConnection(iceServers, new CustomPeerConnectionObserver("remotePeerCreation") { + @Override + public void onIceCandidate(IceCandidate iceCandidate) { + super.onIceCandidate(iceCandidate); + websocket.onIceCandidate(iceCandidate, connectionId); + } + + @Override + public void onAddStream(MediaStream mediaStream) { + super.onAddStream(mediaStream); + activity.setRemoteMediaStream(mediaStream, remoteParticipants.get(connectionId)); + } + + @Override + public void onSignalingChange(PeerConnection.SignalingState signalingState) { + if (PeerConnection.SignalingState.STABLE.equals(signalingState)) { + final RemoteParticipant remoteParticipant = remoteParticipants.get(connectionId); + Iterator it = remoteParticipant.getIceCandidateList().iterator(); + while (it.hasNext()) { + IceCandidate candidate = it.next(); + remoteParticipant.getPeerConnection().addIceCandidate(candidate); + it.remove(); + } + } + } + }); + + MediaStream mediaStream = peerConnectionFactory.createLocalMediaStream("105"); + mediaStream.addTrack(localParticipant.getAudioTrack()); + mediaStream.addTrack(localParticipant.getVideoTrack()); + peerConnection.addStream(mediaStream); + + this.remoteParticipants.get(connectionId).setPeerConnection(peerConnection); + } + + public void createLocalOffer(MediaConstraints constraints) { + localParticipant.getPeerConnection().createOffer(new CustomSdpObserver("local offer sdp") { + @Override + public void onCreateSuccess(SessionDescription sessionDescription) { + super.onCreateSuccess(sessionDescription); + Log.i("createOffer SUCCESS", sessionDescription.toString()); + localParticipant.getPeerConnection().setLocalDescription(new CustomSdpObserver("local set local"), sessionDescription); + websocket.publishVideo(sessionDescription); + } + + @Override + public void onCreateFailure(String s) { + Log.e("createOffer ERROR", s); + } + + }, constraints); + } + + public String getId() { + return this.id; + } + + public String getToken() { + return this.token; + } + + public LocalParticipant getLocalParticipant() { + return this.localParticipant; + } + + public void setLocalParticipant(LocalParticipant localParticipant) { + this.localParticipant = localParticipant; + } + + public RemoteParticipant getRemoteParticipant(String id) { + return this.remoteParticipants.get(id); + } + + public PeerConnectionFactory getPeerConnectionFactory() { + return this.peerConnectionFactory; + } + + public void addRemoteParticipant(RemoteParticipant remoteParticipant) { + this.remoteParticipants.put(remoteParticipant.getConnectionId(), remoteParticipant); + } + + public RemoteParticipant removeRemoteParticipant(String id) { + return this.remoteParticipants.remove(id); + } + + public void leaveSession() { + websocket.setWebsocketCancelled(true); + if (websocket != null) { + websocket.leaveRoom(); + websocket.disconnect(); + } + this.localParticipant.dispose(); + for (RemoteParticipant remoteParticipant : remoteParticipants.values()) { + if (remoteParticipant.getPeerConnection() != null) { + remoteParticipant.getPeerConnection().close(); + } + views_container.removeView(remoteParticipant.getView()); + } + if (peerConnectionFactory != null) { + peerConnectionFactory.dispose(); + peerConnectionFactory = null; + } + } + + public void removeView(View view) { + this.views_container.removeView(view); + } + +} diff --git a/openvidu-android/app/src/main/java/com/example/openviduandroid/utils/CustomHttpClient.java b/openvidu-android/app/src/main/java/com/example/openviduandroid/utils/CustomHttpClient.java new file mode 100644 index 00000000..74d4be36 --- /dev/null +++ b/openvidu-android/app/src/main/java/com/example/openviduandroid/utils/CustomHttpClient.java @@ -0,0 +1,92 @@ +package com.example.openviduandroid.utils; + +import java.io.IOException; +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +public class CustomHttpClient { + + private OkHttpClient client; + private String baseUrl; + private String basicAuth; + + public CustomHttpClient(String baseUrl, String basicAuth) { + this.baseUrl = baseUrl.endsWith("/") ? baseUrl : baseUrl + "/"; + this.basicAuth = basicAuth; + + try { + // Create a trust manager that does not validate certificate chains + final TrustManager[] trustAllCerts = new TrustManager[]{ + new X509TrustManager() { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return null; + } + } + }; + + // Install the all-trusting trust manager + final SSLContext sslContext = SSLContext.getInstance("SSL"); + sslContext.init(null, trustAllCerts, new SecureRandom()); + // Create an ssl socket factory with our all-trusting manager + final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); + + this.client = new OkHttpClient.Builder().sslSocketFactory(sslSocketFactory, new X509TrustManager() { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new java.security.cert.X509Certificate[]{}; + } + }).hostnameVerifier(new HostnameVerifier() { + @Override + public boolean verify(String hostname, SSLSession session) { + return true; + } + }).build(); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public void httpCall(String url, String method, String contentType, RequestBody body, Callback callback) throws IOException { + url = url.startsWith("/") ? url.substring(1) : url; + Request request = new Request.Builder() + .url(this.baseUrl + url) + .header("Authorization", this.basicAuth).header("Content-Type", contentType).method(method, body) + .build(); + Call call = client.newCall(request); + call.enqueue(callback); + } + +} diff --git a/openvidu-android/app/src/main/java/com/example/openviduandroid/websocket/CustomWebSocket.java b/openvidu-android/app/src/main/java/com/example/openviduandroid/websocket/CustomWebSocket.java new file mode 100644 index 00000000..28392d7c --- /dev/null +++ b/openvidu-android/app/src/main/java/com/example/openviduandroid/websocket/CustomWebSocket.java @@ -0,0 +1,559 @@ +package com.example.openviduandroid.websocket; + +import android.os.AsyncTask; +import android.os.Handler; +import android.util.Log; +import android.widget.Toast; + +import com.example.openviduandroid.activities.SessionActivity; +import com.example.openviduandroid.constants.JsonConstants; +import com.example.openviduandroid.observers.CustomSdpObserver; +import com.example.openviduandroid.openvidu.LocalParticipant; +import com.example.openviduandroid.openvidu.Participant; +import com.example.openviduandroid.openvidu.RemoteParticipant; +import com.example.openviduandroid.openvidu.Session; +import com.neovisionaries.ws.client.ThreadType; +import com.neovisionaries.ws.client.WebSocket; +import com.neovisionaries.ws.client.WebSocketException; +import com.neovisionaries.ws.client.WebSocketFactory; +import com.neovisionaries.ws.client.WebSocketFrame; +import com.neovisionaries.ws.client.WebSocketListener; +import com.neovisionaries.ws.client.WebSocketState; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.webrtc.IceCandidate; +import org.webrtc.MediaConstraints; +import org.webrtc.MediaStream; +import org.webrtc.PeerConnection; +import org.webrtc.SessionDescription; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +public class CustomWebSocket extends AsyncTask implements WebSocketListener { + + private final String TAG = "CustomWebSocketListener"; + private final int PING_MESSAGE_INTERVAL = 5; + private final TrustManager[] trustManagers = new TrustManager[]{new X509TrustManager() { + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + + @Override + public void checkServerTrusted(final X509Certificate[] chain, final String authType) throws CertificateException { + Log.i(TAG, ": authType: " + authType); + } + + @Override + public void checkClientTrusted(final X509Certificate[] chain, final String authType) throws CertificateException { + Log.i(TAG, ": authType: " + authType); + } + }}; + private AtomicInteger RPC_ID = new AtomicInteger(0); + private AtomicInteger ID_PING = new AtomicInteger(-1); + private AtomicInteger ID_JOINROOM = new AtomicInteger(-1); + private AtomicInteger ID_LEAVEROOM = new AtomicInteger(-1); + private AtomicInteger ID_PUBLISHVIDEO = new AtomicInteger(-1); + private Map IDS_RECEIVEVIDEO = new ConcurrentHashMap<>(); + private Set IDS_ONICECANDIDATE = Collections.newSetFromMap(new ConcurrentHashMap<>()); + private Session session; + private String openviduUrl; + private SessionActivity activity; + private WebSocket websocket; + private boolean websocketCancelled = false; + + public CustomWebSocket(Session session, String openviduUrl, SessionActivity activity) { + this.session = session; + this.openviduUrl = openviduUrl; + this.activity = activity; + } + + @Override + public void onTextMessage(WebSocket websocket, String text) throws Exception { + Log.i(TAG, "Text Message " + text); + JSONObject json = new JSONObject(text); + if (json.has(JsonConstants.RESULT)) { + handleServerResponse(json); + } else { + handleServerEvent(json); + } + } + + private void handleServerResponse(JSONObject json) throws + JSONException { + final int rpcId = json.getInt(JsonConstants.ID); + JSONObject result = new JSONObject(json.getString(JsonConstants.RESULT)); + + if (result.has("value") && result.getString("value").equals("pong")) { + // Response to ping + Log.i(TAG, "pong"); + + } else if (rpcId == this.ID_JOINROOM.get()) { + // Response to joinRoom + activity.enableLeaveButton(); + + final LocalParticipant localParticipant = this.session.getLocalParticipant(); + final String localConnectionId = result.getString(JsonConstants.ID); + localParticipant.setConnectionId(localConnectionId); + + PeerConnection localPeerConnection = session.createLocalPeerConnection(); + localParticipant.setPeerConnection(localPeerConnection); + + MediaStream stream = this.session.getPeerConnectionFactory().createLocalMediaStream("102"); + stream.addTrack(localParticipant.getAudioTrack()); + stream.addTrack(localParticipant.getVideoTrack()); + localParticipant.getPeerConnection().addStream(stream); + + MediaConstraints sdpConstraints = new MediaConstraints(); + sdpConstraints.mandatory.add(new MediaConstraints.KeyValuePair("offerToReceiveAudio", "true")); + sdpConstraints.mandatory.add(new MediaConstraints.KeyValuePair("offerToReceiveVideo", "true")); + session.createLocalOffer(sdpConstraints); + + if (result.getJSONArray(JsonConstants.VALUE).length() > 0) { + // There were users already connected to the session + addRemoteParticipantsAlreadyInRoom(result); + } + + } else if (rpcId == this.ID_LEAVEROOM.get()) { + // Response to leaveRoom + if (websocket.isOpen()) { + websocket.disconnect(); + } + + } else if (rpcId == this.ID_PUBLISHVIDEO.get()) { + // Response to publishVideo + SessionDescription sessionDescription = new SessionDescription(SessionDescription.Type.ANSWER, result.getString("sdpAnswer")); + this.session.getLocalParticipant().getPeerConnection().setRemoteDescription(new CustomSdpObserver("localSetRemoteDesc"), sessionDescription); + + } else if (this.IDS_RECEIVEVIDEO.containsKey(rpcId)) { + // Response to receiveVideoFrom + SessionDescription sessionDescription = new SessionDescription(SessionDescription.Type.ANSWER, result.getString("sdpAnswer")); + session.getRemoteParticipant(IDS_RECEIVEVIDEO.remove(rpcId)).getPeerConnection().setRemoteDescription(new CustomSdpObserver("remoteSetRemoteDesc"), sessionDescription); + + } else if (this.IDS_ONICECANDIDATE.contains(rpcId)) { + // Response to onIceCandidate + IDS_ONICECANDIDATE.remove(rpcId); + + } else { + Log.e(TAG, "Unrecognized server response: " + result); + } + } + + public void joinRoom() { + Map joinRoomParams = new HashMap<>(); + joinRoomParams.put(JsonConstants.METADATA, "{\"clientData\": \"" + this.session.getLocalParticipant().getParticipantName() + "\"}"); + joinRoomParams.put("secret", ""); + joinRoomParams.put("session", this.session.getId()); + joinRoomParams.put("platform", "Android " + android.os.Build.VERSION.SDK_INT); + joinRoomParams.put("token", this.session.getToken()); + this.ID_JOINROOM.set(this.sendJson(JsonConstants.JOINROOM_METHOD, joinRoomParams)); + } + + public void leaveRoom() { + this.ID_LEAVEROOM.set(this.sendJson(JsonConstants.LEAVEROOM_METHOD)); + } + + public void publishVideo(SessionDescription sessionDescription) { + Map publishVideoParams = new HashMap<>(); + publishVideoParams.put("audioActive", "true"); + publishVideoParams.put("videoActive", "true"); + publishVideoParams.put("doLoopback", "false"); + publishVideoParams.put("frameRate", "30"); + publishVideoParams.put("hasAudio", "true"); + publishVideoParams.put("hasVideo", "true"); + publishVideoParams.put("typeOfVideo", "CAMERA"); + publishVideoParams.put("videoDimensions", "{\"width\":320, \"height\":240}"); + publishVideoParams.put("sdpOffer", sessionDescription.description); + this.ID_PUBLISHVIDEO.set(this.sendJson(JsonConstants.PUBLISHVIDEO_METHOD, publishVideoParams)); + } + + public void receiveVideoFrom(SessionDescription sessionDescription, RemoteParticipant remoteParticipant, String streamId) { + Map receiveVideoFromParams = new HashMap<>(); + receiveVideoFromParams.put("sdpOffer", sessionDescription.description); + receiveVideoFromParams.put("sender", streamId); + this.IDS_RECEIVEVIDEO.put(this.sendJson(JsonConstants.RECEIVEVIDEO_METHOD, receiveVideoFromParams), remoteParticipant.getConnectionId()); + } + + public void onIceCandidate(IceCandidate iceCandidate, String endpointName) { + Map onIceCandidateParams = new HashMap<>(); + if (endpointName != null) { + onIceCandidateParams.put("endpointName", endpointName); + } + onIceCandidateParams.put("candidate", iceCandidate.sdp); + onIceCandidateParams.put("sdpMid", iceCandidate.sdpMid); + onIceCandidateParams.put("sdpMLineIndex", Integer.toString(iceCandidate.sdpMLineIndex)); + this.IDS_ONICECANDIDATE.add(this.sendJson(JsonConstants.ONICECANDIDATE_METHOD, onIceCandidateParams)); + } + + private void handleServerEvent(JSONObject json) throws JSONException { + if (!json.has(JsonConstants.PARAMS)) { + Log.e(TAG, "No params " + json.toString()); + } else { + final JSONObject params = new JSONObject(json.getString(JsonConstants.PARAMS)); + String method = json.getString(JsonConstants.METHOD); + switch (method) { + case JsonConstants.ICE_CANDIDATE: + iceCandidateEvent(params); + break; + case JsonConstants.PARTICIPANT_JOINED: + participantJoinedEvent(params); + break; + case JsonConstants.PARTICIPANT_PUBLISHED: + participantPublishedEvent(params); + break; + case JsonConstants.PARTICIPANT_LEFT: + participantLeftEvent(params); + break; + default: + throw new JSONException("Unknown method: " + method); + } + } + } + + public int sendJson(String method) { + return this.sendJson(method, new HashMap<>()); + } + + public synchronized int sendJson(String method, Map params) { + final int id = RPC_ID.get(); + JSONObject jsonObject = new JSONObject(); + try { + JSONObject paramsJson = new JSONObject(); + for (Map.Entry param : params.entrySet()) { + paramsJson.put(param.getKey(), param.getValue()); + } + jsonObject.put("jsonrpc", JsonConstants.JSON_RPCVERSION); + jsonObject.put("method", method); + jsonObject.put("id", id); + jsonObject.put("params", paramsJson); + } catch (JSONException e) { + Log.i(TAG, "JSONException raised on sendJson", e); + return -1; + } + this.websocket.sendText(jsonObject.toString()); + RPC_ID.incrementAndGet(); + return id; + } + + private void addRemoteParticipantsAlreadyInRoom(JSONObject result) throws + JSONException { + for (int i = 0; i < result.getJSONArray(JsonConstants.VALUE).length(); i++) { + JSONObject participantJson = result.getJSONArray(JsonConstants.VALUE).getJSONObject(i); + RemoteParticipant remoteParticipant = this.newRemoteParticipantAux(participantJson); + JSONArray streams = participantJson.getJSONArray("streams"); + for (int j = 0; j < streams.length(); j++) { + JSONObject stream = streams.getJSONObject(0); + String streamId = stream.getString("id"); + this.subscribeAux(remoteParticipant, streamId); + } + } + } + + private void iceCandidateEvent(JSONObject params) throws JSONException { + IceCandidate iceCandidate = new IceCandidate(params.getString("sdpMid"), params.getInt("sdpMLineIndex"), params.getString("candidate")); + final String connectionId = params.getString("senderConnectionId"); + boolean isRemote = !session.getLocalParticipant().getConnectionId().equals(connectionId); + final Participant participant = isRemote ? session.getRemoteParticipant(connectionId) : session.getLocalParticipant(); + final PeerConnection pc = participant.getPeerConnection(); + + switch (pc.signalingState()) { + case CLOSED: + Log.e("saveIceCandidate error", "PeerConnection object is closed"); + break; + case STABLE: + if (pc.getRemoteDescription() != null) { + participant.getPeerConnection().addIceCandidate(iceCandidate); + } else { + participant.getIceCandidateList().add(iceCandidate); + } + break; + default: + participant.getIceCandidateList().add(iceCandidate); + } + } + + private void participantJoinedEvent(JSONObject params) throws JSONException { + this.newRemoteParticipantAux(params); + } + + private void participantPublishedEvent(JSONObject params) throws + JSONException { + String remoteParticipantId = params.getString(JsonConstants.ID); + final RemoteParticipant remoteParticipant = this.session.getRemoteParticipant(remoteParticipantId); + final String streamId = params.getJSONArray("streams").getJSONObject(0).getString("id"); + this.subscribeAux(remoteParticipant, streamId); + } + + private void participantLeftEvent(JSONObject params) throws JSONException { + final RemoteParticipant remoteParticipant = this.session.removeRemoteParticipant(params.getString("connectionId")); + remoteParticipant.dispose(); + Handler mainHandler = new Handler(activity.getMainLooper()); + Runnable myRunnable = () -> session.removeView(remoteParticipant.getView()); + mainHandler.post(myRunnable); + } + + private RemoteParticipant newRemoteParticipantAux(JSONObject participantJson) throws JSONException { + final String connectionId = participantJson.getString(JsonConstants.ID); + final String participantName = new JSONObject(participantJson.getString(JsonConstants.METADATA)).getString("clientData"); + final RemoteParticipant remoteParticipant = new RemoteParticipant(connectionId, participantName, this.session); + this.activity.createRemoteParticipantVideo(remoteParticipant); + this.session.createRemotePeerConnection(remoteParticipant.getConnectionId()); + return remoteParticipant; + } + + private void subscribeAux(RemoteParticipant remoteParticipant, String streamId) { + remoteParticipant.getPeerConnection().createOffer(new CustomSdpObserver("remote offer sdp") { + @Override + public void onCreateSuccess(SessionDescription sessionDescription) { + super.onCreateSuccess(sessionDescription); + remoteParticipant.getPeerConnection().setLocalDescription(new CustomSdpObserver("remoteSetLocalDesc"), sessionDescription); + receiveVideoFrom(sessionDescription, remoteParticipant, streamId); + } + + @Override + public void onCreateFailure(String s) { + Log.e("createOffer error", s); + } + }, new MediaConstraints()); + } + + public void setWebsocketCancelled(boolean websocketCancelled) { + this.websocketCancelled = websocketCancelled; + } + + public void disconnect() { + this.websocket.disconnect(); + } + + @Override + public void onStateChanged(WebSocket websocket, WebSocketState newState) throws Exception { + Log.i(TAG, "State changed: " + newState.name()); + } + + @Override + public void onConnected(WebSocket ws, Map> headers) throws + Exception { + Log.i(TAG, "Connected"); + pingMessageHandler(); + this.joinRoom(); + } + + @Override + public void onConnectError(WebSocket websocket, WebSocketException cause) throws Exception { + Log.e(TAG, "Connect error: " + cause); + } + + @Override + public void onDisconnected(WebSocket websocket, WebSocketFrame + serverCloseFrame, WebSocketFrame clientCloseFrame, boolean closedByServer) throws Exception { + Log.e(TAG, "Disconnected " + serverCloseFrame.getCloseReason() + " " + clientCloseFrame.getCloseReason() + " " + closedByServer); + } + + @Override + public void onFrame(WebSocket websocket, WebSocketFrame frame) throws Exception { + Log.i(TAG, "Frame"); + } + + @Override + public void onContinuationFrame(WebSocket websocket, WebSocketFrame frame) throws Exception { + Log.i(TAG, "Continuation Frame"); + } + + @Override + public void onTextFrame(WebSocket websocket, WebSocketFrame frame) throws Exception { + Log.i(TAG, "Text Frame"); + } + + @Override + public void onBinaryFrame(WebSocket websocket, WebSocketFrame frame) throws Exception { + Log.i(TAG, "Binary Frame"); + } + + @Override + public void onCloseFrame(WebSocket websocket, WebSocketFrame frame) throws Exception { + Log.i(TAG, "Close Frame"); + } + + @Override + public void onPingFrame(WebSocket websocket, WebSocketFrame frame) throws Exception { + Log.i(TAG, "Ping Frame"); + } + + @Override + public void onPongFrame(WebSocket websocket, WebSocketFrame frame) throws Exception { + Log.i(TAG, "Pong Frame"); + } + + @Override + public void onTextMessage(WebSocket websocket, byte[] data) throws Exception { + + } + + @Override + public void onBinaryMessage(WebSocket websocket, byte[] binary) throws Exception { + Log.i(TAG, "Binary Message"); + } + + @Override + public void onSendingFrame(WebSocket websocket, WebSocketFrame frame) throws Exception { + Log.i(TAG, "Sending Frame"); + } + + @Override + public void onFrameSent(WebSocket websocket, WebSocketFrame frame) throws Exception { + Log.i(TAG, "Frame sent"); + } + + @Override + public void onFrameUnsent(WebSocket websocket, WebSocketFrame frame) throws Exception { + Log.i(TAG, "Frame unsent"); + } + + @Override + public void onThreadCreated(WebSocket websocket, ThreadType threadType, Thread thread) throws + Exception { + Log.i(TAG, "Thread created"); + } + + @Override + public void onThreadStarted(WebSocket websocket, ThreadType threadType, Thread thread) throws + Exception { + Log.i(TAG, "Thread started"); + } + + @Override + public void onThreadStopping(WebSocket websocket, ThreadType threadType, Thread thread) throws + Exception { + Log.i(TAG, "Thread stopping"); + } + + @Override + public void onError(WebSocket websocket, WebSocketException cause) throws Exception { + Log.i(TAG, "Error!"); + } + + @Override + public void onFrameError(WebSocket websocket, WebSocketException cause, WebSocketFrame + frame) throws Exception { + Log.i(TAG, "Frame error!"); + } + + @Override + public void onMessageError(WebSocket websocket, WebSocketException + cause, List frames) throws Exception { + Log.i(TAG, "Message error! " + cause); + } + + @Override + public void onMessageDecompressionError(WebSocket websocket, WebSocketException cause, + byte[] compressed) throws Exception { + Log.i(TAG, "Message decompression error!"); + } + + @Override + public void onTextMessageError(WebSocket websocket, WebSocketException cause, byte[] data) throws + Exception { + Log.i(TAG, "Text message error! " + cause); + } + + @Override + public void onSendError(WebSocket websocket, WebSocketException cause, WebSocketFrame frame) throws + Exception { + Log.i(TAG, "Send error! " + cause); + } + + @Override + public void onUnexpectedError(WebSocket websocket, WebSocketException cause) throws + Exception { + Log.i(TAG, "Unexpected error! " + cause); + } + + @Override + public void handleCallbackError(WebSocket websocket, Throwable cause) throws Exception { + Log.e(TAG, "Handle callback error! " + cause); + } + + @Override + public void onSendingHandshake(WebSocket websocket, String requestLine, List + headers) throws Exception { + Log.i(TAG, "Sending Handshake! Hello!"); + } + + private void pingMessageHandler() { + long initialDelay = 0L; + ScheduledThreadPoolExecutor executor = + new ScheduledThreadPoolExecutor(1); + executor.scheduleWithFixedDelay(() -> { + Map pingParams = new HashMap<>(); + if (ID_PING.get() == -1) { + // First ping call + pingParams.put("interval", "5000"); + } + ID_PING.set(sendJson(JsonConstants.PING_METHOD, pingParams)); + }, initialDelay, PING_MESSAGE_INTERVAL, TimeUnit.SECONDS); + } + + private String getWebSocketAddress(String openviduUrl) { + try { + URL url = new URL(openviduUrl); + return "wss://" + url.getHost() + ":" + url.getPort() + "/openvidu"; + } catch (MalformedURLException e) { + Log.e(TAG, "Wrong URL", e); + e.printStackTrace(); + return ""; + } + } + + @Override + protected Void doInBackground(SessionActivity... sessionActivities) { + try { + WebSocketFactory factory = new WebSocketFactory(); + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, trustManagers, new java.security.SecureRandom()); + factory.setSSLContext(sslContext); + factory.setVerifyHostname(false); + websocket = factory.createSocket(getWebSocketAddress(openviduUrl)); + websocket.addListener(this); + websocket.connect(); + } catch (KeyManagementException | NoSuchAlgorithmException | IOException | WebSocketException e) { + Log.e("WebSocket error", e.getMessage()); + Handler mainHandler = new Handler(activity.getMainLooper()); + Runnable myRunnable = () -> { + Toast toast = Toast.makeText(activity, e.getMessage(), Toast.LENGTH_LONG); + toast.show(); + activity.leaveSession(); + }; + mainHandler.post(myRunnable); + websocketCancelled = true; + } + return null; + } + + @Override + protected void onProgressUpdate(Void... progress) { + Log.i(TAG, "PROGRESS " + Arrays.toString(progress)); + } + +} diff --git a/openvidu-android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/openvidu-android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 00000000..1f6bb290 --- /dev/null +++ b/openvidu-android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/openvidu-android/app/src/main/res/drawable/ic_launcher_background.xml b/openvidu-android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..0d025f9b --- /dev/null +++ b/openvidu-android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/openvidu-android/app/src/main/res/layout/activity_main.xml b/openvidu-android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..2fcf5600 --- /dev/null +++ b/openvidu-android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + + +