mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2026-04-03 06:26:16 +00:00
Convert tabs to spaces
This commit is contained in:
@@ -5,21 +5,21 @@ import java.util.logging.FileHandler;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public class LimeLog {
|
||||
private static final Logger LOGGER = Logger.getLogger(LimeLog.class.getName());
|
||||
private static final Logger LOGGER = Logger.getLogger(LimeLog.class.getName());
|
||||
|
||||
public static void info(String msg) {
|
||||
LOGGER.info(msg);
|
||||
}
|
||||
|
||||
public static void warning(String msg) {
|
||||
LOGGER.warning(msg);
|
||||
}
|
||||
|
||||
public static void severe(String msg) {
|
||||
LOGGER.severe(msg);
|
||||
}
|
||||
|
||||
public static void setFileHandler(String fileName) throws IOException {
|
||||
LOGGER.addHandler(new FileHandler(fileName));
|
||||
}
|
||||
public static void info(String msg) {
|
||||
LOGGER.info(msg);
|
||||
}
|
||||
|
||||
public static void warning(String msg) {
|
||||
LOGGER.warning(msg);
|
||||
}
|
||||
|
||||
public static void severe(String msg) {
|
||||
LOGGER.severe(msg);
|
||||
}
|
||||
|
||||
public static void setFileHandler(String fileName) throws IOException {
|
||||
LOGGER.addHandler(new FileHandler(fileName));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,290 +8,290 @@ import android.view.KeyEvent;
|
||||
* @author Cameron Gutman
|
||||
*/
|
||||
public class KeyboardTranslator {
|
||||
|
||||
/**
|
||||
* GFE's prefix for every key code
|
||||
*/
|
||||
private 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_NUMPAD0 = 96;
|
||||
|
||||
/**
|
||||
* GFE's prefix for every key code
|
||||
*/
|
||||
private 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_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_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_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_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;
|
||||
public static final int VK_CLEAR = 12;
|
||||
public static final int VK_COMMA = 44;
|
||||
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_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_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;
|
||||
|
||||
public static boolean needsShift(int keycode) {
|
||||
switch (keycode)
|
||||
{
|
||||
case KeyEvent.KEYCODE_AT:
|
||||
case KeyEvent.KEYCODE_POUND:
|
||||
case KeyEvent.KEYCODE_PLUS:
|
||||
case KeyEvent.KEYCODE_STAR:
|
||||
return true;
|
||||
public static boolean needsShift(int keycode) {
|
||||
switch (keycode)
|
||||
{
|
||||
case KeyEvent.KEYCODE_AT:
|
||||
case KeyEvent.KEYCODE_POUND:
|
||||
case KeyEvent.KEYCODE_PLUS:
|
||||
case KeyEvent.KEYCODE_STAR:
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates the given keycode and returns the GFE keycode
|
||||
* @param keycode the code to be translated
|
||||
* @return a GFE keycode for the given keycode
|
||||
*/
|
||||
public static short translate(int keycode) {
|
||||
int translated;
|
||||
|
||||
// This is a poor man's mapping between Android key codes
|
||||
// and Windows VK_* codes. For all defined VK_ codes, see:
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx
|
||||
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:
|
||||
translated = 0xA4;
|
||||
break;
|
||||
/**
|
||||
* Translates the given keycode and returns the GFE keycode
|
||||
* @param keycode the code to be translated
|
||||
* @return a GFE keycode for the given keycode
|
||||
*/
|
||||
public static short translate(int keycode) {
|
||||
int translated;
|
||||
|
||||
// This is a poor man's mapping between Android key codes
|
||||
// and Windows VK_* codes. For all defined VK_ codes, see:
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx
|
||||
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:
|
||||
translated = 0xA4;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_ALT_RIGHT:
|
||||
translated = 0xA5;
|
||||
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:
|
||||
translated = 0xA2;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_ALT_RIGHT:
|
||||
translated = 0xA5;
|
||||
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:
|
||||
translated = 0xA2;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_CTRL_RIGHT:
|
||||
translated = 0xA3;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_DEL:
|
||||
translated = VK_BACK_SPACE;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_ENTER:
|
||||
translated = 0x0d;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_CTRL_RIGHT:
|
||||
translated = 0xA3;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_DEL:
|
||||
translated = VK_BACK_SPACE;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_ENTER:
|
||||
translated = 0x0d;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_PLUS:
|
||||
case KeyEvent.KEYCODE_EQUALS:
|
||||
translated = 0xbb;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_ESCAPE:
|
||||
translated = VK_ESCAPE;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_FORWARD_DEL:
|
||||
translated = 0x2e;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_INSERT:
|
||||
translated = 0x2d;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_LEFT_BRACKET:
|
||||
translated = 0xdb;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_PLUS:
|
||||
case KeyEvent.KEYCODE_EQUALS:
|
||||
translated = 0xbb;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_ESCAPE:
|
||||
translated = VK_ESCAPE;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_FORWARD_DEL:
|
||||
translated = 0x2e;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_INSERT:
|
||||
translated = 0x2d;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_LEFT_BRACKET:
|
||||
translated = 0xdb;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_META_LEFT:
|
||||
translated = 0x5b;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_META_LEFT:
|
||||
translated = 0x5b;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_META_RIGHT:
|
||||
translated = 0x5c;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_META_RIGHT:
|
||||
translated = 0x5c;
|
||||
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:
|
||||
translated = 0xA0;
|
||||
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:
|
||||
translated = 0xA0;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_SHIFT_RIGHT:
|
||||
translated = 0xA1;
|
||||
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;
|
||||
case KeyEvent.KEYCODE_SHIFT_RIGHT:
|
||||
translated = 0xA1;
|
||||
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;
|
||||
|
||||
case KeyEvent.KEYCODE_NUMPAD_DIVIDE:
|
||||
translated = 0x6F;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_NUMPAD_DIVIDE:
|
||||
translated = 0x6F;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_NUMPAD_MULTIPLY:
|
||||
translated = 0x6A;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_NUMPAD_MULTIPLY:
|
||||
translated = 0x6A;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_NUMPAD_SUBTRACT:
|
||||
translated = 0x6D;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_NUMPAD_SUBTRACT:
|
||||
translated = 0x6D;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_NUMPAD_ADD:
|
||||
translated = 0x6B;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_NUMPAD_ADD:
|
||||
translated = 0x6B;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_NUMPAD_DOT:
|
||||
translated = 0x6E;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_NUMPAD_DOT:
|
||||
translated = 0x6E;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_AT:
|
||||
translated = 2 + VK_0;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_AT:
|
||||
translated = 2 + VK_0;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_POUND:
|
||||
translated = 3 + VK_0;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_POUND:
|
||||
translated = 3 + VK_0;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_STAR:
|
||||
translated = 8 + VK_0;
|
||||
break;
|
||||
case KeyEvent.KEYCODE_STAR:
|
||||
translated = 8 + VK_0;
|
||||
break;
|
||||
|
||||
default:
|
||||
System.out.println("No key for "+keycode);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return (short) ((KEY_PREFIX << 8) | translated);
|
||||
}
|
||||
default:
|
||||
System.out.println("No key for "+keycode);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return (short) ((KEY_PREFIX << 8) | translated);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -306,7 +306,7 @@ public class VirtualControllerConfigurationLoader {
|
||||
prefEditor.apply();
|
||||
}
|
||||
|
||||
public static void loadFromPreferences(final VirtualController controller, final Context context) {
|
||||
public static void loadFromPreferences(final VirtualController controller, final Context context) {
|
||||
SharedPreferences pref = context.getSharedPreferences(OSC_PREFERENCE, Activity.MODE_PRIVATE);
|
||||
|
||||
for (VirtualControllerElement element : controller.getElements()) {
|
||||
@@ -324,5 +324,5 @@ public class VirtualControllerConfigurationLoader {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,34 +112,34 @@ public abstract class VirtualControllerElement extends View {
|
||||
|
||||
/*
|
||||
protected void actionShowNormalColorChooser() {
|
||||
AmbilWarnaDialog colorDialog = new AmbilWarnaDialog(getContext(), normalColor, true, new AmbilWarnaDialog.OnAmbilWarnaListener() {
|
||||
@Override
|
||||
public void onCancel(AmbilWarnaDialog dialog)
|
||||
{}
|
||||
AmbilWarnaDialog colorDialog = new AmbilWarnaDialog(getContext(), normalColor, true, new AmbilWarnaDialog.OnAmbilWarnaListener() {
|
||||
@Override
|
||||
public void onCancel(AmbilWarnaDialog dialog)
|
||||
{}
|
||||
|
||||
@Override
|
||||
public void onOk(AmbilWarnaDialog dialog, int color) {
|
||||
normalColor = color;
|
||||
invalidate();
|
||||
}
|
||||
});
|
||||
colorDialog.show();
|
||||
}
|
||||
@Override
|
||||
public void onOk(AmbilWarnaDialog dialog, int color) {
|
||||
normalColor = color;
|
||||
invalidate();
|
||||
}
|
||||
});
|
||||
colorDialog.show();
|
||||
}
|
||||
|
||||
protected void actionShowPressedColorChooser() {
|
||||
AmbilWarnaDialog colorDialog = new AmbilWarnaDialog(getContext(), normalColor, true, new AmbilWarnaDialog.OnAmbilWarnaListener() {
|
||||
@Override
|
||||
public void onCancel(AmbilWarnaDialog dialog) {
|
||||
}
|
||||
protected void actionShowPressedColorChooser() {
|
||||
AmbilWarnaDialog colorDialog = new AmbilWarnaDialog(getContext(), normalColor, true, new AmbilWarnaDialog.OnAmbilWarnaListener() {
|
||||
@Override
|
||||
public void onCancel(AmbilWarnaDialog dialog) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOk(AmbilWarnaDialog dialog, int color) {
|
||||
pressedColor = color;
|
||||
invalidate();
|
||||
}
|
||||
});
|
||||
colorDialog.show();
|
||||
}
|
||||
@Override
|
||||
public void onOk(AmbilWarnaDialog dialog, int color) {
|
||||
pressedColor = color;
|
||||
invalidate();
|
||||
}
|
||||
});
|
||||
colorDialog.show();
|
||||
}
|
||||
*/
|
||||
|
||||
protected void actionEnableMove() {
|
||||
@@ -195,11 +195,11 @@ public abstract class VirtualControllerElement extends View {
|
||||
break;
|
||||
}
|
||||
/*
|
||||
case 2: { // set default color
|
||||
case 2: { // set default color
|
||||
actionShowNormalColorChooser();
|
||||
break;
|
||||
}
|
||||
case 3: { // set pressed color
|
||||
case 3: { // set pressed color
|
||||
actionShowPressedColorChooser();
|
||||
break;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,20 +5,20 @@ import java.security.cert.X509Certificate;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
public class ConnectionContext {
|
||||
public String serverAddress;
|
||||
public X509Certificate serverCert;
|
||||
public StreamConfiguration streamConfig;
|
||||
public NvConnectionListener connListener;
|
||||
public SecretKey riKey;
|
||||
public int riKeyId;
|
||||
|
||||
// This is the version quad from the appversion tag of /serverinfo
|
||||
public String serverAppVersion;
|
||||
public String serverGfeVersion;
|
||||
|
||||
public int negotiatedWidth, negotiatedHeight;
|
||||
public int negotiatedFps;
|
||||
public boolean negotiatedHdr;
|
||||
public String serverAddress;
|
||||
public X509Certificate serverCert;
|
||||
public StreamConfiguration streamConfig;
|
||||
public NvConnectionListener connListener;
|
||||
public SecretKey riKey;
|
||||
public int riKeyId;
|
||||
|
||||
// This is the version quad from the appversion tag of /serverinfo
|
||||
public String serverAppVersion;
|
||||
public String serverGfeVersion;
|
||||
|
||||
public int negotiatedWidth, negotiatedHeight;
|
||||
public int negotiatedFps;
|
||||
public boolean negotiatedHdr;
|
||||
|
||||
public int videoCapabilities;
|
||||
}
|
||||
|
||||
@@ -26,320 +26,320 @@ import com.limelight.nvstream.input.MouseButtonPacket;
|
||||
import com.limelight.nvstream.jni.MoonBridge;
|
||||
|
||||
public class NvConnection {
|
||||
// Context parameters
|
||||
private String host;
|
||||
private LimelightCryptoProvider cryptoProvider;
|
||||
private String uniqueId;
|
||||
private ConnectionContext context;
|
||||
private static Semaphore connectionAllowed = new Semaphore(1);
|
||||
private final boolean isMonkey;
|
||||
|
||||
public NvConnection(String host, String uniqueId, StreamConfiguration config, LimelightCryptoProvider cryptoProvider, X509Certificate serverCert)
|
||||
{
|
||||
this.host = host;
|
||||
this.cryptoProvider = cryptoProvider;
|
||||
this.uniqueId = uniqueId;
|
||||
// Context parameters
|
||||
private String host;
|
||||
private LimelightCryptoProvider cryptoProvider;
|
||||
private String uniqueId;
|
||||
private ConnectionContext context;
|
||||
private static Semaphore connectionAllowed = new Semaphore(1);
|
||||
private final boolean isMonkey;
|
||||
|
||||
public NvConnection(String host, String uniqueId, StreamConfiguration config, LimelightCryptoProvider cryptoProvider, X509Certificate serverCert)
|
||||
{
|
||||
this.host = host;
|
||||
this.cryptoProvider = cryptoProvider;
|
||||
this.uniqueId = uniqueId;
|
||||
|
||||
this.context = new ConnectionContext();
|
||||
this.context.streamConfig = config;
|
||||
this.context.serverCert = serverCert;
|
||||
try {
|
||||
// This is unique per connection
|
||||
this.context.riKey = generateRiAesKey();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
// Should never happen
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
this.context.riKeyId = generateRiKeyId();
|
||||
this.isMonkey = ActivityManager.isUserAMonkey();
|
||||
}
|
||||
|
||||
private static SecretKey generateRiAesKey() throws NoSuchAlgorithmException {
|
||||
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
|
||||
|
||||
// RI keys are 128 bits
|
||||
keyGen.init(128);
|
||||
|
||||
return keyGen.generateKey();
|
||||
}
|
||||
|
||||
private static int generateRiKeyId() {
|
||||
return new SecureRandom().nextInt();
|
||||
}
|
||||
this.context = new ConnectionContext();
|
||||
this.context.streamConfig = config;
|
||||
this.context.serverCert = serverCert;
|
||||
try {
|
||||
// This is unique per connection
|
||||
this.context.riKey = generateRiAesKey();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
// Should never happen
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
this.context.riKeyId = generateRiKeyId();
|
||||
this.isMonkey = ActivityManager.isUserAMonkey();
|
||||
}
|
||||
|
||||
private static SecretKey generateRiAesKey() throws NoSuchAlgorithmException {
|
||||
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
|
||||
|
||||
// RI keys are 128 bits
|
||||
keyGen.init(128);
|
||||
|
||||
return keyGen.generateKey();
|
||||
}
|
||||
|
||||
private static int generateRiKeyId() {
|
||||
return new SecureRandom().nextInt();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
// Interrupt any pending connection. This is thread-safe.
|
||||
MoonBridge.interruptConnection();
|
||||
public void stop() {
|
||||
// Interrupt any pending connection. This is thread-safe.
|
||||
MoonBridge.interruptConnection();
|
||||
|
||||
// Moonlight-core is not thread-safe with respect to connection start and stop, so
|
||||
// we must not invoke that functionality in parallel.
|
||||
synchronized (MoonBridge.class) {
|
||||
MoonBridge.stopConnection();
|
||||
MoonBridge.cleanupBridge();
|
||||
}
|
||||
// Moonlight-core is not thread-safe with respect to connection start and stop, so
|
||||
// we must not invoke that functionality in parallel.
|
||||
synchronized (MoonBridge.class) {
|
||||
MoonBridge.stopConnection();
|
||||
MoonBridge.cleanupBridge();
|
||||
}
|
||||
|
||||
// Now a pending connection can be processed
|
||||
connectionAllowed.release();
|
||||
}
|
||||
|
||||
private boolean startApp() throws XmlPullParserException, IOException
|
||||
{
|
||||
NvHTTP h = new NvHTTP(context.serverAddress, uniqueId, context.serverCert, cryptoProvider);
|
||||
// Now a pending connection can be processed
|
||||
connectionAllowed.release();
|
||||
}
|
||||
|
||||
private boolean startApp() throws XmlPullParserException, IOException
|
||||
{
|
||||
NvHTTP h = new NvHTTP(context.serverAddress, uniqueId, context.serverCert, cryptoProvider);
|
||||
|
||||
String serverInfo = h.getServerInfo();
|
||||
|
||||
context.serverAppVersion = h.getServerVersion(serverInfo);
|
||||
if (context.serverAppVersion == null) {
|
||||
context.connListener.displayMessage("Server version malformed");
|
||||
return false;
|
||||
}
|
||||
String serverInfo = h.getServerInfo();
|
||||
|
||||
context.serverAppVersion = h.getServerVersion(serverInfo);
|
||||
if (context.serverAppVersion == null) {
|
||||
context.connListener.displayMessage("Server version malformed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// May be missing for older servers
|
||||
context.serverGfeVersion = h.getGfeVersion(serverInfo);
|
||||
|
||||
if (h.getPairState(serverInfo) != PairingManager.PairState.PAIRED) {
|
||||
context.connListener.displayMessage("Device not paired with computer");
|
||||
return false;
|
||||
}
|
||||
// May be missing for older servers
|
||||
context.serverGfeVersion = h.getGfeVersion(serverInfo);
|
||||
|
||||
if (h.getPairState(serverInfo) != PairingManager.PairState.PAIRED) {
|
||||
context.connListener.displayMessage("Device not paired with computer");
|
||||
return false;
|
||||
}
|
||||
|
||||
context.negotiatedHdr = context.streamConfig.getEnableHdr();
|
||||
if ((h.getServerCodecModeSupport(serverInfo) & 0x200) == 0 && context.negotiatedHdr) {
|
||||
context.connListener.displayTransientMessage("Your GPU does not support streaming HDR. The stream will be SDR.");
|
||||
context.negotiatedHdr = false;
|
||||
}
|
||||
|
||||
//
|
||||
// Decide on negotiated stream parameters now
|
||||
//
|
||||
|
||||
// Check for a supported stream resolution
|
||||
if (context.streamConfig.getHeight() >= 2160 && !h.supports4K(serverInfo)) {
|
||||
// Client wants 4K but the server can't do it
|
||||
context.connListener.displayTransientMessage("You must update GeForce Experience to stream in 4K. The stream will be 1080p.");
|
||||
|
||||
// Lower resolution to 1080p
|
||||
context.negotiatedWidth = 1920;
|
||||
context.negotiatedHeight = 1080;
|
||||
context.negotiatedFps = context.streamConfig.getRefreshRate();
|
||||
}
|
||||
else {
|
||||
// Take what the client wanted
|
||||
context.negotiatedWidth = context.streamConfig.getWidth();
|
||||
context.negotiatedHeight = context.streamConfig.getHeight();
|
||||
context.negotiatedFps = context.streamConfig.getRefreshRate();
|
||||
}
|
||||
|
||||
//
|
||||
// Video stream format will be decided during the RTSP handshake
|
||||
//
|
||||
|
||||
NvApp app = context.streamConfig.getApp();
|
||||
|
||||
// If the client did not provide an exact app ID, do a lookup with the applist
|
||||
if (!context.streamConfig.getApp().isInitialized()) {
|
||||
LimeLog.info("Using deprecated app lookup method - Please specify an app ID in your StreamConfiguration instead");
|
||||
app = h.getAppByName(context.streamConfig.getApp().getAppName());
|
||||
if (app == null) {
|
||||
context.connListener.displayMessage("The app " + context.streamConfig.getApp().getAppName() + " is not in GFE app list");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// If there's a game running, resume it
|
||||
if (h.getCurrentGame(serverInfo) != 0) {
|
||||
try {
|
||||
if (h.getCurrentGame(serverInfo) == app.getAppId()) {
|
||||
if (!h.resumeApp(context)) {
|
||||
context.connListener.displayMessage("Failed to resume existing session");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return quitAndLaunch(h, context);
|
||||
}
|
||||
} catch (GfeHttpResponseException e) {
|
||||
if (e.getErrorCode() == 470) {
|
||||
// This is the error you get when you try to resume a session that's not yours.
|
||||
// Because this is fairly common, we'll display a more detailed message.
|
||||
context.connListener.displayMessage("This session wasn't started by this device," +
|
||||
" so it cannot be resumed. End streaming on the original " +
|
||||
"device or the PC itself and try again. (Error code: "+e.getErrorCode()+")");
|
||||
return false;
|
||||
}
|
||||
else if (e.getErrorCode() == 525) {
|
||||
context.connListener.displayMessage("The application is minimized. Resume it on the PC manually or " +
|
||||
"quit the session and start streaming again.");
|
||||
return false;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
LimeLog.info("Resumed existing game session");
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
return launchNotRunningApp(h, context);
|
||||
}
|
||||
}
|
||||
context.negotiatedHdr = context.streamConfig.getEnableHdr();
|
||||
if ((h.getServerCodecModeSupport(serverInfo) & 0x200) == 0 && context.negotiatedHdr) {
|
||||
context.connListener.displayTransientMessage("Your GPU does not support streaming HDR. The stream will be SDR.");
|
||||
context.negotiatedHdr = false;
|
||||
}
|
||||
|
||||
//
|
||||
// Decide on negotiated stream parameters now
|
||||
//
|
||||
|
||||
// Check for a supported stream resolution
|
||||
if (context.streamConfig.getHeight() >= 2160 && !h.supports4K(serverInfo)) {
|
||||
// Client wants 4K but the server can't do it
|
||||
context.connListener.displayTransientMessage("You must update GeForce Experience to stream in 4K. The stream will be 1080p.");
|
||||
|
||||
// Lower resolution to 1080p
|
||||
context.negotiatedWidth = 1920;
|
||||
context.negotiatedHeight = 1080;
|
||||
context.negotiatedFps = context.streamConfig.getRefreshRate();
|
||||
}
|
||||
else {
|
||||
// Take what the client wanted
|
||||
context.negotiatedWidth = context.streamConfig.getWidth();
|
||||
context.negotiatedHeight = context.streamConfig.getHeight();
|
||||
context.negotiatedFps = context.streamConfig.getRefreshRate();
|
||||
}
|
||||
|
||||
//
|
||||
// Video stream format will be decided during the RTSP handshake
|
||||
//
|
||||
|
||||
NvApp app = context.streamConfig.getApp();
|
||||
|
||||
// If the client did not provide an exact app ID, do a lookup with the applist
|
||||
if (!context.streamConfig.getApp().isInitialized()) {
|
||||
LimeLog.info("Using deprecated app lookup method - Please specify an app ID in your StreamConfiguration instead");
|
||||
app = h.getAppByName(context.streamConfig.getApp().getAppName());
|
||||
if (app == null) {
|
||||
context.connListener.displayMessage("The app " + context.streamConfig.getApp().getAppName() + " is not in GFE app list");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// If there's a game running, resume it
|
||||
if (h.getCurrentGame(serverInfo) != 0) {
|
||||
try {
|
||||
if (h.getCurrentGame(serverInfo) == app.getAppId()) {
|
||||
if (!h.resumeApp(context)) {
|
||||
context.connListener.displayMessage("Failed to resume existing session");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return quitAndLaunch(h, context);
|
||||
}
|
||||
} catch (GfeHttpResponseException e) {
|
||||
if (e.getErrorCode() == 470) {
|
||||
// This is the error you get when you try to resume a session that's not yours.
|
||||
// Because this is fairly common, we'll display a more detailed message.
|
||||
context.connListener.displayMessage("This session wasn't started by this device," +
|
||||
" so it cannot be resumed. End streaming on the original " +
|
||||
"device or the PC itself and try again. (Error code: "+e.getErrorCode()+")");
|
||||
return false;
|
||||
}
|
||||
else if (e.getErrorCode() == 525) {
|
||||
context.connListener.displayMessage("The application is minimized. Resume it on the PC manually or " +
|
||||
"quit the session and start streaming again.");
|
||||
return false;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
LimeLog.info("Resumed existing game session");
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
return launchNotRunningApp(h, context);
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean quitAndLaunch(NvHTTP h, ConnectionContext context) throws IOException,
|
||||
XmlPullParserException {
|
||||
try {
|
||||
if (!h.quitApp()) {
|
||||
context.connListener.displayMessage("Failed to quit previous session! You must quit it manually");
|
||||
return false;
|
||||
}
|
||||
} catch (GfeHttpResponseException e) {
|
||||
if (e.getErrorCode() == 599) {
|
||||
context.connListener.displayMessage("This session wasn't started by this device," +
|
||||
" so it cannot be quit. End streaming on the original " +
|
||||
"device or the PC itself. (Error code: "+e.getErrorCode()+")");
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
protected boolean quitAndLaunch(NvHTTP h, ConnectionContext context) throws IOException,
|
||||
XmlPullParserException {
|
||||
try {
|
||||
if (!h.quitApp()) {
|
||||
context.connListener.displayMessage("Failed to quit previous session! You must quit it manually");
|
||||
return false;
|
||||
}
|
||||
} catch (GfeHttpResponseException e) {
|
||||
if (e.getErrorCode() == 599) {
|
||||
context.connListener.displayMessage("This session wasn't started by this device," +
|
||||
" so it cannot be quit. End streaming on the original " +
|
||||
"device or the PC itself. (Error code: "+e.getErrorCode()+")");
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return launchNotRunningApp(h, context);
|
||||
}
|
||||
|
||||
private boolean launchNotRunningApp(NvHTTP h, ConnectionContext context)
|
||||
throws IOException, XmlPullParserException {
|
||||
// Launch the app since it's not running
|
||||
if (!h.launchApp(context, context.streamConfig.getApp().getAppId(), context.negotiatedHdr)) {
|
||||
context.connListener.displayMessage("Failed to launch application");
|
||||
return false;
|
||||
}
|
||||
|
||||
LimeLog.info("Launched new game session");
|
||||
|
||||
return true;
|
||||
}
|
||||
return launchNotRunningApp(h, context);
|
||||
}
|
||||
|
||||
private boolean launchNotRunningApp(NvHTTP h, ConnectionContext context)
|
||||
throws IOException, XmlPullParserException {
|
||||
// Launch the app since it's not running
|
||||
if (!h.launchApp(context, context.streamConfig.getApp().getAppId(), context.negotiatedHdr)) {
|
||||
context.connListener.displayMessage("Failed to launch application");
|
||||
return false;
|
||||
}
|
||||
|
||||
LimeLog.info("Launched new game session");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void start(final AudioRenderer audioRenderer, final VideoDecoderRenderer videoDecoderRenderer, final NvConnectionListener connectionListener)
|
||||
{
|
||||
new Thread(new Runnable() {
|
||||
public void run() {
|
||||
context.connListener = connectionListener;
|
||||
context.videoCapabilities = videoDecoderRenderer.getCapabilities();
|
||||
public void start(final AudioRenderer audioRenderer, final VideoDecoderRenderer videoDecoderRenderer, final NvConnectionListener connectionListener)
|
||||
{
|
||||
new Thread(new Runnable() {
|
||||
public void run() {
|
||||
context.connListener = connectionListener;
|
||||
context.videoCapabilities = videoDecoderRenderer.getCapabilities();
|
||||
|
||||
String appName = context.streamConfig.getApp().getAppName();
|
||||
String appName = context.streamConfig.getApp().getAppName();
|
||||
|
||||
context.serverAddress = host;
|
||||
context.connListener.stageStarting(appName);
|
||||
context.serverAddress = host;
|
||||
context.connListener.stageStarting(appName);
|
||||
|
||||
try {
|
||||
if (!startApp()) {
|
||||
context.connListener.stageFailed(appName, 0);
|
||||
return;
|
||||
}
|
||||
context.connListener.stageComplete(appName);
|
||||
} catch (XmlPullParserException | IOException e) {
|
||||
e.printStackTrace();
|
||||
context.connListener.displayMessage(e.getMessage());
|
||||
context.connListener.stageFailed(appName, 0);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (!startApp()) {
|
||||
context.connListener.stageFailed(appName, 0);
|
||||
return;
|
||||
}
|
||||
context.connListener.stageComplete(appName);
|
||||
} catch (XmlPullParserException | IOException e) {
|
||||
e.printStackTrace();
|
||||
context.connListener.displayMessage(e.getMessage());
|
||||
context.connListener.stageFailed(appName, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
ByteBuffer ib = ByteBuffer.allocate(16);
|
||||
ib.putInt(context.riKeyId);
|
||||
ByteBuffer ib = ByteBuffer.allocate(16);
|
||||
ib.putInt(context.riKeyId);
|
||||
|
||||
// Acquire the connection semaphore to ensure we only have one
|
||||
// connection going at once.
|
||||
try {
|
||||
connectionAllowed.acquire();
|
||||
} catch (InterruptedException e) {
|
||||
context.connListener.displayMessage(e.getMessage());
|
||||
context.connListener.stageFailed(appName, 0);
|
||||
return;
|
||||
}
|
||||
// Acquire the connection semaphore to ensure we only have one
|
||||
// connection going at once.
|
||||
try {
|
||||
connectionAllowed.acquire();
|
||||
} catch (InterruptedException e) {
|
||||
context.connListener.displayMessage(e.getMessage());
|
||||
context.connListener.stageFailed(appName, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Moonlight-core is not thread-safe with respect to connection start and stop, so
|
||||
// we must not invoke that functionality in parallel.
|
||||
synchronized (MoonBridge.class) {
|
||||
MoonBridge.setupBridge(videoDecoderRenderer, audioRenderer, connectionListener);
|
||||
int ret = MoonBridge.startConnection(context.serverAddress,
|
||||
context.serverAppVersion, context.serverGfeVersion,
|
||||
context.negotiatedWidth, context.negotiatedHeight,
|
||||
context.negotiatedFps, context.streamConfig.getBitrate(),
|
||||
context.streamConfig.getMaxPacketSize(),
|
||||
context.streamConfig.getRemote(), context.streamConfig.getAudioConfiguration(),
|
||||
context.streamConfig.getHevcSupported(),
|
||||
context.negotiatedHdr,
|
||||
context.streamConfig.getHevcBitratePercentageMultiplier(),
|
||||
context.streamConfig.getClientRefreshRateX100(),
|
||||
context.riKey.getEncoded(), ib.array(),
|
||||
context.videoCapabilities);
|
||||
if (ret != 0) {
|
||||
// LiStartConnection() failed, so the caller is not expected
|
||||
// to stop the connection themselves. We need to release their
|
||||
// semaphore count for them.
|
||||
connectionAllowed.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
public void sendMouseMove(final short deltaX, final short deltaY)
|
||||
{
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendMouseMove(deltaX, deltaY);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendMouseButtonDown(final byte mouseButton)
|
||||
{
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendMouseButton(MouseButtonPacket.PRESS_EVENT, mouseButton);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendMouseButtonUp(final byte mouseButton)
|
||||
{
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendMouseButton(MouseButtonPacket.RELEASE_EVENT, mouseButton);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendControllerInput(final short controllerNumber,
|
||||
final short activeGamepadMask, final short buttonFlags,
|
||||
final byte leftTrigger, final byte rightTrigger,
|
||||
final short leftStickX, final short leftStickY,
|
||||
final short rightStickX, final short rightStickY)
|
||||
{
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendMultiControllerInput(controllerNumber, activeGamepadMask, buttonFlags,
|
||||
leftTrigger, rightTrigger, leftStickX, leftStickY, rightStickX, rightStickY);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendControllerInput(final short buttonFlags,
|
||||
final byte leftTrigger, final byte rightTrigger,
|
||||
final short leftStickX, final short leftStickY,
|
||||
final short rightStickX, final short rightStickY)
|
||||
{
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendControllerInput(buttonFlags, leftTrigger, rightTrigger, leftStickX,
|
||||
leftStickY, rightStickX, rightStickY);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendKeyboardInput(final short keyMap, final byte keyDirection, final byte modifier) {
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendKeyboardInput(keyMap, keyDirection, modifier);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendMouseScroll(final byte scrollClicks) {
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendMouseScroll(scrollClicks);
|
||||
}
|
||||
}
|
||||
// Moonlight-core is not thread-safe with respect to connection start and stop, so
|
||||
// we must not invoke that functionality in parallel.
|
||||
synchronized (MoonBridge.class) {
|
||||
MoonBridge.setupBridge(videoDecoderRenderer, audioRenderer, connectionListener);
|
||||
int ret = MoonBridge.startConnection(context.serverAddress,
|
||||
context.serverAppVersion, context.serverGfeVersion,
|
||||
context.negotiatedWidth, context.negotiatedHeight,
|
||||
context.negotiatedFps, context.streamConfig.getBitrate(),
|
||||
context.streamConfig.getMaxPacketSize(),
|
||||
context.streamConfig.getRemote(), context.streamConfig.getAudioConfiguration(),
|
||||
context.streamConfig.getHevcSupported(),
|
||||
context.negotiatedHdr,
|
||||
context.streamConfig.getHevcBitratePercentageMultiplier(),
|
||||
context.streamConfig.getClientRefreshRateX100(),
|
||||
context.riKey.getEncoded(), ib.array(),
|
||||
context.videoCapabilities);
|
||||
if (ret != 0) {
|
||||
// LiStartConnection() failed, so the caller is not expected
|
||||
// to stop the connection themselves. We need to release their
|
||||
// semaphore count for them.
|
||||
connectionAllowed.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
public void sendMouseMove(final short deltaX, final short deltaY)
|
||||
{
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendMouseMove(deltaX, deltaY);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendMouseButtonDown(final byte mouseButton)
|
||||
{
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendMouseButton(MouseButtonPacket.PRESS_EVENT, mouseButton);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendMouseButtonUp(final byte mouseButton)
|
||||
{
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendMouseButton(MouseButtonPacket.RELEASE_EVENT, mouseButton);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendControllerInput(final short controllerNumber,
|
||||
final short activeGamepadMask, final short buttonFlags,
|
||||
final byte leftTrigger, final byte rightTrigger,
|
||||
final short leftStickX, final short leftStickY,
|
||||
final short rightStickX, final short rightStickY)
|
||||
{
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendMultiControllerInput(controllerNumber, activeGamepadMask, buttonFlags,
|
||||
leftTrigger, rightTrigger, leftStickX, leftStickY, rightStickX, rightStickY);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendControllerInput(final short buttonFlags,
|
||||
final byte leftTrigger, final byte rightTrigger,
|
||||
final short leftStickX, final short leftStickY,
|
||||
final short rightStickX, final short rightStickY)
|
||||
{
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendControllerInput(buttonFlags, leftTrigger, rightTrigger, leftStickX,
|
||||
leftStickY, rightStickX, rightStickY);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendKeyboardInput(final short keyMap, final byte keyDirection, final byte modifier) {
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendKeyboardInput(keyMap, keyDirection, modifier);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendMouseScroll(final byte scrollClicks) {
|
||||
if (!isMonkey) {
|
||||
MoonBridge.sendMouseScroll(scrollClicks);
|
||||
}
|
||||
}
|
||||
|
||||
public static String findExternalAddressForMdns(String stunHostname, int stunPort) {
|
||||
return MoonBridge.findExternalAddressIP4(stunHostname, stunPort);
|
||||
}
|
||||
public static String findExternalAddressForMdns(String stunHostname, int stunPort) {
|
||||
return MoonBridge.findExternalAddressIP4(stunHostname, stunPort);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
package com.limelight.nvstream;
|
||||
|
||||
public interface NvConnectionListener {
|
||||
void stageStarting(String stage);
|
||||
void stageComplete(String stage);
|
||||
void stageFailed(String stage, long errorCode);
|
||||
|
||||
void connectionStarted();
|
||||
void connectionTerminated(long errorCode);
|
||||
void connectionStatusUpdate(int connectionStatus);
|
||||
|
||||
void displayMessage(String message);
|
||||
void displayTransientMessage(String message);
|
||||
void stageStarting(String stage);
|
||||
void stageComplete(String stage);
|
||||
void stageFailed(String stage, long errorCode);
|
||||
|
||||
void connectionStarted();
|
||||
void connectionTerminated(long errorCode);
|
||||
void connectionStatusUpdate(int connectionStatus);
|
||||
|
||||
void displayMessage(String message);
|
||||
void displayTransientMessage(String message);
|
||||
|
||||
void rumble(short controllerNumber, short lowFreqMotor, short highFreqMotor);
|
||||
void rumble(short controllerNumber, short lowFreqMotor, short highFreqMotor);
|
||||
}
|
||||
|
||||
@@ -4,230 +4,230 @@ import com.limelight.nvstream.http.NvApp;
|
||||
import com.limelight.nvstream.jni.MoonBridge;
|
||||
|
||||
public class StreamConfiguration {
|
||||
public static final int INVALID_APP_ID = 0;
|
||||
public static final int INVALID_APP_ID = 0;
|
||||
|
||||
public static final int STREAM_CFG_LOCAL = 0;
|
||||
public static final int STREAM_CFG_REMOTE = 1;
|
||||
public static final int STREAM_CFG_AUTO = 2;
|
||||
public static final int STREAM_CFG_LOCAL = 0;
|
||||
public static final int STREAM_CFG_REMOTE = 1;
|
||||
public static final int STREAM_CFG_AUTO = 2;
|
||||
|
||||
private static final int CHANNEL_COUNT_STEREO = 2;
|
||||
private static final int CHANNEL_COUNT_5_1 = 6;
|
||||
|
||||
private static final int CHANNEL_MASK_STEREO = 0x3;
|
||||
private static final int CHANNEL_MASK_5_1 = 0xFC;
|
||||
|
||||
private NvApp app;
|
||||
private int width, height;
|
||||
private int refreshRate;
|
||||
private int clientRefreshRateX100;
|
||||
private int bitrate;
|
||||
private boolean sops;
|
||||
private boolean enableAdaptiveResolution;
|
||||
private boolean playLocalAudio;
|
||||
private int maxPacketSize;
|
||||
private int remote;
|
||||
private int audioChannelMask;
|
||||
private int audioChannelCount;
|
||||
private int audioConfiguration;
|
||||
private boolean supportsHevc;
|
||||
private int hevcBitratePercentageMultiplier;
|
||||
private boolean enableHdr;
|
||||
private int attachedGamepadMask;
|
||||
private static final int CHANNEL_COUNT_STEREO = 2;
|
||||
private static final int CHANNEL_COUNT_5_1 = 6;
|
||||
|
||||
private static final int CHANNEL_MASK_STEREO = 0x3;
|
||||
private static final int CHANNEL_MASK_5_1 = 0xFC;
|
||||
|
||||
private NvApp app;
|
||||
private int width, height;
|
||||
private int refreshRate;
|
||||
private int clientRefreshRateX100;
|
||||
private int bitrate;
|
||||
private boolean sops;
|
||||
private boolean enableAdaptiveResolution;
|
||||
private boolean playLocalAudio;
|
||||
private int maxPacketSize;
|
||||
private int remote;
|
||||
private int audioChannelMask;
|
||||
private int audioChannelCount;
|
||||
private int audioConfiguration;
|
||||
private boolean supportsHevc;
|
||||
private int hevcBitratePercentageMultiplier;
|
||||
private boolean enableHdr;
|
||||
private int attachedGamepadMask;
|
||||
|
||||
public static class Builder {
|
||||
private StreamConfiguration config = new StreamConfiguration();
|
||||
|
||||
public StreamConfiguration.Builder setApp(NvApp app) {
|
||||
config.app = app;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setRemoteConfiguration(int remote) {
|
||||
config.remote = remote;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setResolution(int width, int height) {
|
||||
config.width = width;
|
||||
config.height = height;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setRefreshRate(int refreshRate) {
|
||||
config.refreshRate = refreshRate;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setBitrate(int bitrate) {
|
||||
config.bitrate = bitrate;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setEnableSops(boolean enable) {
|
||||
config.sops = enable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder enableAdaptiveResolution(boolean enable) {
|
||||
config.enableAdaptiveResolution = enable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder enableLocalAudioPlayback(boolean enable) {
|
||||
config.playLocalAudio = enable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setMaxPacketSize(int maxPacketSize) {
|
||||
config.maxPacketSize = maxPacketSize;
|
||||
return this;
|
||||
}
|
||||
public static class Builder {
|
||||
private StreamConfiguration config = new StreamConfiguration();
|
||||
|
||||
public StreamConfiguration.Builder setApp(NvApp app) {
|
||||
config.app = app;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setRemoteConfiguration(int remote) {
|
||||
config.remote = remote;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setResolution(int width, int height) {
|
||||
config.width = width;
|
||||
config.height = height;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setRefreshRate(int refreshRate) {
|
||||
config.refreshRate = refreshRate;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setBitrate(int bitrate) {
|
||||
config.bitrate = bitrate;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setEnableSops(boolean enable) {
|
||||
config.sops = enable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder enableAdaptiveResolution(boolean enable) {
|
||||
config.enableAdaptiveResolution = enable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder enableLocalAudioPlayback(boolean enable) {
|
||||
config.playLocalAudio = enable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setMaxPacketSize(int maxPacketSize) {
|
||||
config.maxPacketSize = maxPacketSize;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setHevcBitratePercentageMultiplier(int multiplier) {
|
||||
config.hevcBitratePercentageMultiplier = multiplier;
|
||||
return this;
|
||||
}
|
||||
public StreamConfiguration.Builder setHevcBitratePercentageMultiplier(int multiplier) {
|
||||
config.hevcBitratePercentageMultiplier = multiplier;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setEnableHdr(boolean enableHdr) {
|
||||
config.enableHdr = enableHdr;
|
||||
return this;
|
||||
}
|
||||
public StreamConfiguration.Builder setEnableHdr(boolean enableHdr) {
|
||||
config.enableHdr = enableHdr;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setAttachedGamepadMask(int attachedGamepadMask) {
|
||||
config.attachedGamepadMask = attachedGamepadMask;
|
||||
return this;
|
||||
}
|
||||
public StreamConfiguration.Builder setAttachedGamepadMask(int attachedGamepadMask) {
|
||||
config.attachedGamepadMask = attachedGamepadMask;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setAttachedGamepadMaskByCount(int gamepadCount) {
|
||||
config.attachedGamepadMask = 0;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (gamepadCount > i) {
|
||||
config.attachedGamepadMask |= 1 << i;
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
public StreamConfiguration.Builder setAttachedGamepadMaskByCount(int gamepadCount) {
|
||||
config.attachedGamepadMask = 0;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (gamepadCount > i) {
|
||||
config.attachedGamepadMask |= 1 << i;
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setClientRefreshRateX100(int refreshRateX100) {
|
||||
config.clientRefreshRateX100 = refreshRateX100;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setAudioConfiguration(int audioConfig) {
|
||||
if (audioConfig == MoonBridge.AUDIO_CONFIGURATION_STEREO) {
|
||||
config.audioChannelCount = CHANNEL_COUNT_STEREO;
|
||||
config.audioChannelMask = CHANNEL_MASK_STEREO;
|
||||
}
|
||||
else if (audioConfig == MoonBridge.AUDIO_CONFIGURATION_51_SURROUND) {
|
||||
config.audioChannelCount = CHANNEL_COUNT_5_1;
|
||||
config.audioChannelMask = CHANNEL_MASK_5_1;
|
||||
}
|
||||
else {
|
||||
throw new IllegalArgumentException("Invalid audio configuration");
|
||||
}
|
||||
public StreamConfiguration.Builder setClientRefreshRateX100(int refreshRateX100) {
|
||||
config.clientRefreshRateX100 = refreshRateX100;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setAudioConfiguration(int audioConfig) {
|
||||
if (audioConfig == MoonBridge.AUDIO_CONFIGURATION_STEREO) {
|
||||
config.audioChannelCount = CHANNEL_COUNT_STEREO;
|
||||
config.audioChannelMask = CHANNEL_MASK_STEREO;
|
||||
}
|
||||
else if (audioConfig == MoonBridge.AUDIO_CONFIGURATION_51_SURROUND) {
|
||||
config.audioChannelCount = CHANNEL_COUNT_5_1;
|
||||
config.audioChannelMask = CHANNEL_MASK_5_1;
|
||||
}
|
||||
else {
|
||||
throw new IllegalArgumentException("Invalid audio configuration");
|
||||
}
|
||||
|
||||
config.audioConfiguration = audioConfig;
|
||||
config.audioConfiguration = audioConfig;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setHevcSupported(boolean supportsHevc) {
|
||||
config.supportsHevc = supportsHevc;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration build() {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
private StreamConfiguration() {
|
||||
// Set default attributes
|
||||
this.app = new NvApp("Steam");
|
||||
this.width = 1280;
|
||||
this.height = 720;
|
||||
this.refreshRate = 60;
|
||||
this.bitrate = 10000;
|
||||
this.maxPacketSize = 1024;
|
||||
this.remote = STREAM_CFG_AUTO;
|
||||
this.sops = true;
|
||||
this.enableAdaptiveResolution = false;
|
||||
this.audioChannelCount = CHANNEL_COUNT_STEREO;
|
||||
this.audioChannelMask = CHANNEL_MASK_STEREO;
|
||||
this.supportsHevc = false;
|
||||
this.enableHdr = false;
|
||||
this.attachedGamepadMask = 0;
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public int getRefreshRate() {
|
||||
return refreshRate;
|
||||
}
|
||||
|
||||
public int getBitrate() {
|
||||
return bitrate;
|
||||
}
|
||||
|
||||
public int getMaxPacketSize() {
|
||||
return maxPacketSize;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration.Builder setHevcSupported(boolean supportsHevc) {
|
||||
config.supportsHevc = supportsHevc;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StreamConfiguration build() {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
private StreamConfiguration() {
|
||||
// Set default attributes
|
||||
this.app = new NvApp("Steam");
|
||||
this.width = 1280;
|
||||
this.height = 720;
|
||||
this.refreshRate = 60;
|
||||
this.bitrate = 10000;
|
||||
this.maxPacketSize = 1024;
|
||||
this.remote = STREAM_CFG_AUTO;
|
||||
this.sops = true;
|
||||
this.enableAdaptiveResolution = false;
|
||||
this.audioChannelCount = CHANNEL_COUNT_STEREO;
|
||||
this.audioChannelMask = CHANNEL_MASK_STEREO;
|
||||
this.supportsHevc = false;
|
||||
this.enableHdr = false;
|
||||
this.attachedGamepadMask = 0;
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public int getRefreshRate() {
|
||||
return refreshRate;
|
||||
}
|
||||
|
||||
public int getBitrate() {
|
||||
return bitrate;
|
||||
}
|
||||
|
||||
public int getMaxPacketSize() {
|
||||
return maxPacketSize;
|
||||
}
|
||||
|
||||
public NvApp getApp() {
|
||||
return app;
|
||||
}
|
||||
|
||||
public boolean getSops() {
|
||||
return sops;
|
||||
}
|
||||
|
||||
public boolean getAdaptiveResolutionEnabled() {
|
||||
return enableAdaptiveResolution;
|
||||
}
|
||||
|
||||
public boolean getPlayLocalAudio() {
|
||||
return playLocalAudio;
|
||||
}
|
||||
|
||||
public int getRemote() {
|
||||
return remote;
|
||||
}
|
||||
|
||||
public int getAudioChannelCount() {
|
||||
return audioChannelCount;
|
||||
}
|
||||
|
||||
public int getAudioChannelMask() {
|
||||
return audioChannelMask;
|
||||
}
|
||||
public NvApp getApp() {
|
||||
return app;
|
||||
}
|
||||
|
||||
public boolean getSops() {
|
||||
return sops;
|
||||
}
|
||||
|
||||
public boolean getAdaptiveResolutionEnabled() {
|
||||
return enableAdaptiveResolution;
|
||||
}
|
||||
|
||||
public boolean getPlayLocalAudio() {
|
||||
return playLocalAudio;
|
||||
}
|
||||
|
||||
public int getRemote() {
|
||||
return remote;
|
||||
}
|
||||
|
||||
public int getAudioChannelCount() {
|
||||
return audioChannelCount;
|
||||
}
|
||||
|
||||
public int getAudioChannelMask() {
|
||||
return audioChannelMask;
|
||||
}
|
||||
|
||||
public int getAudioConfiguration() {
|
||||
return audioConfiguration;
|
||||
}
|
||||
|
||||
public boolean getHevcSupported() {
|
||||
return supportsHevc;
|
||||
}
|
||||
public int getAudioConfiguration() {
|
||||
return audioConfiguration;
|
||||
}
|
||||
|
||||
public boolean getHevcSupported() {
|
||||
return supportsHevc;
|
||||
}
|
||||
|
||||
public int getHevcBitratePercentageMultiplier() {
|
||||
return hevcBitratePercentageMultiplier;
|
||||
}
|
||||
public int getHevcBitratePercentageMultiplier() {
|
||||
return hevcBitratePercentageMultiplier;
|
||||
}
|
||||
|
||||
public boolean getEnableHdr() {
|
||||
return enableHdr;
|
||||
}
|
||||
public boolean getEnableHdr() {
|
||||
return enableHdr;
|
||||
}
|
||||
|
||||
public int getAttachedGamepadMask() {
|
||||
return attachedGamepadMask;
|
||||
}
|
||||
public int getAttachedGamepadMask() {
|
||||
return attachedGamepadMask;
|
||||
}
|
||||
|
||||
public int getClientRefreshRateX100() {
|
||||
return clientRefreshRateX100;
|
||||
}
|
||||
public int getClientRefreshRateX100() {
|
||||
return clientRefreshRateX100;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,57 +1,57 @@
|
||||
package com.limelight.nvstream.av;
|
||||
|
||||
public class ByteBufferDescriptor {
|
||||
public byte[] data;
|
||||
public int offset;
|
||||
public int length;
|
||||
|
||||
public ByteBufferDescriptor nextDescriptor;
|
||||
|
||||
public ByteBufferDescriptor(byte[] data, int offset, int length)
|
||||
{
|
||||
this.data = data;
|
||||
this.offset = offset;
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
public ByteBufferDescriptor(ByteBufferDescriptor desc)
|
||||
{
|
||||
this.data = desc.data;
|
||||
this.offset = desc.offset;
|
||||
this.length = desc.length;
|
||||
}
|
||||
|
||||
public void reinitialize(byte[] data, int offset, int length)
|
||||
{
|
||||
this.data = data;
|
||||
this.offset = offset;
|
||||
this.length = length;
|
||||
this.nextDescriptor = null;
|
||||
}
|
||||
|
||||
public void print()
|
||||
{
|
||||
print(offset, length);
|
||||
}
|
||||
|
||||
public void print(int length)
|
||||
{
|
||||
print(this.offset, length);
|
||||
}
|
||||
|
||||
public void print(int offset, int length)
|
||||
{
|
||||
for (int i = offset; i < offset+length;) {
|
||||
if (i + 8 <= offset+length) {
|
||||
System.out.printf("%x: %02x %02x %02x %02x %02x %02x %02x %02x\n", i,
|
||||
data[i], data[i+1], data[i+2], data[i+3], data[i+4], data[i+5], data[i+6], data[i+7]);
|
||||
i += 8;
|
||||
}
|
||||
else {
|
||||
System.out.printf("%x: %02x \n", i, data[i]);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
System.out.println();
|
||||
}
|
||||
public byte[] data;
|
||||
public int offset;
|
||||
public int length;
|
||||
|
||||
public ByteBufferDescriptor nextDescriptor;
|
||||
|
||||
public ByteBufferDescriptor(byte[] data, int offset, int length)
|
||||
{
|
||||
this.data = data;
|
||||
this.offset = offset;
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
public ByteBufferDescriptor(ByteBufferDescriptor desc)
|
||||
{
|
||||
this.data = desc.data;
|
||||
this.offset = desc.offset;
|
||||
this.length = desc.length;
|
||||
}
|
||||
|
||||
public void reinitialize(byte[] data, int offset, int length)
|
||||
{
|
||||
this.data = data;
|
||||
this.offset = offset;
|
||||
this.length = length;
|
||||
this.nextDescriptor = null;
|
||||
}
|
||||
|
||||
public void print()
|
||||
{
|
||||
print(offset, length);
|
||||
}
|
||||
|
||||
public void print(int length)
|
||||
{
|
||||
print(this.offset, length);
|
||||
}
|
||||
|
||||
public void print(int offset, int length)
|
||||
{
|
||||
for (int i = offset; i < offset+length;) {
|
||||
if (i + 8 <= offset+length) {
|
||||
System.out.printf("%x: %02x %02x %02x %02x %02x %02x %02x %02x\n", i,
|
||||
data[i], data[i+1], data[i+2], data[i+3], data[i+4], data[i+5], data[i+6], data[i+7]);
|
||||
i += 8;
|
||||
}
|
||||
else {
|
||||
System.out.printf("%x: %02x \n", i, data[i]);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
System.out.println();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package com.limelight.nvstream.av.audio;
|
||||
|
||||
public interface AudioRenderer {
|
||||
int setup(int audioConfiguration);
|
||||
int setup(int audioConfiguration);
|
||||
|
||||
void start();
|
||||
void start();
|
||||
|
||||
void stop();
|
||||
|
||||
void playDecodedAudio(short[] audioData);
|
||||
|
||||
void cleanup();
|
||||
void stop();
|
||||
|
||||
void playDecodedAudio(short[] audioData);
|
||||
|
||||
void cleanup();
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
package com.limelight.nvstream.av.video;
|
||||
|
||||
public abstract class VideoDecoderRenderer {
|
||||
public abstract int setup(int format, int width, int height, int redrawRate);
|
||||
public abstract int setup(int format, int width, int height, int redrawRate);
|
||||
|
||||
public abstract void start();
|
||||
public abstract void start();
|
||||
|
||||
public abstract void stop();
|
||||
public abstract void stop();
|
||||
|
||||
// This is called once for each frame-start NALU. This means it will be called several times
|
||||
// for an IDR frame which contains several parameter sets and the I-frame data.
|
||||
public abstract int submitDecodeUnit(byte[] decodeUnitData, int decodeUnitLength, int decodeUnitType,
|
||||
int frameNumber, long receiveTimeMs);
|
||||
|
||||
public abstract void cleanup();
|
||||
// This is called once for each frame-start NALU. This means it will be called several times
|
||||
// for an IDR frame which contains several parameter sets and the I-frame data.
|
||||
public abstract int submitDecodeUnit(byte[] decodeUnitData, int decodeUnitLength, int decodeUnitType,
|
||||
int frameNumber, long receiveTimeMs);
|
||||
|
||||
public abstract void cleanup();
|
||||
|
||||
public abstract int getCapabilities();
|
||||
public abstract int getCapabilities();
|
||||
}
|
||||
|
||||
@@ -4,82 +4,82 @@ import java.security.cert.X509Certificate;
|
||||
|
||||
|
||||
public class ComputerDetails {
|
||||
public enum State {
|
||||
ONLINE, OFFLINE, UNKNOWN
|
||||
}
|
||||
public enum State {
|
||||
ONLINE, OFFLINE, UNKNOWN
|
||||
}
|
||||
|
||||
// Persistent attributes
|
||||
public String uuid;
|
||||
public String name;
|
||||
public String localAddress;
|
||||
public String remoteAddress;
|
||||
public String manualAddress;
|
||||
public String ipv6Address;
|
||||
public String macAddress;
|
||||
public X509Certificate serverCert;
|
||||
// Persistent attributes
|
||||
public String uuid;
|
||||
public String name;
|
||||
public String localAddress;
|
||||
public String remoteAddress;
|
||||
public String manualAddress;
|
||||
public String ipv6Address;
|
||||
public String macAddress;
|
||||
public X509Certificate serverCert;
|
||||
|
||||
// Transient attributes
|
||||
public State state;
|
||||
public String activeAddress;
|
||||
public PairingManager.PairState pairState;
|
||||
public int runningGameId;
|
||||
public String rawAppList;
|
||||
// Transient attributes
|
||||
public State state;
|
||||
public String activeAddress;
|
||||
public PairingManager.PairState pairState;
|
||||
public int runningGameId;
|
||||
public String rawAppList;
|
||||
|
||||
public ComputerDetails() {
|
||||
// Use defaults
|
||||
state = State.UNKNOWN;
|
||||
}
|
||||
public ComputerDetails() {
|
||||
// Use defaults
|
||||
state = State.UNKNOWN;
|
||||
}
|
||||
|
||||
public ComputerDetails(ComputerDetails details) {
|
||||
// Copy details from the other computer
|
||||
update(details);
|
||||
}
|
||||
public ComputerDetails(ComputerDetails details) {
|
||||
// Copy details from the other computer
|
||||
update(details);
|
||||
}
|
||||
|
||||
public void update(ComputerDetails details) {
|
||||
this.state = details.state;
|
||||
this.name = details.name;
|
||||
this.uuid = details.uuid;
|
||||
if (details.activeAddress != null) {
|
||||
this.activeAddress = details.activeAddress;
|
||||
}
|
||||
// We can get IPv4 loopback addresses with GS IPv6 Forwarder
|
||||
if (details.localAddress != null && !details.localAddress.startsWith("127.")) {
|
||||
this.localAddress = details.localAddress;
|
||||
}
|
||||
if (details.remoteAddress != null) {
|
||||
this.remoteAddress = details.remoteAddress;
|
||||
}
|
||||
if (details.manualAddress != null) {
|
||||
this.manualAddress = details.manualAddress;
|
||||
}
|
||||
if (details.ipv6Address != null) {
|
||||
this.ipv6Address = details.ipv6Address;
|
||||
}
|
||||
if (details.macAddress != null && !details.macAddress.equals("00:00:00:00:00:00")) {
|
||||
this.macAddress = details.macAddress;
|
||||
}
|
||||
if (details.serverCert != null) {
|
||||
this.serverCert = details.serverCert;
|
||||
}
|
||||
this.pairState = details.pairState;
|
||||
this.runningGameId = details.runningGameId;
|
||||
this.rawAppList = details.rawAppList;
|
||||
}
|
||||
public void update(ComputerDetails details) {
|
||||
this.state = details.state;
|
||||
this.name = details.name;
|
||||
this.uuid = details.uuid;
|
||||
if (details.activeAddress != null) {
|
||||
this.activeAddress = details.activeAddress;
|
||||
}
|
||||
// We can get IPv4 loopback addresses with GS IPv6 Forwarder
|
||||
if (details.localAddress != null && !details.localAddress.startsWith("127.")) {
|
||||
this.localAddress = details.localAddress;
|
||||
}
|
||||
if (details.remoteAddress != null) {
|
||||
this.remoteAddress = details.remoteAddress;
|
||||
}
|
||||
if (details.manualAddress != null) {
|
||||
this.manualAddress = details.manualAddress;
|
||||
}
|
||||
if (details.ipv6Address != null) {
|
||||
this.ipv6Address = details.ipv6Address;
|
||||
}
|
||||
if (details.macAddress != null && !details.macAddress.equals("00:00:00:00:00:00")) {
|
||||
this.macAddress = details.macAddress;
|
||||
}
|
||||
if (details.serverCert != null) {
|
||||
this.serverCert = details.serverCert;
|
||||
}
|
||||
this.pairState = details.pairState;
|
||||
this.runningGameId = details.runningGameId;
|
||||
this.rawAppList = details.rawAppList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder str = new StringBuilder();
|
||||
str.append("State: ").append(state).append("\n");
|
||||
str.append("Active Address: ").append(activeAddress).append("\n");
|
||||
str.append("Name: ").append(name).append("\n");
|
||||
str.append("UUID: ").append(uuid).append("\n");
|
||||
str.append("Local Address: ").append(localAddress).append("\n");
|
||||
str.append("Remote Address: ").append(remoteAddress).append("\n");
|
||||
str.append("IPv6 Address: ").append(ipv6Address).append("\n");
|
||||
str.append("Manual Address: ").append(manualAddress).append("\n");
|
||||
str.append("MAC Address: ").append(macAddress).append("\n");
|
||||
str.append("Pair State: ").append(pairState).append("\n");
|
||||
str.append("Running Game ID: ").append(runningGameId).append("\n");
|
||||
return str.toString();
|
||||
}
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder str = new StringBuilder();
|
||||
str.append("State: ").append(state).append("\n");
|
||||
str.append("Active Address: ").append(activeAddress).append("\n");
|
||||
str.append("Name: ").append(name).append("\n");
|
||||
str.append("UUID: ").append(uuid).append("\n");
|
||||
str.append("Local Address: ").append(localAddress).append("\n");
|
||||
str.append("Remote Address: ").append(remoteAddress).append("\n");
|
||||
str.append("IPv6 Address: ").append(ipv6Address).append("\n");
|
||||
str.append("Manual Address: ").append(manualAddress).append("\n");
|
||||
str.append("MAC Address: ").append(macAddress).append("\n");
|
||||
str.append("Pair State: ").append(pairState).append("\n");
|
||||
str.append("Running Game ID: ").append(runningGameId).append("\n");
|
||||
return str.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,26 +3,26 @@ package com.limelight.nvstream.http;
|
||||
import java.io.IOException;
|
||||
|
||||
public class GfeHttpResponseException extends IOException {
|
||||
private static final long serialVersionUID = 1543508830807804222L;
|
||||
|
||||
private int errorCode;
|
||||
private String errorMsg;
|
||||
|
||||
public GfeHttpResponseException(int errorCode, String errorMsg) {
|
||||
this.errorCode = errorCode;
|
||||
this.errorMsg = errorMsg;
|
||||
}
|
||||
|
||||
public int getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
public String getErrorMessage() {
|
||||
return errorMsg;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return "GeForce Experience returned error: "+errorMsg+" (Error code: "+errorCode+")";
|
||||
}
|
||||
private static final long serialVersionUID = 1543508830807804222L;
|
||||
|
||||
private int errorCode;
|
||||
private String errorMsg;
|
||||
|
||||
public GfeHttpResponseException(int errorCode, String errorMsg) {
|
||||
this.errorCode = errorCode;
|
||||
this.errorMsg = errorMsg;
|
||||
}
|
||||
|
||||
public int getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
public String getErrorMessage() {
|
||||
return errorMsg;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return "GeForce Experience returned error: "+errorMsg+" (Error code: "+errorCode+")";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import java.security.cert.X509Certificate;
|
||||
import java.security.interfaces.RSAPrivateKey;
|
||||
|
||||
public interface LimelightCryptoProvider {
|
||||
X509Certificate getClientCertificate();
|
||||
RSAPrivateKey getClientPrivateKey();
|
||||
byte[] getPemEncodedClientCertificate();
|
||||
String encodeBase64String(byte[] data);
|
||||
X509Certificate getClientCertificate();
|
||||
RSAPrivateKey getClientPrivateKey();
|
||||
byte[] getPemEncodedClientCertificate();
|
||||
String encodeBase64String(byte[] data);
|
||||
}
|
||||
|
||||
@@ -3,59 +3,59 @@ package com.limelight.nvstream.http;
|
||||
import com.limelight.LimeLog;
|
||||
|
||||
public class NvApp {
|
||||
private String appName = "";
|
||||
private int appId;
|
||||
private boolean initialized;
|
||||
private boolean hdrSupported;
|
||||
|
||||
public NvApp() {}
|
||||
|
||||
public NvApp(String appName) {
|
||||
this.appName = appName;
|
||||
}
|
||||
|
||||
public NvApp(String appName, int appId, boolean hdrSupported) {
|
||||
this.appName = appName;
|
||||
this.appId = appId;
|
||||
this.hdrSupported = hdrSupported;
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
public void setAppName(String appName) {
|
||||
this.appName = appName;
|
||||
}
|
||||
|
||||
public void setAppId(String appId) {
|
||||
try {
|
||||
this.appId = Integer.parseInt(appId);
|
||||
this.initialized = true;
|
||||
} catch (NumberFormatException e) {
|
||||
LimeLog.warning("Malformed app ID: "+appId);
|
||||
}
|
||||
}
|
||||
|
||||
public void setAppId(int appId) {
|
||||
this.appId = appId;
|
||||
this.initialized = true;
|
||||
}
|
||||
private String appName = "";
|
||||
private int appId;
|
||||
private boolean initialized;
|
||||
private boolean hdrSupported;
|
||||
|
||||
public NvApp() {}
|
||||
|
||||
public NvApp(String appName) {
|
||||
this.appName = appName;
|
||||
}
|
||||
|
||||
public NvApp(String appName, int appId, boolean hdrSupported) {
|
||||
this.appName = appName;
|
||||
this.appId = appId;
|
||||
this.hdrSupported = hdrSupported;
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
public void setAppName(String appName) {
|
||||
this.appName = appName;
|
||||
}
|
||||
|
||||
public void setAppId(String appId) {
|
||||
try {
|
||||
this.appId = Integer.parseInt(appId);
|
||||
this.initialized = true;
|
||||
} catch (NumberFormatException e) {
|
||||
LimeLog.warning("Malformed app ID: "+appId);
|
||||
}
|
||||
}
|
||||
|
||||
public void setAppId(int appId) {
|
||||
this.appId = appId;
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
public void setHdrSupported(boolean hdrSupported) {
|
||||
this.hdrSupported = hdrSupported;
|
||||
}
|
||||
|
||||
public String getAppName() {
|
||||
return this.appName;
|
||||
}
|
||||
|
||||
public int getAppId() {
|
||||
return this.appId;
|
||||
}
|
||||
public void setHdrSupported(boolean hdrSupported) {
|
||||
this.hdrSupported = hdrSupported;
|
||||
}
|
||||
|
||||
public String getAppName() {
|
||||
return this.appName;
|
||||
}
|
||||
|
||||
public int getAppId() {
|
||||
return this.appId;
|
||||
}
|
||||
|
||||
public boolean isHdrSupported() {
|
||||
return this.hdrSupported;
|
||||
}
|
||||
|
||||
public boolean isInitialized() {
|
||||
return this.initialized;
|
||||
}
|
||||
public boolean isHdrSupported() {
|
||||
return this.hdrSupported;
|
||||
}
|
||||
|
||||
public boolean isInitialized() {
|
||||
return this.initialized;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,323 +18,323 @@ import java.util.Random;
|
||||
|
||||
public class PairingManager {
|
||||
|
||||
private NvHTTP http;
|
||||
|
||||
private PrivateKey pk;
|
||||
private X509Certificate cert;
|
||||
private SecretKey aesKey;
|
||||
private byte[] pemCertBytes;
|
||||
private NvHTTP http;
|
||||
|
||||
private PrivateKey pk;
|
||||
private X509Certificate cert;
|
||||
private SecretKey aesKey;
|
||||
private byte[] pemCertBytes;
|
||||
|
||||
private X509Certificate serverCert;
|
||||
|
||||
public enum PairState {
|
||||
NOT_PAIRED,
|
||||
PAIRED,
|
||||
PIN_WRONG,
|
||||
FAILED,
|
||||
ALREADY_IN_PROGRESS
|
||||
}
|
||||
|
||||
public PairingManager(NvHTTP http, LimelightCryptoProvider cryptoProvider) {
|
||||
this.http = http;
|
||||
this.cert = cryptoProvider.getClientCertificate();
|
||||
this.pemCertBytes = cryptoProvider.getPemEncodedClientCertificate();
|
||||
this.pk = cryptoProvider.getClientPrivateKey();
|
||||
}
|
||||
|
||||
final private static char[] hexArray = "0123456789ABCDEF".toCharArray();
|
||||
private static String bytesToHex(byte[] bytes) {
|
||||
char[] hexChars = new char[bytes.length * 2];
|
||||
for ( int j = 0; j < bytes.length; j++ ) {
|
||||
int v = bytes[j] & 0xFF;
|
||||
hexChars[j * 2] = hexArray[v >>> 4];
|
||||
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
|
||||
}
|
||||
return new String(hexChars);
|
||||
}
|
||||
|
||||
private static byte[] hexToBytes(String s) {
|
||||
int len = s.length();
|
||||
byte[] data = new byte[len / 2];
|
||||
for (int i = 0; i < len; i += 2) {
|
||||
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
|
||||
+ Character.digit(s.charAt(i+1), 16));
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
private X509Certificate extractPlainCert(String text) throws XmlPullParserException, IOException
|
||||
{
|
||||
String certText = NvHTTP.getXmlString(text, "plaincert");
|
||||
if (certText != null) {
|
||||
byte[] certBytes = hexToBytes(certText);
|
||||
private X509Certificate serverCert;
|
||||
|
||||
public enum PairState {
|
||||
NOT_PAIRED,
|
||||
PAIRED,
|
||||
PIN_WRONG,
|
||||
FAILED,
|
||||
ALREADY_IN_PROGRESS
|
||||
}
|
||||
|
||||
public PairingManager(NvHTTP http, LimelightCryptoProvider cryptoProvider) {
|
||||
this.http = http;
|
||||
this.cert = cryptoProvider.getClientCertificate();
|
||||
this.pemCertBytes = cryptoProvider.getPemEncodedClientCertificate();
|
||||
this.pk = cryptoProvider.getClientPrivateKey();
|
||||
}
|
||||
|
||||
final private static char[] hexArray = "0123456789ABCDEF".toCharArray();
|
||||
private static String bytesToHex(byte[] bytes) {
|
||||
char[] hexChars = new char[bytes.length * 2];
|
||||
for ( int j = 0; j < bytes.length; j++ ) {
|
||||
int v = bytes[j] & 0xFF;
|
||||
hexChars[j * 2] = hexArray[v >>> 4];
|
||||
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
|
||||
}
|
||||
return new String(hexChars);
|
||||
}
|
||||
|
||||
private static byte[] hexToBytes(String s) {
|
||||
int len = s.length();
|
||||
byte[] data = new byte[len / 2];
|
||||
for (int i = 0; i < len; i += 2) {
|
||||
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
|
||||
+ Character.digit(s.charAt(i+1), 16));
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
private X509Certificate extractPlainCert(String text) throws XmlPullParserException, IOException
|
||||
{
|
||||
String certText = NvHTTP.getXmlString(text, "plaincert");
|
||||
if (certText != null) {
|
||||
byte[] certBytes = hexToBytes(certText);
|
||||
|
||||
try {
|
||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||
return (X509Certificate)cf.generateCertificate(new ByteArrayInputStream(certBytes));
|
||||
} catch (CertificateException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] generateRandomBytes(int length)
|
||||
{
|
||||
byte[] rand = new byte[length];
|
||||
new SecureRandom().nextBytes(rand);
|
||||
return rand;
|
||||
}
|
||||
|
||||
private static byte[] saltPin(byte[] salt, String pin) throws UnsupportedEncodingException {
|
||||
byte[] saltedPin = new byte[salt.length + pin.length()];
|
||||
System.arraycopy(salt, 0, saltedPin, 0, salt.length);
|
||||
System.arraycopy(pin.getBytes("UTF-8"), 0, saltedPin, salt.length, pin.length());
|
||||
return saltedPin;
|
||||
}
|
||||
|
||||
private static boolean verifySignature(byte[] data, byte[] signature, Certificate cert) {
|
||||
try {
|
||||
Signature sig = Signature.getInstance("SHA256withRSA");
|
||||
sig.initVerify(cert.getPublicKey());
|
||||
sig.update(data);
|
||||
return sig.verify(signature);
|
||||
} catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] signData(byte[] data, PrivateKey key) {
|
||||
try {
|
||||
Signature sig = Signature.getInstance("SHA256withRSA");
|
||||
sig.initSign(key);
|
||||
sig.update(data);
|
||||
byte[] signature = new byte[256];
|
||||
sig.sign(signature, 0, signature.length);
|
||||
return signature;
|
||||
} catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] decryptAes(byte[] encryptedData, SecretKey secretKey) {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
|
||||
|
||||
int blockRoundedSize = ((encryptedData.length + 15) / 16) * 16;
|
||||
byte[] blockRoundedEncrypted = Arrays.copyOf(encryptedData, blockRoundedSize);
|
||||
byte[] fullDecrypted = new byte[blockRoundedSize];
|
||||
try {
|
||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||
return (X509Certificate)cf.generateCertificate(new ByteArrayInputStream(certBytes));
|
||||
} catch (CertificateException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] generateRandomBytes(int length)
|
||||
{
|
||||
byte[] rand = new byte[length];
|
||||
new SecureRandom().nextBytes(rand);
|
||||
return rand;
|
||||
}
|
||||
|
||||
private static byte[] saltPin(byte[] salt, String pin) throws UnsupportedEncodingException {
|
||||
byte[] saltedPin = new byte[salt.length + pin.length()];
|
||||
System.arraycopy(salt, 0, saltedPin, 0, salt.length);
|
||||
System.arraycopy(pin.getBytes("UTF-8"), 0, saltedPin, salt.length, pin.length());
|
||||
return saltedPin;
|
||||
}
|
||||
|
||||
private static boolean verifySignature(byte[] data, byte[] signature, Certificate cert) {
|
||||
try {
|
||||
Signature sig = Signature.getInstance("SHA256withRSA");
|
||||
sig.initVerify(cert.getPublicKey());
|
||||
sig.update(data);
|
||||
return sig.verify(signature);
|
||||
} catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] signData(byte[] data, PrivateKey key) {
|
||||
try {
|
||||
Signature sig = Signature.getInstance("SHA256withRSA");
|
||||
sig.initSign(key);
|
||||
sig.update(data);
|
||||
byte[] signature = new byte[256];
|
||||
sig.sign(signature, 0, signature.length);
|
||||
return signature;
|
||||
} catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] decryptAes(byte[] encryptedData, SecretKey secretKey) {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
|
||||
|
||||
int blockRoundedSize = ((encryptedData.length + 15) / 16) * 16;
|
||||
byte[] blockRoundedEncrypted = Arrays.copyOf(encryptedData, blockRoundedSize);
|
||||
byte[] fullDecrypted = new byte[blockRoundedSize];
|
||||
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey);
|
||||
cipher.doFinal(blockRoundedEncrypted, 0,
|
||||
blockRoundedSize, fullDecrypted);
|
||||
return fullDecrypted;
|
||||
} catch (GeneralSecurityException e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] encryptAes(byte[] data, SecretKey secretKey) {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
|
||||
|
||||
int blockRoundedSize = ((data.length + 15) / 16) * 16;
|
||||
byte[] blockRoundedData = Arrays.copyOf(data, blockRoundedSize);
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey);
|
||||
cipher.doFinal(blockRoundedEncrypted, 0,
|
||||
blockRoundedSize, fullDecrypted);
|
||||
return fullDecrypted;
|
||||
} catch (GeneralSecurityException e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] encryptAes(byte[] data, SecretKey secretKey) {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
|
||||
|
||||
int blockRoundedSize = ((data.length + 15) / 16) * 16;
|
||||
byte[] blockRoundedData = Arrays.copyOf(data, blockRoundedSize);
|
||||
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
|
||||
return cipher.doFinal(blockRoundedData);
|
||||
} catch (GeneralSecurityException e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static SecretKey generateAesKey(PairingHashAlgorithm hashAlgo, byte[] keyData) {
|
||||
byte[] aesTruncated = Arrays.copyOf(hashAlgo.hashData(keyData), 16);
|
||||
return new SecretKeySpec(aesTruncated, "AES");
|
||||
}
|
||||
|
||||
private static byte[] concatBytes(byte[] a, byte[] b) {
|
||||
byte[] c = new byte[a.length + b.length];
|
||||
System.arraycopy(a, 0, c, 0, a.length);
|
||||
System.arraycopy(b, 0, c, a.length, b.length);
|
||||
return c;
|
||||
}
|
||||
|
||||
public static String generatePinString() {
|
||||
Random r = new Random();
|
||||
return String.format((Locale)null, "%d%d%d%d",
|
||||
r.nextInt(10), r.nextInt(10),
|
||||
r.nextInt(10), r.nextInt(10));
|
||||
}
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
|
||||
return cipher.doFinal(blockRoundedData);
|
||||
} catch (GeneralSecurityException e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static SecretKey generateAesKey(PairingHashAlgorithm hashAlgo, byte[] keyData) {
|
||||
byte[] aesTruncated = Arrays.copyOf(hashAlgo.hashData(keyData), 16);
|
||||
return new SecretKeySpec(aesTruncated, "AES");
|
||||
}
|
||||
|
||||
private static byte[] concatBytes(byte[] a, byte[] b) {
|
||||
byte[] c = new byte[a.length + b.length];
|
||||
System.arraycopy(a, 0, c, 0, a.length);
|
||||
System.arraycopy(b, 0, c, a.length, b.length);
|
||||
return c;
|
||||
}
|
||||
|
||||
public static String generatePinString() {
|
||||
Random r = new Random();
|
||||
return String.format((Locale)null, "%d%d%d%d",
|
||||
r.nextInt(10), r.nextInt(10),
|
||||
r.nextInt(10), r.nextInt(10));
|
||||
}
|
||||
|
||||
public X509Certificate getPairedCert() {
|
||||
return serverCert;
|
||||
}
|
||||
|
||||
public PairState pair(String serverInfo, String pin) throws IOException, XmlPullParserException {
|
||||
PairingHashAlgorithm hashAlgo;
|
||||
public X509Certificate getPairedCert() {
|
||||
return serverCert;
|
||||
}
|
||||
|
||||
public PairState pair(String serverInfo, String pin) throws IOException, XmlPullParserException {
|
||||
PairingHashAlgorithm hashAlgo;
|
||||
|
||||
int serverMajorVersion = http.getServerMajorVersion(serverInfo);
|
||||
LimeLog.info("Pairing with server generation: "+serverMajorVersion);
|
||||
if (serverMajorVersion >= 7) {
|
||||
// Gen 7+ uses SHA-256 hashing
|
||||
hashAlgo = new Sha256PairingHash();
|
||||
}
|
||||
else {
|
||||
// Prior to Gen 7, SHA-1 is used
|
||||
hashAlgo = new Sha1PairingHash();
|
||||
}
|
||||
|
||||
// Generate a salt for hashing the PIN
|
||||
byte[] salt = generateRandomBytes(16);
|
||||
int serverMajorVersion = http.getServerMajorVersion(serverInfo);
|
||||
LimeLog.info("Pairing with server generation: "+serverMajorVersion);
|
||||
if (serverMajorVersion >= 7) {
|
||||
// Gen 7+ uses SHA-256 hashing
|
||||
hashAlgo = new Sha256PairingHash();
|
||||
}
|
||||
else {
|
||||
// Prior to Gen 7, SHA-1 is used
|
||||
hashAlgo = new Sha1PairingHash();
|
||||
}
|
||||
|
||||
// Generate a salt for hashing the PIN
|
||||
byte[] salt = generateRandomBytes(16);
|
||||
|
||||
// Combine the salt and pin, then create an AES key from them
|
||||
byte[] saltAndPin = saltPin(salt, pin);
|
||||
aesKey = generateAesKey(hashAlgo, saltAndPin);
|
||||
|
||||
// Send the salt and get the server cert. This doesn't have a read timeout
|
||||
// because the user must enter the PIN before the server responds
|
||||
String getCert = http.openHttpConnectionToString(http.baseUrlHttp +
|
||||
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&phrase=getservercert&salt="+
|
||||
bytesToHex(salt)+"&clientcert="+bytesToHex(pemCertBytes),
|
||||
false);
|
||||
if (!NvHTTP.getXmlString(getCert, "paired").equals("1")) {
|
||||
return PairState.FAILED;
|
||||
}
|
||||
// Combine the salt and pin, then create an AES key from them
|
||||
byte[] saltAndPin = saltPin(salt, pin);
|
||||
aesKey = generateAesKey(hashAlgo, saltAndPin);
|
||||
|
||||
// Send the salt and get the server cert. This doesn't have a read timeout
|
||||
// because the user must enter the PIN before the server responds
|
||||
String getCert = http.openHttpConnectionToString(http.baseUrlHttp +
|
||||
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&phrase=getservercert&salt="+
|
||||
bytesToHex(salt)+"&clientcert="+bytesToHex(pemCertBytes),
|
||||
false);
|
||||
if (!NvHTTP.getXmlString(getCert, "paired").equals("1")) {
|
||||
return PairState.FAILED;
|
||||
}
|
||||
|
||||
// Save this cert for retrieval later
|
||||
serverCert = extractPlainCert(getCert);
|
||||
if (serverCert == null) {
|
||||
// Attempting to pair while another device is pairing will cause GFE
|
||||
// to give an empty cert in the response.
|
||||
return PairState.ALREADY_IN_PROGRESS;
|
||||
}
|
||||
// Save this cert for retrieval later
|
||||
serverCert = extractPlainCert(getCert);
|
||||
if (serverCert == null) {
|
||||
// Attempting to pair while another device is pairing will cause GFE
|
||||
// to give an empty cert in the response.
|
||||
return PairState.ALREADY_IN_PROGRESS;
|
||||
}
|
||||
|
||||
// Require this cert for TLS to this host
|
||||
http.setServerCert(serverCert);
|
||||
|
||||
// Generate a random challenge and encrypt it with our AES key
|
||||
byte[] randomChallenge = generateRandomBytes(16);
|
||||
byte[] encryptedChallenge = encryptAes(randomChallenge, aesKey);
|
||||
|
||||
// Send the encrypted challenge to the server
|
||||
String challengeResp = http.openHttpConnectionToString(http.baseUrlHttp +
|
||||
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&clientchallenge="+bytesToHex(encryptedChallenge),
|
||||
true);
|
||||
if (!NvHTTP.getXmlString(challengeResp, "paired").equals("1")) {
|
||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||
return PairState.FAILED;
|
||||
}
|
||||
|
||||
// Decode the server's response and subsequent challenge
|
||||
byte[] encServerChallengeResponse = hexToBytes(NvHTTP.getXmlString(challengeResp, "challengeresponse"));
|
||||
byte[] decServerChallengeResponse = decryptAes(encServerChallengeResponse, aesKey);
|
||||
|
||||
byte[] serverResponse = Arrays.copyOfRange(decServerChallengeResponse, 0, hashAlgo.getHashLength());
|
||||
byte[] serverChallenge = Arrays.copyOfRange(decServerChallengeResponse, hashAlgo.getHashLength(), hashAlgo.getHashLength() + 16);
|
||||
|
||||
// Using another 16 bytes secret, compute a challenge response hash using the secret, our cert sig, and the challenge
|
||||
byte[] clientSecret = generateRandomBytes(16);
|
||||
byte[] challengeRespHash = hashAlgo.hashData(concatBytes(concatBytes(serverChallenge, cert.getSignature()), clientSecret));
|
||||
byte[] challengeRespEncrypted = encryptAes(challengeRespHash, aesKey);
|
||||
String secretResp = http.openHttpConnectionToString(http.baseUrlHttp +
|
||||
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&serverchallengeresp="+bytesToHex(challengeRespEncrypted),
|
||||
true);
|
||||
if (!NvHTTP.getXmlString(secretResp, "paired").equals("1")) {
|
||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||
return PairState.FAILED;
|
||||
}
|
||||
|
||||
// Get the server's signed secret
|
||||
byte[] serverSecretResp = hexToBytes(NvHTTP.getXmlString(secretResp, "pairingsecret"));
|
||||
byte[] serverSecret = Arrays.copyOfRange(serverSecretResp, 0, 16);
|
||||
byte[] serverSignature = Arrays.copyOfRange(serverSecretResp, 16, 272);
|
||||
// Require this cert for TLS to this host
|
||||
http.setServerCert(serverCert);
|
||||
|
||||
// Generate a random challenge and encrypt it with our AES key
|
||||
byte[] randomChallenge = generateRandomBytes(16);
|
||||
byte[] encryptedChallenge = encryptAes(randomChallenge, aesKey);
|
||||
|
||||
// Send the encrypted challenge to the server
|
||||
String challengeResp = http.openHttpConnectionToString(http.baseUrlHttp +
|
||||
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&clientchallenge="+bytesToHex(encryptedChallenge),
|
||||
true);
|
||||
if (!NvHTTP.getXmlString(challengeResp, "paired").equals("1")) {
|
||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||
return PairState.FAILED;
|
||||
}
|
||||
|
||||
// Decode the server's response and subsequent challenge
|
||||
byte[] encServerChallengeResponse = hexToBytes(NvHTTP.getXmlString(challengeResp, "challengeresponse"));
|
||||
byte[] decServerChallengeResponse = decryptAes(encServerChallengeResponse, aesKey);
|
||||
|
||||
byte[] serverResponse = Arrays.copyOfRange(decServerChallengeResponse, 0, hashAlgo.getHashLength());
|
||||
byte[] serverChallenge = Arrays.copyOfRange(decServerChallengeResponse, hashAlgo.getHashLength(), hashAlgo.getHashLength() + 16);
|
||||
|
||||
// Using another 16 bytes secret, compute a challenge response hash using the secret, our cert sig, and the challenge
|
||||
byte[] clientSecret = generateRandomBytes(16);
|
||||
byte[] challengeRespHash = hashAlgo.hashData(concatBytes(concatBytes(serverChallenge, cert.getSignature()), clientSecret));
|
||||
byte[] challengeRespEncrypted = encryptAes(challengeRespHash, aesKey);
|
||||
String secretResp = http.openHttpConnectionToString(http.baseUrlHttp +
|
||||
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&serverchallengeresp="+bytesToHex(challengeRespEncrypted),
|
||||
true);
|
||||
if (!NvHTTP.getXmlString(secretResp, "paired").equals("1")) {
|
||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||
return PairState.FAILED;
|
||||
}
|
||||
|
||||
// Get the server's signed secret
|
||||
byte[] serverSecretResp = hexToBytes(NvHTTP.getXmlString(secretResp, "pairingsecret"));
|
||||
byte[] serverSecret = Arrays.copyOfRange(serverSecretResp, 0, 16);
|
||||
byte[] serverSignature = Arrays.copyOfRange(serverSecretResp, 16, 272);
|
||||
|
||||
// Ensure the authenticity of the data
|
||||
if (!verifySignature(serverSecret, serverSignature, serverCert)) {
|
||||
// Cancel the pairing process
|
||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||
|
||||
// Looks like a MITM
|
||||
return PairState.FAILED;
|
||||
}
|
||||
|
||||
// Ensure the server challenge matched what we expected (aka the PIN was correct)
|
||||
byte[] serverChallengeRespHash = hashAlgo.hashData(concatBytes(concatBytes(randomChallenge, serverCert.getSignature()), serverSecret));
|
||||
if (!Arrays.equals(serverChallengeRespHash, serverResponse)) {
|
||||
// Cancel the pairing process
|
||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||
|
||||
// Probably got the wrong PIN
|
||||
return PairState.PIN_WRONG;
|
||||
}
|
||||
|
||||
// Send the server our signed secret
|
||||
byte[] clientPairingSecret = concatBytes(clientSecret, signData(clientSecret, pk));
|
||||
String clientSecretResp = http.openHttpConnectionToString(http.baseUrlHttp +
|
||||
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&clientpairingsecret="+bytesToHex(clientPairingSecret),
|
||||
true);
|
||||
if (!NvHTTP.getXmlString(clientSecretResp, "paired").equals("1")) {
|
||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||
return PairState.FAILED;
|
||||
}
|
||||
|
||||
// Do the initial challenge (seems neccessary for us to show as paired)
|
||||
String pairChallenge = http.openHttpConnectionToString(http.baseUrlHttps +
|
||||
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&phrase=pairchallenge", true);
|
||||
if (!NvHTTP.getXmlString(pairChallenge, "paired").equals("1")) {
|
||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||
return PairState.FAILED;
|
||||
}
|
||||
// Ensure the authenticity of the data
|
||||
if (!verifySignature(serverSecret, serverSignature, serverCert)) {
|
||||
// Cancel the pairing process
|
||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||
|
||||
// Looks like a MITM
|
||||
return PairState.FAILED;
|
||||
}
|
||||
|
||||
// Ensure the server challenge matched what we expected (aka the PIN was correct)
|
||||
byte[] serverChallengeRespHash = hashAlgo.hashData(concatBytes(concatBytes(randomChallenge, serverCert.getSignature()), serverSecret));
|
||||
if (!Arrays.equals(serverChallengeRespHash, serverResponse)) {
|
||||
// Cancel the pairing process
|
||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||
|
||||
// Probably got the wrong PIN
|
||||
return PairState.PIN_WRONG;
|
||||
}
|
||||
|
||||
// Send the server our signed secret
|
||||
byte[] clientPairingSecret = concatBytes(clientSecret, signData(clientSecret, pk));
|
||||
String clientSecretResp = http.openHttpConnectionToString(http.baseUrlHttp +
|
||||
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&clientpairingsecret="+bytesToHex(clientPairingSecret),
|
||||
true);
|
||||
if (!NvHTTP.getXmlString(clientSecretResp, "paired").equals("1")) {
|
||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||
return PairState.FAILED;
|
||||
}
|
||||
|
||||
// Do the initial challenge (seems neccessary for us to show as paired)
|
||||
String pairChallenge = http.openHttpConnectionToString(http.baseUrlHttps +
|
||||
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&phrase=pairchallenge", true);
|
||||
if (!NvHTTP.getXmlString(pairChallenge, "paired").equals("1")) {
|
||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||
return PairState.FAILED;
|
||||
}
|
||||
|
||||
return PairState.PAIRED;
|
||||
}
|
||||
|
||||
private interface PairingHashAlgorithm {
|
||||
int getHashLength();
|
||||
byte[] hashData(byte[] data);
|
||||
}
|
||||
|
||||
private static class Sha1PairingHash implements PairingHashAlgorithm {
|
||||
public int getHashLength() {
|
||||
return 20;
|
||||
}
|
||||
|
||||
public byte[] hashData(byte[] data) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-1");
|
||||
return md.digest(data);
|
||||
}
|
||||
catch (NoSuchAlgorithmException e) {
|
||||
// Shouldn't ever happen
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class Sha256PairingHash implements PairingHashAlgorithm {
|
||||
public int getHashLength() {
|
||||
return 32;
|
||||
}
|
||||
|
||||
public byte[] hashData(byte[] data) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
return md.digest(data);
|
||||
}
|
||||
catch (NoSuchAlgorithmException e) {
|
||||
// Shouldn't ever happen
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return PairState.PAIRED;
|
||||
}
|
||||
|
||||
private interface PairingHashAlgorithm {
|
||||
int getHashLength();
|
||||
byte[] hashData(byte[] data);
|
||||
}
|
||||
|
||||
private static class Sha1PairingHash implements PairingHashAlgorithm {
|
||||
public int getHashLength() {
|
||||
return 20;
|
||||
}
|
||||
|
||||
public byte[] hashData(byte[] data) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-1");
|
||||
return md.digest(data);
|
||||
}
|
||||
catch (NoSuchAlgorithmException e) {
|
||||
// Shouldn't ever happen
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class Sha256PairingHash implements PairingHashAlgorithm {
|
||||
public int getHashLength() {
|
||||
return 32;
|
||||
}
|
||||
|
||||
public byte[] hashData(byte[] data) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
return md.digest(data);
|
||||
}
|
||||
catch (NoSuchAlgorithmException e) {
|
||||
// Shouldn't ever happen
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
package com.limelight.nvstream.input;
|
||||
|
||||
public class ControllerPacket {
|
||||
public static final short A_FLAG = 0x1000;
|
||||
public static final short B_FLAG = 0x2000;
|
||||
public static final short X_FLAG = 0x4000;
|
||||
public static final short Y_FLAG = (short)0x8000;
|
||||
public static final short UP_FLAG = 0x0001;
|
||||
public static final short DOWN_FLAG = 0x0002;
|
||||
public static final short LEFT_FLAG = 0x0004;
|
||||
public static final short RIGHT_FLAG = 0x0008;
|
||||
public static final short LB_FLAG = 0x0100;
|
||||
public static final short RB_FLAG = 0x0200;
|
||||
public static final short PLAY_FLAG = 0x0010;
|
||||
public static final short BACK_FLAG = 0x0020;
|
||||
public static final short LS_CLK_FLAG = 0x0040;
|
||||
public static final short RS_CLK_FLAG = 0x0080;
|
||||
public static final short SPECIAL_BUTTON_FLAG = 0x0400;
|
||||
public static final short A_FLAG = 0x1000;
|
||||
public static final short B_FLAG = 0x2000;
|
||||
public static final short X_FLAG = 0x4000;
|
||||
public static final short Y_FLAG = (short)0x8000;
|
||||
public static final short UP_FLAG = 0x0001;
|
||||
public static final short DOWN_FLAG = 0x0002;
|
||||
public static final short LEFT_FLAG = 0x0004;
|
||||
public static final short RIGHT_FLAG = 0x0008;
|
||||
public static final short LB_FLAG = 0x0100;
|
||||
public static final short RB_FLAG = 0x0200;
|
||||
public static final short PLAY_FLAG = 0x0010;
|
||||
public static final short BACK_FLAG = 0x0020;
|
||||
public static final short LS_CLK_FLAG = 0x0040;
|
||||
public static final short RS_CLK_FLAG = 0x0080;
|
||||
public static final short SPECIAL_BUTTON_FLAG = 0x0400;
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
package com.limelight.nvstream.input;
|
||||
|
||||
public class KeyboardPacket {
|
||||
public static final byte KEY_DOWN = 0x03;
|
||||
public static final byte KEY_UP = 0x04;
|
||||
public static final byte KEY_DOWN = 0x03;
|
||||
public static final byte KEY_UP = 0x04;
|
||||
|
||||
public static final byte MODIFIER_SHIFT = 0x01;
|
||||
public static final byte MODIFIER_CTRL = 0x02;
|
||||
public static final byte MODIFIER_ALT = 0x04;
|
||||
public static final byte MODIFIER_SHIFT = 0x01;
|
||||
public static final byte MODIFIER_CTRL = 0x02;
|
||||
public static final byte MODIFIER_ALT = 0x04;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package com.limelight.nvstream.input;
|
||||
|
||||
public class MouseButtonPacket {
|
||||
public static final byte PRESS_EVENT = 0x07;
|
||||
public static final byte RELEASE_EVENT = 0x08;
|
||||
|
||||
public static final byte BUTTON_LEFT = 0x01;
|
||||
public static final byte BUTTON_MIDDLE = 0x02;
|
||||
public static final byte BUTTON_RIGHT = 0x03;
|
||||
public static final byte BUTTON_X1 = 0x04;
|
||||
public static final byte BUTTON_X2 = 0x05;
|
||||
public static final byte PRESS_EVENT = 0x07;
|
||||
public static final byte RELEASE_EVENT = 0x08;
|
||||
|
||||
public static final byte BUTTON_LEFT = 0x01;
|
||||
public static final byte BUTTON_MIDDLE = 0x02;
|
||||
public static final byte BUTTON_RIGHT = 0x03;
|
||||
public static final byte BUTTON_X1 = 0x04;
|
||||
public static final byte BUTTON_X2 = 0x05;
|
||||
}
|
||||
|
||||
@@ -4,62 +4,62 @@ import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
|
||||
public class MdnsComputer {
|
||||
private InetAddress localAddr;
|
||||
private Inet6Address v6Addr;
|
||||
private String name;
|
||||
private InetAddress localAddr;
|
||||
private Inet6Address v6Addr;
|
||||
private String name;
|
||||
|
||||
public MdnsComputer(String name, InetAddress localAddress, Inet6Address v6Addr) {
|
||||
this.name = name;
|
||||
this.localAddr = localAddress;
|
||||
this.v6Addr = v6Addr;
|
||||
}
|
||||
public MdnsComputer(String name, InetAddress localAddress, Inet6Address v6Addr) {
|
||||
this.name = name;
|
||||
this.localAddr = localAddress;
|
||||
this.v6Addr = v6Addr;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public InetAddress getLocalAddress() {
|
||||
return localAddr;
|
||||
}
|
||||
public InetAddress getLocalAddress() {
|
||||
return localAddr;
|
||||
}
|
||||
|
||||
public Inet6Address getIpv6Address() {
|
||||
return v6Addr;
|
||||
}
|
||||
public Inet6Address getIpv6Address() {
|
||||
return v6Addr;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return name.hashCode();
|
||||
}
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return name.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o instanceof MdnsComputer) {
|
||||
MdnsComputer other = (MdnsComputer)o;
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o instanceof MdnsComputer) {
|
||||
MdnsComputer other = (MdnsComputer)o;
|
||||
|
||||
if (!other.name.equals(name)) {
|
||||
return false;
|
||||
}
|
||||
if (!other.name.equals(name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((other.localAddr != null && localAddr == null) ||
|
||||
(other.localAddr == null && localAddr != null) ||
|
||||
(other.localAddr != null && !other.localAddr.equals(localAddr))) {
|
||||
return false;
|
||||
}
|
||||
if ((other.localAddr != null && localAddr == null) ||
|
||||
(other.localAddr == null && localAddr != null) ||
|
||||
(other.localAddr != null && !other.localAddr.equals(localAddr))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((other.v6Addr != null && v6Addr == null) ||
|
||||
(other.v6Addr == null && v6Addr != null) ||
|
||||
(other.v6Addr != null && !other.v6Addr.equals(v6Addr))) {
|
||||
return false;
|
||||
}
|
||||
if ((other.v6Addr != null && v6Addr == null) ||
|
||||
(other.v6Addr == null && v6Addr != null) ||
|
||||
(other.v6Addr != null && !other.v6Addr.equals(v6Addr))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "["+name+" - "+localAddr+" - "+v6Addr+"]";
|
||||
}
|
||||
@Override
|
||||
public String toString() {
|
||||
return "["+name+" - "+localAddr+" - "+v6Addr+"]";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,388 +21,388 @@ import javax.jmdns.impl.NetworkTopologyDiscoveryImpl;
|
||||
import com.limelight.LimeLog;
|
||||
|
||||
public class MdnsDiscoveryAgent implements ServiceListener {
|
||||
public static final String SERVICE_TYPE = "_nvstream._tcp.local.";
|
||||
|
||||
private MdnsDiscoveryListener listener;
|
||||
private Thread discoveryThread;
|
||||
private HashMap<InetAddress, MdnsComputer> computers = new HashMap<InetAddress, MdnsComputer>();
|
||||
private HashSet<String> pendingResolution = new HashSet<String>();
|
||||
|
||||
// The resolver factory's instance member has a static lifetime which
|
||||
// means our ref count and listener must be static also.
|
||||
private static int resolverRefCount = 0;
|
||||
private static HashSet<ServiceListener> listeners = new HashSet<ServiceListener>();
|
||||
private static ServiceListener nvstreamListener = new ServiceListener() {
|
||||
@Override
|
||||
public void serviceAdded(ServiceEvent event) {
|
||||
HashSet<ServiceListener> localListeners;
|
||||
|
||||
// Copy the listener set into a new set so we can invoke
|
||||
// the callbacks without holding the listeners monitor the
|
||||
// whole time.
|
||||
synchronized (listeners) {
|
||||
localListeners = new HashSet<ServiceListener>(listeners);
|
||||
}
|
||||
|
||||
for (ServiceListener listener : localListeners) {
|
||||
listener.serviceAdded(event);
|
||||
}
|
||||
}
|
||||
public static final String SERVICE_TYPE = "_nvstream._tcp.local.";
|
||||
|
||||
private MdnsDiscoveryListener listener;
|
||||
private Thread discoveryThread;
|
||||
private HashMap<InetAddress, MdnsComputer> computers = new HashMap<InetAddress, MdnsComputer>();
|
||||
private HashSet<String> pendingResolution = new HashSet<String>();
|
||||
|
||||
// The resolver factory's instance member has a static lifetime which
|
||||
// means our ref count and listener must be static also.
|
||||
private static int resolverRefCount = 0;
|
||||
private static HashSet<ServiceListener> listeners = new HashSet<ServiceListener>();
|
||||
private static ServiceListener nvstreamListener = new ServiceListener() {
|
||||
@Override
|
||||
public void serviceAdded(ServiceEvent event) {
|
||||
HashSet<ServiceListener> localListeners;
|
||||
|
||||
// Copy the listener set into a new set so we can invoke
|
||||
// the callbacks without holding the listeners monitor the
|
||||
// whole time.
|
||||
synchronized (listeners) {
|
||||
localListeners = new HashSet<ServiceListener>(listeners);
|
||||
}
|
||||
|
||||
for (ServiceListener listener : localListeners) {
|
||||
listener.serviceAdded(event);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serviceRemoved(ServiceEvent event) {
|
||||
HashSet<ServiceListener> localListeners;
|
||||
|
||||
// Copy the listener set into a new set so we can invoke
|
||||
// the callbacks without holding the listeners monitor the
|
||||
// whole time.
|
||||
synchronized (listeners) {
|
||||
localListeners = new HashSet<ServiceListener>(listeners);
|
||||
}
|
||||
|
||||
for (ServiceListener listener : localListeners) {
|
||||
listener.serviceRemoved(event);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void serviceRemoved(ServiceEvent event) {
|
||||
HashSet<ServiceListener> localListeners;
|
||||
|
||||
// Copy the listener set into a new set so we can invoke
|
||||
// the callbacks without holding the listeners monitor the
|
||||
// whole time.
|
||||
synchronized (listeners) {
|
||||
localListeners = new HashSet<ServiceListener>(listeners);
|
||||
}
|
||||
|
||||
for (ServiceListener listener : localListeners) {
|
||||
listener.serviceRemoved(event);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serviceResolved(ServiceEvent event) {
|
||||
HashSet<ServiceListener> localListeners;
|
||||
|
||||
// Copy the listener set into a new set so we can invoke
|
||||
// the callbacks without holding the listeners monitor the
|
||||
// whole time.
|
||||
synchronized (listeners) {
|
||||
localListeners = new HashSet<ServiceListener>(listeners);
|
||||
}
|
||||
|
||||
for (ServiceListener listener : localListeners) {
|
||||
listener.serviceResolved(event);
|
||||
}
|
||||
}
|
||||
};
|
||||
@Override
|
||||
public void serviceResolved(ServiceEvent event) {
|
||||
HashSet<ServiceListener> localListeners;
|
||||
|
||||
// Copy the listener set into a new set so we can invoke
|
||||
// the callbacks without holding the listeners monitor the
|
||||
// whole time.
|
||||
synchronized (listeners) {
|
||||
localListeners = new HashSet<ServiceListener>(listeners);
|
||||
}
|
||||
|
||||
for (ServiceListener listener : localListeners) {
|
||||
listener.serviceResolved(event);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public static class MyNetworkTopologyDiscovery extends NetworkTopologyDiscoveryImpl {
|
||||
@Override
|
||||
public boolean useInetAddress(NetworkInterface networkInterface, InetAddress interfaceAddress) {
|
||||
// This is an copy of jmDNS's implementation, except we omit the multicast check, since
|
||||
// it seems at least some devices lie about interfaces not supporting multicast when they really do.
|
||||
try {
|
||||
if (!networkInterface.isUp()) {
|
||||
return false;
|
||||
}
|
||||
public static class MyNetworkTopologyDiscovery extends NetworkTopologyDiscoveryImpl {
|
||||
@Override
|
||||
public boolean useInetAddress(NetworkInterface networkInterface, InetAddress interfaceAddress) {
|
||||
// This is an copy of jmDNS's implementation, except we omit the multicast check, since
|
||||
// it seems at least some devices lie about interfaces not supporting multicast when they really do.
|
||||
try {
|
||||
if (!networkInterface.isUp()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
if (!networkInterface.supportsMulticast()) {
|
||||
return false;
|
||||
}
|
||||
*/
|
||||
/*
|
||||
if (!networkInterface.supportsMulticast()) {
|
||||
return false;
|
||||
}
|
||||
*/
|
||||
|
||||
if (networkInterface.isLoopback()) {
|
||||
return false;
|
||||
}
|
||||
if (networkInterface.isLoopback()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (Exception exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
return true;
|
||||
} catch (Exception exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static {
|
||||
// Override jmDNS's default topology discovery class with ours
|
||||
NetworkTopologyDiscovery.Factory.setClassDelegate(new NetworkTopologyDiscovery.Factory.ClassDelegate() {
|
||||
@Override
|
||||
public NetworkTopologyDiscovery newNetworkTopologyDiscovery() {
|
||||
return new MyNetworkTopologyDiscovery();
|
||||
}
|
||||
});
|
||||
}
|
||||
static {
|
||||
// Override jmDNS's default topology discovery class with ours
|
||||
NetworkTopologyDiscovery.Factory.setClassDelegate(new NetworkTopologyDiscovery.Factory.ClassDelegate() {
|
||||
@Override
|
||||
public NetworkTopologyDiscovery newNetworkTopologyDiscovery() {
|
||||
return new MyNetworkTopologyDiscovery();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static JmmDNS referenceResolver() {
|
||||
synchronized (MdnsDiscoveryAgent.class) {
|
||||
JmmDNS instance = JmmDNS.Factory.getInstance();
|
||||
if (++resolverRefCount == 1) {
|
||||
// This will cause the listener to be invoked for known hosts immediately.
|
||||
// JmDNS only supports one listener per service, so we have to do this here
|
||||
// with a static listener.
|
||||
instance.addServiceListener(SERVICE_TYPE, nvstreamListener);
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
private static JmmDNS referenceResolver() {
|
||||
synchronized (MdnsDiscoveryAgent.class) {
|
||||
JmmDNS instance = JmmDNS.Factory.getInstance();
|
||||
if (++resolverRefCount == 1) {
|
||||
// This will cause the listener to be invoked for known hosts immediately.
|
||||
// JmDNS only supports one listener per service, so we have to do this here
|
||||
// with a static listener.
|
||||
instance.addServiceListener(SERVICE_TYPE, nvstreamListener);
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
private static void dereferenceResolver() {
|
||||
synchronized (MdnsDiscoveryAgent.class) {
|
||||
if (--resolverRefCount == 0) {
|
||||
try {
|
||||
JmmDNS.Factory.close();
|
||||
} catch (IOException e) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
private static void dereferenceResolver() {
|
||||
synchronized (MdnsDiscoveryAgent.class) {
|
||||
if (--resolverRefCount == 0) {
|
||||
try {
|
||||
JmmDNS.Factory.close();
|
||||
} catch (IOException e) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public MdnsDiscoveryAgent(MdnsDiscoveryListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
public MdnsDiscoveryAgent(MdnsDiscoveryListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
private void handleResolvedServiceInfo(ServiceInfo info) {
|
||||
synchronized (pendingResolution) {
|
||||
pendingResolution.remove(info.getName());
|
||||
}
|
||||
private void handleResolvedServiceInfo(ServiceInfo info) {
|
||||
synchronized (pendingResolution) {
|
||||
pendingResolution.remove(info.getName());
|
||||
}
|
||||
|
||||
try {
|
||||
handleServiceInfo(info);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
// Invalid DNS response
|
||||
LimeLog.info("mDNS: Invalid response for machine: "+info.getName());
|
||||
return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
handleServiceInfo(info);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
// Invalid DNS response
|
||||
LimeLog.info("mDNS: Invalid response for machine: "+info.getName());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private Inet6Address getLocalAddress(Inet6Address[] addresses) {
|
||||
for (Inet6Address addr : addresses) {
|
||||
if (addr.isLinkLocalAddress() || addr.isSiteLocalAddress()) {
|
||||
return addr;
|
||||
}
|
||||
// fc00::/7 - ULAs
|
||||
else if ((addr.getAddress()[0] & 0xfe) == 0xfc) {
|
||||
return addr;
|
||||
}
|
||||
}
|
||||
private Inet6Address getLocalAddress(Inet6Address[] addresses) {
|
||||
for (Inet6Address addr : addresses) {
|
||||
if (addr.isLinkLocalAddress() || addr.isSiteLocalAddress()) {
|
||||
return addr;
|
||||
}
|
||||
// fc00::/7 - ULAs
|
||||
else if ((addr.getAddress()[0] & 0xfe) == 0xfc) {
|
||||
return addr;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Inet6Address getLinkLocalAddress(Inet6Address[] addresses) {
|
||||
for (Inet6Address addr : addresses) {
|
||||
if (addr.isLinkLocalAddress()) {
|
||||
LimeLog.info("Found link-local address: "+addr.getHostAddress());
|
||||
return addr;
|
||||
}
|
||||
}
|
||||
private Inet6Address getLinkLocalAddress(Inet6Address[] addresses) {
|
||||
for (Inet6Address addr : addresses) {
|
||||
if (addr.isLinkLocalAddress()) {
|
||||
LimeLog.info("Found link-local address: "+addr.getHostAddress());
|
||||
return addr;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Inet6Address getBestIpv6Address(Inet6Address[] addresses) {
|
||||
// First try to find a link local address, so we can match the interface identifier
|
||||
// with a global address (this will work for SLAAC but not DHCPv6).
|
||||
Inet6Address linkLocalAddr = getLinkLocalAddress(addresses);
|
||||
private Inet6Address getBestIpv6Address(Inet6Address[] addresses) {
|
||||
// First try to find a link local address, so we can match the interface identifier
|
||||
// with a global address (this will work for SLAAC but not DHCPv6).
|
||||
Inet6Address linkLocalAddr = getLinkLocalAddress(addresses);
|
||||
|
||||
// We will try once to match a SLAAC interface suffix, then
|
||||
// pick the first matching address
|
||||
for (int tries = 0; tries < 2; tries++) {
|
||||
// We assume the addresses are already sorted in descending order
|
||||
// of preference from Bonjour.
|
||||
for (Inet6Address addr : addresses) {
|
||||
if (addr.isLinkLocalAddress() || addr.isSiteLocalAddress() || addr.isLoopbackAddress()) {
|
||||
// Link-local, site-local, and loopback aren't global
|
||||
LimeLog.info("Ignoring non-global address: "+addr.getHostAddress());
|
||||
continue;
|
||||
}
|
||||
// We will try once to match a SLAAC interface suffix, then
|
||||
// pick the first matching address
|
||||
for (int tries = 0; tries < 2; tries++) {
|
||||
// We assume the addresses are already sorted in descending order
|
||||
// of preference from Bonjour.
|
||||
for (Inet6Address addr : addresses) {
|
||||
if (addr.isLinkLocalAddress() || addr.isSiteLocalAddress() || addr.isLoopbackAddress()) {
|
||||
// Link-local, site-local, and loopback aren't global
|
||||
LimeLog.info("Ignoring non-global address: "+addr.getHostAddress());
|
||||
continue;
|
||||
}
|
||||
|
||||
byte[] addrBytes = addr.getAddress();
|
||||
byte[] addrBytes = addr.getAddress();
|
||||
|
||||
// 2002::/16
|
||||
if (addrBytes[0] == 0x20 && addrBytes[1] == 0x02) {
|
||||
// 6to4 has horrible performance
|
||||
LimeLog.info("Ignoring 6to4 address: "+addr.getHostAddress());
|
||||
continue;
|
||||
}
|
||||
// 2001::/32
|
||||
else if (addrBytes[0] == 0x20 && addrBytes[1] == 0x01 && addrBytes[2] == 0x00 && addrBytes[3] == 0x00) {
|
||||
// Teredo also has horrible performance
|
||||
LimeLog.info("Ignoring Teredo address: "+addr.getHostAddress());
|
||||
continue;
|
||||
}
|
||||
// fc00::/7
|
||||
else if ((addrBytes[0] & 0xfe) == 0xfc) {
|
||||
// ULAs aren't global
|
||||
LimeLog.info("Ignoring ULA: "+addr.getHostAddress());
|
||||
continue;
|
||||
}
|
||||
// 2002::/16
|
||||
if (addrBytes[0] == 0x20 && addrBytes[1] == 0x02) {
|
||||
// 6to4 has horrible performance
|
||||
LimeLog.info("Ignoring 6to4 address: "+addr.getHostAddress());
|
||||
continue;
|
||||
}
|
||||
// 2001::/32
|
||||
else if (addrBytes[0] == 0x20 && addrBytes[1] == 0x01 && addrBytes[2] == 0x00 && addrBytes[3] == 0x00) {
|
||||
// Teredo also has horrible performance
|
||||
LimeLog.info("Ignoring Teredo address: "+addr.getHostAddress());
|
||||
continue;
|
||||
}
|
||||
// fc00::/7
|
||||
else if ((addrBytes[0] & 0xfe) == 0xfc) {
|
||||
// ULAs aren't global
|
||||
LimeLog.info("Ignoring ULA: "+addr.getHostAddress());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Compare the final 64-bit interface identifier and skip the address
|
||||
// if it doesn't match our link-local address.
|
||||
if (linkLocalAddr != null && tries == 0) {
|
||||
boolean matched = true;
|
||||
// Compare the final 64-bit interface identifier and skip the address
|
||||
// if it doesn't match our link-local address.
|
||||
if (linkLocalAddr != null && tries == 0) {
|
||||
boolean matched = true;
|
||||
|
||||
for (int i = 8; i < 16; i++) {
|
||||
if (linkLocalAddr.getAddress()[i] != addr.getAddress()[i]) {
|
||||
matched = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (int i = 8; i < 16; i++) {
|
||||
if (linkLocalAddr.getAddress()[i] != addr.getAddress()[i]) {
|
||||
matched = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!matched) {
|
||||
LimeLog.info("Ignoring non-matching global address: "+addr.getHostAddress());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!matched) {
|
||||
LimeLog.info("Ignoring non-matching global address: "+addr.getHostAddress());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return addr;
|
||||
}
|
||||
}
|
||||
return addr;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void handleServiceInfo(ServiceInfo info) throws UnsupportedEncodingException {
|
||||
Inet4Address v4Addrs[] = info.getInet4Addresses();
|
||||
Inet6Address v6Addrs[] = info.getInet6Addresses();
|
||||
private void handleServiceInfo(ServiceInfo info) throws UnsupportedEncodingException {
|
||||
Inet4Address v4Addrs[] = info.getInet4Addresses();
|
||||
Inet6Address v6Addrs[] = info.getInet6Addresses();
|
||||
|
||||
LimeLog.info("mDNS: "+info.getName()+" has "+v4Addrs.length+" IPv4 addresses");
|
||||
LimeLog.info("mDNS: "+info.getName()+" has "+v6Addrs.length+" IPv6 addresses");
|
||||
LimeLog.info("mDNS: "+info.getName()+" has "+v4Addrs.length+" IPv4 addresses");
|
||||
LimeLog.info("mDNS: "+info.getName()+" has "+v6Addrs.length+" IPv6 addresses");
|
||||
|
||||
Inet6Address v6GlobalAddr = getBestIpv6Address(v6Addrs);
|
||||
Inet6Address v6GlobalAddr = getBestIpv6Address(v6Addrs);
|
||||
|
||||
// Add a computer object for each IPv4 address reported by the PC
|
||||
for (Inet4Address v4Addr : v4Addrs) {
|
||||
synchronized (computers) {
|
||||
MdnsComputer computer = new MdnsComputer(info.getName(), v4Addr, v6GlobalAddr);
|
||||
if (computers.put(computer.getLocalAddress(), computer) == null) {
|
||||
// This was a new entry
|
||||
listener.notifyComputerAdded(computer);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Add a computer object for each IPv4 address reported by the PC
|
||||
for (Inet4Address v4Addr : v4Addrs) {
|
||||
synchronized (computers) {
|
||||
MdnsComputer computer = new MdnsComputer(info.getName(), v4Addr, v6GlobalAddr);
|
||||
if (computers.put(computer.getLocalAddress(), computer) == null) {
|
||||
// This was a new entry
|
||||
listener.notifyComputerAdded(computer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there were no IPv4 addresses, use IPv6 for registration
|
||||
if (v4Addrs.length == 0) {
|
||||
Inet6Address v6LocalAddr = getLocalAddress(v6Addrs);
|
||||
// If there were no IPv4 addresses, use IPv6 for registration
|
||||
if (v4Addrs.length == 0) {
|
||||
Inet6Address v6LocalAddr = getLocalAddress(v6Addrs);
|
||||
|
||||
if (v6LocalAddr != null || v6GlobalAddr != null) {
|
||||
MdnsComputer computer = new MdnsComputer(info.getName(), v6LocalAddr, v6GlobalAddr);
|
||||
if (computers.put(v6LocalAddr != null ?
|
||||
computer.getLocalAddress() : computer.getIpv6Address(), computer) == null) {
|
||||
// This was a new entry
|
||||
listener.notifyComputerAdded(computer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void startDiscovery(final int discoveryIntervalMs) {
|
||||
// Kill any existing discovery before starting a new one
|
||||
stopDiscovery();
|
||||
|
||||
// Add our listener to the set
|
||||
synchronized (listeners) {
|
||||
listeners.add(MdnsDiscoveryAgent.this);
|
||||
}
|
||||
|
||||
discoveryThread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
// This may result in listener callbacks so we must register
|
||||
// our listener first.
|
||||
JmmDNS resolver = referenceResolver();
|
||||
|
||||
try {
|
||||
while (!Thread.interrupted()) {
|
||||
// Start an mDNS request
|
||||
resolver.requestServiceInfo(SERVICE_TYPE, null, discoveryIntervalMs);
|
||||
|
||||
// Run service resolution again for pending machines
|
||||
ArrayList<String> pendingNames;
|
||||
synchronized (pendingResolution) {
|
||||
pendingNames = new ArrayList<String>(pendingResolution);
|
||||
}
|
||||
for (String name : pendingNames) {
|
||||
LimeLog.info("mDNS: Retrying service resolution for machine: "+name);
|
||||
ServiceInfo[] infos = resolver.getServiceInfos(SERVICE_TYPE, name, 500);
|
||||
if (infos != null && infos.length != 0) {
|
||||
LimeLog.info("mDNS: Resolved (retry) with "+infos.length+" service entries");
|
||||
for (ServiceInfo svcinfo : infos) {
|
||||
handleResolvedServiceInfo(svcinfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for the next polling interval
|
||||
try {
|
||||
Thread.sleep(discoveryIntervalMs);
|
||||
} catch (InterruptedException e) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
// Dereference the resolver
|
||||
dereferenceResolver();
|
||||
}
|
||||
}
|
||||
};
|
||||
discoveryThread.setName("mDNS Discovery Thread");
|
||||
discoveryThread.start();
|
||||
}
|
||||
|
||||
public void stopDiscovery() {
|
||||
// Remove our listener from the set
|
||||
synchronized (listeners) {
|
||||
listeners.remove(MdnsDiscoveryAgent.this);
|
||||
}
|
||||
|
||||
// If there's already a running thread, interrupt it
|
||||
if (discoveryThread != null) {
|
||||
discoveryThread.interrupt();
|
||||
discoveryThread = null;
|
||||
}
|
||||
}
|
||||
|
||||
public List<MdnsComputer> getComputerSet() {
|
||||
synchronized (computers) {
|
||||
return new ArrayList<MdnsComputer>(computers.values());
|
||||
}
|
||||
}
|
||||
if (v6LocalAddr != null || v6GlobalAddr != null) {
|
||||
MdnsComputer computer = new MdnsComputer(info.getName(), v6LocalAddr, v6GlobalAddr);
|
||||
if (computers.put(v6LocalAddr != null ?
|
||||
computer.getLocalAddress() : computer.getIpv6Address(), computer) == null) {
|
||||
// This was a new entry
|
||||
listener.notifyComputerAdded(computer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void startDiscovery(final int discoveryIntervalMs) {
|
||||
// Kill any existing discovery before starting a new one
|
||||
stopDiscovery();
|
||||
|
||||
// Add our listener to the set
|
||||
synchronized (listeners) {
|
||||
listeners.add(MdnsDiscoveryAgent.this);
|
||||
}
|
||||
|
||||
discoveryThread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
// This may result in listener callbacks so we must register
|
||||
// our listener first.
|
||||
JmmDNS resolver = referenceResolver();
|
||||
|
||||
try {
|
||||
while (!Thread.interrupted()) {
|
||||
// Start an mDNS request
|
||||
resolver.requestServiceInfo(SERVICE_TYPE, null, discoveryIntervalMs);
|
||||
|
||||
// Run service resolution again for pending machines
|
||||
ArrayList<String> pendingNames;
|
||||
synchronized (pendingResolution) {
|
||||
pendingNames = new ArrayList<String>(pendingResolution);
|
||||
}
|
||||
for (String name : pendingNames) {
|
||||
LimeLog.info("mDNS: Retrying service resolution for machine: "+name);
|
||||
ServiceInfo[] infos = resolver.getServiceInfos(SERVICE_TYPE, name, 500);
|
||||
if (infos != null && infos.length != 0) {
|
||||
LimeLog.info("mDNS: Resolved (retry) with "+infos.length+" service entries");
|
||||
for (ServiceInfo svcinfo : infos) {
|
||||
handleResolvedServiceInfo(svcinfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for the next polling interval
|
||||
try {
|
||||
Thread.sleep(discoveryIntervalMs);
|
||||
} catch (InterruptedException e) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
// Dereference the resolver
|
||||
dereferenceResolver();
|
||||
}
|
||||
}
|
||||
};
|
||||
discoveryThread.setName("mDNS Discovery Thread");
|
||||
discoveryThread.start();
|
||||
}
|
||||
|
||||
public void stopDiscovery() {
|
||||
// Remove our listener from the set
|
||||
synchronized (listeners) {
|
||||
listeners.remove(MdnsDiscoveryAgent.this);
|
||||
}
|
||||
|
||||
// If there's already a running thread, interrupt it
|
||||
if (discoveryThread != null) {
|
||||
discoveryThread.interrupt();
|
||||
discoveryThread = null;
|
||||
}
|
||||
}
|
||||
|
||||
public List<MdnsComputer> getComputerSet() {
|
||||
synchronized (computers) {
|
||||
return new ArrayList<MdnsComputer>(computers.values());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serviceAdded(ServiceEvent event) {
|
||||
LimeLog.info("mDNS: Machine appeared: "+event.getInfo().getName());
|
||||
@Override
|
||||
public void serviceAdded(ServiceEvent event) {
|
||||
LimeLog.info("mDNS: Machine appeared: "+event.getInfo().getName());
|
||||
|
||||
ServiceInfo info = event.getDNS().getServiceInfo(SERVICE_TYPE, event.getInfo().getName(), 500);
|
||||
if (info == null) {
|
||||
// This machine is pending resolution
|
||||
synchronized (pendingResolution) {
|
||||
pendingResolution.add(event.getInfo().getName());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
LimeLog.info("mDNS: Resolved (blocking)");
|
||||
handleResolvedServiceInfo(info);
|
||||
}
|
||||
ServiceInfo info = event.getDNS().getServiceInfo(SERVICE_TYPE, event.getInfo().getName(), 500);
|
||||
if (info == null) {
|
||||
// This machine is pending resolution
|
||||
synchronized (pendingResolution) {
|
||||
pendingResolution.add(event.getInfo().getName());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
LimeLog.info("mDNS: Resolved (blocking)");
|
||||
handleResolvedServiceInfo(info);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serviceRemoved(ServiceEvent event) {
|
||||
LimeLog.info("mDNS: Machine disappeared: "+event.getInfo().getName());
|
||||
@Override
|
||||
public void serviceRemoved(ServiceEvent event) {
|
||||
LimeLog.info("mDNS: Machine disappeared: "+event.getInfo().getName());
|
||||
|
||||
Inet4Address v4Addrs[] = event.getInfo().getInet4Addresses();
|
||||
for (Inet4Address addr : v4Addrs) {
|
||||
synchronized (computers) {
|
||||
MdnsComputer computer = computers.remove(addr);
|
||||
if (computer != null) {
|
||||
listener.notifyComputerRemoved(computer);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Inet4Address v4Addrs[] = event.getInfo().getInet4Addresses();
|
||||
for (Inet4Address addr : v4Addrs) {
|
||||
synchronized (computers) {
|
||||
MdnsComputer computer = computers.remove(addr);
|
||||
if (computer != null) {
|
||||
listener.notifyComputerRemoved(computer);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Inet6Address v6Addrs[] = event.getInfo().getInet6Addresses();
|
||||
for (Inet6Address addr : v6Addrs) {
|
||||
synchronized (computers) {
|
||||
MdnsComputer computer = computers.remove(addr);
|
||||
if (computer != null) {
|
||||
listener.notifyComputerRemoved(computer);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Inet6Address v6Addrs[] = event.getInfo().getInet6Addresses();
|
||||
for (Inet6Address addr : v6Addrs) {
|
||||
synchronized (computers) {
|
||||
MdnsComputer computer = computers.remove(addr);
|
||||
if (computer != null) {
|
||||
listener.notifyComputerRemoved(computer);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serviceResolved(ServiceEvent event) {
|
||||
// We handle this synchronously
|
||||
}
|
||||
@Override
|
||||
public void serviceResolved(ServiceEvent event) {
|
||||
// We handle this synchronously
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.limelight.nvstream.mdns;
|
||||
|
||||
public interface MdnsDiscoveryListener {
|
||||
void notifyComputerAdded(MdnsComputer computer);
|
||||
void notifyComputerRemoved(MdnsComputer computer);
|
||||
void notifyDiscoveryFailure(Exception e);
|
||||
void notifyComputerAdded(MdnsComputer computer);
|
||||
void notifyComputerRemoved(MdnsComputer computer);
|
||||
void notifyDiscoveryFailure(Exception e);
|
||||
}
|
||||
|
||||
@@ -10,90 +10,90 @@ import com.limelight.LimeLog;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
|
||||
public class WakeOnLanSender {
|
||||
private static final int[] PORTS_TO_TRY = new int[] {
|
||||
7, 9, // Standard WOL ports
|
||||
47998, 47999, 48000, 48002, 48010 // Ports opened by GFE
|
||||
};
|
||||
|
||||
public static void sendWolPacket(ComputerDetails computer) throws IOException {
|
||||
DatagramSocket sock = new DatagramSocket(0);
|
||||
byte[] payload = createWolPayload(computer);
|
||||
IOException lastException = null;
|
||||
boolean sentWolPacket = false;
|
||||
private static final int[] PORTS_TO_TRY = new int[] {
|
||||
7, 9, // Standard WOL ports
|
||||
47998, 47999, 48000, 48002, 48010 // Ports opened by GFE
|
||||
};
|
||||
|
||||
public static void sendWolPacket(ComputerDetails computer) throws IOException {
|
||||
DatagramSocket sock = new DatagramSocket(0);
|
||||
byte[] payload = createWolPayload(computer);
|
||||
IOException lastException = null;
|
||||
boolean sentWolPacket = false;
|
||||
|
||||
try {
|
||||
// Try all resolved remote and local addresses and IPv4 broadcast address.
|
||||
// The broadcast address is required to avoid stale ARP cache entries
|
||||
// making the sleeping machine unreachable.
|
||||
for (String unresolvedAddress : new String[] {
|
||||
computer.localAddress, computer.remoteAddress, computer.manualAddress, computer.ipv6Address, "255.255.255.255"
|
||||
}) {
|
||||
try {
|
||||
// Try all resolved remote and local addresses and IPv4 broadcast address.
|
||||
// The broadcast address is required to avoid stale ARP cache entries
|
||||
// making the sleeping machine unreachable.
|
||||
for (String unresolvedAddress : new String[] {
|
||||
computer.localAddress, computer.remoteAddress, computer.manualAddress, computer.ipv6Address, "255.255.255.255"
|
||||
}) {
|
||||
if (unresolvedAddress == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
for (InetAddress resolvedAddress : InetAddress.getAllByName(unresolvedAddress)) {
|
||||
// Try all the ports for each resolved address
|
||||
for (int port : PORTS_TO_TRY) {
|
||||
DatagramPacket dp = new DatagramPacket(payload, payload.length);
|
||||
dp.setAddress(resolvedAddress);
|
||||
dp.setPort(port);
|
||||
sock.send(dp);
|
||||
sentWolPacket = true;
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// We may have addresses that don't resolve on this subnet,
|
||||
// but don't throw and exit the whole function if that happens.
|
||||
// We'll throw it at the end if we didn't send a single packet.
|
||||
e.printStackTrace();
|
||||
lastException = e;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
sock.close();
|
||||
}
|
||||
try {
|
||||
for (InetAddress resolvedAddress : InetAddress.getAllByName(unresolvedAddress)) {
|
||||
// Try all the ports for each resolved address
|
||||
for (int port : PORTS_TO_TRY) {
|
||||
DatagramPacket dp = new DatagramPacket(payload, payload.length);
|
||||
dp.setAddress(resolvedAddress);
|
||||
dp.setPort(port);
|
||||
sock.send(dp);
|
||||
sentWolPacket = true;
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// We may have addresses that don't resolve on this subnet,
|
||||
// but don't throw and exit the whole function if that happens.
|
||||
// We'll throw it at the end if we didn't send a single packet.
|
||||
e.printStackTrace();
|
||||
lastException = e;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
sock.close();
|
||||
}
|
||||
|
||||
// Propagate the DNS resolution exception if we didn't
|
||||
// manage to get a single packet out to the host.
|
||||
if (!sentWolPacket && lastException != null) {
|
||||
throw lastException;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] macStringToBytes(String macAddress) {
|
||||
byte[] macBytes = new byte[6];
|
||||
@SuppressWarnings("resource")
|
||||
Scanner scan = new Scanner(macAddress).useDelimiter(":");
|
||||
for (int i = 0; i < macBytes.length && scan.hasNext(); i++) {
|
||||
try {
|
||||
macBytes[i] = (byte) Integer.parseInt(scan.next(), 16);
|
||||
} catch (NumberFormatException e) {
|
||||
LimeLog.warning("Malformed MAC address: "+macAddress+" (index: "+i+")");
|
||||
break;
|
||||
}
|
||||
}
|
||||
scan.close();
|
||||
return macBytes;
|
||||
}
|
||||
|
||||
private static byte[] createWolPayload(ComputerDetails computer) {
|
||||
byte[] payload = new byte[102];
|
||||
byte[] macAddress = macStringToBytes(computer.macAddress);
|
||||
int i;
|
||||
|
||||
// 6 bytes of FF
|
||||
for (i = 0; i < 6; i++) {
|
||||
payload[i] = (byte)0xFF;
|
||||
}
|
||||
|
||||
// 16 repetitions of the MAC address
|
||||
for (int j = 0; j < 16; j++) {
|
||||
System.arraycopy(macAddress, 0, payload, i, macAddress.length);
|
||||
i += macAddress.length;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
// Propagate the DNS resolution exception if we didn't
|
||||
// manage to get a single packet out to the host.
|
||||
if (!sentWolPacket && lastException != null) {
|
||||
throw lastException;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] macStringToBytes(String macAddress) {
|
||||
byte[] macBytes = new byte[6];
|
||||
@SuppressWarnings("resource")
|
||||
Scanner scan = new Scanner(macAddress).useDelimiter(":");
|
||||
for (int i = 0; i < macBytes.length && scan.hasNext(); i++) {
|
||||
try {
|
||||
macBytes[i] = (byte) Integer.parseInt(scan.next(), 16);
|
||||
} catch (NumberFormatException e) {
|
||||
LimeLog.warning("Malformed MAC address: "+macAddress+" (index: "+i+")");
|
||||
break;
|
||||
}
|
||||
}
|
||||
scan.close();
|
||||
return macBytes;
|
||||
}
|
||||
|
||||
private static byte[] createWolPayload(ComputerDetails computer) {
|
||||
byte[] payload = new byte[102];
|
||||
byte[] macAddress = macStringToBytes(computer.macAddress);
|
||||
int i;
|
||||
|
||||
// 6 bytes of FF
|
||||
for (i = 0; i < 6; i++) {
|
||||
payload[i] = (byte)0xFF;
|
||||
}
|
||||
|
||||
// 16 repetitions of the MAC address
|
||||
for (int j = 0; j < 16; j++) {
|
||||
System.arraycopy(macAddress, 0, payload, i, macAddress.length);
|
||||
i += macAddress.length;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +1,47 @@
|
||||
package com.limelight.utils;
|
||||
|
||||
public class Vector2d {
|
||||
private float x;
|
||||
private float y;
|
||||
private double magnitude;
|
||||
|
||||
public static final Vector2d ZERO = new Vector2d();
|
||||
|
||||
public Vector2d() {
|
||||
initialize(0, 0);
|
||||
}
|
||||
|
||||
public void initialize(float x, float y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.magnitude = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
|
||||
}
|
||||
|
||||
public double getMagnitude() {
|
||||
return magnitude;
|
||||
}
|
||||
|
||||
public void getNormalized(Vector2d vector) {
|
||||
vector.initialize((float)(x / magnitude), (float)(y / magnitude));
|
||||
}
|
||||
|
||||
public void scalarMultiply(double factor) {
|
||||
initialize((float)(x * factor), (float)(y * factor));
|
||||
}
|
||||
|
||||
public void setX(float x) {
|
||||
initialize(x, this.y);
|
||||
}
|
||||
|
||||
public void setY(float y) {
|
||||
initialize(this.x, y);
|
||||
}
|
||||
|
||||
public float getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public float getY() {
|
||||
return y;
|
||||
}
|
||||
private float x;
|
||||
private float y;
|
||||
private double magnitude;
|
||||
|
||||
public static final Vector2d ZERO = new Vector2d();
|
||||
|
||||
public Vector2d() {
|
||||
initialize(0, 0);
|
||||
}
|
||||
|
||||
public void initialize(float x, float y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.magnitude = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
|
||||
}
|
||||
|
||||
public double getMagnitude() {
|
||||
return magnitude;
|
||||
}
|
||||
|
||||
public void getNormalized(Vector2d vector) {
|
||||
vector.initialize((float)(x / magnitude), (float)(y / magnitude));
|
||||
}
|
||||
|
||||
public void scalarMultiply(double factor) {
|
||||
initialize((float)(x * factor), (float)(y * factor));
|
||||
}
|
||||
|
||||
public void setX(float x) {
|
||||
initialize(x, this.y);
|
||||
}
|
||||
|
||||
public void setY(float y) {
|
||||
initialize(this.x, y);
|
||||
}
|
||||
|
||||
public float getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public float getY() {
|
||||
return y;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
package com.limelight;
|
||||
|
||||
public class LimelightBuildProps {
|
||||
public static final boolean ROOT_BUILD = false;
|
||||
public static final boolean ROOT_BUILD = false;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
package com.limelight;
|
||||
|
||||
public class LimelightBuildProps {
|
||||
public static final boolean ROOT_BUILD = true;
|
||||
public static final boolean ROOT_BUILD = true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user