Android AppRTCDemo improvements:

- Add a room list to ConnectActivity with buttons to add/remove rooms.
- Add loopback call button.
- Add option to toggle full screen / letterbox video.
- Add camera fps settings.
- Fix device to landscape orientation for HD video until issue 3936
will be fixed.
- Fix a few crashes by avoiding calling peer connection and
GAE signaling function while connection is closing.
- Better handling GAE channel error - catch channel exceptions and
display dialog with error messages.

BUG=3939, 3935
R=kjellander@webrtc.org, pthatcher@webrtc.org, tkchin@webrtc.org

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

git-svn-id: http://webrtc.googlecode.com/svn/trunk@7601 4adac7df-926f-26a2-2b94-8c16560cd09d
This commit is contained in:
glaznev@webrtc.org
2014-11-03 22:18:52 +00:00
parent 5804936052
commit 5f38c8d1b8
26 changed files with 766 additions and 451 deletions

View File

@@ -253,6 +253,8 @@ public class VideoRendererGui implements GLSurfaceView.Renderer {
private FloatBuffer textureCoords;
// Flag if texture vertices or coordinates update is needed.
private boolean updateTextureProperties;
// Texture properties update lock.
private final Object updateTextureLock = new Object();
// Viewport dimensions.
private int screenWidth;
private int screenHeight;
@@ -319,6 +321,7 @@ public class VideoRendererGui implements GLSurfaceView.Renderer {
scalingType == ScalingType.SCALE_FILL) {
return;
}
synchronized(updateTextureLock) {
// Re - calculate texture vertices to preserve video aspect ratio.
float texRight = this.texRight;
float texLeft = this.texLeft;
@@ -352,9 +355,11 @@ public class VideoRendererGui implements GLSurfaceView.Renderer {
if (scalingType == ScalingType.SCALE_ASPECT_FILL) {
// Need to re-adjust UV coordinates to match display AR.
if (displayAspectRatio > videoAspectRatio) {
texOffsetV = (1.0f - videoAspectRatio / displayAspectRatio) / 2.0f;
texOffsetV = (1.0f - videoAspectRatio / displayAspectRatio) /
2.0f;
} else {
texOffsetU = (1.0f - displayAspectRatio / videoAspectRatio) / 2.0f;
texOffsetU = (1.0f - displayAspectRatio / videoAspectRatio) /
2.0f;
}
}
Log.d(TAG, " Texture vertices: (" + texLeft + "," + texBottom +
@@ -378,6 +383,7 @@ public class VideoRendererGui implements GLSurfaceView.Renderer {
}
updateTextureProperties = false;
}
}
private void draw() {
if (!seenFrame) {
@@ -489,13 +495,16 @@ public class VideoRendererGui implements GLSurfaceView.Renderer {
}
public void setScreenSize(final int screenWidth, final int screenHeight) {
synchronized(updateTextureLock) {
this.screenWidth = screenWidth;
this.screenHeight = screenHeight;
updateTextureProperties = true;
}
}
public void setPosition(int x, int y, int width, int height,
ScalingType scalingType) {
synchronized(updateTextureLock) {
texLeft = (x - 50) / 50.0f;
texTop = (50 - y) / 50.0f;
texRight = Math.min(1.0f, (x + width - 50) / 50.0f);
@@ -503,6 +512,7 @@ public class VideoRendererGui implements GLSurfaceView.Renderer {
this.scalingType = scalingType;
updateTextureProperties = true;
}
}
@Override
public void setSize(final int width, final int height) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 587 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 663 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 743 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 761 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -7,37 +7,66 @@
android:weightSum="1"
android:layout_margin="8dp"
android:layout_centerHorizontal="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<ImageButton
android:id="@+id/add_room_button"
android:background="@android:drawable/ic_menu_add"
android:contentDescription="@string/add_room_description"
android:layout_marginRight="20dp"
android:layout_width="48dp"
android:layout_height="48dp"/>
<ImageButton
android:id="@+id/remove_room_button"
android:background="@android:drawable/ic_delete"
android:contentDescription="@string/remove_room_description"
android:layout_marginRight="20dp"
android:layout_width="48dp"
android:layout_height="48dp"/>
<ImageButton
android:id="@+id/connect_button"
android:background="@android:drawable/sym_action_call"
android:contentDescription="@string/connect_description"
android:layout_marginRight="20dp"
android:layout_width="48dp"
android:layout_height="48dp"/>
<ImageButton
android:id="@+id/connect_loopback_button"
android:background="@drawable/ic_loopback_call"
android:contentDescription="@string/connect_loopback_description"
android:layout_width="48dp"
android:layout_height="48dp"/>
</LinearLayout>
<TextView
android:id="@+id/room_edittext_description"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:lines="1"
android:maxLines="1"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="@string/room_name"/>
android:layout_margin="5dp"
android:text="@string/room_description"/>
<EditText
android:id="@+id/room_edittext"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:id="@+id/room_edittext"
android:imeOptions="actionGo"/>
android:imeOptions="actionDone"/>
<TextView
android:id="@+id/room_listview_description"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:lines="1"
android:maxLines="1"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_margin="15dp"
android:text="@string/room_description"/>
<CheckBox
android:id="@+id/check_loopback"
android:text="@string/room_names"/>
<ListView
android:id="@+id/room_listview"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="@string/loopback_text" />
<Button
android:id="@+id/connect_button"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/connect_text"
android:textAppearance="?android:attr/textAppearanceLarge" />
android:choiceMode="singleChoice"
android:listSelector="@android:color/darker_gray"
android:drawSelectorOnTop="false" />
</LinearLayout>

View File

@@ -13,6 +13,7 @@
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"/>
@@ -22,6 +23,7 @@
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"/>
@@ -29,6 +31,14 @@
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"/>

View File

@@ -12,4 +12,11 @@
<item>640 x 480</item>
<item>320 x 240</item>
</string-array>
<string-array name="cameraFps">
<item>Default</item>
<item>30 fps</item>
<item>15 fps</item>
</string-array>
</resources>

View File

@@ -3,24 +3,29 @@
<string name="app_name" translatable="no">AppRTC</string>
<string name="settings_name" translatable="no">AppRTC Settings</string>
<string name="disconnect_call">Disconnect Call</string>
<string name="room_name">Room name:</string>
<string name="room_names">Room names:</string>
<string name="room_description">
Please enter a room name. Room names are shared with everyone, so think
of something unique and send it to a friend.
</string>
<string name="connect_text">Connect</string>
<string name="loopback_text">Loopback connection</string>
<string name="invalid_url_title">Invalid URL</string>
<string name="invalid_url_text">The URL or room name you entered resulted in an invalid URL: %1$s
</string>
<string name="channel_error_title">Connection error</string>
<string name="connecting_to">Connecting to: %1$s</string>
<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="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>
<string name="connect_description">Connect to the room</string>
<string name="connect_loopback_description">Loopback connection</string>
<!-- Settings strings. -->
<string name="pref_room_key">room_preference</string>
<string name="pref_room_list_key">room_list_preference</string>
<string name="pref_url_key">url_preference</string>
<string name="pref_url_title">Connection URL:</string>
@@ -33,4 +38,10 @@
<string name="pref_resolution_summary">Video resolution.</string>
<string name="pref_resolution_dlg">Enter AppRTC local video resolution.</string>
<string name="pref_resolution_default">Default</string>
<string name="pref_fps_key">fps_preference</string>
<string name="pref_fps_title">Camera fps.</string>
<string name="pref_fps_summary">Camera fps.</string>
<string name="pref_fps_dlg">Enter local camera fps.</string>
<string name="pref_fps_default">Default</string>
</resources>

View File

@@ -15,4 +15,12 @@
android:dialogTitle="@string/pref_resolution_dlg"
android:entries="@array/videoResolutions"
android:entryValues="@array/videoResolutionsValues" />
<ListPreference
android:key="@string/pref_fps_key"
android:title="@string/pref_fps_title"
android:summary="@string/pref_fps_summary"
android:defaultValue="@string/pref_fps_default"
android:dialogTitle="@string/pref_fps_dlg"
android:entries="@array/cameraFps"
android:entryValues="@array/cameraFps" />
</PreferenceScreen>

View File

@@ -80,7 +80,9 @@ public interface AppRTCClient {
}
/**
* Signaling callbacks.
* Callback interface for messages delivered on signalling channel.
*
* Methods are guaranteed to be invoked on the UI thread of |activity|.
*/
public static interface AppRTCSignalingEvents {
/**

View File

@@ -32,6 +32,7 @@ import android.app.AlertDialog;
import android.app.Fragment;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.graphics.Color;
import android.media.AudioManager;
import android.net.Uri;
@@ -45,7 +46,6 @@ import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.Window;
import android.view.WindowManager;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
@@ -58,6 +58,7 @@ import org.webrtc.StatsObserver;
import org.webrtc.StatsReport;
import org.webrtc.VideoRenderer;
import org.webrtc.VideoRendererGui;
import org.webrtc.VideoRendererGui.ScalingType;
/**
* Main Activity of the AppRTCDemo Android app demonstrating interoperability
@@ -76,13 +77,14 @@ public class AppRTCDemoActivity extends Activity
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 roomName;
// Synchronize on quit[0] to avoid teardown-related crashes.
private final Boolean[] quit = new Boolean[] { false };
private ImageButton videoScalingButton;
private boolean iceConnected;
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -102,6 +104,7 @@ public class AppRTCDemoActivity extends Activity
Thread.setDefaultUncaughtExceptionHandler(
new UnhandledExceptionHandler(this));
iceConnected = false;
rootView = findViewById(android.R.id.content);
menuBar = findViewById(R.id.menubar_fragment);
@@ -109,10 +112,9 @@ public class AppRTCDemoActivity extends Activity
videoView = (GLSurfaceView) findViewById(R.id.glview);
VideoRendererGui.setView(videoView);
remoteRender = VideoRendererGui.create(0, 0, 100, 100,
VideoRendererGui.ScalingType.SCALE_ASPECT_FILL);
localRender = VideoRendererGui.create(0, 0, 100, 100,
VideoRendererGui.ScalingType.SCALE_ASPECT_FILL);
scalingType = ScalingType.SCALE_ASPECT_FILL;
remoteRender = VideoRendererGui.create(0, 0, 100, 100, scalingType);
localRender = VideoRendererGui.create(0, 0, 100, 100, scalingType);
videoView.setOnClickListener(
new View.OnClickListener() {
@@ -143,8 +145,10 @@ public class AppRTCDemoActivity extends Activity
new View.OnClickListener() {
@Override
public void onClick(View view) {
if (pc != null) {
pc.switchCamera();
}
}
});
((ImageButton) findViewById(R.id.button_toggle_debug)).setOnClickListener(
@@ -157,6 +161,24 @@ public class AppRTCDemoActivity extends Activity
}
});
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);
@@ -184,7 +206,11 @@ public class AppRTCDemoActivity extends Activity
(loopback != null && loopback.equals("loopback"))) {
logAndToast(getString(R.string.connecting_to, url));
appRtcClient.connectToRoom(url.toString());
if (room != null && !room.equals("")) {
roomName.setText(room);
} else {
roomName.setText("loopback");
}
} else {
logAndToast("Empty or missing room name!");
finish();
@@ -230,6 +256,16 @@ public class AppRTCDemoActivity extends Activity
super.onDestroy();
}
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);
}
}
// Update the heads-up display with information from |reports|.
private void updateHUD(StatsReport[] reports) {
StringBuilder builder = new StringBuilder();
@@ -279,21 +315,28 @@ public class AppRTCDemoActivity extends Activity
// Disconnect from remote resources, dispose of local resources, and exit.
private void disconnect() {
synchronized (quit[0]) {
if (quit[0]) {
return;
}
quit[0] = true;
if (pc != null) {
pc.close();
pc = null;
}
if (appRtcClient != null) {
appRtcClient.disconnect();
appRtcClient = null;
}
if (pc != null) {
pc.close();
pc = null;
}
finish();
}
private void disconnectWithMessage(String errorMessage) {
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();
}
// Poor-man's assert(): die with |msg| unless |condition| is true.
@@ -324,13 +367,16 @@ public class AppRTCDemoActivity extends Activity
logAndToast("Creating peer connection...");
pc = new PeerConnectionClient(
this, localRender, remoteRender, appRtcParameters, this);
if (pc.isHDVideo()) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
} else {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
}
{
final PeerConnectionClient finalPC = pc;
final Runnable repeatedStatsLogger = new Runnable() {
public void run() {
synchronized (quit[0]) {
if (quit[0]) {
if (pc == null) {
return;
}
final Runnable runnableThis = this;
@@ -338,7 +384,7 @@ public class AppRTCDemoActivity extends Activity
videoView.postDelayed(runnableThis, 1000);
return;
}
boolean success = finalPC.getStats(new StatsObserver() {
boolean success = pc.getStats(new StatsObserver() {
public void onComplete(final StatsReport[] reports) {
runOnUiThread(new Runnable() {
public void run() {
@@ -355,7 +401,6 @@ public class AppRTCDemoActivity extends Activity
throw new RuntimeException("getStats() return false!");
}
}
}
};
videoView.postDelayed(repeatedStatsLogger, 1000);
}
@@ -365,6 +410,9 @@ public class AppRTCDemoActivity extends Activity
@Override
public void onChannelOpen() {
if (pc == null) {
return;
}
if (appRtcParameters.initiator) {
logAndToast("Creating OFFER...");
// Create offer. Offer SDP will be sent to answering client in
@@ -375,6 +423,9 @@ public class AppRTCDemoActivity extends Activity
@Override
public void onRemoteDescription(final SessionDescription sdp) {
if (pc == null) {
return;
}
logAndToast("Received remote " + sdp.type + " ...");
pc.setRemoteDescription(sdp);
if (!appRtcParameters.initiator) {
@@ -387,8 +438,10 @@ public class AppRTCDemoActivity extends Activity
@Override
public void onRemoteIceCandidate(final IceCandidate candidate) {
if (pc != null) {
pc.addRemoteIceCandidate(candidate);
}
}
@Override
public void onChannelClose() {
@@ -398,8 +451,7 @@ public class AppRTCDemoActivity extends Activity
@Override
public void onChannelError(int code, String description) {
logAndToast("Channel error: " + code + ". " + description);
disconnect();
disconnectWithMessage(description);
}
// -----Implementation of PeerConnectionClient.PeerConnectionEvents.---------
@@ -407,19 +459,35 @@ public class AppRTCDemoActivity extends Activity
// All callbacks are invoked from UI thread.
@Override
public void onLocalDescription(final SessionDescription sdp) {
if (appRtcClient != null) {
logAndToast("Sending " + sdp.type + " ...");
appRtcClient.sendLocalDescription(sdp);
}
}
@Override
public void onIceCandidate(final IceCandidate candidate) {
if (appRtcClient != null) {
appRtcClient.sendLocalIceCandidate(candidate);
}
}
@Override
public void onIceConnected() {
logAndToast("ICE connected");
VideoRendererGui.update(localRender, 70, 70, 28, 28,
VideoRendererGui.ScalingType.SCALE_ASPECT_FIT);
iceConnected = true;
updateVideoView();
}
@Override
public void onIceDisconnected() {
logAndToast("ICE disconnected");
disconnect();
}
@Override
public void onPeerConnectionError(String description) {
disconnectWithMessage(description);
}
}

View File

@@ -32,7 +32,6 @@ import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.net.Uri;
import android.os.Bundle;
import android.preference.PreferenceManager;
@@ -44,26 +43,40 @@ import android.view.View;
import android.view.View.OnClickListener;
import android.view.inputmethod.EditorInfo;
import android.webkit.URLUtil;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ListView;
import android.widget.TextView;
import java.util.ArrayList;
import org.json.JSONArray;
import org.json.JSONException;
import org.webrtc.MediaCodecVideoEncoder;
/**
* Handles the initial setup where the user selects which room to join.
*/
public class ConnectActivity extends Activity {
private static final String TAG = "ConnectActivity";
private Button connectButton;
private ImageButton addRoomButton;
private ImageButton removeRoomButton;
private ImageButton connectButton;
private ImageButton connectLoopbackButton;
private EditText roomEditText;
private CheckBox loopbackCheckBox;
private ListView roomListView;
private SharedPreferences sharedPref;
private String keyprefUrl;
private String keyprefResolution;
private String keyprefFps;
private String keyprefRoom;
private String keyprefRoomList;
private ArrayList<String> roomList;
private ArrayAdapter<String> adapter;
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -74,7 +87,9 @@ public class ConnectActivity extends Activity {
sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
keyprefUrl = getString(R.string.pref_url_key);
keyprefResolution = getString(R.string.pref_resolution_key);
keyprefFps = getString(R.string.pref_fps_key);
keyprefRoom = getString(R.string.pref_room_key);
keyprefRoomList = getString(R.string.pref_room_list_key);
// If an implicit VIEW intent is launching the app, go directly to that URL.
final Intent intent = getIntent();
@@ -85,17 +100,14 @@ public class ConnectActivity extends Activity {
setContentView(R.layout.activity_connect);
loopbackCheckBox = (CheckBox) findViewById(R.id.check_loopback);
loopbackCheckBox.setChecked(false);
roomEditText = (EditText) findViewById(R.id.room_edittext);
roomEditText.setOnEditorActionListener(
new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(
TextView textView, int i, KeyEvent keyEvent) {
if (i == EditorInfo.IME_ACTION_GO) {
connectButton.performClick();
if (i == EditorInfo.IME_ACTION_DONE) {
addRoomButton.performClick();
return true;
}
return false;
@@ -103,42 +115,18 @@ public class ConnectActivity extends Activity {
});
roomEditText.requestFocus();
connectButton = (Button) findViewById(R.id.connect_button);
connectButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
String url = sharedPref.getString(keyprefUrl,
getString(R.string.pref_url_default));
if (loopbackCheckBox.isChecked()) {
url += "/?debug=loopback";
} else {
url += "/?r=" + roomEditText.getText();
}
roomListView = (ListView) findViewById(R.id.room_listview);
roomListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
// Add video resolution constraints.
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) {
url += "&video=minHeight=" + maxHeight + ",maxHeight=" +
maxHeight + ",minWidth=" + maxWidth + ",maxWidth=" + maxWidth;
}
} catch (NumberFormatException e) {
Log.e(TAG, "Wrong video resolution setting: " + resolution);
}
} else {
if (MediaCodecVideoEncoder.isPlatformSupported()) {
url += "&hd=true";
}
}
// TODO(kjellander): Add support for custom parameters to the URL.
connectToRoom(url);
}
});
addRoomButton = (ImageButton) findViewById(R.id.add_room_button);
addRoomButton.setOnClickListener(addRoomListener);
removeRoomButton = (ImageButton) findViewById(R.id.remove_room_button);
removeRoomButton.setOnClickListener(removeRoomListener);
connectButton = (ImageButton) findViewById(R.id.connect_button);
connectButton.setOnClickListener(connectListener);
connectLoopbackButton =
(ImageButton) findViewById(R.id.connect_loopback_button);
connectLoopbackButton.setOnClickListener(connectListener);
}
@Override
@@ -163,8 +151,10 @@ public class ConnectActivity extends Activity {
public void onPause() {
super.onPause();
String room = roomEditText.getText().toString();
String roomListJson = new JSONArray(roomList).toString();
SharedPreferences.Editor editor = sharedPref.edit();
editor.putString(keyprefRoom, room);
editor.putString(keyprefRoomList, roomListJson);
editor.commit();
}
@@ -173,7 +163,99 @@ public class ConnectActivity extends Activity {
super.onResume();
String room = sharedPref.getString(keyprefRoom, "");
roomEditText.setText(room);
roomList = new ArrayList<String>();
String roomListJson = sharedPref.getString(keyprefRoomList, null);
if (roomListJson != null) {
try {
JSONArray jsonArray = new JSONArray(roomListJson);
for (int i = 0; i < jsonArray.length(); i++) {
roomList.add(jsonArray.get(i).toString());
}
} catch (JSONException e) {
Log.e(TAG, "Failed to load room list: " + e.toString());
}
}
adapter = new ArrayAdapter<String>(
this, android.R.layout.simple_list_item_1, roomList);
roomListView.setAdapter(adapter);
if (adapter.getCount() > 0) {
roomListView.requestFocus();
roomListView.setItemChecked(0, true);
}
}
private final OnClickListener connectListener = new OnClickListener() {
@Override
public void onClick(View view) {
boolean loopback = false;
if (view.getId() == R.id.connect_loopback_button) {
loopback = true;
}
String url = sharedPref.getString(keyprefUrl,
getString(R.string.pref_url_default));
if (loopback) {
url += "/?debug=loopback";
} else {
String roomName = getSelectedItem();
if (roomName == null) {
roomName = roomEditText.getText().toString();
}
url += "/?r=" + roomName;
}
String parametersResolution = null;
String parametersFps = null;
// Add video resolution constraints.
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;
}
} catch (NumberFormatException e) {
Log.e(TAG, "Wrong video resolution setting: " + resolution);
}
}
// Add camera fps constraints.
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;
}
} catch (NumberFormatException e) {
Log.e(TAG, "Wrong camera fps setting: " + fps);
}
}
// Modify connection URL.
if (parametersResolution != null || parametersFps != null) {
url += "&video=";
if (parametersResolution != null) {
url += parametersResolution;
if (parametersFps != null) {
url += ",";
}
}
if (parametersFps != null) {
url += parametersFps;
}
} else {
if (MediaCodecVideoEncoder.isPlatformSupported()) {
url += "&hd=true";
}
}
// TODO(kjellander): Add support for custom parameters to the URL.
connectToRoom(url);
}
};
private void connectToRoom(String roomUrl) {
if (validateUrl(roomUrl)) {
@@ -199,4 +281,42 @@ public class ConnectActivity extends Activity {
}).create().show();
return false;
}
private final OnClickListener addRoomListener = new OnClickListener() {
@Override
public void onClick(View view) {
String newRoom = roomEditText.getText().toString();
if (newRoom.length() > 0 && !roomList.contains(newRoom)) {
adapter.add(newRoom);
adapter.notifyDataSetChanged();
}
}
};
private final OnClickListener removeRoomListener = new OnClickListener() {
@Override
public void onClick(View view) {
String selectedRoom = getSelectedItem();
if (selectedRoom != null) {
adapter.remove(selectedRoom);
adapter.notifyDataSetChanged();
}
}
};
private String getSelectedItem() {
int position = AdapterView.INVALID_POSITION;
if (roomListView.getCheckedItemCount() > 0 && adapter.getCount() > 0) {
position = roomListView.getCheckedItemPosition();
if (position >= adapter.getCount()) {
position = AdapterView.INVALID_POSITION;
}
}
if (position != AdapterView.INVALID_POSITION) {
return adapter.getItem(position);
} else {
return null;
}
}
}

