├── .gitignore ├── .idea ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── encodings.xml ├── gradle.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── yang │ │ └── webrtcdataclient │ │ ├── App.java │ │ ├── MainActivity.java │ │ └── WebRtcClient.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 ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── 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/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 23 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | Groovy 43 | 44 | 45 | Java 46 | 47 | 48 | Potentially confusing code constructsGroovy 49 | 50 | 51 | Threading issuesJava 52 | 53 | 54 | 55 | 56 | Android 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 68 | 69 | $USER_HOME$/.subversion 70 | 71 | 72 | 73 | 74 | 75 | 1.8 76 | 77 | 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebRtcAndroidClient 2 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 25 5 | buildToolsVersion "25.0.3" 6 | defaultConfig { 7 | applicationId "yang.webrtcdataclient" 8 | minSdkVersion 16 9 | targetSdkVersion 25 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:25.3.1' 28 | compile 'com.android.support.constraint:constraint-layout:1.0.2' 29 | testCompile 'junit:junit:4.12' 30 | 31 | compile 'com.github.nkzawa:socket.io-client:0.4.2' 32 | compile 'org.webrtc:google-webrtc:1.0.20371' 33 | } 34 | -------------------------------------------------------------------------------- /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/yangtian/Downloads/adt-bundle-mac-x86_64-20131030/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/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/java/yang/webrtcdataclient/App.java: -------------------------------------------------------------------------------- 1 | package yang.webrtcdataclient; 2 | 3 | import android.app.Application; 4 | 5 | import org.webrtc.PeerConnectionFactory; 6 | 7 | /** 8 | * author: Matthew Yang on 17/10/26 9 | * e-mail: yangtian@yy.com 10 | */ 11 | 12 | public class App extends Application { 13 | 14 | @Override 15 | public void onCreate() { 16 | super.onCreate(); 17 | PeerConnectionFactory.initialize(PeerConnectionFactory 18 | .InitializationOptions 19 | .builder(this) 20 | .createInitializationOptions()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/yang/webrtcdataclient/MainActivity.java: -------------------------------------------------------------------------------- 1 | package yang.webrtcdataclient; 2 | 3 | import android.support.v7.app.AppCompatActivity; 4 | import android.os.Bundle; 5 | import android.text.method.ScrollingMovementMethod; 6 | import android.view.View; 7 | import android.widget.EditText; 8 | import android.widget.TextView; 9 | 10 | public class MainActivity extends AppCompatActivity implements View.OnClickListener, WebRtcClient.WebRtcListener { 11 | 12 | private TextView tvContent; 13 | private EditText editText; 14 | 15 | private WebRtcClient rtcClient; 16 | private StringBuilder sb = new StringBuilder(); 17 | 18 | @Override 19 | protected void onCreate(Bundle savedInstanceState) { 20 | super.onCreate(savedInstanceState); 21 | setContentView(R.layout.activity_main); 22 | rtcClient = new WebRtcClient(); 23 | rtcClient.setWebRtcListener(this); 24 | tvContent = (TextView) findViewById(R.id.tv_content); 25 | tvContent.setMovementMethod(new ScrollingMovementMethod()); 26 | editText = (EditText) findViewById(R.id.edit); 27 | findViewById(R.id.btn_init).setOnClickListener(this); 28 | findViewById(R.id.btn_send).setOnClickListener(this); 29 | } 30 | 31 | private void showSendMessage(String message) { 32 | sb.append("Send:").append(message).append("\n"); 33 | tvContent.setText(sb.toString()); 34 | } 35 | 36 | private void showReceiveMessage(String message) { 37 | sb.append("Receive:").append(message).append("\n"); 38 | tvContent.setText(sb.toString()); 39 | } 40 | 41 | @Override 42 | public void onClick(View v) { 43 | switch (v.getId()) { 44 | case R.id.btn_init: 45 | rtcClient.sendInitMessage(); 46 | showSendMessage("init"); 47 | break; 48 | case R.id.btn_send: 49 | String message = editText.getText().toString(); 50 | rtcClient.sendDataMessageToAllPeer(message); 51 | showSendMessage(message); 52 | break; 53 | default: 54 | break; 55 | } 56 | } 57 | 58 | @Override 59 | public void onReceiveDataChannelMessage(final String message) { 60 | runOnUiThread(new Runnable() { 61 | @Override 62 | public void run() { 63 | showReceiveMessage(message); 64 | } 65 | }); 66 | } 67 | 68 | @Override 69 | protected void onDestroy() { 70 | super.onDestroy(); 71 | rtcClient.release(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/src/main/java/yang/webrtcdataclient/WebRtcClient.java: -------------------------------------------------------------------------------- 1 | package yang.webrtcdataclient; 2 | 3 | import android.util.Log; 4 | 5 | import com.github.nkzawa.emitter.Emitter; 6 | import com.github.nkzawa.socketio.client.IO; 7 | import com.github.nkzawa.socketio.client.Socket; 8 | 9 | import org.json.JSONException; 10 | import org.json.JSONObject; 11 | import org.webrtc.DataChannel; 12 | import org.webrtc.IceCandidate; 13 | import org.webrtc.MediaConstraints; 14 | import org.webrtc.MediaStream; 15 | import org.webrtc.PeerConnection; 16 | import org.webrtc.PeerConnectionFactory; 17 | import org.webrtc.RtpReceiver; 18 | import org.webrtc.SdpObserver; 19 | import org.webrtc.SessionDescription; 20 | 21 | import java.net.URISyntaxException; 22 | import java.nio.ByteBuffer; 23 | import java.util.HashMap; 24 | import java.util.LinkedList; 25 | import java.util.Map; 26 | 27 | /** 28 | * author: Matthew Yang on 17/10/26 29 | * e-mail: yangtian@yy.com 30 | */ 31 | 32 | public class WebRtcClient { 33 | 34 | private final static String TAG = "WebRtcClient"; 35 | private final static String mSocketAddress = "http://172.25.64.1:3000/"; 36 | 37 | private PeerConnectionFactory factory; 38 | private LinkedList iceServers = new LinkedList<>(); 39 | private Socket client; 40 | private String mClientId; 41 | private Map peers = new HashMap<>(); 42 | private MediaConstraints constraints = new MediaConstraints(); 43 | private WebRtcListener webRtcListener; 44 | 45 | private Emitter.Listener messageListener = new Emitter.Listener() { 46 | @Override 47 | public void call(Object... args) { 48 | JSONObject data = (JSONObject) args[0]; 49 | Log.d(TAG, "messageListener call data : " + data); 50 | try { 51 | String from = data.getString("from"); 52 | String type = data.getString("type"); 53 | JSONObject payload = null; 54 | if (!type.equals("init")) { 55 | payload = data.getJSONObject("payload"); 56 | } 57 | switch (type) { 58 | case "init": 59 | onReceiveInit(from); 60 | break; 61 | case "offer": 62 | onReceiveOffer(from, payload); 63 | break; 64 | case "answer": 65 | onReceiveAnswer(from, payload); 66 | break; 67 | case "candidate": 68 | onReceiveCandidate(from, payload); 69 | break; 70 | default: 71 | break; 72 | } 73 | 74 | } catch (JSONException e) { 75 | e.printStackTrace(); 76 | } 77 | } 78 | }; 79 | 80 | private Emitter.Listener clientIdListener = new Emitter.Listener() { 81 | @Override 82 | public void call(Object... args) { 83 | mClientId = (String) args[0]; 84 | Log.d(TAG, "clientIdListener call data : " + mClientId); 85 | } 86 | }; 87 | 88 | public WebRtcClient() { 89 | factory = new PeerConnectionFactory(new PeerConnectionFactory.Options()); 90 | 91 | try { 92 | client = IO.socket(mSocketAddress); 93 | } catch (URISyntaxException e) { 94 | e.printStackTrace(); 95 | } 96 | client.on("message", messageListener); 97 | client.on("id", clientIdListener); 98 | client.connect(); 99 | 100 | iceServers.add(new PeerConnection.IceServer("stun:stun.l.google.com:19302")); 101 | } 102 | 103 | public void setWebRtcListener(WebRtcListener webRtcListener) { 104 | this.webRtcListener = webRtcListener; 105 | } 106 | 107 | /** 108 | * 向信令服务器发送init 109 | */ 110 | public void sendInitMessage() { 111 | client.emit("init"); 112 | } 113 | 114 | /** 115 | * 向信令服务器发消息 116 | * 117 | * @param to id of recipient 118 | * @param type type of message 119 | * @param payload payload of message 120 | * @throws JSONException 121 | */ 122 | public void sendMessage(String to, String type, JSONObject payload) throws JSONException { 123 | JSONObject message = new JSONObject(); 124 | message.put("to", to); 125 | message.put("type", type); 126 | message.put("payload", payload); 127 | message.put("from", mClientId); 128 | client.emit("message", message); 129 | } 130 | 131 | /** 132 | * 向所有连接的peer端发送消息 133 | * 134 | * @param message 135 | */ 136 | public void sendDataMessageToAllPeer(String message) { 137 | for (Peer peer : peers.values()) { 138 | peer.sendDataChannelMessage(message); 139 | } 140 | } 141 | 142 | private Peer getPeer(String from) { 143 | Peer peer; 144 | if (!peers.containsKey(from)) { 145 | peer = addPeer(from); 146 | } else { 147 | peer = peers.get(from); 148 | } 149 | return peer; 150 | } 151 | 152 | private Peer addPeer(String id) { 153 | Peer peer = new Peer(id); 154 | peers.put(id, peer); 155 | return peer; 156 | } 157 | 158 | private void removePeer(String id) { 159 | Peer peer = peers.get(id); 160 | peer.release(); 161 | peers.remove(peer.id); 162 | } 163 | 164 | public void onReceiveInit(String fromUid) { 165 | Log.d(TAG, "onReceiveInit fromUid:" + fromUid); 166 | Peer peer = getPeer(fromUid); 167 | peer.pc.createOffer(peer, constraints); 168 | } 169 | 170 | public void onReceiveOffer(String fromUid, JSONObject payload) { 171 | Log.d(TAG, "onReceiveOffer uid:" + fromUid + " data:" + payload); 172 | try { 173 | Peer peer = getPeer(fromUid); 174 | SessionDescription sdp = new SessionDescription( 175 | SessionDescription.Type.fromCanonicalForm(payload.getString("type")), 176 | payload.getString("sdp") 177 | ); 178 | peer.pc.setRemoteDescription(peer, sdp); 179 | peer.pc.createAnswer(peer, constraints); 180 | } catch (JSONException e) { 181 | e.printStackTrace(); 182 | } 183 | } 184 | 185 | public void onReceiveAnswer(String fromUid, JSONObject payload) { 186 | Log.d(TAG, "onReceiveAnswer uid:" + fromUid + " data:" + payload); 187 | try { 188 | Peer peer = getPeer(fromUid); 189 | SessionDescription sdp = new SessionDescription( 190 | SessionDescription.Type.fromCanonicalForm(payload.getString("type")), 191 | payload.getString("sdp") 192 | ); 193 | peer.pc.setRemoteDescription(peer, sdp); 194 | } catch (JSONException e) { 195 | e.printStackTrace(); 196 | } 197 | } 198 | 199 | public void onReceiveCandidate(String fromUid, JSONObject payload) { 200 | Log.d(TAG, "onReceiveCandidate uid:" + fromUid + " data:" + payload); 201 | try { 202 | Peer peer = getPeer(fromUid); 203 | if (peer.pc.getRemoteDescription() != null) { 204 | IceCandidate candidate = new IceCandidate( 205 | payload.getString("id"), 206 | payload.getInt("label"), 207 | payload.getString("candidate") 208 | ); 209 | peer.pc.addIceCandidate(candidate); 210 | } 211 | } catch (JSONException e) { 212 | e.printStackTrace(); 213 | } 214 | } 215 | 216 | public void release() { 217 | for (Peer peer : peers.values()) { 218 | peer.release(); 219 | } 220 | factory.dispose(); 221 | client.disconnect(); 222 | client.close(); 223 | } 224 | 225 | public class Peer implements SdpObserver, PeerConnection.Observer, DataChannel.Observer { 226 | PeerConnection pc; 227 | String id; 228 | DataChannel dc; 229 | 230 | public Peer(String id) { 231 | Log.d(TAG, "new Peer: " + id); 232 | this.pc = factory.createPeerConnection( 233 | iceServers, //ICE服务器列表 234 | constraints, //MediaConstraints 235 | this); //Context 236 | this.id = id; 237 | 238 | /* 239 | DataChannel.Init 可配参数说明: 240 | ordered:是否保证顺序传输; 241 | maxRetransmitTimeMs:重传允许的最长时间; 242 | maxRetransmits:重传允许的最大次数; 243 | */ 244 | DataChannel.Init init = new DataChannel.Init(); 245 | init.ordered = true; 246 | dc = pc.createDataChannel("dataChannel", init); 247 | } 248 | 249 | public void sendDataChannelMessage(String message) { 250 | byte[] msg = message.getBytes(); 251 | DataChannel.Buffer buffer = new DataChannel.Buffer( 252 | ByteBuffer.wrap(msg), 253 | false); 254 | dc.send(buffer); 255 | } 256 | 257 | public void release() { 258 | pc.dispose(); 259 | dc.close(); 260 | dc.dispose(); 261 | } 262 | 263 | //SdpObserver------------------------------------------------------------------------------- 264 | 265 | @Override 266 | public void onCreateSuccess(SessionDescription sdp) { 267 | Log.d(TAG, "onCreateSuccess: " + sdp.description); 268 | try { 269 | JSONObject payload = new JSONObject(); 270 | payload.put("type", sdp.type.canonicalForm()); 271 | payload.put("sdp", sdp.description); 272 | sendMessage(id, sdp.type.canonicalForm(), payload); 273 | pc.setLocalDescription(Peer.this, sdp); 274 | } catch (JSONException e) { 275 | e.printStackTrace(); 276 | } 277 | } 278 | 279 | @Override 280 | public void onSetSuccess() { 281 | 282 | } 283 | 284 | @Override 285 | public void onCreateFailure(String s) { 286 | 287 | } 288 | 289 | @Override 290 | public void onSetFailure(String s) { 291 | 292 | } 293 | 294 | //DataChannel.Observer---------------------------------------------------------------------- 295 | 296 | @Override 297 | public void onBufferedAmountChange(long l) { 298 | 299 | } 300 | 301 | @Override 302 | public void onStateChange() { 303 | Log.d(TAG, "onDataChannel onStateChange:" + dc.state()); 304 | } 305 | 306 | @Override 307 | public void onMessage(DataChannel.Buffer buffer) { 308 | Log.d(TAG, "onDataChannel onMessage : " + buffer); 309 | ByteBuffer data = buffer.data; 310 | byte[] bytes = new byte[data.capacity()]; 311 | data.get(bytes); 312 | String msg = new String(bytes); 313 | if (webRtcListener != null) { 314 | webRtcListener.onReceiveDataChannelMessage(msg); 315 | } 316 | } 317 | 318 | //PeerConnection.Observer------------------------------------------------------------------- 319 | 320 | @Override 321 | public void onSignalingChange(PeerConnection.SignalingState signalingState) { 322 | 323 | } 324 | 325 | @Override 326 | public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) { 327 | Log.d(TAG, "onIceConnectionChange : " + iceConnectionState.name()); 328 | if (iceConnectionState == PeerConnection.IceConnectionState.DISCONNECTED) { 329 | removePeer(id); 330 | } 331 | } 332 | 333 | @Override 334 | public void onIceConnectionReceivingChange(boolean b) { 335 | 336 | } 337 | 338 | @Override 339 | public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) { 340 | 341 | } 342 | 343 | @Override 344 | public void onIceCandidate(IceCandidate candidate) { 345 | try { 346 | JSONObject payload = new JSONObject(); 347 | payload.put("label", candidate.sdpMLineIndex); 348 | payload.put("id", candidate.sdpMid); 349 | payload.put("candidate", candidate.sdp); 350 | sendMessage(id, "candidate", payload); 351 | } catch (JSONException e) { 352 | e.printStackTrace(); 353 | } 354 | } 355 | 356 | @Override 357 | public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) { 358 | 359 | } 360 | 361 | @Override 362 | public void onAddStream(MediaStream mediaStream) { 363 | 364 | } 365 | 366 | @Override 367 | public void onRemoveStream(MediaStream mediaStream) { 368 | 369 | } 370 | 371 | @Override 372 | public void onDataChannel(DataChannel dataChannel) { 373 | Log.d(TAG, "onDataChannel label:" + dataChannel.label()); 374 | dataChannel.registerObserver(this); 375 | } 376 | 377 | @Override 378 | public void onRenegotiationNeeded() { 379 | 380 | } 381 | 382 | @Override 383 | public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) { 384 | 385 | } 386 | } 387 | 388 | public interface WebRtcListener { 389 | void onReceiveDataChannelMessage(String message); 390 | } 391 | } 392 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 19 | 20 |