Add new features to AppRTCDemo from private repo.

- Add HUD fragment with HUD related controls and more
HUD statistics.
- Create and set all peer connection constraints in
PeerConnectionClient class.
- Handle registration request in web socket class internally
once web socket connection is opened.

R=wzh@webrtc.org

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

Cr-Commit-Position: refs/heads/master@{#8762}
git-svn-id: http://webrtc.googlecode.com/svn/trunk@8762 4adac7df-926f-26a2-2b94-8c16560cd09d
This commit is contained in:
glaznev@webrtc.org 2015-03-17 18:23:31 +00:00
parent 779c3d16b9
commit 2161234cf6
12 changed files with 494 additions and 333 deletions

View File

@ -4,8 +4,7 @@
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:layout_height="match_parent">
<android.opengl.GLSurfaceView
android:id="@+id/glview_call"
@ -16,5 +15,9 @@
android:id="@+id/call_fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<FrameLayout
android:id="@+id/hud_fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>

View File

@ -4,18 +4,7 @@
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_alignParentEnd="true"
android:textStyle="bold"
android:textColor="#C000FF00"
android:textSize="12dp"
android:layout_margin="8dp"/>
android:layout_height="match_parent">
<TextView
android:id="@+id/contact_name_call"
@ -26,23 +15,6 @@
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_alignParentStart="true"
android:layout_width="48dp"
android:layout_height="48dp"/>
<LinearLayout
android:id="@+id/buttons_call_container"
android:orientation="horizontal"

View File

@ -0,0 +1,75 @@
<?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">
<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_alignParentStart="true"
android:layout_width="48dp"
android:layout_height="48dp"/>
<TextView
android:id="@+id/encoder_stat_call"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:textStyle="bold"
android:textColor="#C000FF00"
android:textSize="12dp"
android:layout_margin="8dp"/>
<TableLayout
android:id="@+id/hudview_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TableRow>
<TextView
android:id="@+id/hud_stat_bwe"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:alpha="0.4"
android:padding="2dip"
android:background="@android:color/white"
android:textColor="@android:color/black" />
<TextView
android:id="@+id/hud_stat_connection"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:alpha="0.4"
android:padding="2dip"
android:background="@android:color/white"
android:textColor="@android:color/black" />
</TableRow>
<TableRow>
<TextView
android:id="@+id/hud_stat_video_send"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:alpha="0.4"
android:padding="2dip"
android:background="@android:color/white"
android:textColor="@android:color/black" />
<TextView
android:id="@+id/hud_stat_video_recv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="2dip"
android:alpha="0.4"
android:background="@android:color/white"
android:textColor="@android:color/black" />
</TableRow>
</TableLayout>
</RelativeLayout>

View File

@ -43,7 +43,7 @@ import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
import android.widget.Toast;
import org.webrtc.IceCandidate;
@ -132,6 +132,7 @@ public class CallActivity extends Activity
// Controls
private GLSurfaceView videoView;
CallFragment callFragment;
HudFragment hudFragment;
@Override
public void onCreate(Bundle savedInstanceState) {
@ -142,8 +143,12 @@ public class CallActivity extends Activity
// 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().addFlags(
LayoutParams.FLAG_FULLSCREEN
| LayoutParams.FLAG_KEEP_SCREEN_ON
| LayoutParams.FLAG_DISMISS_KEYGUARD
| LayoutParams.FLAG_SHOW_WHEN_LOCKED
| LayoutParams.FLAG_TURN_SCREEN_ON);
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN
@ -157,6 +162,7 @@ public class CallActivity extends Activity
// Create UI controls.
videoView = (GLSurfaceView) findViewById(R.id.glview_call);
callFragment = new CallFragment();
hudFragment = new HudFragment();
// Create video renderers.
VideoRendererGui.setView(videoView, new Runnable() {
@ -219,11 +225,14 @@ public class CallActivity extends Activity
roomConnectionParameters = new RoomConnectionParameters(
roomUri.toString(), roomId, loopback);
// Send intent arguments to fragment.
// Send intent arguments to fragments.
callFragment.setArguments(intent.getExtras());
// Activate call fragment and start the call.
getFragmentManager().beginTransaction()
.add(R.id.call_fragment_container, callFragment).commit();
hudFragment.setArguments(intent.getExtras());
// Activate call and HUD fragments and start the call.
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.add(R.id.call_fragment_container, callFragment);
ft.add(R.id.hud_fragment_container, hudFragment);
ft.commit();
startCall();
// For command line execution run connection for <runTimeMs> and exit.
@ -296,8 +305,10 @@ public class CallActivity extends Activity
FragmentTransaction ft = getFragmentManager().beginTransaction();
if (callControlFragmentVisible) {
ft.show(callFragment);
ft.show(hudFragment);
} else {
ft.hide(callFragment);
ft.hide(hudFragment);
}
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
ft.commit();
@ -387,6 +398,7 @@ public class CallActivity extends Activity
// Disconnect from remote resources, dispose of local resources, and exit.
private void disconnect() {
activityRunning = false;
if (appRtcClient != null) {
appRtcClient.disconnectFromRoom();
appRtcClient = null;
@ -436,6 +448,18 @@ public class CallActivity extends Activity
logToast.show();
}
private void reportError(final String description) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (!isError) {
isError = true;
disconnectWithErrorMessage(description);
}
}
});
}
// -----Implementation of AppRTCClient.AppRTCSignalingEvents ---------------
// All callbacks are invoked from websocket signaling looper thread and
// are routed to UI thread.
@ -533,15 +557,7 @@ public class CallActivity extends Activity
@Override
public void onChannelError(final String description) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (!isError) {
isError = true;
disconnectWithErrorMessage(description);
}
}
});
reportError(description);
}
// -----Implementation of PeerConnectionClient.PeerConnectionEvents.---------
@ -613,7 +629,7 @@ public class CallActivity extends Activity
@Override
public void run() {
if (!isError && iceConnected) {
callFragment.updateEncoderStatistics(reports);
hudFragment.updateEncoderStatistics(reports);
}
}
});
@ -621,14 +637,6 @@ public class CallActivity extends Activity
@Override
public void onPeerConnectionError(final String description) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (!isError) {
isError = true;
disconnectWithErrorMessage(description);
}
}
});
reportError(description);
}
}