View File

@@ -51,15 +51,12 @@ public class GAEChannelClient {
/**
* Callback interface for messages delivered on the Google AppEngine channel.
*
* Methods are guaranteed to be invoked on the UI thread of |activity| passed
* to GAEChannelClient's constructor.
*/
public interface GAEMessageHandler {
public void onOpen();
public void onMessage(String data);
public void onMessage(final String data);
public void onClose();
public void onError(int code, String description);
public void onError(final int code, final String description);
}
/** Asynchronously open an AppEngine channel. */
@@ -83,8 +80,7 @@ public class GAEChannelClient {
", desc: " + description);
}
});
proxyingMessageHandler =
new ProxyingMessageHandler(activity, handler, token);
proxyingMessageHandler = new ProxyingMessageHandler(handler, token);
webView.addJavascriptInterface(
proxyingMessageHandler, "androidMessageHandler");
webView.loadUrl("file:///android_asset/channel.html");
@@ -102,72 +98,52 @@ public class GAEChannelClient {
}
// Helper class for proxying callbacks from the Java<->JS interaction
// (private, background) thread to the Activity's UI thread.
// (private, background) thread.
private static class ProxyingMessageHandler {
private final Activity activity;
private final GAEMessageHandler handler;
private final boolean[] disconnected = { false };
private boolean disconnected = false;
private final String token;
public
ProxyingMessageHandler(Activity activity, GAEMessageHandler handler,
String token) {
this.activity = activity;
public ProxyingMessageHandler(GAEMessageHandler handler, String token) {
this.handler = handler;
this.token = token;
}
public void disconnect() {
disconnected[0] = true;
disconnected = true;
}
private boolean disconnected() {
return disconnected[0];
}
@JavascriptInterface public String getToken() {
@JavascriptInterface
public String getToken() {
return token;
}
@JavascriptInterface public void onOpen() {
activity.runOnUiThread(new Runnable() {
public void run() {
if (!disconnected()) {
@JavascriptInterface
public void onOpen() {
if (!disconnected) {
handler.onOpen();
}
}
});
}
@JavascriptInterface public void onMessage(final String data) {
activity.runOnUiThread(new Runnable() {
public void run() {
if (!disconnected()) {
@JavascriptInterface
public void onMessage(final String data) {
if (!disconnected) {
handler.onMessage(data);
}
}
});
}
@JavascriptInterface public void onClose() {
activity.runOnUiThread(new Runnable() {
public void run() {
if (!disconnected()) {
@JavascriptInterface
public void onClose() {
if (!disconnected) {
handler.onClose();
}
}
});
}
@JavascriptInterface public void onError(
final int code, final String description) {
activity.runOnUiThread(new Runnable() {
public void run() {
if (!disconnected()) {
@JavascriptInterface
public void onError(final int code, final String description) {
if (!disconnected) {
handler.onError(code, description);
}
}
});
}
}
}

View File

@@ -29,7 +29,6 @@ package org.appspot.apprtc;
import android.app.Activity;
import android.os.AsyncTask;
import android.util.Log;
import android.webkit.JavascriptInterface;
import org.json.JSONArray;
import org.json.JSONException;
@@ -41,7 +40,6 @@ 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;
@@ -83,11 +81,6 @@ public class GAERTCClient implements AppRTCClient {
*/
@Override
public void connectToRoom(String url) {
while (url.indexOf('?') < 0) {
// Keep redirecting until we get a room number.
(new RedirectResolver()).execute(url);
return; // RedirectResolver above calls us back with the next URL.
}
(new RoomParameterGetter()).execute(url);
}
@@ -97,6 +90,7 @@ public class GAERTCClient implements AppRTCClient {
@Override
public void disconnect() {
if (channelClient != null) {
Log.d(TAG, "Closing GAE Channel.");
sendMessage("{\"type\": \"bye\"}");
channelClient.close();
channelClient = null;
@@ -151,68 +145,36 @@ public class GAERTCClient implements AppRTCClient {
}
}
// Load the given URL and return the value of the Location header of the
// resulting 302 response. If the result is not a 302, throws.
private class RedirectResolver extends AsyncTask<String, Void, String> {
@Override
protected String doInBackground(String... urls) {
if (urls.length != 1) {
throw new RuntimeException("Must be called with a single URL");
}
try {
return followRedirect(urls[0]);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
protected void onPostExecute(String url) {
connectToRoom(url);
}
private String followRedirect(String url) throws IOException {
HttpURLConnection connection = (HttpURLConnection)
new URL(url).openConnection();
connection.setInstanceFollowRedirects(false);
int code = connection.getResponseCode();
if (code != HttpURLConnection.HTTP_MOVED_TEMP) {
throw new IOException("Unexpected response: " + code + " for " + url +
", with contents: " + drainStream(connection.getInputStream()));
}
int n = 0;
String name, value;
while ((name = connection.getHeaderFieldKey(n)) != null) {
value = connection.getHeaderField(n);
if (name.equals("Location")) {
return value;
}
++n;
}
throw new IOException("Didn't find Location header!");
}
}
// AsyncTask that converts an AppRTC room URL into the set of signaling
// parameters to use with that room.
private class RoomParameterGetter
extends AsyncTask<String, Void, AppRTCSignalingParameters> {
private Exception exception = null;
@Override
protected AppRTCSignalingParameters doInBackground(String... urls) {
if (urls.length != 1) {
throw new RuntimeException("Must be called with a single URL");
exception = new RuntimeException("Must be called with a single URL");
return null;
}
try {
exception = null;
return getParametersForRoomUrl(urls[0]);
} catch (JSONException e) {
throw new RuntimeException(e);
exception = e;
} catch (IOException e) {
throw new RuntimeException(e);
exception = e;
}
return null;
}
@Override
protected void onPostExecute(AppRTCSignalingParameters params) {
if (exception != null) {
Log.e(TAG, "Room connection error: " + exception.toString());
events.onChannelError(0, exception.getMessage());
return;
}
channelClient =
new GAEChannelClient(activity, channelToken, gaeHandler);
synchronized (sendQueue) {
@@ -445,12 +407,24 @@ public class GAERTCClient implements AppRTCClient {
// Implementation detail: handler for receiving GAE messages and dispatching
// them appropriately.
private class GAEHandler implements GAEChannelClient.GAEMessageHandler {
@JavascriptInterface public void onOpen() {
private boolean channelOpen = false;
public void onOpen() {
activity.runOnUiThread(new Runnable() {
public void run() {
events.onChannelOpen();
channelOpen = true;
}
});
}
@JavascriptInterface public void onMessage(String msg) {
public void onMessage(final String msg) {
Log.d(TAG, "RECEIVE: " + msg);
activity.runOnUiThread(new Runnable() {
public void run() {
if (!channelOpen) {
return;
}
try {
JSONObject json = new JSONObject(msg);
String type = (String) json.get("type");
@@ -468,19 +442,32 @@ public class GAERTCClient implements AppRTCClient {
} else if (type.equals("bye")) {
events.onChannelClose();
} else {
throw new RuntimeException("Unexpected message: " + msg);
events.onChannelError(1, "Unexpected channel message: " + msg);
}
} catch (JSONException e) {
throw new RuntimeException(e);
events.onChannelError(1, "Channel message JSON parsing error: " +
e.toString());
}
}
});
}
@JavascriptInterface public void onClose() {
public void onClose() {
activity.runOnUiThread(new Runnable() {
public void run() {
events.onChannelClose();
channelOpen = false;
}
});
}
@JavascriptInterface public void onError(int code, String description) {
public void onError(final int code, final String description) {
activity.runOnUiThread(new Runnable() {
public void run() {
events.onChannelError(code, description);
channelOpen = false;
}
});
}
}

View File

@@ -37,6 +37,7 @@ import org.webrtc.MediaConstraints;
import org.webrtc.MediaStream;
import org.webrtc.MediaStreamTrack;
import org.webrtc.PeerConnection;
import org.webrtc.MediaConstraints.KeyValuePair;
import org.webrtc.PeerConnection.IceConnectionState;
import org.webrtc.PeerConnectionFactory;
import org.webrtc.SdpObserver;
@@ -60,13 +61,13 @@ public class PeerConnectionClient {
private boolean videoSourceStopped;
private final PCObserver pcObserver = new PCObserver();
private final SDPObserver sdpObserver = new SDPObserver();
private final MediaConstraints videoConstraints;
private final VideoRenderer.Callbacks localRender;
private final VideoRenderer.Callbacks remoteRender;
private LinkedList<IceCandidate> queuedRemoteCandidates =
new LinkedList<IceCandidate>();
private final MediaConstraints sdpMediaConstraints;
private final PeerConnectionEvents events;
private MediaConstraints sdpMediaConstraints;
private MediaConstraints videoConstraints;
private PeerConnectionEvents events;
private boolean isInitiator;
private boolean useFrontFacingCamera = true;
private SessionDescription localSdp = null; // either offer or answer SDP
@@ -79,7 +80,6 @@ public class PeerConnectionClient {
AppRTCSignalingParameters appRtcParameters,
PeerConnectionEvents events) {
this.activity = activity;
this.videoConstraints = appRtcParameters.videoConstraints;
this.localRender = localRender;
this.remoteRender = remoteRender;
this.events = events;
@@ -90,9 +90,9 @@ public class PeerConnectionClient {
"OfferToReceiveAudio", "true"));
sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
"OfferToReceiveVideo", "true"));
videoConstraints = appRtcParameters.videoConstraints;
factory = new PeerConnectionFactory();
MediaConstraints pcConstraints = appRtcParameters.pcConstraints;
pcConstraints.optional.add(
new MediaConstraints.KeyValuePair("RtpDataChannels", "true"));
@@ -122,38 +122,91 @@ public class PeerConnectionClient {
}
}
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;
}
}
public boolean getStats(StatsObserver observer, MediaStreamTrack track) {
return pc.getStats(observer, track);
}
public void createOffer() {
activity.runOnUiThread(new Runnable() {
public void run() {
if (pc != null) {
isInitiator = true;
pc.createOffer(sdpObserver, sdpMediaConstraints);
}
}
});
}
public void createAnswer() {
activity.runOnUiThread(new Runnable() {
public void run() {
if (pc != null) {
isInitiator = false;
pc.createAnswer(sdpObserver, sdpMediaConstraints);
}
}
});
}
public void addRemoteIceCandidate(IceCandidate candidate) {
public void addRemoteIceCandidate(final IceCandidate candidate) {
activity.runOnUiThread(new Runnable() {
public void run() {
if (pc != null) {
if (queuedRemoteCandidates != null) {
queuedRemoteCandidates.add(candidate);
} else {
pc.addIceCandidate(candidate);
}
}
}
});
}
public void setRemoteDescription(SessionDescription sdp) {
public void setRemoteDescription(final SessionDescription sdp) {
activity.runOnUiThread(new Runnable() {
public void run() {
if (pc != null) {
SessionDescription sdpISAC = new SessionDescription(
sdp.type, preferISAC(sdp.description));
Log.d(TAG, "Set remote SDP");
pc.setRemoteDescription(sdpObserver, sdpISAC);
}
}
});
}
public void stopVideoSource() {
if (videoSource != null) {
Log.d(TAG, "Stop video source.");
videoSource.stop();
videoSourceStopped = true;
}
@@ -161,12 +214,16 @@ public class PeerConnectionClient {
public void startVideoSource() {
if (videoSource != null && videoSourceStopped) {
Log.d(TAG, "Restart video source.");
videoSource.restart();
videoSourceStopped = false;
}
}
public void close() {
activity.runOnUiThread(new Runnable() {
public void run() {
Log.d(TAG, "Closing peer connection.");
if (pc != null) {
pc.dispose();
pc = null;
@@ -180,6 +237,8 @@ public class PeerConnectionClient {
factory = null;
}
}
});
}
/**
* SDP/ICE ready callbacks.
@@ -200,6 +259,25 @@ public class PeerConnectionClient {
* CONNECTED).
*/
public void onIceConnected();
/**
* Callback fired once connection is closed (IceConnectionState is
* DISCONNECTED).
*/
public void onIceDisconnected();
/**
* Callback fired once peer connection error happened.
*/
public void onPeerConnectionError(String description);
}
private void reportError(final String errorMessage) {
activity.runOnUiThread(new Runnable() {
public void run() {
events.onPeerConnectionError(errorMessage);
}
});
}
// Cycle through likely device names for the camera and return the first
@@ -225,7 +303,8 @@ public class PeerConnectionClient {
}
}
}
throw new RuntimeException("Failed to open capturer");
reportError("Failed to open capturer");
return null;
}
private VideoTrack createVideoTrack(boolean frontFacing) {
@@ -245,9 +324,9 @@ public class PeerConnectionClient {
}
// Poor-man's assert(): die with |msg| unless |condition| is true.
private static void abortUnless(boolean condition, String msg) {
private void abortUnless(boolean condition, String msg) {
if (!condition) {
throw new RuntimeException(msg);
reportError(msg);
}
}
@@ -362,12 +441,8 @@ public class PeerConnectionClient {
}
@Override
public void onError(){
activity.runOnUiThread(new Runnable() {
public void run() {
throw new RuntimeException("PeerConnection error!");
}
});
public void onError() {
reportError("PeerConnection error!");
}
@Override
@@ -386,6 +461,14 @@ public class PeerConnectionClient {
events.onIceConnected();
}
});
} else if (newState == IceConnectionState.DISCONNECTED) {
activity.runOnUiThread(new Runnable() {
public void run() {
events.onIceDisconnected();
}
});
} else if (newState == IceConnectionState.FAILED) {
reportError("ICE connection failed.");
}
}
@@ -420,14 +503,9 @@ public class PeerConnectionClient {
@Override
public void onDataChannel(final DataChannel dc) {
activity.runOnUiThread(new Runnable() {
public void run() {
throw new RuntimeException(
"AppRTC doesn't use data channels, but got: " + dc.label() +
reportError("AppRTC doesn't use data channels, but got: " + dc.label() +
" anyway!");
}
});
}
@Override
public void onRenegotiationNeeded() {
@@ -447,9 +525,11 @@ public class PeerConnectionClient {
localSdp = sdp;
activity.runOnUiThread(new Runnable() {
public void run() {
if (pc != null) {
Log.d(TAG, "Set local SDP from " + sdp.type);
pc.setLocalDescription(sdpObserver, sdp);
}
}
});
}
@@ -457,6 +537,9 @@ public class PeerConnectionClient {
public void onSetSuccess() {
activity.runOnUiThread(new Runnable() {
public void run() {
if (pc == null) {
return;
}
if (isInitiator) {
// For offering peer connection we first create offer and set
// local SDP, then after receiving answer set remote SDP.
@@ -491,20 +574,12 @@ public class PeerConnectionClient {
@Override
public void onCreateFailure(final String error) {
activity.runOnUiThread(new Runnable() {
public void run() {
throw new RuntimeException("createSDP error: " + error);
}
});
reportError("createSDP error: " + error);
}
@Override
public void onSetFailure(final String error) {
activity.runOnUiThread(new Runnable() {
public void run() {
throw new RuntimeException("setSDP error: " + error);
}
});
reportError("setSDP error: " + error);
}
}
@@ -524,11 +599,7 @@ public class PeerConnectionClient {
@Override
public void onSetFailure(final String error) {
activity.runOnUiThread(new Runnable() {
public void run() {
throw new RuntimeException("setSDP error while switching camera: " + error);
}
});
reportError("setSDP error while switching camera: " + error);
}
}
}

View File

@@ -38,12 +38,14 @@ public class SettingsActivity extends Activity
private SettingsFragment settingsFragment;
private String keyprefUrl;
private String keyprefResolution;
private String keyprefFps;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
keyprefUrl = getString(R.string.pref_url_key);
keyprefResolution = getString(R.string.pref_resolution_key);
keyprefFps = getString(R.string.pref_fps_key);
// Display the fragment as the main content.
settingsFragment = new SettingsFragment();
@@ -61,6 +63,7 @@ public class SettingsActivity extends Activity
sharedPreferences.registerOnSharedPreferenceChangeListener(this);
updateSummary(sharedPreferences, keyprefUrl);
updateSummary(sharedPreferences, keyprefResolution);
updateSummary(sharedPreferences, keyprefFps);
}
@Override
@@ -74,7 +77,8 @@ public class SettingsActivity extends Activity
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
String key) {
if (key.equals(keyprefUrl) || key.equals(keyprefResolution)) {
if (key.equals(keyprefUrl) || key.equals(keyprefResolution) ||
key.equals(keyprefFps)) {
updateSummary(sharedPreferences, key);
}
}

View File

@@ -317,14 +317,26 @@
'examples/android/build.xml',
'examples/android/jni/Android.mk',
'examples/android/project.properties',
'examples/android/res/drawable-hdpi/ic_launcher.png',
'examples/android/res/drawable-hdpi/disconnect.png',
'examples/android/res/drawable-ldpi/ic_launcher.png',
'examples/android/res/drawable-hdpi/ic_action_full_screen.png',
'examples/android/res/drawable-hdpi/ic_action_return_from_full_screen.png',
'examples/android/res/drawable-hdpi/ic_loopback_call.png',
'examples/android/res/drawable-hdpi/ic_launcher.png',
'examples/android/res/drawable-ldpi/disconnect.png',
'examples/android/res/drawable-mdpi/ic_launcher.png',
'examples/android/res/drawable-ldpi/ic_action_full_screen.png',
'examples/android/res/drawable-ldpi/ic_action_return_from_full_screen.png',
'examples/android/res/drawable-ldpi/ic_loopback_call.png',
'examples/android/res/drawable-ldpi/ic_launcher.png',
'examples/android/res/drawable-mdpi/disconnect.png',
'examples/android/res/drawable-xhdpi/ic_launcher.png',
'examples/android/res/drawable-mdpi/ic_action_full_screen.png',
'examples/android/res/drawable-mdpi/ic_action_return_from_full_screen.png',
'examples/android/res/drawable-mdpi/ic_loopback_call.png',
'examples/android/res/drawable-mdpi/ic_launcher.png',
'examples/android/res/drawable-xhdpi/disconnect.png',
'examples/android/res/drawable-xhdpi/ic_action_full_screen.png',
'examples/android/res/drawable-xhdpi/ic_action_return_from_full_screen.png',
'examples/android/res/drawable-xhdpi/ic_loopback_call.png',
'examples/android/res/drawable-xhdpi/ic_launcher.png',
'examples/android/res/layout/activity_connect.xml',
'examples/android/res/layout/activity_fullscreen.xml',
'examples/android/res/layout/fragment_menubar.xml',