Move all the examples from the talk directory into the webrtc examples directory.

Significant changes:

- move the libjingle_examples.gyp file into webrtc directory.
- rename talk/examples/android to webrtc/examples/androidapp to avoid name conflicts.
- update paths in talk/libjingle_tests.gyp to point to webrtc directory for Objective-C test.

BUG=
R=pthatcher@webrtc.org, tkchin@webrtc.org

Review URL: https://codereview.webrtc.org/1235563006 .

Cr-Commit-Position: refs/heads/master@{#9681}
This commit is contained in:
Donald E Curtis
2015-08-05 15:48:13 -07:00
parent 5b4ce3391d
commit a873644897
203 changed files with 1553 additions and 3185 deletions

View File

@ -0,0 +1,356 @@
/*
* Copyright 2014 The WebRTC Project Authors. All rights reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree. An additional intellectual property rights grant can be found
* in the file PATENTS. All contributing project authors may
* be found in the AUTHORS file in the root of the source tree.
*/
package org.appspot.apprtc;
import org.appspot.apprtc.util.AppRTCUtils;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.media.AudioManager;
import android.util.Log;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
/**
* AppRTCAudioManager manages all audio related parts of the AppRTC demo.
*/
public class AppRTCAudioManager {
private static final String TAG = "AppRTCAudioManager";
/**
* AudioDevice is the names of possible audio devices that we currently
* support.
*/
// TODO(henrika): add support for BLUETOOTH as well.
public enum AudioDevice {
SPEAKER_PHONE,
WIRED_HEADSET,
EARPIECE,
}
private final Context apprtcContext;
private final Runnable onStateChangeListener;
private boolean initialized = false;
private AudioManager audioManager;
private int savedAudioMode = AudioManager.MODE_INVALID;
private boolean savedIsSpeakerPhoneOn = false;
private boolean savedIsMicrophoneMute = false;
// For now; always use the speaker phone as default device selection when
// there is a choice between SPEAKER_PHONE and EARPIECE.
// TODO(henrika): it is possible that EARPIECE should be preferred in some
// cases. If so, we should set this value at construction instead.
private final AudioDevice defaultAudioDevice = AudioDevice.SPEAKER_PHONE;
// Proximity sensor object. It measures the proximity of an object in cm
// relative to the view screen of a device and can therefore be used to
// assist device switching (close to ear <=> use headset earpiece if
// available, far from ear <=> use speaker phone).
private AppRTCProximitySensor proximitySensor = null;
// Contains the currently selected audio device.
private AudioDevice selectedAudioDevice;
// Contains a list of available audio devices. A Set collection is used to
// avoid duplicate elements.
private final Set<AudioDevice> audioDevices = new HashSet<AudioDevice>();
// Broadcast receiver for wired headset intent broadcasts.
private BroadcastReceiver wiredHeadsetReceiver;
// This method is called when the proximity sensor reports a state change,
// e.g. from "NEAR to FAR" or from "FAR to NEAR".
private void onProximitySensorChangedState() {
// The proximity sensor should only be activated when there are exactly two
// available audio devices.
if (audioDevices.size() == 2
&& audioDevices.contains(AppRTCAudioManager.AudioDevice.EARPIECE)
&& audioDevices.contains(
AppRTCAudioManager.AudioDevice.SPEAKER_PHONE)) {
if (proximitySensor.sensorReportsNearState()) {
// Sensor reports that a "handset is being held up to a person's ear",
// or "something is covering the light sensor".
setAudioDevice(AppRTCAudioManager.AudioDevice.EARPIECE);
} else {
// Sensor reports that a "handset is removed from a person's ear", or
// "the light sensor is no longer covered".
setAudioDevice(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE);
}
}
}
/** Construction */
static AppRTCAudioManager create(Context context,
Runnable deviceStateChangeListener) {
return new AppRTCAudioManager(context, deviceStateChangeListener);
}
private AppRTCAudioManager(Context context,
Runnable deviceStateChangeListener) {
apprtcContext = context;
onStateChangeListener = deviceStateChangeListener;
audioManager = ((AudioManager) context.getSystemService(
Context.AUDIO_SERVICE));
// Create and initialize the proximity sensor.
// Tablet devices (e.g. Nexus 7) does not support proximity sensors.
// Note that, the sensor will not be active until start() has been called.
proximitySensor = AppRTCProximitySensor.create(context, new Runnable() {
// This method will be called each time a state change is detected.
// Example: user holds his hand over the device (closer than ~5 cm),
// or removes his hand from the device.
public void run() {
onProximitySensorChangedState();
}
});
AppRTCUtils.logDeviceInfo(TAG);
}
public void init() {
Log.d(TAG, "init");
if (initialized) {
return;
}
// Store current audio state so we can restore it when close() is called.
savedAudioMode = audioManager.getMode();
savedIsSpeakerPhoneOn = audioManager.isSpeakerphoneOn();
savedIsMicrophoneMute = audioManager.isMicrophoneMute();
// Request audio focus before making any device switch.
audioManager.requestAudioFocus(null, AudioManager.STREAM_VOICE_CALL,
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
// Start by setting MODE_IN_COMMUNICATION as default audio mode. It is
// required to be in this mode when playout and/or recording starts for
// best possible VoIP performance.
// TODO(henrika): we migh want to start with RINGTONE mode here instead.
audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
// Always disable microphone mute during a WebRTC call.
setMicrophoneMute(false);
// Do initial selection of audio device. This setting can later be changed
// either by adding/removing a wired headset or by covering/uncovering the
// proximity sensor.
updateAudioDeviceState(hasWiredHeadset());
// Register receiver for broadcast intents related to adding/removing a
// wired headset (Intent.ACTION_HEADSET_PLUG).
registerForWiredHeadsetIntentBroadcast();
initialized = true;
}
public void close() {
Log.d(TAG, "close");
if (!initialized) {
return;
}
unregisterForWiredHeadsetIntentBroadcast();
// Restore previously stored audio states.
setSpeakerphoneOn(savedIsSpeakerPhoneOn);
setMicrophoneMute(savedIsMicrophoneMute);
audioManager.setMode(savedAudioMode);
audioManager.abandonAudioFocus(null);
if (proximitySensor != null) {
proximitySensor.stop();
proximitySensor = null;
}
initialized = false;
}
/** Changes selection of the currently active audio device. */
public void setAudioDevice(AudioDevice device) {
Log.d(TAG, "setAudioDevice(device=" + device + ")");
AppRTCUtils.assertIsTrue(audioDevices.contains(device));
switch (device) {
case SPEAKER_PHONE:
setSpeakerphoneOn(true);
selectedAudioDevice = AudioDevice.SPEAKER_PHONE;
break;
case EARPIECE:
setSpeakerphoneOn(false);
selectedAudioDevice = AudioDevice.EARPIECE;
break;
case WIRED_HEADSET:
setSpeakerphoneOn(false);
selectedAudioDevice = AudioDevice.WIRED_HEADSET;
break;
default:
Log.e(TAG, "Invalid audio device selection");
break;
}
onAudioManagerChangedState();
}
/** Returns current set of available/selectable audio devices. */
public Set<AudioDevice> getAudioDevices() {
return Collections.unmodifiableSet(new HashSet<AudioDevice>(audioDevices));
}
/** Returns the currently selected audio device. */
public AudioDevice getSelectedAudioDevice() {
return selectedAudioDevice;
}
/**
* Registers receiver for the broadcasted intent when a wired headset is
* plugged in or unplugged. The received intent will have an extra
* 'state' value where 0 means unplugged, and 1 means plugged.
*/
private void registerForWiredHeadsetIntentBroadcast() {
IntentFilter filter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
/** Receiver which handles changes in wired headset availability. */
wiredHeadsetReceiver = new BroadcastReceiver() {
private static final int STATE_UNPLUGGED = 0;
private static final int STATE_PLUGGED = 1;
private static final int HAS_NO_MIC = 0;
private static final int HAS_MIC = 1;
@Override
public void onReceive(Context context, Intent intent) {
int state = intent.getIntExtra("state", STATE_UNPLUGGED);
int microphone = intent.getIntExtra("microphone", HAS_NO_MIC);
String name = intent.getStringExtra("name");
Log.d(TAG, "BroadcastReceiver.onReceive" + AppRTCUtils.getThreadInfo()
+ ": "
+ "a=" + intent.getAction()
+ ", s=" + (state == STATE_UNPLUGGED ? "unplugged" : "plugged")
+ ", m=" + (microphone == HAS_MIC ? "mic" : "no mic")
+ ", n=" + name
+ ", sb=" + isInitialStickyBroadcast());
boolean hasWiredHeadset = (state == STATE_PLUGGED) ? true : false;
switch (state) {
case STATE_UNPLUGGED:
updateAudioDeviceState(hasWiredHeadset);
break;
case STATE_PLUGGED:
if (selectedAudioDevice != AudioDevice.WIRED_HEADSET) {
updateAudioDeviceState(hasWiredHeadset);
}
break;
default:
Log.e(TAG, "Invalid state");
break;
}
}
};
apprtcContext.registerReceiver(wiredHeadsetReceiver, filter);
}
/** Unregister receiver for broadcasted ACTION_HEADSET_PLUG intent. */
private void unregisterForWiredHeadsetIntentBroadcast() {
apprtcContext.unregisterReceiver(wiredHeadsetReceiver);
wiredHeadsetReceiver = null;
}
/** Sets the speaker phone mode. */
private void setSpeakerphoneOn(boolean on) {
boolean wasOn = audioManager.isSpeakerphoneOn();
if (wasOn == on) {
return;
}
audioManager.setSpeakerphoneOn(on);
}
/** Sets the microphone mute state. */
private void setMicrophoneMute(boolean on) {
boolean wasMuted = audioManager.isMicrophoneMute();
if (wasMuted == on) {
return;
}
audioManager.setMicrophoneMute(on);
}
/** Gets the current earpiece state. */
private boolean hasEarpiece() {
return apprtcContext.getPackageManager().hasSystemFeature(
PackageManager.FEATURE_TELEPHONY);
}
/**
* Checks whether a wired headset is connected or not.
* This is not a valid indication that audio playback is actually over
* the wired headset as audio routing depends on other conditions. We
* only use it as an early indicator (during initialization) of an attached
* wired headset.
*/
@Deprecated
private boolean hasWiredHeadset() {
return audioManager.isWiredHeadsetOn();
}
/** Update list of possible audio devices and make new device selection. */
private void updateAudioDeviceState(boolean hasWiredHeadset) {
// Update the list of available audio devices.
audioDevices.clear();
if (hasWiredHeadset) {
// If a wired headset is connected, then it is the only possible option.
audioDevices.add(AudioDevice.WIRED_HEADSET);
} else {
// No wired headset, hence the audio-device list can contain speaker
// phone (on a tablet), or speaker phone and earpiece (on mobile phone).
audioDevices.add(AudioDevice.SPEAKER_PHONE);
if (hasEarpiece()) {
audioDevices.add(AudioDevice.EARPIECE);
}
}
Log.d(TAG, "audioDevices: " + audioDevices);
// Switch to correct audio device given the list of available audio devices.
if (hasWiredHeadset) {
setAudioDevice(AudioDevice.WIRED_HEADSET);
} else {
setAudioDevice(defaultAudioDevice);
}
}
/** Called each time a new audio device has been added or removed. */
private void onAudioManagerChangedState() {
Log.d(TAG, "onAudioManagerChangedState: devices=" + audioDevices
+ ", selected=" + selectedAudioDevice);
// Enable the proximity sensor if there are two available audio devices
// in the list. Given the current implementation, we know that the choice
// will then be between EARPIECE and SPEAKER_PHONE.
if (audioDevices.size() == 2) {
AppRTCUtils.assertIsTrue(audioDevices.contains(AudioDevice.EARPIECE)
&& audioDevices.contains(AudioDevice.SPEAKER_PHONE));
// Start the proximity sensor.
proximitySensor.start();
} else if (audioDevices.size() == 1) {
// Stop the proximity sensor since it is no longer needed.
proximitySensor.stop();
} else {
Log.e(TAG, "Invalid device list");
}
if (onStateChangeListener != null) {
// Run callback to notify a listening client. The client can then
// use public getters to query the new state.
onStateChangeListener.run();
}
}
}

View File

@ -0,0 +1,125 @@
/*
* Copyright 2013 The WebRTC Project Authors. All rights reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree. An additional intellectual property rights grant can be found
* in the file PATENTS. All contributing project authors may
* be found in the AUTHORS file in the root of the source tree.
*/
package org.appspot.apprtc;
import org.webrtc.IceCandidate;
import org.webrtc.PeerConnection;
import org.webrtc.SessionDescription;
import java.util.List;
/**
* AppRTCClient is the interface representing an AppRTC client.
*/
public interface AppRTCClient {
/**
* Struct holding the connection parameters of an AppRTC room.
*/
public static class RoomConnectionParameters {
public final String roomUrl;
public final String roomId;
public final boolean loopback;
public RoomConnectionParameters(
String roomUrl, String roomId, boolean loopback) {
this.roomUrl = roomUrl;
this.roomId = roomId;
this.loopback = loopback;
}
}
/**
* Asynchronously connect to an AppRTC room URL using supplied connection
* parameters. Once connection is established onConnectedToRoom()
* callback with room parameters is invoked.
*/
public void connectToRoom(RoomConnectionParameters connectionParameters);
/**
* Send offer SDP to the other participant.
*/
public void sendOfferSdp(final SessionDescription sdp);
/**
* Send answer SDP to the other participant.
*/
public void sendAnswerSdp(final SessionDescription sdp);
/**
* Send Ice candidate to the other participant.
*/
public void sendLocalIceCandidate(final IceCandidate candidate);
/**
* Disconnect from room.
*/
public void disconnectFromRoom();
/**
* Struct holding the signaling parameters of an AppRTC room.
*/
public static class SignalingParameters {
public final List<PeerConnection.IceServer> iceServers;
public final boolean initiator;
public final String clientId;
public final String wssUrl;
public final String wssPostUrl;
public final SessionDescription offerSdp;
public final List<IceCandidate> iceCandidates;
public SignalingParameters(
List<PeerConnection.IceServer> iceServers,
boolean initiator, String clientId,
String wssUrl, String wssPostUrl,
SessionDescription offerSdp, List<IceCandidate> iceCandidates) {
this.iceServers = iceServers;
this.initiator = initiator;
this.clientId = clientId;
this.wssUrl = wssUrl;
this.wssPostUrl = wssPostUrl;
this.offerSdp = offerSdp;
this.iceCandidates = iceCandidates;
}
}
/**
* Callback interface for messages delivered on signaling channel.
*
* <p>Methods are guaranteed to be invoked on the UI thread of |activity|.
*/
public static interface SignalingEvents {
/**
* Callback fired once the room's signaling parameters
* SignalingParameters are extracted.
*/
public void onConnectedToRoom(final SignalingParameters params);
/**
* Callback fired once remote SDP is received.
*/
public void onRemoteDescription(final SessionDescription sdp);
/**
* Callback fired once remote Ice candidate is received.
*/
public void onRemoteIceCandidate(final IceCandidate candidate);
/**
* Callback fired once channel is closed.
*/
public void onChannelClose();
/**
* Callback fired once channel error happened.
*/
public void onChannelError(final String description);
}
}

View File

@ -0,0 +1,180 @@
/*
* Copyright 2014 The WebRTC Project Authors. All rights reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree. An additional intellectual property rights grant can be found
* in the file PATENTS. All contributing project authors may
* be found in the AUTHORS file in the root of the source tree.
*/
package org.appspot.apprtc;
import org.appspot.apprtc.util.AppRTCUtils;
import org.appspot.apprtc.util.AppRTCUtils.NonThreadSafe;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Build;
import android.util.Log;
/**
* AppRTCProximitySensor manages functions related to the proximity sensor in
* the AppRTC demo.
* On most device, the proximity sensor is implemented as a boolean-sensor.
* It returns just two values "NEAR" or "FAR". Thresholding is done on the LUX
* value i.e. the LUX value of the light sensor is compared with a threshold.
* A LUX-value more than the threshold means the proximity sensor returns "FAR".
* Anything less than the threshold value and the sensor returns "NEAR".
*/
public class AppRTCProximitySensor implements SensorEventListener {
private static final String TAG = "AppRTCProximitySensor";
// This class should be created, started and stopped on one thread
// (e.g. the main thread). We use |nonThreadSafe| to ensure that this is
// the case. Only active when |DEBUG| is set to true.
private final NonThreadSafe nonThreadSafe = new AppRTCUtils.NonThreadSafe();
private final Runnable onSensorStateListener;
private final SensorManager sensorManager;
private Sensor proximitySensor = null;
private boolean lastStateReportIsNear = false;
/** Construction */
static AppRTCProximitySensor create(Context context,
Runnable sensorStateListener) {
return new AppRTCProximitySensor(context, sensorStateListener);
}
private AppRTCProximitySensor(Context context, Runnable sensorStateListener) {
Log.d(TAG, "AppRTCProximitySensor" + AppRTCUtils.getThreadInfo());
onSensorStateListener = sensorStateListener;
sensorManager = ((SensorManager) context.getSystemService(
Context.SENSOR_SERVICE));
}
/**
* Activate the proximity sensor. Also do initializtion if called for the
* first time.
*/
public boolean start() {
checkIfCalledOnValidThread();
Log.d(TAG, "start" + AppRTCUtils.getThreadInfo());
if (!initDefaultSensor()) {
// Proximity sensor is not supported on this device.
return false;
}
sensorManager.registerListener(
this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL);
return true;
}
/** Deactivate the proximity sensor. */
public void stop() {
checkIfCalledOnValidThread();
Log.d(TAG, "stop" + AppRTCUtils.getThreadInfo());
if (proximitySensor == null) {
return;
}
sensorManager.unregisterListener(this, proximitySensor);
}
/** Getter for last reported state. Set to true if "near" is reported. */
public boolean sensorReportsNearState() {
checkIfCalledOnValidThread();
return lastStateReportIsNear;
}
@Override
public final void onAccuracyChanged(Sensor sensor, int accuracy) {
checkIfCalledOnValidThread();
AppRTCUtils.assertIsTrue(sensor.getType() == Sensor.TYPE_PROXIMITY);
if (accuracy == SensorManager.SENSOR_STATUS_UNRELIABLE) {
Log.e(TAG, "The values returned by this sensor cannot be trusted");
}
}
@Override
public final void onSensorChanged(SensorEvent event) {
checkIfCalledOnValidThread();
AppRTCUtils.assertIsTrue(event.sensor.getType() == Sensor.TYPE_PROXIMITY);
// As a best practice; do as little as possible within this method and
// avoid blocking.
float distanceInCentimeters = event.values[0];
if (distanceInCentimeters < proximitySensor.getMaximumRange()) {
Log.d(TAG, "Proximity sensor => NEAR state");
lastStateReportIsNear = true;
} else {
Log.d(TAG, "Proximity sensor => FAR state");
lastStateReportIsNear = false;
}
// Report about new state to listening client. Client can then call
// sensorReportsNearState() to query the current state (NEAR or FAR).
if (onSensorStateListener != null) {
onSensorStateListener.run();
}
Log.d(TAG, "onSensorChanged" + AppRTCUtils.getThreadInfo() + ": "
+ "accuracy=" + event.accuracy
+ ", timestamp=" + event.timestamp + ", distance=" + event.values[0]);
}
/**
* Get default proximity sensor if it exists. Tablet devices (e.g. Nexus 7)
* does not support this type of sensor and false will be retured in such
* cases.
*/
private boolean initDefaultSensor() {
if (proximitySensor != null) {
return true;
}
proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
if (proximitySensor == null) {
return false;
}
logProximitySensorInfo();
return true;
}
/** Helper method for logging information about the proximity sensor. */
private void logProximitySensorInfo() {
if (proximitySensor == null) {
return;
}
StringBuilder info = new StringBuilder("Proximity sensor: ");
info.append("name=" + proximitySensor.getName());
info.append(", vendor: " + proximitySensor.getVendor());
info.append(", power: " + proximitySensor.getPower());
info.append(", resolution: " + proximitySensor.getResolution());
info.append(", max range: " + proximitySensor.getMaximumRange());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
// Added in API level 9.
info.append(", min delay: " + proximitySensor.getMinDelay());
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
// Added in API level 20.
info.append(", type: " + proximitySensor.getStringType());
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// Added in API level 21.
info.append(", max delay: " + proximitySensor.getMaxDelay());
info.append(", reporting mode: " + proximitySensor.getReportingMode());
info.append(", isWakeUpSensor: " + proximitySensor.isWakeUpSensor());
}
Log.d(TAG, info.toString());
}
/**
* Helper method for debugging purposes. Ensures that method is
* called on same thread as this object was created on.
*/
private void checkIfCalledOnValidThread() {
if (!nonThreadSafe.calledOnValidThread()) {
throw new IllegalStateException("Method is not called on valid thread");
}
}
}

View File

@ -0,0 +1,647 @@
/*
* Copyright 2015 The WebRTC Project Authors. All rights reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree. An additional intellectual property rights grant can be found
* in the file PATENTS. All contributing project authors may
* be found in the AUTHORS file in the root of the source tree.
*/
package org.appspot.apprtc;
import org.appspot.apprtc.AppRTCClient.RoomConnectionParameters;
import org.appspot.apprtc.AppRTCClient.SignalingParameters;
import org.appspot.apprtc.PeerConnectionClient.PeerConnectionParameters;
import org.appspot.apprtc.util.LooperExecutor;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.FragmentTransaction;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.opengl.GLSurfaceView;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.Window;
import android.view.WindowManager.LayoutParams;
import android.widget.Toast;
import org.webrtc.IceCandidate;
import org.webrtc.SessionDescription;
import org.webrtc.StatsReport;
import org.webrtc.VideoRenderer;
import org.webrtc.VideoRendererGui;
import org.webrtc.VideoRendererGui.ScalingType;
/**
* Activity for peer connection call setup, call waiting
* and call view.
*/
public class CallActivity extends Activity
implements AppRTCClient.SignalingEvents,
PeerConnectionClient.PeerConnectionEvents,
CallFragment.OnCallEvents {
public static final String EXTRA_ROOMID =
"org.appspot.apprtc.ROOMID";
public static final String EXTRA_LOOPBACK =
"org.appspot.apprtc.LOOPBACK";
public static final String EXTRA_VIDEO_CALL =
"org.appspot.apprtc.VIDEO_CALL";
public static final String EXTRA_VIDEO_WIDTH =
"org.appspot.apprtc.VIDEO_WIDTH";
public static final String EXTRA_VIDEO_HEIGHT =
"org.appspot.apprtc.VIDEO_HEIGHT";
public static final String EXTRA_VIDEO_FPS =
"org.appspot.apprtc.VIDEO_FPS";
public static final String EXTRA_VIDEO_BITRATE =
"org.appspot.apprtc.VIDEO_BITRATE";
public static final String EXTRA_VIDEOCODEC =
"org.appspot.apprtc.VIDEOCODEC";
public static final String EXTRA_HWCODEC_ENABLED =
"org.appspot.apprtc.HWCODEC";
public static final String EXTRA_AUDIO_BITRATE =
"org.appspot.apprtc.AUDIO_BITRATE";
public static final String EXTRA_AUDIOCODEC =
"org.appspot.apprtc.AUDIOCODEC";
public static final String EXTRA_NOAUDIOPROCESSING_ENABLED =
"org.appspot.apprtc.NOAUDIOPROCESSING";
public static final String EXTRA_CPUOVERUSE_DETECTION =
"org.appspot.apprtc.CPUOVERUSE_DETECTION";
public static final String EXTRA_DISPLAY_HUD =
"org.appspot.apprtc.DISPLAY_HUD";
public static final String EXTRA_CMDLINE =
"org.appspot.apprtc.CMDLINE";
public static final String EXTRA_RUNTIME =
"org.appspot.apprtc.RUNTIME";
private static final String TAG = "CallRTCClient";
// List of mandatory application permissions.
private static final String[] MANDATORY_PERMISSIONS = {
"android.permission.MODIFY_AUDIO_SETTINGS",
"android.permission.RECORD_AUDIO",
"android.permission.INTERNET"
};
// Peer connection statistics callback period in ms.
private static final int STAT_CALLBACK_PERIOD = 1000;
// Local preview screen position before call is connected.
private static final int LOCAL_X_CONNECTING = 0;
private static final int LOCAL_Y_CONNECTING = 0;
private static final int LOCAL_WIDTH_CONNECTING = 100;
private static final int LOCAL_HEIGHT_CONNECTING = 100;
// Local preview screen position after call is connected.
private static final int LOCAL_X_CONNECTED = 72;
private static final int LOCAL_Y_CONNECTED = 72;
private static final int LOCAL_WIDTH_CONNECTED = 25;
private static final int LOCAL_HEIGHT_CONNECTED = 25;
// Remote video screen position
private static final int REMOTE_X = 0;
private static final int REMOTE_Y = 0;
private static final int REMOTE_WIDTH = 100;
private static final int REMOTE_HEIGHT = 100;
private PeerConnectionClient peerConnectionClient = null;
private AppRTCClient appRtcClient;
private SignalingParameters signalingParameters;
private AppRTCAudioManager audioManager = null;
private VideoRenderer.Callbacks localRender;
private VideoRenderer.Callbacks remoteRender;
private ScalingType scalingType;
private Toast logToast;
private boolean commandLineRun;
private int runTimeMs;
private boolean activityRunning;
private RoomConnectionParameters roomConnectionParameters;
private PeerConnectionParameters peerConnectionParameters;
private boolean iceConnected;
private boolean isError;
private boolean callControlFragmentVisible = true;
private long callStartedTimeMs = 0;
// Controls
private GLSurfaceView videoView;
CallFragment callFragment;
HudFragment hudFragment;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Thread.setDefaultUncaughtExceptionHandler(
new UnhandledExceptionHandler(this));
// Set window styles for fullscreen-window size. Needs to be done before
// adding content.
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().addFlags(
LayoutParams.FLAG_FULLSCREEN
| LayoutParams.FLAG_KEEP_SCREEN_ON
| LayoutParams.FLAG_DISMISS_KEYGUARD
| LayoutParams.FLAG_SHOW_WHEN_LOCKED
| LayoutParams.FLAG_TURN_SCREEN_ON);
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
setContentView(R.layout.activity_call);
iceConnected = false;
signalingParameters = null;
scalingType = ScalingType.SCALE_ASPECT_FILL;
// Create UI controls.
videoView = (GLSurfaceView) findViewById(R.id.glview_call);
callFragment = new CallFragment();
hudFragment = new HudFragment();
// Create video renderers.
VideoRendererGui.setView(videoView, new Runnable() {
@Override
public void run() {
createPeerConnectionFactory();
}
});
remoteRender = VideoRendererGui.create(
REMOTE_X, REMOTE_Y,
REMOTE_WIDTH, REMOTE_HEIGHT, scalingType, false);
localRender = VideoRendererGui.create(
LOCAL_X_CONNECTING, LOCAL_Y_CONNECTING,
LOCAL_WIDTH_CONNECTING, LOCAL_HEIGHT_CONNECTING, scalingType, true);
// Show/hide call control fragment on view click.
videoView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
toggleCallControlFragmentVisibility();
}
});
// Check for mandatory permissions.
for (String permission : MANDATORY_PERMISSIONS) {
if (checkCallingOrSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
logAndToast("Permission " + permission + " is not granted");
setResult(RESULT_CANCELED);
finish();
return;
}
}
// Get Intent parameters.
final Intent intent = getIntent();
Uri roomUri = intent.getData();
if (roomUri == null) {
logAndToast(getString(R.string.missing_url));
Log.e(TAG, "Didn't get any URL in intent!");
setResult(RESULT_CANCELED);
finish();
return;
}
String roomId = intent.getStringExtra(EXTRA_ROOMID);
if (roomId == null || roomId.length() == 0) {
logAndToast(getString(R.string.missing_url));
Log.e(TAG, "Incorrect room ID in intent!");
setResult(RESULT_CANCELED);
finish();
return;
}
boolean loopback = intent.getBooleanExtra(EXTRA_LOOPBACK, false);
peerConnectionParameters = new PeerConnectionParameters(
intent.getBooleanExtra(EXTRA_VIDEO_CALL, true),
loopback,
intent.getIntExtra(EXTRA_VIDEO_WIDTH, 0),
intent.getIntExtra(EXTRA_VIDEO_HEIGHT, 0),
intent.getIntExtra(EXTRA_VIDEO_FPS, 0),
intent.getIntExtra(EXTRA_VIDEO_BITRATE, 0),
intent.getStringExtra(EXTRA_VIDEOCODEC),
intent.getBooleanExtra(EXTRA_HWCODEC_ENABLED, true),
intent.getIntExtra(EXTRA_AUDIO_BITRATE, 0),
intent.getStringExtra(EXTRA_AUDIOCODEC),
intent.getBooleanExtra(EXTRA_NOAUDIOPROCESSING_ENABLED, false),
intent.getBooleanExtra(EXTRA_CPUOVERUSE_DETECTION, true));
commandLineRun = intent.getBooleanExtra(EXTRA_CMDLINE, false);
runTimeMs = intent.getIntExtra(EXTRA_RUNTIME, 0);
// Create connection client and connection parameters.
appRtcClient = new WebSocketRTCClient(this, new LooperExecutor());
roomConnectionParameters = new RoomConnectionParameters(
roomUri.toString(), roomId, loopback);
// Send intent arguments to fragments.
callFragment.setArguments(intent.getExtras());
hudFragment.setArguments(intent.getExtras());
// Activate call and HUD fragments and start the call.
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.add(R.id.call_fragment_container, callFragment);
ft.add(R.id.hud_fragment_container, hudFragment);
ft.commit();
startCall();
// For command line execution run connection for <runTimeMs> and exit.
if (commandLineRun && runTimeMs > 0) {
videoView.postDelayed(new Runnable() {
public void run() {
disconnect();
}
}, runTimeMs);
}
}
// Activity interfaces
@Override
public void onPause() {
super.onPause();
videoView.onPause();
activityRunning = false;
if (peerConnectionClient != null) {
peerConnectionClient.stopVideoSource();
}
}
@Override
public void onResume() {
super.onResume();
videoView.onResume();
activityRunning = true;
if (peerConnectionClient != null) {
peerConnectionClient.startVideoSource();
}
}
@Override
protected void onDestroy() {
disconnect();
super.onDestroy();
if (logToast != null) {
logToast.cancel();
}
activityRunning = false;
}
// CallFragment.OnCallEvents interface implementation.
@Override
public void onCallHangUp() {
disconnect();
}
@Override
public void onCameraSwitch() {
if (peerConnectionClient != null) {
peerConnectionClient.switchCamera();
}
}
@Override
public void onVideoScalingSwitch(ScalingType scalingType) {
this.scalingType = scalingType;
updateVideoView();
}
// Helper functions.
private void toggleCallControlFragmentVisibility() {
if (!iceConnected || !callFragment.isAdded()) {
return;
}
// Show/hide call control fragment
callControlFragmentVisible = !callControlFragmentVisible;
FragmentTransaction ft = getFragmentManager().beginTransaction();
if (callControlFragmentVisible) {
ft.show(callFragment);
ft.show(hudFragment);
} else {
ft.hide(callFragment);
ft.hide(hudFragment);
}
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
ft.commit();
}
private void updateVideoView() {
VideoRendererGui.update(remoteRender,
REMOTE_X, REMOTE_Y,
REMOTE_WIDTH, REMOTE_HEIGHT, scalingType, false);
if (iceConnected) {
VideoRendererGui.update(localRender,
LOCAL_X_CONNECTED, LOCAL_Y_CONNECTED,
LOCAL_WIDTH_CONNECTED, LOCAL_HEIGHT_CONNECTED,
ScalingType.SCALE_ASPECT_FIT, true);
} else {
VideoRendererGui.update(localRender,
LOCAL_X_CONNECTING, LOCAL_Y_CONNECTING,
LOCAL_WIDTH_CONNECTING, LOCAL_HEIGHT_CONNECTING, scalingType, true);
}
}
private void startCall() {
if (appRtcClient == null) {
Log.e(TAG, "AppRTC client is not allocated for a call.");
return;
}
callStartedTimeMs = System.currentTimeMillis();
// Start room connection.
logAndToast(getString(R.string.connecting_to,
roomConnectionParameters.roomUrl));
appRtcClient.connectToRoom(roomConnectionParameters);
// Create and audio manager that will take care of audio routing,
// audio modes, audio device enumeration etc.
audioManager = AppRTCAudioManager.create(this, new Runnable() {
// This method will be called each time the audio state (number and
// type of devices) has been changed.
@Override
public void run() {
onAudioManagerChangedState();
}
}
);
// Store existing audio settings and change audio mode to
// MODE_IN_COMMUNICATION for best possible VoIP performance.
Log.d(TAG, "Initializing the audio manager...");
audioManager.init();
}
// Should be called from UI thread
private void callConnected() {
final long delta = System.currentTimeMillis() - callStartedTimeMs;
Log.i(TAG, "Call connected: delay=" + delta + "ms");
// Update video view.
updateVideoView();
// Enable statistics callback.
peerConnectionClient.enableStatsEvents(true, STAT_CALLBACK_PERIOD);
}
private void onAudioManagerChangedState() {
// TODO(henrika): disable video if AppRTCAudioManager.AudioDevice.EARPIECE
// is active.
}
// Create peer connection factory when EGL context is ready.
private void createPeerConnectionFactory() {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (peerConnectionClient == null) {
final long delta = System.currentTimeMillis() - callStartedTimeMs;
Log.d(TAG, "Creating peer connection factory, delay=" + delta + "ms");
peerConnectionClient = PeerConnectionClient.getInstance();
peerConnectionClient.createPeerConnectionFactory(CallActivity.this,
VideoRendererGui.getEGLContext(), peerConnectionParameters,
CallActivity.this);
}
if (signalingParameters != null) {
Log.w(TAG, "EGL context is ready after room connection.");
onConnectedToRoomInternal(signalingParameters);
}
}
});
}
// Disconnect from remote resources, dispose of local resources, and exit.
private void disconnect() {
activityRunning = false;
if (appRtcClient != null) {
appRtcClient.disconnectFromRoom();
appRtcClient = null;
}
if (peerConnectionClient != null) {
peerConnectionClient.close();
peerConnectionClient = null;
}
if (audioManager != null) {
audioManager.close();
audioManager = null;
}
if (iceConnected && !isError) {
setResult(RESULT_OK);
} else {
setResult(RESULT_CANCELED);
}
finish();
}
private void disconnectWithErrorMessage(final String errorMessage) {
if (commandLineRun || !activityRunning) {
Log.e(TAG, "Critical error: " + errorMessage);
disconnect();
} else {
new AlertDialog.Builder(this)
.setTitle(getText(R.string.channel_error_title))
.setMessage(errorMessage)
.setCancelable(false)
.setNeutralButton(R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
dialog.cancel();
disconnect();
}
}).create().show();
}
}
// Log |msg| and Toast about it.
private void logAndToast(String msg) {
Log.d(TAG, msg);
if (logToast != null) {
logToast.cancel();
}
logToast = Toast.makeText(this, msg, Toast.LENGTH_SHORT);
logToast.show();
}
private void reportError(final String description) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (!isError) {
isError = true;
disconnectWithErrorMessage(description);
}
}
});
}
// -----Implementation of AppRTCClient.AppRTCSignalingEvents ---------------
// All callbacks are invoked from websocket signaling looper thread and
// are routed to UI thread.
private void onConnectedToRoomInternal(final SignalingParameters params) {
final long delta = System.currentTimeMillis() - callStartedTimeMs;
signalingParameters = params;
if (peerConnectionClient == null) {
Log.w(TAG, "Room is connected, but EGL context is not ready yet.");
return;
}
logAndToast("Creating peer connection, delay=" + delta + "ms");
peerConnectionClient.createPeerConnection(
localRender, remoteRender, signalingParameters);
if (signalingParameters.initiator) {
logAndToast("Creating OFFER...");
// Create offer. Offer SDP will be sent to answering client in
// PeerConnectionEvents.onLocalDescription event.
peerConnectionClient.createOffer();
} else {
if (params.offerSdp != null) {
peerConnectionClient.setRemoteDescription(params.offerSdp);
logAndToast("Creating ANSWER...");
// Create answer. Answer SDP will be sent to offering client in
// PeerConnectionEvents.onLocalDescription event.
peerConnectionClient.createAnswer();
}
if (params.iceCandidates != null) {
// Add remote ICE candidates from room.
for (IceCandidate iceCandidate : params.iceCandidates) {
peerConnectionClient.addRemoteIceCandidate(iceCandidate);
}
}
}
}
@Override
public void onConnectedToRoom(final SignalingParameters params) {
runOnUiThread(new Runnable() {
@Override
public void run() {
onConnectedToRoomInternal(params);
}
});
}
@Override
public void onRemoteDescription(final SessionDescription sdp) {
final long delta = System.currentTimeMillis() - callStartedTimeMs;
runOnUiThread(new Runnable() {
@Override
public void run() {
if (peerConnectionClient == null) {
Log.e(TAG, "Received remote SDP for non-initilized peer connection.");
return;
}
logAndToast("Received remote " + sdp.type + ", delay=" + delta + "ms");
peerConnectionClient.setRemoteDescription(sdp);
if (!signalingParameters.initiator) {
logAndToast("Creating ANSWER...");
// Create answer. Answer SDP will be sent to offering client in
// PeerConnectionEvents.onLocalDescription event.
peerConnectionClient.createAnswer();
}
}
});
}
@Override
public void onRemoteIceCandidate(final IceCandidate candidate) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (peerConnectionClient == null) {
Log.e(TAG,
"Received ICE candidate for non-initilized peer connection.");
return;
}
peerConnectionClient.addRemoteIceCandidate(candidate);
}
});
}
@Override
public void onChannelClose() {
runOnUiThread(new Runnable() {
@Override
public void run() {
logAndToast("Remote end hung up; dropping PeerConnection");
disconnect();
}
});
}
@Override
public void onChannelError(final String description) {
reportError(description);
}
// -----Implementation of PeerConnectionClient.PeerConnectionEvents.---------
// Send local peer connection SDP and ICE candidates to remote party.
// All callbacks are invoked from peer connection client looper thread and
// are routed to UI thread.
@Override
public void onLocalDescription(final SessionDescription sdp) {
final long delta = System.currentTimeMillis() - callStartedTimeMs;
runOnUiThread(new Runnable() {
@Override
public void run() {
if (appRtcClient != null) {
logAndToast("Sending " + sdp.type + ", delay=" + delta + "ms");
if (signalingParameters.initiator) {
appRtcClient.sendOfferSdp(sdp);
} else {
appRtcClient.sendAnswerSdp(sdp);
}
}
}
});
}
@Override
public void onIceCandidate(final IceCandidate candidate) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (appRtcClient != null) {
appRtcClient.sendLocalIceCandidate(candidate);
}
}
});
}
@Override
public void onIceConnected() {
final long delta = System.currentTimeMillis() - callStartedTimeMs;
runOnUiThread(new Runnable() {
@Override
public void run() {
logAndToast("ICE connected, delay=" + delta + "ms");
iceConnected = true;
callConnected();
}
});
}
@Override
public void onIceDisconnected() {
runOnUiThread(new Runnable() {
@Override
public void run() {
logAndToast("ICE disconnected");
iceConnected = false;
disconnect();
}
});
}
@Override
public void onPeerConnectionClosed() {
}
@Override
public void onPeerConnectionStatsReady(final StatsReport[] reports) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (!isError && iceConnected) {
hudFragment.updateEncoderStatistics(reports);
}
}
});
}
@Override
public void onPeerConnectionError(final String description) {
reportError(description);
}
}

