3 * Copyright 2013, Google Inc.
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met:
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.
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.
28 package org.appspot.apprtc;
30 import android.app.Activity;
31 import android.app.AlertDialog;
32 import android.content.DialogInterface;
33 import android.content.Intent;
34 import android.graphics.Color;
35 import android.graphics.Point;
36 import android.media.AudioManager;
37 import android.os.Bundle;
38 import android.util.Log;
39 import android.util.TypedValue;
40 import android.view.View;
41 import android.view.ViewGroup.LayoutParams;
42 import android.view.WindowManager;
43 import android.webkit.JavascriptInterface;
44 import android.widget.EditText;
45 import android.widget.TextView;
46 import android.widget.Toast;
48 import org.json.JSONException;
49 import org.json.JSONObject;
50 import org.webrtc.DataChannel;
51 import org.webrtc.IceCandidate;
52 import org.webrtc.Logging;
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.VideoRenderer.I420Frame;
64 import org.webrtc.VideoSource;
65 import org.webrtc.VideoTrack;
67 import java.util.EnumSet;
68 import java.util.LinkedList;
69 import java.util.List;
70 import java.util.regex.Matcher;
71 import java.util.regex.Pattern;
74 * Main Activity of the AppRTCDemo Android app demonstrating interoperability
75 * between the Android/Java implementation of PeerConnection and the
76 * apprtc.appspot.com demo webapp.
78 public class AppRTCDemoActivity extends Activity
79 implements AppRTCClient.IceServersObserver {
80 private static final String TAG = "AppRTCDemoActivity";
81 private static boolean factoryStaticInitialized;
82 private PeerConnectionFactory factory;
83 private VideoSource videoSource;
84 private boolean videoSourceStopped;
85 private PeerConnection pc;
86 private final PCObserver pcObserver = new PCObserver();
87 private final SDPObserver sdpObserver = new SDPObserver();
88 private final GAEChannelClient.MessageHandler gaeHandler = new GAEHandler();
89 private AppRTCClient appRtcClient = new AppRTCClient(this, gaeHandler, this);
90 private VideoStreamsView vsv;
91 private Toast logToast;
92 private final LayoutParams hudLayout =
93 new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
94 private TextView hudView;
95 private LinkedList<IceCandidate> queuedRemoteCandidates =
96 new LinkedList<IceCandidate>();
97 // Synchronize on quit[0] to avoid teardown-related crashes.
98 private final Boolean[] quit = new Boolean[] { false };
99 private MediaConstraints sdpMediaConstraints;
102 public void onCreate(Bundle savedInstanceState) {
103 super.onCreate(savedInstanceState);
105 Thread.setDefaultUncaughtExceptionHandler(
106 new UnhandledExceptionHandler(this));
108 getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
109 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
111 Point displaySize = new Point();
112 getWindowManager().getDefaultDisplay().getSize(displaySize);
113 vsv = new VideoStreamsView(this, displaySize);
114 vsv.setOnClickListener(new View.OnClickListener() {
115 @Override public void onClick(View v) {
120 hudView = new TextView(this);
121 hudView.setTextColor(Color.BLACK);
122 hudView.setBackgroundColor(Color.WHITE);
123 hudView.setAlpha(0.4f);
124 hudView.setTextSize(TypedValue.COMPLEX_UNIT_PT, 5);
125 addContentView(hudView, hudLayout);
127 if (!factoryStaticInitialized) {
128 abortUnless(PeerConnectionFactory.initializeAndroidGlobals(this),
129 "Failed to initializeAndroidGlobals");
130 factoryStaticInitialized = true;
133 AudioManager audioManager =
134 ((AudioManager) getSystemService(AUDIO_SERVICE));
135 // TODO(fischman): figure out how to do this Right(tm) and remove the
137 @SuppressWarnings("deprecation")
138 boolean isWiredHeadsetOn = audioManager.isWiredHeadsetOn();
139 audioManager.setMode(isWiredHeadsetOn ?
140 AudioManager.MODE_IN_CALL : AudioManager.MODE_IN_COMMUNICATION);
141 audioManager.setSpeakerphoneOn(!isWiredHeadsetOn);
143 sdpMediaConstraints = new MediaConstraints();
144 sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
145 "OfferToReceiveAudio", "true"));
146 sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
147 "OfferToReceiveVideo", "true"));
149 final Intent intent = getIntent();
150 if ("android.intent.action.VIEW".equals(intent.getAction())) {
151 connectToRoom(intent.getData().toString());
157 private void showGetRoomUI() {
158 final EditText roomInput = new EditText(this);
159 roomInput.setText("https://apprtc.appspot.com/?r=");
160 roomInput.setSelection(roomInput.getText().length());
161 DialogInterface.OnClickListener listener =
162 new DialogInterface.OnClickListener() {
163 @Override public void onClick(DialogInterface dialog, int which) {
164 abortUnless(which == DialogInterface.BUTTON_POSITIVE, "lolwat?");
166 connectToRoom(roomInput.getText().toString());
169 AlertDialog.Builder builder = new AlertDialog.Builder(this);
171 .setMessage("Enter room URL").setView(roomInput)
172 .setPositiveButton("Go!", listener).show();
175 private void connectToRoom(String roomUrl) {
176 logAndToast("Connecting to room...");
177 appRtcClient.connectToRoom(roomUrl);
180 // Toggle visibility of the heads-up display.
181 private void toggleHUD() {
182 if (hudView.getVisibility() == View.VISIBLE) {
183 hudView.setVisibility(View.INVISIBLE);
185 hudView.setVisibility(View.VISIBLE);
189 // Update the heads-up display with information from |reports|.
190 private void updateHUD(StatsReport[] reports) {
191 if (hudView.getText().length() == 0) {
192 logAndToast("Tap the screen to toggle stats visibility");
194 StringBuilder builder = new StringBuilder();
195 for (StatsReport report : reports) {
196 if (!report.id.equals("bweforvideo")) {
199 for (StatsReport.Value value : report.values) {
200 String name = value.name.replace("goog", "").replace("Available", "")
201 .replace("Bandwidth", "").replace("Bitrate", "").replace("Enc", "");
202 builder.append(name).append("=").append(value.value).append(" ");
204 builder.append("\n");
206 hudView.setText(builder.toString() + hudView.getText());
210 public void onPause() {
213 if (videoSource != null) {
215 videoSourceStopped = true;
220 public void onResume() {
223 if (videoSource != null && videoSourceStopped) {
224 videoSource.restart();
229 // Just for fun (and to regression-test bug 2302) make sure that DataChannels
230 // can be created, queried, and disposed.
231 private static void createDataChannelToRegressionTestBug2302(
233 DataChannel dc = pc.createDataChannel("dcLabel", new DataChannel.Init());
234 abortUnless("dcLabel".equals(dc.label()), "Unexpected label corruption?");
240 public void onIceServers(List<PeerConnection.IceServer> iceServers) {
241 factory = new PeerConnectionFactory();
243 MediaConstraints pcConstraints = appRtcClient.pcConstraints();
244 pcConstraints.optional.add(
245 new MediaConstraints.KeyValuePair("RtpDataChannels", "true"));
246 pc = factory.createPeerConnection(iceServers, pcConstraints, pcObserver);
248 createDataChannelToRegressionTestBug2302(pc); // See method comment.
250 // Uncomment to get ALL WebRTC tracing and SENSITIVE libjingle logging.
251 // NOTE: this _must_ happen while |factory| is alive!
252 // Logging.enableTracing(
254 // EnumSet.of(Logging.TraceLevel.TRACE_ALL),
255 // Logging.Severity.LS_SENSITIVE);
258 final PeerConnection finalPC = pc;
259 final Runnable repeatedStatsLogger = new Runnable() {
261 synchronized (quit[0]) {
265 final Runnable runnableThis = this;
266 boolean success = finalPC.getStats(new StatsObserver() {
267 public void onComplete(final StatsReport[] reports) {
268 runOnUiThread(new Runnable() {
273 for (StatsReport report : reports) {
274 Log.d(TAG, "Stats: " + report.toString());
276 vsv.postDelayed(runnableThis, 1000);
280 throw new RuntimeException("getStats() return false!");
285 vsv.postDelayed(repeatedStatsLogger, 1000);
289 logAndToast("Creating local video source...");
290 MediaStream lMS = factory.createLocalMediaStream("ARDAMS");
291 if (appRtcClient.videoConstraints() != null) {
292 VideoCapturer capturer = getVideoCapturer();
293 videoSource = factory.createVideoSource(
294 capturer, appRtcClient.videoConstraints());
295 VideoTrack videoTrack =
296 factory.createVideoTrack("ARDAMSv0", videoSource);
297 videoTrack.addRenderer(new VideoRenderer(new VideoCallbacks(
298 vsv, VideoStreamsView.Endpoint.LOCAL)));
299 lMS.addTrack(videoTrack);
301 if (appRtcClient.audioConstraints() != null) {
302 lMS.addTrack(factory.createAudioTrack(
304 factory.createAudioSource(appRtcClient.audioConstraints())));
306 pc.addStream(lMS, new MediaConstraints());
308 logAndToast("Waiting for ICE candidates...");
311 // Cycle through likely device names for the camera and return the first
312 // capturer that works, or crash if none do.
313 private VideoCapturer getVideoCapturer() {
314 String[] cameraFacing = { "front", "back" };
315 int[] cameraIndex = { 0, 1 };
316 int[] cameraOrientation = { 0, 90, 180, 270 };
317 for (String facing : cameraFacing) {
318 for (int index : cameraIndex) {
319 for (int orientation : cameraOrientation) {
320 String name = "Camera " + index + ", Facing " + facing +
321 ", Orientation " + orientation;
322 VideoCapturer capturer = VideoCapturer.create(name);
323 if (capturer != null) {
324 logAndToast("Using camera: " + name);
330 throw new RuntimeException("Failed to open capturer");
334 protected void onDestroy() {
339 // Poor-man's assert(): die with |msg| unless |condition| is true.
340 private static void abortUnless(boolean condition, String msg) {
342 throw new RuntimeException(msg);
346 // Log |msg| and Toast about it.
347 private void logAndToast(String msg) {
349 if (logToast != null) {
352 logToast = Toast.makeText(this, msg, Toast.LENGTH_SHORT);
356 // Send |json| to the underlying AppEngine Channel.
357 private void sendMessage(JSONObject json) {
358 appRtcClient.sendMessage(json.toString());
361 // Put a |key|->|value| mapping in |json|.
362 private static void jsonPut(JSONObject json, String key, Object value) {
364 json.put(key, value);
365 } catch (JSONException e) {
366 throw new RuntimeException(e);
370 // Mangle SDP to prefer ISAC/16000 over any other audio codec.
371 private static String preferISAC(String sdpDescription) {
372 String[] lines = sdpDescription.split("\r\n");
374 String isac16kRtpMap = null;
375 Pattern isac16kPattern =
376 Pattern.compile("^a=rtpmap:(\\d+) ISAC/16000[\r]?$");
378 (i < lines.length) && (mLineIndex == -1 || isac16kRtpMap == null);
380 if (lines[i].startsWith("m=audio ")) {
384 Matcher isac16kMatcher = isac16kPattern.matcher(lines[i]);
385 if (isac16kMatcher.matches()) {
386 isac16kRtpMap = isac16kMatcher.group(1);
390 if (mLineIndex == -1) {
391 Log.d(TAG, "No m=audio line, so can't prefer iSAC");
392 return sdpDescription;
394 if (isac16kRtpMap == null) {
395 Log.d(TAG, "No ISAC/16000 line, so can't prefer iSAC");
396 return sdpDescription;
398 String[] origMLineParts = lines[mLineIndex].split(" ");
399 StringBuilder newMLine = new StringBuilder();
400 int origPartIndex = 0;
401 // Format is: m=<media> <port> <proto> <fmt> ...
402 newMLine.append(origMLineParts[origPartIndex++]).append(" ");
403 newMLine.append(origMLineParts[origPartIndex++]).append(" ");
404 newMLine.append(origMLineParts[origPartIndex++]).append(" ");
405 newMLine.append(isac16kRtpMap);
406 for (; origPartIndex < origMLineParts.length; ++origPartIndex) {
407 if (!origMLineParts[origPartIndex].equals(isac16kRtpMap)) {
408 newMLine.append(" ").append(origMLineParts[origPartIndex]);
411 lines[mLineIndex] = newMLine.toString();
412 StringBuilder newSdpDescription = new StringBuilder();
413 for (String line : lines) {
414 newSdpDescription.append(line).append("\r\n");
416 return newSdpDescription.toString();
419 // Implementation detail: observe ICE & stream changes and react accordingly.
420 private class PCObserver implements PeerConnection.Observer {
421 @Override public void onIceCandidate(final IceCandidate candidate){
422 runOnUiThread(new Runnable() {
424 JSONObject json = new JSONObject();
425 jsonPut(json, "type", "candidate");
426 jsonPut(json, "label", candidate.sdpMLineIndex);
427 jsonPut(json, "id", candidate.sdpMid);
428 jsonPut(json, "candidate", candidate.sdp);
434 @Override public void onError(){
435 runOnUiThread(new Runnable() {
437 throw new RuntimeException("PeerConnection error!");
442 @Override public void onSignalingChange(
443 PeerConnection.SignalingState newState) {
446 @Override public void onIceConnectionChange(
447 PeerConnection.IceConnectionState newState) {
450 @Override public void onIceGatheringChange(
451 PeerConnection.IceGatheringState newState) {
454 @Override public void onAddStream(final MediaStream stream){
455 runOnUiThread(new Runnable() {
457 abortUnless(stream.audioTracks.size() <= 1 &&
458 stream.videoTracks.size() <= 1,
459 "Weird-looking stream: " + stream);
460 if (stream.videoTracks.size() == 1) {
461 stream.videoTracks.get(0).addRenderer(new VideoRenderer(
462 new VideoCallbacks(vsv, VideoStreamsView.Endpoint.REMOTE)));
468 @Override public void onRemoveStream(final MediaStream stream){
469 runOnUiThread(new Runnable() {
471 stream.videoTracks.get(0).dispose();
476 @Override public void onDataChannel(final DataChannel dc) {
477 runOnUiThread(new Runnable() {
479 throw new RuntimeException(
480 "AppRTC doesn't use data channels, but got: " + dc.label() +
486 @Override public void onRenegotiationNeeded() {
487 // No need to do anything; AppRTC follows a pre-agreed-upon
488 // signaling/negotiation protocol.
492 // Implementation detail: handle offer creation/signaling and answer setting,
493 // as well as adding remote ICE candidates once the answer SDP is set.
494 private class SDPObserver implements SdpObserver {
495 private SessionDescription localSdp;
497 @Override public void onCreateSuccess(final SessionDescription origSdp) {
498 abortUnless(localSdp == null, "multiple SDP create?!?");
499 final SessionDescription sdp = new SessionDescription(
500 origSdp.type, preferISAC(origSdp.description));
502 runOnUiThread(new Runnable() {
504 pc.setLocalDescription(sdpObserver, sdp);
509 // Helper for sending local SDP (offer or answer, depending on role) to the
510 // other participant. Note that it is important to send the output of
511 // create{Offer,Answer} and not merely the current value of
512 // getLocalDescription() because the latter may include ICE candidates that
513 // we might want to filter elsewhere.
514 private void sendLocalDescription() {
515 logAndToast("Sending " + localSdp.type);
516 JSONObject json = new JSONObject();
517 jsonPut(json, "type", localSdp.type.canonicalForm());
518 jsonPut(json, "sdp", localSdp.description);
522 @Override public void onSetSuccess() {
523 runOnUiThread(new Runnable() {
525 if (appRtcClient.isInitiator()) {
526 if (pc.getRemoteDescription() != null) {
527 // We've set our local offer and received & set the remote
528 // answer, so drain candidates.
529 drainRemoteCandidates();
531 // We've just set our local description so time to send it.
532 sendLocalDescription();
535 if (pc.getLocalDescription() == null) {
536 // We just set the remote offer, time to create our answer.
537 logAndToast("Creating answer");
538 pc.createAnswer(SDPObserver.this, sdpMediaConstraints);
540 // Answer now set as local description; send it and drain
542 sendLocalDescription();
543 drainRemoteCandidates();
550 @Override public void onCreateFailure(final String error) {
551 runOnUiThread(new Runnable() {
553 throw new RuntimeException("createSDP error: " + error);
558 @Override public void onSetFailure(final String error) {
559 runOnUiThread(new Runnable() {
561 throw new RuntimeException("setSDP error: " + error);
566 private void drainRemoteCandidates() {
567 for (IceCandidate candidate : queuedRemoteCandidates) {
568 pc.addIceCandidate(candidate);
570 queuedRemoteCandidates = null;
574 // Implementation detail: handler for receiving GAE messages and dispatching
575 // them appropriately.
576 private class GAEHandler implements GAEChannelClient.MessageHandler {
577 @JavascriptInterface public void onOpen() {
578 if (!appRtcClient.isInitiator()) {
581 logAndToast("Creating offer...");
582 pc.createOffer(sdpObserver, sdpMediaConstraints);
585 @JavascriptInterface public void onMessage(String data) {
587 JSONObject json = new JSONObject(data);
588 String type = (String) json.get("type");
589 if (type.equals("candidate")) {
590 IceCandidate candidate = new IceCandidate(
591 (String) json.get("id"),
592 json.getInt("label"),
593 (String) json.get("candidate"));
594 if (queuedRemoteCandidates != null) {
595 queuedRemoteCandidates.add(candidate);
597 pc.addIceCandidate(candidate);
599 } else if (type.equals("answer") || type.equals("offer")) {
600 SessionDescription sdp = new SessionDescription(
601 SessionDescription.Type.fromCanonicalForm(type),
602 preferISAC((String) json.get("sdp")));
603 pc.setRemoteDescription(sdpObserver, sdp);
604 } else if (type.equals("bye")) {
605 logAndToast("Remote end hung up; dropping PeerConnection");
608 throw new RuntimeException("Unexpected message: " + data);
610 } catch (JSONException e) {
611 throw new RuntimeException(e);
615 @JavascriptInterface public void onClose() {
619 @JavascriptInterface public void onError(int code, String description) {
624 // Disconnect from remote resources, dispose of local resources, and exit.
625 private void disconnectAndExit() {
626 synchronized (quit[0]) {
635 if (appRtcClient != null) {
636 appRtcClient.sendMessage("{\"type\": \"bye\"}");
637 appRtcClient.disconnect();
640 if (videoSource != null) {
641 videoSource.dispose();
644 if (factory != null) {
652 // Implementation detail: bridge the VideoRenderer.Callbacks interface to the
653 // VideoStreamsView implementation.
654 private class VideoCallbacks implements VideoRenderer.Callbacks {
655 private final VideoStreamsView view;
656 private final VideoStreamsView.Endpoint stream;
658 public VideoCallbacks(
659 VideoStreamsView view, VideoStreamsView.Endpoint stream) {
661 this.stream = stream;
665 public void setSize(final int width, final int height) {
666 view.queueEvent(new Runnable() {
668 view.setSize(stream, width, height);
674 public void renderFrame(I420Frame frame) {
675 view.queueFrame(stream, frame);