Initial migration to Android Studio

This commit is contained in:
Cameron Gutman
2014-10-29 21:16:09 -07:00
parent 57d919798a
commit d84b4bcf9a
541 changed files with 458 additions and 3371 deletions

View File

@@ -0,0 +1,150 @@
package com.limelight;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.concurrent.LinkedBlockingQueue;
import com.limelight.computers.ComputerManagerService;
import com.limelight.root.R;
import com.limelight.utils.Dialog;
import android.app.Activity;
import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
public class AddComputerManually extends Activity {
private Button addPcButton;
private TextView hostText;
private ComputerManagerService.ComputerManagerBinder managerBinder;
private LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<String>();
private Thread addThread;
private ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, final IBinder binder) {
managerBinder = ((ComputerManagerService.ComputerManagerBinder)binder);
startAddThread();
}
public void onServiceDisconnected(ComponentName className) {
joinAddThread();
managerBinder = null;
}
};
private void doAddPc(String host) {
String msg;
boolean finish = false;
try {
InetAddress addr = InetAddress.getByName(host);
if (!managerBinder.addComputerBlocking(addr)){
msg = "Unable to connect to the specified computer. Make sure the required ports are allowed through the firewall.";
}
else {
msg = "Successfully added computer";
finish = true;
}
} catch (UnknownHostException e) {
msg = "Unable to resolve PC address. Make sure you didn't make a typo in the address.";
}
final boolean toastFinish = finish;
final String toastMsg = msg;
AddComputerManually.this.runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(AddComputerManually.this, toastMsg, Toast.LENGTH_LONG).show();
if (toastFinish && !isFinishing()) {
// Close the activity
AddComputerManually.this.finish();
}
}
});
}
private void startAddThread() {
addThread = new Thread() {
@Override
public void run() {
while (!isInterrupted()) {
String computer;
try {
computer = computersToAdd.take();
} catch (InterruptedException e) {
return;
}
doAddPc(computer);
}
}
};
addThread.setName("UI - AddComputerManually");
addThread.start();
}
private void joinAddThread() {
if (addThread != null) {
addThread.interrupt();
try {
addThread.join();
} catch (InterruptedException e) {}
addThread = null;
}
}
@Override
protected void onStop() {
super.onStop();
Dialog.closeDialogs();
}
@Override
protected void onDestroy() {
super.onDestroy();
if (managerBinder != null) {
joinAddThread();
unbindService(serviceConnection);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_add_computer_manually);
this.addPcButton = (Button) findViewById(R.id.addPc);
this.hostText = (TextView) findViewById(R.id.hostTextView);
// Bind to the ComputerManager service
bindService(new Intent(AddComputerManually.this,
ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE);
addPcButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (hostText.getText().length() == 0) {
Toast.makeText(AddComputerManually.this, "You must enter an IP address", Toast.LENGTH_LONG).show();
return;
}
Toast.makeText(AddComputerManually.this, "Adding PC...", Toast.LENGTH_SHORT).show();
computersToAdd.add(hostText.getText().toString());
}
});
}
}

View File

@@ -0,0 +1,132 @@
package com.limelight;
import com.limelight.root.R;
import com.limelight.utils.Dialog;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.widget.CompoundButton;
import android.widget.RadioButton;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import android.widget.TextView;
import android.widget.CompoundButton.OnCheckedChangeListener;
public class AdvancedSettings extends Activity {
private SharedPreferences prefs;
private RadioButton forceSoftDec, autoDec, forceHardDec;
private SeekBar bitrateSlider;
private TextView bitrateLabel;
private static final int BITRATE_FLOOR = 1;
private static final int BITRATE_CEILING = 100;
@Override
public void onPause() {
SharedPreferences.Editor editor = prefs.edit();
editor.putInt(Game.BITRATE_PREF_STRING, bitrateSlider.getProgress());
editor.apply();
super.onPause();
}
@Override
protected void onStop() {
super.onStop();
Dialog.closeDialogs();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_advanced_settings);
this.forceSoftDec = (RadioButton) findViewById(R.id.softwareDec);
this.autoDec = (RadioButton) findViewById(R.id.autoDec);
this.forceHardDec = (RadioButton) findViewById(R.id.hardwareDec);
this.bitrateLabel = (TextView) findViewById(R.id.bitrateLabel);
this.bitrateSlider = (SeekBar) findViewById(R.id.bitrateSeekBar);
prefs = getSharedPreferences(Game.PREFS_FILE_NAME, Context.MODE_MULTI_PROCESS);
bitrateSlider.setMax(BITRATE_CEILING);
bitrateSlider.setProgress(prefs.getInt(Game.BITRATE_PREF_STRING, Game.DEFAULT_BITRATE));
updateBitrateLabel();
switch (prefs.getInt(Game.DECODER_PREF_STRING, Game.DEFAULT_DECODER)) {
case Game.FORCE_SOFTWARE_DECODER:
forceSoftDec.setChecked(true);
autoDec.setChecked(false);
forceHardDec.setChecked(false);
break;
case Game.AUTOSELECT_DECODER:
forceSoftDec.setChecked(false);
autoDec.setChecked(true);
forceHardDec.setChecked(false);
break;
case Game.FORCE_HARDWARE_DECODER:
forceSoftDec.setChecked(false);
autoDec.setChecked(false);
forceHardDec.setChecked(true);
break;
}
OnCheckedChangeListener occl = new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView,
boolean isChecked) {
if (!isChecked) {
// Ignore non-checked buttons
return;
}
if (buttonView == forceSoftDec) {
prefs.edit().putInt(Game.DECODER_PREF_STRING, Game.FORCE_SOFTWARE_DECODER).commit();
}
else if (buttonView == forceHardDec) {
prefs.edit().putInt(Game.DECODER_PREF_STRING, Game.FORCE_HARDWARE_DECODER).commit();
}
else if (buttonView == autoDec) {
prefs.edit().putInt(Game.DECODER_PREF_STRING, Game.AUTOSELECT_DECODER).commit();
}
}
};
forceSoftDec.setOnCheckedChangeListener(occl);
forceHardDec.setOnCheckedChangeListener(occl);
autoDec.setOnCheckedChangeListener(occl);
this.bitrateSlider.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress,
boolean fromUser) {
// Verify the user's selection
if (fromUser) {
if (progress < BITRATE_FLOOR) {
seekBar.setProgress(BITRATE_FLOOR);
return;
}
}
updateBitrateLabel();
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
}
private void updateBitrateLabel() {
bitrateLabel.setText("Max Bitrate: "+bitrateSlider.getProgress()+" Mbps");
}
}

View File

@@ -0,0 +1,295 @@
package com.limelight;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.List;
import org.xmlpull.v1.XmlPullParserException;
import com.limelight.binding.PlatformBinding;
import com.limelight.nvstream.http.GfeHttpResponseException;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP;
import com.limelight.root.R;
import com.limelight.utils.Dialog;
import com.limelight.utils.SpinnerDialog;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.ContextMenu;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ContextMenu.ContextMenuInfo;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.AdapterView.AdapterContextMenuInfo;
public class AppView extends Activity {
private ListView appList;
private ArrayAdapter<AppObject> appListAdapter;
private InetAddress ipAddress;
private String uniqueId;
private boolean remote;
private final static int RESUME_ID = 1;
private final static int QUIT_ID = 2;
private final static int CANCEL_ID = 3;
public final static String ADDRESS_EXTRA = "Address";
public final static String UNIQUEID_EXTRA = "UniqueId";
public final static String NAME_EXTRA = "Name";
public final static String REMOTE_EXTRA = "Remote";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_app_view);
byte[] address = getIntent().getByteArrayExtra(ADDRESS_EXTRA);
uniqueId = getIntent().getStringExtra(UNIQUEID_EXTRA);
remote = getIntent().getBooleanExtra(REMOTE_EXTRA, false);
if (address == null || uniqueId == null) {
return;
}
String labelText = "App List for "+getIntent().getStringExtra(NAME_EXTRA);
TextView label = (TextView) findViewById(R.id.appListText);
setTitle(labelText);
label.setText(labelText);
try {
ipAddress = InetAddress.getByAddress(address);
} catch (UnknownHostException e) {
return;
}
// Setup the list view
appList = (ListView)findViewById(R.id.pcListView);
appListAdapter = new ArrayAdapter<AppObject>(this, R.layout.simplerow, R.id.rowTextView);
appListAdapter.setNotifyOnChange(false);
appList.setAdapter(appListAdapter);
appList.setItemsCanFocus(true);
appList.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
long id) {
AppObject app = (AppObject) appListAdapter.getItem(pos);
if (app == null || app.app == null) {
return;
}
// Only open the context menu if something is running, otherwise start it
if (getRunningAppId() != -1) {
openContextMenu(arg1);
}
else {
doStart(app.app);
}
}
});
registerForContextMenu(appList);
}
@Override
protected void onDestroy() {
super.onDestroy();
SpinnerDialog.closeDialogs(this);
Dialog.closeDialogs();
}
@Override
protected void onResume() {
super.onResume();
updateAppList();
}
private int getRunningAppId() {
int runningAppId = -1;
for (int i = 0; i < appListAdapter.getCount(); i++) {
AppObject app = appListAdapter.getItem(i);
if (app.app == null) {
continue;
}
if (app.app.getIsRunning()) {
runningAppId = app.app.getAppId();
break;
}
}
return runningAppId;
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
AppObject selectedApp = (AppObject) appListAdapter.getItem(info.position);
if (selectedApp == null || selectedApp.app == null) {
return;
}
int runningAppId = getRunningAppId();
if (runningAppId != -1) {
if (runningAppId == selectedApp.app.getAppId()) {
menu.add(Menu.NONE, RESUME_ID, 1, "Resume Session");
menu.add(Menu.NONE, QUIT_ID, 2, "Quit Session");
}
else {
menu.add(Menu.NONE, RESUME_ID, 1, "Quit Current Game and Start");
menu.add(Menu.NONE, CANCEL_ID, 2, "Cancel");
}
}
}
@Override
public void onContextMenuClosed(Menu menu) {
}
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
AppObject app = (AppObject) appListAdapter.getItem(info.position);
switch (item.getItemId())
{
case RESUME_ID:
// Resume is the same as start for us
doStart(app.app);
return true;
case QUIT_ID:
doQuit(app.app);
return true;
case CANCEL_ID:
return true;
default:
return super.onContextItemSelected(item);
}
}
private static String generateString(NvApp app) {
StringBuilder str = new StringBuilder();
str.append(app.getAppName());
if (app.getIsRunning()) {
str.append(" - Running");
}
return str.toString();
}
private void addListPlaceholder() {
appListAdapter.add(new AppObject("No apps found. Try rescanning for games in GeForce Experience.", null));
}
private void updateAppList() {
final SpinnerDialog spinner = SpinnerDialog.displayDialog(this, "App List", "Refreshing app list...", true);
new Thread() {
@Override
public void run() {
NvHTTP httpConn = new NvHTTP(ipAddress, uniqueId, null, PlatformBinding.getCryptoProvider(AppView.this));
try {
final List<NvApp> appList = httpConn.getAppList();
AppView.this.runOnUiThread(new Runnable() {
@Override
public void run() {
appListAdapter.clear();
if (appList.isEmpty()) {
addListPlaceholder();
}
else {
for (NvApp app : appList) {
appListAdapter.add(new AppObject(generateString(app), app));
}
}
appListAdapter.notifyDataSetChanged();
}
});
// Success case
return;
} catch (GfeHttpResponseException e) {
} catch (IOException e) {
} catch (XmlPullParserException e) {
} finally {
spinner.dismiss();
}
Dialog.displayDialog(AppView.this, "Error", "Failed to get app list", true);
}
}.start();
}
private void doStart(NvApp app) {
Intent intent = new Intent(this, Game.class);
intent.putExtra(Game.EXTRA_HOST, ipAddress.getHostAddress());
intent.putExtra(Game.EXTRA_APP, app.getAppName());
intent.putExtra(Game.EXTRA_UNIQUEID, uniqueId);
intent.putExtra(Game.EXTRA_STREAMING_REMOTE, remote);
startActivity(intent);
}
private void doQuit(final NvApp app) {
Toast.makeText(AppView.this, "Quitting "+app.getAppName()+"...", Toast.LENGTH_SHORT).show();
new Thread(new Runnable() {
@Override
public void run() {
NvHTTP httpConn;
String message;
try {
httpConn = new NvHTTP(ipAddress, uniqueId, null, PlatformBinding.getCryptoProvider(AppView.this));
if (httpConn.quitApp()) {
message = "Successfully quit "+app.getAppName();
}
else {
message = "Failed to quit "+app.getAppName();
}
updateAppList();
} catch (UnknownHostException e) {
message = "Failed to resolve host";
} catch (FileNotFoundException e) {
message = "GFE returned an HTTP 404 error. Make sure your PC is running a supported GPU. Using remote desktop software can also cause this error. "
+ "Try rebooting your machine or reinstalling GFE.";
} catch (Exception e) {
message = e.getMessage();
}
final String toastMessage = message;
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(AppView.this, toastMessage, Toast.LENGTH_LONG).show();
}
});
}
}).start();
}
public class AppObject {
public String text;
public NvApp app;
public AppObject(String text, NvApp app) {
this.text = text;
this.app = app;
}
@Override
public String toString() {
return text;
}
}
}

View File