View File

@ -0,0 +1,118 @@
/*
* Copyright 2015 The WebRTC Project Authors. All rights reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree. An additional intellectual property rights grant can be found
* in the file PATENTS. All contributing project authors may
* be found in the AUTHORS file in the root of the source tree.
*/
package org.appspot.apprtc;
import android.app.Activity;
import android.app.Fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.TextView;
import org.webrtc.VideoRendererGui.ScalingType;
/**
* Fragment for call control.
*/
public class CallFragment extends Fragment {
private View controlView;
private TextView contactView;
private ImageButton disconnectButton;
private ImageButton cameraSwitchButton;
private ImageButton videoScalingButton;
private OnCallEvents callEvents;
private ScalingType scalingType;
private boolean videoCallEnabled = true;
/**
* Call control interface for container activity.
*/
public interface OnCallEvents {
public void onCallHangUp();
public void onCameraSwitch();
public void onVideoScalingSwitch(ScalingType scalingType);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
controlView =
inflater.inflate(R.layout.fragment_call, container, false);
// Create UI controls.
contactView =
(TextView) controlView.findViewById(R.id.contact_name_call);
disconnectButton =
(ImageButton) controlView.findViewById(R.id.button_call_disconnect);
cameraSwitchButton =
(ImageButton) controlView.findViewById(R.id.button_call_switch_camera);
videoScalingButton =
(ImageButton) controlView.findViewById(R.id.button_call_scaling_mode);
// Add buttons click events.
disconnectButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
callEvents.onCallHangUp();
}
});
cameraSwitchButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
callEvents.onCameraSwitch();
}
});
videoScalingButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (scalingType == ScalingType.SCALE_ASPECT_FILL) {
videoScalingButton.setBackgroundResource(
R.drawable.ic_action_full_screen);
scalingType = ScalingType.SCALE_ASPECT_FIT;
} else {
videoScalingButton.setBackgroundResource(
R.drawable.ic_action_return_from_full_screen);
scalingType = ScalingType.SCALE_ASPECT_FILL;
}
callEvents.onVideoScalingSwitch(scalingType);
}
});
scalingType = ScalingType.SCALE_ASPECT_FILL;
return controlView;
}
@Override
public void onStart() {
super.onStart();
Bundle args = getArguments();
if (args != null) {
String contactName = args.getString(CallActivity.EXTRA_ROOMID);
contactView.setText(contactName);
videoCallEnabled = args.getBoolean(CallActivity.EXTRA_VIDEO_CALL, true);
}
if (!videoCallEnabled) {
cameraSwitchButton.setVisibility(View.INVISIBLE);
}
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
callEvents = (OnCallEvents) activity;
}
}

