Add support for swapping feeds in Android AppRTCMobile.

BUG=webrtc:6937

Review-Url: https://codereview.webrtc.org/2682943006
Cr-Commit-Position: refs/heads/master@{#16582}
This commit is contained in:
sakal
2017-02-13 05:58:27 -08:00
committed by Commit bot
parent f6ddbe77cb
commit 07a050f995
5 changed files with 83 additions and 183 deletions

View File

@ -64,6 +64,8 @@ if (is_android) {
} }
android_library("AppRTCMobile_javalib") { android_library("AppRTCMobile_javalib") {
android_manifest = "androidapp/AndroidManifest.xml"
java_files = [ java_files = [
"androidapp/src/org/appspot/apprtc/AppRTCAudioManager.java", "androidapp/src/org/appspot/apprtc/AppRTCAudioManager.java",
"androidapp/src/org/appspot/apprtc/AppRTCBluetoothManager.java", "androidapp/src/org/appspot/apprtc/AppRTCBluetoothManager.java",
@ -77,7 +79,6 @@ if (is_android) {
"androidapp/src/org/appspot/apprtc/DirectRTCClient.java", "androidapp/src/org/appspot/apprtc/DirectRTCClient.java",
"androidapp/src/org/appspot/apprtc/HudFragment.java", "androidapp/src/org/appspot/apprtc/HudFragment.java",
"androidapp/src/org/appspot/apprtc/PeerConnectionClient.java", "androidapp/src/org/appspot/apprtc/PeerConnectionClient.java",
"androidapp/src/org/appspot/apprtc/PercentFrameLayout.java",
"androidapp/src/org/appspot/apprtc/RoomParametersFetcher.java", "androidapp/src/org/appspot/apprtc/RoomParametersFetcher.java",
"androidapp/src/org/appspot/apprtc/SettingsActivity.java", "androidapp/src/org/appspot/apprtc/SettingsActivity.java",
"androidapp/src/org/appspot/apprtc/SettingsFragment.java", "androidapp/src/org/appspot/apprtc/SettingsFragment.java",

View File

@ -19,10 +19,13 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT" /> <uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT" />
<!-- This is a test application that should always be debuggable. -->
<!--suppress HardcodedDebugMode -->
<application android:label="@string/app_name" <application android:label="@string/app_name"
android:icon="@drawable/ic_launcher" android:icon="@drawable/ic_launcher"
android:allowBackup="false" android:allowBackup="false"
android:debuggable="true"> android:debuggable="true"
android:supportsRtl="false">
<activity android:name="ConnectActivity" <activity android:name="ConnectActivity"
android:label="@string/app_name" android:label="@string/app_name"

View File

@ -1,29 +1,18 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout <merge xmlns:android="http://schemas.android.com/apk/res/android">
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<org.appspot.apprtc.PercentFrameLayout
android:id="@+id/remote_video_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<org.webrtc.SurfaceViewRenderer <org.webrtc.SurfaceViewRenderer
android:id="@+id/remote_video_view" android:id="@+id/fullscreen_video_view"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" /> android:layout_height="match_parent" />
</org.appspot.apprtc.PercentFrameLayout>
<org.appspot.apprtc.PercentFrameLayout
android:id="@+id/local_video_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<org.webrtc.SurfaceViewRenderer <org.webrtc.SurfaceViewRenderer
android:id="@+id/local_video_view" android:id="@+id/pip_video_view"
android:layout_height="144dp"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" /> android:layout_gravity="bottom|end"
</org.appspot.apprtc.PercentFrameLayout> android:layout_margin="16dp"/>
<FrameLayout <FrameLayout
android:id="@+id/call_fragment_container" android:id="@+id/call_fragment_container"
@ -34,4 +23,4 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent" />
</RelativeLayout> </merge>

View File

@ -134,34 +134,37 @@ public class CallActivity extends Activity implements AppRTCClient.SignalingEven
// Peer connection statistics callback period in ms. // Peer connection statistics callback period in ms.
private static final int STAT_CALLBACK_PERIOD = 1000; private static final int STAT_CALLBACK_PERIOD = 1000;
// Local preview screen position before call is connected.
private static final int LOCAL_X_CONNECTING = 0; private class ProxyRenderer implements VideoRenderer.Callbacks {
private static final int LOCAL_Y_CONNECTING = 0; private VideoRenderer.Callbacks target;
private static final int LOCAL_WIDTH_CONNECTING = 100;
private static final int LOCAL_HEIGHT_CONNECTING = 100; synchronized public void renderFrame(VideoRenderer.I420Frame frame) {
// Local preview screen position after call is connected. if (target == null) {
private static final int LOCAL_X_CONNECTED = 72; Logging.d(TAG, "Dropping frame in proxy because target is null.");
private static final int LOCAL_Y_CONNECTED = 72; VideoRenderer.renderFrameDone(frame);
private static final int LOCAL_WIDTH_CONNECTED = 25; return;
private static final int LOCAL_HEIGHT_CONNECTED = 25; }
// Remote video screen position
private static final int REMOTE_X = 0; target.renderFrame(frame);
private static final int REMOTE_Y = 0; }
private static final int REMOTE_WIDTH = 100;
private static final int REMOTE_HEIGHT = 100; synchronized public void setTarget(VideoRenderer.Callbacks target) {
this.target = target;
}
}
private final ProxyRenderer remoteProxyRenderer = new ProxyRenderer();
private final ProxyRenderer localProxyRenderer = new ProxyRenderer();
private PeerConnectionClient peerConnectionClient = null; private PeerConnectionClient peerConnectionClient = null;
private AppRTCClient appRtcClient; private AppRTCClient appRtcClient;
private SignalingParameters signalingParameters; private SignalingParameters signalingParameters;
private AppRTCAudioManager audioManager = null; private AppRTCAudioManager audioManager = null;
private EglBase rootEglBase; private EglBase rootEglBase;
private SurfaceViewRenderer localRender; private SurfaceViewRenderer pipRenderer;
private SurfaceViewRenderer remoteRenderScreen; private SurfaceViewRenderer fullscreenRenderer;
private VideoFileRenderer videoFileRenderer; private VideoFileRenderer videoFileRenderer;
private final List<VideoRenderer.Callbacks> remoteRenderers = private final List<VideoRenderer.Callbacks> remoteRenderers =
new ArrayList<VideoRenderer.Callbacks>(); new ArrayList<VideoRenderer.Callbacks>();
private PercentFrameLayout localRenderLayout;
private PercentFrameLayout remoteRenderLayout;
private ScalingType scalingType;
private Toast logToast; private Toast logToast;
private boolean commandLineRun; private boolean commandLineRun;
private int runTimeMs; private int runTimeMs;
@ -176,6 +179,8 @@ public class CallActivity extends Activity implements AppRTCClient.SignalingEven
private boolean screencaptureEnabled = false; private boolean screencaptureEnabled = false;
private static Intent mediaProjectionPermissionResultData; private static Intent mediaProjectionPermissionResultData;
private static int mediaProjectionPermissionResultCode; private static int mediaProjectionPermissionResultCode;
// True if local view is in the fullscreen renderer.
private boolean isSwappedFeeds;
// Controls // Controls
private CallFragment callFragment; private CallFragment callFragment;
@ -198,13 +203,10 @@ public class CallActivity extends Activity implements AppRTCClient.SignalingEven
iceConnected = false; iceConnected = false;
signalingParameters = null; signalingParameters = null;
scalingType = ScalingType.SCALE_ASPECT_FILL;
// Create UI controls. // Create UI controls.
localRender = (SurfaceViewRenderer) findViewById(R.id.local_video_view); pipRenderer = (SurfaceViewRenderer) findViewById(R.id.pip_video_view);
remoteRenderScreen = (SurfaceViewRenderer) findViewById(R.id.remote_video_view); fullscreenRenderer = (SurfaceViewRenderer) findViewById(R.id.fullscreen_video_view);
localRenderLayout = (PercentFrameLayout) findViewById(R.id.local_video_layout);
remoteRenderLayout = (PercentFrameLayout) findViewById(R.id.remote_video_layout);
callFragment = new CallFragment(); callFragment = new CallFragment();
hudFragment = new HudFragment(); hudFragment = new HudFragment();
@ -216,15 +218,23 @@ public class CallActivity extends Activity implements AppRTCClient.SignalingEven
} }
}; };
localRender.setOnClickListener(listener); // Swap feeds on pip view click.
remoteRenderScreen.setOnClickListener(listener); pipRenderer.setOnClickListener(new View.OnClickListener() {
remoteRenderers.add(remoteRenderScreen); @Override
public void onClick(View view) {
setSwappedFeeds(!isSwappedFeeds);
}
});
fullscreenRenderer.setOnClickListener(listener);
remoteRenderers.add(remoteProxyRenderer);
final Intent intent = getIntent(); final Intent intent = getIntent();
// Create video renderers. // Create video renderers.
rootEglBase = EglBase.create(); rootEglBase = EglBase.create();
localRender.init(rootEglBase.getEglBaseContext(), null); pipRenderer.init(rootEglBase.getEglBaseContext(), null);
pipRenderer.setScalingType(ScalingType.SCALE_ASPECT_FIT);
String saveRemoteVideoToFile = intent.getStringExtra(EXTRA_SAVE_REMOTE_VIDEO_TO_FILE); String saveRemoteVideoToFile = intent.getStringExtra(EXTRA_SAVE_REMOTE_VIDEO_TO_FILE);
// When saveRemoteVideoToFile is set we save the video from the remote to a file. // When saveRemoteVideoToFile is set we save the video from the remote to a file.
@ -240,12 +250,14 @@ public class CallActivity extends Activity implements AppRTCClient.SignalingEven
"Failed to open video file for output: " + saveRemoteVideoToFile, e); "Failed to open video file for output: " + saveRemoteVideoToFile, e);
} }
} }
remoteRenderScreen.init(rootEglBase.getEglBaseContext(), null); fullscreenRenderer.init(rootEglBase.getEglBaseContext(), null);
fullscreenRenderer.setScalingType(ScalingType.SCALE_ASPECT_FILL);
localRender.setZOrderMediaOverlay(true); pipRenderer.setZOrderMediaOverlay(true);
localRender.setEnableHardwareScaler(true /* enabled */); pipRenderer.setEnableHardwareScaler(true /* enabled */);
remoteRenderScreen.setEnableHardwareScaler(true /* enabled */); fullscreenRenderer.setEnableHardwareScaler(true /* enabled */);
updateVideoView(); // Start with local feed in fullscreen and swap it to the pip when the call is connected.
setSwappedFeeds(true /* isSwappedFeeds */);
// Check for mandatory permissions. // Check for mandatory permissions.
for (String permission : MANDATORY_PERMISSIONS) { for (String permission : MANDATORY_PERMISSIONS) {
@ -508,8 +520,7 @@ public class CallActivity extends Activity implements AppRTCClient.SignalingEven
@Override @Override
public void onVideoScalingSwitch(ScalingType scalingType) { public void onVideoScalingSwitch(ScalingType scalingType) {
this.scalingType = scalingType; fullscreenRenderer.setScalingType(scalingType);
updateVideoView();
} }
@Override @Override
@ -547,26 +558,6 @@ public class CallActivity extends Activity implements AppRTCClient.SignalingEven
ft.commit(); ft.commit();
} }
private void updateVideoView() {
remoteRenderLayout.setPosition(REMOTE_X, REMOTE_Y, REMOTE_WIDTH, REMOTE_HEIGHT);
remoteRenderScreen.setScalingType(scalingType);
remoteRenderScreen.setMirror(false);
if (iceConnected) {
localRenderLayout.setPosition(
LOCAL_X_CONNECTED, LOCAL_Y_CONNECTED, LOCAL_WIDTH_CONNECTED, LOCAL_HEIGHT_CONNECTED);
localRender.setScalingType(ScalingType.SCALE_ASPECT_FIT);
} else {
localRenderLayout.setPosition(
LOCAL_X_CONNECTING, LOCAL_Y_CONNECTING, LOCAL_WIDTH_CONNECTING, LOCAL_HEIGHT_CONNECTING);
localRender.setScalingType(scalingType);
}
localRender.setMirror(true);
localRender.requestLayout();
remoteRenderScreen.requestLayout();
}
private void startCall() { private void startCall() {
if (appRtcClient == null) { if (appRtcClient == null) {
Log.e(TAG, "AppRTC client is not allocated for a call."); Log.e(TAG, "AppRTC client is not allocated for a call.");
@ -603,10 +594,9 @@ public class CallActivity extends Activity implements AppRTCClient.SignalingEven
Log.w(TAG, "Call is connected in closed or error state"); Log.w(TAG, "Call is connected in closed or error state");
return; return;
} }
// Update video view.
updateVideoView();
// Enable statistics callback. // Enable statistics callback.
peerConnectionClient.enableStatsEvents(true, STAT_CALLBACK_PERIOD); peerConnectionClient.enableStatsEvents(true, STAT_CALLBACK_PERIOD);
setSwappedFeeds(false /* isSwappedFeeds */);
} }
// This method is called when the audio manager reports audio device change, // This method is called when the audio manager reports audio device change,
@ -621,6 +611,8 @@ public class CallActivity extends Activity implements AppRTCClient.SignalingEven
// Disconnect from remote resources, dispose of local resources, and exit. // Disconnect from remote resources, dispose of local resources, and exit.
private void disconnect() { private void disconnect() {
activityRunning = false; activityRunning = false;
remoteProxyRenderer.setTarget(null);
localProxyRenderer.setTarget(null);
if (appRtcClient != null) { if (appRtcClient != null) {
appRtcClient.disconnectFromRoom(); appRtcClient.disconnectFromRoom();
appRtcClient = null; appRtcClient = null;
@ -629,17 +621,17 @@ public class CallActivity extends Activity implements AppRTCClient.SignalingEven
peerConnectionClient.close(); peerConnectionClient.close();
peerConnectionClient = null; peerConnectionClient = null;
} }
if (localRender != null) { if (pipRenderer != null) {
localRender.release(); pipRenderer.release();
localRender = null; pipRenderer = null;
} }
if (videoFileRenderer != null) { if (videoFileRenderer != null) {
videoFileRenderer.release(); videoFileRenderer.release();
videoFileRenderer = null; videoFileRenderer = null;
} }
if (remoteRenderScreen != null) { if (fullscreenRenderer != null) {
remoteRenderScreen.release(); fullscreenRenderer.release();
remoteRenderScreen = null; fullscreenRenderer = null;
} }
if (audioManager != null) { if (audioManager != null) {
audioManager.stop(); audioManager.stop();
@ -728,6 +720,15 @@ public class CallActivity extends Activity implements AppRTCClient.SignalingEven
return videoCapturer; return videoCapturer;
} }
private void setSwappedFeeds(boolean isSwappedFeeds) {
Logging.d(TAG, "setSwappedFeeds: " + isSwappedFeeds);
this.isSwappedFeeds = isSwappedFeeds;
localProxyRenderer.setTarget(isSwappedFeeds ? fullscreenRenderer : pipRenderer);
remoteProxyRenderer.setTarget(isSwappedFeeds ? pipRenderer : fullscreenRenderer);
fullscreenRenderer.setMirror(isSwappedFeeds);
pipRenderer.setMirror(!isSwappedFeeds);
}
// -----Implementation of AppRTCClient.AppRTCSignalingEvents --------------- // -----Implementation of AppRTCClient.AppRTCSignalingEvents ---------------
// All callbacks are invoked from websocket signaling looper thread and // All callbacks are invoked from websocket signaling looper thread and
// are routed to UI thread. // are routed to UI thread.
@ -740,7 +741,7 @@ public class CallActivity extends Activity implements AppRTCClient.SignalingEven
if (peerConnectionParameters.videoCallEnabled) { if (peerConnectionParameters.videoCallEnabled) {
videoCapturer = createVideoCapturer(); videoCapturer = createVideoCapturer();
} }
peerConnectionClient.createPeerConnection(rootEglBase.getEglBaseContext(), localRender, peerConnectionClient.createPeerConnection(rootEglBase.getEglBaseContext(), localProxyRenderer,
remoteRenderers, videoCapturer, signalingParameters); remoteRenderers, videoCapturer, signalingParameters);
if (signalingParameters.initiator) { if (signalingParameters.initiator) {

View File

@ -1,94 +0,0 @@
/*
* Copyright 2015 The WebRTC Project Authors. All rights reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree. An additional intellectual property rights grant can be found
* in the file PATENTS. All contributing project authors may
* be found in the AUTHORS file in the root of the source tree.
*/
package org.appspot.apprtc;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
/**
* Simple container that confines the children to a subrectangle specified as percentage values of
* the container size. The children are centered horizontally and vertically inside the confined
* space.
*/
public class PercentFrameLayout extends ViewGroup {
private int xPercent = 0;
private int yPercent = 0;
private int widthPercent = 100;
private int heightPercent = 100;
public PercentFrameLayout(Context context) {
super(context);
}
public PercentFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public PercentFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setPosition(int xPercent, int yPercent, int widthPercent, int heightPercent) {
this.xPercent = xPercent;
this.yPercent = yPercent;
this.widthPercent = widthPercent;
this.heightPercent = heightPercent;
}
@Override
public boolean shouldDelayChildPressedState() {
return false;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int width = getDefaultSize(Integer.MAX_VALUE, widthMeasureSpec);
final int height = getDefaultSize(Integer.MAX_VALUE, heightMeasureSpec);
setMeasuredDimension(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
final int childWidthMeasureSpec =
MeasureSpec.makeMeasureSpec(width * widthPercent / 100, MeasureSpec.AT_MOST);
final int childHeightMeasureSpec =
MeasureSpec.makeMeasureSpec(height * heightPercent / 100, MeasureSpec.AT_MOST);
for (int i = 0; i < getChildCount(); ++i) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
final int width = right - left;
final int height = bottom - top;
// Sub-rectangle specified by percentage values.
final int subWidth = width * widthPercent / 100;
final int subHeight = height * heightPercent / 100;
final int subLeft = left + width * xPercent / 100;
final int subTop = top + height * yPercent / 100;
for (int i = 0; i < getChildCount(); ++i) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
// Center child both vertically and horizontally.
final int childLeft = subLeft + (subWidth - childWidth) / 2;
final int childTop = subTop + (subHeight - childHeight) / 2;
child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
}
}
}
}