From 5da017735686cea98828d518ccf5bb9785b90063 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 19 Oct 2019 23:59:33 -0700 Subject: [PATCH] Convert tabs to spaces --- app/src/main/java/com/limelight/LimeLog.java | 32 +- .../binding/input/KeyboardTranslator.java | 528 ++++---- .../VirtualControllerConfigurationLoader.java | 4 +- .../VirtualControllerElement.java | 54 +- .../binding/video/MediaCodecHelper.java | 1078 +++++++-------- .../limelight/nvstream/ConnectionContext.java | 28 +- .../com/limelight/nvstream/NvConnection.java | 598 ++++----- .../nvstream/NvConnectionListener.java | 22 +- .../nvstream/StreamConfiguration.java | 420 +++--- .../nvstream/av/ByteBufferDescriptor.java | 106 +- .../nvstream/av/audio/AudioRenderer.java | 14 +- .../av/video/VideoDecoderRenderer.java | 20 +- .../nvstream/http/ComputerDetails.java | 144 +- .../http/GfeHttpResponseException.java | 44 +- .../http/LimelightCryptoProvider.java | 8 +- .../com/limelight/nvstream/http/NvApp.java | 106 +- .../com/limelight/nvstream/http/NvHTTP.java | 1166 ++++++++--------- .../nvstream/http/PairingManager.java | 616 ++++----- .../nvstream/input/ControllerPacket.java | 30 +- .../nvstream/input/KeyboardPacket.java | 10 +- .../nvstream/input/MouseButtonPacket.java | 16 +- .../limelight/nvstream/mdns/MdnsComputer.java | 92 +- .../nvstream/mdns/MdnsDiscoveryAgent.java | 694 +++++----- .../nvstream/mdns/MdnsDiscoveryListener.java | 6 +- .../nvstream/wol/WakeOnLanSender.java | 160 +-- .../java/com/limelight/utils/Vector2d.java | 86 +- .../com.limelight/LimelightBuildProps.java | 2 +- .../com.limelight/LimelightBuildProps.java | 2 +- 28 files changed, 3043 insertions(+), 3043 deletions(-) diff --git a/app/src/main/java/com/limelight/LimeLog.java b/app/src/main/java/com/limelight/LimeLog.java index 9d56afab..ba0ba298 100644 --- a/app/src/main/java/com/limelight/LimeLog.java +++ b/app/src/main/java/com/limelight/LimeLog.java @@ -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)); + } } diff --git a/app/src/main/java/com/limelight/binding/input/KeyboardTranslator.java b/app/src/main/java/com/limelight/binding/input/KeyboardTranslator.java index 00bea126..0aa7fb96 100644 --- a/app/src/main/java/com/limelight/binding/input/KeyboardTranslator.java +++ b/app/src/main/java/com/limelight/binding/input/KeyboardTranslator.java @@ -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); + } } diff --git a/app/src/main/java/com/limelight/binding/input/virtual_controller/VirtualControllerConfigurationLoader.java b/app/src/main/java/com/limelight/binding/input/virtual_controller/VirtualControllerConfigurationLoader.java index 4e024893..5d945b09 100644 --- a/app/src/main/java/com/limelight/binding/input/virtual_controller/VirtualControllerConfigurationLoader.java +++ b/app/src/main/java/com/limelight/binding/input/virtual_controller/VirtualControllerConfigurationLoader.java @@ -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 { } } } - } + } } \ No newline at end of file diff --git a/app/src/main/java/com/limelight/binding/input/virtual_controller/VirtualControllerElement.java b/app/src/main/java/com/limelight/binding/input/virtual_controller/VirtualControllerElement.java index f99cc69a..342ea342 100644 --- a/app/src/main/java/com/limelight/binding/input/virtual_controller/VirtualControllerElement.java +++ b/app/src/main/java/com/limelight/binding/input/virtual_controller/VirtualControllerElement.java @@ -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; } diff --git a/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java b/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java index 2981ec0d..131c7650 100644 --- a/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java +++ b/app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java @@ -23,18 +23,18 @@ import android.os.Build; import com.limelight.LimeLog; public class MediaCodecHelper { - - private static final List preferredDecoders; + + private static final List preferredDecoders; - private static final List blacklistedDecoderPrefixes; - private static final List spsFixupBitstreamFixupDecoderPrefixes; - private static final List blacklistedAdaptivePlaybackPrefixes; - private static final List deprioritizedHevcDecoders; + private static final List blacklistedDecoderPrefixes; + private static final List spsFixupBitstreamFixupDecoderPrefixes; + private static final List blacklistedAdaptivePlaybackPrefixes; + private static final List deprioritizedHevcDecoders; private static final List baselineProfileHackPrefixes; private static final List directSubmitPrefixes; - private static final List constrainedHighProfilePrefixes; - private static final List whitelistedHevcDecoders; - private static final List refFrameInvalidationAvcPrefixes; + private static final List constrainedHighProfilePrefixes; + private static final List whitelistedHevcDecoders; + private static final List refFrameInvalidationAvcPrefixes; private static final List refFrameInvalidationHevcPrefixes; private static final List blacklisted49FpsDecoderPrefixes; @@ -53,592 +53,592 @@ public class MediaCodecHelper { directSubmitPrefixes.add("omx.brcm"); directSubmitPrefixes.add("omx.TI"); directSubmitPrefixes.add("omx.arc"); - directSubmitPrefixes.add("omx.nvidia"); + directSubmitPrefixes.add("omx.nvidia"); } static { refFrameInvalidationAvcPrefixes = new LinkedList<>(); refFrameInvalidationHevcPrefixes = new LinkedList<>(); - // Qualcomm and NVIDIA may be added at runtime - } + // Qualcomm and NVIDIA may be added at runtime + } - static { - preferredDecoders = new LinkedList<>(); - } - - static { - blacklistedDecoderPrefixes = new LinkedList<>(); + static { + preferredDecoders = new LinkedList<>(); + } + + static { + blacklistedDecoderPrefixes = new LinkedList<>(); - // Blacklist software decoders that don't support H264 high profile, - // but exclude the official AOSP and CrOS emulator from this restriction. - if (!Build.HARDWARE.equals("ranchu") && !Build.HARDWARE.equals("cheets")) { - blacklistedDecoderPrefixes.add("omx.google"); - blacklistedDecoderPrefixes.add("AVCDecoder"); - } + // Blacklist software decoders that don't support H264 high profile, + // but exclude the official AOSP and CrOS emulator from this restriction. + if (!Build.HARDWARE.equals("ranchu") && !Build.HARDWARE.equals("cheets")) { + blacklistedDecoderPrefixes.add("omx.google"); + blacklistedDecoderPrefixes.add("AVCDecoder"); + } - // We want to avoid ffmpeg decoders since they're software decoders, - // but on Android-x86 they might be all we have (and also relatively - // performant on a modern x86 processor). - if (!Build.BRAND.equals("Android-x86")) { - blacklistedDecoderPrefixes.add("OMX.ffmpeg"); - } + // We want to avoid ffmpeg decoders since they're software decoders, + // but on Android-x86 they might be all we have (and also relatively + // performant on a modern x86 processor). + if (!Build.BRAND.equals("Android-x86")) { + blacklistedDecoderPrefixes.add("OMX.ffmpeg"); + } - // Force these decoders disabled because: - // 1) They are software decoders, so the performance is terrible - // 2) They crash with our HEVC stream anyway (at least prior to CSD batching) - blacklistedDecoderPrefixes.add("OMX.qcom.video.decoder.hevcswvdec"); - blacklistedDecoderPrefixes.add("OMX.SEC.hevc.sw.dec"); - } - - static { - // If a decoder qualifies for reference frame invalidation, - // these entries will be ignored for those decoders. - spsFixupBitstreamFixupDecoderPrefixes = new LinkedList<>(); - spsFixupBitstreamFixupDecoderPrefixes.add("omx.nvidia"); - spsFixupBitstreamFixupDecoderPrefixes.add("omx.qcom"); + // Force these decoders disabled because: + // 1) They are software decoders, so the performance is terrible + // 2) They crash with our HEVC stream anyway (at least prior to CSD batching) + blacklistedDecoderPrefixes.add("OMX.qcom.video.decoder.hevcswvdec"); + blacklistedDecoderPrefixes.add("OMX.SEC.hevc.sw.dec"); + } + + static { + // If a decoder qualifies for reference frame invalidation, + // these entries will be ignored for those decoders. + spsFixupBitstreamFixupDecoderPrefixes = new LinkedList<>(); + spsFixupBitstreamFixupDecoderPrefixes.add("omx.nvidia"); + spsFixupBitstreamFixupDecoderPrefixes.add("omx.qcom"); spsFixupBitstreamFixupDecoderPrefixes.add("omx.brcm"); baselineProfileHackPrefixes = new LinkedList<>(); baselineProfileHackPrefixes.add("omx.intel"); - blacklistedAdaptivePlaybackPrefixes = new LinkedList<>(); - // The Intel decoder on Lollipop on Nexus Player would increase latency badly - // if adaptive playback was enabled so let's avoid it to be safe. - blacklistedAdaptivePlaybackPrefixes.add("omx.intel"); - // The MediaTek decoder crashes at 1080p when adaptive playback is enabled - // on some Android TV devices with H.265 only. - blacklistedAdaptivePlaybackPrefixes.add("omx.mtk"); + blacklistedAdaptivePlaybackPrefixes = new LinkedList<>(); + // The Intel decoder on Lollipop on Nexus Player would increase latency badly + // if adaptive playback was enabled so let's avoid it to be safe. + blacklistedAdaptivePlaybackPrefixes.add("omx.intel"); + // The MediaTek decoder crashes at 1080p when adaptive playback is enabled + // on some Android TV devices with H.265 only. + blacklistedAdaptivePlaybackPrefixes.add("omx.mtk"); - constrainedHighProfilePrefixes = new LinkedList<>(); - constrainedHighProfilePrefixes.add("omx.intel"); - } - - static { - whitelistedHevcDecoders = new LinkedList<>(); - - // Allow software HEVC decoding in the official AOSP emulator - if (Build.HARDWARE.equals("ranchu") && Build.BRAND.equals("google")) { - whitelistedHevcDecoders.add("omx.google"); - } - - // Exynos seems to be the only HEVC decoder that works reliably - whitelistedHevcDecoders.add("omx.exynos"); - - // On Darcy (Shield 2017), HEVC runs fine with no fixups required. - // For some reason, other X1 implementations require bitstream fixups. - if (Build.DEVICE.equalsIgnoreCase("darcy")) { - whitelistedHevcDecoders.add("omx.nvidia"); - } - else { - // TODO: This needs a similar fixup to the Tegra 3 otherwise it buffers 16 frames - } - - // Sony ATVs have broken MediaTek codecs (decoder hangs after rendering the first frame). - // I know the Fire TV 2 and 3 works, so I'll just whitelist Amazon devices which seem - // to actually be tested. - if (Build.MANUFACTURER.equalsIgnoreCase("Amazon")) { - whitelistedHevcDecoders.add("omx.mtk"); - whitelistedHevcDecoders.add("omx.amlogic"); - } - - // These theoretically have good HEVC decoding capabilities (potentially better than - // their AVC decoders), but haven't been tested enough - //whitelistedHevcDecoders.add("omx.amlogic"); - //whitelistedHevcDecoders.add("omx.rk"); - - // Based on GPU attributes queried at runtime, the omx.qcom prefix will be added - // during initialization to avoid SoCs with broken HEVC decoders. - } - - static { - deprioritizedHevcDecoders = new LinkedList<>(); - - // These are decoders that work but aren't used by default for various reasons. - - // Qualcomm is currently the only decoders in this group. - } - - static { - blacklisted49FpsDecoderPrefixes = new LinkedList<>(); - - // We see a bunch of crashes on MediaTek Android TVs running - // at 49 FPS (PAL 50 Hz - 1). Blacklist this frame rate for - // these devices and hope they fix it in Pie. - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { - blacklisted49FpsDecoderPrefixes.add("omx.mtk"); - } - } - - private static boolean isPowerVR(String glRenderer) { - return glRenderer.toLowerCase().contains("powervr"); - } - - private static String getAdrenoVersionString(String glRenderer) { - glRenderer = glRenderer.toLowerCase().trim(); - - if (!glRenderer.contains("adreno")) { - return null; - } - - Pattern modelNumberPattern = Pattern.compile("(.*)([0-9]{3})(.*)"); - - Matcher matcher = modelNumberPattern.matcher(glRenderer); - if (!matcher.matches()) { - return null; - } - - String modelNumber = matcher.group(2); - LimeLog.info("Found Adreno GPU: "+modelNumber); - return modelNumber; - } - - private static boolean isLowEndSnapdragonRenderer(String glRenderer) { - String modelNumber = getAdrenoVersionString(glRenderer); - if (modelNumber == null) { - // Not an Adreno GPU - return false; - } - - // The current logic is to identify low-end SoCs based on a zero in the x0x place. - return modelNumber.charAt(1) == '0'; - } - - // This is a workaround for some broken devices that report - // only GLES 3.0 even though the GPU is an Adreno 4xx series part. - // An example of such a device is the Huawei Honor 5x with the - // Snapdragon 616 SoC (Adreno 405). - private static boolean isGLES31SnapdragonRenderer(String glRenderer) { - String modelNumber = getAdrenoVersionString(glRenderer); - if (modelNumber == null) { - // Not an Adreno GPU - return false; - } - - // Snapdragon 4xx and higher support GLES 3.1 - return modelNumber.charAt(0) >= '4'; - } - - public static void initialize(Context context, String glRenderer) { - if (initialized) { - return; - } - - ActivityManager activityManager = - (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); - ConfigurationInfo configInfo = activityManager.getDeviceConfigurationInfo(); - if (configInfo.reqGlEsVersion != ConfigurationInfo.GL_ES_VERSION_UNDEFINED) { - LimeLog.info("OpenGL ES version: "+configInfo.reqGlEsVersion); - - isLowEndSnapdragon = isLowEndSnapdragonRenderer(glRenderer); - - // Tegra K1 and later can do reference frame invalidation properly - if (configInfo.reqGlEsVersion >= 0x30000) { - LimeLog.info("Added omx.nvidia to AVC reference frame invalidation support list"); - refFrameInvalidationAvcPrefixes.add("omx.nvidia"); - - LimeLog.info("Added omx.qcom to AVC reference frame invalidation support list"); - refFrameInvalidationAvcPrefixes.add("omx.qcom"); - - // Prior to M, we were tricking the decoder into using baseline profile, which - // won't support RFI properly. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - LimeLog.info("Added omx.intel to AVC reference frame invalidation support list"); - refFrameInvalidationAvcPrefixes.add("omx.intel"); - } - } - - // Qualcomm's early HEVC decoders break hard on our HEVC stream. The best check to - // tell the good from the bad decoders are the generation of Adreno GPU included: - // 3xx - bad - // 4xx - good - // - // The "good" GPUs support GLES 3.1, but we can't just check that directly - // (see comment on isGLES31SnapdragonRenderer). - // - if (isGLES31SnapdragonRenderer(glRenderer)) { - // We prefer reference frame invalidation support (which is only doable on AVC on - // older Qualcomm chips) vs. enabling HEVC by default. The user can override using the settings - // to force HEVC on. If HDR or mobile data will be used, we'll override this and use - // HEVC anyway. - LimeLog.info("Added omx.qcom to deprioritized HEVC decoders based on GLES 3.1+ support"); - deprioritizedHevcDecoders.add("omx.qcom"); - } - else { - blacklistedDecoderPrefixes.add("OMX.qcom.video.decoder.hevc"); - } - - // Older MediaTek SoCs have issues with HEVC rendering but the newer chips with - // PowerVR GPUs have good HEVC support. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isPowerVR(glRenderer)) { - LimeLog.info("Added omx.mtk to HEVC decoders based on PowerVR GPU"); - whitelistedHevcDecoders.add("omx.mtk"); - - // This SoC (MT8176 in GPD XD+) supports AVC RFI too, but the maxNumReferenceFrames setting - // required to make it work adds a huge amount of latency. - LimeLog.info("Added omx.mtk to RFI list for HEVC"); - refFrameInvalidationHevcPrefixes.add("omx.mtk"); - } - } - - initialized = true; + constrainedHighProfilePrefixes = new LinkedList<>(); + constrainedHighProfilePrefixes.add("omx.intel"); } - private static boolean isDecoderInList(List decoderList, String decoderName) { - if (!initialized) { - throw new IllegalStateException("MediaCodecHelper must be initialized before use"); - } + static { + whitelistedHevcDecoders = new LinkedList<>(); - for (String badPrefix : decoderList) { - if (decoderName.length() >= badPrefix.length()) { - String prefix = decoderName.substring(0, badPrefix.length()); - if (prefix.equalsIgnoreCase(badPrefix)) { - return true; - } - } - } - - return false; - } + // Allow software HEVC decoding in the official AOSP emulator + if (Build.HARDWARE.equals("ranchu") && Build.BRAND.equals("google")) { + whitelistedHevcDecoders.add("omx.google"); + } - public static long getMonotonicMillis() { - return System.nanoTime() / 1000000L; - } + // Exynos seems to be the only HEVC decoder that works reliably + whitelistedHevcDecoders.add("omx.exynos"); - public static boolean decoderSupportsAdaptivePlayback(MediaCodecInfo decoderInfo) { - // Possibly enable adaptive playback on KitKat and above - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - if (isDecoderInList(blacklistedAdaptivePlaybackPrefixes, decoderInfo.getName())) { - LimeLog.info("Decoder blacklisted for adaptive playback"); - return false; - } + // On Darcy (Shield 2017), HEVC runs fine with no fixups required. + // For some reason, other X1 implementations require bitstream fixups. + if (Build.DEVICE.equalsIgnoreCase("darcy")) { + whitelistedHevcDecoders.add("omx.nvidia"); + } + else { + // TODO: This needs a similar fixup to the Tegra 3 otherwise it buffers 16 frames + } - try { - if (decoderInfo.getCapabilitiesForType("video/avc"). - isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback)) - { - // This will make getCapabilities() return that adaptive playback is supported - LimeLog.info("Adaptive playback supported (FEATURE_AdaptivePlayback)"); - return true; - } - } catch (Exception e) { - // Tolerate buggy codecs - } - } - - return false; - } + // Sony ATVs have broken MediaTek codecs (decoder hangs after rendering the first frame). + // I know the Fire TV 2 and 3 works, so I'll just whitelist Amazon devices which seem + // to actually be tested. + if (Build.MANUFACTURER.equalsIgnoreCase("Amazon")) { + whitelistedHevcDecoders.add("omx.mtk"); + whitelistedHevcDecoders.add("omx.amlogic"); + } - public static boolean decoderNeedsConstrainedHighProfile(String decoderName) { - return isDecoderInList(constrainedHighProfilePrefixes, decoderName); - } + // These theoretically have good HEVC decoding capabilities (potentially better than + // their AVC decoders), but haven't been tested enough + //whitelistedHevcDecoders.add("omx.amlogic"); + //whitelistedHevcDecoders.add("omx.rk"); + + // Based on GPU attributes queried at runtime, the omx.qcom prefix will be added + // during initialization to avoid SoCs with broken HEVC decoders. + } + + static { + deprioritizedHevcDecoders = new LinkedList<>(); + + // These are decoders that work but aren't used by default for various reasons. + + // Qualcomm is currently the only decoders in this group. + } + + static { + blacklisted49FpsDecoderPrefixes = new LinkedList<>(); + + // We see a bunch of crashes on MediaTek Android TVs running + // at 49 FPS (PAL 50 Hz - 1). Blacklist this frame rate for + // these devices and hope they fix it in Pie. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { + blacklisted49FpsDecoderPrefixes.add("omx.mtk"); + } + } + + private static boolean isPowerVR(String glRenderer) { + return glRenderer.toLowerCase().contains("powervr"); + } + + private static String getAdrenoVersionString(String glRenderer) { + glRenderer = glRenderer.toLowerCase().trim(); + + if (!glRenderer.contains("adreno")) { + return null; + } + + Pattern modelNumberPattern = Pattern.compile("(.*)([0-9]{3})(.*)"); + + Matcher matcher = modelNumberPattern.matcher(glRenderer); + if (!matcher.matches()) { + return null; + } + + String modelNumber = matcher.group(2); + LimeLog.info("Found Adreno GPU: "+modelNumber); + return modelNumber; + } + + private static boolean isLowEndSnapdragonRenderer(String glRenderer) { + String modelNumber = getAdrenoVersionString(glRenderer); + if (modelNumber == null) { + // Not an Adreno GPU + return false; + } + + // The current logic is to identify low-end SoCs based on a zero in the x0x place. + return modelNumber.charAt(1) == '0'; + } + + // This is a workaround for some broken devices that report + // only GLES 3.0 even though the GPU is an Adreno 4xx series part. + // An example of such a device is the Huawei Honor 5x with the + // Snapdragon 616 SoC (Adreno 405). + private static boolean isGLES31SnapdragonRenderer(String glRenderer) { + String modelNumber = getAdrenoVersionString(glRenderer); + if (modelNumber == null) { + // Not an Adreno GPU + return false; + } + + // Snapdragon 4xx and higher support GLES 3.1 + return modelNumber.charAt(0) >= '4'; + } + + public static void initialize(Context context, String glRenderer) { + if (initialized) { + return; + } + + ActivityManager activityManager = + (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + ConfigurationInfo configInfo = activityManager.getDeviceConfigurationInfo(); + if (configInfo.reqGlEsVersion != ConfigurationInfo.GL_ES_VERSION_UNDEFINED) { + LimeLog.info("OpenGL ES version: "+configInfo.reqGlEsVersion); + + isLowEndSnapdragon = isLowEndSnapdragonRenderer(glRenderer); + + // Tegra K1 and later can do reference frame invalidation properly + if (configInfo.reqGlEsVersion >= 0x30000) { + LimeLog.info("Added omx.nvidia to AVC reference frame invalidation support list"); + refFrameInvalidationAvcPrefixes.add("omx.nvidia"); + + LimeLog.info("Added omx.qcom to AVC reference frame invalidation support list"); + refFrameInvalidationAvcPrefixes.add("omx.qcom"); + + // Prior to M, we were tricking the decoder into using baseline profile, which + // won't support RFI properly. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + LimeLog.info("Added omx.intel to AVC reference frame invalidation support list"); + refFrameInvalidationAvcPrefixes.add("omx.intel"); + } + } + + // Qualcomm's early HEVC decoders break hard on our HEVC stream. The best check to + // tell the good from the bad decoders are the generation of Adreno GPU included: + // 3xx - bad + // 4xx - good + // + // The "good" GPUs support GLES 3.1, but we can't just check that directly + // (see comment on isGLES31SnapdragonRenderer). + // + if (isGLES31SnapdragonRenderer(glRenderer)) { + // We prefer reference frame invalidation support (which is only doable on AVC on + // older Qualcomm chips) vs. enabling HEVC by default. The user can override using the settings + // to force HEVC on. If HDR or mobile data will be used, we'll override this and use + // HEVC anyway. + LimeLog.info("Added omx.qcom to deprioritized HEVC decoders based on GLES 3.1+ support"); + deprioritizedHevcDecoders.add("omx.qcom"); + } + else { + blacklistedDecoderPrefixes.add("OMX.qcom.video.decoder.hevc"); + } + + // Older MediaTek SoCs have issues with HEVC rendering but the newer chips with + // PowerVR GPUs have good HEVC support. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isPowerVR(glRenderer)) { + LimeLog.info("Added omx.mtk to HEVC decoders based on PowerVR GPU"); + whitelistedHevcDecoders.add("omx.mtk"); + + // This SoC (MT8176 in GPD XD+) supports AVC RFI too, but the maxNumReferenceFrames setting + // required to make it work adds a huge amount of latency. + LimeLog.info("Added omx.mtk to RFI list for HEVC"); + refFrameInvalidationHevcPrefixes.add("omx.mtk"); + } + } + + initialized = true; + } + + private static boolean isDecoderInList(List decoderList, String decoderName) { + if (!initialized) { + throw new IllegalStateException("MediaCodecHelper must be initialized before use"); + } + + for (String badPrefix : decoderList) { + if (decoderName.length() >= badPrefix.length()) { + String prefix = decoderName.substring(0, badPrefix.length()); + if (prefix.equalsIgnoreCase(badPrefix)) { + return true; + } + } + } + + return false; + } + + public static long getMonotonicMillis() { + return System.nanoTime() / 1000000L; + } + + public static boolean decoderSupportsAdaptivePlayback(MediaCodecInfo decoderInfo) { + // Possibly enable adaptive playback on KitKat and above + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + if (isDecoderInList(blacklistedAdaptivePlaybackPrefixes, decoderInfo.getName())) { + LimeLog.info("Decoder blacklisted for adaptive playback"); + return false; + } + + try { + if (decoderInfo.getCapabilitiesForType("video/avc"). + isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback)) + { + // This will make getCapabilities() return that adaptive playback is supported + LimeLog.info("Adaptive playback supported (FEATURE_AdaptivePlayback)"); + return true; + } + } catch (Exception e) { + // Tolerate buggy codecs + } + } + + return false; + } + + public static boolean decoderNeedsConstrainedHighProfile(String decoderName) { + return isDecoderInList(constrainedHighProfilePrefixes, decoderName); + } public static boolean decoderCanDirectSubmit(String decoderName) { return isDecoderInList(directSubmitPrefixes, decoderName) && !isExynos4Device(); } - - public static boolean decoderNeedsSpsBitstreamRestrictions(String decoderName) { - return isDecoderInList(spsFixupBitstreamFixupDecoderPrefixes, decoderName); - } + + public static boolean decoderNeedsSpsBitstreamRestrictions(String decoderName) { + return isDecoderInList(spsFixupBitstreamFixupDecoderPrefixes, decoderName); + } public static boolean decoderNeedsBaselineSpsHack(String decoderName) { return isDecoderInList(baselineProfileHackPrefixes, decoderName); } - public static boolean decoderBlacklistedFor49Fps(String decoderName) { - return isDecoderInList(blacklisted49FpsDecoderPrefixes, decoderName); - } + public static boolean decoderBlacklistedFor49Fps(String decoderName) { + return isDecoderInList(blacklisted49FpsDecoderPrefixes, decoderName); + } - public static boolean decoderSupportsRefFrameInvalidationAvc(String decoderName, int videoHeight) { - // Reference frame invalidation is broken on low-end Snapdragon SoCs at 1080p. - if (videoHeight > 720 && isLowEndSnapdragon) { - return false; - } + public static boolean decoderSupportsRefFrameInvalidationAvc(String decoderName, int videoHeight) { + // Reference frame invalidation is broken on low-end Snapdragon SoCs at 1080p. + if (videoHeight > 720 && isLowEndSnapdragon) { + return false; + } - // This device seems to crash constantly at 720p, so try disabling - // RFI to see if we can get that under control. - if (Build.DEVICE.equals("b3") || Build.DEVICE.equals("b5")) { - return false; - } + // This device seems to crash constantly at 720p, so try disabling + // RFI to see if we can get that under control. + if (Build.DEVICE.equals("b3") || Build.DEVICE.equals("b5")) { + return false; + } - return isDecoderInList(refFrameInvalidationAvcPrefixes, decoderName); - } + return isDecoderInList(refFrameInvalidationAvcPrefixes, decoderName); + } public static boolean decoderSupportsRefFrameInvalidationHevc(String decoderName) { return isDecoderInList(refFrameInvalidationHevcPrefixes, decoderName); } - public static boolean decoderIsWhitelistedForHevc(String decoderName, boolean meteredData) { - // TODO: Shield Tablet K1/LTE? - // - // NVIDIA does partial HEVC acceleration on the Shield Tablet. I don't know - // whether the performance is good enough to use for streaming, but they're - // using the same omx.nvidia.h265.decode name as the Shield TV which has a - // fully accelerated HEVC pipeline. AFAIK, the only K1 device with this - // partially accelerated HEVC decoder is the Shield Tablet, so I'll - // check for it here. - // - // TODO: Temporarily disabled with NVIDIA HEVC support - /*if (Build.DEVICE.equalsIgnoreCase("shieldtablet")) { - return false; - }*/ + public static boolean decoderIsWhitelistedForHevc(String decoderName, boolean meteredData) { + // TODO: Shield Tablet K1/LTE? + // + // NVIDIA does partial HEVC acceleration on the Shield Tablet. I don't know + // whether the performance is good enough to use for streaming, but they're + // using the same omx.nvidia.h265.decode name as the Shield TV which has a + // fully accelerated HEVC pipeline. AFAIK, the only K1 device with this + // partially accelerated HEVC decoder is the Shield Tablet, so I'll + // check for it here. + // + // TODO: Temporarily disabled with NVIDIA HEVC support + /*if (Build.DEVICE.equalsIgnoreCase("shieldtablet")) { + return false; + }*/ - // Google didn't have official support for HEVC (or more importantly, a CTS test) until - // Lollipop. I've seen some MediaTek devices on 4.4 crash when attempting to use HEVC, - // so I'm restricting HEVC usage to Lollipop and higher. - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - return false; - } + // Google didn't have official support for HEVC (or more importantly, a CTS test) until + // Lollipop. I've seen some MediaTek devices on 4.4 crash when attempting to use HEVC, + // so I'm restricting HEVC usage to Lollipop and higher. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + return false; + } - // - // Software decoders are terrible and we never want to use them. - // We want to catch decoders like: - // OMX.qcom.video.decoder.hevcswvdec - // OMX.SEC.hevc.sw.dec - // - if (decoderName.contains("sw")) { - return false; - } + // + // Software decoders are terrible and we never want to use them. + // We want to catch decoders like: + // OMX.qcom.video.decoder.hevcswvdec + // OMX.SEC.hevc.sw.dec + // + if (decoderName.contains("sw")) { + return false; + } - // Some devices have HEVC decoders that we prefer not to use - // typically because it can't support reference frame invalidation. - // However, we will use it for HDR and for streaming over mobile networks - // since it works fine otherwise. - if (meteredData && isDecoderInList(deprioritizedHevcDecoders, decoderName)) { - LimeLog.info("Selected deprioritized decoder"); - return true; - } + // Some devices have HEVC decoders that we prefer not to use + // typically because it can't support reference frame invalidation. + // However, we will use it for HDR and for streaming over mobile networks + // since it works fine otherwise. + if (meteredData && isDecoderInList(deprioritizedHevcDecoders, decoderName)) { + LimeLog.info("Selected deprioritized decoder"); + return true; + } - return isDecoderInList(whitelistedHevcDecoders, decoderName); - } - - @SuppressWarnings("deprecation") - @SuppressLint("NewApi") - private static LinkedList getMediaCodecList() { - LinkedList infoList = new LinkedList<>(); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS); + return isDecoderInList(whitelistedHevcDecoders, decoderName); + } + + @SuppressWarnings("deprecation") + @SuppressLint("NewApi") + private static LinkedList getMediaCodecList() { + LinkedList infoList = new LinkedList<>(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS); Collections.addAll(infoList, mcl.getCodecInfos()); - } - else { - for (int i = 0; i < MediaCodecList.getCodecCount(); i++) { - infoList.add(MediaCodecList.getCodecInfoAt(i)); - } - } - - return infoList; - } - - @SuppressWarnings("RedundantThrows") + } + else { + for (int i = 0; i < MediaCodecList.getCodecCount(); i++) { + infoList.add(MediaCodecList.getCodecInfoAt(i)); + } + } + + return infoList; + } + + @SuppressWarnings("RedundantThrows") public static String dumpDecoders() throws Exception { - String str = ""; - for (MediaCodecInfo codecInfo : getMediaCodecList()) { - // Skip encoders - if (codecInfo.isEncoder()) { - continue; - } - - str += "Decoder: "+codecInfo.getName()+"\n"; - for (String type : codecInfo.getSupportedTypes()) { - str += "\t"+type+"\n"; - CodecCapabilities caps = codecInfo.getCapabilitiesForType(type); - - for (CodecProfileLevel profile : caps.profileLevels) { - str += "\t\t"+profile.profile+" "+profile.level+"\n"; - } - } - } - return str; - } - - private static MediaCodecInfo findPreferredDecoder() { - // This is a different algorithm than the other findXXXDecoder functions, - // because we want to evaluate the decoders in our list's order - // rather than MediaCodecList's order + String str = ""; + for (MediaCodecInfo codecInfo : getMediaCodecList()) { + // Skip encoders + if (codecInfo.isEncoder()) { + continue; + } + + str += "Decoder: "+codecInfo.getName()+"\n"; + for (String type : codecInfo.getSupportedTypes()) { + str += "\t"+type+"\n"; + CodecCapabilities caps = codecInfo.getCapabilitiesForType(type); + + for (CodecProfileLevel profile : caps.profileLevels) { + str += "\t\t"+profile.profile+" "+profile.level+"\n"; + } + } + } + return str; + } + + private static MediaCodecInfo findPreferredDecoder() { + // This is a different algorithm than the other findXXXDecoder functions, + // because we want to evaluate the decoders in our list's order + // rather than MediaCodecList's order - if (!initialized) { - throw new IllegalStateException("MediaCodecHelper must be initialized before use"); - } - - for (String preferredDecoder : preferredDecoders) { - for (MediaCodecInfo codecInfo : getMediaCodecList()) { - // Skip encoders - if (codecInfo.isEncoder()) { - continue; - } - - // Check for preferred decoders - if (preferredDecoder.equalsIgnoreCase(codecInfo.getName())) { - LimeLog.info("Preferred decoder choice is "+codecInfo.getName()); - return codecInfo; - } - } - } - - return null; - } + if (!initialized) { + throw new IllegalStateException("MediaCodecHelper must be initialized before use"); + } + + for (String preferredDecoder : preferredDecoders) { + for (MediaCodecInfo codecInfo : getMediaCodecList()) { + // Skip encoders + if (codecInfo.isEncoder()) { + continue; + } + + // Check for preferred decoders + if (preferredDecoder.equalsIgnoreCase(codecInfo.getName())) { + LimeLog.info("Preferred decoder choice is "+codecInfo.getName()); + return codecInfo; + } + } + } + + return null; + } - private static boolean isCodecBlacklisted(MediaCodecInfo codecInfo) { - // Use the new isSoftwareOnly() function on Android Q - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - if (codecInfo.isSoftwareOnly()) { - LimeLog.info("Skipping software-only decoder: "+codecInfo.getName()); - return true; - } - } + private static boolean isCodecBlacklisted(MediaCodecInfo codecInfo) { + // Use the new isSoftwareOnly() function on Android Q + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + if (codecInfo.isSoftwareOnly()) { + LimeLog.info("Skipping software-only decoder: "+codecInfo.getName()); + return true; + } + } - // Check for explicitly blacklisted decoders - if (isDecoderInList(blacklistedDecoderPrefixes, codecInfo.getName())) { - LimeLog.info("Skipping blacklisted decoder: "+codecInfo.getName()); - return true; - } + // Check for explicitly blacklisted decoders + if (isDecoderInList(blacklistedDecoderPrefixes, codecInfo.getName())) { + LimeLog.info("Skipping blacklisted decoder: "+codecInfo.getName()); + return true; + } - return false; - } - - public static MediaCodecInfo findFirstDecoder(String mimeType) { - for (MediaCodecInfo codecInfo : getMediaCodecList()) { - // Skip encoders - if (codecInfo.isEncoder()) { - continue; - } - - // Find a decoder that supports the specified video format - for (String mime : codecInfo.getSupportedTypes()) { - if (mime.equalsIgnoreCase(mimeType)) { - // Skip blacklisted codecs - if (isCodecBlacklisted(codecInfo)) { - continue; - } + return false; + } + + public static MediaCodecInfo findFirstDecoder(String mimeType) { + for (MediaCodecInfo codecInfo : getMediaCodecList()) { + // Skip encoders + if (codecInfo.isEncoder()) { + continue; + } + + // Find a decoder that supports the specified video format + for (String mime : codecInfo.getSupportedTypes()) { + if (mime.equalsIgnoreCase(mimeType)) { + // Skip blacklisted codecs + if (isCodecBlacklisted(codecInfo)) { + continue; + } - LimeLog.info("First decoder choice is "+codecInfo.getName()); - return codecInfo; - } - } - } - - return null; - } - - public static MediaCodecInfo findProbableSafeDecoder(String mimeType, int requiredProfile) { - // First look for a preferred decoder by name - MediaCodecInfo info = findPreferredDecoder(); - if (info != null) { - return info; - } - - // Now look for decoders we know are safe - try { - // If this function completes, it will determine if the decoder is safe - return findKnownSafeDecoder(mimeType, requiredProfile); - } catch (Exception e) { - // Some buggy devices seem to throw exceptions - // from getCapabilitiesForType() so we'll just assume - // they're okay and go with the first one we find - return findFirstDecoder(mimeType); - } - } + LimeLog.info("First decoder choice is "+codecInfo.getName()); + return codecInfo; + } + } + } + + return null; + } + + public static MediaCodecInfo findProbableSafeDecoder(String mimeType, int requiredProfile) { + // First look for a preferred decoder by name + MediaCodecInfo info = findPreferredDecoder(); + if (info != null) { + return info; + } + + // Now look for decoders we know are safe + try { + // If this function completes, it will determine if the decoder is safe + return findKnownSafeDecoder(mimeType, requiredProfile); + } catch (Exception e) { + // Some buggy devices seem to throw exceptions + // from getCapabilitiesForType() so we'll just assume + // they're okay and go with the first one we find + return findFirstDecoder(mimeType); + } + } - // We declare this method as explicitly throwing Exception - // since some bad decoders can throw IllegalArgumentExceptions unexpectedly - // and we want to be sure all callers are handling this possibility - @SuppressWarnings("RedundantThrows") + // We declare this method as explicitly throwing Exception + // since some bad decoders can throw IllegalArgumentExceptions unexpectedly + // and we want to be sure all callers are handling this possibility + @SuppressWarnings("RedundantThrows") private static MediaCodecInfo findKnownSafeDecoder(String mimeType, int requiredProfile) throws Exception { - for (MediaCodecInfo codecInfo : getMediaCodecList()) { - // Skip encoders - if (codecInfo.isEncoder()) { - continue; - } - - // Find a decoder that supports the requested video format - for (String mime : codecInfo.getSupportedTypes()) { - if (mime.equalsIgnoreCase(mimeType)) { - LimeLog.info("Examining decoder capabilities of "+codecInfo.getName()); + for (MediaCodecInfo codecInfo : getMediaCodecList()) { + // Skip encoders + if (codecInfo.isEncoder()) { + continue; + } + + // Find a decoder that supports the requested video format + for (String mime : codecInfo.getSupportedTypes()) { + if (mime.equalsIgnoreCase(mimeType)) { + LimeLog.info("Examining decoder capabilities of "+codecInfo.getName()); - // Skip blacklisted codecs - if (isCodecBlacklisted(codecInfo)) { - continue; - } + // Skip blacklisted codecs + if (isCodecBlacklisted(codecInfo)) { + continue; + } - CodecCapabilities caps = codecInfo.getCapabilitiesForType(mime); + CodecCapabilities caps = codecInfo.getCapabilitiesForType(mime); - if (requiredProfile != -1) { - for (CodecProfileLevel profile : caps.profileLevels) { - if (profile.profile == requiredProfile) { - LimeLog.info("Decoder " + codecInfo.getName() + " supports required profile"); - return codecInfo; - } - } + if (requiredProfile != -1) { + for (CodecProfileLevel profile : caps.profileLevels) { + if (profile.profile == requiredProfile) { + LimeLog.info("Decoder " + codecInfo.getName() + " supports required profile"); + return codecInfo; + } + } - LimeLog.info("Decoder " + codecInfo.getName() + " does NOT support required profile"); - } - else { - return codecInfo; - } - } - } - } - - return null; - } - - public static String readCpuinfo() throws Exception { - StringBuilder cpuInfo = new StringBuilder(); - BufferedReader br = new BufferedReader(new FileReader(new File("/proc/cpuinfo"))); - try { - for (;;) { - int ch = br.read(); - if (ch == -1) - break; - cpuInfo.append((char)ch); - } + LimeLog.info("Decoder " + codecInfo.getName() + " does NOT support required profile"); + } + else { + return codecInfo; + } + } + } + } + + return null; + } + + public static String readCpuinfo() throws Exception { + StringBuilder cpuInfo = new StringBuilder(); + BufferedReader br = new BufferedReader(new FileReader(new File("/proc/cpuinfo"))); + try { + for (;;) { + int ch = br.read(); + if (ch == -1) + break; + cpuInfo.append((char)ch); + } - return cpuInfo.toString(); - } finally { - br.close(); - } - } - - private static boolean stringContainsIgnoreCase(String string, String substring) { - return string.toLowerCase(Locale.ENGLISH).contains(substring.toLowerCase(Locale.ENGLISH)); - } - - public static boolean isExynos4Device() { - try { - // Try reading CPU info too look for - String cpuInfo = readCpuinfo(); - - // SMDK4xxx is Exynos 4 - if (stringContainsIgnoreCase(cpuInfo, "SMDK4")) { - LimeLog.info("Found SMDK4 in /proc/cpuinfo"); - return true; - } - - // If we see "Exynos 4" also we'll count it - if (stringContainsIgnoreCase(cpuInfo, "Exynos 4")) { - LimeLog.info("Found Exynos 4 in /proc/cpuinfo"); - return true; - } - } catch (Exception e) { - e.printStackTrace(); - } - - try { - File systemDir = new File("/sys/devices/system"); - File[] files = systemDir.listFiles(); - if (files != null) { - for (File f : files) { - if (stringContainsIgnoreCase(f.getName(), "exynos4")) { - LimeLog.info("Found exynos4 in /sys/devices/system"); - return true; - } - } - } - } catch (Exception e) { - e.printStackTrace(); - } - - return false; - } + return cpuInfo.toString(); + } finally { + br.close(); + } + } + + private static boolean stringContainsIgnoreCase(String string, String substring) { + return string.toLowerCase(Locale.ENGLISH).contains(substring.toLowerCase(Locale.ENGLISH)); + } + + public static boolean isExynos4Device() { + try { + // Try reading CPU info too look for + String cpuInfo = readCpuinfo(); + + // SMDK4xxx is Exynos 4 + if (stringContainsIgnoreCase(cpuInfo, "SMDK4")) { + LimeLog.info("Found SMDK4 in /proc/cpuinfo"); + return true; + } + + // If we see "Exynos 4" also we'll count it + if (stringContainsIgnoreCase(cpuInfo, "Exynos 4")) { + LimeLog.info("Found Exynos 4 in /proc/cpuinfo"); + return true; + } + } catch (Exception e) { + e.printStackTrace(); + } + + try { + File systemDir = new File("/sys/devices/system"); + File[] files = systemDir.listFiles(); + if (files != null) { + for (File f : files) { + if (stringContainsIgnoreCase(f.getName(), "exynos4")) { + LimeLog.info("Found exynos4 in /sys/devices/system"); + return true; + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + + return false; + } } diff --git a/app/src/main/java/com/limelight/nvstream/ConnectionContext.java b/app/src/main/java/com/limelight/nvstream/ConnectionContext.java index f881624f..05b459eb 100644 --- a/app/src/main/java/com/limelight/nvstream/ConnectionContext.java +++ b/app/src/main/java/com/limelight/nvstream/ConnectionContext.java @@ -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; } diff --git a/app/src/main/java/com/limelight/nvstream/NvConnection.java b/app/src/main/java/com/limelight/nvstream/NvConnection.java index 426b4e25..18d786d4 100644 --- a/app/src/main/java/com/limelight/nvstream/NvConnection.java +++ b/app/src/main/java/com/limelight/nvstream/NvConnection.java @@ -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); + } } diff --git a/app/src/main/java/com/limelight/nvstream/NvConnectionListener.java b/app/src/main/java/com/limelight/nvstream/NvConnectionListener.java index 29b98920..91f36710 100644 --- a/app/src/main/java/com/limelight/nvstream/NvConnectionListener.java +++ b/app/src/main/java/com/limelight/nvstream/NvConnectionListener.java @@ -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); } diff --git a/app/src/main/java/com/limelight/nvstream/StreamConfiguration.java b/app/src/main/java/com/limelight/nvstream/StreamConfiguration.java index 991a36e3..be4498f8 100644 --- a/app/src/main/java/com/limelight/nvstream/StreamConfiguration.java +++ b/app/src/main/java/com/limelight/nvstream/StreamConfiguration.java @@ -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; + } } diff --git a/app/src/main/java/com/limelight/nvstream/av/ByteBufferDescriptor.java b/app/src/main/java/com/limelight/nvstream/av/ByteBufferDescriptor.java index e03e3b7c..b2a59101 100644 --- a/app/src/main/java/com/limelight/nvstream/av/ByteBufferDescriptor.java +++ b/app/src/main/java/com/limelight/nvstream/av/ByteBufferDescriptor.java @@ -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(); + } } diff --git a/app/src/main/java/com/limelight/nvstream/av/audio/AudioRenderer.java b/app/src/main/java/com/limelight/nvstream/av/audio/AudioRenderer.java index 6944cc39..f41ff52e 100644 --- a/app/src/main/java/com/limelight/nvstream/av/audio/AudioRenderer.java +++ b/app/src/main/java/com/limelight/nvstream/av/audio/AudioRenderer.java @@ -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(); } diff --git a/app/src/main/java/com/limelight/nvstream/av/video/VideoDecoderRenderer.java b/app/src/main/java/com/limelight/nvstream/av/video/VideoDecoderRenderer.java index c15262f2..ff383448 100644 --- a/app/src/main/java/com/limelight/nvstream/av/video/VideoDecoderRenderer.java +++ b/app/src/main/java/com/limelight/nvstream/av/video/VideoDecoderRenderer.java @@ -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(); } diff --git a/app/src/main/java/com/limelight/nvstream/http/ComputerDetails.java b/app/src/main/java/com/limelight/nvstream/http/ComputerDetails.java index fa1fd7b4..59be15a2 100644 --- a/app/src/main/java/com/limelight/nvstream/http/ComputerDetails.java +++ b/app/src/main/java/com/limelight/nvstream/http/ComputerDetails.java @@ -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(); + } } diff --git a/app/src/main/java/com/limelight/nvstream/http/GfeHttpResponseException.java b/app/src/main/java/com/limelight/nvstream/http/GfeHttpResponseException.java index eb8b6e78..df62bbf1 100644 --- a/app/src/main/java/com/limelight/nvstream/http/GfeHttpResponseException.java +++ b/app/src/main/java/com/limelight/nvstream/http/GfeHttpResponseException.java @@ -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+")"; + } } diff --git a/app/src/main/java/com/limelight/nvstream/http/LimelightCryptoProvider.java b/app/src/main/java/com/limelight/nvstream/http/LimelightCryptoProvider.java index 7e90af13..572c7839 100644 --- a/app/src/main/java/com/limelight/nvstream/http/LimelightCryptoProvider.java +++ b/app/src/main/java/com/limelight/nvstream/http/LimelightCryptoProvider.java @@ -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); } diff --git a/app/src/main/java/com/limelight/nvstream/http/NvApp.java b/app/src/main/java/com/limelight/nvstream/http/NvApp.java index 0b247e2f..70f1692a 100644 --- a/app/src/main/java/com/limelight/nvstream/http/NvApp.java +++ b/app/src/main/java/com/limelight/nvstream/http/NvApp.java @@ -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; + } } diff --git a/app/src/main/java/com/limelight/nvstream/http/NvHTTP.java b/app/src/main/java/com/limelight/nvstream/http/NvHTTP.java index 0f55988d..8d467f7c 100644 --- a/app/src/main/java/com/limelight/nvstream/http/NvHTTP.java +++ b/app/src/main/java/com/limelight/nvstream/http/NvHTTP.java @@ -50,637 +50,637 @@ import okhttp3.ResponseBody; public class NvHTTP { - private String uniqueId; - private PairingManager pm; + private String uniqueId; + private PairingManager pm; - public static final int HTTPS_PORT = 47984; - public static final int HTTP_PORT = 47989; - public static final int CONNECTION_TIMEOUT = 3000; - public static final int READ_TIMEOUT = 5000; + public static final int HTTPS_PORT = 47984; + public static final int HTTP_PORT = 47989; + public static final int CONNECTION_TIMEOUT = 3000; + public static final int READ_TIMEOUT = 5000; - // Print URL and content to logcat on debug builds - private static boolean verbose = BuildConfig.DEBUG; + // Print URL and content to logcat on debug builds + private static boolean verbose = BuildConfig.DEBUG; - public String baseUrlHttps; - public String baseUrlHttp; - - private OkHttpClient httpClient; - private OkHttpClient httpClientWithReadTimeout; - - private X509TrustManager trustManager; - private X509KeyManager keyManager; - private X509Certificate serverCert; + public String baseUrlHttps; + public String baseUrlHttp; + + private OkHttpClient httpClient; + private OkHttpClient httpClientWithReadTimeout; + + private X509TrustManager trustManager; + private X509KeyManager keyManager; + private X509Certificate serverCert; - void setServerCert(X509Certificate serverCert) { - this.serverCert = serverCert; + void setServerCert(X509Certificate serverCert) { + this.serverCert = serverCert; - trustManager = new X509TrustManager() { - public X509Certificate[] getAcceptedIssuers() { - return new X509Certificate[0]; - } - public void checkClientTrusted(X509Certificate[] certs, String authType) { - throw new IllegalStateException("Should never be called"); - } - public void checkServerTrusted(X509Certificate[] certs, String authType) throws CertificateException { - // Check the server certificate if we've paired to this host - if (!certs[0].equals(NvHTTP.this.serverCert)) { - throw new CertificateException("Certificate mismatch"); - } - } - }; - } + trustManager = new X509TrustManager() { + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + public void checkClientTrusted(X509Certificate[] certs, String authType) { + throw new IllegalStateException("Should never be called"); + } + public void checkServerTrusted(X509Certificate[] certs, String authType) throws CertificateException { + // Check the server certificate if we've paired to this host + if (!certs[0].equals(NvHTTP.this.serverCert)) { + throw new CertificateException("Certificate mismatch"); + } + } + }; + } - private void initializeHttpState(final X509Certificate serverCert, final LimelightCryptoProvider cryptoProvider) { - // Set up TrustManager - setServerCert(serverCert); + private void initializeHttpState(final X509Certificate serverCert, final LimelightCryptoProvider cryptoProvider) { + // Set up TrustManager + setServerCert(serverCert); - keyManager = new X509KeyManager() { - public String chooseClientAlias(String[] keyTypes, - Principal[] issuers, Socket socket) { return "Limelight-RSA"; } - public String chooseServerAlias(String keyType, Principal[] issuers, - Socket socket) { return null; } - public X509Certificate[] getCertificateChain(String alias) { - return new X509Certificate[] {cryptoProvider.getClientCertificate()}; - } - public String[] getClientAliases(String keyType, Principal[] issuers) { return null; } - public PrivateKey getPrivateKey(String alias) { - return cryptoProvider.getClientPrivateKey(); - } - public String[] getServerAliases(String keyType, Principal[] issuers) { return null; } - }; + keyManager = new X509KeyManager() { + public String chooseClientAlias(String[] keyTypes, + Principal[] issuers, Socket socket) { return "Limelight-RSA"; } + public String chooseServerAlias(String keyType, Principal[] issuers, + Socket socket) { return null; } + public X509Certificate[] getCertificateChain(String alias) { + return new X509Certificate[] {cryptoProvider.getClientCertificate()}; + } + public String[] getClientAliases(String keyType, Principal[] issuers) { return null; } + public PrivateKey getPrivateKey(String alias) { + return cryptoProvider.getClientPrivateKey(); + } + public String[] getServerAliases(String keyType, Principal[] issuers) { return null; } + }; - // Ignore differences between given hostname and certificate hostname - HostnameVerifier hv = new HostnameVerifier() { - public boolean verify(String hostname, SSLSession session) { return true; } - }; + // Ignore differences between given hostname and certificate hostname + HostnameVerifier hv = new HostnameVerifier() { + public boolean verify(String hostname, SSLSession session) { return true; } + }; - httpClient = new OkHttpClient.Builder() - .connectionPool(new ConnectionPool(0, 1, TimeUnit.MILLISECONDS)) - .hostnameVerifier(hv) - .readTimeout(0, TimeUnit.MILLISECONDS) - .connectTimeout(CONNECTION_TIMEOUT, TimeUnit.MILLISECONDS) - .build(); - - httpClientWithReadTimeout = httpClient.newBuilder() - .readTimeout(READ_TIMEOUT, TimeUnit.MILLISECONDS) - .build(); - } - - public NvHTTP(String address, String uniqueId, X509Certificate serverCert, LimelightCryptoProvider cryptoProvider) throws IOException { - // Use the same UID for all Moonlight clients so we can quit games - // started by other Moonlight clients. - this.uniqueId = "0123456789ABCDEF"; + httpClient = new OkHttpClient.Builder() + .connectionPool(new ConnectionPool(0, 1, TimeUnit.MILLISECONDS)) + .hostnameVerifier(hv) + .readTimeout(0, TimeUnit.MILLISECONDS) + .connectTimeout(CONNECTION_TIMEOUT, TimeUnit.MILLISECONDS) + .build(); + + httpClientWithReadTimeout = httpClient.newBuilder() + .readTimeout(READ_TIMEOUT, TimeUnit.MILLISECONDS) + .build(); + } + + public NvHTTP(String address, String uniqueId, X509Certificate serverCert, LimelightCryptoProvider cryptoProvider) throws IOException { + // Use the same UID for all Moonlight clients so we can quit games + // started by other Moonlight clients. + this.uniqueId = "0123456789ABCDEF"; - initializeHttpState(serverCert, cryptoProvider); + initializeHttpState(serverCert, cryptoProvider); - try { - // The URI constructor takes care of escaping IPv6 literals - this.baseUrlHttps = new URI("https", null, address, HTTPS_PORT, null, null, null).toString(); - this.baseUrlHttp = new URI("http", null, address, HTTP_PORT, null, null, null).toString(); - } catch (URISyntaxException e) { - // Encapsulate URISyntaxException into IOException for callers to handle more easily - throw new IOException(e); - } + try { + // The URI constructor takes care of escaping IPv6 literals + this.baseUrlHttps = new URI("https", null, address, HTTPS_PORT, null, null, null).toString(); + this.baseUrlHttp = new URI("http", null, address, HTTP_PORT, null, null, null).toString(); + } catch (URISyntaxException e) { + // Encapsulate URISyntaxException into IOException for callers to handle more easily + throw new IOException(e); + } - this.pm = new PairingManager(this, cryptoProvider); - } - - String buildUniqueIdUuidString() { - return "uniqueid="+uniqueId+"&uuid="+UUID.randomUUID(); - } - - static String getXmlString(Reader r, String tagname) throws XmlPullParserException, IOException { - XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); - factory.setNamespaceAware(true); - XmlPullParser xpp = factory.newPullParser(); + this.pm = new PairingManager(this, cryptoProvider); + } + + String buildUniqueIdUuidString() { + return "uniqueid="+uniqueId+"&uuid="+UUID.randomUUID(); + } + + static String getXmlString(Reader r, String tagname) throws XmlPullParserException, IOException { + XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); + factory.setNamespaceAware(true); + XmlPullParser xpp = factory.newPullParser(); - xpp.setInput(r); - int eventType = xpp.getEventType(); - Stack currentTag = new Stack(); - - while (eventType != XmlPullParser.END_DOCUMENT) { - switch (eventType) { - case (XmlPullParser.START_TAG): - if (xpp.getName().equals("root")) { - verifyResponseStatus(xpp); - } - currentTag.push(xpp.getName()); - break; - case (XmlPullParser.END_TAG): - currentTag.pop(); - break; - case (XmlPullParser.TEXT): - if (currentTag.peek().equals(tagname)) { - return xpp.getText().trim(); - } - break; - } - eventType = xpp.next(); - } + xpp.setInput(r); + int eventType = xpp.getEventType(); + Stack currentTag = new Stack(); + + while (eventType != XmlPullParser.END_DOCUMENT) { + switch (eventType) { + case (XmlPullParser.START_TAG): + if (xpp.getName().equals("root")) { + verifyResponseStatus(xpp); + } + currentTag.push(xpp.getName()); + break; + case (XmlPullParser.END_TAG): + currentTag.pop(); + break; + case (XmlPullParser.TEXT): + if (currentTag.peek().equals(tagname)) { + return xpp.getText().trim(); + } + break; + } + eventType = xpp.next(); + } - return null; - } + return null; + } - static String getXmlString(String str, String tagname) throws XmlPullParserException, IOException { - return getXmlString(new StringReader(str), tagname); - } - - private static void verifyResponseStatus(XmlPullParser xpp) throws GfeHttpResponseException { - int statusCode = Integer.parseInt(xpp.getAttributeValue(XmlPullParser.NO_NAMESPACE, "status_code")); - if (statusCode != 200) { - throw new GfeHttpResponseException(statusCode, xpp.getAttributeValue(XmlPullParser.NO_NAMESPACE, "status_message")); - } - } - - public String getServerInfo() throws IOException, XmlPullParserException { - String resp; - - // - // TODO: Shield Hub uses HTTP for this and is able to get an accurate PairStatus with HTTP. - // For some reason, we always see PairStatus is 0 over HTTP and only 1 over HTTPS. It looks - // like there are extra request headers required to make this stuff work over HTTP. - // + static String getXmlString(String str, String tagname) throws XmlPullParserException, IOException { + return getXmlString(new StringReader(str), tagname); + } + + private static void verifyResponseStatus(XmlPullParser xpp) throws GfeHttpResponseException { + int statusCode = Integer.parseInt(xpp.getAttributeValue(XmlPullParser.NO_NAMESPACE, "status_code")); + if (statusCode != 200) { + throw new GfeHttpResponseException(statusCode, xpp.getAttributeValue(XmlPullParser.NO_NAMESPACE, "status_message")); + } + } + + public String getServerInfo() throws IOException, XmlPullParserException { + String resp; + + // + // TODO: Shield Hub uses HTTP for this and is able to get an accurate PairStatus with HTTP. + // For some reason, we always see PairStatus is 0 over HTTP and only 1 over HTTPS. It looks + // like there are extra request headers required to make this stuff work over HTTP. + // - // When we have a pinned cert, use HTTPS to fetch serverinfo and fall back on cert mismatch - if (serverCert != null) { - try { - try { - resp = openHttpConnectionToString(baseUrlHttps + "/serverinfo?"+buildUniqueIdUuidString(), true); - } catch (SSLHandshakeException e) { - // Detect if we failed due to a server cert mismatch - if (e.getCause() instanceof CertificateException) { - // Jump to the GfeHttpResponseException exception handler to retry - // over HTTP which will allow us to pair again to update the cert - throw new GfeHttpResponseException(401, "Server certificate mismatch"); - } - else { - throw e; - } - } + // When we have a pinned cert, use HTTPS to fetch serverinfo and fall back on cert mismatch + if (serverCert != null) { + try { + try { + resp = openHttpConnectionToString(baseUrlHttps + "/serverinfo?"+buildUniqueIdUuidString(), true); + } catch (SSLHandshakeException e) { + // Detect if we failed due to a server cert mismatch + if (e.getCause() instanceof CertificateException) { + // Jump to the GfeHttpResponseException exception handler to retry + // over HTTP which will allow us to pair again to update the cert + throw new GfeHttpResponseException(401, "Server certificate mismatch"); + } + else { + throw e; + } + } - // This will throw an exception if the request came back with a failure status. - // We want this because it will throw us into the HTTP case if the client is unpaired. - getServerVersion(resp); - } - catch (GfeHttpResponseException e) { - if (e.getErrorCode() == 401) { - // Cert validation error - fall back to HTTP - return openHttpConnectionToString(baseUrlHttp + "/serverinfo", true); - } + // This will throw an exception if the request came back with a failure status. + // We want this because it will throw us into the HTTP case if the client is unpaired. + getServerVersion(resp); + } + catch (GfeHttpResponseException e) { + if (e.getErrorCode() == 401) { + // Cert validation error - fall back to HTTP + return openHttpConnectionToString(baseUrlHttp + "/serverinfo", true); + } - // If it's not a cert validation error, throw it - throw e; - } + // If it's not a cert validation error, throw it + throw e; + } - return resp; - } - else { - // No pinned cert, so use HTTP - return openHttpConnectionToString(baseUrlHttp + "/serverinfo", true); - } - } - - public ComputerDetails getComputerDetails() throws IOException, XmlPullParserException { - ComputerDetails details = new ComputerDetails(); - String serverInfo = getServerInfo(); - - details.name = getXmlString(serverInfo, "hostname"); - if (details.name == null || details.name.isEmpty()) { - details.name = "UNKNOWN"; - } + return resp; + } + else { + // No pinned cert, so use HTTP + return openHttpConnectionToString(baseUrlHttp + "/serverinfo", true); + } + } + + public ComputerDetails getComputerDetails() throws IOException, XmlPullParserException { + ComputerDetails details = new ComputerDetails(); + String serverInfo = getServerInfo(); + + details.name = getXmlString(serverInfo, "hostname"); + if (details.name == null || details.name.isEmpty()) { + details.name = "UNKNOWN"; + } - details.uuid = getXmlString(serverInfo, "uniqueid"); - details.macAddress = getXmlString(serverInfo, "mac"); - details.localAddress = getXmlString(serverInfo, "LocalIP"); + details.uuid = getXmlString(serverInfo, "uniqueid"); + details.macAddress = getXmlString(serverInfo, "mac"); + details.localAddress = getXmlString(serverInfo, "LocalIP"); - // This may be null, but that's okay - details.remoteAddress = getXmlString(serverInfo, "ExternalIP"); + // This may be null, but that's okay + details.remoteAddress = getXmlString(serverInfo, "ExternalIP"); - // This has some extra logic to always report unpaired if the pinned cert isn't there - details.pairState = getPairState(serverInfo); - - try { - details.runningGameId = getCurrentGame(serverInfo); - } catch (NumberFormatException e) { - details.runningGameId = 0; - } - - // We could reach it so it's online - details.state = ComputerDetails.State.ONLINE; - - return details; - } + // This has some extra logic to always report unpaired if the pinned cert isn't there + details.pairState = getPairState(serverInfo); + + try { + details.runningGameId = getCurrentGame(serverInfo); + } catch (NumberFormatException e) { + details.runningGameId = 0; + } + + // We could reach it so it's online + details.state = ComputerDetails.State.ONLINE; + + return details; + } - // This hack is Android-specific but we do it on all platforms - // because it doesn't really matter - private OkHttpClient performAndroidTlsHack(OkHttpClient client) { - // Doing this each time we create a socket is required - // to avoid the SSLv3 fallback that causes connection failures - try { - SSLContext sc = SSLContext.getInstance("TLS"); - sc.init(new KeyManager[] { keyManager }, new TrustManager[] { trustManager }, new SecureRandom()); - return client.newBuilder().sslSocketFactory(sc.getSocketFactory(), trustManager).build(); - } catch (NoSuchAlgorithmException | KeyManagementException e) { - throw new RuntimeException(e); - } - } + // This hack is Android-specific but we do it on all platforms + // because it doesn't really matter + private OkHttpClient performAndroidTlsHack(OkHttpClient client) { + // Doing this each time we create a socket is required + // to avoid the SSLv3 fallback that causes connection failures + try { + SSLContext sc = SSLContext.getInstance("TLS"); + sc.init(new KeyManager[] { keyManager }, new TrustManager[] { trustManager }, new SecureRandom()); + return client.newBuilder().sslSocketFactory(sc.getSocketFactory(), trustManager).build(); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + throw new RuntimeException(e); + } + } - public X509Certificate getCertificateIfTrusted() { - try { - Response resp = httpClient.newCall(new Request.Builder().url(baseUrlHttps).get().build()).execute(); - Handshake handshake = resp.handshake(); - if (handshake != null) { - return (X509Certificate)handshake.peerCertificates().get(0); - } - } catch (IOException ignored) {} + public X509Certificate getCertificateIfTrusted() { + try { + Response resp = httpClient.newCall(new Request.Builder().url(baseUrlHttps).get().build()).execute(); + Handshake handshake = resp.handshake(); + if (handshake != null) { + return (X509Certificate)handshake.peerCertificates().get(0); + } + } catch (IOException ignored) {} - return null; - } + return null; + } - // Read timeout should be enabled for any HTTP query that requires no outside action - // on the GFE server. Examples of queries that DO require outside action are launch, resume, and quit. - // The initial pair query does require outside action (user entering a PIN) but subsequent pairing - // queries do not. - private ResponseBody openHttpConnection(String url, boolean enableReadTimeout) throws IOException { - Request request = new Request.Builder().url(url).get().build(); - Response response; + // Read timeout should be enabled for any HTTP query that requires no outside action + // on the GFE server. Examples of queries that DO require outside action are launch, resume, and quit. + // The initial pair query does require outside action (user entering a PIN) but subsequent pairing + // queries do not. + private ResponseBody openHttpConnection(String url, boolean enableReadTimeout) throws IOException { + Request request = new Request.Builder().url(url).get().build(); + Response response; - if (serverCert == null && !url.startsWith(baseUrlHttp)) { - throw new IllegalStateException("Attempted HTTPS fetch without pinned cert"); - } + if (serverCert == null && !url.startsWith(baseUrlHttp)) { + throw new IllegalStateException("Attempted HTTPS fetch without pinned cert"); + } - if (enableReadTimeout) { - response = performAndroidTlsHack(httpClientWithReadTimeout).newCall(request).execute(); - } - else { - response = performAndroidTlsHack(httpClient).newCall(request).execute(); - } + if (enableReadTimeout) { + response = performAndroidTlsHack(httpClientWithReadTimeout).newCall(request).execute(); + } + else { + response = performAndroidTlsHack(httpClient).newCall(request).execute(); + } - ResponseBody body = response.body(); - - if (response.isSuccessful()) { - return body; - } - - // Unsuccessful, so close the response body - if (body != null) { - body.close(); - } - - if (response.code() == 404) { - throw new FileNotFoundException(url); - } - else { - throw new IOException("HTTP request failed: "+response.code()); - } - } - - String openHttpConnectionToString(String url, boolean enableReadTimeout) throws IOException { - try { - if (verbose) { - LimeLog.info("Requesting URL: "+url); - } + ResponseBody body = response.body(); + + if (response.isSuccessful()) { + return body; + } + + // Unsuccessful, so close the response body + if (body != null) { + body.close(); + } + + if (response.code() == 404) { + throw new FileNotFoundException(url); + } + else { + throw new IOException("HTTP request failed: "+response.code()); + } + } + + String openHttpConnectionToString(String url, boolean enableReadTimeout) throws IOException { + try { + if (verbose) { + LimeLog.info("Requesting URL: "+url); + } - ResponseBody resp = openHttpConnection(url, enableReadTimeout); - String respString = resp.string(); - resp.close(); + ResponseBody resp = openHttpConnection(url, enableReadTimeout); + String respString = resp.string(); + resp.close(); - if (verbose) { - LimeLog.info(url+" -> "+respString); - } + if (verbose) { + LimeLog.info(url+" -> "+respString); + } - return respString; - } catch (IOException e) { - if (verbose) { - e.printStackTrace(); - } - - throw e; - } - } + return respString; + } catch (IOException e) { + if (verbose) { + e.printStackTrace(); + } + + throw e; + } + } - public String getServerVersion(String serverInfo) throws XmlPullParserException, IOException { - return getXmlString(serverInfo, "appversion"); - } + public String getServerVersion(String serverInfo) throws XmlPullParserException, IOException { + return getXmlString(serverInfo, "appversion"); + } - public PairingManager.PairState getPairState() throws IOException, XmlPullParserException { - return getPairState(getServerInfo()); - } + public PairingManager.PairState getPairState() throws IOException, XmlPullParserException { + return getPairState(getServerInfo()); + } - public PairingManager.PairState getPairState(String serverInfo) throws IOException, XmlPullParserException { - // If we don't have a server cert, we can't be paired even if the host thinks we are - if (serverCert == null) { - return PairState.NOT_PAIRED; - } + public PairingManager.PairState getPairState(String serverInfo) throws IOException, XmlPullParserException { + // If we don't have a server cert, we can't be paired even if the host thinks we are + if (serverCert == null) { + return PairState.NOT_PAIRED; + } - if (!NvHTTP.getXmlString(serverInfo, "PairStatus").equals("1")) { - return PairState.NOT_PAIRED; - } + if (!NvHTTP.getXmlString(serverInfo, "PairStatus").equals("1")) { + return PairState.NOT_PAIRED; + } - return PairState.PAIRED; - } - - public long getMaxLumaPixelsH264(String serverInfo) throws XmlPullParserException, IOException { - String str = getXmlString(serverInfo, "MaxLumaPixelsH264"); - if (str != null) { - try { - return Long.parseLong(str); - } catch (NumberFormatException e) { - return 0; - } - } else { - return 0; - } - } - - public long getMaxLumaPixelsHEVC(String serverInfo) throws XmlPullParserException, IOException { - String str = getXmlString(serverInfo, "MaxLumaPixelsHEVC"); - if (str != null) { - try { - return Long.parseLong(str); - } catch (NumberFormatException e) { - return 0; - } - } else { - return 0; - } - } + return PairState.PAIRED; + } + + public long getMaxLumaPixelsH264(String serverInfo) throws XmlPullParserException, IOException { + String str = getXmlString(serverInfo, "MaxLumaPixelsH264"); + if (str != null) { + try { + return Long.parseLong(str); + } catch (NumberFormatException e) { + return 0; + } + } else { + return 0; + } + } + + public long getMaxLumaPixelsHEVC(String serverInfo) throws XmlPullParserException, IOException { + String str = getXmlString(serverInfo, "MaxLumaPixelsHEVC"); + if (str != null) { + try { + return Long.parseLong(str); + } catch (NumberFormatException e) { + return 0; + } + } else { + return 0; + } + } - // Possible meaning of bits - // Bit 0: H.264 Baseline - // Bit 1: H.264 High - // ---- - // Bit 8: HEVC Main - // Bit 9: HEVC Main10 - // Bit 10: HEVC Main10 4:4:4 - // Bit 11: ??? - public long getServerCodecModeSupport(String serverInfo) throws XmlPullParserException, IOException { - String str = getXmlString(serverInfo, "ServerCodecModeSupport"); - if (str != null) { - try { - return Long.parseLong(str); - } catch (NumberFormatException e) { - return 0; - } - } else { - return 0; - } - } - - public String getGpuType(String serverInfo) throws XmlPullParserException, IOException { - return getXmlString(serverInfo, "gputype"); - } + // Possible meaning of bits + // Bit 0: H.264 Baseline + // Bit 1: H.264 High + // ---- + // Bit 8: HEVC Main + // Bit 9: HEVC Main10 + // Bit 10: HEVC Main10 4:4:4 + // Bit 11: ??? + public long getServerCodecModeSupport(String serverInfo) throws XmlPullParserException, IOException { + String str = getXmlString(serverInfo, "ServerCodecModeSupport"); + if (str != null) { + try { + return Long.parseLong(str); + } catch (NumberFormatException e) { + return 0; + } + } else { + return 0; + } + } + + public String getGpuType(String serverInfo) throws XmlPullParserException, IOException { + return getXmlString(serverInfo, "gputype"); + } - public String getGfeVersion(String serverInfo) throws XmlPullParserException, IOException { - return getXmlString(serverInfo, "GfeVersion"); - } - - public boolean supports4K(String serverInfo) throws XmlPullParserException, IOException { - // Only allow 4K on GFE 3.x - String gfeVersionStr = getXmlString(serverInfo, "GfeVersion"); - if (gfeVersionStr == null || gfeVersionStr.startsWith("2.")) { - return false; - } + public String getGfeVersion(String serverInfo) throws XmlPullParserException, IOException { + return getXmlString(serverInfo, "GfeVersion"); + } + + public boolean supports4K(String serverInfo) throws XmlPullParserException, IOException { + // Only allow 4K on GFE 3.x + String gfeVersionStr = getXmlString(serverInfo, "GfeVersion"); + if (gfeVersionStr == null || gfeVersionStr.startsWith("2.")) { + return false; + } - return true; - } + return true; + } - public int getCurrentGame(String serverInfo) throws IOException, XmlPullParserException { - // GFE 2.8 started keeping currentgame set to the last game played. As a result, it no longer - // has the semantics that its name would indicate. To contain the effects of this change as much - // as possible, we'll force the current game to zero if the server isn't in a streaming session. - String serverState = getXmlString(serverInfo, "state"); - if (serverState != null && serverState.endsWith("_SERVER_BUSY")) { - String game = getXmlString(serverInfo, "currentgame"); - return Integer.parseInt(game); - } - else { - return 0; - } - } + public int getCurrentGame(String serverInfo) throws IOException, XmlPullParserException { + // GFE 2.8 started keeping currentgame set to the last game played. As a result, it no longer + // has the semantics that its name would indicate. To contain the effects of this change as much + // as possible, we'll force the current game to zero if the server isn't in a streaming session. + String serverState = getXmlString(serverInfo, "state"); + if (serverState != null && serverState.endsWith("_SERVER_BUSY")) { + String game = getXmlString(serverInfo, "currentgame"); + return Integer.parseInt(game); + } + else { + return 0; + } + } - public NvApp getAppById(int appId) throws IOException, XmlPullParserException { - LinkedList appList = getAppList(); - for (NvApp appFromList : appList) { - if (appFromList.getAppId() == appId) { - return appFromList; - } - } - return null; - } - - /* NOTE: Only use this function if you know what you're doing. - * It's totally valid to have two apps named the same thing, - * or even nothing at all! Look apps up by ID if at all possible - * using the above function */ - public NvApp getAppByName(String appName) throws IOException, XmlPullParserException { - LinkedList appList = getAppList(); - for (NvApp appFromList : appList) { - if (appFromList.getAppName().equalsIgnoreCase(appName)) { - return appFromList; - } - } - return null; - } + public NvApp getAppById(int appId) throws IOException, XmlPullParserException { + LinkedList appList = getAppList(); + for (NvApp appFromList : appList) { + if (appFromList.getAppId() == appId) { + return appFromList; + } + } + return null; + } + + /* NOTE: Only use this function if you know what you're doing. + * It's totally valid to have two apps named the same thing, + * or even nothing at all! Look apps up by ID if at all possible + * using the above function */ + public NvApp getAppByName(String appName) throws IOException, XmlPullParserException { + LinkedList appList = getAppList(); + for (NvApp appFromList : appList) { + if (appFromList.getAppName().equalsIgnoreCase(appName)) { + return appFromList; + } + } + return null; + } - public PairingManager getPairingManager() { - return pm; - } - - public static LinkedList getAppListByReader(Reader r) throws XmlPullParserException, IOException { - XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); - factory.setNamespaceAware(true); - XmlPullParser xpp = factory.newPullParser(); + public PairingManager getPairingManager() { + return pm; + } + + public static LinkedList getAppListByReader(Reader r) throws XmlPullParserException, IOException { + XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); + factory.setNamespaceAware(true); + XmlPullParser xpp = factory.newPullParser(); - xpp.setInput(r); - int eventType = xpp.getEventType(); - LinkedList appList = new LinkedList(); - Stack currentTag = new Stack(); - boolean rootTerminated = false; + xpp.setInput(r); + int eventType = xpp.getEventType(); + LinkedList appList = new LinkedList(); + Stack currentTag = new Stack(); + boolean rootTerminated = false; - while (eventType != XmlPullParser.END_DOCUMENT) { - switch (eventType) { - case (XmlPullParser.START_TAG): - if (xpp.getName().equals("root")) { - verifyResponseStatus(xpp); - } - currentTag.push(xpp.getName()); - if (xpp.getName().equals("App")) { - appList.addLast(new NvApp()); - } - break; - case (XmlPullParser.END_TAG): - currentTag.pop(); - if (xpp.getName().equals("root")) { - rootTerminated = true; - } - break; - case (XmlPullParser.TEXT): - NvApp app = appList.getLast(); - if (currentTag.peek().equals("AppTitle")) { - app.setAppName(xpp.getText().trim()); - } else if (currentTag.peek().equals("ID")) { - app.setAppId(xpp.getText().trim()); - } else if (currentTag.peek().equals("IsHdrSupported")) { - app.setHdrSupported(xpp.getText().trim().equals("1")); - } - break; - } - eventType = xpp.next(); - } - - // Throw a malformed XML exception if we've not seen the root tag ended - if (!rootTerminated) { - throw new XmlPullParserException("Malformed XML: Root tag was not terminated"); - } - - // Ensure that all apps in the list are initialized - ListIterator i = appList.listIterator(); - while (i.hasNext()) { - NvApp app = i.next(); - - // Remove uninitialized apps - if (!app.isInitialized()) { - LimeLog.warning("GFE returned incomplete app: "+app.getAppId()+" "+app.getAppName()); - i.remove(); - } - } - - return appList; - } - - public String getAppListRaw() throws MalformedURLException, IOException { - return openHttpConnectionToString(baseUrlHttps + "/applist?"+buildUniqueIdUuidString(), true); - } - - public LinkedList getAppList() throws GfeHttpResponseException, IOException, XmlPullParserException { - if (verbose) { - // Use the raw function so the app list is printed - return getAppListByReader(new StringReader(getAppListRaw())); - } - else { - ResponseBody resp = openHttpConnection(baseUrlHttps + "/applist?" + buildUniqueIdUuidString(), true); - LinkedList appList = getAppListByReader(new InputStreamReader(resp.byteStream())); - resp.close(); - return appList; - } - } - - public void unpair() throws IOException { - openHttpConnectionToString(baseUrlHttp + "/unpair?"+buildUniqueIdUuidString(), true); - } - - public InputStream getBoxArt(NvApp app) throws IOException { - ResponseBody resp = openHttpConnection(baseUrlHttps + "/appasset?"+ buildUniqueIdUuidString() + - "&appid=" + app.getAppId() + "&AssetType=2&AssetIdx=0", true); - return resp.byteStream(); - } - - public int getServerMajorVersion(String serverInfo) throws XmlPullParserException, IOException { - int[] appVersionQuad = getServerAppVersionQuad(serverInfo); - if (appVersionQuad != null) { - return appVersionQuad[0]; - } - else { - return 0; - } - } - - public int[] getServerAppVersionQuad(String serverInfo) throws XmlPullParserException, IOException { - try { - String serverVersion = getServerVersion(serverInfo); - if (serverVersion == null) { - LimeLog.warning("Missing server version field"); - return null; - } - String[] serverVersionSplit = serverVersion.split("\\."); - if (serverVersionSplit.length != 4) { - LimeLog.warning("Malformed server version field"); - return null; - } - int[] ret = new int[serverVersionSplit.length]; - for (int i = 0; i < ret.length; i++) { - ret[i] = Integer.parseInt(serverVersionSplit[i]); - } - return ret; - } catch (NumberFormatException e) { - e.printStackTrace(); - return null; - } - } + while (eventType != XmlPullParser.END_DOCUMENT) { + switch (eventType) { + case (XmlPullParser.START_TAG): + if (xpp.getName().equals("root")) { + verifyResponseStatus(xpp); + } + currentTag.push(xpp.getName()); + if (xpp.getName().equals("App")) { + appList.addLast(new NvApp()); + } + break; + case (XmlPullParser.END_TAG): + currentTag.pop(); + if (xpp.getName().equals("root")) { + rootTerminated = true; + } + break; + case (XmlPullParser.TEXT): + NvApp app = appList.getLast(); + if (currentTag.peek().equals("AppTitle")) { + app.setAppName(xpp.getText().trim()); + } else if (currentTag.peek().equals("ID")) { + app.setAppId(xpp.getText().trim()); + } else if (currentTag.peek().equals("IsHdrSupported")) { + app.setHdrSupported(xpp.getText().trim().equals("1")); + } + break; + } + eventType = xpp.next(); + } + + // Throw a malformed XML exception if we've not seen the root tag ended + if (!rootTerminated) { + throw new XmlPullParserException("Malformed XML: Root tag was not terminated"); + } + + // Ensure that all apps in the list are initialized + ListIterator i = appList.listIterator(); + while (i.hasNext()) { + NvApp app = i.next(); + + // Remove uninitialized apps + if (!app.isInitialized()) { + LimeLog.warning("GFE returned incomplete app: "+app.getAppId()+" "+app.getAppName()); + i.remove(); + } + } + + return appList; + } + + public String getAppListRaw() throws MalformedURLException, IOException { + return openHttpConnectionToString(baseUrlHttps + "/applist?"+buildUniqueIdUuidString(), true); + } + + public LinkedList getAppList() throws GfeHttpResponseException, IOException, XmlPullParserException { + if (verbose) { + // Use the raw function so the app list is printed + return getAppListByReader(new StringReader(getAppListRaw())); + } + else { + ResponseBody resp = openHttpConnection(baseUrlHttps + "/applist?" + buildUniqueIdUuidString(), true); + LinkedList appList = getAppListByReader(new InputStreamReader(resp.byteStream())); + resp.close(); + return appList; + } + } + + public void unpair() throws IOException { + openHttpConnectionToString(baseUrlHttp + "/unpair?"+buildUniqueIdUuidString(), true); + } + + public InputStream getBoxArt(NvApp app) throws IOException { + ResponseBody resp = openHttpConnection(baseUrlHttps + "/appasset?"+ buildUniqueIdUuidString() + + "&appid=" + app.getAppId() + "&AssetType=2&AssetIdx=0", true); + return resp.byteStream(); + } + + public int getServerMajorVersion(String serverInfo) throws XmlPullParserException, IOException { + int[] appVersionQuad = getServerAppVersionQuad(serverInfo); + if (appVersionQuad != null) { + return appVersionQuad[0]; + } + else { + return 0; + } + } + + public int[] getServerAppVersionQuad(String serverInfo) throws XmlPullParserException, IOException { + try { + String serverVersion = getServerVersion(serverInfo); + if (serverVersion == null) { + LimeLog.warning("Missing server version field"); + return null; + } + String[] serverVersionSplit = serverVersion.split("\\."); + if (serverVersionSplit.length != 4) { + LimeLog.warning("Malformed server version field"); + return null; + } + int[] ret = new int[serverVersionSplit.length]; + for (int i = 0; i < ret.length; i++) { + ret[i] = Integer.parseInt(serverVersionSplit[i]); + } + return ret; + } catch (NumberFormatException e) { + e.printStackTrace(); + return null; + } + } - 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); - } - - public boolean launchApp(ConnectionContext context, int appId, boolean enableHdr) throws IOException, XmlPullParserException { - // Using an FPS value over 60 causes SOPS to default to 720p60, - // so force it to 60 when starting. This won't impact our ability - // to get > 60 FPS while actually streaming though. - int fps = context.negotiatedFps > 60 ? 60 : context.negotiatedFps; + 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); + } + + public boolean launchApp(ConnectionContext context, int appId, boolean enableHdr) throws IOException, XmlPullParserException { + // Using an FPS value over 60 causes SOPS to default to 720p60, + // so force it to 60 when starting. This won't impact our ability + // to get > 60 FPS while actually streaming though. + int fps = context.negotiatedFps > 60 ? 60 : context.negotiatedFps; - // Using an unsupported resolution (not 720p, 1080p, or 4K) causes - // GFE to force SOPS to 720p60. This is fine for < 720p resolutions like - // 360p or 480p, but it is not ideal for 1440p and other resolutions. - // When we detect an unsupported resolution, disable SOPS unless it's under 720p. - // FIXME: Detect support resolutions using the serverinfo response, not a hardcoded list - boolean enableSops = context.streamConfig.getSops(); - if (context.negotiatedWidth * context.negotiatedHeight > 1280 * 720 && - context.negotiatedWidth * context.negotiatedHeight != 1920 * 1080 && - context.negotiatedWidth * context.negotiatedHeight != 3840 * 2160) { - LimeLog.info("Disabling SOPS due to non-standard resolution: "+context.negotiatedWidth+"x"+context.negotiatedHeight); - enableSops = false; - } + // Using an unsupported resolution (not 720p, 1080p, or 4K) causes + // GFE to force SOPS to 720p60. This is fine for < 720p resolutions like + // 360p or 480p, but it is not ideal for 1440p and other resolutions. + // When we detect an unsupported resolution, disable SOPS unless it's under 720p. + // FIXME: Detect support resolutions using the serverinfo response, not a hardcoded list + boolean enableSops = context.streamConfig.getSops(); + if (context.negotiatedWidth * context.negotiatedHeight > 1280 * 720 && + context.negotiatedWidth * context.negotiatedHeight != 1920 * 1080 && + context.negotiatedWidth * context.negotiatedHeight != 3840 * 2160) { + LimeLog.info("Disabling SOPS due to non-standard resolution: "+context.negotiatedWidth+"x"+context.negotiatedHeight); + enableSops = false; + } - String xmlStr = openHttpConnectionToString(baseUrlHttps + - "/launch?" + buildUniqueIdUuidString() + - "&appid=" + appId + - "&mode=" + context.negotiatedWidth + "x" + context.negotiatedHeight + "x" + fps + - "&additionalStates=1&sops=" + (enableSops ? 1 : 0) + - "&rikey="+bytesToHex(context.riKey.getEncoded()) + - "&rikeyid="+context.riKeyId + - (!enableHdr ? "" : "&hdrMode=1&clientHdrCapVersion=0&clientHdrCapSupportedFlagsInUint32=0&clientHdrCapMetaDataId=NV_STATIC_METADATA_TYPE_1&clientHdrCapDisplayData=0x0x0x0x0x0x0x0x0x0x0") + - "&localAudioPlayMode=" + (context.streamConfig.getPlayLocalAudio() ? 1 : 0) + - "&surroundAudioInfo=" + ((context.streamConfig.getAudioChannelMask() << 16) + context.streamConfig.getAudioChannelCount()) + - (context.streamConfig.getAttachedGamepadMask() != 0 ? "&remoteControllersBitmap=" + context.streamConfig.getAttachedGamepadMask() : "") + - (context.streamConfig.getAttachedGamepadMask() != 0 ? "&gcmap=" + context.streamConfig.getAttachedGamepadMask() : ""), - false); - String gameSession = getXmlString(xmlStr, "gamesession"); - return gameSession != null && !gameSession.equals("0"); - } - - public boolean resumeApp(ConnectionContext context) throws IOException, XmlPullParserException { - String xmlStr = openHttpConnectionToString(baseUrlHttps + "/resume?" + buildUniqueIdUuidString() + - "&rikey="+bytesToHex(context.riKey.getEncoded()) + - "&rikeyid="+context.riKeyId + - "&surroundAudioInfo=" + ((context.streamConfig.getAudioChannelMask() << 16) + context.streamConfig.getAudioChannelCount()), - false); - String resume = getXmlString(xmlStr, "resume"); - return Integer.parseInt(resume) != 0; - } - - public boolean quitApp() throws IOException, XmlPullParserException { - String xmlStr = openHttpConnectionToString(baseUrlHttps + "/cancel?" + buildUniqueIdUuidString(), false); - String cancel = getXmlString(xmlStr, "cancel"); - if (Integer.parseInt(cancel) == 0) { - return false; - } + String xmlStr = openHttpConnectionToString(baseUrlHttps + + "/launch?" + buildUniqueIdUuidString() + + "&appid=" + appId + + "&mode=" + context.negotiatedWidth + "x" + context.negotiatedHeight + "x" + fps + + "&additionalStates=1&sops=" + (enableSops ? 1 : 0) + + "&rikey="+bytesToHex(context.riKey.getEncoded()) + + "&rikeyid="+context.riKeyId + + (!enableHdr ? "" : "&hdrMode=1&clientHdrCapVersion=0&clientHdrCapSupportedFlagsInUint32=0&clientHdrCapMetaDataId=NV_STATIC_METADATA_TYPE_1&clientHdrCapDisplayData=0x0x0x0x0x0x0x0x0x0x0") + + "&localAudioPlayMode=" + (context.streamConfig.getPlayLocalAudio() ? 1 : 0) + + "&surroundAudioInfo=" + ((context.streamConfig.getAudioChannelMask() << 16) + context.streamConfig.getAudioChannelCount()) + + (context.streamConfig.getAttachedGamepadMask() != 0 ? "&remoteControllersBitmap=" + context.streamConfig.getAttachedGamepadMask() : "") + + (context.streamConfig.getAttachedGamepadMask() != 0 ? "&gcmap=" + context.streamConfig.getAttachedGamepadMask() : ""), + false); + String gameSession = getXmlString(xmlStr, "gamesession"); + return gameSession != null && !gameSession.equals("0"); + } + + public boolean resumeApp(ConnectionContext context) throws IOException, XmlPullParserException { + String xmlStr = openHttpConnectionToString(baseUrlHttps + "/resume?" + buildUniqueIdUuidString() + + "&rikey="+bytesToHex(context.riKey.getEncoded()) + + "&rikeyid="+context.riKeyId + + "&surroundAudioInfo=" + ((context.streamConfig.getAudioChannelMask() << 16) + context.streamConfig.getAudioChannelCount()), + false); + String resume = getXmlString(xmlStr, "resume"); + return Integer.parseInt(resume) != 0; + } + + public boolean quitApp() throws IOException, XmlPullParserException { + String xmlStr = openHttpConnectionToString(baseUrlHttps + "/cancel?" + buildUniqueIdUuidString(), false); + String cancel = getXmlString(xmlStr, "cancel"); + if (Integer.parseInt(cancel) == 0) { + return false; + } - // Newer GFE versions will just return success even if quitting fails - // if we're not the original requestor. - if (getCurrentGame(getServerInfo()) != 0) { - // Generate a synthetic GfeResponseException letting the caller know - // that they can't kill someone else's stream. - throw new GfeHttpResponseException(599, ""); - } + // Newer GFE versions will just return success even if quitting fails + // if we're not the original requestor. + if (getCurrentGame(getServerInfo()) != 0) { + // Generate a synthetic GfeResponseException letting the caller know + // that they can't kill someone else's stream. + throw new GfeHttpResponseException(599, ""); + } - return true; - } + return true; + } } diff --git a/app/src/main/java/com/limelight/nvstream/http/PairingManager.java b/app/src/main/java/com/limelight/nvstream/http/PairingManager.java index dd9aae3c..bad83814 100644 --- a/app/src/main/java/com/limelight/nvstream/http/PairingManager.java +++ b/app/src/main/java/com/limelight/nvstream/http/PairingManager.java @@ -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; + } + } + } } diff --git a/app/src/main/java/com/limelight/nvstream/input/ControllerPacket.java b/app/src/main/java/com/limelight/nvstream/input/ControllerPacket.java index 4257ffaa..155c6077 100644 --- a/app/src/main/java/com/limelight/nvstream/input/ControllerPacket.java +++ b/app/src/main/java/com/limelight/nvstream/input/ControllerPacket.java @@ -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; } \ No newline at end of file diff --git a/app/src/main/java/com/limelight/nvstream/input/KeyboardPacket.java b/app/src/main/java/com/limelight/nvstream/input/KeyboardPacket.java index 680b5bbb..3305e086 100644 --- a/app/src/main/java/com/limelight/nvstream/input/KeyboardPacket.java +++ b/app/src/main/java/com/limelight/nvstream/input/KeyboardPacket.java @@ -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; } diff --git a/app/src/main/java/com/limelight/nvstream/input/MouseButtonPacket.java b/app/src/main/java/com/limelight/nvstream/input/MouseButtonPacket.java index 6460690d..1d06e043 100644 --- a/app/src/main/java/com/limelight/nvstream/input/MouseButtonPacket.java +++ b/app/src/main/java/com/limelight/nvstream/input/MouseButtonPacket.java @@ -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; } diff --git a/app/src/main/java/com/limelight/nvstream/mdns/MdnsComputer.java b/app/src/main/java/com/limelight/nvstream/mdns/MdnsComputer.java index d52445d4..7b2afd61 100644 --- a/app/src/main/java/com/limelight/nvstream/mdns/MdnsComputer.java +++ b/app/src/main/java/com/limelight/nvstream/mdns/MdnsComputer.java @@ -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+"]"; + } } diff --git a/app/src/main/java/com/limelight/nvstream/mdns/MdnsDiscoveryAgent.java b/app/src/main/java/com/limelight/nvstream/mdns/MdnsDiscoveryAgent.java index 1b2736fc..e20b177c 100644 --- a/app/src/main/java/com/limelight/nvstream/mdns/MdnsDiscoveryAgent.java +++ b/app/src/main/java/com/limelight/nvstream/mdns/MdnsDiscoveryAgent.java @@ -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 computers = new HashMap(); - private HashSet pendingResolution = new HashSet(); - - // 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 listeners = new HashSet(); - private static ServiceListener nvstreamListener = new ServiceListener() { - @Override - public void serviceAdded(ServiceEvent event) { - HashSet 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(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 computers = new HashMap(); + private HashSet pendingResolution = new HashSet(); + + // 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 listeners = new HashSet(); + private static ServiceListener nvstreamListener = new ServiceListener() { + @Override + public void serviceAdded(ServiceEvent event) { + HashSet 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(listeners); + } + + for (ServiceListener listener : localListeners) { + listener.serviceAdded(event); + } + } - @Override - public void serviceRemoved(ServiceEvent event) { - HashSet 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(listeners); - } - - for (ServiceListener listener : localListeners) { - listener.serviceRemoved(event); - } - } + @Override + public void serviceRemoved(ServiceEvent event) { + HashSet 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(listeners); + } + + for (ServiceListener listener : localListeners) { + listener.serviceRemoved(event); + } + } - @Override - public void serviceResolved(ServiceEvent event) { - HashSet 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(listeners); - } - - for (ServiceListener listener : localListeners) { - listener.serviceResolved(event); - } - } - }; + @Override + public void serviceResolved(ServiceEvent event) { + HashSet 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(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 pendingNames; - synchronized (pendingResolution) { - pendingNames = new ArrayList(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 getComputerSet() { - synchronized (computers) { - return new ArrayList(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 pendingNames; + synchronized (pendingResolution) { + pendingNames = new ArrayList(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 getComputerSet() { + synchronized (computers) { + return new ArrayList(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 + } } diff --git a/app/src/main/java/com/limelight/nvstream/mdns/MdnsDiscoveryListener.java b/app/src/main/java/com/limelight/nvstream/mdns/MdnsDiscoveryListener.java index 5d856e7b..3f69a120 100644 --- a/app/src/main/java/com/limelight/nvstream/mdns/MdnsDiscoveryListener.java +++ b/app/src/main/java/com/limelight/nvstream/mdns/MdnsDiscoveryListener.java @@ -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); } diff --git a/app/src/main/java/com/limelight/nvstream/wol/WakeOnLanSender.java b/app/src/main/java/com/limelight/nvstream/wol/WakeOnLanSender.java index 7af9dd7d..7f2f0843 100644 --- a/app/src/main/java/com/limelight/nvstream/wol/WakeOnLanSender.java +++ b/app/src/main/java/com/limelight/nvstream/wol/WakeOnLanSender.java @@ -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; + } } diff --git a/app/src/main/java/com/limelight/utils/Vector2d.java b/app/src/main/java/com/limelight/utils/Vector2d.java index d01b78ba..a8178a2b 100644 --- a/app/src/main/java/com/limelight/utils/Vector2d.java +++ b/app/src/main/java/com/limelight/utils/Vector2d.java @@ -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; + } } diff --git a/app/src/nonRoot/java/com.limelight/LimelightBuildProps.java b/app/src/nonRoot/java/com.limelight/LimelightBuildProps.java index c3934275..fec35c5c 100644 --- a/app/src/nonRoot/java/com.limelight/LimelightBuildProps.java +++ b/app/src/nonRoot/java/com.limelight/LimelightBuildProps.java @@ -1,5 +1,5 @@ package com.limelight; public class LimelightBuildProps { - public static final boolean ROOT_BUILD = false; + public static final boolean ROOT_BUILD = false; } diff --git a/app/src/root/java/com.limelight/LimelightBuildProps.java b/app/src/root/java/com.limelight/LimelightBuildProps.java index dc313dbc..e5c792df 100644 --- a/app/src/root/java/com.limelight/LimelightBuildProps.java +++ b/app/src/root/java/com.limelight/LimelightBuildProps.java @@ -1,5 +1,5 @@ package com.limelight; public class LimelightBuildProps { - public static final boolean ROOT_BUILD = true; + public static final boolean ROOT_BUILD = true; }