View File

@ -0,0 +1,403 @@
/*
* Copyright 2014 The WebRTC Project Authors. All rights reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree. An additional intellectual property rights grant can be found
* in the file PATENTS. All contributing project authors may
* be found in the AUTHORS file in the root of the source tree.
*/
package org.appspot.apprtc;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.inputmethod.EditorInfo;
import android.webkit.URLUtil;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ListView;
import android.widget.TextView;
import org.json.JSONArray;
import org.json.JSONException;
import java.util.ArrayList;
import java.util.Random;
/**
* Handles the initial setup where the user selects which room to join.
*/
public class ConnectActivity extends Activity {
private static final String TAG = "ConnectActivity";
private static final int CONNECTION_REQUEST = 1;
private static boolean commandLineRun = false;
private ImageButton addRoomButton;
private ImageButton removeRoomButton;
private ImageButton connectButton;
private ImageButton connectLoopbackButton;
private EditText roomEditText;
private ListView roomListView;
private SharedPreferences sharedPref;
private String keyprefVideoCallEnabled;
private String keyprefResolution;
private String keyprefFps;
private String keyprefVideoBitrateType;
private String keyprefVideoBitrateValue;
private String keyprefVideoCodec;
private String keyprefAudioBitrateType;
private String keyprefAudioBitrateValue;
private String keyprefAudioCodec;
private String keyprefHwCodecAcceleration;
private String keyprefNoAudioProcessingPipeline;
private String keyprefCpuUsageDetection;
private String keyprefDisplayHud;
private String keyprefRoomServerUrl;
private String keyprefRoom;
private String keyprefRoomList;
private ArrayList<String> roomList;
private ArrayAdapter<String> adapter;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Get setting keys.
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
keyprefVideoCallEnabled = getString(R.string.pref_videocall_key);
keyprefResolution = getString(R.string.pref_resolution_key);
keyprefFps = getString(R.string.pref_fps_key);
keyprefVideoBitrateType = getString(R.string.pref_startvideobitrate_key);
keyprefVideoBitrateValue = getString(R.string.pref_startvideobitratevalue_key);
keyprefVideoCodec = getString(R.string.pref_videocodec_key);
keyprefHwCodecAcceleration = getString(R.string.pref_hwcodec_key);
keyprefAudioBitrateType = getString(R.string.pref_startaudiobitrate_key);
keyprefAudioBitrateValue = getString(R.string.pref_startaudiobitratevalue_key);
keyprefAudioCodec = getString(R.string.pref_audiocodec_key);
keyprefNoAudioProcessingPipeline = getString(R.string.pref_noaudioprocessing_key);
keyprefCpuUsageDetection = getString(R.string.pref_cpu_usage_detection_key);
keyprefDisplayHud = getString(R.string.pref_displayhud_key);
keyprefRoomServerUrl = getString(R.string.pref_room_server_url_key);
keyprefRoom = getString(R.string.pref_room_key);
keyprefRoomList = getString(R.string.pref_room_list_key);
setContentView(R.layout.activity_connect);
roomEditText = (EditText) findViewById(R.id.room_edittext);
roomEditText.setOnEditorActionListener(
new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(
TextView textView, int i, KeyEvent keyEvent) {
if (i == EditorInfo.IME_ACTION_DONE) {
addRoomButton.performClick();
return true;
}
return false;
}
});
roomEditText.requestFocus();
roomListView = (ListView) findViewById(R.id.room_listview);
roomListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
addRoomButton = (ImageButton) findViewById(R.id.add_room_button);
addRoomButton.setOnClickListener(addRoomListener);
removeRoomButton = (ImageButton) findViewById(R.id.remove_room_button);
removeRoomButton.setOnClickListener(removeRoomListener);
connectButton = (ImageButton) findViewById(R.id.connect_button);
connectButton.setOnClickListener(connectListener);
connectLoopbackButton =
(ImageButton) findViewById(R.id.connect_loopback_button);
connectLoopbackButton.setOnClickListener(connectListener);
// If an implicit VIEW intent is launching the app, go directly to that URL.
final Intent intent = getIntent();
if ("android.intent.action.VIEW".equals(intent.getAction())
&& !commandLineRun) {
commandLineRun = true;
boolean loopback = intent.getBooleanExtra(
CallActivity.EXTRA_LOOPBACK, false);
int runTimeMs = intent.getIntExtra(
CallActivity.EXTRA_RUNTIME, 0);
String room = sharedPref.getString(keyprefRoom, "");
roomEditText.setText(room);
connectToRoom(loopback, runTimeMs);
return;
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.connect_menu, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle presses on the action bar items.
if (item.getItemId() == R.id.action_settings) {
Intent intent = new Intent(this, SettingsActivity.class);
startActivity(intent);
return true;
} else {
return super.onOptionsItemSelected(item);
}
}
@Override
public void onPause() {
super.onPause();
String room = roomEditText.getText().toString();
String roomListJson = new JSONArray(roomList).toString();
SharedPreferences.Editor editor = sharedPref.edit();
editor.putString(keyprefRoom, room);
editor.putString(keyprefRoomList, roomListJson);
editor.commit();
}
@Override
public void onResume() {
super.onResume();
String room = sharedPref.getString(keyprefRoom, "");
roomEditText.setText(room);
roomList = new ArrayList<String>();
String roomListJson = sharedPref.getString(keyprefRoomList, null);
if (roomListJson != null) {
try {
JSONArray jsonArray = new JSONArray(roomListJson);
for (int i = 0; i < jsonArray.length(); i++) {
roomList.add(jsonArray.get(i).toString());
}
} catch (JSONException e) {
Log.e(TAG, "Failed to load room list: " + e.toString());
}
}
adapter = new ArrayAdapter<String>(
this, android.R.layout.simple_list_item_1, roomList);
roomListView.setAdapter(adapter);
if (adapter.getCount() > 0) {
roomListView.requestFocus();
roomListView.setItemChecked(0, true);
}
}
@Override
protected void onActivityResult(
int requestCode, int resultCode, Intent data) {
if (requestCode == CONNECTION_REQUEST && commandLineRun) {
Log.d(TAG, "Return: " + resultCode);
setResult(resultCode);
commandLineRun = false;
finish();
}
}
private final OnClickListener connectListener = new OnClickListener() {
@Override
public void onClick(View view) {
boolean loopback = false;
if (view.getId() == R.id.connect_loopback_button) {
loopback = true;
}
commandLineRun = false;
connectToRoom(loopback, 0);
}
};
private void connectToRoom(boolean loopback, int runTimeMs) {
// Get room name (random for loopback).
String roomId;
if (loopback) {
roomId = Integer.toString((new Random()).nextInt(100000000));
} else {
roomId = getSelectedItem();
if (roomId == null) {
roomId = roomEditText.getText().toString();
}
}
String roomUrl = sharedPref.getString(
keyprefRoomServerUrl,
getString(R.string.pref_room_server_url_default));
// Video call enabled flag.
boolean videoCallEnabled = sharedPref.getBoolean(keyprefVideoCallEnabled,
Boolean.valueOf(getString(R.string.pref_videocall_default)));
// Get default codecs.
String videoCodec = sharedPref.getString(keyprefVideoCodec,
getString(R.string.pref_videocodec_default));
String audioCodec = sharedPref.getString(keyprefAudioCodec,
getString(R.string.pref_audiocodec_default));
// Check HW codec flag.
boolean hwCodec = sharedPref.getBoolean(keyprefHwCodecAcceleration,
Boolean.valueOf(getString(R.string.pref_hwcodec_default)));
// Check Disable Audio Processing flag.
boolean noAudioProcessing = sharedPref.getBoolean(
keyprefNoAudioProcessingPipeline,
Boolean.valueOf(getString(R.string.pref_noaudioprocessing_default)));
// Get video resolution from settings.
int videoWidth = 0;
int videoHeight = 0;
String resolution = sharedPref.getString(keyprefResolution,
getString(R.string.pref_resolution_default));
String[] dimensions = resolution.split("[ x]+");
if (dimensions.length == 2) {
try {
videoWidth = Integer.parseInt(dimensions[0]);
videoHeight = Integer.parseInt(dimensions[1]);
} catch (NumberFormatException e) {
videoWidth = 0;
videoHeight = 0;
Log.e(TAG, "Wrong video resolution setting: " + resolution);
}
}
// Get camera fps from settings.
int cameraFps = 0;
String fps = sharedPref.getString(keyprefFps,
getString(R.string.pref_fps_default));
String[] fpsValues = fps.split("[ x]+");
if (fpsValues.length == 2) {
try {
cameraFps = Integer.parseInt(fpsValues[0]);
} catch (NumberFormatException e) {
Log.e(TAG, "Wrong camera fps setting: " + fps);
}
}
// Get video and audio start bitrate.
int videoStartBitrate = 0;
String bitrateTypeDefault = getString(
R.string.pref_startvideobitrate_default);
String bitrateType = sharedPref.getString(
keyprefVideoBitrateType, bitrateTypeDefault);
if (!bitrateType.equals(bitrateTypeDefault)) {
String bitrateValue = sharedPref.getString(keyprefVideoBitrateValue,
getString(R.string.pref_startvideobitratevalue_default));
videoStartBitrate = Integer.parseInt(bitrateValue);
}
int audioStartBitrate = 0;
bitrateTypeDefault = getString(R.string.pref_startaudiobitrate_default);
bitrateType = sharedPref.getString(
keyprefAudioBitrateType, bitrateTypeDefault);
if (!bitrateType.equals(bitrateTypeDefault)) {
String bitrateValue = sharedPref.getString(keyprefAudioBitrateValue,
getString(R.string.pref_startaudiobitratevalue_default));
audioStartBitrate = Integer.parseInt(bitrateValue);
}
// Test if CpuOveruseDetection should be disabled. By default is on.
boolean cpuOveruseDetection = sharedPref.getBoolean(
keyprefCpuUsageDetection,
Boolean.valueOf(
getString(R.string.pref_cpu_usage_detection_default)));
// Check statistics display option.
boolean displayHud = sharedPref.getBoolean(keyprefDisplayHud,
Boolean.valueOf(getString(R.string.pref_displayhud_default)));
// Start AppRTCDemo activity.
Log.d(TAG, "Connecting to room " + roomId + " at URL " + roomUrl);
if (validateUrl(roomUrl)) {
Uri uri = Uri.parse(roomUrl);
Intent intent = new Intent(this, CallActivity.class);
intent.setData(uri);
intent.putExtra(CallActivity.EXTRA_ROOMID, roomId);
intent.putExtra(CallActivity.EXTRA_LOOPBACK, loopback);
intent.putExtra(CallActivity.EXTRA_VIDEO_CALL, videoCallEnabled);
intent.putExtra(CallActivity.EXTRA_VIDEO_WIDTH, videoWidth);
intent.putExtra(CallActivity.EXTRA_VIDEO_HEIGHT, videoHeight);
intent.putExtra(CallActivity.EXTRA_VIDEO_FPS, cameraFps);
intent.putExtra(CallActivity.EXTRA_VIDEO_BITRATE, videoStartBitrate);
intent.putExtra(CallActivity.EXTRA_VIDEOCODEC, videoCodec);
intent.putExtra(CallActivity.EXTRA_HWCODEC_ENABLED, hwCodec);
intent.putExtra(CallActivity.EXTRA_NOAUDIOPROCESSING_ENABLED,
noAudioProcessing);
intent.putExtra(CallActivity.EXTRA_AUDIO_BITRATE, audioStartBitrate);
intent.putExtra(CallActivity.EXTRA_AUDIOCODEC, audioCodec);
intent.putExtra(CallActivity.EXTRA_CPUOVERUSE_DETECTION,
cpuOveruseDetection);
intent.putExtra(CallActivity.EXTRA_DISPLAY_HUD, displayHud);
intent.putExtra(CallActivity.EXTRA_CMDLINE, commandLineRun);
intent.putExtra(CallActivity.EXTRA_RUNTIME, runTimeMs);
startActivityForResult(intent, CONNECTION_REQUEST);
}
}
private boolean validateUrl(String url) {
if (URLUtil.isHttpsUrl(url) || URLUtil.isHttpUrl(url)) {
return true;
}
new AlertDialog.Builder(this)
.setTitle(getText(R.string.invalid_url_title))
.setMessage(getString(R.string.invalid_url_text, url))
.setCancelable(false)
.setNeutralButton(R.string.ok, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
dialog.cancel();
}
}).create().show();
return false;
}
private final OnClickListener addRoomListener = new OnClickListener() {
@Override
public void onClick(View view) {
String newRoom = roomEditText.getText().toString();
if (newRoom.length() > 0 && !roomList.contains(newRoom)) {
adapter.add(newRoom);
adapter.notifyDataSetChanged();
}
}
};
private final OnClickListener removeRoomListener = new OnClickListener() {
@Override
public void onClick(View view) {
String selectedRoom = getSelectedItem();
if (selectedRoom != null) {
adapter.remove(selectedRoom);
adapter.notifyDataSetChanged();
}
}
};
private String getSelectedItem() {
int position = AdapterView.INVALID_POSITION;
if (roomListView.getCheckedItemCount() > 0 && adapter.getCount() > 0) {
position = roomListView.getCheckedItemPosition();
if (position >= adapter.getCount()) {
position = AdapterView.INVALID_POSITION;
}
}
if (position != AdapterView.INVALID_POSITION) {
return adapter.getItem(position);
} else {
return null;
}
}
}

