5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Android Studio project for AppRTCDemo of WebRTC project
2 |
3 | This should let you build AppRTCDemo without much effort.
4 | I built WebRTC distribution on 9/6/2014, extracted `trunk/talk/examples/android`,
5 | and it is imported to Android Studio.
6 |
7 | ## Background
8 |
9 | It is not straightforward to build WebRTC for Android on Mac OS X.
10 |
11 | - WebRTC can be only built on Linux.
12 | - You need to have 6G+ disk space to build.
13 | - Git clone takes up an hour.
14 | - It requries a lot of libraries/packages installed.
15 |
16 | However, it turned out that at the end of the day, you just need a few files to
17 | build it on Android.
18 |
19 | ## Prerequisite
20 |
21 | While this requries almost no effort to build, you need:
22 |
23 | - Android NDK installed on your machine.
24 | - Point the NDK installation via `local.properties`
25 |
--------------------------------------------------------------------------------
/WebRTCDemo.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/import-summary.txt:
--------------------------------------------------------------------------------
1 | ECLIPSE ANDROID PROJECT IMPORT SUMMARY
2 | ======================================
3 |
4 | Ignored Files:
5 | --------------
6 | The following files were *not* copied into the new Gradle project; you
7 | should evaluate whether these are still needed in your project and if
8 | so manually move them:
9 |
10 | * README
11 | * ant.properties
12 | * build.xml
13 |
14 | Moved Files:
15 | ------------
16 | Android Gradle projects use a different directory structure than ADT
17 | Eclipse projects. Here's how the projects were restructured:
18 |
19 | * AndroidManifest.xml => app/src/main/AndroidManifest.xml
20 | * assets/ => app/src/main/assets/
21 | * jni/ => app/src/main/jni/
22 | * libs/armeabi-v7a/libjingle_peerconnection_so.so => app/src/main/jniLibs/armeabi-v7a/libjingle_peerconnection_so.so
23 | * libs/libjingle_peerconnection.jar => app/libs/libjingle_peerconnection.jar
24 | * res/ => app/src/main/res/
25 | * src/ => app/src/main/java/
26 |
27 | Next Steps:
28 | -----------
29 | You can now build the project. The Gradle project needs network
30 | connectivity to download dependencies.
31 |
32 | Bugs:
33 | -----
34 | If for some reason your project does not build, and you determine that
35 | it is due to a bug or limitation of the Eclipse to Gradle importer,
36 | please file a bug at http://b.android.com with category
37 | Component-Tools.
38 |
39 | (This import summary is for your information only, and can be deleted
40 | after import once you are satisfied with the results.)
41 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2011, The WebRTC project authors. All rights reserved.
2 |
3 | Redistribution and use in source and binary forms, with or without
4 | modification, are permitted provided that the following conditions are
5 | met:
6 |
7 | * Redistributions of source code must retain the above copyright
8 | notice, this list of conditions and the following disclaimer.
9 |
10 | * Redistributions in binary form must reproduce the above copyright
11 | notice, this list of conditions and the following disclaimer in
12 | the documentation and/or other materials provided with the
13 | distribution.
14 |
15 | * Neither the name of Google nor the names of its contributors may
16 | be used to endorse or promote products derived from this software
17 | without specific prior written permission.
18 |
19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
22 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/app/src/main/java/org/appspot/apprtc/AppRTCGLView.java:
--------------------------------------------------------------------------------
1 | /*
2 | * libjingle
3 | * Copyright 2014, 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.content.Context;
31 | import android.graphics.Point;
32 | import android.opengl.GLSurfaceView;
33 |
34 | public class AppRTCGLView extends GLSurfaceView {
35 | private Point screenDimensions;
36 |
37 | public AppRTCGLView(Context c, Point screenDimensions) {
38 | super(c);
39 | this.screenDimensions = screenDimensions;
40 | }
41 |
42 | public void updateDisplaySize(Point screenDimensions) {
43 | this.screenDimensions = screenDimensions;
44 | }
45 |
46 | @Override
47 | protected void onMeasure(int unusedX, int unusedY) {
48 | // Go big or go home!
49 | setMeasuredDimension(screenDimensions.x, screenDimensions.y);
50 | }
51 |
52 | @Override
53 | protected void onAttachedToWindow() {
54 | super.onAttachedToWindow();
55 | setSystemUiVisibility(SYSTEM_UI_FLAG_HIDE_NAVIGATION |
56 | SYSTEM_UI_FLAG_FULLSCREEN | SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/app/src/main/java/org/appspot/apprtc/UnhandledExceptionHandler.java:
--------------------------------------------------------------------------------
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.util.Log;
34 | import android.util.TypedValue;
35 | import android.widget.ScrollView;
36 | import android.widget.TextView;
37 |
38 | import java.io.PrintWriter;
39 | import java.io.StringWriter;
40 |
41 | /**
42 | * Singleton helper: install a default unhandled exception handler which shows
43 | * an informative dialog and kills the app. Useful for apps whose
44 | * error-handling consists of throwing RuntimeExceptions.
45 | * NOTE: almost always more useful to
46 | * Thread.setDefaultUncaughtExceptionHandler() rather than
47 | * Thread.setUncaughtExceptionHandler(), to apply to background threads as well.
48 | */
49 | public class UnhandledExceptionHandler
50 | implements Thread.UncaughtExceptionHandler {
51 | private static final String TAG = "AppRTCDemoActivity";
52 | private final Activity activity;
53 |
54 | public UnhandledExceptionHandler(final Activity activity) {
55 | this.activity = activity;
56 | }
57 |
58 | public void uncaughtException(Thread unusedThread, final Throwable e) {
59 | activity.runOnUiThread(new Runnable() {
60 | @Override public void run() {
61 | String title = "Fatal error: " + getTopLevelCauseMessage(e);
62 | String msg = getRecursiveStackTrace(e);
63 | TextView errorView = new TextView(activity);
64 | errorView.setText(msg);
65 | errorView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 8);
66 | ScrollView scrollingContainer = new ScrollView(activity);
67 | scrollingContainer.addView(errorView);
68 | Log.e(TAG, title + "\n\n" + msg);
69 | DialogInterface.OnClickListener listener =
70 | new DialogInterface.OnClickListener() {
71 | @Override public void onClick(
72 | DialogInterface dialog, int which) {
73 | dialog.dismiss();
74 | System.exit(1);
75 | }
76 | };
77 | AlertDialog.Builder builder =
78 | new AlertDialog.Builder(activity);
79 | builder
80 | .setTitle(title)
81 | .setView(scrollingContainer)
82 | .setPositiveButton("Exit", listener).show();
83 | }
84 | });
85 | }
86 |
87 | // Returns the Message attached to the original Cause of |t|.
88 | private static String getTopLevelCauseMessage(Throwable t) {
89 | Throwable topLevelCause = t;
90 | while (topLevelCause.getCause() != null) {
91 | topLevelCause = topLevelCause.getCause();
92 | }
93 | return topLevelCause.getMessage();
94 | }
95 |
96 | // Returns a human-readable String of the stacktrace in |t|, recursively
97 | // through all Causes that led to |t|.
98 | private static String getRecursiveStackTrace(Throwable t) {
99 | StringWriter writer = new StringWriter();
100 | t.printStackTrace(new PrintWriter(writer));
101 | return writer.toString();
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # For Cygwin, ensure paths are in UNIX format before anything is touched.
46 | if $cygwin ; then
47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
48 | fi
49 |
50 | # Attempt to set APP_HOME
51 | # Resolve links: $0 may be a link
52 | PRG="$0"
53 | # Need this for relative symlinks.
54 | while [ -h "$PRG" ] ; do
55 | ls=`ls -ld "$PRG"`
56 | link=`expr "$ls" : '.*-> \(.*\)$'`
57 | if expr "$link" : '/.*' > /dev/null; then
58 | PRG="$link"
59 | else
60 | PRG=`dirname "$PRG"`"/$link"
61 | fi
62 | done
63 | SAVED="`pwd`"
64 | cd "`dirname \"$PRG\"`/" >&-
65 | APP_HOME="`pwd -P`"
66 | cd "$SAVED" >&-
67 |
68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
69 |
70 | # Determine the Java command to use to start the JVM.
71 | if [ -n "$JAVA_HOME" ] ; then
72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
73 | # IBM's JDK on AIX uses strange locations for the executables
74 | JAVACMD="$JAVA_HOME/jre/sh/java"
75 | else
76 | JAVACMD="$JAVA_HOME/bin/java"
77 | fi
78 | if [ ! -x "$JAVACMD" ] ; then
79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
80 |
81 | Please set the JAVA_HOME variable in your environment to match the
82 | location of your Java installation."
83 | fi
84 | else
85 | JAVACMD="java"
86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
87 |
88 | Please set the JAVA_HOME variable in your environment to match the
89 | location of your Java installation."
90 | fi
91 |
92 | # Increase the maximum file descriptors if we can.
93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
94 | MAX_FD_LIMIT=`ulimit -H -n`
95 | if [ $? -eq 0 ] ; then
96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
97 | MAX_FD="$MAX_FD_LIMIT"
98 | fi
99 | ulimit -n $MAX_FD
100 | if [ $? -ne 0 ] ; then
101 | warn "Could not set maximum file descriptor limit: $MAX_FD"
102 | fi
103 | else
104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
105 | fi
106 | fi
107 |
108 | # For Darwin, add options to specify how the application appears in the dock
109 | if $darwin; then
110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
111 | fi
112 |
113 | # For Cygwin, switch paths to Windows format before running java
114 | if $cygwin ; then
115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
158 | function splitJvmOpts() {
159 | JVM_OPTS=("$@")
160 | }
161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
163 |
164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
165 |
--------------------------------------------------------------------------------
/app/app.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
--------------------------------------------------------------------------------
/app/src/main/java/org/appspot/apprtc/GAEChannelClient.java:
--------------------------------------------------------------------------------
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.annotation.SuppressLint;
31 | import android.app.Activity;
32 | import android.util.Log;
33 | import android.webkit.ConsoleMessage;
34 | import android.webkit.JavascriptInterface;
35 | import android.webkit.WebChromeClient;
36 | import android.webkit.WebView;
37 | import android.webkit.WebViewClient;
38 |
39 | /**
40 | * Java-land version of Google AppEngine's JavaScript Channel API:
41 | * https://developers.google.com/appengine/docs/python/channel/javascript
42 | *
43 | * Requires a hosted HTML page that opens the desired channel and dispatches JS
44 | * on{Open,Message,Close,Error}() events to a global object named
45 | * "androidMessageHandler".
46 | */
47 | public class GAEChannelClient {
48 | private static final String TAG = "GAEChannelClient";
49 | private WebView webView;
50 | private final ProxyingMessageHandler proxyingMessageHandler;
51 |
52 | /**
53 | * Callback interface for messages delivered on the Google AppEngine channel.
54 | *
55 | * Methods are guaranteed to be invoked on the UI thread of |activity| passed
56 | * to GAEChannelClient's constructor.
57 | */
58 | public interface MessageHandler {
59 | public void onOpen();
60 | public void onMessage(String data);
61 | public void onClose();
62 | public void onError(int code, String description);
63 | }
64 |
65 | /** Asynchronously open an AppEngine channel. */
66 | @SuppressLint("SetJavaScriptEnabled")
67 | public GAEChannelClient(
68 | Activity activity, String token, MessageHandler handler) {
69 | webView = new WebView(activity);
70 | webView.getSettings().setJavaScriptEnabled(true);
71 | webView.setWebChromeClient(new WebChromeClient() { // Purely for debugging.
72 | public boolean onConsoleMessage (ConsoleMessage msg) {
73 | Log.d(TAG, "console: " + msg.message() + " at " +
74 | msg.sourceId() + ":" + msg.lineNumber());
75 | return false;
76 | }
77 | });
78 | webView.setWebViewClient(new WebViewClient() { // Purely for debugging.
79 | public void onReceivedError(
80 | WebView view, int errorCode, String description,
81 | String failingUrl) {
82 | Log.e(TAG, "JS error: " + errorCode + " in " + failingUrl +
83 | ", desc: " + description);
84 | }
85 | });
86 | proxyingMessageHandler =
87 | new ProxyingMessageHandler(activity, handler, token);
88 | webView.addJavascriptInterface(
89 | proxyingMessageHandler, "androidMessageHandler");
90 | webView.loadUrl("file:///android_asset/channel.html");
91 | }
92 |
93 | /** Close the connection to the AppEngine channel. */
94 | public void close() {
95 | if (webView == null) {
96 | return;
97 | }
98 | proxyingMessageHandler.disconnect();
99 | webView.removeJavascriptInterface("androidMessageHandler");
100 | webView.loadUrl("about:blank");
101 | webView = null;
102 | }
103 |
104 | // Helper class for proxying callbacks from the Java<->JS interaction
105 | // (private, background) thread to the Activity's UI thread.
106 | private static class ProxyingMessageHandler {
107 | private final Activity activity;
108 | private final MessageHandler handler;
109 | private final boolean[] disconnected = { false };
110 | private final String token;
111 |
112 | public
113 | ProxyingMessageHandler(Activity activity, MessageHandler handler,
114 | String token) {
115 | this.activity = activity;
116 | this.handler = handler;
117 | this.token = token;
118 | }
119 |
120 | public void disconnect() {
121 | disconnected[0] = true;
122 | }
123 |
124 | private boolean disconnected() {
125 | return disconnected[0];
126 | }
127 |
128 | @JavascriptInterface public String getToken() {
129 | return token;
130 | }
131 |
132 | @JavascriptInterface public void onOpen() {
133 | activity.runOnUiThread(new Runnable() {
134 | public void run() {
135 | if (!disconnected()) {
136 | handler.onOpen();
137 | }
138 | }
139 | });
140 | }
141 |
142 | @JavascriptInterface public void onMessage(final String data) {
143 | activity.runOnUiThread(new Runnable() {
144 | public void run() {
145 | if (!disconnected()) {
146 | handler.onMessage(data);
147 | }
148 | }
149 | });
150 | }
151 |
152 | @JavascriptInterface public void onClose() {
153 | activity.runOnUiThread(new Runnable() {
154 | public void run() {
155 | if (!disconnected()) {
156 | handler.onClose();
157 | }
158 | }
159 | });
160 | }
161 |
162 | @JavascriptInterface public void onError(
163 | final int code, final String description) {
164 | activity.runOnUiThread(new Runnable() {
165 | public void run() {
166 | if (!disconnected()) {
167 | handler.onError(code, description);
168 | }
169 | }
170 | });
171 | }
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/app/src/main/java/org/appspot/apprtc/AppRTCClient.java:
--------------------------------------------------------------------------------
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.os.AsyncTask;
32 | import android.util.Log;
33 |
34 | import org.json.JSONArray;
35 | import org.json.JSONException;
36 | import org.json.JSONObject;
37 | import org.webrtc.MediaConstraints;
38 | import org.webrtc.PeerConnection;
39 |
40 | import java.io.IOException;
41 | import java.io.InputStream;
42 | import java.net.HttpURLConnection;
43 | import java.net.URL;
44 | import java.net.URLConnection;
45 | import java.util.LinkedList;
46 | import java.util.List;
47 | import java.util.Scanner;
48 |
49 | /**
50 | * Negotiates signaling for chatting with apprtc.appspot.com "rooms".
51 | * Uses the client<->server specifics of the apprtc AppEngine webapp.
52 | *
53 | * To use: create an instance of this object (registering a message handler) and
54 | * call connectToRoom(). Once that's done call sendMessage() and wait for the
55 | * registered handler to be called with received messages.
56 | */
57 | public class AppRTCClient {
58 | private static final String TAG = "AppRTCClient";
59 | private GAEChannelClient channelClient;
60 | private final Activity activity;
61 | private final GAEChannelClient.MessageHandler gaeHandler;
62 | private final IceServersObserver iceServersObserver;
63 |
64 | // These members are only read/written under sendQueue's lock.
65 | private LinkedList sendQueue = new LinkedList();
66 | private AppRTCSignalingParameters appRTCSignalingParameters;
67 |
68 | /**
69 | * Callback fired once the room's signaling parameters specify the set of
70 | * ICE servers to use.
71 | */
72 | public static interface IceServersObserver {
73 | public void onIceServers(List iceServers);
74 | }
75 |
76 | public AppRTCClient(
77 | Activity activity, GAEChannelClient.MessageHandler gaeHandler,
78 | IceServersObserver iceServersObserver) {
79 | this.activity = activity;
80 | this.gaeHandler = gaeHandler;
81 | this.iceServersObserver = iceServersObserver;
82 | }
83 |
84 | /**
85 | * Asynchronously connect to an AppRTC room URL, e.g.
86 | * https://apprtc.appspot.com/?r=NNN and register message-handling callbacks
87 | * on its GAE Channel.
88 | */
89 | public void connectToRoom(String url) {
90 | while (url.indexOf('?') < 0) {
91 | // Keep redirecting until we get a room number.
92 | (new RedirectResolver()).execute(url);
93 | return; // RedirectResolver above calls us back with the next URL.
94 | }
95 | (new RoomParameterGetter()).execute(url);
96 | }
97 |
98 | /**
99 | * Disconnect from the GAE Channel.
100 | */
101 | public void disconnect() {
102 | if (channelClient != null) {
103 | channelClient.close();
104 | channelClient = null;
105 | }
106 | }
107 |
108 | /**
109 | * Queue a message for sending to the room's channel and send it if already
110 | * connected (other wise queued messages are drained when the channel is
111 | eventually established).
112 | */
113 | public synchronized void sendMessage(String msg) {
114 | synchronized (sendQueue) {
115 | sendQueue.add(msg);
116 | }
117 | requestQueueDrainInBackground();
118 | }
119 |
120 | public boolean isInitiator() {
121 | return appRTCSignalingParameters.initiator;
122 | }
123 |
124 | public MediaConstraints pcConstraints() {
125 | return appRTCSignalingParameters.pcConstraints;
126 | }
127 |
128 | public MediaConstraints videoConstraints() {
129 | return appRTCSignalingParameters.videoConstraints;
130 | }
131 |
132 | public MediaConstraints audioConstraints() {
133 | return appRTCSignalingParameters.audioConstraints;
134 | }
135 |
136 | // Struct holding the signaling parameters of an AppRTC room.
137 | private class AppRTCSignalingParameters {
138 | public final List iceServers;
139 | public final String gaeBaseHref;
140 | public final String channelToken;
141 | public final String postMessageUrl;
142 | public final boolean initiator;
143 | public final MediaConstraints pcConstraints;
144 | public final MediaConstraints videoConstraints;
145 | public final MediaConstraints audioConstraints;
146 |
147 | public AppRTCSignalingParameters(
148 | List iceServers,
149 | String gaeBaseHref, String channelToken, String postMessageUrl,
150 | boolean initiator, MediaConstraints pcConstraints,
151 | MediaConstraints videoConstraints, MediaConstraints audioConstraints) {
152 | this.iceServers = iceServers;
153 | this.gaeBaseHref = gaeBaseHref;
154 | this.channelToken = channelToken;
155 | this.postMessageUrl = postMessageUrl;
156 | this.initiator = initiator;
157 | this.pcConstraints = pcConstraints;
158 | this.videoConstraints = videoConstraints;
159 | this.audioConstraints = audioConstraints;
160 | }
161 | }
162 |
163 | // Load the given URL and return the value of the Location header of the
164 | // resulting 302 response. If the result is not a 302, throws.
165 | private class RedirectResolver extends AsyncTask {
166 | @Override
167 | protected String doInBackground(String... urls) {
168 | if (urls.length != 1) {
169 | throw new RuntimeException("Must be called with a single URL");
170 | }
171 | try {
172 | return followRedirect(urls[0]);
173 | } catch (IOException e) {
174 | throw new RuntimeException(e);
175 | }
176 | }
177 |
178 | @Override
179 | protected void onPostExecute(String url) {
180 | connectToRoom(url);
181 | }
182 |
183 | private String followRedirect(String url) throws IOException {
184 | HttpURLConnection connection = (HttpURLConnection)
185 | new URL(url).openConnection();
186 | connection.setInstanceFollowRedirects(false);
187 | int code = connection.getResponseCode();
188 | if (code != HttpURLConnection.HTTP_MOVED_TEMP) {
189 | throw new IOException("Unexpected response: " + code + " for " + url +
190 | ", with contents: " + drainStream(connection.getInputStream()));
191 | }
192 | int n = 0;
193 | String name, value;
194 | while ((name = connection.getHeaderFieldKey(n)) != null) {
195 | value = connection.getHeaderField(n);
196 | if (name.equals("Location")) {
197 | return value;
198 | }
199 | ++n;
200 | }
201 | throw new IOException("Didn't find Location header!");
202 | }
203 | }
204 |
205 | // AsyncTask that converts an AppRTC room URL into the set of signaling
206 | // parameters to use with that room.
207 | private class RoomParameterGetter
208 | extends AsyncTask {
209 | @Override
210 | protected AppRTCSignalingParameters doInBackground(String... urls) {
211 | if (urls.length != 1) {
212 | throw new RuntimeException("Must be called with a single URL");
213 | }
214 | try {
215 | return getParametersForRoomUrl(urls[0]);
216 | } catch (JSONException e) {
217 | throw new RuntimeException(e);
218 | } catch (IOException e) {
219 | throw new RuntimeException(e);
220 | }
221 | }
222 |
223 | @Override
224 | protected void onPostExecute(AppRTCSignalingParameters params) {
225 | channelClient =
226 | new GAEChannelClient(activity, params.channelToken, gaeHandler);
227 | synchronized (sendQueue) {
228 | appRTCSignalingParameters = params;
229 | }
230 | requestQueueDrainInBackground();
231 | iceServersObserver.onIceServers(appRTCSignalingParameters.iceServers);
232 | }
233 |
234 | // Fetches |url| and fishes the signaling parameters out of the JSON.
235 | private AppRTCSignalingParameters getParametersForRoomUrl(String url)
236 | throws IOException, JSONException {
237 | url = url + "&t=json";
238 | JSONObject roomJson = new JSONObject(
239 | drainStream((new URL(url)).openConnection().getInputStream()));
240 |
241 | if (roomJson.has("error")) {
242 | JSONArray errors = roomJson.getJSONArray("error_messages");
243 | throw new IOException(errors.toString());
244 | }
245 |
246 | String gaeBaseHref = url.substring(0, url.indexOf('?'));
247 | String token = roomJson.getString("token");
248 | String postMessageUrl = "/message?r=" +
249 | roomJson.getString("room_key") + "&u=" +
250 | roomJson.getString("me");
251 | boolean initiator = roomJson.getInt("initiator") == 1;
252 | LinkedList iceServers =
253 | iceServersFromPCConfigJSON(roomJson.getString("pc_config"));
254 |
255 | boolean isTurnPresent = false;
256 | for (PeerConnection.IceServer server : iceServers) {
257 | if (server.uri.startsWith("turn:")) {
258 | isTurnPresent = true;
259 | break;
260 | }
261 | }
262 | if (!isTurnPresent) {
263 | iceServers.add(requestTurnServer(roomJson.getString("turn_url")));
264 | }
265 |
266 | MediaConstraints pcConstraints = constraintsFromJSON(
267 | roomJson.getString("pc_constraints"));
268 | addDTLSConstraintIfMissing(pcConstraints);
269 | Log.d(TAG, "pcConstraints: " + pcConstraints);
270 | MediaConstraints videoConstraints = constraintsFromJSON(
271 | getAVConstraints("video",
272 | roomJson.getString("media_constraints")));
273 | Log.d(TAG, "videoConstraints: " + videoConstraints);
274 | MediaConstraints audioConstraints = constraintsFromJSON(
275 | getAVConstraints("audio",
276 | roomJson.getString("media_constraints")));
277 | Log.d(TAG, "audioConstraints: " + audioConstraints);
278 |
279 | return new AppRTCSignalingParameters(
280 | iceServers, gaeBaseHref, token, postMessageUrl, initiator,
281 | pcConstraints, videoConstraints, audioConstraints);
282 | }
283 |
284 | // Mimic Chrome and set DtlsSrtpKeyAgreement to true if not set to false by
285 | // the web-app.
286 | private void addDTLSConstraintIfMissing(
287 | MediaConstraints pcConstraints) {
288 | for (MediaConstraints.KeyValuePair pair : pcConstraints.mandatory) {
289 | if (pair.getKey().equals("DtlsSrtpKeyAgreement")) {
290 | return;
291 | }
292 | }
293 | for (MediaConstraints.KeyValuePair pair : pcConstraints.optional) {
294 | if (pair.getKey().equals("DtlsSrtpKeyAgreement")) {
295 | return;
296 | }
297 | }
298 | // DTLS isn't being suppressed (e.g. for debug=loopback calls), so enable
299 | // it by default.
300 | pcConstraints.optional.add(
301 | new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true"));
302 | }
303 |
304 | // Return the constraints specified for |type| of "audio" or "video" in
305 | // |mediaConstraintsString|.
306 | private String getAVConstraints(
307 | String type, String mediaConstraintsString) {
308 | try {
309 | JSONObject json = new JSONObject(mediaConstraintsString);
310 | // Tricksy handling of values that are allowed to be (boolean or
311 | // MediaTrackConstraints) by the getUserMedia() spec. There are three
312 | // cases below.
313 | if (!json.has(type) || !json.optBoolean(type, true)) {
314 | // Case 1: "audio"/"video" is not present, or is an explicit "false"
315 | // boolean.
316 | return null;
317 | }
318 | if (json.optBoolean(type, false)) {
319 | // Case 2: "audio"/"video" is an explicit "true" boolean.
320 | return "{\"mandatory\": {}, \"optional\": []}";
321 | }
322 | // Case 3: "audio"/"video" is an object.
323 | return json.getJSONObject(type).toString();
324 | } catch (JSONException e) {
325 | throw new RuntimeException(e);
326 | }
327 | }
328 |
329 | private MediaConstraints constraintsFromJSON(String jsonString) {
330 | if (jsonString == null) {
331 | return null;
332 | }
333 | try {
334 | MediaConstraints constraints = new MediaConstraints();
335 | JSONObject json = new JSONObject(jsonString);
336 | JSONObject mandatoryJSON = json.optJSONObject("mandatory");
337 | if (mandatoryJSON != null) {
338 | JSONArray mandatoryKeys = mandatoryJSON.names();
339 | if (mandatoryKeys != null) {
340 | for (int i = 0; i < mandatoryKeys.length(); ++i) {
341 | String key = mandatoryKeys.getString(i);
342 | String value = mandatoryJSON.getString(key);
343 | constraints.mandatory.add(
344 | new MediaConstraints.KeyValuePair(key, value));
345 | }
346 | }
347 | }
348 | JSONArray optionalJSON = json.optJSONArray("optional");
349 | if (optionalJSON != null) {
350 | for (int i = 0; i < optionalJSON.length(); ++i) {
351 | JSONObject keyValueDict = optionalJSON.getJSONObject(i);
352 | String key = keyValueDict.names().getString(0);
353 | String value = keyValueDict.getString(key);
354 | constraints.optional.add(
355 | new MediaConstraints.KeyValuePair(key, value));
356 | }
357 | }
358 | return constraints;
359 | } catch (JSONException e) {
360 | throw new RuntimeException(e);
361 | }
362 | }
363 |
364 | // Requests & returns a TURN ICE Server based on a request URL. Must be run
365 | // off the main thread!
366 | private PeerConnection.IceServer requestTurnServer(String url) {
367 | try {
368 | URLConnection connection = (new URL(url)).openConnection();
369 | connection.addRequestProperty("user-agent", "Mozilla/5.0");
370 | connection.addRequestProperty("origin", "https://apprtc.appspot.com");
371 | String response = drainStream(connection.getInputStream());
372 | JSONObject responseJSON = new JSONObject(response);
373 | String uri = responseJSON.getJSONArray("uris").getString(0);
374 | String username = responseJSON.getString("username");
375 | String password = responseJSON.getString("password");
376 | return new PeerConnection.IceServer(uri, username, password);
377 | } catch (JSONException e) {
378 | throw new RuntimeException(e);
379 | } catch (IOException e) {
380 | throw new RuntimeException(e);
381 | }
382 | }
383 | }
384 |
385 | // Return the list of ICE servers described by a WebRTCPeerConnection
386 | // configuration string.
387 | private LinkedList iceServersFromPCConfigJSON(
388 | String pcConfig) {
389 | try {
390 | JSONObject json = new JSONObject(pcConfig);
391 | JSONArray servers = json.getJSONArray("iceServers");
392 | LinkedList ret =
393 | new LinkedList();
394 | for (int i = 0; i < servers.length(); ++i) {
395 | JSONObject server = servers.getJSONObject(i);
396 | String url = server.getString("urls");
397 | String credential =
398 | server.has("credential") ? server.getString("credential") : "";
399 | ret.add(new PeerConnection.IceServer(url, "", credential));
400 | }
401 | return ret;
402 | } catch (JSONException e) {
403 | throw new RuntimeException(e);
404 | }
405 | }
406 |
407 | // Request an attempt to drain the send queue, on a background thread.
408 | private void requestQueueDrainInBackground() {
409 | (new AsyncTask() {
410 | public Void doInBackground(Void... unused) {
411 | maybeDrainQueue();
412 | return null;
413 | }
414 | }).execute();
415 | }
416 |
417 | // Send all queued messages if connected to the room.
418 | private void maybeDrainQueue() {
419 | synchronized (sendQueue) {
420 | if (appRTCSignalingParameters == null) {
421 | return;
422 | }
423 | try {
424 | for (String msg : sendQueue) {
425 | URLConnection connection = new URL(
426 | appRTCSignalingParameters.gaeBaseHref +
427 | appRTCSignalingParameters.postMessageUrl).openConnection();
428 | connection.setDoOutput(true);
429 | connection.getOutputStream().write(msg.getBytes("UTF-8"));
430 | if (!connection.getHeaderField(null).startsWith("HTTP/1.1 200 ")) {
431 | throw new IOException(
432 | "Non-200 response to POST: " + connection.getHeaderField(null) +
433 | " for msg: " + msg);
434 | }
435 | }
436 | } catch (IOException e) {
437 | throw new RuntimeException(e);
438 | }
439 | sendQueue.clear();
440 | }
441 | }
442 |
443 | // Return the contents of an InputStream as a String.
444 | private static String drainStream(InputStream in) {
445 | Scanner s = new Scanner(in).useDelimiter("\\A");
446 | String output = s.hasNext() ? s.next() : "";
447 | Log.d(TAG, "### OUTPUT:" + output);
448 | return output;
449 | }
450 | }
451 |
--------------------------------------------------------------------------------
/app/src/main/java/org/appspot/apprtc/AppRTCDemoActivity.java:
--------------------------------------------------------------------------------
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 queuedRemoteCandidates =
97 | new LinkedList();
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 | // bweforvideo to show statistics for video Bandwidth Estimation,
204 | // which is global per-session.
205 | if (report.id.equals("bweforvideo")) {
206 | for (StatsReport.Value value : report.values) {
207 | String name = value.name.replace("goog", "")
208 | .replace("Available", "").replace("Bandwidth", "")
209 | .replace("Bitrate", "").replace("Enc", "");
210 |
211 | builder.append(name).append("=").append(value.value)
212 | .append(" ");
213 | }
214 | builder.append("\n");
215 | } else if (report.type.equals("googCandidatePair")) {
216 | String activeConnectionStats = getActiveConnectionStats(report);
217 | if (activeConnectionStats == null) {
218 | continue;
219 | }
220 | builder.append(activeConnectionStats);
221 | } else {
222 | continue;
223 | }
224 | builder.append("\n");
225 | }
226 | hudView.setText(builder.toString() + hudView.getText());
227 | }
228 |
229 | // Return the active connection stats else return null
230 | private String getActiveConnectionStats(StatsReport report) {
231 | StringBuilder activeConnectionbuilder = new StringBuilder();
232 | // googCandidatePair to show information about the active
233 | // connection.
234 | for (StatsReport.Value value : report.values) {
235 | if (value.name.equals("googActiveConnection")
236 | && value.value.equals("false")) {
237 | return null;
238 | }
239 | String name = value.name.replace("goog", "");
240 | activeConnectionbuilder.append(name).append("=")
241 | .append(value.value).append("\n");
242 | }
243 | return activeConnectionbuilder.toString();
244 | }
245 |
246 | @Override
247 | public void onPause() {
248 | super.onPause();
249 | vsv.onPause();
250 | if (videoSource != null) {
251 | videoSource.stop();
252 | videoSourceStopped = true;
253 | }
254 | }
255 |
256 | @Override
257 | public void onResume() {
258 | super.onResume();
259 | vsv.onResume();
260 | if (videoSource != null && videoSourceStopped) {
261 | videoSource.restart();
262 | }
263 | }
264 |
265 | @Override
266 | public void onConfigurationChanged (Configuration newConfig) {
267 | Point displaySize = new Point();
268 | getWindowManager().getDefaultDisplay().getSize(displaySize);
269 | vsv.updateDisplaySize(displaySize);
270 | super.onConfigurationChanged(newConfig);
271 | }
272 |
273 | // Just for fun (and to regression-test bug 2302) make sure that DataChannels
274 | // can be created, queried, and disposed.
275 | private static void createDataChannelToRegressionTestBug2302(
276 | PeerConnection pc) {
277 | DataChannel dc = pc.createDataChannel("dcLabel", new DataChannel.Init());
278 | abortUnless("dcLabel".equals(dc.label()), "Unexpected label corruption?");
279 | dc.close();
280 | dc.dispose();
281 | }
282 |
283 | @Override
284 | public void onIceServers(List iceServers) {
285 | factory = new PeerConnectionFactory();
286 |
287 | MediaConstraints pcConstraints = appRtcClient.pcConstraints();
288 | pcConstraints.optional.add(
289 | new MediaConstraints.KeyValuePair("RtpDataChannels", "true"));
290 | pc = factory.createPeerConnection(iceServers, pcConstraints, pcObserver);
291 |
292 | createDataChannelToRegressionTestBug2302(pc); // See method comment.
293 |
294 | // Uncomment to get ALL WebRTC tracing and SENSITIVE libjingle logging.
295 | // NOTE: this _must_ happen while |factory| is alive!
296 | // Logging.enableTracing(
297 | // "logcat:",
298 | // EnumSet.of(Logging.TraceLevel.TRACE_ALL),
299 | // Logging.Severity.LS_SENSITIVE);
300 |
301 | {
302 | final PeerConnection finalPC = pc;
303 | final Runnable repeatedStatsLogger = new Runnable() {
304 | public void run() {
305 | synchronized (quit[0]) {
306 | if (quit[0]) {
307 | return;
308 | }
309 | final Runnable runnableThis = this;
310 | if (hudView.getVisibility() == View.INVISIBLE) {
311 | vsv.postDelayed(runnableThis, 1000);
312 | return;
313 | }
314 | boolean success = finalPC.getStats(new StatsObserver() {
315 | public void onComplete(final StatsReport[] reports) {
316 | runOnUiThread(new Runnable() {
317 | public void run() {
318 | updateHUD(reports);
319 | }
320 | });
321 | for (StatsReport report : reports) {
322 | Log.d(TAG, "Stats: " + report.toString());
323 | }
324 | vsv.postDelayed(runnableThis, 1000);
325 | }
326 | }, null);
327 | if (!success) {
328 | throw new RuntimeException("getStats() return false!");
329 | }
330 | }
331 | }
332 | };
333 | vsv.postDelayed(repeatedStatsLogger, 1000);
334 | }
335 |
336 | {
337 | logAndToast("Creating local video source...");
338 | MediaStream lMS = factory.createLocalMediaStream("ARDAMS");
339 | if (appRtcClient.videoConstraints() != null) {
340 | VideoCapturer capturer = getVideoCapturer();
341 | videoSource = factory.createVideoSource(
342 | capturer, appRtcClient.videoConstraints());
343 | VideoTrack videoTrack =
344 | factory.createVideoTrack("ARDAMSv0", videoSource);
345 | videoTrack.addRenderer(new VideoRenderer(localRender));
346 | lMS.addTrack(videoTrack);
347 | }
348 | if (appRtcClient.audioConstraints() != null) {
349 | lMS.addTrack(factory.createAudioTrack(
350 | "ARDAMSa0",
351 | factory.createAudioSource(appRtcClient.audioConstraints())));
352 | }
353 | pc.addStream(lMS, new MediaConstraints());
354 | }
355 | logAndToast("Waiting for ICE candidates...");
356 | }
357 |
358 | // Cycle through likely device names for the camera and return the first
359 | // capturer that works, or crash if none do.
360 | private VideoCapturer getVideoCapturer() {
361 | String[] cameraFacing = { "front", "back" };
362 | int[] cameraIndex = { 0, 1 };
363 | int[] cameraOrientation = { 0, 90, 180, 270 };
364 | for (String facing : cameraFacing) {
365 | for (int index : cameraIndex) {
366 | for (int orientation : cameraOrientation) {
367 | String name = "Camera " + index + ", Facing " + facing +
368 | ", Orientation " + orientation;
369 | VideoCapturer capturer = VideoCapturer.create(name);
370 | if (capturer != null) {
371 | logAndToast("Using camera: " + name);
372 | return capturer;
373 | }
374 | }
375 | }
376 | }
377 | throw new RuntimeException("Failed to open capturer");
378 | }
379 |
380 | @Override
381 | protected void onDestroy() {
382 | disconnectAndExit();
383 | super.onDestroy();
384 | }
385 |
386 | // Poor-man's assert(): die with |msg| unless |condition| is true.
387 | private static void abortUnless(boolean condition, String msg) {
388 | if (!condition) {
389 | throw new RuntimeException(msg);
390 | }
391 | }
392 |
393 | // Log |msg| and Toast about it.
394 | private void logAndToast(String msg) {
395 | Log.d(TAG, msg);
396 | if (logToast != null) {
397 | logToast.cancel();
398 | }
399 | logToast = Toast.makeText(this, msg, Toast.LENGTH_SHORT);
400 | logToast.show();
401 | }
402 |
403 | // Send |json| to the underlying AppEngine Channel.
404 | private void sendMessage(JSONObject json) {
405 | appRtcClient.sendMessage(json.toString());
406 | }
407 |
408 | // Put a |key|->|value| mapping in |json|.
409 | private static void jsonPut(JSONObject json, String key, Object value) {
410 | try {
411 | json.put(key, value);
412 | } catch (JSONException e) {
413 | throw new RuntimeException(e);
414 | }
415 | }
416 |
417 | // Mangle SDP to prefer ISAC/16000 over any other audio codec.
418 | private static String preferISAC(String sdpDescription) {
419 | String[] lines = sdpDescription.split("\r\n");
420 | int mLineIndex = -1;
421 | String isac16kRtpMap = null;
422 | Pattern isac16kPattern =
423 | Pattern.compile("^a=rtpmap:(\\d+) ISAC/16000[\r]?$");
424 | for (int i = 0;
425 | (i < lines.length) && (mLineIndex == -1 || isac16kRtpMap == null);
426 | ++i) {
427 | if (lines[i].startsWith("m=audio ")) {
428 | mLineIndex = i;
429 | continue;
430 | }
431 | Matcher isac16kMatcher = isac16kPattern.matcher(lines[i]);
432 | if (isac16kMatcher.matches()) {
433 | isac16kRtpMap = isac16kMatcher.group(1);
434 | continue;
435 | }
436 | }
437 | if (mLineIndex == -1) {
438 | Log.d(TAG, "No m=audio line, so can't prefer iSAC");
439 | return sdpDescription;
440 | }
441 | if (isac16kRtpMap == null) {
442 | Log.d(TAG, "No ISAC/16000 line, so can't prefer iSAC");
443 | return sdpDescription;
444 | }
445 | String[] origMLineParts = lines[mLineIndex].split(" ");
446 | StringBuilder newMLine = new StringBuilder();
447 | int origPartIndex = 0;
448 | // Format is: m= ...
449 | newMLine.append(origMLineParts[origPartIndex++]).append(" ");
450 | newMLine.append(origMLineParts[origPartIndex++]).append(" ");
451 | newMLine.append(origMLineParts[origPartIndex++]).append(" ");
452 | newMLine.append(isac16kRtpMap);
453 | for (; origPartIndex < origMLineParts.length; ++origPartIndex) {
454 | if (!origMLineParts[origPartIndex].equals(isac16kRtpMap)) {
455 | newMLine.append(" ").append(origMLineParts[origPartIndex]);
456 | }
457 | }
458 | lines[mLineIndex] = newMLine.toString();
459 | StringBuilder newSdpDescription = new StringBuilder();
460 | for (String line : lines) {
461 | newSdpDescription.append(line).append("\r\n");
462 | }
463 | return newSdpDescription.toString();
464 | }
465 |
466 | // Implementation detail: observe ICE & stream changes and react accordingly.
467 | private class PCObserver implements PeerConnection.Observer {
468 | @Override public void onIceCandidate(final IceCandidate candidate){
469 | runOnUiThread(new Runnable() {
470 | public void run() {
471 | JSONObject json = new JSONObject();
472 | jsonPut(json, "type", "candidate");
473 | jsonPut(json, "label", candidate.sdpMLineIndex);
474 | jsonPut(json, "id", candidate.sdpMid);
475 | jsonPut(json, "candidate", candidate.sdp);
476 | sendMessage(json);
477 | }
478 | });
479 | }
480 |
481 | @Override public void onError(){
482 | runOnUiThread(new Runnable() {
483 | public void run() {
484 | throw new RuntimeException("PeerConnection error!");
485 | }
486 | });
487 | }
488 |
489 | @Override public void onSignalingChange(
490 | PeerConnection.SignalingState newState) {
491 | }
492 |
493 | @Override public void onIceConnectionChange(
494 | PeerConnection.IceConnectionState newState) {
495 | }
496 |
497 | @Override public void onIceGatheringChange(
498 | PeerConnection.IceGatheringState newState) {
499 | }
500 |
501 | @Override public void onAddStream(final MediaStream stream){
502 | runOnUiThread(new Runnable() {
503 | public void run() {
504 | abortUnless(stream.audioTracks.size() <= 1 &&
505 | stream.videoTracks.size() <= 1,
506 | "Weird-looking stream: " + stream);
507 | if (stream.videoTracks.size() == 1) {
508 | stream.videoTracks.get(0).addRenderer(
509 | new VideoRenderer(remoteRender));
510 | }
511 | }
512 | });
513 | }
514 |
515 | @Override public void onRemoveStream(final MediaStream stream){
516 | runOnUiThread(new Runnable() {
517 | public void run() {
518 | stream.videoTracks.get(0).dispose();
519 | }
520 | });
521 | }
522 |
523 | @Override public void onDataChannel(final DataChannel dc) {
524 | runOnUiThread(new Runnable() {
525 | public void run() {
526 | throw new RuntimeException(
527 | "AppRTC doesn't use data channels, but got: " + dc.label() +
528 | " anyway!");
529 | }
530 | });
531 | }
532 |
533 | @Override public void onRenegotiationNeeded() {
534 | // No need to do anything; AppRTC follows a pre-agreed-upon
535 | // signaling/negotiation protocol.
536 | }
537 | }
538 |
539 | // Implementation detail: handle offer creation/signaling and answer setting,
540 | // as well as adding remote ICE candidates once the answer SDP is set.
541 | private class SDPObserver implements SdpObserver {
542 | private SessionDescription localSdp;
543 |
544 | @Override public void onCreateSuccess(final SessionDescription origSdp) {
545 | abortUnless(localSdp == null, "multiple SDP create?!?");
546 | final SessionDescription sdp = new SessionDescription(
547 | origSdp.type, preferISAC(origSdp.description));
548 | localSdp = sdp;
549 | runOnUiThread(new Runnable() {
550 | public void run() {
551 | pc.setLocalDescription(sdpObserver, sdp);
552 | }
553 | });
554 | }
555 |
556 | // Helper for sending local SDP (offer or answer, depending on role) to the
557 | // other participant. Note that it is important to send the output of
558 | // create{Offer,Answer} and not merely the current value of
559 | // getLocalDescription() because the latter may include ICE candidates that
560 | // we might want to filter elsewhere.
561 | private void sendLocalDescription() {
562 | logAndToast("Sending " + localSdp.type);
563 | JSONObject json = new JSONObject();
564 | jsonPut(json, "type", localSdp.type.canonicalForm());
565 | jsonPut(json, "sdp", localSdp.description);
566 | sendMessage(json);
567 | }
568 |
569 | @Override public void onSetSuccess() {
570 | runOnUiThread(new Runnable() {
571 | public void run() {
572 | if (appRtcClient.isInitiator()) {
573 | if (pc.getRemoteDescription() != null) {
574 | // We've set our local offer and received & set the remote
575 | // answer, so drain candidates.
576 | drainRemoteCandidates();
577 | } else {
578 | // We've just set our local description so time to send it.
579 | sendLocalDescription();
580 | }
581 | } else {
582 | if (pc.getLocalDescription() == null) {
583 | // We just set the remote offer, time to create our answer.
584 | logAndToast("Creating answer");
585 | pc.createAnswer(SDPObserver.this, sdpMediaConstraints);
586 | } else {
587 | // Answer now set as local description; send it and drain
588 | // candidates.
589 | sendLocalDescription();
590 | drainRemoteCandidates();
591 | }
592 | }
593 | }
594 | });
595 | }
596 |
597 | @Override public void onCreateFailure(final String error) {
598 | runOnUiThread(new Runnable() {
599 | public void run() {
600 | throw new RuntimeException("createSDP error: " + error);
601 | }
602 | });
603 | }
604 |
605 | @Override public void onSetFailure(final String error) {
606 | runOnUiThread(new Runnable() {
607 | public void run() {
608 | throw new RuntimeException("setSDP error: " + error);
609 | }
610 | });
611 | }
612 |
613 | private void drainRemoteCandidates() {
614 | for (IceCandidate candidate : queuedRemoteCandidates) {
615 | pc.addIceCandidate(candidate);
616 | }
617 | queuedRemoteCandidates = null;
618 | }
619 | }
620 |
621 | // Implementation detail: handler for receiving GAE messages and dispatching
622 | // them appropriately.
623 | private class GAEHandler implements GAEChannelClient.MessageHandler {
624 | @JavascriptInterface public void onOpen() {
625 | if (!appRtcClient.isInitiator()) {
626 | return;
627 | }
628 | logAndToast("Creating offer...");
629 | pc.createOffer(sdpObserver, sdpMediaConstraints);
630 | }
631 |
632 | @JavascriptInterface public void onMessage(String data) {
633 | try {
634 | JSONObject json = new JSONObject(data);
635 | String type = (String) json.get("type");
636 | if (type.equals("candidate")) {
637 | IceCandidate candidate = new IceCandidate(
638 | (String) json.get("id"),
639 | json.getInt("label"),
640 | (String) json.get("candidate"));
641 | if (queuedRemoteCandidates != null) {
642 | queuedRemoteCandidates.add(candidate);
643 | } else {
644 | pc.addIceCandidate(candidate);
645 | }
646 | } else if (type.equals("answer") || type.equals("offer")) {
647 | SessionDescription sdp = new SessionDescription(
648 | SessionDescription.Type.fromCanonicalForm(type),
649 | preferISAC((String) json.get("sdp")));
650 | pc.setRemoteDescription(sdpObserver, sdp);
651 | } else if (type.equals("bye")) {
652 | logAndToast("Remote end hung up; dropping PeerConnection");
653 | disconnectAndExit();
654 | } else {
655 | throw new RuntimeException("Unexpected message: " + data);
656 | }
657 | } catch (JSONException e) {
658 | throw new RuntimeException(e);
659 | }
660 | }
661 |
662 | @JavascriptInterface public void onClose() {
663 | disconnectAndExit();
664 | }
665 |
666 | @JavascriptInterface public void onError(int code, String description) {
667 | disconnectAndExit();
668 | }
669 | }
670 |
671 | // Disconnect from remote resources, dispose of local resources, and exit.
672 | private void disconnectAndExit() {
673 | synchronized (quit[0]) {
674 | if (quit[0]) {
675 | return;
676 | }
677 | quit[0] = true;
678 | if (pc != null) {
679 | pc.dispose();
680 | pc = null;
681 | }
682 | if (appRtcClient != null) {
683 | appRtcClient.sendMessage("{\"type\": \"bye\"}");
684 | appRtcClient.disconnect();
685 | appRtcClient = null;
686 | }
687 | if (videoSource != null) {
688 | videoSource.dispose();
689 | videoSource = null;
690 | }
691 | if (factory != null) {
692 | factory.dispose();
693 | factory = null;
694 | }
695 | finish();
696 | }
697 | }
698 |
699 | }
700 |
--------------------------------------------------------------------------------