@@ -0,0 +1,862 @@
package com.limelight;
import com.limelight.binding.PlatformBinding;
import com.limelight.binding.input.ControllerHandler;
import com.limelight.binding.input.KeyboardTranslator;
import com.limelight.binding.input.TouchContext;
import com.limelight.binding.input.evdev.EvdevListener;
import com.limelight.binding.input.evdev.EvdevWatcher;
import com.limelight.binding.video.ConfigurableDecoderRenderer;
import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.NvConnectionListener;
import com.limelight.nvstream.StreamConfiguration;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.input.KeyboardPacket;
import com.limelight.nvstream.input.MouseButtonPacket;
import com.limelight.root.R;
import com.limelight.utils.Dialog;
import com.limelight.utils.SpinnerDialog;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Point;
import android.media.AudioManager;
import android.net.ConnectivityManager;
import android.net.wifi.WifiManager;
import android.os.Bundle;
import android.os.Handler;
import android.view.Display;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.View.OnGenericMotionListener;
import android.view.View.OnSystemUiVisibilityChangeListener;
import android.view.View.OnTouchListener;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Toast;
public class Game extends Activity implements SurfaceHolder.Callback,
OnGenericMotionListener, OnTouchListener, NvConnectionListener, EvdevListener,
OnSystemUiVisibilityChangeListener
{
private int lastMouseX = Integer.MIN_VALUE;
private int lastMouseY = Integer.MIN_VALUE;
private int lastButtonState = 0;
// Only 2 touches are supported
private TouchContext[] touchContextMap = new TouchContext[2];
private ControllerHandler controllerHandler;
private KeyboardTranslator keybTranslator;
private int height;
private int width;
private Point screenSize = new Point(0, 0);
private NvConnection conn;
private SpinnerDialog spinner;
private boolean displayedFailureDialog = false;
private boolean connecting = false;
private boolean connected = false;
private boolean stretchToFit;
private boolean toastsDisabled;
private EvdevWatcher evdevWatcher;
private int modifierFlags = 0;
private boolean grabbedInput = true;
private boolean grabComboDown = false;
private ConfigurableDecoderRenderer decoderRenderer;
private WifiManager.WifiLock wifiLock;
private int drFlags = 0;
public static final String EXTRA_HOST = "Host";
public static final String EXTRA_APP = "App";
public static final String EXTRA_UNIQUEID = "UniqueId";
public static final String EXTRA_STREAMING_REMOTE = "Remote";
public static final String PREFS_FILE_NAME = "gameprefs";
public static final String WIDTH_PREF_STRING = "ResH";
public static final String HEIGHT_PREF_STRING = "ResV";
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 String SOPS_PREF_STRING = "Sops";
public static final String DISABLE_TOASTS_PREF_STRING = "NoToasts";
public static final int BITRATE_DEFAULT_720_30 = 5;
public static final int BITRATE_DEFAULT_720_60 = 10;
public static final int BITRATE_DEFAULT_1080_30 = 10;
public static final int BITRATE_DEFAULT_1080_60 = 30;
public static final int DEFAULT_WIDTH = 1280;
public static final int DEFAULT_HEIGHT = 720;
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 boolean DEFAULT_SOPS = true;
public static final boolean DEFAULT_DISABLE_TOASTS = false;
public static final int FORCE_HARDWARE_DECODER = -1;
public static final int AUTOSELECT_DECODER = 0;
public static final int FORCE_SOFTWARE_DECODER = 1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// We don't want a title bar
requestWindowFeature(Window.FEATURE_NO_TITLE);
// Full-screen and don't let the display go off
getWindow().addFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN |
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
// If we're going to use immersive mode, we want to have
// the entire screen
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN);
}
// Listen for UI visibility events
getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(this);
// Change volume button behavior
setVolumeControlStream(AudioManager.STREAM_MUSIC);
// Inflate the content
setContentView(R.layout.activity_game);
// Start the spinner
spinner = SpinnerDialog.displayDialog(this, "Establishing Connection", "Starting connection", true);
// Read the stream preferences
SharedPreferences prefs = getSharedPreferences(PREFS_FILE_NAME, Context.MODE_MULTI_PROCESS);
switch (prefs.getInt(Game.DECODER_PREF_STRING, Game.DEFAULT_DECODER)) {
case Game.FORCE_SOFTWARE_DECODER:
drFlags |= VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING;
break;
case Game.AUTOSELECT_DECODER:
break;
case Game.FORCE_HARDWARE_DECODER:
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;
boolean sops;
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);
sops = prefs.getBoolean(SOPS_PREF_STRING, DEFAULT_SOPS);
toastsDisabled = prefs.getBoolean(DISABLE_TOASTS_PREF_STRING, DEFAULT_DISABLE_TOASTS);
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);
// Warn the user if they're on a metered connection
checkDataConnection();
// Make sure Wi-Fi is fully powered up
WifiManager wifiMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE);
wifiLock = wifiMgr.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "Limelight");
wifiLock.setReferenceCounted(false);
wifiLock.acquire();
String host = Game.this.getIntent().getStringExtra(EXTRA_HOST);
String app = Game.this.getIntent().getStringExtra(EXTRA_APP);
String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID);
decoderRenderer = new ConfigurableDecoderRenderer();
decoderRenderer.initializeWithFlags(drFlags);
StreamConfiguration config =
new StreamConfiguration(app, width, height,
refreshRate, bitrate * 1000, sops,
(decoderRenderer.getCapabilities() &
VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION) != 0);
// Initialize the connection
conn = new NvConnection(host, uniqueId, Game.this, config, PlatformBinding.getCryptoProvider(this));
keybTranslator = new KeyboardTranslator(conn);
controllerHandler = new ControllerHandler(conn);
SurfaceHolder sh = sv.getHolder();
if (stretchToFit || !decoderRenderer.isHardwareAccelerated()) {
// Set the surface to the size of the video
sh.setFixedSize(width, height);
}
// Initialize touch contexts
for (int i = 0; i < touchContextMap.length; i++) {
touchContextMap[i] = new TouchContext(conn, i);
}
if (LimelightBuildProps.ROOT_BUILD) {
// Start watching for raw input
evdevWatcher = new EvdevWatcher(this);
evdevWatcher.start();
}
// The connection will be started when the surface gets created
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);
if (mgr.isActiveNetworkMetered()) {
displayTransientMessage("Warning: Your active network connection is metered!");
}
}
@SuppressLint("InlinedApi")
private Runnable hideSystemUi = new Runnable() {
@Override
public void run() {
// Use immersive mode on 4.4+ or standard low profile on previous builds
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
Game.this.getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_FULLSCREEN |
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
}
else {
Game.this.getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_FULLSCREEN |
View.SYSTEM_UI_FLAG_LOW_PROFILE);
}
}
};
private void hideSystemUi(int delay) {
Handler h = getWindow().getDecorView().getHandler();
if (h != null) {
h.removeCallbacks(hideSystemUi);
h.postDelayed(hideSystemUi, delay);
}
}
@Override
protected void onStop() {
super.onStop();
SpinnerDialog.closeDialogs(this);
Dialog.closeDialogs();
displayedFailureDialog = true;
stopConnection();
int averageEndToEndLat = decoderRenderer.getAverageEndToEndLatency();
int averageDecoderLat = decoderRenderer.getAverageDecoderLatency();
String message = null;
if (averageEndToEndLat > 0) {
message = "Average client-side frame latency: "+averageEndToEndLat+" ms";
if (averageDecoderLat > 0) {
message += " (hardware decoder latency: "+averageDecoderLat+" ms)";
}
}
else if (averageDecoderLat > 0) {
message = "Average hardware decoder latency: "+averageDecoderLat+" ms";
}
if (message != null) {
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
}
finish();
}
@Override
protected void onDestroy() {
super.onDestroy();
wifiLock.release();
}
private Runnable toggleGrab = new Runnable() {
@Override
public void run() {
if (evdevWatcher != null) {
if (grabbedInput) {
evdevWatcher.ungrabAll();
}
else {
evdevWatcher.regrabAll();
}
}
grabbedInput = !grabbedInput;
}
};
// Returns true if the key stroke was consumed
private boolean handleSpecialKeys(short translatedKey, boolean down) {
int modifierMask = 0;
// Mask off the high byte
translatedKey &= 0xff;
if (translatedKey == KeyboardTranslator.VK_CONTROL) {
modifierMask = KeyboardPacket.MODIFIER_CTRL;
}
else if (translatedKey == KeyboardTranslator.VK_SHIFT) {
modifierMask = KeyboardPacket.MODIFIER_SHIFT;
}
else if (translatedKey == KeyboardTranslator.VK_ALT) {
modifierMask = KeyboardPacket.MODIFIER_ALT;
}
if (down) {
this.modifierFlags |= modifierMask;
}
else {
this.modifierFlags &= ~modifierMask;
}
// Check if Ctrl+Shift+Z is pressed
if (translatedKey == KeyboardTranslator.VK_Z &&
(modifierFlags & (KeyboardPacket.MODIFIER_CTRL | KeyboardPacket.MODIFIER_SHIFT)) ==
(KeyboardPacket.MODIFIER_CTRL | KeyboardPacket.MODIFIER_SHIFT))
{
if (down) {
// Now that we've pressed the magic combo
// we'll wait for one of the keys to come up
grabComboDown = true;
}
else {
// Toggle the grab if Z comes up
Handler h = getWindow().getDecorView().getHandler();
if (h != null) {
h.postDelayed(toggleGrab, 250);
}
grabComboDown = false;
}
return true;
}
// Toggle the grab if control or shift comes up
else if (grabComboDown) {
Handler h = getWindow().getDecorView().getHandler();
if (h != null) {
h.postDelayed(toggleGrab, 250);
}
grabComboDown = false;
return true;
}
// Not a special combo
return false;
}
private static byte getModifierState(KeyEvent event) {
byte modifier = 0;
if (event.isShiftPressed()) {
modifier |= KeyboardPacket.MODIFIER_SHIFT;
}
if (event.isCtrlPressed()) {
modifier |= KeyboardPacket.MODIFIER_CTRL;
}
if (event.isAltPressed()) {
modifier |= KeyboardPacket.MODIFIER_ALT;
}
return modifier;
}
private byte getModifierState() {
return (byte) modifierFlags;
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
InputDevice dev = event.getDevice();
if (dev == null) {
return super.onKeyDown(keyCode, event);
}
// Pass-through virtual navigation keys
if ((event.getFlags() & KeyEvent.FLAG_VIRTUAL_HARD_KEY) != 0) {
return super.onKeyDown(keyCode, event);
}
// Try the controller handler first
boolean handled = controllerHandler.handleButtonDown(keyCode, event);
if (!handled) {
// Try the keyboard handler
short translated = keybTranslator.translate(event.getKeyCode());
if (translated == 0) {
return super.onKeyDown(keyCode, event);
}
// Let this method take duplicate key down events
if (handleSpecialKeys(translated, true)) {
return true;
}
// Eat repeat down events
if (event.getRepeatCount() > 0) {
return true;
}
// Pass through keyboard input if we're not grabbing
if (!grabbedInput) {
return super.onKeyDown(keyCode, event);
}
keybTranslator.sendKeyDown(translated,
getModifierState(event));
}
return true;
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
InputDevice dev = event.getDevice();
if (dev == null) {
return super.onKeyUp(keyCode, event);
}
// Pass-through virtual navigation keys
if ((event.getFlags() & KeyEvent.FLAG_VIRTUAL_HARD_KEY) != 0) {
return super.onKeyUp(keyCode, event);
}
// Try the controller handler first
boolean handled = controllerHandler.handleButtonUp(keyCode, event);
if (!handled) {
// Try the keyboard handler
short translated = keybTranslator.translate(event.getKeyCode());
if (translated == 0) {
return super.onKeyUp(keyCode, event);
}
if (handleSpecialKeys(translated, false)) {
return true;
}
// Pass through keyboard input if we're not grabbing
if (!grabbedInput) {
return super.onKeyUp(keyCode, event);
}
keybTranslator.sendKeyUp(translated,
getModifierState(event));
}
return true;
}
private TouchContext getTouchContext(int actionIndex)
{
if (actionIndex < touchContextMap.length) {
return touchContextMap[actionIndex];
}
else {
return null;
}
}
// Returns true if the event was consumed
private boolean handleMotionEvent(MotionEvent event) {
// Pass through keyboard input if we're not grabbing
if (!grabbedInput) {
return false;
}
if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
if (controllerHandler.handleMotionEvent(event)) {
return true;
}
}
else if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0)
{
// This case is for touch-based input devices
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN ||
event.getSource() == InputDevice.SOURCE_STYLUS)
{
int actionIndex = event.getActionIndex();
int eventX = (int)event.getX(actionIndex);
int eventY = (int)event.getY(actionIndex);
TouchContext context = getTouchContext(actionIndex);
if (context == null) {
return false;
}
switch (event.getActionMasked())
{
case MotionEvent.ACTION_POINTER_DOWN:
case MotionEvent.ACTION_DOWN:
context.touchDownEvent(eventX, eventY);
break;
case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_UP:
context.touchUpEvent(eventX, eventY);
if (actionIndex == 0 && event.getPointerCount() > 1) {
// The original secondary touch now becomes primary
context.touchDownEvent((int)event.getX(1), (int)event.getY(1));
}
break;
case MotionEvent.ACTION_MOVE:
// ACTION_MOVE is special because it always has actionIndex == 0
// We'll call the move handlers for all indexes manually
for (int i = 0; i < touchContextMap.length; i++) {
touchContextMap[i].touchMoveEvent(eventX, eventY);
}
break;
default:
return false;
}
}
// This case is for mice
else if (event.getSource() == InputDevice.SOURCE_MOUSE)
{
int changedButtons = event.getButtonState() ^ lastButtonState;
if (event.getActionMasked() == MotionEvent.ACTION_SCROLL) {
// Send the vertical scroll packet
byte vScrollClicks = (byte) event.getAxisValue(MotionEvent.AXIS_VSCROLL);
conn.sendMouseScroll(vScrollClicks);
}
if ((changedButtons & MotionEvent.BUTTON_PRIMARY) != 0) {
if ((event.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0) {
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_LEFT);
}
else {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
}
}
if ((changedButtons & MotionEvent.BUTTON_SECONDARY) != 0) {
if ((event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) {
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
}
else {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
}
}
if ((changedButtons & MotionEvent.BUTTON_TERTIARY) != 0) {
if ((event.getButtonState() & MotionEvent.BUTTON_TERTIARY) != 0) {
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_MIDDLE);
}
else {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_MIDDLE);
}
}
updateMousePosition((int)event.getX(), (int)event.getY());
lastButtonState = event.getButtonState();
}
else
{
// Unknown source
return false;
}
// Handled a known source
return true;
}
// Unknown class
return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!handleMotionEvent(event)) {
return super.onTouchEvent(event);
}
return true;
}
@Override
public boolean onGenericMotionEvent(MotionEvent event) {
if (!handleMotionEvent(event)) {
return super.onGenericMotionEvent(event);
}
return true;
}
private void updateMousePosition(int eventX, int eventY) {
// Send a mouse move if we already have a mouse location
// and the mouse coordinates change
if (lastMouseX != Integer.MIN_VALUE &&
lastMouseY != Integer.MIN_VALUE &&
!(lastMouseX == eventX && lastMouseY == eventY))
{
int deltaX = eventX - lastMouseX;
int deltaY = eventY - lastMouseY;
// Scale the deltas if the device resolution is different
// than the stream resolution
deltaX = (int)Math.round((double)deltaX * ((double)width / (double)screenSize.x));
deltaY = (int)Math.round((double)deltaY * ((double)height / (double)screenSize.y));
conn.sendMouseMove((short)deltaX, (short)deltaY);
}
// Update pointer location for delta calculation next time
lastMouseX = eventX;
lastMouseY = eventY;
}
@Override
public boolean onGenericMotion(View v, MotionEvent event) {
return handleMotionEvent(event);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouch(View v, MotionEvent event) {
return handleMotionEvent(event);
}
@Override
public void stageStarting(Stage stage) {
if (spinner != null) {
spinner.setMessage("Starting "+stage.getName());
}
}
@Override
public void stageComplete(Stage stage) {
}
private void stopConnection() {
if (connecting || connected) {
connecting = connected = false;
conn.stop();
}
// Close the Evdev watcher to allow use of captured input devices
if (evdevWatcher != null) {
evdevWatcher.shutdown();
evdevWatcher = null;
}
}
@Override
public void stageFailed(Stage stage) {
if (spinner != null) {
spinner.dismiss();
spinner = null;
}
if (!displayedFailureDialog) {
displayedFailureDialog = true;
stopConnection();
Dialog.displayDialog(this, "Connection Error", "Starting "+stage.getName()+" failed", true);
}
}
@Override
public void connectionTerminated(Exception e) {
if (!displayedFailureDialog) {
displayedFailureDialog = true;
e.printStackTrace();
stopConnection();
Dialog.displayDialog(this, "Connection Terminated", "The connection failed unexpectedly", true);
}
}
@Override
public void connectionStarted() {
if (spinner != null) {
spinner.dismiss();
spinner = null;
}
connecting = false;
connected = true;
hideSystemUi(1000);
}
@Override
public void displayMessage(final String message) {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(Game.this, message, Toast.LENGTH_LONG).show();
}
});
}
@Override
public void displayTransientMessage(final String message) {
if (!toastsDisabled) {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(Game.this, message, Toast.LENGTH_LONG).show();
}
});
}
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
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 && decoderRenderer.isHardwareAccelerated()) {
resizeSurfaceWithAspectRatio((SurfaceView) findViewById(R.id.surfaceView), width, height);
}
conn.start(PlatformBinding.getDeviceName(), holder, drFlags,
PlatformBinding.getAudioRenderer(), decoderRenderer);
}
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (connected) {
stopConnection();
}
}
@Override
public void mouseMove(int deltaX, int deltaY) {
conn.sendMouseMove((short) deltaX, (short) deltaY);
}
@Override
public void mouseButtonEvent(int buttonId, boolean down) {
byte buttonIndex;
switch (buttonId)
{
case EvdevListener.BUTTON_LEFT:
buttonIndex = MouseButtonPacket.BUTTON_LEFT;
break;
case EvdevListener.BUTTON_MIDDLE:
buttonIndex = MouseButtonPacket.BUTTON_MIDDLE;
break;
case EvdevListener.BUTTON_RIGHT:
buttonIndex = MouseButtonPacket.BUTTON_RIGHT;
break;
default:
LimeLog.warning("Unhandled button: "+buttonId);
return;
}
if (down) {
conn.sendMouseButtonDown(buttonIndex);
}
else {
conn.sendMouseButtonUp(buttonIndex);
}
}
@Override
public void mouseScroll(byte amount) {
conn.sendMouseScroll(amount);
}
@Override
public void keyboardEvent(boolean buttonDown, short keyCode) {
short keyMap = keybTranslator.translate(keyCode);
if (keyMap != 0) {
if (handleSpecialKeys(keyMap, buttonDown)) {
return;
}
if (buttonDown) {
keybTranslator.sendKeyDown(keyMap, getModifierState());
}
else {
keybTranslator.sendKeyUp(keyMap, getModifierState());
}
}
}
@Override
public void onSystemUiVisibilityChange(int visibility) {
// Don't do anything if we're not connected
if (!connected) {
return;
}
// This flag is set for all devices
if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
hideSystemUi(2000);
}
// This flag is only set on 4.4+
else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT &&
(visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0) {
hideSystemUi(2000);
}
// This flag is only set before 4.4+
else if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.KITKAT &&
(visibility & View.SYSTEM_UI_FLAG_LOW_PROFILE) == 0) {
hideSystemUi(2000);
}
}
}

View File

@@ -0,0 +1,5 @@
package com.limelight;
public class LimelightBuildProps {
public static final boolean ROOT_BUILD = true;
}

View File

@@ -0,0 +1,619 @@
package com.limelight;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import com.limelight.binding.PlatformBinding;
import com.limelight.binding.crypto.AndroidCryptoProvider;
import com.limelight.computers.ComputerManagerListener;
import com.limelight.computers.ComputerManagerService;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvHTTP;
import com.limelight.nvstream.http.PairingManager;
import com.limelight.nvstream.http.PairingManager.PairState;
import com.limelight.nvstream.wol.WakeOnLanSender;
import com.limelight.root.R;
import com.limelight.utils.Dialog;
import android.app.Activity;
import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.res.Configuration;
import android.os.Bundle;
import android.os.IBinder;
import android.view.ContextMenu;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.View.OnClickListener;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.Toast;
import android.widget.AdapterView.AdapterContextMenuInfo;
public class PcView extends Activity {
private Button settingsButton, addComputerButton;
private ListView pcList;
private ArrayAdapter<ComputerObject> pcListAdapter;
private ComputerManagerService.ComputerManagerBinder managerBinder;
private boolean freezeUpdates, runningPolling;
private ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder binder) {
final ComputerManagerService.ComputerManagerBinder localBinder =
((ComputerManagerService.ComputerManagerBinder)binder);
// Wait in a separate thread to avoid stalling the UI
new Thread() {
@Override
public void run() {
// Wait for the binder to be ready
localBinder.waitForReady();
// Now make the binder visible
managerBinder = localBinder;
// Start updates
startComputerUpdates();
// Force a keypair to be generated early to avoid discovery delays
new AndroidCryptoProvider(PcView.this).getClientCertificate();
}
}.start();
}
public void onServiceDisconnected(ComponentName className) {
managerBinder = null;
}
};
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// Reinitialize views just in case orientation changed
initializeViews();
}
private final static int APP_LIST_ID = 1;
private final static int PAIR_ID = 2;
private final static int UNPAIR_ID = 3;
private final static int WOL_ID = 4;
private final static int DELETE_ID = 5;
private void initializeViews() {
setContentView(R.layout.activity_pc_view);
// Setup the list view
settingsButton = (Button)findViewById(R.id.settingsButton);
addComputerButton = (Button)findViewById(R.id.manuallyAddPc);
pcList = (ListView)findViewById(R.id.pcListView);
pcList.setAdapter(pcListAdapter);
pcList.setItemsCanFocus(true);
pcList.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
long id) {
ComputerObject computer = (ComputerObject) pcListAdapter.getItem(pos);
if (computer.details == null) {
// Placeholder item; no context menu for it
return;
}
else if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
// Open the context menu if a PC is offline
openContextMenu(arg1);
}
else if (computer.details.pairState != PairState.PAIRED) {
// Pair an unpaired machine by default
doPair(computer.details);
}
else {
doAppList(computer.details);
}
}
});
registerForContextMenu(pcList);
settingsButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
startActivity(new Intent(PcView.this, StreamSettings.class));
}
});
addComputerButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent i = new Intent(PcView.this, AddComputerManually.class);
startActivity(i);
}
});
if (pcListAdapter.isEmpty()) {
addListPlaceholder();
}
else {
pcListAdapter.notifyDataSetChanged();
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Bind to the computer manager service
bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection,
Service.BIND_AUTO_CREATE);
pcListAdapter = new ArrayAdapter<ComputerObject>(this, R.layout.simplerow, R.id.rowTextView);
pcListAdapter.setNotifyOnChange(false);
initializeViews();
}
private void startComputerUpdates() {
if (managerBinder != null) {
if (runningPolling) {
return;
}
freezeUpdates = false;
managerBinder.startPolling(new ComputerManagerListener() {
@Override
public void notifyComputerUpdated(final ComputerDetails details) {
if (!freezeUpdates) {
PcView.this.runOnUiThread(new Runnable() {
@Override
public void run() {
updateListView(details);
}
});
}
}
});
runningPolling = true;
}
}
private void stopComputerUpdates(boolean wait) {
if (managerBinder != null) {
if (!runningPolling) {
return;
}
freezeUpdates = true;
managerBinder.stopPolling();
if (wait) {
managerBinder.waitForPollingStopped();
}
runningPolling = false;
}
}
@Override
public void onDestroy() {
super.onDestroy();
if (managerBinder != null) {
unbindService(serviceConnection);
}
}
@Override
protected void onResume() {
super.onResume();
startComputerUpdates();
}
@Override
protected void onPause() {
super.onPause();
stopComputerUpdates(false);
}
@Override
protected void onStop() {
super.onStop();
Dialog.closeDialogs();
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
stopComputerUpdates(false);
// Call superclass
super.onCreateContextMenu(menu, v, menuInfo);
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
ComputerObject computer = (ComputerObject) pcListAdapter.getItem(info.position);
if (computer == null || computer.details == null) {
startComputerUpdates();
return;
}
// Inflate the context menu
if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
menu.add(Menu.NONE, WOL_ID, 1, "Send Wake-On-LAN request");
menu.add(Menu.NONE, DELETE_ID, 2, "Delete PC");
}
else if (computer.details.pairState != PairState.PAIRED) {
menu.add(Menu.NONE, PAIR_ID, 1, "Pair with PC");
if (computer.details.reachability == ComputerDetails.Reachability.REMOTE) {
menu.add(Menu.NONE, DELETE_ID, 2, "Delete PC");
}
}
else {
menu.add(Menu.NONE, APP_LIST_ID, 1, "View Game List");
menu.add(Menu.NONE, UNPAIR_ID, 2, "Unpair");
}
}
@Override
public void onContextMenuClosed(Menu menu) {
startComputerUpdates();
}
private void doPair(final ComputerDetails computer) {
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
Toast.makeText(PcView.this, "Computer is offline", Toast.LENGTH_SHORT).show();
return;
}
if (computer.runningGameId != 0) {
Toast.makeText(PcView.this, "Computer is currently in a game. " +
"You must close the game before pairing.", Toast.LENGTH_LONG).show();
return;
}
if (managerBinder == null) {
Toast.makeText(PcView.this, "The ComputerManager service is not running. " +
"Please wait a few seconds or restart the app.", Toast.LENGTH_LONG).show();
return;
}
Toast.makeText(PcView.this, "Pairing...", Toast.LENGTH_SHORT).show();
new Thread(new Runnable() {
@Override
public void run() {
NvHTTP httpConn;
String message;
try {
// Stop updates and wait while pairing
stopComputerUpdates(true);
InetAddress addr = null;
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
addr = computer.localIp;
}
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
addr = computer.remoteIp;
}
httpConn = new NvHTTP(addr,
managerBinder.getUniqueId(),
PlatformBinding.getDeviceName(),
PlatformBinding.getCryptoProvider(PcView.this));
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
message = "Already paired";
}
else {
final String pinStr = PairingManager.generatePinString();
// Spin the dialog off in a thread because it blocks
Dialog.displayDialog(PcView.this, "Pairing", "Please enter the following PIN on the target PC: "+pinStr, false);
PairingManager.PairState pairState = httpConn.pair(pinStr);
if (pairState == PairingManager.PairState.PIN_WRONG) {
message = "Incorrect PIN";
}
else if (pairState == PairingManager.PairState.FAILED) {
message = "Pairing failed";
}
else if (pairState == PairingManager.PairState.PAIRED) {
message = "Paired successfully";
}
else {
// Should be no other values
message = null;
}
}
} catch (UnknownHostException e) {
message = "Failed to resolve host";
} catch (FileNotFoundException e) {
message = "GFE returned an HTTP 404 error. Make sure your PC is running a supported GPU. Using remote desktop software can also cause this error. "
+ "Try rebooting your machine or reinstalling GFE.";
} catch (Exception e) {
message = e.getMessage();
}
Dialog.closeDialogs();
final String toastMessage = message;
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show();
}
});
// Start polling again
startComputerUpdates();
}
}).start();
}
private void doWakeOnLan(final ComputerDetails computer) {
if (computer.reachability != ComputerDetails.Reachability.OFFLINE) {
Toast.makeText(PcView.this, "Computer is online", Toast.LENGTH_SHORT).show();
return;
}
if (computer.macAddress == null) {
Toast.makeText(PcView.this, "Unable to wake PC because GFE didn't send a MAC address", Toast.LENGTH_SHORT).show();
return;
}
Toast.makeText(PcView.this, "Waking PC...", Toast.LENGTH_SHORT).show();
new Thread(new Runnable() {
@Override
public void run() {
String message;
try {
WakeOnLanSender.sendWolPacket(computer);
message = "It may take a few seconds for your PC to wake up. " +
"If it doesn't, make sure it's configured properly for Wake-On-LAN.";
} catch (IOException e) {
message = "Failed to send Wake-On-LAN packets";
}
final String toastMessage = message;
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show();
}
});
}
}).start();
}
private void doUnpair(final ComputerDetails computer) {
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
Toast.makeText(PcView.this, "Computer is offline", Toast.LENGTH_SHORT).show();
return;
}
if (managerBinder == null) {
Toast.makeText(PcView.this, "The ComputerManager service is not running. " +
"Please wait a few seconds or restart the app.", Toast.LENGTH_LONG).show();
return;
}
Toast.makeText(PcView.this, "Unpairing...", Toast.LENGTH_SHORT).show();
new Thread(new Runnable() {
@Override
public void run() {
NvHTTP httpConn;
String message;
try {
InetAddress addr = null;
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
addr = computer.localIp;
}
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
addr = computer.remoteIp;
}
httpConn = new NvHTTP(addr,
managerBinder.getUniqueId(),
PlatformBinding.getDeviceName(),
PlatformBinding.getCryptoProvider(PcView.this));
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
httpConn.unpair();
if (httpConn.getPairState() == PairingManager.PairState.NOT_PAIRED) {
message = "Unpaired successfully";
}
else {
message = "Failed to unpair";
}
}
else {
message = "Device was not paired";
}
} catch (UnknownHostException e) {
message = "Failed to resolve host";
} catch (FileNotFoundException e) {
message = "GFE returned an HTTP 404 error. Make sure your PC is running a supported GPU. Using remote desktop software can also cause this error. "
+ "Try rebooting your machine or reinstalling GFE.";
} catch (Exception e) {
message = e.getMessage();
}
final String toastMessage = message;
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show();
}
});
}
}).start();
}
private void doAppList(ComputerDetails computer) {
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
Toast.makeText(PcView.this, "Computer is offline", Toast.LENGTH_SHORT).show();
return;
}
if (managerBinder == null) {
Toast.makeText(PcView.this, "The ComputerManager service is not running. " +
"Please wait a few seconds or restart the app.", Toast.LENGTH_LONG).show();
return;
}
Intent i = new Intent(this, AppView.class);
i.putExtra(AppView.NAME_EXTRA, computer.name);
i.putExtra(AppView.UNIQUEID_EXTRA, managerBinder.getUniqueId());
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
i.putExtra(AppView.ADDRESS_EXTRA, computer.localIp.getAddress());
i.putExtra(AppView.REMOTE_EXTRA, false);
}
else {
i.putExtra(AppView.ADDRESS_EXTRA, computer.remoteIp.getAddress());
i.putExtra(AppView.REMOTE_EXTRA, true);
}
startActivity(i);
}
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
ComputerObject computer = (ComputerObject) pcListAdapter.getItem(info.position);
switch (item.getItemId())
{
case PAIR_ID:
doPair(computer.details);
return true;
case UNPAIR_ID:
doUnpair(computer.details);
return true;
case WOL_ID:
doWakeOnLan(computer.details);
return true;
case DELETE_ID:
if (managerBinder == null) {
Toast.makeText(PcView.this, "The ComputerManager service is not running. " +
"Please wait a few seconds or restart the app.", Toast.LENGTH_LONG).show();
return true;
}
managerBinder.removeComputer(computer.details.name);
removeListView(computer.details);
return true;
case APP_LIST_ID:
doAppList(computer.details);
return true;
default:
return super.onContextItemSelected(item);
}
}
private static String generateString(ComputerDetails details) {
StringBuilder str = new StringBuilder();
str.append(details.name).append(" - ");
if (details.state == ComputerDetails.State.ONLINE) {
str.append("Online ");
if (details.reachability == ComputerDetails.Reachability.LOCAL) {
str.append("(Local) - ");
}
else {
str.append("(Remote) - ");
}
if (details.pairState == PairState.PAIRED) {
if (details.runningGameId == 0) {
str.append("Available");
}
else {
str.append("In Game");
}
}
else {
str.append("Not Paired");
}
}
else {
str.append("Offline");
}
return str.toString();
}
private void addListPlaceholder() {
pcListAdapter.add(new ComputerObject("Discovery is running. No computers found yet. " +
"If your PC doesn't show up in about 15 seconds, " +
"make sure your computer is running GFE or add your PC manually using the button above.", null));
}
private void removeListView(ComputerDetails details) {
for (int i = 0; i < pcListAdapter.getCount(); i++) {
ComputerObject computer = pcListAdapter.getItem(i);
if (details.equals(computer.details)) {
pcListAdapter.remove(computer);
break;
}
}
if (pcListAdapter.getCount() == 0) {
// Add the placeholder if we're down to 0 computers
addListPlaceholder();
}
}
private void updateListView(ComputerDetails details) {
String computerString = generateString(details);
ComputerObject existingEntry = null;
boolean placeholderPresent = false;
for (int i = 0; i < pcListAdapter.getCount(); i++) {
ComputerObject computer = pcListAdapter.getItem(i);
// If there's a placeholder, there's nothing else
if (computer.details == null) {
placeholderPresent = true;
break;
}
// Check if this is the same computer
if (details.equals(computer.details)) {
existingEntry = computer;
break;
}
}
if (existingEntry != null) {
// Replace the information in the existing entry
existingEntry.text = computerString;
existingEntry.details = details;
}
else {
// If the placeholder is the only object, remove it
if (placeholderPresent) {
pcListAdapter.remove(pcListAdapter.getItem(0));
}
// Add a new entry
pcListAdapter.add(new ComputerObject(computerString, details));
}
// Notify the view that the data has changed
pcListAdapter.notifyDataSetChanged();
}
public class ComputerObject {
public String text;
public ComputerDetails details;
public ComputerObject(String text, ComputerDetails details) {
this.text = text;
this.details = details;
}
@Override
public String toString() {
return text;
}
}
}