View File

@ -0,0 +1,299 @@
/*
* Copyright 2015 The WebRTC Project Authors. All rights reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree. An additional intellectual property rights grant can be found
* in the file PATENTS. All contributing project authors may
* be found in the AUTHORS file in the root of the source tree.
*/
package org.appspot.apprtc;
import android.util.Log;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.InputMismatchException;
import java.util.Scanner;
/**
* Simple CPU monitor. The caller creates a CpuMonitor object which can then
* be used via sampleCpuUtilization() to collect the percentual use of the
* cumulative CPU capacity for all CPUs running at their nominal frequency. 3
* values are generated: (1) getCpuCurrent() returns the use since the last
* sampleCpuUtilization(), (2) getCpuAvg3() returns the use since 3 prior
* calls, and (3) getCpuAvgAll() returns the use over all SAMPLE_SAVE_NUMBER
* calls.
*
* <p>CPUs in Android are often "offline", and while this of course means 0 Hz
* as current frequency, in this state we cannot even get their nominal
* frequency. We therefore tread carefully, and allow any CPU to be missing.
* Missing CPUs are assumed to have the same nominal frequency as any close
* lower-numbered CPU, but as soon as it is online, we'll get their proper
* frequency and remember it. (Since CPU 0 in practice always seem to be
* online, this unidirectional frequency inheritance should be no problem in
* practice.)
*
* <p>Caveats:
* o No provision made for zany "turbo" mode, common in the x86 world.
* o No provision made for ARM big.LITTLE; if CPU n can switch behind our
* back, we might get incorrect estimates.
* o This is not thread-safe. To call asynchronously, create different
* CpuMonitor objects.
*
* <p>If we can gather enough info to generate a sensible result,
* sampleCpuUtilization returns true. It is designed to never through an
* exception.
*
* <p>sampleCpuUtilization should not be called too often in its present form,
* since then deltas would be small and the percent values would fluctuate and
* be unreadable. If it is desirable to call it more often than say once per
* second, one would need to increase SAMPLE_SAVE_NUMBER and probably use
* Queue<Integer> to avoid copying overhead.
*
* <p>Known problems:
* 1. Nexus 7 devices running Kitkat have a kernel which often output an
* incorrect 'idle' field in /proc/stat. The value is close to twice the
* correct value, and then returns to back to correct reading. Both when
* jumping up and back down we might create faulty CPU load readings.
*/
class CpuMonitor {
private static final int SAMPLE_SAVE_NUMBER = 10; // Assumed to be >= 3.
private int[] percentVec = new int[SAMPLE_SAVE_NUMBER];
private int sum3 = 0;
private int sum10 = 0;
private static final String TAG = "CpuMonitor";
private long[] cpuFreq;
private int cpusPresent;
private double lastPercentFreq = -1;
private int cpuCurrent;
private int cpuAvg3;
private int cpuAvgAll;
private boolean initialized = false;
private String[] maxPath;
private String[] curPath;
ProcStat lastProcStat;
private class ProcStat {
final long runTime;
final long idleTime;
ProcStat(long aRunTime, long aIdleTime) {
runTime = aRunTime;
idleTime = aIdleTime;
}
}
private void init() {
try {
FileReader fin = new FileReader("/sys/devices/system/cpu/present");
try {
BufferedReader rdr = new BufferedReader(fin);
Scanner scanner = new Scanner(rdr).useDelimiter("[-\n]");
scanner.nextInt(); // Skip leading number 0.
cpusPresent = 1 + scanner.nextInt();
scanner.close();
} catch (Exception e) {
Log.e(TAG, "Cannot do CPU stats due to /sys/devices/system/cpu/present parsing problem");
} finally {
fin.close();
}
} catch (FileNotFoundException e) {
Log.e(TAG, "Cannot do CPU stats since /sys/devices/system/cpu/present is missing");
} catch (IOException e) {
Log.e(TAG, "Error closing file");
}
cpuFreq = new long [cpusPresent];
maxPath = new String [cpusPresent];
curPath = new String [cpusPresent];
for (int i = 0; i < cpusPresent; i++) {
cpuFreq[i] = 0; // Frequency "not yet determined".
maxPath[i] = "/sys/devices/system/cpu/cpu" + i + "/cpufreq/cpuinfo_max_freq";
curPath[i] = "/sys/devices/system/cpu/cpu" + i + "/cpufreq/scaling_cur_freq";
}
lastProcStat = new ProcStat(0, 0);
initialized = true;
}
/**
* Re-measure CPU use. Call this method at an interval of around 1/s.
* This method returns true on success. The fields
* cpuCurrent, cpuAvg3, and cpuAvgAll are updated on success, and represents:
* cpuCurrent: The CPU use since the last sampleCpuUtilization call.
* cpuAvg3: The average CPU over the last 3 calls.
* cpuAvgAll: The average CPU over the last SAMPLE_SAVE_NUMBER calls.
*/
public boolean sampleCpuUtilization() {
long lastSeenMaxFreq = 0;
long cpufreqCurSum = 0;
long cpufreqMaxSum = 0;
if (!initialized) {
init();
}
for (int i = 0; i < cpusPresent; i++) {
/*
* For each CPU, attempt to first read its max frequency, then its
* current frequency. Once as the max frequency for a CPU is found,
* save it in cpuFreq[].
*/
if (cpuFreq[i] == 0) {
// We have never found this CPU's max frequency. Attempt to read it.
long cpufreqMax = readFreqFromFile(maxPath[i]);
if (cpufreqMax > 0) {
lastSeenMaxFreq = cpufreqMax;
cpuFreq[i] = cpufreqMax;
maxPath[i] = null; // Kill path to free its memory.
}
} else {
lastSeenMaxFreq = cpuFreq[i]; // A valid, previously read value.
}
long cpufreqCur = readFreqFromFile(curPath[i]);
cpufreqCurSum += cpufreqCur;
/* Here, lastSeenMaxFreq might come from
* 1. cpuFreq[i], or
* 2. a previous iteration, or
* 3. a newly read value, or
* 4. hypothetically from the pre-loop dummy.
*/
cpufreqMaxSum += lastSeenMaxFreq;
}
if (cpufreqMaxSum == 0) {
Log.e(TAG, "Could not read max frequency for any CPU");
return false;
}
/*
* Since the cycle counts are for the period between the last invocation
* and this present one, we average the percentual CPU frequencies between
* now and the beginning of the measurement period. This is significantly
* incorrect only if the frequencies have peeked or dropped in between the
* invocations.
*/
double newPercentFreq = 100.0 * cpufreqCurSum / cpufreqMaxSum;
double percentFreq;
if (lastPercentFreq > 0) {
percentFreq = (lastPercentFreq + newPercentFreq) * 0.5;
} else {
percentFreq = newPercentFreq;
}
lastPercentFreq = newPercentFreq;
ProcStat procStat = readIdleAndRunTime();
if (procStat == null) {
return false;
}
long diffRunTime = procStat.runTime - lastProcStat.runTime;
long diffIdleTime = procStat.idleTime - lastProcStat.idleTime;
// Save new measurements for next round's deltas.
lastProcStat = procStat;
long allTime = diffRunTime + diffIdleTime;
int percent = allTime == 0 ? 0 : (int) Math.round(percentFreq * diffRunTime / allTime);
percent = Math.max(0, Math.min(percent, 100));
// Subtract old relevant measurement, add newest.
sum3 += percent - percentVec[2];
// Subtract oldest measurement, add newest.
sum10 += percent - percentVec[SAMPLE_SAVE_NUMBER - 1];
// Rotate saved percent values, save new measurement in vacated spot.
for (int i = SAMPLE_SAVE_NUMBER - 1; i > 0; i--) {
percentVec[i] = percentVec[i - 1];
}
percentVec[0] = percent;
cpuCurrent = percent;
cpuAvg3 = sum3 / 3;
cpuAvgAll = sum10 / SAMPLE_SAVE_NUMBER;
return true;
}
public int getCpuCurrent() {
return cpuCurrent;
}
public int getCpuAvg3() {
return cpuAvg3;
}
public int getCpuAvgAll() {
return cpuAvgAll;
}
/**
* Read a single integer value from the named file. Return the read value
* or if an error occurs return 0.
*/
private long readFreqFromFile(String fileName) {
long number = 0;
try {
FileReader fin = new FileReader(fileName);
try {
BufferedReader rdr = new BufferedReader(fin);
Scanner scannerC = new Scanner(rdr);
number = scannerC.nextLong();
scannerC.close();
} catch (Exception e) {
// CPU presumably got offline just after we opened file.
} finally {
fin.close();
}
} catch (FileNotFoundException e) {
// CPU is offline, not an error.
} catch (IOException e) {
Log.e(TAG, "Error closing file");
}
return number;
}
/*
* Read the current utilization of all CPUs using the cumulative first line
* of /proc/stat.
*/
private ProcStat readIdleAndRunTime() {
long runTime = 0;
long idleTime = 0;
try {
FileReader fin = new FileReader("/proc/stat");
try {
BufferedReader rdr = new BufferedReader(fin);
Scanner scanner = new Scanner(rdr);
scanner.next();
long user = scanner.nextLong();
long nice = scanner.nextLong();
long sys = scanner.nextLong();
runTime = user + nice + sys;
idleTime = scanner.nextLong();
scanner.close();
} catch (Exception e) {
Log.e(TAG, "Problems parsing /proc/stat");
return null;
} finally {
fin.close();
}
} catch (FileNotFoundException e) {
Log.e(TAG, "Cannot open /proc/stat for reading");
return null;
} catch (IOException e) {
Log.e(TAG, "Problems reading /proc/stat");
return null;
}
return new ProcStat(runTime, idleTime);
}
}

