├── .gitignore ├── README.md ├── app ├── build.gradle ├── libs │ └── autobanh.jar └── src │ ├── androidTest │ └── java │ │ └── org │ │ └── appspot │ │ └── apprtc │ │ └── test │ │ └── PeerConnectionClientTest.java │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── org │ │ └── appspot │ │ └── apprtc │ │ ├── AppRTCAudioManager.java │ │ ├── AppRTCBluetoothManager.java │ │ ├── AppRTCClient.java │ │ ├── AppRTCProximitySensor.java │ │ ├── CallActivity.java │ │ ├── CallFragment.java │ │ ├── CaptureQualityController.java │ │ ├── ConnectActivity.java │ │ ├── CpuMonitor.java │ │ ├── DirectRTCClient.java │ │ ├── HudFragment.java │ │ ├── PeerConnectionClient.java │ │ ├── RecordedAudioToFileController.java │ │ ├── RoomParametersFetcher.java │ │ ├── RtcEventLog.java │ │ ├── SettingsActivity.java │ │ ├── SettingsFragment.java │ │ ├── TCPChannelClient.java │ │ ├── UnhandledExceptionHandler.java │ │ ├── WebSocketChannelClient.java │ │ ├── WebSocketRTCClient.java │ │ └── util │ │ ├── AppRTCUtils.java │ │ └── AsyncHttpURLConnection.java │ └── res │ ├── drawable-hdpi │ ├── disconnect.png │ ├── ic_action_full_screen.png │ ├── ic_action_return_from_full_screen.png │ ├── ic_launcher.png │ └── ic_loopback_call.png │ ├── drawable-ldpi │ ├── disconnect.png │ ├── ic_action_full_screen.png │ ├── ic_action_return_from_full_screen.png │ ├── ic_launcher.png │ └── ic_loopback_call.png │ ├── drawable-mdpi │ ├── disconnect.png │ ├── ic_action_full_screen.png │ ├── ic_action_return_from_full_screen.png │ ├── ic_launcher.png │ └── ic_loopback_call.png │ ├── drawable-xhdpi │ ├── disconnect.png │ ├── ic_action_full_screen.png │ ├── ic_action_return_from_full_screen.png │ ├── ic_launcher.png │ └── ic_loopback_call.png │ ├── layout │ ├── activity_call.xml │ ├── activity_connect.xml │ ├── fragment_call.xml │ └── fragment_hud.xml │ ├── menu │ └── connect_menu.xml │ ├── values-v17 │ └── styles.xml │ ├── values-v21 │ └── styles.xml │ ├── values │ ├── arrays.xml │ └── strings.xml │ └── xml │ └── preferences.xml ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── import-summary.txt └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Intellij 4 | .idea/ 5 | *.iml 6 | *.iws 7 | out/ 8 | gen/ 9 | bin/ 10 | 11 | 12 | # Built application files 13 | *.apk 14 | *.ap_ 15 | *.aab 16 | 17 | # Files for the ART/Dalvik VM 18 | *.dex 19 | 20 | # Java class files 21 | *.class 22 | 23 | # Generated files 24 | bin/ 25 | gen/ 26 | out/ 27 | release/ 28 | 29 | # Gradle files 30 | .gradle/ 31 | build/ 32 | 33 | # Local configuration file (sdk path, etc) 34 | local.properties 35 | 36 | # Proguard folder generated by Eclipse 37 | proguard/ 38 | 39 | # Log Files 40 | *.log 41 | 42 | # Android Studio Navigation editor temp files 43 | .navigation/ 44 | 45 | # Android Studio captures folder 46 | captures/ 47 | 48 | # IntelliJ 49 | *.iml 50 | .idea/workspace.xml 51 | .idea/tasks.xml 52 | .idea/gradle.xml 53 | .idea/assetWizardSettings.xml 54 | .idea/dictionaries 55 | .idea/libraries 56 | # Android Studio 3 in .gitignore file. 57 | .idea/caches 58 | .idea/modules.xml 59 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 60 | .idea/navEditor.xmle 61 | 62 | # External native build folder generated in Android Studio 2.2 and later 63 | .externalNativeBuild 64 | 65 | # Freeline 66 | freeline.py 67 | freeline/ 68 | freeline_project_description.json 69 | 70 | # fastlane 71 | fastlane/report.xml 72 | fastlane/Preview.html 73 | fastlane/screenshots 74 | fastlane/test_output 75 | fastlane/readme.md 76 | 77 | # Version control 78 | vcs.xml 79 | 80 | # lint 81 | lint/intermediates/ 82 | lint/generated/ 83 | lint/outputs/ 84 | lint/tmp/ 85 | # lint/reports/ 86 | 87 | *.hprof 88 | 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android Webrtc example 2 | This is working example of WebRTC app from [official webrtc src](https://webrtc.googlesource.com/src/+/refs/heads/master/examples/androidapp/) which can be built with the latest Android Studio(3.6.3). 3 | 4 | This app uses a dependency to latest webrtc Android library: org.webrtc:google-webrtc:1.0.32006 5 | 6 | 7 | 8 | 9 | # How to test 10 | 1. Go to https://appr.tc from any browser and create any room number 11 | 2. Start the Android app. Enter the room number and press call. Video call should start. 12 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | 5 | defaultConfig { 6 | applicationId "org.appspot.apprtc" 7 | minSdkVersion 24 8 | compileSdkVersion 29 9 | 10 | compileOptions { 11 | sourceCompatibility JavaVersion.VERSION_1_8 12 | targetCompatibility JavaVersion.VERSION_1_8 13 | } 14 | 15 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' 22 | } 23 | } 24 | } 25 | 26 | dependencies { 27 | implementation fileTree(dir: 'libs', include: ['*.jar']) 28 | implementation 'org.webrtc:google-webrtc:1.0.32006' 29 | implementation 'com.android.support:support-annotations:28.0.0' 30 | androidTestImplementation 'junit:junit:4.12' 31 | androidTestImplementation'com.android.support.test:runner:1.0.2' 32 | } 33 | 34 | -------------------------------------------------------------------------------- /app/libs/autobanh.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agent10/androidwebrtcexample/4017f64b878670c4c5a98e8380fd7ed315282ee8/app/libs/autobanh.jar -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 28 | 29 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 50 | 51 | 52 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /app/src/main/java/org/appspot/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 org.appspot.apprtc; 12 | 13 | import org.webrtc.IceCandidate; 14 | import org.webrtc.PeerConnection; 15 | import org.webrtc.SessionDescription; 16 | 17 | import java.util.List; 18 | 19 | /** 20 | * AppRTCClient is the interface representing an AppRTC client. 21 | */ 22 | public interface AppRTCClient { 23 | /** 24 | * Struct holding the connection parameters of an AppRTC room. 25 | */ 26 | class RoomConnectionParameters { 27 | public final String roomUrl; 28 | public final String roomId; 29 | public final boolean loopback; 30 | public final String urlParameters; 31 | public RoomConnectionParameters( 32 | String roomUrl, String roomId, boolean loopback, String urlParameters) { 33 | this.roomUrl = roomUrl; 34 | this.roomId = roomId; 35 | this.loopback = loopback; 36 | this.urlParameters = urlParameters; 37 | } 38 | public RoomConnectionParameters(String roomUrl, String roomId, boolean loopback) { 39 | this(roomUrl, roomId, loopback, null /* urlParameters */); 40 | } 41 | } 42 | 43 | /** 44 | * Asynchronously connect to an AppRTC room URL using supplied connection 45 | * parameters. Once connection is established onConnectedToRoom() 46 | * callback with room parameters is invoked. 47 | */ 48 | void connectToRoom(RoomConnectionParameters connectionParameters); 49 | 50 | /** 51 | * Send offer SDP to the other participant. 52 | */ 53 | void sendOfferSdp(final SessionDescription sdp); 54 | 55 | /** 56 | * Send answer SDP to the other participant. 57 | */ 58 | void sendAnswerSdp(final SessionDescription sdp); 59 | 60 | /** 61 | * Send Ice candidate to the other participant. 62 | */ 63 | void sendLocalIceCandidate(final IceCandidate candidate); 64 | 65 | /** 66 | * Send removed ICE candidates to the other participant. 67 | */ 68 | void sendLocalIceCandidateRemovals(final IceCandidate[] candidates); 69 | 70 | /** 71 | * Disconnect from room. 72 | */ 73 | void disconnectFromRoom(); 74 | 75 | /** 76 | * Struct holding the signaling parameters of an AppRTC room. 77 | */ 78 | class SignalingParameters { 79 | public final List iceServers; 80 | public final boolean initiator; 81 | public final String clientId; 82 | public final String wssUrl; 83 | public final String wssPostUrl; 84 | public final SessionDescription offerSdp; 85 | public final List iceCandidates; 86 | 87 | public SignalingParameters(List iceServers, boolean initiator, 88 | String clientId, String wssUrl, String wssPostUrl, SessionDescription offerSdp, 89 | List iceCandidates) { 90 | this.iceServers = iceServers; 91 | this.initiator = initiator; 92 | this.clientId = clientId; 93 | this.wssUrl = wssUrl; 94 | this.wssPostUrl = wssPostUrl; 95 | this.offerSdp = offerSdp; 96 | this.iceCandidates = iceCandidates; 97 | } 98 | } 99 | 100 | /** 101 | * Callback interface for messages delivered on signaling channel. 102 | * 103 | *