View File

@ -30,36 +30,26 @@ 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 TextView contactView;
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;
private final CpuMonitor cpuMonitor = new CpuMonitor();
private boolean videoCallEnabled = true;
/**
* Call control interface for container activity.
@ -77,20 +67,14 @@ public class CallFragment extends Fragment {
inflater.inflate(R.layout.fragment_call, container, false);
// Create UI controls.
encoderStatView =
(TextView) controlView.findViewById(R.id.encoder_stat_call);
roomIdView =
contactView =
(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() {
@ -124,17 +108,6 @@ public class CallFragment extends Fragment {
});
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;
}
@ -144,22 +117,13 @@ public class CallFragment extends Fragment {
Bundle args = getArguments();
if (args != null) {
String roomId = args.getString(CallActivity.EXTRA_ROOMID);
roomIdView.setText(roomId);
displayHud = args.getBoolean(CallActivity.EXTRA_DISPLAY_HUD, false);
String contactName = args.getString(CallActivity.EXTRA_ROOMID);
contactView.setText(contactName);
videoCallEnabled = args.getBoolean(CallActivity.EXTRA_VIDEO_CALL, true);
}
if (!videoCallEnabled) {
cameraSwitchButton.setVisibility(View.INVISIBLE);
}
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
@ -168,73 +132,4 @@ public class CallFragment extends Fragment {
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");
}
if (cpuMonitor.sampleCpuUtilization()) {
stat.append("CPU%: ")
.append(cpuMonitor.getCpuCurrent())
.append("/")
.append(cpuMonitor.getCpuAvg3())
.append("/")
.append(cpuMonitor.getCpuAvgAll());
}
encoderStatView.setText(stat.toString());
hudView.setText(bweBuilder.toString() + hudView.getText());
}
}

View File

@ -0,0 +1,217 @@
/*
* 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.Fragment;
import android.os.Bundle;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.TextView;
import org.webrtc.StatsReport;
import java.util.HashMap;
import java.util.Map;
/**
* Fragment for HUD statistics display.
*/
public class HudFragment extends Fragment {
private View controlView;
private TextView encoderStatView;
private TextView hudViewBwe;
private TextView hudViewConnection;
private TextView hudViewVideoSend;
private TextView hudViewVideoRecv;
private ImageButton toggleDebugButton;
private boolean videoCallEnabled;
private boolean displayHud;
private volatile boolean isRunning;
private final CpuMonitor cpuMonitor = new CpuMonitor();
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
controlView = inflater.inflate(R.layout.fragment_hud, container, false);
// Create UI controls.
encoderStatView = (TextView) controlView.findViewById(R.id.encoder_stat_call);
hudViewBwe = (TextView) controlView.findViewById(R.id.hud_stat_bwe);
hudViewConnection = (TextView) controlView.findViewById(R.id.hud_stat_connection);
hudViewVideoSend = (TextView) controlView.findViewById(R.id.hud_stat_video_send);
hudViewVideoRecv = (TextView) controlView.findViewById(R.id.hud_stat_video_recv);
toggleDebugButton = (ImageButton) controlView.findViewById(R.id.button_toggle_debug);
toggleDebugButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (displayHud) {
int visibility = (hudViewBwe.getVisibility() == View.VISIBLE)
? View.INVISIBLE : View.VISIBLE;
hudViewsSetProperties(visibility);
}
}
});
return controlView;
}
@Override
public void onStart() {
super.onStart();
Bundle args = getArguments();
if (args != null) {
videoCallEnabled = args.getBoolean(CallActivity.EXTRA_VIDEO_CALL, true);
displayHud = args.getBoolean(CallActivity.EXTRA_DISPLAY_HUD, false);
}
int visibility = displayHud ? View.VISIBLE : View.INVISIBLE;
encoderStatView.setVisibility(visibility);
toggleDebugButton.setVisibility(visibility);
hudViewsSetProperties(View.INVISIBLE);
isRunning = true;
}
@Override
public void onStop() {
isRunning = false;
super.onStop();
}
private void hudViewsSetProperties(int visibility) {
hudViewBwe.setVisibility(visibility);
hudViewConnection.setVisibility(visibility);
hudViewVideoSend.setVisibility(visibility);
hudViewVideoRecv.setVisibility(visibility);
hudViewBwe.setTextSize(TypedValue.COMPLEX_UNIT_PT, 5);
hudViewConnection.setTextSize(TypedValue.COMPLEX_UNIT_PT, 5);
hudViewVideoSend.setTextSize(TypedValue.COMPLEX_UNIT_PT, 5);
hudViewVideoRecv.setTextSize(TypedValue.COMPLEX_UNIT_PT, 5);
}
private Map<String, String> getReportMap(StatsReport report) {
Map<String, String> reportMap = new HashMap<String, String>();
for (StatsReport.Value value : report.values) {
reportMap.put(value.name, value.value);
}
return reportMap;
}
public void updateEncoderStatistics(final StatsReport[] reports) {
if (!isRunning || !displayHud) {
return;
}
StringBuilder encoderStat = new StringBuilder(128);
StringBuilder bweStat = new StringBuilder();
StringBuilder connectionStat = new StringBuilder();
StringBuilder videoSendStat = new StringBuilder();
StringBuilder videoRecvStat = new StringBuilder();
String fps = null;
String targetBitrate = null;
String actualBitrate = null;
for (StatsReport report : reports) {
if (report.type.equals("ssrc") && report.id.contains("ssrc")
&& report.id.contains("send")) {
// Send video statistics.
Map<String, String> reportMap = getReportMap(report);
String trackId = reportMap.get("googTrackId");
if (trackId != null && trackId.contains(PeerConnectionClient.VIDEO_TRACK_ID)) {
fps = reportMap.get("googFrameRateSent");
videoSendStat.append(report.id).append("\n");
for (StatsReport.Value value : report.values) {
String name = value.name.replace("goog", "");
videoSendStat.append(name).append("=").append(value.value).append("\n");
}
}
} else if (report.type.equals("ssrc") && report.id.contains("ssrc")
&& report.id.contains("recv")) {
// Receive video statistics.
Map<String, String> reportMap = getReportMap(report);
// Check if this stat is for video track.
String frameWidth = reportMap.get("googFrameWidthReceived");
if (frameWidth != null) {
videoRecvStat.append(report.id).append("\n");
for (StatsReport.Value value : report.values) {
String name = value.name.replace("goog", "");
videoRecvStat.append(name).append("=").append(value.value).append("\n");
}
}
} else if (report.id.equals("bweforvideo")) {
// BWE statistics.
Map<String, String> reportMap = getReportMap(report);
targetBitrate = reportMap.get("googTargetEncBitrate");
actualBitrate = reportMap.get("googActualEncBitrate");
bweStat.append(report.id).append("\n");
for (StatsReport.Value value : report.values) {
String name = value.name.replace("goog", "").replace("Available", "");
bweStat.append(name).append("=").append(value.value).append("\n");
}
} else if (report.type.equals("googCandidatePair")) {
// Connection statistics.
Map<String, String> reportMap = getReportMap(report);
String activeConnection = reportMap.get("googActiveConnection");
if (activeConnection != null && activeConnection.equals("true")) {
connectionStat.append(report.id).append("\n");
for (StatsReport.Value value : report.values) {
String name = value.name.replace("goog", "");
connectionStat.append(name).append("=").append(value.value).append("\n");
}
}
}
}
hudViewBwe.setText(bweStat.toString());
hudViewConnection.setText(connectionStat.toString());
hudViewVideoSend.setText(videoSendStat.toString());
hudViewVideoRecv.setText(videoRecvStat.toString());
if (videoCallEnabled) {
if (fps != null) {
encoderStat.append("Fps: ").append(fps).append("\n");
}
if (targetBitrate != null) {
encoderStat.append("Target BR: ").append(targetBitrate).append("\n");
}
if (actualBitrate != null) {
encoderStat.append("Actual BR: ").append(actualBitrate).append("\n");
}
}
if (cpuMonitor.sampleCpuUtilization()) {
encoderStat.append("CPU%: ")
.append(cpuMonitor.getCpuCurrent()).append("/")
.append(cpuMonitor.getCpuAvg3()).append("/")
.append(cpuMonitor.getCpuAvgAll());
}
encoderStatView.setText(encoderStat.toString());
}
}