View File

@ -0,0 +1,200 @@
/*
* Copyright 2015 The WebRTC Project Authors. All rights reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree. An additional intellectual property rights grant can be found
* in the file PATENTS. All contributing project authors may
* be found in the AUTHORS file in the root of the source tree.
*/
package org.appspot.apprtc;
import android.app.Fragment;
import android.os.Bundle;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.TextView;
import org.webrtc.StatsReport;
import java.util.HashMap;
import java.util.Map;
/**
* Fragment for HUD statistics display.
*/
public class HudFragment extends Fragment {
private View controlView;
private TextView encoderStatView;
private TextView hudViewBwe;
private TextView hudViewConnection;
private TextView hudViewVideoSend;
private TextView hudViewVideoRecv;
private ImageButton toggleDebugButton;
private boolean videoCallEnabled;
private boolean displayHud;
private volatile boolean isRunning;
private final CpuMonitor cpuMonitor = new CpuMonitor();
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
controlView = inflater.inflate(R.layout.fragment_hud, container, false);
// Create UI controls.
encoderStatView = (TextView) controlView.findViewById(R.id.encoder_stat_call);
hudViewBwe = (TextView) controlView.findViewById(R.id.hud_stat_bwe);
hudViewConnection = (TextView) controlView.findViewById(R.id.hud_stat_connection);
hudViewVideoSend = (TextView) controlView.findViewById(R.id.hud_stat_video_send);
hudViewVideoRecv = (TextView) controlView.findViewById(R.id.hud_stat_video_recv);
toggleDebugButton = (ImageButton) controlView.findViewById(R.id.button_toggle_debug);
toggleDebugButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (displayHud) {
int visibility = (hudViewBwe.getVisibility() == View.VISIBLE)
? View.INVISIBLE : View.VISIBLE;
hudViewsSetProperties(visibility);
}
}
});
return controlView;
}
@Override
public void onStart() {
super.onStart();
Bundle args = getArguments();
if (args != null) {
videoCallEnabled = args.getBoolean(CallActivity.EXTRA_VIDEO_CALL, true);
displayHud = args.getBoolean(CallActivity.EXTRA_DISPLAY_HUD, false);
}
int visibility = displayHud ? View.VISIBLE : View.INVISIBLE;
encoderStatView.setVisibility(visibility);
toggleDebugButton.setVisibility(visibility);
hudViewsSetProperties(View.INVISIBLE);
isRunning = true;
}
@Override
public void onStop() {
isRunning = false;
super.onStop();
}
private void hudViewsSetProperties(int visibility) {
hudViewBwe.setVisibility(visibility);
hudViewConnection.setVisibility(visibility);
hudViewVideoSend.setVisibility(visibility);
hudViewVideoRecv.setVisibility(visibility);
hudViewBwe.setTextSize(TypedValue.COMPLEX_UNIT_PT, 5);
hudViewConnection.setTextSize(TypedValue.COMPLEX_UNIT_PT, 5);
hudViewVideoSend.setTextSize(TypedValue.COMPLEX_UNIT_PT, 5);
hudViewVideoRecv.setTextSize(TypedValue.COMPLEX_UNIT_PT, 5);
}
private Map<String, String> getReportMap(StatsReport report) {
Map<String, String> reportMap = new HashMap<String, String>();
for (StatsReport.Value value : report.values) {
reportMap.put(value.name, value.value);
}
return reportMap;
}
public void updateEncoderStatistics(final StatsReport[] reports) {
if (!isRunning || !displayHud) {
return;
}
StringBuilder encoderStat = new StringBuilder(128);
StringBuilder bweStat = new StringBuilder();
StringBuilder connectionStat = new StringBuilder();
StringBuilder videoSendStat = new StringBuilder();
StringBuilder videoRecvStat = new StringBuilder();
String fps = null;
String targetBitrate = null;
String actualBitrate = null;
for (StatsReport report : reports) {
if (report.type.equals("ssrc") && report.id.contains("ssrc")
&& report.id.contains("send")) {
// Send video statistics.
Map<String, String> reportMap = getReportMap(report);
String trackId = reportMap.get("googTrackId");
if (trackId != null && trackId.contains(PeerConnectionClient.VIDEO_TRACK_ID)) {
fps = reportMap.get("googFrameRateSent");
videoSendStat.append(report.id).append("\n");
for (StatsReport.Value value : report.values) {
String name = value.name.replace("goog", "");
videoSendStat.append(name).append("=").append(value.value).append("\n");
}
}
} else if (report.type.equals("ssrc") && report.id.contains("ssrc")
&& report.id.contains("recv")) {
// Receive video statistics.
Map<String, String> reportMap = getReportMap(report);
// Check if this stat is for video track.
String frameWidth = reportMap.get("googFrameWidthReceived");
if (frameWidth != null) {
videoRecvStat.append(report.id).append("\n");
for (StatsReport.Value value : report.values) {
String name = value.name.replace("goog", "");
videoRecvStat.append(name).append("=").append(value.value).append("\n");
}
}
} else if (report.id.equals("bweforvideo")) {
// BWE statistics.
Map<String, String> reportMap = getReportMap(report);
targetBitrate = reportMap.get("googTargetEncBitrate");
actualBitrate = reportMap.get("googActualEncBitrate");
bweStat.append(report.id).append("\n");
for (StatsReport.Value value : report.values) {
String name = value.name.replace("goog", "").replace("Available", "");
bweStat.append(name).append("=").append(value.value).append("\n");
}
} else if (report.type.equals("googCandidatePair")) {
// Connection statistics.
Map<String, String> reportMap = getReportMap(report);
String activeConnection = reportMap.get("googActiveConnection");
if (activeConnection != null && activeConnection.equals("true")) {
connectionStat.append(report.id).append("\n");
for (StatsReport.Value value : report.values) {
String name = value.name.replace("goog", "");
connectionStat.append(name).append("=").append(value.value).append("\n");
}
}
}
}
hudViewBwe.setText(bweStat.toString());
hudViewConnection.setText(connectionStat.toString());
hudViewVideoSend.setText(videoSendStat.toString());
hudViewVideoRecv.setText(videoRecvStat.toString());
if (videoCallEnabled) {
if (fps != null) {
encoderStat.append("Fps: ").append(fps).append("\n");
}
if (targetBitrate != null) {
encoderStat.append("Target BR: ").append(targetBitrate).append("\n");
}
if (actualBitrate != null) {
encoderStat.append("Actual BR: ").append(actualBitrate).append("\n");
}
}
if (cpuMonitor.sampleCpuUtilization()) {
encoderStat.append("CPU%: ")
.append(cpuMonitor.getCpuCurrent()).append("/")
.append(cpuMonitor.getCpuAvg3()).append("/")
.append(cpuMonitor.getCpuAvgAll());
}
encoderStatView.setText(encoderStat.toString());
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,222 @@
/*
* Copyright 2014 The WebRTC Project Authors. All rights reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree. An additional intellectual property rights grant can be found
* in the file PATENTS. All contributing project authors may
* be found in the AUTHORS file in the root of the source tree.
*/
package org.appspot.apprtc;
import org.appspot.apprtc.AppRTCClient.SignalingParameters;
import org.appspot.apprtc.util.AsyncHttpURLConnection;
import org.appspot.apprtc.util.AsyncHttpURLConnection.AsyncHttpEvents;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.webrtc.IceCandidate;
import org.webrtc.PeerConnection;
import org.webrtc.SessionDescription;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.LinkedList;
import java.util.Scanner;
/**
* AsyncTask that converts an AppRTC room URL into the set of signaling
* parameters to use with that room.
*/
public class RoomParametersFetcher {
private static final String TAG = "RoomRTCClient";
private static final int TURN_HTTP_TIMEOUT_MS = 5000;
private final RoomParametersFetcherEvents events;
private final String roomUrl;
private final String roomMessage;
private AsyncHttpURLConnection httpConnection;
/**
* Room parameters fetcher callbacks.
*/
public static interface RoomParametersFetcherEvents {
/**
* Callback fired once the room's signaling parameters
* SignalingParameters are extracted.
*/
public void onSignalingParametersReady(final SignalingParameters params);
/**
* Callback for room parameters extraction error.
*/
public void onSignalingParametersError(final String description);
}
public RoomParametersFetcher(String roomUrl, String roomMessage,
final RoomParametersFetcherEvents events) {
this.roomUrl = roomUrl;
this.roomMessage = roomMessage;
this.events = events;
}
public void makeRequest() {
Log.d(TAG, "Connecting to room: " + roomUrl);
httpConnection = new AsyncHttpURLConnection(
"POST", roomUrl, roomMessage,
new AsyncHttpEvents() {
@Override
public void onHttpError(String errorMessage) {
Log.e(TAG, "Room connection error: " + errorMessage);
events.onSignalingParametersError(errorMessage);
}
@Override
public void onHttpComplete(String response) {
roomHttpResponseParse(response);
}
});
httpConnection.send();
}
private void roomHttpResponseParse(String response) {
Log.d(TAG, "Room response: " + response);
try {
LinkedList<IceCandidate> iceCandidates = null;
SessionDescription offerSdp = null;
JSONObject roomJson = new JSONObject(response);
String result = roomJson.getString("result");
if (!result.equals("SUCCESS")) {
events.onSignalingParametersError("Room response error: " + result);
return;
}
response = roomJson.getString("params");
roomJson = new JSONObject(response);
String roomId = roomJson.getString("room_id");
String clientId = roomJson.getString("client_id");
String wssUrl = roomJson.getString("wss_url");
String wssPostUrl = roomJson.getString("wss_post_url");
boolean initiator = (roomJson.getBoolean("is_initiator"));
if (!initiator) {
iceCandidates = new LinkedList<IceCandidate>();
String messagesString = roomJson.getString("messages");
JSONArray messages = new JSONArray(messagesString);
for (int i = 0; i < messages.length(); ++i) {
String messageString = messages.getString(i);
JSONObject message = new JSONObject(messageString);
String messageType = message.getString("type");
Log.d(TAG, "GAE->C #" + i + " : " + messageString);
if (messageType.equals("offer")) {
offerSdp = new SessionDescription(
SessionDescription.Type.fromCanonicalForm(messageType),
message.getString("sdp"));
} else if (messageType.equals("candidate")) {
IceCandidate candidate = new IceCandidate(
message.getString("id"),
message.getInt("label"),
message.getString("candidate"));
iceCandidates.add(candidate);
} else {
Log.e(TAG, "Unknown message: " + messageString);
}
}
}
Log.d(TAG, "RoomId: " + roomId + ". ClientId: " + clientId);
Log.d(TAG, "Initiator: " + initiator);
Log.d(TAG, "WSS url: " + wssUrl);
Log.d(TAG, "WSS POST url: " + wssPostUrl);
LinkedList<PeerConnection.IceServer> iceServers =
iceServersFromPCConfigJSON(roomJson.getString("pc_config"));
boolean isTurnPresent = false;
for (PeerConnection.IceServer server : iceServers) {
Log.d(TAG, "IceServer: " + server);
if (server.uri.startsWith("turn:")) {
isTurnPresent = true;
break;
}
}
// Request TURN servers.
if (!isTurnPresent) {
LinkedList<PeerConnection.IceServer> turnServers =
requestTurnServers(roomJson.getString("turn_url"));
for (PeerConnection.IceServer turnServer : turnServers) {
Log.d(TAG, "TurnServer: " + turnServer);
iceServers.add(turnServer);
}
}
SignalingParameters params = new SignalingParameters(
iceServers, initiator,
clientId, wssUrl, wssPostUrl,
offerSdp, iceCandidates);
events.onSignalingParametersReady(params);
} catch (JSONException e) {
events.onSignalingParametersError(
"Room JSON parsing error: " + e.toString());
} catch (IOException e) {
events.onSignalingParametersError("Room IO error: " + e.toString());
}
}
// Requests & returns a TURN ICE Server based on a request URL. Must be run
// off the main thread!
private LinkedList<PeerConnection.IceServer> requestTurnServers(String url)
throws IOException, JSONException {
LinkedList<PeerConnection.IceServer> turnServers =
new LinkedList<PeerConnection.IceServer>();
Log.d(TAG, "Request TURN from: " + url);
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
connection.setConnectTimeout(TURN_HTTP_TIMEOUT_MS);
connection.setReadTimeout(TURN_HTTP_TIMEOUT_MS);
int responseCode = connection.getResponseCode();
if (responseCode != 200) {
throw new IOException("Non-200 response when requesting TURN server from "
+ url + " : " + connection.getHeaderField(null));
}
InputStream responseStream = connection.getInputStream();
String response = drainStream(responseStream);
connection.disconnect();
Log.d(TAG, "TURN response: " + response);
JSONObject responseJSON = new JSONObject(response);
String username = responseJSON.getString("username");
String password = responseJSON.getString("password");
JSONArray turnUris = responseJSON.getJSONArray("uris");
for (int i = 0; i < turnUris.length(); i++) {
String uri = turnUris.getString(i);
turnServers.add(new PeerConnection.IceServer(uri, username, password));
}
return turnServers;
}
// Return the list of ICE servers described by a WebRTCPeerConnection
// configuration string.
private LinkedList<PeerConnection.IceServer> iceServersFromPCConfigJSON(
String pcConfig) throws JSONException {
JSONObject json = new JSONObject(pcConfig);
JSONArray servers = json.getJSONArray("iceServers");
LinkedList<PeerConnection.IceServer> ret =
new LinkedList<PeerConnection.IceServer>();
for (int i = 0; i < servers.length(); ++i) {
JSONObject server = servers.getJSONObject(i);
String url = server.getString("urls");
String credential =
server.has("credential") ? server.getString("credential") : "";
ret.add(new PeerConnection.IceServer(url, "", credential));
}
return ret;
}
// Return the contents of an InputStream as a String.
private static String drainStream(InputStream in) {
Scanner s = new Scanner(in).useDelimiter("\\A");
return s.hasNext() ? s.next() : "";
}
}

View File

@ -0,0 +1,177 @@
/*
* Copyright 2014 The WebRTC Project Authors. All rights reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree. An additional intellectual property rights grant can be found
* in the file PATENTS. All contributing project authors may
* be found in the AUTHORS file in the root of the source tree.
*/
package org.appspot.apprtc;
import android.app.Activity;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.os.Bundle;
import android.preference.Preference;
/**
* Settings activity for AppRTC.
*/
public class SettingsActivity extends Activity
implements OnSharedPreferenceChangeListener{
private SettingsFragment settingsFragment;
private String keyprefVideoCall;
private String keyprefResolution;
private String keyprefFps;
private String keyprefStartVideoBitrateType;
private String keyprefStartVideoBitrateValue;
private String keyPrefVideoCodec;
private String keyprefHwCodec;
private String keyprefStartAudioBitrateType;
private String keyprefStartAudioBitrateValue;
private String keyPrefAudioCodec;
private String keyprefNoAudioProcessing;
private String keyprefCpuUsageDetection;
private String keyPrefRoomServerUrl;
private String keyPrefDisplayHud;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
keyprefVideoCall = getString(R.string.pref_videocall_key);
keyprefResolution = getString(R.string.pref_resolution_key);
keyprefFps = getString(R.string.pref_fps_key);
keyprefStartVideoBitrateType = getString(R.string.pref_startvideobitrate_key);
keyprefStartVideoBitrateValue = getString(R.string.pref_startvideobitratevalue_key);
keyPrefVideoCodec = getString(R.string.pref_videocodec_key);
keyprefHwCodec = getString(R.string.pref_hwcodec_key);
keyprefStartAudioBitrateType = getString(R.string.pref_startaudiobitrate_key);
keyprefStartAudioBitrateValue = getString(R.string.pref_startaudiobitratevalue_key);
keyPrefAudioCodec = getString(R.string.pref_audiocodec_key);
keyprefNoAudioProcessing = getString(R.string.pref_noaudioprocessing_key);
keyprefCpuUsageDetection = getString(R.string.pref_cpu_usage_detection_key);
keyPrefRoomServerUrl = getString(R.string.pref_room_server_url_key);
keyPrefDisplayHud = getString(R.string.pref_displayhud_key);
// Display the fragment as the main content.
settingsFragment = new SettingsFragment();
getFragmentManager().beginTransaction()
.replace(android.R.id.content, settingsFragment)
.commit();
}
@Override
protected void onResume() {
super.onResume();
// Set summary to be the user-description for the selected value
SharedPreferences sharedPreferences =
settingsFragment.getPreferenceScreen().getSharedPreferences();
sharedPreferences.registerOnSharedPreferenceChangeListener(this);
updateSummaryB(sharedPreferences, keyprefVideoCall);
updateSummary(sharedPreferences, keyprefResolution);
updateSummary(sharedPreferences, keyprefFps);
updateSummary(sharedPreferences, keyprefStartVideoBitrateType);
updateSummaryBitrate(sharedPreferences, keyprefStartVideoBitrateValue);
setVideoBitrateEnable(sharedPreferences);
updateSummary(sharedPreferences, keyPrefVideoCodec);
updateSummaryB(sharedPreferences, keyprefHwCodec);
updateSummary(sharedPreferences, keyprefStartAudioBitrateType);
updateSummaryBitrate(sharedPreferences, keyprefStartAudioBitrateValue);
setAudioBitrateEnable(sharedPreferences);
updateSummary(sharedPreferences, keyPrefAudioCodec);
updateSummaryB(sharedPreferences, keyprefNoAudioProcessing);
updateSummaryB(sharedPreferences, keyprefCpuUsageDetection);
updateSummary(sharedPreferences, keyPrefRoomServerUrl);
updateSummaryB(sharedPreferences, keyPrefDisplayHud);
}
@Override
protected void onPause() {
super.onPause();
SharedPreferences sharedPreferences =
settingsFragment.getPreferenceScreen().getSharedPreferences();
sharedPreferences.unregisterOnSharedPreferenceChangeListener(this);
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
String key) {
if (key.equals(keyprefResolution)
|| key.equals(keyprefFps)
|| key.equals(keyprefStartVideoBitrateType)
|| key.equals(keyPrefVideoCodec)
|| key.equals(keyprefStartAudioBitrateType)
|| key.equals(keyPrefAudioCodec)
|| key.equals(keyPrefRoomServerUrl)) {
updateSummary(sharedPreferences, key);
} else if (key.equals(keyprefStartVideoBitrateValue)
|| key.equals(keyprefStartAudioBitrateValue)) {
updateSummaryBitrate(sharedPreferences, key);
} else if (key.equals(keyprefVideoCall)
|| key.equals(keyprefHwCodec)
|| key.equals(keyprefNoAudioProcessing)
|| key.equals(keyprefCpuUsageDetection)
|| key.equals(keyPrefDisplayHud)) {
updateSummaryB(sharedPreferences, key);
}
if (key.equals(keyprefStartVideoBitrateType)) {
setVideoBitrateEnable(sharedPreferences);
}
if (key.equals(keyprefStartAudioBitrateType)) {
setAudioBitrateEnable(sharedPreferences);
}
}
private void updateSummary(SharedPreferences sharedPreferences, String key) {
Preference updatedPref = settingsFragment.findPreference(key);
// Set summary to be the user-description for the selected value
updatedPref.setSummary(sharedPreferences.getString(key, ""));
}
private void updateSummaryBitrate(
SharedPreferences sharedPreferences, String key) {
Preference updatedPref = settingsFragment.findPreference(key);
updatedPref.setSummary(sharedPreferences.getString(key, "") + " kbps");
}
private void updateSummaryB(SharedPreferences sharedPreferences, String key) {
Preference updatedPref = settingsFragment.findPreference(key);
updatedPref.setSummary(sharedPreferences.getBoolean(key, true)
? getString(R.string.pref_value_enabled)
: getString(R.string.pref_value_disabled));
}
private void setVideoBitrateEnable(SharedPreferences sharedPreferences) {
Preference bitratePreferenceValue =
settingsFragment.findPreference(keyprefStartVideoBitrateValue);
String bitrateTypeDefault = getString(R.string.pref_startvideobitrate_default);
String bitrateType = sharedPreferences.getString(
keyprefStartVideoBitrateType, bitrateTypeDefault);
if (bitrateType.equals(bitrateTypeDefault)) {
bitratePreferenceValue.setEnabled(false);
} else {
bitratePreferenceValue.setEnabled(true);
}
}
private void setAudioBitrateEnable(SharedPreferences sharedPreferences) {
Preference bitratePreferenceValue =
settingsFragment.findPreference(keyprefStartAudioBitrateValue);
String bitrateTypeDefault = getString(R.string.pref_startaudiobitrate_default);
String bitrateType = sharedPreferences.getString(
keyprefStartAudioBitrateType, bitrateTypeDefault);
if (bitrateType.equals(bitrateTypeDefault)) {
bitratePreferenceValue.setEnabled(false);
} else {
bitratePreferenceValue.setEnabled(true);
}
}
}