View File

@@ -0,0 +1,6 @@
package com.limelight;
/* This is a dummy class to allow for a separate icon
* and launcher for TV.
*/
public class PcViewTv extends PcView {}

View File

@@ -0,0 +1,146 @@
package com.limelight;
import com.limelight.root.R;
import com.limelight.utils.Dialog;
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;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
public class StreamSettings extends Activity {
private Button advancedSettingsButton;
private SharedPreferences prefs;
private RadioButton rbutton720p30, rbutton720p60, rbutton1080p30, rbutton1080p60;
private CheckBox stretchToFill, enableSops, toastsDisabled;
@Override
protected void onStop() {
super.onStop();
Dialog.closeDialogs();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_stream_settings);
this.stretchToFill = (CheckBox) findViewById(R.id.stretchToFill);
this.enableSops = (CheckBox) findViewById(R.id.enableSops);
this.toastsDisabled = (CheckBox) findViewById(R.id.disableToasts);
this.advancedSettingsButton = (Button) findViewById(R.id.advancedSettingsButton);
this.rbutton720p30 = (RadioButton) findViewById(R.id.config720p30Selected);
this.rbutton720p60 = (RadioButton) findViewById(R.id.config720p60Selected);
this.rbutton1080p30 = (RadioButton) findViewById(R.id.config1080p30Selected);
this.rbutton1080p60 = (RadioButton) findViewById(R.id.config1080p60Selected);
prefs = getSharedPreferences(Game.PREFS_FILE_NAME, Context.MODE_MULTI_PROCESS);
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));
enableSops.setChecked(prefs.getBoolean(Game.SOPS_PREF_STRING, Game.DEFAULT_SOPS));
toastsDisabled.setChecked(prefs.getBoolean(Game.DISABLE_TOASTS_PREF_STRING, Game.DEFAULT_DISABLE_TOASTS));
rbutton720p30.setChecked(false);
rbutton720p60.setChecked(false);
rbutton1080p30.setChecked(false);
rbutton1080p60.setChecked(false);
if (res720p) {
if (fps30) {
rbutton720p30.setChecked(true);
}
else {
rbutton720p60.setChecked(true);
}
}
else {
if (fps30) {
rbutton1080p30.setChecked(true);
}
else {
rbutton1080p60.setChecked(true);
}
}
OnCheckedChangeListener occl = new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView,
boolean isChecked) {
if (!isChecked) {
// Ignore non-checked buttons
return;
}
if (buttonView == rbutton720p30) {
prefs.edit().putInt(Game.WIDTH_PREF_STRING, 1280).
putInt(Game.HEIGHT_PREF_STRING, 720).
putInt(Game.REFRESH_RATE_PREF_STRING, 30).
putInt(Game.BITRATE_PREF_STRING, Game.BITRATE_DEFAULT_720_30).commit();
}
else if (buttonView == rbutton720p60) {
prefs.edit().putInt(Game.WIDTH_PREF_STRING, 1280).
putInt(Game.HEIGHT_PREF_STRING, 720).
putInt(Game.REFRESH_RATE_PREF_STRING, 60).
putInt(Game.BITRATE_PREF_STRING, Game.BITRATE_DEFAULT_720_60).commit();
}
else if (buttonView == rbutton1080p30) {
prefs.edit().putInt(Game.WIDTH_PREF_STRING, 1920).
putInt(Game.HEIGHT_PREF_STRING, 1080).
putInt(Game.REFRESH_RATE_PREF_STRING, 30).
putInt(Game.BITRATE_PREF_STRING, Game.BITRATE_DEFAULT_1080_30).commit();
}
else if (buttonView == rbutton1080p60) {
prefs.edit().putInt(Game.WIDTH_PREF_STRING, 1920).
putInt(Game.HEIGHT_PREF_STRING, 1080).
putInt(Game.REFRESH_RATE_PREF_STRING, 60).
putInt(Game.BITRATE_PREF_STRING, Game.BITRATE_DEFAULT_1080_60).commit();
}
}
};
rbutton720p30.setOnCheckedChangeListener(occl);
rbutton720p60.setOnCheckedChangeListener(occl);
rbutton1080p30.setOnCheckedChangeListener(occl);
rbutton1080p60.setOnCheckedChangeListener(occl);
advancedSettingsButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent i = new Intent(StreamSettings.this, AdvancedSettings.class);
startActivity(i);
}
});
stretchToFill.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView,
boolean isChecked) {
prefs.edit().putBoolean(Game.STRETCH_PREF_STRING, isChecked).commit();
}
});
enableSops.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView,
boolean isChecked) {
prefs.edit().putBoolean(Game.SOPS_PREF_STRING, isChecked).commit();
}
});
toastsDisabled.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView,
boolean isChecked) {
prefs.edit().putBoolean(Game.DISABLE_TOASTS_PREF_STRING, isChecked).commit();
}
});
}
}

View File

@@ -0,0 +1,24 @@
package com.limelight.binding;
import android.content.Context;
import com.limelight.binding.audio.AndroidAudioRenderer;
import com.limelight.binding.crypto.AndroidCryptoProvider;
import com.limelight.nvstream.av.audio.AudioRenderer;
import com.limelight.nvstream.http.LimelightCryptoProvider;
public class PlatformBinding {
public static String getDeviceName() {
String deviceName = android.os.Build.MODEL;
deviceName = deviceName.replace(" ", "");
return deviceName;
}
public static AudioRenderer getAudioRenderer() {
return new AndroidAudioRenderer();
}
public static LimelightCryptoProvider getCryptoProvider(Context c) {
return new AndroidCryptoProvider(c);
}
}

View File

@@ -0,0 +1,70 @@
package com.limelight.binding.audio;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import com.limelight.LimeLog;
import com.limelight.nvstream.av.audio.AudioRenderer;
public class AndroidAudioRenderer implements AudioRenderer {
public static final int FRAME_SIZE = 960;
private AudioTrack track;
@Override
public boolean streamInitialized(int channelCount, int sampleRate) {
int channelConfig;
int bufferSize;
switch (channelCount)
{
case 1:
channelConfig = AudioFormat.CHANNEL_OUT_MONO;
break;
case 2:
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
break;
default:
LimeLog.severe("Decoder returned unhandled channel count");
return false;
}
bufferSize = Math.max(AudioTrack.getMinBufferSize(sampleRate,
channelConfig,
AudioFormat.ENCODING_PCM_16BIT),
FRAME_SIZE * 2);
// Round to next frame
bufferSize = (((bufferSize + (FRAME_SIZE - 1)) / FRAME_SIZE) * FRAME_SIZE);
LimeLog.info("Audio track buffer size: "+bufferSize);
track = new AudioTrack(AudioManager.STREAM_MUSIC,
sampleRate,
channelConfig,
AudioFormat.ENCODING_PCM_16BIT,
bufferSize,
AudioTrack.MODE_STREAM);
track.play();
return true;
}
@Override
public void playDecodedAudio(byte[] audioData, int offset, int length) {
track.write(audioData, offset, length);
}
@Override
public void streamClosing() {
if (track != null) {
track.release();
}
}
@Override
public int getCapabilities() {
return 0;
}
}

View File

@@ -0,0 +1,280 @@
package com.limelight.binding.crypto;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.StringWriter;
import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.security.Security;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.X500NameBuilder;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import android.annotation.SuppressLint;
import android.content.Context;
import android.util.Base64;
import com.limelight.LimeLog;
import com.limelight.nvstream.http.LimelightCryptoProvider;
public class AndroidCryptoProvider implements LimelightCryptoProvider {
private File certFile;
private File keyFile;
private X509Certificate cert;
private RSAPrivateKey key;
private byte[] pemCertBytes;
private static Object globalCryptoLock = new Object();
static {
// Install the Bouncy Castle provider
Security.addProvider(new BouncyCastleProvider());
}
public AndroidCryptoProvider(Context c) {
String dataPath = c.getFilesDir().getAbsolutePath();
certFile = new File(dataPath + File.separator + "client.crt");
keyFile = new File(dataPath + File.separator + "client.key");
}
private byte[] loadFileToBytes(File f) {
if (!f.exists()) {
return null;
}
try {
FileInputStream fin = new FileInputStream(f);
byte[] fileData = new byte[(int) f.length()];
fin.read(fileData);
fin.close();
return fileData;
} catch (IOException e) {
return null;
}
}
private boolean loadCertKeyPair() {
byte[] certBytes = loadFileToBytes(certFile);
byte[] keyBytes = loadFileToBytes(keyFile);
// If either file was missing, we definitely can't succeed
if (certBytes == null || keyBytes == null) {
LimeLog.info("Missing cert or key; need to generate a new one");
return false;
}
try {
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", "BC");
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certBytes));
pemCertBytes = certBytes;
KeyFactory keyFactory = KeyFactory.getInstance("RSA", "BC");
key = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
} catch (CertificateException e) {
// May happen if the cert is corrupt
LimeLog.warning("Corrupted certificate");
return false;
} catch (NoSuchAlgorithmException e) {
// Should never happen
e.printStackTrace();
return false;
} catch (InvalidKeySpecException e) {
// May happen if the key is corrupt
LimeLog.warning("Corrupted key");
return false;
} catch (NoSuchProviderException e) {
// Should never happen
e.printStackTrace();
return false;
}
return true;
}
@SuppressLint("TrulyRandom")
private boolean generateCertKeyPair() {
byte[] snBytes = new byte[8];
new SecureRandom().nextBytes(snBytes);
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
} catch (NoSuchAlgorithmException e1) {
// Should never happen
e1.printStackTrace();
return false;
} catch (NoSuchProviderException e) {
// Should never happen
e.printStackTrace();
return false;
}
Date now = new Date();
// Expires in 20 years
Calendar calendar = Calendar.getInstance();
calendar.setTime(now);
calendar.add(Calendar.YEAR, 20);
Date expirationDate = calendar.getTime();
BigInteger serial = new BigInteger(snBytes).abs();
X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
nameBuilder.addRDN(BCStyle.CN, "NVIDIA GameStream Client");
X500Name name = nameBuilder.build();
X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(name, serial, now, expirationDate, Locale.ENGLISH, name,
SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()));
try {
ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate());
cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen));
key = (RSAPrivateKey) keyPair.getPrivate();
} catch (Exception e) {
// Nothing should go wrong here
e.printStackTrace();
return false;
}
LimeLog.info("Generated a new key pair");
// Save the resulting pair
saveCertKeyPair();
return true;
}
private void saveCertKeyPair() {
try {
FileOutputStream certOut = new FileOutputStream(certFile);
FileOutputStream keyOut = new FileOutputStream(keyFile);
// Write the certificate in OpenSSL PEM format (important for the server)
StringWriter strWriter = new StringWriter();
JcaPEMWriter pemWriter = new JcaPEMWriter(strWriter);
pemWriter.writeObject(cert);
pemWriter.close();
// Line endings MUST be UNIX for the PC to accept the cert properly
OutputStreamWriter certWriter = new OutputStreamWriter(certOut);
String pemStr = strWriter.getBuffer().toString();
for (int i = 0; i < pemStr.length(); i++) {
char c = pemStr.charAt(i);
if (c != '\r')
certWriter.append(c);
}
certWriter.close();
// Write the private out in PKCS8 format
keyOut.write(key.getEncoded());
certOut.close();
keyOut.close();
LimeLog.info("Saved generated key pair to disk");
} catch (IOException e) {
// This isn't good because it means we'll have
// to re-pair next time
e.printStackTrace();
}
}
public X509Certificate getClientCertificate() {
// Use a lock here to ensure only one guy will be generating or loading
// the certificate and key at a time
synchronized (globalCryptoLock) {
// Return a loaded cert if we have one
if (cert != null) {
return cert;
}
// No loaded cert yet, let's see if we have one on disk
if (loadCertKeyPair()) {
// Got one
return cert;
}
// Try to generate a new key pair
if (!generateCertKeyPair()) {
// Failed
return null;
}
// Load the generated pair
loadCertKeyPair();
return cert;
}
}
public RSAPrivateKey getClientPrivateKey() {
// Use a lock here to ensure only one guy will be generating or loading
// the certificate and key at a time
synchronized (globalCryptoLock) {
// Return a loaded key if we have one
if (key != null) {
return key;
}
// No loaded key yet, let's see if we have one on disk
if (loadCertKeyPair()) {
// Got one
return key;
}
// Try to generate a new key pair
if (!generateCertKeyPair()) {
// Failed
return null;
}
// Load the generated pair
loadCertKeyPair();
return key;
}
}
public byte[] getPemEncodedClientCertificate() {
synchronized (globalCryptoLock) {
// Call our helper function to do the cert loading/generation for us
getClientCertificate();
// Return a cached value if we have it
return pemCertBytes;
}
}
@Override
public String encodeBase64String(byte[] data) {
return Base64.encodeToString(data, Base64.NO_WRAP);
}
}

View File

