├── .gitignore ├── .idea ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── gradle.xml ├── misc.xml ├── modules.xml └── runConfigurations.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── sergiopaniegoblanco │ │ └── webrtcexampleapp │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── sergiopaniegoblanco │ │ │ └── webrtcexampleapp │ │ │ ├── CustomPeerConnectionObserver.java │ │ │ ├── CustomSdpObserver.java │ │ │ ├── CustomWebSocketListener.java │ │ │ └── MainActivity.java │ └── res │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── sergiopaniegoblanco │ └── webrtcexampleapp │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── index.js └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 33 | 34 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 82 | 92 | 93 | 94 | 95 | 96 | 97 | 99 | 100 | 101 | 102 | 103 | 104 | 109 | 110 | 111 | 112 | 113 | 114 | 1.8 115 | 116 | 121 | 122 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebRTCAndroidExample 2 | This is the simpliest example of an app that uses WebRTC to send video I could made. I followed some tutorials I found on the Internet, 3 | specially [this series of posts](https://vivekc.xyz/getting-started-with-webrtc-for-android-daab1e268ff4) and Google's official [Codelab](https://codelabs.developers.google.com/codelabs/webrtc-web/#0). 4 | 5 | I wrote a brief post on the issue that you can check out [here](https://medium.com/@SergioPaniego/tutorial-on-how-to-make-the-simplest-webrtc-android-app-daacb5c8d133). 6 | 7 | I'm using WebSocket protocol to communicate with the server. 8 | 9 | ## How to run it 10 | First install the Web Socket module 11 | ``` 12 | npm install websocket 13 | ``` 14 | Having the module install, you only need to have nodeJS installed in your computer to run the server part that will work as signaling service. To start the server just type the following command 15 | 16 | ``` 17 | node index.js 18 | ``` 19 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 26 5 | buildToolsVersion '26.0.2' 6 | defaultConfig { 7 | applicationId "com.sergiopaniegoblanco.webrtcexampleapp" 8 | minSdkVersion 21 9 | targetSdkVersion 26 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 25 | exclude group: 'com.android.support', module: 'support-annotations' 26 | }) 27 | compile 'com.android.support:appcompat-v7:26.+' 28 | compile 'com.android.support.constraint:constraint-layout:1.0.2' 29 | compile 'fi.vtt.nubomedia:kurento-room-client-android:1.1.2' 30 | testCompile 'junit:junit:4.12' 31 | compile 'org.whispersystems:webrtc-android:M59' 32 | compile 'com.jakewharton:butterknife:8.8.1' 33 | annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1' 34 | // compile "org.java-websocket:Java-WebSocket:1.3.0" 35 | compile 'com.squareup.okhttp3:okhttp:3.9.0' 36 | 37 | } 38 | 39 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/sergiopaniegoblanco/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/sergiopaniegoblanco/webrtcexampleapp/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.sergiopaniegoblanco.webrtcexampleapp; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.sergiopaniegoblanco.webrtcexampleapp", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/sergiopaniegoblanco/webrtcexampleapp/CustomPeerConnectionObserver.java: -------------------------------------------------------------------------------- 1 | package com.sergiopaniegoblanco.webrtcexampleapp; 2 | 3 | import android.util.Log; 4 | 5 | import org.webrtc.DataChannel; 6 | import org.webrtc.IceCandidate; 7 | import org.webrtc.MediaStream; 8 | import org.webrtc.PeerConnection; 9 | import org.webrtc.RtpReceiver; 10 | 11 | /** 12 | * Created by sergiopaniegoblanco on 28/09/2017. 13 | */ 14 | 15 | public class CustomPeerConnectionObserver implements PeerConnection.Observer { 16 | 17 | private String logTag = this.getClass().getCanonicalName(); 18 | 19 | CustomPeerConnectionObserver(String logTag) { 20 | this.logTag = this.logTag+" "+logTag; 21 | } 22 | 23 | @Override 24 | public void onSignalingChange(PeerConnection.SignalingState signalingState) { 25 | Log.d(logTag, "onSignalingChange() called with: signalingState = [" + signalingState + "]"); 26 | } 27 | 28 | @Override 29 | public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) { 30 | Log.d(logTag, "onIceConnectionChange() called with: iceConnectionState = [" + iceConnectionState + "]"); 31 | } 32 | 33 | @Override 34 | public void onIceConnectionReceivingChange(boolean b) { 35 | Log.d(logTag, "onIceConnectionReceivingChange() called with: b = [" + b + "]"); 36 | } 37 | 38 | @Override 39 | public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) { 40 | Log.d(logTag, "onIceGatheringChange() called with: iceGatheringState = [" + iceGatheringState + "]"); 41 | } 42 | 43 | @Override 44 | public void onIceCandidate(IceCandidate iceCandidate) { 45 | Log.d(logTag, "onIceCandidate() called with: iceCandidate = [" + iceCandidate + "]"); 46 | } 47 | 48 | @Override 49 | public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) { 50 | Log.d(logTag, "onIceCandidatesRemoved() called with: iceCandidates = [" + iceCandidates + "]"); 51 | } 52 | 53 | @Override 54 | public void onAddStream(MediaStream mediaStream) { 55 | Log.d(logTag, "onAddStream() called with: mediaStream = [" + mediaStream + "]"); 56 | } 57 | 58 | @Override 59 | public void onRemoveStream(MediaStream mediaStream) { 60 | Log.d(logTag, "onRemoveStream() called with: mediaStream = [" + mediaStream + "]"); 61 | } 62 | 63 | @Override 64 | public void onDataChannel(DataChannel dataChannel) { 65 | Log.d(logTag, "onDataChannel() called with: dataChannel = [" + dataChannel + "]"); 66 | } 67 | 68 | @Override 69 | public void onRenegotiationNeeded() { 70 | Log.d(logTag, "onRenegotiationNeeded() called"); 71 | } 72 | 73 | @Override 74 | public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) { 75 | 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/src/main/java/com/sergiopaniegoblanco/webrtcexampleapp/CustomSdpObserver.java: -------------------------------------------------------------------------------- 1 | package com.sergiopaniegoblanco.webrtcexampleapp; 2 | 3 | import android.util.Log; 4 | 5 | import org.webrtc.SdpObserver; 6 | import org.webrtc.SessionDescription; 7 | 8 | /** 9 | * Created by sergiopaniegoblanco on 30/09/2017. 10 | */ 11 | 12 | public class CustomSdpObserver implements SdpObserver { 13 | 14 | 15 | private String tag = this.getClass().getCanonicalName(); 16 | 17 | CustomSdpObserver(String logTag) { 18 | this.tag = this.tag + " " + logTag; 19 | } 20 | 21 | 22 | @Override 23 | public void onCreateSuccess(SessionDescription sessionDescription) { 24 | Log.d(tag, "onCreateSuccess() called with: sessionDescription = [" + sessionDescription + "]"); 25 | } 26 | 27 | @Override 28 | public void onSetSuccess() { 29 | Log.d(tag, "onSetSuccess() called"); 30 | } 31 | 32 | @Override 33 | public void onCreateFailure(String s) { 34 | Log.d(tag, "onCreateFailure() called with: s = [" + s + "]"); 35 | } 36 | 37 | @Override 38 | public void onSetFailure(String s) { 39 | Log.d(tag, "onSetFailure() called with: s = [" + s + "]"); 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sergiopaniegoblanco/webrtcexampleapp/CustomWebSocketListener.java: -------------------------------------------------------------------------------- 1 | package com.sergiopaniegoblanco.webrtcexampleapp; 2 | 3 | import org.json.JSONException; 4 | import org.json.JSONObject; 5 | import org.webrtc.IceCandidate; 6 | import org.webrtc.MediaConstraints; 7 | import org.webrtc.PeerConnection; 8 | import org.webrtc.SessionDescription; 9 | 10 | import okhttp3.Response; 11 | import okhttp3.WebSocket; 12 | import okhttp3.WebSocketListener; 13 | import okio.ByteString; 14 | 15 | /** 16 | * Created by sergiopaniegoblanco on 02/12/2017. 17 | */ 18 | 19 | public final class CustomWebSocketListener extends WebSocketListener { 20 | 21 | private MainActivity mainActivity; 22 | private PeerConnection peerConnection; 23 | private WebSocket webSocket; 24 | 25 | public CustomWebSocketListener(MainActivity mainActivity, PeerConnection peerConnection) { 26 | super(); 27 | this.mainActivity = mainActivity; 28 | this.peerConnection = peerConnection; 29 | } 30 | 31 | public void setWebSocket(WebSocket webSocket) { 32 | this.webSocket = webSocket; 33 | } 34 | 35 | @Override 36 | public void onOpen(WebSocket webSocket, Response response) { 37 | System.out.println("Open : " + response); 38 | } 39 | @Override 40 | public void onMessage(WebSocket webSocket, String text) { 41 | try { 42 | JSONObject json = new JSONObject(new JSONObject(text).getString("utf8Data")); 43 | if (json.getString("type").equals("candidate")) { 44 | saveIceCandidate(json); 45 | } else { 46 | if (json.getString("type").equals("OFFER")) { 47 | saveOfferAndAnswer(json); 48 | } else if (json.getString("type").equals("ANSWER")) { 49 | saveAnswer(json); 50 | } 51 | } 52 | } catch (JSONException e) { 53 | e.printStackTrace(); 54 | } 55 | } 56 | @Override 57 | public void onMessage(WebSocket webSocket, ByteString bytes) { 58 | System.out.println("Receiving bytes : " + bytes.hex()); 59 | } 60 | @Override 61 | public void onClosing(WebSocket webSocket, int code, String reason) { 62 | System.out.println("Closing : " + code + " / " + reason); 63 | } 64 | @Override 65 | public void onFailure(WebSocket webSocket, Throwable t, Response response) { 66 | System.out.println("Error : " + t.getMessage()); 67 | } 68 | 69 | public void saveIceCandidate(JSONObject json) throws JSONException { 70 | IceCandidate iceCandidate = new IceCandidate(json.getString("id"),Integer.parseInt(json.getString("label")),json.getString("candidate")); 71 | peerConnection.addIceCandidate(iceCandidate); 72 | } 73 | 74 | public void saveAnswer(JSONObject json) throws JSONException { 75 | SessionDescription sessionDescription; 76 | sessionDescription = new SessionDescription(SessionDescription.Type.ANSWER, json.getString("sdp")); 77 | mainActivity.setRemoteDescription(sessionDescription); 78 | mainActivity.setRemoteDescription(sessionDescription); 79 | } 80 | 81 | public void saveOfferAndAnswer(JSONObject json) throws JSONException { 82 | SessionDescription sessionDescription = new SessionDescription(SessionDescription.Type.OFFER, json.getString("sdp")); 83 | peerConnection.setRemoteDescription(new CustomSdpObserver("remoteSetRemoteDesc"), sessionDescription); 84 | 85 | peerConnection.createAnswer(new CustomSdpObserver("remoteCreateOffer") { 86 | @Override 87 | public void onCreateSuccess(SessionDescription sessionDescription) { 88 | super.onCreateSuccess(sessionDescription); 89 | peerConnection.setLocalDescription(new CustomSdpObserver("remoteSetLocalDesc"), sessionDescription); 90 | try { 91 | JSONObject json = new JSONObject(); 92 | json.put("type", sessionDescription.type); 93 | json.put("sdp", sessionDescription.description); 94 | webSocket.send(json.toString()); 95 | } catch (JSONException e) { 96 | e.printStackTrace(); 97 | } 98 | } 99 | }, new MediaConstraints()); 100 | } 101 | } -------------------------------------------------------------------------------- /app/src/main/java/com/sergiopaniegoblanco/webrtcexampleapp/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.sergiopaniegoblanco.webrtcexampleapp; 2 | 3 | import android.Manifest; 4 | import android.content.pm.PackageManager; 5 | import android.os.Bundle; 6 | import android.support.v4.app.ActivityCompat; 7 | import android.support.v4.content.ContextCompat; 8 | import android.support.v7.app.AppCompatActivity; 9 | import android.view.View; 10 | import android.widget.Button; 11 | 12 | 13 | import org.json.JSONException; 14 | import org.json.JSONObject; 15 | import org.webrtc.AudioSource; 16 | import org.webrtc.AudioTrack; 17 | import org.webrtc.Camera1Enumerator; 18 | import org.webrtc.CameraEnumerator; 19 | import org.webrtc.EglBase; 20 | import org.webrtc.IceCandidate; 21 | import org.webrtc.MediaConstraints; 22 | import org.webrtc.MediaStream; 23 | import org.webrtc.PeerConnection; 24 | import org.webrtc.PeerConnectionFactory; 25 | import org.webrtc.SessionDescription; 26 | import org.webrtc.SurfaceViewRenderer; 27 | import org.webrtc.VideoCapturer; 28 | import org.webrtc.VideoRenderer; 29 | import org.webrtc.VideoSource; 30 | import org.webrtc.VideoTrack; 31 | 32 | 33 | import java.util.ArrayList; 34 | import java.util.List; 35 | 36 | import butterknife.BindView; 37 | import butterknife.ButterKnife; 38 | import okhttp3.OkHttpClient; 39 | import okhttp3.Request; 40 | import okhttp3.WebSocket; 41 | 42 | public class MainActivity extends AppCompatActivity { 43 | 44 | private final int MY_PERMISSIONS_REQUEST_CAMERA = 100; 45 | private final int MY_PERMISSIONS_REQUEST_RECORD_AUDIO = 101; 46 | private final int MY_PERMISSIONS_REQUEST = 102; 47 | 48 | private PeerConnection localPeer, remotePeer; 49 | private PeerConnectionFactory peerConnectionFactory; 50 | private VideoRenderer remoteRenderer; 51 | private AudioTrack localAudioTrack; 52 | private VideoTrack localVideoTrack; 53 | private String socketAddress = "http://10.0.2.2:1337"; 54 | private String session = "/SessionA"; 55 | private String secret = "?secret=MY_SECRET"; 56 | private OkHttpClient webSocket1; 57 | private WebSocket ws1; 58 | private OkHttpClient webSocket2; 59 | private WebSocket ws2; 60 | 61 | @BindView(R.id.start_call) 62 | Button start_call; 63 | @BindView(R.id.init_call) 64 | Button init_call; 65 | @BindView(R.id.end_call) 66 | Button end_call; 67 | @BindView(R.id.remote_gl_surface_view) 68 | SurfaceViewRenderer remoteVideoView; 69 | @BindView(R.id.local_gl_surface_view) 70 | SurfaceViewRenderer localVideoView; 71 | 72 | @Override 73 | protected void onCreate(Bundle savedInstanceState) { 74 | super.onCreate(savedInstanceState); 75 | setContentView(R.layout.activity_main); 76 | askForPermissions(); 77 | ButterKnife.bind(this); 78 | initViews(); 79 | } 80 | 81 | public void setRemoteDescription(SessionDescription sessionDescription) { 82 | localPeer.setRemoteDescription(new CustomSdpObserver("localSetRemoteDesc"), sessionDescription); 83 | } 84 | 85 | public void askForPermissions() { 86 | if ((ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) 87 | != PackageManager.PERMISSION_GRANTED) && 88 | (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) 89 | != PackageManager.PERMISSION_GRANTED)) { 90 | ActivityCompat.requestPermissions(this, 91 | new String[]{Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO}, 92 | MY_PERMISSIONS_REQUEST); 93 | } else if (ContextCompat.checkSelfPermission(this, 94 | Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { 95 | ActivityCompat.requestPermissions(this, 96 | new String[]{Manifest.permission.RECORD_AUDIO}, 97 | MY_PERMISSIONS_REQUEST_RECORD_AUDIO); 98 | 99 | } else if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) 100 | != PackageManager.PERMISSION_GRANTED) { 101 | ActivityCompat.requestPermissions(this, 102 | new String[]{Manifest.permission.CAMERA}, 103 | MY_PERMISSIONS_REQUEST_CAMERA); 104 | } 105 | } 106 | 107 | public void initViews() { 108 | localVideoView.setMirror(true); 109 | remoteVideoView.setMirror(false); 110 | EglBase rootEglBase = EglBase.create(); 111 | localVideoView.init(rootEglBase.getEglBaseContext(), null); 112 | localVideoView.setZOrderMediaOverlay(true); 113 | remoteVideoView.init(rootEglBase.getEglBaseContext(), null); 114 | remoteVideoView.setZOrderMediaOverlay(true); 115 | } 116 | 117 | public void start(View view) { 118 | start_call.setEnabled(false); 119 | init_call.setEnabled(true); 120 | PeerConnectionFactory.initializeAndroidGlobals(this, true); 121 | 122 | PeerConnectionFactory.Options options = new PeerConnectionFactory.Options(); 123 | peerConnectionFactory = new PeerConnectionFactory(options); 124 | 125 | VideoCapturer videoGrabberAndroid = createVideoGrabber(); 126 | MediaConstraints constraints = new MediaConstraints(); 127 | 128 | VideoSource videoSource = peerConnectionFactory.createVideoSource(videoGrabberAndroid); 129 | localVideoTrack = peerConnectionFactory.createVideoTrack("100", videoSource); 130 | 131 | AudioSource audioSource = peerConnectionFactory.createAudioSource(constraints); 132 | localAudioTrack = peerConnectionFactory.createAudioTrack("101", audioSource); 133 | 134 | videoGrabberAndroid.startCapture(1000, 1000, 30); 135 | 136 | final VideoRenderer localRenderer = new VideoRenderer(localVideoView); 137 | localVideoTrack.addRenderer(localRenderer); 138 | 139 | MediaConstraints sdpConstraints = new MediaConstraints(); 140 | sdpConstraints.mandatory.add(new MediaConstraints.KeyValuePair("offerToReceiveAudio", "true")); 141 | sdpConstraints.mandatory.add(new MediaConstraints.KeyValuePair("offerToReceiveVideo", "true")); 142 | 143 | createLocalPeerConnection(sdpConstraints); 144 | createLocalSocket(); 145 | 146 | MediaStream stream = peerConnectionFactory.createLocalMediaStream("102"); 147 | stream.addTrack(localAudioTrack); 148 | stream.addTrack(localVideoTrack); 149 | localPeer.addStream(stream); 150 | 151 | createLocalOffer(sdpConstraints); 152 | } 153 | 154 | public void createLocalPeerConnection(MediaConstraints sdpConstraints) { 155 | final List iceServers = new ArrayList<>(); 156 | PeerConnection.IceServer iceServer = new PeerConnection.IceServer("stun:stun.l.google.com:19302"); 157 | iceServers.add(iceServer); 158 | 159 | localPeer = peerConnectionFactory.createPeerConnection(iceServers, sdpConstraints, new CustomPeerConnectionObserver("localPeerCreation") { 160 | @Override 161 | public void onIceCandidate(IceCandidate iceCandidate) { 162 | super.onIceCandidate(iceCandidate); 163 | try { 164 | JSONObject json = new JSONObject(); 165 | json.put("type", "candidate"); 166 | json.put("label", iceCandidate.sdpMLineIndex); 167 | json.put("id", iceCandidate.sdpMid); 168 | json.put("candidate", iceCandidate.sdp); 169 | ws1.send(json.toString()); 170 | } catch (JSONException e) { 171 | e.printStackTrace(); 172 | } 173 | } 174 | }); 175 | } 176 | 177 | public void createLocalSocket() { 178 | Request request = new Request.Builder().url(socketAddress).build(); 179 | CustomWebSocketListener listener = new CustomWebSocketListener(this, localPeer); 180 | OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder(); 181 | webSocket1 = okHttpClientBuilder.build(); 182 | ws1 = webSocket1.newWebSocket(request, listener); 183 | webSocket1.dispatcher().executorService().shutdown(); 184 | } 185 | 186 | public void createLocalOffer(MediaConstraints sdpConstraints) { 187 | localPeer.createOffer(new CustomSdpObserver("localCreateOffer") { 188 | @Override 189 | public void onCreateSuccess(SessionDescription sessionDescription) { 190 | super.onCreateSuccess(sessionDescription); 191 | localPeer.setLocalDescription(new CustomSdpObserver("localSetLocalDesc"), sessionDescription); 192 | try { 193 | JSONObject json = new JSONObject(); 194 | json.put("type", sessionDescription.type); 195 | json.put("sdp", sessionDescription.description); 196 | ws1.send(json.toString()); 197 | } catch (JSONException e) { 198 | e.printStackTrace(); 199 | } 200 | } 201 | }, sdpConstraints); 202 | } 203 | 204 | public void call(View view) { 205 | start_call.setEnabled(false); 206 | init_call.setEnabled(false); 207 | end_call.setEnabled(true); 208 | 209 | createRemotePeerConnection(); 210 | createRemoteSocket(); 211 | } 212 | 213 | public void createRemotePeerConnection() { 214 | final List iceServers = new ArrayList<>(); 215 | PeerConnection.IceServer iceServer = new PeerConnection.IceServer("stun:stun.l.google.com:19302"); 216 | iceServers.add(iceServer); 217 | 218 | MediaConstraints sdpConstraints = new MediaConstraints(); 219 | sdpConstraints.mandatory.add(new MediaConstraints.KeyValuePair("offerToReceiveAudio", "true")); 220 | sdpConstraints.mandatory.add(new MediaConstraints.KeyValuePair("offerToReceiveVideo", "true")); 221 | 222 | remotePeer = peerConnectionFactory.createPeerConnection(iceServers, sdpConstraints, new CustomPeerConnectionObserver("remotePeerCreation") { 223 | 224 | @Override 225 | public void onIceCandidate(IceCandidate iceCandidate) { 226 | super.onIceCandidate(iceCandidate); 227 | try { 228 | JSONObject json = new JSONObject(); 229 | json.put("type", "candidate"); 230 | json.put("label", iceCandidate.sdpMLineIndex); 231 | json.put("id", iceCandidate.sdpMid); 232 | json.put("candidate", iceCandidate.sdp); 233 | ws2.send(json.toString()); 234 | } catch (JSONException e) { 235 | e.printStackTrace(); 236 | } 237 | } 238 | 239 | public void onAddStream(MediaStream mediaStream) { 240 | super.onAddStream(mediaStream); 241 | gotRemoteStream(mediaStream); 242 | } 243 | 244 | @Override 245 | public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) { 246 | super.onIceGatheringChange(iceGatheringState); 247 | 248 | } 249 | }); 250 | } 251 | 252 | public void createRemoteSocket() { 253 | Request request = new Request.Builder().url(socketAddress).build(); 254 | CustomWebSocketListener listener = new CustomWebSocketListener(this, remotePeer); 255 | OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder(); 256 | webSocket2 = okHttpClientBuilder.build(); 257 | ws2 = webSocket2.newWebSocket(request, listener); 258 | listener.setWebSocket(ws2); 259 | webSocket2.dispatcher().executorService().shutdown(); 260 | } 261 | 262 | public void hangup(View view) { 263 | ws1.send("bye"); 264 | ws2.send("bye"); 265 | localPeer.close(); 266 | remotePeer.close(); 267 | localPeer = null; 268 | remotePeer = null; 269 | start_call.setEnabled(true); 270 | init_call.setEnabled(false); 271 | end_call.setEnabled(false); 272 | } 273 | 274 | public VideoCapturer createVideoGrabber() { 275 | VideoCapturer videoCapturer; 276 | videoCapturer = createCameraGrabber(new Camera1Enumerator(false)); 277 | return videoCapturer; 278 | } 279 | 280 | public VideoCapturer createCameraGrabber(CameraEnumerator enumerator) { 281 | final String[] deviceNames = enumerator.getDeviceNames(); 282 | 283 | for (String deviceName : deviceNames) { 284 | if (enumerator.isFrontFacing(deviceName)) { 285 | VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null); 286 | 287 | if (videoCapturer != null) { 288 | return videoCapturer; 289 | } 290 | } 291 | } 292 | 293 | for (String deviceName : deviceNames) { 294 | if (!enumerator.isFrontFacing(deviceName)) { 295 | VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null); 296 | if (videoCapturer != null) { 297 | return videoCapturer; 298 | } 299 | } 300 | } 301 | 302 | return null; 303 | } 304 | 305 | private void gotRemoteStream(MediaStream stream) { 306 | final VideoTrack videoTrack = stream.videoTracks.getFirst(); 307 | runOnUiThread(new Runnable() { 308 | @Override 309 | public void run() { 310 | try { 311 | remoteRenderer = new VideoRenderer(remoteVideoView); 312 | remoteVideoView.setVisibility(View.VISIBLE); 313 | videoTrack.addRenderer(remoteRenderer); 314 | } catch (Exception e) { 315 | e.printStackTrace(); 316 | } 317 | } 318 | }); 319 | } 320 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 22 | 23 | 29 | 30 | 36 | 37 | 38 | 39 | 40 | 53 | 54 |