View File

@ -82,6 +82,7 @@ public class PeerConnectionClient {
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 String DTLS_SRTP_KEY_AGREEMENT_CONSTRAINT = "DtlsSrtpKeyAgreement";
private static final int HD_VIDEO_WIDTH = 1280;
private static final int HD_VIDEO_HEIGHT = 720;
private static final int MAX_VIDEO_WIDTH = 1280;
@ -103,13 +104,15 @@ public class PeerConnectionClient {
private VideoRenderer.Callbacks localRender;
private VideoRenderer.Callbacks remoteRender;
private SignalingParameters signalingParameters;
private MediaConstraints pcConstraints;
private MediaConstraints videoConstraints;
private MediaConstraints audioConstraints;
private MediaConstraints sdpMediaConstraints;
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 boolean isInitiator;
private SessionDescription localSdp = null; // either offer or answer SDP
@ -210,6 +213,7 @@ public class PeerConnectionClient {
final PeerConnectionEvents events) {
this.peerConnectionParameters = peerConnectionParameters;
this.events = events;
videoCallEnabled = peerConnectionParameters.videoCallEnabled;
executor.requestStart();
executor.execute(new Runnable() {
@Override
@ -230,59 +234,10 @@ public class PeerConnectionClient {
this.localRender = localRender;
this.remoteRender = remoteRender;
this.signalingParameters = signalingParameters;
// Merge video constraints from signaling parameters and peer connection
// parameters.
videoConstraints = signalingParameters.videoConstraints;
if (signalingParameters.videoConstraints == null) {
videoCallEnabled = false;
}
// Check if there is a camera on device and disable video call if not.
numberOfCameras = VideoCapturerAndroid.getDeviceCount();
if (numberOfCameras == 0) {
Log.w(TAG, "No camera on device. Switch to audio only call.");
videoCallEnabled = false;
}
if (videoCallEnabled) {
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)
&& peerConnectionParameters.videoCodecHwAcceleration
&& MediaCodecVideoEncoder.isVp8HwSupported()) {
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() {
createMediaConstraintsInternal();
createPeerConnectionInternal();
}
});
@ -303,7 +258,6 @@ public class PeerConnectionClient {
Log.d(TAG, "Create peer connection factory with EGLContext "
+ renderEGLContext + ". Use video: "
+ peerConnectionParameters.videoCallEnabled);
videoCallEnabled = peerConnectionParameters.videoCallEnabled;
isError = false;
// Check if VP9 is used by default.
if (videoCallEnabled && peerConnectionParameters.videoCodec != null
@ -340,18 +294,68 @@ public class PeerConnectionClient {
protected void configureFactory(PeerConnectionFactory factory) {
}
private void createPeerConnectionInternal() {
if (factory == null || isError) {
Log.e(TAG, "Peerconnection factory is not created");
return;
private void createMediaConstraintsInternal() {
// Create peer connection constraints.
pcConstraints = new MediaConstraints();
// Enable DTLS for normal calls and disable for loopback calls.
if (peerConnectionParameters.loopback) {
pcConstraints.optional.add(
new MediaConstraints.KeyValuePair(DTLS_SRTP_KEY_AGREEMENT_CONSTRAINT, "false"));
} else {
pcConstraints.optional.add(
new MediaConstraints.KeyValuePair(DTLS_SRTP_KEY_AGREEMENT_CONSTRAINT, "true"));
}
Log.d(TAG, "Create peer connection");
if (videoConstraints != null) {
Log.d(TAG, "VideoConstraints: " + videoConstraints.toString());
}
isInitiator = signalingParameters.initiator;
queuedRemoteCandidates = new LinkedList<IceCandidate>();
// Check if there is a camera on device and disable video call if not.
numberOfCameras = VideoCapturerAndroid.getDeviceCount();
if (numberOfCameras == 0) {
Log.w(TAG, "No camera on device. Switch to audio only call.");
videoCallEnabled = false;
}
// Create video constraints if video call is enabled.
if (videoCallEnabled) {
videoConstraints = new MediaConstraints();
int videoWidth = peerConnectionParameters.videoWidth;
int videoHeight = peerConnectionParameters.videoHeight;
// If VP8 HW video encoder is supported and video resolution is not
// specified force it to HD.
if ((videoWidth == 0 || videoHeight == 0)
&& peerConnectionParameters.videoCodecHwAcceleration
&& MediaCodecVideoEncoder.isVp8HwSupported()) {
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)));
}
}
// Create audio constraints.
audioConstraints = new MediaConstraints();
// Create SDP constraints.
sdpMediaConstraints = new MediaConstraints();
sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
"OfferToReceiveAudio", "true"));
@ -362,10 +366,20 @@ public class PeerConnectionClient {
sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
"OfferToReceiveVideo", "false"));
}
}
private void createPeerConnectionInternal() {
if (factory == null || isError) {
Log.e(TAG, "Peerconnection factory is not created");
return;
}
Log.d(TAG, "Create peer connection");
Log.d(TAG, "PCConstraints: " + pcConstraints.toString());
if (videoConstraints != null) {
Log.d(TAG, "VideoConstraints: " + videoConstraints.toString());
}
queuedRemoteCandidates = new LinkedList<IceCandidate>();
MediaConstraints pcConstraints = signalingParameters.pcConstraints;
pcConstraints.optional.add(
new MediaConstraints.KeyValuePair("RtpDataChannels", "true"));
peerConnection = factory.createPeerConnection(
signalingParameters.iceServers, pcConstraints, pcObserver);
isInitiator = false;
@ -390,11 +404,9 @@ public class PeerConnectionClient {
mediaStream.addTrack(createVideoTrack(videoCapturer));
}
if (signalingParameters.audioConstraints != null) {
mediaStream.addTrack(factory.createAudioTrack(
AUDIO_TRACK_ID,
factory.createAudioSource(signalingParameters.audioConstraints)));
}
mediaStream.addTrack(factory.createAudioTrack(
AUDIO_TRACK_ID,
factory.createAudioSource(audioConstraints)));
peerConnection.addStream(mediaStream);
Log.d(TAG, "Peer connection created.");
@ -611,11 +623,9 @@ public class PeerConnectionClient {
}
private VideoTrack createVideoTrack(VideoCapturerAndroid capturer) {
videoSource = factory.createVideoSource(
capturer, signalingParameters.videoConstraints);
videoSource = factory.createVideoSource(capturer, videoConstraints);
localVideoTrack =
factory.createVideoTrack(VIDEO_TRACK_ID, videoSource);
localVideoTrack = factory.createVideoTrack(VIDEO_TRACK_ID, videoSource);
localVideoTrack.setEnabled(renderVideo);
localVideoTrack.addRenderer(new VideoRenderer(localRender));
return localVideoTrack;
@ -700,8 +710,8 @@ public class PeerConnectionClient {
if (isAudio) {
mediaDescription = "m=audio ";
}
for (int i = 0; (i < lines.length) &&
(mLineIndex == -1 || codecRtpMap == null); i++) {
for (int i = 0; (i < lines.length)
&& (mLineIndex == -1 || codecRtpMap == null); i++) {
if (lines[i].startsWith(mediaDescription)) {
mLineIndex = i;
continue;
@ -720,23 +730,27 @@ public class PeerConnectionClient {
Log.w(TAG, "No rtpmap for " + codec);
return sdpDescription;
}
Log.d(TAG, "Found " + codec + " rtpmap " + codecRtpMap + ", prefer at " +
lines[mLineIndex]);
Log.d(TAG, "Found " + codec + " rtpmap " + codecRtpMap + ", prefer at "
+ lines[mLineIndex]);
String[] origMLineParts = lines[mLineIndex].split(" ");
StringBuilder newMLine = new StringBuilder();
int origPartIndex = 0;
// Format is: m=<media> <port> <proto> <fmt> ...
newMLine.append(origMLineParts[origPartIndex++]).append(" ");
newMLine.append(origMLineParts[origPartIndex++]).append(" ");
newMLine.append(origMLineParts[origPartIndex++]).append(" ");
newMLine.append(codecRtpMap);
for (; origPartIndex < origMLineParts.length; origPartIndex++) {
if (!origMLineParts[origPartIndex].equals(codecRtpMap)) {
newMLine.append(" ").append(origMLineParts[origPartIndex]);
if (origMLineParts.length > 3) {
StringBuilder newMLine = new StringBuilder();
int origPartIndex = 0;
// Format is: m=<media> <port> <proto> <fmt> ...
newMLine.append(origMLineParts[origPartIndex++]).append(" ");
newMLine.append(origMLineParts[origPartIndex++]).append(" ");
newMLine.append(origMLineParts[origPartIndex++]).append(" ");
newMLine.append(codecRtpMap);
for (; origPartIndex < origMLineParts.length; origPartIndex++) {
if (!origMLineParts[origPartIndex].equals(codecRtpMap)) {
newMLine.append(" ").append(origMLineParts[origPartIndex]);
}
}
lines[mLineIndex] = newMLine.toString();
Log.d(TAG, "Change media description: " + lines[mLineIndex]);
} else {
Log.e(TAG, "Wrong SDP media description format: " + lines[mLineIndex]);
}
lines[mLineIndex] = newMLine.toString();
Log.d(TAG, "Change media description: " + lines[mLineIndex]);
StringBuilder newSdpDescription = new StringBuilder();
for (String line : lines) {
newSdpDescription.append(line).append("\r\n");

View File

@ -43,8 +43,8 @@ import org.webrtc.SessionDescription;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.LinkedList;
import java.util.Scanner;
@ -54,10 +54,10 @@ import java.util.Scanner;
*/
public class RoomParametersFetcher {
private static final String TAG = "RoomRTCClient";
private static final int TURN_HTTP_TIMEOUT_MS = 5000;
private final RoomParametersFetcherEvents events;
private final boolean loopback;
private final String registerUrl;
private final String registerMessage;
private final String roomUrl;
private final String roomMessage;
private AsyncHttpURLConnection httpConnection;
/**
@ -76,18 +76,17 @@ public class RoomParametersFetcher {
public void onSignalingParametersError(final String description);
}
public RoomParametersFetcher(boolean loopback, String registerUrl,
String registerMessage, final RoomParametersFetcherEvents events) {
this.loopback = loopback;
this.registerUrl = registerUrl;
this.registerMessage = registerMessage;
public RoomParametersFetcher(String roomUrl, String roomMessage,
final RoomParametersFetcherEvents events) {
this.roomUrl = roomUrl;
this.roomMessage = roomMessage;
this.events = events;
}
public void makeRequest() {
Log.d(TAG, "Connecting to room: " + registerUrl);
Log.d(TAG, "Connecting to room: " + roomUrl);
httpConnection = new AsyncHttpURLConnection(
"POST", registerUrl, registerMessage,
"POST", roomUrl, roomMessage,
new AsyncHttpEvents() {
@Override
public void onHttpError(String errorMessage) {
@ -161,6 +160,7 @@ public class RoomParametersFetcher {
break;
}
}
// Request TURN servers.
if (!isTurnPresent) {
LinkedList<PeerConnection.IceServer> turnServers =
requestTurnServers(roomJson.getString("turn_url"));
@ -170,17 +170,13 @@ public class RoomParametersFetcher {
}
}
MediaConstraints pcConstraints = constraintsFromJSON(
roomJson.getString("pc_constraints"));
addDTLSConstraintIfMissing(pcConstraints, loopback);
MediaConstraints pcConstraints = constraintsFromJSON(roomJson.getString("pc_constraints"));
Log.d(TAG, "pcConstraints: " + pcConstraints);
MediaConstraints videoConstraints = constraintsFromJSON(
getAVConstraints("video",
roomJson.getString("media_constraints")));
getAVConstraints("video", roomJson.getString("media_constraints")));
Log.d(TAG, "videoConstraints: " + videoConstraints);
MediaConstraints audioConstraints = constraintsFromJSON(
getAVConstraints("audio",
roomJson.getString("media_constraints")));
getAVConstraints("audio", roomJson.getString("media_constraints")));
Log.d(TAG, "audioConstraints: " + audioConstraints);
SignalingParameters params = new SignalingParameters(
@ -197,35 +193,10 @@ public class RoomParametersFetcher {
}
}
// Mimic Chrome and set DtlsSrtpKeyAgreement to true if not set to false by
// the web-app.
private void addDTLSConstraintIfMissing(
MediaConstraints pcConstraints, boolean loopback) {
for (MediaConstraints.KeyValuePair pair : pcConstraints.mandatory) {
if (pair.getKey().equals("DtlsSrtpKeyAgreement")) {
return;
}
}
for (MediaConstraints.KeyValuePair pair : pcConstraints.optional) {
if (pair.getKey().equals("DtlsSrtpKeyAgreement")) {
return;
}
}
// DTLS isn't being specified (e.g. for debug=loopback calls), so enable
// it for normal calls and disable for loopback calls.
if (loopback) {
pcConstraints.optional.add(
new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "false"));
} else {
pcConstraints.optional.add(
new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true"));
}
}
// Return the constraints specified for |type| of "audio" or "video" in
// |mediaConstraintsString|.
private String getAVConstraints (
String type, String mediaConstraintsString) throws JSONException {
String type, String mediaConstraintsString) throws JSONException {
JSONObject json = new JSONObject(mediaConstraintsString);
// Tricky handling of values that are allowed to be (boolean or
// MediaTrackConstraints) by the getUserMedia() spec. There are three
@ -282,10 +253,17 @@ public class RoomParametersFetcher {
LinkedList<PeerConnection.IceServer> turnServers =
new LinkedList<PeerConnection.IceServer>();
Log.d(TAG, "Request TURN from: " + url);
URLConnection connection = (new URL(url)).openConnection();
connection.addRequestProperty("user-agent", "Mozilla/5.0");
connection.addRequestProperty("origin", "https://apprtc.appspot.com");
String response = drainStream(connection.getInputStream());
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
connection.setConnectTimeout(TURN_HTTP_TIMEOUT_MS);
connection.setReadTimeout(TURN_HTTP_TIMEOUT_MS);
int responseCode = connection.getResponseCode();
if (responseCode != 200) {
throw new IOException("Non-200 response when requesting TURN server from "
+ url + " : " + connection.getHeaderField(null));
}
InputStream responseStream = connection.getInputStream();
String response = drainStream(responseStream);
connection.disconnect();
Log.d(TAG, "TURN response: " + response);
JSONObject responseJSON = new JSONObject(response);
String username = responseJSON.getString("username");
@ -317,7 +295,7 @@ public class RoomParametersFetcher {
}
// Return the contents of an InputStream as a String.
private String drainStream(InputStream in) {
private static String drainStream(InputStream in) {
Scanner s = new Scanner(in).useDelimiter("\\A");
return s.hasNext() ? s.next() : "";
}

View File

@ -82,14 +82,12 @@ public class WebSocketChannelClient {
* All events are dispatched from a looper executor thread.
*/
public interface WebSocketChannelEvents {
public void onWebSocketOpen();
public void onWebSocketMessage(final String message);
public void onWebSocketClose();
public void onWebSocketError(final String description);
}
public WebSocketChannelClient(LooperExecutor executor,
WebSocketChannelEvents events) {
public WebSocketChannelClient(LooperExecutor executor, WebSocketChannelEvents events) {
this.executor = executor;
this.events = events;
roomID = null;
@ -102,8 +100,7 @@ public class WebSocketChannelClient {
return state;
}
public void connect(final String wsUrl, final String postUrl,
final String roomID, final String clientID) {
public void connect(final String wsUrl, final String postUrl) {
checkIfCalledOnValidThread();
if (state != WebSocketConnectionState.NEW) {
Log.e(TAG, "WebSocket is already connected.");
@ -111,8 +108,6 @@ public class WebSocketChannelClient {
}
wsServerUrl = wsUrl;
postServerUrl = postUrl;
this.roomID = roomID;
this.clientID = clientID;
closeEvent = false;
Log.d(TAG, "Connecting WebSocket to: " + wsUrl + ". Post URL: " + postUrl);
@ -127,12 +122,15 @@ public class WebSocketChannelClient {
}
}
public void register() {
public void register(final String roomID, final String clientID) {
checkIfCalledOnValidThread();
this.roomID = roomID;
this.clientID = clientID;
if (state != WebSocketConnectionState.CONNECTED) {
Log.w(TAG, "WebSocket register() in state " + state);
Log.d(TAG, "WebSocket register() in state " + state);
return;
}
Log.d(TAG, "Registering WebSocket for room " + roomID + ". CLientID: " + clientID);
JSONObject json = new JSONObject();
try {
json.put("cmd", "register");
@ -271,7 +269,10 @@ public class WebSocketChannelClient {
@Override
public void run() {
state = WebSocketConnectionState.CONNECTED;
events.onWebSocketOpen();
// Check if we have pending register request.
if (roomID != null && clientID != null) {
register(roomID, clientID);
}
}
});
}

View File

@ -77,6 +77,7 @@ public class WebSocketRTCClient implements AppRTCClient,
this.events = events;
this.executor = executor;
roomState = ConnectionState.NEW;
executor.requestStart();
}
// --------------------------------------------------------------------
@ -86,7 +87,6 @@ public class WebSocketRTCClient implements AppRTCClient,
@Override
public void connectToRoom(RoomConnectionParameters connectionParameters) {
this.connectionParameters = connectionParameters;
executor.requestStart();
executor.execute(new Runnable() {
@Override
public void run() {
@ -131,8 +131,7 @@ public class WebSocketRTCClient implements AppRTCClient,
}
};
new RoomParametersFetcher(connectionParameters.loopback, connectionUrl,
null, callbacks).makeRequest();
new RoomParametersFetcher(connectionUrl, null, callbacks).makeRequest();
}
// Disconnect from room and send bye messages - runs on a local looper thread.
@ -193,9 +192,9 @@ public class WebSocketRTCClient implements AppRTCClient,
// Fire connection and signaling parameters events.
events.onConnectedToRoom(signalingParameters);
// Connect to WebSocket server.
wsClient.connect(signalingParameters.wssUrl, signalingParameters.wssPostUrl,
connectionParameters.roomId, signalingParameters.clientId);
// Connect and register WebSocket client.
wsClient.connect(signalingParameters.wssUrl, signalingParameters.wssPostUrl);
wsClient.register(connectionParameters.roomId, signalingParameters.clientId);
}
// Send local offer SDP to the other participant.
@ -274,12 +273,6 @@ public class WebSocketRTCClient implements AppRTCClient,
// WebSocketChannelEvents interface implementation.
// All events are called by WebSocketChannelClient on a local looper thread
// (passed to WebSocket client constructor).
@Override
public void onWebSocketOpen() {
Log.d(TAG, "Websocket connection completed. Registering...");
wsClient.register();
}
@Override
public void onWebSocketMessage(final String msg) {
if (wsClient.getState() != WebSocketConnectionState.REGISTERED) {

View File

@ -40,6 +40,7 @@ import java.util.Scanner;
*/
public class AsyncHttpURLConnection {
private static final int HTTP_TIMEOUT_MS = 8000;
private static final String HTTP_ORIGIN = "https://apprtc.appspot.com";
private final String method;
private final String url;
private final String message;
@ -83,6 +84,8 @@ public class AsyncHttpURLConnection {
connection.setDoInput(true);
connection.setConnectTimeout(HTTP_TIMEOUT_MS);
connection.setReadTimeout(HTTP_TIMEOUT_MS);
// TODO(glaznev) - query request origin from pref_room_server_url_key preferences.
connection.addRequestProperty("origin", HTTP_ORIGIN);
boolean doOutput = false;
if (method.equals("POST")) {
doOutput = true;
@ -102,6 +105,7 @@ public class AsyncHttpURLConnection {
// Get response.
int responseCode = connection.getResponseCode();
if (responseCode != 200) {
connection.disconnect();
events.onHttpError("Non-200 response to " + method + " to URL: "
+ url + " : " + connection.getHeaderField(null));
return;
@ -109,6 +113,7 @@ public class AsyncHttpURLConnection {
InputStream responseStream = connection.getInputStream();
String response = drainStream(responseStream);
responseStream.close();
connection.disconnect();
events.onHttpComplete(response);
} catch (SocketTimeoutException e) {
events.onHttpError("HTTP " + method + " to " + url + " timeout");

View File

@ -85,7 +85,7 @@ public class LooperExecutor extends Thread implements Executor {
handler.post(new Runnable() {
@Override
public void run() {
Looper.myLooper().quitSafely();
Looper.myLooper().quit();
Log.d(TAG, "Looper thread finished.");
}
});