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.content.res.Configuration;
     35 import android.graphics.Color;
     36 import android.graphics.Point;
     37 import android.media.AudioManager;
     38 import android.os.Bundle;
     39 import android.util.Log;
     40 import android.util.TypedValue;
     41 import android.view.View;
     42 import android.view.ViewGroup.LayoutParams;
     43 import android.view.WindowManager;
     44 import android.webkit.JavascriptInterface;
     45 import android.widget.EditText;
     46 import android.widget.TextView;
     47 import android.widget.Toast;
     48 
     49 import org.json.JSONException;
     50 import org.json.JSONObject;
     51 import org.webrtc.DataChannel;
     52 import org.webrtc.IceCandidate;
     53 import org.webrtc.MediaConstraints;
     54 import org.webrtc.MediaStream;
     55 import org.webrtc.PeerConnection;
     56 import org.webrtc.PeerConnectionFactory;
     57 import org.webrtc.SdpObserver;
     58 import org.webrtc.SessionDescription;
     59 import org.webrtc.StatsObserver;
     60 import org.webrtc.StatsReport;
     61 import org.webrtc.VideoCapturer;
     62 import org.webrtc.VideoRenderer;
     63 import org.webrtc.VideoRendererGui;
     64 import org.webrtc.VideoSource;
     65 import org.webrtc.VideoTrack;
     66 
     67 import java.util.LinkedList;
     68 import java.util.List;
     69 import java.util.regex.Matcher;
     70 import java.util.regex.Pattern;
     71 
     72 /**
     73  * Main Activity of the AppRTCDemo Android app demonstrating interoperability
     74  * between the Android/Java implementation of PeerConnection and the
     75  * apprtc.appspot.com demo webapp.
     76  */
     77 public class AppRTCDemoActivity extends Activity
     78     implements AppRTCClient.IceServersObserver {
     79   private static final String TAG = "AppRTCDemoActivity";
     80   private static boolean factoryStaticInitialized;
     81   private PeerConnectionFactory factory;
     82   private VideoSource videoSource;
     83   private boolean videoSourceStopped;
     84   private PeerConnection pc;
     85   private final PCObserver pcObserver = new PCObserver();
     86   private final SDPObserver sdpObserver = new SDPObserver();
     87   private final GAEChannelClient.MessageHandler gaeHandler = new GAEHandler();
     88   private AppRTCClient appRtcClient = new AppRTCClient(this, gaeHandler, this);
     89   private AppRTCGLView vsv;
     90   private VideoRenderer.Callbacks localRender;
     91   private VideoRenderer.Callbacks remoteRender;
     92   private Toast logToast;
     93   private final LayoutParams hudLayout =
     94       new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
     95   private TextView hudView;
     96   private LinkedList<IceCandidate> queuedRemoteCandidates =
     97       new LinkedList<IceCandidate>();
     98   // Synchronize on quit[0] to avoid teardown-related crashes.
     99   private final Boolean[] quit = new Boolean[] { false };
    100   private MediaConstraints sdpMediaConstraints;
    101 
    102   @Override
    103   public void onCreate(Bundle savedInstanceState) {
    104     super.onCreate(savedInstanceState);
    105 
    106     Thread.setDefaultUncaughtExceptionHandler(
    107         new UnhandledExceptionHandler(this));
    108 
    109     getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
    110     getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
    111 
    112     Point displaySize = new Point();
    113     getWindowManager().getDefaultDisplay().getRealSize(displaySize);
    114 
    115     vsv = new AppRTCGLView(this, displaySize);
    116     VideoRendererGui.setView(vsv);
    117     remoteRender = VideoRendererGui.create(0, 0, 100, 100);
    118     localRender = VideoRendererGui.create(70, 5, 25, 25);
    119 
    120     vsv.setOnClickListener(new View.OnClickListener() {
    121         @Override public void onClick(View v) {
    122           toggleHUD();
    123         }
    124       });
    125     setContentView(vsv);
    126     logAndToast("Tap the screen to toggle stats visibility");
    127 
    128     hudView = new TextView(this);
    129     hudView.setTextColor(Color.BLACK);
    130     hudView.setBackgroundColor(Color.WHITE);
    131     hudView.setAlpha(0.4f);
    132     hudView.setTextSize(TypedValue.COMPLEX_UNIT_PT, 5);
    133     hudView.setVisibility(View.INVISIBLE);
    134     addContentView(hudView, hudLayout);
    135 
    136     if (!factoryStaticInitialized) {
    137       abortUnless(PeerConnectionFactory.initializeAndroidGlobals(
    138           this, true, true),
    139         "Failed to initializeAndroidGlobals");
    140       factoryStaticInitialized = true;
    141     }
    142 
    143     AudioManager audioManager =
    144         ((AudioManager) getSystemService(AUDIO_SERVICE));
    145     // TODO(fischman): figure out how to do this Right(tm) and remove the
    146     // suppression.
    147     @SuppressWarnings("deprecation")
    148     boolean isWiredHeadsetOn = audioManager.isWiredHeadsetOn();
    149     audioManager.setMode(isWiredHeadsetOn ?
    150         AudioManager.MODE_IN_CALL : AudioManager.MODE_IN_COMMUNICATION);
    151     audioManager.setSpeakerphoneOn(!isWiredHeadsetOn);
    152 
    153     sdpMediaConstraints = new MediaConstraints();
    154     sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
    155         "OfferToReceiveAudio", "true"));
    156     sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
    157         "OfferToReceiveVideo", "true"));
    158 
    159     final Intent intent = getIntent();
    160     if ("android.intent.action.VIEW".equals(intent.getAction())) {
    161       connectToRoom(intent.getData().toString());
    162       return;
    163     }
    164     showGetRoomUI();
    165   }
    166 
    167   private void showGetRoomUI() {
    168     final EditText roomInput = new EditText(this);
    169     roomInput.setText("https://apprtc.appspot.com/?r=");
    170     roomInput.setSelection(roomInput.getText().length());
    171     DialogInterface.OnClickListener listener =
    172         new DialogInterface.OnClickListener() {
    173           @Override public void onClick(DialogInterface dialog, int which) {
    174             abortUnless(which == DialogInterface.BUTTON_POSITIVE, "lolwat?");
    175             dialog.dismiss();
    176             connectToRoom(roomInput.getText().toString());
    177           }
    178         };
    179     AlertDialog.Builder builder = new AlertDialog.Builder(this);
    180     builder
    181         .setMessage("Enter room URL").setView(roomInput)
    182         .setPositiveButton("Go!", listener).show();
    183   }
    184 
    185   private void connectToRoom(String roomUrl) {
    186     logAndToast("Connecting to room...");
    187     appRtcClient.connectToRoom(roomUrl);
    188   }
    189 
    190   // Toggle visibility of the heads-up display.
    191   private void toggleHUD() {
    192     if (hudView.getVisibility() == View.VISIBLE) {
    193       hudView.setVisibility(View.INVISIBLE);
    194     } else {
    195       hudView.setVisibility(View.VISIBLE);
    196     }
    197   }
    198 
    199   // Update the heads-up display with information from |reports|.
    200   private void updateHUD(StatsReport[] reports) {
    201     StringBuilder builder = new StringBuilder();
    202     for (StatsReport report : reports) {
    203       if (!report.id.equals("bweforvideo")) {
    204         continue;
    205       }
    206       for (StatsReport.Value value : report.values) {
    207         String name = value.name.replace("goog", "").replace("Available", "")
    208             .replace("Bandwidth", "").replace("Bitrate", "").replace("Enc", "");
    209         builder.append(name).append("=").append(value.value).append(" ");
    210       }
    211       builder.append("\n");
    212     }
    213     hudView.setText(builder.toString() + hudView.getText());
    214   }
    215 
    216   @Override
    217   public void onPause() {
    218     super.onPause();
    219     vsv.onPause();
    220     if (videoSource != null) {
    221       videoSource.stop();
    222       videoSourceStopped = true;
    223     }
    224   }
    225 
    226   @Override
    227   public void onResume() {
    228     super.onResume();
    229     vsv.onResume();
    230     if (videoSource != null && videoSourceStopped) {
    231       videoSource.restart();
    232     }
    233   }
    234 
    235   @Override
    236   public void onConfigurationChanged (Configuration newConfig) {
    237     Point displaySize = new Point();
    238     getWindowManager().getDefaultDisplay().getSize(displaySize);
    239     vsv.updateDisplaySize(displaySize);
    240     super.onConfigurationChanged(newConfig);
    241   }
    242 
    243   // Just for fun (and to regression-test bug 2302) make sure that DataChannels
    244   // can be created, queried, and disposed.
    245   private static void createDataChannelToRegressionTestBug2302(
    246       PeerConnection pc) {
    247     DataChannel dc = pc.createDataChannel("dcLabel", new DataChannel.Init());
    248     abortUnless("dcLabel".equals(dc.label()), "Unexpected label corruption?");
    249     dc.close();
    250     dc.dispose();
    251   }
    252 
    253   @Override
    254   public void onIceServers(List<PeerConnection.IceServer> iceServers) {
    255     factory = new PeerConnectionFactory();
    256 
    257     MediaConstraints pcConstraints = appRtcClient.pcConstraints();
    258     pcConstraints.optional.add(
    259         new MediaConstraints.KeyValuePair("RtpDataChannels", "true"));
    260     pc = factory.createPeerConnection(iceServers, pcConstraints, pcObserver);
    261 
    262     createDataChannelToRegressionTestBug2302(pc);  // See method comment.
    263 
    264     // Uncomment to get ALL WebRTC tracing and SENSITIVE libjingle logging.
    265     // NOTE: this _must_ happen while |factory| is alive!
    266     // Logging.enableTracing(
    267     //     "logcat:",
    268     //     EnumSet.of(Logging.TraceLevel.TRACE_ALL),
    269     //     Logging.Severity.LS_SENSITIVE);
    270 
    271     {
    272       final PeerConnection finalPC = pc;
    273       final Runnable repeatedStatsLogger = new Runnable() {
    274           public void run() {
    275             synchronized (quit[0]) {
    276               if (quit[0]) {
    277                 return;
    278               }
    279               final Runnable runnableThis = this;
    280               if (hudView.getVisibility() == View.INVISIBLE) {
    281                 vsv.postDelayed(runnableThis, 1000);
    282                 return;
    283               }
    284               boolean success = finalPC.getStats(new StatsObserver() {
    285                   public void onComplete(final StatsReport[] reports) {
    286                     runOnUiThread(new Runnable() {
    287                         public void run() {
    288                           updateHUD(reports);
    289                         }
    290                       });
    291                     for (StatsReport report : reports) {
    292                       Log.d(TAG, "Stats: " + report.toString());
    293                     }
    294                     vsv.postDelayed(runnableThis, 1000);
    295                   }
    296                 }, null);
    297               if (!success) {
    298                 throw new RuntimeException("getStats() return false!");
    299               }
    300             }
    301           }
    302         };
    303       vsv.postDelayed(repeatedStatsLogger, 1000);
    304     }
    305 
    306     {
    307       logAndToast("Creating local video source...");
    308       MediaStream lMS = factory.createLocalMediaStream("ARDAMS");
    309       if (appRtcClient.videoConstraints() != null) {
    310         VideoCapturer capturer = getVideoCapturer();
    311         videoSource = factory.createVideoSource(
    312             capturer, appRtcClient.videoConstraints());
    313         VideoTrack videoTrack =
    314             factory.createVideoTrack("ARDAMSv0", videoSource);
    315         videoTrack.addRenderer(new VideoRenderer(localRender));
    316         lMS.addTrack(videoTrack);
    317       }
    318       if (appRtcClient.audioConstraints() != null) {
    319         lMS.addTrack(factory.createAudioTrack(
    320             "ARDAMSa0",
    321             factory.createAudioSource(appRtcClient.audioConstraints())));
    322       }
    323       pc.addStream(lMS, new MediaConstraints());
    324     }
    325     logAndToast("Waiting for ICE candidates...");
    326   }
    327 
    328   // Cycle through likely device names for the camera and return the first
    329   // capturer that works, or crash if none do.
    330   private VideoCapturer getVideoCapturer() {
    331     String[] cameraFacing = { "front", "back" };
    332     int[] cameraIndex = { 0, 1 };
    333     int[] cameraOrientation = { 0, 90, 180, 270 };
    334     for (String facing : cameraFacing) {
    335       for (int index : cameraIndex) {
    336         for (int orientation : cameraOrientation) {
    337           String name = "Camera " + index + ", Facing " + facing +
    338               ", Orientation " + orientation;
    339           VideoCapturer capturer = VideoCapturer.create(name);
    340           if (capturer != null) {
    341             logAndToast("Using camera: " + name);
    342             return capturer;
    343           }
    344         }
    345       }
    346     }
    347     throw new RuntimeException("Failed to open capturer");
    348   }
    349 
    350   @Override
    351   protected void onDestroy() {
    352     disconnectAndExit();
    353     super.onDestroy();
    354   }
    355 
    356   // Poor-man's assert(): die with |msg| unless |condition| is true.
    357   private static void abortUnless(boolean condition, String msg) {
    358     if (!condition) {
    359       throw new RuntimeException(msg);
    360     }
    361   }
    362 
    363   // Log |msg| and Toast about it.
    364   private void logAndToast(String msg) {
    365     Log.d(TAG, msg);
    366     if (logToast != null) {
    367       logToast.cancel();
    368     }
    369     logToast = Toast.makeText(this, msg, Toast.LENGTH_SHORT);
    370     logToast.show();
    371   }
    372 
    373   // Send |json| to the underlying AppEngine Channel.
    374   private void sendMessage(JSONObject json) {
    375     appRtcClient.sendMessage(json.toString());
    376   }
    377 
    378   // Put a |key|->|value| mapping in |json|.
    379   private static void jsonPut(JSONObject json, String key, Object value) {
    380     try {
    381       json.put(key, value);
    382     } catch (JSONException e) {
    383       throw new RuntimeException(e);
    384     }
    385   }
    386 
    387   // Mangle SDP to prefer ISAC/16000 over any other audio codec.
    388   private static String preferISAC(String sdpDescription) {
    389     String[] lines = sdpDescription.split("\r\n");
    390     int mLineIndex = -1;
    391     String isac16kRtpMap = null;
    392     Pattern isac16kPattern =
    393         Pattern.compile("^a=rtpmap:(\\d+) ISAC/16000[\r]?$");
    394     for (int i = 0;
    395          (i < lines.length) && (mLineIndex == -1 || isac16kRtpMap == null);
    396          ++i) {
    397       if (lines[i].startsWith("m=audio ")) {
    398         mLineIndex = i;
    399         continue;
    400       }
    401       Matcher isac16kMatcher = isac16kPattern.matcher(lines[i]);
    402       if (isac16kMatcher.matches()) {
    403         isac16kRtpMap = isac16kMatcher.group(1);
    404         continue;
    405       }
    406     }
    407     if (mLineIndex == -1) {
    408       Log.d(TAG, "No m=audio line, so can't prefer iSAC");
    409       return sdpDescription;
    410     }
    411     if (isac16kRtpMap == null) {
    412       Log.d(TAG, "No ISAC/16000 line, so can't prefer iSAC");
    413       return sdpDescription;
    414     }
    415     String[] origMLineParts = lines[mLineIndex].split(" ");
    416     StringBuilder newMLine = new StringBuilder();
    417     int origPartIndex = 0;
    418     // Format is: m=<media> <port> <proto> <fmt> ...
    419     newMLine.append(origMLineParts[origPartIndex++]).append(" ");
    420     newMLine.append(origMLineParts[origPartIndex++]).append(" ");
    421     newMLine.append(origMLineParts[origPartIndex++]).append(" ");
    422     newMLine.append(isac16kRtpMap);
    423     for (; origPartIndex < origMLineParts.length; ++origPartIndex) {
    424       if (!origMLineParts[origPartIndex].equals(isac16kRtpMap)) {
    425         newMLine.append(" ").append(origMLineParts[origPartIndex]);
    426       }
    427     }
    428     lines[mLineIndex] = newMLine.toString();
    429     StringBuilder newSdpDescription = new StringBuilder();
    430     for (String line : lines) {
    431       newSdpDescription.append(line).append("\r\n");
    432     }
    433     return newSdpDescription.toString();
    434   }
    435 
    436   // Implementation detail: observe ICE & stream changes and react accordingly.
    437   private class PCObserver implements PeerConnection.Observer {
    438     @Override public void onIceCandidate(final IceCandidate candidate){
    439       runOnUiThread(new Runnable() {
    440           public void run() {
    441             JSONObject json = new JSONObject();
    442             jsonPut(json, "type", "candidate");
    443             jsonPut(json, "label", candidate.sdpMLineIndex);
    444             jsonPut(json, "id", candidate.sdpMid);
    445             jsonPut(json, "candidate", candidate.sdp);
    446             sendMessage(json);
    447           }
    448         });
    449     }
    450 
    451     @Override public void onError(){
    452       runOnUiThread(new Runnable() {
    453           public void run() {
    454             throw new RuntimeException("PeerConnection error!");
    455           }
    456         });
    457     }
    458 
    459     @Override public void onSignalingChange(
    460         PeerConnection.SignalingState newState) {
    461     }
    462 
    463     @Override public void onIceConnectionChange(
    464         PeerConnection.IceConnectionState newState) {
    465     }
    466 
    467     @Override public void onIceGatheringChange(
    468         PeerConnection.IceGatheringState newState) {
    469     }
    470 
    471     @Override public void onAddStream(final MediaStream stream){
    472       runOnUiThread(new Runnable() {
    473           public void run() {
    474             abortUnless(stream.audioTracks.size() <= 1 &&
    475                 stream.videoTracks.size() <= 1,
    476                 "Weird-looking stream: " + stream);
    477             if (stream.videoTracks.size() == 1) {
    478               stream.videoTracks.get(0).addRenderer(
    479                   new VideoRenderer(remoteRender));
    480             }
    481           }
    482         });
    483     }
    484 
    485     @Override public void onRemoveStream(final MediaStream stream){
    486       runOnUiThread(new Runnable() {
    487           public void run() {
    488             stream.videoTracks.get(0).dispose();
    489           }
    490         });
    491     }
    492 
    493     @Override public void onDataChannel(final DataChannel dc) {
    494       runOnUiThread(new Runnable() {
    495           public void run() {
    496             throw new RuntimeException(
    497                 "AppRTC doesn't use data channels, but got: " + dc.label() +
    498                 " anyway!");
    499           }
    500         });
    501     }
    502 
    503     @Override public void onRenegotiationNeeded() {
    504       // No need to do anything; AppRTC follows a pre-agreed-upon
    505       // signaling/negotiation protocol.
    506     }
    507   }
    508 
    509   // Implementation detail: handle offer creation/signaling and answer setting,
    510   // as well as adding remote ICE candidates once the answer SDP is set.
    511   private class SDPObserver implements SdpObserver {
    512     private SessionDescription localSdp;
    513 
    514     @Override public void onCreateSuccess(final SessionDescription origSdp) {
    515       abortUnless(localSdp == null, "multiple SDP create?!?");
    516       final SessionDescription sdp = new SessionDescription(
    517           origSdp.type, preferISAC(origSdp.description));
    518       localSdp = sdp;
    519       runOnUiThread(new Runnable() {
    520           public void run() {
    521             pc.setLocalDescription(sdpObserver, sdp);
    522           }
    523         });
    524     }
    525 
    526     // Helper for sending local SDP (offer or answer, depending on role) to the
    527     // other participant.  Note that it is important to send the output of
    528     // create{Offer,Answer} and not merely the current value of
    529     // getLocalDescription() because the latter may include ICE candidates that
    530     // we might want to filter elsewhere.
    531     private void sendLocalDescription() {
    532       logAndToast("Sending " + localSdp.type);
    533       JSONObject json = new JSONObject();
    534       jsonPut(json, "type", localSdp.type.canonicalForm());
    535       jsonPut(json, "sdp", localSdp.description);
    536       sendMessage(json);
    537     }
    538 
    539     @Override public void onSetSuccess() {
    540       runOnUiThread(new Runnable() {
    541           public void run() {
    542             if (appRtcClient.isInitiator()) {
    543               if (pc.getRemoteDescription() != null) {
    544                 // We've set our local offer and received & set the remote
    545                 // answer, so drain candidates.
    546                 drainRemoteCandidates();
    547               } else {
    548                 // We've just set our local description so time to send it.
    549                 sendLocalDescription();
    550               }
    551             } else {
    552               if (pc.getLocalDescription() == null) {
    553                 // We just set the remote offer, time to create our answer.
    554                 logAndToast("Creating answer");
    555                 pc.createAnswer(SDPObserver.this, sdpMediaConstraints);
    556               } else {
    557                 // Answer now set as local description; send it and drain
    558                 // candidates.
    559                 sendLocalDescription();
    560                 drainRemoteCandidates();
    561               }
    562             }
    563           }
    564         });
    565     }
    566 
    567     @Override public void onCreateFailure(final String error) {
    568       runOnUiThread(new Runnable() {
    569           public void run() {
    570             throw new RuntimeException("createSDP error: " + error);
    571           }
    572         });
    573     }
    574 
    575     @Override public void onSetFailure(final String error) {
    576       runOnUiThread(new Runnable() {
    577           public void run() {
    578             throw new RuntimeException("setSDP error: " + error);
    579           }
    580         });
    581     }
    582 
    583     private void drainRemoteCandidates() {
    584       for (IceCandidate candidate : queuedRemoteCandidates) {
    585         pc.addIceCandidate(candidate);
    586       }
    587       queuedRemoteCandidates = null;
    588     }
    589   }
    590 
    591   // Implementation detail: handler for receiving GAE messages and dispatching
    592   // them appropriately.
    593   private class GAEHandler implements GAEChannelClient.MessageHandler {
    594     @JavascriptInterface public void onOpen() {
    595       if (!appRtcClient.isInitiator()) {
    596         return;
    597       }
    598       logAndToast("Creating offer...");
    599       pc.createOffer(sdpObserver, sdpMediaConstraints);
    600     }
    601 
    602     @JavascriptInterface public void onMessage(String data) {
    603       try {
    604         JSONObject json = new JSONObject(data);
    605         String type = (String) json.get("type");
    606         if (type.equals("candidate")) {
    607           IceCandidate candidate = new IceCandidate(
    608               (String) json.get("id"),
    609               json.getInt("label"),
    610               (String) json.get("candidate"));
    611           if (queuedRemoteCandidates != null) {
    612             queuedRemoteCandidates.add(candidate);
    613           } else {
    614             pc.addIceCandidate(candidate);
    615           }
    616         } else if (type.equals("answer") || type.equals("offer")) {
    617           SessionDescription sdp = new SessionDescription(
    618               SessionDescription.Type.fromCanonicalForm(type),
    619               preferISAC((String) json.get("sdp")));
    620           pc.setRemoteDescription(sdpObserver, sdp);
    621         } else if (type.equals("bye")) {
    622           logAndToast("Remote end hung up; dropping PeerConnection");
    623           disconnectAndExit();
    624         } else {
    625           throw new RuntimeException("Unexpected message: " + data);
    626         }
    627       } catch (JSONException e) {
    628         throw new RuntimeException(e);
    629       }
    630     }
    631 
    632     @JavascriptInterface public void onClose() {
    633       disconnectAndExit();
    634     }
    635 
    636     @JavascriptInterface public void onError(int code, String description) {
    637       disconnectAndExit();
    638     }
    639   }
    640 
    641   // Disconnect from remote resources, dispose of local resources, and exit.
    642   private void disconnectAndExit() {
    643     synchronized (quit[0]) {
    644       if (quit[0]) {
    645         return;
    646       }
    647       quit[0] = true;
    648       if (pc != null) {
    649         pc.dispose();
    650         pc = null;
    651       }
    652       if (appRtcClient != null) {
    653         appRtcClient.sendMessage("{\"type\": \"bye\"}");
    654         appRtcClient.disconnect();
    655         appRtcClient = null;
    656       }
    657       if (videoSource != null) {
    658         videoSource.dispose();
    659         videoSource = null;
    660       }
    661       if (factory != null) {
    662         factory.dispose();
    663         factory = null;
    664       }
    665       finish();
    666     }
    667   }
    668 
    669 }
    670