Home | History | Annotate | Download | only in apprtc
      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