1 /* 2 * libjingle 3 * Copyright 2013, Google Inc. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions are met: 7 * 8 * 1. Redistributions of source code must retain the above copyright notice, 9 * this list of conditions and the following disclaimer. 10 * 2. Redistributions in binary form must reproduce the above copyright notice, 11 * this list of conditions and the following disclaimer in the documentation 12 * and/or other materials provided with the distribution. 13 * 3. The name of the author may not be used to endorse or promote products 14 * derived from this software without specific prior written permission. 15 * 16 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED 17 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 18 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 19 * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 21 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 22 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 23 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 24 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 25 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 */ 27 28 package org.appspot.apprtc; 29 30 import android.app.Activity; 31 import android.app.AlertDialog; 32 import android.content.DialogInterface; 33 import android.content.Intent; 34 import android.graphics.Point; 35 import android.media.AudioManager; 36 import android.os.Bundle; 37 import android.os.PowerManager; 38 import android.util.Log; 39 import android.webkit.JavascriptInterface; 40 import android.widget.EditText; 41 import android.widget.Toast; 42 43 import org.json.JSONException; 44 import org.json.JSONObject; 45 import org.webrtc.DataChannel; 46 import org.webrtc.IceCandidate; 47 import org.webrtc.Logging; 48 import org.webrtc.MediaConstraints; 49 import org.webrtc.MediaStream; 50 import org.webrtc.PeerConnection; 51 import org.webrtc.PeerConnectionFactory; 52 import org.webrtc.SdpObserver; 53 import org.webrtc.SessionDescription; 54 import org.webrtc.StatsObserver; 55 import org.webrtc.StatsReport; 56 import org.webrtc.VideoCapturer; 57 import org.webrtc.VideoRenderer; 58 import org.webrtc.VideoRenderer.I420Frame; 59 import org.webrtc.VideoSource; 60 import org.webrtc.VideoTrack; 61 62 import java.util.EnumSet; 63 import java.util.LinkedList; 64 import java.util.List; 65 66 /** 67 * Main Activity of the AppRTCDemo Android app demonstrating interoperability 68 * between the Android/Java implementation of PeerConnection and the 69 * apprtc.appspot.com demo webapp. 70 */ 71 public class AppRTCDemoActivity extends Activity 72 implements AppRTCClient.IceServersObserver { 73 private static final String TAG = "AppRTCDemoActivity"; 74 private PeerConnection pc; 75 private final PCObserver pcObserver = new PCObserver(); 76 private final SDPObserver sdpObserver = new SDPObserver(); 77 private final GAEChannelClient.MessageHandler gaeHandler = new GAEHandler(); 78 private AppRTCClient appRtcClient = new AppRTCClient(this, gaeHandler, this); 79 private VideoStreamsView vsv; 80 private Toast logToast; 81 private LinkedList<IceCandidate> queuedRemoteCandidates = 82 new LinkedList<IceCandidate>(); 83 // Synchronize on quit[0] to avoid teardown-related crashes. 84 private final Boolean[] quit = new Boolean[] { false }; 85 private MediaConstraints sdpMediaConstraints; 86 private PowerManager.WakeLock wakeLock; 87 88 @Override 89 public void onCreate(Bundle savedInstanceState) { 90 super.onCreate(savedInstanceState); 91 92 // Since the error-handling of this demo consists of throwing 93 // RuntimeExceptions and we assume that'll terminate the app, we install 94 // this default handler so it's applied to background threads as well. 95 Thread.setDefaultUncaughtExceptionHandler( 96 new Thread.UncaughtExceptionHandler() { 97 public void uncaughtException(Thread t, Throwable e) { 98 e.printStackTrace(); 99 System.exit(-1); 100 } 101 }); 102 103 // Uncomment to get ALL WebRTC tracing and SENSITIVE libjingle logging. 104 // Logging.enableTracing( 105 // "/sdcard/trace.txt", 106 // EnumSet.of(Logging.TraceLevel.TRACE_ALL), 107 // Logging.Severity.LS_SENSITIVE); 108 109 PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE); 110 wakeLock = powerManager.newWakeLock( 111 PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "AppRTCDemo"); 112 wakeLock.acquire(); 113 114 Point displaySize = new Point(); 115 getWindowManager().getDefaultDisplay().getSize(displaySize); 116 vsv = new VideoStreamsView(this, displaySize); 117 setContentView(vsv); 118 119 abortUnless(PeerConnectionFactory.initializeAndroidGlobals(this), 120 "Failed to initializeAndroidGlobals"); 121 122 AudioManager audioManager = 123 ((AudioManager) getSystemService(AUDIO_SERVICE)); 124 audioManager.setMode(audioManager.isWiredHeadsetOn() ? 125 AudioManager.MODE_IN_CALL : AudioManager.MODE_IN_COMMUNICATION); 126 audioManager.setSpeakerphoneOn(!audioManager.isWiredHeadsetOn()); 127 128 sdpMediaConstraints = new MediaConstraints(); 129 sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair( 130 "OfferToReceiveAudio", "true")); 131 sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair( 132 "OfferToReceiveVideo", "true")); 133 134 final Intent intent = getIntent(); 135 if ("android.intent.action.VIEW".equals(intent.getAction())) { 136 connectToRoom(intent.getData().toString()); 137 return; 138 } 139 showGetRoomUI(); 140 } 141 142 private void showGetRoomUI() { 143 final EditText roomInput = new EditText(this); 144 roomInput.setText("https://apprtc.appspot.com/?r="); 145 roomInput.setSelection(roomInput.getText().length()); 146 DialogInterface.OnClickListener listener = 147 new DialogInterface.OnClickListener() { 148 @Override public void onClick(DialogInterface dialog, int which) { 149 abortUnless(which == DialogInterface.BUTTON_POSITIVE, "lolwat?"); 150 dialog.dismiss(); 151 connectToRoom(roomInput.getText().toString()); 152 } 153 }; 154 AlertDialog.Builder builder = new AlertDialog.Builder(this); 155 builder 156 .setMessage("Enter room URL").setView(roomInput) 157 .setPositiveButton("Go!", listener).show(); 158 } 159 160 private void connectToRoom(String roomUrl) { 161 logAndToast("Connecting to room..."); 162 appRtcClient.connectToRoom(roomUrl); 163 } 164 165 @Override 166 public void onPause() { 167 super.onPause(); 168 vsv.onPause(); 169 // TODO(fischman): IWBN to support pause/resume, but the WebRTC codebase 170 // isn't ready for that yet; e.g. 171 // https://code.google.com/p/webrtc/issues/detail?id=1407 172 // Instead, simply exit instead of pausing (the alternative leads to 173 // system-borking with wedged cameras; e.g. b/8224551) 174 disconnectAndExit(); 175 } 176 177 @Override 178 public void onResume() { 179 // The onResume() is a lie! See TODO(fischman) in onPause() above. 180 super.onResume(); 181 vsv.onResume(); 182 } 183 184 @Override 185 public void onIceServers(List<PeerConnection.IceServer> iceServers) { 186 PeerConnectionFactory factory = new PeerConnectionFactory(); 187 188 pc = factory.createPeerConnection( 189 iceServers, appRtcClient.pcConstraints(), pcObserver); 190 191 { 192 final PeerConnection finalPC = pc; 193 final Runnable repeatedStatsLogger = new Runnable() { 194 public void run() { 195 synchronized (quit[0]) { 196 if (quit[0]) { 197 return; 198 } 199 final Runnable runnableThis = this; 200 boolean success = finalPC.getStats(new StatsObserver() { 201 public void onComplete(StatsReport[] reports) { 202 for (StatsReport report : reports) { 203 Log.d(TAG, "Stats: " + report.toString()); 204 } 205 vsv.postDelayed(runnableThis, 10000); 206 } 207 }, null); 208 if (!success) { 209 throw new RuntimeException("getStats() return false!"); 210 } 211 } 212 } 213 }; 214 vsv.postDelayed(repeatedStatsLogger, 10000); 215 } 216 217 { 218 logAndToast("Creating local video source..."); 219 VideoCapturer capturer = getVideoCapturer(); 220 VideoSource videoSource = factory.createVideoSource( 221 capturer, appRtcClient.videoConstraints()); 222 MediaStream lMS = factory.createLocalMediaStream("ARDAMS"); 223 VideoTrack videoTrack = factory.createVideoTrack("ARDAMSv0", videoSource); 224 videoTrack.addRenderer(new VideoRenderer(new VideoCallbacks( 225 vsv, VideoStreamsView.Endpoint.LOCAL))); 226 lMS.addTrack(videoTrack); 227 lMS.addTrack(factory.createAudioTrack("ARDAMSa0")); 228 pc.addStream(lMS, new MediaConstraints()); 229 } 230 logAndToast("Waiting for ICE candidates..."); 231 } 232 233 // Cycle through likely device names for the camera and return the first 234 // capturer that works, or crash if none do. 235 private VideoCapturer getVideoCapturer() { 236 String[] cameraFacing = { "front", "back" }; 237 int[] cameraIndex = { 0, 1 }; 238 int[] cameraOrientation = { 0, 90, 180, 270 }; 239 for (String facing : cameraFacing) { 240 for (int index : cameraIndex) { 241 for (int orientation : cameraOrientation) { 242 String name = "Camera " + index + ", Facing " + facing + 243 ", Orientation " + orientation; 244 VideoCapturer capturer = VideoCapturer.create(name); 245 if (capturer != null) { 246 logAndToast("Using camera: " + name); 247 return capturer; 248 } 249 } 250 } 251 } 252 throw new RuntimeException("Failed to open capturer"); 253 } 254 255 @Override 256 public void onDestroy() { 257 super.onDestroy(); 258 } 259 260 // Poor-man's assert(): die with |msg| unless |condition| is true. 261 private static void abortUnless(boolean condition, String msg) { 262 if (!condition) { 263 throw new RuntimeException(msg); 264 } 265 } 266 267 // Log |msg| and Toast about it. 268 private void logAndToast(String msg) { 269 Log.d(TAG, msg); 270 if (logToast != null) { 271 logToast.cancel(); 272 } 273 logToast = Toast.makeText(this, msg, Toast.LENGTH_SHORT); 274 logToast.show(); 275 } 276 277 // Send |json| to the underlying AppEngine Channel. 278 private void sendMessage(JSONObject json) { 279 appRtcClient.sendMessage(json.toString()); 280 } 281 282 // Put a |key|->|value| mapping in |json|. 283 private static void jsonPut(JSONObject json, String key, Object value) { 284 try { 285 json.put(key, value); 286 } catch (JSONException e) { 287 throw new RuntimeException(e); 288 } 289 } 290 291 // Implementation detail: observe ICE & stream changes and react accordingly. 292 private class PCObserver implements PeerConnection.Observer { 293 @Override public void onIceCandidate(final IceCandidate candidate){ 294 runOnUiThread(new Runnable() { 295 public void run() { 296 JSONObject json = new JSONObject(); 297 jsonPut(json, "type", "candidate"); 298 jsonPut(json, "label", candidate.sdpMLineIndex); 299 jsonPut(json, "id", candidate.sdpMid); 300 jsonPut(json, "candidate", candidate.sdp); 301 sendMessage(json); 302 } 303 }); 304 } 305 306 @Override public void onError(){ 307 runOnUiThread(new Runnable() { 308 public void run() { 309 throw new RuntimeException("PeerConnection error!"); 310 } 311 }); 312 } 313 314 @Override public void onSignalingChange( 315 PeerConnection.SignalingState newState) { 316 } 317 318 @Override public void onIceConnectionChange( 319 PeerConnection.IceConnectionState newState) { 320 } 321 322 @Override public void onIceGatheringChange( 323 PeerConnection.IceGatheringState newState) { 324 } 325 326 @Override public void onAddStream(final MediaStream stream){ 327 runOnUiThread(new Runnable() { 328 public void run() { 329 abortUnless(stream.audioTracks.size() == 1 && 330 stream.videoTracks.size() == 1, 331 "Weird-looking stream: " + stream); 332 stream.videoTracks.get(0).addRenderer(new VideoRenderer( 333 new VideoCallbacks(vsv, VideoStreamsView.Endpoint.REMOTE))); 334 } 335 }); 336 } 337 338 @Override public void onRemoveStream(final MediaStream stream){ 339 runOnUiThread(new Runnable() { 340 public void run() { 341 stream.videoTracks.get(0).dispose(); 342 } 343 }); 344 } 345 346 @Override public void onDataChannel(final DataChannel dc) { 347 runOnUiThread(new Runnable() { 348 public void run() { 349 throw new RuntimeException( 350 "AppRTC doesn't use data channels, but got: " + dc.label() + 351 " anyway!"); 352 } 353 }); 354 } 355 } 356 357 // Implementation detail: handle offer creation/signaling and answer setting, 358 // as well as adding remote ICE candidates once the answer SDP is set. 359 private class SDPObserver implements SdpObserver { 360 @Override public void onCreateSuccess(final SessionDescription sdp) { 361 runOnUiThread(new Runnable() { 362 public void run() { 363 logAndToast("Sending " + sdp.type); 364 JSONObject json = new JSONObject(); 365 jsonPut(json, "type", sdp.type.canonicalForm()); 366 jsonPut(json, "sdp", sdp.description); 367 sendMessage(json); 368 pc.setLocalDescription(sdpObserver, sdp); 369 } 370 }); 371 } 372 373 @Override public void onSetSuccess() { 374 runOnUiThread(new Runnable() { 375 public void run() { 376 if (appRtcClient.isInitiator()) { 377 if (pc.getRemoteDescription() != null) { 378 // We've set our local offer and received & set the remote 379 // answer, so drain candidates. 380 drainRemoteCandidates(); 381 } 382 } else { 383 if (pc.getLocalDescription() == null) { 384 // We just set the remote offer, time to create our answer. 385 logAndToast("Creating answer"); 386 pc.createAnswer(SDPObserver.this, sdpMediaConstraints); 387 } else { 388 // Sent our answer and set it as local description; drain 389 // candidates. 390 drainRemoteCandidates(); 391 } 392 } 393 } 394 }); 395 } 396 397 @Override public void onCreateFailure(final String error) { 398 runOnUiThread(new Runnable() { 399 public void run() { 400 throw new RuntimeException("createSDP error: " + error); 401 } 402 }); 403 } 404 405 @Override public void onSetFailure(final String error) { 406 runOnUiThread(new Runnable() { 407 public void run() { 408 throw new RuntimeException("setSDP error: " + error); 409 } 410 }); 411 } 412 413 private void drainRemoteCandidates() { 414 for (IceCandidate candidate : queuedRemoteCandidates) { 415 pc.addIceCandidate(candidate); 416 } 417 queuedRemoteCandidates = null; 418 } 419 } 420 421 // Implementation detail: handler for receiving GAE messages and dispatching 422 // them appropriately. 423 private class GAEHandler implements GAEChannelClient.MessageHandler { 424 @JavascriptInterface public void onOpen() { 425 if (!appRtcClient.isInitiator()) { 426 return; 427 } 428 logAndToast("Creating offer..."); 429 pc.createOffer(sdpObserver, sdpMediaConstraints); 430 } 431 432 @JavascriptInterface public void onMessage(String data) { 433 try { 434 JSONObject json = new JSONObject(data); 435 String type = (String) json.get("type"); 436 if (type.equals("candidate")) { 437 IceCandidate candidate = new IceCandidate( 438 (String) json.get("id"), 439 json.getInt("label"), 440 (String) json.get("candidate")); 441 if (queuedRemoteCandidates != null) { 442 queuedRemoteCandidates.add(candidate); 443 } else { 444 pc.addIceCandidate(candidate); 445 } 446 } else if (type.equals("answer") || type.equals("offer")) { 447 SessionDescription sdp = new SessionDescription( 448 SessionDescription.Type.fromCanonicalForm(type), 449 (String) json.get("sdp")); 450 pc.setRemoteDescription(sdpObserver, sdp); 451 } else if (type.equals("bye")) { 452 logAndToast("Remote end hung up; dropping PeerConnection"); 453 disconnectAndExit(); 454 } else { 455 throw new RuntimeException("Unexpected message: " + data); 456 } 457 } catch (JSONException e) { 458 throw new RuntimeException(e); 459 } 460 } 461 462 @JavascriptInterface public void onClose() { 463 disconnectAndExit(); 464 } 465 466 @JavascriptInterface public void onError(int code, String description) { 467 disconnectAndExit(); 468 } 469 } 470 471 // Disconnect from remote resources, dispose of local resources, and exit. 472 private void disconnectAndExit() { 473 synchronized (quit[0]) { 474 if (quit[0]) { 475 return; 476 } 477 quit[0] = true; 478 wakeLock.release(); 479 if (pc != null) { 480 pc.dispose(); 481 pc = null; 482 } 483 if (appRtcClient != null) { 484 appRtcClient.sendMessage("{\"type\": \"bye\"}"); 485 appRtcClient.disconnect(); 486 appRtcClient = null; 487 } 488 finish(); 489 } 490 } 491 492 // Implementation detail: bridge the VideoRenderer.Callbacks interface to the 493 // VideoStreamsView implementation. 494 private class VideoCallbacks implements VideoRenderer.Callbacks { 495 private final VideoStreamsView view; 496 private final VideoStreamsView.Endpoint stream; 497 498 public VideoCallbacks( 499 VideoStreamsView view, VideoStreamsView.Endpoint stream) { 500 this.view = view; 501 this.stream = stream; 502 } 503 504 @Override 505 public void setSize(final int width, final int height) { 506 view.queueEvent(new Runnable() { 507 public void run() { 508 view.setSize(stream, width, height); 509 } 510 }); 511 } 512 513 @Override 514 public void renderFrame(I420Frame frame) { 515 view.queueFrame(stream, frame); 516 } 517 } 518 } 519