summaryrefslogtreecommitdiff
path: root/examples/android/src/org/appspot/apprtc/AppRTCDemoActivity.java
diff options
context:
space:
mode:
Diffstat (limited to 'examples/android/src/org/appspot/apprtc/AppRTCDemoActivity.java')
-rw-r--r--examples/android/src/org/appspot/apprtc/AppRTCDemoActivity.java499
1 files changed, 499 insertions, 0 deletions
diff --git a/examples/android/src/org/appspot/apprtc/AppRTCDemoActivity.java b/examples/android/src/org/appspot/apprtc/AppRTCDemoActivity.java
new file mode 100644
index 0000000..bd17323
--- /dev/null
+++ b/examples/android/src/org/appspot/apprtc/AppRTCDemoActivity.java
@@ -0,0 +1,499 @@
+/*
+ * libjingle
+ * Copyright 2013, Google Inc.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * 3. The name of the author may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.appspot.apprtc;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.graphics.Point;
+import android.media.AudioManager;
+import android.os.Bundle;
+import android.os.PowerManager;
+import android.util.Log;
+import android.webkit.JavascriptInterface;
+import android.widget.EditText;
+import android.widget.Toast;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.webrtc.IceCandidate;
+import org.webrtc.MediaConstraints;
+import org.webrtc.MediaStream;
+import org.webrtc.PeerConnection;
+import org.webrtc.PeerConnectionFactory;
+import org.webrtc.SdpObserver;
+import org.webrtc.SessionDescription;
+import org.webrtc.StatsObserver;
+import org.webrtc.StatsReport;
+import org.webrtc.VideoCapturer;
+import org.webrtc.VideoRenderer;
+import org.webrtc.VideoRenderer.I420Frame;
+import org.webrtc.VideoSource;
+import org.webrtc.VideoTrack;
+
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Main Activity of the AppRTCDemo Android app demonstrating interoperability
+ * between the Android/Java implementation of PeerConnection and the
+ * apprtc.appspot.com demo webapp.
+ */
+public class AppRTCDemoActivity extends Activity
+ implements AppRTCClient.IceServersObserver {
+ private static final String TAG = "AppRTCDemoActivity";
+ private PeerConnection pc;
+ private final PCObserver pcObserver = new PCObserver();
+ private final SDPObserver sdpObserver = new SDPObserver();
+ private final GAEChannelClient.MessageHandler gaeHandler = new GAEHandler();
+ private AppRTCClient appRtcClient = new AppRTCClient(this, gaeHandler, this);
+ private VideoStreamsView vsv;
+ private Toast logToast;
+ private LinkedList<IceCandidate> queuedRemoteCandidates =
+ new LinkedList<IceCandidate>();
+ // Synchronize on quit[0] to avoid teardown-related crashes.
+ private final Boolean[] quit = new Boolean[] { false };
+ private MediaConstraints sdpMediaConstraints;
+ private PowerManager.WakeLock wakeLock;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Since the error-handling of this demo consists of throwing
+ // RuntimeExceptions and we assume that'll terminate the app, we install
+ // this default handler so it's applied to background threads as well.
+ Thread.setDefaultUncaughtExceptionHandler(
+ new Thread.UncaughtExceptionHandler() {
+ public void uncaughtException(Thread t, Throwable e) {
+ e.printStackTrace();
+ System.exit(-1);
+ }
+ });
+
+ PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE);
+ wakeLock = powerManager.newWakeLock(
+ PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "AppRTCDemo");
+ wakeLock.acquire();
+
+ Point displaySize = new Point();
+ getWindowManager().getDefaultDisplay().getSize(displaySize);
+ vsv = new VideoStreamsView(this, displaySize);
+ setContentView(vsv);
+
+ abortUnless(PeerConnectionFactory.initializeAndroidGlobals(this),
+ "Failed to initializeAndroidGlobals");
+
+ AudioManager audioManager =
+ ((AudioManager) getSystemService(AUDIO_SERVICE));
+ audioManager.setMode(audioManager.isWiredHeadsetOn() ?
+ AudioManager.MODE_IN_CALL : AudioManager.MODE_IN_COMMUNICATION);
+ audioManager.setSpeakerphoneOn(!audioManager.isWiredHeadsetOn());
+
+ sdpMediaConstraints = new MediaConstraints();
+ sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
+ "OfferToReceiveAudio", "true"));
+ sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
+ "OfferToReceiveVideo", "true"));
+
+ final Intent intent = getIntent();
+ if ("android.intent.action.VIEW".equals(intent.getAction())) {
+ connectToRoom(intent.getData().toString());
+ return;
+ }
+ showGetRoomUI();
+ }
+
+ private void showGetRoomUI() {
+ final EditText roomInput = new EditText(this);
+ roomInput.setText("https://apprtc.appspot.com/?r=");
+ roomInput.setSelection(roomInput.getText().length());
+ DialogInterface.OnClickListener listener =
+ new DialogInterface.OnClickListener() {
+ @Override public void onClick(DialogInterface dialog, int which) {
+ abortUnless(which == DialogInterface.BUTTON_POSITIVE, "lolwat?");
+ dialog.dismiss();
+ connectToRoom(roomInput.getText().toString());
+ }
+ };
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder
+ .setMessage("Enter room URL").setView(roomInput)
+ .setPositiveButton("Go!", listener).show();
+ }
+
+ private void connectToRoom(String roomUrl) {
+ logAndToast("Connecting to room...");
+ appRtcClient.connectToRoom(roomUrl);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ vsv.onPause();
+ // TODO(fischman): IWBN to support pause/resume, but the WebRTC codebase
+ // isn't ready for that yet; e.g.
+ // https://code.google.com/p/webrtc/issues/detail?id=1407
+ // Instead, simply exit instead of pausing (the alternative leads to
+ // system-borking with wedged cameras; e.g. b/8224551)
+ disconnectAndExit();
+ }
+
+ @Override
+ public void onResume() {
+ // The onResume() is a lie! See TODO(fischman) in onPause() above.
+ super.onResume();
+ vsv.onResume();
+ }
+
+ @Override
+ public void onIceServers(List<PeerConnection.IceServer> iceServers) {
+ PeerConnectionFactory factory = new PeerConnectionFactory();
+
+ pc = factory.createPeerConnection(
+ iceServers, appRtcClient.pcConstraints(), pcObserver);
+
+ {
+ final PeerConnection finalPC = pc;
+ final Runnable repeatedStatsLogger = new Runnable() {
+ public void run() {
+ synchronized (quit[0]) {
+ if (quit[0]) {
+ return;
+ }
+ final Runnable runnableThis = this;
+ boolean success = finalPC.getStats(new StatsObserver() {
+ public void onComplete(StatsReport[] reports) {
+ for (StatsReport report : reports) {
+ Log.d(TAG, "Stats: " + report.toString());
+ }
+ vsv.postDelayed(runnableThis, 10000);
+ }
+ }, null);
+ if (!success) {
+ throw new RuntimeException("getStats() return false!");
+ }
+ }
+ }
+ };
+ vsv.postDelayed(repeatedStatsLogger, 10000);
+ }
+
+ {
+ logAndToast("Creating local video source...");
+ VideoCapturer capturer = getVideoCapturer();
+ VideoSource videoSource = factory.createVideoSource(
+ capturer, appRtcClient.videoConstraints());
+ MediaStream lMS = factory.createLocalMediaStream("ARDAMS");
+ VideoTrack videoTrack = factory.createVideoTrack("ARDAMSv0", videoSource);
+ videoTrack.addRenderer(new VideoRenderer(new VideoCallbacks(
+ vsv, VideoStreamsView.Endpoint.LOCAL)));
+ lMS.addTrack(videoTrack);
+ lMS.addTrack(factory.createAudioTrack("ARDAMSa0"));
+ pc.addStream(lMS, new MediaConstraints());
+ }
+ logAndToast("Waiting for ICE candidates...");
+ }
+
+ // Cycle through likely device names for the camera and return the first
+ // capturer that works, or crash if none do.
+ private VideoCapturer getVideoCapturer() {
+ String[] cameraFacing = { "front", "back" };
+ int[] cameraIndex = { 0, 1 };
+ int[] cameraOrientation = { 0, 90, 180, 270 };
+ for (String facing : cameraFacing) {
+ for (int index : cameraIndex) {
+ for (int orientation : cameraOrientation) {
+ String name = "Camera " + index + ", Facing " + facing +
+ ", Orientation " + orientation;
+ VideoCapturer capturer = VideoCapturer.create(name);
+ if (capturer != null) {
+ logAndToast("Using camera: " + name);
+ return capturer;
+ }
+ }
+ }
+ }
+ throw new RuntimeException("Failed to open capturer");
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ }
+
+ // Poor-man's assert(): die with |msg| unless |condition| is true.
+ private static void abortUnless(boolean condition, String msg) {
+ if (!condition) {
+ throw new RuntimeException(msg);
+ }
+ }
+
+ // Log |msg| and Toast about it.
+ private void logAndToast(String msg) {
+ Log.d(TAG, msg);
+ if (logToast != null) {
+ logToast.cancel();
+ }
+ logToast = Toast.makeText(this, msg, Toast.LENGTH_SHORT);
+ logToast.show();
+ }
+
+ // Send |json| to the underlying AppEngine Channel.
+ private void sendMessage(JSONObject json) {
+ appRtcClient.sendMessage(json.toString());
+ }
+
+ // Put a |key|->|value| mapping in |json|.
+ private static void jsonPut(JSONObject json, String key, Object value) {
+ try {
+ json.put(key, value);
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ // Implementation detail: observe ICE & stream changes and react accordingly.
+ private class PCObserver implements PeerConnection.Observer {
+ @Override public void onIceCandidate(final IceCandidate candidate){
+ runOnUiThread(new Runnable() {
+ public void run() {
+ JSONObject json = new JSONObject();
+ jsonPut(json, "type", "candidate");
+ jsonPut(json, "label", candidate.sdpMLineIndex);
+ jsonPut(json, "id", candidate.sdpMid);
+ jsonPut(json, "candidate", candidate.sdp);
+ sendMessage(json);
+ }
+ });
+ }
+
+ @Override public void onError(){
+ runOnUiThread(new Runnable() {
+ public void run() {
+ throw new RuntimeException("PeerConnection error!");
+ }
+ });
+ }
+
+ @Override public void onSignalingChange(
+ PeerConnection.SignalingState newState) {
+ }
+
+ @Override public void onIceConnectionChange(
+ PeerConnection.IceConnectionState newState) {
+ }
+
+ @Override public void onIceGatheringChange(
+ PeerConnection.IceGatheringState newState) {
+ }
+
+ @Override public void onAddStream(final MediaStream stream){
+ runOnUiThread(new Runnable() {
+ public void run() {
+ abortUnless(stream.audioTracks.size() == 1 &&
+ stream.videoTracks.size() == 1,
+ "Weird-looking stream: " + stream);
+ stream.videoTracks.get(0).addRenderer(new VideoRenderer(
+ new VideoCallbacks(vsv, VideoStreamsView.Endpoint.REMOTE)));
+ }
+ });
+ }
+
+ @Override public void onRemoveStream(final MediaStream stream){
+ runOnUiThread(new Runnable() {
+ public void run() {
+ stream.videoTracks.get(0).dispose();
+ }
+ });
+ }
+ }
+
+ // Implementation detail: handle offer creation/signaling and answer setting,
+ // as well as adding remote ICE candidates once the answer SDP is set.
+ private class SDPObserver implements SdpObserver {
+ @Override public void onCreateSuccess(final SessionDescription sdp) {
+ runOnUiThread(new Runnable() {
+ public void run() {
+ logAndToast("Sending " + sdp.type);
+ JSONObject json = new JSONObject();
+ jsonPut(json, "type", sdp.type.canonicalForm());
+ jsonPut(json, "sdp", sdp.description);
+ sendMessage(json);
+ pc.setLocalDescription(sdpObserver, sdp);
+ }
+ });
+ }
+
+ @Override public void onSetSuccess() {
+ runOnUiThread(new Runnable() {
+ public void run() {
+ if (appRtcClient.isInitiator()) {
+ if (pc.getRemoteDescription() != null) {
+ // We've set our local offer and received & set the remote
+ // answer, so drain candidates.
+ drainRemoteCandidates();
+ }
+ } else {
+ if (pc.getLocalDescription() == null) {
+ // We just set the remote offer, time to create our answer.
+ logAndToast("Creating answer");
+ pc.createAnswer(SDPObserver.this, sdpMediaConstraints);
+ } else {
+ // Sent our answer and set it as local description; drain
+ // candidates.
+ drainRemoteCandidates();
+ }
+ }
+ }
+ });
+ }
+
+ @Override public void onCreateFailure(final String error) {
+ runOnUiThread(new Runnable() {
+ public void run() {
+ throw new RuntimeException("createSDP error: " + error);
+ }
+ });
+ }
+
+ @Override public void onSetFailure(final String error) {
+ runOnUiThread(new Runnable() {
+ public void run() {
+ throw new RuntimeException("setSDP error: " + error);
+ }
+ });
+ }
+
+ private void drainRemoteCandidates() {
+ for (IceCandidate candidate : queuedRemoteCandidates) {
+ pc.addIceCandidate(candidate);
+ }
+ queuedRemoteCandidates = null;
+ }
+ }
+
+ // Implementation detail: handler for receiving GAE messages and dispatching
+ // them appropriately.
+ private class GAEHandler implements GAEChannelClient.MessageHandler {
+ @JavascriptInterface public void onOpen() {
+ if (!appRtcClient.isInitiator()) {
+ return;
+ }
+ logAndToast("Creating offer...");
+ pc.createOffer(sdpObserver, sdpMediaConstraints);
+ }
+
+ @JavascriptInterface public void onMessage(String data) {
+ try {
+ JSONObject json = new JSONObject(data);
+ String type = (String) json.get("type");
+ if (type.equals("candidate")) {
+ IceCandidate candidate = new IceCandidate(
+ (String) json.get("id"),
+ json.getInt("label"),
+ (String) json.get("candidate"));
+ if (queuedRemoteCandidates != null) {
+ queuedRemoteCandidates.add(candidate);
+ } else {
+ pc.addIceCandidate(candidate);
+ }
+ } else if (type.equals("answer") || type.equals("offer")) {
+ SessionDescription sdp = new SessionDescription(
+ SessionDescription.Type.fromCanonicalForm(type),
+ (String) json.get("sdp"));
+ pc.setRemoteDescription(sdpObserver, sdp);
+ } else if (type.equals("bye")) {
+ logAndToast("Remote end hung up; dropping PeerConnection");
+ disconnectAndExit();
+ } else {
+ throw new RuntimeException("Unexpected message: " + data);
+ }
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @JavascriptInterface public void onClose() {
+ disconnectAndExit();
+ }
+
+ @JavascriptInterface public void onError(int code, String description) {
+ disconnectAndExit();
+ }
+ }
+
+ // Disconnect from remote resources, dispose of local resources, and exit.
+ private void disconnectAndExit() {
+ synchronized (quit[0]) {
+ if (quit[0]) {
+ return;
+ }
+ quit[0] = true;
+ wakeLock.release();
+ if (pc != null) {
+ pc.dispose();
+ pc = null;
+ }
+ if (appRtcClient != null) {
+ appRtcClient.sendMessage("{\"type\": \"bye\"}");
+ appRtcClient.disconnect();
+ appRtcClient = null;
+ }
+ finish();
+ }
+ }
+
+ // Implementation detail: bridge the VideoRenderer.Callbacks interface to the
+ // VideoStreamsView implementation.
+ private class VideoCallbacks implements VideoRenderer.Callbacks {
+ private final VideoStreamsView view;
+ private final VideoStreamsView.Endpoint stream;
+
+ public VideoCallbacks(
+ VideoStreamsView view, VideoStreamsView.Endpoint stream) {
+ this.view = view;
+ this.stream = stream;
+ }
+
+ @Override
+ public void setSize(final int width, final int height) {
+ view.queueEvent(new Runnable() {
+ public void run() {
+ view.setSize(stream, width, height);
+ }
+ });
+ }
+
+ @Override
+ public void renderFrame(I420Frame frame) {
+ view.queueFrame(stream, frame);
+ }
+ }
+}