@@ -0,0 +1,642 @@
package com.limelight.binding.input;
import java.util.HashMap;
import android.os.SystemClock;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.input.ControllerPacket;
import com.limelight.utils.Vector2d;
public class ControllerHandler {
private short inputMap = 0x0000;
private byte leftTrigger = 0x00;
private byte rightTrigger = 0x00;
private short rightStickX = 0x0000;
private short rightStickY = 0x0000;
private short leftStickX = 0x0000;
private short leftStickY = 0x0000;
private int emulatingButtonFlags = 0;
// Used for OUYA bumper state tracking since they force all buttons
// up when the OUYA button goes down. We watch the last time we get
// a bumper up and compare that to our maximum delay when we receive
// a Start button press to see if we should activate one of our
// emulated button combos.
private long lastLbUpTime = 0;
private long lastRbUpTime = 0;
private static final int MAXIMUM_BUMPER_UP_DELAY_MS = 100;
private static final int MINIMUM_BUTTON_DOWN_TIME_MS = 25;
private static final int EMULATING_SPECIAL = 0x1;
private static final int EMULATING_SELECT = 0x2;
private static final int EMULATED_SPECIAL_UP_DELAY_MS = 100;
private static final int EMULATED_SELECT_UP_DELAY_MS = 30;
private Vector2d inputVector = new Vector2d();
private Vector2d normalizedInputVector = new Vector2d();
private HashMap<String, ControllerMapping> mappings = new HashMap<String, ControllerMapping>();
private NvConnection conn;
public ControllerHandler(NvConnection conn) {
this.conn = conn;
// We want limelight-common to scale the axis values to match Xinput values
ControllerPacket.enableAxisScaling = true;
}
private static InputDevice.MotionRange getMotionRangeForJoystickAxis(InputDevice dev, int axis) {
InputDevice.MotionRange range;
// First get the axis for SOURCE_JOYSTICK
range = dev.getMotionRange(axis, InputDevice.SOURCE_JOYSTICK);
if (range == null) {
// Now try the axis for SOURCE_GAMEPAD
range = dev.getMotionRange(axis, InputDevice.SOURCE_GAMEPAD);
}
return range;
}
private ControllerMapping createMappingForDevice(InputDevice dev) {
ControllerMapping mapping = new ControllerMapping();
mapping.leftStickXAxis = MotionEvent.AXIS_X;
mapping.leftStickYAxis = MotionEvent.AXIS_Y;
InputDevice.MotionRange leftTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_LTRIGGER);
InputDevice.MotionRange rightTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RTRIGGER);
InputDevice.MotionRange brakeRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_BRAKE);
InputDevice.MotionRange gasRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_GAS);
if (leftTriggerRange != null && rightTriggerRange != null)
{
// Some controllers use LTRIGGER and RTRIGGER (like Ouya)
mapping.leftTriggerAxis = MotionEvent.AXIS_LTRIGGER;
mapping.rightTriggerAxis = MotionEvent.AXIS_RTRIGGER;
}
else if (brakeRange != null && gasRange != null)
{
// Others use GAS and BRAKE (like Moga)
mapping.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
mapping.rightTriggerAxis = MotionEvent.AXIS_GAS;
}
else
{
InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX);
InputDevice.MotionRange ryRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RY);
if (rxRange != null && ryRange != null) {
String devName = dev.getName();
if (devName.contains("Xbox") || devName.contains("XBox") || devName.contains("X-Box")) {
// Xbox controllers use RX and RY for right stick
mapping.rightStickXAxis = MotionEvent.AXIS_RX;
mapping.rightStickYAxis = MotionEvent.AXIS_RY;
// Xbox controllers use Z and RZ for triggers
mapping.leftTriggerAxis = MotionEvent.AXIS_Z;
mapping.rightTriggerAxis = MotionEvent.AXIS_RZ;
mapping.triggersIdleNegative = true;
mapping.isXboxController = true;
}
else {
// DS4 controller uses RX and RY for triggers
mapping.leftTriggerAxis = MotionEvent.AXIS_RX;
mapping.rightTriggerAxis = MotionEvent.AXIS_RY;
mapping.triggersIdleNegative = true;
mapping.isDualShock4 = true;
}
}
}
if (mapping.rightStickXAxis == -1 && mapping.rightStickYAxis == -1) {
InputDevice.MotionRange zRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_Z);
InputDevice.MotionRange rzRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RZ);
// Most other controllers use Z and RZ for the right stick
if (zRange != null && rzRange != null) {
mapping.rightStickXAxis = MotionEvent.AXIS_Z;
mapping.rightStickYAxis = MotionEvent.AXIS_RZ;
}
else {
InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX);
InputDevice.MotionRange ryRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RY);
// Try RX and RY now
if (rxRange != null && ryRange != null) {
mapping.rightStickXAxis = MotionEvent.AXIS_RX;
mapping.rightStickYAxis = MotionEvent.AXIS_RY;
}
}
}
// Some devices have "hats" for d-pads
InputDevice.MotionRange hatXRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_HAT_X);
InputDevice.MotionRange hatYRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_HAT_Y);
if (hatXRange != null && hatYRange != null) {
mapping.hatXAxis = MotionEvent.AXIS_HAT_X;
mapping.hatYAxis = MotionEvent.AXIS_HAT_Y;
mapping.hatXDeadzone = hatXRange.getFlat();
mapping.hatYDeadzone = hatYRange.getFlat();
}
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
InputDevice.MotionRange lsXRange = getMotionRangeForJoystickAxis(dev, mapping.leftStickXAxis);
InputDevice.MotionRange lsYRange = getMotionRangeForJoystickAxis(dev, mapping.leftStickYAxis);
if (lsXRange != null && lsYRange != null) {
// The flat values should never be negative but we'll deal with it if they are
mapping.leftStickDeadzoneRadius = Math.max(Math.abs(lsXRange.getFlat()),
Math.abs(lsYRange.getFlat()));
// Some devices (certain OUYAs at least) report a deadzone that's larger
// than the entire range of their axis likely due to some system software bug.
// If we see a very large deadzone, simply ignore the value and use our default.
if (mapping.leftStickDeadzoneRadius > 0.5f) {
mapping.leftStickDeadzoneRadius = 0;
}
// If there isn't a (reasonable) deadzone at all, use 20%
if (mapping.leftStickDeadzoneRadius < 0.02f) {
mapping.leftStickDeadzoneRadius = 0.20f;
}
// Check that the deadzone is 15% at minimum
else if (mapping.leftStickDeadzoneRadius < 0.15f) {
mapping.leftStickDeadzoneRadius = 0.15f;
}
}
}
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) {
InputDevice.MotionRange rsXRange = getMotionRangeForJoystickAxis(dev, mapping.rightStickXAxis);
InputDevice.MotionRange rsYRange = getMotionRangeForJoystickAxis(dev, mapping.rightStickYAxis);
if (rsXRange != null && rsYRange != null) {
// The flat values should never be negative but we'll deal with it if they are
mapping.rightStickDeadzoneRadius = Math.max(Math.abs(rsXRange.getFlat()),
Math.abs(rsYRange.getFlat()));
// Some devices (certain OUYAs at least) report a deadzone that's larger
// than the entire range of their axis likely due to some system software bug.
// If we see a very large deadzone, simply ignore the value and use our default.
if (mapping.rightStickDeadzoneRadius > 0.5f) {
mapping.rightStickDeadzoneRadius = 0;
}
// If there isn't a (reasonable) deadzone at all, use 20%
if (mapping.rightStickDeadzoneRadius < 0.02f) {
mapping.rightStickDeadzoneRadius = 0.20f;
}
// Check that the deadzone is 15% at minimum
else if (mapping.rightStickDeadzoneRadius < 0.15f) {
mapping.rightStickDeadzoneRadius = 0.15f;
}
}
}
return mapping;
}
private ControllerMapping getMappingForDevice(InputDevice dev) {
// Unknown devices can't be handled
if (dev == null) {
return null;
}
String descriptor = dev.getDescriptor();
// Return the existing mapping if it exists
ControllerMapping mapping = mappings.get(descriptor);
if (mapping != null) {
return mapping;
}
// Otherwise create a new mapping
mapping = createMappingForDevice(dev);
mappings.put(descriptor, mapping);
return mapping;
}
private void sendControllerInputPacket() {
conn.sendControllerInput(inputMap, leftTrigger, rightTrigger,
leftStickX, leftStickY, rightStickX, rightStickY);
}
private static int handleRemapping(ControllerMapping mapping, KeyEvent event) {
if (mapping.isDualShock4) {
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_BUTTON_Y:
return KeyEvent.KEYCODE_BUTTON_L1;
case KeyEvent.KEYCODE_BUTTON_Z:
return KeyEvent.KEYCODE_BUTTON_R1;
case KeyEvent.KEYCODE_BUTTON_C:
return KeyEvent.KEYCODE_BUTTON_B;
case KeyEvent.KEYCODE_BUTTON_X:
return KeyEvent.KEYCODE_BUTTON_Y;
case KeyEvent.KEYCODE_BUTTON_B:
return KeyEvent.KEYCODE_BUTTON_A;
case KeyEvent.KEYCODE_BUTTON_A:
return KeyEvent.KEYCODE_BUTTON_X;
case KeyEvent.KEYCODE_BUTTON_SELECT:
return KeyEvent.KEYCODE_BUTTON_THUMBL;
case KeyEvent.KEYCODE_BUTTON_START:
return KeyEvent.KEYCODE_BUTTON_THUMBR;
case KeyEvent.KEYCODE_BUTTON_L2:
return KeyEvent.KEYCODE_BUTTON_SELECT;
case KeyEvent.KEYCODE_BUTTON_R2:
return KeyEvent.KEYCODE_BUTTON_START;
// These are duplicate trigger events
case KeyEvent.KEYCODE_BUTTON_R1:
case KeyEvent.KEYCODE_BUTTON_L1:
return 0;
}
}
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) {
switch (event.getKeyCode()) {
// These are duplicate dpad events for hat input
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 0;
}
}
else if (mapping.hatXAxis == -1 &&
mapping.hatYAxis == -1 &&
mapping.isXboxController &&
event.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN) {
// If there's not a proper Xbox controller mapping, we'll translate the raw d-pad
// scan codes into proper key codes
switch (event.getScanCode())
{
case 704:
return KeyEvent.KEYCODE_DPAD_LEFT;
case 705:
return KeyEvent.KEYCODE_DPAD_RIGHT;
case 706:
return KeyEvent.KEYCODE_DPAD_UP;
case 707:
return KeyEvent.KEYCODE_DPAD_DOWN;
}
}
return event.getKeyCode();
}
private Vector2d handleDeadZone(float x, float y, float deadzoneRadius) {
// Reinitialize our cached Vector2d object
inputVector.initialize(x, y);
if (inputVector.getMagnitude() <= deadzoneRadius) {
// Deadzone -- return the zero vector
return Vector2d.ZERO;
}
else {
// Scale the input based on the distance from the deadzone
inputVector.getNormalized(normalizedInputVector);
normalizedInputVector.scalarMultiply((inputVector.getMagnitude() - deadzoneRadius) / (1.0f - deadzoneRadius));
// Bound the X value to -1.0 to 1.0
if (normalizedInputVector.getX() > 1.0f) {
normalizedInputVector.setX(1.0f);
}
else if (normalizedInputVector.getX() < -1.0f) {
normalizedInputVector.setX(-1.0f);
}
// Bound the Y value to -1.0 to 1.0
if (normalizedInputVector.getY() > 1.0f) {
normalizedInputVector.setY(1.0f);
}
else if (normalizedInputVector.getY() < -1.0f) {
normalizedInputVector.setY(-1.0f);
}
return normalizedInputVector;
}
}
public boolean handleMotionEvent(MotionEvent event) {
ControllerMapping mapping = getMappingForDevice(event.getDevice());
if (mapping == null) {
return false;
}
// Handle left stick events outside of the deadzone
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
Vector2d leftStickVector = handleDeadZone(event.getAxisValue(mapping.leftStickXAxis),
event.getAxisValue(mapping.leftStickYAxis), mapping.leftStickDeadzoneRadius);
leftStickX = (short)(leftStickVector.getX() * 0x7FFE);
leftStickY = (short)(-leftStickVector.getY() * 0x7FFE);
}
// Handle right stick events outside of the deadzone
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) {
Vector2d rightStickVector = handleDeadZone(event.getAxisValue(mapping.rightStickXAxis),
event.getAxisValue(mapping.rightStickYAxis), mapping.rightStickDeadzoneRadius);
rightStickX = (short)(rightStickVector.getX() * 0x7FFE);
rightStickY = (short)(-rightStickVector.getY() * 0x7FFE);
}
// Handle controllers with analog triggers
if (mapping.leftTriggerAxis != -1 && mapping.rightTriggerAxis != -1) {
float L2 = event.getAxisValue(mapping.leftTriggerAxis);
float R2 = event.getAxisValue(mapping.rightTriggerAxis);
if (mapping.triggersIdleNegative) {
L2 = (L2 + 1) / 2;
R2 = (R2 + 1) / 2;
}
leftTrigger = (byte)(L2 * 0xFF);
rightTrigger = (byte)(R2 * 0xFF);
}
// Hats emulate d-pad events
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) {
float hatX = event.getAxisValue(MotionEvent.AXIS_HAT_X);
float hatY = event.getAxisValue(MotionEvent.AXIS_HAT_Y);
inputMap &= ~(ControllerPacket.LEFT_FLAG | ControllerPacket.RIGHT_FLAG);
if (hatX < -(0.5 + mapping.hatXDeadzone)) {
inputMap |= ControllerPacket.LEFT_FLAG;
}
else if (hatX > (0.5 + mapping.hatXDeadzone)) {
inputMap |= ControllerPacket.RIGHT_FLAG;
}
inputMap &= ~(ControllerPacket.UP_FLAG | ControllerPacket.DOWN_FLAG);
if (hatY < -(0.5 + mapping.hatYDeadzone)) {
inputMap |= ControllerPacket.UP_FLAG;
}
else if (hatY > (0.5 + mapping.hatYDeadzone)) {
inputMap |= ControllerPacket.DOWN_FLAG;
}
}
sendControllerInputPacket();
return true;
}
public boolean handleButtonUp(int keyCode, KeyEvent event) {
ControllerMapping mapping = getMappingForDevice(event.getDevice());
if (mapping == null) {
return false;
}
keyCode = handleRemapping(mapping, event);
if (keyCode == 0) {
return true;
}
// 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.
if (SystemClock.uptimeMillis() - event.getDownTime() < ControllerHandler.MINIMUM_BUTTON_DOWN_TIME_MS)
{
// Since our sleep time is so short (10 ms), it shouldn't cause a problem doing this in the
// UI thread.
try {
Thread.sleep(ControllerHandler.MINIMUM_BUTTON_DOWN_TIME_MS);
} catch (InterruptedException e) {}
}
switch (keyCode) {
case KeyEvent.KEYCODE_BUTTON_MODE:
inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_START:
case KeyEvent.KEYCODE_MENU:
inputMap &= ~ControllerPacket.PLAY_FLAG;
break;
case KeyEvent.KEYCODE_BACK:
case KeyEvent.KEYCODE_BUTTON_SELECT:
inputMap &= ~ControllerPacket.BACK_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_LEFT:
inputMap &= ~ControllerPacket.LEFT_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
inputMap &= ~ControllerPacket.RIGHT_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_UP:
inputMap &= ~ControllerPacket.UP_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
inputMap &= ~ControllerPacket.DOWN_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_B:
inputMap &= ~ControllerPacket.B_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_BUTTON_A:
inputMap &= ~ControllerPacket.A_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_X:
inputMap &= ~ControllerPacket.X_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_Y:
inputMap &= ~ControllerPacket.Y_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_L1:
inputMap &= ~ControllerPacket.LB_FLAG;
lastLbUpTime = SystemClock.uptimeMillis();
break;
case KeyEvent.KEYCODE_BUTTON_R1:
inputMap &= ~ControllerPacket.RB_FLAG;
lastRbUpTime = SystemClock.uptimeMillis();
break;
case KeyEvent.KEYCODE_BUTTON_THUMBL:
inputMap &= ~ControllerPacket.LS_CLK_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_THUMBR:
inputMap &= ~ControllerPacket.RS_CLK_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_L2:
leftTrigger = 0;
break;
case KeyEvent.KEYCODE_BUTTON_R2:
rightTrigger = 0;
break;
default:
return false;
}
// Check if we're emulating the select button
if ((emulatingButtonFlags & ControllerHandler.EMULATING_SELECT) != 0)
{
// If either start or LB is up, select comes up too
if ((inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
(inputMap & ControllerPacket.LB_FLAG) == 0)
{
inputMap &= ~ControllerPacket.BACK_FLAG;
emulatingButtonFlags &= ~ControllerHandler.EMULATING_SELECT;
try {
Thread.sleep(EMULATED_SELECT_UP_DELAY_MS);
} catch (InterruptedException e) {}
}
}
// Check if we're emulating the special button
if ((emulatingButtonFlags & ControllerHandler.EMULATING_SPECIAL) != 0)
{
// If either start or select and RB is up, the special button comes up too
if ((inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
((inputMap & ControllerPacket.BACK_FLAG) == 0 &&
(inputMap & ControllerPacket.RB_FLAG) == 0))
{
inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
emulatingButtonFlags &= ~ControllerHandler.EMULATING_SPECIAL;
try {
Thread.sleep(EMULATED_SPECIAL_UP_DELAY_MS);
} catch (InterruptedException e) {}
}
}
sendControllerInputPacket();
return true;
}
public boolean handleButtonDown(int keyCode, KeyEvent event) {
ControllerMapping mapping = getMappingForDevice(event.getDevice());
if (mapping == null) {
return false;
}
keyCode = handleRemapping(mapping, event);
if (keyCode == 0) {
return true;
}
switch (keyCode) {
case KeyEvent.KEYCODE_BUTTON_MODE:
inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_START:
case KeyEvent.KEYCODE_MENU:
inputMap |= ControllerPacket.PLAY_FLAG;
break;
case KeyEvent.KEYCODE_BACK:
case KeyEvent.KEYCODE_BUTTON_SELECT:
inputMap |= ControllerPacket.BACK_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_LEFT:
inputMap |= ControllerPacket.LEFT_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
inputMap |= ControllerPacket.RIGHT_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_UP:
inputMap |= ControllerPacket.UP_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
inputMap |= ControllerPacket.DOWN_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_B:
inputMap |= ControllerPacket.B_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_BUTTON_A:
inputMap |= ControllerPacket.A_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_X:
inputMap |= ControllerPacket.X_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_Y:
inputMap |= ControllerPacket.Y_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_L1:
inputMap |= ControllerPacket.LB_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_R1:
inputMap |= ControllerPacket.RB_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_THUMBL:
inputMap |= ControllerPacket.LS_CLK_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_THUMBR:
inputMap |= ControllerPacket.RS_CLK_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_L2:
leftTrigger = (byte)0xFF;
break;
case KeyEvent.KEYCODE_BUTTON_R2:
rightTrigger = (byte)0xFF;
break;
default:
return false;
}
// Start+LB acts like select for controllers with one button
if ((inputMap & ControllerPacket.PLAY_FLAG) != 0 &&
((inputMap & ControllerPacket.LB_FLAG) != 0 ||
SystemClock.uptimeMillis() - lastLbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS))
{
inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG);
inputMap |= ControllerPacket.BACK_FLAG;
emulatingButtonFlags |= ControllerHandler.EMULATING_SELECT;
}
// We detect select+start or start+RB as the special button combo
if (((inputMap & ControllerPacket.RB_FLAG) != 0 ||
(SystemClock.uptimeMillis() - lastRbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS) ||
(inputMap & ControllerPacket.BACK_FLAG) != 0) &&
(inputMap & ControllerPacket.PLAY_FLAG) != 0)
{
inputMap &= ~(ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG);
inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL;
}
sendControllerInputPacket();
return true;
}
class ControllerMapping {
public int leftStickXAxis = -1;
public int leftStickYAxis = -1;
public float leftStickDeadzoneRadius;
public int rightStickXAxis = -1;
public int rightStickYAxis = -1;
public float rightStickDeadzoneRadius;
public int leftTriggerAxis = -1;
public int rightTriggerAxis = -1;
public boolean triggersIdleNegative;
public int hatXAxis = -1;
public int hatYAxis = -1;
public float hatXDeadzone;
public float hatYDeadzone;
public boolean isDualShock4;
public boolean isXboxController;
}
}

View File

