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">
</activity>
<activity android:name="AppRTCDemoActivity"
<activity android:name="CallActivity"
android:label="@string/app_name"
android:screenOrientation="fullUser"
android:configChanges="orientation|screenSize"
android:theme="@style/AppRTCDemoActivityTheme">
android:theme="@style/CallActivityTheme">
</activity>
</application>
</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="ok">OK</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="add_room_description">Add new room to 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_title">CPU overuse detection.</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_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_title">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>

View File

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

View File

@ -58,4 +58,10 @@
android:defaultValue="@string/pref_room_server_url_default"
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>

View File

@ -38,12 +38,28 @@ import java.util.List;
* AppRTCClient is the interface representing an AppRTC client.
*/
public interface AppRTCClient {
/**
* Asynchronously connect to an AppRTC room URL, e.g.
* https://apprtc.appspot.com/?r=NNN. Once connection is established
* onConnectedToRoom() callback with room parameters is invoked.
* Struct holding the connection parameters of an AppRTC room.
*/
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.
@ -68,13 +84,12 @@ public interface AppRTCClient {
/**
* Struct holding the signaling parameters of an AppRTC room.
*/
public class SignalingParameters {
public static class SignalingParameters {
public final List<PeerConnection.IceServer> iceServers;
public final boolean initiator;
public final MediaConstraints pcConstraints;
public final MediaConstraints videoConstraints;
public final MediaConstraints audioConstraints;
public final String roomId;
public final String clientId;
public final String wssUrl;
public final String wssPostUrl;
@ -85,15 +100,13 @@ public interface AppRTCClient {
List<PeerConnection.IceServer> iceServers,
boolean initiator, MediaConstraints pcConstraints,
MediaConstraints videoConstraints, MediaConstraints audioConstraints,
String roomId, String clientId,
String wssUrl, String wssPostUrl,
String clientId, String wssUrl, String wssPostUrl,
SessionDescription offerSdp, List<IceCandidate> iceCandidates) {
this.iceServers = iceServers;
this.initiator = initiator;
this.pcConstraints = pcConstraints;
this.videoConstraints = videoConstraints;
this.audioConstraints = audioConstraints;
this.roomId = roomId;
this.clientId = clientId;
this.wssUrl = wssUrl;
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.JSONException;
import org.webrtc.MediaCodecVideoEncoder;
import java.util.ArrayList;
import java.util.Random;
@ -61,14 +60,6 @@ import java.util.Random;
* Handles the initial setup where the user selects which room to join.
*/
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 int CONNECTION_REQUEST = 1;
private static boolean commandLineRun = false;
@ -87,6 +78,7 @@ public class ConnectActivity extends Activity {
private String keyprefVideoCodec;
private String keyprefHwCodecAcceleration;
private String keyprefCpuUsageDetection;
private String keyprefDisplayHud;
private String keyprefRoomServerUrl;
private String keyprefRoom;
private String keyprefRoomList;
@ -107,6 +99,7 @@ public class ConnectActivity extends Activity {
keyprefVideoCodec = getString(R.string.pref_videocodec_key);
keyprefHwCodecAcceleration = getString(R.string.pref_hwcodec_key);
keyprefCpuUsageDetection = getString(R.string.pref_cpu_usage_detection_key);
keyprefDisplayHud = getString(R.string.pref_displayhud_key);
keyprefRoomServerUrl = getString(R.string.pref_room_server_url_key);
keyprefRoom = getString(R.string.pref_room_key);
keyprefRoomList = getString(R.string.pref_room_list_key);
@ -146,8 +139,10 @@ public class ConnectActivity extends Activity {
if ("android.intent.action.VIEW".equals(intent.getAction())
&& !commandLineRun) {
commandLineRun = true;
boolean loopback = intent.getBooleanExtra(EXTRA_LOOPBACK, false);
int runTimeMs = intent.getIntExtra(EXTRA_RUNTIME, 0);
boolean loopback = intent.getBooleanExtra(
CallActivity.EXTRA_LOOPBACK, false);
int runTimeMs = intent.getIntExtra(
CallActivity.EXTRA_RUNTIME, 0);
String room = sharedPref.getString(keyprefRoom, "");
roomEditText.setText(room);
connectToRoom(loopback, runTimeMs);
@ -216,6 +211,7 @@ public class ConnectActivity extends Activity {
if (requestCode == CONNECTION_REQUEST && commandLineRun) {
Log.d(TAG, "Return: " + resultCode);
setResult(resultCode);
commandLineRun = false;
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) {
// Get room name (random for loopback).
String roomName;
String roomId;
if (loopback) {
roomName = Integer.toString((new Random()).nextInt(100000000));
roomId = Integer.toString((new Random()).nextInt(100000000));
} else {
roomName = getSelectedItem();
if (roomName == null) {
roomName = roomEditText.getText().toString();
roomId = getSelectedItem();
if (roomId == null) {
roomId = roomEditText.getText().toString();
}
}
String url = sharedPref.getString(
String roomUrl = sharedPref.getString(
keyprefRoomServerUrl,
getString(R.string.pref_room_server_url_default));
url = WebSocketRTCClient.getGAEConnectionUrl(url, roomName);
// Get default video codec.
String videoCodec = sharedPref.getString(keyprefVideoCodec,
@ -267,60 +252,36 @@ public class ConnectActivity extends Activity {
boolean hwCodec = sharedPref.getBoolean(keyprefHwCodecAcceleration,
Boolean.valueOf(getString(R.string.pref_hwcodec_default)));
// Add video resolution constraints.
String parametersResolution = null;
String parametersFps = null;
// Get video resolution from settings.
int videoWidth = 0;
int videoHeight = 0;
String resolution = sharedPref.getString(keyprefResolution,
getString(R.string.pref_resolution_default));
String[] dimensions = resolution.split("[ x]+");
if (dimensions.length == 2) {
try {
int maxWidth = Integer.parseInt(dimensions[0]);
int maxHeight = Integer.parseInt(dimensions[1]);
if (maxWidth > 0 && maxHeight > 0) {
parametersResolution = "minHeight=" + maxHeight + ",maxHeight="
+ maxHeight + ",minWidth=" + maxWidth + ",maxWidth=" + maxWidth;
}
videoWidth = Integer.parseInt(dimensions[0]);
videoHeight = Integer.parseInt(dimensions[1]);
} catch (NumberFormatException e) {
videoWidth = 0;
videoHeight = 0;
Log.e(TAG, "Wrong video resolution setting: " + resolution);
}
}
// Add camera fps constraints.
// Get camera fps from settings.
int cameraFps = 0;
String fps = sharedPref.getString(keyprefFps,
getString(R.string.pref_fps_default));
String[] fpsValues = fps.split("[ x]+");
if (fpsValues.length == 2) {
try {
int cameraFps = Integer.parseInt(fpsValues[0]);
if (cameraFps > 0) {
parametersFps = "minFrameRate=" + cameraFps
+ ",maxFrameRate=" + cameraFps;
}
cameraFps = Integer.parseInt(fpsValues[0]);
} catch (NumberFormatException e) {
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.
int startBitrate = 0;
String bitrateTypeDefault = getString(R.string.pref_startbitrate_default);
@ -337,23 +298,31 @@ public class ConnectActivity extends Activity {
keyprefCpuUsageDetection,
Boolean.valueOf(
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.
Log.d(TAG, "Connecting to room " + roomName + " at URL " + url);
if (validateUrl(url)) {
Uri uri = Uri.parse(url);
Intent intent = new Intent(this, AppRTCDemoActivity.class);
Log.d(TAG, "Connecting to room " + roomId + " at URL " + roomUrl);
if (validateUrl(roomUrl)) {
Uri uri = Uri.parse(roomUrl);
Intent intent = new Intent(this, CallActivity.class);
intent.setData(uri);
intent.putExtra(EXTRA_ROOMNAME, roomName);
intent.putExtra(EXTRA_LOOPBACK, loopback);
intent.putExtra(EXTRA_CMDLINE, commandLineRun);
intent.putExtra(EXTRA_RUNTIME, runTimeMs);
intent.putExtra(EXTRA_BITRATE, startBitrate);
intent.putExtra(EXTRA_VIDEOCODEC, videoCodec);
intent.putExtra(EXTRA_HWCODEC, hwCodec);
intent.putExtra(CallActivity.EXTRA_ROOMID, roomId);
intent.putExtra(CallActivity.EXTRA_LOOPBACK, loopback);
intent.putExtra(CallActivity.EXTRA_VIDEOCODEC, videoCodec);
intent.putExtra(CallActivity.EXTRA_HWCODEC, hwCodec);
intent.putExtra(CallActivity.EXTRA_VIDEO_BITRATE, startBitrate);
intent.putExtra(CallActivity.EXTRA_VIDEO_WIDTH, videoWidth);
intent.putExtra(CallActivity.EXTRA_VIDEO_HEIGHT, videoHeight);
intent.putExtra(CallActivity.EXTRA_VIDEO_FPS, cameraFps);
intent.putExtra(CallActivity.EXTRA_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);
}
}

View File

@ -36,21 +36,25 @@ import android.util.Log;
import org.webrtc.DataChannel;
import org.webrtc.IceCandidate;
import org.webrtc.MediaCodecVideoEncoder;
import org.webrtc.MediaConstraints;
import org.webrtc.MediaConstraints.KeyValuePair;
import org.webrtc.MediaStream;
import org.webrtc.MediaStreamTrack;
import org.webrtc.PeerConnection;
import org.webrtc.PeerConnection.IceConnectionState;
import org.webrtc.PeerConnectionFactory;
import org.webrtc.SdpObserver;
import org.webrtc.SessionDescription;
import org.webrtc.StatsObserver;
import org.webrtc.StatsReport;
import org.webrtc.VideoCapturer;
import org.webrtc.VideoRenderer;
import org.webrtc.VideoSource;
import org.webrtc.VideoTrack;
import java.util.LinkedList;
import java.util.Timer;
import java.util.TimerTask;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -61,39 +65,77 @@ import java.util.regex.Pattern;
* All PeerConnectionEvents callbacks are invoked from the same looper thread.
*/
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 boolean PREFER_ISAC = false;
public static final String VIDEO_CODEC_VP8 = "VP8";
public static final String VIDEO_CODEC_VP9 = "VP9";
private static final String FIELD_TRIAL_VP9 = "WebRTC-SupportVP9/Enabled/";
public static final String VIDEO_TRACK_ID = "ARDAMSv0";
public static final String AUDIO_TRACK_ID = "ARDAMSa0";
private static final String MAX_VIDEO_WIDTH_CONSTRAINT = "maxWidth";
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 PeerConnectionFactory factory = null;
private PeerConnection pc = null;
private PeerConnection peerConnection = null;
private VideoSource videoSource;
private boolean videoSourceStopped = false;
private boolean isError = false;
private boolean videoCodecHwAcceleration;
private final Timer statsTimer = new Timer();
private final PCObserver pcObserver = new PCObserver();
private final SDPObserver sdpObserver = new SDPObserver();
private VideoRenderer.Callbacks localRender;
private VideoRenderer.Callbacks remoteRender;
private SignalingParameters signalingParameters;
private MediaConstraints videoConstraints;
private PeerConnectionParameters peerConnectionParameters;
// Queued remote ICE candidates are consumed only after both local and
// remote descriptions are set. Similarly local ICE candidates are sent to
// remote peer after both local and remote description are set.
private LinkedList<IceCandidate> queuedRemoteCandidates = null;
private MediaConstraints sdpMediaConstraints;
private PeerConnectionEvents events;
private int startBitrate;
private boolean isInitiator;
private boolean useFrontFacingCamera = true;
private SessionDescription localSdp = null; // either offer or answer SDP
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 {
/**
@ -123,11 +165,15 @@ public class PeerConnectionClient {
*/
public void onPeerConnectionClosed();
/**
* Callback fired once peer connection statistics is ready.
*/
public void onPeerConnectionStatsReady(final StatsReport[] reports);
/**
* Callback fired once peer connection error happened.
*/
public void onPeerConnectionError(final String description);
}
public PeerConnectionClient() {
@ -141,12 +187,13 @@ public class PeerConnectionClient {
final EGLContext renderEGLContext,
final PeerConnectionEvents events) {
this.events = events;
this.videoCodecHwAcceleration = videoCodecHwAcceleration;
executor.requestStart();
executor.execute(new Runnable() {
@Override
public void run() {
createPeerConnectionFactoryInternal(
context, videoCodec, videoCodecHwAcceleration, renderEGLContext);
context, videoCodec, renderEGLContext);
}
});
}
@ -155,11 +202,51 @@ public class PeerConnectionClient {
final VideoRenderer.Callbacks localRender,
final VideoRenderer.Callbacks remoteRender,
final SignalingParameters signalingParameters,
final int startBitrate) {
final PeerConnectionParameters peerConnectionParameters) {
this.localRender = localRender;
this.remoteRender = remoteRender;
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() {
@Override
public void run() {
@ -181,7 +268,6 @@ public class PeerConnectionClient {
private void createPeerConnectionFactoryInternal(
Context context,
String videoCodec,
boolean videoCodecHwAcceleration,
EGLContext renderEGLContext) {
Log.d(TAG, "Create peer connection factory with EGLContext "
+ renderEGLContext);
@ -204,7 +290,8 @@ public class PeerConnectionClient {
Log.e(TAG, "Peerconnection factory is not created");
return;
}
Log.d(TAG, "Create peer connection.");
Log.d(TAG, "Create peer connection. VideoConstraints: "
+ videoConstraints.toString());
isInitiator = signalingParameters.initiator;
queuedRemoteCandidates = new LinkedList<IceCandidate>();
@ -217,8 +304,8 @@ public class PeerConnectionClient {
MediaConstraints pcConstraints = signalingParameters.pcConstraints;
pcConstraints.optional.add(
new MediaConstraints.KeyValuePair("RtpDataChannels", "true"));
pc = factory.createPeerConnection(signalingParameters.iceServers,
pcConstraints, pcObserver);
peerConnection = factory.createPeerConnection(
signalingParameters.iceServers, pcConstraints, pcObserver);
isInitiator = false;
// Uncomment to get ALL WebRTC tracing and SENSITIVE libjingle logging.
@ -229,7 +316,7 @@ public class PeerConnectionClient {
// Logging.Severity.LS_SENSITIVE);
mediaStream = factory.createLocalMediaStream("ARDAMS");
if (signalingParameters.videoConstraints != null) {
if (videoConstraints != null) {
mediaStream.addTrack(createVideoTrack(useFrontFacingCamera));
}
@ -238,15 +325,17 @@ public class PeerConnectionClient {
AUDIO_TRACK_ID,
factory.createAudioSource(signalingParameters.audioConstraints)));
}
pc.addStream(mediaStream);
peerConnection.addStream(mediaStream);
Log.d(TAG, "Peer connection created.");
}
private void closeInternal() {
Log.d(TAG, "Closing peer connection.");
if (pc != null) {
pc.dispose();
pc = null;
statsTimer.cancel();
if (peerConnection != null) {
peerConnection.dispose();
peerConnection = null;
}
if (videoSource != null) {
videoSource.dispose();
@ -261,22 +350,90 @@ public class PeerConnectionClient {
events.onPeerConnectionClosed();
}
public boolean getStats(StatsObserver observer, MediaStreamTrack track) {
if (pc != null && !isError) {
return pc.getStats(observer, track);
public boolean isHDVideo() {
if (videoConstraints == null) {
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 {
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() {
executor.execute(new Runnable() {
@Override
public void run() {
if (pc != null && !isError) {
if (peerConnection != null && !isError) {
Log.d(TAG, "PC Create OFFER");
isInitiator = true;
pc.createOffer(sdpObserver, sdpMediaConstraints);
peerConnection.createOffer(sdpObserver, sdpMediaConstraints);
}
}
});
@ -286,10 +443,10 @@ public class PeerConnectionClient {
executor.execute(new Runnable() {
@Override
public void run() {
if (pc != null && !isError) {
if (peerConnection != null && !isError) {
Log.d(TAG, "PC create ANSWER");
isInitiator = false;
pc.createAnswer(sdpObserver, sdpMediaConstraints);
peerConnection.createAnswer(sdpObserver, sdpMediaConstraints);
}
}
});
@ -299,11 +456,11 @@ public class PeerConnectionClient {
executor.execute(new Runnable() {
@Override
public void run() {
if (pc != null && !isError) {
if (peerConnection != null && !isError) {
if (queuedRemoteCandidates != null) {
queuedRemoteCandidates.add(candidate);
} else {
pc.addIceCandidate(candidate);
peerConnection.addIceCandidate(candidate);
}
}
}
@ -314,20 +471,21 @@ public class PeerConnectionClient {
executor.execute(new Runnable() {
@Override
public void run() {
if (pc == null || isError) {
if (peerConnection == null || isError) {
return;
}
String sdpDescription = sdp.description;
if (PREFER_ISAC) {
sdpDescription = preferISAC(sdpDescription);
}
if (startBitrate > 0) {
sdpDescription = setStartBitrate(sdpDescription, startBitrate);
if (peerConnectionParameters.videoStartBitrate > 0) {
sdpDescription = setStartBitrate(sdpDescription,
peerConnectionParameters.videoStartBitrate);
}
Log.d(TAG, "Set remote SDP.");
SessionDescription sdpRemote = new SessionDescription(
sdp.type, sdpDescription);
pc.setRemoteDescription(sdpObserver, sdpRemote);
peerConnection.setRemoteDescription(sdpObserver, sdpRemote);
}
});
}
@ -405,13 +563,14 @@ public class PeerConnectionClient {
videoSource.dispose();
}
videoSource = factory.createVideoSource(
capturer, signalingParameters.videoConstraints);
videoSource = factory.createVideoSource(capturer, videoConstraints);
String trackExtension = frontFacing ? "frontFacing" : "backFacing";
VideoTrack videoTrack =
localVideoTrack =
factory.createVideoTrack(VIDEO_TRACK_ID + trackExtension, videoSource);
videoTrack.addRenderer(new VideoRenderer(localRender));
return videoTrack;
localVideoTrack.setEnabled(renderVideo);
localVideoTrack.addRenderer(new VideoRenderer(localRender));
return localVideoTrack;
}
private static String setStartBitrate(
@ -500,23 +659,24 @@ public class PeerConnectionClient {
if (queuedRemoteCandidates != null) {
Log.d(TAG, "Add " + queuedRemoteCandidates.size() + " remote candidates");
for (IceCandidate candidate : queuedRemoteCandidates) {
pc.addIceCandidate(candidate);
peerConnection.addIceCandidate(candidate);
}
queuedRemoteCandidates = null;
}
}
private void switchCameraInternal() {
if (signalingParameters.videoConstraints == null) {
if (videoConstraints == null) {
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.");
return;
}
Log.d(TAG, "Switch camera");
pc.removeStream(mediaStream);
peerConnection.removeStream(mediaStream);
VideoTrack currentTrack = mediaStream.videoTracks.get(0);
mediaStream.removeTrack(currentTrack);
@ -530,9 +690,9 @@ public class PeerConnectionClient {
useFrontFacingCamera = !useFrontFacingCamera;
VideoTrack newTrack = createVideoTrack(useFrontFacingCamera);
mediaStream.addTrack(newTrack);
pc.addStream(mediaStream);
peerConnection.addStream(mediaStream);
SessionDescription remoteDesc = pc.getRemoteDescription();
SessionDescription remoteDesc = peerConnection.getRemoteDescription();
if (localSdp == null || remoteDesc == null) {
Log.d(TAG, "Switching camera before the negotiation started.");
return;
@ -542,11 +702,15 @@ public class PeerConnectionClient {
localSdp.description.replaceAll(trackId, newTrack.id()));
if (isInitiator) {
pc.setLocalDescription(new SwitchCameraSdbObserver(), localSdp);
pc.setRemoteDescription(new SwitchCameraSdbObserver(), remoteDesc);
peerConnection.setLocalDescription(
new SwitchCameraSdbObserver(), localSdp);
peerConnection.setRemoteDescription(
new SwitchCameraSdbObserver(), remoteDesc);
} else {
pc.setRemoteDescription(new SwitchCameraSdbObserver(), remoteDesc);
pc.setLocalDescription(new SwitchCameraSdbObserver(), localSdp);
peerConnection.setRemoteDescription(
new SwitchCameraSdbObserver(), remoteDesc);
peerConnection.setLocalDescription(
new SwitchCameraSdbObserver(), localSdp);
}
Log.d(TAG, "Switch camera done");
}
@ -555,7 +719,7 @@ public class PeerConnectionClient {
executor.execute(new Runnable() {
@Override
public void run() {
if (pc != null && !isError) {
if (peerConnection != null && !isError) {
switchCameraInternal();
}
}
@ -609,7 +773,7 @@ public class PeerConnectionClient {
executor.execute(new Runnable() {
@Override
public void run() {
if (pc == null || isError) {
if (peerConnection == null || isError) {
return;
}
if (stream.audioTracks.size() > 1 || stream.videoTracks.size() > 1) {
@ -617,8 +781,9 @@ public class PeerConnectionClient {
return;
}
if (stream.videoTracks.size() == 1) {
stream.videoTracks.get(0).addRenderer(
new VideoRenderer(remoteRender));
remoteVideoTrack = stream.videoTracks.get(0);
remoteVideoTrack.setEnabled(renderVideo);
remoteVideoTrack.addRenderer(new VideoRenderer(remoteRender));
}
}
});
@ -629,9 +794,10 @@ public class PeerConnectionClient {
executor.execute(new Runnable() {
@Override
public void run() {
if (pc == null || isError) {
if (peerConnection == null || isError) {
return;
}
remoteVideoTrack = null;
stream.videoTracks.get(0).dispose();
}
});
@ -669,9 +835,9 @@ public class PeerConnectionClient {
executor.execute(new Runnable() {
@Override
public void run() {
if (pc != null && !isError) {
if (peerConnection != null && !isError) {
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() {
@Override
public void run() {
if (pc == null || isError) {
if (peerConnection == null || isError) {
return;
}
if (isInitiator) {
// For offering peer connection we first create offer and set
// 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.
Log.d(TAG, "Local SDP set succesfully");
events.onLocalDescription(localSdp);
@ -701,7 +867,7 @@ public class PeerConnectionClient {
} else {
// For answering peer connection we set remote SDP and then
// 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
// remote and send local ICE candidates.
Log.d(TAG, "Local SDP set succesfully");

View File

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

View File

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

View File

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

View File

@ -36,7 +36,7 @@ import java.net.URL;
import java.util.Scanner;
/**
* Asynchronious http requests implementation.
* Asynchronous http requests implementation.
*/
public class AsyncHttpURLConnection {
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.PeerConnectionClient;
import org.appspot.apprtc.PeerConnectionClient.PeerConnectionEvents;
import org.appspot.apprtc.PeerConnectionClient.PeerConnectionParameters;
import org.appspot.apprtc.util.LooperExecutor;
import org.webrtc.IceCandidate;
import org.webrtc.MediaConstraints;
import org.webrtc.PeerConnection;
import org.webrtc.SessionDescription;
import org.webrtc.StatsReport;
import org.webrtc.VideoRenderer;
import android.test.InstrumentationTestCase;
@ -167,6 +169,10 @@ public class PeerConnectionClientTest extends InstrumentationTestCase
fail("PC Error: " + description);
}
@Override
public void onPeerConnectionStatsReady(StatsReport[] reports) {
}
// Helper wait functions.
private boolean waitForLocalSDP(int timeoutMs)
throws InterruptedException {
@ -220,8 +226,7 @@ public class PeerConnectionClientTest extends InstrumentationTestCase
SignalingParameters signalingParameters = new SignalingParameters(
iceServers, true,
pcConstraints, videoConstraints, audioConstraints,
null, null,
null, null,
null, null, null,
null, null);
return signalingParameters;
}
@ -229,12 +234,14 @@ public class PeerConnectionClientTest extends InstrumentationTestCase
PeerConnectionClient createPeerConnectionClient(MockRenderer localRenderer,
MockRenderer remoteRenderer) {
SignalingParameters signalingParameters = getTestSignalingParameters();
PeerConnectionParameters peerConnectionParameters =
new PeerConnectionParameters(0, 0, 0, 0, false);
PeerConnectionClient client = new PeerConnectionClient();
client.createPeerConnectionFactory(
getInstrumentation().getContext(), "VP8", true, null, this);
client.createPeerConnection(
localRenderer, remoteRenderer, signalingParameters, 1000);
client.createPeerConnection(localRenderer, remoteRenderer,
signalingParameters, peerConnectionParameters);
client.createOffer();
return client;
}
@ -313,4 +320,5 @@ public class PeerConnectionClientTest extends InstrumentationTestCase
assertTrue(waitForPeerConnectionClosed(WAIT_TIMEOUT));
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_loopback_call.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_fullscreen.xml',
'examples/android/res/layout/fragment_menubar.xml',
'examples/android/res/layout/fragment_call.xml',
'examples/android/res/menu/connect_menu.xml',
'examples/android/res/values/arrays.xml',
'examples/android/res/values/strings.xml',
'examples/android/res/xml/preferences.xml',
'examples/android/src/org/appspot/apprtc/AppRTCAudioManager.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/CallActivity.java',
'examples/android/src/org/appspot/apprtc/CallFragment.java',
'examples/android/src/org/appspot/apprtc/ConnectActivity.java',
'examples/android/src/org/appspot/apprtc/PeerConnectionClient.java',
'examples/android/src/org/appspot/apprtc/RoomParametersFetcher.java',