Compare commits

..

134 Commits

Author SHA1 Message Date
Cameron Gutman 92f24d20db Bump version to 3.1.13 2015-11-11 17:28:42 -08:00
Cameron Gutman 0dd43df7aa Update common for GFE 2.8 support 2015-11-11 17:28:32 -08:00
Cameron Gutman 1675586a29 Add uses-feature for USB host 2015-11-11 17:28:10 -08:00
Cameron Gutman a1e511b19a Remove ACCESS_SUPERUSER permission since it's deprecated in SuperSU 2015-11-11 17:27:55 -08:00
Cameron Gutman e89e803d54 Zero controller values before removing a controller 2015-11-08 19:05:22 -08:00
Cameron Gutman 4486a126ad Fix some listener bugs in the XB1 driver 2015-11-08 19:03:12 -08:00
Cameron Gutman d740e7a521 Add an Xbox One controller driver developed based on the xpad driver in the Linux kernel 2015-11-08 16:12:18 -08:00
Cameron Gutman fe3b649fe9 Bump version to 3.1.12 2015-10-31 17:07:55 -07:00
Cameron Gutman 7223efb9f8 Update common to fix video corruption bugs 2015-10-31 16:45:40 -07:00
Cameron Gutman c3296cce3d Use setFixedSize if aspect ratios are compatible. This seems necessary for 4K video. 2015-10-31 16:25:15 -07:00
Cameron Gutman 5ef20aba21 Decrease polling period and increase polls before declaring the machine offline. Try requesting the app list again every 2 seconds if the app list has not been received yet. 2015-10-28 01:36:35 -07:00
Cameron Gutman 54eaee3f79 Use a lock to prevent serverinfo polling on a machine while applist is pending 2015-10-28 01:15:09 -07:00
Cameron Gutman 4c82da1f5c Update common with image quality improvements 2015-10-28 00:42:24 -07:00
Cameron Gutman 080dc01c21 Use a reference resolution rather than the actual stream resolution when scaling mouse movement 2015-10-28 00:24:26 -07:00
Cameron Gutman f09fbf4ba6 Fix incorrect usage of SeqParameterSet.read() by feeding it possibly escaped Annex B NALUs 2015-10-28 00:24:16 -07:00
Cameron Gutman ad10413714 Update decoder code 2015-10-19 22:37:46 -07:00
Cameron Gutman c9014da186 Transition to Opus Multistream Decoder API 2015-10-17 17:16:58 -07:00
Cameron Gutman c025f9f02b Reduce code duplication 2015-10-17 15:44:24 -07:00
Cameron Gutman b737acedb0 Bump version to 3.1.11 2015-10-15 02:00:29 -07:00
Cameron Gutman f15bfe3038 Add support for mouse drag using long press 2015-10-15 01:50:05 -07:00
Cameron Gutman 8938f51292 Fix weird stair-stepping upward mouse movement on devices with a low scaling factor caused by rounding error (Nexus 9) 2015-10-15 01:48:31 -07:00
Cameron Gutman 4b92b8f714 Fix bug allowing computer polling to continue when the stream is resumed from the PcView activity 2015-10-15 00:55:05 -07:00
Cameron Gutman 5f13b9bca4 Don't set constraints 4 & 5 when using baseline profile hack 2015-10-13 19:29:36 -07:00
Cameron Gutman 2f219aac6f Only apply the constrained high profile SPS modification to Intel devices to avoid crashing other devices 2015-10-12 20:54:50 -07:00
Cameron Gutman 1d9efb30e2 Update to version 3.1.10 2015-10-11 17:11:48 -07:00
Cameron Gutman ed7be00881 Update IML file 2015-10-11 17:11:08 -07:00
Cameron Gutman a6003f6bff Remove MediaTek decoders from the decoders that need bitstream restrictions. The correct fix was to lower level_idc to reduce the required buffering. On newer MediaTek chipsets, sending bitstream restrictions actually slows down decoding by a factor of 3. 2015-10-11 16:57:37 -07:00
Cameron Gutman 4619045375 Revert "Update common to increase SSL handshake timeout"
This reverts commit 57b0da1a3a.
2015-10-11 14:54:45 -07:00
Cameron Gutman e61b8f1b34 Try a TCP connection before trying HTTPS to quickly eliminate transport layer connectivity issues 2015-10-11 14:39:02 -07:00
Cameron Gutman 79b6ec839a Fix machines becoming unreachable after they report IP addresses that they can't be contacted with 2015-10-11 14:16:38 -07:00
Cameron Gutman fd12e30c53 Set constraint flags corresponding to Constrained High Profile on KitKat and higher. Fixes Nexus Player high latency on Android 6.0. 2015-10-10 23:28:48 -07:00
Cameron Gutman 87a9ca4318 Make touchscreen and stylus support more robust (supporting Bluetooth stylus in 6.0 and hopefully fixing broken touchscreen input on some devices) 2015-10-10 19:17:19 -07:00
Cameron Gutman 3f64411174 Only reload the PcView activity if UI settings were changed 2015-10-10 18:53:10 -07:00
Cameron Gutman 57b0da1a3a Update common to increase SSL handshake timeout 2015-10-10 18:15:01 -07:00
Cameron Gutman 7d3e74a67f Allow the offline context menu to be opened when the PC state is unknown 2015-10-10 18:10:57 -07:00
Cameron Gutman d704e322df Use RGB_565 for box art to reduce image size in memory 2015-10-10 18:09:51 -07:00
Cameron Gutman f598153818 Small improvements to Media Codec DR 2015-10-10 15:17:24 -07:00
Cameron Gutman f395a0c170 Fix warning 2015-10-10 15:04:49 -07:00
Cameron Gutman 654b33d27f Update build tools version to 23.0.1 2015-10-10 15:04:16 -07:00
Cameron Gutman 6c12da96c9 Add patched Jcodec library built from master a5d138efec2e940897e7e3d91a63a1f58abedd95 with changes from https://github.com/jcodec/jcodec/pull/90 2015-10-10 15:03:47 -07:00
Cameron Gutman 1a6f639b81 Fix discovery issues when adding a PC 2015-10-10 14:43:29 -07:00
Cameron Gutman 59a00a38c9 Limit box art assets to 5 MB each to prevent OOM crashes 2015-10-10 14:43:17 -07:00
Cameron Gutman 2beee168e3 Update README.md with additional download links 2015-08-30 12:37:23 -07:00
Cameron Gutman a92bbc7e5a Increment version to 3.1.9 2015-08-18 00:44:01 -07:00
Cameron Gutman fbc921dd07 Update build files 2015-08-18 00:39:01 -07:00
Cameron Gutman 59c6c3d777 Target Android 6.0 SDK 2015-08-18 00:38:49 -07:00
Cameron Gutman e7ab61c8d0 Use jmDNS 3.4.2 which works properly on Android 6.0 2015-08-17 23:14:20 -07:00
Cameron Gutman 7023760782 Use monotonic system time for rendering timestamps. This is required now in Android 6.0 since these timestamps are propagated to the codec. 2015-08-17 18:40:25 -07:00
Cameron Gutman 932ce435b5 Remove unnecessary buffer clear 2015-08-17 18:26:33 -07:00
Cameron Gutman af384d88f7 Handle potentially poisoned bitmap cache caused by truncated images from the server 2015-08-17 17:59:15 -07:00
Cameron Gutman 792846ddad Update MediaCodec renderer to avoid deprecated features on Lollipop 2015-08-17 17:52:57 -07:00
Cameron Gutman 1187d9c78c Update libraries 2015-08-14 09:29:56 -07:00
Cameron Gutman 37db9ab072 Update common with latest fixes 2015-08-12 00:39:07 -07:00
Cameron Gutman fb40060560 Upgrade common jar to incorporate IDR frame fix 2015-08-04 23:46:20 -07:00
Cameron Gutman a4f4887647 Upgrade build tools and libraries 2015-08-04 23:46:03 -07:00
Cameron Gutman f1d7f556fd Bump to version 3.1.8 2015-07-21 18:03:37 -07:00
Cameron Gutman 1e70e1d329 GFE 2.5.11 update to fix black screen on Fire TV Stick 2015-07-18 17:06:41 -07:00
Cameron Gutman e02a009635 Add support for the Razer Serval controller. The start and select buttons are manually handled for devices without a mapping for them. The back button is ignored so it can be used to exit the stream. 2015-07-18 00:46:25 -07:00
Cameron Gutman bd6ff35603 Update to 3.1.7 2015-06-15 10:37:58 -07:00
Cameron Gutman 1cb7727dc7 Update common 2015-06-15 10:28:31 -07:00
Cameron Gutman 0c73e3d0ae Only propagate a decoder exception if it happens at the beginning of a stream 2015-06-15 10:28:09 -07:00
Cameron Gutman 6371d364e1 Lint warning cleanup 2015-05-29 23:22:40 -05:00
Cameron Gutman ded9c9140d Handle being online but not having a known reachability 2015-05-29 23:20:04 -05:00
Cameron Gutman 7c8a108e28 Use the leanback feature on API 21+ devices 2015-05-29 23:18:56 -05:00
Cameron Gutman 2a18ffcdba Update to Gradle 1.2.3 2015-05-19 10:10:18 -05:00
Cameron Gutman 381d0d5e81 Add support for multi-window functionality on Samsung devices 2015-05-10 00:02:04 -05:00
Cameron Gutman be126acfd1 Update version info to 3.1.6 2015-05-05 20:52:53 -04:00
Cameron Gutman fc2f5cfe4d Manually pass through Samsung capacitive buttons 2015-05-05 20:20:37 -04:00
Cameron Gutman 9878902a89 Use IDs to track controllers instead of descriptors. Fixes #64 2015-05-05 20:08:58 -04:00
Cameron Gutman f1230d46f3 Android Studio 1.2 and Grade 1.2.2 update 2015-05-05 20:02:53 -04:00
Michelle Bergeron d8822392f1 Link to site 2015-05-03 23:36:19 -07:00
Cameron Gutman 1d9cf71517 Total Eclipse of the Lime 2015-04-21 21:50:40 -04:00
Cameron Gutman 2160e87fef Fix division by zero in ARC 2015-03-31 20:29:22 -04:00
Cameron Gutman 88249ba8aa Enable direct submission for ARC 2015-03-31 19:59:16 -04:00
Cameron Gutman 2856617fb3 Only release controller numbers if they were reserved 2015-03-31 19:58:47 -04:00
Cameron Gutman d822980d5a Fix missing close of Closeables caught by StrictMode 2015-03-29 23:25:00 -04:00
Cameron Gutman b5ba59b413 Fix database reference leak 2015-03-29 23:06:32 -04:00
Cameron Gutman 1148e0163c Only assign a controller number when a valid controller input has been received. Fixes misdetection of other input devices as controllers (issue #65). 2015-03-29 22:54:48 -04:00
Cameron Gutman cf36c7adb1 Increment version 2015-03-25 02:33:46 -04:00
Cameron Gutman eac6998e17 Update the latency message strings to be more clear that this isn't end to end latency 2015-03-25 01:20:55 -04:00
Cameron Gutman 17afbffdb5 Include the time it takes to get an input buffer in the frame latency calculation 2015-03-25 01:08:23 -04:00
Cameron Gutman 072a439c2d Update common and decode unit API 2015-03-25 00:32:22 -04:00
Cameron Gutman c533600983 Update for 3.1.3 release 2015-03-23 17:26:37 -04:00
Cameron Gutman 5847fbb6b6 Add TI decoders to the direct submit whitelist 2015-03-23 17:14:02 -04:00
Cameron Gutman 1876b30c1b Forgot this file 2015-03-23 16:51:57 -04:00
Cameron Gutman 5c71f55993 Add another Exynos prefix 2015-03-23 16:51:32 -04:00
Cameron Gutman 9c0960d03d Add options to quit and resume streaming from the PC view 2015-03-23 16:36:43 -04:00
Cameron Gutman 29a395f3f4 Prevent updating the UI while quitting is in progress 2015-03-23 15:57:29 -04:00
Cameron Gutman a676b8d8e6 Restore the legacy path and only use direct submit for certain whitelisted decoders 2015-03-23 15:51:11 -04:00
Cameron Gutman 7ab0be3b62 Optimize app grid performance on lower end devices 2015-03-23 15:12:25 -04:00
Cameron Gutman 115853fed2 Update version to 3.1.3-beta1 2015-03-16 21:29:07 -04:00
Cameron Gutman 60beb81ae4 Target API 22 2015-03-16 21:28:49 -04:00
Cameron Gutman 5310375d42 Target Android 5.1 2015-03-16 21:28:33 -04:00
Cameron Gutman 7ce29e3a09 Add a workaround for the Nexus 9 dropping frames with the new renderer 2015-03-16 21:26:02 -04:00
Cameron Gutman 42c65f4f16 Use smaller deadzones for SHIELD controllers 2015-03-16 19:36:09 -04:00
Cameron Gutman bf2cc2a4d5 Don't assign controller numbers to devices that don't have an analog stick 2015-03-16 19:35:43 -04:00
Cameron Gutman 6d6d7121f6 Remove the Playpad Pro hack that worked around an issue with old firmware and caused the D-pad to be unresponsive on updated firmware. Fixes #41 2015-03-15 14:30:56 -07:00
Cameron Gutman 2ab67380d6 Use direct submit decoding for MediaCodec. Based on my profiling of a few devices, dequeueInputBuffer and queueInputBuffer don't take much time anyway. It allows us to stop our semi-busy looping which saves power. The depacketizer can avoid expensive synchronization and additional context switching which costs time and CPU cycles. 2015-03-09 01:49:52 -05:00
Cameron Gutman 899387caa1 Use a separate executor for network loads to avoid stalling cached loads. Optimize background cache fill loads. 2015-03-02 18:34:21 -05:00
Cameron Gutman 56c8a9e6fe Use the regular serverinfo query to update the running status of apps 2015-03-02 17:05:45 -05:00
Cameron Gutman 896288a40b Use AsyncTasks and attached Drawables to track background image loading 2015-03-02 17:03:08 -05:00
Cameron Gutman fc8ce5e4b9 Quiet down disk cache misses 2015-03-02 16:13:54 -05:00
Cameron Gutman 4affc3c4ce Update to 3.1.2 release 2015-02-27 18:12:49 -05:00
Cameron Gutman 067be54715 Show the discovery in progress view if no computers remain after one is deleted 2015-02-27 18:05:02 -05:00
Cameron Gutman 0dad2dc64b Only close the app list activity if the PC is offline not unknown 2015-02-27 15:15:01 -05:00
Cameron Gutman 867b703644 Evict cached bitmaps when closing the app list 2015-02-27 15:13:43 -05:00
Cameron Gutman 3d398ef6dd Update common 2015-02-27 14:22:35 -05:00
Cameron Gutman 85d95b2d8e Revert "Immediately show the PC as offline if the first poll fails"
This reverts commit 7b12fd1ad2.
2015-02-27 13:52:17 -05:00
Cameron Gutman d091d9db6b Start apps by ID to work correctly with duplicate app names 2015-02-27 13:42:40 -05:00
Cameron Gutman e081ab5239 Code cleanup and Lint suggestions 2015-02-27 01:43:24 -05:00
Cameron Gutman 7b12fd1ad2 Immediately show the PC as offline if the first poll fails 2015-02-27 01:33:33 -05:00
Cameron Gutman 80d8c5953e Rewrite the app art caching and fetching (again!) to finally address OOM problems and speed up art loading 2015-02-27 01:16:06 -05:00
Cameron Gutman 194037ff41 Clear the bitmap cache since it can get pretty large 2015-02-26 22:04:39 -05:00
Cameron Gutman 094d642739 Stop scaling bitmaps down 2015-02-26 22:04:22 -05:00
Cameron Gutman 010e03252e Encapsulate the cache IO streams in buffered streams 2015-02-26 21:39:26 -05:00
Cameron Gutman 98638186b5 Use weak references to allow the image views to be garbage collected while a load is in progress 2015-02-26 21:05:33 -05:00
Cameron Gutman c5293ef21f Reduce the size of the LRU cache by 2 2015-02-26 21:04:40 -05:00
Cameron Gutman 366a1c91b8 Always close the output stream 2015-02-26 21:04:17 -05:00
Cameron Gutman 157450e674 Update common with stricter applist parser 2015-02-26 18:33:18 -05:00
Cameron Gutman 1b8d2bc81c Cancel asset fetching when the app view is paused 2015-02-26 18:30:02 -05:00
Cameron Gutman f1787c43e5 Generalize the polling grace period to all users of CMS 2015-02-26 18:27:50 -05:00
Cameron Gutman 95ea88e932 Only replace the MAC address if the existing one is non-null 2015-02-26 15:11:46 -05:00
Cameron Gutman f2b8461bb9 Increment version to beta 3.1.2 2015-02-25 23:15:10 -05:00
Cameron Gutman 7838a787df Fix a bug in app removal 2015-02-25 22:27:38 -05:00
Cameron Gutman cc3f2ecb07 Always close the cache output stream if an exception occurs 2015-02-25 22:15:41 -05:00
Cameron Gutman 833b7c3916 Fetch app assets in the background while in the app view 2015-02-25 21:57:54 -05:00
Cameron Gutman 90209f2ca2 Update iml files generated by Android Studio 1.1 2015-02-25 21:07:59 -05:00
Cameron Gutman 2681036c32 Update common 2015-02-25 21:07:44 -05:00
Cameron Gutman ee58071ff1 Fix huge performance issues when dealing with large app lists 2015-02-25 21:07:35 -05:00
Cameron Gutman e222f2f6c3 Fix fast polling 2015-02-22 18:34:28 -05:00
Cameron Gutman 0b7becb161 Remove unused function 2015-02-22 18:10:08 -05:00
Cameron Gutman bf795ab7a5 Fix removal of apps in app list updates 2015-02-22 18:04:42 -05:00
Cameron Gutman 59df38ae8a Cancel app icon requests when the view is recycled 2015-02-22 17:49:52 -05:00
Cameron Gutman e04ff048b8 Implement a fast polling method to speed up polling. Save the old MAC address if it's empty. 2015-02-11 17:04:31 -05:00
53 changed files with 2562 additions and 910 deletions
+9 -11
View File
@@ -1,14 +1,14 @@
#Limelight
#Moonlight
Limelight is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
[Moonlight](http://moonlight-stream.com) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
We reverse engineered the Shield streaming software and created a version that can be run on any Android device.
Limelight will allow you to stream your full collection of games from your Windows PC to your Android device,
Moonlight will allow you to stream your full collection of games from your Windows PC to your Android device,
whether in your own home or over the internet.
[Limelight-pc](https://github.com/limelight-stream/limelight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/limelight-stream/limelight-ios) and [Windows and Windows Phone](https://github.com/limelight-stream/limelight-windows) are also in development.
[Moonlight-pc](https://github.com/moonlight-stream/moonlight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/moonlight-stream/moonlight-ios) and [Windows and Windows Phone](https://github.com/moonlight-stream/moonlight-windows) are also in development.
Check our [wiki](https://github.com/limelight-stream/limelight-android/wiki) for more detailed information or a troubleshooting guide.
Check our [wiki](https://github.com/moonlight-stream/moonlight-android/wiki) for more detailed information or a troubleshooting guide.
##Features
@@ -18,8 +18,8 @@ Check our [wiki](https://github.com/limelight-stream/limelight-android/wiki) for
##Installation
* Download and install Limelight for Android from
[Google Play](https://play.google.com/store/apps/details?id=com.limelight)
* Download and install Moonlight for Android from
[Google Play](https://play.google.com/store/apps/details?id=com.limelight), [Amazon App Store](http://www.amazon.com/gp/product/B00JK4MFN2), or directly from the [releases page](https://github.com/moonlight-stream/moonlight-android/releases)
* Download [GeForce Experience](http://www.geforce.com/geforce-experience) and install on your Windows PC
##Requirements
@@ -33,7 +33,7 @@ Check our [wiki](https://github.com/limelight-stream/limelight-android/wiki) for
* Turn on GameStream in the GFE settings
* If you are connecting from outside the same network, turn on internet
streaming
* When on the same network as your PC, open Limelight and tap on your PC in the list
* When on the same network as your PC, open Moonlight and tap on your PC in the list
* Accept the pairing confirmation on your PC
* Tap your PC again to view the list of apps to stream
* Play games!
@@ -46,8 +46,6 @@ This project is being actively developed at [XDA Developers](http://forum.xda-de
2. Write code
3. Send Pull Requests
Check out our [website](http://limelight-stream.com) for project links and information.
##Authors
* [Cameron Gutman](https://github.com/cgutman)
@@ -55,5 +53,5 @@ Check out our [website](http://limelight-stream.com) for project links and infor
* [Aaron Neyer](https://github.com/Aaronneyer)
* [Andrew Hennessy](https://github.com/yetanothername)
Limelight is the work of students at [Case Western](http://case.edu) and was
Moonlight is the work of students at [Case Western](http://case.edu) and was
started as a project at [MHacks](http://mhacks.org).
+25 -25
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" type="JAVA_MODULE" version="4">
<module external.linked.project.id=":app" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" external.system.module.group="moonlight-android" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="android-gradle" name="Android-Gradle">
<configuration>
@@ -9,11 +9,15 @@
<facet type="android" name="Android">
<configuration>
<option name="SELECTED_BUILD_VARIANT" value="nonRootDebug" />
<option name="SELECTED_TEST_ARTIFACT" value="_android_test_" />
<option name="ASSEMBLE_TASK_NAME" value="assembleNonRootDebug" />
<option name="COMPILE_JAVA_TASK_NAME" value="compileNonRootDebugSources" />
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleNonRootDebugTest" />
<option name="SOURCE_GEN_TASK_NAME" value="generateNonRootDebugSources" />
<option name="TEST_SOURCE_GEN_TASK_NAME" value="generateNonRootDebugTestSources" />
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleNonRootDebugAndroidTest" />
<option name="COMPILE_JAVA_TEST_TASK_NAME" value="compileNonRootDebugAndroidTestSources" />
<afterSyncTasks>
<task>generateNonRootDebugAndroidTestSources</task>
<task>generateNonRootDebugSources</task>
</afterSyncTasks>
<option name="ALLOW_USER_CONFIGURATION" value="false" />
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
@@ -22,8 +26,9 @@
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" inherit-compiler-output="false">
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7" inherit-compiler-output="false">
<output url="file://$MODULE_DIR$/build/intermediates/classes/nonRoot/debug" />
<output-test url="file://$MODULE_DIR$/build/intermediates/classes/androidTest/nonRoot/debug" />
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/nonRoot/debug" isTestSource="false" generated="true" />
@@ -31,7 +36,7 @@
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/nonRoot/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/nonRoot/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/nonRoot/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/nonRoot/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/nonRoot/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/assets" type="java-resource" />
@@ -39,12 +44,12 @@
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/jni" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/test/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/test/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/test/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/test/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/test/nonRoot/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/test/nonRoot/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/androidTest/nonRoot/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/androidTest/nonRoot/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/assets" type="java-resource" />
@@ -101,20 +106,15 @@
<excludeFolder url="file://$MODULE_DIR$/build/outputs" />
<excludeFolder url="file://$MODULE_DIR$/build/tmp" />
</content>
<orderEntry type="jdk" jdkName="Android API 21 Platform" jdkType="Android SDK" />
<orderEntry type="jdk" jdkName="Android API 23 Platform" jdkType="Android SDK" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" exported="" name="bcprov-jdk15on-1.51" level="project" />
<orderEntry type="library" exported="" name="androidasync-2.0.5" level="project" />
<orderEntry type="library" exported="" name="gson-2.3.1" level="project" />
<orderEntry type="library" exported="" name="support-v4-r7" level="project" />
<orderEntry type="library" exported="" name="jmdns-fixed" level="project" />
<orderEntry type="library" exported="" name="bcpkix-jdk15on-1.51" level="project" />
<orderEntry type="library" exported="" name="bcprov-jdk15on-1.52" level="project" />
<orderEntry type="library" exported="" name="bcpkix-jdk15on-1.52" level="project" />
<orderEntry type="library" exported="" name="tinyrtsp" level="project" />
<orderEntry type="library" exported="" name="limelight-common" level="project" />
<orderEntry type="library" exported="" name="ion-2.0.5" level="project" />
<orderEntry type="library" exported="" name="okhttp-2.2.0" level="project" />
<orderEntry type="library" exported="" name="jcodec-0.1.9" level="project" />
<orderEntry type="library" exported="" name="okio-1.2.0" level="project" />
<orderEntry type="library" exported="" name="jmdns-3.4.2" level="project" />
<orderEntry type="library" exported="" name="okhttp-2.4.0" level="project" />
<orderEntry type="library" exported="" name="jcodec-0.1.9-patched" level="project" />
<orderEntry type="library" exported="" name="okio-1.5.0" level="project" />
</component>
</module>
</module>
+11 -16
View File
@@ -4,15 +4,15 @@ import org.apache.tools.ant.taskdefs.condition.Os
apply plugin: 'com.android.application'
android {
compileSdkVersion 21
buildToolsVersion "21.1.2"
compileSdkVersion 23
buildToolsVersion "23.0.2"
defaultConfig {
minSdkVersion 16
targetSdkVersion 21
targetSdkVersion 23
versionName "3.1.1"
versionCode = 54
versionName "3.1.13"
versionCode = 72
}
productFlavors {
@@ -62,19 +62,14 @@ android {
}
dependencies {
compile group: 'org.jcodec', name: 'jcodec', version: '0.1.9'
compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.52'
compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.52'
compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.51'
compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.51'
compile group: 'com.squareup.okhttp', name: 'okhttp', version:'2.4.0'
compile group: 'com.squareup.okio', name:'okio', version:'1.5.0'
compile group: 'com.google.android', name: 'support-v4', version:'r7'
compile group: 'com.koushikdutta.ion', name: 'ion', version:'2.0.5'
compile group: 'com.google.code.gson', name: 'gson', version:'2.3.1'
compile group: 'com.squareup.okhttp', name: 'okhttp', version:'2.2.0'
compile group: 'com.squareup.okio', name:'okio', version:'1.2.0'
compile files('libs/jmdns-fixed.jar')
compile files('libs/jmdns-3.4.2.jar')
compile files('libs/limelight-common.jar')
compile files('libs/tinyrtsp.jar')
compile files('libs/jcodec-0.1.9-patched.jar')
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+9
View File
@@ -11,11 +11,16 @@
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
<uses-feature android:name="android.hardware.wifi" android:required="false" />
<uses-feature android:name="android.hardware.gamepad" android:required="false" />
<uses-feature android:name="android.hardware.usb.host" android:required="false" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:theme="@style/AppTheme" >
<!-- Samsung multi-window support -->
<uses-library android:name="com.sec.android.app.multiwindow" android:required="false" />
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
<!-- Launcher for traditional devices -->
<activity
@@ -24,6 +29,7 @@
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
<category android:name="tv.ouya.intent.category.APP" />
</intent-filter>
</activity>
@@ -76,6 +82,9 @@
<service
android:name=".computers.ComputerManagerService"
android:label="Computer Management Service" />
<service
android:name=".binding.input.driver.UsbDriverService"
android:label="Usb Driver Service" />
</application>
</manifest>
+121 -119
View File
@@ -1,14 +1,10 @@
package com.limelight;
import java.io.FileNotFoundException;
import java.io.StringReader;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import com.limelight.binding.PlatformBinding;
import com.limelight.computers.ComputerManagerListener;
import com.limelight.computers.ComputerManagerService;
import com.limelight.grid.AppGridAdapter;
@@ -20,14 +16,13 @@ import com.limelight.ui.AdapterFragment;
import com.limelight.ui.AdapterFragmentCallbacks;
import com.limelight.utils.CacheHelper;
import com.limelight.utils.Dialog;
import com.limelight.utils.ServerHelper;
import com.limelight.utils.SpinnerDialog;
import com.limelight.utils.UiHelper;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Service;
import android.content.ComponentName;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.res.Configuration;
@@ -53,9 +48,8 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
private ComputerManagerService.ApplistPoller poller;
private SpinnerDialog blockingLoadSpinner;
private String lastRawApplist;
private int consecutiveAppListFailures = 0;
private final static int CONSECUTIVE_FAILURE_LIMIT = 3;
private int lastRunningAppId;
private boolean suspendGridUpdates;
private final static int START_OR_RESUME_ID = 1;
private final static int QUIT_ID = 2;
@@ -113,11 +107,6 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
}
};
private InetAddress getAddress() {
return computer.reachability == ComputerDetails.Reachability.LOCAL ?
computer.localIp : computer.remoteIp;
}
private void startComputerUpdates() {
if (managerBinder == null) {
return;
@@ -126,38 +115,47 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
managerBinder.startPolling(new ComputerManagerListener() {
@Override
public void notifyComputerUpdated(ComputerDetails details) {
// Do nothing if updates are suspended
if (suspendGridUpdates) {
return;
}
// Don't care about other computers
if (!details.uuid.toString().equalsIgnoreCase(uuidString)) {
return;
}
if (details.state != ComputerDetails.State.ONLINE) {
consecutiveAppListFailures++;
if (details.state == ComputerDetails.State.OFFLINE) {
// The PC is unreachable now
AppView.this.runOnUiThread(new Runnable() {
@Override
public void run() {
// Display a toast to the user and quit the activity
Toast.makeText(AppView.this, getResources().getText(R.string.lost_connection), Toast.LENGTH_SHORT).show();
finish();
}
});
if (consecutiveAppListFailures >= CONSECUTIVE_FAILURE_LIMIT) {
// The PC is unreachable now
AppView.this.runOnUiThread(new Runnable() {
@Override
public void run() {
// Display a toast to the user and quit the activity
Toast.makeText(AppView.this, getResources().getText(R.string.lost_connection), Toast.LENGTH_SHORT).show();
finish();
}
});
return;
}
// App list is the same or empty
if (details.rawAppList == null || details.rawAppList.equals(lastRawApplist)) {
// Let's check if the running app ID changed
if (details.runningGameId != lastRunningAppId) {
// Update the currently running game using the app ID
lastRunningAppId = details.runningGameId;
updateUiWithServerinfo(details);
}
return;
}
consecutiveAppListFailures = 0;
// App list is the same or empty; nothing to do
if (details.rawAppList == null || details.rawAppList.equals(lastRawApplist)) {
return;
}
lastRunningAppId = details.runningGameId;
lastRawApplist = details.rawAppList;
try {
lastRawApplist = details.rawAppList;
updateUiWithAppList(NvHTTP.getAppListByReader(new StringReader(details.rawAppList)));
if (blockingLoadSpinner != null) {
@@ -182,6 +180,10 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
if (managerBinder != null) {
managerBinder.stopPolling();
}
if (appGridAdapter != null) {
appGridAdapter.cancelQueuedOperations();
}
}
@Override
@@ -295,33 +297,6 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
public void onContextMenuClosed(Menu menu) {
}
private void displayQuitConfirmationDialog(final Runnable onYes, final Runnable onNo) {
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which){
case DialogInterface.BUTTON_POSITIVE:
if (onYes != null) {
onYes.run();
}
break;
case DialogInterface.BUTTON_NEGATIVE:
if (onNo != null) {
onNo.run();
}
break;
}
}
};
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage(getResources().getString(R.string.applist_quit_confirmation))
.setPositiveButton(getResources().getString(R.string.yes), dialogClickListener)
.setNegativeButton(getResources().getString(R.string.no), dialogClickListener)
.show();
}
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
@@ -329,25 +304,37 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
switch (item.getItemId()) {
case START_WTIH_QUIT:
// Display a confirmation dialog first
displayQuitConfirmationDialog(new Runnable() {
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
@Override
public void run() {
doStart(app.app);
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
}
}, null);
return true;
case START_OR_RESUME_ID:
// Resume is the same as start for us
doStart(app.app);
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
return true;
case QUIT_ID:
// Display a confirmation dialog first
displayQuitConfirmationDialog(new Runnable() {
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
@Override
public void run() {
doQuit(app.app);
suspendGridUpdates = true;
ServerHelper.doQuit(AppView.this,
ServerHelper.getCurrentAddressFromComputer(computer),
app.app, managerBinder, new Runnable() {
@Override
public void run() {
// Trigger a poll immediately
suspendGridUpdates = false;
if (poller != null) {
poller.pollNow();
}
}
});
}
}, null);
return true;
@@ -360,12 +347,51 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
}
}
private void updateUiWithServerinfo(final ComputerDetails details) {
AppView.this.runOnUiThread(new Runnable() {
@Override
public void run() {
boolean updated = false;
// Look through our current app list to tag the running app
for (int i = 0; i < appGridAdapter.getCount(); i++) {
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
// There can only be one or zero apps running.
if (existingApp.app.getIsRunning() &&
existingApp.app.getAppId() == details.runningGameId) {
// This app was running and still is, so we're done now
return;
}
else if (existingApp.app.getAppId() == details.runningGameId) {
// This app wasn't running but now is
existingApp.app.setIsRunning(true);
updated = true;
}
else if (existingApp.app.getIsRunning()) {
// This app was running but now isn't
existingApp.app.setIsRunning(false);
updated = true;
}
else {
// This app wasn't running and still isn't
}
}
if (updated) {
appGridAdapter.notifyDataSetChanged();
}
}
});
}
private void updateUiWithAppList(final List<NvApp> appList) {
AppView.this.runOnUiThread(new Runnable() {
@Override
public void run() {
boolean updated = false;
// First handle app updates and additions
for (NvApp app : appList) {
boolean foundExistingApp = false;
@@ -375,7 +401,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
if (existingApp.app.getAppId() == app.getAppId()) {
// Found the app; update its properties
if (existingApp.app.getIsRunning() != app.getIsRunning()) {
existingApp.app.setIsRunningBoolean(app.getIsRunning());
existingApp.app.setIsRunning(app.getIsRunning());
updated = true;
}
if (!existingApp.app.getAppName().equals(app.getAppName())) {
@@ -395,6 +421,34 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
}
}
// Next handle app removals
int i = 0;
while (i < appGridAdapter.getCount()) {
boolean foundExistingApp = false;
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
// Check if this app is in the latest list
for (NvApp app : appList) {
if (existingApp.app.getAppId() == app.getAppId()) {
foundExistingApp = true;
break;
}
}
// This app was removed in the latest app list
if (!foundExistingApp) {
appGridAdapter.removeApp(existingApp);
updated = true;
// Check this same index again because the item at i+1 is now at i after
// the removal
continue;
}
// Move on to the next item
i++;
}
if (updated) {
appGridAdapter.notifyDataSetChanged();
}
@@ -402,58 +456,6 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
});
}
private void doStart(NvApp app) {
Intent intent = new Intent(this, Game.class);
intent.putExtra(Game.EXTRA_HOST,
computer.reachability == ComputerDetails.Reachability.LOCAL ?
computer.localIp.getHostAddress() : computer.remoteIp.getHostAddress());
intent.putExtra(Game.EXTRA_APP, app.getAppName());
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
intent.putExtra(Game.EXTRA_STREAMING_REMOTE,
computer.reachability != ComputerDetails.Reachability.LOCAL);
startActivity(intent);
}
private void doQuit(final NvApp app) {
Toast.makeText(AppView.this, getResources().getString(R.string.applist_quit_app)+" "+app.getAppName()+"...", Toast.LENGTH_SHORT).show();
new Thread(new Runnable() {
@Override
public void run() {
NvHTTP httpConn;
String message;
try {
httpConn = new NvHTTP(getAddress(),
managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(AppView.this));
if (httpConn.quitApp()) {
message = getResources().getString(R.string.applist_quit_success)+" "+app.getAppName();
}
else {
message = getResources().getString(R.string.applist_quit_fail)+" "+app.getAppName();
}
} catch (UnknownHostException e) {
message = getResources().getString(R.string.error_unknown_host);
} catch (FileNotFoundException e) {
message = getResources().getString(R.string.error_404);
} catch (Exception e) {
message = e.getMessage();
} finally {
// Trigger a poll immediately
if (poller != null) {
poller.pollNow();
}
}
final String toastMessage = message;
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(AppView.this, toastMessage, Toast.LENGTH_LONG).show();
}
});
}
}).start();
}
@Override
public int getAdapterFragmentLayoutId() {
return PreferenceConfiguration.readPreferences(this).listMode ?
@@ -474,7 +476,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
if (getRunningAppId() != -1) {
openContextMenu(arg1);
} else {
doStart(app.app);
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
}
}
});
+123 -72
View File
@@ -1,11 +1,11 @@
package com.limelight;
import com.limelight.LimelightBuildProps;
import com.limelight.binding.PlatformBinding;
import com.limelight.binding.input.ControllerHandler;
import com.limelight.binding.input.KeyboardTranslator;
import com.limelight.binding.input.TouchContext;
import com.limelight.binding.input.driver.UsbDriverService;
import com.limelight.binding.input.evdev.EvdevListener;
import com.limelight.binding.input.evdev.EvdevWatcher;
import com.limelight.binding.video.ConfigurableDecoderRenderer;
@@ -13,6 +13,7 @@ import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.NvConnectionListener;
import com.limelight.nvstream.StreamConfiguration;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.input.KeyboardPacket;
import com.limelight.nvstream.input.MouseButtonPacket;
import com.limelight.preferences.PreferenceConfiguration;
@@ -22,7 +23,11 @@ import com.limelight.utils.SpinnerDialog;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.res.Configuration;
import android.graphics.Point;
import android.hardware.input.InputManager;
@@ -31,6 +36,7 @@ import android.net.ConnectivityManager;
import android.net.wifi.WifiManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.SystemClock;
import android.view.Display;
import android.view.InputDevice;
@@ -63,6 +69,9 @@ public class Game extends Activity implements SurfaceHolder.Callback,
private final TouchContext[] touchContextMap = new TouchContext[2];
private long threeFingerDownTime = 0;
private static final double REFERENCE_HORIZ_RES = 1280;
private static final double REFERENCE_VERT_RES = 720;
private static final int THREE_FINGER_TAP_THRESHOLD = 300;
private ControllerHandler controllerHandler;
@@ -76,6 +85,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
private boolean displayedFailureDialog = false;
private boolean connecting = false;
private boolean connected = false;
private boolean deferredSurfaceResize = false;
private EvdevWatcher evdevWatcher;
private int modifierFlags = 0;
@@ -88,8 +98,24 @@ public class Game extends Activity implements SurfaceHolder.Callback,
private int drFlags = 0;
private boolean connectedToUsbDriverService = false;
private ServiceConnection usbDriverServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
UsbDriverService.UsbDriverBinder binder = (UsbDriverService.UsbDriverBinder) iBinder;
binder.setListener(controllerHandler);
connectedToUsbDriverService = true;
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
connectedToUsbDriverService = false;
}
};
public static final String EXTRA_HOST = "Host";
public static final String EXTRA_APP = "App";
public static final String EXTRA_APP_NAME = "AppName";
public static final String EXTRA_APP_ID = "AppId";
public static final String EXTRA_UNIQUEID = "UniqueId";
public static final String EXTRA_STREAMING_REMOTE = "Remote";
@@ -171,17 +197,23 @@ public class Game extends Activity implements SurfaceHolder.Callback,
wifiLock.acquire();
String host = Game.this.getIntent().getStringExtra(EXTRA_HOST);
String app = Game.this.getIntent().getStringExtra(EXTRA_APP);
String appName = Game.this.getIntent().getStringExtra(EXTRA_APP_NAME);
int appId = Game.this.getIntent().getIntExtra(EXTRA_APP_ID, StreamConfiguration.INVALID_APP_ID);
String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID);
boolean remote = Game.this.getIntent().getBooleanExtra(EXTRA_STREAMING_REMOTE, false);
if (appId == StreamConfiguration.INVALID_APP_ID) {
finish();
return;
}
decoderRenderer = new ConfigurableDecoderRenderer();
decoderRenderer.initializeWithFlags(drFlags);
StreamConfiguration config = new StreamConfiguration.Builder()
.setResolution(prefConfig.width, prefConfig.height)
.setRefreshRate(prefConfig.fps)
.setApp(app)
.setApp(new NvApp(appName, appId))
.setBitrate(prefConfig.bitrate * 1000)
.setEnableSops(prefConfig.enableSops)
.enableAdaptiveResolution((decoderRenderer.getCapabilities() &
@@ -199,17 +231,36 @@ public class Game extends Activity implements SurfaceHolder.Callback,
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
inputManager.registerInputDeviceListener(controllerHandler, null);
boolean aspectRatioMatch = false;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
// On KitKat and later (where we can use the whole screen via immersive mode), we'll
// calculate whether we need to scale by aspect ratio or not. If not, we'll use
// setFixedSize so we can handle 4K properly. The only known devices that have
// >= 4K screens have exactly 4K screens, so we'll be able to hit this good path
// on these devices. On Marshmallow, we can start changing to 4K manually but no
// 4K devices run 6.0 at the moment.
double screenAspectRatio = ((double)screenSize.y) / screenSize.x;
double streamAspectRatio = ((double)prefConfig.height) / prefConfig.width;
if (Math.abs(screenAspectRatio - streamAspectRatio) < 0.001) {
LimeLog.info("Stream has compatible aspect ratio with output display");
aspectRatioMatch = true;
}
}
SurfaceHolder sh = sv.getHolder();
if (prefConfig.stretchVideo || !decoderRenderer.isHardwareAccelerated()) {
if (prefConfig.stretchVideo || !decoderRenderer.isHardwareAccelerated() || aspectRatioMatch) {
// Set the surface to the size of the video
sh.setFixedSize(prefConfig.width, prefConfig.height);
}
else {
deferredSurfaceResize = true;
}
// Initialize touch contexts
for (int i = 0; i < touchContextMap.length; i++) {
touchContextMap[i] = new TouchContext(conn, i,
((double)prefConfig.width / (double)screenSize.x),
((double)prefConfig.height / (double)screenSize.y));
(REFERENCE_HORIZ_RES / (double)screenSize.x),
(REFERENCE_VERT_RES / (double)screenSize.y));
}
if (LimelightBuildProps.ROOT_BUILD) {
@@ -218,6 +269,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
evdevWatcher.start();
}
if (prefConfig.usbDriver) {
// Start the USB driver
bindService(new Intent(this, UsbDriverService.class),
usbDriverServiceConnection, Service.BIND_AUTO_CREATE);
}
// The connection will be started when the surface gets created
sh.addCallback(this);
}
@@ -285,6 +342,13 @@ public class Game extends Activity implements SurfaceHolder.Callback,
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
inputManager.unregisterInputDeviceListener(controllerHandler);
wifiLock.release();
if (connectedToUsbDriverService) {
// Unbind from the discovery service
unbindService(usbDriverServiceConnection);
}
displayedFailureDialog = true;
stopConnection();
@@ -308,13 +372,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
finish();
}
@Override
protected void onDestroy() {
super.onDestroy();
wifiLock.release();
}
private final Runnable toggleGrab = new Runnable() {
@Override
public void run() {
@@ -512,9 +569,56 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
else if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0)
{
// This case is for mice
if (event.getSource() == InputDevice.SOURCE_MOUSE)
{
int changedButtons = event.getButtonState() ^ lastButtonState;
if (event.getActionMasked() == MotionEvent.ACTION_SCROLL) {
// Send the vertical scroll packet
byte vScrollClicks = (byte) event.getAxisValue(MotionEvent.AXIS_VSCROLL);
conn.sendMouseScroll(vScrollClicks);
}
if ((changedButtons & MotionEvent.BUTTON_PRIMARY) != 0) {
if ((event.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0) {
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_LEFT);
}
else {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
}
}
if ((changedButtons & MotionEvent.BUTTON_SECONDARY) != 0) {
if ((event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) {
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
}
else {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
}
}
if ((changedButtons & MotionEvent.BUTTON_TERTIARY) != 0) {
if ((event.getButtonState() & MotionEvent.BUTTON_TERTIARY) != 0) {
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_MIDDLE);
}
else {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_MIDDLE);
}
}
// First process the history
for (int i = 0; i < event.getHistorySize(); i++) {
updateMousePosition((int)event.getHistoricalX(i), (int)event.getHistoricalY(i));
}
// Now process the current values
updateMousePosition((int)event.getX(), (int)event.getY());
lastButtonState = event.getButtonState();
}
// This case is for touch-based input devices
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN ||
event.getSource() == InputDevice.SOURCE_STYLUS)
else
{
int actionIndex = event.getActionIndex();
@@ -593,59 +697,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
return false;
}
}
// This case is for mice
else if (event.getSource() == InputDevice.SOURCE_MOUSE)
{
int changedButtons = event.getButtonState() ^ lastButtonState;
if (event.getActionMasked() == MotionEvent.ACTION_SCROLL) {
// Send the vertical scroll packet
byte vScrollClicks = (byte) event.getAxisValue(MotionEvent.AXIS_VSCROLL);
conn.sendMouseScroll(vScrollClicks);
}
if ((changedButtons & MotionEvent.BUTTON_PRIMARY) != 0) {
if ((event.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0) {
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_LEFT);
}
else {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
}
}
if ((changedButtons & MotionEvent.BUTTON_SECONDARY) != 0) {
if ((event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) {
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
}
else {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
}
}
if ((changedButtons & MotionEvent.BUTTON_TERTIARY) != 0) {
if ((event.getButtonState() & MotionEvent.BUTTON_TERTIARY) != 0) {
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_MIDDLE);
}
else {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_MIDDLE);
}
}
// First process the history
for (int i = 0; i < event.getHistorySize(); i++) {
updateMousePosition((int)event.getHistoricalX(i), (int)event.getHistoricalY(i));
}
// Now process the current values
updateMousePosition((int)event.getX(), (int)event.getY());
lastButtonState = event.getButtonState();
}
else
{
// Unknown source
return false;
}
// Handled a known source
return true;
@@ -679,8 +730,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Scale the deltas if the device resolution is different
// than the stream resolution
deltaX = (int)Math.round((double)deltaX * ((double)prefConfig.width / (double)screenSize.x));
deltaY = (int)Math.round((double)deltaY * ((double)prefConfig.height / (double)screenSize.y));
deltaX = (int)Math.round((double)deltaX * (REFERENCE_HORIZ_RES / (double)screenSize.x));
deltaY = (int)Math.round((double)deltaY * (REFERENCE_VERT_RES / (double)screenSize.y));
conn.sendMouseMove((short)deltaX, (short)deltaY);
}
@@ -798,7 +849,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Resize the surface to match the aspect ratio of the video
// This must be done after the surface is created.
if (!prefConfig.stretchVideo && decoderRenderer.isHardwareAccelerated()) {
if (deferredSurfaceResize) {
resizeSurfaceWithAspectRatio((SurfaceView) findViewById(R.id.surfaceView),
prefConfig.width, prefConfig.height);
}
+92 -42
View File
@@ -12,6 +12,7 @@ import com.limelight.computers.ComputerManagerListener;
import com.limelight.computers.ComputerManagerService;
import com.limelight.grid.PcGridAdapter;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP;
import com.limelight.nvstream.http.PairingManager;
import com.limelight.nvstream.http.PairingManager.PairState;
@@ -22,6 +23,7 @@ import com.limelight.preferences.StreamSettings;
import com.limelight.ui.AdapterFragment;
import com.limelight.ui.AdapterFragmentCallbacks;
import com.limelight.utils.Dialog;
import com.limelight.utils.ServerHelper;
import com.limelight.utils.UiHelper;
import android.app.Activity;
@@ -51,7 +53,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
private RelativeLayout noPcFoundLayout;
private PcGridAdapter pcGridAdapter;
private ComputerManagerService.ComputerManagerBinder managerBinder;
private boolean freezeUpdates, runningPolling;
private boolean freezeUpdates, runningPolling, hasResumed;
private final ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder binder) {
final ComputerManagerService.ComputerManagerBinder localBinder =
@@ -93,6 +95,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
private final static int UNPAIR_ID = 3;
private final static int WOL_ID = 4;
private final static int DELETE_ID = 5;
private final static int RESUME_ID = 6;
private final static int QUIT_ID = 7;
private void initializeViews() {
setContentView(R.layout.activity_pc_view);
@@ -211,6 +215,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
protected void onResume() {
super.onResume();
hasResumed = true;
startComputerUpdates();
}
@@ -218,6 +223,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
protected void onPause() {
super.onPause();
hasResumed = false;
stopComputerUpdates(false);
}
@@ -237,13 +243,10 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
if (computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
startComputerUpdates();
return;
}
// Inflate the context menu
if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE ||
computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
menu.add(Menu.NONE, WOL_ID, 1, getResources().getString(R.string.pcview_menu_send_wol));
menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc));
}
@@ -252,17 +255,26 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc));
}
else {
menu.add(Menu.NONE, APP_LIST_ID, 1, getResources().getString(R.string.pcview_menu_app_list));
if (computer.details.runningGameId != 0) {
menu.add(Menu.NONE, RESUME_ID, 1, getResources().getString(R.string.applist_menu_resume));
menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit));
}
menu.add(Menu.NONE, APP_LIST_ID, 3, getResources().getString(R.string.pcview_menu_app_list));
// FIXME: We used to be able to unpair here but it's been broken since GFE 2.1.x, so I've replaced
// it with delete which actually work
menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc));
menu.add(Menu.NONE, DELETE_ID, 4, getResources().getString(R.string.pcview_menu_delete_pc));
}
}
@Override
public void onContextMenuClosed(Menu menu) {
startComputerUpdates();
// For some reason, this gets called again _after_ onPause() is called on this activity.
// We don't want to start computer updates again, so we need to keep track of whether we're paused.
if (hasResumed) {
startComputerUpdates();
}
}
private void doPair(final ComputerDetails computer) {
@@ -297,6 +309,10 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
addr = computer.remoteIp;
}
else {
LimeLog.warning("Unknown reachability - using local IP");
addr = computer.localIp;
}
httpConn = new NvHTTP(addr,
managerBinder.getUniqueId(),
@@ -365,7 +381,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
}
private void doWakeOnLan(final ComputerDetails computer) {
if (computer.reachability != ComputerDetails.Reachability.OFFLINE) {
if (computer.state == ComputerDetails.State.ONLINE) {
Toast.makeText(PcView.this, getResources().getString(R.string.wol_pc_online), Toast.LENGTH_SHORT).show();
return;
}
@@ -422,6 +438,10 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
addr = computer.remoteIp;
}
else {
LimeLog.warning("Unknown reachability - using local IP");
addr = computer.localIp;
}
httpConn = new NvHTTP(addr,
managerBinder.getUniqueId(),
@@ -477,36 +497,61 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
switch (item.getItemId())
{
case PAIR_ID:
doPair(computer.details);
return true;
case UNPAIR_ID:
doUnpair(computer.details);
return true;
case WOL_ID:
doWakeOnLan(computer.details);
return true;
case DELETE_ID:
if (managerBinder == null) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
final ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
switch (item.getItemId()) {
case PAIR_ID:
doPair(computer.details);
return true;
}
managerBinder.removeComputer(computer.details.name);
removeComputer(computer.details);
return true;
case APP_LIST_ID:
doAppList(computer.details);
return true;
case UNPAIR_ID:
doUnpair(computer.details);
return true;
default:
return super.onContextItemSelected(item);
case WOL_ID:
doWakeOnLan(computer.details);
return true;
case DELETE_ID:
if (managerBinder == null) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
return true;
}
managerBinder.removeComputer(computer.details.name);
removeComputer(computer.details);
return true;
case APP_LIST_ID:
doAppList(computer.details);
return true;
case RESUME_ID:
if (managerBinder == null) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
return true;
}
ServerHelper.doStart(this, new NvApp("app", computer.details.runningGameId), computer.details, managerBinder);
return true;
case QUIT_ID:
if (managerBinder == null) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
return true;
}
// Display a confirmation dialog first
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
@Override
public void run() {
ServerHelper.doQuit(PcView.this,
ServerHelper.getCurrentAddressFromComputer(computer.details),
new NvApp("app", 0), managerBinder, null);
}
}, null);
return true;
default:
return super.onContextItemSelected(item);
}
}
@@ -517,6 +562,12 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
if (details.equals(computer.details)) {
pcGridAdapter.removeComputer(computer);
pcGridAdapter.notifyDataSetChanged();
if (pcGridAdapter.getCount() == 0) {
// Show the "Discovery in progress" view
noPcFoundLayout.setVisibility(View.VISIBLE);
}
break;
}
}
@@ -566,10 +617,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
long id) {
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(pos);
if (computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
// Do nothing
} else if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
// Open the context menu if a PC is offline
if (computer.details.reachability == ComputerDetails.Reachability.UNKNOWN ||
computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
// Open the context menu if a PC is offline or refreshing
openContextMenu(arg1);
} else if (computer.details.pairState != PairState.PAIRED) {
// Pair an unpaired machine by default
@@ -9,14 +9,13 @@ import com.limelight.nvstream.av.audio.AudioRenderer;
public class AndroidAudioRenderer implements AudioRenderer {
private static final int FRAME_SIZE = 960;
private AudioTrack track;
@Override
public boolean streamInitialized(int channelCount, int sampleRate) {
public boolean streamInitialized(int channelCount, int channelMask, int samplesPerFrame, int sampleRate) {
int channelConfig;
int bufferSize;
int bytesPerFrame = (samplesPerFrame * 2);
switch (channelCount)
{
@@ -26,6 +25,12 @@ public class AndroidAudioRenderer implements AudioRenderer {
case 2:
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
break;
case 4:
channelConfig = AudioFormat.CHANNEL_OUT_QUAD;
break;
case 6:
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
break;
default:
LimeLog.severe("Decoder returned unhandled channel count");
return false;
@@ -38,7 +43,7 @@ public class AndroidAudioRenderer implements AudioRenderer {
// use the recommended larger buffer size.
try {
// Buffer two frames of audio if possible
bufferSize = FRAME_SIZE * 2;
bufferSize = bytesPerFrame * 2;
track = new AudioTrack(AudioManager.STREAM_MUSIC,
sampleRate,
@@ -59,10 +64,10 @@ public class AndroidAudioRenderer implements AudioRenderer {
bufferSize = Math.max(AudioTrack.getMinBufferSize(sampleRate,
channelConfig,
AudioFormat.ENCODING_PCM_16BIT),
FRAME_SIZE * 2);
bytesPerFrame * 2);
// Round to next frame
bufferSize = (((bufferSize + (FRAME_SIZE - 1)) / FRAME_SIZE) * FRAME_SIZE);
bufferSize = (((bufferSize + (bytesPerFrame - 1)) / bytesPerFrame) * bytesPerFrame);
track = new AudioTrack(AudioManager.STREAM_MUSIC,
sampleRate,
@@ -1,21 +1,20 @@
package com.limelight.binding.input;
import java.util.HashMap;
import java.util.Map;
import android.hardware.input.InputManager;
import android.os.SystemClock;
import android.util.SparseArray;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import com.limelight.LimeLog;
import com.limelight.binding.input.driver.UsbDriverListener;
import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.input.ControllerPacket;
import com.limelight.ui.GameGestures;
import com.limelight.utils.Vector2d;
public class ControllerHandler implements InputManager.InputDeviceListener {
public class ControllerHandler implements InputManager.InputDeviceListener, UsbDriverListener {
private static final int MAXIMUM_BUMPER_UP_DELAY_MS = 100;
@@ -31,11 +30,12 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
private final Vector2d inputVector = new Vector2d();
private final HashMap<String, ControllerContext> contexts = new HashMap<String, ControllerContext>();
private final SparseArray<InputDeviceContext> inputDeviceContexts = new SparseArray<>();
private final SparseArray<UsbDeviceContext> usbDeviceContexts = new SparseArray<>();
private final NvConnection conn;
private final double stickDeadzone;
private final ControllerContext defaultContext = new ControllerContext();
private final InputDeviceContext defaultContext = new InputDeviceContext();
private final GameGestures gestures;
private boolean hasGameController;
@@ -52,8 +52,8 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
deadzonePercentage = 10;
int[] ids = InputDevice.getDeviceIds();
for (int i = 0; i < ids.length; i++) {
InputDevice dev = InputDevice.getDevice(ids[i]);
for (int id : ids) {
InputDevice dev = InputDevice.getDevice(id);
if ((dev.getSources() & InputDevice.SOURCE_JOYSTICK) != 0 ||
(dev.getSources() & InputDevice.SOURCE_GAMEPAD) != 0) {
// This looks like a gamepad, but we'll check X and Y to be sure
@@ -97,18 +97,6 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
return range;
}
private short assignNewControllerNumber() {
for (short i = 0; i < 4; i++) {
if ((currentControllers & (1 << i)) == 0) {
// Found an unused controller value
currentControllers |= (1 << i);
return i;
}
}
return 0;
}
@Override
public void onInputDeviceAdded(int deviceId) {
// Nothing happening here yet
@@ -116,13 +104,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
@Override
public void onInputDeviceRemoved(int deviceId) {
for (Map.Entry<String, ControllerContext> device : contexts.entrySet()) {
if (device.getValue().id == deviceId) {
LimeLog.info("Removed controller: "+device.getValue().name);
releaseControllerNumber(device.getValue().controllerNumber);
contexts.remove(device.getKey());
return;
}
InputDeviceContext context = inputDeviceContexts.get(deviceId);
if (context != null) {
LimeLog.info("Removed controller: "+context.name+" ("+deviceId+")");
releaseControllerNumber(context);
inputDeviceContexts.remove(deviceId);
}
}
@@ -133,13 +119,96 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
onInputDeviceAdded(deviceId);
}
private void releaseControllerNumber(int controllerNumber) {
LimeLog.info("Controller number "+controllerNumber+" is now available");
currentControllers &= ~(1 << controllerNumber);
private void releaseControllerNumber(GenericControllerContext context) {
// If this device sent data as a gamepad, zero the values before removing
if (context.assignedControllerNumber) {
conn.sendControllerInput(context.controllerNumber, (short) 0,
(byte) 0, (byte) 0,
(short) 0, (short) 0,
(short) 0, (short) 0);
}
// If we reserved a controller number, remove that reservation
if (context.reservedControllerNumber) {
LimeLog.info("Controller number "+context.controllerNumber+" is now available");
currentControllers &= ~(1 << context.controllerNumber);
}
}
private ControllerContext createContextForDevice(InputDevice dev) {
ControllerContext context = new ControllerContext();
// Called before sending input but after we've determined that this
// is definitely a controller (not a keyboard, mouse, or something else)
private void assignControllerNumberIfNeeded(GenericControllerContext context) {
if (context.assignedControllerNumber) {
return;
}
if (context instanceof InputDeviceContext) {
InputDeviceContext devContext = (InputDeviceContext) context;
LimeLog.info(devContext.name+" ("+context.id+") needs a controller number assigned");
if (devContext.name != null && devContext.name.contains("gpio-keys")) {
// This is the back button on Shield portable consoles
LimeLog.info("Built-in buttons hardcoded as controller 0");
context.controllerNumber = 0;
}
else if (multiControllerEnabled && devContext.hasJoystickAxes) {
context.controllerNumber = 0;
LimeLog.info("Reserving the next available controller number");
for (short i = 0; i < 4; i++) {
if ((currentControllers & (1 << i)) == 0) {
// Found an unused controller value
currentControllers |= (1 << i);
context.controllerNumber = i;
context.reservedControllerNumber = true;
break;
}
}
}
else {
LimeLog.info("Not reserving a controller number");
context.controllerNumber = 0;
}
}
else {
if (multiControllerEnabled) {
context.controllerNumber = 0;
LimeLog.info("Reserving the next available controller number");
for (short i = 0; i < 4; i++) {
if ((currentControllers & (1 << i)) == 0) {
// Found an unused controller value
currentControllers |= (1 << i);
context.controllerNumber = i;
context.reservedControllerNumber = true;
break;
}
}
}
else {
LimeLog.info("Not reserving a controller number");
context.controllerNumber = 0;
}
}
LimeLog.info("Assigned as controller "+context.controllerNumber);
context.assignedControllerNumber = true;
}
private UsbDeviceContext createUsbDeviceContextForDevice(int deviceId) {
UsbDeviceContext context = new UsbDeviceContext();
context.id = deviceId;
context.leftStickDeadzoneRadius = (float) stickDeadzone;
context.rightStickDeadzoneRadius = (float) stickDeadzone;
context.triggerDeadzone = 0.13f;
return context;
}
private InputDeviceContext createInputDeviceContextForDevice(InputDevice dev) {
InputDeviceContext context = new InputDeviceContext();
String devName = dev.getName();
LimeLog.info("Creating controller context for device: "+devName);
@@ -251,6 +320,15 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
}
}
// Ignore the back buttonn if a controller has both buttons
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
boolean[] hasSelectKey = dev.hasKeys(KeyEvent.KEYCODE_BUTTON_SELECT, KeyEvent.KEYCODE_BACK, 0);
if (hasSelectKey[0] && hasSelectKey[1]) {
LimeLog.info("Ignoring back button because select is present");
context.ignoreBack = true;
}
}
if (devName != null) {
// For the Nexus Player (and probably other ATV devices), we should
// use the back button as start since it doesn't have a start/menu button
@@ -274,56 +352,54 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
else if (devName.contains("Fire TV Remote") || devName.contains("Nexus Remote")) {
// It's only a remote if it doesn't any sticks
if (!context.hasJoystickAxes) {
context.isRemote = true;
context.ignoreBack = true;
}
}
// NYKO Playpad has a fake hat that mimics the left stick for some reason
else if (devName.contains("NYKO PLAYPAD")) {
context.hatXAxis = -1;
context.hatYAxis = -1;
// SHIELD controllers will use small stick deadzones
else if (devName.contains("SHIELD")) {
context.leftStickDeadzoneRadius = 0.07f;
context.rightStickDeadzoneRadius = 0.07f;
}
// Samsung's face buttons appear as a non-virtual button so we'll explicitly ignore
// back presses on this device
else if (devName.equals("sec_touchscreen")) {
context.ignoreBack = true;
}
// The Serval has a couple of unknown buttons that are start and select. It also has
// a back button which we want to ignore since there's already a select button.
else if (devName.contains("Razer Serval")) {
context.isServal = true;
context.ignoreBack = true;
}
}
LimeLog.info("Analog stick deadzone: "+context.leftStickDeadzoneRadius+" "+context.rightStickDeadzoneRadius);
LimeLog.info("Trigger deadzone: "+context.triggerDeadzone);
if (devName != null && devName.equals("gpio-keys")) {
// This is the back button on Shield portable consoles
context.controllerNumber = 0;
}
else if (multiControllerEnabled) {
context.controllerNumber = assignNewControllerNumber();
}
else {
context.controllerNumber = 0;
}
LimeLog.info("Assigned as controller "+context.controllerNumber);
return context;
}
private ControllerContext getContextForDevice(InputDevice dev) {
private InputDeviceContext getContextForDevice(InputDevice dev) {
// Unknown devices use the default context
if (dev == null) {
return defaultContext;
}
String descriptor = dev.getDescriptor();
// Return the existing context if it exists
ControllerContext context = contexts.get(descriptor);
InputDeviceContext context = inputDeviceContexts.get(dev.getId());
if (context != null) {
return context;
}
// Otherwise create a new context
context = createContextForDevice(dev);
contexts.put(descriptor, context);
context = createInputDeviceContextForDevice(dev);
inputDeviceContexts.put(dev.getId(), context);
return context;
}
private void sendControllerInputPacket(ControllerContext context) {
private void sendControllerInputPacket(GenericControllerContext context) {
assignControllerNumberIfNeeded(context);
conn.sendControllerInput(context.controllerNumber, context.inputMap,
context.leftTrigger, context.rightTrigger,
context.leftStickX, context.leftStickY,
@@ -332,9 +408,9 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
// Return a valid keycode, 0 to consume, or -1 to not consume the event
// Device MAY BE NULL
private int handleRemapping(ControllerContext context, KeyEvent event) {
// For remotes, don't capture the back button
if (context.isRemote) {
private int handleRemapping(InputDeviceContext context, KeyEvent event) {
// Don't capture the back button if configured
if (context.ignoreBack) {
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
return -1;
}
@@ -378,6 +454,16 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
return 0;
}
}
// If this is a Serval controller sending an unknown key code, it's probably
// the start and select buttons
else if (context.isServal && event.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN) {
switch (event.getScanCode()) {
case 314:
return KeyEvent.KEYCODE_BUTTON_SELECT;
case 315:
return KeyEvent.KEYCODE_BUTTON_START;
}
}
if (context.hatXAxis != -1 && context.hatYAxis != -1) {
switch (event.getKeyCode()) {
@@ -460,7 +546,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
// evaluates the deadzone.
}
private void handleAxisSet(ControllerContext context, float lsX, float lsY, float rsX,
private void handleAxisSet(InputDeviceContext context, float lsX, float lsY, float rsX,
float rsY, float lt, float rt, float hatX, float hatY) {
if (context.leftStickXAxis != -1 && context.leftStickYAxis != -1) {
@@ -520,7 +606,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
}
public boolean handleMotionEvent(MotionEvent event) {
ControllerContext context = getContextForDevice(event.getDevice());
InputDeviceContext context = getContextForDevice(event.getDevice());
float lsX = 0, lsY = 0, rsX = 0, rsY = 0, rt = 0, lt = 0, hatX = 0, hatY = 0;
// We purposefully ignore the historical values in the motion event as it makes
@@ -552,7 +638,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
}
public boolean handleButtonUp(KeyEvent event) {
ControllerContext context = getContextForDevice(event.getDevice());
InputDeviceContext context = getContextForDevice(event.getDevice());
int keyCode = handleRemapping(context, event);
if (keyCode == 0) {
@@ -677,7 +763,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
}
public boolean handleButtonDown(KeyEvent event) {
ControllerContext context = getContextForDevice(event.getDevice());
InputDeviceContext context = getContextForDevice(event.getDevice());
int keyCode = handleRemapping(context, event);
if (keyCode == 0) {
@@ -777,33 +863,67 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
return true;
}
class ControllerContext {
public String name;
@Override
public void reportControllerState(int controllerId, short buttonFlags,
float leftStickX, float leftStickY,
float rightStickX, float rightStickY,
float leftTrigger, float rightTrigger) {
UsbDeviceContext context = usbDeviceContexts.get(controllerId);
Vector2d leftStickVector = populateCachedVector(leftStickX, leftStickY);
handleDeadZone(leftStickVector, context.leftStickDeadzoneRadius);
context.leftStickX = (short) (leftStickVector.getX() * 0x7FFE);
context.leftStickY = (short) (-leftStickVector.getY() * 0x7FFE);
Vector2d rightStickVector = populateCachedVector(rightStickX, rightStickY);
handleDeadZone(rightStickVector, context.rightStickDeadzoneRadius);
context.rightStickX = (short) (rightStickVector.getX() * 0x7FFE);
context.rightStickY = (short) (-rightStickVector.getY() * 0x7FFE);
if (leftTrigger <= context.triggerDeadzone) {
leftTrigger = 0;
}
if (rightTrigger <= context.triggerDeadzone) {
rightTrigger = 0;
}
context.leftTrigger = (byte)(leftTrigger * 0xFF);
context.rightTrigger = (byte)(rightTrigger * 0xFF);
context.inputMap = buttonFlags;
sendControllerInputPacket(context);
}
@Override
public void deviceRemoved(int controllerId) {
UsbDeviceContext context = usbDeviceContexts.get(controllerId);
if (context != null) {
LimeLog.info("Removed controller: "+controllerId);
releaseControllerNumber(context);
usbDeviceContexts.remove(controllerId);
}
}
@Override
public void deviceAdded(int controllerId) {
UsbDeviceContext context = createUsbDeviceContextForDevice(controllerId);
usbDeviceContexts.put(controllerId, context);
}
class GenericControllerContext {
public int id;
public int leftStickXAxis = -1;
public int leftStickYAxis = -1;
public float leftStickDeadzoneRadius;
public int rightStickXAxis = -1;
public int rightStickYAxis = -1;
public float rightStickDeadzoneRadius;
public int leftTriggerAxis = -1;
public int rightTriggerAxis = -1;
public boolean triggersIdleNegative;
public float triggerDeadzone;
public int hatXAxis = -1;
public int hatYAxis = -1;
public boolean isDualShock4;
public boolean isXboxController;
public boolean backIsStart;
public boolean modeIsSelect;
public boolean isRemote;
public boolean hasJoystickAxes;
public boolean assignedControllerNumber;
public boolean reservedControllerNumber;
public short controllerNumber;
public short inputMap = 0x0000;
@@ -813,6 +933,32 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
public short rightStickY = 0x0000;
public short leftStickX = 0x0000;
public short leftStickY = 0x0000;
}
class InputDeviceContext extends GenericControllerContext {
public String name;
public int leftStickXAxis = -1;
public int leftStickYAxis = -1;
public int rightStickXAxis = -1;
public int rightStickYAxis = -1;
public int leftTriggerAxis = -1;
public int rightTriggerAxis = -1;
public boolean triggersIdleNegative;
public int hatXAxis = -1;
public int hatYAxis = -1;
public boolean isDualShock4;
public boolean isXboxController;
public boolean isServal;
public boolean backIsStart;
public boolean modeIsSelect;
public boolean ignoreBack;
public boolean hasJoystickAxes;
public int emulatingButtonFlags = 0;
// Used for OUYA bumper state tracking since they force all buttons
@@ -825,4 +971,6 @@ public class ControllerHandler implements InputManager.InputDeviceListener {
public long startDownTime = 0;
}
class UsbDeviceContext extends GenericControllerContext {}
}
@@ -3,6 +3,9 @@ package com.limelight.binding.input;
import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.input.MouseButtonPacket;
import java.util.Timer;
import java.util.TimerTask;
public class TouchContext {
private int lastTouchX = 0;
private int lastTouchY = 0;
@@ -10,14 +13,20 @@ public class TouchContext {
private int originalTouchY = 0;
private long originalTouchTime = 0;
private boolean cancelled;
private boolean confirmedMove;
private boolean confirmedDrag;
private Timer dragTimer;
private double distanceMoved;
private final NvConnection conn;
private final int actionIndex;
private final double xFactor;
private final double yFactor;
private static final int TAP_MOVEMENT_THRESHOLD = 10;
private static final int TAP_MOVEMENT_THRESHOLD = 20;
private static final int TAP_DISTANCE_THRESHOLD = 25;
private static final int TAP_TIME_THRESHOLD = 250;
private static final int DRAG_TIME_THRESHOLD = 650;
public TouchContext(NvConnection conn, int actionIndex, double xFactor, double yFactor)
{
@@ -32,15 +41,19 @@ public class TouchContext {
return actionIndex;
}
private boolean isWithinTapBounds(int touchX, int touchY)
{
int xDelta = Math.abs(touchX - originalTouchX);
int yDelta = Math.abs(touchY - originalTouchY);
return xDelta <= TAP_MOVEMENT_THRESHOLD &&
yDelta <= TAP_MOVEMENT_THRESHOLD;
}
private boolean isTap()
{
int xDelta = Math.abs(lastTouchX - originalTouchX);
int yDelta = Math.abs(lastTouchY - originalTouchY);
long timeDelta = System.currentTimeMillis() - originalTouchTime;
return xDelta <= TAP_MOVEMENT_THRESHOLD &&
yDelta <= TAP_MOVEMENT_THRESHOLD &&
timeDelta <= TAP_TIME_THRESHOLD;
return isWithinTapBounds(lastTouchX, lastTouchY) && timeDelta <= TAP_TIME_THRESHOLD;
}
private byte getMouseButtonIndex()
@@ -58,7 +71,13 @@ public class TouchContext {
originalTouchX = lastTouchX = eventX;
originalTouchY = lastTouchY = eventY;
originalTouchTime = System.currentTimeMillis();
cancelled = false;
cancelled = confirmedDrag = confirmedMove = false;
distanceMoved = 0;
if (actionIndex == 0) {
// Start the timer for engaging a drag
startDragTimer();
}
return true;
}
@@ -69,10 +88,17 @@ public class TouchContext {
return;
}
if (isTap())
{
byte buttonIndex = getMouseButtonIndex();
// Cancel the drag timer
cancelDragTimer();
byte buttonIndex = getMouseButtonIndex();
if (confirmedDrag) {
// Raise the button after a drag
conn.sendMouseButtonUp(buttonIndex);
}
else if (isTap())
{
// Lower the mouse button
conn.sendMouseButtonDown(buttonIndex);
@@ -87,24 +113,101 @@ public class TouchContext {
}
}
private synchronized void startDragTimer() {
dragTimer = new Timer(true);
dragTimer.schedule(new TimerTask() {
@Override
public void run() {
synchronized (TouchContext.this) {
// Check if someone already set move
if (confirmedMove) {
return;
}
// Check if someone cancelled us
if (dragTimer == null) {
return;
}
// Uncancellable now
dragTimer = null;
// We haven't been cancelled before the timer expired so begin dragging
confirmedDrag = true;
conn.sendMouseButtonDown(getMouseButtonIndex());
}
}
}, DRAG_TIME_THRESHOLD);
}
private synchronized void cancelDragTimer() {
if (dragTimer != null) {
dragTimer.cancel();
dragTimer = null;
}
}
private synchronized void checkForConfirmedMove(int eventX, int eventY) {
// If we've already confirmed something, get out now
if (confirmedMove || confirmedDrag) {
return;
}
// If it leaves the tap bounds before the drag time expires, it's a move.
if (!isWithinTapBounds(eventX, eventY)) {
confirmedMove = true;
cancelDragTimer();
return;
}
// Check if we've exceeded the maximum distance moved
distanceMoved += Math.sqrt(Math.pow(eventX - lastTouchX, 2) + Math.pow(eventY - lastTouchY, 2));
if (distanceMoved >= TAP_DISTANCE_THRESHOLD) {
confirmedMove = true;
cancelDragTimer();
return;
}
}
public boolean touchMoveEvent(int eventX, int eventY)
{
if (eventX != lastTouchX || eventY != lastTouchY)
{
// We only send moves for the primary touch point
// We only send moves and drags for the primary touch point
if (actionIndex == 0) {
checkForConfirmedMove(eventX, eventY);
int deltaX = eventX - lastTouchX;
int deltaY = eventY - lastTouchY;
// Scale the deltas based on the factors passed to our constructor
deltaX = (int)Math.round((double)deltaX * xFactor);
deltaY = (int)Math.round((double)deltaY * yFactor);
deltaX = (int)Math.round((double)Math.abs(deltaX) * xFactor);
deltaY = (int)Math.round((double)Math.abs(deltaY) * yFactor);
// Fix up the signs
if (eventX < lastTouchX) {
deltaX = -deltaX;
}
if (eventY < lastTouchY) {
deltaY = -deltaY;
}
// If the scaling factor ended up rounding deltas to zero, wait until they are
// non-zero to update lastTouch that way devices that report small touch events often
// will work correctly
if (deltaX != 0) {
lastTouchX = eventX;
}
if (deltaY != 0) {
lastTouchY = eventY;
}
conn.sendMouseMove((short)deltaX, (short)deltaY);
}
lastTouchX = eventX;
lastTouchY = eventY;
else {
lastTouchX = eventX;
lastTouchY = eventY;
}
}
return true;
@@ -112,6 +215,14 @@ public class TouchContext {
public void cancelTouch() {
cancelled = true;
// Cancel the drag timer
cancelDragTimer();
// If it was a confirmed drag, we'll need to raise the button now
if (confirmedDrag) {
conn.sendMouseButtonUp(getMouseButtonIndex());
}
}
public boolean isCancelled() {
@@ -0,0 +1,11 @@
package com.limelight.binding.input.driver;
public interface UsbDriverListener {
void reportControllerState(int controllerId, short buttonFlags,
float leftStickX, float leftStickY,
float rightStickX, float rightStickY,
float leftTrigger, float rightTrigger);
void deviceRemoved(int controllerId);
void deviceAdded(int controllerId);
}
@@ -0,0 +1,164 @@
package com.limelight.binding.input.driver;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbManager;
import android.os.Binder;
import android.os.IBinder;
import java.util.ArrayList;
public class UsbDriverService extends Service implements UsbDriverListener {
private static final String ACTION_USB_PERMISSION =
"com.limelight.USB_PERMISSION";
private UsbManager usbManager;
private final UsbEventReceiver receiver = new UsbEventReceiver();
private final UsbDriverBinder binder = new UsbDriverBinder();
private final ArrayList<XboxOneController> controllers = new ArrayList<>();
private UsbDriverListener listener;
private static int nextDeviceId;
@Override
public void reportControllerState(int controllerId, short buttonFlags, float leftStickX, float leftStickY, float rightStickX, float rightStickY, float leftTrigger, float rightTrigger) {
// Call through to the client's listener
if (listener != null) {
listener.reportControllerState(controllerId, buttonFlags, leftStickX, leftStickY, rightStickX, rightStickY, leftTrigger, rightTrigger);
}
}
@Override
public void deviceRemoved(int controllerId) {
// Remove the the controller from our list (if not removed already)
for (XboxOneController controller : controllers) {
if (controller.getControllerId() == controllerId) {
controllers.remove(controller);
break;
}
}
// Call through to the client's listener
if (listener != null) {
listener.deviceRemoved(controllerId);
}
}
@Override
public void deviceAdded(int controllerId) {
// Call through to the client's listener
if (listener != null) {
listener.deviceAdded(controllerId);
}
}
public class UsbEventReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
// Initial attachment broadcast
if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
UsbDevice device = (UsbDevice) intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
// Continue the state machine
handleUsbDeviceState(device);
}
// Subsequent permission dialog completion intent
else if (action.equals(ACTION_USB_PERMISSION)) {
UsbDevice device = (UsbDevice) intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
// If we got this far, we've already found we're able to handle this device
if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
handleUsbDeviceState(device);
}
}
}
}
public class UsbDriverBinder extends Binder {
public void setListener(UsbDriverListener listener) {
UsbDriverService.this.listener = listener;
// Report all controllerMap that already exist
if (listener != null) {
for (XboxOneController controller : controllers) {
listener.deviceAdded(controller.getControllerId());
}
}
}
}
private void handleUsbDeviceState(UsbDevice device) {
// Are we able to operate it?
if (XboxOneController.canClaimDevice(device)) {
// Do we have permission yet?
if (!usbManager.hasPermission(device)) {
// Let's ask for permission
usbManager.requestPermission(device, PendingIntent.getBroadcast(UsbDriverService.this, 0, new Intent(ACTION_USB_PERMISSION), 0));
return;
}
// Open the device
UsbDeviceConnection connection = usbManager.openDevice(device);
// Try to initialize it
XboxOneController controller = new XboxOneController(device, connection, nextDeviceId++, this);
if (!controller.start()) {
connection.close();
return;
}
// Add this controller to the list
controllers.add(controller);
}
}
@Override
public void onCreate() {
this.usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
// Register for USB attach broadcasts and permission completions
IntentFilter filter = new IntentFilter();
filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
filter.addAction(ACTION_USB_PERMISSION);
registerReceiver(receiver, filter);
// Enumerate existing devices
for (UsbDevice dev : usbManager.getDeviceList().values()) {
if (XboxOneController.canClaimDevice(dev)) {
// Start the process of claiming this device
handleUsbDeviceState(dev);
}
}
}
@Override
public void onDestroy() {
// Stop the attachment receiver
unregisterReceiver(receiver);
// Remove listeners
listener = null;
// Stop all controllers
while (controllers.size() > 0) {
// Stop and remove the controller
controllers.remove(0).stop();
}
}
@Override
public IBinder onBind(Intent intent) {
return binder;
}
}
@@ -0,0 +1,233 @@
package com.limelight.binding.input.driver;
import android.hardware.usb.UsbConstants;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbEndpoint;
import android.hardware.usb.UsbInterface;
import com.limelight.LimeLog;
import com.limelight.binding.video.MediaCodecHelper;
import com.limelight.nvstream.input.ControllerPacket;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
public class XboxOneController {
private final UsbDevice device;
private final UsbDeviceConnection connection;
private final int deviceId;
private Thread inputThread;
private UsbDriverListener listener;
private boolean stopped;
private short buttonFlags;
private float leftTrigger, rightTrigger;
private float rightStickX, rightStickY;
private float leftStickX, leftStickY;
private static final int MICROSOFT_VID = 0x045e;
private static final int XB1_IFACE_SUBCLASS = 71;
private static final int XB1_IFACE_PROTOCOL = 208;
// FIXME: odata_serial
private static final byte[] XB1_INIT_DATA = {0x05, 0x20, 0x00, 0x01, 0x00};
public XboxOneController(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
this.device = device;
this.connection = connection;
this.deviceId = deviceId;
this.listener = listener;
}
public int getControllerId() {
return this.deviceId;
}
private void setButtonFlag(int buttonFlag, int data) {
if (data != 0) {
buttonFlags |= buttonFlag;
}
else {
buttonFlags &= ~buttonFlag;
}
}
private void reportInput() {
listener.reportControllerState(deviceId, buttonFlags, leftStickX, leftStickY,
rightStickX, rightStickY, leftTrigger, rightTrigger);
}
private void processButtons(ByteBuffer buffer) {
byte b = buffer.get();
setButtonFlag(ControllerPacket.PLAY_FLAG, b & 0x04);
setButtonFlag(ControllerPacket.BACK_FLAG, b & 0x08);
setButtonFlag(ControllerPacket.A_FLAG, b & 0x10);
setButtonFlag(ControllerPacket.B_FLAG, b & 0x20);
setButtonFlag(ControllerPacket.X_FLAG, b & 0x40);
setButtonFlag(ControllerPacket.Y_FLAG, b & 0x80);
b = buffer.get();
setButtonFlag(ControllerPacket.LEFT_FLAG, b & 0x04);
setButtonFlag(ControllerPacket.RIGHT_FLAG, b & 0x08);
setButtonFlag(ControllerPacket.UP_FLAG, b & 0x01);
setButtonFlag(ControllerPacket.DOWN_FLAG, b & 0x02);
setButtonFlag(ControllerPacket.LB_FLAG, b & 0x10);
setButtonFlag(ControllerPacket.RB_FLAG, b & 0x20);
setButtonFlag(ControllerPacket.LS_CLK_FLAG, b & 0x40);
setButtonFlag(ControllerPacket.RS_CLK_FLAG, b & 0x80);
leftTrigger = buffer.getShort() / 1023.0f;
rightTrigger = buffer.getShort() / 1023.0f;
leftStickX = buffer.getShort() / 32767.0f;
leftStickY = ~buffer.getShort() / 32767.0f;
rightStickX = buffer.getShort() / 32767.0f;
rightStickY = ~buffer.getShort() / 32767.0f;
reportInput();
}
private void processPacket(ByteBuffer buffer) {
switch (buffer.get())
{
case 0x20:
buffer.position(buffer.position()+3);
processButtons(buffer);
break;
case 0x07:
buffer.position(buffer.position() + 3);
setButtonFlag(ControllerPacket.SPECIAL_BUTTON_FLAG, buffer.get() & 0x01);
reportInput();
break;
}
}
private void startInputThread(final UsbEndpoint inEndpt) {
inputThread = new Thread() {
public void run() {
while (!isInterrupted() && !stopped) {
byte[] buffer = new byte[64];
int res;
//
// There's no way that I can tell to determine if a device has failed
// or if the timeout has simply expired. We'll check how long the transfer
// took to fail and assume the device failed if it happened before the timeout
// expired.
//
do {
// Read the next input state packet
long lastMillis = MediaCodecHelper.getMonotonicMillis();
res = connection.bulkTransfer(inEndpt, buffer, buffer.length, 3000);
if (res == -1 && MediaCodecHelper.getMonotonicMillis() - lastMillis < 1000) {
LimeLog.warning("Detected device I/O error");
XboxOneController.this.stop();
break;
}
} while (res == -1 && !isInterrupted() && !stopped);
if (res == -1 || stopped) {
break;
}
processPacket(ByteBuffer.wrap(buffer, 0, res).order(ByteOrder.LITTLE_ENDIAN));
}
}
};
inputThread.setName("Xbox One Controller - Input Thread");
inputThread.start();
}
public boolean start() {
// Force claim all interfaces
for (int i = 0; i < device.getInterfaceCount(); i++) {
UsbInterface iface = device.getInterface(i);
if (!connection.claimInterface(iface, true)) {
LimeLog.warning("Failed to claim interfaces");
return false;
}
}
// Find the endpoints
UsbEndpoint outEndpt = null;
UsbEndpoint inEndpt = null;
UsbInterface iface = device.getInterface(0);
for (int i = 0; i < iface.getEndpointCount(); i++) {
UsbEndpoint endpt = iface.getEndpoint(i);
if (endpt.getDirection() == UsbConstants.USB_DIR_IN) {
if (inEndpt != null) {
LimeLog.warning("Found duplicate IN endpoint");
return false;
}
inEndpt = endpt;
}
else if (endpt.getDirection() == UsbConstants.USB_DIR_OUT) {
if (outEndpt != null) {
LimeLog.warning("Found duplicate OUT endpoint");
return false;
}
outEndpt = endpt;
}
}
// Make sure the required endpoints were present
if (inEndpt == null || outEndpt == null) {
LimeLog.warning("Missing required endpoint");
return false;
}
// Send the initialization packet
int res = connection.bulkTransfer(outEndpt, XB1_INIT_DATA, XB1_INIT_DATA.length, 3000);
if (res != XB1_INIT_DATA.length) {
LimeLog.warning("Initialization transfer failed: "+res);
return false;
}
// Start listening for controller input
startInputThread(inEndpt);
// Report this device added via the listener
listener.deviceAdded(deviceId);
return true;
}
public void stop() {
if (stopped) {
return;
}
stopped = true;
// Stop the input thread
if (inputThread != null) {
inputThread.interrupt();
inputThread = null;
}
// Report the device removed
listener.deviceRemoved(deviceId);
// Close the USB connection
connection.close();
}
public static boolean canClaimDevice(UsbDevice device) {
return device.getVendorId() == MICROSOFT_VID &&
device.getInterfaceCount() >= 1 &&
device.getInterface(0).getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
device.getInterface(0).getInterfaceSubclass() == XB1_IFACE_SUBCLASS &&
device.getInterface(0).getInterfaceProtocol() == XB1_IFACE_PROTOCOL;
}
}
@@ -177,11 +177,10 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
rendererThread = new Thread() {
@Override
public void run() {
long nextFrameTime = System.currentTimeMillis();
DecodeUnit du;
long nextFrameTime = MediaCodecHelper.getMonotonicMillis();
while (!isInterrupted())
{
long diff = nextFrameTime - System.currentTimeMillis();
long diff = nextFrameTime - MediaCodecHelper.getMonotonicMillis();
if (diff > WAIT_CEILING_MS) {
try {
@@ -204,7 +203,7 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
}
private long computePresentationTimeMs(int frameRate) {
return System.currentTimeMillis() + (1000 / frameRate);
return MediaCodecHelper.getMonotonicMillis() + (1000 / frameRate);
}
@Override
@@ -232,7 +231,8 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
if (decodeUnit.getDataLength() <= DECODER_BUFFER_SIZE) {
decoderBuffer.clear();
for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
for (ByteBufferDescriptor bbd = decodeUnit.getBufferHead();
bbd != null; bbd = bbd.nextDescriptor) {
decoderBuffer.put(bbd.data, bbd.offset, bbd.length);
}
@@ -242,7 +242,8 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
data = new byte[decodeUnit.getDataLength()+AvcDecoder.getInputPaddingSize()];
int offset = 0;
for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
for (ByteBufferDescriptor bbd = decodeUnit.getBufferHead();
bbd != null; bbd = bbd.nextDescriptor) {
System.arraycopy(bbd.data, bbd.offset, data, offset, bbd.length);
offset += bbd.length;
}
@@ -250,7 +251,7 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
boolean success = (AvcDecoder.decode(data, 0, decodeUnit.getDataLength()) == 0);
if (success) {
long timeAfterDecode = System.currentTimeMillis();
long timeAfterDecode = MediaCodecHelper.getMonotonicMillis();
// Add delta time to the totals (excluding probable outliers)
long delta = timeAfterDecode - decodeUnit.getReceiveTimestamp();
@@ -1,5 +1,6 @@
package com.limelight.binding.video;
import com.limelight.nvstream.av.DecodeUnit;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.av.video.VideoDepacketizer;
@@ -55,6 +56,11 @@ public class ConfigurableDecoderRenderer extends EnhancedDecoderRenderer {
return decoderRenderer.getCapabilities();
}
@Override
public void directSubmitDecodeUnit(DecodeUnit du) {
decoderRenderer.directSubmitDecodeUnit(du);
}
@Override
public int getAverageDecoderLatency() {
if (decoderRenderer != null) {
@@ -2,6 +2,6 @@ package com.limelight.binding.video;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
public abstract class EnhancedDecoderRenderer implements VideoDecoderRenderer {
public abstract class EnhancedDecoderRenderer extends VideoDecoderRenderer {
public abstract String getDecoderName();
}
@@ -4,6 +4,7 @@ import java.nio.ByteBuffer;
import java.util.Locale;
import java.util.concurrent.locks.LockSupport;
import org.jcodec.codecs.h264.H264Utils;
import org.jcodec.codecs.h264.io.model.SeqParameterSet;
import org.jcodec.codecs.h264.io.model.VUIParameters;
@@ -13,7 +14,6 @@ import com.limelight.nvstream.av.DecodeUnit;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.av.video.VideoDepacketizer;
import android.annotation.TargetApi;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
@@ -22,15 +22,17 @@ import android.media.MediaCodec.CodecException;
import android.os.Build;
import android.view.SurfaceHolder;
@SuppressWarnings("unused")
public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
private ByteBuffer[] videoDecoderInputBuffers;
// Used on versions < 5.0
private ByteBuffer[] legacyInputBuffers;
private MediaCodec videoDecoder;
private Thread rendererThread;
private boolean needsSpsBitstreamFixup, isExynos4;
private final boolean needsSpsBitstreamFixup, isExynos4;
private VideoDepacketizer depacketizer;
private boolean adaptivePlayback;
private final boolean adaptivePlayback, directSubmit;
private final boolean constrainedHighProfile;
private int initialWidth, initialHeight;
private boolean needsBaselineSpsHack;
@@ -46,9 +48,6 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
private int numPpsIn;
private int numIframeIn;
private static final boolean ENABLE_ASYNC_RENDERER = false;
@TargetApi(Build.VERSION_CODES.KITKAT)
public MediaCodecDecoderRenderer() {
//dumpDecoders();
@@ -58,15 +57,20 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
}
if (decoder == null) {
// This case is handled later in setup()
needsSpsBitstreamFixup = isExynos4 =
adaptivePlayback = directSubmit =
constrainedHighProfile = false;
return;
}
decoderName = decoder.getName();
// Set decoder-specific attributes
directSubmit = MediaCodecHelper.decoderCanDirectSubmit(decoderName, decoder);
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(decoderName, decoder);
needsSpsBitstreamFixup = MediaCodecHelper.decoderNeedsSpsBitstreamRestrictions(decoderName, decoder);
needsBaselineSpsHack = MediaCodecHelper.decoderNeedsBaselineSpsHack(decoderName, decoder);
constrainedHighProfile = MediaCodecHelper.decoderNeedsConstrainedHighProfile(decoderName, decoder);
isExynos4 = MediaCodecHelper.isExynos4Device();
if (needsSpsBitstreamFixup) {
LimeLog.info("Decoder "+decoderName+" needs SPS bitstream restrictions fixup");
@@ -74,12 +78,17 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
if (needsBaselineSpsHack) {
LimeLog.info("Decoder "+decoderName+" needs baseline SPS hack");
}
if (constrainedHighProfile) {
LimeLog.info("Decoder "+decoderName+" needs constrained high profile");
}
if (isExynos4) {
LimeLog.info("Decoder "+decoderName+" is on Exynos 4");
}
if (directSubmit) {
LimeLog.info("Decoder "+decoderName+" will use direct submit");
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
this.initialWidth = width;
@@ -107,52 +116,6 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
videoFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, height);
}
// On Lollipop, we use asynchronous mode to avoid having a busy looping renderer thread
if (ENABLE_ASYNC_RENDERER && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
videoDecoder.setCallback(new MediaCodec.Callback() {
@Override
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
LimeLog.info("Output format changed");
LimeLog.info("New output Format: " + format);
}
@Override
public void onOutputBufferAvailable(MediaCodec codec, int index,
BufferInfo info) {
try {
// FIXME: It looks like we can't frameskip here
codec.releaseOutputBuffer(index, true);
} catch (Exception e) {
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
}
}
@Override
public void onInputBufferAvailable(MediaCodec codec, int index) {
try {
submitDecodeUnit(depacketizer.takeNextDecodeUnit(), codec.getInputBuffer(index), index);
} catch (InterruptedException e) {
// What do we do here?
e.printStackTrace();
} catch (Exception e) {
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
}
}
@Override
public void onError(MediaCodec codec, CodecException e) {
if (e.isTransient()) {
LimeLog.warning(e.getDiagnosticInfo());
e.printStackTrace();
}
else {
LimeLog.severe(e.getDiagnosticInfo());
e.printStackTrace();
}
}
});
}
videoDecoder.configure(videoFormat, ((SurfaceHolder)renderTarget).getSurface(), null, 0);
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
@@ -161,8 +124,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
return true;
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void handleDecoderException(MediaCodecDecoderRenderer dr, Exception e, ByteBuffer buf, int codecFlags) {
private void handleDecoderException(Exception e, ByteBuffer buf, int codecFlags) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (e instanceof CodecException) {
CodecException codecExc = (CodecException) e;
@@ -177,23 +139,100 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
}
}
if (buf != null || codecFlags != 0) {
throw new RendererException(dr, e, buf, codecFlags);
}
else {
throw new RendererException(dr, e);
// Only throw if this happens at the beginning of a stream
if (totalFrames < 60) {
if (buf != null || codecFlags != 0) {
throw new RendererException(this, e, buf, codecFlags);
}
else {
throw new RendererException(this, e);
}
}
}
private void startRendererThread()
private void startDirectSubmitRendererThread()
{
rendererThread = new Thread() {
@Override
public void run() {
BufferInfo info = new BufferInfo();
while (!isInterrupted()) {
try {
// Try to output a frame
int outIndex = videoDecoder.dequeueOutputBuffer(info, 50000);
if (outIndex >= 0) {
long presentationTimeUs = info.presentationTimeUs;
int lastIndex = outIndex;
// Get the last output buffer in the queue
while ((outIndex = videoDecoder.dequeueOutputBuffer(info, 0)) >= 0) {
videoDecoder.releaseOutputBuffer(lastIndex, false);
lastIndex = outIndex;
presentationTimeUs = info.presentationTimeUs;
}
// Render the last buffer
videoDecoder.releaseOutputBuffer(lastIndex, true);
// Add delta time to the totals (excluding probable outliers)
long delta = MediaCodecHelper.getMonotonicMillis() - (presentationTimeUs / 1000);
if (delta >= 0 && delta < 1000) {
decoderTimeMs += delta;
totalTimeMs += delta;
}
} else {
switch (outIndex) {
case MediaCodec.INFO_TRY_AGAIN_LATER:
break;
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
LimeLog.info("Output format changed");
LimeLog.info("New output Format: " + videoDecoder.getOutputFormat());
break;
default:
break;
}
}
} catch (Exception e) {
handleDecoderException(e, null, 0);
}
}
}
};
rendererThread.setName("Video - Renderer (MediaCodec)");
rendererThread.setPriority(Thread.NORM_PRIORITY + 2);
rendererThread.start();
}
private int dequeueInputBuffer(boolean wait, boolean infiniteWait) {
int index;
long startTime, queueTime;
startTime = MediaCodecHelper.getMonotonicMillis();
index = videoDecoder.dequeueInputBuffer(wait ? (infiniteWait ? -1 : 3000) : 0);
if (index < 0) {
return index;
}
queueTime = MediaCodecHelper.getMonotonicMillis();
if (queueTime - startTime >= 20) {
LimeLog.warning("Queue input buffer ran long: " + (queueTime - startTime) + " ms");
}
return index;
}
private void startLegacyRendererThread()
{
rendererThread = new Thread() {
@SuppressWarnings("deprecation")
@Override
public void run() {
BufferInfo info = new BufferInfo();
DecodeUnit du = null;
int inputIndex = -1;
long lastDuDequeueTime = 0;
while (!isInterrupted())
{
// In order to get as much data to the decoder as early as possible,
@@ -201,22 +240,26 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
if (inputIndex == -1 && du == null) {
try {
for (int i = 0; i < 5; i++) {
inputIndex = videoDecoder.dequeueInputBuffer(0);
inputIndex = dequeueInputBuffer(false, false);
du = depacketizer.pollNextDecodeUnit();
if (du != null) {
lastDuDequeueTime = MediaCodecHelper.getMonotonicMillis();
notifyDuReceived(du);
}
// Stop if we can't get a DU or input buffer
if (du == null || inputIndex == -1) {
break;
}
submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex);
submitDecodeUnit(du, inputIndex);
du = null;
inputIndex = -1;
}
} catch (Exception e) {
inputIndex = -1;
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
handleDecoderException(e, null, 0);
}
}
@@ -231,22 +274,31 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
// If we've got a DU waiting to be given to the decoder,
// wait a full 3 ms for an input buffer. Otherwise
// just see if we can get one immediately.
inputIndex = videoDecoder.dequeueInputBuffer(du != null ? 3000 : 0);
inputIndex = dequeueInputBuffer(du != null, false);
} catch (Exception e) {
inputIndex = -1;
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
handleDecoderException(e, null, 0);
}
}
// Grab a decode unit if we don't have one already
if (du == null) {
du = depacketizer.pollNextDecodeUnit();
if (du != null) {
lastDuDequeueTime = MediaCodecHelper.getMonotonicMillis();
notifyDuReceived(du);
}
}
// If we've got both a decode unit and an input buffer, we'll
// submit now. Otherwise, we wait until we have one.
if (du != null && inputIndex >= 0) {
submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex);
long submissionTime = MediaCodecHelper.getMonotonicMillis();
if (submissionTime - lastDuDequeueTime >= 20) {
LimeLog.warning("Receiving an input buffer took too long: "+(submissionTime - lastDuDequeueTime)+" ms");
}
submitDecodeUnit(du, inputIndex);
// DU and input buffer have both been consumed
du = null;
@@ -272,33 +324,30 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
videoDecoder.releaseOutputBuffer(lastIndex, true);
// Add delta time to the totals (excluding probable outliers)
long delta = System.currentTimeMillis()-(presentationTimeUs/1000);
long delta = MediaCodecHelper.getMonotonicMillis()-(presentationTimeUs/1000);
if (delta >= 0 && delta < 1000) {
decoderTimeMs += delta;
totalTimeMs += delta;
}
} else {
switch (outIndex) {
case MediaCodec.INFO_TRY_AGAIN_LATER:
// Getting an input buffer may already block
// so don't park if we still need to do that
if (inputIndex >= 0) {
LockSupport.parkNanos(1);
}
break;
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
LimeLog.info("Output buffers changed");
break;
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
LimeLog.info("Output format changed");
LimeLog.info("New output Format: " + videoDecoder.getOutputFormat());
break;
default:
break;
case MediaCodec.INFO_TRY_AGAIN_LATER:
// Getting an input buffer may already block
// so don't park if we still need to do that
if (inputIndex >= 0) {
LockSupport.parkNanos(1);
}
break;
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
LimeLog.info("Output format changed");
LimeLog.info("New output Format: " + videoDecoder.getOutputFormat());
break;
default:
break;
}
}
} catch (Exception e) {
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
handleDecoderException(e, null, 0);
}
}
}
@@ -316,11 +365,17 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
// Start the decoder
videoDecoder.start();
// On devices pre-Lollipop, we'll use a rendering thread
if (!ENABLE_ASYNC_RENDERER || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
videoDecoderInputBuffers = videoDecoder.getInputBuffers();
startRendererThread();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
legacyInputBuffers = videoDecoder.getInputBuffers();
}
if (directSubmit) {
startDirectSubmitRendererThread();
}
else {
startLegacyRendererThread();
}
return true;
}
@@ -353,11 +408,11 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
for (i = 0; i < 25; i++) {
try {
videoDecoder.queueInputBuffer(inputBufferIndex,
0, length,
offset, length,
timestampUs, codecFlags);
break;
} catch (Exception e) {
handleDecoderException(this, e, null, codecFlags);
handleDecoderException(e, null, codecFlags);
lastException = e;
}
}
@@ -367,16 +422,44 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
}
}
@SuppressWarnings("deprecation")
private void submitDecodeUnit(DecodeUnit decodeUnit, ByteBuffer buf, int inputBufferIndex) {
long currentTime = System.currentTimeMillis();
long delta = currentTime-decodeUnit.getReceiveTimestamp();
if (delta >= 0 && delta < 1000) {
totalTimeMs += currentTime-decodeUnit.getReceiveTimestamp();
totalFrames++;
// Using the new getInputBuffer() API on Lollipop allows
// the framework to do some performance optimizations for us
private ByteBuffer getEmptyInputBuffer(int inputBufferIndex) {
ByteBuffer buf;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
buf = videoDecoder.getInputBuffer(inputBufferIndex);
}
else {
buf = legacyInputBuffers[inputBufferIndex];
// Clear old input data pre-Lollipop
buf.clear();
}
long timestampUs = currentTime * 1000;
return buf;
}
private void doProfileSpecificSpsPatching(SeqParameterSet sps) {
// Some devices benefit from setting constraint flags 4 & 5 to make this Constrained
// High Profile which allows the decoder to assume there will be no B-frames and
// reduce delay and buffering accordingly. Some devices (Marvell, Exynos 4) don't
// like it so we only set them on devices that are confirmed to benefit from it.
if (sps.profile_idc == 100 && constrainedHighProfile) {
LimeLog.info("Setting constraint set flags for constrained high profile");
sps.constraint_set_4_flag = true;
sps.constraint_set_5_flag = true;
}
else {
// Force the constraints unset otherwise (some may be set by default)
sps.constraint_set_4_flag = false;
sps.constraint_set_5_flag = false;
}
}
@SuppressWarnings("deprecation")
private void submitDecodeUnit(DecodeUnit decodeUnit, int inputBufferIndex) {
long timestampUs = System.nanoTime() / 1000;
if (timestampUs <= lastTimestampUs) {
// We can't submit multiple buffers with the same timestamp
// so bump it up by one before queuing
@@ -384,8 +467,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
}
lastTimestampUs = timestampUs;
// Clear old input data
buf.clear();
ByteBuffer buf = getEmptyInputBuffer(inputBufferIndex);
int codecFlags = 0;
int decodeUnitFlags = decodeUnit.getFlags();
@@ -400,7 +482,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
boolean needsSpsReplay = false;
if ((decodeUnitFlags & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) {
ByteBufferDescriptor header = decodeUnit.getBufferList().get(0);
ByteBufferDescriptor header = decodeUnit.getBufferHead();
if (header.data[header.offset+4] == 0x67) {
numSpsIn++;
@@ -409,7 +491,9 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
// Skip to the start of the NALU data
spsBuf.position(header.offset+5);
SeqParameterSet sps = SeqParameterSet.read(spsBuf);
// The H264Utils.readSPS function safely handles
// Annex B NALUs (including NALUs with escape sequences)
SeqParameterSet sps = H264Utils.readSPS(spsBuf);
// Some decoders rely on H264 level to decide how many buffers are needed
// Since we only need one frame buffered, we'll set the level as low as we can
@@ -437,19 +521,44 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
LimeLog.info("Patching num_ref_frames in SPS");
sps.num_ref_frames = 1;
// GFE 2.5.11 changed the SPS to add additional extensions
// Some devices don't like these so we remove them here.
sps.vuiParams.video_signal_type_present_flag = false;
sps.vuiParams.colour_description_present_flag = false;
sps.vuiParams.chroma_loc_info_present_flag = false;
if (needsSpsBitstreamFixup || isExynos4) {
// The SPS that comes in the current H264 bytestream doesn't set bitstream_restriction_flag
// or max_dec_frame_buffering which increases decoding latency on Tegra.
LimeLog.info("Adding bitstream restrictions");
sps.vuiParams.bitstreamRestriction = new VUIParameters.BitstreamRestriction();
sps.vuiParams.bitstreamRestriction.motion_vectors_over_pic_boundaries_flag = true;
// GFE 2.5.11 started sending bitstream restrictions
if (sps.vuiParams.bitstreamRestriction == null) {
LimeLog.info("Adding bitstream restrictions");
sps.vuiParams.bitstreamRestriction = new VUIParameters.BitstreamRestriction();
sps.vuiParams.bitstreamRestriction.motion_vectors_over_pic_boundaries_flag = true;
sps.vuiParams.bitstreamRestriction.log2_max_mv_length_horizontal = 16;
sps.vuiParams.bitstreamRestriction.log2_max_mv_length_vertical = 16;
sps.vuiParams.bitstreamRestriction.num_reorder_frames = 0;
}
else {
LimeLog.info("Patching bitstream restrictions");
}
// Some devices throw errors if max_dec_frame_buffering < num_ref_frames
sps.vuiParams.bitstreamRestriction.max_dec_frame_buffering = sps.num_ref_frames;
// These values are the defaults for the fields, but they are more aggressive
// than what GFE sends in 2.5.11, but it doesn't seem to cause picture problems.
sps.vuiParams.bitstreamRestriction.max_bytes_per_pic_denom = 2;
sps.vuiParams.bitstreamRestriction.max_bits_per_mb_denom = 1;
sps.vuiParams.bitstreamRestriction.log2_max_mv_length_horizontal = 16;
sps.vuiParams.bitstreamRestriction.log2_max_mv_length_vertical = 16;
sps.vuiParams.bitstreamRestriction.num_reorder_frames = 0;
sps.vuiParams.bitstreamRestriction.max_dec_frame_buffering = 1;
// log2_max_mv_length_horizontal and log2_max_mv_length_vertical are set to more
// conservative values by GFE 2.5.11. We'll let those values stand.
}
else {
// Devices that didn't/couldn't get bitstream restrictions before GFE 2.5.11
// will continue to not receive them now
sps.vuiParams.bitstreamRestriction = null;
}
// If we need to hack this SPS to say we're baseline, do so now
@@ -459,11 +568,16 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
savedSps = sps;
}
// Patch the SPS constraint flags
doProfileSpecificSpsPatching(sps);
// Write the annex B header
buf.put(header.data, header.offset, 5);
// Write the modified SPS to the input buffer
sps.write(buf);
// The H264Utils.writeSPS function safely handles
// Annex B NALUs (including NALUs with escape sequences)
ByteBuffer escapedNalu = H264Utils.writeSPS(sps, header.length);
buf.put(escapedNalu);
queueInputBuffer(inputBufferIndex,
0, buf.position(),
@@ -485,8 +599,8 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
}
// Copy data from our buffer list into the input buffer
for (ByteBufferDescriptor desc : decodeUnit.getBufferList())
{
for (ByteBufferDescriptor desc = decodeUnit.getBufferHead();
desc != null; desc = desc.nextDescriptor) {
buf.put(desc.data, desc.offset, desc.length);
}
@@ -502,10 +616,8 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
}
private void replaySps() {
int inputIndex = videoDecoder.dequeueInputBuffer(-1);
ByteBuffer inputBuffer = videoDecoderInputBuffers[inputIndex];
inputBuffer.clear();
int inputIndex = dequeueInputBuffer(true, true);
ByteBuffer inputBuffer = getEmptyInputBuffer(inputIndex);
// Write the Annex B header
inputBuffer.put(new byte[]{0x00, 0x00, 0x00, 0x01, 0x67});
@@ -513,6 +625,9 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
// Switch the H264 profile back to high
savedSps.profile_idc = 100;
// Patch the SPS constraint flags
doProfileSpecificSpsPatching(savedSps);
// Write the SPS data
savedSps.write(inputBuffer);
@@ -522,7 +637,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
// Queue the new SPS
queueInputBuffer(inputIndex,
0, inputBuffer.position(),
System.currentTimeMillis() * 1000,
System.nanoTime() / 1000,
MediaCodec.BUFFER_FLAG_CODEC_CONFIG);
LimeLog.info("SPS replay complete");
@@ -530,8 +645,15 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
@Override
public int getCapabilities() {
return adaptivePlayback ?
int caps = 0;
caps |= adaptivePlayback ?
VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION : 0;
caps |= directSubmit ?
VideoDecoderRenderer.CAPABILITY_DIRECT_SUBMIT : 0;
return caps;
}
@Override
@@ -555,6 +677,35 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
return decoderName;
}
private void notifyDuReceived(DecodeUnit du) {
long currentTime = MediaCodecHelper.getMonotonicMillis();
long delta = currentTime-du.getReceiveTimestamp();
if (delta >= 0 && delta < 1000) {
totalTimeMs += currentTime-du.getReceiveTimestamp();
totalFrames++;
}
}
@Override
public void directSubmitDecodeUnit(DecodeUnit du) {
int inputIndex;
notifyDuReceived(du);
for (;;) {
try {
inputIndex = dequeueInputBuffer(true, true);
break;
} catch (Exception e) {
handleDecoderException(e, null, 0);
}
}
if (inputIndex >= 0) {
submitDecodeUnit(du, inputIndex);
}
}
public class RendererException extends RuntimeException {
private static final long serialVersionUID = 8985937536997012406L;
@@ -582,6 +733,8 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
str += "Initial video dimensions: "+renderer.initialWidth+"x"+renderer.initialHeight+"\n";
str += "In stats: "+renderer.numSpsIn+", "+renderer.numPpsIn+", "+renderer.numIframeIn+"\n";
str += "Total frames: "+renderer.totalFrames+"\n";
str += "Average end-to-end client latency: "+getAverageEndToEndLatency()+"ms\n";
str += "Average hardware decoder latency: "+getAverageDecoderLatency()+"ms\n";
if (currentBuffer != null) {
str += "Current buffer: ";
@@ -26,7 +26,23 @@ public class MediaCodecHelper {
private static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
private static final List<String> whitelistedAdaptiveResolutionPrefixes;
private static final List<String> baselineProfileHackPrefixes;
private static final List<String> directSubmitPrefixes;
private static final List<String> constrainedHighProfilePrefixes;
static {
directSubmitPrefixes = new LinkedList<String>();
// These decoders have low enough input buffer latency that they
// can be directly invoked from the receive thread
directSubmitPrefixes.add("omx.qcom");
directSubmitPrefixes.add("omx.sec");
directSubmitPrefixes.add("omx.exynos");
directSubmitPrefixes.add("omx.intel");
directSubmitPrefixes.add("omx.brcm");
directSubmitPrefixes.add("omx.TI");
directSubmitPrefixes.add("omx.arc");
}
static {
preferredDecoders = new LinkedList<String>();
}
@@ -43,7 +59,6 @@ public class MediaCodecHelper {
spsFixupBitstreamFixupDecoderPrefixes = new LinkedList<String>();
spsFixupBitstreamFixupDecoderPrefixes.add("omx.nvidia");
spsFixupBitstreamFixupDecoderPrefixes.add("omx.qcom");
spsFixupBitstreamFixupDecoderPrefixes.add("omx.mtk");
spsFixupBitstreamFixupDecoderPrefixes.add("omx.brcm");
baselineProfileHackPrefixes = new LinkedList<String>();
@@ -54,6 +69,9 @@ public class MediaCodecHelper {
whitelistedAdaptiveResolutionPrefixes.add("omx.qcom");
whitelistedAdaptiveResolutionPrefixes.add("omx.sec");
whitelistedAdaptiveResolutionPrefixes.add("omx.TI");
constrainedHighProfilePrefixes = new LinkedList<String>();
constrainedHighProfilePrefixes.add("omx.intel");
}
private static boolean isDecoderInList(List<String> decoderList, String decoderName) {
@@ -68,6 +86,10 @@ public class MediaCodecHelper {
return false;
}
public static long getMonotonicMillis() {
return System.nanoTime() / 1000000L;
}
@TargetApi(Build.VERSION_CODES.KITKAT)
public static boolean decoderSupportsAdaptivePlayback(String decoderName, MediaCodecInfo decoderInfo) {
@@ -97,6 +119,14 @@ public class MediaCodecHelper {
return false;
}
public static boolean decoderNeedsConstrainedHighProfile(String decoderName, MediaCodecInfo decoderInfo) {
return isDecoderInList(constrainedHighProfilePrefixes, decoderName);
}
public static boolean decoderCanDirectSubmit(String decoderName, MediaCodecInfo decoderInfo) {
return isDecoderInList(directSubmitPrefixes, decoderName) && !isExynos4Device();
}
public static boolean decoderNeedsSpsBitstreamRestrictions(String decoderName, MediaCodecInfo decoderInfo) {
return isDecoderInList(spsFixupBitstreamFixupDecoderPrefixes, decoderName);
@@ -1,9 +1,11 @@
package com.limelight.computers;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringReader;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
@@ -29,8 +31,12 @@ import android.os.IBinder;
import org.xmlpull.v1.XmlPullParserException;
public class ComputerManagerService extends Service {
private static final int POLLING_PERIOD_MS = 3000;
private static final int SERVERINFO_POLLING_PERIOD_MS = 1500;
private static final int APPLIST_POLLING_PERIOD_MS = 30000;
private static final int APPLIST_FAILED_POLLING_RETRY_MS = 2000;
private static final int MDNS_QUERY_PERIOD_MS = 1000;
private static final int FAST_POLL_TIMEOUT = 500;
private static final int OFFLINE_POLL_TRIES = 5;
private final ComputerManagerBinder binder = new ComputerManagerBinder();
@@ -64,8 +70,7 @@ public class ComputerManagerService extends Service {
};
// Returns true if the details object was modified
private boolean runPoll(ComputerDetails details, boolean newPc)
{
private boolean runPoll(ComputerDetails details, boolean newPc, int offlineCount) throws InterruptedException {
if (!getLocalDatabaseReference()) {
return false;
}
@@ -73,12 +78,23 @@ public class ComputerManagerService extends Service {
activePolls.incrementAndGet();
// Poll the machine
if (!doPollMachine(details)) {
details.state = ComputerDetails.State.OFFLINE;
details.reachability = ComputerDetails.Reachability.OFFLINE;
}
try {
if (!pollComputer(details)) {
if (!newPc && offlineCount < OFFLINE_POLL_TRIES) {
// Return without calling the listener
releaseLocalDatabaseReference();
return false;
}
activePolls.decrementAndGet();
details.state = ComputerDetails.State.OFFLINE;
details.reachability = ComputerDetails.Reachability.OFFLINE;
}
} catch (InterruptedException e) {
releaseLocalDatabaseReference();
throw e;
} finally {
activePolls.decrementAndGet();
}
// If it's online, update our persistent state
if (details.state == ComputerDetails.State.ONLINE) {
@@ -88,7 +104,7 @@ public class ComputerManagerService extends Service {
if (dbManager.getComputerByName(details.name) == null) {
// It's gone
releaseLocalDatabaseReference();
return true;
return false;
}
}
@@ -104,24 +120,34 @@ public class ComputerManagerService extends Service {
return true;
}
private Thread createPollingThread(final ComputerDetails details) {
private Thread createPollingThread(final PollingTuple tuple) {
Thread t = new Thread() {
@Override
public void run() {
while (!isInterrupted() && pollingActive) {
// Check if this poll has modified the details
runPoll(details, false);
// Wait until the next polling interval
int offlineCount = 0;
while (!isInterrupted() && pollingActive) {
try {
Thread.sleep(POLLING_PERIOD_MS);
// Only allow one request to the machine at a time
synchronized (tuple.networkLock) {
// Check if this poll has modified the details
if (!runPoll(tuple.computer, false, offlineCount)) {
LimeLog.warning(tuple.computer.name + " is offline (try " + offlineCount + ")");
offlineCount++;
} else {
offlineCount = 0;
}
}
// Wait until the next polling interval
Thread.sleep(SERVERINFO_POLLING_PERIOD_MS);
} catch (InterruptedException e) {
break;
}
}
}
};
t.setName("Polling thread for "+details.localIp.getHostAddress());
t.setName("Polling thread for " + tuple.computer.localIp.getHostAddress());
return t;
}
@@ -143,7 +169,7 @@ public class ComputerManagerService extends Service {
// Report this computer initially
listener.notifyComputerUpdated(tuple.computer);
tuple.thread = createPollingThread(tuple.computer);
tuple.thread = createPollingThread(tuple);
tuple.thread.start();
}
}
@@ -260,7 +286,7 @@ public class ComputerManagerService extends Service {
// Start a polling thread if polling is active
if (pollingActive && tuple.thread == null) {
tuple.thread = createPollingThread(details);
tuple.thread = createPollingThread(tuple);
tuple.thread.start();
}
@@ -270,7 +296,10 @@ public class ComputerManagerService extends Service {
}
// If we got here, we didn't find an entry
PollingTuple tuple = new PollingTuple(details, pollingActive ? createPollingThread(details) : null);
PollingTuple tuple = new PollingTuple(details, null);
if (pollingActive) {
tuple.thread = createPollingThread(tuple);
}
pollingTuples.add(tuple);
if (tuple.thread != null) {
tuple.thread.start();
@@ -285,7 +314,11 @@ public class ComputerManagerService extends Service {
fakeDetails.remoteIp = addr;
// Block while we try to fill the details
runPoll(fakeDetails, true);
try {
runPoll(fakeDetails, true, 0);
} catch (InterruptedException e) {
return false;
}
// If the machine is reachable, it was successful
if (fakeDetails.state == ComputerDetails.State.ONLINE) {
@@ -341,6 +374,11 @@ public class ComputerManagerService extends Service {
}
private ComputerDetails tryPollIp(ComputerDetails details, InetAddress ipAddr) {
// Fast poll this address first to determine if we can connect at the TCP layer
if (!fastPollIp(ipAddr)) {
return null;
}
try {
NvHTTP http = new NvHTTP(ipAddr, idManager.getUniqueId(),
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
@@ -361,14 +399,91 @@ public class ComputerManagerService extends Service {
}
}
private boolean pollComputer(ComputerDetails details, boolean localFirst) {
// Just try to establish a TCP connection to speculatively detect a running
// GFE server
private boolean fastPollIp(InetAddress addr) {
Socket s = new Socket();
try {
s.connect(new InetSocketAddress(addr, NvHTTP.HTTPS_PORT), FAST_POLL_TIMEOUT);
s.close();
return true;
} catch (IOException e) {
return false;
}
}
private void startFastPollThread(final InetAddress addr, final boolean[] info) {
Thread t = new Thread() {
@Override
public void run() {
boolean pollRes = fastPollIp(addr);
synchronized (info) {
info[0] = true; // Done
info[1] = pollRes; // Polling result
info.notify();
}
}
};
t.setName("Fast Poll - "+addr.getHostAddress());
t.start();
}
private ComputerDetails.Reachability fastPollPc(final InetAddress local, final InetAddress remote) throws InterruptedException {
final boolean[] remoteInfo = new boolean[2];
final boolean[] localInfo = new boolean[2];
startFastPollThread(local, localInfo);
startFastPollThread(remote, remoteInfo);
// Check local first
synchronized (localInfo) {
while (!localInfo[0]) {
localInfo.wait(500);
}
if (localInfo[1]) {
return ComputerDetails.Reachability.LOCAL;
}
}
// Now remote
synchronized (remoteInfo) {
while (!remoteInfo[0]) {
remoteInfo.wait(500);
}
if (remoteInfo[1]) {
return ComputerDetails.Reachability.REMOTE;
}
}
return ComputerDetails.Reachability.OFFLINE;
}
private ReachabilityTuple pollForReachability(ComputerDetails details) throws InterruptedException {
ComputerDetails polledDetails;
ComputerDetails.Reachability reachability;
// If the local address is routable across the Internet,
// always consider this PC remote to be conservative
if (details.localIp.equals(details.remoteIp)) {
localFirst = false;
reachability = ComputerDetails.Reachability.REMOTE;
}
else {
// Do a TCP-level connection to the HTTP server to see if it's listening
LimeLog.info("Starting fast poll for "+details.name+" ("+details.localIp+", "+details.remoteIp+")");
reachability = fastPollPc(details.localIp, details.remoteIp);
LimeLog.info("Fast poll for "+details.name+" returned "+reachability.toString());
// If no connection could be established to either IP address, there's nothing we can do
if (reachability == ComputerDetails.Reachability.OFFLINE) {
return null;
}
}
boolean localFirst = (reachability == ComputerDetails.Reachability.LOCAL);
if (localFirst) {
polledDetails = tryPollIp(details, details.localIp);
@@ -377,6 +492,7 @@ public class ComputerManagerService extends Service {
polledDetails = tryPollIp(details, details.remoteIp);
}
InetAddress reachableAddr = null;
if (polledDetails == null && !details.localIp.equals(details.remoteIp)) {
// Failed, so let's try the fallback
if (!localFirst) {
@@ -386,40 +502,67 @@ public class ComputerManagerService extends Service {
polledDetails = tryPollIp(details, details.remoteIp);
}
// The fallback poll worked
if (polledDetails != null) {
polledDetails.reachability = !localFirst ? ComputerDetails.Reachability.LOCAL :
ComputerDetails.Reachability.REMOTE;
// The fallback poll worked
reachableAddr = !localFirst ? details.localIp : details.remoteIp;
}
}
else if (polledDetails != null) {
polledDetails.reachability = localFirst ? ComputerDetails.Reachability.LOCAL :
ComputerDetails.Reachability.REMOTE;
reachableAddr = localFirst ? details.localIp : details.remoteIp;
}
// Machine was unreachable both tries
if (polledDetails == null) {
if (reachableAddr == null) {
return null;
}
if (polledDetails.remoteIp.equals(reachableAddr)) {
polledDetails.reachability = ComputerDetails.Reachability.REMOTE;
}
else if (polledDetails.localIp.equals(reachableAddr)) {
polledDetails.reachability = ComputerDetails.Reachability.LOCAL;
}
else {
polledDetails.reachability = ComputerDetails.Reachability.UNKNOWN;
}
return new ReachabilityTuple(polledDetails, reachableAddr);
}
private boolean pollComputer(ComputerDetails details) throws InterruptedException {
ReachabilityTuple initialReachTuple = pollForReachability(details);
if (initialReachTuple == null) {
return false;
}
// If we got here, it's reachable
details.update(polledDetails);
return true;
}
if (initialReachTuple.computer.reachability == ComputerDetails.Reachability.UNKNOWN) {
// Neither IP address reported in the serverinfo response was the one we used.
// Poll again to see if we can contact this machine on either of its reported addresses.
ReachabilityTuple confirmationReachTuple = pollForReachability(initialReachTuple.computer);
if (confirmationReachTuple == null) {
// Neither of those seem to work, so we'll hold onto the address that did work
initialReachTuple.computer.localIp = initialReachTuple.reachableAddress;
initialReachTuple.computer.reachability = ComputerDetails.Reachability.LOCAL;
}
else {
// We got it on one of the returned addresses; replace the original reach tuple
// with the new one
initialReachTuple = confirmationReachTuple;
}
}
private boolean doPollMachine(ComputerDetails details) {
if (details.reachability == ComputerDetails.Reachability.UNKNOWN ||
details.reachability == ComputerDetails.Reachability.OFFLINE) {
// Always try local first to avoid potential UDP issues when
// attempting to stream via the router's external IP address
// behind its NAT
return pollComputer(details, true);
}
else {
// If we're already reached a machine via a particular IP address,
// always try that one first
return pollComputer(details, details.reachability == ComputerDetails.Reachability.LOCAL);
// Save the old MAC address
String savedMacAddress = details.macAddress;
// If we got here, it's reachable
details.update(initialReachTuple.computer);
// If the new MAC address is empty, restore the old one (workaround for GFE bug)
if (details.macAddress.equals("00:00:00:00:00:00") && savedMacAddress != null) {
LimeLog.info("MAC address was empty; using existing value: "+savedMacAddress);
details.macAddress = savedMacAddress;
}
return true;
}
@Override
@@ -470,6 +613,7 @@ public class ComputerManagerService extends Service {
private Thread thread;
private final ComputerDetails computer;
private final Object pollEvent = new Object();
private boolean receivedAppList = false;
public ApplistPoller(ComputerDetails computer) {
this.computer = computer;
@@ -484,7 +628,15 @@ public class ComputerManagerService extends Service {
private boolean waitPollingDelay() {
try {
synchronized (pollEvent) {
pollEvent.wait(POLLING_PERIOD_MS);
if (receivedAppList) {
// If we've already reported an app list successfully,
// wait the full polling period
pollEvent.wait(APPLIST_POLLING_PERIOD_MS);
}
else {
// If we've failed to get an app list so far, retry much earlier
pollEvent.wait(APPLIST_FAILED_POLLING_RETRY_MS);
}
}
} catch (InterruptedException e) {
return false;
@@ -493,6 +645,18 @@ public class ComputerManagerService extends Service {
return thread != null && !thread.isInterrupted();
}
private PollingTuple getPollingTuple(ComputerDetails details) {
synchronized (pollingTuples) {
for (PollingTuple tuple : pollingTuples) {
if (details.uuid.equals(tuple.computer.uuid)) {
return tuple;
}
}
}
return null;
}
public void start() {
thread = new Thread() {
@Override
@@ -523,18 +687,43 @@ public class ComputerManagerService extends Service {
NvHTTP http = new NvHTTP(selectedAddr, idManager.getUniqueId(),
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
PollingTuple tuple = getPollingTuple(computer);
try {
// Query the app list from the server
String appList = http.getAppListRaw();
String appList;
if (tuple != null) {
// If we're polling this machine too, grab the network lock
// while doing the app list request to prevent other requests
// from being issued in the meantime.
synchronized (tuple.networkLock) {
appList = http.getAppListRaw();
}
}
else {
// No polling is happening now, so we just call it directly
appList = http.getAppListRaw();
}
List<NvApp> list = NvHTTP.getAppListByReader(new StringReader(appList));
if (appList != null && !appList.isEmpty() && !list.isEmpty()) {
// Open the cache file
FileOutputStream cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid.toString());
CacheHelper.writeStringToOutputStream(cacheOut, appList);
cacheOut.close();
OutputStream cacheOut = null;
try {
cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid.toString());
CacheHelper.writeStringToOutputStream(cacheOut, appList);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (cacheOut != null) {
cacheOut.close();
}
} catch (IOException ignored) {}
}
// Update the computer
computer.rawAppList = appList;
receivedAppList = true;
// Notify that the app list has been updated
// and ensure that the thread is still active
@@ -571,9 +760,21 @@ public class ComputerManagerService extends Service {
class PollingTuple {
public Thread thread;
public final ComputerDetails computer;
public final Object networkLock;
public PollingTuple(ComputerDetails computer, Thread thread) {
this.computer = computer;
this.thread = thread;
this.networkLock = new Object();
}
}
class ReachabilityTuple {
public final InetAddress reachableAddress;
public final ComputerDetails computer;
public ReachabilityTuple(ComputerDetails computer, InetAddress reachableAddress) {
this.computer = computer;
this.reachableAddress = reachableAddress;
}
}
@@ -1,110 +1,68 @@
package com.limelight.grid;
import android.content.Context;
import android.graphics.Bitmap;
import android.app.Activity;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.widget.ImageView;
import android.widget.TextView;
import com.koushikdutta.async.future.FutureCallback;
import com.koushikdutta.ion.Ion;
import com.limelight.AppView;
import com.limelight.LimeLog;
import com.limelight.R;
import com.limelight.binding.PlatformBinding;
import com.limelight.grid.assets.CachedAppAssetLoader;
import com.limelight.grid.assets.DiskAssetLoader;
import com.limelight.grid.assets.MemoryAssetLoader;
import com.limelight.grid.assets.NetworkAssetLoader;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.LimelightCryptoProvider;
import com.limelight.utils.CacheHelper;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.UUID;
import java.util.concurrent.Future;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509KeyManager;
import javax.net.ssl.X509TrustManager;
import java.security.cert.X509Certificate;
@SuppressWarnings("unchecked")
public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
private static final int ART_WIDTH_PX = 300;
private static final int SMALL_WIDTH_DP = 100;
private static final int LARGE_WIDTH_DP = 150;
private final ComputerDetails computer;
private final String uniqueId;
private final LimelightCryptoProvider cryptoProvider;
private final SSLContext sslContext;
private final HashMap<ImageView, Future> pendingRequests = new HashMap<ImageView, Future>();
private final CachedAppAssetLoader loader;
public AppGridAdapter(Context context, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) throws NoSuchAlgorithmException, KeyManagementException {
super(context, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item), R.drawable.image_loading);
public AppGridAdapter(Activity activity, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) {
super(activity, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item), R.drawable.image_loading);
this.computer = computer;
this.uniqueId = uniqueId;
int dpi = activity.getResources().getDisplayMetrics().densityDpi;
int dp;
cryptoProvider = PlatformBinding.getCryptoProvider(context);
if (small) {
dp = SMALL_WIDTH_DP;
}
else {
dp = LARGE_WIDTH_DP;
}
sslContext = SSLContext.getInstance("SSL");
sslContext.init(ourKeyman, trustAllCerts, new SecureRandom());
double scalingDivisor = ART_WIDTH_PX / (dp * (dpi / 160.0));
if (scalingDivisor < 1.0) {
// We don't want to make them bigger before draw-time
scalingDivisor = 1.0;
}
LimeLog.info("Art scaling divisor: " + scalingDivisor);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = (int) scalingDivisor;
this.loader = new CachedAppAssetLoader(computer, scalingDivisor,
new NetworkAssetLoader(context, uniqueId),
new MemoryAssetLoader(),
new DiskAssetLoader(context.getCacheDir()),
BitmapFactory.decodeResource(activity.getResources(),
R.drawable.image_loading, options));
}
private final TrustManager[] trustAllCerts = new TrustManager[] {
new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
public void checkClientTrusted(X509Certificate[] certs, String authType) {}
public void checkServerTrusted(X509Certificate[] certs, String authType) {}
}};
private final KeyManager[] ourKeyman = new 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; }
};
public void cancelQueuedOperations() {
loader.cancelForegroundLoads();
loader.cancelBackgroundLoads();
loader.freeCacheMemory();
}
private void sortList() {
Collections.sort(itemList, new Comparator<AppView.AppObject>() {
@@ -115,62 +73,22 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
});
}
private InetAddress getCurrentAddress() {
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
return computer.localIp;
}
else {
return computer.remoteIp;
}
}
public void addApp(AppView.AppObject app) {
// Queue a request to fetch this bitmap into cache
loader.queueCacheLoad(app.app);
// Add the app to our sorted list
itemList.add(app);
sortList();
}
public void abortPendingRequests() {
HashMap<ImageView, Future> tempMap;
synchronized (pendingRequests) {
// Copy the pending requests under a lock
tempMap = new HashMap<ImageView, Future>(pendingRequests);
}
for (Future f : tempMap.values()) {
if (!f.isCancelled() && !f.isDone()) {
f.cancel(true);
}
}
synchronized (pendingRequests) {
// Remove cancelled requests
for (ImageView v : tempMap.keySet()) {
pendingRequests.remove(v);
}
}
public void removeApp(AppView.AppObject app) {
itemList.remove(app);
}
// TODO: Handle pruning of bitmap cache
private void populateBitmapCache(UUID uuid, int appId, Bitmap bitmap) {
try {
// PNG ignores quality setting
FileOutputStream out = CacheHelper.openCacheFileForOutput(context.getCacheDir(), "boxart", uuid.toString(), appId+".png");
bitmap.compress(Bitmap.CompressFormat.PNG, 0, out);
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public boolean populateImageView(final ImageView imgView, final AppView.AppObject obj) {
// Clear existing contents of the image view
imgView.setAlpha(0.0f);
// Check the on-disk cache
new ImageCacheRequest(imgView, obj.app.getAppId()).execute();
public boolean populateImageView(ImageView imgView, AppView.AppObject obj) {
// Let the cached asset loader handle it
loader.populateImageView(obj.app, imgView);
return true;
}
@@ -194,96 +112,4 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
// No overlay
return false;
}
private class ImageCacheRequest extends AsyncTask<Void, Void, Bitmap> {
private final ImageView view;
private final int appId;
public ImageCacheRequest(ImageView view, int appId) {
this.view = view;
this.appId = appId;
}
@Override
protected Bitmap doInBackground(Void... v) {
InputStream in = null;
try {
in = CacheHelper.openCacheFileForInput(context.getCacheDir(), "boxart", computer.uuid.toString(), appId + ".png");
return BitmapFactory.decodeStream(in);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (in != null) {
try {
in.close();
} catch (IOException ignored) {}
}
}
return null;
}
private void fadeInImage(ImageView view) {
view.animate().alpha(1.0f).setDuration(250).start();
}
@Override
protected void onPostExecute(Bitmap result) {
if (result != null) {
// Disk cache was read successfully
LimeLog.info("Image disk cache hit for (" + computer.uuid + ", " + appId + ")");
view.setImageBitmap(result);
fadeInImage(view);
}
else {
LimeLog.info("Image disk cache miss for ("+computer.uuid+", "+appId+")");
LimeLog.info("Requesting: "+"https://" + getCurrentAddress().getHostAddress() + ":47984/appasset?uniqueid=" + uniqueId + "&appid=" +
appId + "&AssetType=2&AssetIdx=0");
// Load the placeholder image
view.setImageResource(defaultImageRes);
fadeInImage(view);
// Set SSL contexts correctly to allow us to authenticate
Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setTrustManagers(trustAllCerts);
Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setSSLContext(sslContext);
Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setHostnameVerifier(hv);
// Kick off the deferred image load
synchronized (pendingRequests) {
Future<Bitmap> f = Ion.with(context)
.load("https://" + getCurrentAddress().getHostAddress() + ":47984/appasset?uniqueid=" + uniqueId + "&appid=" +
appId + "&AssetType=2&AssetIdx=0")
.asBitmap()
.setCallback(new FutureCallback<Bitmap>() {
@Override
public void onCompleted(Exception e, final Bitmap result) {
synchronized (pendingRequests) {
pendingRequests.remove(view);
}
if (result != null) {
// Make the view visible now
view.setImageBitmap(result);
fadeInImage(view);
// Populate the disk cache if we got an image back.
// We do it in a new thread because it can be very expensive, especially
// when we do the initial load where lots of disk I/O is happening at once.
new Thread() {
@Override
public void run() {
populateBitmapCache(computer.uuid, appId, result);
}
}.start();
}
else {
// Leave the loading icon as is (probably should change this eventually...)
}
}
});
pendingRequests.put(view, f);
}
}
}
}
}
@@ -0,0 +1,337 @@
package com.limelight.grid.assets;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.widget.ImageView;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class CachedAppAssetLoader {
private static final int MAX_CONCURRENT_DISK_LOADS = 3;
private static final int MAX_CONCURRENT_NETWORK_LOADS = 3;
private static final int MAX_CONCURRENT_CACHE_LOADS = 1;
private static final int MAX_PENDING_CACHE_LOADS = 100;
private static final int MAX_PENDING_NETWORK_LOADS = 40;
private static final int MAX_PENDING_DISK_LOADS = 40;
private final ThreadPoolExecutor cacheExecutor = new ThreadPoolExecutor(
MAX_CONCURRENT_CACHE_LOADS, MAX_CONCURRENT_CACHE_LOADS,
Long.MAX_VALUE, TimeUnit.DAYS,
new LinkedBlockingQueue<Runnable>(MAX_PENDING_CACHE_LOADS),
new ThreadPoolExecutor.DiscardOldestPolicy());
private final ThreadPoolExecutor foregroundExecutor = new ThreadPoolExecutor(
MAX_CONCURRENT_DISK_LOADS, MAX_CONCURRENT_DISK_LOADS,
Long.MAX_VALUE, TimeUnit.DAYS,
new LinkedBlockingQueue<Runnable>(MAX_PENDING_DISK_LOADS),
new ThreadPoolExecutor.DiscardOldestPolicy());
private final ThreadPoolExecutor networkExecutor = new ThreadPoolExecutor(
MAX_CONCURRENT_NETWORK_LOADS, MAX_CONCURRENT_NETWORK_LOADS,
Long.MAX_VALUE, TimeUnit.DAYS,
new LinkedBlockingQueue<Runnable>(MAX_PENDING_NETWORK_LOADS),
new ThreadPoolExecutor.DiscardOldestPolicy());
private final ComputerDetails computer;
private final double scalingDivider;
private final NetworkAssetLoader networkLoader;
private final MemoryAssetLoader memoryLoader;
private final DiskAssetLoader diskLoader;
private final Bitmap placeholderBitmap;
public CachedAppAssetLoader(ComputerDetails computer, double scalingDivider,
NetworkAssetLoader networkLoader, MemoryAssetLoader memoryLoader,
DiskAssetLoader diskLoader, Bitmap placeholderBitmap) {
this.computer = computer;
this.scalingDivider = scalingDivider;
this.networkLoader = networkLoader;
this.memoryLoader = memoryLoader;
this.diskLoader = diskLoader;
this.placeholderBitmap = placeholderBitmap;
}
public void cancelBackgroundLoads() {
Runnable r;
while ((r = cacheExecutor.getQueue().poll()) != null) {
cacheExecutor.remove(r);
}
}
public void cancelForegroundLoads() {
Runnable r;
while ((r = foregroundExecutor.getQueue().poll()) != null) {
foregroundExecutor.remove(r);
}
while ((r = networkExecutor.getQueue().poll()) != null) {
networkExecutor.remove(r);
}
}
public void freeCacheMemory() {
memoryLoader.clearCache();
}
private Bitmap doNetworkAssetLoad(LoaderTuple tuple, LoaderTask task) {
// Try 3 times
for (int i = 0; i < 3; i++) {
// Check again whether we've been cancelled or the image view is gone
if (task != null && (task.isCancelled() || task.imageViewRef.get() == null)) {
return null;
}
InputStream in = networkLoader.getBitmapStream(tuple);
if (in != null) {
// Write the stream straight to disk
diskLoader.populateCacheWithStream(tuple, in);
// Close the network input stream
try {
in.close();
} catch (IOException ignored) {}
// If there's a task associated with this load, we should return the bitmap
if (task != null) {
// If the cached bitmap is valid, return it. Otherwise, we'll try the load again
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
if (bmp != null) {
return bmp;
}
}
else {
// Otherwise it's a background load and we return nothing
return null;
}
}
// Wait 1 second with a bit of fuzz
try {
Thread.sleep((int) (1000 + (Math.random() * 500)));
} catch (InterruptedException e) {
return null;
}
}
return null;
}
private class LoaderTask extends AsyncTask<LoaderTuple, Void, Bitmap> {
private final WeakReference<ImageView> imageViewRef;
private final boolean diskOnly;
private LoaderTuple tuple;
public LoaderTask(ImageView imageView, boolean diskOnly) {
this.imageViewRef = new WeakReference<ImageView>(imageView);
this.diskOnly = diskOnly;
}
@Override
protected Bitmap doInBackground(LoaderTuple... params) {
tuple = params[0];
// Check whether it has been cancelled or the image view is gone
if (isCancelled() || imageViewRef.get() == null) {
return null;
}
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
if (bmp == null) {
if (!diskOnly) {
// Try to load the asset from the network
bmp = doNetworkAssetLoad(tuple, this);
} else {
// Report progress to display the placeholder and spin
// off the network-capable task
publishProgress();
}
}
// Cache the bitmap
if (bmp != null) {
memoryLoader.populateCache(tuple, bmp);
}
return bmp;
}
@Override
protected void onProgressUpdate(Void... nothing) {
// Do nothing if cancelled
if (isCancelled()) {
return;
}
// If the current loader task for this view isn't us, do nothing
final ImageView imageView = imageViewRef.get();
if (getLoaderTask(imageView) == this) {
// Set off another loader task on the network executor
LoaderTask task = new LoaderTask(imageView, false);
AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getResources(), placeholderBitmap, task);
imageView.setAlpha(1.0f);
imageView.setImageDrawable(asyncDrawable);
task.executeOnExecutor(networkExecutor, tuple);
}
}
@Override
protected void onPostExecute(Bitmap bitmap) {
// Do nothing if cancelled
if (isCancelled()) {
return;
}
final ImageView imageView = imageViewRef.get();
if (getLoaderTask(imageView) == this) {
// Set the bitmap
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
}
// Show the view
imageView.setAlpha(1.0f);
}
}
}
static class AsyncDrawable extends BitmapDrawable {
private final WeakReference<LoaderTask> loaderTaskReference;
public AsyncDrawable(Resources res, Bitmap bitmap,
LoaderTask loaderTask) {
super(res, bitmap);
loaderTaskReference = new WeakReference<LoaderTask>(loaderTask);
}
public LoaderTask getLoaderTask() {
return loaderTaskReference.get();
}
}
private static LoaderTask getLoaderTask(ImageView imageView) {
if (imageView == null) {
return null;
}
final Drawable drawable = imageView.getDrawable();
// If our drawable is in play, get the loader task
if (drawable instanceof AsyncDrawable) {
final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
return asyncDrawable.getLoaderTask();
}
return null;
}
private static boolean cancelPendingLoad(LoaderTuple tuple, ImageView imageView) {
final LoaderTask loaderTask = getLoaderTask(imageView);
// Check if any task was pending for this image view
if (loaderTask != null && !loaderTask.isCancelled()) {
final LoaderTuple taskTuple = loaderTask.tuple;
// Cancel the task if it's not already loading the same data
if (taskTuple == null || !taskTuple.equals(tuple)) {
loaderTask.cancel(true);
} else {
// It's already loading what we want
return false;
}
}
// Allow the load to proceed
return true;
}
public void queueCacheLoad(NvApp app) {
final LoaderTuple tuple = new LoaderTuple(computer, app);
if (memoryLoader.loadBitmapFromCache(tuple) != null) {
// It's in memory which means it must also be on disk
return;
}
// Queue a fetch in the cache executor
cacheExecutor.execute(new Runnable() {
@Override
public void run() {
// Check if the image is cached on disk
if (diskLoader.checkCacheExists(tuple)) {
return;
}
// Try to load the asset from the network and cache result on disk
doNetworkAssetLoad(tuple, null);
}
});
}
public void populateImageView(NvApp app, ImageView view) {
LoaderTuple tuple = new LoaderTuple(computer, app);
// If there's already a task in progress for this view,
// cancel it. If the task is already loading the same image,
// we return and let that load finish.
if (!cancelPendingLoad(tuple, view)) {
return;
}
// First, try the memory cache in the current context
Bitmap bmp = memoryLoader.loadBitmapFromCache(tuple);
if (bmp != null) {
// Show the bitmap immediately
view.setAlpha(1.0f);
view.setImageBitmap(bmp);
return;
}
// If it's not in memory, create an async task to load it. This task will be attached
// via AsyncDrawable to this view.
final LoaderTask task = new LoaderTask(view, true);
final AsyncDrawable asyncDrawable = new AsyncDrawable(view.getResources(), placeholderBitmap, task);
view.setAlpha(0.0f);
view.setImageDrawable(asyncDrawable);
// Run the task on our foreground executor
task.executeOnExecutor(foregroundExecutor, tuple);
}
public class LoaderTuple {
public final ComputerDetails computer;
public final NvApp app;
public LoaderTuple(ComputerDetails computer, NvApp app) {
this.computer = computer;
this.app = app;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof LoaderTuple)) {
return false;
}
LoaderTuple other = (LoaderTuple) o;
return computer.uuid.equals(other.computer.uuid) && app.getAppId() == other.app.getAppId();
}
@Override
public String toString() {
return "("+computer.uuid+", "+app.getAppId()+")";
}
}
}
@@ -0,0 +1,83 @@
package com.limelight.grid.assets;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import com.limelight.LimeLog;
import com.limelight.utils.CacheHelper;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class DiskAssetLoader {
// 5 MB
private final long MAX_ASSET_SIZE = 5 * 1024 * 1024;
private final File cacheDir;
public DiskAssetLoader(File cacheDir) {
this.cacheDir = cacheDir;
}
public boolean checkCacheExists(CachedAppAssetLoader.LoaderTuple tuple) {
return CacheHelper.cacheFileExists(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
}
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
InputStream in = null;
Bitmap bmp = null;
try {
// Make sure the cached asset doesn't exceed the maximum size
if (CacheHelper.getFileSize(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png") > MAX_ASSET_SIZE) {
LimeLog.warning("Removing cached tuple exceeding size threshold: "+tuple);
CacheHelper.deleteCacheFile(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
return null;
}
in = CacheHelper.openCacheFileForInput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = sampleSize;
options.inPreferredConfig = Bitmap.Config.RGB_565;
bmp = BitmapFactory.decodeStream(in, null, options);
} catch (IOException ignored) {
} finally {
if (in != null) {
try {
in.close();
} catch (IOException ignored) {}
}
}
if (bmp != null) {
LimeLog.info("Disk cache hit for tuple: "+tuple);
}
return bmp;
}
public void populateCacheWithStream(CachedAppAssetLoader.LoaderTuple tuple, InputStream input) {
OutputStream out = null;
boolean success = false;
try {
out = CacheHelper.openCacheFileForOutput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
CacheHelper.writeInputStreamToOutputStream(input, out, MAX_ASSET_SIZE);
success = true;
} catch (IOException e) {
e.printStackTrace();
} finally {
if (out != null) {
try {
out.close();
} catch (IOException ignored) {}
}
if (!success) {
LimeLog.warning("Unable to populate cache with tuple: "+tuple);
CacheHelper.deleteCacheFile(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
}
}
}
}
@@ -0,0 +1,37 @@
package com.limelight.grid.assets;
import android.graphics.Bitmap;
import android.util.LruCache;
import com.limelight.LimeLog;
public class MemoryAssetLoader {
private static final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
private static final LruCache<String, Bitmap> memoryCache = new LruCache<String, Bitmap>(maxMemory / 16) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// Sizeof returns kilobytes
return bitmap.getByteCount() / 1024;
}
};
private static String constructKey(CachedAppAssetLoader.LoaderTuple tuple) {
return tuple.computer.uuid.toString()+"-"+tuple.app.getAppId();
}
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple) {
Bitmap bmp = memoryCache.get(constructKey(tuple));
if (bmp != null) {
LimeLog.info("Memory cache hit for tuple: "+tuple);
}
return bmp;
}
public void populateCache(CachedAppAssetLoader.LoaderTuple tuple, Bitmap bitmap) {
memoryCache.put(constructKey(tuple), bitmap);
}
public void clearCache() {
memoryCache.evictAll();
}
}
@@ -0,0 +1,49 @@
package com.limelight.grid.assets;
import android.content.Context;
import com.limelight.LimeLog;
import com.limelight.binding.PlatformBinding;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvHTTP;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
public class NetworkAssetLoader {
private final Context context;
private final String uniqueId;
public NetworkAssetLoader(Context context, String uniqueId) {
this.context = context;
this.uniqueId = uniqueId;
}
public InputStream getBitmapStream(CachedAppAssetLoader.LoaderTuple tuple) {
NvHTTP http = new NvHTTP(getCurrentAddress(tuple.computer), uniqueId, null, PlatformBinding.getCryptoProvider(context));
InputStream in = null;
try {
in = http.getBoxArt(tuple.app);
} catch (IOException ignored) {}
if (in != null) {
LimeLog.info("Network asset load complete: " + tuple);
}
else {
LimeLog.info("Network asset load failed: " + tuple);
}
return in;
}
private static InetAddress getCurrentAddress(ComputerDetails computer) {
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
return computer.localIp;
}
else {
return computer.remoteIp;
}
}
}
@@ -3,6 +3,7 @@ package com.limelight.preferences;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Build;
import android.preference.PreferenceManager;
public class PreferenceConfiguration {
@@ -18,6 +19,7 @@ public class PreferenceConfiguration {
private static final String LIST_MODE_PREF_STRING = "checkbox_list_mode";
private static final String SMALL_ICONS_PREF_STRING = "checkbox_small_icon_mode";
private static final String MULTI_CONTROLLER_PREF_STRING = "checkbox_multi_controller";
private static final String USB_DRIVER_PREF_SRING = "checkbox_usb_driver";
private static final int BITRATE_DEFAULT_720_30 = 5;
private static final int BITRATE_DEFAULT_720_60 = 10;
@@ -35,6 +37,7 @@ public class PreferenceConfiguration {
public static final String DEFAULT_LANGUAGE = "default";
private static final boolean DEFAULT_LIST_MODE = false;
private static final boolean DEFAULT_MULTI_CONTROLLER = true;
private static final boolean DEFAULT_USB_DRIVER = true;
public static final int FORCE_HARDWARE_DECODER = -1;
public static final int AUTOSELECT_DECODER = 0;
@@ -46,7 +49,7 @@ public class PreferenceConfiguration {
public int deadzonePercentage;
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
public String language;
public boolean listMode, smallIconMode, multiController;
public boolean listMode, smallIconMode, multiController, usbDriver;
public static int getDefaultBitrate(String resFpsString) {
if (resFpsString.equals("720p30")) {
@@ -69,9 +72,18 @@ public class PreferenceConfiguration {
public static boolean getDefaultSmallMode(Context context) {
PackageManager manager = context.getPackageManager();
if (manager != null && manager.hasSystemFeature(PackageManager.FEATURE_TELEVISION)) {
if (manager != null) {
// TVs shouldn't use small mode by default
return false;
if (manager.hasSystemFeature(PackageManager.FEATURE_TELEVISION)) {
return false;
}
// API 21 uses LEANBACK instead of TELEVISION
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
if (manager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
return false;
}
}
}
// Use small mode on anything smaller than a 7" tablet
@@ -80,24 +92,7 @@ public class PreferenceConfiguration {
public static int getDefaultBitrate(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
String str = prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS);
if (str.equals("720p30")) {
return BITRATE_DEFAULT_720_30;
}
else if (str.equals("720p60")) {
return BITRATE_DEFAULT_720_60;
}
else if (str.equals("1080p30")) {
return BITRATE_DEFAULT_1080_30;
}
else if (str.equals("1080p60")) {
return BITRATE_DEFAULT_1080_60;
}
else {
// Should never get here
return DEFAULT_BITRATE;
}
return getDefaultBitrate(prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS));
}
private static int getDecoderValue(Context context) {
@@ -166,6 +161,7 @@ public class PreferenceConfiguration {
config.listMode = prefs.getBoolean(LIST_MODE_PREF_STRING, DEFAULT_LIST_MODE);
config.smallIconMode = prefs.getBoolean(SMALL_ICONS_PREF_STRING, getDefaultSmallMode(context));
config.multiController = prefs.getBoolean(MULTI_CONTROLLER_PREF_STRING, DEFAULT_MULTI_CONTROLLER);
config.usbDriver = prefs.getBoolean(USB_DRIVER_PREF_SRING, DEFAULT_USB_DRIVER);
return config;
}
@@ -16,14 +16,17 @@ import com.limelight.utils.UiHelper;
import java.util.Locale;
public class StreamSettings extends Activity {
private PreferenceConfiguration previousPrefs;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String locale = PreferenceConfiguration.readPreferences(this).language;
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
previousPrefs = PreferenceConfiguration.readPreferences(this);
if (!previousPrefs.language.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
Configuration config = new Configuration(getResources().getConfiguration());
config.locale = new Locale(locale);
config.locale = new Locale(previousPrefs.language);
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
}
@@ -39,10 +42,16 @@ public class StreamSettings extends Activity {
public void onBackPressed() {
finish();
// Restart the PC view to apply UI changes
Intent intent = new Intent(this, PcView.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent, null);
// Check for changes that require a UI reload to take effect
PreferenceConfiguration newPrefs = PreferenceConfiguration.readPreferences(this);
if (newPrefs.listMode != previousPrefs.listMode ||
newPrefs.smallIconMode != previousPrefs.smallIconMode ||
!newPrefs.language.equals(previousPrefs.language)) {
// Restart the PC view to apply UI changes
Intent intent = new Intent(this, PcView.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent, null);
}
}
public static class SettingsFragment extends PreferenceFragment {
@@ -1,5 +1,7 @@
package com.limelight.utils;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
@@ -28,12 +30,37 @@ public class CacheHelper {
return f;
}
public static FileInputStream openCacheFileForInput(File root, String... path) throws FileNotFoundException {
return new FileInputStream(openPath(false, root, path));
public static long getFileSize(File root, String... path) {
return openPath(false, root, path).length();
}
public static FileOutputStream openCacheFileForOutput(File root, String... path) throws FileNotFoundException {
return new FileOutputStream(openPath(true, root, path));
public static boolean deleteCacheFile(File root, String... path) {
return openPath(false, root, path).delete();
}
public static boolean cacheFileExists(File root, String... path) {
return openPath(false, root, path).exists();
}
public static InputStream openCacheFileForInput(File root, String... path) throws FileNotFoundException {
return new BufferedInputStream(new FileInputStream(openPath(false, root, path)));
}
public static OutputStream openCacheFileForOutput(File root, String... path) throws FileNotFoundException {
return new BufferedOutputStream(new FileOutputStream(openPath(true, root, path)));
}
public static void writeInputStreamToOutputStream(InputStream in, OutputStream out, long maxLength) throws IOException {
byte[] buf = new byte[4096];
int bytesRead;
while ((bytesRead = in.read(buf)) != -1) {
maxLength -= bytesRead;
if (maxLength <= 0) {
throw new IOException("Stream exceeded max size");
}
out.write(buf, 0, bytesRead);
}
}
public static String readInputStreamToString(InputStream in) throws IOException {
@@ -46,6 +73,10 @@ public class CacheHelper {
sb.append(buf, 0, bytesRead);
}
try {
in.close();
} catch (IOException ignored) {}
return sb.toString();
}
@@ -0,0 +1,90 @@
package com.limelight.utils;
import android.app.Activity;
import android.content.Intent;
import android.widget.Toast;
import com.limelight.Game;
import com.limelight.R;
import com.limelight.binding.PlatformBinding;
import com.limelight.computers.ComputerManagerService;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.GfeHttpResponseException;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP;
import java.io.FileNotFoundException;
import java.net.InetAddress;
import java.net.UnknownHostException;
public class ServerHelper {
public static InetAddress getCurrentAddressFromComputer(ComputerDetails computer) {
return computer.reachability == ComputerDetails.Reachability.LOCAL ?
computer.localIp : computer.remoteIp;
}
public static void doStart(Activity parent, NvApp app, ComputerDetails computer,
ComputerManagerService.ComputerManagerBinder managerBinder) {
Intent intent = new Intent(parent, Game.class);
intent.putExtra(Game.EXTRA_HOST,
computer.reachability == ComputerDetails.Reachability.LOCAL ?
computer.localIp.getHostAddress() : computer.remoteIp.getHostAddress());
intent.putExtra(Game.EXTRA_APP_NAME, app.getAppName());
intent.putExtra(Game.EXTRA_APP_ID, app.getAppId());
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
intent.putExtra(Game.EXTRA_STREAMING_REMOTE,
computer.reachability != ComputerDetails.Reachability.LOCAL);
parent.startActivity(intent);
}
public static void doQuit(final Activity parent,
final InetAddress address,
final NvApp app,
final ComputerManagerService.ComputerManagerBinder managerBinder,
final Runnable onComplete) {
Toast.makeText(parent, parent.getResources().getString(R.string.applist_quit_app) + " " + app.getAppName() + "...", Toast.LENGTH_SHORT).show();
new Thread(new Runnable() {
@Override
public void run() {
NvHTTP httpConn;
String message;
try {
httpConn = new NvHTTP(address,
managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(parent));
if (httpConn.quitApp()) {
message = parent.getResources().getString(R.string.applist_quit_success) + " " + app.getAppName();
} else {
message = parent.getResources().getString(R.string.applist_quit_fail) + " " + app.getAppName();
}
} catch (GfeHttpResponseException e) {
if (e.getErrorCode() == 599) {
message = "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()+")";
}
else {
message = e.getMessage();
}
} catch (UnknownHostException e) {
message = parent.getResources().getString(R.string.error_unknown_host);
} catch (FileNotFoundException e) {
message = parent.getResources().getString(R.string.error_404);
} catch (Exception e) {
message = e.getMessage();
} finally {
if (onComplete != null) {
onComplete.run();
}
}
final String toastMessage = message;
parent.runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(parent, toastMessage, Toast.LENGTH_LONG).show();
}
});
}
}).start();
}
}
@@ -1,11 +1,15 @@
package com.limelight.utils;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.UiModeManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Configuration;
import android.view.View;
import com.limelight.R;
public class UiHelper {
// Values from https://developer.android.com/training/tv/start/layouts.html
@@ -28,4 +32,31 @@ public class UiHelper {
horizontalPaddingPixels, verticalPaddingPixels);
}
}
public static void displayQuitConfirmationDialog(Activity parent, final Runnable onYes, final Runnable onNo) {
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which){
case DialogInterface.BUTTON_POSITIVE:
if (onYes != null) {
onYes.run();
}
break;
case DialogInterface.BUTTON_NEGATIVE:
if (onNo != null) {
onNo.run();
}
break;
}
}
};
AlertDialog.Builder builder = new AlertDialog.Builder(parent);
builder.setMessage(parent.getResources().getString(R.string.applist_quit_confirmation))
.setPositiveButton(parent.getResources().getString(R.string.yes), dialogClickListener)
.setNegativeButton(parent.getResources().getString(R.string.no), dialogClickListener)
.show();
}
}
+15 -27
View File
@@ -1,17 +1,21 @@
#include <stdlib.h>
#include <opus.h>
#include <opus_multistream.h>
#include "nv_opus_dec.h"
OpusDecoder* decoder;
OpusMSDecoder* decoder;
// This function must be called before
// any other decoding functions
int nv_opus_init(void) {
int nv_opus_init(int sampleRate, int channelCount, int streams,
int coupledStreams, const unsigned char *mapping) {
int err;
decoder = opus_decoder_create(
nv_opus_get_sample_rate(),
nv_opus_get_channel_count(),
&err);
decoder = opus_multistream_decoder_create(
sampleRate,
channelCount,
streams,
coupledStreams,
mapping,
&err);
return err;
}
@@ -19,36 +23,20 @@ int nv_opus_init(void) {
// decoding is finished
void nv_opus_destroy(void) {
if (decoder != NULL) {
opus_decoder_destroy(decoder);
opus_multistream_decoder_destroy(decoder);
}
}
// The Opus stream is stereo
int nv_opus_get_channel_count(void) {
return 2;
}
// This number assumes 16-bit samples at 48 KHz with 2.5 ms frames
int nv_opus_get_max_out_shorts(void) {
return 240*nv_opus_get_channel_count();
}
// The Opus stream is 48 KHz
int nv_opus_get_sample_rate(void) {
return 48000;
}
// outpcmdata must be 5760*2 shorts in length
// packets must be decoded in order
// a packet loss must call this function with NULL indata and 0 inlen
// returns the number of decoded samples
int nv_opus_decode(unsigned char* indata, int inlen, short* outpcmdata) {
int nv_opus_decode(unsigned char* indata, int inlen, short* outpcmdata, int framesize) {
int err;
// Decoding to 16-bit PCM with FEC off
// Maximum length assuming 48KHz sample rate
err = opus_decode(decoder, indata, inlen,
outpcmdata, 512, 0);
err = opus_multistream_decode(decoder, indata, inlen,
outpcmdata, framesize, 0);
return err;
}
+3 -5
View File
@@ -1,6 +1,4 @@
int nv_opus_init(void);
int nv_opus_init(int sampleRate, int channelCount, int streams,
int coupledStreams, const unsigned char *mapping);
void nv_opus_destroy(void);
int nv_opus_get_channel_count(void);
int nv_opus_get_max_out_shorts(void);
int nv_opus_get_sample_rate(void);
int nv_opus_decode(unsigned char* indata, int inlen, short* outpcmdata);
int nv_opus_decode(unsigned char* indata, int inlen, short* outpcmdata, int framesize);
+25 -24
View File
@@ -3,11 +3,26 @@
#include <stdlib.h>
#include <jni.h>
static int SamplesPerChannel;
static int ChannelCount;
// This function must be called before
// any other decoding functions
JNIEXPORT jint JNICALL
Java_com_limelight_nvstream_av_audio_OpusDecoder_init(JNIEnv *env, jobject this) {
return nv_opus_init();
Java_com_limelight_nvstream_av_audio_OpusDecoder_init(JNIEnv *env, jobject this, int sampleRate,
int samplesPerChannel, int channelCount, int streams,
int coupledStreams, jbyteArray mapping) {
jbyte* jni_mapping_data;
jint ret;
SamplesPerChannel = samplesPerChannel;
ChannelCount = channelCount;
jni_mapping_data = (*env)->GetByteArrayElements(env, mapping, 0);
ret = nv_opus_init(sampleRate, channelCount, streams, coupledStreams, jni_mapping_data);
(*env)->ReleaseByteArrayElements(env, mapping, jni_mapping_data, JNI_ABORT);
return ret;
}
// This function must be called after
@@ -17,28 +32,9 @@ Java_com_limelight_nvstream_av_audio_OpusDecoder_destroy(JNIEnv *env, jobject th
nv_opus_destroy();
}
// The Opus stream is stereo
JNIEXPORT jint JNICALL
Java_com_limelight_nvstream_av_audio_OpusDecoder_getChannelCount(JNIEnv *env, jobject this) {
return nv_opus_get_channel_count();
}
// This number assumes 2 channels at 48 KHz
JNIEXPORT jint JNICALL
Java_com_limelight_nvstream_av_audio_OpusDecoder_getMaxOutputShorts(JNIEnv *env, jobject this) {
return nv_opus_get_max_out_shorts();
}
// The Opus stream is 48 KHz
JNIEXPORT jint JNICALL
Java_com_limelight_nvstream_av_audio_OpusDecoder_getSampleRate(JNIEnv *env, jobject this) {
return nv_opus_get_sample_rate();
}
// outpcmdata must be 5760*2 shorts in length
// packets must be decoded in order
// a packet loss must call this function with NULL indata and 0 inlen
// returns the number of decoded samples
// returns the number of decoded bytes
JNIEXPORT jint JNICALL
Java_com_limelight_nvstream_av_audio_OpusDecoder_decode(
JNIEnv *env, jobject this, // JNI parameters
@@ -53,13 +49,18 @@ Java_com_limelight_nvstream_av_audio_OpusDecoder_decode(
if (indata != NULL) {
jni_input_data = (*env)->GetByteArrayElements(env, indata, 0);
ret = nv_opus_decode(&jni_input_data[inoff], inlen, (jshort*)jni_pcm_data);
ret = nv_opus_decode(&jni_input_data[inoff], inlen, (jshort*)jni_pcm_data, SamplesPerChannel);
// The input data isn't changed so it can be safely aborted
(*env)->ReleaseByteArrayElements(env, indata, jni_input_data, JNI_ABORT);
}
else {
ret = nv_opus_decode(NULL, 0, (jshort*)jni_pcm_data);
ret = nv_opus_decode(NULL, 0, (jshort*)jni_pcm_data, SamplesPerChannel);
}
// Convert samples (2 bytes) per channel to total bytes returned
if (ret > 0) {
ret *= ChannelCount * 2;
}
(*env)->ReleaseByteArrayElements(env, outpcmdata, jni_pcm_data, 0);
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 65 KiB

+2 -2
View File
@@ -1,4 +1,4 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -10,4 +10,4 @@
android:layout_height="match_parent"
android:layout_gravity="center" />
</FrameLayout>
</merge>
+1 -1
View File
@@ -102,7 +102,7 @@
<string name="category_ui_settings">Impostazioni Interfaccia</string>
<string name="title_language_list">Lingua</string>
<string name="summary_language_list">Lingua da usare in Limelight</string>
<string name="summary_language_list">Lingua da usare in Moonlight</string>
<string name="title_checkbox_list_mode">Usa lista invece della griglia</string>
<string name="summary_checkbox_list_mode">Visualizza applicazioni e computers in una lista invece di una griglia</string>
<string name="title_checkbox_small_icon_mode">Usa icone piccole</string>
+5 -3
View File
@@ -44,9 +44,9 @@
<string name="conn_establishing_title">Establishing Connection</string>
<string name="conn_establishing_msg">Starting connection</string>
<string name="conn_metered">Warning: Your active network connection is metered!</string>
<string name="conn_client_latency">Average client-side frame latency:</string>
<string name="conn_client_latency">Average frame decoding latency:</string>
<string name="conn_client_latency_hw">hardware decoder latency:</string>
<string name="conn_hardware_latency">Average hardware decoder latency:</string>
<string name="conn_hardware_latency">Average hardware decoding latency:</string>
<string name="conn_starting">Starting</string>
<string name="conn_error_title">Connection Error</string>
<string name="conn_error_msg">Failed to start</string>
@@ -99,10 +99,12 @@
<string name="summary_checkbox_multi_controller">When unchecked, all controllers appear as one</string>
<string name="title_seekbar_deadzone">Adjust analog stick deadzone</string>
<string name="suffix_seekbar_deadzone">%</string>
<string name="title_checkbox_xb1_driver">Xbox One controller driver</string>
<string name="summary_checkbox_xb1_driver">Enables a built-in USB driver for devices without native Xbox One controller support.</string>
<string name="category_ui_settings">UI Settings</string>
<string name="title_language_list">Language</string>
<string name="summary_language_list">Language to use for Limelight</string>
<string name="summary_language_list">Language to use for Moonlight</string>
<string name="title_checkbox_list_mode">Use lists instead of grids</string>
<string name="summary_checkbox_list_mode">Display apps and PCs in lists instead of grids</string>
<string name="title_checkbox_small_icon_mode">Use small icons</string>
+5
View File
@@ -38,6 +38,11 @@
android:title="@string/title_checkbox_multi_controller"
android:summary="@string/summary_checkbox_multi_controller"
android:defaultValue="true" />
<CheckBoxPreference
android:key="checkbox_usb_driver"
android:title="@string/title_checkbox_xb1_driver"
android:summary="@string/summary_checkbox_xb1_driver"
android:defaultValue="true" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/category_host_settings">
<CheckBoxPreference
+1 -1
View File
@@ -2,5 +2,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Non-root application name -->
<application android:label="Limelight" />
<application android:label="Moonlight" />
</manifest>
+1 -4
View File
@@ -1,9 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Root permissions -->
<uses-permission android:name="android.permission.ACCESS_SUPERUSER" />
<!-- Root application name -->
<application android:label="Limelight (Root)" />
<application android:label="Moonlight (Root)" />
</manifest>
+1 -1
View File
@@ -4,7 +4,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.0.1'
classpath 'com.android.tools.build:gradle:1.3.0'
}
}
+10 -4
View File
@@ -12,8 +12,8 @@ This file serves to document some of the decoder errata when using MediaCodec ha
4. Some decoders require num_ref_frames=1 and max_dec_frame_buffering=1 to avoid crashing on SPS on first I-frame
- Affected decoders: Qualcomm in GS3 on 4.3+, Exynos 4 at 1080p only
5. Some decoders will hang if max_dec_frame_buffering is not present
- Affected decoders: MediaTek decoder in Fire HD 7 (2014)
5. Some decoders will hang or crash if max_dec_frame_buffering is not present and level_idc is >= 50
- Affected decoders: MediaTek decoder in Fire HD 6/7 (2014)
6. Some decoders will hang if max_dec_frame_buffering IS present
- Affected decoders: Exynos 5 in Galaxy Note 10.1 (2014)
@@ -21,5 +21,11 @@ This file serves to document some of the decoder errata when using MediaCodec ha
7. Some decoders will not enter low latency mode if adaptive playback is enabled
- Affected decoders: Intel decoder in Nexus Player
8. Some decoders will not enter low latency mode if the profile isn't baseline in the first SPS.
- Affected decoders: Intel decoder in Nexus Player
8. Some decoders will not enter low latency mode if the profile isn't baseline in the first SPS because B-frames may be present.
- Affected decoders: Intel decoder in Nexus Player (prior to Android 6.0)
9. Some decoders will not enter low latency mode if the profile isn't constrained high profile because B-frames may be present.
- Affected decoders: Intel decoder in Nexus Player (after Android 6.0)
10. Some decoders actually suffer increased latency when max_dec_frame_buffering=1
- Affected decoders: MediaTek decoder in Fire TV 2015
@@ -1,15 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" type="JAVA_MODULE" version="4">
<module external.linked.project.id="moonlight-android" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" external.system.module.group="" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="java-gradle" name="Java-Gradle">
<configuration>
<option name="BUILD_FOLDER_PATH" value="$MODULE_DIR$/build" />
<option name="BUILDABLE" value="false" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" inherit-compiler-output="false">
<output url="file://$MODULE_DIR$/build/classes/main" />
<output-test url="file://$MODULE_DIR$/build/classes/test" />
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.gradle" />
@@ -17,5 +16,4 @@
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
</module>