@@ -0,0 +1,260 @@
package com.limelight.binding.input;
import android.view.KeyEvent;
import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.input.KeycodeTranslator;
/**
* Class to translate a Android key code into the codes GFE is expecting
* @author Diego Waxemberg
* @author Cameron Gutman
*/
public class KeyboardTranslator extends KeycodeTranslator {
/**
* GFE's prefix for every key code
*/
public static final short KEY_PREFIX = (short) 0x80;
public static final int VK_0 = 48;
public static final int VK_9 = 57;
public static final int VK_A = 65;
public static final int VK_Z = 90;
public static final int VK_ALT = 18;
public static final int VK_NUMPAD0 = 96;
public static final int VK_BACK_SLASH = 92;
public static final int VK_CAPS_LOCK = 20;
public static final int VK_CLEAR = 12;
public static final int VK_COMMA = 44;
public static final int VK_CONTROL = 17;
public static final int VK_BACK_SPACE = 8;
public static final int VK_EQUALS = 61;
public static final int VK_ESCAPE = 27;
public static final int VK_F1 = 112;
public static final int VK_PERIOD = 46;
public static final int VK_INSERT = 155;
public static final int VK_OPEN_BRACKET = 91;
public static final int VK_WINDOWS = 524;
public static final int VK_MINUS = 45;
public static final int VK_END = 35;
public static final int VK_HOME = 36;
public static final int VK_NUM_LOCK = 144;
public static final int VK_PAGE_UP = 33;
public static final int VK_PAGE_DOWN = 34;
public static final int VK_PLUS = 521;
public static final int VK_CLOSE_BRACKET = 93;
public static final int VK_SCROLL_LOCK = 145;
public static final int VK_SEMICOLON = 59;
public static final int VK_SHIFT = 16;
public static final int VK_SLASH = 47;
public static final int VK_SPACE = 32;
public static final int VK_PRINTSCREEN = 154;
public static final int VK_TAB = 9;
public static final int VK_LEFT = 37;
public static final int VK_RIGHT = 39;
public static final int VK_UP = 38;
public static final int VK_DOWN = 40;
public static final int VK_BACK_QUOTE = 192;
public static final int VK_QUOTE = 222;
public static final int VK_PAUSE = 19;
/**
* Constructs a new translator for the specified connection
* @param conn the connection to which the translated codes are sent
*/
public KeyboardTranslator(NvConnection conn) {
super(conn);
}
/**
* Translates the given keycode and returns the GFE keycode
* @param keycode the code to be translated
* @return a GFE keycode for the given keycode
*/
@Override
public short translate(int keycode) {
int translated;
/* There seems to be no clean mapping between Android key codes
* and what Nvidia sends over the wire. If someone finds one,
* I'll happily delete this code :)
*/
if (keycode >= KeyEvent.KEYCODE_0 &&
keycode <= KeyEvent.KEYCODE_9) {
translated = (keycode - KeyEvent.KEYCODE_0) + VK_0;
}
else if (keycode >= KeyEvent.KEYCODE_A &&
keycode <= KeyEvent.KEYCODE_Z) {
translated = (keycode - KeyEvent.KEYCODE_A) + VK_A;
}
else if (keycode >= KeyEvent.KEYCODE_NUMPAD_0 &&
keycode <= KeyEvent.KEYCODE_NUMPAD_9) {
translated = (keycode - KeyEvent.KEYCODE_NUMPAD_0) + VK_NUMPAD0;
}
else if (keycode >= KeyEvent.KEYCODE_F1 &&
keycode <= KeyEvent.KEYCODE_F12) {
translated = (keycode - KeyEvent.KEYCODE_F1) + VK_F1;
}
else {
switch (keycode) {
case KeyEvent.KEYCODE_ALT_LEFT:
case KeyEvent.KEYCODE_ALT_RIGHT:
translated = VK_ALT;
break;
case KeyEvent.KEYCODE_BACKSLASH:
translated = 0xdc;
break;
case KeyEvent.KEYCODE_CAPS_LOCK:
translated = VK_CAPS_LOCK;
break;
case KeyEvent.KEYCODE_CLEAR:
translated = VK_CLEAR;
break;
case KeyEvent.KEYCODE_COMMA:
translated = 0xbc;
break;
case KeyEvent.KEYCODE_CTRL_LEFT:
case KeyEvent.KEYCODE_CTRL_RIGHT:
translated = VK_CONTROL;
break;
case KeyEvent.KEYCODE_DEL:
translated = VK_BACK_SPACE;
break;
case KeyEvent.KEYCODE_ENTER:
translated = 0x0d;
break;
case KeyEvent.KEYCODE_EQUALS:
translated = 0xbb;
break;
case KeyEvent.KEYCODE_ESCAPE:
translated = VK_ESCAPE;
break;
case KeyEvent.KEYCODE_FORWARD_DEL:
// Nvidia maps period to delete
translated = VK_PERIOD;
break;
case KeyEvent.KEYCODE_INSERT:
translated = -1;
break;
case KeyEvent.KEYCODE_LEFT_BRACKET:
translated = 0xdb;
break;
case KeyEvent.KEYCODE_META_LEFT:
case KeyEvent.KEYCODE_META_RIGHT:
translated = VK_WINDOWS;
break;
case KeyEvent.KEYCODE_MINUS:
translated = 0xbd;
break;
case KeyEvent.KEYCODE_MOVE_END:
translated = VK_END;
break;
case KeyEvent.KEYCODE_MOVE_HOME:
translated = VK_HOME;
break;
case KeyEvent.KEYCODE_NUM_LOCK:
translated = VK_NUM_LOCK;
break;
case KeyEvent.KEYCODE_PAGE_DOWN:
translated = VK_PAGE_DOWN;
break;
case KeyEvent.KEYCODE_PAGE_UP:
translated = VK_PAGE_UP;
break;
case KeyEvent.KEYCODE_PERIOD:
translated = 0xbe;
break;
case KeyEvent.KEYCODE_RIGHT_BRACKET:
translated = 0xdd;
break;
case KeyEvent.KEYCODE_SCROLL_LOCK:
translated = VK_SCROLL_LOCK;
break;
case KeyEvent.KEYCODE_SEMICOLON:
translated = 0xba;
break;
case KeyEvent.KEYCODE_SHIFT_LEFT:
case KeyEvent.KEYCODE_SHIFT_RIGHT:
translated = VK_SHIFT;
break;
case KeyEvent.KEYCODE_SLASH:
translated = 0xbf;
break;
case KeyEvent.KEYCODE_SPACE:
translated = VK_SPACE;
break;
case KeyEvent.KEYCODE_SYSRQ:
// Android defines this as SysRq/PrntScrn
translated = VK_PRINTSCREEN;
break;
case KeyEvent.KEYCODE_TAB:
translated = VK_TAB;
break;
case KeyEvent.KEYCODE_DPAD_LEFT:
translated = VK_LEFT;
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
translated = VK_RIGHT;
break;
case KeyEvent.KEYCODE_DPAD_UP:
translated = VK_UP;
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
translated = VK_DOWN;
break;
case KeyEvent.KEYCODE_GRAVE:
translated = VK_BACK_QUOTE;
break;
case KeyEvent.KEYCODE_APOSTROPHE:
translated = 0xde;
break;
case KeyEvent.KEYCODE_BREAK:
translated = VK_PAUSE;
break;
default:
System.out.println("No key for "+keycode);
return 0;
}
}
return (short) ((KEY_PREFIX << 8) | translated);
}
}

View File

@@ -0,0 +1,93 @@
package com.limelight.binding.input;
import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.input.MouseButtonPacket;
public class TouchContext {
private int lastTouchX = 0;
private int lastTouchY = 0;
private int originalTouchX = 0;
private int originalTouchY = 0;
private long originalTouchTime = 0;
private NvConnection conn;
private int actionIndex;
private static final int TAP_MOVEMENT_THRESHOLD = 10;
private static final int TAP_TIME_THRESHOLD = 250;
public TouchContext(NvConnection conn, int actionIndex)
{
this.conn = conn;
this.actionIndex = actionIndex;
}
private boolean isTap()
{
int xDelta = Math.abs(lastTouchX - originalTouchX);
int yDelta = Math.abs(lastTouchY - originalTouchY);
long timeDelta = System.currentTimeMillis() - originalTouchTime;
return xDelta <= TAP_MOVEMENT_THRESHOLD &&
yDelta <= TAP_MOVEMENT_THRESHOLD &&
timeDelta <= TAP_TIME_THRESHOLD;
}
private byte getMouseButtonIndex()
{
if (actionIndex == 1) {
return MouseButtonPacket.BUTTON_RIGHT;
}
else {
return MouseButtonPacket.BUTTON_LEFT;
}
}
public boolean touchDownEvent(int eventX, int eventY)
{
originalTouchX = lastTouchX = eventX;
originalTouchY = lastTouchY = eventY;
originalTouchTime = System.currentTimeMillis();
return true;
}
public void touchUpEvent(int eventX, int eventY)
{
if (isTap())
{
byte buttonIndex = getMouseButtonIndex();
// Lower the mouse button
conn.sendMouseButtonDown(buttonIndex);
// We need to sleep a bit here because some games
// do input detection by polling
try {
Thread.sleep(100);
} catch (InterruptedException e) {}
// Raise the mouse button
conn.sendMouseButtonUp(buttonIndex);
}
}
public boolean touchMoveEvent(int eventX, int eventY)
{
if (eventX != lastTouchX || eventY != lastTouchY)
{
// We only send moves for the primary touch point
if (actionIndex == 0) {
conn.sendMouseMove((short)(eventX - lastTouchX),
(short)(eventY - lastTouchY));
}
lastTouchX = eventX;
lastTouchY = eventY;
return true;
}
return false;
}
}

View File

@@ -0,0 +1,41 @@
package com.limelight.binding.input.evdev;
public class EvdevEvent {
public static final int EVDEV_MIN_EVENT_SIZE = 16;
public static final int EVDEV_MAX_EVENT_SIZE = 24;
/* Event types */
public static final short EV_SYN = 0x00;
public static final short EV_KEY = 0x01;
public static final short EV_REL = 0x02;
public static final short EV_MSC = 0x04;
/* Relative axes */
public static final short REL_X = 0x00;
public static final short REL_Y = 0x01;
public static final short REL_WHEEL = 0x08;
/* Buttons */
public static final short BTN_LEFT = 0x110;
public static final short BTN_RIGHT = 0x111;
public static final short BTN_MIDDLE = 0x112;
public static final short BTN_SIDE = 0x113;
public static final short BTN_EXTRA = 0x114;
public static final short BTN_FORWARD = 0x115;
public static final short BTN_BACK = 0x116;
public static final short BTN_TASK = 0x117;
public static final short BTN_GAMEPAD = 0x130;
/* Keys */
public static final short KEY_Q = 16;
public short type;
public short code;
public int value;
public EvdevEvent(short type, short code, int value) {
this.type = type;
this.code = code;
this.value = value;
}
}

View File

@@ -0,0 +1,167 @@
package com.limelight.binding.input.evdev;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import com.limelight.LimeLog;
public class EvdevHandler {
private String absolutePath;
private EvdevListener listener;
private boolean shutdown = false;
private int fd = -1;
private Thread handlerThread = new Thread() {
@Override
public void run() {
// All the finally blocks here make this code look like a mess
// but it's important that we get this right to avoid causing
// system-wide input problems.
// Open the /dev/input/eventX file
fd = EvdevReader.open(absolutePath);
if (fd == -1) {
LimeLog.warning("Unable to open "+absolutePath);
return;
}
try {
// Check if it's a mouse or keyboard, but not a gamepad
if ((!EvdevReader.isMouse(fd) && !EvdevReader.isAlphaKeyboard(fd)) ||
EvdevReader.isGamepad(fd)) {
// We only handle keyboards and mice
return;
}
// Grab it for ourselves
if (!EvdevReader.grab(fd)) {
LimeLog.warning("Unable to grab "+absolutePath);
return;
}
LimeLog.info("Grabbed device for raw keyboard/mouse input: "+absolutePath);
ByteBuffer buffer = ByteBuffer.allocate(EvdevEvent.EVDEV_MAX_EVENT_SIZE).order(ByteOrder.nativeOrder());
try {
int deltaX = 0;
int deltaY = 0;
byte deltaScroll = 0;
while (!isInterrupted() && !shutdown) {
EvdevEvent event = EvdevReader.read(fd, buffer);
if (event == null) {
return;
}
switch (event.type)
{
case EvdevEvent.EV_SYN:
if (deltaX != 0 || deltaY != 0) {
listener.mouseMove(deltaX, deltaY);
deltaX = deltaY = 0;
}
if (deltaScroll != 0) {
listener.mouseScroll(deltaScroll);
deltaScroll = 0;
}
break;
case EvdevEvent.EV_REL:
switch (event.code)
{
case EvdevEvent.REL_X:
deltaX = event.value;
break;
case EvdevEvent.REL_Y:
deltaY = event.value;
break;
case EvdevEvent.REL_WHEEL:
deltaScroll = (byte) event.value;
break;
}
break;
case EvdevEvent.EV_KEY:
switch (event.code)
{
case EvdevEvent.BTN_LEFT:
listener.mouseButtonEvent(EvdevListener.BUTTON_LEFT,
event.value != 0);
break;
case EvdevEvent.BTN_MIDDLE:
listener.mouseButtonEvent(EvdevListener.BUTTON_MIDDLE,
event.value != 0);
break;
case EvdevEvent.BTN_RIGHT:
listener.mouseButtonEvent(EvdevListener.BUTTON_RIGHT,
event.value != 0);
break;
case EvdevEvent.BTN_SIDE:
case EvdevEvent.BTN_EXTRA:
case EvdevEvent.BTN_FORWARD:
case EvdevEvent.BTN_BACK:
case EvdevEvent.BTN_TASK:
// Other unhandled mouse buttons
break;
default:
// We got some unrecognized button. This means
// someone is trying to use the other device in this
// "combination" input device. We'll try to handle
// it via keyboard, but we're not going to disconnect
// if we can't
short keyCode = EvdevTranslator.translateEvdevKeyCode(event.code);
if (keyCode != 0) {
listener.keyboardEvent(event.value != 0, keyCode);
}
break;
}
break;
case EvdevEvent.EV_MSC:
break;
}
}
} finally {
// Release our grab
EvdevReader.ungrab(fd);
}
} finally {
// Close the file
EvdevReader.close(fd);
}
}
};
public EvdevHandler(String absolutePath, EvdevListener listener) {
this.absolutePath = absolutePath;
this.listener = listener;
}
public void start() {
handlerThread.start();
}
public void stop() {
// Close the fd. It doesn't matter if this races
// with the handler thread. We'll close this out from
// under the thread to wake it up
if (fd != -1) {
EvdevReader.close(fd);
}
shutdown = true;
handlerThread.interrupt();
try {
handlerThread.join();
} catch (InterruptedException e) {}
}
public void notifyDeleted() {
stop();
}
}

View File

@@ -0,0 +1,12 @@
package com.limelight.binding.input.evdev;
public interface EvdevListener {
public static final int BUTTON_LEFT = 1;
public static final int BUTTON_MIDDLE = 2;
public static final int BUTTON_RIGHT = 3;
public void mouseMove(int deltaX, int deltaY);
public void mouseButtonEvent(int buttonId, boolean down);
public void mouseScroll(byte amount);
public void keyboardEvent(boolean buttonDown, short keyCode);
}

View File

