Merge fixes and changed for Android AppRTCDemo from internal repo.

- Rename AppRTCDemoActivity to CallActivity and move UI controls
to a fragment.
- Add option to enable/disable statistics.
- Move peer connection and video constraints from URL to peer
connection client.
- Variable renaming.

R=jiayl@webrtc.org, wzh@webrtc.org

Review URL: https://webrtc-codereview.appspot.com/33299004

Cr-Commit-Position: refs/heads/master@{#8319}
git-svn-id: http://webrtc.googlecode.com/svn/trunk@8319 4adac7df-926f-26a2-2b94-8c16560cd09d
This commit is contained in:
glaznev@webrtc.org
2015-02-10 23:04:13 +00:00
parent d35a5c3506
commit bc40324d9c
20 changed files with 1361 additions and 1019 deletions

View File

@ -39,11 +39,11 @@
android:label="@string/settings_name"> android:label="@string/settings_name">
</activity> </activity>
<activity android:name="AppRTCDemoActivity" <activity android:name="CallActivity"
android:label="@string/app_name" android:label="@string/app_name"
android:screenOrientation="fullUser" android:screenOrientation="fullUser"
android:configChanges="orientation|screenSize" android:configChanges="orientation|screenSize"
android:theme="@style/AppRTCDemoActivityTheme"> android:theme="@style/CallActivityTheme">
</activity> </activity>
</application> </application>
</manifest> </manifest>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.opengl.GLSurfaceView
android:id="@+id/glview_call"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<FrameLayout
android:id="@+id/call_fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>

View File

@ -1,42 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.opengl.GLSurfaceView
android:id="@+id/glview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<TextView
android:id="@+id/encoder_stat"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:textStyle="bold"
android:textColor="#C000FF00"
android:textSize="12dp"
android:layout_margin="8dp"/>
<TextView
android:id="@+id/room_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_above="@+id/menubar_fragment"
android:textSize="24sp"
android:layout_margin="8dp"/>
<fragment
android:name="org.appspot.apprtc.AppRTCDemoActivity$MenuBarFragment"
android:id="@+id/menubar_fragment"
android:layout_centerHorizontal="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginBottom="32dp"
tools:layout="@layout/fragment_menubar"/>
</RelativeLayout>

View File

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/encoder_stat_call"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:textStyle="bold"
android:textColor="#C000FF00"
android:textSize="12dp"
android:layout_margin="8dp"/>
<TextView
android:id="@+id/contact_name_call"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_above="@+id/buttons_call_container"
android:textSize="24sp"
android:layout_margin="8dp"/>
<TextView
android:id="@+id/hud_stat_call"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:alpha="0.4"
android:background="@android:color/white"
android:textColor="@android:color/black" />
<ImageButton
android:id="@+id/button_toggle_debug"
android:background="@android:drawable/ic_menu_info_details"
android:contentDescription="@string/toggle_debug"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_width="48dp"
android:layout_height="48dp"/>
<LinearLayout
android:id="@+id/buttons_call_container"
android:orientation="horizontal"
android:layout_alignParentBottom="true"
android:layout_marginBottom="32dp"
android:layout_centerHorizontal="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageButton
android:id="@+id/button_call_disconnect"
android:background="@drawable/disconnect"
android:contentDescription="@string/disconnect_call"
android:layout_marginRight="16dp"
android:layout_width="48dp"
android:layout_height="48dp"/>
<ImageButton
android:id="@+id/button_call_switch_camera"
android:background="@android:drawable/ic_menu_camera"
android:contentDescription="@string/switch_camera"
android:layout_marginRight="8dp"
android:layout_width="48dp"
android:layout_height="48dp"/>
<ImageButton
android:id="@+id/button_call_scaling_mode"
android:background="@drawable/ic_action_return_from_full_screen"
android:contentDescription="@string/disconnect_call"
android:layout_width="48dp"
android:layout_height="48dp"/>
</LinearLayout>
</RelativeLayout>

View File

@ -1,45 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:context="org.appspot.apprtc.AppRTCDemoActivity$MenuBarFragment"
android:id="@+id/menubar"
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical|center_horizontal">
<ImageButton
android:id="@+id/button_disconnect"
android:background="@drawable/disconnect"
android:contentDescription="@string/disconnect_call"
android:layout_marginRight="16dp"
android:layout_width="48dp"
android:layout_height="48dp"/>
<!-- TODO(kjellander): Add audio and video mute buttons. -->
<ImageButton
android:id="@+id/button_switch_camera"
android:background="@android:drawable/ic_menu_camera"
android:contentDescription="@string/switch_camera"
android:layout_marginRight="8dp"
android:layout_width="48dp"
android:layout_height="48dp"/>
<ImageButton
android:id="@+id/button_toggle_debug"
android:background="@android:drawable/ic_menu_info_details"
android:contentDescription="@string/disconnect_call"
android:layout_marginRight="8dp"
android:layout_width="48dp"
android:layout_height="48dp"/>
<ImageButton
android:id="@+id/button_scaling_mode"
android:background="@drawable/ic_action_return_from_full_screen"
android:contentDescription="@string/disconnect_call"
android:layout_width="48dp"
android:layout_height="48dp"/>
</LinearLayout>

View File

@ -17,6 +17,7 @@
<string name="missing_url">FATAL ERROR: Missing URL to connect to.</string> <string name="missing_url">FATAL ERROR: Missing URL to connect to.</string>
<string name="ok">OK</string> <string name="ok">OK</string>
<string name="switch_camera">Switch front/back camera</string> <string name="switch_camera">Switch front/back camera</string>
<string name="toggle_debug">Toggle debug view</string>
<string name="action_settings">Settings</string> <string name="action_settings">Settings</string>
<string name="add_room_description">Add new room to the list</string> <string name="add_room_description">Add new room to the list</string>
<string name="remove_room_description">Remove room from the list</string> <string name="remove_room_description">Remove room from the list</string>
@ -40,7 +41,7 @@
<string name="pref_cpu_usage_detection_key">cpu_usage_detection</string> <string name="pref_cpu_usage_detection_key">cpu_usage_detection</string>
<string name="pref_cpu_usage_detection_title">CPU overuse detection.</string> <string name="pref_cpu_usage_detection_title">CPU overuse detection.</string>
<string name="pref_cpu_usage_detection_dlg">Adapt transmission to CPU status.</string> <string name="pref_cpu_usage_detection_dlg">Adapt transmission to CPU status.</string>
<string name="pref_cpu_usage_detection_default">true</string> <string name="pref_cpu_usage_detection_default" translatable="false">true</string>
<string name="pref_startbitrate_key">startbitrate_preference</string> <string name="pref_startbitrate_key">startbitrate_preference</string>
<string name="pref_startbitrate_title">Start bitrate setting.</string> <string name="pref_startbitrate_title">Start bitrate setting.</string>
@ -68,6 +69,11 @@
<string name="pref_room_server_url_key">room_server_url_preference</string> <string name="pref_room_server_url_key">room_server_url_preference</string>
<string name="pref_room_server_url_title">Room server URL.</string> <string name="pref_room_server_url_title">Room server URL.</string>
<string name="pref_room_server_url_dlg">Enter a room server URL.</string> <string name="pref_room_server_url_dlg">Enter a room server URL.</string>
<string name="pref_room_server_url_default">https://apprtc.appspot.com</string> <string name="pref_room_server_url_default" translatable="false">https://apprtc.appspot.com</string>
<string name="pref_displayhud_key">displayhud_preference</string>
<string name="pref_displayhud_title">Display call statistics.</string>
<string name="pref_displayhud_dlg">Display call statistics.</string>
<string name="pref_displayhud_default" translatable="false">false</string>
</resources> </resources>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="AppRTCDemoActivityTheme" parent="android:Theme.Black"> <style name="CallActivityTheme" parent="android:Theme.Black">
<item name="android:windowActionBar">false</item> <item name="android:windowActionBar">false</item>
<item name="android:windowFullscreen">true</item> <item name="android:windowFullscreen">true</item>
<item name="android:windowNoTitle">true</item> <item name="android:windowNoTitle">true</item>

View File

@ -58,4 +58,10 @@
android:defaultValue="@string/pref_room_server_url_default" android:defaultValue="@string/pref_room_server_url_default"
android:dialogTitle="@string/pref_room_server_url_dlg" /> android:dialogTitle="@string/pref_room_server_url_dlg" />
<CheckBoxPreference
android:key="@string/pref_displayhud_key"
android:title="@string/pref_displayhud_title"
android:dialogTitle="@string/pref_displayhud_dlg"
android:defaultValue="@string/pref_displayhud_default" />
</PreferenceScreen> </PreferenceScreen>

View File

@ -38,12 +38,28 @@ import java.util.List;
* AppRTCClient is the interface representing an AppRTC client. * AppRTCClient is the interface representing an AppRTC client.
*/ */
public interface AppRTCClient { public interface AppRTCClient {
/** /**
* Asynchronously connect to an AppRTC room URL, e.g. * Struct holding the connection parameters of an AppRTC room.
* https://apprtc.appspot.com/?r=NNN. Once connection is established
* onConnectedToRoom() callback with room parameters is invoked.
*/ */
public void connectToRoom(final String url, final boolean loopback); 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. * Send offer SDP to the other participant.
@ -68,13 +84,12 @@ public interface AppRTCClient {
/** /**
* Struct holding the signaling parameters of an AppRTC room. * Struct holding the signaling parameters of an AppRTC room.
*/ */
public class SignalingParameters { public static class SignalingParameters {
public final List<PeerConnection.IceServer> iceServers; public final List<PeerConnection.IceServer> iceServers;
public final boolean initiator; public final boolean initiator;
public final MediaConstraints pcConstraints; public final MediaConstraints pcConstraints;
public final MediaConstraints videoConstraints; public final MediaConstraints videoConstraints;
public final MediaConstraints audioConstraints; public final MediaConstraints audioConstraints;
public final String roomId;
public final String clientId; public final String clientId;
public final String wssUrl; public final String wssUrl;
public final String wssPostUrl; public final String wssPostUrl;
@ -85,15 +100,13 @@ public interface AppRTCClient {
List<PeerConnection.IceServer> iceServers, List<PeerConnection.IceServer> iceServers,
boolean initiator, MediaConstraints pcConstraints, boolean initiator, MediaConstraints pcConstraints,
MediaConstraints videoConstraints, MediaConstraints audioConstraints, MediaConstraints videoConstraints, MediaConstraints audioConstraints,
String roomId, String clientId, String clientId, String wssUrl, String wssPostUrl,
String wssUrl, String wssPostUrl,
SessionDescription offerSdp, List<IceCandidate> iceCandidates) { SessionDescription offerSdp, List<IceCandidate> iceCandidates) {
this.iceServers = iceServers; this.iceServers = iceServers;
this.initiator = initiator; this.initiator = initiator;
this.pcConstraints = pcConstraints; this.pcConstraints = pcConstraints;
this.videoConstraints = videoConstraints; this.videoConstraints = videoConstraints;
this.audioConstraints = audioConstraints; this.audioConstraints = audioConstraints;
this.roomId = roomId;
this.clientId = clientId; this.clientId = clientId;
this.wssUrl = wssUrl; this.wssUrl = wssUrl;
this.wssPostUrl = wssPostUrl; this.wssPostUrl = wssPostUrl;

View File

@ -1,691 +0,0 @@
/*
* libjingle
* Copyright 2013 Google Inc.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. The name of the author may not be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.appspot.apprtc;
import org.appspot.apprtc.AppRTCClient.SignalingParameters;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Fragment;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Color;
import android.net.Uri;
import android.opengl.GLSurfaceView;
import android.os.Bundle;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.Window;
import android.view.WindowManager;
import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
import org.webrtc.IceCandidate;
import org.webrtc.SessionDescription;
import org.webrtc.StatsObserver;
import org.webrtc.StatsReport;
import org.webrtc.VideoRenderer;
import org.webrtc.VideoRendererGui;
import org.webrtc.VideoRendererGui.ScalingType;
import java.util.HashMap;
import java.util.Map;
/**
* Activity of the AppRTCDemo Android app demonstrating interoperability
* between the Android/Java implementation of PeerConnection and the
* apprtc.appspot.com demo webapp.
*/
public class AppRTCDemoActivity extends Activity
implements AppRTCClient.SignalingEvents,
PeerConnectionClient.PeerConnectionEvents {
private static final String TAG = "AppRTCClient";
private PeerConnectionClient pc = null;
private AppRTCClient appRtcClient;
private SignalingParameters signalingParameters;
private AppRTCAudioManager audioManager = null;
private View rootView;
private View menuBar;
private GLSurfaceView videoView;
private VideoRenderer.Callbacks localRender;
private VideoRenderer.Callbacks remoteRender;
private ScalingType scalingType;
private Toast logToast;
private final LayoutParams hudLayout =
new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
private TextView hudView;
private TextView encoderStatView;
private TextView roomNameView;
private ImageButton videoScalingButton;
private String roomName;
private boolean commandLineRun;
private boolean activityRunning;
private int runTimeMs;
private int startBitrate;
private String videoCodec;
private boolean hwCodecAcceleration;
private boolean iceConnected;
private boolean isError;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Set window styles for fullscreen-window size. Needs to be done before
// adding content.
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_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_fullscreen);
Thread.setDefaultUncaughtExceptionHandler(
new UnhandledExceptionHandler(this));
iceConnected = false;
signalingParameters = null;
rootView = findViewById(android.R.id.content);
encoderStatView = (TextView) findViewById(R.id.encoder_stat);
menuBar = findViewById(R.id.menubar_fragment);
roomNameView = (TextView) findViewById(R.id.room_name);
videoView = (GLSurfaceView) findViewById(R.id.glview);
VideoRendererGui.setView(videoView, new Runnable() {
@Override
public void run() {
createPeerConnectionFactory();
}
});
scalingType = ScalingType.SCALE_ASPECT_FILL;
remoteRender = VideoRendererGui.create(0, 0, 100, 100, scalingType, false);
localRender = VideoRendererGui.create(0, 0, 100, 100, scalingType, true);
videoView.setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View view) {
int visibility = menuBar.getVisibility() == View.VISIBLE
? View.INVISIBLE : View.VISIBLE;
encoderStatView.setVisibility(visibility);
menuBar.setVisibility(visibility);
roomNameView.setVisibility(visibility);
if (visibility == View.VISIBLE) {
encoderStatView.bringToFront();
menuBar.bringToFront();
roomNameView.bringToFront();
rootView.invalidate();
}
}
});
((ImageButton) findViewById(R.id.button_disconnect)).setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View view) {
logAndToast("Disconnecting call.");
disconnect();
}
});
((ImageButton) findViewById(R.id.button_switch_camera)).setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View view) {
if (pc != null) {
pc.switchCamera();
}
}
});
((ImageButton) findViewById(R.id.button_toggle_debug)).setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View view) {
int visibility = hudView.getVisibility() == View.VISIBLE
? View.INVISIBLE : View.VISIBLE;
hudView.setVisibility(visibility);
}
});
videoScalingButton = (ImageButton) findViewById(R.id.button_scaling_mode);
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;
}
updateVideoView();
}
});
hudView = new TextView(this);
hudView.setTextColor(Color.BLACK);
hudView.setBackgroundColor(Color.WHITE);
hudView.setAlpha(0.4f);
hudView.setTextSize(TypedValue.COMPLEX_UNIT_PT, 5);
hudView.setVisibility(View.INVISIBLE);
addContentView(hudView, hudLayout);
final Intent intent = getIntent();
Uri url = intent.getData();
roomName = intent.getStringExtra(ConnectActivity.EXTRA_ROOMNAME);
boolean loopback = intent.getBooleanExtra(
ConnectActivity.EXTRA_LOOPBACK, false);
commandLineRun = intent.getBooleanExtra(
ConnectActivity.EXTRA_CMDLINE, false);
runTimeMs = intent.getIntExtra(ConnectActivity.EXTRA_RUNTIME, 0);
startBitrate = intent.getIntExtra(ConnectActivity.EXTRA_BITRATE, 0);
if (intent.hasExtra(ConnectActivity.EXTRA_VIDEOCODEC)) {
videoCodec = intent.getStringExtra(ConnectActivity.EXTRA_VIDEOCODEC);
} else {
videoCodec = PeerConnectionClient.VIDEO_CODEC_VP8; // use VP8 by default.
}
hwCodecAcceleration = intent.getBooleanExtra(
ConnectActivity.EXTRA_HWCODEC, true);
if (url != null) {
if (loopback || (roomName != null && !roomName.equals(""))) {
// Start room connection.
logAndToast(getString(R.string.connecting_to, url));
appRtcClient = new WebSocketRTCClient(this);
appRtcClient.connectToRoom(url.toString(), loopback);
if (loopback) {
roomNameView.setText("loopback");
} else {
roomNameView.setText(roomName);
}
// 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();
// For command line execution run connection for <runTimeMs> and exit.
if (commandLineRun && runTimeMs > 0) {
videoView.postDelayed(new Runnable() {
public void run() {
disconnect();
}
}, runTimeMs);
}
} else {
logAndToast("Empty or missing room name!");
setResult(RESULT_CANCELED);
finish();
}
} else {
logAndToast(getString(R.string.missing_url));
Log.e(TAG, "Didn't get any URL in intent!");
setResult(RESULT_CANCELED);
finish();
}
}
// Create peer connection factory when EGL context is ready.
private void createPeerConnectionFactory() {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (pc == null) {
pc = new PeerConnectionClient();
pc.createPeerConnectionFactory(AppRTCDemoActivity.this,
videoCodec, hwCodecAcceleration,
VideoRendererGui.getEGLContext(), AppRTCDemoActivity.this);
}
if (signalingParameters != null) {
Log.w(TAG, "EGL context is ready after room connection.");
onConnectedToRoomInternal(signalingParameters);
}
}
});
}
/**
* MenuBar fragment for AppRTC.
*/
public static class MenuBarFragment extends Fragment {
@Override
public View onCreateView(
LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_menubar, container, false);
}
}
@Override
public void onPause() {
super.onPause();
videoView.onPause();
activityRunning = false;
if (pc != null) {
pc.stopVideoSource();
}
}
@Override
public void onResume() {
super.onResume();
videoView.onResume();
activityRunning = true;
if (pc != null) {
pc.startVideoSource();
}
}
@Override
protected void onDestroy() {
disconnect();
super.onDestroy();
if (logToast != null) {
logToast.cancel();
}
activityRunning = false;
}
private void updateVideoView() {
VideoRendererGui.update(remoteRender, 0, 0, 100, 100, scalingType);
if (iceConnected) {
VideoRendererGui.update(localRender, 70, 70, 28, 28,
ScalingType.SCALE_ASPECT_FIT);
} else {
VideoRendererGui.update(localRender, 0, 0, 100, 100, scalingType);
}
}
private void onAudioManagerChangedState() {
// TODO(henrika): disable video if AppRTCAudioManager.AudioDevice.EARPIECE
// is active.
}
// Disconnect from remote resources, dispose of local resources, and exit.
private void disconnect() {
if (appRtcClient != null) {
appRtcClient.disconnectFromRoom();
appRtcClient = null;
}
if (pc != null) {
pc.close();
pc = 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() {
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();
}
// Return the active connection stats,
// or null if active connection is not found.
private String getActiveConnectionStats(StatsReport report) {
StringBuilder activeConnectionbuilder = new StringBuilder();
// googCandidatePair to show information about the active
// connection.
for (StatsReport.Value value : report.values) {
if (value.name.equals("googActiveConnection")
&& value.value.equals("false")) {
return null;
}
String name = value.name.replace("goog", "");
activeConnectionbuilder.append(name).append("=")
.append(value.value).append("\n");
}
return activeConnectionbuilder.toString();
}
// Update the heads-up display with information from |reports|.
private void updateHUD(StatsReport[] reports) {
StringBuilder builder = new StringBuilder();
for (StatsReport report : reports) {
Log.d(TAG, "Stats: " + report.toString());
// bweforvideo to show statistics for video Bandwidth Estimation,
// which is global per-session.
if (report.id.equals("bweforvideo")) {
for (StatsReport.Value value : report.values) {
String name = value.name.replace("goog", "")
.replace("Available", "").replace("Bandwidth", "")
.replace("Bitrate", "").replace("Enc", "");
builder.append(name).append("=").append(value.value)
.append(" ");
}
builder.append("\n");
} else if (report.type.equals("googCandidatePair")) {
String activeConnectionStats = getActiveConnectionStats(report);
if (activeConnectionStats == null) {
continue;
}
builder.append(activeConnectionStats);
} else {
continue;
}
builder.append("\n");
}
hudView.setText(builder.toString() + hudView.getText());
}
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;
}
// Update encoder statistics view with information from |reports|.
private void updateEncoderStatistics(StatsReport[] reports) {
if (!iceConnected) {
return;
}
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")) {
Map<String, String> reportMap = getReportMap(report);
String trackId = reportMap.get("googTrackId");
if (trackId != null
&& trackId.contains(PeerConnectionClient.VIDEO_TRACK_ID)) {
fps = reportMap.get("googFrameRateSent");
}
} else if (report.id.equals("bweforvideo")) {
Map<String, String> reportMap = getReportMap(report);
targetBitrate = reportMap.get("googTargetEncBitrate");
actualBitrate = reportMap.get("googActualEncBitrate");
}
}
String stat = "";
if (fps != null) {
stat += "Fps: " + fps + "\n";
}
if (targetBitrate != null) {
stat += "Target BR: " + targetBitrate + "\n";
}
if (actualBitrate != null) {
stat += "Actual BR: " + actualBitrate;
}
encoderStatView.setText(stat);
}
// -----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) {
signalingParameters = params;
if (pc == null) {
Log.w(TAG, "Room is connected, but EGL context is not ready yet.");
return;
}
logAndToast("Creating peer connection...");
pc.createPeerConnection(
localRender, remoteRender, signalingParameters, startBitrate);
// Schedule statistics display.
final Runnable repeatedStatsLogger = new Runnable() {
public void run() {
if (pc == null) {
return;
}
final Runnable runnableThis = this;
if (hudView.getVisibility() == View.INVISIBLE
&& encoderStatView.getVisibility() == View.INVISIBLE) {
videoView.postDelayed(runnableThis, 1000);
return;
}
boolean success = pc.getStats(new StatsObserver() {
public void onComplete(final StatsReport[] reports) {
runOnUiThread(new Runnable() {
public void run() {
if (hudView.getVisibility() == View.VISIBLE) {
updateHUD(reports);
}
if (encoderStatView.getVisibility() == View.VISIBLE) {
updateEncoderStatistics(reports);
}
}
});
videoView.postDelayed(runnableThis, 1000);
}
}, null);
if (!success) {
Log.w(TAG, "getStats() return false!");
videoView.postDelayed(runnableThis, 1000);
}
}
};
videoView.postDelayed(repeatedStatsLogger, 1000);
if (signalingParameters.initiator) {
logAndToast("Creating OFFER...");
// Create offer. Offer SDP will be sent to answering client in
// PeerConnectionEvents.onLocalDescription event.
pc.createOffer();
}
}
@Override
public void onConnectedToRoom(final SignalingParameters params) {
runOnUiThread(new Runnable() {
@Override
public void run() {
onConnectedToRoomInternal(params);
}
});
}
@Override
public void onRemoteDescription(final SessionDescription sdp) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (pc == null) {
return;
}
logAndToast("Received remote " + sdp.type + " ...");
pc.setRemoteDescription(sdp);
if (!signalingParameters.initiator) {
logAndToast("Creating ANSWER...");
// Create answer. Answer SDP will be sent to offering client in
// PeerConnectionEvents.onLocalDescription event.
pc.createAnswer();
}
}
});
}
@Override
public void onRemoteIceCandidate(final IceCandidate candidate) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (pc != null) {
pc.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) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (!isError) {
isError = true;
disconnectWithErrorMessage(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) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (appRtcClient != null) {
logAndToast("Sending " + sdp.type + " ...");
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() {
runOnUiThread(new Runnable() {
@Override
public void run() {
logAndToast("ICE connected");
iceConnected = true;
updateVideoView();
}
});
}
@Override
public void onIceDisconnected() {
runOnUiThread(new Runnable() {
@Override
public void run() {
logAndToast("ICE disconnected");
iceConnected = false;
disconnect();
}
});
}
@Override
public void onPeerConnectionClosed() {
}
@Override
public void onPeerConnectionError(final String description) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (!isError) {
isError = true;
disconnectWithErrorMessage(description);
}
}
});
}
}

