diff --git a/AndroidManifest.xml b/AndroidManifest.xml index eed7b299..5099010a 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,8 +1,8 @@  + android:versionCode="26" + android:versionName="2.5.1" > ?[package:][type:]na public static final int discoveryText=0x7f08000b; public static final int hardwareDec=0x7f080005; public static final int hostTextView=0x7f080000; - public static final int manuallyAddPc=0x7f080013; + public static final int manuallyAddPc=0x7f080014; public static final int pcListView=0x7f080008; - public static final int rowTextView=0x7f080014; + public static final int rowTextView=0x7f080015; public static final int settingsButton=0x7f08000c; public static final int softwareDec=0x7f080003; public static final int streamConfigGroup=0x7f08000d; + public static final int stretchToFill=0x7f080013; public static final int surfaceView=0x7f08000a; } public static final class layout { diff --git a/libs/limelight-common.jar b/libs/limelight-common.jar index a27ca0df..92d35157 100644 Binary files a/libs/limelight-common.jar and b/libs/limelight-common.jar differ diff --git a/res/layout/activity_game.xml b/res/layout/activity_game.xml index 396013e8..c64a8ab3 100644 --- a/res/layout/activity_game.xml +++ b/res/layout/activity_game.xml @@ -2,12 +2,13 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="#0099cc" + android:background="#000" tools:context=".Game" > + android:layout_height="match_parent" + android:layout_gravity="center" /> diff --git a/res/layout/activity_stream_settings.xml b/res/layout/activity_stream_settings.xml index 8847b66a..0f69e4f9 100644 --- a/res/layout/activity_stream_settings.xml +++ b/res/layout/activity_stream_settings.xml @@ -54,7 +54,7 @@ android:id="@+id/advancedSettingsButton" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_below="@+id/streamConfigGroup" + android:layout_below="@+id/stretchToFill" android:layout_centerHorizontal="true" android:layout_marginTop="15dp" android:text="Advanced Settings" /> @@ -67,6 +67,14 @@ android:layout_centerHorizontal="true" android:text="Add PC Manually" /> + + diff --git a/src/com/limelight/AppView.java b/src/com/limelight/AppView.java index 622b2cbe..bce04f30 100644 --- a/src/com/limelight/AppView.java +++ b/src/com/limelight/AppView.java @@ -94,8 +94,8 @@ public class AppView extends Activity { } @Override - protected void onStop() { - super.onStop(); + protected void onDestroy() { + super.onDestroy(); Dialog.closeDialogs(); SpinnerDialog.closeDialogs(); diff --git a/src/com/limelight/Game.java b/src/com/limelight/Game.java index 5c7e7cf4..1fc7c9b1 100644 --- a/src/com/limelight/Game.java +++ b/src/com/limelight/Game.java @@ -32,6 +32,7 @@ import android.view.SurfaceView; import android.view.View; import android.view.View.OnGenericMotionListener; import android.view.View.OnTouchListener; +import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; import android.widget.Toast; @@ -58,6 +59,8 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM private boolean connecting = false; private boolean connected = false; + private boolean stretchToFit; + private ConfigurableDecoderRenderer decoderRenderer; private WifiManager.WifiLock wifiLock; @@ -76,6 +79,7 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM public static final String REFRESH_RATE_PREF_STRING = "FPS"; public static final String DECODER_PREF_STRING = "Decoder"; public static final String BITRATE_PREF_STRING = "Bitrate"; + public static final String STRETCH_PREF_STRING = "Stretch"; public static final int BITRATE_DEFAULT_720_30 = 5; public static final int BITRATE_DEFAULT_720_60 = 10; @@ -87,6 +91,7 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM public static final int DEFAULT_REFRESH_RATE = 60; public static final int DEFAULT_DECODER = 0; public static final int DEFAULT_BITRATE = BITRATE_DEFAULT_720_60; + public static final boolean DEFAULT_STRETCH = false; public static final int FORCE_HARDWARE_DECODER = -1; public static final int AUTOSELECT_DECODER = 0; @@ -121,13 +126,6 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM // Inflate the content setContentView(R.layout.activity_game); - // Listen for events on the game surface - SurfaceView sv = (SurfaceView) findViewById(R.id.surfaceView); - sv.setOnGenericMotionListener(this); - sv.setOnTouchListener(this); - - SurfaceHolder sh = sv.getHolder(); - // Start the spinner spinner = SpinnerDialog.displayDialog(this, "Establishing Connection", "Starting connection", true); @@ -143,16 +141,32 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM drFlags |= VideoDecoderRenderer.FLAG_FORCE_HARDWARE_DECODING; break; } + + stretchToFit = prefs.getBoolean(STRETCH_PREF_STRING, DEFAULT_STRETCH); + if (stretchToFit) { + drFlags |= VideoDecoderRenderer.FLAG_FILL_SCREEN; + } int refreshRate, bitrate; width = prefs.getInt(WIDTH_PREF_STRING, DEFAULT_WIDTH); height = prefs.getInt(HEIGHT_PREF_STRING, DEFAULT_HEIGHT); refreshRate = prefs.getInt(REFRESH_RATE_PREF_STRING, DEFAULT_REFRESH_RATE); bitrate = prefs.getInt(BITRATE_PREF_STRING, DEFAULT_BITRATE); - sh.setFixedSize(width, height); Display display = getWindowManager().getDefaultDisplay(); display.getSize(screenSize); + + // Listen for events on the game surface + SurfaceView sv = (SurfaceView) findViewById(R.id.surfaceView); + sv.setOnGenericMotionListener(this); + sv.setOnTouchListener(this); + + SurfaceHolder sh = sv.getHolder(); + + if (stretchToFit) { + // Set the surface to the size of the video + sh.setFixedSize(width, height); + } // Warn the user if they're on a metered connection checkDataConnection(); @@ -181,6 +195,21 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM sh.addCallback(this); } + private void resizeSurfaceWithAspectRatio(SurfaceView sv, double vidWidth, double vidHeight) + { + // Get the visible width of the activity + double visibleWidth = getWindow().getDecorView().getWidth(); + + ViewGroup.LayoutParams lp = sv.getLayoutParams(); + + // Calculate the new size of the SurfaceView + lp.width = (int) visibleWidth; + lp.height = (int) ((vidHeight / vidWidth) * visibleWidth); + + // Apply the size change + sv.setLayoutParams(lp); + } + private void checkDataConnection() { ConnectivityManager mgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); @@ -233,7 +262,7 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM int averageDecoderLat = decoderRenderer.getAverageDecoderLatency(); String message = null; if (averageEndToEndLat > 0) { - message = "Average total frame latency: "+averageEndToEndLat+" ms"; + message = "Average client-side frame latency: "+averageEndToEndLat+" ms"; if (averageDecoderLat > 0) { message += " (hardware decoder latency: "+averageDecoderLat+" ms)"; } @@ -270,10 +299,27 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM return modifier; } + private static boolean isSourceFlagSet(int sourcesFlags, int flag) { + return (sourcesFlags & flag) == flag; + } + @Override public boolean onKeyDown(int keyCode, KeyEvent event) { - if (event.getDevice() != null && - (event.getDevice().getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC)) { + InputDevice dev = event.getDevice(); + if (dev == null) { + return super.onKeyDown(keyCode, event); + } + + int source = dev.getSources(); + boolean handled = false; + if (isSourceFlagSet(source, InputDevice.SOURCE_DPAD) || + isSourceFlagSet(source, InputDevice.SOURCE_GAMEPAD) || + isSourceFlagSet(source, InputDevice.SOURCE_JOYSTICK)) + { + handled = controllerHandler.handleButtonDown(keyCode, event); + } + + if (!handled) { short translated = keybTranslator.translate(event.getKeyCode()); if (translated == 0) { return super.onKeyDown(keyCode, event); @@ -282,12 +328,7 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM keybTranslator.sendKeyDown(translated, getModifierState(event)); } - else { - if (!controllerHandler.handleButtonDown(keyCode, event)) { - return super.onKeyDown(keyCode, event); - } - } - + return true; } @@ -302,22 +343,30 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM h.postDelayed(hideSystemUi, 2000); } } - - if (event.getDevice() != null && - (event.getDevice().getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC)) { + + InputDevice dev = event.getDevice(); + if (dev == null) { + return super.onKeyUp(keyCode, event); + } + + int source = dev.getSources(); + boolean handled = false; + if (isSourceFlagSet(source, InputDevice.SOURCE_DPAD) || + isSourceFlagSet(source, InputDevice.SOURCE_GAMEPAD) || + isSourceFlagSet(source, InputDevice.SOURCE_JOYSTICK)) + { + handled = controllerHandler.handleButtonUp(keyCode, event); + } + + if (!handled) { short translated = keybTranslator.translate(event.getKeyCode()); if (translated == 0) { return super.onKeyUp(keyCode, event); } - + keybTranslator.sendKeyUp(translated, getModifierState(event)); } - else { - if (!controllerHandler.handleButtonUp(keyCode, event)) { - return super.onKeyUp(keyCode, event); - } - } return true; } @@ -562,6 +611,13 @@ public class Game extends Activity implements SurfaceHolder.Callback, OnGenericM public void surfaceCreated(SurfaceHolder holder) { if (!connected && !connecting) { connecting = true; + + // Resize the surface to match the aspect ratio of the video + // This must be done after the surface is created. + if (!stretchToFit) { + resizeSurfaceWithAspectRatio((SurfaceView) findViewById(R.id.surfaceView), width, height); + } + conn.start(PlatformBinding.getDeviceName(), holder, drFlags, PlatformBinding.getAudioRenderer(), decoderRenderer); } diff --git a/src/com/limelight/StreamSettings.java b/src/com/limelight/StreamSettings.java index 4ff25255..fb31b6b0 100644 --- a/src/com/limelight/StreamSettings.java +++ b/src/com/limelight/StreamSettings.java @@ -6,6 +6,7 @@ import android.os.Bundle; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; +import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.RadioButton; @@ -18,6 +19,7 @@ public class StreamSettings extends Activity { private Button advancedSettingsButton, addComputerButton; private SharedPreferences prefs; private RadioButton rbutton720p30, rbutton720p60, rbutton1080p30, rbutton1080p60; + private CheckBox stretchToFill; @Override protected void onStop() { @@ -32,6 +34,7 @@ public class StreamSettings extends Activity { setContentView(R.layout.activity_stream_settings); + this.stretchToFill = (CheckBox) findViewById(R.id.stretchToFill); this.advancedSettingsButton = (Button) findViewById(R.id.advancedSettingsButton); this.addComputerButton = (Button) findViewById(R.id.manuallyAddPc); this.rbutton720p30 = (RadioButton) findViewById(R.id.config720p30Selected); @@ -44,6 +47,8 @@ public class StreamSettings extends Activity { boolean res720p = prefs.getInt(Game.HEIGHT_PREF_STRING, Game.DEFAULT_HEIGHT) == 720; boolean fps30 = prefs.getInt(Game.REFRESH_RATE_PREF_STRING, Game.DEFAULT_REFRESH_RATE) == 30; + stretchToFill.setChecked(prefs.getBoolean(Game.STRETCH_PREF_STRING, Game.DEFAULT_STRETCH)); + rbutton720p30.setChecked(false); rbutton720p60.setChecked(false); rbutton1080p30.setChecked(false); @@ -119,5 +124,12 @@ public class StreamSettings extends Activity { startActivity(i); } }); + stretchToFill.setOnCheckedChangeListener(new OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, + boolean isChecked) { + prefs.edit().putBoolean(Game.STRETCH_PREF_STRING, isChecked).commit(); + } + }); } } diff --git a/src/com/limelight/binding/input/ControllerHandler.java b/src/com/limelight/binding/input/ControllerHandler.java index 789fe7be..3a85af60 100644 --- a/src/com/limelight/binding/input/ControllerHandler.java +++ b/src/com/limelight/binding/input/ControllerHandler.java @@ -148,6 +148,10 @@ public class ControllerHandler { } } + mapping.isDpad = (dev.getSources() & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD; + mapping.isGamepad = (dev.getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD || + (dev.getSources() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK; + return mapping; } @@ -177,7 +181,48 @@ public class ControllerHandler { leftStickX, leftStickY, rightStickX, rightStickY); } - private int handleRemapping(ControllerMapping mapping, int keyCode) { + private static boolean isEventExpected(ControllerMapping mapping, int keyCode) { + if (mapping.isDpad) { + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_LEFT: + case KeyEvent.KEYCODE_DPAD_RIGHT: + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_DPAD_DOWN: + return true; + } + } + + if (mapping.isGamepad) { + switch (keyCode) { + case KeyEvent.KEYCODE_BUTTON_MODE: + case KeyEvent.KEYCODE_BUTTON_START: + case KeyEvent.KEYCODE_MENU: + case KeyEvent.KEYCODE_BACK: + case KeyEvent.KEYCODE_BUTTON_SELECT: + case KeyEvent.KEYCODE_DPAD_LEFT: + case KeyEvent.KEYCODE_DPAD_RIGHT: + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_DPAD_DOWN: + case KeyEvent.KEYCODE_BUTTON_B: + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_BUTTON_A: + case KeyEvent.KEYCODE_BUTTON_X: + case KeyEvent.KEYCODE_BUTTON_Y: + case KeyEvent.KEYCODE_BUTTON_L1: + case KeyEvent.KEYCODE_BUTTON_R1: + case KeyEvent.KEYCODE_BUTTON_THUMBL: + case KeyEvent.KEYCODE_BUTTON_THUMBR: + case KeyEvent.KEYCODE_BUTTON_L2: + case KeyEvent.KEYCODE_BUTTON_R2: + return true; + } + } + + return false; + } + + private static int handleRemapping(ControllerMapping mapping, int keyCode) { if (mapping.isDualShock4) { switch (keyCode) { case KeyEvent.KEYCODE_BUTTON_Y: @@ -317,6 +362,10 @@ public class ControllerHandler { return true; } + if (!isEventExpected(mapping, keyCode)) { + return false; + } + // If the button hasn't been down long enough, sleep for a bit before sending the up event // This allows "instant" button presses (like OUYA's virtual menu button) to work. This // path should not be triggered during normal usage. @@ -440,6 +489,10 @@ public class ControllerHandler { return true; } + if (!isEventExpected(mapping, keyCode)) { + return false; + } + switch (keyCode) { case KeyEvent.KEYCODE_BUTTON_MODE: inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG; @@ -549,5 +602,7 @@ public class ControllerHandler { public float hatYDeadzone; public boolean isDualShock4; + public boolean isDpad; + public boolean isGamepad; } } diff --git a/src/com/limelight/binding/video/AndroidCpuDecoderRenderer.java b/src/com/limelight/binding/video/AndroidCpuDecoderRenderer.java index 611c3555..7521ad0c 100644 --- a/src/com/limelight/binding/video/AndroidCpuDecoderRenderer.java +++ b/src/com/limelight/binding/video/AndroidCpuDecoderRenderer.java @@ -5,6 +5,7 @@ import java.io.File; import java.io.FileReader; import java.io.IOException; import java.nio.ByteBuffer; +import java.util.concurrent.locks.LockSupport; import android.graphics.PixelFormat; import android.os.Build; @@ -169,6 +170,7 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer { long diff = nextFrameTime - System.currentTimeMillis(); if (diff > WAIT_CEILING_MS) { + LockSupport.parkNanos(1); continue; } diff --git a/src/com/limelight/binding/video/MediaCodecDecoderRenderer.java b/src/com/limelight/binding/video/MediaCodecDecoderRenderer.java index d91c5509..8a180362 100644 --- a/src/com/limelight/binding/video/MediaCodecDecoderRenderer.java +++ b/src/com/limelight/binding/video/MediaCodecDecoderRenderer.java @@ -3,6 +3,7 @@ package com.limelight.binding.video; import java.nio.ByteBuffer; import java.util.LinkedList; import java.util.List; +import java.util.concurrent.locks.LockSupport; import com.limelight.LimeLog; import com.limelight.nvstream.av.ByteBufferDescriptor; @@ -203,6 +204,9 @@ public class MediaCodecDecoderRenderer implements VideoDecoderRenderer { } } else { switch (outIndex) { + case MediaCodec.INFO_TRY_AGAIN_LATER: + LockSupport.parkNanos(1); + break; case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED: LimeLog.info("Output buffers changed"); break; diff --git a/src/com/limelight/utils/Dialog.java b/src/com/limelight/utils/Dialog.java index c4bb6e6b..d95dcfb3 100644 --- a/src/com/limelight/utils/Dialog.java +++ b/src/com/limelight/utils/Dialog.java @@ -11,7 +11,7 @@ public class Dialog implements Runnable { private Activity activity; private boolean endAfterDismiss; - AlertDialog alert; + private AlertDialog alert; private static ArrayList rundownDialogs = new ArrayList(); @@ -25,13 +25,15 @@ public class Dialog implements Runnable { public static void closeDialogs() { - for (Dialog d : rundownDialogs) { - if (d.alert.isShowing()) { - d.alert.dismiss(); + synchronized (rundownDialogs) { + for (Dialog d : rundownDialogs) { + if (d.alert.isShowing()) { + d.alert.dismiss(); + } } + + rundownDialogs.clear(); } - - rundownDialogs.clear(); } public static void displayDialog(Activity activity, String title, String message, boolean endAfterDismiss) @@ -54,16 +56,21 @@ public class Dialog implements Runnable { alert.setButton(AlertDialog.BUTTON_NEUTRAL, "OK", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { - alert.dismiss(); - rundownDialogs.remove(this); + synchronized (rundownDialogs) { + rundownDialogs.remove(this); + alert.dismiss(); + } - if (endAfterDismiss) + if (endAfterDismiss) { activity.finish(); + } } - }); + }); - rundownDialogs.add(this); - alert.show(); + synchronized (rundownDialogs) { + rundownDialogs.add(this); + alert.show(); + } } } diff --git a/src/com/limelight/utils/SpinnerDialog.java b/src/com/limelight/utils/SpinnerDialog.java index a5ec6991..29a35bb2 100644 --- a/src/com/limelight/utils/SpinnerDialog.java +++ b/src/com/limelight/utils/SpinnerDialog.java @@ -33,13 +33,15 @@ public class SpinnerDialog implements Runnable,OnCancelListener { public static void closeDialogs() { - for (SpinnerDialog d : rundownDialogs) { - if (d.progress.isShowing()) { - d.progress.dismiss(); + synchronized (rundownDialogs) { + for (SpinnerDialog d : rundownDialogs) { + if (d.progress.isShowing()) { + d.progress.dismiss(); + } } + + rundownDialogs.clear(); } - - rundownDialogs.clear(); } public void dismiss() @@ -85,18 +87,27 @@ public class SpinnerDialog implements Runnable,OnCancelListener { progress.setCancelable(false); } - progress.show(); + synchronized (rundownDialogs) { + rundownDialogs.add(this); + progress.show(); + } } else { - if (progress.isShowing()) { - progress.dismiss(); + synchronized (rundownDialogs) { + if (rundownDialogs.remove(this) && progress.isShowing()) { + progress.dismiss(); + } } } } @Override public void onCancel(DialogInterface dialog) { + synchronized (rundownDialogs) { + rundownDialogs.remove(this); + } + // This will only be called if finish was true, so we don't need to check again activity.finish(); }