blob: 3ad26afcae50fb83d6c294d9620b99b2151a1e95 [file] [log] [blame]
/*
* libjingle
* Copyright 2013, Google Inc.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. The name of the author may not be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.appspot.apprtc;
import android.app.Activity;
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.net.Uri;
import android.opengl.GLSurfaceView;
import android.os.Bundle;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.Window;
import android.view.WindowManager;
import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
import org.appspot.apprtc.AppRTCClient.AppRTCSignalingParameters;
import org.webrtc.IceCandidate;
import org.webrtc.PeerConnectionFactory;
import org.webrtc.SessionDescription;
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
* between the Android/Java implementation of PeerConnection and the
* apprtc.appspot.com demo webapp.
*/
public class AppRTCDemoActivity extends Activity
implements AppRTCClient.AppRTCSignalingEvents,
PeerConnectionClient.PeerConnectionEvents {
private static final String TAG = "AppRTCClient";
private PeerConnectionClient pc;
private AppRTCClient appRtcClient = new GAERTCClient(this, this);
private AppRTCSignalingParameters appRtcParameters;
private AppRTCAudioManager audioManager = null;
private View rootView;
private View menuBar;
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;
private ImageButton videoScalingButton;
private boolean iceConnected;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Set window styles for fullscreen-window size. Needs to be done before
// adding content.
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_FULLSCREEN |
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
setContentView(R.layout.activity_fullscreen);
Thread.setDefaultUncaughtExceptionHandler(
new UnhandledExceptionHandler(this));
iceConnected = false;
rootView = findViewById(android.R.id.content);
menuBar = findViewById(R.id.menubar_fragment);
roomName = (TextView) findViewById(R.id.room_name);
videoView = (GLSurfaceView) findViewById(R.id.glview);
VideoRendererGui.setView(videoView);
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() {
@Override
public void onClick(View view) {
int visibility = menuBar.getVisibility() == View.VISIBLE
? View.INVISIBLE : View.VISIBLE;
menuBar.setVisibility(visibility);
roomName.setVisibility(visibility);
if (visibility == View.VISIBLE) {
menuBar.bringToFront();
roomName.bringToFront();
rootView.invalidate();
}
}
});
((ImageButton) findViewById(R.id.button_disconnect)).setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View view) {
logAndToast("Disconnecting call.");
disconnect();
}
});
((ImageButton) findViewById(R.id.button_switch_camera)).setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View view) {
if (pc != null) {
pc.switchCamera();
}
}
});
((ImageButton) findViewById(R.id.button_toggle_debug)).setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View view) {
int visibility = hudView.getVisibility() == View.VISIBLE
? View.INVISIBLE : View.VISIBLE;
hudView.setVisibility(visibility);
}
});
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);
hudView.setAlpha(0.4f);
hudView.setTextSize(TypedValue.COMPLEX_UNIT_PT, 5);
hudView.setVisibility(View.INVISIBLE);
addContentView(hudView, hudLayout);
// Create and audio manager that will take care of audio routing,
// audio modes, audio device enumeration etc.
audioManager = AppRTCAudioManager.create(this);
final Intent intent = getIntent();
Uri url = intent.getData();
if (url != null) {
String room = url.getQueryParameter("r");
String loopback = url.getQueryParameter("debug");
if ((room != null && !room.equals("")) ||
(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();
}
} else {
logAndToast(getString(R.string.missing_url));
Log.wtf(TAG, "Didn't get any URL in intent!");
finish();
}
}
public static class MenuBarFragment extends Fragment {
@Override
public View onCreateView(
LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_menubar, container, false);
}
}
@Override
public void onPause() {
super.onPause();
videoView.onPause();
if (pc != null) {
pc.stopVideoSource();
}
}
@Override
public void onResume() {
super.onResume();
videoView.onResume();
if (pc != null) {
pc.startVideoSource();
}
}
@Override
protected void onDestroy() {
disconnect();
if (audioManager != null) {
audioManager.close();
audioManager = null;
}
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();
for (StatsReport report : reports) {
// bweforvideo to show statistics for video Bandwidth Estimation,
// which is global per-session.
if (report.id.equals("bweforvideo")) {
for (StatsReport.Value value : report.values) {
String name = value.name.replace("goog", "")
.replace("Available", "").replace("Bandwidth", "")
.replace("Bitrate", "").replace("Enc", "");
builder.append(name).append("=").append(value.value)
.append(" ");
}
builder.append("\n");
} else if (report.type.equals("googCandidatePair")) {
String activeConnectionStats = getActiveConnectionStats(report);
if (activeConnectionStats == null) {
continue;
}
builder.append(activeConnectionStats);
} else {
continue;
}
builder.append("\n");
}
hudView.setText(builder.toString() + hudView.getText());
}
// Return the active connection stats else return null
private String getActiveConnectionStats(StatsReport report) {
StringBuilder activeConnectionbuilder = new StringBuilder();
// googCandidatePair to show information about the active
// connection.
for (StatsReport.Value value : report.values) {
if (value.name.equals("googActiveConnection")
&& value.value.equals("false")) {
return null;
}
String name = value.name.replace("goog", "");
activeConnectionbuilder.append(name).append("=")
.append(value.value).append("\n");
}
return activeConnectionbuilder.toString();
}
// Disconnect from remote resources, dispose of local resources, and exit.
private void disconnect() {
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.
private static void abortUnless(boolean condition, String msg) {
if (!condition) {
throw new RuntimeException(msg);
}
}
// Log |msg| and Toast about it.
private void logAndToast(String msg) {
Log.d(TAG, msg);
if (logToast != null) {
logToast.cancel();
}
logToast = Toast.makeText(this, msg, Toast.LENGTH_SHORT);
logToast.show();
}
// -----Implementation of AppRTCClient.AppRTCSignalingEvents ---------------
// All events are called from UI thread.
@Override
public void onConnectedToRoom(final AppRTCSignalingParameters params) {
if (audioManager != null) {
// Store existing audio settings and change audio mode to
// MODE_IN_COMMUNICATION for best possible VoIP performance.
logAndToast("Initializing the audio manager...");
audioManager.init();
}
appRtcParameters = params;
abortUnless(PeerConnectionFactory.initializeAndroidGlobals(
this, true, true, VideoRendererGui.getEGLContext()),
"Failed to initializeAndroidGlobals");
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 Runnable repeatedStatsLogger = new Runnable() {
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());
}
videoView.postDelayed(runnableThis, 1000);
}
}, null);
if (!success) {
throw new RuntimeException("getStats() return false!");
}
}
};
videoView.postDelayed(repeatedStatsLogger, 1000);
}
logAndToast("Waiting for remote connection...");
}
@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
// PeerConnectionEvents.onLocalDescription event.
pc.createOffer();
}
}
@Override
public void onRemoteDescription(final SessionDescription sdp) {
if (pc == null) {
return;
}
logAndToast("Received remote " + sdp.type + " ...");
pc.setRemoteDescription(sdp);
if (!appRtcParameters.initiator) {
logAndToast("Creating ANSWER...");
// Create answer. Answer SDP will be sent to offering client in
// PeerConnectionEvents.onLocalDescription event.
pc.createAnswer();
}
}
@Override
public void onRemoteIceCandidate(final IceCandidate candidate) {
if (pc != null) {
pc.addRemoteIceCandidate(candidate);
}
}
@Override
public void onChannelClose() {
logAndToast("Remote end hung up; dropping PeerConnection");
disconnect();
}
@Override
public void onChannelError(int code, String description) {
disconnectWithMessage(description);
}
// -----Implementation of PeerConnectionClient.PeerConnectionEvents.---------
// Send local peer connection SDP and ICE candidates to remote party.
// 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");
iceConnected = true;
updateVideoView();
}
@Override
public void onIceDisconnected() {
logAndToast("ICE disconnected");
disconnect();
}
@Override
public void onPeerConnectionError(String description) {
disconnectWithMessage(description);
}
}