├── app ├── .gitignore ├── libs │ └── autobanh.jar ├── src │ ├── main │ │ ├── res │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── drawable-hdpi │ │ │ │ ├── disconnect.png │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_loopback_call.png │ │ │ │ ├── ic_action_full_screen.png │ │ │ │ └── ic_action_return_from_full_screen.png │ │ │ ├── drawable-ldpi │ │ │ │ ├── disconnect.png │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_loopback_call.png │ │ │ │ ├── ic_action_full_screen.png │ │ │ │ └── ic_action_return_from_full_screen.png │ │ │ ├── drawable-mdpi │ │ │ │ ├── disconnect.png │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_loopback_call.png │ │ │ │ ├── ic_action_full_screen.png │ │ │ │ └── ic_action_return_from_full_screen.png │ │ │ ├── drawable-xhdpi │ │ │ │ ├── disconnect.png │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_loopback_call.png │ │ │ │ ├── ic_action_full_screen.png │ │ │ │ └── ic_action_return_from_full_screen.png │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── values │ │ │ │ ├── colors.xml │ │ │ │ ├── styles.xml │ │ │ │ ├── arrays.xml │ │ │ │ └── strings.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── values-v17 │ │ │ │ └── styles.xml │ │ │ ├── values-v21 │ │ │ │ └── styles.xml │ │ │ ├── menu │ │ │ │ └── connect_menu.xml │ │ │ ├── layout │ │ │ │ ├── activity_call.xml │ │ │ │ ├── activity_connect.xml │ │ │ │ ├── fragment_hud.xml │ │ │ │ └── fragment_call.xml │ │ │ ├── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── drawable │ │ │ │ └── ic_launcher_background.xml │ │ │ └── xml │ │ │ │ └── preferences.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── someshk │ │ │ │ └── apprtc │ │ │ │ ├── SettingsFragment.java │ │ │ │ ├── util │ │ │ │ ├── AppRTCUtils.java │ │ │ │ └── AsyncHttpURLConnection.java │ │ │ │ ├── RtcEventLog.java │ │ │ │ ├── UnhandledExceptionHandler.java │ │ │ │ ├── CaptureQualityController.java │ │ │ │ ├── AppRTCClient.java │ │ │ │ ├── CallFragment.java │ │ │ │ ├── RecordedAudioToFileController.java │ │ │ │ ├── AppRTCProximitySensor.java │ │ │ │ ├── HudFragment.java │ │ │ │ ├── RoomParametersFetcher.java │ │ │ │ ├── WebSocketChannelClient.java │ │ │ │ ├── TCPChannelClient.java │ │ │ │ ├── DirectRTCClient.java │ │ │ │ ├── SettingsActivity.java │ │ │ │ └── WebSocketRTCClient.java │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── someshk │ │ │ └── apprtc │ │ │ └── ExampleUnitTest.java │ └── androidTest │ │ └── java │ │ └── com │ │ └── someshk │ │ └── apprtc │ │ └── ExampleInstrumentedTest.java ├── proguard-rules.pro └── build.gradle ├── settings.gradle ├── OWNERS ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── .idea ├── encodings.xml ├── modules.xml ├── runConfigurations.xml ├── gradle.xml ├── codeStyles │ └── Project.xml └── misc.xml ├── README ├── gradle.properties ├── gradlew.bat └── gradlew /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | magjed@webrtc.org 2 | sakal@webrtc.org 3 | 4 | per-file *.py=phoglund@webrtc.org 5 | -------------------------------------------------------------------------------- /app/libs/autobanh.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/libs/autobanh.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/disconnect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/drawable-hdpi/disconnect.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-ldpi/disconnect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/drawable-ldpi/disconnect.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-ldpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/drawable-ldpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/disconnect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/drawable-mdpi/disconnect.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/disconnect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/drawable-xhdpi/disconnect.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_loopback_call.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/drawable-hdpi/ic_loopback_call.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-ldpi/ic_loopback_call.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/drawable-ldpi/ic_loopback_call.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_loopback_call.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/drawable-mdpi/ic_loopback_call.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_loopback_call.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/drawable-xhdpi/ic_loopback_call.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_action_full_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/drawable-hdpi/ic_action_full_screen.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-ldpi/ic_action_full_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/drawable-ldpi/ic_action_full_screen.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_action_full_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/drawable-mdpi/ic_action_full_screen.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_full_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/drawable-xhdpi/ic_action_full_screen.png -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_action_return_from_full_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/drawable-hdpi/ic_action_return_from_full_screen.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-ldpi/ic_action_return_from_full_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/drawable-ldpi/ic_action_return_from_full_screen.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_action_return_from_full_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/drawable-mdpi/ic_action_return_from_full_screen.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_return_from_full_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/drawable-xhdpi/ic_action_return_from_full_screen.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | This directory contains an example Android client for https://appr.tc 2 | 3 | Original Code is taken from http://chromium.googlesource.com project and is property of their respective owners. I just made the changes to some of the file so it can use new Gradle build system. 4 | 5 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Oct 06 21:53:44 CST 2025 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values-v17/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/test/java/com/someshk/apprtc/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.someshk.apprtc; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() throws Exception { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/res/menu/connect_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 13 | 14 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | android.enableJetifier=true 13 | android.useAndroidX=true 14 | org.gradle.jvmargs=-Xmx1536m 15 | 16 | # When configured, Gradle will run in incubating parallel mode. 17 | # This option should only be used with decoupled projects. More details, visit 18 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 19 | # org.gradle.parallel=true 20 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/someshk/apprtc/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.someshk.apprtc; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.someshk.apprtc", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/someshk/apprtc/SettingsFragment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 The WebRTC Project Authors. All rights reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. An additional intellectual property rights grant can be found 7 | * in the file PATENTS. All contributing project authors may 8 | * be found in the AUTHORS file in the root of the source tree. 9 | */ 10 | 11 | package com.someshk.apprtc; 12 | 13 | import android.os.Bundle; 14 | import android.preference.PreferenceFragment; 15 | 16 | /** 17 | * Settings fragment for AppRTC. 18 | */ 19 | public class SettingsFragment extends PreferenceFragment { 20 | @Override 21 | public void onCreate(Bundle savedInstanceState) { 22 | super.onCreate(savedInstanceState); 23 | // Load the preferences from an XML resource 24 | addPreferencesFromResource(R.xml.preferences); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_call.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 12 | 17 | 18 | 24 | 25 | 29 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | signingConfigs { 5 | release { 6 | } 7 | } 8 | compileSdkVersion 33 9 | defaultConfig { 10 | applicationId "com.someshk.apprtc" 11 | minSdkVersion 21 12 | targetSdkVersion 33 13 | versionCode 1 14 | versionName "1.0" 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | } 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | compileOptions { 24 | sourceCompatibility JavaVersion.VERSION_17 25 | targetCompatibility JavaVersion.VERSION_17 26 | } 27 | ndkVersion '21.4.7075529' 28 | } 29 | 30 | dependencies { 31 | implementation files('libs/libwebrtc.aar') 32 | implementation files('libs/autobanh.jar') 33 | implementation "androidx.preference:preference-ktx:1.2.0" // 请使用最新版本 34 | implementation 'androidx.appcompat:appcompat:1.1.0' 35 | // implementation 'org.webrtc:google-webrtc:1.0.+' 36 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 37 | testImplementation 'junit:junit:4.13' 38 | androidTestImplementation 'androidx.test.ext:junit:1.1.1' 39 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 40 | implementation files('libs/autobanh.jar') 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/res/values/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Default 5 | 4K (3840 x 2160) 6 | Full HD (1920 x 1080) 7 | HD (1280 x 720) 8 | VGA (640 x 480) 9 | QVGA (320 x 240) 10 | 11 | 12 | 13 | Default 14 | 3840 x 2160 15 | 1920 x 1080 16 | 1280 x 720 17 | 640 x 480 18 | 320 x 240 19 | 20 | 21 | 22 | Default 23 | 30 fps 24 | 15 fps 25 | 26 | 27 | 28 | Default 29 | Manual 30 | 31 | 32 | 33 | VP8 34 | VP9 35 | H264 Baseline 36 | H264 High 37 | 38 | 39 | 40 | OPUS 41 | ISAC 42 | 43 | 44 | 45 | Auto (proximity sensor) 46 | Enabled 47 | Disabled 48 | 49 | 50 | 51 | auto 52 | true 53 | false 54 | 55 | 56 | 57 | Remove favorite 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/someshk/apprtc/util/AppRTCUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 The WebRTC Project Authors. All rights reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. An additional intellectual property rights grant can be found 7 | * in the file PATENTS. All contributing project authors may 8 | * be found in the AUTHORS file in the root of the source tree. 9 | */ 10 | 11 | package com.someshk.apprtc.util; 12 | 13 | import android.os.Build; 14 | import android.util.Log; 15 | 16 | /** 17 | * AppRTCUtils provides helper functions for managing thread safety. 18 | */ 19 | public final class AppRTCUtils { 20 | private AppRTCUtils() { 21 | } 22 | 23 | /** 24 | * Helper method which throws an exception when an assertion has failed. 25 | */ 26 | public static void assertIsTrue(boolean condition) { 27 | if (!condition) { 28 | throw new AssertionError("Expected condition to be true"); 29 | } 30 | } 31 | 32 | /** 33 | * Helper method for building a string of thread information. 34 | */ 35 | public static String getThreadInfo() { 36 | return "@[name=" + Thread.currentThread().getName() + ", id=" + Thread.currentThread().getId() 37 | + "]"; 38 | } 39 | 40 | /** 41 | * Information about the current build, taken from system properties. 42 | */ 43 | public static void logDeviceInfo(String tag) { 44 | Log.d(tag, "Android SDK: " + Build.VERSION.SDK_INT + ", " 45 | + "Release: " + Build.VERSION.RELEASE + ", " 46 | + "Brand: " + Build.BRAND + ", " 47 | + "Device: " + Build.DEVICE + ", " 48 | + "Id: " + Build.ID + ", " 49 | + "Hardware: " + Build.HARDWARE + ", " 50 | + "Manufacturer: " + Build.MANUFACTURER + ", " 51 | + "Model: " + Build.MODEL + ", " 52 | + "Product: " + Build.PRODUCT); 53 | } 54 | } -------------------------------------------------------------------------------- /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/com/someshk/apprtc/RtcEventLog.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. An additional intellectual property rights grant can be found 7 | * in the file PATENTS. All contributing project authors may 8 | * be found in the AUTHORS file in the root of the source tree. 9 | */ 10 | 11 | package com.someshk.apprtc; 12 | 13 | import android.os.ParcelFileDescriptor; 14 | import android.util.Log; 15 | 16 | import org.webrtc.PeerConnection; 17 | 18 | import java.io.File; 19 | import java.io.IOException; 20 | 21 | public class RtcEventLog { 22 | private static final String TAG = "RtcEventLog"; 23 | private static final int OUTPUT_FILE_MAX_BYTES = 10_000_000; 24 | private final PeerConnection peerConnection; 25 | private RtcEventLogState state = RtcEventLogState.INACTIVE; 26 | 27 | enum RtcEventLogState { 28 | INACTIVE, 29 | STARTED, 30 | STOPPED, 31 | } 32 | 33 | public RtcEventLog(PeerConnection peerConnection) { 34 | if (peerConnection == null) { 35 | throw new NullPointerException("The peer connection is null."); 36 | } 37 | this.peerConnection = peerConnection; 38 | } 39 | 40 | public void start(final File outputFile) { 41 | if (state == RtcEventLogState.STARTED) { 42 | Log.e(TAG, "RtcEventLog has already started."); 43 | return; 44 | } 45 | final ParcelFileDescriptor fileDescriptor; 46 | try { 47 | fileDescriptor = ParcelFileDescriptor.open(outputFile, 48 | ParcelFileDescriptor.MODE_READ_WRITE | ParcelFileDescriptor.MODE_CREATE 49 | | ParcelFileDescriptor.MODE_TRUNCATE); 50 | } catch (IOException e) { 51 | Log.e(TAG, "Failed to create a new file", e); 52 | return; 53 | } 54 | // Passes ownership of the file to WebRTC. 55 | boolean success = 56 | peerConnection.startRtcEventLog(fileDescriptor.detachFd(), OUTPUT_FILE_MAX_BYTES); 57 | if (!success) { 58 | Log.e(TAG, "Failed to start RTC event log."); 59 | return; 60 | } 61 | state = RtcEventLogState.STARTED; 62 | Log.d(TAG, "RtcEventLog started."); 63 | } 64 | 65 | public void stop() { 66 | if (state != RtcEventLogState.STARTED) { 67 | Log.e(TAG, "RtcEventLog was not started."); 68 | return; 69 | } 70 | peerConnection.stopRtcEventLog(); 71 | state = RtcEventLogState.STOPPED; 72 | Log.d(TAG, "RtcEventLog stopped."); 73 | } 74 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_connect.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 16 | 17 | 23 | 24 | 32 | 33 | 39 | 40 | 46 | 47 | 48 | 58 | 59 | 63 | 64 | 69 | 70 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_hud.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 16 | 17 | 26 | 27 | 31 | 32 | 33 | 41 | 42 | 50 | 51 | 52 | 53 | 54 | 62 | 63 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 22 | 23 | 25 | 26 | 33 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 58 | 59 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_call.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 16 | 17 | 26 | 27 | 34 | 35 | 42 | 43 | 49 | 50 | 57 | 58 | 59 | 67 | 68 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /app/src/main/java/com/someshk/apprtc/UnhandledExceptionHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 The WebRTC Project Authors. All rights reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. An additional intellectual property rights grant can be found 7 | * in the file PATENTS. All contributing project authors may 8 | * be found in the AUTHORS file in the root of the source tree. 9 | */ 10 | 11 | package com.someshk.apprtc; 12 | 13 | import android.app.Activity; 14 | import android.app.AlertDialog; 15 | import android.content.DialogInterface; 16 | import android.util.Log; 17 | import android.util.TypedValue; 18 | import android.widget.ScrollView; 19 | import android.widget.TextView; 20 | 21 | import java.io.PrintWriter; 22 | import java.io.StringWriter; 23 | 24 | /** 25 | * Singleton helper: install a default unhandled exception handler which shows 26 | * an informative dialog and kills the app. Useful for apps whose 27 | * error-handling consists of throwing RuntimeExceptions. 28 | * NOTE: almost always more useful to 29 | * Thread.setDefaultUncaughtExceptionHandler() rather than 30 | * Thread.setUncaughtExceptionHandler(), to apply to background threads as well. 31 | */ 32 | public class UnhandledExceptionHandler implements Thread.UncaughtExceptionHandler { 33 | private static final String TAG = "AppRTCMobileActivity"; 34 | private final Activity activity; 35 | 36 | public UnhandledExceptionHandler(final Activity activity) { 37 | this.activity = activity; 38 | } 39 | 40 | @Override 41 | public void uncaughtException(Thread unusedThread, final Throwable e) { 42 | activity.runOnUiThread(new Runnable() { 43 | @Override 44 | public void run() { 45 | String title = "Fatal error: " + getTopLevelCauseMessage(e); 46 | String msg = getRecursiveStackTrace(e); 47 | TextView errorView = new TextView(activity); 48 | errorView.setText(msg); 49 | errorView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 8); 50 | ScrollView scrollingContainer = new ScrollView(activity); 51 | scrollingContainer.addView(errorView); 52 | Log.e(TAG, title + "\n\n" + msg); 53 | DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { 54 | @Override 55 | public void onClick(DialogInterface dialog, int which) { 56 | dialog.dismiss(); 57 | System.exit(1); 58 | } 59 | }; 60 | AlertDialog.Builder builder = new AlertDialog.Builder(activity); 61 | builder.setTitle(title) 62 | .setView(scrollingContainer) 63 | .setPositiveButton("Exit", listener) 64 | .show(); 65 | } 66 | }); 67 | } 68 | 69 | // Returns the Message attached to the original Cause of |t|. 70 | private static String getTopLevelCauseMessage(Throwable t) { 71 | Throwable topLevelCause = t; 72 | while (topLevelCause.getCause() != null) { 73 | topLevelCause = topLevelCause.getCause(); 74 | } 75 | return topLevelCause.getMessage(); 76 | } 77 | 78 | // Returns a human-readable String of the stacktrace in |t|, recursively 79 | // through all Causes that led to |t|. 80 | private static String getRecursiveStackTrace(Throwable t) { 81 | StringWriter writer = new StringWriter(); 82 | t.printStackTrace(new PrintWriter(writer)); 83 | return writer.toString(); 84 | } 85 | } -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | xmlns:android 14 | 15 | ^$ 16 | 17 | 18 | 19 |
20 |
21 | 22 | 23 | 24 | xmlns:.* 25 | 26 | ^$ 27 | 28 | 29 | BY_NAME 30 | 31 |
32 |
33 | 34 | 35 | 36 | .*:id 37 | 38 | http://schemas.android.com/apk/res/android 39 | 40 | 41 | 42 |
43 |
44 | 45 | 46 | 47 | .*:name 48 | 49 | http://schemas.android.com/apk/res/android 50 | 51 | 52 | 53 |
54 |
55 | 56 | 57 | 58 | name 59 | 60 | ^$ 61 | 62 | 63 | 64 |
65 |
66 | 67 | 68 | 69 | style 70 | 71 | ^$ 72 | 73 | 74 | 75 |
76 |
77 | 78 | 79 | 80 | .* 81 | 82 | ^$ 83 | 84 | 85 | BY_NAME 86 | 87 |
88 |
89 | 90 | 91 | 92 | .* 93 | 94 | http://schemas.android.com/apk/res/android 95 | 96 | 97 | ANDROID_ATTRIBUTE_ORDER 98 | 99 |
100 |
101 | 102 | 103 | 104 | .* 105 | 106 | .* 107 | 108 | 109 | BY_NAME 110 | 111 |
112 |
113 |
114 |
115 |
116 |
-------------------------------------------------------------------------------- /app/src/main/java/com/someshk/apprtc/util/AsyncHttpURLConnection.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 The WebRTC Project Authors. All rights reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. An additional intellectual property rights grant can be found 7 | * in the file PATENTS. All contributing project authors may 8 | * be found in the AUTHORS file in the root of the source tree. 9 | */ 10 | 11 | package com.someshk.apprtc.util; 12 | 13 | import java.io.IOException; 14 | import java.io.InputStream; 15 | import java.io.OutputStream; 16 | import java.net.HttpURLConnection; 17 | import java.net.SocketTimeoutException; 18 | import java.net.URL; 19 | import java.util.Scanner; 20 | 21 | /** 22 | * Asynchronous http requests implementation. 23 | */ 24 | public class AsyncHttpURLConnection { 25 | private static final int HTTP_TIMEOUT_MS = 8000; 26 | private static final String HTTP_ORIGIN = "https://appr.tc"; 27 | private final String method; 28 | private final String url; 29 | private final String message; 30 | private final AsyncHttpEvents events; 31 | private String contentType; 32 | 33 | /** 34 | * Http requests callbacks. 35 | */ 36 | public interface AsyncHttpEvents { 37 | void onHttpError(String errorMessage); 38 | 39 | void onHttpComplete(String response); 40 | } 41 | 42 | public AsyncHttpURLConnection(String method, String url, String message, AsyncHttpEvents events) { 43 | this.method = method; 44 | this.url = url; 45 | this.message = message; 46 | this.events = events; 47 | } 48 | 49 | public void setContentType(String contentType) { 50 | this.contentType = contentType; 51 | } 52 | 53 | public void send() { 54 | new Thread(this::sendHttpMessage).start(); 55 | } 56 | 57 | private void sendHttpMessage() { 58 | try { 59 | HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); 60 | byte[] postData = new byte[0]; 61 | if (message != null) { 62 | postData = message.getBytes("UTF-8"); 63 | } 64 | connection.setRequestMethod(method); 65 | connection.setUseCaches(false); 66 | connection.setDoInput(true); 67 | connection.setConnectTimeout(HTTP_TIMEOUT_MS); 68 | connection.setReadTimeout(HTTP_TIMEOUT_MS); 69 | // TODO(glaznev) - query request origin from pref_room_server_url_key preferences. 70 | connection.addRequestProperty("origin", HTTP_ORIGIN); 71 | boolean doOutput = false; 72 | if (method.equals("POST")) { 73 | doOutput = true; 74 | connection.setDoOutput(true); 75 | connection.setFixedLengthStreamingMode(postData.length); 76 | } 77 | if (contentType == null) { 78 | connection.setRequestProperty("Content-Type", "text/plain; charset=utf-8"); 79 | } else { 80 | connection.setRequestProperty("Content-Type", contentType); 81 | } 82 | // Send POST request. 83 | if (doOutput && postData.length > 0) { 84 | OutputStream outStream = connection.getOutputStream(); 85 | outStream.write(postData); 86 | outStream.close(); 87 | } 88 | // Get response. 89 | int responseCode = connection.getResponseCode(); 90 | if (responseCode != 200) { 91 | events.onHttpError("Non-200 response to " + method + " to URL: " + url + " : " 92 | + connection.getHeaderField(null)); 93 | connection.disconnect(); 94 | return; 95 | } 96 | InputStream responseStream = connection.getInputStream(); 97 | String response = drainStream(responseStream); 98 | responseStream.close(); 99 | connection.disconnect(); 100 | events.onHttpComplete(response); 101 | } catch (SocketTimeoutException e) { 102 | events.onHttpError("HTTP " + method + " to " + url + " timeout"); 103 | } catch (IOException e) { 104 | events.onHttpError("HTTP " + method + " to " + url + " error: " + e.getMessage()); 105 | } 106 | } 107 | 108 | // Return the contents of an InputStream as a String. 109 | private static String drainStream(InputStream in) { 110 | Scanner s = new Scanner(in, "UTF-8").useDelimiter("\\A"); 111 | return s.hasNext() ? s.next() : ""; 112 | } 113 | } -------------------------------------------------------------------------------- /app/src/main/java/com/someshk/apprtc/CaptureQualityController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 The WebRTC Project Authors. All rights reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. An additional intellectual property rights grant can be found 7 | * in the file PATENTS. All contributing project authors may 8 | * be found in the AUTHORS file in the root of the source tree. 9 | */ 10 | 11 | package com.someshk.apprtc; 12 | 13 | import android.widget.SeekBar; 14 | import android.widget.TextView; 15 | 16 | import org.webrtc.CameraEnumerationAndroid.CaptureFormat; 17 | 18 | import java.util.Arrays; 19 | import java.util.Collections; 20 | import java.util.Comparator; 21 | import java.util.List; 22 | 23 | /** 24 | * Control capture format based on a seekbar listener. 25 | */ 26 | public class CaptureQualityController implements SeekBar.OnSeekBarChangeListener { 27 | private final List formats = 28 | Arrays.asList(new CaptureFormat(1280, 720, 0, 30000), new CaptureFormat(960, 540, 0, 30000), 29 | new CaptureFormat(640, 480, 0, 30000), new CaptureFormat(480, 360, 0, 30000), 30 | new CaptureFormat(320, 240, 0, 30000), new CaptureFormat(256, 144, 0, 30000)); 31 | // Prioritize framerate below this threshold and resolution above the threshold. 32 | private static final int FRAMERATE_THRESHOLD = 15; 33 | private TextView captureFormatText; 34 | public CallFragment.OnCallEvents callEvents; 35 | private int width; 36 | private int height; 37 | private int framerate; 38 | private double targetBandwidth; 39 | 40 | public CaptureQualityController( 41 | TextView captureFormatText, CallFragment.OnCallEvents callEvents) { 42 | this.captureFormatText = captureFormatText; 43 | this.callEvents = callEvents; 44 | } 45 | 46 | private final Comparator compareFormats = new Comparator() { 47 | @Override 48 | public int compare(CaptureFormat first, CaptureFormat second) { 49 | int firstFps = calculateFramerate(targetBandwidth, first); 50 | int secondFps = calculateFramerate(targetBandwidth, second); 51 | if ((firstFps >= FRAMERATE_THRESHOLD && secondFps >= FRAMERATE_THRESHOLD) 52 | || firstFps == secondFps) { 53 | // Compare resolution. 54 | return first.width * first.height - second.width * second.height; 55 | } else { 56 | // Compare fps. 57 | return firstFps - secondFps; 58 | } 59 | } 60 | }; 61 | 62 | @Override 63 | public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 64 | if (progress == 0) { 65 | width = 0; 66 | height = 0; 67 | framerate = 0; 68 | captureFormatText.setText(R.string.muted); 69 | return; 70 | } 71 | // Extract max bandwidth (in millipixels / second). 72 | long maxCaptureBandwidth = java.lang.Long.MIN_VALUE; 73 | for (CaptureFormat format : formats) { 74 | maxCaptureBandwidth = 75 | Math.max(maxCaptureBandwidth, (long) format.width * format.height * format.framerate.max); 76 | } 77 | // Fraction between 0 and 1. 78 | double bandwidthFraction = (double) progress / 100.0; 79 | // Make a log-scale transformation, still between 0 and 1. 80 | final double kExpConstant = 3.0; 81 | bandwidthFraction = 82 | (Math.exp(kExpConstant * bandwidthFraction) - 1) / (Math.exp(kExpConstant) - 1); 83 | targetBandwidth = bandwidthFraction * maxCaptureBandwidth; 84 | // Choose the best format given a target bandwidth. 85 | final CaptureFormat bestFormat = Collections.max(formats, compareFormats); 86 | width = bestFormat.width; 87 | height = bestFormat.height; 88 | framerate = calculateFramerate(targetBandwidth, bestFormat); 89 | captureFormatText.setText( 90 | String.format(captureFormatText.getContext().getString(R.string.format_description), width, 91 | height, framerate)); 92 | } 93 | 94 | @Override 95 | public void onStartTrackingTouch(SeekBar seekBar) { 96 | } 97 | 98 | @Override 99 | public void onStopTrackingTouch(SeekBar seekBar) { 100 | callEvents.onCaptureFormatChange(width, height, framerate); 101 | } 102 | 103 | // Return the highest frame rate possible based on bandwidth and format. 104 | private int calculateFramerate(double bandwidth, CaptureFormat format) { 105 | return (int) Math.round( 106 | Math.min(format.framerate.max, (int) Math.round(bandwidth / (format.width * format.height))) 107 | / 1000.0); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /app/src/main/java/com/someshk/apprtc/AppRTCClient.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 The WebRTC Project Authors. All rights reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. An additional intellectual property rights grant can be found 7 | * in the file PATENTS. All contributing project authors may 8 | * be found in the AUTHORS file in the root of the source tree. 9 | */ 10 | 11 | package com.someshk.apprtc; 12 | 13 | 14 | import org.webrtc.IceCandidate; 15 | import org.webrtc.PeerConnection; 16 | import org.webrtc.SessionDescription; 17 | 18 | import java.util.List; 19 | 20 | /** 21 | * AppRTCClient is the interface representing an AppRTC client. 22 | */ 23 | public interface AppRTCClient { 24 | /** 25 | * Struct holding the connection parameters of an AppRTC room. 26 | */ 27 | class RoomConnectionParameters { 28 | public final String roomUrl; 29 | public final String roomId; 30 | public final boolean loopback; 31 | public final String urlParameters; 32 | 33 | public RoomConnectionParameters( 34 | String roomUrl, String roomId, boolean loopback, String urlParameters) { 35 | this.roomUrl = roomUrl; 36 | this.roomId = roomId; 37 | this.loopback = loopback; 38 | this.urlParameters = urlParameters; 39 | } 40 | 41 | public RoomConnectionParameters(String roomUrl, String roomId, boolean loopback) { 42 | this(roomUrl, roomId, loopback, null /* urlParameters */); 43 | } 44 | } 45 | 46 | /** 47 | * Asynchronously connect to an AppRTC room URL using supplied connection 48 | * parameters. Once connection is established onConnectedToRoom() 49 | * callback with room parameters is invoked. 50 | */ 51 | void connectToRoom(RoomConnectionParameters connectionParameters); 52 | 53 | /** 54 | * Send offer SDP to the other participant. 55 | */ 56 | void sendOfferSdp(final SessionDescription sdp); 57 | 58 | /** 59 | * Send answer SDP to the other participant. 60 | */ 61 | void sendAnswerSdp(final SessionDescription sdp); 62 | 63 | /** 64 | * Send Ice candidate to the other participant. 65 | */ 66 | void sendLocalIceCandidate(final IceCandidate candidate); 67 | 68 | /** 69 | * Send removed ICE candidates to the other participant. 70 | */ 71 | void sendLocalIceCandidateRemovals(final IceCandidate[] candidates); 72 | 73 | /** 74 | * Disconnect from room. 75 | */ 76 | void disconnectFromRoom(); 77 | 78 | /** 79 | * Struct holding the signaling parameters of an AppRTC room. 80 | */ 81 | class SignalingParameters { 82 | public final List iceServers; 83 | public final boolean initiator; 84 | public final String clientId; 85 | public final String wssUrl; 86 | public final String wssPostUrl; 87 | public final SessionDescription offerSdp; 88 | public final List iceCandidates; 89 | 90 | public SignalingParameters(List iceServers, boolean initiator, 91 | String clientId, String wssUrl, String wssPostUrl, SessionDescription offerSdp, 92 | List iceCandidates) { 93 | this.iceServers = iceServers; 94 | this.initiator = initiator; 95 | this.clientId = clientId; 96 | this.wssUrl = wssUrl; 97 | this.wssPostUrl = wssPostUrl; 98 | this.offerSdp = offerSdp; 99 | this.iceCandidates = iceCandidates; 100 | } 101 | } 102 | 103 | /** 104 | * Callback interface for messages delivered on signaling channel. 105 | * 106 | *

Methods are guaranteed to be invoked on the UI thread of |activity|. 107 | */ 108 | interface SignalingEvents { 109 | /** 110 | * Callback fired once the room's signaling parameters 111 | * SignalingParameters are extracted. 112 | */ 113 | void onConnectedToRoom(final SignalingParameters params); 114 | 115 | /** 116 | * Callback fired once remote SDP is received. 117 | */ 118 | void onRemoteDescription(final SessionDescription sdp); 119 | 120 | /** 121 | * Callback fired once remote Ice candidate is received. 122 | */ 123 | void onRemoteIceCandidate(final IceCandidate candidate); 124 | 125 | /** 126 | * Callback fired once remote Ice candidate removals are received. 127 | */ 128 | void onRemoteIceCandidatesRemoved(final IceCandidate[] candidates); 129 | 130 | /** 131 | * Callback fired once channel is closed. 132 | */ 133 | void onChannelClose(); 134 | 135 | /** 136 | * Callback fired once channel error happened. 137 | */ 138 | void onChannelError(final String description); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /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 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /app/src/main/java/com/someshk/apprtc/CallFragment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 The WebRTC Project Authors. All rights reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. An additional intellectual property rights grant can be found 7 | * in the file PATENTS. All contributing project authors may 8 | * be found in the AUTHORS file in the root of the source tree. 9 | */ 10 | 11 | package com.someshk.apprtc; 12 | 13 | import android.app.Activity; 14 | import android.app.Fragment; 15 | import android.os.Bundle; 16 | import android.view.LayoutInflater; 17 | import android.view.View; 18 | import android.view.ViewGroup; 19 | import android.widget.ImageButton; 20 | import android.widget.SeekBar; 21 | import android.widget.TextView; 22 | 23 | import org.webrtc.RendererCommon.ScalingType; 24 | 25 | /** 26 | * Fragment for call control. 27 | */ 28 | public class CallFragment extends Fragment { 29 | private TextView contactView; 30 | private ImageButton cameraSwitchButton; 31 | private ImageButton videoScalingButton; 32 | private ImageButton toggleMuteButton; 33 | private TextView captureFormatText; 34 | private SeekBar captureFormatSlider; 35 | public OnCallEvents callEvents; 36 | private ScalingType scalingType; 37 | private boolean videoCallEnabled = true; 38 | 39 | /** 40 | * Call control interface for container activity. 41 | */ 42 | public interface OnCallEvents { 43 | void onCallHangUp(); 44 | 45 | void onCameraSwitch(); 46 | 47 | void onVideoScalingSwitch(ScalingType scalingType); 48 | 49 | void onCaptureFormatChange(int width, int height, int framerate); 50 | 51 | boolean onToggleMic(); 52 | } 53 | 54 | @Override 55 | public View onCreateView( 56 | LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 57 | View controlView = inflater.inflate(R.layout.fragment_call, container, false); 58 | // Create UI controls. 59 | contactView = controlView.findViewById(R.id.contact_name_call); 60 | ImageButton disconnectButton = controlView.findViewById(R.id.button_call_disconnect); 61 | cameraSwitchButton = controlView.findViewById(R.id.button_call_switch_camera); 62 | videoScalingButton = controlView.findViewById(R.id.button_call_scaling_mode); 63 | toggleMuteButton = controlView.findViewById(R.id.button_call_toggle_mic); 64 | captureFormatText = controlView.findViewById(R.id.capture_format_text_call); 65 | captureFormatSlider = controlView.findViewById(R.id.capture_format_slider_call); 66 | // Add buttons click events. 67 | disconnectButton.setOnClickListener(new View.OnClickListener() { 68 | @Override 69 | public void onClick(View view) { 70 | callEvents.onCallHangUp(); 71 | } 72 | }); 73 | cameraSwitchButton.setOnClickListener(new View.OnClickListener() { 74 | @Override 75 | public void onClick(View view) { 76 | callEvents.onCameraSwitch(); 77 | } 78 | }); 79 | videoScalingButton.setOnClickListener(new View.OnClickListener() { 80 | @Override 81 | public void onClick(View view) { 82 | if (scalingType == ScalingType.SCALE_ASPECT_FILL) { 83 | videoScalingButton.setBackgroundResource(R.drawable.ic_action_full_screen); 84 | scalingType = ScalingType.SCALE_ASPECT_FIT; 85 | } else { 86 | videoScalingButton.setBackgroundResource(R.drawable.ic_action_return_from_full_screen); 87 | scalingType = ScalingType.SCALE_ASPECT_FILL; 88 | } 89 | callEvents.onVideoScalingSwitch(scalingType); 90 | } 91 | }); 92 | scalingType = ScalingType.SCALE_ASPECT_FILL; 93 | toggleMuteButton.setOnClickListener(new View.OnClickListener() { 94 | @Override 95 | public void onClick(View view) { 96 | boolean enabled = callEvents.onToggleMic(); 97 | toggleMuteButton.setAlpha(enabled ? 1.0f : 0.3f); 98 | } 99 | }); 100 | return controlView; 101 | } 102 | 103 | @Override 104 | public void onStart() { 105 | super.onStart(); 106 | boolean captureSliderEnabled = false; 107 | Bundle args = getArguments(); 108 | if (args != null) { 109 | String contactName = args.getString(CallActivity.EXTRA_ROOMID); 110 | contactView.setText(contactName); 111 | videoCallEnabled = args.getBoolean(CallActivity.EXTRA_VIDEO_CALL, true); 112 | captureSliderEnabled = videoCallEnabled 113 | && args.getBoolean(CallActivity.EXTRA_VIDEO_CAPTUREQUALITYSLIDER_ENABLED, false); 114 | } 115 | if (!videoCallEnabled) { 116 | cameraSwitchButton.setVisibility(View.INVISIBLE); 117 | } 118 | if (captureSliderEnabled) { 119 | captureFormatSlider.setOnSeekBarChangeListener( 120 | new CaptureQualityController(captureFormatText, callEvents)); 121 | } else { 122 | captureFormatText.setVisibility(View.GONE); 123 | captureFormatSlider.setVisibility(View.GONE); 124 | } 125 | } 126 | 127 | // TODO(sakal): Replace with onAttach(Context) once we only support API level 23+. 128 | @SuppressWarnings("deprecation") 129 | @Override 130 | public void onAttach(Activity activity) { 131 | super.onAttach(activity); 132 | callEvents = (OnCallEvents) activity; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 36 | 37 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 85 | 95 | 96 | 97 | 98 | 99 | 100 | 102 | -------------------------------------------------------------------------------- /app/src/main/java/com/someshk/apprtc/RecordedAudioToFileController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. An additional intellectual property rights grant can be found 7 | * in the file PATENTS. All contributing project authors may 8 | * be found in the AUTHORS file in the root of the source tree. 9 | */ 10 | 11 | package com.someshk.apprtc; 12 | 13 | import android.media.AudioFormat; 14 | import android.os.Environment; 15 | import androidx.annotation.Nullable; 16 | import android.util.Log; 17 | 18 | import org.webrtc.audio.JavaAudioDeviceModule; 19 | 20 | import java.io.File; 21 | import java.io.FileNotFoundException; 22 | import java.io.FileOutputStream; 23 | import java.io.IOException; 24 | import java.io.OutputStream; 25 | import java.util.concurrent.ExecutorService; 26 | 27 | /** 28 | * Implements the WebRtcAudioRecordSamplesReadyCallback interface and writes 29 | * recorded raw audio samples to an output file. 30 | */ 31 | public class RecordedAudioToFileController implements JavaAudioDeviceModule.SamplesReadyCallback { 32 | private static final String TAG = "RecordedAudioToFile"; 33 | private static final long MAX_FILE_SIZE_IN_BYTES = 58348800L; 34 | private final Object lock = new Object(); 35 | private final ExecutorService executor; 36 | @Nullable 37 | private OutputStream rawAudioFileOutputStream; 38 | private boolean isRunning; 39 | private long fileSizeInBytes; 40 | 41 | public RecordedAudioToFileController(ExecutorService executor) { 42 | Log.d(TAG, "ctor"); 43 | this.executor = executor; 44 | } 45 | 46 | /** 47 | * Should be called on the same executor thread as the one provided at 48 | * construction. 49 | */ 50 | public boolean start() { 51 | Log.d(TAG, "start"); 52 | if (!isExternalStorageWritable()) { 53 | Log.e(TAG, "Writing to external media is not possible"); 54 | return false; 55 | } 56 | synchronized (lock) { 57 | isRunning = true; 58 | } 59 | return true; 60 | } 61 | 62 | /** 63 | * Should be called on the same executor thread as the one provided at 64 | * construction. 65 | */ 66 | public void stop() { 67 | Log.d(TAG, "stop"); 68 | synchronized (lock) { 69 | isRunning = false; 70 | if (rawAudioFileOutputStream != null) { 71 | try { 72 | rawAudioFileOutputStream.close(); 73 | } catch (IOException e) { 74 | Log.e(TAG, "Failed to close file with saved input audio: " + e); 75 | } 76 | rawAudioFileOutputStream = null; 77 | } 78 | fileSizeInBytes = 0; 79 | } 80 | } 81 | 82 | // Checks if external storage is available for read and write. 83 | private boolean isExternalStorageWritable() { 84 | String state = Environment.getExternalStorageState(); 85 | if (Environment.MEDIA_MOUNTED.equals(state)) { 86 | return true; 87 | } 88 | return false; 89 | } 90 | 91 | // Utilizes audio parameters to create a file name which contains sufficient 92 | // information so that the file can be played using an external file player. 93 | // Example: /sdcard/recorded_audio_16bits_48000Hz_mono.pcm. 94 | private void openRawAudioOutputFile(int sampleRate, int channelCount) { 95 | final String fileName = Environment.getExternalStorageDirectory().getPath() + File.separator 96 | + "recorded_audio_16bits_" + String.valueOf(sampleRate) + "Hz" 97 | + ((channelCount == 1) ? "_mono" : "_stereo") + ".pcm"; 98 | final File outputFile = new File(fileName); 99 | try { 100 | rawAudioFileOutputStream = new FileOutputStream(outputFile); 101 | } catch (FileNotFoundException e) { 102 | Log.e(TAG, "Failed to open audio output file: " + e.getMessage()); 103 | } 104 | Log.d(TAG, "Opened file for recording: " + fileName); 105 | } 106 | 107 | // Called when new audio samples are ready. 108 | @Override 109 | public void onWebRtcAudioRecordSamplesReady(JavaAudioDeviceModule.AudioSamples samples) { 110 | // The native audio layer on Android should use 16-bit PCM format. 111 | if (samples.getAudioFormat() != AudioFormat.ENCODING_PCM_16BIT) { 112 | Log.e(TAG, "Invalid audio format"); 113 | return; 114 | } 115 | synchronized (lock) { 116 | // Abort early if stop() has been called. 117 | if (!isRunning) { 118 | return; 119 | } 120 | // Open a new file for the first callback only since it allows us to add audio parameters to 121 | // the file name. 122 | if (rawAudioFileOutputStream == null) { 123 | openRawAudioOutputFile(samples.getSampleRate(), samples.getChannelCount()); 124 | fileSizeInBytes = 0; 125 | } 126 | } 127 | // Append the recorded 16-bit audio samples to the open output file. 128 | executor.execute(() -> { 129 | if (rawAudioFileOutputStream != null) { 130 | try { 131 | // Set a limit on max file size. 58348800 bytes corresponds to 132 | // approximately 10 minutes of recording in mono at 48kHz. 133 | if (fileSizeInBytes < MAX_FILE_SIZE_IN_BYTES) { 134 | // Writes samples.getData().length bytes to output stream. 135 | rawAudioFileOutputStream.write(samples.getData()); 136 | fileSizeInBytes += samples.getData().length; 137 | } 138 | } catch (IOException e) { 139 | Log.e(TAG, "Failed to write audio to file: " + e.getMessage()); 140 | } 141 | } 142 | }); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/java/com/someshk/apprtc/AppRTCProximitySensor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 The WebRTC Project Authors. All rights reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. An additional intellectual property rights grant can be found 7 | * in the file PATENTS. All contributing project authors may 8 | * be found in the AUTHORS file in the root of the source tree. 9 | */ 10 | 11 | package com.someshk.apprtc; 12 | 13 | import android.content.Context; 14 | import android.hardware.Sensor; 15 | import android.hardware.SensorEvent; 16 | import android.hardware.SensorEventListener; 17 | import android.hardware.SensorManager; 18 | import android.os.Build; 19 | import androidx.annotation.Nullable; 20 | import android.util.Log; 21 | 22 | import com.someshk.apprtc.util.AppRTCUtils; 23 | 24 | import org.webrtc.ThreadUtils; 25 | 26 | /** 27 | * AppRTCProximitySensor manages functions related to the proximity sensor in 28 | * the AppRTC demo. 29 | * On most device, the proximity sensor is implemented as a boolean-sensor. 30 | * It returns just two values "NEAR" or "FAR". Thresholding is done on the LUX 31 | * value i.e. the LUX value of the light sensor is compared with a threshold. 32 | * A LUX-value more than the threshold means the proximity sensor returns "FAR". 33 | * Anything less than the threshold value and the sensor returns "NEAR". 34 | */ 35 | public class AppRTCProximitySensor implements SensorEventListener { 36 | private static final String TAG = "AppRTCProximitySensor"; 37 | // This class should be created, started and stopped on one thread 38 | // (e.g. the main thread). We use |nonThreadSafe| to ensure that this is 39 | // the case. Only active when |DEBUG| is set to true. 40 | private final ThreadUtils.ThreadChecker threadChecker = new ThreadUtils.ThreadChecker(); 41 | private final Runnable onSensorStateListener; 42 | private final SensorManager sensorManager; 43 | @Nullable 44 | private Sensor proximitySensor; 45 | private boolean lastStateReportIsNear; 46 | 47 | /** 48 | * Construction 49 | */ 50 | static AppRTCProximitySensor create(Context context, Runnable sensorStateListener) { 51 | return new AppRTCProximitySensor(context, sensorStateListener); 52 | } 53 | 54 | private AppRTCProximitySensor(Context context, Runnable sensorStateListener) { 55 | Log.d(TAG, "AppRTCProximitySensor" + AppRTCUtils.getThreadInfo()); 56 | onSensorStateListener = sensorStateListener; 57 | sensorManager = ((SensorManager) context.getSystemService(Context.SENSOR_SERVICE)); 58 | } 59 | 60 | /** 61 | * Activate the proximity sensor. Also do initialization if called for the 62 | * first time. 63 | */ 64 | public boolean start() { 65 | threadChecker.checkIsOnValidThread(); 66 | Log.d(TAG, "start" + AppRTCUtils.getThreadInfo()); 67 | if (!initDefaultSensor()) { 68 | // Proximity sensor is not supported on this device. 69 | return false; 70 | } 71 | sensorManager.registerListener(this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL); 72 | return true; 73 | } 74 | 75 | /** 76 | * Deactivate the proximity sensor. 77 | */ 78 | public void stop() { 79 | threadChecker.checkIsOnValidThread(); 80 | Log.d(TAG, "stop" + AppRTCUtils.getThreadInfo()); 81 | if (proximitySensor == null) { 82 | return; 83 | } 84 | sensorManager.unregisterListener(this, proximitySensor); 85 | } 86 | 87 | /** 88 | * Getter for last reported state. Set to true if "near" is reported. 89 | */ 90 | public boolean sensorReportsNearState() { 91 | threadChecker.checkIsOnValidThread(); 92 | return lastStateReportIsNear; 93 | } 94 | 95 | @Override 96 | public final void onAccuracyChanged(Sensor sensor, int accuracy) { 97 | threadChecker.checkIsOnValidThread(); 98 | AppRTCUtils.assertIsTrue(sensor.getType() == Sensor.TYPE_PROXIMITY); 99 | if (accuracy == SensorManager.SENSOR_STATUS_UNRELIABLE) { 100 | Log.e(TAG, "The values returned by this sensor cannot be trusted"); 101 | } 102 | } 103 | 104 | @Override 105 | public final void onSensorChanged(SensorEvent event) { 106 | threadChecker.checkIsOnValidThread(); 107 | AppRTCUtils.assertIsTrue(event.sensor.getType() == Sensor.TYPE_PROXIMITY); 108 | // As a best practice; do as little as possible within this method and 109 | // avoid blocking. 110 | float distanceInCentimeters = event.values[0]; 111 | if (distanceInCentimeters < proximitySensor.getMaximumRange()) { 112 | Log.d(TAG, "Proximity sensor => NEAR state"); 113 | lastStateReportIsNear = true; 114 | } else { 115 | Log.d(TAG, "Proximity sensor => FAR state"); 116 | lastStateReportIsNear = false; 117 | } 118 | // Report about new state to listening client. Client can then call 119 | // sensorReportsNearState() to query the current state (NEAR or FAR). 120 | if (onSensorStateListener != null) { 121 | onSensorStateListener.run(); 122 | } 123 | Log.d(TAG, "onSensorChanged" + AppRTCUtils.getThreadInfo() + ": " 124 | + "accuracy=" + event.accuracy + ", timestamp=" + event.timestamp + ", distance=" 125 | + event.values[0]); 126 | } 127 | 128 | /** 129 | * Get default proximity sensor if it exists. Tablet devices (e.g. Nexus 7) 130 | * does not support this type of sensor and false will be returned in such 131 | * cases. 132 | */ 133 | private boolean initDefaultSensor() { 134 | if (proximitySensor != null) { 135 | return true; 136 | } 137 | proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY); 138 | if (proximitySensor == null) { 139 | return false; 140 | } 141 | logProximitySensorInfo(); 142 | return true; 143 | } 144 | 145 | /** 146 | * Helper method for logging information about the proximity sensor. 147 | */ 148 | private void logProximitySensorInfo() { 149 | if (proximitySensor == null) { 150 | return; 151 | } 152 | StringBuilder info = new StringBuilder("Proximity sensor: "); 153 | info.append("name=").append(proximitySensor.getName()); 154 | info.append(", vendor: ").append(proximitySensor.getVendor()); 155 | info.append(", power: ").append(proximitySensor.getPower()); 156 | info.append(", resolution: ").append(proximitySensor.getResolution()); 157 | info.append(", max range: ").append(proximitySensor.getMaximumRange()); 158 | info.append(", min delay: ").append(proximitySensor.getMinDelay()); 159 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { 160 | // Added in API level 20. 161 | info.append(", type: ").append(proximitySensor.getStringType()); 162 | } 163 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 164 | // Added in API level 21. 165 | info.append(", max delay: ").append(proximitySensor.getMaxDelay()); 166 | info.append(", reporting mode: ").append(proximitySensor.getReportingMode()); 167 | info.append(", isWakeUpSensor: ").append(proximitySensor.isWakeUpSensor()); 168 | } 169 | Log.d(TAG, info.toString()); 170 | } 171 | } -------------------------------------------------------------------------------- /app/src/main/java/com/someshk/apprtc/HudFragment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 The WebRTC Project Authors. All rights reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. An additional intellectual property rights grant can be found 7 | * in the file PATENTS. All contributing project authors may 8 | * be found in the AUTHORS file in the root of the source tree. 9 | */ 10 | 11 | package com.someshk.apprtc; 12 | 13 | import android.app.Fragment; 14 | import android.os.Bundle; 15 | import android.util.TypedValue; 16 | import android.view.LayoutInflater; 17 | import android.view.View; 18 | import android.view.ViewGroup; 19 | import android.widget.ImageButton; 20 | import android.widget.TextView; 21 | 22 | import org.webrtc.StatsReport; 23 | 24 | import java.util.HashMap; 25 | import java.util.Map; 26 | 27 | /** 28 | * Fragment for HUD statistics display. 29 | */ 30 | public class HudFragment extends Fragment { 31 | private TextView encoderStatView; 32 | private TextView hudViewBwe; 33 | private TextView hudViewConnection; 34 | private TextView hudViewVideoSend; 35 | private TextView hudViewVideoRecv; 36 | private ImageButton toggleDebugButton; 37 | private boolean videoCallEnabled; 38 | private boolean displayHud; 39 | private volatile boolean isRunning; 40 | public CpuMonitor cpuMonitor; 41 | 42 | @Override 43 | public View onCreateView( 44 | LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 45 | View controlView = inflater.inflate(R.layout.fragment_hud, container, false); 46 | // Create UI controls. 47 | encoderStatView = controlView.findViewById(R.id.encoder_stat_call); 48 | hudViewBwe = controlView.findViewById(R.id.hud_stat_bwe); 49 | hudViewConnection = controlView.findViewById(R.id.hud_stat_connection); 50 | hudViewVideoSend = controlView.findViewById(R.id.hud_stat_video_send); 51 | hudViewVideoRecv = controlView.findViewById(R.id.hud_stat_video_recv); 52 | toggleDebugButton = controlView.findViewById(R.id.button_toggle_debug); 53 | toggleDebugButton.setOnClickListener(new View.OnClickListener() { 54 | @Override 55 | public void onClick(View view) { 56 | if (displayHud) { 57 | int visibility = 58 | (hudViewBwe.getVisibility() == View.VISIBLE) ? View.INVISIBLE : View.VISIBLE; 59 | hudViewsSetProperties(visibility); 60 | } 61 | } 62 | }); 63 | return controlView; 64 | } 65 | 66 | @Override 67 | public void onStart() { 68 | super.onStart(); 69 | Bundle args = getArguments(); 70 | if (args != null) { 71 | videoCallEnabled = args.getBoolean(CallActivity.EXTRA_VIDEO_CALL, true); 72 | displayHud = args.getBoolean(CallActivity.EXTRA_DISPLAY_HUD, false); 73 | } 74 | int visibility = displayHud ? View.VISIBLE : View.INVISIBLE; 75 | encoderStatView.setVisibility(visibility); 76 | toggleDebugButton.setVisibility(visibility); 77 | hudViewsSetProperties(View.INVISIBLE); 78 | isRunning = true; 79 | } 80 | 81 | @Override 82 | public void onStop() { 83 | isRunning = false; 84 | super.onStop(); 85 | } 86 | 87 | public void setCpuMonitor(CpuMonitor cpuMonitor) { 88 | this.cpuMonitor = cpuMonitor; 89 | } 90 | 91 | private void hudViewsSetProperties(int visibility) { 92 | hudViewBwe.setVisibility(visibility); 93 | hudViewConnection.setVisibility(visibility); 94 | hudViewVideoSend.setVisibility(visibility); 95 | hudViewVideoRecv.setVisibility(visibility); 96 | hudViewBwe.setTextSize(TypedValue.COMPLEX_UNIT_PT, 5); 97 | hudViewConnection.setTextSize(TypedValue.COMPLEX_UNIT_PT, 5); 98 | hudViewVideoSend.setTextSize(TypedValue.COMPLEX_UNIT_PT, 5); 99 | hudViewVideoRecv.setTextSize(TypedValue.COMPLEX_UNIT_PT, 5); 100 | } 101 | 102 | private Map getReportMap(StatsReport report) { 103 | Map reportMap = new HashMap<>(); 104 | for (StatsReport.Value value : report.values) { 105 | reportMap.put(value.name, value.value); 106 | } 107 | return reportMap; 108 | } 109 | 110 | public void updateEncoderStatistics(final StatsReport[] reports) { 111 | if (!isRunning || !displayHud) { 112 | return; 113 | } 114 | StringBuilder encoderStat = new StringBuilder(128); 115 | StringBuilder bweStat = new StringBuilder(); 116 | StringBuilder connectionStat = new StringBuilder(); 117 | StringBuilder videoSendStat = new StringBuilder(); 118 | StringBuilder videoRecvStat = new StringBuilder(); 119 | String fps = null; 120 | String targetBitrate = null; 121 | String actualBitrate = null; 122 | for (StatsReport report : reports) { 123 | if (report.type.equals("ssrc") && report.id.contains("ssrc") && report.id.contains("send")) { 124 | // Send video statistics. 125 | Map reportMap = getReportMap(report); 126 | String trackId = reportMap.get("googTrackId"); 127 | if (trackId != null && trackId.contains(PeerConnectionClient.VIDEO_TRACK_ID)) { 128 | fps = reportMap.get("googFrameRateSent"); 129 | videoSendStat.append(report.id).append("\n"); 130 | for (StatsReport.Value value : report.values) { 131 | String name = value.name.replace("goog", ""); 132 | videoSendStat.append(name).append("=").append(value.value).append("\n"); 133 | } 134 | } 135 | } else if (report.type.equals("ssrc") && report.id.contains("ssrc") 136 | && report.id.contains("recv")) { 137 | // Receive video statistics. 138 | Map reportMap = getReportMap(report); 139 | // Check if this stat is for video track. 140 | String frameWidth = reportMap.get("googFrameWidthReceived"); 141 | if (frameWidth != null) { 142 | videoRecvStat.append(report.id).append("\n"); 143 | for (StatsReport.Value value : report.values) { 144 | String name = value.name.replace("goog", ""); 145 | videoRecvStat.append(name).append("=").append(value.value).append("\n"); 146 | } 147 | } 148 | } else if (report.id.equals("bweforvideo")) { 149 | // BWE statistics. 150 | Map reportMap = getReportMap(report); 151 | targetBitrate = reportMap.get("googTargetEncBitrate"); 152 | actualBitrate = reportMap.get("googActualEncBitrate"); 153 | bweStat.append(report.id).append("\n"); 154 | for (StatsReport.Value value : report.values) { 155 | String name = value.name.replace("goog", "").replace("Available", ""); 156 | bweStat.append(name).append("=").append(value.value).append("\n"); 157 | } 158 | } else if (report.type.equals("googCandidatePair")) { 159 | // Connection statistics. 160 | Map reportMap = getReportMap(report); 161 | String activeConnection = reportMap.get("googActiveConnection"); 162 | if (activeConnection != null && activeConnection.equals("true")) { 163 | connectionStat.append(report.id).append("\n"); 164 | for (StatsReport.Value value : report.values) { 165 | String name = value.name.replace("goog", ""); 166 | connectionStat.append(name).append("=").append(value.value).append("\n"); 167 | } 168 | } 169 | } 170 | } 171 | hudViewBwe.setText(bweStat.toString()); 172 | hudViewConnection.setText(connectionStat.toString()); 173 | hudViewVideoSend.setText(videoSendStat.toString()); 174 | hudViewVideoRecv.setText(videoRecvStat.toString()); 175 | if (videoCallEnabled) { 176 | if (fps != null) { 177 | encoderStat.append("Fps: ").append(fps).append("\n"); 178 | } 179 | if (targetBitrate != null) { 180 | encoderStat.append("Target BR: ").append(targetBitrate).append("\n"); 181 | } 182 | if (actualBitrate != null) { 183 | encoderStat.append("Actual BR: ").append(actualBitrate).append("\n"); 184 | } 185 | } 186 | if (cpuMonitor != null) { 187 | encoderStat.append("CPU%: ") 188 | .append(cpuMonitor.getCpuUsageCurrent()) 189 | .append("/") 190 | .append(cpuMonitor.getCpuUsageAverage()) 191 | .append(". Freq: ") 192 | .append(cpuMonitor.getFrequencyScaleAverage()); 193 | } 194 | encoderStatView.setText(encoderStat.toString()); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /app/src/main/java/com/someshk/apprtc/RoomParametersFetcher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 The WebRTC Project Authors. All rights reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. An additional intellectual property rights grant can be found 7 | * in the file PATENTS. All contributing project authors may 8 | * be found in the AUTHORS file in the root of the source tree. 9 | */ 10 | 11 | package com.someshk.apprtc; 12 | 13 | import android.util.Log; 14 | 15 | import com.someshk.apprtc.AppRTCClient.SignalingParameters; 16 | import com.someshk.apprtc.util.AsyncHttpURLConnection; 17 | import com.someshk.apprtc.util.AsyncHttpURLConnection.AsyncHttpEvents; 18 | 19 | import org.json.JSONArray; 20 | import org.json.JSONException; 21 | import org.json.JSONObject; 22 | import org.webrtc.IceCandidate; 23 | import org.webrtc.PeerConnection; 24 | import org.webrtc.SessionDescription; 25 | 26 | import java.io.IOException; 27 | import java.io.InputStream; 28 | import java.net.HttpURLConnection; 29 | import java.net.URL; 30 | import java.util.ArrayList; 31 | import java.util.List; 32 | import java.util.Scanner; 33 | 34 | /** 35 | * AsyncTask that converts an AppRTC room URL into the set of signaling 36 | * parameters to use with that room. 37 | */ 38 | public class RoomParametersFetcher { 39 | private static final String TAG = "RoomRTCClient"; 40 | private static final int TURN_HTTP_TIMEOUT_MS = 5000; 41 | private final RoomParametersFetcherEvents events; 42 | private final String roomUrl; 43 | private final String roomMessage; 44 | 45 | /** 46 | * Room parameters fetcher callbacks. 47 | */ 48 | public interface RoomParametersFetcherEvents { 49 | /** 50 | * Callback fired once the room's signaling parameters 51 | * SignalingParameters are extracted. 52 | */ 53 | void onSignalingParametersReady(final SignalingParameters params); 54 | 55 | /** 56 | * Callback for room parameters extraction error. 57 | */ 58 | void onSignalingParametersError(final String description); 59 | } 60 | 61 | public RoomParametersFetcher( 62 | String roomUrl, String roomMessage, final RoomParametersFetcherEvents events) { 63 | this.roomUrl = roomUrl; 64 | this.roomMessage = roomMessage; 65 | this.events = events; 66 | } 67 | 68 | public void makeRequest() { 69 | Log.d(TAG, "Connecting to room: " + roomUrl); 70 | AsyncHttpURLConnection httpConnection = 71 | new AsyncHttpURLConnection("POST", roomUrl, roomMessage, new AsyncHttpEvents() { 72 | @Override 73 | public void onHttpError(String errorMessage) { 74 | Log.e(TAG, "Room connection error: " + errorMessage); 75 | events.onSignalingParametersError(errorMessage); 76 | } 77 | 78 | @Override 79 | public void onHttpComplete(String response) { 80 | roomHttpResponseParse(response); 81 | } 82 | }); 83 | httpConnection.send(); 84 | } 85 | 86 | private void roomHttpResponseParse(String response) { 87 | Log.d(TAG, "Room response: " + response); 88 | try { 89 | List iceCandidates = null; 90 | SessionDescription offerSdp = null; 91 | JSONObject roomJson = new JSONObject(response); 92 | String result = roomJson.getString("result"); 93 | if (!result.equals("SUCCESS")) { 94 | events.onSignalingParametersError("Room response error: " + result); 95 | return; 96 | } 97 | response = roomJson.getString("params"); 98 | roomJson = new JSONObject(response); 99 | String roomId = roomJson.getString("room_id"); 100 | String clientId = roomJson.getString("client_id"); 101 | String wssUrl = roomJson.getString("wss_url"); 102 | String wssPostUrl = roomJson.getString("wss_post_url"); 103 | boolean initiator = (roomJson.getBoolean("is_initiator")); 104 | if (!initiator) { 105 | iceCandidates = new ArrayList<>(); 106 | String messagesString = roomJson.getString("messages"); 107 | JSONArray messages = new JSONArray(messagesString); 108 | for (int i = 0; i < messages.length(); ++i) { 109 | String messageString = messages.getString(i); 110 | JSONObject message = new JSONObject(messageString); 111 | String messageType = message.getString("type"); 112 | Log.d(TAG, "GAE->C #" + i + " : " + messageString); 113 | if (messageType.equals("offer")) { 114 | offerSdp = new SessionDescription( 115 | SessionDescription.Type.fromCanonicalForm(messageType), message.getString("sdp")); 116 | } else if (messageType.equals("candidate")) { 117 | IceCandidate candidate = new IceCandidate( 118 | message.getString("id"), message.getInt("label"), message.getString("candidate")); 119 | iceCandidates.add(candidate); 120 | } else { 121 | Log.e(TAG, "Unknown message: " + messageString); 122 | } 123 | } 124 | } 125 | Log.d(TAG, "RoomId: " + roomId + ". ClientId: " + clientId); 126 | Log.d(TAG, "Initiator: " + initiator); 127 | Log.d(TAG, "WSS url: " + wssUrl); 128 | Log.d(TAG, "WSS POST url: " + wssPostUrl); 129 | List iceServers = 130 | iceServersFromPCConfigJSON(roomJson.getString("pc_config")); 131 | boolean isTurnPresent = false; 132 | for (PeerConnection.IceServer server : iceServers) { 133 | Log.d(TAG, "IceServer: " + server); 134 | for (String uri : server.urls) { 135 | if (uri.startsWith("turn:")) { 136 | isTurnPresent = true; 137 | break; 138 | } 139 | } 140 | } 141 | // Request TURN servers. 142 | if (!isTurnPresent && !roomJson.optString("ice_server_url").isEmpty()) { 143 | List turnServers = 144 | requestTurnServers(roomJson.getString("ice_server_url")); 145 | for (PeerConnection.IceServer turnServer : turnServers) { 146 | Log.d(TAG, "TurnServer: " + turnServer); 147 | iceServers.add(turnServer); 148 | } 149 | } 150 | SignalingParameters params = new SignalingParameters( 151 | iceServers, initiator, clientId, wssUrl, wssPostUrl, offerSdp, iceCandidates); 152 | events.onSignalingParametersReady(params); 153 | } catch (JSONException e) { 154 | events.onSignalingParametersError("Room JSON parsing error: " + e.toString()); 155 | } catch (IOException e) { 156 | events.onSignalingParametersError("Room IO error: " + e.toString()); 157 | } 158 | } 159 | 160 | // Requests & returns a TURN ICE Server based on a request URL. Must be run 161 | // off the main thread! 162 | private List requestTurnServers(String url) 163 | throws IOException, JSONException { 164 | List turnServers = new ArrayList<>(); 165 | Log.d(TAG, "Request TURN from: " + url); 166 | HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); 167 | connection.setDoOutput(true); 168 | connection.setRequestProperty("REFERER", "https://appr.tc"); 169 | connection.setConnectTimeout(TURN_HTTP_TIMEOUT_MS); 170 | connection.setReadTimeout(TURN_HTTP_TIMEOUT_MS); 171 | int responseCode = connection.getResponseCode(); 172 | if (responseCode != 200) { 173 | throw new IOException("Non-200 response when requesting TURN server from " + url + " : " 174 | + connection.getHeaderField(null)); 175 | } 176 | InputStream responseStream = connection.getInputStream(); 177 | String response = drainStream(responseStream); 178 | connection.disconnect(); 179 | Log.d(TAG, "TURN response: " + response); 180 | JSONObject responseJSON = new JSONObject(response); 181 | JSONArray iceServers = responseJSON.getJSONArray("iceServers"); 182 | for (int i = 0; i < iceServers.length(); ++i) { 183 | JSONObject server = iceServers.getJSONObject(i); 184 | JSONArray turnUrls = server.getJSONArray("urls"); 185 | String username = server.has("username") ? server.getString("username") : ""; 186 | String credential = server.has("credential") ? server.getString("credential") : ""; 187 | for (int j = 0; j < turnUrls.length(); j++) { 188 | String turnUrl = turnUrls.getString(j); 189 | PeerConnection.IceServer turnServer = 190 | PeerConnection.IceServer.builder(turnUrl) 191 | .setUsername(username) 192 | .setPassword(credential) 193 | .createIceServer(); 194 | turnServers.add(turnServer); 195 | } 196 | } 197 | return turnServers; 198 | } 199 | 200 | // Return the list of ICE servers described by a WebRTCPeerConnection 201 | // configuration string. 202 | private List iceServersFromPCConfigJSON(String pcConfig) 203 | throws JSONException { 204 | JSONObject json = new JSONObject(pcConfig); 205 | JSONArray servers = json.getJSONArray("iceServers"); 206 | List ret = new ArrayList<>(); 207 | for (int i = 0; i < servers.length(); ++i) { 208 | JSONObject server = servers.getJSONObject(i); 209 | String url = server.getString("urls"); 210 | String credential = server.has("credential") ? server.getString("credential") : ""; 211 | PeerConnection.IceServer turnServer = 212 | PeerConnection.IceServer.builder(url) 213 | .setPassword(credential) 214 | .createIceServer(); 215 | ret.add(turnServer); 216 | } 217 | return ret; 218 | } 219 | 220 | // Return the contents of an InputStream as a String. 221 | private static String drainStream(InputStream in) { 222 | Scanner s = new Scanner(in, "UTF-8").useDelimiter("\\A"); 223 | return s.hasNext() ? s.next() : ""; 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /app/src/main/res/xml/preferences.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 12 | 13 | 17 | 18 | 22 | 23 | 30 | 31 | 38 | 39 | 44 | 45 | 52 | 53 | 59 | 60 | 67 | 68 | 73 | 74 | 79 | 80 | 85 | 86 | 87 | 90 | 91 | 98 | 99 | 105 | 106 | 113 | 114 | 119 | 120 | 125 | 126 | 131 | 132 | 137 | 138 | 143 | 144 | 149 | 150 | 155 | 156 | 160 | 161 | 168 | 169 | 170 | 173 | 174 | 178 | 179 | 183 | 184 | 190 | 191 | 195 | 196 | 202 | 203 | 209 | 210 | 216 | 217 | 218 | 221 | 222 | 228 | 229 | 234 | 235 | 240 | 241 | 245 | 246 | 247 | 248 | -------------------------------------------------------------------------------- /app/src/main/java/com/someshk/apprtc/WebSocketChannelClient.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 The WebRTC Project Authors. All rights reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. An additional intellectual property rights grant can be found 7 | * in the file PATENTS. All contributing project authors may 8 | * be found in the AUTHORS file in the root of the source tree. 9 | */ 10 | 11 | package com.someshk.apprtc; 12 | 13 | import android.os.Handler; 14 | import androidx.annotation.Nullable; 15 | import android.util.Log; 16 | 17 | import com.someshk.apprtc.util.AsyncHttpURLConnection; 18 | import com.someshk.apprtc.util.AsyncHttpURLConnection.AsyncHttpEvents; 19 | 20 | import org.json.JSONException; 21 | import org.json.JSONObject; 22 | 23 | import java.net.URI; 24 | import java.net.URISyntaxException; 25 | import java.util.ArrayList; 26 | import java.util.List; 27 | 28 | import de.tavendo.autobahn.WebSocket.WebSocketConnectionObserver; 29 | import de.tavendo.autobahn.WebSocketConnection; 30 | import de.tavendo.autobahn.WebSocketException; 31 | 32 | 33 | /** 34 | * WebSocket client implementation. 35 | * 36 | *

All public methods should be called from a looper executor thread 37 | * passed in a constructor, otherwise exception will be thrown. 38 | * All events are dispatched on the same thread. 39 | */ 40 | public class WebSocketChannelClient { 41 | private static final String TAG = "WSChannelRTCClient"; 42 | private static final int CLOSE_TIMEOUT = 1000; 43 | private final WebSocketChannelEvents events; 44 | private final Handler handler; 45 | private WebSocketConnection ws; 46 | private String wsServerUrl; 47 | private String postServerUrl; 48 | @Nullable 49 | private String roomID; 50 | @Nullable 51 | private String clientID; 52 | private WebSocketConnectionState state; 53 | // Do not remove this member variable. If this is removed, the observer gets garbage collected and 54 | // this causes test breakages. 55 | private WebSocketObserver wsObserver; 56 | private final Object closeEventLock = new Object(); 57 | private boolean closeEvent; 58 | // WebSocket send queue. Messages are added to the queue when WebSocket 59 | // client is not registered and are consumed in register() call. 60 | private final List wsSendQueue = new ArrayList<>(); 61 | 62 | /** 63 | * Possible WebSocket connection states. 64 | */ 65 | public enum WebSocketConnectionState {NEW, CONNECTED, REGISTERED, CLOSED, ERROR} 66 | 67 | /** 68 | * Callback interface for messages delivered on WebSocket. 69 | * All events are dispatched from a looper executor thread. 70 | */ 71 | public interface WebSocketChannelEvents { 72 | void onWebSocketMessage(final String message); 73 | 74 | void onWebSocketClose(); 75 | 76 | void onWebSocketError(final String description); 77 | } 78 | 79 | public WebSocketChannelClient(Handler handler, WebSocketChannelEvents events) { 80 | this.handler = handler; 81 | this.events = events; 82 | roomID = null; 83 | clientID = null; 84 | state = WebSocketConnectionState.NEW; 85 | } 86 | 87 | public WebSocketConnectionState getState() { 88 | return state; 89 | } 90 | 91 | public void connect(final String wsUrl, final String postUrl) { 92 | checkIfCalledOnValidThread(); 93 | if (state != WebSocketConnectionState.NEW) { 94 | Log.e(TAG, "WebSocket is already connected."); 95 | return; 96 | } 97 | wsServerUrl = wsUrl; 98 | postServerUrl = postUrl; 99 | closeEvent = false; 100 | Log.d(TAG, "Connecting WebSocket to: " + wsUrl + ". Post URL: " + postUrl); 101 | ws = new WebSocketConnection(); 102 | wsObserver = new WebSocketObserver(); 103 | try { 104 | ws.connect(new URI(wsServerUrl), wsObserver); 105 | } catch (URISyntaxException e) { 106 | reportError("URI error: " + e.getMessage()); 107 | } catch (WebSocketException e) { 108 | reportError("WebSocket connection error: " + e.getMessage()); 109 | } 110 | } 111 | 112 | public void register(final String roomID, final String clientID) { 113 | checkIfCalledOnValidThread(); 114 | this.roomID = roomID; 115 | this.clientID = clientID; 116 | if (state != WebSocketConnectionState.CONNECTED) { 117 | Log.w(TAG, "WebSocket register() in state " + state); 118 | return; 119 | } 120 | Log.d(TAG, "Registering WebSocket for room " + roomID + ". ClientID: " + clientID); 121 | JSONObject json = new JSONObject(); 122 | try { 123 | json.put("cmd", "register"); 124 | json.put("roomid", roomID); 125 | json.put("clientid", clientID); 126 | Log.d(TAG, "C->WSS: " + json.toString()); 127 | ws.sendTextMessage(json.toString()); 128 | state = WebSocketConnectionState.REGISTERED; 129 | // Send any previously accumulated messages. 130 | for (String sendMessage : wsSendQueue) { 131 | send(sendMessage); 132 | } 133 | wsSendQueue.clear(); 134 | } catch (JSONException e) { 135 | reportError("WebSocket register JSON error: " + e.getMessage()); 136 | } 137 | } 138 | 139 | public void send(String message) { 140 | checkIfCalledOnValidThread(); 141 | switch (state) { 142 | case NEW: 143 | case CONNECTED: 144 | // Store outgoing messages and send them after websocket client 145 | // is registered. 146 | Log.d(TAG, "WS ACC: " + message); 147 | wsSendQueue.add(message); 148 | return; 149 | case ERROR: 150 | case CLOSED: 151 | Log.e(TAG, "WebSocket send() in error or closed state : " + message); 152 | return; 153 | case REGISTERED: 154 | JSONObject json = new JSONObject(); 155 | try { 156 | json.put("cmd", "send"); 157 | json.put("msg", message); 158 | message = json.toString(); 159 | Log.d(TAG, "C->WSS: " + message); 160 | ws.sendTextMessage(message); 161 | } catch (JSONException e) { 162 | reportError("WebSocket send JSON error: " + e.getMessage()); 163 | } 164 | break; 165 | } 166 | } 167 | 168 | // This call can be used to send WebSocket messages before WebSocket 169 | // connection is opened. 170 | public void post(String message) { 171 | checkIfCalledOnValidThread(); 172 | sendWSSMessage("POST", message); 173 | } 174 | 175 | public void disconnect(boolean waitForComplete) { 176 | checkIfCalledOnValidThread(); 177 | Log.d(TAG, "Disconnect WebSocket. State: " + state); 178 | if (state == WebSocketConnectionState.REGISTERED) { 179 | // Send "bye" to WebSocket server. 180 | send("{\"type\": \"bye\"}"); 181 | state = WebSocketConnectionState.CONNECTED; 182 | // Send http DELETE to http WebSocket server. 183 | sendWSSMessage("DELETE", ""); 184 | } 185 | // Close WebSocket in CONNECTED or ERROR states only. 186 | if (state == WebSocketConnectionState.CONNECTED || state == WebSocketConnectionState.ERROR) { 187 | ws.disconnect(); 188 | state = WebSocketConnectionState.CLOSED; 189 | // Wait for websocket close event to prevent websocket library from 190 | // sending any pending messages to deleted looper thread. 191 | if (waitForComplete) { 192 | synchronized (closeEventLock) { 193 | while (!closeEvent) { 194 | try { 195 | closeEventLock.wait(CLOSE_TIMEOUT); 196 | break; 197 | } catch (InterruptedException e) { 198 | Log.e(TAG, "Wait error: " + e.toString()); 199 | } 200 | } 201 | } 202 | } 203 | } 204 | Log.d(TAG, "Disconnecting WebSocket done."); 205 | } 206 | 207 | private void reportError(final String errorMessage) { 208 | Log.e(TAG, errorMessage); 209 | handler.post(new Runnable() { 210 | @Override 211 | public void run() { 212 | if (state != WebSocketConnectionState.ERROR) { 213 | state = WebSocketConnectionState.ERROR; 214 | events.onWebSocketError(errorMessage); 215 | } 216 | } 217 | }); 218 | } 219 | 220 | // Asynchronously send POST/DELETE to WebSocket server. 221 | private void sendWSSMessage(final String method, final String message) { 222 | String postUrl = postServerUrl + "/" + roomID + "/" + clientID; 223 | Log.d(TAG, "WS " + method + " : " + postUrl + " : " + message); 224 | AsyncHttpURLConnection httpConnection = 225 | new AsyncHttpURLConnection(method, postUrl, message, new AsyncHttpEvents() { 226 | @Override 227 | public void onHttpError(String errorMessage) { 228 | reportError("WS " + method + " error: " + errorMessage); 229 | } 230 | 231 | @Override 232 | public void onHttpComplete(String response) { 233 | } 234 | }); 235 | httpConnection.send(); 236 | } 237 | 238 | // Helper method for debugging purposes. Ensures that WebSocket method is 239 | // called on a looper thread. 240 | private void checkIfCalledOnValidThread() { 241 | if (Thread.currentThread() != handler.getLooper().getThread()) { 242 | throw new IllegalStateException("WebSocket method is not called on valid thread"); 243 | } 244 | } 245 | 246 | private class WebSocketObserver implements WebSocketConnectionObserver { 247 | @Override 248 | public void onOpen() { 249 | Log.d(TAG, "WebSocket connection opened to: " + wsServerUrl); 250 | handler.post(new Runnable() { 251 | @Override 252 | public void run() { 253 | state = WebSocketConnectionState.CONNECTED; 254 | // Check if we have pending register request. 255 | if (roomID != null && clientID != null) { 256 | register(roomID, clientID); 257 | } 258 | } 259 | }); 260 | } 261 | 262 | @Override 263 | public void onClose(WebSocketCloseNotification code, String reason) { 264 | Log.d(TAG, "WebSocket connection closed. Code: " + code + ". Reason: " + reason + ". State: " 265 | + state); 266 | synchronized (closeEventLock) { 267 | closeEvent = true; 268 | closeEventLock.notify(); 269 | } 270 | handler.post(new Runnable() { 271 | @Override 272 | public void run() { 273 | if (state != WebSocketConnectionState.CLOSED) { 274 | state = WebSocketConnectionState.CLOSED; 275 | events.onWebSocketClose(); 276 | } 277 | } 278 | }); 279 | } 280 | 281 | @Override 282 | public void onTextMessage(String payload) { 283 | Log.d(TAG, "WSS->C: " + payload); 284 | final String message = payload; 285 | handler.post(new Runnable() { 286 | @Override 287 | public void run() { 288 | if (state == WebSocketConnectionState.CONNECTED 289 | || state == WebSocketConnectionState.REGISTERED) { 290 | events.onWebSocketMessage(message); 291 | } 292 | } 293 | }); 294 | } 295 | 296 | @Override 297 | public void onRawTextMessage(byte[] payload) { 298 | } 299 | 300 | @Override 301 | public void onBinaryMessage(byte[] payload) { 302 | } 303 | } 304 | } -------------------------------------------------------------------------------- /app/src/main/java/com/someshk/apprtc/TCPChannelClient.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 The WebRTC Project Authors. All rights reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. An additional intellectual property rights grant can be found 7 | * in the file PATENTS. All contributing project authors may 8 | * be found in the AUTHORS file in the root of the source tree. 9 | */ 10 | 11 | package com.someshk.apprtc; 12 | 13 | import androidx.annotation.Nullable; 14 | import android.util.Log; 15 | 16 | import org.webrtc.ThreadUtils; 17 | 18 | import java.io.BufferedReader; 19 | import java.io.IOException; 20 | import java.io.InputStreamReader; 21 | import java.io.OutputStreamWriter; 22 | import java.io.PrintWriter; 23 | import java.net.InetAddress; 24 | import java.net.ServerSocket; 25 | import java.net.Socket; 26 | import java.net.UnknownHostException; 27 | import java.nio.charset.Charset; 28 | import java.util.concurrent.ExecutorService; 29 | 30 | /** 31 | * Replacement for WebSocketChannelClient for direct communication between two IP addresses. Handles 32 | * the signaling between the two clients using a TCP connection. 33 | *

34 | * All public methods should be called from a looper executor thread 35 | * passed in a constructor, otherwise exception will be thrown. 36 | * All events are dispatched on the same thread. 37 | */ 38 | public class TCPChannelClient { 39 | private static final String TAG = "TCPChannelClient"; 40 | private final ExecutorService executor; 41 | private final ThreadUtils.ThreadChecker executorThreadCheck; 42 | private final TCPChannelEvents eventListener; 43 | private TCPSocket socket; 44 | 45 | /** 46 | * Callback interface for messages delivered on TCP Connection. All callbacks are invoked from the 47 | * looper executor thread. 48 | */ 49 | public interface TCPChannelEvents { 50 | void onTCPConnected(boolean server); 51 | 52 | void onTCPMessage(String message); 53 | 54 | void onTCPError(String description); 55 | 56 | void onTCPClose(); 57 | } 58 | 59 | /** 60 | * Initializes the TCPChannelClient. If IP is a local IP address, starts a listening server on 61 | * that IP. If not, instead connects to the IP. 62 | * 63 | * @param eventListener Listener that will receive events from the client. 64 | * @param ip IP address to listen on or connect to. 65 | * @param port Port to listen on or connect to. 66 | */ 67 | public TCPChannelClient( 68 | ExecutorService executor, TCPChannelEvents eventListener, String ip, int port) { 69 | this.executor = executor; 70 | executorThreadCheck = new ThreadUtils.ThreadChecker(); 71 | executorThreadCheck.detachThread(); 72 | this.eventListener = eventListener; 73 | InetAddress address; 74 | try { 75 | address = InetAddress.getByName(ip); 76 | } catch (UnknownHostException e) { 77 | reportError("Invalid IP address."); 78 | return; 79 | } 80 | if (address.isAnyLocalAddress()) { 81 | socket = new TCPSocketServer(address, port); 82 | } else { 83 | socket = new TCPSocketClient(address, port); 84 | } 85 | socket.start(); 86 | } 87 | 88 | /** 89 | * Disconnects the client if not already disconnected. This will fire the onTCPClose event. 90 | */ 91 | public void disconnect() { 92 | executorThreadCheck.checkIsOnValidThread(); 93 | socket.disconnect(); 94 | } 95 | 96 | /** 97 | * Sends a message on the socket. 98 | * 99 | * @param message Message to be sent. 100 | */ 101 | public void send(String message) { 102 | executorThreadCheck.checkIsOnValidThread(); 103 | socket.send(message); 104 | } 105 | 106 | /** 107 | * Helper method for firing onTCPError events. Calls onTCPError on the executor thread. 108 | */ 109 | private void reportError(final String message) { 110 | Log.e(TAG, "TCP Error: " + message); 111 | executor.execute(new Runnable() { 112 | @Override 113 | public void run() { 114 | eventListener.onTCPError(message); 115 | } 116 | }); 117 | } 118 | 119 | /** 120 | * Base class for server and client sockets. Contains a listening thread that will call 121 | * eventListener.onTCPMessage on new messages. 122 | */ 123 | private abstract class TCPSocket extends Thread { 124 | // Lock for editing out and rawSocket 125 | protected final Object rawSocketLock; 126 | @Nullable 127 | private PrintWriter out; 128 | @Nullable 129 | private Socket rawSocket; 130 | 131 | /** 132 | * Connect to the peer, potentially a slow operation. 133 | * 134 | * @return Socket connection, null if connection failed. 135 | */ 136 | @Nullable 137 | public abstract Socket connect(); 138 | 139 | /** 140 | * Returns true if sockets is a server rawSocket. 141 | */ 142 | public abstract boolean isServer(); 143 | 144 | TCPSocket() { 145 | rawSocketLock = new Object(); 146 | } 147 | 148 | /** 149 | * The listening thread. 150 | */ 151 | @Override 152 | public void run() { 153 | Log.d(TAG, "Listening thread started..."); 154 | // Receive connection to temporary variable first, so we don't block. 155 | Socket tempSocket = connect(); 156 | BufferedReader in; 157 | Log.d(TAG, "TCP connection established."); 158 | synchronized (rawSocketLock) { 159 | if (rawSocket != null) { 160 | Log.e(TAG, "Socket already existed and will be replaced."); 161 | } 162 | rawSocket = tempSocket; 163 | // Connecting failed, error has already been reported, just exit. 164 | if (rawSocket == null) { 165 | return; 166 | } 167 | try { 168 | out = new PrintWriter( 169 | new OutputStreamWriter(rawSocket.getOutputStream(), Charset.forName("UTF-8")), true); 170 | in = new BufferedReader( 171 | new InputStreamReader(rawSocket.getInputStream(), Charset.forName("UTF-8"))); 172 | } catch (IOException e) { 173 | reportError("Failed to open IO on rawSocket: " + e.getMessage()); 174 | return; 175 | } 176 | } 177 | Log.v(TAG, "Execute onTCPConnected"); 178 | executor.execute(new Runnable() { 179 | @Override 180 | public void run() { 181 | Log.v(TAG, "Run onTCPConnected"); 182 | eventListener.onTCPConnected(isServer()); 183 | } 184 | }); 185 | while (true) { 186 | final String message; 187 | try { 188 | message = in.readLine(); 189 | } catch (IOException e) { 190 | synchronized (rawSocketLock) { 191 | // If socket was closed, this is expected. 192 | if (rawSocket == null) { 193 | break; 194 | } 195 | } 196 | reportError("Failed to read from rawSocket: " + e.getMessage()); 197 | break; 198 | } 199 | // No data received, rawSocket probably closed. 200 | if (message == null) { 201 | break; 202 | } 203 | executor.execute(new Runnable() { 204 | @Override 205 | public void run() { 206 | Log.v(TAG, "Receive: " + message); 207 | eventListener.onTCPMessage(message); 208 | } 209 | }); 210 | } 211 | Log.d(TAG, "Receiving thread exiting..."); 212 | // Close the rawSocket if it is still open. 213 | disconnect(); 214 | } 215 | 216 | /** 217 | * Closes the rawSocket if it is still open. Also fires the onTCPClose event. 218 | */ 219 | public void disconnect() { 220 | try { 221 | synchronized (rawSocketLock) { 222 | if (rawSocket != null) { 223 | rawSocket.close(); 224 | rawSocket = null; 225 | out = null; 226 | executor.execute(new Runnable() { 227 | @Override 228 | public void run() { 229 | eventListener.onTCPClose(); 230 | } 231 | }); 232 | } 233 | } 234 | } catch (IOException e) { 235 | reportError("Failed to close rawSocket: " + e.getMessage()); 236 | } 237 | } 238 | 239 | /** 240 | * Sends a message on the socket. Should only be called on the executor thread. 241 | */ 242 | public void send(String message) { 243 | Log.v(TAG, "Send: " + message); 244 | synchronized (rawSocketLock) { 245 | if (out == null) { 246 | reportError("Sending data on closed socket."); 247 | return; 248 | } 249 | out.write(message + "\n"); 250 | out.flush(); 251 | } 252 | } 253 | } 254 | 255 | private class TCPSocketServer extends TCPSocket { 256 | // Server socket is also guarded by rawSocketLock. 257 | @Nullable 258 | private ServerSocket serverSocket; 259 | final private InetAddress address; 260 | final private int port; 261 | 262 | public TCPSocketServer(InetAddress address, int port) { 263 | this.address = address; 264 | this.port = port; 265 | } 266 | 267 | /** 268 | * Opens a listening socket and waits for a connection. 269 | */ 270 | @Nullable 271 | @Override 272 | public Socket connect() { 273 | Log.d(TAG, "Listening on [" + address.getHostAddress() + "]:" + Integer.toString(port)); 274 | final ServerSocket tempSocket; 275 | try { 276 | tempSocket = new ServerSocket(port, 0, address); 277 | } catch (IOException e) { 278 | reportError("Failed to create server socket: " + e.getMessage()); 279 | return null; 280 | } 281 | synchronized (rawSocketLock) { 282 | if (serverSocket != null) { 283 | Log.e(TAG, "Server rawSocket was already listening and new will be opened."); 284 | } 285 | serverSocket = tempSocket; 286 | } 287 | try { 288 | return tempSocket.accept(); 289 | } catch (IOException e) { 290 | reportError("Failed to receive connection: " + e.getMessage()); 291 | return null; 292 | } 293 | } 294 | 295 | /** 296 | * Closes the listening socket and calls super. 297 | */ 298 | @Override 299 | public void disconnect() { 300 | try { 301 | synchronized (rawSocketLock) { 302 | if (serverSocket != null) { 303 | serverSocket.close(); 304 | serverSocket = null; 305 | } 306 | } 307 | } catch (IOException e) { 308 | reportError("Failed to close server socket: " + e.getMessage()); 309 | } 310 | super.disconnect(); 311 | } 312 | 313 | @Override 314 | public boolean isServer() { 315 | return true; 316 | } 317 | } 318 | 319 | private class TCPSocketClient extends TCPSocket { 320 | final private InetAddress address; 321 | final private int port; 322 | 323 | public TCPSocketClient(InetAddress address, int port) { 324 | this.address = address; 325 | this.port = port; 326 | } 327 | 328 | /** 329 | * Connects to the peer. 330 | */ 331 | @Nullable 332 | @Override 333 | public Socket connect() { 334 | Log.d(TAG, "Connecting to [" + address.getHostAddress() + "]:" + Integer.toString(port)); 335 | try { 336 | return new Socket(address, port); 337 | } catch (IOException e) { 338 | reportError("Failed to connect: " + e.getMessage()); 339 | return null; 340 | } 341 | } 342 | 343 | @Override 344 | public boolean isServer() { 345 | return false; 346 | } 347 | } 348 | } 349 | -------------------------------------------------------------------------------- /app/src/main/java/com/someshk/apprtc/DirectRTCClient.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 The WebRTC Project Authors. All rights reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. An additional intellectual property rights grant can be found 7 | * in the file PATENTS. All contributing project authors may 8 | * be found in the AUTHORS file in the root of the source tree. 9 | */ 10 | 11 | package com.someshk.apprtc; 12 | 13 | import androidx.annotation.Nullable; 14 | import android.util.Log; 15 | 16 | import org.json.JSONArray; 17 | import org.json.JSONException; 18 | import org.json.JSONObject; 19 | import org.webrtc.IceCandidate; 20 | import org.webrtc.SessionDescription; 21 | 22 | import java.util.ArrayList; 23 | import java.util.concurrent.ExecutorService; 24 | import java.util.concurrent.Executors; 25 | import java.util.regex.Matcher; 26 | import java.util.regex.Pattern; 27 | 28 | /** 29 | * Implementation of AppRTCClient that uses direct TCP connection as the signaling channel. 30 | * This eliminates the need for an external server. This class does not support loopback 31 | * connections. 32 | */ 33 | public class DirectRTCClient implements AppRTCClient, TCPChannelClient.TCPChannelEvents { 34 | private static final String TAG = "DirectRTCClient"; 35 | private static final int DEFAULT_PORT = 8888; 36 | // Regex pattern used for checking if room id looks like an IP. 37 | static final Pattern IP_PATTERN = Pattern.compile("(" 38 | // IPv4 39 | + "((\\d+\\.){3}\\d+)|" 40 | // IPv6 41 | + "\\[((([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4})?::" 42 | + "(([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4})?)\\]|" 43 | + "\\[(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4})\\]|" 44 | // IPv6 without [] 45 | + "((([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4})?::(([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4})?)|" 46 | + "(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4})|" 47 | // Literals 48 | + "localhost" 49 | + ")" 50 | // Optional port number 51 | + "(:(\\d+))?"); 52 | private final ExecutorService executor; 53 | private final SignalingEvents events; 54 | @Nullable 55 | private TCPChannelClient tcpClient; 56 | private RoomConnectionParameters connectionParameters; 57 | 58 | private enum ConnectionState {NEW, CONNECTED, CLOSED, ERROR} 59 | 60 | // All alterations of the room state should be done from inside the looper thread. 61 | private ConnectionState roomState; 62 | 63 | public DirectRTCClient(SignalingEvents events) { 64 | this.events = events; 65 | executor = Executors.newSingleThreadExecutor(); 66 | roomState = ConnectionState.NEW; 67 | } 68 | 69 | /** 70 | * Connects to the room, roomId in connectionsParameters is required. roomId must be a valid 71 | * IP address matching IP_PATTERN. 72 | */ 73 | @Override 74 | public void connectToRoom(RoomConnectionParameters connectionParameters) { 75 | this.connectionParameters = connectionParameters; 76 | if (connectionParameters.loopback) { 77 | reportError("Loopback connections aren't supported by DirectRTCClient."); 78 | } 79 | executor.execute(new Runnable() { 80 | @Override 81 | public void run() { 82 | connectToRoomInternal(); 83 | } 84 | }); 85 | } 86 | 87 | @Override 88 | public void disconnectFromRoom() { 89 | executor.execute(new Runnable() { 90 | @Override 91 | public void run() { 92 | disconnectFromRoomInternal(); 93 | } 94 | }); 95 | } 96 | 97 | /** 98 | * Connects to the room. 99 | *

100 | * Runs on the looper thread. 101 | */ 102 | private void connectToRoomInternal() { 103 | this.roomState = ConnectionState.NEW; 104 | String endpoint = connectionParameters.roomId; 105 | Matcher matcher = IP_PATTERN.matcher(endpoint); 106 | if (!matcher.matches()) { 107 | reportError("roomId must match IP_PATTERN for DirectRTCClient."); 108 | return; 109 | } 110 | String ip = matcher.group(1); 111 | String portStr = matcher.group(matcher.groupCount()); 112 | int port; 113 | if (portStr != null) { 114 | try { 115 | port = Integer.parseInt(portStr); 116 | } catch (NumberFormatException e) { 117 | reportError("Invalid port number: " + portStr); 118 | return; 119 | } 120 | } else { 121 | port = DEFAULT_PORT; 122 | } 123 | tcpClient = new TCPChannelClient(executor, this, ip, port); 124 | } 125 | 126 | /** 127 | * Disconnects from the room. 128 | *

129 | * Runs on the looper thread. 130 | */ 131 | private void disconnectFromRoomInternal() { 132 | roomState = ConnectionState.CLOSED; 133 | if (tcpClient != null) { 134 | tcpClient.disconnect(); 135 | tcpClient = null; 136 | } 137 | executor.shutdown(); 138 | } 139 | 140 | @Override 141 | public void sendOfferSdp(final SessionDescription sdp) { 142 | executor.execute(new Runnable() { 143 | @Override 144 | public void run() { 145 | if (roomState != ConnectionState.CONNECTED) { 146 | reportError("Sending offer SDP in non connected state."); 147 | return; 148 | } 149 | JSONObject json = new JSONObject(); 150 | jsonPut(json, "sdp", sdp.description); 151 | jsonPut(json, "type", "offer"); 152 | sendMessage(json.toString()); 153 | } 154 | }); 155 | } 156 | 157 | @Override 158 | public void sendAnswerSdp(final SessionDescription sdp) { 159 | executor.execute(new Runnable() { 160 | @Override 161 | public void run() { 162 | JSONObject json = new JSONObject(); 163 | jsonPut(json, "sdp", sdp.description); 164 | jsonPut(json, "type", "answer"); 165 | sendMessage(json.toString()); 166 | } 167 | }); 168 | } 169 | 170 | @Override 171 | public void sendLocalIceCandidate(final IceCandidate candidate) { 172 | executor.execute(new Runnable() { 173 | @Override 174 | public void run() { 175 | JSONObject json = new JSONObject(); 176 | jsonPut(json, "type", "candidate"); 177 | jsonPut(json, "label", candidate.sdpMLineIndex); 178 | jsonPut(json, "id", candidate.sdpMid); 179 | jsonPut(json, "candidate", candidate.sdp); 180 | if (roomState != ConnectionState.CONNECTED) { 181 | reportError("Sending ICE candidate in non connected state."); 182 | return; 183 | } 184 | sendMessage(json.toString()); 185 | } 186 | }); 187 | } 188 | 189 | /** 190 | * Send removed Ice candidates to the other participant. 191 | */ 192 | @Override 193 | public void sendLocalIceCandidateRemovals(final IceCandidate[] candidates) { 194 | executor.execute(new Runnable() { 195 | @Override 196 | public void run() { 197 | JSONObject json = new JSONObject(); 198 | jsonPut(json, "type", "remove-candidates"); 199 | JSONArray jsonArray = new JSONArray(); 200 | for (final IceCandidate candidate : candidates) { 201 | jsonArray.put(toJsonCandidate(candidate)); 202 | } 203 | jsonPut(json, "candidates", jsonArray); 204 | if (roomState != ConnectionState.CONNECTED) { 205 | reportError("Sending ICE candidate removals in non connected state."); 206 | return; 207 | } 208 | sendMessage(json.toString()); 209 | } 210 | }); 211 | } 212 | // ------------------------------------------------------------------- 213 | // TCPChannelClient event handlers 214 | 215 | /** 216 | * If the client is the server side, this will trigger onConnectedToRoom. 217 | */ 218 | @Override 219 | public void onTCPConnected(boolean isServer) { 220 | if (isServer) { 221 | roomState = ConnectionState.CONNECTED; 222 | SignalingParameters parameters = new SignalingParameters( 223 | // Ice servers are not needed for direct connections. 224 | new ArrayList<>(), 225 | isServer, // Server side acts as the initiator on direct connections. 226 | null, // clientId 227 | null, // wssUrl 228 | null, // wwsPostUrl 229 | null, // offerSdp 230 | null // iceCandidates 231 | ); 232 | events.onConnectedToRoom(parameters); 233 | } 234 | } 235 | 236 | @Override 237 | public void onTCPMessage(String msg) { 238 | try { 239 | JSONObject json = new JSONObject(msg); 240 | String type = json.optString("type"); 241 | if (type.equals("candidate")) { 242 | events.onRemoteIceCandidate(toJavaCandidate(json)); 243 | } else if (type.equals("remove-candidates")) { 244 | JSONArray candidateArray = json.getJSONArray("candidates"); 245 | IceCandidate[] candidates = new IceCandidate[candidateArray.length()]; 246 | for (int i = 0; i < candidateArray.length(); ++i) { 247 | candidates[i] = toJavaCandidate(candidateArray.getJSONObject(i)); 248 | } 249 | events.onRemoteIceCandidatesRemoved(candidates); 250 | } else if (type.equals("answer")) { 251 | SessionDescription sdp = new SessionDescription( 252 | SessionDescription.Type.fromCanonicalForm(type), json.getString("sdp")); 253 | events.onRemoteDescription(sdp); 254 | } else if (type.equals("offer")) { 255 | SessionDescription sdp = new SessionDescription( 256 | SessionDescription.Type.fromCanonicalForm(type), json.getString("sdp")); 257 | SignalingParameters parameters = new SignalingParameters( 258 | // Ice servers are not needed for direct connections. 259 | new ArrayList<>(), 260 | false, // This code will only be run on the client side. So, we are not the initiator. 261 | null, // clientId 262 | null, // wssUrl 263 | null, // wssPostUrl 264 | sdp, // offerSdp 265 | null // iceCandidates 266 | ); 267 | roomState = ConnectionState.CONNECTED; 268 | events.onConnectedToRoom(parameters); 269 | } else { 270 | reportError("Unexpected TCP message: " + msg); 271 | } 272 | } catch (JSONException e) { 273 | reportError("TCP message JSON parsing error: " + e.toString()); 274 | } 275 | } 276 | 277 | @Override 278 | public void onTCPError(String description) { 279 | reportError("TCP connection error: " + description); 280 | } 281 | 282 | @Override 283 | public void onTCPClose() { 284 | events.onChannelClose(); 285 | } 286 | 287 | // -------------------------------------------------------------------- 288 | // Helper functions. 289 | private void reportError(final String errorMessage) { 290 | Log.e(TAG, errorMessage); 291 | executor.execute(new Runnable() { 292 | @Override 293 | public void run() { 294 | if (roomState != ConnectionState.ERROR) { 295 | roomState = ConnectionState.ERROR; 296 | events.onChannelError(errorMessage); 297 | } 298 | } 299 | }); 300 | } 301 | 302 | private void sendMessage(final String message) { 303 | executor.execute(new Runnable() { 304 | @Override 305 | public void run() { 306 | tcpClient.send(message); 307 | } 308 | }); 309 | } 310 | 311 | // Put a |key|->|value| mapping in |json|. 312 | private static void jsonPut(JSONObject json, String key, Object value) { 313 | try { 314 | json.put(key, value); 315 | } catch (JSONException e) { 316 | throw new RuntimeException(e); 317 | } 318 | } 319 | 320 | // Converts a Java candidate to a JSONObject. 321 | private static JSONObject toJsonCandidate(final IceCandidate candidate) { 322 | JSONObject json = new JSONObject(); 323 | jsonPut(json, "label", candidate.sdpMLineIndex); 324 | jsonPut(json, "id", candidate.sdpMid); 325 | jsonPut(json, "candidate", candidate.sdp); 326 | return json; 327 | } 328 | 329 | // Converts a JSON candidate to a Java object. 330 | private static IceCandidate toJavaCandidate(JSONObject json) throws JSONException { 331 | return new IceCandidate( 332 | json.getString("id"), json.getInt("label"), json.getString("candidate")); 333 | } 334 | } 335 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | AppRTC 4 | AppRTC Settings 5 | Disconnect Call 6 | 7 | Please enter a room name. Room names are shared with everyone, so think 8 | of something unique and send it to a friend. 9 | 10 | Favorites 11 | No favorites 12 | Invalid URL 13 | The URL or room name you entered resulted in an invalid URL: %1$s 14 | 15 | Connection error 16 | Connecting to: %1$s 17 | FATAL ERROR: Missing URL to connect to. 18 | Camera2 only supports capturing to texture. Either disable Camera2 or enable capturing to texture in the options. 19 | OK 20 | Switch front/back camera 21 | Slide to change capture format 22 | Muted 23 | Toggle debug view 24 | Toggle microphone on/off 25 | Settings 26 | Loopback connection 27 | Connect to the room 28 | Add favorite 29 | %1$dx%2$d @ %3$d fps 30 | 31 | 32 | room_preference 33 | room_list_preference 34 | 35 | video_settings_key 36 | WebRTC video settings. 37 | 38 | videocall_preference 39 | Video call. 40 | Enable video in a call. 41 | true 42 | 43 | screencapture_preference 44 | Use screencapture. 45 | false 46 | 47 | camera2_preference 48 | Use Camera2. 49 | true 50 | Not supported on this device. 51 | 52 | resolution_preference 53 | Video resolution. 54 | Enter AppRTC local video resolution. 55 | Default 56 | 57 | fps_preference 58 | Camera fps. 59 | Enter local camera fps. 60 | Default 61 | 62 | capturequalityslider_preference 63 | Capture quality slider. 64 | Enable slider for changing capture quality. 65 | false 66 | 67 | maxvideobitrate_preference 68 | Maximum video bitrate setting. 69 | Maximum video bitrate setting. 70 | Default 71 | 72 | maxvideobitratevalue_preference 73 | Video encoder maximum bitrate. 74 | Enter video encoder maximum bitrate in kbps. 75 | 1700 76 | 77 | videocodec_preference 78 | Default video codec. 79 | Select default video codec. 80 | VP8 81 | 82 | hwcodec_preference 83 | Video codec hardware acceleration. 84 | Use hardware accelerated video codec (if available). 85 | true 86 | 87 | capturetotexture_preference 88 | Video capture to surface texture. 89 | Capture video to textures (if available). 90 | true 91 | 92 | flexfec_preference 93 | Codec-agnostic Flexible FEC. 94 | Enable FlexFEC. 95 | false 96 | 97 | Enabled 98 | Disabled 99 | 100 | audio_settings_key 101 | WebRTC audio settings. 102 | 103 | startaudiobitrate_preference 104 | Audio bitrate setting. 105 | Audio bitrate setting. 106 | Default 107 | 108 | startaudiobitratevalue_preference 109 | Audio codec bitrate. 110 | Enter audio codec bitrate in kbps. 111 | 32 112 | 113 | audiocodec_preference 114 | Default audio codec. 115 | Select default audio codec. 116 | OPUS 117 | 118 | audioprocessing_preference 119 | Disable audio processing. 120 | Disable audio processing pipeline. 121 | false 122 | 123 | aecdump_preference 124 | Create aecdump. 125 | Enable diagnostic audio recordings. 126 | false 127 | 128 | enable_key 129 | Save input audio to file. 130 | Save input audio to file. 131 | false 132 | 133 | opensles_preference 134 | Use OpenSL ES for audio playback. 135 | Use OpenSL ES for audio playback. 136 | false 137 | 138 | disable_built_in_aec_preference 139 | Disable hardware AEC. 140 | Disable hardware AEC. 141 | false 142 | Hardware AEC is not available 143 | 144 | disable_built_in_agc_preference 145 | Disable hardware AGC. 146 | Disable hardware AGC. 147 | false 148 | Hardware AGC is not available 149 | 150 | disable_built_in_ns_preference 151 | Disable hardware NS. 152 | Disable hardware NS. 153 | false 154 | Hardware NS is not available 155 | 156 | disable_webrtc_agc_and_hpf_preference 157 | Disable WebRTC AGC and HPF. 158 | false 159 | 160 | speakerphone_preference 161 | Speakerphone. 162 | Speakerphone. 163 | auto 164 | 165 | data_settings_key 166 | WebRTC data channel settings. 167 | 168 | enable_datachannel_preference 169 | Enable datachannel. 170 | true 171 | 172 | ordered_preference 173 | Order messages. 174 | true 175 | 176 | Subprotocol 177 | Subprotocol. 178 | Enter subprotocol. 179 | 180 | 181 | negotiated_preference 182 | Negotiated. 183 | false 184 | 185 | max_retransmit_time_ms_preference 186 | Max delay to retransmit. 187 | Enter max delay to retransmit (in ms). 188 | -1 189 | 190 | max_retransmits_preference 191 | Max attempts to retransmit. 192 | Enter max attempts to retransmit. 193 | -1 194 | 195 | data_id_preference 196 | Data id. 197 | Enter data channel id. 198 | -1 199 | 200 | misc_settings_key 201 | Miscellaneous settings. 202 | 203 | room_server_url_preference 204 | Room server URL. 205 | Enter a room server URL. 206 | https://appr.tc 207 | 208 | displayhud_preference 209 | Display call statistics. 210 | Display call statistics. 211 | false 212 | 213 | tracing_preference 214 | Debug performance tracing. 215 | Debug performance tracing. 216 | false 217 | 218 | enable_rtceventlog_key 219 | Enable RtcEventLog. 220 | false 221 | 222 | -------------------------------------------------------------------------------- /app/src/main/java/com/someshk/apprtc/SettingsActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 The WebRTC Project Authors. All rights reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. An additional intellectual property rights grant can be found 7 | * in the file PATENTS. All contributing project authors may 8 | * be found in the AUTHORS file in the root of the source tree. 9 | */ 10 | 11 | package com.someshk.apprtc; 12 | 13 | import android.app.Activity; 14 | import android.content.SharedPreferences; 15 | import android.content.SharedPreferences.OnSharedPreferenceChangeListener; 16 | import android.os.Bundle; 17 | import android.preference.ListPreference; 18 | import android.preference.Preference; 19 | 20 | import org.webrtc.Camera2Enumerator; 21 | import org.webrtc.audio.JavaAudioDeviceModule; 22 | 23 | /** 24 | * Settings activity for AppRTC. 25 | */ 26 | public class SettingsActivity extends Activity implements OnSharedPreferenceChangeListener { 27 | private SettingsFragment settingsFragment; 28 | private String keyprefVideoCall; 29 | private String keyprefScreencapture; 30 | private String keyprefCamera2; 31 | private String keyprefResolution; 32 | private String keyprefFps; 33 | private String keyprefCaptureQualitySlider; 34 | private String keyprefMaxVideoBitrateType; 35 | private String keyprefMaxVideoBitrateValue; 36 | private String keyPrefVideoCodec; 37 | private String keyprefHwCodec; 38 | private String keyprefCaptureToTexture; 39 | private String keyprefFlexfec; 40 | private String keyprefStartAudioBitrateType; 41 | private String keyprefStartAudioBitrateValue; 42 | private String keyPrefAudioCodec; 43 | private String keyprefNoAudioProcessing; 44 | private String keyprefAecDump; 45 | private String keyprefEnableSaveInputAudioToFile; 46 | private String keyprefOpenSLES; 47 | private String keyprefDisableBuiltInAEC; 48 | private String keyprefDisableBuiltInAGC; 49 | private String keyprefDisableBuiltInNS; 50 | private String keyprefDisableWebRtcAGCAndHPF; 51 | private String keyprefSpeakerphone; 52 | private String keyPrefRoomServerUrl; 53 | private String keyPrefDisplayHud; 54 | private String keyPrefTracing; 55 | private String keyprefEnabledRtcEventLog; 56 | private String keyprefEnableDataChannel; 57 | private String keyprefOrdered; 58 | private String keyprefMaxRetransmitTimeMs; 59 | private String keyprefMaxRetransmits; 60 | private String keyprefDataProtocol; 61 | private String keyprefNegotiated; 62 | private String keyprefDataId; 63 | 64 | @Override 65 | protected void onCreate(Bundle savedInstanceState) { 66 | super.onCreate(savedInstanceState); 67 | keyprefVideoCall = getString(R.string.pref_videocall_key); 68 | keyprefScreencapture = getString(R.string.pref_screencapture_key); 69 | keyprefCamera2 = getString(R.string.pref_camera2_key); 70 | keyprefResolution = getString(R.string.pref_resolution_key); 71 | keyprefFps = getString(R.string.pref_fps_key); 72 | keyprefCaptureQualitySlider = getString(R.string.pref_capturequalityslider_key); 73 | keyprefMaxVideoBitrateType = getString(R.string.pref_maxvideobitrate_key); 74 | keyprefMaxVideoBitrateValue = getString(R.string.pref_maxvideobitratevalue_key); 75 | keyPrefVideoCodec = getString(R.string.pref_videocodec_key); 76 | keyprefHwCodec = getString(R.string.pref_hwcodec_key); 77 | keyprefCaptureToTexture = getString(R.string.pref_capturetotexture_key); 78 | keyprefFlexfec = getString(R.string.pref_flexfec_key); 79 | keyprefStartAudioBitrateType = getString(R.string.pref_startaudiobitrate_key); 80 | keyprefStartAudioBitrateValue = getString(R.string.pref_startaudiobitratevalue_key); 81 | keyPrefAudioCodec = getString(R.string.pref_audiocodec_key); 82 | keyprefNoAudioProcessing = getString(R.string.pref_noaudioprocessing_key); 83 | keyprefAecDump = getString(R.string.pref_aecdump_key); 84 | keyprefEnableSaveInputAudioToFile = 85 | getString(R.string.pref_enable_save_input_audio_to_file_key); 86 | keyprefOpenSLES = getString(R.string.pref_opensles_key); 87 | keyprefDisableBuiltInAEC = getString(R.string.pref_disable_built_in_aec_key); 88 | keyprefDisableBuiltInAGC = getString(R.string.pref_disable_built_in_agc_key); 89 | keyprefDisableBuiltInNS = getString(R.string.pref_disable_built_in_ns_key); 90 | keyprefDisableWebRtcAGCAndHPF = getString(R.string.pref_disable_webrtc_agc_and_hpf_key); 91 | keyprefSpeakerphone = getString(R.string.pref_speakerphone_key); 92 | keyprefEnableDataChannel = getString(R.string.pref_enable_datachannel_key); 93 | keyprefOrdered = getString(R.string.pref_ordered_key); 94 | keyprefMaxRetransmitTimeMs = getString(R.string.pref_max_retransmit_time_ms_key); 95 | keyprefMaxRetransmits = getString(R.string.pref_max_retransmits_key); 96 | keyprefDataProtocol = getString(R.string.pref_data_protocol_key); 97 | keyprefNegotiated = getString(R.string.pref_negotiated_key); 98 | keyprefDataId = getString(R.string.pref_data_id_key); 99 | keyPrefRoomServerUrl = getString(R.string.pref_room_server_url_key); 100 | keyPrefDisplayHud = getString(R.string.pref_displayhud_key); 101 | keyPrefTracing = getString(R.string.pref_tracing_key); 102 | keyprefEnabledRtcEventLog = getString(R.string.pref_enable_rtceventlog_key); 103 | // Display the fragment as the main content. 104 | settingsFragment = new SettingsFragment(); 105 | getFragmentManager() 106 | .beginTransaction() 107 | .replace(android.R.id.content, settingsFragment) 108 | .commit(); 109 | } 110 | 111 | @Override 112 | protected void onResume() { 113 | super.onResume(); 114 | // Set summary to be the user-description for the selected value 115 | SharedPreferences sharedPreferences = 116 | settingsFragment.getPreferenceScreen().getSharedPreferences(); 117 | sharedPreferences.registerOnSharedPreferenceChangeListener(this); 118 | updateSummaryB(sharedPreferences, keyprefVideoCall); 119 | updateSummaryB(sharedPreferences, keyprefScreencapture); 120 | updateSummaryB(sharedPreferences, keyprefCamera2); 121 | updateSummary(sharedPreferences, keyprefResolution); 122 | updateSummary(sharedPreferences, keyprefFps); 123 | updateSummaryB(sharedPreferences, keyprefCaptureQualitySlider); 124 | updateSummary(sharedPreferences, keyprefMaxVideoBitrateType); 125 | updateSummaryBitrate(sharedPreferences, keyprefMaxVideoBitrateValue); 126 | setVideoBitrateEnable(sharedPreferences); 127 | updateSummary(sharedPreferences, keyPrefVideoCodec); 128 | updateSummaryB(sharedPreferences, keyprefHwCodec); 129 | updateSummaryB(sharedPreferences, keyprefCaptureToTexture); 130 | updateSummaryB(sharedPreferences, keyprefFlexfec); 131 | updateSummary(sharedPreferences, keyprefStartAudioBitrateType); 132 | updateSummaryBitrate(sharedPreferences, keyprefStartAudioBitrateValue); 133 | setAudioBitrateEnable(sharedPreferences); 134 | updateSummary(sharedPreferences, keyPrefAudioCodec); 135 | updateSummaryB(sharedPreferences, keyprefNoAudioProcessing); 136 | updateSummaryB(sharedPreferences, keyprefAecDump); 137 | updateSummaryB(sharedPreferences, keyprefEnableSaveInputAudioToFile); 138 | updateSummaryB(sharedPreferences, keyprefOpenSLES); 139 | updateSummaryB(sharedPreferences, keyprefDisableBuiltInAEC); 140 | updateSummaryB(sharedPreferences, keyprefDisableBuiltInAGC); 141 | updateSummaryB(sharedPreferences, keyprefDisableBuiltInNS); 142 | updateSummaryB(sharedPreferences, keyprefDisableWebRtcAGCAndHPF); 143 | updateSummaryList(sharedPreferences, keyprefSpeakerphone); 144 | updateSummaryB(sharedPreferences, keyprefEnableDataChannel); 145 | updateSummaryB(sharedPreferences, keyprefOrdered); 146 | updateSummary(sharedPreferences, keyprefMaxRetransmitTimeMs); 147 | updateSummary(sharedPreferences, keyprefMaxRetransmits); 148 | updateSummary(sharedPreferences, keyprefDataProtocol); 149 | updateSummaryB(sharedPreferences, keyprefNegotiated); 150 | updateSummary(sharedPreferences, keyprefDataId); 151 | setDataChannelEnable(sharedPreferences); 152 | updateSummary(sharedPreferences, keyPrefRoomServerUrl); 153 | updateSummaryB(sharedPreferences, keyPrefDisplayHud); 154 | updateSummaryB(sharedPreferences, keyPrefTracing); 155 | updateSummaryB(sharedPreferences, keyprefEnabledRtcEventLog); 156 | if (!Camera2Enumerator.isSupported(this)) { 157 | Preference camera2Preference = settingsFragment.findPreference(keyprefCamera2); 158 | camera2Preference.setSummary(getString(R.string.pref_camera2_not_supported)); 159 | camera2Preference.setEnabled(false); 160 | } 161 | if (!JavaAudioDeviceModule.isBuiltInAcousticEchoCancelerSupported()) { 162 | Preference disableBuiltInAECPreference = 163 | settingsFragment.findPreference(keyprefDisableBuiltInAEC); 164 | disableBuiltInAECPreference.setSummary(getString(R.string.pref_built_in_aec_not_available)); 165 | disableBuiltInAECPreference.setEnabled(false); 166 | } 167 | Preference disableBuiltInAGCPreference = 168 | settingsFragment.findPreference(keyprefDisableBuiltInAGC); 169 | disableBuiltInAGCPreference.setSummary(getString(R.string.pref_built_in_agc_not_available)); 170 | disableBuiltInAGCPreference.setEnabled(false); 171 | if (!JavaAudioDeviceModule.isBuiltInNoiseSuppressorSupported()) { 172 | Preference disableBuiltInNSPreference = 173 | settingsFragment.findPreference(keyprefDisableBuiltInNS); 174 | disableBuiltInNSPreference.setSummary(getString(R.string.pref_built_in_ns_not_available)); 175 | disableBuiltInNSPreference.setEnabled(false); 176 | } 177 | } 178 | 179 | @Override 180 | protected void onPause() { 181 | super.onPause(); 182 | SharedPreferences sharedPreferences = 183 | settingsFragment.getPreferenceScreen().getSharedPreferences(); 184 | sharedPreferences.unregisterOnSharedPreferenceChangeListener(this); 185 | } 186 | 187 | @Override 188 | public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { 189 | // clang-format off 190 | if (key.equals(keyprefResolution) 191 | || key.equals(keyprefFps) 192 | || key.equals(keyprefMaxVideoBitrateType) 193 | || key.equals(keyPrefVideoCodec) 194 | || key.equals(keyprefStartAudioBitrateType) 195 | || key.equals(keyPrefAudioCodec) 196 | || key.equals(keyPrefRoomServerUrl) 197 | || key.equals(keyprefMaxRetransmitTimeMs) 198 | || key.equals(keyprefMaxRetransmits) 199 | || key.equals(keyprefDataProtocol) 200 | || key.equals(keyprefDataId)) { 201 | updateSummary(sharedPreferences, key); 202 | } else if (key.equals(keyprefMaxVideoBitrateValue) 203 | || key.equals(keyprefStartAudioBitrateValue)) { 204 | updateSummaryBitrate(sharedPreferences, key); 205 | } else if (key.equals(keyprefVideoCall) 206 | || key.equals(keyprefScreencapture) 207 | || key.equals(keyprefCamera2) 208 | || key.equals(keyPrefTracing) 209 | || key.equals(keyprefCaptureQualitySlider) 210 | || key.equals(keyprefHwCodec) 211 | || key.equals(keyprefCaptureToTexture) 212 | || key.equals(keyprefFlexfec) 213 | || key.equals(keyprefNoAudioProcessing) 214 | || key.equals(keyprefAecDump) 215 | || key.equals(keyprefEnableSaveInputAudioToFile) 216 | || key.equals(keyprefOpenSLES) 217 | || key.equals(keyprefDisableBuiltInAEC) 218 | || key.equals(keyprefDisableBuiltInAGC) 219 | || key.equals(keyprefDisableBuiltInNS) 220 | || key.equals(keyprefDisableWebRtcAGCAndHPF) 221 | || key.equals(keyPrefDisplayHud) 222 | || key.equals(keyprefEnableDataChannel) 223 | || key.equals(keyprefOrdered) 224 | || key.equals(keyprefNegotiated) 225 | || key.equals(keyprefEnabledRtcEventLog)) { 226 | updateSummaryB(sharedPreferences, key); 227 | } else if (key.equals(keyprefSpeakerphone)) { 228 | updateSummaryList(sharedPreferences, key); 229 | } 230 | // clang-format on 231 | if (key.equals(keyprefMaxVideoBitrateType)) { 232 | setVideoBitrateEnable(sharedPreferences); 233 | } 234 | if (key.equals(keyprefStartAudioBitrateType)) { 235 | setAudioBitrateEnable(sharedPreferences); 236 | } 237 | if (key.equals(keyprefEnableDataChannel)) { 238 | setDataChannelEnable(sharedPreferences); 239 | } 240 | } 241 | 242 | private void updateSummary(SharedPreferences sharedPreferences, String key) { 243 | Preference updatedPref = settingsFragment.findPreference(key); 244 | // Set summary to be the user-description for the selected value 245 | updatedPref.setSummary(sharedPreferences.getString(key, "")); 246 | } 247 | 248 | private void updateSummaryBitrate(SharedPreferences sharedPreferences, String key) { 249 | Preference updatedPref = settingsFragment.findPreference(key); 250 | updatedPref.setSummary(sharedPreferences.getString(key, "") + " kbps"); 251 | } 252 | 253 | private void updateSummaryB(SharedPreferences sharedPreferences, String key) { 254 | Preference updatedPref = settingsFragment.findPreference(key); 255 | updatedPref.setSummary(sharedPreferences.getBoolean(key, true) 256 | ? getString(R.string.pref_value_enabled) 257 | : getString(R.string.pref_value_disabled)); 258 | } 259 | 260 | private void updateSummaryList(SharedPreferences sharedPreferences, String key) { 261 | ListPreference updatedPref = (ListPreference) settingsFragment.findPreference(key); 262 | updatedPref.setSummary(updatedPref.getEntry()); 263 | } 264 | 265 | private void setVideoBitrateEnable(SharedPreferences sharedPreferences) { 266 | Preference bitratePreferenceValue = 267 | settingsFragment.findPreference(keyprefMaxVideoBitrateValue); 268 | String bitrateTypeDefault = getString(R.string.pref_maxvideobitrate_default); 269 | String bitrateType = 270 | sharedPreferences.getString(keyprefMaxVideoBitrateType, bitrateTypeDefault); 271 | if (bitrateType.equals(bitrateTypeDefault)) { 272 | bitratePreferenceValue.setEnabled(false); 273 | } else { 274 | bitratePreferenceValue.setEnabled(true); 275 | } 276 | } 277 | 278 | private void setAudioBitrateEnable(SharedPreferences sharedPreferences) { 279 | Preference bitratePreferenceValue = 280 | settingsFragment.findPreference(keyprefStartAudioBitrateValue); 281 | String bitrateTypeDefault = getString(R.string.pref_startaudiobitrate_default); 282 | String bitrateType = 283 | sharedPreferences.getString(keyprefStartAudioBitrateType, bitrateTypeDefault); 284 | if (bitrateType.equals(bitrateTypeDefault)) { 285 | bitratePreferenceValue.setEnabled(false); 286 | } else { 287 | bitratePreferenceValue.setEnabled(true); 288 | } 289 | } 290 | 291 | private void setDataChannelEnable(SharedPreferences sharedPreferences) { 292 | boolean enabled = sharedPreferences.getBoolean(keyprefEnableDataChannel, true); 293 | settingsFragment.findPreference(keyprefOrdered).setEnabled(enabled); 294 | settingsFragment.findPreference(keyprefMaxRetransmitTimeMs).setEnabled(enabled); 295 | settingsFragment.findPreference(keyprefMaxRetransmits).setEnabled(enabled); 296 | settingsFragment.findPreference(keyprefDataProtocol).setEnabled(enabled); 297 | settingsFragment.findPreference(keyprefNegotiated).setEnabled(enabled); 298 | settingsFragment.findPreference(keyprefDataId).setEnabled(enabled); 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /app/src/main/java/com/someshk/apprtc/WebSocketRTCClient.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 The WebRTC Project Authors. All rights reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. An additional intellectual property rights grant can be found 7 | * in the file PATENTS. All contributing project authors may 8 | * be found in the AUTHORS file in the root of the source tree. 9 | */ 10 | 11 | package com.someshk.apprtc; 12 | 13 | 14 | import android.os.Handler; 15 | import android.os.HandlerThread; 16 | import androidx.annotation.Nullable; 17 | import android.util.Log; 18 | 19 | import com.someshk.apprtc.RoomParametersFetcher.RoomParametersFetcherEvents; 20 | import com.someshk.apprtc.WebSocketChannelClient.WebSocketChannelEvents; 21 | import com.someshk.apprtc.WebSocketChannelClient.WebSocketConnectionState; 22 | import com.someshk.apprtc.util.AsyncHttpURLConnection; 23 | import com.someshk.apprtc.util.AsyncHttpURLConnection.AsyncHttpEvents; 24 | 25 | import org.json.JSONArray; 26 | import org.json.JSONException; 27 | import org.json.JSONObject; 28 | import org.webrtc.IceCandidate; 29 | import org.webrtc.SessionDescription; 30 | 31 | /** 32 | * Negotiates signaling for chatting with https://appr.tc "rooms". 33 | * Uses the client<->server specifics of the apprtc AppEngine webapp. 34 | * 35 | *

To use: create an instance of this object (registering a message handler) and 36 | * call connectToRoom(). Once room connection is established 37 | * onConnectedToRoom() callback with room parameters is invoked. 38 | * Messages to other party (with local Ice candidates and answer SDP) can 39 | * be sent after WebSocket connection is established. 40 | */ 41 | public class WebSocketRTCClient implements AppRTCClient, WebSocketChannelEvents { 42 | private static final String TAG = "WSRTCClient"; 43 | private static final String ROOM_JOIN = "join"; 44 | private static final String ROOM_MESSAGE = "message"; 45 | private static final String ROOM_LEAVE = "leave"; 46 | 47 | private enum ConnectionState {NEW, CONNECTED, CLOSED, ERROR} 48 | 49 | private enum MessageType {MESSAGE, LEAVE} 50 | 51 | private final Handler handler; 52 | private boolean initiator; 53 | private SignalingEvents events; 54 | private WebSocketChannelClient wsClient; 55 | private ConnectionState roomState; 56 | private RoomConnectionParameters connectionParameters; 57 | private String messageUrl; 58 | private String leaveUrl; 59 | 60 | public WebSocketRTCClient(SignalingEvents events) { 61 | this.events = events; 62 | roomState = ConnectionState.NEW; 63 | final HandlerThread handlerThread = new HandlerThread(TAG); 64 | handlerThread.start(); 65 | handler = new Handler(handlerThread.getLooper()); 66 | } 67 | 68 | // -------------------------------------------------------------------- 69 | // AppRTCClient interface implementation. 70 | // Asynchronously connect to an AppRTC room URL using supplied connection 71 | // parameters, retrieves room parameters and connect to WebSocket server. 72 | @Override 73 | public void connectToRoom(RoomConnectionParameters connectionParameters) { 74 | this.connectionParameters = connectionParameters; 75 | handler.post(new Runnable() { 76 | @Override 77 | public void run() { 78 | connectToRoomInternal(); 79 | } 80 | }); 81 | } 82 | 83 | @Override 84 | public void disconnectFromRoom() { 85 | handler.post(new Runnable() { 86 | @Override 87 | public void run() { 88 | disconnectFromRoomInternal(); 89 | handler.getLooper().quit(); 90 | } 91 | }); 92 | } 93 | 94 | // Connects to room - function runs on a local looper thread. 95 | private void connectToRoomInternal() { 96 | String connectionUrl = getConnectionUrl(connectionParameters); 97 | Log.d(TAG, "Connect to room: " + connectionUrl); 98 | roomState = ConnectionState.NEW; 99 | wsClient = new WebSocketChannelClient(handler, this); 100 | RoomParametersFetcherEvents callbacks = new RoomParametersFetcherEvents() { 101 | @Override 102 | public void onSignalingParametersReady(final SignalingParameters params) { 103 | WebSocketRTCClient.this.handler.post(new Runnable() { 104 | @Override 105 | public void run() { 106 | WebSocketRTCClient.this.signalingParametersReady(params); 107 | } 108 | }); 109 | } 110 | 111 | @Override 112 | public void onSignalingParametersError(String description) { 113 | WebSocketRTCClient.this.reportError(description); 114 | } 115 | }; 116 | new RoomParametersFetcher(connectionUrl, null, callbacks).makeRequest(); 117 | } 118 | 119 | // Disconnect from room and send bye messages - runs on a local looper thread. 120 | private void disconnectFromRoomInternal() { 121 | Log.d(TAG, "Disconnect. Room state: " + roomState); 122 | if (roomState == ConnectionState.CONNECTED) { 123 | Log.d(TAG, "Closing room."); 124 | sendPostMessage(MessageType.LEAVE, leaveUrl, null); 125 | } 126 | roomState = ConnectionState.CLOSED; 127 | if (wsClient != null) { 128 | wsClient.disconnect(true); 129 | } 130 | } 131 | 132 | // Helper functions to get connection, post message and leave message URLs 133 | private String getConnectionUrl(RoomConnectionParameters connectionParameters) { 134 | return connectionParameters.roomUrl + "/" + ROOM_JOIN + "/" + connectionParameters.roomId 135 | + getQueryString(connectionParameters); 136 | } 137 | 138 | private String getMessageUrl( 139 | RoomConnectionParameters connectionParameters, SignalingParameters signalingParameters) { 140 | return connectionParameters.roomUrl + "/" + ROOM_MESSAGE + "/" + connectionParameters.roomId 141 | + "/" + signalingParameters.clientId + getQueryString(connectionParameters); 142 | } 143 | 144 | private String getLeaveUrl( 145 | RoomConnectionParameters connectionParameters, SignalingParameters signalingParameters) { 146 | return connectionParameters.roomUrl + "/" + ROOM_LEAVE + "/" + connectionParameters.roomId + "/" 147 | + signalingParameters.clientId + getQueryString(connectionParameters); 148 | } 149 | 150 | private String getQueryString(RoomConnectionParameters connectionParameters) { 151 | if (connectionParameters.urlParameters != null) { 152 | return "?" + connectionParameters.urlParameters; 153 | } else { 154 | return ""; 155 | } 156 | } 157 | 158 | // Callback issued when room parameters are extracted. Runs on local 159 | // looper thread. 160 | private void signalingParametersReady(final SignalingParameters signalingParameters) { 161 | Log.d(TAG, "Room connection completed."); 162 | if (connectionParameters.loopback 163 | && (!signalingParameters.initiator || signalingParameters.offerSdp != null)) { 164 | reportError("Loopback room is busy."); 165 | return; 166 | } 167 | if (!connectionParameters.loopback && !signalingParameters.initiator 168 | && signalingParameters.offerSdp == null) { 169 | Log.w(TAG, "No offer SDP in room response."); 170 | } 171 | initiator = signalingParameters.initiator; 172 | messageUrl = getMessageUrl(connectionParameters, signalingParameters); 173 | leaveUrl = getLeaveUrl(connectionParameters, signalingParameters); 174 | Log.d(TAG, "Message URL: " + messageUrl); 175 | Log.d(TAG, "Leave URL: " + leaveUrl); 176 | roomState = ConnectionState.CONNECTED; 177 | // Fire connection and signaling parameters events. 178 | events.onConnectedToRoom(signalingParameters); 179 | // Connect and register WebSocket client. 180 | wsClient.connect(signalingParameters.wssUrl, signalingParameters.wssPostUrl); 181 | wsClient.register(connectionParameters.roomId, signalingParameters.clientId); 182 | } 183 | 184 | // Send local offer SDP to the other participant. 185 | @Override 186 | public void sendOfferSdp(final SessionDescription sdp) { 187 | handler.post(new Runnable() { 188 | @Override 189 | public void run() { 190 | if (roomState != ConnectionState.CONNECTED) { 191 | reportError("Sending offer SDP in non connected state."); 192 | return; 193 | } 194 | JSONObject json = new JSONObject(); 195 | jsonPut(json, "sdp", sdp.description); 196 | jsonPut(json, "type", "offer"); 197 | sendPostMessage(MessageType.MESSAGE, messageUrl, json.toString()); 198 | if (connectionParameters.loopback) { 199 | // In loopback mode rename this offer to answer and route it back. 200 | SessionDescription sdpAnswer = new SessionDescription( 201 | SessionDescription.Type.fromCanonicalForm("answer"), sdp.description); 202 | events.onRemoteDescription(sdpAnswer); 203 | } 204 | } 205 | }); 206 | } 207 | 208 | // Send local answer SDP to the other participant. 209 | @Override 210 | public void sendAnswerSdp(final SessionDescription sdp) { 211 | handler.post(new Runnable() { 212 | @Override 213 | public void run() { 214 | if (connectionParameters.loopback) { 215 | Log.e(TAG, "Sending answer in loopback mode."); 216 | return; 217 | } 218 | JSONObject json = new JSONObject(); 219 | jsonPut(json, "sdp", sdp.description); 220 | jsonPut(json, "type", "answer"); 221 | wsClient.send(json.toString()); 222 | } 223 | }); 224 | } 225 | 226 | // Send Ice candidate to the other participant. 227 | @Override 228 | public void sendLocalIceCandidate(final IceCandidate candidate) { 229 | handler.post(new Runnable() { 230 | @Override 231 | public void run() { 232 | JSONObject json = new JSONObject(); 233 | jsonPut(json, "type", "candidate"); 234 | jsonPut(json, "label", candidate.sdpMLineIndex); 235 | jsonPut(json, "id", candidate.sdpMid); 236 | jsonPut(json, "candidate", candidate.sdp); 237 | if (initiator) { 238 | // Call initiator sends ice candidates to GAE server. 239 | if (roomState != ConnectionState.CONNECTED) { 240 | reportError("Sending ICE candidate in non connected state."); 241 | return; 242 | } 243 | sendPostMessage(MessageType.MESSAGE, messageUrl, json.toString()); 244 | if (connectionParameters.loopback) { 245 | events.onRemoteIceCandidate(candidate); 246 | } 247 | } else { 248 | // Call receiver sends ice candidates to websocket server. 249 | wsClient.send(json.toString()); 250 | } 251 | } 252 | }); 253 | } 254 | 255 | // Send removed Ice candidates to the other participant. 256 | @Override 257 | public void sendLocalIceCandidateRemovals(final IceCandidate[] candidates) { 258 | handler.post(new Runnable() { 259 | @Override 260 | public void run() { 261 | JSONObject json = new JSONObject(); 262 | jsonPut(json, "type", "remove-candidates"); 263 | JSONArray jsonArray = new JSONArray(); 264 | for (final IceCandidate candidate : candidates) { 265 | jsonArray.put(toJsonCandidate(candidate)); 266 | } 267 | jsonPut(json, "candidates", jsonArray); 268 | if (initiator) { 269 | // Call initiator sends ice candidates to GAE server. 270 | if (roomState != ConnectionState.CONNECTED) { 271 | reportError("Sending ICE candidate removals in non connected state."); 272 | return; 273 | } 274 | sendPostMessage(MessageType.MESSAGE, messageUrl, json.toString()); 275 | if (connectionParameters.loopback) { 276 | events.onRemoteIceCandidatesRemoved(candidates); 277 | } 278 | } else { 279 | // Call receiver sends ice candidates to websocket server. 280 | wsClient.send(json.toString()); 281 | } 282 | } 283 | }); 284 | } 285 | 286 | // -------------------------------------------------------------------- 287 | // WebSocketChannelEvents interface implementation. 288 | // All events are called by WebSocketChannelClient on a local looper thread 289 | // (passed to WebSocket client constructor). 290 | @Override 291 | public void onWebSocketMessage(final String msg) { 292 | if (wsClient.getState() != WebSocketConnectionState.REGISTERED) { 293 | Log.e(TAG, "Got WebSocket message in non registered state."); 294 | return; 295 | } 296 | try { 297 | JSONObject json = new JSONObject(msg); 298 | String msgText = json.getString("msg"); 299 | String errorText = json.optString("error"); 300 | if (msgText.length() > 0) { 301 | json = new JSONObject(msgText); 302 | String type = json.optString("type"); 303 | if (type.equals("candidate")) { 304 | events.onRemoteIceCandidate(toJavaCandidate(json)); 305 | } else if (type.equals("remove-candidates")) { 306 | JSONArray candidateArray = json.getJSONArray("candidates"); 307 | IceCandidate[] candidates = new IceCandidate[candidateArray.length()]; 308 | for (int i = 0; i < candidateArray.length(); ++i) { 309 | candidates[i] = toJavaCandidate(candidateArray.getJSONObject(i)); 310 | } 311 | events.onRemoteIceCandidatesRemoved(candidates); 312 | } else if (type.equals("answer")) { 313 | if (initiator) { 314 | SessionDescription sdp = new SessionDescription( 315 | SessionDescription.Type.fromCanonicalForm(type), json.getString("sdp")); 316 | events.onRemoteDescription(sdp); 317 | } else { 318 | reportError("Received answer for call initiator: " + msg); 319 | } 320 | } else if (type.equals("offer")) { 321 | if (!initiator) { 322 | SessionDescription sdp = new SessionDescription( 323 | SessionDescription.Type.fromCanonicalForm(type), json.getString("sdp")); 324 | events.onRemoteDescription(sdp); 325 | } else { 326 | reportError("Received offer for call receiver: " + msg); 327 | } 328 | } else if (type.equals("bye")) { 329 | events.onChannelClose(); 330 | } else { 331 | reportError("Unexpected WebSocket message: " + msg); 332 | } 333 | } else { 334 | if (errorText != null && errorText.length() > 0) { 335 | reportError("WebSocket error message: " + errorText); 336 | } else { 337 | reportError("Unexpected WebSocket message: " + msg); 338 | } 339 | } 340 | } catch (JSONException e) { 341 | reportError("WebSocket message JSON parsing error: " + e.toString()); 342 | } 343 | } 344 | 345 | @Override 346 | public void onWebSocketClose() { 347 | events.onChannelClose(); 348 | } 349 | 350 | @Override 351 | public void onWebSocketError(String description) { 352 | reportError("WebSocket error: " + description); 353 | } 354 | 355 | // -------------------------------------------------------------------- 356 | // Helper functions. 357 | private void reportError(final String errorMessage) { 358 | Log.e(TAG, errorMessage); 359 | handler.post(new Runnable() { 360 | @Override 361 | public void run() { 362 | if (roomState != ConnectionState.ERROR) { 363 | roomState = ConnectionState.ERROR; 364 | events.onChannelError(errorMessage); 365 | } 366 | } 367 | }); 368 | } 369 | 370 | // Put a |key|->|value| mapping in |json|. 371 | private static void jsonPut(JSONObject json, String key, Object value) { 372 | try { 373 | json.put(key, value); 374 | } catch (JSONException e) { 375 | throw new RuntimeException(e); 376 | } 377 | } 378 | 379 | // Send SDP or ICE candidate to a room server. 380 | private void sendPostMessage( 381 | final MessageType messageType, final String url, @Nullable final String message) { 382 | String logInfo = url; 383 | if (message != null) { 384 | logInfo += ". Message: " + message; 385 | } 386 | Log.d(TAG, "C->GAE: " + logInfo); 387 | AsyncHttpURLConnection httpConnection = 388 | new AsyncHttpURLConnection("POST", url, message, new AsyncHttpEvents() { 389 | @Override 390 | public void onHttpError(String errorMessage) { 391 | reportError("GAE POST error: " + errorMessage); 392 | } 393 | 394 | @Override 395 | public void onHttpComplete(String response) { 396 | if (messageType == MessageType.MESSAGE) { 397 | try { 398 | JSONObject roomJson = new JSONObject(response); 399 | String result = roomJson.getString("result"); 400 | if (!result.equals("SUCCESS")) { 401 | reportError("GAE POST error: " + result); 402 | } 403 | } catch (JSONException e) { 404 | reportError("GAE POST JSON error: " + e.toString()); 405 | } 406 | } 407 | } 408 | }); 409 | httpConnection.send(); 410 | } 411 | 412 | // Converts a Java candidate to a JSONObject. 413 | private JSONObject toJsonCandidate(final IceCandidate candidate) { 414 | JSONObject json = new JSONObject(); 415 | jsonPut(json, "label", candidate.sdpMLineIndex); 416 | jsonPut(json, "id", candidate.sdpMid); 417 | jsonPut(json, "candidate", candidate.sdp); 418 | return json; 419 | } 420 | 421 | // Converts a JSON candidate to a Java object. 422 | IceCandidate toJavaCandidate(JSONObject json) throws JSONException { 423 | return new IceCandidate( 424 | json.getString("id"), json.getInt("label"), json.getString("candidate")); 425 | } 426 | } --------------------------------------------------------------------------------