@@ -0,0 +1,102 @@
package com.limelight.binding.input.evdev;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.Locale;
import com.limelight.LimeLog;
public class EvdevReader {
static {
System.loadLibrary("evdev_reader");
}
// Requires root to chmod /dev/input/eventX
public static boolean setPermissions(String[] files, int octalPermissions) {
ProcessBuilder builder = new ProcessBuilder("su");
try {
Process p = builder.start();
OutputStream stdin = p.getOutputStream();
for (String file : files) {
stdin.write(String.format((Locale)null, "chmod %o %s\n", octalPermissions, file).getBytes("UTF-8"));
}
stdin.write("exit\n".getBytes("UTF-8"));
stdin.flush();
p.waitFor();
p.destroy();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
return false;
}
// Returns the fd to be passed to other function or -1 on error
public static native int open(String fileName);
// Prevent other apps (including Android itself) from using the device while "grabbed"
public static native boolean grab(int fd);
public static native boolean ungrab(int fd);
// Used for checking device capabilities
public static native boolean hasRelAxis(int fd, short axis);
public static native boolean hasAbsAxis(int fd, short axis);
public static native boolean hasKey(int fd, short key);
public static boolean isMouse(int fd) {
// This is the same check that Android does in EventHub.cpp
return hasRelAxis(fd, EvdevEvent.REL_X) &&
hasRelAxis(fd, EvdevEvent.REL_Y) &&
hasKey(fd, EvdevEvent.BTN_LEFT);
}
public static boolean isAlphaKeyboard(int fd) {
// This is the same check that Android does in EventHub.cpp
return hasKey(fd, EvdevEvent.KEY_Q);
}
public static boolean isGamepad(int fd) {
return hasKey(fd, EvdevEvent.BTN_GAMEPAD);
}
// Returns the bytes read or -1 on error
private static native int read(int fd, byte[] buffer);
// Takes a byte buffer to use to read the output into.
// This buffer MUST be in native byte order and at least
// EVDEV_MAX_EVENT_SIZE bytes long.
public static EvdevEvent read(int fd, ByteBuffer buffer) {
int bytesRead = read(fd, buffer.array());
if (bytesRead < 0) {
LimeLog.warning("Failed to read: "+bytesRead);
return null;
}
else if (bytesRead < EvdevEvent.EVDEV_MIN_EVENT_SIZE) {
LimeLog.warning("Short read: "+bytesRead);
return null;
}
buffer.limit(bytesRead);
buffer.rewind();
// Throw away the time stamp
if (bytesRead == EvdevEvent.EVDEV_MAX_EVENT_SIZE) {
buffer.getLong();
buffer.getLong();
} else {
buffer.getInt();
buffer.getInt();
}
return new EvdevEvent(buffer.getShort(), buffer.getShort(), buffer.getInt());
}
// Closes the fd from open()
public static native int close(int fd);
}

View File

@@ -0,0 +1,139 @@
package com.limelight.binding.input.evdev;
import android.view.KeyEvent;
public class EvdevTranslator {
public static final short EVDEV_KEY_CODES[] = {
0, //KeyEvent.VK_RESERVED
KeyEvent.KEYCODE_ESCAPE,
KeyEvent.KEYCODE_1,
KeyEvent.KEYCODE_2,
KeyEvent.KEYCODE_3,
KeyEvent.KEYCODE_4,
KeyEvent.KEYCODE_5,
KeyEvent.KEYCODE_6,
KeyEvent.KEYCODE_7,
KeyEvent.KEYCODE_8,
KeyEvent.KEYCODE_9,
KeyEvent.KEYCODE_0,
KeyEvent.KEYCODE_MINUS,
KeyEvent.KEYCODE_EQUALS,
KeyEvent.KEYCODE_DEL,
KeyEvent.KEYCODE_TAB,
KeyEvent.KEYCODE_Q,
KeyEvent.KEYCODE_W,
KeyEvent.KEYCODE_E,
KeyEvent.KEYCODE_R,
KeyEvent.KEYCODE_T,
KeyEvent.KEYCODE_Y,
KeyEvent.KEYCODE_U,
KeyEvent.KEYCODE_I,
KeyEvent.KEYCODE_O,
KeyEvent.KEYCODE_P,
KeyEvent.KEYCODE_LEFT_BRACKET,
KeyEvent.KEYCODE_RIGHT_BRACKET,
KeyEvent.KEYCODE_ENTER,
KeyEvent.KEYCODE_CTRL_LEFT,
KeyEvent.KEYCODE_A,
KeyEvent.KEYCODE_S,
KeyEvent.KEYCODE_D,
KeyEvent.KEYCODE_F,
KeyEvent.KEYCODE_G,
KeyEvent.KEYCODE_H,
KeyEvent.KEYCODE_J,
KeyEvent.KEYCODE_K,
KeyEvent.KEYCODE_L,
KeyEvent.KEYCODE_SEMICOLON,
KeyEvent.KEYCODE_APOSTROPHE,
KeyEvent.KEYCODE_GRAVE,
KeyEvent.KEYCODE_SHIFT_LEFT,
KeyEvent.KEYCODE_BACKSLASH,
KeyEvent.KEYCODE_Z,
KeyEvent.KEYCODE_X,
KeyEvent.KEYCODE_C,
KeyEvent.KEYCODE_V,
KeyEvent.KEYCODE_B,
KeyEvent.KEYCODE_N,
KeyEvent.KEYCODE_M,
KeyEvent.KEYCODE_COMMA,
KeyEvent.KEYCODE_PERIOD,
KeyEvent.KEYCODE_SLASH,
KeyEvent.KEYCODE_SHIFT_RIGHT,
KeyEvent.KEYCODE_NUMPAD_MULTIPLY,
KeyEvent.KEYCODE_ALT_LEFT,
KeyEvent.KEYCODE_SPACE,
KeyEvent.KEYCODE_CAPS_LOCK,
KeyEvent.KEYCODE_F1,
KeyEvent.KEYCODE_F2,
KeyEvent.KEYCODE_F3,
KeyEvent.KEYCODE_F4,
KeyEvent.KEYCODE_F5,
KeyEvent.KEYCODE_F6,
KeyEvent.KEYCODE_F7,
KeyEvent.KEYCODE_F8,
KeyEvent.KEYCODE_F9,
KeyEvent.KEYCODE_F10,
KeyEvent.KEYCODE_NUM_LOCK,
KeyEvent.KEYCODE_SCROLL_LOCK,
KeyEvent.KEYCODE_NUMPAD_7,
KeyEvent.KEYCODE_NUMPAD_8,
KeyEvent.KEYCODE_NUMPAD_9,
KeyEvent.KEYCODE_NUMPAD_SUBTRACT,
KeyEvent.KEYCODE_NUMPAD_4,
KeyEvent.KEYCODE_NUMPAD_5,
KeyEvent.KEYCODE_NUMPAD_6,
KeyEvent.KEYCODE_NUMPAD_ADD,
KeyEvent.KEYCODE_NUMPAD_1,
KeyEvent.KEYCODE_NUMPAD_2,
KeyEvent.KEYCODE_NUMPAD_3,
KeyEvent.KEYCODE_NUMPAD_0,
KeyEvent.KEYCODE_NUMPAD_DOT,
0,
0, //KeyEvent.VK_ZENKAKUHANKAKU,
0, //KeyEvent.VK_102ND,
KeyEvent.KEYCODE_F11,
KeyEvent.KEYCODE_F12,
0, //KeyEvent.VK_RO,
0, //KeyEvent.VK_KATAKANA,
0, //KeyEvent.VK_HIRAGANA,
0, //KeyEvent.VK_HENKAN,
0, //KeyEvent.VK_KATAKANAHIRAGANA,
0, //KeyEvent.VK_MUHENKAN,
0, //KeyEvent.VK_KPJPCOMMA,
KeyEvent.KEYCODE_NUMPAD_ENTER,
KeyEvent.KEYCODE_CTRL_RIGHT,
KeyEvent.KEYCODE_NUMPAD_DIVIDE,
KeyEvent.KEYCODE_SYSRQ,
KeyEvent.KEYCODE_ALT_RIGHT,
0, //KeyEvent.VK_LINEFEED,
KeyEvent.KEYCODE_HOME,
KeyEvent.KEYCODE_DPAD_UP,
KeyEvent.KEYCODE_PAGE_UP,
KeyEvent.KEYCODE_DPAD_LEFT,
KeyEvent.KEYCODE_DPAD_RIGHT,
KeyEvent.KEYCODE_MOVE_END,
KeyEvent.KEYCODE_DPAD_DOWN,
KeyEvent.KEYCODE_PAGE_DOWN,
KeyEvent.KEYCODE_INSERT,
KeyEvent.KEYCODE_FORWARD_DEL,
0, //KeyEvent.VK_MACRO,
0, //KeyEvent.VK_MUTE,
0, //KeyEvent.VK_VOLUMEDOWN,
0, //KeyEvent.VK_VOLUMEUP,
0, //KeyEvent.VK_POWER, /* SC System Power Down */
KeyEvent.KEYCODE_NUMPAD_EQUALS,
0, //KeyEvent.VK_KPPLUSMINUS,
KeyEvent.KEYCODE_BREAK,
0, //KeyEvent.VK_SCALE, /* AL Compiz Scale (Expose) */
};
public static short translateEvdevKeyCode(short evdevKeyCode) {
if (evdevKeyCode < EVDEV_KEY_CODES.length) {
return EVDEV_KEY_CODES[evdevKeyCode];
}
return 0;
}
}

View File

@@ -0,0 +1,171 @@
package com.limelight.binding.input.evdev;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import com.limelight.LimeLog;
import android.os.FileObserver;
public class EvdevWatcher {
private static final String PATH = "/dev/input";
private static final String REQUIRED_FILE_PREFIX = "event";
private HashMap<String, EvdevHandler> handlers = new HashMap<String, EvdevHandler>();
private boolean shutdown = false;
private boolean init = false;
private boolean ungrabbed = false;
private EvdevListener listener;
private Thread startThread;
private FileObserver observer = new FileObserver(PATH, FileObserver.CREATE | FileObserver.DELETE) {
@Override
public void onEvent(int event, String fileName) {
if (fileName == null) {
return;
}
if (!fileName.startsWith(REQUIRED_FILE_PREFIX)) {
return;
}
synchronized (handlers) {
if (shutdown) {
return;
}
if ((event & FileObserver.CREATE) != 0) {
LimeLog.info("Starting evdev handler for "+fileName);
if (!init) {
// If this a real new device, update permissions again so we can read it
EvdevReader.setPermissions(new String[]{PATH + "/" + fileName}, 0666);
}
EvdevHandler handler = new EvdevHandler(PATH + "/" + fileName, listener);
// If we're ungrabbed now, don't start the handler
if (!ungrabbed) {
handler.start();
}
handlers.put(fileName, handler);
}
if ((event & FileObserver.DELETE) != 0) {
LimeLog.info("Halting evdev handler for "+fileName);
EvdevHandler handler = handlers.remove(fileName);
if (handler != null) {
handler.notifyDeleted();
}
}
}
}
};
public EvdevWatcher(EvdevListener listener) {
this.listener = listener;
}
private File[] rundownWithPermissionsChange(int newPermissions) {
// Rundown existing files
File devInputDir = new File(PATH);
File[] files = devInputDir.listFiles();
if (files == null) {
return new File[0];
}
// Set desired permissions
String[] filePaths = new String[files.length];
for (int i = 0; i < files.length; i++) {
filePaths[i] = files[i].getAbsolutePath();
}
EvdevReader.setPermissions(filePaths, newPermissions);
return files;
}
public void ungrabAll() {
synchronized (handlers) {
// Note that we're ungrabbed for now
ungrabbed = true;
// Stop all handlers
for (EvdevHandler handler : handlers.values()) {
handler.stop();
}
}
}
public void regrabAll() {
synchronized (handlers) {
// We're regrabbing everything now
ungrabbed = false;
for (Map.Entry<String, EvdevHandler> entry : handlers.entrySet()) {
// We need to recreate each entry since we can't reuse a stopped one
entry.setValue(new EvdevHandler(PATH + "/" + entry.getKey(), listener));
entry.getValue().start();
}
}
}
public void start() {
startThread = new Thread() {
@Override
public void run() {
// List all files and allow us access
File[] files = rundownWithPermissionsChange(0666);
init = true;
for (File f : files) {
observer.onEvent(FileObserver.CREATE, f.getName());
}
// Done with initial onEvent calls
init = false;
// Start watching for new files
observer.startWatching();
synchronized (startThread) {
// Wait to be awoken again by shutdown()
try {
startThread.wait();
} catch (InterruptedException e) {}
}
// Giveup eventX permissions
rundownWithPermissionsChange(066);
}
};
startThread.start();
}
public void shutdown() {
// Let start thread cleanup on it's own sweet time
synchronized (startThread) {
startThread.notify();
}
// Stop the observer
observer.stopWatching();
synchronized (handlers) {
// Stop creating new handlers
shutdown = true;
// If we've already ungrabbed, there's nothing else to do
if (ungrabbed) {
return;
}
// Stop all handlers
for (EvdevHandler handler : handlers.values()) {
handler.stop();
}
}
}
}

View File

@@ -0,0 +1,262 @@
package com.limelight.binding.video;
import java.io.BufferedReader;
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;
import android.view.SurfaceHolder;
import com.limelight.LimeLog;
import com.limelight.nvstream.av.ByteBufferDescriptor;
import com.limelight.nvstream.av.DecodeUnit;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.av.video.VideoDepacketizer;
import com.limelight.nvstream.av.video.cpu.AvcDecoder;
public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
private Thread rendererThread;
private int targetFps;
private static final int DECODER_BUFFER_SIZE = 92*1024;
private ByteBuffer decoderBuffer;
// Only sleep if the difference is above this value
private static final int WAIT_CEILING_MS = 8;
private static final int LOW_PERF = 1;
private static final int MED_PERF = 2;
private static final int HIGH_PERF = 3;
private int totalFrames;
private long totalTimeMs;
private int cpuCount = Runtime.getRuntime().availableProcessors();
@SuppressWarnings("unused")
private int findOptimalPerformanceLevel() {
StringBuilder cpuInfo = new StringBuilder();
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(new File("/proc/cpuinfo")));
for (;;) {
int ch = br.read();
if (ch == -1)
break;
cpuInfo.append((char)ch);
}
// Here we're doing very simple heuristics based on CPU model
String cpuInfoStr = cpuInfo.toString();
// We order them from greatest to least for proper detection
// of devices with multiple sets of cores (like Exynos 5 Octa)
// TODO Make this better (only even kind of works on ARM)
if (Build.FINGERPRINT.contains("generic")) {
// Emulator
return LOW_PERF;
}
else if (cpuInfoStr.contains("0xc0f")) {
// Cortex-A15
return MED_PERF;
}
else if (cpuInfoStr.contains("0xc09")) {
// Cortex-A9
return LOW_PERF;
}
else if (cpuInfoStr.contains("0xc07")) {
// Cortex-A7
return LOW_PERF;
}
else {
// Didn't have anything we're looking for
return MED_PERF;
}
} catch (IOException e) {
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {}
}
}
// Couldn't read cpuinfo, so assume medium
return MED_PERF;
}
@Override
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
this.targetFps = redrawRate;
int perfLevel = LOW_PERF; //findOptimalPerformanceLevel();
int threadCount;
int avcFlags = 0;
switch (perfLevel) {
case HIGH_PERF:
// Single threaded low latency decode is ideal but hard to acheive
avcFlags = AvcDecoder.LOW_LATENCY_DECODE;
threadCount = 1;
break;
case LOW_PERF:
// Disable the loop filter for performance reasons
avcFlags = AvcDecoder.DISABLE_LOOP_FILTER |
AvcDecoder.FAST_BILINEAR_FILTERING |
AvcDecoder.FAST_DECODE;
// Use plenty of threads to try to utilize the CPU as best we can
threadCount = cpuCount - 1;
break;
default:
case MED_PERF:
avcFlags = AvcDecoder.BILINEAR_FILTERING |
AvcDecoder.FAST_DECODE;
// Only use 2 threads to minimize frame processing latency
threadCount = 2;
break;
}
// If the user wants quality, we'll remove the low IQ flags
if ((drFlags & VideoDecoderRenderer.FLAG_PREFER_QUALITY) != 0) {
// Make sure the loop filter is enabled
avcFlags &= ~AvcDecoder.DISABLE_LOOP_FILTER;
// Disable the non-compliant speed optimizations
avcFlags &= ~AvcDecoder.FAST_DECODE;
LimeLog.info("Using high quality decoding");
}
SurfaceHolder sh = (SurfaceHolder)renderTarget;
sh.setFormat(PixelFormat.RGBX_8888);
int err = AvcDecoder.init(width, height, avcFlags, threadCount);
if (err != 0) {
throw new IllegalStateException("AVC decoder initialization failure: "+err);
}
AvcDecoder.setRenderTarget(sh.getSurface());
decoderBuffer = ByteBuffer.allocate(DECODER_BUFFER_SIZE + AvcDecoder.getInputPaddingSize());
LimeLog.info("Using software decoding (performance level: "+perfLevel+")");
return true;
}
@Override
public boolean start(final VideoDepacketizer depacketizer) {
rendererThread = new Thread() {
@Override
public void run() {
long nextFrameTime = System.currentTimeMillis();
DecodeUnit du;
while (!isInterrupted())
{
du = depacketizer.pollNextDecodeUnit();
if (du != null) {
submitDecodeUnit(du);
depacketizer.freeDecodeUnit(du);
}
long diff = nextFrameTime - System.currentTimeMillis();
if (diff > WAIT_CEILING_MS) {
LockSupport.parkNanos(1);
continue;
}
nextFrameTime = computePresentationTimeMs(targetFps);
AvcDecoder.redraw();
}
}
};
rendererThread.setName("Video - Renderer (CPU)");
rendererThread.setPriority(Thread.MAX_PRIORITY);
rendererThread.start();
return true;
}
private long computePresentationTimeMs(int frameRate) {
return System.currentTimeMillis() + (1000 / frameRate);
}
@Override
public void stop() {
rendererThread.interrupt();
try {
rendererThread.join();
} catch (InterruptedException e) { }
}
@Override
public void release() {
AvcDecoder.destroy();
}
private boolean submitDecodeUnit(DecodeUnit decodeUnit) {
byte[] data;
// Use the reserved decoder buffer if this decode unit will fit
if (decodeUnit.getDataLength() <= DECODER_BUFFER_SIZE) {
decoderBuffer.clear();
for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
decoderBuffer.put(bbd.data, bbd.offset, bbd.length);
}
data = decoderBuffer.array();
}
else {
data = new byte[decodeUnit.getDataLength()+AvcDecoder.getInputPaddingSize()];
int offset = 0;
for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
System.arraycopy(bbd.data, bbd.offset, data, offset, bbd.length);
offset += bbd.length;
}
}
boolean success = (AvcDecoder.decode(data, 0, decodeUnit.getDataLength()) == 0);
if (success) {
long timeAfterDecode = System.currentTimeMillis();
// Add delta time to the totals (excluding probable outliers)
long delta = timeAfterDecode - decodeUnit.getReceiveTimestamp();
if (delta >= 0 && delta < 300) {
totalTimeMs += delta;
totalFrames++;
}
}
return success;
}
@Override
public int getCapabilities() {
return 0;
}
@Override
public int getAverageDecoderLatency() {
return 0;
}
@Override
public int getAverageEndToEndLatency() {
if (totalFrames == 0) {
return 0;
}
return (int)(totalTimeMs / totalFrames);
}
}

View File

@@ -0,0 +1,77 @@
package com.limelight.binding.video;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.av.video.VideoDepacketizer;
public class ConfigurableDecoderRenderer implements VideoDecoderRenderer {
private VideoDecoderRenderer decoderRenderer;
@Override
public void release() {
if (decoderRenderer != null) {
decoderRenderer.release();
}
}
@Override
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
if (decoderRenderer == null) {
throw new IllegalStateException("ConfigurableDecoderRenderer not initialized");
}
return decoderRenderer.setup(width, height, redrawRate, renderTarget, drFlags);
}
public void initializeWithFlags(int drFlags) {
if ((drFlags & VideoDecoderRenderer.FLAG_FORCE_HARDWARE_DECODING) != 0 ||
((drFlags & VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING) == 0 &&
MediaCodecHelper.findProbableSafeDecoder() != null)) {
decoderRenderer = new MediaCodecDecoderRenderer();
}
else {
decoderRenderer = new AndroidCpuDecoderRenderer();
}
}
public boolean isHardwareAccelerated() {
if (decoderRenderer == null) {
throw new IllegalStateException("ConfigurableDecoderRenderer not initialized");
}
return (decoderRenderer instanceof MediaCodecDecoderRenderer);
}
@Override
public boolean start(VideoDepacketizer depacketizer) {
return decoderRenderer.start(depacketizer);
}
@Override
public void stop() {
decoderRenderer.stop();
}
@Override
public int getCapabilities() {
return decoderRenderer.getCapabilities();
}
@Override
public int getAverageDecoderLatency() {
if (decoderRenderer != null) {
return decoderRenderer.getAverageDecoderLatency();
}
else {
return 0;
}
}
@Override
public int getAverageEndToEndLatency() {
if (decoderRenderer != null) {
return decoderRenderer.getAverageEndToEndLatency();
}
else {
return 0;
}
}
}

View File