Methods are guaranteed to be invoked on the UI thread of |activity|. 104 | */ 105 | interface SignalingEvents { 106 | /** 107 | * Callback fired once the room's signaling parameters 108 | * SignalingParameters are extracted. 109 | */ 110 | void onConnectedToRoom(final SignalingParameters params); 111 | 112 | /** 113 | * Callback fired once remote SDP is received. 114 | */ 115 | void onRemoteDescription(final SessionDescription sdp); 116 | 117 | /** 118 | * Callback fired once remote Ice candidate is received. 119 | */ 120 | void onRemoteIceCandidate(final IceCandidate candidate); 121 | 122 | /** 123 | * Callback fired once remote Ice candidate removals are received. 124 | */ 125 | void onRemoteIceCandidatesRemoved(final IceCandidate[] candidates); 126 | 127 | /** 128 | * Callback fired once channel is closed. 129 | */ 130 | void onChannelClose(); 131 | 132 | /** 133 | * Callback fired once channel error happened. 134 | */ 135 | void onChannelError(final String description); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /app/src/main/java/org/appspot/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 org.appspot.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 android.support.annotation.Nullable; 20 | import android.util.Log; 21 | import org.appspot.apprtc.util.AppRTCUtils; 22 | import org.webrtc.ThreadUtils; 23 | 24 | /** 25 | * AppRTCProximitySensor manages functions related to the proximity sensor in 26 | * the AppRTC demo. 27 | * On most device, the proximity sensor is implemented as a boolean-sensor. 28 | * It returns just two values "NEAR" or "FAR". Thresholding is done on the LUX 29 | * value i.e. the LUX value of the light sensor is compared with a threshold. 30 | * A LUX-value more than the threshold means the proximity sensor returns "FAR". 31 | * Anything less than the threshold value and the sensor returns "NEAR". 32 | */ 33 | public class AppRTCProximitySensor implements SensorEventListener { 34 | private static final String TAG = "AppRTCProximitySensor"; 35 | 36 | // This class should be created, started and stopped on one thread 37 | // (e.g. the main thread). We use |nonThreadSafe| to ensure that this is 38 | // the case. Only active when |DEBUG| is set to true. 39 | private final ThreadUtils.ThreadChecker threadChecker = new ThreadUtils.ThreadChecker(); 40 | 41 | private final Runnable onSensorStateListener; 42 | private final SensorManager sensorManager; 43 | @Nullable private Sensor proximitySensor; 44 | private boolean lastStateReportIsNear; 45 | 46 | /** Construction */ 47 | static AppRTCProximitySensor create(Context context, Runnable sensorStateListener) { 48 | return new AppRTCProximitySensor(context, sensorStateListener); 49 | } 50 | 51 | private AppRTCProximitySensor(Context context, Runnable sensorStateListener) { 52 | Log.d(TAG, "AppRTCProximitySensor" + AppRTCUtils.getThreadInfo()); 53 | onSensorStateListener = sensorStateListener; 54 | sensorManager = ((SensorManager) context.getSystemService(Context.SENSOR_SERVICE)); 55 | } 56 | 57 | /** 58 | * Activate the proximity sensor. Also do initialization if called for the 59 | * first time. 60 | */ 61 | public boolean start() { 62 | threadChecker.checkIsOnValidThread(); 63 | Log.d(TAG, "start" + AppRTCUtils.getThreadInfo()); 64 | if (!initDefaultSensor()) { 65 | // Proximity sensor is not supported on this device. 66 | return false; 67 | } 68 | sensorManager.registerListener(this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL); 69 | return true; 70 | } 71 | 72 | /** Deactivate the proximity sensor. */ 73 | public void stop() { 74 | threadChecker.checkIsOnValidThread(); 75 | Log.d(TAG, "stop" + AppRTCUtils.getThreadInfo()); 76 | if (proximitySensor == null) { 77 | return; 78 | } 79 | sensorManager.unregisterListener(this, proximitySensor); 80 | } 81 | 82 | /** Getter for last reported state. Set to true if "near" is reported. */ 83 | public boolean sensorReportsNearState() { 84 | threadChecker.checkIsOnValidThread(); 85 | return lastStateReportIsNear; 86 | } 87 | 88 | @Override 89 | public final void onAccuracyChanged(Sensor sensor, int accuracy) { 90 | threadChecker.checkIsOnValidThread(); 91 | AppRTCUtils.assertIsTrue(sensor.getType() == Sensor.TYPE_PROXIMITY); 92 | if (accuracy == SensorManager.SENSOR_STATUS_UNRELIABLE) { 93 | Log.e(TAG, "The values returned by this sensor cannot be trusted"); 94 | } 95 | } 96 | 97 | @Override 98 | public final void onSensorChanged(SensorEvent event) { 99 | threadChecker.checkIsOnValidThread(); 100 | AppRTCUtils.assertIsTrue(event.sensor.getType() == Sensor.TYPE_PROXIMITY); 101 | // As a best practice; do as little as possible within this method and 102 | // avoid blocking. 103 | float distanceInCentimeters = event.values[0]; 104 | if (distanceInCentimeters < proximitySensor.getMaximumRange()) { 105 | Log.d(TAG, "Proximity sensor => NEAR state"); 106 | lastStateReportIsNear = true; 107 | } else { 108 | Log.d(TAG, "Proximity sensor => FAR state"); 109 | lastStateReportIsNear = false; 110 | } 111 | 112 | // Report about new state to listening client. Client can then call 113 | // sensorReportsNearState() to query the current state (NEAR or FAR). 114 | if (onSensorStateListener != null) { 115 | onSensorStateListener.run(); 116 | } 117 | 118 | Log.d(TAG, "onSensorChanged" + AppRTCUtils.getThreadInfo() + ": " 119 | + "accuracy=" + event.accuracy + ", timestamp=" + event.timestamp + ", distance=" 120 | + event.values[0]); 121 | } 122 | 123 | /** 124 | * Get default proximity sensor if it exists. Tablet devices (e.g. Nexus 7) 125 | * does not support this type of sensor and false will be returned in such 126 | * cases. 127 | */ 128 | private boolean initDefaultSensor() { 129 | if (proximitySensor != null) { 130 | return true; 131 | } 132 | proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY); 133 | if (proximitySensor == null) { 134 | return false; 135 | } 136 | logProximitySensorInfo(); 137 | return true; 138 | } 139 | 140 | /** Helper method for logging information about the proximity sensor. */ 141 | private void logProximitySensorInfo() { 142 | if (proximitySensor == null) { 143 | return; 144 | } 145 | StringBuilder info = new StringBuilder("Proximity sensor: "); 146 | info.append("name=").append(proximitySensor.getName()); 147 | info.append(", vendor: ").append(proximitySensor.getVendor()); 148 | info.append(", power: ").append(proximitySensor.getPower()); 149 | info.append(", resolution: ").append(proximitySensor.getResolution()); 150 | info.append(", max range: ").append(proximitySensor.getMaximumRange()); 151 | info.append(", min delay: ").append(proximitySensor.getMinDelay()); 152 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { 153 | // Added in API level 20. 154 | info.append(", type: ").append(proximitySensor.getStringType()); 155 | } 156 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 157 | // Added in API level 21. 158 | info.append(", max delay: ").append(proximitySensor.getMaxDelay()); 159 | info.append(", reporting mode: ").append(proximitySensor.getReportingMode()); 160 | info.append(", isWakeUpSensor: ").append(proximitySensor.isWakeUpSensor()); 161 | } 162 | Log.d(TAG, info.toString()); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /app/src/main/java/org/appspot/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 org.appspot.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 | private 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 | void onCameraSwitch(); 45 | void onVideoScalingSwitch(ScalingType scalingType); 46 | void onCaptureFormatChange(int width, int height, int framerate); 47 | boolean onToggleMic(); 48 | } 49 | 50 | @Override 51 | public View onCreateView( 52 | LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 53 | View controlView = inflater.inflate(R.layout.fragment_call, container, false); 54 | 55 | // Create UI controls. 56 | contactView = controlView.findViewById(R.id.contact_name_call); 57 | ImageButton disconnectButton = controlView.findViewById(R.id.button_call_disconnect); 58 | cameraSwitchButton = controlView.findViewById(R.id.button_call_switch_camera); 59 | videoScalingButton = controlView.findViewById(R.id.button_call_scaling_mode); 60 | toggleMuteButton = controlView.findViewById(R.id.button_call_toggle_mic); 61 | captureFormatText = controlView.findViewById(R.id.capture_format_text_call); 62 | captureFormatSlider = controlView.findViewById(R.id.capture_format_slider_call); 63 | 64 | // Add buttons click events. 65 | disconnectButton.setOnClickListener(new View.OnClickListener() { 66 | @Override 67 | public void onClick(View view) { 68 | callEvents.onCallHangUp(); 69 | } 70 | }); 71 | 72 | cameraSwitchButton.setOnClickListener(new View.OnClickListener() { 73 | @Override 74 | public void onClick(View view) { 75 | callEvents.onCameraSwitch(); 76 | } 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 | 94 | toggleMuteButton.setOnClickListener(new View.OnClickListener() { 95 | @Override 96 | public void onClick(View view) { 97 | boolean enabled = callEvents.onToggleMic(); 98 | toggleMuteButton.setAlpha(enabled ? 1.0f : 0.3f); 99 | } 100 | }); 101 | 102 | return controlView; 103 | } 104 | 105 | @Override 106 | public void onStart() { 107 | super.onStart(); 108 | 109 | boolean captureSliderEnabled = false; 110 | Bundle args = getArguments(); 111 | if (args != null) { 112 | String contactName = args.getString(CallActivity.EXTRA_ROOMID); 113 | contactView.setText(contactName); 114 | videoCallEnabled = args.getBoolean(CallActivity.EXTRA_VIDEO_CALL, true); 115 | captureSliderEnabled = videoCallEnabled 116 | && args.getBoolean(CallActivity.EXTRA_VIDEO_CAPTUREQUALITYSLIDER_ENABLED, false); 117 | } 118 | if (!videoCallEnabled) { 119 | cameraSwitchButton.setVisibility(View.INVISIBLE); 120 | } 121 | if (captureSliderEnabled) { 122 | captureFormatSlider.setOnSeekBarChangeListener( 123 | new CaptureQualityController(captureFormatText, callEvents)); 124 | } else { 125 | captureFormatText.setVisibility(View.GONE); 126 | captureFormatSlider.setVisibility(View.GONE); 127 | } 128 | } 129 | 130 | // TODO(sakal): Replace with onAttach(Context) once we only support API level 23+. 131 | @SuppressWarnings("deprecation") 132 | @Override 133 | public void onAttach(Activity activity) { 134 | super.onAttach(activity); 135 | callEvents = (OnCallEvents) activity; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /app/src/main/java/org/appspot/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 org.appspot.apprtc; 12 | 13 | import android.widget.SeekBar; 14 | import android.widget.TextView; 15 | import java.util.Arrays; 16 | import java.util.Collections; 17 | import java.util.Comparator; 18 | import java.util.List; 19 | import org.webrtc.CameraEnumerationAndroid.CaptureFormat; 20 | 21 | /** 22 | * Control capture format based on a seekbar listener. 23 | */ 24 | public class CaptureQualityController implements SeekBar.OnSeekBarChangeListener { 25 | private final List formats = 26 | Arrays.asList(new CaptureFormat(1280, 720, 0, 30000), new CaptureFormat(960, 540, 0, 30000), 27 | new CaptureFormat(640, 480, 0, 30000), new CaptureFormat(480, 360, 0, 30000), 28 | new CaptureFormat(320, 240, 0, 30000), new CaptureFormat(256, 144, 0, 30000)); 29 | // Prioritize framerate below this threshold and resolution above the threshold. 30 | private static final int FRAMERATE_THRESHOLD = 15; 31 | private TextView captureFormatText; 32 | private CallFragment.OnCallEvents callEvents; 33 | private int width; 34 | private int height; 35 | private int framerate; 36 | private double targetBandwidth; 37 | 38 | public CaptureQualityController( 39 | TextView captureFormatText, CallFragment.OnCallEvents callEvents) { 40 | this.captureFormatText = captureFormatText; 41 | this.callEvents = callEvents; 42 | } 43 | 44 | private final Comparator compareFormats = new Comparator() { 45 | @Override 46 | public int compare(CaptureFormat first, CaptureFormat second) { 47 | int firstFps = calculateFramerate(targetBandwidth, first); 48 | int secondFps = calculateFramerate(targetBandwidth, second); 49 | 50 | if ((firstFps >= FRAMERATE_THRESHOLD && secondFps >= FRAMERATE_THRESHOLD) 51 | || firstFps == secondFps) { 52 | // Compare resolution. 53 | return first.width * first.height - second.width * second.height; 54 | } else { 55 | // Compare fps. 56 | return firstFps - secondFps; 57 | } 58 | } 59 | }; 60 | 61 | @Override 62 | public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 63 | if (progress == 0) { 64 | width = 0; 65 | height = 0; 66 | framerate = 0; 67 | captureFormatText.setText(R.string.muted); 68 | return; 69 | } 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 | 78 | // Fraction between 0 and 1. 79 | double bandwidthFraction = (double) progress / 100.0; 80 | // Make a log-scale transformation, still between 0 and 1. 81 | final double kExpConstant = 3.0; 82 | bandwidthFraction = 83 | (Math.exp(kExpConstant * bandwidthFraction) - 1) / (Math.exp(kExpConstant) - 1); 84 | targetBandwidth = bandwidthFraction * maxCaptureBandwidth; 85 | 86 | // Choose the best format given a target bandwidth. 87 | final CaptureFormat bestFormat = Collections.max(formats, compareFormats); 88 | width = bestFormat.width; 89 | height = bestFormat.height; 90 | framerate = calculateFramerate(targetBandwidth, bestFormat); 91 | captureFormatText.setText( 92 | String.format(captureFormatText.getContext().getString(R.string.format_description), width, 93 | height, framerate)); 94 | } 95 | 96 | @Override 97 | public void onStartTrackingTouch(SeekBar seekBar) {} 98 | 99 | @Override 100 | public void onStopTrackingTouch(SeekBar seekBar) { 101 | callEvents.onCaptureFormatChange(width, height, framerate); 102 | } 103 | 104 | // Return the highest frame rate possible based on bandwidth and format. 105 | private int calculateFramerate(double bandwidth, CaptureFormat format) { 106 | return (int) Math.round( 107 | Math.min(format.framerate.max, (int) Math.round(bandwidth / (format.width * format.height))) 108 | / 1000.0); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /app/src/main/java/org/appspot/apprtc/CpuMonitor.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 org.appspot.apprtc; 12 | 13 | import android.annotation.TargetApi; 14 | import android.content.Context; 15 | import android.content.Intent; 16 | import android.content.IntentFilter; 17 | import android.os.BatteryManager; 18 | import android.os.Build; 19 | import android.os.SystemClock; 20 | import android.support.annotation.Nullable; 21 | import android.util.Log; 22 | import java.io.BufferedReader; 23 | import java.io.FileInputStream; 24 | import java.io.FileNotFoundException; 25 | import java.io.IOException; 26 | import java.io.InputStreamReader; 27 | import java.nio.charset.Charset; 28 | import java.util.Arrays; 29 | import java.util.Scanner; 30 | import java.util.concurrent.Executors; 31 | import java.util.concurrent.Future; 32 | import java.util.concurrent.ScheduledExecutorService; 33 | import java.util.concurrent.TimeUnit; 34 | 35 | /** 36 | * Simple CPU monitor. The caller creates a CpuMonitor object which can then 37 | * be used via sampleCpuUtilization() to collect the percentual use of the 38 | * cumulative CPU capacity for all CPUs running at their nominal frequency. 3 39 | * values are generated: (1) getCpuCurrent() returns the use since the last 40 | * sampleCpuUtilization(), (2) getCpuAvg3() returns the use since 3 prior 41 | * calls, and (3) getCpuAvgAll() returns the use over all SAMPLE_SAVE_NUMBER 42 | * calls. 43 | * 44 | *

CPUs in Android are often "offline", and while this of course means 0 Hz 45 | * as current frequency, in this state we cannot even get their nominal 46 | * frequency. We therefore tread carefully, and allow any CPU to be missing. 47 | * Missing CPUs are assumed to have the same nominal frequency as any close 48 | * lower-numbered CPU, but as soon as it is online, we'll get their proper 49 | * frequency and remember it. (Since CPU 0 in practice always seem to be 50 | * online, this unidirectional frequency inheritance should be no problem in 51 | * practice.) 52 | * 53 | *

Caveats: 54 | * o No provision made for zany "turbo" mode, common in the x86 world. 55 | * o No provision made for ARM big.LITTLE; if CPU n can switch behind our 56 | * back, we might get incorrect estimates. 57 | * o This is not thread-safe. To call asynchronously, create different 58 | * CpuMonitor objects. 59 | * 60 | *

If we can gather enough info to generate a sensible result, 61 | * sampleCpuUtilization returns true. It is designed to never throw an 62 | * exception. 63 | * 64 | *

sampleCpuUtilization should not be called too often in its present form, 65 | * since then deltas would be small and the percent values would fluctuate and 66 | * be unreadable. If it is desirable to call it more often than say once per 67 | * second, one would need to increase SAMPLE_SAVE_NUMBER and probably use 68 | * Queue to avoid copying overhead. 69 | * 70 | *

Known problems: 71 | * 1. Nexus 7 devices running Kitkat have a kernel which often output an 72 | * incorrect 'idle' field in /proc/stat. The value is close to twice the 73 | * correct value, and then returns to back to correct reading. Both when 74 | * jumping up and back down we might create faulty CPU load readings. 75 | */ 76 | @TargetApi(Build.VERSION_CODES.KITKAT) 77 | class CpuMonitor { 78 | private static final String TAG = "CpuMonitor"; 79 | private static final int MOVING_AVERAGE_SAMPLES = 5; 80 | 81 | private static final int CPU_STAT_SAMPLE_PERIOD_MS = 2000; 82 | private static final int CPU_STAT_LOG_PERIOD_MS = 6000; 83 | 84 | private final Context appContext; 85 | // User CPU usage at current frequency. 86 | private final MovingAverage userCpuUsage; 87 | // System CPU usage at current frequency. 88 | private final MovingAverage systemCpuUsage; 89 | // Total CPU usage relative to maximum frequency. 90 | private final MovingAverage totalCpuUsage; 91 | // CPU frequency in percentage from maximum. 92 | private final MovingAverage frequencyScale; 93 | 94 | @Nullable 95 | private ScheduledExecutorService executor; 96 | private long lastStatLogTimeMs; 97 | private long[] cpuFreqMax; 98 | private int cpusPresent; 99 | private int actualCpusPresent; 100 | private boolean initialized; 101 | private boolean cpuOveruse; 102 | private String[] maxPath; 103 | private String[] curPath; 104 | private double[] curFreqScales; 105 | @Nullable 106 | private ProcStat lastProcStat; 107 | 108 | private static class ProcStat { 109 | final long userTime; 110 | final long systemTime; 111 | final long idleTime; 112 | 113 | ProcStat(long userTime, long systemTime, long idleTime) { 114 | this.userTime = userTime; 115 | this.systemTime = systemTime; 116 | this.idleTime = idleTime; 117 | } 118 | } 119 | 120 | private static class MovingAverage { 121 | private final int size; 122 | private double sum; 123 | private double currentValue; 124 | private double[] circBuffer; 125 | private int circBufferIndex; 126 | 127 | public MovingAverage(int size) { 128 | if (size <= 0) { 129 | throw new AssertionError("Size value in MovingAverage ctor should be positive."); 130 | } 131 | this.size = size; 132 | circBuffer = new double[size]; 133 | } 134 | 135 | public void reset() { 136 | Arrays.fill(circBuffer, 0); 137 | circBufferIndex = 0; 138 | sum = 0; 139 | currentValue = 0; 140 | } 141 | 142 | public void addValue(double value) { 143 | sum -= circBuffer[circBufferIndex]; 144 | circBuffer[circBufferIndex++] = value; 145 | currentValue = value; 146 | sum += value; 147 | if (circBufferIndex >= size) { 148 | circBufferIndex = 0; 149 | } 150 | } 151 | 152 | public double getCurrent() { 153 | return currentValue; 154 | } 155 | 156 | public double getAverage() { 157 | return sum / (double) size; 158 | } 159 | } 160 | 161 | public static boolean isSupported() { 162 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT 163 | && Build.VERSION.SDK_INT < Build.VERSION_CODES.N; 164 | } 165 | 166 | public CpuMonitor(Context context) { 167 | if (!isSupported()) { 168 | throw new RuntimeException("CpuMonitor is not supported on this Android version."); 169 | } 170 | 171 | Log.d(TAG, "CpuMonitor ctor."); 172 | appContext = context.getApplicationContext(); 173 | userCpuUsage = new MovingAverage(MOVING_AVERAGE_SAMPLES); 174 | systemCpuUsage = new MovingAverage(MOVING_AVERAGE_SAMPLES); 175 | totalCpuUsage = new MovingAverage(MOVING_AVERAGE_SAMPLES); 176 | frequencyScale = new MovingAverage(MOVING_AVERAGE_SAMPLES); 177 | lastStatLogTimeMs = SystemClock.elapsedRealtime(); 178 | 179 | scheduleCpuUtilizationTask(); 180 | } 181 | 182 | public void pause() { 183 | if (executor != null) { 184 | Log.d(TAG, "pause"); 185 | executor.shutdownNow(); 186 | executor = null; 187 | } 188 | } 189 | 190 | public void resume() { 191 | Log.d(TAG, "resume"); 192 | resetStat(); 193 | scheduleCpuUtilizationTask(); 194 | } 195 | 196 | // TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression. 197 | @SuppressWarnings("NoSynchronizedMethodCheck") 198 | public synchronized void reset() { 199 | if (executor != null) { 200 | Log.d(TAG, "reset"); 201 | resetStat(); 202 | cpuOveruse = false; 203 | } 204 | } 205 | 206 | // TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression. 207 | @SuppressWarnings("NoSynchronizedMethodCheck") 208 | public synchronized int getCpuUsageCurrent() { 209 | return doubleToPercent(userCpuUsage.getCurrent() + systemCpuUsage.getCurrent()); 210 | } 211 | 212 | // TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression. 213 | @SuppressWarnings("NoSynchronizedMethodCheck") 214 | public synchronized int getCpuUsageAverage() { 215 | return doubleToPercent(userCpuUsage.getAverage() + systemCpuUsage.getAverage()); 216 | } 217 | 218 | // TODO(bugs.webrtc.org/8491): Remove NoSynchronizedMethodCheck suppression. 219 | @SuppressWarnings("NoSynchronizedMethodCheck") 220 | public synchronized int getFrequencyScaleAverage() { 221 | return doubleToPercent(frequencyScale.getAverage()); 222 | } 223 | 224 | private void scheduleCpuUtilizationTask() { 225 | if (executor != null) { 226 | executor.shutdownNow(); 227 | executor = null; 228 | } 229 | 230 | executor = Executors.newSingleThreadScheduledExecutor(); 231 | @SuppressWarnings("unused") // Prevent downstream linter warnings. 232 | Future possiblyIgnoredError = executor.scheduleAtFixedRate(new Runnable() { 233 | @Override 234 | public void run() { 235 | cpuUtilizationTask(); 236 | } 237 | }, 0, CPU_STAT_SAMPLE_PERIOD_MS, TimeUnit.MILLISECONDS); 238 | } 239 | 240 | private void cpuUtilizationTask() { 241 | boolean cpuMonitorAvailable = sampleCpuUtilization(); 242 | if (cpuMonitorAvailable 243 | && SystemClock.elapsedRealtime() - lastStatLogTimeMs >= CPU_STAT_LOG_PERIOD_MS) { 244 | lastStatLogTimeMs = SystemClock.elapsedRealtime(); 245 | String statString = getStatString(); 246 | Log.d(TAG, statString); 247 | } 248 | } 249 | 250 | private void init() { 251 | try (FileInputStream fin = new FileInputStream("/sys/devices/system/cpu/present"); 252 | InputStreamReader streamReader = new InputStreamReader(fin, Charset.forName("UTF-8")); 253 | BufferedReader reader = new BufferedReader(streamReader); 254 | Scanner scanner = new Scanner(reader).useDelimiter("[-\n]");) { 255 | scanner.nextInt(); // Skip leading number 0. 256 | cpusPresent = 1 + scanner.nextInt(); 257 | scanner.close(); 258 | } catch (FileNotFoundException e) { 259 | Log.e(TAG, "Cannot do CPU stats since /sys/devices/system/cpu/present is missing"); 260 | } catch (IOException e) { 261 | Log.e(TAG, "Error closing file"); 262 | } catch (Exception e) { 263 | Log.e(TAG, "Cannot do CPU stats due to /sys/devices/system/cpu/present parsing problem"); 264 | } 265 | 266 | cpuFreqMax = new long[cpusPresent]; 267 | maxPath = new String[cpusPresent]; 268 | curPath = new String[cpusPresent]; 269 | curFreqScales = new double[cpusPresent]; 270 | for (int i = 0; i < cpusPresent; i++) { 271 | cpuFreqMax[i] = 0; // Frequency "not yet determined". 272 | curFreqScales[i] = 0; 273 | maxPath[i] = "/sys/devices/system/cpu/cpu" + i + "/cpufreq/cpuinfo_max_freq"; 274 | curPath[i] = "/sys/devices/system/cpu/cpu" + i + "/cpufreq/scaling_cur_freq"; 275 | } 276 | 277 | lastProcStat = new ProcStat(0, 0, 0); 278 | resetStat(); 279 | 280 | initialized = true; 281 | } 282 | 283 | private synchronized void resetStat() { 284 | userCpuUsage.reset(); 285 | systemCpuUsage.reset(); 286 | totalCpuUsage.reset(); 287 | frequencyScale.reset(); 288 | lastStatLogTimeMs = SystemClock.elapsedRealtime(); 289 | } 290 | 291 | private int getBatteryLevel() { 292 | // Use sticky broadcast with null receiver to read battery level once only. 293 | Intent intent = appContext.registerReceiver( 294 | null /* receiver */, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); 295 | 296 | int batteryLevel = 0; 297 | int batteryScale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100); 298 | if (batteryScale > 0) { 299 | batteryLevel = 300 | (int) (100f * intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0) / batteryScale); 301 | } 302 | return batteryLevel; 303 | } 304 | 305 | /** 306 | * Re-measure CPU use. Call this method at an interval of around 1/s. 307 | * This method returns true on success. The fields 308 | * cpuCurrent, cpuAvg3, and cpuAvgAll are updated on success, and represents: 309 | * cpuCurrent: The CPU use since the last sampleCpuUtilization call. 310 | * cpuAvg3: The average CPU over the last 3 calls. 311 | * cpuAvgAll: The average CPU over the last SAMPLE_SAVE_NUMBER calls. 312 | */ 313 | private synchronized boolean sampleCpuUtilization() { 314 | long lastSeenMaxFreq = 0; 315 | long cpuFreqCurSum = 0; 316 | long cpuFreqMaxSum = 0; 317 | 318 | if (!initialized) { 319 | init(); 320 | } 321 | if (cpusPresent == 0) { 322 | return false; 323 | } 324 | 325 | actualCpusPresent = 0; 326 | for (int i = 0; i < cpusPresent; i++) { 327 | /* 328 | * For each CPU, attempt to first read its max frequency, then its 329 | * current frequency. Once as the max frequency for a CPU is found, 330 | * save it in cpuFreqMax[]. 331 | */ 332 | 333 | curFreqScales[i] = 0; 334 | if (cpuFreqMax[i] == 0) { 335 | // We have never found this CPU's max frequency. Attempt to read it. 336 | long cpufreqMax = readFreqFromFile(maxPath[i]); 337 | if (cpufreqMax > 0) { 338 | Log.d(TAG, "Core " + i + ". Max frequency: " + cpufreqMax); 339 | lastSeenMaxFreq = cpufreqMax; 340 | cpuFreqMax[i] = cpufreqMax; 341 | maxPath[i] = null; // Kill path to free its memory. 342 | } 343 | } else { 344 | lastSeenMaxFreq = cpuFreqMax[i]; // A valid, previously read value. 345 | } 346 | 347 | long cpuFreqCur = readFreqFromFile(curPath[i]); 348 | if (cpuFreqCur == 0 && lastSeenMaxFreq == 0) { 349 | // No current frequency information for this CPU core - ignore it. 350 | continue; 351 | } 352 | if (cpuFreqCur > 0) { 353 | actualCpusPresent++; 354 | } 355 | cpuFreqCurSum += cpuFreqCur; 356 | 357 | /* Here, lastSeenMaxFreq might come from 358 | * 1. cpuFreq[i], or 359 | * 2. a previous iteration, or 360 | * 3. a newly read value, or 361 | * 4. hypothetically from the pre-loop dummy. 362 | */ 363 | cpuFreqMaxSum += lastSeenMaxFreq; 364 | if (lastSeenMaxFreq > 0) { 365 | curFreqScales[i] = (double) cpuFreqCur / lastSeenMaxFreq; 366 | } 367 | } 368 | 369 | if (cpuFreqCurSum == 0 || cpuFreqMaxSum == 0) { 370 | Log.e(TAG, "Could not read max or current frequency for any CPU"); 371 | return false; 372 | } 373 | 374 | /* 375 | * Since the cycle counts are for the period between the last invocation 376 | * and this present one, we average the percentual CPU frequencies between 377 | * now and the beginning of the measurement period. This is significantly 378 | * incorrect only if the frequencies have peeked or dropped in between the 379 | * invocations. 380 | */ 381 | double currentFrequencyScale = cpuFreqCurSum / (double) cpuFreqMaxSum; 382 | if (frequencyScale.getCurrent() > 0) { 383 | currentFrequencyScale = (frequencyScale.getCurrent() + currentFrequencyScale) * 0.5; 384 | } 385 | 386 | ProcStat procStat = readProcStat(); 387 | if (procStat == null) { 388 | return false; 389 | } 390 | 391 | long diffUserTime = procStat.userTime - lastProcStat.userTime; 392 | long diffSystemTime = procStat.systemTime - lastProcStat.systemTime; 393 | long diffIdleTime = procStat.idleTime - lastProcStat.idleTime; 394 | long allTime = diffUserTime + diffSystemTime + diffIdleTime; 395 | 396 | if (currentFrequencyScale == 0 || allTime == 0) { 397 | return false; 398 | } 399 | 400 | // Update statistics. 401 | frequencyScale.addValue(currentFrequencyScale); 402 | 403 | double currentUserCpuUsage = diffUserTime / (double) allTime; 404 | userCpuUsage.addValue(currentUserCpuUsage); 405 | 406 | double currentSystemCpuUsage = diffSystemTime / (double) allTime; 407 | systemCpuUsage.addValue(currentSystemCpuUsage); 408 | 409 | double currentTotalCpuUsage = 410 | (currentUserCpuUsage + currentSystemCpuUsage) * currentFrequencyScale; 411 | totalCpuUsage.addValue(currentTotalCpuUsage); 412 | 413 | // Save new measurements for next round's deltas. 414 | lastProcStat = procStat; 415 | 416 | return true; 417 | } 418 | 419 | private int doubleToPercent(double d) { 420 | return (int) (d * 100 + 0.5); 421 | } 422 | 423 | private synchronized String getStatString() { 424 | StringBuilder stat = new StringBuilder(); 425 | stat.append("CPU User: ") 426 | .append(doubleToPercent(userCpuUsage.getCurrent())) 427 | .append("/") 428 | .append(doubleToPercent(userCpuUsage.getAverage())) 429 | .append(". System: ") 430 | .append(doubleToPercent(systemCpuUsage.getCurrent())) 431 | .append("/") 432 | .append(doubleToPercent(systemCpuUsage.getAverage())) 433 | .append(". Freq: ") 434 | .append(doubleToPercent(frequencyScale.getCurrent())) 435 | .append("/") 436 | .append(doubleToPercent(frequencyScale.getAverage())) 437 | .append(". Total usage: ") 438 | .append(doubleToPercent(totalCpuUsage.getCurrent())) 439 | .append("/") 440 | .append(doubleToPercent(totalCpuUsage.getAverage())) 441 | .append(". Cores: ") 442 | .append(actualCpusPresent); 443 | stat.append("( "); 444 | for (int i = 0; i < cpusPresent; i++) { 445 | stat.append(doubleToPercent(curFreqScales[i])).append(" "); 446 | } 447 | stat.append("). Battery: ").append(getBatteryLevel()); 448 | if (cpuOveruse) { 449 | stat.append(". Overuse."); 450 | } 451 | return stat.toString(); 452 | } 453 | 454 | /** 455 | * Read a single integer value from the named file. Return the read value 456 | * or if an error occurs return 0. 457 | */ 458 | private long readFreqFromFile(String fileName) { 459 | long number = 0; 460 | try (FileInputStream stream = new FileInputStream(fileName); 461 | InputStreamReader streamReader = new InputStreamReader(stream, Charset.forName("UTF-8")); 462 | BufferedReader reader = new BufferedReader(streamReader)) { 463 | String line = reader.readLine(); 464 | number = parseLong(line); 465 | } catch (FileNotFoundException e) { 466 | // CPU core is off, so file with its scaling frequency .../cpufreq/scaling_cur_freq 467 | // is not present. This is not an error. 468 | } catch (IOException e) { 469 | // CPU core is off, so file with its scaling frequency .../cpufreq/scaling_cur_freq 470 | // is empty. This is not an error. 471 | } 472 | return number; 473 | } 474 | 475 | private static long parseLong(String value) { 476 | long number = 0; 477 | try { 478 | number = Long.parseLong(value); 479 | } catch (NumberFormatException e) { 480 | Log.e(TAG, "parseLong error.", e); 481 | } 482 | return number; 483 | } 484 | 485 | /* 486 | * Read the current utilization of all CPUs using the cumulative first line 487 | * of /proc/stat. 488 | */ 489 | @SuppressWarnings("StringSplitter") 490 | private @Nullable ProcStat readProcStat() { 491 | long userTime = 0; 492 | long systemTime = 0; 493 | long idleTime = 0; 494 | try (FileInputStream stream = new FileInputStream("/proc/stat"); 495 | InputStreamReader streamReader = new InputStreamReader(stream, Charset.forName("UTF-8")); 496 | BufferedReader reader = new BufferedReader(streamReader)) { 497 | // line should contain something like this: 498 | // cpu 5093818 271838 3512830 165934119 101374 447076 272086 0 0 0 499 | // user nice system idle iowait irq softirq 500 | String line = reader.readLine(); 501 | String[] lines = line.split("\\s+"); 502 | int length = lines.length; 503 | if (length >= 5) { 504 | userTime = parseLong(lines[1]); // user 505 | userTime += parseLong(lines[2]); // nice 506 | systemTime = parseLong(lines[3]); // system 507 | idleTime = parseLong(lines[4]); // idle 508 | } 509 | if (length >= 8) { 510 | userTime += parseLong(lines[5]); // iowait 511 | systemTime += parseLong(lines[6]); // irq 512 | systemTime += parseLong(lines[7]); // softirq 513 | } 514 | } catch (FileNotFoundException e) { 515 | Log.e(TAG, "Cannot open /proc/stat for reading", e); 516 | return null; 517 | } catch (Exception e) { 518 | Log.e(TAG, "Problems parsing /proc/stat", e); 519 | return null; 520 | } 521 | return new ProcStat(userTime, systemTime, idleTime); 522 | } 523 | } 524 | -------------------------------------------------------------------------------- /app/src/main/java/org/appspot/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 org.appspot.apprtc; 12 | 13 | import android.support.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 | 37 | // Regex pattern used for checking if room id looks like an IP. 38 | static final Pattern IP_PATTERN = Pattern.compile("(" 39 | // IPv4 40 | + "((\\d+\\.){3}\\d+)|" 41 | // IPv6 42 | + "\\[((([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4})?::" 43 | + "(([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4})?)\\]|" 44 | + "\\[(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4})\\]|" 45 | // IPv6 without [] 46 | + "((([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4})?::(([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4})?)|" 47 | + "(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4})|" 48 | // Literals 49 | + "localhost" 50 | + ")" 51 | // Optional port number 52 | + "(:(\\d+))?"); 53 | 54 | private final ExecutorService executor; 55 | private final SignalingEvents events; 56 | @Nullable 57 | private TCPChannelClient tcpClient; 58 | private RoomConnectionParameters connectionParameters; 59 | 60 | private enum ConnectionState { NEW, CONNECTED, CLOSED, ERROR } 61 | 62 | // All alterations of the room state should be done from inside the looper thread. 63 | private ConnectionState roomState; 64 | 65 | public DirectRTCClient(SignalingEvents events) { 66 | this.events = events; 67 | 68 | executor = Executors.newSingleThreadExecutor(); 69 | roomState = ConnectionState.NEW; 70 | } 71 | 72 | /** 73 | * Connects to the room, roomId in connectionsParameters is required. roomId must be a valid 74 | * IP address matching IP_PATTERN. 75 | */ 76 | @Override 77 | public void connectToRoom(RoomConnectionParameters connectionParameters) { 78 | this.connectionParameters = connectionParameters; 79 | 80 | if (connectionParameters.loopback) { 81 | reportError("Loopback connections aren't supported by DirectRTCClient."); 82 | } 83 | 84 | executor.execute(new Runnable() { 85 | @Override 86 | public void run() { 87 | connectToRoomInternal(); 88 | } 89 | }); 90 | } 91 | 92 | @Override 93 | public void disconnectFromRoom() { 94 | executor.execute(new Runnable() { 95 | @Override 96 | public void run() { 97 | disconnectFromRoomInternal(); 98 | } 99 | }); 100 | } 101 | 102 | /** 103 | * Connects to the room. 104 | * 105 | * Runs on the looper thread. 106 | */ 107 | private void connectToRoomInternal() { 108 | this.roomState = ConnectionState.NEW; 109 | 110 | String endpoint = connectionParameters.roomId; 111 | 112 | Matcher matcher = IP_PATTERN.matcher(endpoint); 113 | if (!matcher.matches()) { 114 | reportError("roomId must match IP_PATTERN for DirectRTCClient."); 115 | return; 116 | } 117 | 118 | String ip = matcher.group(1); 119 | String portStr = matcher.group(matcher.groupCount()); 120 | int port; 121 | 122 | if (portStr != null) { 123 | try { 124 | port = Integer.parseInt(portStr); 125 | } catch (NumberFormatException e) { 126 | reportError("Invalid port number: " + portStr); 127 | return; 128 | } 129 | } else { 130 | port = DEFAULT_PORT; 131 | } 132 | 133 | tcpClient = new TCPChannelClient(executor, this, ip, port); 134 | } 135 | 136 | /** 137 | * Disconnects from the room. 138 | * 139 | * Runs on the looper thread. 140 | */ 141 | private void disconnectFromRoomInternal() { 142 | roomState = ConnectionState.CLOSED; 143 | 144 | if (tcpClient != null) { 145 | tcpClient.disconnect(); 146 | tcpClient = null; 147 | } 148 | executor.shutdown(); 149 | } 150 | 151 | @Override 152 | public void sendOfferSdp(final SessionDescription sdp) { 153 | executor.execute(new Runnable() { 154 | @Override 155 | public void run() { 156 | if (roomState != ConnectionState.CONNECTED) { 157 | reportError("Sending offer SDP in non connected state."); 158 | return; 159 | } 160 | JSONObject json = new JSONObject(); 161 | jsonPut(json, "sdp", sdp.description); 162 | jsonPut(json, "type", "offer"); 163 | sendMessage(json.toString()); 164 | } 165 | }); 166 | } 167 | 168 | @Override 169 | public void sendAnswerSdp(final SessionDescription sdp) { 170 | executor.execute(new Runnable() { 171 | @Override 172 | public void run() { 173 | JSONObject json = new JSONObject(); 174 | jsonPut(json, "sdp", sdp.description); 175 | jsonPut(json, "type", "answer"); 176 | sendMessage(json.toString()); 177 | } 178 | }); 179 | } 180 | 181 | @Override 182 | public void sendLocalIceCandidate(final IceCandidate candidate) { 183 | executor.execute(new Runnable() { 184 | @Override 185 | public void run() { 186 | JSONObject json = new JSONObject(); 187 | jsonPut(json, "type", "candidate"); 188 | jsonPut(json, "label", candidate.sdpMLineIndex); 189 | jsonPut(json, "id", candidate.sdpMid); 190 | jsonPut(json, "candidate", candidate.sdp); 191 | 192 | if (roomState != ConnectionState.CONNECTED) { 193 | reportError("Sending ICE candidate in non connected state."); 194 | return; 195 | } 196 | sendMessage(json.toString()); 197 | } 198 | }); 199 | } 200 | 201 | /** Send removed Ice candidates to the other participant. */ 202 | @Override 203 | public void sendLocalIceCandidateRemovals(final IceCandidate[] candidates) { 204 | executor.execute(new Runnable() { 205 | @Override 206 | public void run() { 207 | JSONObject json = new JSONObject(); 208 | jsonPut(json, "type", "remove-candidates"); 209 | JSONArray jsonArray = new JSONArray(); 210 | for (final IceCandidate candidate : candidates) { 211 | jsonArray.put(toJsonCandidate(candidate)); 212 | } 213 | jsonPut(json, "candidates", jsonArray); 214 | 215 | if (roomState != ConnectionState.CONNECTED) { 216 | reportError("Sending ICE candidate removals in non connected state."); 217 | return; 218 | } 219 | sendMessage(json.toString()); 220 | } 221 | }); 222 | } 223 | 224 | // ------------------------------------------------------------------- 225 | // TCPChannelClient event handlers 226 | 227 | /** 228 | * If the client is the server side, this will trigger onConnectedToRoom. 229 | */ 230 | @Override 231 | public void onTCPConnected(boolean isServer) { 232 | if (isServer) { 233 | roomState = ConnectionState.CONNECTED; 234 | 235 | SignalingParameters parameters = new SignalingParameters( 236 | // Ice servers are not needed for direct connections. 237 | new ArrayList<>(), 238 | isServer, // Server side acts as the initiator on direct connections. 239 | null, // clientId 240 | null, // wssUrl 241 | null, // wwsPostUrl 242 | null, // offerSdp 243 | null // iceCandidates 244 | ); 245 | events.onConnectedToRoom(parameters); 246 | } 247 | } 248 | 249 | @Override 250 | public void onTCPMessage(String msg) { 251 | try { 252 | JSONObject json = new JSONObject(msg); 253 | String type = json.optString("type"); 254 | if (type.equals("candidate")) { 255 | events.onRemoteIceCandidate(toJavaCandidate(json)); 256 | } else if (type.equals("remove-candidates")) { 257 | JSONArray candidateArray = json.getJSONArray("candidates"); 258 | IceCandidate[] candidates = new IceCandidate[candidateArray.length()]; 259 | for (int i = 0; i < candidateArray.length(); ++i) { 260 | candidates[i] = toJavaCandidate(candidateArray.getJSONObject(i)); 261 | } 262 | events.onRemoteIceCandidatesRemoved(candidates); 263 | } else if (type.equals("answer")) { 264 | SessionDescription sdp = new SessionDescription( 265 | SessionDescription.Type.fromCanonicalForm(type), json.getString("sdp")); 266 | events.onRemoteDescription(sdp); 267 | } else if (type.equals("offer")) { 268 | SessionDescription sdp = new SessionDescription( 269 | SessionDescription.Type.fromCanonicalForm(type), json.getString("sdp")); 270 | 271 | SignalingParameters parameters = new SignalingParameters( 272 | // Ice servers are not needed for direct connections. 273 | new ArrayList<>(), 274 | false, // This code will only be run on the client side. So, we are not the initiator. 275 | null, // clientId 276 | null, // wssUrl 277 | null, // wssPostUrl 278 | sdp, // offerSdp 279 | null // iceCandidates 280 | ); 281 | roomState = ConnectionState.CONNECTED; 282 | events.onConnectedToRoom(parameters); 283 | } else { 284 | reportError("Unexpected TCP message: " + msg); 285 | } 286 | } catch (JSONException e) { 287 | reportError("TCP message JSON parsing error: " + e.toString()); 288 | } 289 | } 290 | 291 | @Override 292 | public void onTCPError(String description) { 293 | reportError("TCP connection error: " + description); 294 | } 295 | 296 | @Override 297 | public void onTCPClose() { 298 | events.onChannelClose(); 299 | } 300 | 301 | // -------------------------------------------------------------------- 302 | // Helper functions. 303 | private void reportError(final String errorMessage) { 304 | Log.e(TAG, errorMessage); 305 | executor.execute(new Runnable() { 306 | @Override 307 | public void run() { 308 | if (roomState != ConnectionState.ERROR) { 309 | roomState = ConnectionState.ERROR; 310 | events.onChannelError(errorMessage); 311 | } 312 | } 313 | }); 314 | } 315 | 316 | private void sendMessage(final String message) { 317 | executor.execute(new Runnable() { 318 | @Override 319 | public void run() { 320 | tcpClient.send(message); 321 | } 322 | }); 323 | } 324 | 325 | // Put a |key|->|value| mapping in |json|. 326 | private static void jsonPut(JSONObject json, String key, Object value) { 327 | try { 328 | json.put(key, value); 329 | } catch (JSONException e) { 330 | throw new RuntimeException(e); 331 | } 332 | } 333 | 334 | // Converts a Java candidate to a JSONObject. 335 | private static JSONObject toJsonCandidate(final IceCandidate candidate) { 336 | JSONObject json = new JSONObject(); 337 | jsonPut(json, "label", candidate.sdpMLineIndex); 338 | jsonPut(json, "id", candidate.sdpMid); 339 | jsonPut(json, "candidate", candidate.sdp); 340 | return json; 341 | } 342 | 343 | // Converts a JSON candidate to a Java object. 344 | private static IceCandidate toJavaCandidate(JSONObject json) throws JSONException { 345 | return new IceCandidate( 346 | json.getString("id"), json.getInt("label"), json.getString("candidate")); 347 | } 348 | } 349 | -------------------------------------------------------------------------------- /app/src/main/java/org/appspot/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 org.appspot.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 | private 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 | 47 | // Create UI controls. 48 | encoderStatView = controlView.findViewById(R.id.encoder_stat_call); 49 | hudViewBwe = controlView.findViewById(R.id.hud_stat_bwe); 50 | hudViewConnection = controlView.findViewById(R.id.hud_stat_connection); 51 | hudViewVideoSend = controlView.findViewById(R.id.hud_stat_video_send); 52 | hudViewVideoRecv = controlView.findViewById(R.id.hud_stat_video_recv); 53 | toggleDebugButton = controlView.findViewById(R.id.button_toggle_debug); 54 | 55 | toggleDebugButton.setOnClickListener(new View.OnClickListener() { 56 | @Override 57 | public void onClick(View view) { 58 | if (displayHud) { 59 | int visibility = 60 | (hudViewBwe.getVisibility() == View.VISIBLE) ? View.INVISIBLE : View.VISIBLE; 61 | hudViewsSetProperties(visibility); 62 | } 63 | } 64 | }); 65 | 66 | return controlView; 67 | } 68 | 69 | @Override 70 | public void onStart() { 71 | super.onStart(); 72 | 73 | Bundle args = getArguments(); 74 | if (args != null) { 75 | videoCallEnabled = args.getBoolean(CallActivity.EXTRA_VIDEO_CALL, true); 76 | displayHud = args.getBoolean(CallActivity.EXTRA_DISPLAY_HUD, false); 77 | } 78 | int visibility = displayHud ? View.VISIBLE : View.INVISIBLE; 79 | encoderStatView.setVisibility(visibility); 80 | toggleDebugButton.setVisibility(visibility); 81 | hudViewsSetProperties(View.INVISIBLE); 82 | isRunning = true; 83 | } 84 | 85 | @Override 86 | public void onStop() { 87 | isRunning = false; 88 | super.onStop(); 89 | } 90 | 91 | public void setCpuMonitor(CpuMonitor cpuMonitor) { 92 | this.cpuMonitor = cpuMonitor; 93 | } 94 | 95 | private void hudViewsSetProperties(int visibility) { 96 | hudViewBwe.setVisibility(visibility); 97 | hudViewConnection.setVisibility(visibility); 98 | hudViewVideoSend.setVisibility(visibility); 99 | hudViewVideoRecv.setVisibility(visibility); 100 | hudViewBwe.setTextSize(TypedValue.COMPLEX_UNIT_PT, 5); 101 | hudViewConnection.setTextSize(TypedValue.COMPLEX_UNIT_PT, 5); 102 | hudViewVideoSend.setTextSize(TypedValue.COMPLEX_UNIT_PT, 5); 103 | hudViewVideoRecv.setTextSize(TypedValue.COMPLEX_UNIT_PT, 5); 104 | } 105 | 106 | private Map getReportMap(StatsReport report) { 107 | Map reportMap = new HashMap<>(); 108 | for (StatsReport.Value value : report.values) { 109 | reportMap.put(value.name, value.value); 110 | } 111 | return reportMap; 112 | } 113 | 114 | public void updateEncoderStatistics(final StatsReport[] reports) { 115 | if (!isRunning || !displayHud) { 116 | return; 117 | } 118 | StringBuilder encoderStat = new StringBuilder(128); 119 | StringBuilder bweStat = new StringBuilder(); 120 | StringBuilder connectionStat = new StringBuilder(); 121 | StringBuilder videoSendStat = new StringBuilder(); 122 | StringBuilder videoRecvStat = new StringBuilder(); 123 | String fps = null; 124 | String targetBitrate = null; 125 | String actualBitrate = null; 126 | 127 | for (StatsReport report : reports) { 128 | if (report.type.equals("ssrc") && report.id.contains("ssrc") && report.id.contains("send")) { 129 | // Send video statistics. 130 | Map reportMap = getReportMap(report); 131 | String trackId = reportMap.get("googTrackId"); 132 | if (trackId != null && trackId.contains(PeerConnectionClient.VIDEO_TRACK_ID)) { 133 | fps = reportMap.get("googFrameRateSent"); 134 | videoSendStat.append(report.id).append("\n"); 135 | for (StatsReport.Value value : report.values) { 136 | String name = value.name.replace("goog", ""); 137 | videoSendStat.append(name).append("=").append(value.value).append("\n"); 138 | } 139 | } 140 | } else if (report.type.equals("ssrc") && report.id.contains("ssrc") 141 | && report.id.contains("recv")) { 142 | // Receive video statistics. 143 | Map reportMap = getReportMap(report); 144 | // Check if this stat is for video track. 145 | String frameWidth = reportMap.get("googFrameWidthReceived"); 146 | if (frameWidth != null) { 147 | videoRecvStat.append(report.id).append("\n"); 148 | for (StatsReport.Value value : report.values) { 149 | String name = value.name.replace("goog", ""); 150 | videoRecvStat.append(name).append("=").append(value.value).append("\n"); 151 | } 152 | } 153 | } else if (report.id.equals("bweforvideo")) { 154 | // BWE statistics. 155 | Map reportMap = getReportMap(report); 156 | targetBitrate = reportMap.get("googTargetEncBitrate"); 157 | actualBitrate = reportMap.get("googActualEncBitrate"); 158 | 159 | bweStat.append(report.id).append("\n"); 160 | for (StatsReport.Value value : report.values) { 161 | String name = value.name.replace("goog", "").replace("Available", ""); 162 | bweStat.append(name).append("=").append(value.value).append("\n"); 163 | } 164 | } else if (report.type.equals("googCandidatePair")) { 165 | // Connection statistics. 166 | Map reportMap = getReportMap(report); 167 | String activeConnection = reportMap.get("googActiveConnection"); 168 | if (activeConnection != null && activeConnection.equals("true")) { 169 | connectionStat.append(report.id).append("\n"); 170 | for (StatsReport.Value value : report.values) { 171 | String name = value.name.replace("goog", ""); 172 | connectionStat.append(name).append("=").append(value.value).append("\n"); 173 | } 174 | } 175 | } 176 | } 177 | hudViewBwe.setText(bweStat.toString()); 178 | hudViewConnection.setText(connectionStat.toString()); 179 | hudViewVideoSend.setText(videoSendStat.toString()); 180 | hudViewVideoRecv.setText(videoRecvStat.toString()); 181 | 182 | if (videoCallEnabled) { 183 | if (fps != null) { 184 | encoderStat.append("Fps: ").append(fps).append("\n"); 185 | } 186 | if (targetBitrate != null) { 187 | encoderStat.append("Target BR: ").append(targetBitrate).append("\n"); 188 | } 189 | if (actualBitrate != null) { 190 | encoderStat.append("Actual BR: ").append(actualBitrate).append("\n"); 191 | } 192 | } 193 | 194 | if (cpuMonitor != null) { 195 | encoderStat.append("CPU%: ") 196 | .append(cpuMonitor.getCpuUsageCurrent()) 197 | .append("/") 198 | .append(cpuMonitor.getCpuUsageAverage()) 199 | .append(". Freq: ") 200 | .append(cpuMonitor.getFrequencyScaleAverage()); 201 | } 202 | encoderStatView.setText(encoderStat.toString()); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /app/src/main/java/org/appspot/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 org.appspot.apprtc; 12 | 13 | import android.media.AudioFormat; 14 | import android.os.Environment; 15 | import android.support.annotation.Nullable; 16 | import android.util.Log; 17 | import java.io.File; 18 | import java.io.FileNotFoundException; 19 | import java.io.FileOutputStream; 20 | import java.io.IOException; 21 | import java.io.OutputStream; 22 | import java.util.concurrent.ExecutorService; 23 | import org.webrtc.audio.JavaAudioDeviceModule; 24 | import org.webrtc.audio.JavaAudioDeviceModule.SamplesReadyCallback; 25 | 26 | /** 27 | * Implements the AudioRecordSamplesReadyCallback interface and writes 28 | * recorded raw audio samples to an output file. 29 | */ 30 | public class RecordedAudioToFileController implements SamplesReadyCallback { 31 | private static final String TAG = "RecordedAudioToFile"; 32 | private static final long MAX_FILE_SIZE_IN_BYTES = 58348800L; 33 | 34 | private final Object lock = new Object(); 35 | private final ExecutorService executor; 36 | @Nullable private OutputStream rawAudioFileOutputStream; 37 | private boolean isRunning; 38 | private long fileSizeInBytes; 39 | 40 | public RecordedAudioToFileController(ExecutorService executor) { 41 | Log.d(TAG, "ctor"); 42 | this.executor = executor; 43 | } 44 | 45 | /** 46 | * Should be called on the same executor thread as the one provided at 47 | * construction. 48 | */ 49 | public boolean start() { 50 | Log.d(TAG, "start"); 51 | if (!isExternalStorageWritable()) { 52 | Log.e(TAG, "Writing to external media is not possible"); 53 | return false; 54 | } 55 | synchronized (lock) { 56 | isRunning = true; 57 | } 58 | return true; 59 | } 60 | 61 | /** 62 | * Should be called on the same executor thread as the one provided at 63 | * construction. 64 | */ 65 | public void stop() { 66 | Log.d(TAG, "stop"); 67 | synchronized (lock) { 68 | isRunning = false; 69 | if (rawAudioFileOutputStream != null) { 70 | try { 71 | rawAudioFileOutputStream.close(); 72 | } catch (IOException e) { 73 | Log.e(TAG, "Failed to close file with saved input audio: " + e); 74 | } 75 | rawAudioFileOutputStream = null; 76 | } 77 | fileSizeInBytes = 0; 78 | } 79 | } 80 | 81 | // Checks if external storage is available for read and write. 82 | private boolean isExternalStorageWritable() { 83 | String state = Environment.getExternalStorageState(); 84 | if (Environment.MEDIA_MOUNTED.equals(state)) { 85 | return true; 86 | } 87 | return false; 88 | } 89 | 90 | // Utilizes audio parameters to create a file name which contains sufficient 91 | // information so that the file can be played using an external file player. 92 | // Example: /sdcard/recorded_audio_16bits_48000Hz_mono.pcm. 93 | private void openRawAudioOutputFile(int sampleRate, int channelCount) { 94 | final String fileName = Environment.getExternalStorageDirectory().getPath() + File.separator 95 | + "recorded_audio_16bits_" + String.valueOf(sampleRate) + "Hz" 96 | + ((channelCount == 1) ? "_mono" : "_stereo") + ".pcm"; 97 | final File outputFile = new File(fileName); 98 | try { 99 | rawAudioFileOutputStream = new FileOutputStream(outputFile); 100 | } catch (FileNotFoundException e) { 101 | Log.e(TAG, "Failed to open audio output file: " + e.getMessage()); 102 | } 103 | Log.d(TAG, "Opened file for recording: " + fileName); 104 | } 105 | 106 | // Called when new audio samples are ready. 107 | @Override 108 | public void onWebRtcAudioRecordSamplesReady(JavaAudioDeviceModule.AudioSamples samples) { 109 | // The native audio layer on Android should use 16-bit PCM format. 110 | if (samples.getAudioFormat() != AudioFormat.ENCODING_PCM_16BIT) { 111 | Log.e(TAG, "Invalid audio format"); 112 | return; 113 | } 114 | synchronized (lock) { 115 | // Abort early if stop() has been called. 116 | if (!isRunning) { 117 | return; 118 | } 119 | // Open a new file for the first callback only since it allows us to add audio parameters to 120 | // the file name. 121 | if (rawAudioFileOutputStream == null) { 122 | openRawAudioOutputFile(samples.getSampleRate(), samples.getChannelCount()); 123 | fileSizeInBytes = 0; 124 | } 125 | } 126 | // Append the recorded 16-bit audio samples to the open output file. 127 | executor.execute(() -> { 128 | if (rawAudioFileOutputStream != null) { 129 | try { 130 | // Set a limit on max file size. 58348800 bytes corresponds to 131 | // approximately 10 minutes of recording in mono at 48kHz. 132 | if (fileSizeInBytes < MAX_FILE_SIZE_IN_BYTES) { 133 | // Writes samples.getData().length bytes to output stream. 134 | rawAudioFileOutputStream.write(samples.getData()); 135 | fileSizeInBytes += samples.getData().length; 136 | } 137 | } catch (IOException e) { 138 | Log.e(TAG, "Failed to write audio to file: " + e.getMessage()); 139 | } 140 | } 141 | }); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /app/src/main/java/org/appspot/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 org.appspot.apprtc; 12 | 13 | import android.util.Log; 14 | import java.io.IOException; 15 | import java.io.InputStream; 16 | import java.net.HttpURLConnection; 17 | import java.net.URL; 18 | import java.util.ArrayList; 19 | import java.util.Scanner; 20 | import java.util.List; 21 | import org.appspot.apprtc.AppRTCClient.SignalingParameters; 22 | import org.appspot.apprtc.util.AsyncHttpURLConnection; 23 | import org.appspot.apprtc.util.AsyncHttpURLConnection.AsyncHttpEvents; 24 | import org.json.JSONArray; 25 | import org.json.JSONException; 26 | import org.json.JSONObject; 27 | import org.webrtc.IceCandidate; 28 | import org.webrtc.PeerConnection; 29 | import org.webrtc.SessionDescription; 30 | 31 | /** 32 | * AsyncTask that converts an AppRTC room URL into the set of signaling 33 | * parameters to use with that room. 34 | */ 35 | public class RoomParametersFetcher { 36 | private static final String TAG = "RoomRTCClient"; 37 | private static final int TURN_HTTP_TIMEOUT_MS = 5000; 38 | private final RoomParametersFetcherEvents events; 39 | private final String roomUrl; 40 | private final String roomMessage; 41 | 42 | /** 43 | * Room parameters fetcher callbacks. 44 | */ 45 | public interface RoomParametersFetcherEvents { 46 | /** 47 | * Callback fired once the room's signaling parameters 48 | * SignalingParameters are extracted. 49 | */ 50 | void onSignalingParametersReady(final SignalingParameters params); 51 | 52 | /** 53 | * Callback for room parameters extraction error. 54 | */ 55 | void onSignalingParametersError(final String description); 56 | } 57 | 58 | public RoomParametersFetcher( 59 | String roomUrl, String roomMessage, final RoomParametersFetcherEvents events) { 60 | this.roomUrl = roomUrl; 61 | this.roomMessage = roomMessage; 62 | this.events = events; 63 | } 64 | 65 | public void makeRequest() { 66 | Log.d(TAG, "Connecting to room: " + roomUrl); 67 | AsyncHttpURLConnection httpConnection = 68 | new AsyncHttpURLConnection("POST", roomUrl, roomMessage, new AsyncHttpEvents() { 69 | @Override 70 | public void onHttpError(String errorMessage) { 71 | Log.e(TAG, "Room connection error: " + errorMessage); 72 | events.onSignalingParametersError(errorMessage); 73 | } 74 | 75 | @Override 76 | public void onHttpComplete(String response) { 77 | roomHttpResponseParse(response); 78 | } 79 | }); 80 | httpConnection.send(); 81 | } 82 | 83 | private void roomHttpResponseParse(String response) { 84 | Log.d(TAG, "Room response: " + response); 85 | try { 86 | List iceCandidates = null; 87 | SessionDescription offerSdp = null; 88 | JSONObject roomJson = new JSONObject(response); 89 | 90 | String result = roomJson.getString("result"); 91 | if (!result.equals("SUCCESS")) { 92 | events.onSignalingParametersError("Room response error: " + result); 93 | return; 94 | } 95 | response = roomJson.getString("params"); 96 | roomJson = new JSONObject(response); 97 | String roomId = roomJson.getString("room_id"); 98 | String clientId = roomJson.getString("client_id"); 99 | String wssUrl = roomJson.getString("wss_url"); 100 | String wssPostUrl = roomJson.getString("wss_post_url"); 101 | boolean initiator = (roomJson.getBoolean("is_initiator")); 102 | if (!initiator) { 103 | iceCandidates = new ArrayList<>(); 104 | String messagesString = roomJson.getString("messages"); 105 | JSONArray messages = new JSONArray(messagesString); 106 | for (int i = 0; i < messages.length(); ++i) { 107 | String messageString = messages.getString(i); 108 | JSONObject message = new JSONObject(messageString); 109 | String messageType = message.getString("type"); 110 | Log.d(TAG, "GAE->C #" + i + " : " + messageString); 111 | if (messageType.equals("offer")) { 112 | offerSdp = new SessionDescription( 113 | SessionDescription.Type.fromCanonicalForm(messageType), message.getString("sdp")); 114 | } else if (messageType.equals("candidate")) { 115 | IceCandidate candidate = new IceCandidate( 116 | message.getString("id"), message.getInt("label"), message.getString("candidate")); 117 | iceCandidates.add(candidate); 118 | } else { 119 | Log.e(TAG, "Unknown message: " + messageString); 120 | } 121 | } 122 | } 123 | Log.d(TAG, "RoomId: " + roomId + ". ClientId: " + clientId); 124 | Log.d(TAG, "Initiator: " + initiator); 125 | Log.d(TAG, "WSS url: " + wssUrl); 126 | Log.d(TAG, "WSS POST url: " + wssPostUrl); 127 | 128 | List iceServers = 129 | iceServersFromPCConfigJSON(roomJson.getString("pc_config")); 130 | boolean isTurnPresent = false; 131 | for (PeerConnection.IceServer server : iceServers) { 132 | Log.d(TAG, "IceServer: " + server); 133 | for (String uri : server.urls) { 134 | if (uri.startsWith("turn:")) { 135 | isTurnPresent = true; 136 | break; 137 | } 138 | } 139 | } 140 | // Request TURN servers. 141 | if (!isTurnPresent && !roomJson.optString("ice_server_url").isEmpty()) { 142 | List turnServers = 143 | requestTurnServers(roomJson.getString("ice_server_url")); 144 | for (PeerConnection.IceServer turnServer : turnServers) { 145 | Log.d(TAG, "TurnServer: " + turnServer); 146 | iceServers.add(turnServer); 147 | } 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/java/org/appspot/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 org.appspot.apprtc; 12 | 13 | import android.os.ParcelFileDescriptor; 14 | import android.util.Log; 15 | import java.io.File; 16 | import java.io.IOException; 17 | import org.webrtc.PeerConnection; 18 | 19 | public class RtcEventLog { 20 | private static final String TAG = "RtcEventLog"; 21 | private static final int OUTPUT_FILE_MAX_BYTES = 10_000_000; 22 | private final PeerConnection peerConnection; 23 | private RtcEventLogState state = RtcEventLogState.INACTIVE; 24 | 25 | enum RtcEventLogState { 26 | INACTIVE, 27 | STARTED, 28 | STOPPED, 29 | } 30 | 31 | public RtcEventLog(PeerConnection peerConnection) { 32 | if (peerConnection == null) { 33 | throw new NullPointerException("The peer connection is null."); 34 | } 35 | this.peerConnection = peerConnection; 36 | } 37 | 38 | public void start(final File outputFile) { 39 | if (state == RtcEventLogState.STARTED) { 40 | Log.e(TAG, "RtcEventLog has already started."); 41 | return; 42 | } 43 | final ParcelFileDescriptor fileDescriptor; 44 | try { 45 | fileDescriptor = ParcelFileDescriptor.open(outputFile, 46 | ParcelFileDescriptor.MODE_READ_WRITE | ParcelFileDescriptor.MODE_CREATE 47 | | ParcelFileDescriptor.MODE_TRUNCATE); 48 | } catch (IOException e) { 49 | Log.e(TAG, "Failed to create a new file", e); 50 | return; 51 | } 52 | 53 | // Passes ownership of the file to WebRTC. 54 | boolean success = 55 | peerConnection.startRtcEventLog(fileDescriptor.detachFd(), OUTPUT_FILE_MAX_BYTES); 56 | if (!success) { 57 | Log.e(TAG, "Failed to start RTC event log."); 58 | return; 59 | } 60 | state = RtcEventLogState.STARTED; 61 | Log.d(TAG, "RtcEventLog started."); 62 | } 63 | 64 | public void stop() { 65 | if (state != RtcEventLogState.STARTED) { 66 | Log.e(TAG, "RtcEventLog was not started."); 67 | return; 68 | } 69 | peerConnection.stopRtcEventLog(); 70 | state = RtcEventLogState.STOPPED; 71 | Log.d(TAG, "RtcEventLog stopped."); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/src/main/java/org/appspot/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 org.appspot.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 | import org.webrtc.Camera2Enumerator; 20 | import org.webrtc.audio.JavaAudioDeviceModule; 21 | 22 | /** 23 | * Settings activity for AppRTC. 24 | */ 25 | public class SettingsActivity extends Activity implements OnSharedPreferenceChangeListener { 26 | private SettingsFragment settingsFragment; 27 | private String keyprefVideoCall; 28 | private String keyprefScreencapture; 29 | private String keyprefCamera2; 30 | private String keyprefResolution; 31 | private String keyprefFps; 32 | private String keyprefCaptureQualitySlider; 33 | private String keyprefMaxVideoBitrateType; 34 | private String keyprefMaxVideoBitrateValue; 35 | private String keyPrefVideoCodec; 36 | private String keyprefHwCodec; 37 | private String keyprefCaptureToTexture; 38 | private String keyprefFlexfec; 39 | 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 | 53 | private String keyPrefRoomServerUrl; 54 | private String keyPrefDisplayHud; 55 | private String keyPrefTracing; 56 | private String keyprefEnabledRtcEventLog; 57 | 58 | private String keyprefEnableDataChannel; 59 | private String keyprefOrdered; 60 | private String keyprefMaxRetransmitTimeMs; 61 | private String keyprefMaxRetransmits; 62 | private String keyprefDataProtocol; 63 | private String keyprefNegotiated; 64 | private String keyprefDataId; 65 | 66 | @Override 67 | protected void onCreate(Bundle savedInstanceState) { 68 | super.onCreate(savedInstanceState); 69 | keyprefVideoCall = getString(R.string.pref_videocall_key); 70 | keyprefScreencapture = getString(R.string.pref_screencapture_key); 71 | keyprefCamera2 = getString(R.string.pref_camera2_key); 72 | keyprefResolution = getString(R.string.pref_resolution_key); 73 | keyprefFps = getString(R.string.pref_fps_key); 74 | keyprefCaptureQualitySlider = getString(R.string.pref_capturequalityslider_key); 75 | keyprefMaxVideoBitrateType = getString(R.string.pref_maxvideobitrate_key); 76 | keyprefMaxVideoBitrateValue = getString(R.string.pref_maxvideobitratevalue_key); 77 | keyPrefVideoCodec = getString(R.string.pref_videocodec_key); 78 | keyprefHwCodec = getString(R.string.pref_hwcodec_key); 79 | keyprefCaptureToTexture = getString(R.string.pref_capturetotexture_key); 80 | keyprefFlexfec = getString(R.string.pref_flexfec_key); 81 | 82 | keyprefStartAudioBitrateType = getString(R.string.pref_startaudiobitrate_key); 83 | keyprefStartAudioBitrateValue = getString(R.string.pref_startaudiobitratevalue_key); 84 | keyPrefAudioCodec = getString(R.string.pref_audiocodec_key); 85 | keyprefNoAudioProcessing = getString(R.string.pref_noaudioprocessing_key); 86 | keyprefAecDump = getString(R.string.pref_aecdump_key); 87 | keyprefEnableSaveInputAudioToFile = 88 | getString(R.string.pref_enable_save_input_audio_to_file_key); 89 | keyprefOpenSLES = getString(R.string.pref_opensles_key); 90 | keyprefDisableBuiltInAEC = getString(R.string.pref_disable_built_in_aec_key); 91 | keyprefDisableBuiltInAGC = getString(R.string.pref_disable_built_in_agc_key); 92 | keyprefDisableBuiltInNS = getString(R.string.pref_disable_built_in_ns_key); 93 | keyprefDisableWebRtcAGCAndHPF = getString(R.string.pref_disable_webrtc_agc_and_hpf_key); 94 | keyprefSpeakerphone = getString(R.string.pref_speakerphone_key); 95 | 96 | keyprefEnableDataChannel = getString(R.string.pref_enable_datachannel_key); 97 | keyprefOrdered = getString(R.string.pref_ordered_key); 98 | keyprefMaxRetransmitTimeMs = getString(R.string.pref_max_retransmit_time_ms_key); 99 | keyprefMaxRetransmits = getString(R.string.pref_max_retransmits_key); 100 | keyprefDataProtocol = getString(R.string.pref_data_protocol_key); 101 | keyprefNegotiated = getString(R.string.pref_negotiated_key); 102 | keyprefDataId = getString(R.string.pref_data_id_key); 103 | 104 | keyPrefRoomServerUrl = getString(R.string.pref_room_server_url_key); 105 | keyPrefDisplayHud = getString(R.string.pref_displayhud_key); 106 | keyPrefTracing = getString(R.string.pref_tracing_key); 107 | keyprefEnabledRtcEventLog = getString(R.string.pref_enable_rtceventlog_key); 108 | 109 | // Display the fragment as the main content. 110 | settingsFragment = new SettingsFragment(); 111 | getFragmentManager() 112 | .beginTransaction() 113 | .replace(android.R.id.content, settingsFragment) 114 | .commit(); 115 | } 116 | 117 | @Override 118 | protected void onResume() { 119 | super.onResume(); 120 | // Set summary to be the user-description for the selected value 121 | SharedPreferences sharedPreferences = 122 | settingsFragment.getPreferenceScreen().getSharedPreferences(); 123 | sharedPreferences.registerOnSharedPreferenceChangeListener(this); 124 | updateSummaryB(sharedPreferences, keyprefVideoCall); 125 | updateSummaryB(sharedPreferences, keyprefScreencapture); 126 | updateSummaryB(sharedPreferences, keyprefCamera2); 127 | updateSummary(sharedPreferences, keyprefResolution); 128 | updateSummary(sharedPreferences, keyprefFps); 129 | updateSummaryB(sharedPreferences, keyprefCaptureQualitySlider); 130 | updateSummary(sharedPreferences, keyprefMaxVideoBitrateType); 131 | updateSummaryBitrate(sharedPreferences, keyprefMaxVideoBitrateValue); 132 | setVideoBitrateEnable(sharedPreferences); 133 | updateSummary(sharedPreferences, keyPrefVideoCodec); 134 | updateSummaryB(sharedPreferences, keyprefHwCodec); 135 | updateSummaryB(sharedPreferences, keyprefCaptureToTexture); 136 | updateSummaryB(sharedPreferences, keyprefFlexfec); 137 | 138 | updateSummary(sharedPreferences, keyprefStartAudioBitrateType); 139 | updateSummaryBitrate(sharedPreferences, keyprefStartAudioBitrateValue); 140 | setAudioBitrateEnable(sharedPreferences); 141 | updateSummary(sharedPreferences, keyPrefAudioCodec); 142 | updateSummaryB(sharedPreferences, keyprefNoAudioProcessing); 143 | updateSummaryB(sharedPreferences, keyprefAecDump); 144 | updateSummaryB(sharedPreferences, keyprefEnableSaveInputAudioToFile); 145 | updateSummaryB(sharedPreferences, keyprefOpenSLES); 146 | updateSummaryB(sharedPreferences, keyprefDisableBuiltInAEC); 147 | updateSummaryB(sharedPreferences, keyprefDisableBuiltInAGC); 148 | updateSummaryB(sharedPreferences, keyprefDisableBuiltInNS); 149 | updateSummaryB(sharedPreferences, keyprefDisableWebRtcAGCAndHPF); 150 | updateSummaryList(sharedPreferences, keyprefSpeakerphone); 151 | 152 | updateSummaryB(sharedPreferences, keyprefEnableDataChannel); 153 | updateSummaryB(sharedPreferences, keyprefOrdered); 154 | updateSummary(sharedPreferences, keyprefMaxRetransmitTimeMs); 155 | updateSummary(sharedPreferences, keyprefMaxRetransmits); 156 | updateSummary(sharedPreferences, keyprefDataProtocol); 157 | updateSummaryB(sharedPreferences, keyprefNegotiated); 158 | updateSummary(sharedPreferences, keyprefDataId); 159 | setDataChannelEnable(sharedPreferences); 160 | 161 | updateSummary(sharedPreferences, keyPrefRoomServerUrl); 162 | updateSummaryB(sharedPreferences, keyPrefDisplayHud); 163 | updateSummaryB(sharedPreferences, keyPrefTracing); 164 | updateSummaryB(sharedPreferences, keyprefEnabledRtcEventLog); 165 | 166 | if (!Camera2Enumerator.isSupported(this)) { 167 | Preference camera2Preference = settingsFragment.findPreference(keyprefCamera2); 168 | 169 | camera2Preference.setSummary(getString(R.string.pref_camera2_not_supported)); 170 | camera2Preference.setEnabled(false); 171 | } 172 | 173 | if (!JavaAudioDeviceModule.isBuiltInAcousticEchoCancelerSupported()) { 174 | Preference disableBuiltInAECPreference = 175 | settingsFragment.findPreference(keyprefDisableBuiltInAEC); 176 | 177 | disableBuiltInAECPreference.setSummary(getString(R.string.pref_built_in_aec_not_available)); 178 | disableBuiltInAECPreference.setEnabled(false); 179 | } 180 | 181 | Preference disableBuiltInAGCPreference = 182 | settingsFragment.findPreference(keyprefDisableBuiltInAGC); 183 | 184 | disableBuiltInAGCPreference.setSummary(getString(R.string.pref_built_in_agc_not_available)); 185 | disableBuiltInAGCPreference.setEnabled(false); 186 | 187 | if (!JavaAudioDeviceModule.isBuiltInNoiseSuppressorSupported()) { 188 | Preference disableBuiltInNSPreference = 189 | settingsFragment.findPreference(keyprefDisableBuiltInNS); 190 | 191 | disableBuiltInNSPreference.setSummary(getString(R.string.pref_built_in_ns_not_available)); 192 | disableBuiltInNSPreference.setEnabled(false); 193 | } 194 | } 195 | 196 | @Override 197 | protected void onPause() { 198 | super.onPause(); 199 | SharedPreferences sharedPreferences = 200 | settingsFragment.getPreferenceScreen().getSharedPreferences(); 201 | sharedPreferences.unregisterOnSharedPreferenceChangeListener(this); 202 | } 203 | 204 | @Override 205 | public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { 206 | // clang-format off 207 | if (key.equals(keyprefResolution) 208 | || key.equals(keyprefFps) 209 | || key.equals(keyprefMaxVideoBitrateType) 210 | || key.equals(keyPrefVideoCodec) 211 | || key.equals(keyprefStartAudioBitrateType) 212 | || key.equals(keyPrefAudioCodec) 213 | || key.equals(keyPrefRoomServerUrl) 214 | || key.equals(keyprefMaxRetransmitTimeMs) 215 | || key.equals(keyprefMaxRetransmits) 216 | || key.equals(keyprefDataProtocol) 217 | || key.equals(keyprefDataId)) { 218 | updateSummary(sharedPreferences, key); 219 | } else if (key.equals(keyprefMaxVideoBitrateValue) 220 | || key.equals(keyprefStartAudioBitrateValue)) { 221 | updateSummaryBitrate(sharedPreferences, key); 222 | } else if (key.equals(keyprefVideoCall) 223 | || key.equals(keyprefScreencapture) 224 | || key.equals(keyprefCamera2) 225 | || key.equals(keyPrefTracing) 226 | || key.equals(keyprefCaptureQualitySlider) 227 | || key.equals(keyprefHwCodec) 228 | || key.equals(keyprefCaptureToTexture) 229 | || key.equals(keyprefFlexfec) 230 | || key.equals(keyprefNoAudioProcessing) 231 | || key.equals(keyprefAecDump) 232 | || key.equals(keyprefEnableSaveInputAudioToFile) 233 | || key.equals(keyprefOpenSLES) 234 | || key.equals(keyprefDisableBuiltInAEC) 235 | || key.equals(keyprefDisableBuiltInAGC) 236 | || key.equals(keyprefDisableBuiltInNS) 237 | || key.equals(keyprefDisableWebRtcAGCAndHPF) 238 | || key.equals(keyPrefDisplayHud) 239 | || key.equals(keyprefEnableDataChannel) 240 | || key.equals(keyprefOrdered) 241 | || key.equals(keyprefNegotiated) 242 | || key.equals(keyprefEnabledRtcEventLog)) { 243 | updateSummaryB(sharedPreferences, key); 244 | } else if (key.equals(keyprefSpeakerphone)) { 245 | updateSummaryList(sharedPreferences, key); 246 | } 247 | // clang-format on 248 | if (key.equals(keyprefMaxVideoBitrateType)) { 249 | setVideoBitrateEnable(sharedPreferences); 250 | } 251 | if (key.equals(keyprefStartAudioBitrateType)) { 252 | setAudioBitrateEnable(sharedPreferences); 253 | } 254 | if (key.equals(keyprefEnableDataChannel)) { 255 | setDataChannelEnable(sharedPreferences); 256 | } 257 | } 258 | 259 | private void updateSummary(SharedPreferences sharedPreferences, String key) { 260 | Preference updatedPref = settingsFragment.findPreference(key); 261 | // Set summary to be the user-description for the selected value 262 | updatedPref.setSummary(sharedPreferences.getString(key, "")); 263 | } 264 | 265 | private void updateSummaryBitrate(SharedPreferences sharedPreferences, String key) { 266 | Preference updatedPref = settingsFragment.findPreference(key); 267 | updatedPref.setSummary(sharedPreferences.getString(key, "") + " kbps"); 268 | } 269 | 270 | private void updateSummaryB(SharedPreferences sharedPreferences, String key) { 271 | Preference updatedPref = settingsFragment.findPreference(key); 272 | updatedPref.setSummary(sharedPreferences.getBoolean(key, true) 273 | ? getString(R.string.pref_value_enabled) 274 | : getString(R.string.pref_value_disabled)); 275 | } 276 | 277 | private void updateSummaryList(SharedPreferences sharedPreferences, String key) { 278 | ListPreference updatedPref = (ListPreference) settingsFragment.findPreference(key); 279 | updatedPref.setSummary(updatedPref.getEntry()); 280 | } 281 | 282 | private void setVideoBitrateEnable(SharedPreferences sharedPreferences) { 283 | Preference bitratePreferenceValue = 284 | settingsFragment.findPreference(keyprefMaxVideoBitrateValue); 285 | String bitrateTypeDefault = getString(R.string.pref_maxvideobitrate_default); 286 | String bitrateType = 287 | sharedPreferences.getString(keyprefMaxVideoBitrateType, bitrateTypeDefault); 288 | if (bitrateType.equals(bitrateTypeDefault)) { 289 | bitratePreferenceValue.setEnabled(false); 290 | } else { 291 | bitratePreferenceValue.setEnabled(true); 292 | } 293 | } 294 | 295 | private void setAudioBitrateEnable(SharedPreferences sharedPreferences) { 296 | Preference bitratePreferenceValue = 297 | settingsFragment.findPreference(keyprefStartAudioBitrateValue); 298 | String bitrateTypeDefault = getString(R.string.pref_startaudiobitrate_default); 299 | String bitrateType = 300 | sharedPreferences.getString(keyprefStartAudioBitrateType, bitrateTypeDefault); 301 | if (bitrateType.equals(bitrateTypeDefault)) { 302 | bitratePreferenceValue.setEnabled(false); 303 | } else { 304 | bitratePreferenceValue.setEnabled(true); 305 | } 306 | } 307 | 308 | private void setDataChannelEnable(SharedPreferences sharedPreferences) { 309 | boolean enabled = sharedPreferences.getBoolean(keyprefEnableDataChannel, true); 310 | settingsFragment.findPreference(keyprefOrdered).setEnabled(enabled); 311 | settingsFragment.findPreference(keyprefMaxRetransmitTimeMs).setEnabled(enabled); 312 | settingsFragment.findPreference(keyprefMaxRetransmits).setEnabled(enabled); 313 | settingsFragment.findPreference(keyprefDataProtocol).setEnabled(enabled); 314 | settingsFragment.findPreference(keyprefNegotiated).setEnabled(enabled); 315 | settingsFragment.findPreference(keyprefDataId).setEnabled(enabled); 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /app/src/main/java/org/appspot/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 org.appspot.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/java/org/appspot/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 org.appspot.apprtc; 12 | 13 | import android.support.annotation.Nullable; 14 | import android.util.Log; 15 | import java.io.BufferedReader; 16 | import java.io.IOException; 17 | import java.io.InputStreamReader; 18 | import java.io.OutputStreamWriter; 19 | import java.io.PrintWriter; 20 | import java.net.InetAddress; 21 | import java.net.ServerSocket; 22 | import java.net.Socket; 23 | import java.net.UnknownHostException; 24 | import java.nio.charset.Charset; 25 | import java.util.concurrent.ExecutorService; 26 | import org.webrtc.ThreadUtils; 27 | 28 | /** 29 | * Replacement for WebSocketChannelClient for direct communication between two IP addresses. Handles 30 | * the signaling between the two clients using a TCP connection. 31 | *

32 | * All public methods should be called from a looper executor thread 33 | * passed in a constructor, otherwise exception will be thrown. 34 | * All events are dispatched on the same thread. 35 | */ 36 | public class TCPChannelClient { 37 | private static final String TAG = "TCPChannelClient"; 38 | 39 | private final ExecutorService executor; 40 | private final ThreadUtils.ThreadChecker executorThreadCheck; 41 | private final TCPChannelEvents eventListener; 42 | private TCPSocket socket; 43 | 44 | /** 45 | * Callback interface for messages delivered on TCP Connection. All callbacks are invoked from the 46 | * looper executor thread. 47 | */ 48 | public interface TCPChannelEvents { 49 | void onTCPConnected(boolean server); 50 | void onTCPMessage(String message); 51 | void onTCPError(String description); 52 | void onTCPClose(); 53 | } 54 | 55 | /** 56 | * Initializes the TCPChannelClient. If IP is a local IP address, starts a listening server on 57 | * that IP. If not, instead connects to the IP. 58 | * 59 | * @param eventListener Listener that will receive events from the client. 60 | * @param ip IP address to listen on or connect to. 61 | * @param port Port to listen on or connect to. 62 | */ 63 | public TCPChannelClient( 64 | ExecutorService executor, TCPChannelEvents eventListener, String ip, int port) { 65 | this.executor = executor; 66 | executorThreadCheck = new ThreadUtils.ThreadChecker(); 67 | executorThreadCheck.detachThread(); 68 | this.eventListener = eventListener; 69 | 70 | InetAddress address; 71 | try { 72 | address = InetAddress.getByName(ip); 73 | } catch (UnknownHostException e) { 74 | reportError("Invalid IP address."); 75 | return; 76 | } 77 | 78 | if (address.isAnyLocalAddress()) { 79 | socket = new TCPSocketServer(address, port); 80 | } else { 81 | socket = new TCPSocketClient(address, port); 82 | } 83 | 84 | socket.start(); 85 | } 86 | 87 | /** 88 | * Disconnects the client if not already disconnected. This will fire the onTCPClose event. 89 | */ 90 | public void disconnect() { 91 | executorThreadCheck.checkIsOnValidThread(); 92 | 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 | 104 | socket.send(message); 105 | } 106 | 107 | /** 108 | * Helper method for firing onTCPError events. Calls onTCPError on the executor thread. 109 | */ 110 | private void reportError(final String message) { 111 | Log.e(TAG, "TCP Error: " + message); 112 | executor.execute(new Runnable() { 113 | @Override 114 | public void run() { 115 | eventListener.onTCPError(message); 116 | } 117 | }); 118 | } 119 | 120 | /** 121 | * Base class for server and client sockets. Contains a listening thread that will call 122 | * eventListener.onTCPMessage on new messages. 123 | */ 124 | private abstract class TCPSocket extends Thread { 125 | // Lock for editing out and rawSocket 126 | protected final Object rawSocketLock; 127 | @Nullable 128 | private PrintWriter out; 129 | @Nullable 130 | private Socket rawSocket; 131 | 132 | /** 133 | * Connect to the peer, potentially a slow operation. 134 | * 135 | * @return Socket connection, null if connection failed. 136 | */ 137 | @Nullable 138 | public abstract Socket connect(); 139 | 140 | /** Returns true if sockets is a server rawSocket. */ 141 | public abstract boolean isServer(); 142 | 143 | TCPSocket() { 144 | rawSocketLock = new Object(); 145 | } 146 | 147 | /** 148 | * The listening thread. 149 | */ 150 | @Override 151 | public void run() { 152 | Log.d(TAG, "Listening thread started..."); 153 | 154 | // Receive connection to temporary variable first, so we don't block. 155 | Socket tempSocket = connect(); 156 | BufferedReader in; 157 | 158 | Log.d(TAG, "TCP connection established."); 159 | 160 | synchronized (rawSocketLock) { 161 | if (rawSocket != null) { 162 | Log.e(TAG, "Socket already existed and will be replaced."); 163 | } 164 | 165 | rawSocket = tempSocket; 166 | 167 | // Connecting failed, error has already been reported, just exit. 168 | if (rawSocket == null) { 169 | return; 170 | } 171 | 172 | try { 173 | out = new PrintWriter( 174 | new OutputStreamWriter(rawSocket.getOutputStream(), Charset.forName("UTF-8")), true); 175 | in = new BufferedReader( 176 | new InputStreamReader(rawSocket.getInputStream(), Charset.forName("UTF-8"))); 177 | } catch (IOException e) { 178 | reportError("Failed to open IO on rawSocket: " + e.getMessage()); 179 | return; 180 | } 181 | } 182 | 183 | Log.v(TAG, "Execute onTCPConnected"); 184 | executor.execute(new Runnable() { 185 | @Override 186 | public void run() { 187 | Log.v(TAG, "Run onTCPConnected"); 188 | eventListener.onTCPConnected(isServer()); 189 | } 190 | }); 191 | 192 | while (true) { 193 | final String message; 194 | try { 195 | message = in.readLine(); 196 | } catch (IOException e) { 197 | synchronized (rawSocketLock) { 198 | // If socket was closed, this is expected. 199 | if (rawSocket == null) { 200 | break; 201 | } 202 | } 203 | 204 | reportError("Failed to read from rawSocket: " + e.getMessage()); 205 | break; 206 | } 207 | 208 | // No data received, rawSocket probably closed. 209 | if (message == null) { 210 | break; 211 | } 212 | 213 | executor.execute(new Runnable() { 214 | @Override 215 | public void run() { 216 | Log.v(TAG, "Receive: " + message); 217 | eventListener.onTCPMessage(message); 218 | } 219 | }); 220 | } 221 | 222 | Log.d(TAG, "Receiving thread exiting..."); 223 | 224 | // Close the rawSocket if it is still open. 225 | disconnect(); 226 | } 227 | 228 | /** Closes the rawSocket if it is still open. Also fires the onTCPClose event. */ 229 | public void disconnect() { 230 | try { 231 | synchronized (rawSocketLock) { 232 | if (rawSocket != null) { 233 | rawSocket.close(); 234 | rawSocket = null; 235 | out = null; 236 | 237 | executor.execute(new Runnable() { 238 | @Override 239 | public void run() { 240 | eventListener.onTCPClose(); 241 | } 242 | }); 243 | } 244 | } 245 | } catch (IOException e) { 246 | reportError("Failed to close rawSocket: " + e.getMessage()); 247 | } 248 | } 249 | 250 | /** 251 | * Sends a message on the socket. Should only be called on the executor thread. 252 | */ 253 | public void send(String message) { 254 | Log.v(TAG, "Send: " + message); 255 | 256 | synchronized (rawSocketLock) { 257 | if (out == null) { 258 | reportError("Sending data on closed socket."); 259 | return; 260 | } 261 | 262 | out.write(message + "\n"); 263 | out.flush(); 264 | } 265 | } 266 | } 267 | 268 | private class TCPSocketServer extends TCPSocket { 269 | // Server socket is also guarded by rawSocketLock. 270 | @Nullable 271 | private ServerSocket serverSocket; 272 | 273 | final private InetAddress address; 274 | final private int port; 275 | 276 | public TCPSocketServer(InetAddress address, int port) { 277 | this.address = address; 278 | this.port = port; 279 | } 280 | 281 | /** Opens a listening socket and waits for a connection. */ 282 | @Nullable 283 | @Override 284 | public Socket connect() { 285 | Log.d(TAG, "Listening on [" + address.getHostAddress() + "]:" + Integer.toString(port)); 286 | 287 | final ServerSocket tempSocket; 288 | try { 289 | tempSocket = new ServerSocket(port, 0, address); 290 | } catch (IOException e) { 291 | reportError("Failed to create server socket: " + e.getMessage()); 292 | return null; 293 | } 294 | 295 | synchronized (rawSocketLock) { 296 | if (serverSocket != null) { 297 | Log.e(TAG, "Server rawSocket was already listening and new will be opened."); 298 | } 299 | 300 | serverSocket = tempSocket; 301 | } 302 | 303 | try { 304 | return tempSocket.accept(); 305 | } catch (IOException e) { 306 | reportError("Failed to receive connection: " + e.getMessage()); 307 | return null; 308 | } 309 | } 310 | 311 | /** Closes the listening socket and calls super. */ 312 | @Override 313 | public void disconnect() { 314 | try { 315 | synchronized (rawSocketLock) { 316 | if (serverSocket != null) { 317 | serverSocket.close(); 318 | serverSocket = null; 319 | } 320 | } 321 | } catch (IOException e) { 322 | reportError("Failed to close server socket: " + e.getMessage()); 323 | } 324 | 325 | super.disconnect(); 326 | } 327 | 328 | @Override 329 | public boolean isServer() { 330 | return true; 331 | } 332 | } 333 | 334 | private class TCPSocketClient extends TCPSocket { 335 | final private InetAddress address; 336 | final private int port; 337 | 338 | public TCPSocketClient(InetAddress address, int port) { 339 | this.address = address; 340 | this.port = port; 341 | } 342 | 343 | /** Connects to the peer. */ 344 | @Nullable 345 | @Override 346 | public Socket connect() { 347 | Log.d(TAG, "Connecting to [" + address.getHostAddress() + "]:" + Integer.toString(port)); 348 | 349 | try { 350 | return new Socket(address, port); 351 | } catch (IOException e) { 352 | reportError("Failed to connect: " + e.getMessage()); 353 | return null; 354 | } 355 | } 356 | 357 | @Override 358 | public boolean isServer() { 359 | return false; 360 | } 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /app/src/main/java/org/appspot/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 org.appspot.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 | } 86 | -------------------------------------------------------------------------------- /app/src/main/java/org/appspot/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 org.appspot.apprtc; 12 | 13 | import android.os.Handler; 14 | import android.support.annotation.Nullable; 15 | import android.util.Log; 16 | import de.tavendo.autobahn.WebSocket.WebSocketConnectionObserver; 17 | import de.tavendo.autobahn.WebSocketConnection; 18 | import de.tavendo.autobahn.WebSocketException; 19 | import java.net.URI; 20 | import java.net.URISyntaxException; 21 | import java.util.ArrayList; 22 | import java.util.List; 23 | import org.appspot.apprtc.util.AsyncHttpURLConnection; 24 | import org.appspot.apprtc.util.AsyncHttpURLConnection.AsyncHttpEvents; 25 | import org.json.JSONException; 26 | import org.json.JSONObject; 27 | 28 | /** 29 | * WebSocket client implementation. 30 | * 31 | *

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

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

2 | 7 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/values-v17/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |