├── .idea ├── .name ├── copyright │ └── profiles_settings.xml ├── scopes │ └── scope_settings.xml ├── encodings.xml ├── vcs.xml ├── libraries │ └── libjingle_peerconnection.xml ├── modules.xml ├── gradle.xml ├── compiler.xml └── misc.xml ├── settings.gradle ├── .gitignore ├── app ├── src │ └── main │ │ ├── jni │ │ └── Android.mk │ │ ├── res │ │ ├── drawable-hdpi │ │ │ └── ic_launcher.png │ │ ├── drawable-ldpi │ │ │ └── ic_launcher.png │ │ ├── drawable-mdpi │ │ │ └── ic_launcher.png │ │ ├── drawable-xhdpi │ │ │ └── ic_launcher.png │ │ └── values │ │ │ └── strings.xml │ │ ├── jniLibs │ │ └── armeabi-v7a │ │ │ └── libjingle_peerconnection_so.so │ │ ├── assets │ │ └── channel.html │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── org │ │ └── appspot │ │ └── apprtc │ │ ├── AppRTCGLView.java │ │ ├── UnhandledExceptionHandler.java │ │ ├── GAEChannelClient.java │ │ ├── AppRTCClient.java │ │ └── AppRTCDemoActivity.java ├── libs │ └── libjingle_peerconnection.jar ├── build.gradle └── app.iml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── README.md ├── WebRTCDemo.iml ├── import-summary.txt ├── LICENSE ├── gradlew.bat └── gradlew /.idea/.name: -------------------------------------------------------------------------------- 1 | webrtc-demo -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea/workspace.xml 4 | .DS_Store 5 | /build 6 | /app/build 7 | -------------------------------------------------------------------------------- /app/src/main/jni/Android.mk: -------------------------------------------------------------------------------- 1 | # This space intentionally left blank (required for Android build system). 2 | 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaku/WebRTCDemo/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/libs/libjingle_peerconnection.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaku/WebRTCDemo/HEAD/app/libs/libjingle_peerconnection.jar -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaku/WebRTCDemo/HEAD/app/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-ldpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaku/WebRTCDemo/HEAD/app/src/main/res/drawable-ldpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaku/WebRTCDemo/HEAD/app/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaku/WebRTCDemo/HEAD/app/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | AppRTC 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/jniLibs/armeabi-v7a/libjingle_peerconnection_so.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaku/WebRTCDemo/HEAD/app/src/main/jniLibs/armeabi-v7a/libjingle_peerconnection_so.so -------------------------------------------------------------------------------- /.idea/scopes/scope_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Apr 10 15:27:10 PDT 2013 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=http\://services.gradle.org/distributions/gradle-1.12-all.zip 7 | -------------------------------------------------------------------------------- /.idea/libraries/libjingle_peerconnection.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 19 5 | buildToolsVersion "19.1.0" 6 | 7 | defaultConfig { 8 | applicationId "org.appspot.apprtc" 9 | minSdkVersion 13 10 | targetSdkVersion 17 11 | } 12 | 13 | buildTypes { 14 | release { 15 | runProguard false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | compile files('libs/libjingle_peerconnection.jar') 23 | } 24 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 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 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/assets/channel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Abstraction issues 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | Android API 17 Platform 30 | 31 | 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 | 8 | 9 | 10 | 11 | 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 | --------------------------------------------------------------------------------