@@ -0,0 +1,541 @@
package com.limelight.binding.video;
import java.nio.ByteBuffer;
import java.util.Locale;
import java.util.concurrent.locks.LockSupport;
import org.jcodec.codecs.h264.io.model.SeqParameterSet;
import org.jcodec.codecs.h264.io.model.VUIParameters;
import com.limelight.LimeLog;
import com.limelight.nvstream.av.ByteBufferDescriptor;
import com.limelight.nvstream.av.DecodeUnit;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.av.video.VideoDepacketizer;
import android.annotation.TargetApi;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.media.MediaCodec.BufferInfo;
import android.media.MediaCodec.CodecException;
import android.os.Build;
import android.view.SurfaceHolder;
@SuppressWarnings("unused")
public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
private ByteBuffer[] videoDecoderInputBuffers;
private MediaCodec videoDecoder;
private Thread rendererThread;
private boolean needsSpsBitstreamFixup, isExynos4;
private VideoDepacketizer depacketizer;
private boolean adaptivePlayback;
private int initialWidth, initialHeight;
private long lastTimestampUs;
private long totalTimeMs;
private long decoderTimeMs;
private int totalFrames;
private String decoderName;
private int numSpsIn;
private int numPpsIn;
private int numIframeIn;
private static final boolean ENABLE_ASYNC_RENDERER = false;
@TargetApi(Build.VERSION_CODES.KITKAT)
public MediaCodecDecoderRenderer() {
//dumpDecoders();
MediaCodecInfo decoder = MediaCodecHelper.findProbableSafeDecoder();
if (decoder == null) {
decoder = MediaCodecHelper.findFirstDecoder();
}
if (decoder == null) {
// This case is handled later in setup()
return;
}
decoderName = decoder.getName();
// Set decoder-specific attributes
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(decoderName, decoder);
needsSpsBitstreamFixup = MediaCodecHelper.decoderNeedsSpsBitstreamRestrictions(decoderName, decoder);
if (needsSpsBitstreamFixup) {
LimeLog.info("Decoder "+decoderName+" needs SPS bitstream restrictions fixup");
}
isExynos4 = MediaCodecHelper.isExynos4Device();
if (isExynos4) {
LimeLog.info("Decoder "+decoderName+" is on Exynos 4");
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
this.initialWidth = width;
this.initialHeight = height;
if (decoderName == null) {
LimeLog.severe("No available hardware decoder!");
return false;
}
// Codecs have been known to throw all sorts of crazy runtime exceptions
// due to implementation problems
try {
videoDecoder = MediaCodec.createByCodecName(decoderName);
} catch (Exception e) {
return false;
}
MediaFormat videoFormat = MediaFormat.createVideoFormat("video/avc", width, height);
// Adaptive playback can also be enabled by the whitelist on pre-KitKat devices
// so we don't fill these pre-KitKat
if (adaptivePlayback && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
videoFormat.setInteger(MediaFormat.KEY_MAX_WIDTH, width);
videoFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, height);
}
// On Lollipop, we use asynchronous mode to avoid having a busy looping renderer thread
if (ENABLE_ASYNC_RENDERER && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
videoDecoder.setCallback(new MediaCodec.Callback() {
@Override
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
LimeLog.info("Output format changed");
LimeLog.info("New output Format: " + format);
}
@Override
public void onOutputBufferAvailable(MediaCodec codec, int index,
BufferInfo info) {
try {
// FIXME: It looks like we can't frameskip here
codec.releaseOutputBuffer(index, true);
} catch (Exception e) {
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
}
}
@Override
public void onInputBufferAvailable(MediaCodec codec, int index) {
try {
submitDecodeUnit(depacketizer.takeNextDecodeUnit(), codec.getInputBuffer(index), index);
} catch (InterruptedException e) {
// What do we do here?
e.printStackTrace();
} catch (Exception e) {
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
}
}
@Override
public void onError(MediaCodec codec, CodecException e) {
if (e.isTransient()) {
LimeLog.warning(e.getDiagnosticInfo());
e.printStackTrace();
}
else {
LimeLog.severe(e.getDiagnosticInfo());
e.printStackTrace();
}
}
});
}
videoDecoder.configure(videoFormat, ((SurfaceHolder)renderTarget).getSurface(), null, 0);
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
LimeLog.info("Using hardware decoding");
return true;
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void handleDecoderException(MediaCodecDecoderRenderer dr, Exception e, ByteBuffer buf, int codecFlags) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (e instanceof CodecException) {
CodecException codecExc = (CodecException) e;
if (codecExc.isTransient()) {
// We'll let transient exceptions go
LimeLog.warning(codecExc.getDiagnosticInfo());
return;
}
LimeLog.severe(codecExc.getDiagnosticInfo());
}
}
if (buf != null || codecFlags != 0) {
throw new RendererException(dr, e, buf, codecFlags);
}
else {
throw new RendererException(dr, e);
}
}
private void startRendererThread()
{
rendererThread = new Thread() {
@SuppressWarnings("deprecation")
@Override
public void run() {
BufferInfo info = new BufferInfo();
DecodeUnit du = null;
int inputIndex = -1;
while (!isInterrupted())
{
// In order to get as much data to the decoder as early as possible,
// try to submit up to 5 decode units at once without blocking.
if (inputIndex == -1 && du == null) {
try {
for (int i = 0; i < 5; i++) {
inputIndex = videoDecoder.dequeueInputBuffer(0);
du = depacketizer.pollNextDecodeUnit();
// Stop if we can't get a DU or input buffer
if (du == null || inputIndex == -1) {
break;
}
submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex);
du = null;
inputIndex = -1;
}
} catch (Exception e) {
inputIndex = -1;
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
}
}
// Grab an input buffer if we don't have one already.
// This way we can have one ready hopefully by the time
// the depacketizer is done with this frame. It's important
// that this can timeout because it's possible that we could exhaust
// the decoder's input buffers and deadlocks because aren't pulling
// frames out of the other end.
if (inputIndex == -1) {
try {
// If we've got a DU waiting to be given to the decoder,
// wait a full 3 ms for an input buffer. Otherwise
// just see if we can get one immediately.
inputIndex = videoDecoder.dequeueInputBuffer(du != null ? 3000 : 0);
} catch (Exception e) {
inputIndex = -1;
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
}
}
// Grab a decode unit if we don't have one already
if (du == null) {
du = depacketizer.pollNextDecodeUnit();
}
// If we've got both a decode unit and an input buffer, we'll
// submit now. Otherwise, we wait until we have one.
if (du != null && inputIndex >= 0) {
submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex);
// DU and input buffer have both been consumed
du = null;
inputIndex = -1;
}
// Try to output a frame
try {
int outIndex = videoDecoder.dequeueOutputBuffer(info, 0);
if (outIndex >= 0) {
long presentationTimeUs = info.presentationTimeUs;
int lastIndex = outIndex;
// Get the last output buffer in the queue
while ((outIndex = videoDecoder.dequeueOutputBuffer(info, 0)) >= 0) {
videoDecoder.releaseOutputBuffer(lastIndex, false);
lastIndex = outIndex;
presentationTimeUs = info.presentationTimeUs;
}
// Render the last buffer
videoDecoder.releaseOutputBuffer(lastIndex, true);
// Add delta time to the totals (excluding probable outliers)
long delta = System.currentTimeMillis()-(presentationTimeUs/1000);
if (delta > 5 && delta < 300) {
decoderTimeMs += delta;
totalTimeMs += delta;
}
} else {
switch (outIndex) {
case MediaCodec.INFO_TRY_AGAIN_LATER:
// Getting an input buffer may already block
// so don't park if we still need to do that
if (inputIndex >= 0) {
LockSupport.parkNanos(1);
}
break;
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
LimeLog.info("Output buffers changed");
break;
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
LimeLog.info("Output format changed");
LimeLog.info("New output Format: " + videoDecoder.getOutputFormat());
break;
default:
break;
}
}
} catch (Exception e) {
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
}
}
}
};
rendererThread.setName("Video - Renderer (MediaCodec)");
rendererThread.setPriority(Thread.MAX_PRIORITY);
rendererThread.start();
}
@SuppressWarnings("deprecation")
@Override
public boolean start(VideoDepacketizer depacketizer) {
this.depacketizer = depacketizer;
// Start the decoder
videoDecoder.start();
// On devices pre-Lollipop, we'll use a rendering thread
if (!ENABLE_ASYNC_RENDERER || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
videoDecoderInputBuffers = videoDecoder.getInputBuffers();
startRendererThread();
}
return true;
}
@Override
public void stop() {
if (rendererThread != null) {
// Halt the rendering thread
rendererThread.interrupt();
try {
rendererThread.join();
} catch (InterruptedException e) { }
}
// Stop the decoder
videoDecoder.stop();
}
@Override
public void release() {
if (videoDecoder != null) {
videoDecoder.release();
}
}
private void queueInputBuffer(int inputBufferIndex, int offset, int length, long timestampUs, int codecFlags) {
// Try 25 times to submit the input buffer before throwing a real exception
int i;
Exception lastException = null;
for (i = 0; i < 25; i++) {
try {
videoDecoder.queueInputBuffer(inputBufferIndex,
0, length,
timestampUs, codecFlags);
break;
} catch (Exception e) {
handleDecoderException(this, e, null, codecFlags);
lastException = e;
}
}
if (i == 25) {
throw new RendererException(this, lastException, null, codecFlags);
}
}
@SuppressWarnings("deprecation")
private void submitDecodeUnit(DecodeUnit decodeUnit, ByteBuffer buf, int inputBufferIndex) {
long currentTime = System.currentTimeMillis();
long delta = currentTime-decodeUnit.getReceiveTimestamp();
if (delta >= 0 && delta < 300) {
totalTimeMs += currentTime-decodeUnit.getReceiveTimestamp();
totalFrames++;
}
long timestampUs = currentTime * 1000;
if (timestampUs <= lastTimestampUs) {
// We can't submit multiple buffers with the same timestamp
// so bump it up by one before queuing
timestampUs = lastTimestampUs + 1;
}
lastTimestampUs = timestampUs;
// Clear old input data
buf.clear();
int codecFlags = 0;
int decodeUnitFlags = decodeUnit.getFlags();
if ((decodeUnitFlags & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) {
codecFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
}
if ((decodeUnitFlags & DecodeUnit.DU_FLAG_SYNC_FRAME) != 0) {
codecFlags |= MediaCodec.BUFFER_FLAG_SYNC_FRAME;
numIframeIn++;
}
if ((decodeUnitFlags & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) {
ByteBufferDescriptor header = decodeUnit.getBufferList().get(0);
if (header.data[header.offset+4] == 0x67) {
numSpsIn++;
ByteBuffer spsBuf = ByteBuffer.wrap(header.data);
// Skip to the start of the NALU data
spsBuf.position(header.offset+5);
SeqParameterSet sps = SeqParameterSet.read(spsBuf);
// TI OMAP4 requires a reference frame count of 1 to decode successfully. Exynos 4
// also requires this fixup.
//
// I'm doing this fixup for all devices because I haven't seen any devices that
// this causes issues for. At worst, it seems to do nothing and at best it fixes
// issues with video lag, hangs, and crashes.
LimeLog.info("Patching num_ref_frames in SPS");
sps.num_ref_frames = 1;
if (needsSpsBitstreamFixup || isExynos4) {
// The SPS that comes in the current H264 bytestream doesn't set bitstream_restriction_flag
// or max_dec_frame_buffering which increases decoding latency on Tegra.
LimeLog.info("Adding bitstream restrictions");
sps.vuiParams.bitstreamRestriction = new VUIParameters.BitstreamRestriction();
sps.vuiParams.bitstreamRestriction.motion_vectors_over_pic_boundaries_flag = true;
sps.vuiParams.bitstreamRestriction.max_bytes_per_pic_denom = 2;
sps.vuiParams.bitstreamRestriction.max_bits_per_mb_denom = 1;
sps.vuiParams.bitstreamRestriction.log2_max_mv_length_horizontal = 16;
sps.vuiParams.bitstreamRestriction.log2_max_mv_length_vertical = 16;
sps.vuiParams.bitstreamRestriction.num_reorder_frames = 0;
sps.vuiParams.bitstreamRestriction.max_dec_frame_buffering = 1;
}
// Write the annex B header
buf.put(header.data, header.offset, 5);
// Write the modified SPS to the input buffer
sps.write(buf);
queueInputBuffer(inputBufferIndex,
0, buf.position(),
timestampUs, codecFlags);
depacketizer.freeDecodeUnit(decodeUnit);
return;
} else if (header.data[header.offset+4] == 0x68) {
numPpsIn++;
}
}
// Copy data from our buffer list into the input buffer
for (ByteBufferDescriptor desc : decodeUnit.getBufferList())
{
buf.put(desc.data, desc.offset, desc.length);
}
queueInputBuffer(inputBufferIndex,
0, decodeUnit.getDataLength(),
timestampUs, codecFlags);
depacketizer.freeDecodeUnit(decodeUnit);
return;
}
@Override
public int getCapabilities() {
return adaptivePlayback ?
VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION : 0;
}
@Override
public int getAverageDecoderLatency() {
if (totalFrames == 0) {
return 0;
}
return (int)(decoderTimeMs / totalFrames);
}
@Override
public int getAverageEndToEndLatency() {
if (totalFrames == 0) {
return 0;
}
return (int)(totalTimeMs / totalFrames);
}
public class RendererException extends RuntimeException {
private static final long serialVersionUID = 8985937536997012406L;
private Exception originalException;
private MediaCodecDecoderRenderer renderer;
private ByteBuffer currentBuffer;
private int currentCodecFlags;
public RendererException(MediaCodecDecoderRenderer renderer, Exception e) {
this.renderer = renderer;
this.originalException = e;
}
public RendererException(MediaCodecDecoderRenderer renderer, Exception e, ByteBuffer currentBuffer, int currentCodecFlags) {
this.renderer = renderer;
this.originalException = e;
this.currentBuffer = currentBuffer;
this.currentCodecFlags = currentCodecFlags;
}
public String toString() {
String str = "";
str += "Decoder: "+renderer.decoderName+"\n";
str += "Initial video dimensions: "+renderer.initialWidth+"x"+renderer.initialHeight+"\n";
str += "In stats: "+renderer.numSpsIn+", "+renderer.numPpsIn+", "+renderer.numIframeIn+"\n";
str += "Total frames: "+renderer.totalFrames+"\n";
if (currentBuffer != null) {
str += "Current buffer: ";
currentBuffer.flip();
while (currentBuffer.hasRemaining() && currentBuffer.position() < 10) {
str += String.format((Locale)null, "%02x ", currentBuffer.get());
}
str += "\n";
str += "Buffer codec flags: "+currentCodecFlags+"\n";
}
str += "Is Exynos 4: "+renderer.isExynos4+"\n";
str += "/proc/cpuinfo:\n";
try {
str += MediaCodecHelper.readCpuinfo();
} catch (Exception e) {
str += e.getMessage();
}
str += "Full decoder dump:\n";
try {
str += MediaCodecHelper.dumpDecoders();
} catch (Exception e) {
str += e.getMessage();
}
str += originalException.toString();
return str;
}
}
}

View File

@@ -0,0 +1,299 @@
package com.limelight.binding.video;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.media.MediaCodecInfo.CodecCapabilities;
import android.media.MediaCodecInfo.CodecProfileLevel;
import android.os.Build;
import com.limelight.LimeLog;
public class MediaCodecHelper {
public static final List<String> preferredDecoders;
public static final List<String> blacklistedDecoderPrefixes;
public static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
public static final List<String> whitelistedAdaptiveResolutionPrefixes;
static {
preferredDecoders = new LinkedList<String>();
}
static {
blacklistedDecoderPrefixes = new LinkedList<String>();
// Software decoders that don't support H264 high profile
blacklistedDecoderPrefixes.add("omx.google");
blacklistedDecoderPrefixes.add("AVCDecoder");
}
static {
spsFixupBitstreamFixupDecoderPrefixes = new LinkedList<String>();
spsFixupBitstreamFixupDecoderPrefixes.add("omx.nvidia");
spsFixupBitstreamFixupDecoderPrefixes.add("omx.qcom");
spsFixupBitstreamFixupDecoderPrefixes.add("omx.mtk");
whitelistedAdaptiveResolutionPrefixes = new LinkedList<String>();
whitelistedAdaptiveResolutionPrefixes.add("omx.nvidia");
whitelistedAdaptiveResolutionPrefixes.add("omx.qcom");
whitelistedAdaptiveResolutionPrefixes.add("omx.sec");
whitelistedAdaptiveResolutionPrefixes.add("omx.TI");
}
private static boolean isDecoderInList(List<String> decoderList, String decoderName) {
for (String badPrefix : decoderList) {
if (decoderName.length() >= badPrefix.length()) {
String prefix = decoderName.substring(0, badPrefix.length());
if (prefix.equalsIgnoreCase(badPrefix)) {
return true;
}
}
}
return false;
}
@TargetApi(Build.VERSION_CODES.KITKAT)
public static boolean decoderSupportsAdaptivePlayback(String decoderName, MediaCodecInfo decoderInfo) {
if (isDecoderInList(whitelistedAdaptiveResolutionPrefixes, decoderName)) {
LimeLog.info("Adaptive playback supported (whitelist)");
return true;
}
// Possibly enable adaptive playback on KitKat and above
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
try {
if (decoderInfo.getCapabilitiesForType("video/avc").
isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback))
{
// This will make getCapabilities() return that adaptive playback is supported
LimeLog.info("Adaptive playback supported (FEATURE_AdaptivePlayback)");
return true;
}
} catch (Exception e) {
// Tolerate buggy codecs
}
}
return false;
}
public static boolean decoderNeedsSpsBitstreamRestrictions(String decoderName, MediaCodecInfo decoderInfo) {
return isDecoderInList(spsFixupBitstreamFixupDecoderPrefixes, decoderName);
}
@SuppressWarnings("deprecation")
@SuppressLint("NewApi")
private static LinkedList<MediaCodecInfo> getMediaCodecList() {
LinkedList<MediaCodecInfo> infoList = new LinkedList<MediaCodecInfo>();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
for (MediaCodecInfo info : mcl.getCodecInfos()) {
infoList.add(info);
}
}
else {
for (int i = 0; i < MediaCodecList.getCodecCount(); i++) {
infoList.add(MediaCodecList.getCodecInfoAt(i));
}
}
return infoList;
}
public static String dumpDecoders() throws Exception {
String str = "";
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
// Skip encoders
if (codecInfo.isEncoder()) {
continue;
}
str += "Decoder: "+codecInfo.getName()+"\n";
for (String type : codecInfo.getSupportedTypes()) {
str += "\t"+type+"\n";
CodecCapabilities caps = codecInfo.getCapabilitiesForType(type);
for (CodecProfileLevel profile : caps.profileLevels) {
str += "\t\t"+profile.profile+" "+profile.level+"\n";
}
}
}
return str;
}
public static MediaCodecInfo findPreferredDecoder() {
// This is a different algorithm than the other findXXXDecoder functions,
// because we want to evaluate the decoders in our list's order
// rather than MediaCodecList's order
for (String preferredDecoder : preferredDecoders) {
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
// Skip encoders
if (codecInfo.isEncoder()) {
continue;
}
// Check for preferred decoders
if (preferredDecoder.equalsIgnoreCase(codecInfo.getName())) {
LimeLog.info("Preferred decoder choice is "+codecInfo.getName());
return codecInfo;
}
}
}
return null;
}
public static MediaCodecInfo findFirstDecoder() {
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
// Skip encoders
if (codecInfo.isEncoder()) {
continue;
}
// Check for explicitly blacklisted decoders
if (isDecoderInList(blacklistedDecoderPrefixes, codecInfo.getName())) {
LimeLog.info("Skipping blacklisted decoder: "+codecInfo.getName());
continue;
}
// Find a decoder that supports H.264
for (String mime : codecInfo.getSupportedTypes()) {
if (mime.equalsIgnoreCase("video/avc")) {
LimeLog.info("First decoder choice is "+codecInfo.getName());
return codecInfo;
}
}
}
return null;
}
public static MediaCodecInfo findProbableSafeDecoder() {
// First look for a preferred decoder by name
MediaCodecInfo info = findPreferredDecoder();
if (info != null) {
return info;
}
// Now look for decoders we know are safe
try {
// If this function completes, it will determine if the decoder is safe
return findKnownSafeDecoder();
} catch (Exception e) {
// Some buggy devices seem to throw exceptions
// from getCapabilitiesForType() so we'll just assume
// they're okay and go with the first one we find
return findFirstDecoder();
}
}
// We declare this method as explicitly throwing Exception
// since some bad decoders can throw IllegalArgumentExceptions unexpectedly
// and we want to be sure all callers are handling this possibility
public static MediaCodecInfo findKnownSafeDecoder() throws Exception {
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
// Skip encoders
if (codecInfo.isEncoder()) {
continue;
}
// Check for explicitly blacklisted decoders
if (isDecoderInList(blacklistedDecoderPrefixes, codecInfo.getName())) {
LimeLog.info("Skipping blacklisted decoder: "+codecInfo.getName());
continue;
}
// Find a decoder that supports H.264 high profile
for (String mime : codecInfo.getSupportedTypes()) {
if (mime.equalsIgnoreCase("video/avc")) {
LimeLog.info("Examining decoder capabilities of "+codecInfo.getName());
CodecCapabilities caps = codecInfo.getCapabilitiesForType(mime);
for (CodecProfileLevel profile : caps.profileLevels) {
if (profile.profile == CodecProfileLevel.AVCProfileHigh) {
LimeLog.info("Decoder "+codecInfo.getName()+" supports high profile");
LimeLog.info("Selected decoder: "+codecInfo.getName());
return codecInfo;
}
}
LimeLog.info("Decoder "+codecInfo.getName()+" does NOT support high profile");
}
}
}
return null;
}
public static String readCpuinfo() throws Exception {
StringBuilder cpuInfo = new StringBuilder();
BufferedReader br = new BufferedReader(new FileReader(new File("/proc/cpuinfo")));
try {
for (;;) {
int ch = br.read();
if (ch == -1)
break;
cpuInfo.append((char)ch);
}
return cpuInfo.toString();
} finally {
br.close();
}
}
private static boolean stringContainsIgnoreCase(String string, String substring) {
return string.toLowerCase(Locale.ENGLISH).contains(substring.toLowerCase(Locale.ENGLISH));
}
public static boolean isExynos4Device() {
try {
// Try reading CPU info too look for
String cpuInfo = readCpuinfo();
// SMDK4xxx is Exynos 4
if (stringContainsIgnoreCase(cpuInfo, "SMDK4")) {
LimeLog.info("Found SMDK4 in /proc/cpuinfo");
return true;
}
// If we see "Exynos 4" also we'll count it
if (stringContainsIgnoreCase(cpuInfo, "Exynos 4")) {
LimeLog.info("Found Exynos 4 in /proc/cpuinfo");
return true;
}
} catch (Exception e) {
e.printStackTrace();
}
try {
File systemDir = new File("/sys/devices/system");
File[] files = systemDir.listFiles();
if (files != null) {
for (File f : files) {
if (stringContainsIgnoreCase(f.getName(), "exynos4")) {
LimeLog.info("Found exynos4 in /sys/devices/system");
return true;
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
}

View File

@@ -0,0 +1,164 @@
package com.limelight.computers;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import com.limelight.LimeLog;
import com.limelight.nvstream.http.ComputerDetails;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
public class ComputerDatabaseManager {
private static final String COMPUTER_DB_NAME = "computers.db";
private static final String COMPUTER_TABLE_NAME = "Computers";
private static final String COMPUTER_NAME_COLUMN_NAME = "ComputerName";
private static final String COMPUTER_UUID_COLUMN_NAME = "UUID";
private static final String LOCAL_IP_COLUMN_NAME = "LocalIp";
private static final String REMOTE_IP_COLUMN_NAME = "RemoteIp";
private static final String MAC_COLUMN_NAME = "Mac";
private SQLiteDatabase computerDb;
public ComputerDatabaseManager(Context c) {
try {
// Create or open an existing DB
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
} catch (SQLiteException e) {
// Delete the DB and try again
c.deleteDatabase(COMPUTER_DB_NAME);
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
}
initializeDb();
}
public void close() {
computerDb.close();
}
private void initializeDb() {
// Create tables if they aren't already there
computerDb.execSQL(String.format((Locale)null, "CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY," +
" %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL)",
COMPUTER_TABLE_NAME,
COMPUTER_NAME_COLUMN_NAME, COMPUTER_UUID_COLUMN_NAME, LOCAL_IP_COLUMN_NAME,
REMOTE_IP_COLUMN_NAME, MAC_COLUMN_NAME));
}
public void deleteComputer(String name) {
computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null);
}
public boolean updateComputer(ComputerDetails details) {
ContentValues values = new ContentValues();
values.put(COMPUTER_NAME_COLUMN_NAME, details.name);
values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid.toString());
values.put(LOCAL_IP_COLUMN_NAME, details.localIp.getAddress());
values.put(REMOTE_IP_COLUMN_NAME, details.remoteIp.getAddress());
values.put(MAC_COLUMN_NAME, details.macAddress);
return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
}
public List<ComputerDetails> getAllComputers() {
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
LinkedList<ComputerDetails> computerList = new LinkedList<ComputerDetails>();
while (c.moveToNext()) {
ComputerDetails details = new ComputerDetails();
details.name = c.getString(0);
String uuidStr = c.getString(1);
try {
details.uuid = UUID.fromString(uuidStr);
} catch (IllegalArgumentException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted UUID for "+details.name);
}
try {
details.localIp = InetAddress.getByAddress(c.getBlob(2));
} catch (UnknownHostException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted local IP for "+details.name);
}
try {
details.remoteIp = InetAddress.getByAddress(c.getBlob(3));
} catch (UnknownHostException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted remote IP for "+details.name);
}
details.macAddress = c.getString(4);
// This signifies we don't have dynamic state (like pair state)
details.state = ComputerDetails.State.UNKNOWN;
// If a field is corrupt or missing, skip the database entry
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
details.macAddress == null) {
continue;
}
computerList.add(details);
}
c.close();
return computerList;
}
public ComputerDetails getComputerByName(String name) {
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME+" WHERE "+COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null);
ComputerDetails details = new ComputerDetails();
if (!c.moveToFirst()) {
// No matching computer
c.close();
return null;
}
details.name = c.getString(0);
String uuidStr = c.getString(1);
try {
details.uuid = UUID.fromString(uuidStr);
} catch (IllegalArgumentException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted UUID for "+details.name);
}
try {
details.localIp = InetAddress.getByAddress(c.getBlob(2));
} catch (UnknownHostException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted local IP for "+details.name);
}
try {
details.remoteIp = InetAddress.getByAddress(c.getBlob(3));
} catch (UnknownHostException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted remote IP for "+details.name);
}
details.macAddress = c.getString(4);
c.close();
// If a field is corrupt or missing, delete the database entry
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
details.macAddress == null) {
deleteComputer(details.name);
return null;
}
return details;
}
}