View File

@ -0,0 +1,618 @@
/*
* libjingle
* Copyright 2015 Google Inc.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. The name of the author may not be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
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.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;
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_HWCODEC =
"org.appspot.apprtc.HWCODEC";
public static final String EXTRA_VIDEO_BITRATE =
"org.appspot.apprtc.VIDEO_BITRATE";
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_VIDEOCODEC =
"org.appspot.apprtc.VIDEOCODEC";
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";
// 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 hwCodecAcceleration;
private String videoCodec;
private boolean iceConnected;
private boolean isError;
private boolean callControlFragmentVisible = true;
// Controls
private GLSurfaceView videoView;
CallFragment callFragment;
@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(WindowManager.LayoutParams.FLAG_FULLSCREEN);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_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();
// 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();
}
});
// 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);
hwCodecAcceleration = intent.getBooleanExtra(EXTRA_HWCODEC, true);
if (intent.hasExtra(EXTRA_VIDEOCODEC)) {
videoCodec = intent.getStringExtra(EXTRA_VIDEOCODEC);
} else {
videoCodec = PeerConnectionClient.VIDEO_CODEC_VP8; // use VP8 by default.
}
peerConnectionParameters = new PeerConnectionParameters(
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.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 fragment.
callFragment.setArguments(intent.getExtras());
// Activate call fragment and start the call.
getFragmentManager().beginTransaction()
.add(R.id.call_fragment_container, callFragment).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);
} else {
ft.hide(callFragment);
}
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
ft.commit();
}
private void updateVideoView() {
VideoRendererGui.update(remoteRender,
REMOTE_X, REMOTE_Y,
REMOTE_WIDTH, REMOTE_HEIGHT, scalingType);
if (iceConnected) {
VideoRendererGui.update(localRender,
LOCAL_X_CONNECTED, LOCAL_Y_CONNECTED,
LOCAL_WIDTH_CONNECTED, LOCAL_HEIGHT_CONNECTED,
ScalingType.SCALE_ASPECT_FIT);
} else {
VideoRendererGui.update(localRender,
LOCAL_X_CONNECTING, LOCAL_Y_CONNECTING,
LOCAL_WIDTH_CONNECTING, LOCAL_HEIGHT_CONNECTING, scalingType);
}
}
private void startCall() {
if (appRtcClient == null) {
Log.e(TAG, "AppRTC client is not allocated for a call.");
return;
}
// 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() {
// 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) {
peerConnectionClient = new PeerConnectionClient();
peerConnectionClient.createPeerConnectionFactory(CallActivity.this,
videoCodec, hwCodecAcceleration,
VideoRendererGui.getEGLContext(), 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() {
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();
}
// -----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) {
signalingParameters = params;
if (peerConnectionClient == null) {
Log.w(TAG, "Room is connected, but EGL context is not ready yet.");
return;
}
logAndToast("Creating peer connection...");
peerConnectionClient.createPeerConnection(
localRender, remoteRender,
signalingParameters, peerConnectionParameters);
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) {
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 + " ...");
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) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (!isError) {
isError = true;
disconnectWithErrorMessage(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) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (appRtcClient != null) {
logAndToast("Sending " + sdp.type + " ...");
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() {
runOnUiThread(new Runnable() {
@Override
public void run() {
logAndToast("ICE connected");
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) {
callFragment.updateEncoderStatistics(reports);
}
}
});
}
@Override
public void onPeerConnectionError(final String description) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (!isError) {
isError = true;
disconnectWithErrorMessage(description);
}
}
});
}
}

View File

@ -0,0 +1,230 @@
/*
* libjingle
* Copyright 2015 Google Inc.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. The name of the author may not be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.appspot.apprtc;
import android.app.Activity;
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 org.webrtc.VideoRendererGui.ScalingType;
import java.util.HashMap;
import java.util.Map;
/**
* Fragment for call control.
*/
public class CallFragment extends Fragment {
private View controlView;
private TextView encoderStatView;
private TextView roomIdView;
private ImageButton disconnectButton;
private ImageButton cameraSwitchButton;
private ImageButton videoScalingButton;
private ImageButton toggleDebugButton;
private OnCallEvents callEvents;
private ScalingType scalingType;
private boolean displayHud;
private volatile boolean isRunning;
private TextView hudView;
/**
* 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.
encoderStatView =
(TextView) controlView.findViewById(R.id.encoder_stat_call);
roomIdView =
(TextView) controlView.findViewById(R.id.contact_name_call);
hudView =
(TextView) controlView.findViewById(R.id.hud_stat_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);
toggleDebugButton =
(ImageButton) controlView.findViewById(R.id.button_toggle_debug);
// 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;
toggleDebugButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (displayHud) {
int visibility = (hudView.getVisibility() == View.VISIBLE)
? View.INVISIBLE : View.VISIBLE;
hudView.setVisibility(visibility);
}
}
});
return controlView;
}
@Override
public void onStart() {
super.onStart();
Bundle args = getArguments();
if (args != null) {
String roomId = args.getString(CallActivity.EXTRA_ROOMID);
roomIdView.setText(roomId);
displayHud = args.getBoolean(CallActivity.EXTRA_DISPLAY_HUD, false);
}
int visibility = displayHud ? View.VISIBLE : View.INVISIBLE;
encoderStatView.setVisibility(visibility);
toggleDebugButton.setVisibility(visibility);
hudView.setVisibility(View.INVISIBLE);
hudView.setTextSize(TypedValue.COMPLEX_UNIT_PT, 5);
isRunning = true;
}
@Override
public void onStop() {
isRunning = false;
super.onStop();
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
callEvents = (OnCallEvents) activity;
}
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;
}
String fps = null;
String targetBitrate = null;
String actualBitrate = null;
StringBuilder bweBuilder = new StringBuilder();
for (StatsReport report : reports) {
if (report.type.equals("ssrc") && report.id.contains("ssrc")
&& report.id.contains("send")) {
Map<String, String> reportMap = getReportMap(report);
String trackId = reportMap.get("googTrackId");
if (trackId != null
&& trackId.contains(PeerConnectionClient.VIDEO_TRACK_ID)) {
fps = reportMap.get("googFrameRateSent");
}
} else if (report.id.equals("bweforvideo")) {
Map<String, String> reportMap = getReportMap(report);
targetBitrate = reportMap.get("googTargetEncBitrate");
actualBitrate = reportMap.get("googActualEncBitrate");
for (StatsReport.Value value : report.values) {
String name = value.name.replace("goog", "")
.replace("Available", "").replace("Bandwidth", "")
.replace("Bitrate", "").replace("Enc", "");
bweBuilder.append(name).append("=").append(value.value)
.append(" ");
}
bweBuilder.append("\n");
}
}
StringBuilder stat = new StringBuilder(128);
if (fps != null) {
stat.append("Fps: ")
.append(fps)
.append("\n");
}
if (targetBitrate != null) {
stat.append("Target BR: ")
.append(targetBitrate)
.append("\n");
}
if (actualBitrate != null) {
stat.append("Actual BR: ")
.append(actualBitrate)
.append("\n");
}
encoderStatView.setText(stat.toString());
hudView.setText(bweBuilder.toString() + hudView.getText());
}
}

View File

@ -52,7 +52,6 @@ import android.widget.TextView;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import org.webrtc.MediaCodecVideoEncoder;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Random; import java.util.Random;
@ -61,14 +60,6 @@ import java.util.Random;
* Handles the initial setup where the user selects which room to join. * Handles the initial setup where the user selects which room to join.
*/ */
public class ConnectActivity extends Activity { public class ConnectActivity extends Activity {
public static final String EXTRA_ROOMNAME = "org.appspot.apprtc.ROOMNAME";
public static final String EXTRA_LOOPBACK = "org.appspot.apprtc.LOOPBACK";
public static final String EXTRA_CMDLINE = "org.appspot.apprtc.CMDLINE";
public static final String EXTRA_RUNTIME = "org.appspot.apprtc.RUNTIME";
public static final String EXTRA_BITRATE = "org.appspot.apprtc.BITRATE";
public static final String EXTRA_VIDEOCODEC = "org.appspot.apprtc.VIDEOCODEC";
public static final String EXTRA_HWCODEC = "org.appspot.apprtc.HWCODEC";
private static final String TAG = "ConnectActivity"; private static final String TAG = "ConnectActivity";
private static final int CONNECTION_REQUEST = 1; private static final int CONNECTION_REQUEST = 1;
private static boolean commandLineRun = false; private static boolean commandLineRun = false;
@ -87,6 +78,7 @@ public class ConnectActivity extends Activity {
private String keyprefVideoCodec; private String keyprefVideoCodec;
private String keyprefHwCodecAcceleration; private String keyprefHwCodecAcceleration;
private String keyprefCpuUsageDetection; private String keyprefCpuUsageDetection;
private String keyprefDisplayHud;
private String keyprefRoomServerUrl; private String keyprefRoomServerUrl;
private String keyprefRoom; private String keyprefRoom;
private String keyprefRoomList; private String keyprefRoomList;
@ -107,6 +99,7 @@ public class ConnectActivity extends Activity {
keyprefVideoCodec = getString(R.string.pref_videocodec_key); keyprefVideoCodec = getString(R.string.pref_videocodec_key);
keyprefHwCodecAcceleration = getString(R.string.pref_hwcodec_key); keyprefHwCodecAcceleration = getString(R.string.pref_hwcodec_key);
keyprefCpuUsageDetection = getString(R.string.pref_cpu_usage_detection_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); keyprefRoomServerUrl = getString(R.string.pref_room_server_url_key);
keyprefRoom = getString(R.string.pref_room_key); keyprefRoom = getString(R.string.pref_room_key);
keyprefRoomList = getString(R.string.pref_room_list_key); keyprefRoomList = getString(R.string.pref_room_list_key);
@ -146,8 +139,10 @@ public class ConnectActivity extends Activity {
if ("android.intent.action.VIEW".equals(intent.getAction()) if ("android.intent.action.VIEW".equals(intent.getAction())
&& !commandLineRun) { && !commandLineRun) {
commandLineRun = true; commandLineRun = true;
boolean loopback = intent.getBooleanExtra(EXTRA_LOOPBACK, false); boolean loopback = intent.getBooleanExtra(
int runTimeMs = intent.getIntExtra(EXTRA_RUNTIME, 0); CallActivity.EXTRA_LOOPBACK, false);
int runTimeMs = intent.getIntExtra(
CallActivity.EXTRA_RUNTIME, 0);
String room = sharedPref.getString(keyprefRoom, ""); String room = sharedPref.getString(keyprefRoom, "");
roomEditText.setText(room); roomEditText.setText(room);
connectToRoom(loopback, runTimeMs); connectToRoom(loopback, runTimeMs);
@ -216,6 +211,7 @@ public class ConnectActivity extends Activity {
if (requestCode == CONNECTION_REQUEST && commandLineRun) { if (requestCode == CONNECTION_REQUEST && commandLineRun) {
Log.d(TAG, "Return: " + resultCode); Log.d(TAG, "Return: " + resultCode);
setResult(resultCode); setResult(resultCode);
commandLineRun = false;
finish(); finish();
} }
} }
@ -232,32 +228,21 @@ public class ConnectActivity extends Activity {
} }
}; };
private String appendQueryParameter(String url, String parameter) {
String newUrl = url;
if (newUrl.contains("?")) {
newUrl += "&" + parameter;
} else {
newUrl += "?" + parameter;
}
return newUrl;
}
private void connectToRoom(boolean loopback, int runTimeMs) { private void connectToRoom(boolean loopback, int runTimeMs) {
// Get room name (random for loopback). // Get room name (random for loopback).
String roomName; String roomId;
if (loopback) { if (loopback) {
roomName = Integer.toString((new Random()).nextInt(100000000)); roomId = Integer.toString((new Random()).nextInt(100000000));
} else { } else {
roomName = getSelectedItem(); roomId = getSelectedItem();
if (roomName == null) { if (roomId == null) {
roomName = roomEditText.getText().toString(); roomId = roomEditText.getText().toString();
} }
} }
String url = sharedPref.getString( String roomUrl = sharedPref.getString(
keyprefRoomServerUrl, keyprefRoomServerUrl,
getString(R.string.pref_room_server_url_default)); getString(R.string.pref_room_server_url_default));
url = WebSocketRTCClient.getGAEConnectionUrl(url, roomName);
// Get default video codec. // Get default video codec.
String videoCodec = sharedPref.getString(keyprefVideoCodec, String videoCodec = sharedPref.getString(keyprefVideoCodec,
@ -267,60 +252,36 @@ public class ConnectActivity extends Activity {
boolean hwCodec = sharedPref.getBoolean(keyprefHwCodecAcceleration, boolean hwCodec = sharedPref.getBoolean(keyprefHwCodecAcceleration,
Boolean.valueOf(getString(R.string.pref_hwcodec_default))); Boolean.valueOf(getString(R.string.pref_hwcodec_default)));
// Add video resolution constraints. // Get video resolution from settings.
String parametersResolution = null; int videoWidth = 0;
String parametersFps = null; int videoHeight = 0;
String resolution = sharedPref.getString(keyprefResolution, String resolution = sharedPref.getString(keyprefResolution,
getString(R.string.pref_resolution_default)); getString(R.string.pref_resolution_default));
String[] dimensions = resolution.split("[ x]+"); String[] dimensions = resolution.split("[ x]+");
if (dimensions.length == 2) { if (dimensions.length == 2) {
try { try {
int maxWidth = Integer.parseInt(dimensions[0]); videoWidth = Integer.parseInt(dimensions[0]);
int maxHeight = Integer.parseInt(dimensions[1]); videoHeight = Integer.parseInt(dimensions[1]);
if (maxWidth > 0 && maxHeight > 0) {
parametersResolution = "minHeight=" + maxHeight + ",maxHeight="
+ maxHeight + ",minWidth=" + maxWidth + ",maxWidth=" + maxWidth;
}
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
videoWidth = 0;
videoHeight = 0;
Log.e(TAG, "Wrong video resolution setting: " + resolution); Log.e(TAG, "Wrong video resolution setting: " + resolution);
} }
} }
// Add camera fps constraints. // Get camera fps from settings.
int cameraFps = 0;
String fps = sharedPref.getString(keyprefFps, String fps = sharedPref.getString(keyprefFps,
getString(R.string.pref_fps_default)); getString(R.string.pref_fps_default));
String[] fpsValues = fps.split("[ x]+"); String[] fpsValues = fps.split("[ x]+");
if (fpsValues.length == 2) { if (fpsValues.length == 2) {
try { try {
int cameraFps = Integer.parseInt(fpsValues[0]); cameraFps = Integer.parseInt(fpsValues[0]);
if (cameraFps > 0) {
parametersFps = "minFrameRate=" + cameraFps
+ ",maxFrameRate=" + cameraFps;
}
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
Log.e(TAG, "Wrong camera fps setting: " + fps); Log.e(TAG, "Wrong camera fps setting: " + fps);
} }
} }
// Modify connection URL.
if (parametersResolution != null || parametersFps != null) {
String urlVideoParameters = "video=";
if (parametersResolution != null) {
urlVideoParameters += parametersResolution;
if (parametersFps != null) {
urlVideoParameters += ",";
}
}
if (parametersFps != null) {
urlVideoParameters += parametersFps;
}
url = appendQueryParameter(url, urlVideoParameters);
} else {
if (hwCodec && MediaCodecVideoEncoder.isPlatformSupported()) {
url = appendQueryParameter(url, "hd=true");
}
}
// Get start bitrate. // Get start bitrate.
int startBitrate = 0; int startBitrate = 0;
String bitrateTypeDefault = getString(R.string.pref_startbitrate_default); String bitrateTypeDefault = getString(R.string.pref_startbitrate_default);
@ -337,23 +298,31 @@ public class ConnectActivity extends Activity {
keyprefCpuUsageDetection, keyprefCpuUsageDetection,
Boolean.valueOf( Boolean.valueOf(
getString(R.string.pref_cpu_usage_detection_default))); getString(R.string.pref_cpu_usage_detection_default)));
if (!cpuOveruseDetection) {
url = appendQueryParameter(url, "googCpuOveruseDetection=false"); // Check statistics display option.
} boolean displayHud = sharedPref.getBoolean(keyprefDisplayHud,
Boolean.valueOf(getString(R.string.pref_displayhud_default)));
// Start AppRTCDemo activity. // Start AppRTCDemo activity.
Log.d(TAG, "Connecting to room " + roomName + " at URL " + url); Log.d(TAG, "Connecting to room " + roomId + " at URL " + roomUrl);
if (validateUrl(url)) { if (validateUrl(roomUrl)) {
Uri uri = Uri.parse(url); Uri uri = Uri.parse(roomUrl);
Intent intent = new Intent(this, AppRTCDemoActivity.class); Intent intent = new Intent(this, CallActivity.class);
intent.setData(uri); intent.setData(uri);
intent.putExtra(EXTRA_ROOMNAME, roomName); intent.putExtra(CallActivity.EXTRA_ROOMID, roomId);
intent.putExtra(EXTRA_LOOPBACK, loopback); intent.putExtra(CallActivity.EXTRA_LOOPBACK, loopback);
intent.putExtra(EXTRA_CMDLINE, commandLineRun); intent.putExtra(CallActivity.EXTRA_VIDEOCODEC, videoCodec);
intent.putExtra(EXTRA_RUNTIME, runTimeMs); intent.putExtra(CallActivity.EXTRA_HWCODEC, hwCodec);
intent.putExtra(EXTRA_BITRATE, startBitrate); intent.putExtra(CallActivity.EXTRA_VIDEO_BITRATE, startBitrate);
intent.putExtra(EXTRA_VIDEOCODEC, videoCodec); intent.putExtra(CallActivity.EXTRA_VIDEO_WIDTH, videoWidth);
intent.putExtra(EXTRA_HWCODEC, hwCodec); intent.putExtra(CallActivity.EXTRA_VIDEO_HEIGHT, videoHeight);
intent.putExtra(CallActivity.EXTRA_VIDEO_FPS, cameraFps);
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); startActivityForResult(intent, CONNECTION_REQUEST);
} }
} }

View File

@ -36,21 +36,25 @@ import android.util.Log;
import org.webrtc.DataChannel; import org.webrtc.DataChannel;
import org.webrtc.IceCandidate; import org.webrtc.IceCandidate;
import org.webrtc.MediaCodecVideoEncoder;
import org.webrtc.MediaConstraints; import org.webrtc.MediaConstraints;
import org.webrtc.MediaConstraints.KeyValuePair;
import org.webrtc.MediaStream; import org.webrtc.MediaStream;
import org.webrtc.MediaStreamTrack;
import org.webrtc.PeerConnection; import org.webrtc.PeerConnection;
import org.webrtc.PeerConnection.IceConnectionState; import org.webrtc.PeerConnection.IceConnectionState;
import org.webrtc.PeerConnectionFactory; import org.webrtc.PeerConnectionFactory;
import org.webrtc.SdpObserver; import org.webrtc.SdpObserver;
import org.webrtc.SessionDescription; import org.webrtc.SessionDescription;
import org.webrtc.StatsObserver; import org.webrtc.StatsObserver;
import org.webrtc.StatsReport;
import org.webrtc.VideoCapturer; import org.webrtc.VideoCapturer;
import org.webrtc.VideoRenderer; import org.webrtc.VideoRenderer;
import org.webrtc.VideoSource; import org.webrtc.VideoSource;
import org.webrtc.VideoTrack; import org.webrtc.VideoTrack;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.Timer;
import java.util.TimerTask;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -61,39 +65,77 @@ import java.util.regex.Pattern;
* All PeerConnectionEvents callbacks are invoked from the same looper thread. * All PeerConnectionEvents callbacks are invoked from the same looper thread.
*/ */
public class PeerConnectionClient { public class PeerConnectionClient {
public static final String VIDEO_TRACK_ID = "ARDAMSv0";
public static final String AUDIO_TRACK_ID = "ARDAMSa0";
private static final String TAG = "PCRTCClient"; private static final String TAG = "PCRTCClient";
private static final boolean PREFER_ISAC = false; private static final boolean PREFER_ISAC = false;
public static final String VIDEO_CODEC_VP8 = "VP8"; public static final String VIDEO_CODEC_VP8 = "VP8";
public static final String VIDEO_CODEC_VP9 = "VP9"; public static final String VIDEO_CODEC_VP9 = "VP9";
private static final String FIELD_TRIAL_VP9 = "WebRTC-SupportVP9/Enabled/"; private static final String FIELD_TRIAL_VP9 = "WebRTC-SupportVP9/Enabled/";
public static final String VIDEO_TRACK_ID = "ARDAMSv0"; private static final String MAX_VIDEO_WIDTH_CONSTRAINT = "maxWidth";
public static final String AUDIO_TRACK_ID = "ARDAMSa0"; private static final String MIN_VIDEO_WIDTH_CONSTRAINT = "minWidth";
private static final String MAX_VIDEO_HEIGHT_CONSTRAINT = "maxHeight";
private static final String MIN_VIDEO_HEIGHT_CONSTRAINT = "minHeight";
private static final String MAX_VIDEO_FPS_CONSTRAINT = "maxFrameRate";
private static final String MIN_VIDEO_FPS_CONSTRAINT = "minFrameRate";
private static final int HD_VIDEO_WIDTH = 1280;
private static final int HD_VIDEO_HEIGHT = 720;
private static final int MAX_VIDEO_WIDTH = 1280;
private static final int MAX_VIDEO_HEIGHT = 1280;
private static final int MAX_VIDEO_FPS = 30;
private final LooperExecutor executor; private final LooperExecutor executor;
private PeerConnectionFactory factory = null; private PeerConnectionFactory factory = null;
private PeerConnection pc = null; private PeerConnection peerConnection = null;
private VideoSource videoSource; private VideoSource videoSource;
private boolean videoSourceStopped = false; private boolean videoSourceStopped = false;
private boolean isError = false; private boolean isError = false;
private boolean videoCodecHwAcceleration;
private final Timer statsTimer = new Timer();
private final PCObserver pcObserver = new PCObserver(); private final PCObserver pcObserver = new PCObserver();
private final SDPObserver sdpObserver = new SDPObserver(); private final SDPObserver sdpObserver = new SDPObserver();
private VideoRenderer.Callbacks localRender; private VideoRenderer.Callbacks localRender;
private VideoRenderer.Callbacks remoteRender; private VideoRenderer.Callbacks remoteRender;
private SignalingParameters signalingParameters; private SignalingParameters signalingParameters;
private MediaConstraints videoConstraints;
private PeerConnectionParameters peerConnectionParameters;
// Queued remote ICE candidates are consumed only after both local and // Queued remote ICE candidates are consumed only after both local and
// remote descriptions are set. Similarly local ICE candidates are sent to // remote descriptions are set. Similarly local ICE candidates are sent to
// remote peer after both local and remote description are set. // remote peer after both local and remote description are set.
private LinkedList<IceCandidate> queuedRemoteCandidates = null; private LinkedList<IceCandidate> queuedRemoteCandidates = null;
private MediaConstraints sdpMediaConstraints; private MediaConstraints sdpMediaConstraints;
private PeerConnectionEvents events; private PeerConnectionEvents events;
private int startBitrate;
private boolean isInitiator; private boolean isInitiator;
private boolean useFrontFacingCamera = true; private boolean useFrontFacingCamera = true;
private SessionDescription localSdp = null; // either offer or answer SDP private SessionDescription localSdp = null; // either offer or answer SDP
private MediaStream mediaStream = null; private MediaStream mediaStream = null;
// enableVideo is set to true if video should be rendered and sent.
private boolean renderVideo = true;
private VideoTrack localVideoTrack = null;
private VideoTrack remoteVideoTrack = null;
/** /**
* SDP/ICE ready callbacks. * Peer connection parameters.
*/
public static class PeerConnectionParameters {
public final int videoWidth;
public final int videoHeight;
public final int videoFps;
public final int videoStartBitrate;
public final boolean cpuOveruseDetection;
public PeerConnectionParameters(int videoWidth, int videoHeight,
int videoFps, int videoStartBitrate, boolean cpuOveruseDetection) {
this.videoWidth = videoWidth;
this.videoHeight = videoHeight;
this.videoFps = videoFps;
this.videoStartBitrate = videoStartBitrate;
this.cpuOveruseDetection = cpuOveruseDetection;
}
}
/**
* Peer connection events.
*/ */
public static interface PeerConnectionEvents { public static interface PeerConnectionEvents {
/** /**
@ -123,11 +165,15 @@ public class PeerConnectionClient {
*/ */
public void onPeerConnectionClosed(); public void onPeerConnectionClosed();
/**
* Callback fired once peer connection statistics is ready.
*/
public void onPeerConnectionStatsReady(final StatsReport[] reports);
/** /**
* Callback fired once peer connection error happened. * Callback fired once peer connection error happened.
*/ */
public void onPeerConnectionError(final String description); public void onPeerConnectionError(final String description);
} }
public PeerConnectionClient() { public PeerConnectionClient() {
@ -141,12 +187,13 @@ public class PeerConnectionClient {
final EGLContext renderEGLContext, final EGLContext renderEGLContext,
final PeerConnectionEvents events) { final PeerConnectionEvents events) {
this.events = events; this.events = events;
this.videoCodecHwAcceleration = videoCodecHwAcceleration;
executor.requestStart(); executor.requestStart();
executor.execute(new Runnable() { executor.execute(new Runnable() {
@Override @Override
public void run() { public void run() {
createPeerConnectionFactoryInternal( createPeerConnectionFactoryInternal(
context, videoCodec, videoCodecHwAcceleration, renderEGLContext); context, videoCodec, renderEGLContext);
} }
}); });
} }
@ -155,11 +202,51 @@ public class PeerConnectionClient {
final VideoRenderer.Callbacks localRender, final VideoRenderer.Callbacks localRender,
final VideoRenderer.Callbacks remoteRender, final VideoRenderer.Callbacks remoteRender,
final SignalingParameters signalingParameters, final SignalingParameters signalingParameters,
final int startBitrate) { final PeerConnectionParameters peerConnectionParameters) {
this.localRender = localRender; this.localRender = localRender;
this.remoteRender = remoteRender; this.remoteRender = remoteRender;
this.signalingParameters = signalingParameters; this.signalingParameters = signalingParameters;
this.startBitrate = startBitrate; this.peerConnectionParameters = peerConnectionParameters;
// Merge video constraints from signaling parameters and peer connection
// parameters.
videoConstraints = signalingParameters.videoConstraints;
if (videoConstraints != null && peerConnectionParameters != null) {
int videoWidth = peerConnectionParameters.videoWidth;
int videoHeight = peerConnectionParameters.videoHeight;
// If HW video encoder is supported and video resolution is not
// specified force it to HD.
if ((videoWidth == 0 || videoHeight == 0) && videoCodecHwAcceleration &&
MediaCodecVideoEncoder.isPlatformSupported()) {
videoWidth = HD_VIDEO_WIDTH;
videoHeight = HD_VIDEO_HEIGHT;
}
// Add video resolution constraints.
if (videoWidth > 0 && videoHeight > 0) {
videoWidth = Math.min(videoWidth, MAX_VIDEO_WIDTH);
videoHeight = Math.min(videoHeight, MAX_VIDEO_HEIGHT);
videoConstraints.mandatory.add(new KeyValuePair(
MIN_VIDEO_WIDTH_CONSTRAINT, Integer.toString(videoWidth)));
videoConstraints.mandatory.add(new KeyValuePair(
MAX_VIDEO_WIDTH_CONSTRAINT, Integer.toString(videoWidth)));
videoConstraints.mandatory.add(new KeyValuePair(
MIN_VIDEO_HEIGHT_CONSTRAINT, Integer.toString(videoHeight)));
videoConstraints.mandatory.add(new KeyValuePair(
MAX_VIDEO_HEIGHT_CONSTRAINT, Integer.toString(videoHeight)));
}
// Add fps constraints.
int videoFps = peerConnectionParameters.videoFps;
if (videoFps > 0) {
videoFps = Math.min(videoFps, MAX_VIDEO_FPS);
videoConstraints.mandatory.add(new KeyValuePair(
MIN_VIDEO_FPS_CONSTRAINT, Integer.toString(videoFps)));
videoConstraints.mandatory.add(new KeyValuePair(
MAX_VIDEO_FPS_CONSTRAINT, Integer.toString(videoFps)));
}
}
executor.execute(new Runnable() { executor.execute(new Runnable() {
@Override @Override
public void run() { public void run() {
@ -181,7 +268,6 @@ public class PeerConnectionClient {
private void createPeerConnectionFactoryInternal( private void createPeerConnectionFactoryInternal(
Context context, Context context,
String videoCodec, String videoCodec,
boolean videoCodecHwAcceleration,
EGLContext renderEGLContext) { EGLContext renderEGLContext) {
Log.d(TAG, "Create peer connection factory with EGLContext " Log.d(TAG, "Create peer connection factory with EGLContext "
+ renderEGLContext); + renderEGLContext);
@ -204,7 +290,8 @@ public class PeerConnectionClient {
Log.e(TAG, "Peerconnection factory is not created"); Log.e(TAG, "Peerconnection factory is not created");
return; return;
} }
Log.d(TAG, "Create peer connection."); Log.d(TAG, "Create peer connection. VideoConstraints: "
+ videoConstraints.toString());
isInitiator = signalingParameters.initiator; isInitiator = signalingParameters.initiator;
queuedRemoteCandidates = new LinkedList<IceCandidate>(); queuedRemoteCandidates = new LinkedList<IceCandidate>();
@ -217,8 +304,8 @@ public class PeerConnectionClient {
MediaConstraints pcConstraints = signalingParameters.pcConstraints; MediaConstraints pcConstraints = signalingParameters.pcConstraints;
pcConstraints.optional.add( pcConstraints.optional.add(
new MediaConstraints.KeyValuePair("RtpDataChannels", "true")); new MediaConstraints.KeyValuePair("RtpDataChannels", "true"));
pc = factory.createPeerConnection(signalingParameters.iceServers, peerConnection = factory.createPeerConnection(
pcConstraints, pcObserver); signalingParameters.iceServers, pcConstraints, pcObserver);
isInitiator = false; isInitiator = false;
// Uncomment to get ALL WebRTC tracing and SENSITIVE libjingle logging. // Uncomment to get ALL WebRTC tracing and SENSITIVE libjingle logging.
@ -229,7 +316,7 @@ public class PeerConnectionClient {
// Logging.Severity.LS_SENSITIVE); // Logging.Severity.LS_SENSITIVE);
mediaStream = factory.createLocalMediaStream("ARDAMS"); mediaStream = factory.createLocalMediaStream("ARDAMS");
if (signalingParameters.videoConstraints != null) { if (videoConstraints != null) {
mediaStream.addTrack(createVideoTrack(useFrontFacingCamera)); mediaStream.addTrack(createVideoTrack(useFrontFacingCamera));
} }
@ -238,15 +325,17 @@ public class PeerConnectionClient {
AUDIO_TRACK_ID, AUDIO_TRACK_ID,
factory.createAudioSource(signalingParameters.audioConstraints))); factory.createAudioSource(signalingParameters.audioConstraints)));
} }
pc.addStream(mediaStream); peerConnection.addStream(mediaStream);
Log.d(TAG, "Peer connection created."); Log.d(TAG, "Peer connection created.");
} }
private void closeInternal() { private void closeInternal() {
Log.d(TAG, "Closing peer connection."); Log.d(TAG, "Closing peer connection.");
if (pc != null) { statsTimer.cancel();
pc.dispose(); if (peerConnection != null) {
pc = null; peerConnection.dispose();
peerConnection = null;
} }
if (videoSource != null) { if (videoSource != null) {
videoSource.dispose(); videoSource.dispose();
@ -261,22 +350,90 @@ public class PeerConnectionClient {
events.onPeerConnectionClosed(); events.onPeerConnectionClosed();
} }
public boolean getStats(StatsObserver observer, MediaStreamTrack track) { public boolean isHDVideo() {
if (pc != null && !isError) { if (videoConstraints == null) {
return pc.getStats(observer, track); return false;
}
int minWidth = 0;
int minHeight = 0;
for (KeyValuePair keyValuePair : videoConstraints.mandatory) {
if (keyValuePair.getKey().equals("minWidth")) {
try {
minWidth = Integer.parseInt(keyValuePair.getValue());
} catch (NumberFormatException e) {
Log.e(TAG, "Can not parse video width from video constraints");
}
} else if (keyValuePair.getKey().equals("minHeight")) {
try {
minHeight = Integer.parseInt(keyValuePair.getValue());
} catch (NumberFormatException e) {
Log.e(TAG, "Can not parse video height from video constraints");
}
}
}
if (minWidth * minHeight >= 1280 * 720) {
return true;
} else { } else {
return false; return false;
} }
} }
private void getStats() {
if (peerConnection == null || isError) {
return;
}
boolean success = peerConnection.getStats(new StatsObserver() {
@Override
public void onComplete(final StatsReport[] reports) {
events.onPeerConnectionStatsReady(reports);
}
}, null);
if (!success) {
Log.e(TAG, "getStats() returns false!");
}
}
public void enableStatsEvents(boolean enable, int periodMs) {
if (enable) {
statsTimer.schedule(new TimerTask() {
@Override
public void run() {
executor.execute(new Runnable() {
@Override
public void run() {
getStats();
}
});
}
}, 0, periodMs);
} else {
statsTimer.cancel();
}
}
public void setVideoEnabled(final boolean enable) {
executor.execute(new Runnable() {
@Override
public void run() {
renderVideo = enable;
if (localVideoTrack != null) {
localVideoTrack.setEnabled(renderVideo);
}
if (remoteVideoTrack != null) {
remoteVideoTrack.setEnabled(renderVideo);
}
}
});
}
public void createOffer() { public void createOffer() {
executor.execute(new Runnable() { executor.execute(new Runnable() {
@Override @Override
public void run() { public void run() {
if (pc != null && !isError) { if (peerConnection != null && !isError) {
Log.d(TAG, "PC Create OFFER"); Log.d(TAG, "PC Create OFFER");
isInitiator = true; isInitiator = true;
pc.createOffer(sdpObserver, sdpMediaConstraints); peerConnection.createOffer(sdpObserver, sdpMediaConstraints);
} }
} }
}); });
@ -286,10 +443,10 @@ public class PeerConnectionClient {
executor.execute(new Runnable() { executor.execute(new Runnable() {
@Override @Override
public void run() { public void run() {
if (pc != null && !isError) { if (peerConnection != null && !isError) {
Log.d(TAG, "PC create ANSWER"); Log.d(TAG, "PC create ANSWER");
isInitiator = false; isInitiator = false;
pc.createAnswer(sdpObserver, sdpMediaConstraints); peerConnection.createAnswer(sdpObserver, sdpMediaConstraints);
} }
} }
}); });
@ -299,11 +456,11 @@ public class PeerConnectionClient {
executor.execute(new Runnable() { executor.execute(new Runnable() {
@Override @Override
public void run() { public void run() {
if (pc != null && !isError) { if (peerConnection != null && !isError) {
if (queuedRemoteCandidates != null) { if (queuedRemoteCandidates != null) {
queuedRemoteCandidates.add(candidate); queuedRemoteCandidates.add(candidate);
} else { } else {
pc.addIceCandidate(candidate); peerConnection.addIceCandidate(candidate);
} }
} }
} }
@ -314,20 +471,21 @@ public class PeerConnectionClient {
executor.execute(new Runnable() { executor.execute(new Runnable() {
@Override @Override
public void run() { public void run() {
if (pc == null || isError) { if (peerConnection == null || isError) {
return; return;
} }
String sdpDescription = sdp.description; String sdpDescription = sdp.description;
if (PREFER_ISAC) { if (PREFER_ISAC) {
sdpDescription = preferISAC(sdpDescription); sdpDescription = preferISAC(sdpDescription);
} }
if (startBitrate > 0) { if (peerConnectionParameters.videoStartBitrate > 0) {
sdpDescription = setStartBitrate(sdpDescription, startBitrate); sdpDescription = setStartBitrate(sdpDescription,
peerConnectionParameters.videoStartBitrate);
} }
Log.d(TAG, "Set remote SDP."); Log.d(TAG, "Set remote SDP.");
SessionDescription sdpRemote = new SessionDescription( SessionDescription sdpRemote = new SessionDescription(
sdp.type, sdpDescription); sdp.type, sdpDescription);
pc.setRemoteDescription(sdpObserver, sdpRemote); peerConnection.setRemoteDescription(sdpObserver, sdpRemote);
} }
}); });
} }
@ -405,13 +563,14 @@ public class PeerConnectionClient {
videoSource.dispose(); videoSource.dispose();
} }
videoSource = factory.createVideoSource( videoSource = factory.createVideoSource(capturer, videoConstraints);
capturer, signalingParameters.videoConstraints);
String trackExtension = frontFacing ? "frontFacing" : "backFacing"; String trackExtension = frontFacing ? "frontFacing" : "backFacing";
VideoTrack videoTrack =
localVideoTrack =
factory.createVideoTrack(VIDEO_TRACK_ID + trackExtension, videoSource); factory.createVideoTrack(VIDEO_TRACK_ID + trackExtension, videoSource);
videoTrack.addRenderer(new VideoRenderer(localRender)); localVideoTrack.setEnabled(renderVideo);
return videoTrack; localVideoTrack.addRenderer(new VideoRenderer(localRender));
return localVideoTrack;
} }
private static String setStartBitrate( private static String setStartBitrate(
@ -500,23 +659,24 @@ public class PeerConnectionClient {
if (queuedRemoteCandidates != null) { if (queuedRemoteCandidates != null) {
Log.d(TAG, "Add " + queuedRemoteCandidates.size() + " remote candidates"); Log.d(TAG, "Add " + queuedRemoteCandidates.size() + " remote candidates");
for (IceCandidate candidate : queuedRemoteCandidates) { for (IceCandidate candidate : queuedRemoteCandidates) {
pc.addIceCandidate(candidate); peerConnection.addIceCandidate(candidate);
} }
queuedRemoteCandidates = null; queuedRemoteCandidates = null;
} }
} }
private void switchCameraInternal() { private void switchCameraInternal() {
if (signalingParameters.videoConstraints == null) { if (videoConstraints == null) {
return; // No video is sent. return; // No video is sent.
} }
if (pc.signalingState() != PeerConnection.SignalingState.STABLE) { if (peerConnection.signalingState()
!= PeerConnection.SignalingState.STABLE) {
Log.e(TAG, "Switching camera during negotiation is not handled."); Log.e(TAG, "Switching camera during negotiation is not handled.");
return; return;
} }
Log.d(TAG, "Switch camera"); Log.d(TAG, "Switch camera");
pc.removeStream(mediaStream); peerConnection.removeStream(mediaStream);
VideoTrack currentTrack = mediaStream.videoTracks.get(0); VideoTrack currentTrack = mediaStream.videoTracks.get(0);
mediaStream.removeTrack(currentTrack); mediaStream.removeTrack(currentTrack);
@ -530,9 +690,9 @@ public class PeerConnectionClient {
useFrontFacingCamera = !useFrontFacingCamera; useFrontFacingCamera = !useFrontFacingCamera;
VideoTrack newTrack = createVideoTrack(useFrontFacingCamera); VideoTrack newTrack = createVideoTrack(useFrontFacingCamera);
mediaStream.addTrack(newTrack); mediaStream.addTrack(newTrack);
pc.addStream(mediaStream); peerConnection.addStream(mediaStream);
SessionDescription remoteDesc = pc.getRemoteDescription(); SessionDescription remoteDesc = peerConnection.getRemoteDescription();
if (localSdp == null || remoteDesc == null) { if (localSdp == null || remoteDesc == null) {
Log.d(TAG, "Switching camera before the negotiation started."); Log.d(TAG, "Switching camera before the negotiation started.");
return; return;
@ -542,11 +702,15 @@ public class PeerConnectionClient {
localSdp.description.replaceAll(trackId, newTrack.id())); localSdp.description.replaceAll(trackId, newTrack.id()));
if (isInitiator) { if (isInitiator) {
pc.setLocalDescription(new SwitchCameraSdbObserver(), localSdp); peerConnection.setLocalDescription(
pc.setRemoteDescription(new SwitchCameraSdbObserver(), remoteDesc); new SwitchCameraSdbObserver(), localSdp);
peerConnection.setRemoteDescription(
new SwitchCameraSdbObserver(), remoteDesc);
} else { } else {
pc.setRemoteDescription(new SwitchCameraSdbObserver(), remoteDesc); peerConnection.setRemoteDescription(
pc.setLocalDescription(new SwitchCameraSdbObserver(), localSdp); new SwitchCameraSdbObserver(), remoteDesc);
peerConnection.setLocalDescription(
new SwitchCameraSdbObserver(), localSdp);
} }
Log.d(TAG, "Switch camera done"); Log.d(TAG, "Switch camera done");
} }
@ -555,7 +719,7 @@ public class PeerConnectionClient {
executor.execute(new Runnable() { executor.execute(new Runnable() {
@Override @Override
public void run() { public void run() {
if (pc != null && !isError) { if (peerConnection != null && !isError) {
switchCameraInternal(); switchCameraInternal();
} }
} }
@ -609,7 +773,7 @@ public class PeerConnectionClient {
executor.execute(new Runnable() { executor.execute(new Runnable() {
@Override @Override
public void run() { public void run() {
if (pc == null || isError) { if (peerConnection == null || isError) {
return; return;
} }
if (stream.audioTracks.size() > 1 || stream.videoTracks.size() > 1) { if (stream.audioTracks.size() > 1 || stream.videoTracks.size() > 1) {
@ -617,8 +781,9 @@ public class PeerConnectionClient {
return; return;
} }
if (stream.videoTracks.size() == 1) { if (stream.videoTracks.size() == 1) {
stream.videoTracks.get(0).addRenderer( remoteVideoTrack = stream.videoTracks.get(0);
new VideoRenderer(remoteRender)); remoteVideoTrack.setEnabled(renderVideo);
remoteVideoTrack.addRenderer(new VideoRenderer(remoteRender));
} }
} }
}); });
@ -629,9 +794,10 @@ public class PeerConnectionClient {
executor.execute(new Runnable() { executor.execute(new Runnable() {
@Override @Override
public void run() { public void run() {
if (pc == null || isError) { if (peerConnection == null || isError) {
return; return;
} }
remoteVideoTrack = null;
stream.videoTracks.get(0).dispose(); stream.videoTracks.get(0).dispose();
} }
}); });
@ -669,9 +835,9 @@ public class PeerConnectionClient {
executor.execute(new Runnable() { executor.execute(new Runnable() {
@Override @Override
public void run() { public void run() {
if (pc != null && !isError) { if (peerConnection != null && !isError) {
Log.d(TAG, "Set local SDP from " + sdp.type); Log.d(TAG, "Set local SDP from " + sdp.type);
pc.setLocalDescription(sdpObserver, sdp); peerConnection.setLocalDescription(sdpObserver, sdp);
} }
} }
}); });
@ -682,13 +848,13 @@ public class PeerConnectionClient {
executor.execute(new Runnable() { executor.execute(new Runnable() {
@Override @Override
public void run() { public void run() {
if (pc == null || isError) { if (peerConnection == null || isError) {
return; return;
} }
if (isInitiator) { if (isInitiator) {
// For offering peer connection we first create offer and set // For offering peer connection we first create offer and set
// local SDP, then after receiving answer set remote SDP. // local SDP, then after receiving answer set remote SDP.
if (pc.getRemoteDescription() == null) { if (peerConnection.getRemoteDescription() == null) {
// We've just set our local SDP so time to send it. // We've just set our local SDP so time to send it.
Log.d(TAG, "Local SDP set succesfully"); Log.d(TAG, "Local SDP set succesfully");
events.onLocalDescription(localSdp); events.onLocalDescription(localSdp);
@ -701,7 +867,7 @@ public class PeerConnectionClient {
} else { } else {
// For answering peer connection we set remote SDP and then // For answering peer connection we set remote SDP and then
// create answer and set local SDP. // create answer and set local SDP.
if (pc.getLocalDescription() != null) { if (peerConnection.getLocalDescription() != null) {
// We've just set our local SDP so time to send it, drain // We've just set our local SDP so time to send it, drain
// remote and send local ICE candidates. // remote and send local ICE candidates.
Log.d(TAG, "Local SDP set succesfully"); Log.d(TAG, "Local SDP set succesfully");

View File

@ -56,6 +56,8 @@ public class RoomParametersFetcher {
private static final String TAG = "RoomRTCClient"; private static final String TAG = "RoomRTCClient";
private final RoomParametersFetcherEvents events; private final RoomParametersFetcherEvents events;
private final boolean loopback; private final boolean loopback;
private final String registerUrl;
private final String registerMessage;
private AsyncHttpURLConnection httpConnection; private AsyncHttpURLConnection httpConnection;
/** /**
@ -75,12 +77,17 @@ public class RoomParametersFetcher {
} }
public RoomParametersFetcher(boolean loopback, String registerUrl, public RoomParametersFetcher(boolean loopback, String registerUrl,
final RoomParametersFetcherEvents events) { String registerMessage, final RoomParametersFetcherEvents events) {
Log.d(TAG, "Connecting to room: " + registerUrl);
this.loopback = loopback; this.loopback = loopback;
this.registerUrl = registerUrl;
this.registerMessage = registerMessage;
this.events = events; this.events = events;
}
httpConnection = new AsyncHttpURLConnection("POST", registerUrl, null, public void makeRequest() {
Log.d(TAG, "Connecting to room: " + registerUrl);
httpConnection = new AsyncHttpURLConnection(
"POST", registerUrl, registerMessage,
new AsyncHttpEvents() { new AsyncHttpEvents() {
@Override @Override
public void onHttpError(String errorMessage) { public void onHttpError(String errorMessage) {
@ -179,8 +186,7 @@ public class RoomParametersFetcher {
SignalingParameters params = new SignalingParameters( SignalingParameters params = new SignalingParameters(
iceServers, initiator, iceServers, initiator,
pcConstraints, videoConstraints, audioConstraints, pcConstraints, videoConstraints, audioConstraints,
roomId, clientId, clientId, wssUrl, wssPostUrl,
wssUrl, wssPostUrl,
offerSdp, iceCandidates); offerSdp, iceCandidates);
events.onSignalingParametersReady(params); events.onSignalingParametersReady(params);
} catch (JSONException e) { } catch (JSONException e) {

View File

@ -47,6 +47,7 @@ public class SettingsActivity extends Activity
private String keyprefHwCodec; private String keyprefHwCodec;
private String keyprefCpuUsageDetection; private String keyprefCpuUsageDetection;
private String keyPrefRoomServerUrl; private String keyPrefRoomServerUrl;
private String keyPrefDisplayHud;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@ -59,6 +60,7 @@ public class SettingsActivity extends Activity
keyprefHwCodec = getString(R.string.pref_hwcodec_key); keyprefHwCodec = getString(R.string.pref_hwcodec_key);
keyprefCpuUsageDetection = getString(R.string.pref_cpu_usage_detection_key); keyprefCpuUsageDetection = getString(R.string.pref_cpu_usage_detection_key);
keyPrefRoomServerUrl = getString(R.string.pref_room_server_url_key); keyPrefRoomServerUrl = getString(R.string.pref_room_server_url_key);
keyPrefDisplayHud = getString(R.string.pref_displayhud_key);
// Display the fragment as the main content. // Display the fragment as the main content.
settingsFragment = new SettingsFragment(); settingsFragment = new SettingsFragment();
@ -83,6 +85,7 @@ public class SettingsActivity extends Activity
updateSummaryB(sharedPreferences, keyprefHwCodec); updateSummaryB(sharedPreferences, keyprefHwCodec);
updateSummaryB(sharedPreferences, keyprefCpuUsageDetection); updateSummaryB(sharedPreferences, keyprefCpuUsageDetection);
updateSummary(sharedPreferences, keyPrefRoomServerUrl); updateSummary(sharedPreferences, keyPrefRoomServerUrl);
updateSummaryB(sharedPreferences, keyPrefDisplayHud);
} }
@Override @Override
@ -105,7 +108,7 @@ public class SettingsActivity extends Activity
} else if (key.equals(keyprefStartBitrateValue)) { } else if (key.equals(keyprefStartBitrateValue)) {
updateSummaryBitrate(sharedPreferences, key); updateSummaryBitrate(sharedPreferences, key);
} else if (key.equals(keyprefCpuUsageDetection) } else if (key.equals(keyprefCpuUsageDetection)
|| key.equals(keyprefHwCodec)) { || key.equals(keyprefHwCodec) || key.equals(keyPrefDisplayHud)) {
updateSummaryB(sharedPreferences, key); updateSummaryB(sharedPreferences, key);
} }
if (key.equals(keyprefStartBitrateType)) { if (key.equals(keyprefStartBitrateType)) {

View File

@ -54,43 +54,43 @@ import org.webrtc.SessionDescription;
public class WebSocketRTCClient implements AppRTCClient, public class WebSocketRTCClient implements AppRTCClient,
WebSocketChannelEvents { WebSocketChannelEvents {
private static final String TAG = "WSRTCClient"; private static final String TAG = "WSRTCClient";
public static final String GAE_JOIN = "join"; private static final String ROOM_JOIN = "join";
private static final String GAE_MESSAGE = "message"; private static final String ROOM_MESSAGE = "message";
private static final String GAE_LEAVE = "leave"; private static final String ROOM_LEAVE = "leave";
private enum ConnectionState { private enum ConnectionState {
NEW, CONNECTED, CLOSED, ERROR NEW, CONNECTED, CLOSED, ERROR
}; };
private enum MessageType { private enum MessageType {
MESSAGE, BYE MESSAGE, LEAVE
}; };
private final LooperExecutor executor; private final LooperExecutor executor;
private boolean loopback;
private boolean initiator; private boolean initiator;
private SignalingEvents events; private SignalingEvents events;
private WebSocketChannelClient wsClient; private WebSocketChannelClient wsClient;
private ConnectionState roomState; private ConnectionState roomState;
private String roomUrl; private RoomConnectionParameters connectionParameters;
private String postMessageUrl; private String messageUrl;
private String byeMessageUrl; private String leaveUrl;
public WebSocketRTCClient(SignalingEvents events) { public WebSocketRTCClient(SignalingEvents events, LooperExecutor executor) {
this.events = events; this.events = events;
executor = new LooperExecutor(); this.executor = executor;
roomState = ConnectionState.NEW;
} }
// -------------------------------------------------------------------- // --------------------------------------------------------------------
// AppRTCClient interface implementation. // AppRTCClient interface implementation.
// Asynchronously connect to an AppRTC room URL, e.g. // Asynchronously connect to an AppRTC room URL using supplied connection
// https://apprtc.appspot.com/register/<room>, retrieve room parameters // parameters, retrieves room parameters and connect to WebSocket server.
// and connect to WebSocket server.
@Override @Override
public void connectToRoom(final String url, final boolean loopback) { public void connectToRoom(RoomConnectionParameters connectionParameters) {
this.connectionParameters = connectionParameters;
executor.requestStart(); executor.requestStart();
executor.execute(new Runnable() { executor.execute(new Runnable() {
@Override @Override
public void run() { public void run() {
connectToRoomInternal(url, loopback); connectToRoomInternal();
} }
}); });
} }
@ -107,33 +107,32 @@ public class WebSocketRTCClient implements AppRTCClient,
} }
// Connects to room - function runs on a local looper thread. // Connects to room - function runs on a local looper thread.
private void connectToRoomInternal(String url, boolean loopback) { private void connectToRoomInternal() {
Log.d(TAG, "Connect to room: " + url); String connectionUrl = getConnectionUrl(connectionParameters);
this.loopback = loopback; Log.d(TAG, "Connect to room: " + connectionUrl);
roomState = ConnectionState.NEW; roomState = ConnectionState.NEW;
roomUrl = url.substring(0, url.indexOf("/" + GAE_JOIN));
// Create WebSocket client.
wsClient = new WebSocketChannelClient(executor, this); wsClient = new WebSocketChannelClient(executor, this);
// Get room parameters.
new RoomParametersFetcher(loopback, url, RoomParametersFetcherEvents callbacks = new RoomParametersFetcherEvents() {
new RoomParametersFetcherEvents() {
@Override @Override
public void onSignalingParametersReady( public void onSignalingParametersReady(
final SignalingParameters params) { final SignalingParameters params) {
executor.execute(new Runnable() { WebSocketRTCClient.this.executor.execute(new Runnable() {
@Override @Override
public void run() { public void run() {
signalingParametersReady(params); WebSocketRTCClient.this.signalingParametersReady(params);
} }
}); });
} }
@Override @Override
public void onSignalingParametersError(String description) { public void onSignalingParametersError(String description) {
reportError(description); WebSocketRTCClient.this.reportError(description);
} }
} };
);
new RoomParametersFetcher(connectionParameters.loopback, connectionUrl,
null, callbacks).makeRequest();
} }
// Disconnect from room and send bye messages - runs on a local looper thread. // Disconnect from room and send bye messages - runs on a local looper thread.
@ -141,7 +140,7 @@ public class WebSocketRTCClient implements AppRTCClient,
Log.d(TAG, "Disconnect. Room state: " + roomState); Log.d(TAG, "Disconnect. Room state: " + roomState);
if (roomState == ConnectionState.CONNECTED) { if (roomState == ConnectionState.CONNECTED) {
Log.d(TAG, "Closing room."); Log.d(TAG, "Closing room.");
sendPostMessage(MessageType.BYE, byeMessageUrl, ""); sendPostMessage(MessageType.LEAVE, leaveUrl, null);
} }
roomState = ConnectionState.CLOSED; roomState = ConnectionState.CLOSED;
if (wsClient != null) { if (wsClient != null) {
@ -150,57 +149,53 @@ public class WebSocketRTCClient implements AppRTCClient,
} }
// Helper functions to get connection, post message and leave message URLs // Helper functions to get connection, post message and leave message URLs
public static String getGAEConnectionUrl(String roomUrl, String roomId) { private String getConnectionUrl(
return roomUrl + "/" + GAE_JOIN + "/" + roomId; RoomConnectionParameters connectionParameters) {
return connectionParameters.roomUrl + "/" + ROOM_JOIN + "/"
+ connectionParameters.roomId;
} }
private String getGAEPostMessageUrl(String roomUrl, String roomId, private String getMessageUrl(RoomConnectionParameters connectionParameters,
String clientId) { SignalingParameters signalingParameters) {
return roomUrl + "/" + GAE_MESSAGE + "/" + roomId + "/" + clientId; return connectionParameters.roomUrl + "/" + ROOM_MESSAGE + "/"
+ connectionParameters.roomId + "/" + signalingParameters.clientId;
} }
private String getGAEByeMessageUrl(String roomUrl, String roomId, private String getLeaveUrl(RoomConnectionParameters connectionParameters,
String clientId) { SignalingParameters signalingParameters) {
return roomUrl + "/" + GAE_LEAVE + "/" + roomId + "/" + clientId; return connectionParameters.roomUrl + "/" + ROOM_LEAVE + "/"
+ connectionParameters.roomId + "/" + signalingParameters.clientId;
} }
// Callback issued when room parameters are extracted. Runs on local // Callback issued when room parameters are extracted. Runs on local
// looper thread. // looper thread.
private void signalingParametersReady(final SignalingParameters params) { private void signalingParametersReady(
final SignalingParameters signalingParameters) {
Log.d(TAG, "Room connection completed."); Log.d(TAG, "Room connection completed.");
if (loopback && (!params.initiator || params.offerSdp != null)) { if (connectionParameters.loopback
&& (!signalingParameters.initiator
|| signalingParameters.offerSdp != null)) {
reportError("Loopback room is busy."); reportError("Loopback room is busy.");
return; return;
} }
if (!loopback && !params.initiator && params.offerSdp == null) { if (!connectionParameters.loopback
&& !signalingParameters.initiator
&& signalingParameters.offerSdp == null) {
Log.w(TAG, "No offer SDP in room response."); Log.w(TAG, "No offer SDP in room response.");
} }
initiator = params.initiator; initiator = signalingParameters.initiator;
postMessageUrl = getGAEPostMessageUrl( messageUrl = getMessageUrl(connectionParameters, signalingParameters);
roomUrl, params.roomId, params.clientId); leaveUrl = getLeaveUrl(connectionParameters, signalingParameters);
byeMessageUrl = getGAEByeMessageUrl( Log.d(TAG, "Message URL: " + messageUrl);
roomUrl, params.roomId, params.clientId); Log.d(TAG, "Leave URL: " + leaveUrl);
roomState = ConnectionState.CONNECTED; roomState = ConnectionState.CONNECTED;
// Fire connection and signaling parameters events. // Fire connection and signaling parameters events.
events.onConnectedToRoom(params); events.onConnectedToRoom(signalingParameters);
// Connect to WebSocket server. // Connect to WebSocket server.
wsClient.connect( wsClient.connect(signalingParameters.wssUrl, signalingParameters.wssPostUrl,
params.wssUrl, params.wssPostUrl, params.roomId, params.clientId); connectionParameters.roomId, signalingParameters.clientId);
// For call receiver get sdp offer and ice candidates
// from room parameters and fire corresponding events.
if (!params.initiator) {
if (params.offerSdp != null) {
events.onRemoteDescription(params.offerSdp);
}
if (params.iceCandidates != null) {
for (IceCandidate iceCandidate : params.iceCandidates) {
events.onRemoteIceCandidate(iceCandidate);
}
}
}
} }
// Send local offer SDP to the other participant. // Send local offer SDP to the other participant.
@ -216,8 +211,8 @@ public class WebSocketRTCClient implements AppRTCClient,
JSONObject json = new JSONObject(); JSONObject json = new JSONObject();
jsonPut(json, "sdp", sdp.description); jsonPut(json, "sdp", sdp.description);
jsonPut(json, "type", "offer"); jsonPut(json, "type", "offer");
sendPostMessage(MessageType.MESSAGE, postMessageUrl, json.toString()); sendPostMessage(MessageType.MESSAGE, messageUrl, json.toString());
if (loopback) { if (connectionParameters.loopback) {
// In loopback mode rename this offer to answer and route it back. // In loopback mode rename this offer to answer and route it back.
SessionDescription sdpAnswer = new SessionDescription( SessionDescription sdpAnswer = new SessionDescription(
SessionDescription.Type.fromCanonicalForm("answer"), SessionDescription.Type.fromCanonicalForm("answer"),
@ -234,7 +229,7 @@ public class WebSocketRTCClient implements AppRTCClient,
executor.execute(new Runnable() { executor.execute(new Runnable() {
@Override @Override
public void run() { public void run() {
if (loopback) { if (connectionParameters.loopback) {
Log.e(TAG, "Sending answer in loopback mode."); Log.e(TAG, "Sending answer in loopback mode.");
return; return;
} }
@ -267,8 +262,8 @@ public class WebSocketRTCClient implements AppRTCClient,
reportError("Sending ICE candidate in non connected state."); reportError("Sending ICE candidate in non connected state.");
return; return;
} }
sendPostMessage(MessageType.MESSAGE, postMessageUrl, json.toString()); sendPostMessage(MessageType.MESSAGE, messageUrl, json.toString());
if (loopback) { if (connectionParameters.loopback) {
events.onRemoteIceCandidate(candidate); events.onRemoteIceCandidate(candidate);
} }
} else { } else {
@ -384,11 +379,11 @@ public class WebSocketRTCClient implements AppRTCClient,
// Send SDP or ICE candidate to a room server. // Send SDP or ICE candidate to a room server.
private void sendPostMessage( private void sendPostMessage(
final MessageType messageType, final String url, final String message) { final MessageType messageType, final String url, final String message) {
if (messageType == MessageType.BYE) { String logInfo = url;
Log.d(TAG, "C->GAE: " + url); if (message != null) {
} else { logInfo += ". Message: " + message;
Log.d(TAG, "C->GAE: " + message);
} }
Log.d(TAG, "C->GAE: " + logInfo);
AsyncHttpURLConnection httpConnection = new AsyncHttpURLConnection( AsyncHttpURLConnection httpConnection = new AsyncHttpURLConnection(
"POST", url, message, new AsyncHttpEvents() { "POST", url, message, new AsyncHttpEvents() {
@Override @Override

View File

@ -36,7 +36,7 @@ import java.net.URL;
import java.util.Scanner; import java.util.Scanner;
/** /**
* Asynchronious http requests implementation. * Asynchronous http requests implementation.
*/ */
public class AsyncHttpURLConnection { public class AsyncHttpURLConnection {
private static final int HTTP_TIMEOUT_MS = 5000; private static final int HTTP_TIMEOUT_MS = 5000;

View File

@ -35,11 +35,13 @@ import java.util.concurrent.TimeUnit;
import org.appspot.apprtc.AppRTCClient.SignalingParameters; import org.appspot.apprtc.AppRTCClient.SignalingParameters;
import org.appspot.apprtc.PeerConnectionClient; import org.appspot.apprtc.PeerConnectionClient;
import org.appspot.apprtc.PeerConnectionClient.PeerConnectionEvents; import org.appspot.apprtc.PeerConnectionClient.PeerConnectionEvents;
import org.appspot.apprtc.PeerConnectionClient.PeerConnectionParameters;
import org.appspot.apprtc.util.LooperExecutor; import org.appspot.apprtc.util.LooperExecutor;
import org.webrtc.IceCandidate; import org.webrtc.IceCandidate;
import org.webrtc.MediaConstraints; import org.webrtc.MediaConstraints;
import org.webrtc.PeerConnection; import org.webrtc.PeerConnection;
import org.webrtc.SessionDescription; import org.webrtc.SessionDescription;
import org.webrtc.StatsReport;
import org.webrtc.VideoRenderer; import org.webrtc.VideoRenderer;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
@ -167,6 +169,10 @@ public class PeerConnectionClientTest extends InstrumentationTestCase
fail("PC Error: " + description); fail("PC Error: " + description);
} }
@Override
public void onPeerConnectionStatsReady(StatsReport[] reports) {
}
// Helper wait functions. // Helper wait functions.
private boolean waitForLocalSDP(int timeoutMs) private boolean waitForLocalSDP(int timeoutMs)
throws InterruptedException { throws InterruptedException {
@ -220,8 +226,7 @@ public class PeerConnectionClientTest extends InstrumentationTestCase
SignalingParameters signalingParameters = new SignalingParameters( SignalingParameters signalingParameters = new SignalingParameters(
iceServers, true, iceServers, true,
pcConstraints, videoConstraints, audioConstraints, pcConstraints, videoConstraints, audioConstraints,
null, null, null, null, null,
null, null,
null, null); null, null);
return signalingParameters; return signalingParameters;
} }
@ -229,12 +234,14 @@ public class PeerConnectionClientTest extends InstrumentationTestCase
PeerConnectionClient createPeerConnectionClient(MockRenderer localRenderer, PeerConnectionClient createPeerConnectionClient(MockRenderer localRenderer,
MockRenderer remoteRenderer) { MockRenderer remoteRenderer) {
SignalingParameters signalingParameters = getTestSignalingParameters(); SignalingParameters signalingParameters = getTestSignalingParameters();
PeerConnectionParameters peerConnectionParameters =
new PeerConnectionParameters(0, 0, 0, 0, false);
PeerConnectionClient client = new PeerConnectionClient(); PeerConnectionClient client = new PeerConnectionClient();
client.createPeerConnectionFactory( client.createPeerConnectionFactory(
getInstrumentation().getContext(), "VP8", true, null, this); getInstrumentation().getContext(), "VP8", true, null, this);
client.createPeerConnection( client.createPeerConnection(localRenderer, remoteRenderer,
localRenderer, remoteRenderer, signalingParameters, 1000); signalingParameters, peerConnectionParameters);
client.createOffer(); client.createOffer();
return client; return client;
} }
@ -313,4 +320,5 @@ public class PeerConnectionClientTest extends InstrumentationTestCase
assertTrue(waitForPeerConnectionClosed(WAIT_TIMEOUT)); assertTrue(waitForPeerConnectionClosed(WAIT_TIMEOUT));
Log.d(TAG, "testLoopback Done."); Log.d(TAG, "testLoopback Done.");
} }
} }

View File

@ -355,17 +355,18 @@
'examples/android/res/drawable-xhdpi/ic_action_return_from_full_screen.png', 'examples/android/res/drawable-xhdpi/ic_action_return_from_full_screen.png',
'examples/android/res/drawable-xhdpi/ic_loopback_call.png', 'examples/android/res/drawable-xhdpi/ic_loopback_call.png',
'examples/android/res/drawable-xhdpi/ic_launcher.png', 'examples/android/res/drawable-xhdpi/ic_launcher.png',
'examples/android/res/layout/activity_call.xml',
'examples/android/res/layout/activity_connect.xml', 'examples/android/res/layout/activity_connect.xml',
'examples/android/res/layout/activity_fullscreen.xml', 'examples/android/res/layout/fragment_call.xml',
'examples/android/res/layout/fragment_menubar.xml',
'examples/android/res/menu/connect_menu.xml', 'examples/android/res/menu/connect_menu.xml',
'examples/android/res/values/arrays.xml', 'examples/android/res/values/arrays.xml',
'examples/android/res/values/strings.xml', 'examples/android/res/values/strings.xml',
'examples/android/res/xml/preferences.xml', 'examples/android/res/xml/preferences.xml',
'examples/android/src/org/appspot/apprtc/AppRTCAudioManager.java', 'examples/android/src/org/appspot/apprtc/AppRTCAudioManager.java',
'examples/android/src/org/appspot/apprtc/AppRTCClient.java', 'examples/android/src/org/appspot/apprtc/AppRTCClient.java',
'examples/android/src/org/appspot/apprtc/AppRTCDemoActivity.java',
'examples/android/src/org/appspot/apprtc/AppRTCProximitySensor.java', 'examples/android/src/org/appspot/apprtc/AppRTCProximitySensor.java',
'examples/android/src/org/appspot/apprtc/CallActivity.java',
'examples/android/src/org/appspot/apprtc/CallFragment.java',
'examples/android/src/org/appspot/apprtc/ConnectActivity.java', 'examples/android/src/org/appspot/apprtc/ConnectActivity.java',
'examples/android/src/org/appspot/apprtc/PeerConnectionClient.java', 'examples/android/src/org/appspot/apprtc/PeerConnectionClient.java',
'examples/android/src/org/appspot/apprtc/RoomParametersFetcher.java', 'examples/android/src/org/appspot/apprtc/RoomParametersFetcher.java',