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:
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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
@ -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() : "";
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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() : "";
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user