From 2161234cf6260feb4ec4e7e4ec1d6fd6c041df1f Mon Sep 17 00:00:00 2001 From: "glaznev@webrtc.org" Date: Tue, 17 Mar 2015 18:23:31 +0000 Subject: [PATCH] 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 --- .../android/res/layout/activity_call.xml | 7 +- .../android/res/layout/fragment_call.xml | 30 +-- .../android/res/layout/fragment_hud.xml | 75 ++++++ .../src/org/appspot/apprtc/CallActivity.java | 60 ++--- .../src/org/appspot/apprtc/CallFragment.java | 123 +--------- .../src/org/appspot/apprtc/HudFragment.java | 217 ++++++++++++++++++ .../appspot/apprtc/PeerConnectionClient.java | 194 ++++++++-------- .../appspot/apprtc/RoomParametersFetcher.java | 76 +++--- .../apprtc/WebSocketChannelClient.java | 21 +- .../appspot/apprtc/WebSocketRTCClient.java | 17 +- .../apprtc/util/AsyncHttpURLConnection.java | 5 + .../appspot/apprtc/util/LooperExecutor.java | 2 +- 12 files changed, 494 insertions(+), 333 deletions(-) create mode 100644 talk/examples/android/res/layout/fragment_hud.xml create mode 100644 talk/examples/android/src/org/appspot/apprtc/HudFragment.java diff --git a/talk/examples/android/res/layout/activity_call.xml b/talk/examples/android/res/layout/activity_call.xml index 0aca7ad70b..a18f758568 100644 --- a/talk/examples/android/res/layout/activity_call.xml +++ b/talk/examples/android/res/layout/activity_call.xml @@ -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"> + diff --git a/talk/examples/android/res/layout/fragment_call.xml b/talk/examples/android/res/layout/fragment_call.xml index f0d5d3c58c..70a9a28197 100644 --- a/talk/examples/android/res/layout/fragment_call.xml +++ b/talk/examples/android/res/layout/fragment_call.xml @@ -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"> - - + android:layout_height="match_parent"> - - - - + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/talk/examples/android/src/org/appspot/apprtc/CallActivity.java b/talk/examples/android/src/org/appspot/apprtc/CallActivity.java index 815081183a..a4255b5f27 100644 --- a/talk/examples/android/src/org/appspot/apprtc/CallActivity.java +++ b/talk/examples/android/src/org/appspot/apprtc/CallActivity.java @@ -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 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); } } diff --git a/talk/examples/android/src/org/appspot/apprtc/CallFragment.java b/talk/examples/android/src/org/appspot/apprtc/CallFragment.java index dc7695c4ef..d0b670da73 100644 --- a/talk/examples/android/src/org/appspot/apprtc/CallFragment.java +++ b/talk/examples/android/src/org/appspot/apprtc/CallFragment.java @@ -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 getReportMap(StatsReport report) { - Map reportMap = new HashMap(); - 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 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 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()); - } } diff --git a/talk/examples/android/src/org/appspot/apprtc/HudFragment.java b/talk/examples/android/src/org/appspot/apprtc/HudFragment.java new file mode 100644 index 0000000000..c1f8fa2b85 --- /dev/null +++ b/talk/examples/android/src/org/appspot/apprtc/HudFragment.java @@ -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 getReportMap(StatsReport report) { + Map reportMap = new HashMap(); + 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 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 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 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 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()); + } +} diff --git a/talk/examples/android/src/org/appspot/apprtc/PeerConnectionClient.java b/talk/examples/android/src/org/appspot/apprtc/PeerConnectionClient.java index 04517c0d9d..67290bef51 100644 --- a/talk/examples/android/src/org/appspot/apprtc/PeerConnectionClient.java +++ b/talk/examples/android/src/org/appspot/apprtc/PeerConnectionClient.java @@ -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 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(); + // 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(); - 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= ... - 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= ... + 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"); diff --git a/talk/examples/android/src/org/appspot/apprtc/RoomParametersFetcher.java b/talk/examples/android/src/org/appspot/apprtc/RoomParametersFetcher.java index 2bb3cee98f..92bfbd73f3 100644 --- a/talk/examples/android/src/org/appspot/apprtc/RoomParametersFetcher.java +++ b/talk/examples/android/src/org/appspot/apprtc/RoomParametersFetcher.java @@ -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 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 turnServers = new LinkedList(); 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() : ""; } diff --git a/talk/examples/android/src/org/appspot/apprtc/WebSocketChannelClient.java b/talk/examples/android/src/org/appspot/apprtc/WebSocketChannelClient.java index 494b8b6722..c783816e52 100644 --- a/talk/examples/android/src/org/appspot/apprtc/WebSocketChannelClient.java +++ b/talk/examples/android/src/org/appspot/apprtc/WebSocketChannelClient.java @@ -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); + } } }); } diff --git a/talk/examples/android/src/org/appspot/apprtc/WebSocketRTCClient.java b/talk/examples/android/src/org/appspot/apprtc/WebSocketRTCClient.java index c055435502..060477f135 100644 --- a/talk/examples/android/src/org/appspot/apprtc/WebSocketRTCClient.java +++ b/talk/examples/android/src/org/appspot/apprtc/WebSocketRTCClient.java @@ -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) { diff --git a/talk/examples/android/src/org/appspot/apprtc/util/AsyncHttpURLConnection.java b/talk/examples/android/src/org/appspot/apprtc/util/AsyncHttpURLConnection.java index 888434f881..d1fddb4f5b 100644 --- a/talk/examples/android/src/org/appspot/apprtc/util/AsyncHttpURLConnection.java +++ b/talk/examples/android/src/org/appspot/apprtc/util/AsyncHttpURLConnection.java @@ -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"); diff --git a/talk/examples/android/src/org/appspot/apprtc/util/LooperExecutor.java b/talk/examples/android/src/org/appspot/apprtc/util/LooperExecutor.java index 0aa4c39bf9..a57030351f 100644 --- a/talk/examples/android/src/org/appspot/apprtc/util/LooperExecutor.java +++ b/talk/examples/android/src/org/appspot/apprtc/util/LooperExecutor.java @@ -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."); } });