View File

@ -0,0 +1,27 @@
/*
* Copyright 2014 The WebRTC Project Authors. All rights reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree. An additional intellectual property rights grant can be found
* in the file PATENTS. All contributing project authors may
* be found in the AUTHORS file in the root of the source tree.
*/
package org.appspot.apprtc;
import android.os.Bundle;
import android.preference.PreferenceFragment;
/**
* Settings fragment for AppRTC.
*/
public class SettingsFragment extends PreferenceFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.preferences);
}
}

View File

@ -0,0 +1,86 @@
/*
* Copyright 2013 The WebRTC Project Authors. All rights reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree. An additional intellectual property rights grant can be found
* in the file PATENTS. All contributing project authors may
* be found in the AUTHORS file in the root of the source tree.
*/
package org.appspot.apprtc;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.util.Log;
import android.util.TypedValue;
import android.widget.ScrollView;
import android.widget.TextView;
import java.io.PrintWriter;
import java.io.StringWriter;
/**
* Singleton helper: install a default unhandled exception handler which shows
* an informative dialog and kills the app. Useful for apps whose
* error-handling consists of throwing RuntimeExceptions.
* NOTE: almost always more useful to
* Thread.setDefaultUncaughtExceptionHandler() rather than
* Thread.setUncaughtExceptionHandler(), to apply to background threads as well.
*/
public class UnhandledExceptionHandler
implements Thread.UncaughtExceptionHandler {
private static final String TAG = "AppRTCDemoActivity";
private final Activity activity;
public UnhandledExceptionHandler(final Activity activity) {
this.activity = activity;
}
public void uncaughtException(Thread unusedThread, final Throwable e) {
activity.runOnUiThread(new Runnable() {
@Override public void run() {
String title = "Fatal error: " + getTopLevelCauseMessage(e);
String msg = getRecursiveStackTrace(e);
TextView errorView = new TextView(activity);
errorView.setText(msg);
errorView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 8);
ScrollView scrollingContainer = new ScrollView(activity);
scrollingContainer.addView(errorView);
Log.e(TAG, title + "\n\n" + msg);
DialogInterface.OnClickListener listener =
new DialogInterface.OnClickListener() {
@Override public void onClick(
DialogInterface dialog, int which) {
dialog.dismiss();
System.exit(1);
}
};
AlertDialog.Builder builder =
new AlertDialog.Builder(activity);
builder
.setTitle(title)
.setView(scrollingContainer)
.setPositiveButton("Exit", listener).show();
}
});
}
// Returns the Message attached to the original Cause of |t|.
private static String getTopLevelCauseMessage(Throwable t) {
Throwable topLevelCause = t;
while (topLevelCause.getCause() != null) {
topLevelCause = topLevelCause.getCause();
}
return topLevelCause.getMessage();
}
// Returns a human-readable String of the stacktrace in |t|, recursively
// through all Causes that led to |t|.
private static String getRecursiveStackTrace(Throwable t) {
StringWriter writer = new StringWriter();
t.printStackTrace(new PrintWriter(writer));
return writer.toString();
}
}

