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 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 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
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 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 |