View File

@@ -0,0 +1,7 @@
package com.limelight.computers;
import com.limelight.nvstream.http.ComputerDetails;
public interface ComputerManagerListener {
public void notifyComputerUpdated(ComputerDetails details);
}

View File

@@ -0,0 +1,378 @@
package com.limelight.computers;
import java.net.InetAddress;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import com.limelight.LimeLog;
import com.limelight.binding.PlatformBinding;
import com.limelight.discovery.DiscoveryService;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvHTTP;
import com.limelight.nvstream.mdns.MdnsComputer;
import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Binder;
import android.os.IBinder;
public class ComputerManagerService extends Service {
private static final int MAX_CONCURRENT_REQUESTS = 4;
private static final int POLLING_PERIOD_MS = 5000;
private static final int MDNS_QUERY_PERIOD_MS = 1000;
private ComputerManagerBinder binder = new ComputerManagerBinder();
private ComputerDatabaseManager dbManager;
private AtomicInteger dbRefCount = new AtomicInteger(0);
private IdentityManager idManager;
private ThreadPoolExecutor pollingPool;
private Timer pollingTimer;
private ComputerManagerListener listener = null;
private AtomicInteger activePolls = new AtomicInteger(0);
private boolean stopped;
private DiscoveryService.DiscoveryBinder discoveryBinder;
private ServiceConnection discoveryServiceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder binder) {
synchronized (discoveryServiceConnection) {
DiscoveryService.DiscoveryBinder privateBinder = ((DiscoveryService.DiscoveryBinder)binder);
// Set us as the event listener
privateBinder.setListener(createDiscoveryListener());
// Signal a possible waiter that we're all setup
discoveryBinder = privateBinder;
discoveryServiceConnection.notifyAll();
}
}
public void onServiceDisconnected(ComponentName className) {
discoveryBinder = null;
}
};
public class ComputerManagerBinder extends Binder {
public void startPolling(ComputerManagerListener listener) {
// Not stopped
stopped = false;
// Set the listener
ComputerManagerService.this.listener = listener;
// Start mDNS autodiscovery too
discoveryBinder.startDiscovery(MDNS_QUERY_PERIOD_MS);
// Start polling known machines
pollingTimer = new Timer();
pollingTimer.schedule(getTimerTask(), 0, POLLING_PERIOD_MS);
}
public void waitForReady() {
synchronized (discoveryServiceConnection) {
try {
while (discoveryBinder == null) {
// Wait for the bind notification
discoveryServiceConnection.wait(1000);
}
} catch (InterruptedException e) {
}
}
}
public void waitForPollingStopped() {
while (activePolls.get() != 0) {
try {
Thread.sleep(250);
} catch (InterruptedException e) {}
}
}
public boolean addComputerBlocking(InetAddress addr) {
return ComputerManagerService.this.addComputerBlocking(addr);
}
public void addComputer(InetAddress addr) {
ComputerManagerService.this.addComputer(addr);
}
public void removeComputer(String name) {
ComputerManagerService.this.removeComputer(name);
}
public void stopPolling() {
// Just call the unbind handler to cleanup
ComputerManagerService.this.onUnbind(null);
}
public String getUniqueId() {
return idManager.getUniqueId();
}
}
@Override
public boolean onUnbind(Intent intent) {
// Stopped now
stopped = true;
// Stop mDNS autodiscovery
discoveryBinder.stopDiscovery();
// Stop polling
if (pollingTimer != null) {
pollingTimer.cancel();
pollingTimer = null;
}
// Remove the listener
listener = null;
return false;
}
private MdnsDiscoveryListener createDiscoveryListener() {
return new MdnsDiscoveryListener() {
@Override
public void notifyComputerAdded(MdnsComputer computer) {
// Kick off a serverinfo poll on this machine
addComputer(computer.getAddress());
}
@Override
public void notifyComputerRemoved(MdnsComputer computer) {
// Nothing to do here
}
@Override
public void notifyDiscoveryFailure(Exception e) {
LimeLog.severe("mDNS discovery failed");
e.printStackTrace();
}
};
}
public void addComputer(InetAddress addr) {
// Setup a placeholder
ComputerDetails fakeDetails = new ComputerDetails();
fakeDetails.localIp = addr;
fakeDetails.remoteIp = addr;
// Put it in the thread pool to process later
pollingPool.execute(getPollingRunnable(fakeDetails));
}
public boolean addComputerBlocking(InetAddress addr) {
// Setup a placeholder
ComputerDetails fakeDetails = new ComputerDetails();
fakeDetails.localIp = addr;
fakeDetails.remoteIp = addr;
// Block while we try to fill the details
getPollingRunnable(fakeDetails).run();
// If the machine is reachable, it was successful
return fakeDetails.state == ComputerDetails.State.ONLINE;
}
public void removeComputer(String name) {
if (!getLocalDatabaseReference()) {
return;
}
// Remove it from the database
dbManager.deleteComputer(name);
releaseLocalDatabaseReference();
}
private boolean getLocalDatabaseReference() {
if (dbRefCount.get() == 0) {
return false;
}
dbRefCount.incrementAndGet();
return true;
}
private void releaseLocalDatabaseReference() {
if (dbRefCount.decrementAndGet() == 0) {
dbManager.close();
}
}
private TimerTask getTimerTask() {
return new TimerTask() {
@Override
public void run() {
if (!getLocalDatabaseReference()) {
return;
}
List<ComputerDetails> computerList = dbManager.getAllComputers();
releaseLocalDatabaseReference();
for (ComputerDetails computer : computerList) {
pollingPool.execute(getPollingRunnable(computer));
}
}
};
}
private ComputerDetails tryPollIp(InetAddress ipAddr) {
try {
NvHTTP http = new NvHTTP(ipAddr, idManager.getUniqueId(),
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
return http.getComputerDetails();
} catch (Exception e) {
return null;
}
}
private boolean pollComputer(ComputerDetails details, boolean localFirst) {
ComputerDetails polledDetails;
if (localFirst) {
polledDetails = tryPollIp(details.localIp);
}
else {
polledDetails = tryPollIp(details.remoteIp);
}
if (polledDetails == null && !details.localIp.equals(details.remoteIp)) {
// Failed, so let's try the fallback
if (!localFirst) {
polledDetails = tryPollIp(details.localIp);
}
else {
polledDetails = tryPollIp(details.remoteIp);
}
// The fallback poll worked
if (polledDetails != null) {
polledDetails.reachability = !localFirst ? ComputerDetails.Reachability.LOCAL :
ComputerDetails.Reachability.REMOTE;
}
}
else if (polledDetails != null) {
polledDetails.reachability = localFirst ? ComputerDetails.Reachability.LOCAL :
ComputerDetails.Reachability.REMOTE;
}
// Machine was unreachable both tries
if (polledDetails == null) {
return false;
}
// If we got here, it's reachable
details.update(polledDetails);
return true;
}
private boolean doPollMachine(ComputerDetails details) {
return pollComputer(details, true);
}
private Runnable getPollingRunnable(final ComputerDetails details) {
return new Runnable() {
@Override
public void run() {
boolean newPc = (details.name == null);
// This is called from addComputerManually() where we don't
// want to block the initial poll if polling is disabled, so
// we explicitly let this through if we've never seen this
// PC before. This path won't be triggered normally when polling
// is disabled because the mDNS discovery is stopped.
if (stopped && !newPc) {
return;
}
if (!getLocalDatabaseReference()) {
return;
}
activePolls.incrementAndGet();
// Poll the machine
if (!doPollMachine(details)) {
details.state = ComputerDetails.State.OFFLINE;
details.reachability = ComputerDetails.Reachability.OFFLINE;
}
activePolls.decrementAndGet();
// If it's online, update our persistent state
if (details.state == ComputerDetails.State.ONLINE) {
if (!newPc) {
// Check if it's in the database because it could have been
// removed after this was issued
if (dbManager.getComputerByName(details.name) == null) {
// It's gone
releaseLocalDatabaseReference();
return;
}
}
dbManager.updateComputer(details);
}
// Don't call the listener if this is a failed lookup of a new PC
if ((!newPc || details.state == ComputerDetails.State.ONLINE) && listener != null) {
listener.notifyComputerUpdated(details);
}
releaseLocalDatabaseReference();
}
};
}
@Override
public void onCreate() {
// Bind to the discovery service
bindService(new Intent(this, DiscoveryService.class),
discoveryServiceConnection, Service.BIND_AUTO_CREATE);
// Create the thread pool for updating computer state
pollingPool = new ThreadPoolExecutor(MAX_CONCURRENT_REQUESTS, MAX_CONCURRENT_REQUESTS,
Long.MAX_VALUE, TimeUnit.DAYS, new LinkedBlockingQueue<Runnable>(),
new ThreadPoolExecutor.DiscardPolicy());
// Lookup or generate this device's UID
idManager = new IdentityManager(this);
// Initialize the DB
dbManager = new ComputerDatabaseManager(this);
dbRefCount.set(1);
}
@Override
public void onDestroy() {
if (discoveryBinder != null) {
// Unbind from the discovery service
unbindService(discoveryServiceConnection);
}
// Stop the thread pool
pollingPool.shutdownNow();
// FIXME: Should await termination here but we have timeout issues in HttpURLConnection
// Remove the initial DB reference
releaseLocalDatabaseReference();
}
@Override
public IBinder onBind(Intent intent) {
return binder;
}
}

View File

@@ -0,0 +1,86 @@
package com.limelight.computers;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.Locale;
import java.util.Random;
import com.limelight.LimeLog;
import android.content.Context;
public class IdentityManager {
private static final String UNIQUE_ID_FILE_NAME = "uniqueid";
private static final int UID_SIZE_IN_BYTES = 8;
private String uniqueId;
public IdentityManager(Context c) {
uniqueId = loadUniqueId(c);
if (uniqueId == null) {
uniqueId = generateNewUniqueId(c);
}
LimeLog.info("UID is now: "+uniqueId);
}
public String getUniqueId() {
return uniqueId;
}
private static String loadUniqueId(Context c) {
// 2 Hex digits per byte
char[] uid = new char[UID_SIZE_IN_BYTES * 2];
InputStreamReader reader = null;
LimeLog.info("Reading UID from disk");
try {
reader = new InputStreamReader(c.openFileInput(UNIQUE_ID_FILE_NAME));
if (reader.read(uid) != UID_SIZE_IN_BYTES * 2)
{
LimeLog.severe("UID file data is truncated");
return null;
}
return new String(uid);
} catch (FileNotFoundException e) {
LimeLog.info("No UID file found");
return null;
} catch (IOException e) {
LimeLog.severe("Error while reading UID file");
e.printStackTrace();
return null;
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {}
}
}
}
private static String generateNewUniqueId(Context c) {
// Generate a new UID hex string
LimeLog.info("Generating new UID");
String uidStr = String.format((Locale)null, "%016x", new Random().nextLong());
OutputStreamWriter writer = null;
try {
writer = new OutputStreamWriter(c.openFileOutput(UNIQUE_ID_FILE_NAME, 0));
writer.write(uidStr);
LimeLog.info("UID written to disk");
} catch (IOException e) {
LimeLog.severe("Error while writing UID file");
e.printStackTrace();
} finally {
if (writer != null) {
try {
writer.close();
} catch (IOException e) {}
}
}
// We can return a UID even if I/O fails
return uidStr;
}
}

View File

@@ -0,0 +1,90 @@
package com.limelight.discovery;
import java.util.List;
import com.limelight.nvstream.mdns.MdnsComputer;
import com.limelight.nvstream.mdns.MdnsDiscoveryAgent;
import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiManager.MulticastLock;
import android.os.Binder;
import android.os.IBinder;
public class DiscoveryService extends Service {
private MdnsDiscoveryAgent discoveryAgent;
private MdnsDiscoveryListener boundListener;
private MulticastLock multicastLock;
public class DiscoveryBinder extends Binder {
public void setListener(MdnsDiscoveryListener listener) {
boundListener = listener;
}
public void startDiscovery(int queryIntervalMs) {
multicastLock.acquire();
discoveryAgent.startDiscovery(queryIntervalMs);
}
public void stopDiscovery() {
discoveryAgent.stopDiscovery();
multicastLock.release();
}
public List<MdnsComputer> getComputerSet() {
return discoveryAgent.getComputerSet();
}
}
@Override
public void onCreate() {
WifiManager wifiMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE);
multicastLock = wifiMgr.createMulticastLock("Limelight mDNS");
multicastLock.setReferenceCounted(false);
discoveryAgent = new MdnsDiscoveryAgent(new MdnsDiscoveryListener() {
@Override
public void notifyComputerAdded(MdnsComputer computer) {
if (boundListener != null) {
boundListener.notifyComputerAdded(computer);
}
}
@Override
public void notifyComputerRemoved(MdnsComputer computer) {
if (boundListener != null) {
boundListener.notifyComputerRemoved(computer);
}
}
@Override
public void notifyDiscoveryFailure(Exception e) {
if (boundListener != null) {
boundListener.notifyDiscoveryFailure(e);
}
}
});
}
private DiscoveryBinder binder = new DiscoveryBinder();
@Override
public IBinder onBind(Intent intent) {
return binder;
}
@Override
public boolean onUnbind(Intent intent) {
// Stop any discovery session
discoveryAgent.stopDiscovery();
multicastLock.release();
// Unbind the listener
boundListener = null;
return false;
}
}

View File

@@ -0,0 +1,44 @@
package com.limelight.nvstream.av.video.cpu;
public class AvcDecoder {
static {
// FFMPEG dependencies
System.loadLibrary("avutil-52");
System.loadLibrary("swresample-0");
System.loadLibrary("swscale-2");
System.loadLibrary("avcodec-55");
System.loadLibrary("avformat-55");
System.loadLibrary("nv_avc_dec");
}
/** Disables the deblocking filter at the cost of image quality */
public static final int DISABLE_LOOP_FILTER = 0x1;
/** Uses the low latency decode flag (disables multithreading) */
public static final int LOW_LATENCY_DECODE = 0x2;
/** Threads process each slice, rather than each frame */
public static final int SLICE_THREADING = 0x4;
/** Uses nonstandard speedup tricks */
public static final int FAST_DECODE = 0x8;
/** Uses bilinear filtering instead of bicubic */
public static final int BILINEAR_FILTERING = 0x10;
/** Uses a faster bilinear filtering with lower image quality */
public static final int FAST_BILINEAR_FILTERING = 0x20;
/** Disables color conversion (output is NV21) */
public static final int NO_COLOR_CONVERSION = 0x40;
public static native int init(int width, int height, int perflvl, int threadcount);
public static native void destroy();
// Rendering API when NO_COLOR_CONVERSION == 0
public static native boolean setRenderTarget(Object androidSurface);
public static native boolean getRgbFrameInt(int[] rgbFrame, int bufferSize);
public static native boolean getRgbFrame(byte[] rgbFrame, int bufferSize);
public static native boolean redraw();
// Rendering API when NO_COLOR_CONVERSION == 1
public static native boolean getRawFrame(byte[] yuvFrame, int bufferSize);
public static native int getInputPaddingSize();
public static native int decode(byte[] indata, int inoff, int inlen);
}

View File

@@ -0,0 +1,76 @@
package com.limelight.utils;
import java.util.ArrayList;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
public class Dialog implements Runnable {
private String title, message;
private Activity activity;
private boolean endAfterDismiss;
private AlertDialog alert;
private static ArrayList<Dialog> rundownDialogs = new ArrayList<Dialog>();
public Dialog(Activity activity, String title, String message, boolean endAfterDismiss)
{
this.activity = activity;
this.title = title;
this.message = message;
this.endAfterDismiss = endAfterDismiss;
}
public static void closeDialogs()
{
synchronized (rundownDialogs) {
for (Dialog d : rundownDialogs) {
if (d.alert.isShowing()) {
d.alert.dismiss();
}
}
rundownDialogs.clear();
}
}
public static void displayDialog(Activity activity, String title, String message, boolean endAfterDismiss)
{
activity.runOnUiThread(new Dialog(activity, title, message, endAfterDismiss));
}
@Override
public void run() {
// If we're dying, don't bother creating a dialog
if (activity.isFinishing())
return;
alert = new AlertDialog.Builder(activity).create();
alert.setTitle(title);
alert.setMessage(message);
alert.setCancelable(false);
alert.setCanceledOnTouchOutside(false);
alert.setButton(AlertDialog.BUTTON_NEUTRAL, "OK", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
synchronized (rundownDialogs) {
rundownDialogs.remove(this);
alert.dismiss();
}
if (endAfterDismiss) {
activity.finish();
}
}
});
synchronized (rundownDialogs) {
rundownDialogs.add(this);
alert.show();
}
}
}

View File

@@ -0,0 +1,119 @@
package com.limelight.utils;
import java.util.ArrayList;
import java.util.Iterator;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener;
public class SpinnerDialog implements Runnable,OnCancelListener {
private String title, message;
private Activity activity;
private ProgressDialog progress;
private boolean finish;
private static ArrayList<SpinnerDialog> rundownDialogs = new ArrayList<SpinnerDialog>();
public SpinnerDialog(Activity activity, String title, String message, boolean finish)
{
this.activity = activity;
this.title = title;
this.message = message;
this.progress = null;
this.finish = finish;
}
public static SpinnerDialog displayDialog(Activity activity, String title, String message, boolean finish)
{
SpinnerDialog spinner = new SpinnerDialog(activity, title, message, finish);
activity.runOnUiThread(spinner);
return spinner;
}
public static void closeDialogs(Activity activity)
{
synchronized (rundownDialogs) {
Iterator<SpinnerDialog> i = rundownDialogs.iterator();
while (i.hasNext()) {
SpinnerDialog dialog = i.next();
if (dialog.activity == activity) {
i.remove();
if (dialog.progress.isShowing()) {
dialog.progress.dismiss();
}
}
}
}
}
public void dismiss()
{
// Running again with progress != null will destroy it
activity.runOnUiThread(this);
}
public void setMessage(final String message)
{
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
progress.setMessage(message);
}
});
}
@Override
public void run() {
// If we're dying, don't bother doing anything
if (activity.isFinishing()) {
return;
}
if (progress == null)
{
progress = new ProgressDialog(activity);
progress.setTitle(title);
progress.setMessage(message);
progress.setProgressStyle(ProgressDialog.STYLE_SPINNER);
progress.setOnCancelListener(this);
// If we want to finish the activity when this is killed, make it cancellable
if (finish)
{
progress.setCancelable(true);
progress.setCanceledOnTouchOutside(false);
}
else
{
progress.setCancelable(false);
}
synchronized (rundownDialogs) {
rundownDialogs.add(this);
progress.show();
}
}
else
{
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();
}
}