View File

@ -0,0 +1,305 @@
/*
* Copyright 2014 The WebRTC Project Authors. All rights reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree. An additional intellectual property rights grant can be found
* in the file PATENTS. All contributing project authors may
* be found in the AUTHORS file in the root of the source tree.
*/
package org.appspot.apprtc;
import org.appspot.apprtc.util.AsyncHttpURLConnection;
import org.appspot.apprtc.util.AsyncHttpURLConnection.AsyncHttpEvents;
import org.appspot.apprtc.util.LooperExecutor;
import android.util.Log;
import de.tavendo.autobahn.WebSocket.WebSocketConnectionObserver;
import de.tavendo.autobahn.WebSocketConnection;
import de.tavendo.autobahn.WebSocketException;
import org.json.JSONException;
import org.json.JSONObject;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.LinkedList;
/**
* WebSocket client implementation.
*
* <p>All public methods should be called from a looper executor thread
* passed in a constructor, otherwise exception will be thrown.
* All events are dispatched on the same thread.
*/
public class WebSocketChannelClient {
private static final String TAG = "WSChannelRTCClient";
private static final int CLOSE_TIMEOUT = 1000;
private final WebSocketChannelEvents events;
private final LooperExecutor executor;
private WebSocketConnection ws;
private WebSocketObserver wsObserver;
private String wsServerUrl;
private String postServerUrl;
private String roomID;
private String clientID;
private WebSocketConnectionState state;
private final Object closeEventLock = new Object();
private boolean closeEvent;
// WebSocket send queue. Messages are added to the queue when WebSocket
// client is not registered and are consumed in register() call.
private final LinkedList<String> wsSendQueue;
/**
* Possible WebSocket connection states.
*/
public enum WebSocketConnectionState {
NEW, CONNECTED, REGISTERED, CLOSED, ERROR
};
/**
* Callback interface for messages delivered on WebSocket.
* All events are dispatched from a looper executor thread.
*/
public interface WebSocketChannelEvents {
public void onWebSocketMessage(final String message);
public void onWebSocketClose();
public void onWebSocketError(final String description);
}
public WebSocketChannelClient(LooperExecutor executor, WebSocketChannelEvents events) {
this.executor = executor;
this.events = events;
roomID = null;
clientID = null;
wsSendQueue = new LinkedList<String>();
state = WebSocketConnectionState.NEW;
}
public WebSocketConnectionState getState() {
return state;
}
public void connect(final String wsUrl, final String postUrl) {
checkIfCalledOnValidThread();
if (state != WebSocketConnectionState.NEW) {
Log.e(TAG, "WebSocket is already connected.");
return;
}
wsServerUrl = wsUrl;
postServerUrl = postUrl;
closeEvent = false;
Log.d(TAG, "Connecting WebSocket to: " + wsUrl + ". Post URL: " + postUrl);
ws = new WebSocketConnection();
wsObserver = new WebSocketObserver();
try {
ws.connect(new URI(wsServerUrl), wsObserver);
} catch (URISyntaxException e) {
reportError("URI error: " + e.getMessage());
} catch (WebSocketException e) {
reportError("WebSocket connection error: " + e.getMessage());
}
}
public void register(final String roomID, final String clientID) {
checkIfCalledOnValidThread();
this.roomID = roomID;
this.clientID = clientID;
if (state != WebSocketConnectionState.CONNECTED) {
Log.w(TAG, "WebSocket register() in state " + state);
return;
}
Log.d(TAG, "Registering WebSocket for room " + roomID + ". CLientID: " + clientID);
JSONObject json = new JSONObject();
try {
json.put("cmd", "register");
json.put("roomid", roomID);
json.put("clientid", clientID);
Log.d(TAG, "C->WSS: " + json.toString());
ws.sendTextMessage(json.toString());
state = WebSocketConnectionState.REGISTERED;
// Send any previously accumulated messages.
for (String sendMessage : wsSendQueue) {
send(sendMessage);
}
wsSendQueue.clear();
} catch (JSONException e) {
reportError("WebSocket register JSON error: " + e.getMessage());
}
}
public void send(String message) {
checkIfCalledOnValidThread();
switch (state) {
case NEW:
case CONNECTED:
// Store outgoing messages and send them after websocket client
// is registered.
Log.d(TAG, "WS ACC: " + message);
wsSendQueue.add(message);
return;
case ERROR:
case CLOSED:
Log.e(TAG, "WebSocket send() in error or closed state : " + message);
return;
case REGISTERED:
JSONObject json = new JSONObject();
try {
json.put("cmd", "send");
json.put("msg", message);
message = json.toString();
Log.d(TAG, "C->WSS: " + message);
ws.sendTextMessage(message);
} catch (JSONException e) {
reportError("WebSocket send JSON error: " + e.getMessage());
}
break;
}
return;
}
// This call can be used to send WebSocket messages before WebSocket
// connection is opened.
public void post(String message) {
checkIfCalledOnValidThread();
sendWSSMessage("POST", message);
}
public void disconnect(boolean waitForComplete) {
checkIfCalledOnValidThread();
Log.d(TAG, "Disonnect WebSocket. State: " + state);
if (state == WebSocketConnectionState.REGISTERED) {
// Send "bye" to WebSocket server.
send("{\"type\": \"bye\"}");
state = WebSocketConnectionState.CONNECTED;
// Send http DELETE to http WebSocket server.
sendWSSMessage("DELETE", "");
}
// Close WebSocket in CONNECTED or ERROR states only.
if (state == WebSocketConnectionState.CONNECTED
|| state == WebSocketConnectionState.ERROR) {
ws.disconnect();
state = WebSocketConnectionState.CLOSED;
// Wait for websocket close event to prevent websocket library from
// sending any pending messages to deleted looper thread.
if (waitForComplete) {
synchronized (closeEventLock) {
while (!closeEvent) {
try {
closeEventLock.wait(CLOSE_TIMEOUT);
break;
} catch (InterruptedException e) {
Log.e(TAG, "Wait error: " + e.toString());
}
}
}
}
}
Log.d(TAG, "Disonnecting WebSocket done.");
}
private void reportError(final String errorMessage) {
Log.e(TAG, errorMessage);
executor.execute(new Runnable() {
@Override
public void run() {
if (state != WebSocketConnectionState.ERROR) {
state = WebSocketConnectionState.ERROR;
events.onWebSocketError(errorMessage);
}
}
});
}
// Asynchronously send POST/DELETE to WebSocket server.
private void sendWSSMessage(final String method, final String message) {
String postUrl = postServerUrl + "/" + roomID + "/" + clientID;
Log.d(TAG, "WS " + method + " : " + postUrl + " : " + message);
AsyncHttpURLConnection httpConnection = new AsyncHttpURLConnection(
method, postUrl, message, new AsyncHttpEvents() {
@Override
public void onHttpError(String errorMessage) {
reportError("WS " + method + " error: " + errorMessage);
}
@Override
public void onHttpComplete(String response) {
}
});
httpConnection.send();
}
// Helper method for debugging purposes. Ensures that WebSocket method is
// called on a looper thread.
private void checkIfCalledOnValidThread() {
if (!executor.checkOnLooperThread()) {
throw new IllegalStateException(
"WebSocket method is not called on valid thread");
}
}
private class WebSocketObserver implements WebSocketConnectionObserver {
@Override
public void onOpen() {
Log.d(TAG, "WebSocket connection opened to: " + wsServerUrl);
executor.execute(new Runnable() {
@Override
public void run() {
state = WebSocketConnectionState.CONNECTED;
// Check if we have pending register request.
if (roomID != null && clientID != null) {
register(roomID, clientID);
}
}
});
}
@Override
public void onClose(WebSocketCloseNotification code, String reason) {
Log.d(TAG, "WebSocket connection closed. Code: " + code
+ ". Reason: " + reason + ". State: " + state);
synchronized (closeEventLock) {
closeEvent = true;
closeEventLock.notify();
}
executor.execute(new Runnable() {
@Override
public void run() {
if (state != WebSocketConnectionState.CLOSED) {
state = WebSocketConnectionState.CLOSED;
events.onWebSocketClose();
}
}
});
}
@Override
public void onTextMessage(String payload) {
Log.d(TAG, "WSS->C: " + payload);
final String message = payload;
executor.execute(new Runnable() {
@Override
public void run() {
if (state == WebSocketConnectionState.CONNECTED
|| state == WebSocketConnectionState.REGISTERED) {
events.onWebSocketMessage(message);
}
}
});
}
@Override
public void onRawTextMessage(byte[] payload) {
}
@Override
public void onBinaryMessage(byte[] payload) {
}
}
}

View File

