diff options
Diffstat (limited to 'examples/android/src/org/appspot/apprtc/AppRTCDemoActivity.java')
-rw-r--r-- | examples/android/src/org/appspot/apprtc/AppRTCDemoActivity.java | 499 |
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); + } + } +} |