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,64 +321,68 @@ public class VideoRendererGui implements GLSurfaceView.Renderer {
scalingType == ScalingType.SCALE_FILL) {
return;
}
// Re - calculate texture vertices to preserve video aspect ratio.
float texRight = this.texRight;
float texLeft = this.texLeft;
float texTop = this.texTop;
float texBottom = this.texBottom;
float texOffsetU = 0;
float texOffsetV = 0;
float displayWidth = (texRight - texLeft) * screenWidth / 2;
float displayHeight = (texTop - texBottom) * screenHeight / 2;
Log.d(TAG, "ID: " + id + ". Display: " + displayWidth +
" x " + displayHeight + ". Video: " + videoWidth +
" x " + videoHeight);
if (displayWidth > 1 && displayHeight > 1 &&
videoWidth > 1 && videoHeight > 1) {
float displayAspectRatio = displayWidth / displayHeight;
float videoAspectRatio = (float)videoWidth / videoHeight;
if (scalingType == ScalingType.SCALE_ASPECT_FIT) {
// Need to re-adjust vertices width or height to match video AR.
if (displayAspectRatio > videoAspectRatio) {
float deltaX = (displayWidth - videoAspectRatio * displayHeight) /
instance.screenWidth;
texRight -= deltaX;
texLeft += deltaX;
} else {
float deltaY = (displayHeight - displayWidth / videoAspectRatio) /
instance.screenHeight;
texTop -= deltaY;
texBottom += deltaY;
synchronized(updateTextureLock) {
// Re - calculate texture vertices to preserve video aspect ratio.
float texRight = this.texRight;
float texLeft = this.texLeft;
float texTop = this.texTop;
float texBottom = this.texBottom;
float texOffsetU = 0;
float texOffsetV = 0;
float displayWidth = (texRight - texLeft) * screenWidth / 2;
float displayHeight = (texTop - texBottom) * screenHeight / 2;
Log.d(TAG, "ID: " + id + ". Display: " + displayWidth +
" x " + displayHeight + ". Video: " + videoWidth +
" x " + videoHeight);
if (displayWidth > 1 && displayHeight > 1 &&
videoWidth > 1 && videoHeight > 1) {
float displayAspectRatio = displayWidth / displayHeight;
float videoAspectRatio = (float)videoWidth / videoHeight;
if (scalingType == ScalingType.SCALE_ASPECT_FIT) {
// Need to re-adjust vertices width or height to match video AR.
if (displayAspectRatio > videoAspectRatio) {
float deltaX = (displayWidth - videoAspectRatio * displayHeight) /
instance.screenWidth;
texRight -= deltaX;
texLeft += deltaX;
} else {
float deltaY = (displayHeight - displayWidth / videoAspectRatio) /
instance.screenHeight;
texTop -= deltaY;
texBottom += deltaY;
}
}
}
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;
} else {
texOffsetU = (1.0f - displayAspectRatio / videoAspectRatio) / 2.0f;
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;
} else {
texOffsetU = (1.0f - displayAspectRatio / videoAspectRatio) /
2.0f;
}
}
}
Log.d(TAG, " Texture vertices: (" + texLeft + "," + texBottom +
") - (" + texRight + "," + texTop + ")");
float textureVeticesFloat[] = new float[] {
texLeft, texTop,
texLeft, texBottom,
texRight, texTop,
texRight, texBottom
};
textureVertices = directNativeFloatBuffer(textureVeticesFloat);
Log.d(TAG, " Texture vertices: (" + texLeft + "," + texBottom +
") - (" + texRight + "," + texTop + ")");
float textureVeticesFloat[] = new float[] {
texLeft, texTop,
texLeft, texBottom,
texRight, texTop,
texRight, texBottom
};
textureVertices = directNativeFloatBuffer(textureVeticesFloat);
Log.d(TAG, " Texture UV offsets: " + texOffsetU + ", " + texOffsetV);
float textureCoordinatesFloat[] = new float[] {
texOffsetU, texOffsetV, // left top
texOffsetU, 1.0f - texOffsetV, // left bottom
1.0f - texOffsetU, texOffsetV, // right top
1.0f - texOffsetU, 1.0f - texOffsetV // right bottom
};
textureCoords = directNativeFloatBuffer(textureCoordinatesFloat);
Log.d(TAG, " Texture UV offsets: " + texOffsetU + ", " + texOffsetV);
float textureCoordinatesFloat[] = new float[] {
texOffsetU, texOffsetV, // left top
texOffsetU, 1.0f - texOffsetV, // left bottom
1.0f - texOffsetU, texOffsetV, // right top
1.0f - texOffsetU, 1.0f - texOffsetV // right bottom
};
textureCoords = directNativeFloatBuffer(textureCoordinatesFloat);
}
updateTextureProperties = false;
}
updateTextureProperties = false;
}
private void draw() {
@ -489,19 +495,23 @@ public class VideoRendererGui implements GLSurfaceView.Renderer {
}
public void setScreenSize(final int screenWidth, final int screenHeight) {
this.screenWidth = screenWidth;
this.screenHeight = screenHeight;
updateTextureProperties = true;
synchronized(updateTextureLock) {
this.screenWidth = screenWidth;
this.screenHeight = screenHeight;
updateTextureProperties = true;
}
}
public void setPosition(int x, int y, int width, int height,
ScalingType scalingType) {
texLeft = (x - 50) / 50.0f;
texTop = (50 - y) / 50.0f;
texRight = Math.min(1.0f, (x + width - 50) / 50.0f);
texBottom = Math.max(-1.0f, (50 - y - height) / 50.0f);
this.scalingType = scalingType;
updateTextureProperties = true;
synchronized(updateTextureLock) {
texLeft = (x - 50) / 50.0f;
texTop = (50 - y) / 50.0f;
texRight = Math.min(1.0f, (x + width - 50) / 50.0f);
texBottom = Math.max(-1.0f, (50 - y - height) / 50.0f);
this.scalingType = scalingType;
updateTextureProperties = true;
}
}
@Override

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,7 +145,9 @@ public class AppRTCDemoActivity extends Activity
new View.OnClickListener() {
@Override
public void onClick(View view) {
pc.switchCamera();
if (pc != null) {
pc.switchCamera();
}
}
});
@ -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());
roomName.setText(room);
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;
}
finish();
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,39 +367,41 @@ 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]) {
return;
}
final Runnable runnableThis = this;
if (hudView.getVisibility() == View.INVISIBLE) {
videoView.postDelayed(runnableThis, 1000);
return;
}
boolean success = finalPC.getStats(new StatsObserver() {
public void onComplete(final StatsReport[] reports) {
runOnUiThread(new Runnable() {
public void run() {
updateHUD(reports);
}
});
for (StatsReport report : reports) {
Log.d(TAG, "Stats: " + report.toString());
}
videoView.postDelayed(runnableThis, 1000);
public void run() {
if (pc == null) {
return;
}
final Runnable runnableThis = this;
if (hudView.getVisibility() == View.INVISIBLE) {
videoView.postDelayed(runnableThis, 1000);
return;
}
boolean success = pc.getStats(new StatsObserver() {
public void onComplete(final StatsReport[] reports) {
runOnUiThread(new Runnable() {
public void run() {
updateHUD(reports);
}
});
for (StatsReport report : reports) {
Log.d(TAG, "Stats: " + report.toString());
}
}, null);
if (!success) {
throw new RuntimeException("getStats() return false!");
}
videoView.postDelayed(runnableThis, 1000);
}
}, null);
if (!success) {
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,7 +438,9 @@ public class AppRTCDemoActivity extends Activity
@Override
public void onRemoteIceCandidate(final IceCandidate candidate) {
pc.addRemoteIceCandidate(candidate);
if (pc != null) {
pc.addRemoteIceCandidate(candidate);
}
}
@Override
@ -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) {
logAndToast("Sending " + sdp.type + " ...");
appRtcClient.sendLocalDescription(sdp);
if (appRtcClient != null) {
logAndToast("Sending " + sdp.type + " ...");
appRtcClient.sendLocalDescription(sdp);
}
}
@Override
public void onIceCandidate(final IceCandidate candidate) {
appRtcClient.sendLocalIceCandidate(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,8 +163,100 @@ 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)) {
Uri url = Uri.parse(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()) {
handler.onOpen();
}
}
});
@JavascriptInterface
public void onOpen() {
if (!disconnected) {
handler.onOpen();
}
}
@JavascriptInterface public void onMessage(final String data) {
activity.runOnUiThread(new Runnable() {
public void run() {
if (!disconnected()) {
handler.onMessage(data);
}
}
});
@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()) {
handler.onClose();
}
}
});
@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()) {
handler.onError(code, description);
}
}
});
@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,42 +407,67 @@ 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() {
events.onChannelOpen();
}
private boolean channelOpen = false;
@JavascriptInterface public void onMessage(String msg) {
Log.d(TAG, "RECEIVE: " + msg);
try {
JSONObject json = new JSONObject(msg);
String type = (String) json.get("type");
if (type.equals("candidate")) {
IceCandidate candidate = new IceCandidate(
(String) json.get("id"),
json.getInt("label"),
(String) json.get("candidate"));
events.onRemoteIceCandidate(candidate);
} else if (type.equals("answer") || type.equals("offer")) {
SessionDescription sdp = new SessionDescription(
SessionDescription.Type.fromCanonicalForm(type),
(String)json.get("sdp"));
events.onRemoteDescription(sdp);
} else if (type.equals("bye")) {
events.onChannelClose();
} else {
throw new RuntimeException("Unexpected message: " + msg);
public void onOpen() {
activity.runOnUiThread(new Runnable() {
public void run() {
events.onChannelOpen();
channelOpen = true;
}
} catch (JSONException e) {
throw new RuntimeException(e);
}
});
}
@JavascriptInterface public void onClose() {
events.onChannelClose();
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");
if (type.equals("candidate")) {
IceCandidate candidate = new IceCandidate(
(String) json.get("id"),
json.getInt("label"),
(String) json.get("candidate"));
events.onRemoteIceCandidate(candidate);
} else if (type.equals("answer") || type.equals("offer")) {
SessionDescription sdp = new SessionDescription(
SessionDescription.Type.fromCanonicalForm(type),
(String)json.get("sdp"));
events.onRemoteDescription(sdp);
} else if (type.equals("bye")) {
events.onChannelClose();
} else {
events.onChannelError(1, "Unexpected channel message: " + msg);
}
} catch (JSONException e) {
events.onChannelError(1, "Channel message JSON parsing error: " +
e.toString());
}
}
});
}
@JavascriptInterface public void onError(int code, String description) {
events.onChannelError(code, description);
public void onClose() {
activity.runOnUiThread(new Runnable() {
public void run() {
events.onChannelClose();
channelOpen = false;
}
});
}
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() {
isInitiator = true;
pc.createOffer(sdpObserver, sdpMediaConstraints);
activity.runOnUiThread(new Runnable() {
public void run() {
if (pc != null) {
isInitiator = true;
pc.createOffer(sdpObserver, sdpMediaConstraints);
}
}
});
}
public void createAnswer() {
isInitiator = false;
pc.createAnswer(sdpObserver, sdpMediaConstraints);
activity.runOnUiThread(new Runnable() {
public void run() {
if (pc != null) {
isInitiator = false;
pc.createAnswer(sdpObserver, sdpMediaConstraints);
}
}
});
}
public void addRemoteIceCandidate(IceCandidate candidate) {
if (queuedRemoteCandidates != null) {
queuedRemoteCandidates.add(candidate);
} else {
pc.addIceCandidate(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) {
SessionDescription sdpISAC = new SessionDescription(
sdp.type, preferISAC(sdp.description));
Log.d(TAG, "Set remote SDP");
pc.setRemoteDescription(sdpObserver, sdpISAC);
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,24 +214,30 @@ public class PeerConnectionClient {
public void startVideoSource() {
if (videoSource != null && videoSourceStopped) {
Log.d(TAG, "Restart video source.");
videoSource.restart();
videoSourceStopped = false;
}
}
public void close() {
if (pc != null) {
pc.dispose();
pc = null;
}
if (videoSource != null) {
videoSource.dispose();
videoSource = null;
}
if (factory != null) {
factory.dispose();
factory = null;
}
activity.runOnUiThread(new Runnable() {
public void run() {
Log.d(TAG, "Closing peer connection.");
if (pc != null) {
pc.dispose();
pc = null;
}
if (videoSource != null) {
videoSource.dispose();
videoSource = null;
}
if (factory != null) {
factory.dispose();
factory = null;
}
}
});
}
/**
@ -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,29 +303,30 @@ public class PeerConnectionClient {
}
}
}
throw new RuntimeException("Failed to open capturer");
reportError("Failed to open capturer");
return null;
}
private VideoTrack createVideoTrack(boolean frontFacing) {
VideoCapturer capturer = getVideoCapturer(frontFacing);
if (videoSource != null) {
videoSource.stop();
videoSource.dispose();
}
VideoCapturer capturer = getVideoCapturer(frontFacing);
if (videoSource != null) {
videoSource.stop();
videoSource.dispose();
}
videoSource = factory.createVideoSource(
capturer, videoConstraints);
String trackExtension = frontFacing ? "frontFacing" : "backFacing";
VideoTrack videoTrack =
factory.createVideoTrack("ARDAMSv0" + trackExtension, videoSource);
videoTrack.addRenderer(new VideoRenderer(localRender));
return videoTrack;
videoSource = factory.createVideoSource(
capturer, videoConstraints);
String trackExtension = frontFacing ? "frontFacing" : "backFacing";
VideoTrack videoTrack =
factory.createVideoTrack("ARDAMSv0" + trackExtension, videoSource);
videoTrack.addRenderer(new VideoRenderer(localRender));
return videoTrack;
}
// 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);
}
}
@ -355,19 +434,15 @@ public class PeerConnectionClient {
@Override
public void onIceCandidate(final IceCandidate candidate){
activity.runOnUiThread(new Runnable() {
public void run() {
events.onIceCandidate(candidate);
}
});
public void run() {
events.onIceCandidate(candidate);
}
});
}
@Override
public void onError(){
activity.runOnUiThread(new Runnable() {
public void run() {
throw new RuntimeException("PeerConnection error!");
}
});
public void onError() {
reportError("PeerConnection error!");
}
@Override
@ -386,47 +461,50 @@ 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.");
}
}
@Override
public void onIceGatheringChange(
PeerConnection.IceGatheringState newState) {
PeerConnection.IceGatheringState newState) {
}
@Override
public void onAddStream(final MediaStream stream){
activity.runOnUiThread(new Runnable() {
public void run() {
abortUnless(stream.audioTracks.size() <= 1 &&
stream.videoTracks.size() <= 1,
"Weird-looking stream: " + stream);
if (stream.videoTracks.size() == 1) {
stream.videoTracks.get(0).addRenderer(
new VideoRenderer(remoteRender));
}
public void run() {
abortUnless(stream.audioTracks.size() <= 1 &&
stream.videoTracks.size() <= 1,
"Weird-looking stream: " + stream);
if (stream.videoTracks.size() == 1) {
stream.videoTracks.get(0).addRenderer(
new VideoRenderer(remoteRender));
}
});
}
});
}
@Override
public void onRemoveStream(final MediaStream stream){
activity.runOnUiThread(new Runnable() {
public void run() {
stream.videoTracks.get(0).dispose();
}
});
public void run() {
stream.videoTracks.get(0).dispose();
}
});
}
@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() +
" anyway!");
}
});
reportError("AppRTC doesn't use data channels, but got: " + dc.label() +
" anyway!");
}
@Override
@ -446,65 +524,62 @@ public class PeerConnectionClient {
origSdp.type, preferISAC(origSdp.description));
localSdp = sdp;
activity.runOnUiThread(new Runnable() {
public void run() {
public void run() {
if (pc != null) {
Log.d(TAG, "Set local SDP from " + sdp.type);
pc.setLocalDescription(sdpObserver, sdp);
}
});
}
});
}
@Override
public void onSetSuccess() {
activity.runOnUiThread(new Runnable() {
public void run() {
if (isInitiator) {
// For offering peer connection we first create offer and set
// local SDP, then after receiving answer set remote SDP.
if (pc.getRemoteDescription() == null) {
// We've just set our local SDP so time to send it.
Log.d(TAG, "Local SDP set succesfully");
events.onLocalDescription(localSdp);
} else {
// We've just set remote description,
// so drain remote ICE candidates.
Log.d(TAG, "Remote SDP set succesfully");
drainRemoteCandidates();
}
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.
if (pc.getRemoteDescription() == null) {
// We've just set our local SDP so time to send it.
Log.d(TAG, "Local SDP set succesfully");
events.onLocalDescription(localSdp);
} else {
// For answering peer connection we set remote SDP and then
// create answer and set local SDP.
if (pc.getLocalDescription() != null) {
// We've just set our local SDP so time to send it and drain
// remote ICE candidates.
Log.d(TAG, "Local SDP set succesfully");
events.onLocalDescription(localSdp);
drainRemoteCandidates();
} else {
// We've just set remote SDP - do nothing for now -
// answer will be created soon.
Log.d(TAG, "Remote SDP set succesfully");
}
// We've just set remote description,
// so drain remote ICE candidates.
Log.d(TAG, "Remote SDP set succesfully");
drainRemoteCandidates();
}
} else {
// For answering peer connection we set remote SDP and then
// create answer and set local SDP.
if (pc.getLocalDescription() != null) {
// We've just set our local SDP so time to send it and drain
// remote ICE candidates.
Log.d(TAG, "Local SDP set succesfully");
events.onLocalDescription(localSdp);
drainRemoteCandidates();
} else {
// We've just set remote SDP - do nothing for now -
// answer will be created soon.
Log.d(TAG, "Remote SDP set succesfully");
}
}
});
}
});
}
@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',