@ -0,0 +1,379 @@
/*
* Copyright 2014 The WebRTC Project Authors. All rights reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree. An additional intellectual property rights grant can be found
* in the file PATENTS. All contributing project authors may
* be found in the AUTHORS file in the root of the source tree.
*/
package org.appspot.apprtc;
import org.appspot.apprtc.RoomParametersFetcher.RoomParametersFetcherEvents;
import org.appspot.apprtc.WebSocketChannelClient.WebSocketChannelEvents;
import org.appspot.apprtc.WebSocketChannelClient.WebSocketConnectionState;
import org.appspot.apprtc.util.AsyncHttpURLConnection;
import org.appspot.apprtc.util.AsyncHttpURLConnection.AsyncHttpEvents;
import org.appspot.apprtc.util.LooperExecutor;
import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
import org.webrtc.IceCandidate;
import org.webrtc.SessionDescription;
/**
* Negotiates signaling for chatting with apprtc.appspot.com "rooms".
* Uses the client<->server specifics of the apprtc AppEngine webapp.
*
* <p>To use: create an instance of this object (registering a message handler) and
* call connectToRoom(). Once room connection is established
* onConnectedToRoom() callback with room parameters is invoked.
* Messages to other party (with local Ice candidates and answer SDP) can
* be sent after WebSocket connection is established.
*/
public class WebSocketRTCClient implements AppRTCClient,
WebSocketChannelEvents {
private static final String TAG = "WSRTCClient";
private static final String ROOM_JOIN = "join";
private static final String ROOM_MESSAGE = "message";
private static final String ROOM_LEAVE = "leave";
private enum ConnectionState {
NEW, CONNECTED, CLOSED, ERROR
};
private enum MessageType {
MESSAGE, LEAVE
};
private final LooperExecutor executor;
private boolean initiator;
private SignalingEvents events;
private WebSocketChannelClient wsClient;
private ConnectionState roomState;
private RoomConnectionParameters connectionParameters;
private String messageUrl;
private String leaveUrl;
public WebSocketRTCClient(SignalingEvents events, LooperExecutor executor) {
this.events = events;
this.executor = executor;
roomState = ConnectionState.NEW;
executor.requestStart();
}
// --------------------------------------------------------------------
// AppRTCClient interface implementation.
// Asynchronously connect to an AppRTC room URL using supplied connection
// parameters, retrieves room parameters and connect to WebSocket server.
@Override
public void connectToRoom(RoomConnectionParameters connectionParameters) {
this.connectionParameters = connectionParameters;
executor.execute(new Runnable() {
@Override
public void run() {
connectToRoomInternal();
}
});
}
@Override
public void disconnectFromRoom() {
executor.execute(new Runnable() {
@Override
public void run() {
disconnectFromRoomInternal();
}
});
executor.requestStop();
}
// Connects to room - function runs on a local looper thread.
private void connectToRoomInternal() {
String connectionUrl = getConnectionUrl(connectionParameters);
Log.d(TAG, "Connect to room: " + connectionUrl);
roomState = ConnectionState.NEW;
wsClient = new WebSocketChannelClient(executor, this);
RoomParametersFetcherEvents callbacks = new RoomParametersFetcherEvents() {
@Override
public void onSignalingParametersReady(
final SignalingParameters params) {
WebSocketRTCClient.this.executor.execute(new Runnable() {
@Override
public void run() {
WebSocketRTCClient.this.signalingParametersReady(params);
}
});
}
@Override
public void onSignalingParametersError(String description) {
WebSocketRTCClient.this.reportError(description);
}
};
new RoomParametersFetcher(connectionUrl, null, callbacks).makeRequest();
}
// Disconnect from room and send bye messages - runs on a local looper thread.
private void disconnectFromRoomInternal() {
Log.d(TAG, "Disconnect. Room state: " + roomState);
if (roomState == ConnectionState.CONNECTED) {
Log.d(TAG, "Closing room.");
sendPostMessage(MessageType.LEAVE, leaveUrl, null);
}
roomState = ConnectionState.CLOSED;
if (wsClient != null) {
wsClient.disconnect(true);
}
}
// Helper functions to get connection, post message and leave message URLs
private String getConnectionUrl(
RoomConnectionParameters connectionParameters) {
return connectionParameters.roomUrl + "/" + ROOM_JOIN + "/"
+ connectionParameters.roomId;
}
private String getMessageUrl(RoomConnectionParameters connectionParameters,
SignalingParameters signalingParameters) {
return connectionParameters.roomUrl + "/" + ROOM_MESSAGE + "/"
+ connectionParameters.roomId + "/" + signalingParameters.clientId;
}
private String getLeaveUrl(RoomConnectionParameters connectionParameters,
SignalingParameters signalingParameters) {
return connectionParameters.roomUrl + "/" + ROOM_LEAVE + "/"
+ connectionParameters.roomId + "/" + signalingParameters.clientId;
}
// Callback issued when room parameters are extracted. Runs on local
// looper thread.
private void signalingParametersReady(
final SignalingParameters signalingParameters) {
Log.d(TAG, "Room connection completed.");
if (connectionParameters.loopback
&& (!signalingParameters.initiator
|| signalingParameters.offerSdp != null)) {
reportError("Loopback room is busy.");
return;
}
if (!connectionParameters.loopback
&& !signalingParameters.initiator
&& signalingParameters.offerSdp == null) {
Log.w(TAG, "No offer SDP in room response.");
}
initiator = signalingParameters.initiator;
messageUrl = getMessageUrl(connectionParameters, signalingParameters);
leaveUrl = getLeaveUrl(connectionParameters, signalingParameters);
Log.d(TAG, "Message URL: " + messageUrl);
Log.d(TAG, "Leave URL: " + leaveUrl);
roomState = ConnectionState.CONNECTED;
// Fire connection and signaling parameters events.
events.onConnectedToRoom(signalingParameters);
// Connect and register WebSocket client.
wsClient.connect(signalingParameters.wssUrl, signalingParameters.wssPostUrl);
wsClient.register(connectionParameters.roomId, signalingParameters.clientId);
}
// Send local offer SDP to the other participant.
@Override
public void sendOfferSdp(final SessionDescription sdp) {
executor.execute(new Runnable() {
@Override
public void run() {
if (roomState != ConnectionState.CONNECTED) {
reportError("Sending offer SDP in non connected state.");
return;
}
JSONObject json = new JSONObject();
jsonPut(json, "sdp", sdp.description);
jsonPut(json, "type", "offer");
sendPostMessage(MessageType.MESSAGE, messageUrl, json.toString());
if (connectionParameters.loopback) {
// In loopback mode rename this offer to answer and route it back.
SessionDescription sdpAnswer = new SessionDescription(
SessionDescription.Type.fromCanonicalForm("answer"),
sdp.description);
events.onRemoteDescription(sdpAnswer);
}
}
});
}
// Send local answer SDP to the other participant.
@Override
public void sendAnswerSdp(final SessionDescription sdp) {
executor.execute(new Runnable() {
@Override
public void run() {
if (connectionParameters.loopback) {
Log.e(TAG, "Sending answer in loopback mode.");
return;
}
JSONObject json = new JSONObject();
jsonPut(json, "sdp", sdp.description);
jsonPut(json, "type", "answer");
wsClient.send(json.toString());
}
});
}
// Send Ice candidate to the other participant.
@Override
public void sendLocalIceCandidate(final IceCandidate candidate) {
executor.execute(new Runnable() {
@Override
public void run() {
JSONObject json = new JSONObject();
jsonPut(json, "type", "candidate");
jsonPut(json, "label", candidate.sdpMLineIndex);
jsonPut(json, "id", candidate.sdpMid);
jsonPut(json, "candidate", candidate.sdp);
if (initiator) {
// Call initiator sends ice candidates to GAE server.
if (roomState != ConnectionState.CONNECTED) {
reportError("Sending ICE candidate in non connected state.");
return;
}
sendPostMessage(MessageType.MESSAGE, messageUrl, json.toString());
if (connectionParameters.loopback) {
events.onRemoteIceCandidate(candidate);
}
} else {
// Call receiver sends ice candidates to websocket server.
wsClient.send(json.toString());
}
}
});
}
// --------------------------------------------------------------------
// WebSocketChannelEvents interface implementation.
// All events are called by WebSocketChannelClient on a local looper thread
// (passed to WebSocket client constructor).
@Override
public void onWebSocketMessage(final String msg) {
if (wsClient.getState() != WebSocketConnectionState.REGISTERED) {
Log.e(TAG, "Got WebSocket message in non registered state.");
return;
}
try {
JSONObject json = new JSONObject(msg);
String msgText = json.getString("msg");
String errorText = json.optString("error");
if (msgText.length() > 0) {
json = new JSONObject(msgText);
String type = json.optString("type");
if (type.equals("candidate")) {
IceCandidate candidate = new IceCandidate(
json.getString("id"),
json.getInt("label"),
json.getString("candidate"));
events.onRemoteIceCandidate(candidate);
} else if (type.equals("answer")) {
if (initiator) {
SessionDescription sdp = new SessionDescription(
SessionDescription.Type.fromCanonicalForm(type),
json.getString("sdp"));
events.onRemoteDescription(sdp);
} else {
reportError("Received answer for call initiator: " + msg);
}
} else if (type.equals("offer")) {
if (!initiator) {
SessionDescription sdp = new SessionDescription(
SessionDescription.Type.fromCanonicalForm(type),
json.getString("sdp"));
events.onRemoteDescription(sdp);
} else {
reportError("Received offer for call receiver: " + msg);
}
} else if (type.equals("bye")) {
events.onChannelClose();
} else {
reportError("Unexpected WebSocket message: " + msg);
}
} else {
if (errorText != null && errorText.length() > 0) {
reportError("WebSocket error message: " + errorText);
} else {
reportError("Unexpected WebSocket message: " + msg);
}
}
} catch (JSONException e) {
reportError("WebSocket message JSON parsing error: " + e.toString());
}
}
@Override
public void onWebSocketClose() {
events.onChannelClose();
}
@Override
public void onWebSocketError(String description) {
reportError("WebSocket error: " + description);
}
// --------------------------------------------------------------------
// Helper functions.
private void reportError(final String errorMessage) {
Log.e(TAG, errorMessage);
executor.execute(new Runnable() {
@Override
public void run() {
if (roomState != ConnectionState.ERROR) {
roomState = ConnectionState.ERROR;
events.onChannelError(errorMessage);
}
}
});
}
// Put a |key|->|value| mapping in |json|.
private static void jsonPut(JSONObject json, String key, Object value) {
try {
json.put(key, value);
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
// Send SDP or ICE candidate to a room server.
private void sendPostMessage(
final MessageType messageType, final String url, final String message) {
String logInfo = url;
if (message != null) {
logInfo += ". Message: " + message;
}
Log.d(TAG, "C->GAE: " + logInfo);
AsyncHttpURLConnection httpConnection = new AsyncHttpURLConnection(
"POST", url, message, new AsyncHttpEvents() {
@Override
public void onHttpError(String errorMessage) {
reportError("GAE POST error: " + errorMessage);
}
@Override
public void onHttpComplete(String response) {
if (messageType == MessageType.MESSAGE) {
try {
JSONObject roomJson = new JSONObject(response);
String result = roomJson.getString("result");
if (!result.equals("SUCCESS")) {
reportError("GAE POST error: " + result);
}
} catch (JSONException e) {
reportError("GAE POST JSON error: " + e.toString());
}
}
}
});
httpConnection.send();
}
}

View File

@ -0,0 +1,67 @@
/*
* Copyright 2014 The WebRTC Project Authors. All rights reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree. An additional intellectual property rights grant can be found
* in the file PATENTS. All contributing project authors may
* be found in the AUTHORS file in the root of the source tree.
*/
package org.appspot.apprtc.util;
import android.os.Build;
import android.util.Log;
/**
* AppRTCUtils provides helper functions for managing thread safety.
*/
public final class AppRTCUtils {
private AppRTCUtils() {
}
/**
* NonThreadSafe is a helper class used to help verify that methods of a
* class are called from the same thread.
*/
public static class NonThreadSafe {
private final Long threadId;
public NonThreadSafe() {
// Store thread ID of the creating thread.
threadId = Thread.currentThread().getId();
}
/** Checks if the method is called on the valid/creating thread. */
public boolean calledOnValidThread() {
return threadId.equals(Thread.currentThread().getId());
}
}
/** Helper method which throws an exception when an assertion has failed. */
public static void assertIsTrue(boolean condition) {
if (!condition) {
throw new AssertionError("Expected condition to be true");
}
}
/** Helper method for building a string of thread information.*/
public static String getThreadInfo() {
return "@[name=" + Thread.currentThread().getName()
+ ", id=" + Thread.currentThread().getId() + "]";
}
/** Information about the current build, taken from system properties. */
public static void logDeviceInfo(String tag) {
Log.d(tag, "Android SDK: " + Build.VERSION.SDK_INT + ", "
+ "Release: " + Build.VERSION.RELEASE + ", "
+ "Brand: " + Build.BRAND + ", "
+ "Device: " + Build.DEVICE + ", "
+ "Id: " + Build.ID + ", "
+ "Hardware: " + Build.HARDWARE + ", "
+ "Manufacturer: " + Build.MANUFACTURER + ", "
+ "Model: " + Build.MODEL + ", "
+ "Product: " + Build.PRODUCT);
}
}

View File

@ -0,0 +1,122 @@
/*
* Copyright 2015 The WebRTC Project Authors. All rights reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree. An additional intellectual property rights grant can be found
* in the file PATENTS. All contributing project authors may
* be found in the AUTHORS file in the root of the source tree.
*/
package org.appspot.apprtc.util;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.util.Scanner;
/**
* Asynchronous http requests implementation.
*/
public class AsyncHttpURLConnection {
private static final int HTTP_TIMEOUT_MS = 8000;
private static final String HTTP_ORIGIN = "https://apprtc.appspot.com";
private final String method;
private final String url;
private final String message;
private final AsyncHttpEvents events;
private String contentType;
/**
* Http requests callbacks.
*/
public interface AsyncHttpEvents {
public void onHttpError(String errorMessage);
public void onHttpComplete(String response);
}
public AsyncHttpURLConnection(String method, String url, String message,
AsyncHttpEvents events) {
this.method = method;
this.url = url;
this.message = message;
this.events = events;
}
public void setContentType(String contentType) {
this.contentType = contentType;
}
public void send() {
Runnable runHttp = new Runnable() {
public void run() {
sendHttpMessage();
}
};
new Thread(runHttp).start();
}
private void sendHttpMessage() {
try {
HttpURLConnection connection =
(HttpURLConnection) new URL(url).openConnection();
byte[] postData = new byte[0];
if (message != null) {
postData = message.getBytes("UTF-8");
}
connection.setRequestMethod(method);
connection.setUseCaches(false);
connection.setDoInput(true);
connection.setConnectTimeout(HTTP_TIMEOUT_MS);
connection.setReadTimeout(HTTP_TIMEOUT_MS);
// TODO(glaznev) - query request origin from pref_room_server_url_key preferences.
connection.addRequestProperty("origin", HTTP_ORIGIN);
boolean doOutput = false;
if (method.equals("POST")) {
doOutput = true;
connection.setDoOutput(true);
connection.setFixedLengthStreamingMode(postData.length);
}
if (contentType == null) {
connection.setRequestProperty("Content-Type", "text/plain; charset=utf-8");
} else {
connection.setRequestProperty("Content-Type", contentType);
}
// Send POST request.
if (doOutput && postData.length > 0) {
OutputStream outStream = connection.getOutputStream();
outStream.write(postData);
outStream.close();
}
// Get response.
int responseCode = connection.getResponseCode();
if (responseCode != 200) {
events.onHttpError("Non-200 response to " + method + " to URL: "
+ url + " : " + connection.getHeaderField(null));
connection.disconnect();
return;
}
InputStream responseStream = connection.getInputStream();
String response = drainStream(responseStream);
responseStream.close();
connection.disconnect();
events.onHttpComplete(response);
} catch (SocketTimeoutException e) {
events.onHttpError("HTTP " + method + " to " + url + " timeout");
} catch (IOException e) {
events.onHttpError("HTTP " + method + " to " + url + " error: "
+ e.getMessage());
}
}
// Return the contents of an InputStream as a String.
private static String drainStream(InputStream in) {
Scanner s = new Scanner(in).useDelimiter("\\A");
return s.hasNext() ? s.next() : "";
}
}

View File

@ -0,0 +1,95 @@
/*
* Copyright 2015 The WebRTC Project Authors. All rights reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree. An additional intellectual property rights grant can be found
* in the file PATENTS. All contributing project authors may
* be found in the AUTHORS file in the root of the source tree.
*/
package org.appspot.apprtc.util;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import java.util.concurrent.Executor;
/**
* Looper based executor class.
*/
public class LooperExecutor extends Thread implements Executor {
private static final String TAG = "LooperExecutor";
// Object used to signal that looper thread has started and Handler instance
// associated with looper thread has been allocated.
private final Object looperStartedEvent = new Object();
private Handler handler = null;
private boolean running = false;
private long threadId;
@Override
public void run() {
Looper.prepare();
synchronized (looperStartedEvent) {
Log.d(TAG, "Looper thread started.");
handler = new Handler();
threadId = Thread.currentThread().getId();
looperStartedEvent.notify();
}
Looper.loop();
}
public synchronized void requestStart() {
if (running) {
return;
}
running = true;
handler = null;
start();
// Wait for Hander allocation.
synchronized (looperStartedEvent) {
while (handler == null) {
try {
looperStartedEvent.wait();
} catch (InterruptedException e) {
Log.e(TAG, "Can not start looper thread");
running = false;
}
}
}
}
public synchronized void requestStop() {
if (!running) {
return;
}
running = false;
handler.post(new Runnable() {
@Override
public void run() {
Looper.myLooper().quit();
Log.d(TAG, "Looper thread finished.");
}
});
}
// Checks if current thread is a looper thread.
public boolean checkOnLooperThread() {
return (Thread.currentThread().getId() == threadId);
}
@Override
public synchronized void execute(final Runnable runnable) {
if (!running) {
Log.w(TAG, "Running looper executor without calling requestStart()");
return;
}
if (Thread.currentThread().getId() == threadId) {
runnable.run();
} else {
handler.post(runnable);
}
}
}