Compare commits

...

221 Commits

Author SHA1 Message Date
Cameron Gutman fa761debc4 Fix root build 2018-10-05 01:44:03 -07:00
Cameron Gutman 62e175f069 Avoid crashing when opening an app context menu in list mode 2018-10-05 01:42:19 -07:00
Cameron Gutman d7d8c40565 Version 5.9.3 2018-10-05 01:34:59 -07:00
Cameron Gutman 64de13ab50 Try to disambiguate right clicks from back presses 2018-10-05 01:29:18 -07:00
Cameron Gutman 2f02939638 Always process key events before the IME 2018-10-05 01:10:27 -07:00
Cameron Gutman 1d7c8697e9 Add support for X1 and X2 mouse buttons 2018-10-05 00:56:30 -07:00
Cameron Gutman 7dea322bbd Update build tools to 28.0.3 2018-09-29 15:37:31 -07:00
Cameron Gutman 349ecb16ab Increment version code 2018-09-29 15:36:21 -07:00
Cameron Gutman a3867735c1 Update to AGP 3.2 2018-09-29 15:31:13 -07:00
Cameron Gutman 5b087e9f70 Update common-c with split encode change 2018-09-22 20:22:59 -07:00
Cameron Gutman eed18223eb Version 5.9.2 2018-09-18 20:36:53 -07:00
Cameron Gutman 30d4d2a918 Update moonlight-common 2018-09-18 20:27:08 -07:00
Cameron Gutman 30f666c70e Update AGP to 3.2 rc3 2018-09-18 20:23:25 -07:00
Cameron Gutman 209fead0e8 Only add the create shortcut option if the box art is present to avoid crashing 2018-09-18 20:22:16 -07:00
Cameron Gutman 5c6889bf6d Version 5.9.1 2018-08-12 00:45:19 -07:00
Cameron Gutman 7d24900756 Update build tools to 28.0.2 2018-08-12 00:44:09 -07:00
Cameron Gutman 79a75b9d19 Update common with audio fix and game launch fix 2018-08-12 00:40:56 -07:00
Cameron Gutman 29b64992bd Enable stale and no-response bots to reduce inactive issues 2018-08-12 00:20:03 -07:00
Cameron Gutman c9b14540f2 Remove reference to old Moonlight Java project 2018-08-10 22:29:59 -07:00
Cameron Gutman 546843a26c Fix crash on quit confirmation prompt 2018-07-28 00:13:20 -07:00
Cameron Gutman d03d260535 Add status bar and navigation bar color on L+ 2018-07-27 23:48:20 -07:00
Cameron Gutman 6946e3c7a2 Just use the PC name as the app list title 2018-07-27 23:42:57 -07:00
Cameron Gutman b79d328961 Version 5.9 2018-07-16 18:46:01 -07:00
Cameron Gutman c313797d93 Make OSC reconfigure button non-focusable so it doesn't eat hardware enter/space presses. Fixes #611 2018-07-16 18:40:19 -07:00
Cameron Gutman c8cb8e1346 Update build tools to 28.0.1 2018-07-16 18:25:33 -07:00
Cameron Gutman 6a9f8da14e Update common to reduce syscall overhead 2018-07-16 18:21:48 -07:00
Cameron Gutman ff9260a0fd Update AGP to 3.2-beta4 2018-07-16 18:16:35 -07:00
zacklmoore 62bedb1609 Pinned Game Shortcuts (Android Oreo) (#603)
* Initial changes to add game shortcuts.

* Initial working cut.

* Cleanup and converting strings to resource strings.

* Additional cleanup.

* Removed a blank line

* Changes based on review feedback.

* Forgot to save some changes before commiting...

* Standardized dialogs and tried to fix the dialogs auto-closing when the PCView is already opened.
2018-07-06 21:53:19 -07:00
Cameron Gutman a519723d44 Monkey-proof Moonlight 2018-06-20 01:26:59 -07:00
Cameron Gutman 36191781ed Version 5.8.2 2018-06-16 17:05:44 -07:00
Cameron Gutman 61b6a49669 Correct MT8176 errata 2018-06-16 16:37:50 -07:00
Cameron Gutman e97845e46e Add comments and documentation on MT8176 testing 2018-06-16 16:31:06 -07:00
Cameron Gutman 6bba68207d Ignore spurious ACTION_HOVER_ENTER with wrong coordinates and KEYCODE_BACK repeats. Fixes #554 2018-06-16 15:57:44 -07:00
Cameron Gutman 0e17cccc06 Process historical values for relative mouse events 2018-06-16 15:22:01 -07:00
Cameron Gutman 918e922e40 Avoid processing mouse move history 2018-06-16 15:14:16 -07:00
Cameron Gutman a08854ddfd Properly handle SOURCE_MOUSE_RELATIVE in the mouse back button hack. Fixes #424 2018-06-16 15:01:11 -07:00
Cameron Gutman eb6f15c2b7 Add dynamic method for allowing back buttons for navigation 2018-06-16 14:32:07 -07:00
Cameron Gutman 2cd9e31684 Update gitignore to handle app bundles and other new files dropped by AS 2018-06-16 14:27:54 -07:00
Cameron Gutman 791d6624e2 Update to AGP 3.2 alpha 18 2018-06-16 14:21:14 -07:00
Cameron Gutman af41021271 Use HEVC by default on MediaTek SoCs with PowerVR graphics 2018-06-14 22:55:10 -07:00
Cameron Gutman d726d939f4 Version 5.8.1-r3 2018-06-09 21:49:41 -07:00
Cameron Gutman 748085e7bb Update common to fix reconnection issue 2018-06-09 21:39:26 -07:00
Cameron Gutman d57d19174b Version 5.8.1 r2 2018-06-09 18:16:41 -07:00
Cameron Gutman efebe1828a Update common with Lint fixes 2018-06-09 18:14:39 -07:00
Cameron Gutman 06007e0597 Add a button for adding a PC manually 2018-06-09 18:14:09 -07:00
Cameron Gutman 3a868045d7 Allow the display to go off if the stream disconnects 2018-06-09 17:48:07 -07:00
Cameron Gutman e0a7ff1880 Remove in-progress toast for WOL 2018-06-09 17:23:59 -07:00
Cameron Gutman 88d43bbd40 Disable density splits until I can figure out why we're crashing 2018-06-08 01:13:00 -07:00
Cameron Gutman 30ff319b13 Version 5.8.1 2018-06-08 01:08:09 -07:00
Cameron Gutman 9a0f48b799 Add support for display cutouts on P 2018-06-08 01:05:32 -07:00
Cameron Gutman b52c8a1a8f Use ImageDecoder API on P and higher quality decodes on non-low ram devices 2018-06-08 00:46:40 -07:00
Cameron Gutman 3fde115670 Update to AGP 3.2 alpha 17 2018-06-08 00:24:39 -07:00
Cameron Gutman b6f4d8ff1e Target API 28 2018-06-08 00:23:41 -07:00
Cameron Gutman a7d85a7dd5 Update to AGP 3.2-alpha16 2018-05-29 18:40:26 -07:00
Cameron Gutman 9b238ab6c3 Version 5.8 (take 2) 2018-05-27 20:05:51 -07:00
Cameron Gutman f82ee97c05 Update common to fix channel mapping error in 5.1 high quality mode 2018-05-27 20:04:28 -07:00
Cameron Gutman 35fb96f9f4 Version 5.8 2018-05-27 18:56:03 -07:00
Cameron Gutman 37371906d5 Update common for audio and JNI library size improvements 2018-05-27 18:51:28 -07:00
Cameron Gutman 83a9539f4b Version 5.7.7 2018-05-21 19:24:26 -07:00
Cameron Gutman b214fe5301 Update to AGP 3.2-alpha15 2018-05-21 18:55:36 -07:00
Cameron Gutman 57779b4e89 Always expose gamepad 1 in single controller mode 2018-05-21 18:55:02 -07:00
Cameron Gutman 547932f8b2 Version 5.7.6 2018-05-13 22:23:22 -07:00
Cameron Gutman 762fa0fe2f Tighten ProGuard rules for BC 2018-05-12 21:25:41 -07:00
Cameron Gutman 9cedc57df2 Move the portrait activity_pc_view.xml to the default directory 2018-05-12 18:33:47 -07:00
Cameron Gutman ba81f8096a Allow software decoding in CrOS emulator 2018-05-11 19:28:30 -07:00
Cameron Gutman c4fa654166 Fix split breaking language chooser 2018-05-08 21:24:49 -07:00
Cameron Gutman 8ac440b68b Update AGP for AS 3.2 and app bundles 2018-05-08 21:22:10 -07:00
Cameron Gutman 165386b941 Update AGP and D8 2018-05-08 18:43:49 -07:00
Cameron Gutman 3a7398f321 Use ProGuard for minification 2018-05-08 18:43:23 -07:00
Cameron Gutman ebb1d0dfa2 Version 5.7.5 2018-04-21 23:48:04 -07:00
Cameron Gutman 1ca1ed5d20 Increase OSC analog stick size 2018-04-21 23:46:55 -07:00
Cameron Gutman b416bafb78 Hide OSC in PiP and scale properly in multi-window 2018-04-21 23:37:38 -07:00
Cameron Gutman 3a301b74a6 Update to D8 v1.0.23 2018-04-21 23:17:04 -07:00
Cameron Gutman 71d463f063 Avoid crashing from unexpected enterPictureInPictureMode() exceptions 2018-04-21 21:32:59 -07:00
Cameron Gutman 1fae816223 Remove the spinner threads (and battery saver option to disable them) 2018-04-21 21:29:42 -07:00
Cameron Gutman 989d6fc169 Fix for broken keyboard d-pad and Shift+Space behavior on Samsung devices 2018-04-21 16:24:23 -07:00
Cameron Gutman 381509b3a6 Properly handle joysticks that only return events for one trigger axis 2018-04-21 15:09:57 -07:00
Cameron Gutman d8ae40376e Update to AGP 3.1.1 2018-04-09 20:32:06 -07:00
Cameron Gutman 4ea93f5e68 Version 5.7.4 2018-04-08 21:21:37 -07:00
Cameron Gutman cd84c8f30e Fix grammar issue in decoder crash message 2018-04-08 21:19:49 -07:00
Cameron Gutman 8d4cdca7c3 Fix RFI disabling for KDDI/b5_jp_kdi/b5:7.0/NRD90U/1801120299534:user/release-keys and KDDI/b3_jp_kdi/b3:7.0/NRD90U/180120857f434:user/release-keys 2018-04-08 19:56:01 -07:00
Cameron Gutman c0239c36fd Update language around decoder crashes 2018-03-27 20:52:01 -07:00
Cameron Gutman 9d9f729e42 Address another buggy LGE variant (b5_jp_kdi) 2018-03-27 20:48:16 -07:00
Cameron Gutman 6c5fe18b6e Update Gradle for AS 3.1 2018-03-26 23:30:33 -07:00
Cameron Gutman 1994bf6522 Version 5.7.3.1 for Amazon 2018-03-24 23:48:04 -07:00
Cameron Gutman 31381e5664 Add Amlogic SoC to HEVC whitelist for Fire TV 3 now that maxNumReferenceFrames support has been out for a while 2018-03-24 23:47:05 -07:00
Cameron Gutman fac1b1d7e5 Version 5.7.3 2018-03-24 13:05:31 -07:00
Cameron Gutman 40c406051c Ignore non-relative MotionEvents on Oreo to fix mouse jumping when toggling capture 2018-03-20 20:21:21 -07:00
Cameron Gutman 8bac873e67 Make sure the joystick actually has relevant axes to avoid FPing on some weird keyboards 2018-03-20 19:47:33 -07:00
Cameron Gutman a170e1efd7 Update to Gradle 4.5 2018-03-20 19:21:02 -07:00
Cameron Gutman 17bffa8d78 Fix race condition between polling return and onPause() 2018-03-20 19:10:04 -07:00
Cameron Gutman 289222749b Cover another broken G Pad III 8.0 FHD variant (b3_open_kr) 2018-03-20 18:54:48 -07:00
Cameron Gutman 81d84600d4 Start connection in onSurfaceChanged() just in case we render our first frame prior to surface configuration 2018-03-20 18:37:31 -07:00
Cameron Gutman 0b15fd582d Update gitignore and delete iml file 2018-03-20 18:06:02 -07:00
Cameron Gutman cbe4a1cde6 Update Gradle for AS 3.1 RC3 2018-03-20 18:02:34 -07:00
Cameron Gutman 89ef16c02e Fix level_idc 31 patch 2018-03-18 00:59:58 -07:00
Cameron Gutman 58b6ed8d00 Update Gradle wrapper 2018-03-11 18:12:01 -07:00
Cameron Gutman 7d01e1a7a4 Fix landscape orientation lock 2018-03-11 15:31:10 -07:00
Cameron Gutman ab769a1606 Version 5.7.2 2018-03-07 18:53:38 -08:00
Nikita Glazkov 3ac9abbab1 Russian translation (#546)
* Fix russian translation

* Complete russian translation
2018-03-07 18:44:33 -08:00
Marco 288efd0726 Added new strings (#542) 2018-03-07 18:43:33 -08:00
Nikita Glazkov d2d0ed65d6 Different app label for debug builds (#545)
* Different app label for debug builds

* Remove underscores from app labels
2018-03-07 18:41:34 -08:00
Cameron Gutman e697ed72db Add missing <network-security-config> tag 2018-03-07 18:17:00 -08:00
Cameron Gutman b657c746be Pass the BouncyCastle provider directly rather than by name, since the latter doesn't work on Android P (at least DP1) 2018-03-07 17:56:50 -08:00
Cameron Gutman 947f8db2d5 Update for Android Studio 3.1 2018-03-07 17:55:49 -08:00
Cameron Gutman 15857efd36 Add network security config allowing plaintext for Android P 2018-03-07 11:47:19 -08:00
Cameron Gutman 3fd0f20e10 Version 5.7.1 2018-03-01 22:18:32 -08:00
Cameron Gutman a2e64fd7df Fix crash when running Dutch language. Fixes #543 2018-03-01 22:09:49 -08:00
Cameron Gutman a620dc7d0c Version 5.7 2018-02-25 13:41:09 -08:00
Cameron Gutman 9d7a28e408 Implement deletion of OSC settings 2018-02-25 13:33:52 -08:00
Cameron Gutman 3244344fc7 Add preference dependencies for USB and OSC 2018-02-25 13:12:37 -08:00
Cameron Gutman 75057f2d39 Persist OSC configuration between launches 2018-02-25 13:07:07 -08:00
Cameron Gutman bbec3402d9 Reduce opacity of OSC configuration button 2018-02-25 12:18:45 -08:00
Cameron Gutman dcf4dac8dd Only add L3/R3 buttons for the L3/R3-only config, since the analog sticks work for this 2018-02-25 12:17:59 -08:00
Cameron Gutman d98f484aaf Change OSC configuration button to work better on rounded screen devices 2018-02-25 12:12:23 -08:00
Cameron Gutman 0218a9ce14 Small string update 2018-02-24 21:07:18 -08:00
Cameron Gutman 0ec6dcd67e Add 360p option and change bitrate to kbps 2018-02-24 21:05:45 -08:00
Cameron Gutman 88f9b68db7 Add mouse emulation and bind all USB devices options 2018-02-24 20:17:14 -08:00
Cameron Gutman 3c2fd32d1e Improve error reporting for incorrect IP address 2018-02-24 19:36:23 -08:00
Cameron Gutman 6557cba307 Add support for scrolling with d-pad in mouse emulation mode 2018-02-24 18:57:09 -08:00
Cameron Gutman ae6f797436 Handle right-clicks that are synthesized into back button presses 2018-02-19 17:29:18 -08:00
Cameron Gutman 3442a64f4d Update common to fix audio dropouts. Fixes #523 2018-02-19 01:21:13 -08:00
Cameron Gutman 37ddccde0c Version 5.6.7 2018-02-14 18:39:04 -08:00
Cameron Gutman ffc59c6bd6 Update moonlight-common to fix pairing timeout issue 2018-02-14 18:34:45 -08:00
Cameron Gutman 88f84a0c12 Version 5.6.6 2018-02-10 17:21:36 -08:00
Cameron Gutman 03ecf3e5ac Fix crash on Knox devices with USB blocking policies 2018-02-10 16:56:01 -08:00
Cameron Gutman 617c8582b4 Fix crash on MediaTek PAL Android TVs 2018-02-10 16:42:45 -08:00
Cameron Gutman ef3b28295b Update dependency versions 2018-02-05 18:38:23 -08:00
Cameron Gutman 3bcd2ee068 Ignore bogus refresh rates just to be on the safe side 2018-02-04 15:26:40 -08:00
Cameron Gutman d4ff58b3ad Version 5.6.5 2018-02-04 12:59:52 -08:00
Cameron Gutman c797318ece Use frame drop hack to reduce latency and micro-stuttering for now 2018-02-04 12:59:04 -08:00
Cameron Gutman 82387d23f8 Send client's display refresh rate to server for better frame pacing 2018-02-03 22:09:42 -08:00
Cameron Gutman 766e629be5 Use applicationId com.limelight.unofficial for release builds by default 2018-02-03 19:45:18 -08:00
Cameron Gutman b93aa42c0c Fix detection on HEVC support on some buggy devices 2018-01-28 21:16:28 -08:00
Cameron Gutman 36f132942f Version 5.6.4 2018-01-20 15:13:17 -08:00
Cameron Gutman e4c251e7ee Ignore NVIDIA mouse capture extension on root builds to avoid broken LineageOS implementation 2018-01-20 02:31:40 -08:00
Cameron Gutman fb54bd5c78 Send the initial number of connected gamepads during launch to fix some games like L4D2 2018-01-20 01:16:25 -08:00
Cameron Gutman 8d4c86e113 Update common to support sending initial gamepads and fixing WoL 2018-01-20 01:11:39 -08:00
Cameron Gutman 7fafb8e0ff Revert extractNativeLibraries=false change due to install failure on Fire TV 3 2018-01-11 00:03:03 -08:00
Cameron Gutman fbcbe09255 Version 5.6.3 2018-01-10 00:40:08 -08:00
Cameron Gutman e336a4446a Update common to work with GFE 3.12 2018-01-10 00:38:15 -08:00
Cameron Gutman ffb35b2cdd Use smaller packets for streaming at 1080p and below to attempt to mitigate some reported regressions with v5.6.2 2018-01-09 23:38:25 -08:00
Cameron Gutman 2d0af6281c Ensure polling threads terminate even when polling resumes immediately 2017-12-29 14:05:29 -08:00
Cameron Gutman 472a7f6c8a Version 5.6.2 2017-12-27 22:45:07 -08:00
Cameron Gutman cd06559c66 Also count link-local addresses as local 2017-12-27 22:41:21 -08:00
Cameron Gutman d833933aaa Allow up to 1 second for fast poll to address connection flakiness 2017-12-27 22:27:35 -08:00
Cameron Gutman dc3495d59b Improve local vs. remote heuristics 2017-12-27 21:43:12 -08:00
Cameron Gutman e3a2e40043 Shrink large box art down to the normal size by changing sample size 2017-12-27 21:28:38 -08:00
Cameron Gutman 31e1fb743e Update common to address some null PC name crashes 2017-12-27 20:36:07 -08:00
Cameron Gutman bc59f11096 Disable RFI on b3_att_us 2017-12-27 20:34:02 -08:00
Cameron Gutman 6d97775aa9 Try disabling RFI if the previous run crashes 2017-12-27 20:32:34 -08:00
Cameron Gutman 3fff34e08a Don't extract native libraries for non-root build 2017-12-27 19:40:49 -08:00
Cameron Gutman 15e856dccb Move AudioTrack flush to cleanup() callback since all sample submission has ceased by then 2017-12-06 20:43:58 -08:00
Cameron Gutman 07d04171c3 Force HEVC enabled if HDR is requested 2017-12-05 17:38:25 -08:00
Cameron Gutman 42bd93cb3a Update common to fix mDNS race condition 2017-12-05 17:33:55 -08:00
Cameron Gutman 7d289f1134 Fix race conditions when frames are submitted after stop() has been called 2017-12-05 17:28:04 -08:00
Cameron Gutman 214461e123 Version 5.6.1 2017-12-01 00:42:43 -08:00
Cameron Gutman b0144a3256 Update decoder-errata.txt with HEVC errata 2017-12-01 00:40:32 -08:00
Cameron Gutman 3171256c6e Remove EvdevCaptureProvider components from non-root build 2017-12-01 00:37:25 -08:00
Cameron Gutman 5c69f6716c Don't build evdev_reader for the non-root variant 2017-12-01 00:10:55 -08:00
Cameron Gutman 6264781539 Update common to get decoder compatibility fixes 2017-11-30 23:47:08 -08:00
Cameron Gutman 0225f534d0 Fix H.265 streaming issues with MediaTek Android TV devices 2017-11-29 20:27:33 -08:00
Cameron Gutman 284a31737e Catch input buffer too small 2017-11-28 19:33:34 -08:00
Cameron Gutman b37a2dea57 Fix help display on some Android TV devices 2017-11-25 15:08:22 -08:00
Cameron Gutman 5c865e7f36 Version 5.6 r4 2017-11-25 14:33:41 -08:00
Cameron Gutman 04d9aea8c8 Detect and report decoder hangs 2017-11-25 14:27:04 -08:00
Cameron Gutman b6f52db9c3 Fix crash when input events are received and no H.264 decoder is present 2017-11-25 13:35:46 -08:00
Cameron Gutman 99d2e40683 Reset HDR when decoder crashes 3 times in a row 2017-11-25 13:21:04 -08:00
Cameron Gutman 02c4ed2724 Improve decoder crash reporting reliability 2017-11-25 13:19:30 -08:00
Cameron Gutman 5f4aab8f94 Improve decoder crash reporting detail 2017-11-25 12:56:54 -08:00
Cameron Gutman ec65901003 Report frames rendered in decoder crash report 2017-11-25 11:25:04 -08:00
Cameron Gutman 915acee88d Version 5.6 r3 2017-11-23 11:41:20 -08:00
Cameron Gutman 300d444f71 Ensure inForeground is set before CMS binding can complete 2017-11-23 11:34:22 -08:00
Cameron Gutman f37ab40c2f Fix race condition between completeOnCreate() and onConfigurationChanged() 2017-11-23 11:25:51 -08:00
Cameron Gutman 16e285d926 Version 5.6 r2 2017-11-21 21:33:06 -08:00
Cameron Gutman f2d122a275 Fix screen dimensions for portrait devices 2017-11-21 20:18:28 -08:00
Cameron Gutman bfa5a6349e Ensure MediaCodecHelper is initialized before evaluating codecs 2017-11-21 19:27:08 -08:00
Cameron Gutman a56689aea3 Always include resolutions that fit on the display 2017-11-21 19:18:41 -08:00
Cameron Gutman 3a5ba820cb Version 5.6 2017-11-20 23:08:43 -08:00
Cameron Gutman ec69fef36f Ignore back button presses on the default context 2017-11-20 22:46:57 -08:00
Cameron Gutman ff38074f55 Report GL Renderer in RendererException 2017-11-20 22:38:22 -08:00
Cameron Gutman 85d0ce0c40 Update Gradle to 3.0.1 2017-11-20 22:28:54 -08:00
Cameron Gutman 777129ca90 Move GLRenderer fetching into PcView to avoid race conditions inside Game activity and cache the result 2017-11-20 22:28:19 -08:00
Cameron Gutman 06156c4d68 Ignore back from goodix_fp device 2017-11-20 21:03:36 -08:00
Cameron Gutman 1c725b9dac Don't use reference picture invalidation on low-end Snapdragon SoCs 2017-11-20 20:56:31 -08:00
Cameron Gutman f761ee52db Exclude resolutions that are not supported by the decoders 2017-11-18 19:47:39 -08:00
Cameron Gutman 05e8cfcc0a Report adaptive playback status in crash reports 2017-11-18 18:31:12 -08:00
Cameron Gutman 912925ef2c Disable performance optimizations when in multi-window 2017-11-18 17:14:40 -08:00
Cameron Gutman 4deb881ec8 Enable adaptive playback on non-Intel devices 2017-11-18 16:37:17 -08:00
Cameron Gutman f55d6308ce Pass source rect to PiP to smoothly animate to 16:9 2017-11-18 16:29:03 -08:00
Cameron Gutman 44a3a141c0 Submit H.264 CSD in a single blob to try to prevent some decoder crashes 2017-11-18 15:14:25 -08:00
Cameron Gutman 37b5ba004c Fix IDR frame NALU drop race condition 2017-11-18 14:43:04 -08:00
Cameron Gutman b774b47213 Update for NDK 16 (deprecating MIPS) 2017-11-18 13:38:45 -08:00
Cameron Gutman 74dc00445e Version 5.5 2017-11-10 01:19:23 -08:00
Cameron Gutman 3b4563d5ea Suppress digital trigger events if an analog trigger axis is present. Fixes #465 2017-11-10 00:50:02 -08:00
Cameron Gutman 38669817b4 Update common to fix HEVC artifacts in some apps 2017-11-10 00:10:21 -08:00
Cameron Gutman 8f1d3ae04e Add support for PiP on Oreo 2017-11-09 23:28:22 -08:00
Cameron Gutman 74ed95871b Exclude HDR toggle when the device doesn't support it 2017-11-09 21:57:33 -08:00
Cameron Gutman cc5d67616c Prevent false USB access prompts due to races with kernel input stack bringup 2017-11-09 21:14:10 -08:00
Cameron Gutman eed7f09e6f Fix numpad operator keys not working 2017-11-07 22:03:40 -08:00
Cameron Gutman e3c1d23744 Fix SHIELD remote back button not working 2017-11-07 21:45:07 -08:00
Cameron Gutman c4b1200b43 Update build tools to 27.0.1 2017-11-07 21:44:27 -08:00
Cameron Gutman dff09f33a3 Fix shift not working on soft keyboard 2017-11-07 00:27:27 -08:00
Cameron Gutman 1f6b1dc2fe Send different VK codes for left and right ctrl/alt/shift keys. Fixes #318 2017-11-06 23:38:48 -08:00
Cameron Gutman 3f118dae93 Add HDR support and tweak HEVC supported decoders 2017-11-05 19:31:05 -08:00
Cameron Gutman 91a30ff6fe Target O MR1 2017-11-05 15:43:11 -08:00
BryanHaley 5102669b06 Virtual L3 R3 Buttons (#453)
* Added virtual L3 R3 options to better support gamepads missing these buttons.

* Update preferences.xml
2017-11-05 13:57:02 -08:00
Cameron Gutman 2e2f09be00 Fix frame drops when stopping the stream 2017-11-05 13:49:06 -08:00
Cameron Gutman c402103fe3 Avoid colliding with System UI in multi-window mode 2017-11-05 13:15:06 -08:00
Cameron Gutman 5e5df8abc8 Add never drop frames option for devices with micro-stuttering issues 2017-11-05 12:29:33 -08:00
Cameron Gutman d125eb7b16 Update to gradle 3.0.0 2017-11-05 12:08:16 -08:00
Cameron Gutman a116858493 Add .debug suffix to debug builds 2017-11-05 12:07:52 -08:00
Cameron Gutman 5f3b333e98 Version 5.2.1 2017-10-17 00:38:59 -07:00
Cameron Gutman 80a37855c7 Merge branch 'master' of github.com:moonlight-stream/moonlight-android 2017-10-17 00:37:00 -07:00
Cameron Gutman 5db1ec8ec0 Fix support for GFE 3.10 2017-10-17 00:35:36 -07:00
Cameron Gutman 8911c58e50 Block OMX.ffmpeg software decoders 2017-10-17 00:31:26 -07:00
Cameron Gutman 780a64694d Fix NPE when input device is removed during enumeration 2017-10-17 00:07:51 -07:00
Cameron Gutman 3c5ea9c8c3 Remove Nvidia's HEVC decoder from the hard blacklist now that it seems to be fine on Foster NRD90M 2017-10-08 22:06:06 -07:00
Cameron Gutman 40d1436ce3 Update for AS 3.0 Beta 7 2017-10-04 19:30:36 -07:00
Cameron Gutman dbb02acd37 Reintroduce the 75% HEVC bitrate multiplier that the old streaming core had 2017-09-25 21:39:53 -07:00
Cameron Gutman 20c4eac4ef Force HEVC disabled on Qualcomm SoCs older than Snapdragon 805 2017-09-19 21:21:23 -07:00
81 changed files with 3068 additions and 1124 deletions
+8
View File
@@ -0,0 +1,8 @@
# ProBot No Response (https://probot.github.io/apps/no-response/)
daysUntilClose: 7
responseRequiredLabel: 'need more info'
closeComment: >
This issue has been automatically closed because there was no response to a
request for more information from the issue opener. Please leave a comment or
open a new issue if you have additional information related to this issue.
+14
View File
@@ -0,0 +1,14 @@
# ProBot Stale (https://probot.github.io/apps/stale/)
daysUntilStale: 90
daysUntilClose: 7
exemptLabels:
- accepted
- bug
- enhancement
- meta
staleLabel: stale
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs.
closeComment: false
+5 -2
View File
@@ -1,6 +1,9 @@
#built application files
# built application files
*.apk
*.ap_
*.aab
output.json
out/
# files for the dex VM
*.dex
@@ -30,7 +33,7 @@ Thumbs.db
#.idea/workspace.xml - remove # and delete .idea if it better suit your needs.
.gradle
build/
app/app.iml
*.iml
# Compiled JNI libraries folder
**/jniLibs
+1 -3
View File
@@ -1,4 +1,4 @@
# Moonlight
# Moonlight Android
[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.
@@ -6,8 +6,6 @@ We reverse engineered the Shield streaming software and created a version that c
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.
[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/moonlight-stream/moonlight-docs/wiki) for more detailed information or a troubleshooting guide.
## Features
+73 -19
View File
@@ -1,18 +1,15 @@
import com.android.builder.model.ProductFlavor
import org.apache.tools.ant.taskdefs.condition.Os
apply plugin: 'com.android.application'
android {
compileSdkVersion 26
buildToolsVersion '26.0.1'
buildToolsVersion '28.0.3'
compileSdkVersion 28
defaultConfig {
minSdkVersion 16
targetSdkVersion 26
targetSdkVersion 28
versionName "5.2"
versionCode = 131
versionName "5.9.3"
versionCode = 172
}
flavorDimensions "root"
@@ -23,20 +20,24 @@ android {
// version to devices running O on the Play Store.
maxSdkVersion 25
applicationId "com.limelight.root"
ndk {
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64", "mips", "mips64"
externalNativeBuild {
ndkBuild {
arguments "PRODUCT_FLAVOR=root"
}
}
applicationId "com.limelight.root"
dimension "root"
}
nonRoot {
applicationId "com.limelight"
ndk {
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64", "mips", "mips64"
externalNativeBuild {
ndkBuild {
arguments "PRODUCT_FLAVOR=nonRoot"
}
}
applicationId "com.limelight"
dimension "root"
}
}
@@ -45,10 +46,63 @@ android {
disable 'MissingTranslation'
}
bundle {
language {
// Avoid splitting by language, since we allow users
// to manually switch language in settings.
enableSplit = false
}
density {
// FIXME: This should not be neccessary but we get
// weird crashes due to missing drawable resources
// when this split is enabled.
enableSplit = false
}
}
buildTypes {
debug {
applicationIdSuffix ".debug"
minifyEnabled true
useProguard false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt')
// To whomever is releasing/using an APK in release mode with
// Moonlight's official application ID, please stop. I see every
// single one of your crashes in my Play Console and it makes
// Moonlight's reliability look worse and makes it more difficult
// to distinguish real crashes from your crashy VR app. Seriously,
// 44 of the *same* native crash in 72 hours and a few each of
// several other crashes.
//
// This is technically not your fault. I would have hoped Google
// would validate the signature of the APK before attributing
// the crash to it. I asked their Play Store support about this
// and they said they don't and don't have plans to, so that sucks.
//
// In any case, it's bad form to release an APK using someone
// else's application ID. There is no legitimate reason, that
// anyone would need to comment out the following line, except me
// when I release an official signed Moonlight build. If you feel
// like doing so would solve something, I can tell you it will not.
// You can't upgrade an app while retaining data without having the
// same signature as the official version. Nor can you post it on
// the Play Store, since that application ID is already taken.
// Reputable APK hosting websites similarly validate the signature
// is consistent with the Play Store and won't allow an APK that
// isn't signed the same as the original.
//
// I wish any and all people using Moonlight as the basis of other
// cool projects the best of luck with their efforts. All I ask
// is to please change the applicationId before you publish.
//
// TL;DR: Leave the following line alone!
applicationIdSuffix ".unofficial"
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
@@ -60,9 +114,9 @@ android {
}
dependencies {
implementation 'org.bouncycastle:bcprov-jdk15on:1.57'
implementation 'org.bouncycastle:bcpkix-jdk15on:1.57'
implementation files('libs/jcodec-0.1.9-patched.jar')
implementation 'org.bouncycastle:bcprov-jdk15on:1.59'
implementation 'org.bouncycastle:bcpkix-jdk15on:1.59'
implementation 'org.jcodec:jcodec:0.2.3'
implementation project(':moonlight-common')
}
Binary file not shown.
+27
View File
@@ -0,0 +1,27 @@
# Don't obfuscate code
-dontobfuscate
# Our code
-keep class com.limelight.binding.input.evdev.* {*;}
# Moonlight common
-keep class com.limelight.nvstream.jni.* {*;}
# Okio
-keep class sun.misc.Unsafe {*;}
-dontwarn java.nio.file.*
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
-dontwarn okio.**
# BouncyCastle
-keep class org.bouncycastle.jcajce.provider.asymmetric.* {*;}
-keep class org.bouncycastle.jcajce.provider.asymmetric.util.* {*;}
-keep class org.bouncycastle.jcajce.provider.asymmetric.rsa.* {*;}
-keep class org.bouncycastle.jcajce.provider.digest.** {*;}
-keep class org.bouncycastle.jcajce.provider.symmetric.** {*;}
-keep class org.bouncycastle.jcajce.spec.* {*;}
-keep class org.bouncycastle.jce.** {*;}
-dontwarn javax.naming.**
# jMDNS
-dontwarn javax.jmdns.impl.DNSCache
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_label" translatable="false">Moonlight (Debug)</string>
<string name="app_label_root" translatable="false">Moonlight (Root Debug)</string>
</resources>
+8 -2
View File
@@ -32,6 +32,7 @@
<application
android:allowBackup="true"
android:fullBackupContent="@xml/backup_rules"
android:networkSecurityConfig="@xml/network_security_config"
android:isGame="true"
android:banner="@drawable/atv_banner"
android:appCategory="game"
@@ -66,8 +67,9 @@
</activity>
<!-- Small hack to support launcher shortcuts without relaunching over and over again when the back button is pressed -->
<activity
android:name=".AppViewShortcutTrampoline"
android:name=".ShortcutTrampoline"
android:noHistory="true"
android:exported="true"
android:resizeableActivity="true"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
<meta-data
@@ -98,12 +100,16 @@
android:name="android.support.PARENT_ACTIVITY"
android:value="com.limelight.PcView" />
</activity>
<!-- This will fall back to sensorLandscape at runtime on Android 4.2 and below -->
<activity
android:name=".Game"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
android:screenOrientation="sensorLandscape"
android:screenOrientation="userLandscape"
android:noHistory="true"
android:supportsPictureInPicture="true"
android:resizeableActivity="true"
android:launchMode="singleTask"
android:excludeFromRecents="true"
android:theme="@style/StreamTheme">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
+47 -6
View File
@@ -26,6 +26,10 @@ import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.view.ContextMenu;
@@ -36,6 +40,7 @@ import android.view.ContextMenu.ContextMenuInfo;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.AdapterView.AdapterContextMenuInfo;
@@ -56,7 +61,9 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
private final static int START_OR_RESUME_ID = 1;
private final static int QUIT_ID = 2;
private final static int CANCEL_ID = 3;
private final static int START_WTIH_QUIT = 4;
private final static int START_WITH_QUIT = 4;
private final static int VIEW_DETAILS_ID = 5;
private final static int CREATE_SHORTCUT_ID = 6;
public final static String NAME_EXTRA = "Name";
public final static String UUID_EXTRA = "UUID";
@@ -235,6 +242,10 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Assume we're in the foreground when created to avoid a race
// between binding to CMS and onResume()
inForeground = true;
shortcutHelper = new ShortcutHelper(this);
UiHelper.setLocale(this);
@@ -247,10 +258,9 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
String computerName = getIntent().getStringExtra(NAME_EXTRA);
String labelText = getResources().getString(R.string.title_applist)+" "+computerName;
TextView label = findViewById(R.id.appListText);
setTitle(labelText);
label.setText(labelText);
setTitle(computerName);
label.setText(computerName);
// Add a launcher shortcut for this PC (forced, since this is user interaction)
shortcutHelper.createAppViewShortcut(uuidString, computerName, uuidString, true);
@@ -300,6 +310,9 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
protected void onResume() {
super.onResume();
// Display a decoder crash notification if we've returned after a crash
UiHelper.showDecoderCrashDialog(this);
inForeground = true;
startComputerUpdates();
}
@@ -324,10 +337,25 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit));
}
else {
menu.add(Menu.NONE, START_WTIH_QUIT, 1, getResources().getString(R.string.applist_menu_quit_and_start));
menu.add(Menu.NONE, START_WITH_QUIT, 1, getResources().getString(R.string.applist_menu_quit_and_start));
menu.add(Menu.NONE, CANCEL_ID, 2, getResources().getString(R.string.applist_menu_cancel));
}
}
menu.add(Menu.NONE, VIEW_DETAILS_ID, 3, getResources().getString(R.string.applist_menu_details));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Only add an option to create shortcut if box art is loaded
// and when we're in grid-mode (not list-mode).
ImageView appImageView = info.targetView.findViewById(R.id.grid_image);
if (appImageView != null) {
// We have a grid ImageView, so we must be in grid-mode
BitmapDrawable drawable = (BitmapDrawable)appImageView.getDrawable();
if (drawable != null && drawable.getBitmap() != null) {
// We have a bitmap loaded too
menu.add(Menu.NONE, CREATE_SHORTCUT_ID, 4, getResources().getString(R.string.applist_menu_scut));
}
}
}
}
@Override
@@ -339,7 +367,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
final AppObject app = (AppObject) appGridAdapter.getItem(info.position);
switch (item.getItemId()) {
case START_WTIH_QUIT:
case START_WITH_QUIT:
// Display a confirmation dialog first
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
@Override
@@ -379,6 +407,19 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
case CANCEL_ID:
return true;
case VIEW_DETAILS_ID:
Dialog.displayDialog(AppView.this, getResources().getString(R.string.title_details),
getResources().getString(R.string.applist_details_id) + " " + app.app.getAppId(), false);
return true;
case CREATE_SHORTCUT_ID:
ImageView appImageView = info.targetView.findViewById(R.id.grid_image);
Bitmap appBits = ((BitmapDrawable)appImageView.getDrawable()).getBitmap();
if (!shortcutHelper.createPinnedGameShortcut(uuidString + Integer.valueOf(app.app.getAppId()).toString(), appBits, computer, app.app)) {
Toast.makeText(AppView.this, getResources().getString(R.string.unable_to_pin_shortcut), Toast.LENGTH_LONG).show();
}
return true;
default:
return super.onContextItemSelected(item);
}
@@ -1,164 +0,0 @@
package com.limelight;
import android.app.Activity;
import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import com.limelight.computers.ComputerManagerListener;
import com.limelight.computers.ComputerManagerService;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp;
import com.limelight.utils.Dialog;
import com.limelight.utils.ServerHelper;
import com.limelight.utils.SpinnerDialog;
import com.limelight.utils.UiHelper;
import java.util.ArrayList;
import java.util.UUID;
public class AppViewShortcutTrampoline extends Activity {
private String uuidString;
private ComputerDetails computer;
private SpinnerDialog blockingLoadSpinner;
public final static String UUID_EXTRA = "UUID";
private ComputerManagerService.ComputerManagerBinder managerBinder;
private final ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder binder) {
final ComputerManagerService.ComputerManagerBinder localBinder =
((ComputerManagerService.ComputerManagerBinder)binder);
// Wait in a separate thread to avoid stalling the UI
new Thread() {
@Override
public void run() {
// Wait for the binder to be ready
localBinder.waitForReady();
// Now make the binder visible
managerBinder = localBinder;
// Get the computer object
computer = managerBinder.getComputer(UUID.fromString(uuidString));
// Force CMS to repoll this machine
managerBinder.invalidateStateForComputer(computer.uuid);
// Start polling
managerBinder.startPolling(new ComputerManagerListener() {
@Override
public void notifyComputerUpdated(final ComputerDetails details) {
// Don't care about other computers
if (!details.uuid.toString().equalsIgnoreCase(uuidString)) {
return;
}
if (details.state != ComputerDetails.State.UNKNOWN) {
runOnUiThread(new Runnable() {
@Override
public void run() {
// Stop showing the spinner
if (blockingLoadSpinner != null) {
blockingLoadSpinner.dismiss();
blockingLoadSpinner = null;
}
if (details.state == ComputerDetails.State.ONLINE) {
// Close this activity
finish();
// Create a new activity stack for this launch
ArrayList<Intent> intentStack = new ArrayList<>();
Intent i;
// Add the PC view at the back (and clear the task)
i = new Intent(AppViewShortcutTrampoline.this, PcView.class);
i.setAction(Intent.ACTION_MAIN);
i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
intentStack.add(i);
// Take this intent's data and create an intent to start the app view
i = new Intent(getIntent());
i.setClass(AppViewShortcutTrampoline.this, AppView.class);
intentStack.add(i);
// If a game is running, we'll make the stream the top level activity
if (details.runningGameId != 0) {
intentStack.add(ServerHelper.createStartIntent(AppViewShortcutTrampoline.this,
new NvApp("app", details.runningGameId), details, managerBinder));
}
// Now start the activities
startActivities(intentStack.toArray(new Intent[]{}));
}
else if (details.state == ComputerDetails.State.OFFLINE) {
// Computer offline - display an error dialog
Dialog.displayDialog(AppViewShortcutTrampoline.this,
getResources().getString(R.string.conn_error_title),
getResources().getString(R.string.error_pc_offline),
true);
}
// We don't want any more callbacks from now on, so go ahead
// and unbind from the service
if (managerBinder != null) {
managerBinder.stopPolling();
unbindService(serviceConnection);
managerBinder = null;
}
}
});
}
}
});
}
}.start();
}
public void onServiceDisconnected(ComponentName className) {
managerBinder = null;
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
UiHelper.notifyNewRootView(this);
uuidString = getIntent().getStringExtra(UUID_EXTRA);
// Bind to the computer manager service
bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
Service.BIND_AUTO_CREATE);
blockingLoadSpinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.conn_establishing_title),
getResources().getString(R.string.applist_connect_msg), true);
}
@Override
protected void onPause() {
super.onPause();
if (blockingLoadSpinner != null) {
blockingLoadSpinner.dismiss();
blockingLoadSpinner = null;
}
Dialog.closeDialogs();
if (managerBinder != null) {
managerBinder.stopPolling();
unbindService(serviceConnection);
managerBinder = null;
}
finish();
}
}
+453 -134
View File
@@ -20,6 +20,7 @@ import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.input.KeyboardPacket;
import com.limelight.nvstream.input.MouseButtonPacket;
import com.limelight.nvstream.jni.MoonBridge;
import com.limelight.preferences.GlPreferences;
import com.limelight.preferences.PreferenceConfiguration;
import com.limelight.ui.GameGestures;
import com.limelight.ui.StreamView;
@@ -29,14 +30,19 @@ import com.limelight.utils.SpinnerDialog;
import com.limelight.utils.UiHelper;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.PictureInPictureParams;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.graphics.Point;
import android.graphics.Rect;
import android.hardware.input.InputManager;
import android.media.AudioManager;
import android.net.ConnectivityManager;
@@ -46,6 +52,7 @@ import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.SystemClock;
import android.util.Rational;
import android.view.Display;
import android.view.InputDevice;
import android.view.KeyEvent;
@@ -64,7 +71,7 @@ import android.widget.Toast;
public class Game extends Activity implements SurfaceHolder.Callback,
OnGenericMotionListener, OnTouchListener, NvConnectionListener, EvdevListener,
OnSystemUiVisibilityChangeListener, GameGestures
OnSystemUiVisibilityChangeListener, GameGestures, StreamView.InputCallbacks
{
private int lastMouseX = Integer.MIN_VALUE;
private int lastMouseY = Integer.MIN_VALUE;
@@ -90,16 +97,21 @@ public class Game extends Activity implements SurfaceHolder.Callback,
private boolean displayedFailureDialog = false;
private boolean connecting = false;
private boolean connected = false;
private boolean surfaceCreated = false;
private boolean attemptedConnection = false;
private InputCaptureProvider inputCaptureProvider;
private int modifierFlags = 0;
private boolean grabbedInput = true;
private boolean grabComboDown = false;
private StreamView streamView;
private boolean gotBackPointerEvent = false;
private boolean syntheticBackDown = false;
private ShortcutHelper shortcutHelper;
private MediaCodecDecoderRenderer decoderRenderer;
private boolean reportedCrash;
private WifiManager.WifiLock wifiLock;
@@ -125,22 +137,19 @@ public class Game extends Activity implements SurfaceHolder.Callback,
public static final String EXTRA_STREAMING_REMOTE = "Remote";
public static final String EXTRA_PC_UUID = "UUID";
public static final String EXTRA_PC_NAME = "PcName";
public static final String EXTRA_APP_HDR = "HDR";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
shortcutHelper = new ShortcutHelper(this);
UiHelper.setLocale(this);
// We don't want a title bar
requestWindowFeature(Window.FEATURE_NO_TITLE);
// Full-screen and don't let the display go off
getWindow().addFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN |
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
// Full-screen
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
// If we're going to use immersive mode, we want to have
// the entire screen
@@ -153,6 +162,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
getWindow().addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN);
}
// We specified userLandscape in the manifest which isn't supported until 4.3,
// so we must fall back at runtime to sensorLandscape.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
}
// Listen for UI visibility events
getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(this);
@@ -170,11 +185,18 @@ public class Game extends Activity implements SurfaceHolder.Callback,
prefConfig = PreferenceConfiguration.readPreferences(this);
tombstonePrefs = Game.this.getSharedPreferences("DecoderTombstone", 0);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && prefConfig.stretchVideo) {
// Allow the activity to layout under notches if the fill-screen option
// was turned on by the user
getWindow().getAttributes().layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
}
// Listen for events on the game surface
streamView = findViewById(R.id.surfaceView);
streamView.setOnGenericMotionListener(this);
streamView.setOnTouchListener(this);
streamView.setInputCallbacks(this);
inputCaptureProvider = InputCaptureManager.getInputCaptureProvider(this, this);
@@ -191,7 +213,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
// Warn the user if they're on a metered connection
checkDataConnection();
ConnectivityManager connMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
if (connMgr.isActiveNetworkMetered()) {
displayTransientMessage(getResources().getString(R.string.conn_metered));
}
// Make sure Wi-Fi is fully powered up
WifiManager wifiMgr = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
@@ -206,6 +231,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
boolean remote = Game.this.getIntent().getBooleanExtra(EXTRA_STREAMING_REMOTE, false);
String uuid = Game.this.getIntent().getStringExtra(EXTRA_PC_UUID);
String pcName = Game.this.getIntent().getStringExtra(EXTRA_PC_NAME);
boolean willStreamHdr = Game.this.getIntent().getBooleanExtra(EXTRA_APP_HDR, false);
if (appId == StreamConfiguration.INVALID_APP_ID) {
finish();
@@ -213,15 +239,51 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
// Add a launcher shortcut for this PC (forced, since this is user interaction)
shortcutHelper = new ShortcutHelper(this);
shortcutHelper.createAppViewShortcut(uuid, pcName, uuid, true);
shortcutHelper.reportShortcutUsed(uuid);
// Initialize the MediaCodec helper before creating the decoder
MediaCodecHelper.initializeWithContext(this);
GlPreferences glPrefs = GlPreferences.readPreferences(this);
MediaCodecHelper.initialize(this, glPrefs.glRenderer);
decoderRenderer = new MediaCodecDecoderRenderer(prefConfig.videoFormat,
prefConfig.bitrate,
prefConfig.batterySaver,
// Check if the user has enabled HDR
if (prefConfig.enableHdr) {
// Check if the app supports it
if (!willStreamHdr) {
Toast.makeText(this, "This game does not support HDR10", Toast.LENGTH_SHORT).show();
}
// It does, so start our HDR checklist
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// We already know the app supports HDR if willStreamHdr is set.
Display display = getWindowManager().getDefaultDisplay();
Display.HdrCapabilities hdrCaps = display.getHdrCapabilities();
// We must now ensure our display is compatible with HDR10
boolean foundHdr10 = false;
for (int hdrType : hdrCaps.getSupportedHdrTypes()) {
if (hdrType == Display.HdrCapabilities.HDR_TYPE_HDR10) {
LimeLog.info("Display supports HDR10");
foundHdr10 = true;
}
}
if (!foundHdr10) {
// Nope, no HDR for us :(
willStreamHdr = false;
Toast.makeText(this, "Display does not support HDR10", Toast.LENGTH_LONG).show();
}
}
else {
Toast.makeText(this, "HDR requires Android 7.0 or later", Toast.LENGTH_LONG).show();
willStreamHdr = false;
}
}
else {
willStreamHdr = false;
}
decoderRenderer = new MediaCodecDecoderRenderer(prefConfig,
new CrashListener() {
@Override
public void notifyCrash(Exception e) {
@@ -230,37 +292,93 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// We must use commit because the app will crash when we return from this function
tombstonePrefs.edit().putInt("CrashCount", tombstonePrefs.getInt("CrashCount", 0) + 1).commit();
reportedCrash = true;
}
},
tombstonePrefs.getInt("CrashCount", 0));
tombstonePrefs.getInt("CrashCount", 0),
connMgr.isActiveNetworkMetered(),
willStreamHdr,
glPrefs.glRenderer
);
// Don't stream HDR if the decoder can't support it
if (willStreamHdr && !decoderRenderer.isHevcMain10Hdr10Supported()) {
willStreamHdr = false;
Toast.makeText(this, "Decoder does not support HEVC Main10HDR10", Toast.LENGTH_LONG).show();
}
// Display a message to the user if H.265 was forced on but we still didn't find a decoder
if (prefConfig.videoFormat == PreferenceConfiguration.FORCE_H265_ON && !decoderRenderer.isHevcSupported()) {
Toast.makeText(this, "No H.265 decoder found.\nFalling back to H.264.", Toast.LENGTH_LONG).show();
}
if (!decoderRenderer.isAvcSupported()) {
if (spinner != null) {
spinner.dismiss();
spinner = null;
}
// If we can't find an AVC decoder, we can't proceed
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title),
"This device or ROM doesn't support hardware accelerated H.264 playback.", true);
return;
int gamepadMask = ControllerHandler.getAttachedControllerMask(this);
if (!prefConfig.multiController) {
// Always set gamepad 1 present for when multi-controller is
// disabled for games that don't properly support detection
// of gamepads removed and replugged at runtime.
gamepadMask = 1;
}
if (prefConfig.onscreenController) {
// If we're using OSC, always set at least gamepad 1.
gamepadMask |= 1;
}
// Set to the optimal mode for streaming
float displayRefreshRate = prepareDisplayForRendering();
LimeLog.info("Display refresh rate: "+displayRefreshRate);
// HACK: Despite many efforts to ensure low latency consistent frame
// delivery, the best non-lossy mechanism is to buffer 1 extra frame
// in the output pipeline. Android does some buffering on its end
// in SurfaceFlinger and it's difficult (impossible?) to inspect
// the precise state of the buffer queue to the screen after we
// release a frame for rendering.
//
// Since buffering a frame adds latency and we are primarily a
// latency-optimized client, rather than one designed for picture-perfect
// accuracy, we will synthetically induce a negative pressure on the display
// output pipeline by driving the decoder input pipeline under the speed
// that the display can refresh. This ensures a constant negative pressure
// to keep latency down but does induce a periodic frame loss. However, this
// periodic frame loss is *way* less than what we'd already get in Marshmallow's
// display pipeline where frames are dropped outside of our control if they land
// on the same V-sync.
//
// Hopefully, we can get rid of this once someone comes up with a better way
// to track the state of the pipeline and time frames.
int roundedRefreshRate = Math.round(displayRefreshRate);
if (!prefConfig.disableFrameDrop && prefConfig.fps >= roundedRefreshRate) {
if (roundedRefreshRate <= 49) {
// Let's avoid clearly bogus refresh rates and fall back to legacy rendering
decoderRenderer.enableLegacyFrameDropRendering();
LimeLog.info("Bogus refresh rate: "+roundedRefreshRate);
}
// HACK: Avoid crashing on some MTK devices
else if (roundedRefreshRate == 50 && decoderRenderer.is49FpsBlacklisted()) {
// Use the old rendering strategy on these broken devices
decoderRenderer.enableLegacyFrameDropRendering();
}
else {
prefConfig.fps = roundedRefreshRate - 1;
LimeLog.info("Adjusting FPS target for screen to "+prefConfig.fps);
}
}
StreamConfiguration config = new StreamConfiguration.Builder()
.setResolution(prefConfig.width, prefConfig.height)
.setRefreshRate(prefConfig.fps)
.setApp(new NvApp(appName, appId))
.setBitrate(prefConfig.bitrate * 1000)
.setApp(new NvApp(appName, appId, willStreamHdr))
.setBitrate(prefConfig.bitrate)
.setEnableSops(prefConfig.enableSops)
.enableLocalAudioPlayback(prefConfig.playHostAudio)
.setMaxPacketSize(remote ? 1024 : 1292)
.setMaxPacketSize((remote || prefConfig.width <= 1920) ? 1024 : 1292)
.setRemote(remote)
.setHevcBitratePercentageMultiplier(75)
.setHevcSupported(decoderRenderer.isHevcSupported())
.setEnableHdr(willStreamHdr)
.setAttachedGamepadMask(gamepadMask)
.setClientRefreshRateX100((int)(displayRefreshRate * 100))
.setAudioConfiguration(prefConfig.enable51Surround ?
MoonBridge.AUDIO_CONFIGURATION_51_SURROUND :
MoonBridge.AUDIO_CONFIGURATION_STEREO)
@@ -268,14 +386,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Initialize the connection
conn = new NvConnection(host, uniqueId, config, PlatformBinding.getCryptoProvider(this));
controllerHandler = new ControllerHandler(this, conn, this, prefConfig.multiController, prefConfig.deadzonePercentage);
controllerHandler = new ControllerHandler(this, conn, this, prefConfig);
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
inputManager.registerInputDeviceListener(controllerHandler, null);
// Set to the optimal mode for streaming
prepareDisplayForRendering();
// Initialize touch contexts
for (int i = 0; i < touchContextMap.length; i++) {
touchContextMap[i] = new TouchContext(conn, i,
@@ -303,10 +418,66 @@ public class Game extends Activity implements SurfaceHolder.Callback,
usbDriverServiceConnection, Service.BIND_AUTO_CREATE);
}
if (!decoderRenderer.isAvcSupported()) {
if (spinner != null) {
spinner.dismiss();
spinner = null;
}
// If we can't find an AVC decoder, we can't proceed
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title),
"This device or ROM doesn't support hardware accelerated H.264 playback.", true);
return;
}
// The connection will be started when the surface gets created
streamView.getHolder().addCallback(this);
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (virtualController != null) {
// Refresh layout of OSC for possible new screen size
virtualController.refreshLayout();
// Hide OSC in PiP
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (isInPictureInPictureMode()) {
virtualController.hide();
}
else {
virtualController.show();
}
}
}
}
@Override
public void onUserLeaveHint() {
super.onUserLeaveHint();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (prefConfig.enablePip && connected) {
try {
// This has thrown all sorts of weird exceptions on Samsung devices
// running Oreo. Just eat them and close gracefully on leave, rather
// than crashing.
enterPictureInPictureMode(
new PictureInPictureParams.Builder()
.setAspectRatio(new Rational(prefConfig.width, prefConfig.height))
.setSourceRectHint(new Rect(
streamView.getLeft(), streamView.getTop(),
streamView.getRight(), streamView.getBottom()))
.build());
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
@@ -321,9 +492,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
}
private void prepareDisplayForRendering() {
private float prepareDisplayForRendering() {
Display display = getWindowManager().getDefaultDisplay();
WindowManager.LayoutParams windowLayoutParams = getWindow().getAttributes();
float displayRefreshRate;
// On M, we can explicitly set the optimal display mode
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@@ -361,6 +533,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
LimeLog.info("Selected display mode: "+bestMode.getPhysicalWidth()+"x"+
bestMode.getPhysicalHeight()+"x"+bestMode.getRefreshRate());
windowLayoutParams.preferredDisplayModeId = bestMode.getModeId();
displayRefreshRate = bestMode.getRefreshRate();
}
// On L, we can at least tell the OS that we want 60 Hz
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
@@ -373,6 +546,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
LimeLog.info("Selected refresh rate: "+bestRefreshRate);
windowLayoutParams.preferredRefreshRate = bestRefreshRate;
displayRefreshRate = bestRefreshRate;
}
else {
// Otherwise, the active display refresh rate is just
// whatever is currently in use.
displayRefreshRate = display.getRefreshRate();
}
// Apply the display mode change
@@ -408,22 +587,22 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Set the surface to scale based on the aspect ratio of the stream
streamView.setDesiredAspectRatio((double)prefConfig.width / (double)prefConfig.height);
}
}
private void checkDataConnection()
{
ConnectivityManager mgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
if (mgr.isActiveNetworkMetered()) {
displayTransientMessage(getResources().getString(R.string.conn_metered));
}
return displayRefreshRate;
}
@SuppressLint("InlinedApi")
private final Runnable hideSystemUi = new Runnable() {
@Override
public void run() {
// In multi-window mode on N+, we need to drop our layout flags or we'll
// be drawing underneath the system UI.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInMultiWindowMode()) {
Game.this.getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
}
// Use immersive mode on 4.4+ or standard low profile on previous builds
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Game.this.getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
@@ -448,6 +627,34 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
}
@Override
@TargetApi(Build.VERSION_CODES.N)
public void onMultiWindowModeChanged(boolean isInMultiWindowMode) {
super.onMultiWindowModeChanged(isInMultiWindowMode);
// In multi-window, we don't want to use the full-screen layout
// flag. It will cause us to collide with the system UI.
// This function will also be called for PiP so we can cover
// that case here too.
if (isInMultiWindowMode) {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
// Disable performance optimizations for foreground
getWindow().setSustainedPerformanceMode(false);
decoderRenderer.notifyVideoBackground();
}
else {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
// Enable performance optimizations for foreground
getWindow().setSustainedPerformanceMode(true);
decoderRenderer.notifyVideoForeground();
}
// Correct the system UI visibility flags
hideSystemUi(50);
}
@Override
protected void onDestroy() {
super.onDestroy();
@@ -496,7 +703,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Add the video codec to the post-stream toast
if (message != null) {
if (videoFormat == MoonBridge.VIDEO_FORMAT_H265) {
if (videoFormat == MoonBridge.VIDEO_FORMAT_H265_MAIN10) {
message += " [H.265 HDR]";
}
else if (videoFormat == MoonBridge.VIDEO_FORMAT_H265) {
message += " [H.265]";
}
else if (videoFormat == MoonBridge.VIDEO_FORMAT_H264) {
@@ -508,9 +718,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
}
// Clear the tombstone count
if (tombstonePrefs.getInt("CrashCount", 0) != 0) {
tombstonePrefs.edit().putInt("CrashCount", 0).apply();
// Clear the tombstone count if we terminated normally
if (!reportedCrash && tombstonePrefs.getInt("CrashCount", 0) != 0) {
tombstonePrefs.edit()
.putInt("CrashCount", 0)
.putInt("LastNotifiedCrashCount", 0)
.apply();
}
}
@@ -532,19 +745,19 @@ public class Game extends Activity implements SurfaceHolder.Callback,
};
// Returns true if the key stroke was consumed
private boolean handleSpecialKeys(short translatedKey, boolean down) {
private boolean handleSpecialKeys(int androidKeyCode, boolean down) {
int modifierMask = 0;
// Mask off the high byte
translatedKey &= 0xff;
if (translatedKey == KeyboardTranslator.VK_CONTROL) {
if (androidKeyCode == KeyEvent.KEYCODE_CTRL_LEFT ||
androidKeyCode == KeyEvent.KEYCODE_CTRL_RIGHT) {
modifierMask = KeyboardPacket.MODIFIER_CTRL;
}
else if (translatedKey == KeyboardTranslator.VK_SHIFT) {
else if (androidKeyCode == KeyEvent.KEYCODE_SHIFT_LEFT ||
androidKeyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) {
modifierMask = KeyboardPacket.MODIFIER_SHIFT;
}
else if (translatedKey == KeyboardTranslator.VK_ALT) {
else if (androidKeyCode == KeyEvent.KEYCODE_ALT_LEFT ||
androidKeyCode == KeyEvent.KEYCODE_ALT_RIGHT) {
modifierMask = KeyboardPacket.MODIFIER_ALT;
}
@@ -556,7 +769,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
// Check if Ctrl+Shift+Z is pressed
if (translatedKey == KeyboardTranslator.VK_Z &&
if (androidKeyCode == KeyEvent.KEYCODE_Z &&
(modifierFlags & (KeyboardPacket.MODIFIER_CTRL | KeyboardPacket.MODIFIER_SHIFT)) ==
(KeyboardPacket.MODIFIER_CTRL | KeyboardPacket.MODIFIER_SHIFT))
{
@@ -612,18 +825,32 @@ public class Game extends Activity implements SurfaceHolder.Callback,
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
return handleKeyDown(event) || super.onKeyDown(keyCode, event);
}
@Override
public boolean handleKeyDown(KeyEvent event) {
// Pass-through virtual navigation keys
if ((event.getFlags() & KeyEvent.FLAG_VIRTUAL_HARD_KEY) != 0) {
return super.onKeyDown(keyCode, event);
return false;
}
// Handle a synthetic back button event that some Android OS versions
// create as a result of a right-click. This event WILL repeat if
// the right mouse button is held down, so we ignore those.
if ((event.getSource() == InputDevice.SOURCE_MOUSE ||
event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE) &&
event.getKeyCode() == KeyEvent.KEYCODE_BACK &&
event.getRepeatCount() == 0 &&
!gotBackPointerEvent) {
syntheticBackDown = true;
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
return true;
}
boolean handled = false;
boolean detectedGamepad = event.getDevice() != null && ((event.getDevice().getSources() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK ||
(event.getDevice().getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD);
if (detectedGamepad || (event.getDevice() == null ||
event.getDevice().getKeyboardType() != InputDevice.KEYBOARD_TYPE_ALPHABETIC
)) {
if (ControllerHandler.isGameControllerDevice(event.getDevice())) {
// Always try the controller handler first, unless it's an alphanumeric keyboard device.
// Otherwise, controller handler will eat keyboard d-pad events.
handled = controllerHandler.handleButtonDown(event);
@@ -633,20 +860,20 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Try the keyboard handler
short translated = KeyboardTranslator.translate(event.getKeyCode());
if (translated == 0) {
return super.onKeyDown(keyCode, event);
return false;
}
// Let this method take duplicate key down events
if (handleSpecialKeys(translated, true)) {
if (handleSpecialKeys(event.getKeyCode(), true)) {
return true;
}
// Pass through keyboard input if we're not grabbing
if (!grabbedInput) {
return super.onKeyDown(keyCode, event);
return false;
}
conn.sendKeyboardInput(translated, KeyboardPacket.KEY_DOWN, getModifierState());
conn.sendKeyboardInput(translated, KeyboardPacket.KEY_DOWN, getModifierState(event));
}
return true;
@@ -654,17 +881,32 @@ public class Game extends Activity implements SurfaceHolder.Callback,
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
return handleKeyUp(event) || super.onKeyUp(keyCode, event);
}
@Override
public boolean handleKeyUp(KeyEvent event) {
// Pass-through virtual navigation keys
if ((event.getFlags() & KeyEvent.FLAG_VIRTUAL_HARD_KEY) != 0) {
return super.onKeyUp(keyCode, event);
return false;
}
// Handle a synthetic back button event that some Android OS versions
// create as a result of a right-click.
if ((event.getSource() == InputDevice.SOURCE_MOUSE ||
event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE) &&
event.getKeyCode() == KeyEvent.KEYCODE_BACK &&
(!gotBackPointerEvent || syntheticBackDown)) {
// We need to raise the button if gotBackPointerEvent is true
// in the case where it transitioned to true after we already
// sent the right click down event.
syntheticBackDown = false;
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
return true;
}
boolean handled = false;
boolean detectedGamepad = event.getDevice() != null && ((event.getDevice().getSources() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK ||
(event.getDevice().getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD);
if (detectedGamepad || (event.getDevice() == null ||
event.getDevice().getKeyboardType() != InputDevice.KEYBOARD_TYPE_ALPHABETIC
)) {
if (ControllerHandler.isGameControllerDevice(event.getDevice())) {
// Always try the controller handler first, unless it's an alphanumeric keyboard device.
// Otherwise, controller handler will eat keyboard d-pad events.
handled = controllerHandler.handleButtonUp(event);
@@ -674,16 +916,16 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Try the keyboard handler
short translated = KeyboardTranslator.translate(event.getKeyCode());
if (translated == 0) {
return super.onKeyUp(keyCode, event);
return false;
}
if (handleSpecialKeys(translated, false)) {
if (handleSpecialKeys(event.getKeyCode(), false)) {
return true;
}
// Pass through keyboard input if we're not grabbing
if (!grabbedInput) {
return super.onKeyUp(keyCode, event);
return false;
}
conn.sendKeyboardInput(translated, KeyboardPacket.KEY_UP, getModifierState(event));
@@ -726,6 +968,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
{
// This case is for mice
if (event.getSource() == InputDevice.SOURCE_MOUSE ||
event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE ||
(event.getPointerCount() >= 1 &&
event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE))
{
@@ -741,6 +984,13 @@ public class Game extends Activity implements SurfaceHolder.Callback,
byte vScrollClicks = (byte) event.getAxisValue(MotionEvent.AXIS_VSCROLL);
conn.sendMouseScroll(vScrollClicks);
}
else if (event.getActionMasked() == MotionEvent.ACTION_HOVER_ENTER ||
event.getActionMasked() == MotionEvent.ACTION_HOVER_EXIT) {
// On some devices (Galaxy S8 without Oreo pointer capture), we can
// get spurious ACTION_HOVER_ENTER events when right clicking with
// incorrect X and Y coordinates. Just eat this event without processing it.
return true;
}
if ((changedButtons & MotionEvent.BUTTON_PRIMARY) != 0) {
if ((event.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0) {
@@ -758,6 +1008,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
else {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
}
// Don't use the KEYCODE_BACK hack (which interferes with mice
// with actual back buttons) since we're getting right clicks
// using this callback.
gotBackPointerEvent = true;
}
if ((changedButtons & MotionEvent.BUTTON_TERTIARY) != 0) {
@@ -769,6 +1024,28 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
}
if ((changedButtons & MotionEvent.BUTTON_BACK) != 0) {
if ((event.getButtonState() & MotionEvent.BUTTON_BACK) != 0) {
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_X1);
}
else {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_X1);
}
// Don't use the KEYCODE_BACK hack. That will cause this
// button press to trigger a right-click.
gotBackPointerEvent = true;
}
if ((changedButtons & MotionEvent.BUTTON_FORWARD) != 0) {
if ((event.getButtonState() & MotionEvent.BUTTON_FORWARD) != 0) {
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_X2);
}
else {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_X2);
}
}
// Get relative axis values if we can
if (inputCaptureProvider.eventHasRelativeMouseAxes(event)) {
// Send the deltas straight from the motion event
@@ -780,13 +1057,14 @@ public class Game extends Activity implements SurfaceHolder.Callback,
lastMouseX = (int)event.getX();
lastMouseY = (int)event.getY();
}
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// We get a normal (non-relative) MotionEvent when starting pointer capture to synchronize the
// location of the cursor with our app. We don't want this, so we must discard this event.
lastMouseX = (int)event.getX();
lastMouseY = (int)event.getY();
}
else {
// 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
// Don't process the history. We just want the current position now.
updateMousePosition((int)event.getX(), (int)event.getY());
}
@@ -934,10 +1212,15 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
@Override
public void stageStarting(String stage) {
if (spinner != null) {
spinner.setMessage(getResources().getString(R.string.conn_starting)+" "+stage);
}
public void stageStarting(final String stage) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (spinner != null) {
spinner.setMessage(getResources().getString(R.string.conn_starting) + " " + stage);
}
}
});
}
@Override
@@ -947,75 +1230,93 @@ public class Game extends Activity implements SurfaceHolder.Callback,
private void stopConnection() {
if (connecting || connected) {
connecting = connected = false;
conn.stop();
// Stop may take a few hundred ms to do some network I/O to tell
// the server we're going away and clean up. Let it run in a separate
// thread to keep things smooth for the UI. Inside moonlight-common,
// we prevent another thread from starting a connection before and
// during the process of stopping this one.
new Thread() {
public void run() {
conn.stop();
}
}.start();
}
}
@Override
public void stageFailed(String stage, long errorCode) {
if (spinner != null) {
spinner.dismiss();
spinner = null;
}
public void stageFailed(final String stage, final long errorCode) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (spinner != null) {
spinner.dismiss();
spinner = null;
}
// Enable cursor visibility again
inputCaptureProvider.disableCapture();
if (!displayedFailureDialog) {
displayedFailureDialog = true;
LimeLog.severe(stage + " failed: " + errorCode);
if (!displayedFailureDialog) {
displayedFailureDialog = true;
LimeLog.severe(stage+" failed: "+errorCode);
// If video initialization failed and the surface is still valid, display extra information for the user
if (stage.contains("video") && streamView.getHolder().getSurface().isValid()) {
runOnUiThread(new Runnable() {
@Override
public void run() {
// If video initialization failed and the surface is still valid, display extra information for the user
if (stage.contains("video") && streamView.getHolder().getSurface().isValid()) {
Toast.makeText(Game.this, "Video decoder failed to initialize. Your device may not support the selected resolution.", Toast.LENGTH_LONG).show();
}
});
}
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title),
getResources().getString(R.string.conn_error_msg)+" "+stage, true);
}
Dialog.displayDialog(Game.this, getResources().getString(R.string.conn_error_title),
getResources().getString(R.string.conn_error_msg) + " " + stage, true);
}
}
});
}
@Override
public void connectionTerminated(long errorCode) {
// Enable cursor visibility again
inputCaptureProvider.disableCapture();
public void connectionTerminated(final long errorCode) {
runOnUiThread(new Runnable() {
@Override
public void run() {
// Let the display go to sleep now
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
if (!displayedFailureDialog) {
displayedFailureDialog = true;
LimeLog.severe("Connection terminated: "+errorCode);
stopConnection();
// Enable cursor visibility again
inputCaptureProvider.disableCapture();
Dialog.displayDialog(this, getResources().getString(R.string.conn_terminated_title),
getResources().getString(R.string.conn_terminated_msg), true);
}
if (!displayedFailureDialog) {
displayedFailureDialog = true;
LimeLog.severe("Connection terminated: " + errorCode);
stopConnection();
Dialog.displayDialog(Game.this, getResources().getString(R.string.conn_terminated_title),
getResources().getString(R.string.conn_terminated_msg), true);
}
}
});
}
@Override
public void connectionStarted() {
if (spinner != null) {
spinner.dismiss();
spinner = null;
}
connecting = false;
connected = true;
runOnUiThread(new Runnable() {
@Override
public void run() {
if (spinner != null) {
spinner.dismiss();
spinner = null;
}
connected = true;
connecting = false;
// Hide the mouse cursor now. Doing it before
// dismissing the spinner seems to be undone
// when the spinner gets displayed.
inputCaptureProvider.enableCapture();
// Keep the display on
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
hideSystemUi(1000);
}
});
hideSystemUi(1000);
}
@Override
@@ -1042,12 +1343,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
if (!surfaceCreated) {
throw new IllegalStateException("Surface changed before creation!");
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
if (!connected && !connecting) {
connecting = true;
if (!attemptedConnection) {
attemptedConnection = true;
decoderRenderer.setRenderTarget(holder);
conn.start(PlatformBinding.getAudioRenderer(), decoderRenderer, Game.this);
@@ -1055,12 +1356,23 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
// Let the decoder know immediately that the surface is gone
decoderRenderer.prepareForStop();
public void surfaceCreated(SurfaceHolder holder) {
surfaceCreated = true;
}
if (connected) {
stopConnection();
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (!surfaceCreated) {
throw new IllegalStateException("Surface destroyed before creation!");
}
if (attemptedConnection) {
// Let the decoder know immediately that the surface is gone
decoderRenderer.prepareForStop();
if (connected) {
stopConnection();
}
}
}
@@ -1084,6 +1396,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
case EvdevListener.BUTTON_RIGHT:
buttonIndex = MouseButtonPacket.BUTTON_RIGHT;
break;
case EvdevListener.BUTTON_X1:
buttonIndex = MouseButtonPacket.BUTTON_X1;
break;
case EvdevListener.BUTTON_X2:
buttonIndex = MouseButtonPacket.BUTTON_X2;
break;
default:
LimeLog.warning("Unhandled button: "+buttonId);
return;
@@ -1106,7 +1424,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
public void keyboardEvent(boolean buttonDown, short keyCode) {
short keyMap = KeyboardTranslator.translate(keyCode);
if (keyMap != 0) {
if (handleSpecialKeys(keyMap, buttonDown)) {
// handleSpecialKeys() takes the Android keycode
if (handleSpecialKeys(keyCode, buttonDown)) {
return;
}
+79 -33
View File
@@ -16,6 +16,7 @@ import com.limelight.nvstream.http.PairingManager;
import com.limelight.nvstream.http.PairingManager.PairState;
import com.limelight.nvstream.wol.WakeOnLanSender;
import com.limelight.preferences.AddComputerManually;
import com.limelight.preferences.GlPreferences;
import com.limelight.preferences.PreferenceConfiguration;
import com.limelight.preferences.StreamSettings;
import com.limelight.ui.AdapterFragment;
@@ -27,12 +28,14 @@ import com.limelight.utils.ShortcutHelper;
import com.limelight.utils.UiHelper;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.opengl.GLSurfaceView;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.preference.PreferenceManager;
@@ -50,12 +53,15 @@ import android.widget.RelativeLayout;
import android.widget.Toast;
import android.widget.AdapterView.AdapterContextMenuInfo;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
public class PcView extends Activity implements AdapterFragmentCallbacks {
private RelativeLayout noPcFoundLayout;
private PcGridAdapter pcGridAdapter;
private ShortcutHelper shortcutHelper;
private ComputerManagerService.ComputerManagerBinder managerBinder;
private boolean freezeUpdates, runningPolling, inForeground;
private boolean freezeUpdates, runningPolling, inForeground, completeOnCreateCalled;
private final ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder binder) {
final ComputerManagerService.ComputerManagerBinder localBinder =
@@ -85,11 +91,19 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
}
};
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// Reinitialize views just in case orientation changed
initializeViews();
// Only reinitialize views if completeOnCreate() was called
// before this callback. If it was not, completeOnCreate() will
// handle initializing views with the config change accounted for.
// This is not prone to races because both callbacks are invoked
// in the main thread.
if (completeOnCreateCalled) {
// Reinitialize views just in case orientation changed
initializeViews();
}
}
private final static int APP_LIST_ID = 1;
@@ -99,6 +113,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
private final static int DELETE_ID = 5;
private final static int RESUME_ID = 6;
private final static int QUIT_ID = 7;
private final static int VIEW_DETAILS_ID = 8;
private void initializeViews() {
setContentView(R.layout.activity_pc_view);
@@ -151,6 +166,52 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Assume we're in the foreground when created to avoid a race
// between binding to CMS and onResume()
inForeground = true;
// Create a GLSurfaceView to fetch GLRenderer unless we have
// a cached result already.
final GlPreferences glPrefs = GlPreferences.readPreferences(this);
if (!glPrefs.savedFingerprint.equals(Build.FINGERPRINT) || glPrefs.glRenderer.isEmpty()) {
GLSurfaceView surfaceView = new GLSurfaceView(this);
surfaceView.setRenderer(new GLSurfaceView.Renderer() {
@Override
public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
// Save the GLRenderer string so we don't need to do this next time
glPrefs.glRenderer = gl10.glGetString(GL10.GL_RENDERER);
glPrefs.savedFingerprint = Build.FINGERPRINT;
glPrefs.writePreferences();
LimeLog.info("Fetched GL Renderer: " + glPrefs.glRenderer);
runOnUiThread(new Runnable() {
@Override
public void run() {
completeOnCreate();
}
});
}
@Override
public void onSurfaceChanged(GL10 gl10, int i, int i1) {
}
@Override
public void onDrawFrame(GL10 gl10) {
}
});
setContentView(surfaceView);
}
else {
LimeLog.info("Cached GL Renderer: " + glPrefs.glRenderer);
completeOnCreate();
}
}
private void completeOnCreate() {
completeOnCreateCalled = true;
shortcutHelper = new ShortcutHelper(this);
UiHelper.setLocale(this);
@@ -164,32 +225,6 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
PreferenceConfiguration.readPreferences(this).smallIconMode);
initializeViews();
SharedPreferences prefs = getSharedPreferences("DecoderTombstone", 0);
int crashCount = prefs.getInt("CrashCount", 0);
int lastNotifiedCrashCount = prefs.getInt("LastNotifiedCrashCount", 0);
// Remember the last crash count we notified at, so we don't
// display the crash dialog every time the app is started until
// they stream again
if (crashCount != 0 && crashCount != lastNotifiedCrashCount) {
if (crashCount % 3 == 0) {
// At 3 consecutive crashes, we'll forcefully reset their settings
PreferenceConfiguration.resetStreamingSettings(this);
Dialog.displayDialog(this,
getResources().getString(R.string.title_decoding_reset),
getResources().getString(R.string.message_decoding_reset),
false);
}
else {
Dialog.displayDialog(this,
getResources().getString(R.string.title_decoding_error),
getResources().getString(R.string.message_decoding_error),
false);
}
prefs.edit().putInt("LastNotifiedCrashCount", crashCount).apply();
}
}
private void startComputerUpdates() {
@@ -245,6 +280,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
protected void onResume() {
super.onResume();
// Display a decoder crash notification if we've returned after a crash
UiHelper.showDecoderCrashDialog(this);
inForeground = true;
startComputerUpdates();
}
@@ -296,6 +334,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
// it with delete which actually work
menu.add(Menu.NONE, DELETE_ID, 4, getResources().getString(R.string.pcview_menu_delete_pc));
}
menu.add(Menu.NONE, VIEW_DETAILS_ID, 5, getResources().getString(R.string.pcview_menu_details));
}
@Override
@@ -416,7 +455,6 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
return;
}
Toast.makeText(PcView.this, getResources().getString(R.string.wol_waking_pc), Toast.LENGTH_SHORT).show();
new Thread(new Runnable() {
@Override
public void run() {
@@ -525,6 +563,10 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
return true;
case DELETE_ID:
if (ActivityManager.isUserAMonkey()) {
LimeLog.info("Ignoring delete PC request from monkey");
return true;
}
if (managerBinder == null) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
return true;
@@ -543,7 +585,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
return true;
}
ServerHelper.doStart(this, new NvApp("app", computer.details.runningGameId), computer.details, managerBinder);
ServerHelper.doStart(this, new NvApp("app", computer.details.runningGameId, false), computer.details, managerBinder);
return true;
case QUIT_ID:
@@ -558,11 +600,15 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
public void run() {
ServerHelper.doQuit(PcView.this,
ServerHelper.getCurrentAddressFromComputer(computer.details),
new NvApp("app", 0), managerBinder, null);
new NvApp("app", 0, false), managerBinder, null);
}
}, null);
return true;
case VIEW_DETAILS_ID:
Dialog.displayDialog(PcView.this, getResources().getString(R.string.title_details), computer.details.toString(), false);
return true;
default:
return super.onContextItemSelected(item);
}
@@ -0,0 +1,267 @@
package com.limelight;
import android.app.Activity;
import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import com.limelight.computers.ComputerManagerListener;
import com.limelight.computers.ComputerManagerService;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.PairingManager;
import com.limelight.utils.Dialog;
import com.limelight.utils.ServerHelper;
import com.limelight.utils.SpinnerDialog;
import com.limelight.utils.UiHelper;
import java.util.ArrayList;
import java.util.UUID;
public class ShortcutTrampoline extends Activity {
private String uuidString;
private String appIdString;
private ArrayList<Intent> intentStack = new ArrayList<>();
private ComputerDetails computer;
private SpinnerDialog blockingLoadSpinner;
public final static String APP_ID_EXTRA = "AppId";
private ComputerManagerService.ComputerManagerBinder managerBinder;
private final ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder binder) {
final ComputerManagerService.ComputerManagerBinder localBinder =
((ComputerManagerService.ComputerManagerBinder)binder);
// Wait in a separate thread to avoid stalling the UI
new Thread() {
@Override
public void run() {
// Wait for the binder to be ready
localBinder.waitForReady();
// Now make the binder visible
managerBinder = localBinder;
// Get the computer object
computer = managerBinder.getComputer(UUID.fromString(uuidString));
if (computer == null) {
Dialog.displayDialog(ShortcutTrampoline.this,
getResources().getString(R.string.conn_error_title),
getResources().getString(R.string.scut_pc_not_found),
true);
if (blockingLoadSpinner != null) {
blockingLoadSpinner.dismiss();
blockingLoadSpinner = null;
}
if (managerBinder != null) {
unbindService(serviceConnection);
managerBinder = null;
}
return;
}
// Force CMS to repoll this machine
managerBinder.invalidateStateForComputer(computer.uuid);
// Start polling
managerBinder.startPolling(new ComputerManagerListener() {
@Override
public void notifyComputerUpdated(final ComputerDetails details) {
// Don't care about other computers
if (!details.uuid.toString().equalsIgnoreCase(uuidString)) {
return;
}
if (details.state != ComputerDetails.State.UNKNOWN) {
runOnUiThread(new Runnable() {
@Override
public void run() {
// Stop showing the spinner
if (blockingLoadSpinner != null) {
blockingLoadSpinner.dismiss();
blockingLoadSpinner = null;
}
// If the managerBinder was destroyed before this callback,
// just finish the activity.
if (managerBinder == null) {
finish();
return;
}
if (details.state == ComputerDetails.State.ONLINE && details.pairState == PairingManager.PairState.PAIRED) {
// Launch game if provided app ID, otherwise launch app view
if (appIdString != null && appIdString.length() > 0) {
if (details.runningGameId == 0 || details.runningGameId == Integer.parseInt(appIdString)) {
intentStack.add(ServerHelper.createStartIntent(ShortcutTrampoline.this,
new NvApp("app", Integer.parseInt(appIdString), false), details, managerBinder));
// Close this activity
finish();
// Now start the activities
startActivities(intentStack.toArray(new Intent[]{}));
} else {
// Create the start intent immediately, so we can safely unbind the managerBinder
// below before we return.
final Intent startIntent = ServerHelper.createStartIntent(ShortcutTrampoline.this,
new NvApp("app", Integer.parseInt(appIdString), false), details, managerBinder);
UiHelper.displayQuitConfirmationDialog(ShortcutTrampoline.this, new Runnable() {
@Override
public void run() {
intentStack.add(startIntent);
// Close this activity
finish();
// Now start the activities
startActivities(intentStack.toArray(new Intent[]{}));
}
}, new Runnable() {
@Override
public void run() {
// Close this activity
finish();
}
});
}
} else {
// Close this activity
finish();
// Add the PC view at the back (and clear the task)
Intent i;
i = new Intent(ShortcutTrampoline.this, PcView.class);
i.setAction(Intent.ACTION_MAIN);
i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
intentStack.add(i);
// Take this intent's data and create an intent to start the app view
i = new Intent(getIntent());
i.setClass(ShortcutTrampoline.this, AppView.class);
intentStack.add(i);
// If a game is running, we'll make the stream the top level activity
if (details.runningGameId != 0) {
intentStack.add(ServerHelper.createStartIntent(ShortcutTrampoline.this,
new NvApp("app", details.runningGameId, false), details, managerBinder));
}
// Now start the activities
startActivities(intentStack.toArray(new Intent[]{}));
}
}
else if (details.state == ComputerDetails.State.OFFLINE) {
// Computer offline - display an error dialog
Dialog.displayDialog(ShortcutTrampoline.this,
getResources().getString(R.string.conn_error_title),
getResources().getString(R.string.error_pc_offline),
true);
} else if (details.pairState != PairingManager.PairState.PAIRED) {
// Computer not paired - display an error dialog
Dialog.displayDialog(ShortcutTrampoline.this,
getResources().getString(R.string.conn_error_title),
getResources().getString(R.string.scut_not_paired),
true);
}
// We don't want any more callbacks from now on, so go ahead
// and unbind from the service
if (managerBinder != null) {
managerBinder.stopPolling();
unbindService(serviceConnection);
managerBinder = null;
}
}
});
}
}
});
}
}.start();
}
public void onServiceDisconnected(ComponentName className) {
managerBinder = null;
}
};
protected boolean validateInput() {
// Validate UUID
try {
UUID.fromString(uuidString);
} catch (IllegalArgumentException ex) {
Dialog.displayDialog(ShortcutTrampoline.this,
getResources().getString(R.string.conn_error_title),
getResources().getString(R.string.scut_invalid_uuid),
true);
return false;
}
// Validate App ID (if provided)
if (appIdString != null && !appIdString.isEmpty()) {
try {
Integer.parseInt(appIdString);
} catch (NumberFormatException ex) {
Dialog.displayDialog(ShortcutTrampoline.this,
getResources().getString(R.string.conn_error_title),
getResources().getString(R.string.scut_invalid_app_id),
true);
return false;
}
}
return true;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
UiHelper.notifyNewRootView(this);
uuidString = getIntent().getStringExtra(AppView.UUID_EXTRA);
appIdString = getIntent().getStringExtra(APP_ID_EXTRA);
if (validateInput()) {
// Bind to the computer manager service
bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
Service.BIND_AUTO_CREATE);
blockingLoadSpinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.conn_establishing_title),
getResources().getString(R.string.applist_connect_msg), true);
}
}
@Override
protected void onStop() {
super.onStop();
if (blockingLoadSpinner != null) {
blockingLoadSpinner.dismiss();
blockingLoadSpinner = null;
}
Dialog.closeDialogs();
if (managerBinder != null) {
managerBinder.stopPolling();
unbindService(serviceConnection);
managerBinder = null;
}
finish();
}
}
@@ -176,14 +176,14 @@ public class AndroidAudioRenderer implements AudioRenderer {
public void start() {}
@Override
public void stop() {
// Immediately drop all pending data
track.pause();
track.flush();
}
public void stop() {}
@Override
public void cleanup() {
// Immediately drop all pending data
track.pause();
track.flush();
track.release();
}
}
@@ -13,8 +13,8 @@ import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Provider;
import java.security.SecureRandom;
import java.security.Security;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
@@ -54,10 +54,7 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
private static final Object globalCryptoLock = new Object();
static {
// Install the Bouncy Castle provider
Security.addProvider(new BouncyCastleProvider());
}
private static final Provider bcProvider = new BouncyCastleProvider();
public AndroidCryptoProvider(Context c) {
String dataPath = c.getFilesDir().getAbsolutePath();
@@ -96,10 +93,10 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
}
try {
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", "BC");
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", bcProvider);
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certBytes));
pemCertBytes = certBytes;
KeyFactory keyFactory = KeyFactory.getInstance("RSA", "BC");
KeyFactory keyFactory = KeyFactory.getInstance("RSA", bcProvider);
key = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
} catch (CertificateException e) {
// May happen if the cert is corrupt
@@ -113,10 +110,6 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
// May happen if the key is corrupt
LimeLog.warning("Corrupted key");
return false;
} catch (NoSuchProviderException e) {
// Should never happen
e.printStackTrace();
return false;
}
return true;
@@ -129,17 +122,13 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", bcProvider);
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
} catch (NoSuchAlgorithmException e1) {
// Should never happen
e1.printStackTrace();
return false;
} catch (NoSuchProviderException e) {
// Should never happen
e.printStackTrace();
return false;
}
Date now = new Date();
@@ -160,8 +149,8 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()));
try {
ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate());
cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen));
ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider(bcProvider).build(keyPair.getPrivate());
cert = new JcaX509CertificateConverter().setProvider(bcProvider).getCertificate(certBuilder.build(sigGen));
key = (RSAPrivateKey) keyPair.getPrivate();
} catch (Exception e) {
// Nothing should go wrong here
@@ -2,6 +2,9 @@ package com.limelight.binding.input;
import android.content.Context;
import android.hardware.input.InputManager;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbManager;
import android.os.Build;
import android.os.SystemClock;
import android.util.SparseArray;
import android.view.InputDevice;
@@ -12,12 +15,15 @@ import android.widget.Toast;
import com.limelight.LimeLog;
import com.limelight.binding.input.driver.UsbDriverListener;
import com.limelight.binding.input.driver.UsbDriverService;
import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.input.ControllerPacket;
import com.limelight.nvstream.input.MouseButtonPacket;
import com.limelight.preferences.PreferenceConfiguration;
import com.limelight.ui.GameGestures;
import com.limelight.utils.Vector2d;
import java.lang.reflect.InvocationTargetException;
import java.util.Timer;
import java.util.TimerTask;
@@ -47,18 +53,18 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
private final GameGestures gestures;
private boolean hasGameController;
private final boolean multiControllerEnabled;
private short currentControllers;
private final PreferenceConfiguration prefConfig;
private short currentControllers, initialControllers;
public ControllerHandler(Context activityContext, NvConnection conn, GameGestures gestures, boolean multiControllerEnabled, int deadzonePercentage) {
public ControllerHandler(Context activityContext, NvConnection conn, GameGestures gestures, PreferenceConfiguration prefConfig) {
this.activityContext = activityContext;
this.conn = conn;
this.gestures = gestures;
this.multiControllerEnabled = multiControllerEnabled;
this.prefConfig = prefConfig;
// HACK: For now we're hardcoding a 10% deadzone. Some deadzone
// is required for controller batching support to work.
deadzonePercentage = 10;
int deadzonePercentage = 10;
int[] ids = InputDevice.getDeviceIds();
for (int id : ids) {
@@ -96,6 +102,18 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
defaultContext.rightTriggerAxis = MotionEvent.AXIS_GAS;
defaultContext.controllerNumber = (short) 0;
defaultContext.assignedControllerNumber = true;
// Some devices (GPD XD) have a back button which sends input events
// with device ID == 0. This hits the default context which would normally
// consume these. Instead, let's ignore them since that's probably the
// most likely case.
defaultContext.ignoreBack = true;
// Get the initially attached set of gamepads. As each gamepad receives
// its initial InputEvent, we will move these from this set onto the
// currentControllers set which will allow them to properly unplug
// if they are removed.
initialControllers = getAttachedControllerMask(activityContext);
}
private static InputDevice.MotionRange getMotionRangeForJoystickAxis(InputDevice dev, int axis) {
@@ -133,8 +151,75 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
onInputDeviceAdded(deviceId);
}
private static boolean hasJoystickAxes(InputDevice device) {
return (device.getSources() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK &&
getMotionRangeForJoystickAxis(device, MotionEvent.AXIS_X) != null &&
getMotionRangeForJoystickAxis(device, MotionEvent.AXIS_Y) != null;
}
private static boolean hasGamepadButtons(InputDevice device) {
return (device.getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD;
}
public static boolean isGameControllerDevice(InputDevice device) {
if (device == null) {
return true;
}
if (hasJoystickAxes(device) || hasGamepadButtons(device)) {
// Has real joystick axes or gamepad buttons
return true;
}
// Otherwise, we'll try anything that claims to be a non-alphabetic keyboard
return device.getKeyboardType() != InputDevice.KEYBOARD_TYPE_ALPHABETIC;
}
public static short getAttachedControllerMask(Context context) {
int count = 0;
short mask = 0;
// Count all input devices that are gamepads
InputManager im = (InputManager) context.getSystemService(Context.INPUT_SERVICE);
for (int id : im.getInputDeviceIds()) {
InputDevice dev = im.getInputDevice(id);
if (dev == null) {
continue;
}
if (hasJoystickAxes(dev)) {
LimeLog.info("Counting InputDevice: "+dev.getName());
mask |= 1 << count++;
}
}
// Count all USB devices that match our drivers
if (PreferenceConfiguration.readPreferences(context).usbDriver) {
UsbManager usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
for (UsbDevice dev : usbManager.getDeviceList().values()) {
// We explicitly ask not to claim devices that appear as InputDevices
// otherwise we will double count them.
if (UsbDriverService.shouldClaimDevice(dev, false)) {
LimeLog.info("Counting UsbDevice: "+dev.getDeviceName());
mask |= 1 << count++;
}
}
}
LimeLog.info("Enumerated "+count+" gamepads");
return mask;
}
private void releaseControllerNumber(GenericControllerContext context) {
// If this device sent data as a gamepad, zero the values before removing
// 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);
}
// If this device sent data as a gamepad, zero the values before removing.
// We must do this after clearing the currentControllers entry so this
// causes the device to be removed on the server PC.
if (context.assignedControllerNumber) {
conn.sendControllerInput(context.controllerNumber, getActiveControllerMask(),
(short) 0,
@@ -142,12 +227,6 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
(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);
}
}
// Called before sending input but after we've determined that this
@@ -167,7 +246,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
LimeLog.info("Built-in buttons hardcoded as controller 0");
context.controllerNumber = 0;
}
else if (multiControllerEnabled && devContext.hasJoystickAxes) {
else if (prefConfig.multiController && devContext.hasJoystickAxes) {
context.controllerNumber = 0;
LimeLog.info("Reserving the next available controller number");
@@ -175,6 +254,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
if ((currentControllers & (1 << i)) == 0) {
// Found an unused controller value
currentControllers |= (1 << i);
// Take this value out of the initial gamepad set
initialControllers &= ~(1 << i);
context.controllerNumber = i;
context.reservedControllerNumber = true;
break;
@@ -187,7 +270,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
}
}
else {
if (multiControllerEnabled) {
if (prefConfig.multiController) {
context.controllerNumber = 0;
LimeLog.info("Reserving the next available controller number");
@@ -195,6 +278,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
if ((currentControllers & (1 << i)) == 0) {
// Found an unused controller value
currentControllers |= (1 << i);
// Take this value out of the initial gamepad set
initialControllers &= ~(1 << i);
context.controllerNumber = i;
context.reservedControllerNumber = true;
break;
@@ -223,6 +310,79 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
return context;
}
private static boolean isExternal(InputDevice dev) {
try {
// Landroid/view/InputDevice;->isExternal()Z is on the light graylist in Android P
return (Boolean)dev.getClass().getMethod("isExternal").invoke(dev);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (ClassCastException e) {
e.printStackTrace();
}
// Answer true if we don't know
return true;
}
private boolean shouldIgnoreBack(InputDevice dev) {
String devName = dev.getName();
// The Serval has a Select button but the framework doesn't
// know about that because it uses a non-standard scancode.
if (devName.contains("Razer Serval")) {
return true;
}
// Classify this device as a remote by name if it has no joystick axes
if (getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_X) == null &&
getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_Y) == null &&
devName.toLowerCase().contains("remote")) {
return true;
}
// Otherwise, dynamically try to determine whether we should allow this
// back button to function for navigation.
//
// First, check if this is an internal device we're being called on.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && !isExternal(dev)) {
InputManager im = (InputManager) activityContext.getSystemService(Context.INPUT_SERVICE);
boolean foundInternalGamepad = false;
boolean foundInternalSelect = false;
for (int id : im.getInputDeviceIds()) {
InputDevice currentDev = im.getInputDevice(id);
// Ignore external devices
if (currentDev == null || isExternal(currentDev)) {
continue;
}
// Note that we are explicitly NOT excluding the current device we're examining here,
// since the other gamepad buttons may be on our current device and that's fine.
boolean[] keys = currentDev.hasKeys(KeyEvent.KEYCODE_BUTTON_SELECT, KeyEvent.KEYCODE_BUTTON_A);
if (keys[0]) {
foundInternalSelect = true;
}
if (keys[1]) {
foundInternalGamepad = true;
}
}
// Allow the back button to function for navigation if we either:
// a) have no internal gamepad (most phones)
// b) have an internal gamepad but also have an internal select button (GPD XD)
// but not:
// c) have an internal gamepad but no internal select button (NVIDIA SHIELD Portable)
return !foundInternalGamepad || foundInternalSelect;
}
return false;
}
private InputDeviceContext createInputDeviceContextForDevice(InputDevice dev) {
InputDeviceContext context = new InputDeviceContext();
String devName = dev.getName();
@@ -355,6 +515,8 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
}
}
context.ignoreBack = shouldIgnoreBack(dev);
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
@@ -374,28 +536,15 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
// so we increase the deadzone on them to minimize this
context.triggerDeadzone = 0.30f;
}
// Classify this device as a remote by name
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.ignoreBack = true;
}
}
// 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") || devName.equals("sec_touchkey")) {
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;
}
// The Xbox One S Bluetooth controller has some mappings that need fixing up.
// However, Microsoft released a firmware update with no change to VID/PID
@@ -464,8 +613,8 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
}
private short getActiveControllerMask() {
if (multiControllerEnabled) {
return currentControllers;
if (prefConfig.multiController) {
return (short)(currentControllers | initialControllers);
}
else {
// Only Player 1 is active with multi-controller disabled
@@ -551,6 +700,16 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
}
}
if ((changedMask & ControllerPacket.UP_FLAG) != 0) {
if ((inputMap & ControllerPacket.UP_FLAG) != 0) {
conn.sendMouseScroll((byte) 1);
}
}
if ((changedMask & ControllerPacket.DOWN_FLAG) != 0) {
if ((inputMap & ControllerPacket.DOWN_FLAG) != 0) {
conn.sendMouseScroll((byte) -1);
}
}
conn.sendControllerInput(controllerNumber, getActiveControllerMask(),
(short)0, (byte)0, (byte)0, (short)0, (short)0, (short)0, (short)0);
@@ -762,16 +921,16 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
// to normal behavior, so ignore triggersIdleNegative for each trigger until
// first touch.
if (lt != 0) {
context.leftTriggerUsed = true;
context.leftTriggerAxisUsed = true;
}
if (rt != 0) {
context.rightTriggerUsed = true;
context.rightTriggerAxisUsed = true;
}
if (context.triggersIdleNegative) {
if (context.leftTriggerUsed) {
if (context.leftTriggerAxisUsed) {
lt = (lt + 1) / 2;
}
if (context.rightTriggerUsed) {
if (context.rightTriggerAxisUsed) {
rt = (rt + 1) / 2;
}
}
@@ -917,7 +1076,8 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
// Make sure it's real by checking that the key is actually down before taking
// any action.
if ((context.inputMap & ControllerPacket.PLAY_FLAG) != 0 &&
SystemClock.uptimeMillis() - context.startDownTime > ControllerHandler.START_DOWN_TIME_MOUSE_MODE_MS) {
SystemClock.uptimeMillis() - context.startDownTime > ControllerHandler.START_DOWN_TIME_MOUSE_MODE_MS &&
prefConfig.mouseEmulation) {
toggleMouseEmulation(context);
}
context.inputMap &= ~ControllerPacket.PLAY_FLAG;
@@ -966,9 +1126,17 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
context.inputMap &= ~ControllerPacket.RS_CLK_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_L2:
if (context.leftTriggerAxisUsed) {
// Suppress this digital event if an analog trigger is active
return true;
}
context.leftTrigger = 0;
break;
case KeyEvent.KEYCODE_BUTTON_R2:
if (context.rightTriggerAxisUsed) {
// Suppress this digital event if an analog trigger is active
return true;
}
context.rightTrigger = 0;
break;
default:
@@ -1078,9 +1246,17 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
context.inputMap |= ControllerPacket.RS_CLK_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_L2:
if (context.leftTriggerAxisUsed) {
// Suppress this digital event if an analog trigger is active
return true;
}
context.leftTrigger = (byte)0xFF;
break;
case KeyEvent.KEYCODE_BUTTON_R2:
if (context.rightTriggerAxisUsed) {
// Suppress this digital event if an analog trigger is active
return true;
}
context.rightTrigger = (byte)0xFF;
break;
default:
@@ -1209,7 +1385,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
public int leftTriggerAxis = -1;
public int rightTriggerAxis = -1;
public boolean triggersIdleNegative;
public boolean leftTriggerUsed, rightTriggerUsed;
public boolean leftTriggerAxisUsed, rightTriggerAxisUsed;
public int hatXAxis = -1;
public int hatYAxis = -1;
@@ -18,22 +18,15 @@ public class KeyboardTranslator {
public static final int VK_9 = 57;
public static final int VK_A = 65;
public static final int VK_Z = 90;
public static final int VK_ALT = 18;
public static final int VK_NUMPAD0 = 96;
public static final int VK_BACK_SLASH = 92;
public static final int VK_CAPS_LOCK = 20;
public static final int VK_CLEAR = 12;
public static final int VK_COMMA = 44;
public static final int VK_CONTROL = 17;
public static final int VK_BACK_SPACE = 8;
public static final int VK_EQUALS = 61;
public static final int VK_ESCAPE = 27;
public static final int VK_F1 = 112;
public static final int VK_PERIOD = 46;
public static final int VK_INSERT = 155;
public static final int VK_OPEN_BRACKET = 91;
public static final int VK_WINDOWS = 524;
public static final int VK_MINUS = 45;
public static final int VK_END = 35;
public static final int VK_HOME = 36;
public static final int VK_NUM_LOCK = 144;
@@ -43,7 +36,6 @@ public class KeyboardTranslator {
public static final int VK_CLOSE_BRACKET = 93;
public static final int VK_SCROLL_LOCK = 145;
public static final int VK_SEMICOLON = 59;
public static final int VK_SHIFT = 16;
public static final int VK_SLASH = 47;
public static final int VK_SPACE = 32;
public static final int VK_PRINTSCREEN = 154;
@@ -64,10 +56,9 @@ public class KeyboardTranslator {
public static short translate(int keycode) {
int translated;
/* There seems to be no clean mapping between Android key codes
* and what Nvidia sends over the wire. If someone finds one,
* I'll happily delete this code :)
*/
// This is a poor man's mapping between Android key codes
// and Windows VK_* codes. For all defined VK_ codes, see:
// https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx
if (keycode >= KeyEvent.KEYCODE_0 &&
keycode <= KeyEvent.KEYCODE_9) {
translated = (keycode - KeyEvent.KEYCODE_0) + VK_0;
@@ -87,8 +78,11 @@ public class KeyboardTranslator {
else {
switch (keycode) {
case KeyEvent.KEYCODE_ALT_LEFT:
translated = 0xA4;
break;
case KeyEvent.KEYCODE_ALT_RIGHT:
translated = VK_ALT;
translated = 0xA5;
break;
case KeyEvent.KEYCODE_BACKSLASH:
@@ -108,8 +102,11 @@ public class KeyboardTranslator {
break;
case KeyEvent.KEYCODE_CTRL_LEFT:
translated = 0xA2;
break;
case KeyEvent.KEYCODE_CTRL_RIGHT:
translated = VK_CONTROL;
translated = 0xA3;
break;
case KeyEvent.KEYCODE_DEL:
@@ -129,23 +126,25 @@ public class KeyboardTranslator {
break;
case KeyEvent.KEYCODE_FORWARD_DEL:
// Nvidia maps period to delete
translated = VK_PERIOD;
translated = 0x2e;
break;
case KeyEvent.KEYCODE_INSERT:
translated = -1;
translated = 0x2d;
break;
case KeyEvent.KEYCODE_LEFT_BRACKET:
translated = 0xdb;
break;
case KeyEvent.KEYCODE_META_LEFT:
case KeyEvent.KEYCODE_META_RIGHT:
translated = VK_WINDOWS;
translated = 0x5b;
break;
case KeyEvent.KEYCODE_META_RIGHT:
translated = 0x5c;
break;
case KeyEvent.KEYCODE_MINUS:
translated = 0xbd;
break;
@@ -187,8 +186,11 @@ public class KeyboardTranslator {
break;
case KeyEvent.KEYCODE_SHIFT_LEFT:
translated = 0xA0;
break;
case KeyEvent.KEYCODE_SHIFT_RIGHT:
translated = VK_SHIFT;
translated = 0xA1;
break;
case KeyEvent.KEYCODE_SLASH:
@@ -235,6 +237,26 @@ public class KeyboardTranslator {
case KeyEvent.KEYCODE_BREAK:
translated = VK_PAUSE;
break;
case KeyEvent.KEYCODE_NUMPAD_DIVIDE:
translated = 0x6F;
break;
case KeyEvent.KEYCODE_NUMPAD_MULTIPLY:
translated = 0x6A;
break;
case KeyEvent.KEYCODE_NUMPAD_SUBTRACT:
translated = 0x6D;
break;
case KeyEvent.KEYCODE_NUMPAD_ADD:
translated = 0x6B;
break;
case KeyEvent.KEYCODE_NUMPAD_DOT:
translated = 0x6E;
break;
default:
System.out.println("No key for "+keycode);
@@ -43,11 +43,19 @@ public class AndroidNativePointerCaptureProvider extends InputCaptureProvider {
@Override
public float getRelativeAxisX(MotionEvent event) {
return event.getX();
float x = event.getX();
for (int i = 0; i < event.getHistorySize(); i++) {
x += event.getHistoricalX(i);
}
return x;
}
@Override
public float getRelativeAxisY(MotionEvent event) {
return event.getY();
float y = event.getY();
for (int i = 0; i < event.getHistorySize(); i++) {
y += event.getHistoricalY(i);
}
return y;
}
}
@@ -3,8 +3,9 @@ package com.limelight.binding.input.capture;
import android.app.Activity;
import com.limelight.LimeLog;
import com.limelight.LimelightBuildProps;
import com.limelight.R;
import com.limelight.binding.input.evdev.EvdevCaptureProvider;
import com.limelight.binding.input.evdev.EvdevCaptureProviderShim;
import com.limelight.binding.input.evdev.EvdevListener;
public class InputCaptureManager {
@@ -13,13 +14,15 @@ public class InputCaptureManager {
LimeLog.info("Using Android O+ native mouse capture");
return new AndroidNativePointerCaptureProvider(activity.findViewById(R.id.surfaceView));
}
else if (ShieldCaptureProvider.isCaptureProviderSupported()) {
// LineageOS implemented broken NVIDIA capture extensions, so avoid using them on root builds.
// See https://github.com/LineageOS/android_frameworks_base/commit/d304f478a023430f4712dbdc3ee69d9ad02cebd3
else if (!LimelightBuildProps.ROOT_BUILD && ShieldCaptureProvider.isCaptureProviderSupported()) {
LimeLog.info("Using NVIDIA mouse capture extension");
return new ShieldCaptureProvider(activity);
}
else if (EvdevCaptureProvider.isCaptureProviderSupported()) {
else if (EvdevCaptureProviderShim.isCaptureProviderSupported()) {
LimeLog.info("Using Evdev mouse capture");
return new EvdevCaptureProvider(activity, rootListener);
return EvdevCaptureProviderShim.createEvdevCaptureProvider(activity, rootListener);
}
else if (AndroidPointerIconCaptureProvider.isCaptureProviderSupported()) {
// Android N's native capture can't capture over system UI elements
@@ -11,10 +11,14 @@ import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbManager;
import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.view.InputDevice;
import android.widget.Toast;
import com.limelight.LimeLog;
import com.limelight.R;
import com.limelight.preferences.PreferenceConfiguration;
import java.util.ArrayList;
@@ -24,6 +28,7 @@ public class UsbDriverService extends Service implements UsbDriverListener {
"com.limelight.USB_PERMISSION";
private UsbManager usbManager;
private PreferenceConfiguration prefConfig;
private final UsbEventReceiver receiver = new UsbEventReceiver();
private final UsbDriverBinder binder = new UsbDriverBinder();
@@ -72,10 +77,22 @@ public class UsbDriverService extends Service implements UsbDriverListener {
// Initial attachment broadcast
if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
final UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
// Continue the state machine
handleUsbDeviceState(device);
// shouldClaimDevice() looks at the kernel's enumerated input
// devices to make its decision about whether to prompt to take
// control of the device. The kernel bringing up the input stack
// may race with this callback and cause us to prompt when the
// kernel is capable of running the device. Let's post a delayed
// message to process this state change to allow the kernel
// some time to bring up the stack.
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
// Continue the state machine
handleUsbDeviceState(device);
}
}, 1000);
}
// Subsequent permission dialog completion intent
else if (action.equals(ACTION_USB_PERMISSION)) {
@@ -104,11 +121,21 @@ public class UsbDriverService extends Service implements UsbDriverListener {
private void handleUsbDeviceState(UsbDevice device) {
// Are we able to operate it?
if (shouldClaimDevice(device)) {
if (shouldClaimDevice(device, prefConfig.bindAllUsb)) {
// 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));
try {
// This function is not documented as throwing any exceptions (denying access
// is indicated by calling the PendingIntent with a false result). However,
// Samsung Knox has some policies which block this request, but rather than
// just returning a false result or returning 0 enumerated devices,
// they throw an undocumented SecurityException from this call, crashing
// the whole app. :(
usbManager.requestPermission(device, PendingIntent.getBroadcast(UsbDriverService.this, 0, new Intent(ACTION_USB_PERMISSION), 0));
} catch (SecurityException e) {
Toast.makeText(this, this.getText(R.string.error_usb_prohibited), Toast.LENGTH_LONG).show();
}
return;
}
@@ -144,13 +171,17 @@ public class UsbDriverService extends Service implements UsbDriverListener {
}
}
private boolean isRecognizedInputDevice(UsbDevice device) {
private static boolean isRecognizedInputDevice(UsbDevice device) {
// On KitKat and later, we can determine if this VID and PID combo
// matches an existing input device and defer to the built-in controller
// support in that case. Prior to KitKat, we'll always return true to be safe.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
for (int id : InputDevice.getDeviceIds()) {
InputDevice inputDev = InputDevice.getDevice(id);
if (inputDev == null) {
// Device was removed while looping
continue;
}
if (inputDev.getVendorId() == device.getVendorId() &&
inputDev.getProductId() == device.getProductId()) {
@@ -165,16 +196,17 @@ public class UsbDriverService extends Service implements UsbDriverListener {
}
}
private boolean shouldClaimDevice(UsbDevice device) {
public static boolean shouldClaimDevice(UsbDevice device, boolean claimAllAvailable) {
// We always bind to XB1 controllers but only bind to XB360 controllers
// if we know the kernel isn't already driving this device.
return XboxOneController.canClaimDevice(device) ||
(!isRecognizedInputDevice(device) && Xbox360Controller.canClaimDevice(device));
((!isRecognizedInputDevice(device) || claimAllAvailable) && Xbox360Controller.canClaimDevice(device));
}
@Override
public void onCreate() {
this.usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
this.prefConfig = PreferenceConfiguration.readPreferences(this);
// Register for USB attach broadcasts and permission completions
IntentFilter filter = new IntentFilter();
@@ -184,7 +216,7 @@ public class UsbDriverService extends Service implements UsbDriverListener {
// Enumerate existing devices
for (UsbDevice dev : usbManager.getDeviceList().values()) {
if (shouldClaimDevice(dev)) {
if (shouldClaimDevice(dev, prefConfig.bindAllUsb)) {
// Start the process of claiming this device
handleUsbDeviceState(dev);
}
@@ -0,0 +1,24 @@
package com.limelight.binding.input.evdev;
import android.app.Activity;
import com.limelight.LimelightBuildProps;
import com.limelight.binding.input.capture.InputCaptureProvider;
public class EvdevCaptureProviderShim {
public static boolean isCaptureProviderSupported() {
return LimelightBuildProps.ROOT_BUILD;
}
// We need to construct our capture provider using reflection because it isn't included in non-root builds
public static InputCaptureProvider createEvdevCaptureProvider(Activity activity, EvdevListener listener) {
try {
Class providerClass = Class.forName("com.limelight.binding.input.evdev.EvdevCaptureProvider");
return (InputCaptureProvider) providerClass.getConstructors()[0].newInstance(activity, listener);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}
@@ -4,6 +4,8 @@ public interface EvdevListener {
int BUTTON_LEFT = 1;
int BUTTON_MIDDLE = 2;
int BUTTON_RIGHT = 3;
int BUTTON_X1 = 4;
int BUTTON_X2 = 5;
void mouseMove(int deltaX, int deltaY);
void mouseButtonEvent(int buttonId, boolean down);
@@ -164,8 +164,8 @@ public class AnalogStick extends VirtualControllerElement {
}
}
public AnalogStick(VirtualController controller, Context context) {
super(controller, context);
public AnalogStick(VirtualController controller, Context context, int elementId) {
super(controller, context, elementId);
// reset stick position
position_stick_x = getWidth() / 2;
position_stick_y = getHeight() / 2;
@@ -210,7 +210,7 @@ public class AnalogStick extends VirtualControllerElement {
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
// calculate new radius sizes depending
radius_complete = getPercent(getCorrectWidth() / 2, 90);
radius_complete = getPercent(getCorrectWidth() / 2, 100);
radius_dead_zone = getPercent(getCorrectWidth() / 2, 30);
radius_analog_stick = getPercent(getCorrectWidth() / 2, 20);
@@ -120,8 +120,8 @@ public class DigitalButton extends VirtualControllerElement {
}
}
public DigitalButton(VirtualController controller, int layer, Context context) {
super(controller, context);
public DigitalButton(VirtualController controller, int elementId, int layer, Context context) {
super(controller, context, elementId);
this.layer = layer;
}
@@ -27,7 +27,7 @@ public class DigitalPad extends VirtualControllerElement {
private final Paint paint = new Paint();
public DigitalPad(VirtualController controller, Context context) {
super(controller, context);
super(controller, context, EID_DPAD);
}
public void addDigitalPadListener(DigitalPadListener listener) {
@@ -10,7 +10,7 @@ import com.limelight.nvstream.input.ControllerPacket;
public class LeftAnalogStick extends AnalogStick {
public LeftAnalogStick(final VirtualController controller, final Context context) {
super(controller, context);
super(controller, context, EID_LS);
addAnalogStickListener(new AnalogStick.AnalogStickListener() {
@Override
@@ -8,7 +8,7 @@ import android.content.Context;
public class LeftTrigger extends DigitalButton {
public LeftTrigger(final VirtualController controller, final int layer, final Context context) {
super(controller, layer, context);
super(controller, EID_LT, layer, context);
addDigitalButtonListener(new DigitalButton.DigitalButtonListener() {
@Override
public void onClick() {
@@ -10,7 +10,7 @@ import com.limelight.nvstream.input.ControllerPacket;
public class RightAnalogStick extends AnalogStick {
public RightAnalogStick(final VirtualController controller, final Context context) {
super(controller, context);
super(controller, context, EID_RS);
addAnalogStickListener(new AnalogStick.AnalogStickListener() {
@Override
@@ -8,7 +8,7 @@ import android.content.Context;
public class RightTrigger extends DigitalButton {
public RightTrigger(final VirtualController controller, final int layer, final Context context) {
super(controller, layer, context);
super(controller, EID_RT, layer, context);
addDigitalButtonListener(new DigitalButton.DigitalButtonListener() {
@Override
public void onClick() {
@@ -45,7 +45,6 @@ public class VirtualController {
ControllerMode currentMode = ControllerMode.Active;
ControllerInputContext inputContext = new ControllerInputContext();
private RelativeLayout.LayoutParams layoutParamsButtonConfigure = null;
private Button buttonConfigure = null;
private List<VirtualControllerElement> elements = new ArrayList<>();
@@ -60,6 +59,8 @@ public class VirtualController {
frame_layout.addView(relative_layout);
buttonConfigure = new Button(context);
buttonConfigure.setAlpha(0.25f);
buttonConfigure.setFocusable(false);
buttonConfigure.setBackgroundResource(R.drawable.ic_settings);
buttonConfigure.setOnClickListener(new View.OnClickListener() {
@Override
@@ -68,6 +69,7 @@ public class VirtualController {
if (currentMode == ControllerMode.Configuration) {
currentMode = ControllerMode.Active;
VirtualControllerConfigurationLoader.saveProfile(VirtualController.this, context);
message = "Exiting configuration mode";
} else {
currentMode = ControllerMode.Configuration;
@@ -85,6 +87,14 @@ public class VirtualController {
});
}
public void hide() {
relative_layout.setVisibility(View.INVISIBLE);
}
public void show() {
relative_layout.setVisibility(View.VISIBLE);
}
public void removeElements() {
for (VirtualControllerElement element : elements) {
relative_layout.removeView(element);
@@ -116,11 +126,19 @@ public class VirtualController {
DisplayMetrics screen = context.getResources().getDisplayMetrics();
int buttonSize = (int)(screen.heightPixels*0.05f);
layoutParamsButtonConfigure = new RelativeLayout.LayoutParams(buttonSize, buttonSize);
relative_layout.addView(buttonConfigure, layoutParamsButtonConfigure);
int buttonSize = (int)(screen.heightPixels*0.06f);
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(buttonSize, buttonSize);
params.addRule(RelativeLayout.ALIGN_PARENT_LEFT, RelativeLayout.TRUE);
params.addRule(RelativeLayout.ALIGN_PARENT_TOP, RelativeLayout.TRUE);
params.leftMargin = 15;
params.topMargin = 15;
relative_layout.addView(buttonConfigure, params);
// Start with the default layout
VirtualControllerConfigurationLoader.createDefaultLayout(this, context);
// Apply user preferences onto the default layout
VirtualControllerConfigurationLoader.loadFromPreferences(this, context);
}
public ControllerMode getControllerMode() {
@@ -4,13 +4,19 @@
package com.limelight.binding.input.virtual_controller;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.DisplayMetrics;
import com.limelight.nvstream.input.ControllerPacket;
import com.limelight.preferences.PreferenceConfiguration;
import org.json.JSONException;
import org.json.JSONObject;
public class VirtualControllerConfigurationLoader {
private static final String PROFILE_PATH = "profiles";
public static final String OSC_PREFERENCE = "OSC";
private static int getPercent(
int percent,
@@ -58,6 +64,7 @@ public class VirtualControllerConfigurationLoader {
}
private static DigitalButton createDigitalButton(
final int elementId,
final int keyShort,
final int keyLong,
final int layer,
@@ -65,7 +72,7 @@ public class VirtualControllerConfigurationLoader {
final int icon,
final VirtualController controller,
final Context context) {
DigitalButton button = new DigitalButton(controller, layer, context);
DigitalButton button = new DigitalButton(controller, elementId, layer, context);
button.setText(text);
button.setIcon(icon);
@@ -146,140 +153,176 @@ public class VirtualControllerConfigurationLoader {
public static void createDefaultLayout(final VirtualController controller, final Context context) {
DisplayMetrics screen = context.getResources().getDisplayMetrics();
PreferenceConfiguration config = PreferenceConfiguration.readPreferences(context);
// NOTE: Some of these getPercent() expressions seem like they can be combined
// into a single call. Due to floating point rounding, this isn't actually possible.
controller.addElement(createDigitalPad(controller, context),
getPercent(5, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels),
getPercent(30, screen.widthPixels),
getPercent(40, screen.heightPixels)
);
if (!config.onlyL3R3)
{
controller.addElement(createDigitalPad(controller, context),
getPercent(5, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels),
getPercent(30, screen.widthPixels),
getPercent(40, screen.heightPixels)
);
controller.addElement(createDigitalButton(
ControllerPacket.A_FLAG, 0, 1, "A", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels)+getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels)+2*getPercent(BUTTON_HEIGHT, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createDigitalButton(
VirtualControllerElement.EID_A,
ControllerPacket.A_FLAG, 0, 1, "A", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels) + getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels) + 2 * getPercent(BUTTON_HEIGHT, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createDigitalButton(
ControllerPacket.B_FLAG, 0, 1, "B", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels)+2*getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels)+getPercent(BUTTON_HEIGHT, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createDigitalButton(
VirtualControllerElement.EID_B,
ControllerPacket.B_FLAG, 0, 1, "B", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels) + 2 * getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels) + getPercent(BUTTON_HEIGHT, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createDigitalButton(
ControllerPacket.X_FLAG, 0, 1, "X", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels)+getPercent(BUTTON_HEIGHT, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createDigitalButton(
VirtualControllerElement.EID_X,
ControllerPacket.X_FLAG, 0, 1, "X", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels) + getPercent(BUTTON_HEIGHT, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createDigitalButton(
ControllerPacket.Y_FLAG, 0, 1, "Y", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels)+getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createDigitalButton(
VirtualControllerElement.EID_Y,
ControllerPacket.Y_FLAG, 0, 1, "Y", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels) + getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createLeftTrigger(
0, "LT", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createLeftTrigger(
0, "LT", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createRightTrigger(
0, "RT", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels)+2*getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createRightTrigger(
0, "RT", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels) + 2 * getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createDigitalButton(
ControllerPacket.LB_FLAG, 0, 1, "LB", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels)+2*getPercent(BUTTON_HEIGHT, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createDigitalButton(
VirtualControllerElement.EID_LB,
ControllerPacket.LB_FLAG, 0, 1, "LB", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels) + 2 * getPercent(BUTTON_HEIGHT, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createDigitalButton(
ControllerPacket.RB_FLAG, 0, 1, "RB", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels)+2*getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels)+2*getPercent(BUTTON_HEIGHT, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createDigitalButton(
VirtualControllerElement.EID_RB,
ControllerPacket.RB_FLAG, 0, 1, "RB", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels) + 2 * getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels) + 2 * getPercent(BUTTON_HEIGHT, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createLeftStick(controller, context),
getPercent(5, screen.widthPixels),
getPercent(50, screen.heightPixels),
getPercent(40, screen.widthPixels),
getPercent(50, screen.heightPixels)
);
controller.addElement(createLeftStick(controller, context),
getPercent(5, screen.widthPixels),
getPercent(50, screen.heightPixels),
getPercent(40, screen.widthPixels),
getPercent(50, screen.heightPixels)
);
controller.addElement(createRightStick(controller, context),
getPercent(55, screen.widthPixels),
getPercent(50, screen.heightPixels),
getPercent(40, screen.widthPixels),
getPercent(50, screen.heightPixels)
);
controller.addElement(createRightStick(controller, context),
getPercent(55, screen.widthPixels),
getPercent(50, screen.heightPixels),
getPercent(40, screen.widthPixels),
getPercent(50, screen.heightPixels)
);
controller.addElement(createDigitalButton(
ControllerPacket.BACK_FLAG, 0, 2, "BACK", -1, controller, context),
getPercent(40, screen.widthPixels),
getPercent(90, screen.heightPixels),
getPercent(10, screen.widthPixels),
getPercent(10, screen.heightPixels)
);
controller.addElement(createDigitalButton(
VirtualControllerElement.EID_BACK,
ControllerPacket.BACK_FLAG, 0, 2, "BACK", -1, controller, context),
getPercent(40, screen.widthPixels),
getPercent(90, screen.heightPixels),
getPercent(10, screen.widthPixels),
getPercent(10, screen.heightPixels)
);
controller.addElement(createDigitalButton(
ControllerPacket.PLAY_FLAG, 0, 3, "START", -1, controller, context),
getPercent(40, screen.widthPixels)+getPercent(10, screen.widthPixels),
getPercent(90, screen.heightPixels),
getPercent(10, screen.widthPixels),
getPercent(10, screen.heightPixels)
);
controller.addElement(createDigitalButton(
VirtualControllerElement.EID_START,
ControllerPacket.PLAY_FLAG, 0, 3, "START", -1, controller, context),
getPercent(40, screen.widthPixels) + getPercent(10, screen.widthPixels),
getPercent(90, screen.heightPixels),
getPercent(10, screen.widthPixels),
getPercent(10, screen.heightPixels)
);
}
else {
controller.addElement(createDigitalButton(
VirtualControllerElement.EID_LSB,
ControllerPacket.LS_CLK_FLAG, 0, 1, "L3", -1, controller, context),
getPercent(2, screen.widthPixels),
getPercent(80, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createDigitalButton(
VirtualControllerElement.EID_RSB,
ControllerPacket.RS_CLK_FLAG, 0, 1, "R3", -1, controller, context),
getPercent(89, screen.widthPixels),
getPercent(80, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
}
}
/*
NOT IMPLEMENTED YET,
this should later be used to store and load a profile for the virtual controller
public static void saveProfile(final String name,
final VirtualController controller,
public static void saveProfile(final VirtualController controller,
final Context context) {
SharedPreferences.Editor prefEditor = context.getSharedPreferences(OSC_PREFERENCE, Activity.MODE_PRIVATE).edit();
SharedPreferences preferences = context.getSharedPreferences(PROFILE_PATH + "/" +
name, Activity.MODE_PRIVATE);
JSONArray elementConfigurations = new JSONArray();
for (VirtualControllerElement element : controller.getElements()) {
JSONObject elementConfiguration = new JSONObject();
String prefKey = ""+element.elementId;
try {
elementConfiguration.put("TYPE", element.getClass().getName());
elementConfiguration.put("CONFIGURATION", element.getConfiguration());
elementConfigurations.put(elementConfiguration);
} catch (Exception e) {
prefEditor.putString(prefKey, element.getConfiguration().toString());
} catch (JSONException e) {
e.printStackTrace();
}
}
SharedPreferences.Editor editor= preferences.edit();
editor.putString("ELEMENTS", elementConfigurations.toString());
prefEditor.apply();
}
public static void loadFromPreferences(final VirtualController controller) {
public static void loadFromPreferences(final VirtualController controller, final Context context) {
SharedPreferences pref = context.getSharedPreferences(OSC_PREFERENCE, Activity.MODE_PRIVATE);
for (VirtualControllerElement element : controller.getElements()) {
String prefKey = ""+element.elementId;
String jsonConfig = pref.getString(prefKey, null);
if (jsonConfig != null) {
try {
element.loadConfiguration(new JSONObject(jsonConfig));
} catch (JSONException e) {
e.printStackTrace();
// Remove the corrupt element from the preferences
pref.edit().remove(prefKey).apply();
}
}
}
}
*/
}
@@ -14,10 +14,30 @@ import android.view.MotionEvent;
import android.view.View;
import android.widget.RelativeLayout;
import org.json.JSONException;
import org.json.JSONObject;
public abstract class VirtualControllerElement extends View {
protected static boolean _PRINT_DEBUG_INFORMATION = false;
public static final int EID_DPAD = 1;
public static final int EID_LT = 2;
public static final int EID_RT = 3;
public static final int EID_LB = 4;
public static final int EID_RB = 5;
public static final int EID_A = 6;
public static final int EID_B = 7;
public static final int EID_X = 8;
public static final int EID_Y = 9;
public static final int EID_BACK = 10;
public static final int EID_START = 11;
public static final int EID_LS = 12;
public static final int EID_RS = 13;
public static final int EID_LSB = 14;
public static final int EID_RSB = 15;
protected VirtualController virtualController;
protected final int elementId;
private final Paint paint = new Paint();
@@ -40,10 +60,11 @@ public abstract class VirtualControllerElement extends View {
private Mode currentMode = Mode.Normal;
protected VirtualControllerElement(VirtualController controller, Context context) {
protected VirtualControllerElement(VirtualController controller, Context context, int elementId) {
super(context);
this.virtualController = controller;
this.elementId = elementId;
}
protected void moveElement(int pressed_x, int pressed_y, int x, int y) {
@@ -278,13 +299,28 @@ public abstract class VirtualControllerElement extends View {
return getWidth() > getHeight() ? getHeight() : getWidth();
}
/**
public JSONObject getConfiguration () {
JSONObject configuration = new JSONObject();
return configuration;
}
public void loadConfiguration (JSONObject configuration) {
}
*/
public JSONObject getConfiguration() throws JSONException {
JSONObject configuration = new JSONObject();
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
configuration.put("LEFT", layoutParams.leftMargin);
configuration.put("TOP", layoutParams.topMargin);
configuration.put("WIDTH", layoutParams.width);
configuration.put("HEIGHT", layoutParams.height);
return configuration;
}
public void loadConfiguration(JSONObject configuration) throws JSONException {
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin = configuration.getInt("LEFT");
layoutParams.topMargin = configuration.getInt("TOP");
layoutParams.width = configuration.getInt("WIDTH");
layoutParams.height = configuration.getInt("HEIGHT");
requestLayout();
}
}
@@ -12,13 +12,13 @@ import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.jni.MoonBridge;
import com.limelight.preferences.PreferenceConfiguration;
import android.content.SharedPreferences;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.media.MediaCodec.BufferInfo;
import android.media.MediaCodec.CodecException;
import android.os.Build;
import android.util.Range;
import android.view.SurfaceHolder;
public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
@@ -32,13 +32,14 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
private MediaCodecInfo avcDecoder;
private MediaCodecInfo hevcDecoder;
// Used for HEVC only
private byte[] vpsBuffer;
private byte[] spsBuffer;
private byte[] ppsBuffer;
private boolean submittedCsd;
private boolean submitCsdNextCall;
private MediaCodec videoDecoder;
private Thread rendererThread;
private Thread[] spinnerThreads;
private boolean needsSpsBitstreamFixup, isExynos4;
private boolean adaptivePlayback, directSubmit;
private boolean constrainedHighProfile;
@@ -51,6 +52,9 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
private CrashListener crashListener;
private boolean reportedCrash;
private int consecutiveCrashCount;
private String glRenderer;
private boolean foreground = true;
private boolean legacyFrameDropRendering = false;
private boolean needsBaselineSpsHack;
private SeqParameterSet savedSps;
@@ -62,12 +66,13 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
private long lastTimestampUs;
private long decoderTimeMs;
private long totalTimeMs;
private int totalFrames;
private int totalFramesReceived;
private int totalFramesRendered;
private int frameLossEvents;
private int framesLost;
private int lastFrameNumber;
private int refreshRate;
private int bitrate;
private PreferenceConfiguration prefs;
private int numSpsIn;
private int numPpsIn;
@@ -81,9 +86,9 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
return decoder;
}
private MediaCodecInfo findHevcDecoder(int videoFormat) {
private MediaCodecInfo findHevcDecoder(PreferenceConfiguration prefs, boolean meteredNetwork, boolean requestedHdr) {
// Don't return anything if H.265 is forced off
if (videoFormat == PreferenceConfiguration.FORCE_H265_OFF) {
if (prefs.videoFormat == PreferenceConfiguration.FORCE_H265_OFF) {
return null;
}
@@ -94,10 +99,11 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
// for even required levels of HEVC.
MediaCodecInfo decoderInfo = MediaCodecHelper.findProbableSafeDecoder("video/hevc", -1);
if (decoderInfo != null) {
if (!MediaCodecHelper.decoderIsWhitelistedForHevc(decoderInfo.getName())) {
if (!MediaCodecHelper.decoderIsWhitelistedForHevc(decoderInfo.getName(), meteredNetwork)) {
LimeLog.info("Found HEVC decoder, but it's not whitelisted - "+decoderInfo.getName());
if (videoFormat == PreferenceConfiguration.FORCE_H265_ON) {
// HDR implies HEVC forced on, since HEVCMain10HDR10 is required for HDR
if (prefs.videoFormat == PreferenceConfiguration.FORCE_H265_ON || requestedHdr) {
LimeLog.info("Forcing H265 enabled despite non-whitelisted decoder");
}
else {
@@ -113,21 +119,16 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
this.renderTarget = renderTarget;
}
public MediaCodecDecoderRenderer(int videoFormat, int bitrate, boolean batterySaver,
CrashListener crashListener, int consecutiveCrashCount) {
public MediaCodecDecoderRenderer(PreferenceConfiguration prefs,
CrashListener crashListener, int consecutiveCrashCount,
boolean meteredData, boolean requestedHdr,
String glRenderer) {
//dumpDecoders();
this.bitrate = bitrate;
this.prefs = prefs;
this.crashListener = crashListener;
this.consecutiveCrashCount = consecutiveCrashCount;
// Disable spinner threads in battery saver mode
if (batterySaver) {
spinnerThreads = new Thread[0];
}
else {
spinnerThreads = new Thread[Runtime.getRuntime().availableProcessors()];
}
this.glRenderer = glRenderer;
avcDecoder = findAvcDecoder();
if (avcDecoder != null) {
@@ -137,7 +138,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
LimeLog.warning("No AVC decoder found");
}
hevcDecoder = findHevcDecoder(videoFormat);
hevcDecoder = findHevcDecoder(prefs, meteredData, requestedHdr);
if (hevcDecoder != null) {
LimeLog.info("Selected HEVC decoder: "+hevcDecoder.getName());
}
@@ -152,10 +153,15 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
// shared between AVC and HEVC decoders on the same device.
if (avcDecoder != null) {
directSubmit = MediaCodecHelper.decoderCanDirectSubmit(avcDecoder.getName());
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(avcDecoder.getName());
refFrameInvalidationAvc = MediaCodecHelper.decoderSupportsRefFrameInvalidationAvc(avcDecoder.getName());
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(avcDecoder);
refFrameInvalidationAvc = MediaCodecHelper.decoderSupportsRefFrameInvalidationAvc(avcDecoder.getName(), prefs.height);
refFrameInvalidationHevc = MediaCodecHelper.decoderSupportsRefFrameInvalidationHevc(avcDecoder.getName());
if (consecutiveCrashCount % 2 == 1) {
refFrameInvalidationAvc = refFrameInvalidationHevc = false;
LimeLog.warning("Disabling RFI due to previous crash");
}
if (directSubmit) {
LimeLog.info("Decoder "+avcDecoder.getName()+" will use direct submit");
}
@@ -176,6 +182,38 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
return avcDecoder != null;
}
public boolean is49FpsBlacklisted() {
return avcDecoder != null && MediaCodecHelper.decoderBlacklistedFor49Fps(avcDecoder.getName());
}
public void enableLegacyFrameDropRendering() {
LimeLog.info("Legacy frame drop rendering enabled");
legacyFrameDropRendering = true;
}
public boolean isHevcMain10Hdr10Supported() {
if (hevcDecoder == null) {
return false;
}
for (MediaCodecInfo.CodecProfileLevel profileLevel : hevcDecoder.getCapabilitiesForType("video/hevc").profileLevels) {
if (profileLevel.profile == MediaCodecInfo.CodecProfileLevel.HEVCProfileMain10HDR10) {
LimeLog.info("HEVC decoder "+hevcDecoder.getName()+" supports HEVC Main10 HDR10");
return true;
}
}
return false;
}
public void notifyVideoForeground() {
foreground = true;
}
public void notifyVideoBackground() {
foreground = false;
}
public int getActiveVideoFormat() {
return this.videoFormat;
}
@@ -190,7 +228,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
String mimeType;
String selectedDecoderName;
if (videoFormat == MoonBridge.VIDEO_FORMAT_H264) {
if ((videoFormat & MoonBridge.VIDEO_FORMAT_MASK_H264) != 0) {
mimeType = "video/avc";
selectedDecoderName = avcDecoder.getName();
@@ -219,7 +257,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
refFrameInvalidationActive = refFrameInvalidationAvc;
}
else if (videoFormat == MoonBridge.VIDEO_FORMAT_H265) {
else if ((videoFormat & MoonBridge.VIDEO_FORMAT_MASK_H265) != 0) {
mimeType = "video/hevc";
selectedDecoderName = hevcDecoder.getName();
@@ -367,7 +405,16 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
}
// Render the last buffer
videoDecoder.releaseOutputBuffer(lastIndex, true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && !legacyFrameDropRendering) {
// Use a PTS that will cause this frame to never be dropped if frame dropping
// is disabled
videoDecoder.releaseOutputBuffer(lastIndex, 0);
}
else {
videoDecoder.releaseOutputBuffer(lastIndex, true);
}
totalFramesRendered++;
// Add delta time to the totals (excluding probable outliers)
long delta = MediaCodecHelper.getMonotonicMillis() - (presentationTimeUs / 1000);
@@ -400,32 +447,9 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
rendererThread.start();
}
private void startSpinnerThreads() {
LimeLog.info("Using "+spinnerThreads.length+" spinner threads");
for (int i = 0; i < spinnerThreads.length; i++) {
spinnerThreads[i] = new Thread() {
@Override
public void run() {
// This thread exists to keep the CPU at a higher DVFS state on devices
// where the governor scales clock speed sporadically, causing dropped frames.
while (!stopping) {
try {
Thread.sleep(0, 1);
} catch (InterruptedException e) {
break;
}
}
}
};
spinnerThreads[i].setName("Spinner-"+i);
spinnerThreads[i].setPriority(Thread.MIN_PRIORITY);
spinnerThreads[i].start();
}
}
private int dequeueInputBuffer() {
int index = -1;
long startTime, queueTime;
long startTime;
startTime = MediaCodecHelper.getMonotonicMillis();
@@ -438,14 +462,24 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
return MediaCodec.INFO_TRY_AGAIN_LATER;
}
if (index < 0) {
return index;
int deltaMs = (int)(MediaCodecHelper.getMonotonicMillis() - startTime);
if (deltaMs >= 20) {
LimeLog.warning("Dequeue input buffer ran long: " + deltaMs + " ms");
}
queueTime = MediaCodecHelper.getMonotonicMillis();
if (queueTime - startTime >= 20) {
LimeLog.warning("Queue input buffer ran long: " + (queueTime - startTime) + " ms");
if (index < 0) {
// We've been hung for 5 seconds and no other exception was reported,
// so generate a decoder hung exception
if (deltaMs >= 5000 && initialException == null) {
DecoderHungException decoderHungException = new DecoderHungException(deltaMs);
if (!reportedCrash) {
reportedCrash = true;
crashListener.notifyCrash(decoderHungException);
}
throw new RendererException(this, decoderHungException);
}
return index;
}
return index;
@@ -454,7 +488,6 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
@Override
public void start() {
startRendererThread();
startSpinnerThreads();
}
// !!! May be called even if setup()/start() fails !!!
@@ -473,34 +506,10 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
// May be called already, but we'll call it now to be safe
prepareForStop();
try {
// Invalidate pending decode buffers
videoDecoder.flush();
} catch (Exception e) {
e.printStackTrace();
}
// Wait for the renderer thread to shut down
try {
rendererThread.join();
} catch (InterruptedException ignored) { }
try {
// Stop the video decoder
videoDecoder.stop();
} catch (Exception e) {
e.printStackTrace();
}
// Halt the spinner threads
for (Thread t : spinnerThreads) {
t.interrupt();
}
for (Thread t : spinnerThreads) {
try {
t.join();
} catch (InterruptedException ignored) { }
}
}
@Override
@@ -548,22 +557,28 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
// 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) {
if (sps.profileIdc == 100 && constrainedHighProfile) {
LimeLog.info("Setting constraint set flags for constrained high profile");
sps.constraint_set_4_flag = true;
sps.constraint_set_5_flag = true;
sps.constraintSet4Flag = true;
sps.constraintSet5Flag = 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;
sps.constraintSet4Flag = false;
sps.constraintSet5Flag = false;
}
}
@SuppressWarnings("deprecation")
@Override
public int submitDecodeUnit(byte[] frameData, int frameLength, int frameNumber, long receiveTimeMs) {
totalFrames++;
public int submitDecodeUnit(byte[] decodeUnitData, int decodeUnitLength, int decodeUnitType,
int frameNumber, long receiveTimeMs) {
if (stopping) {
// Don't bother if we're stopping
return MoonBridge.DR_OK;
}
totalFramesReceived++;
// We can receive the same "frame" multiple times if it's an IDR frame.
// In that case, each frame start NALU is submitted independently.
@@ -593,14 +608,12 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
lastTimestampUs = timestampUs;
int codecFlags = 0;
boolean needsSpsReplay = false;
// H264 SPS
if (frameData[4] == 0x67) {
if (decodeUnitData[4] == 0x67) {
numSpsIn++;
codecFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
ByteBuffer spsBuf = ByteBuffer.wrap(frameData);
ByteBuffer spsBuf = ByteBuffer.wrap(decodeUnitData);
// Skip to the start of the NALU data
spsBuf.position(5);
@@ -614,15 +627,20 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
// for known resolution combinations. Reference frame invalidation may need
// these, so leave them be for those decoders.
if (!refFrameInvalidationActive) {
if (initialWidth == 1280 && initialHeight == 720) {
if (initialWidth <= 720 && initialHeight <= 480) {
// Max 5 buffered frames at 720x480x60
LimeLog.info("Patching level_idc to 31");
sps.levelIdc = 31;
}
else if (initialWidth <= 1280 && initialHeight <= 720) {
// Max 5 buffered frames at 1280x720x60
LimeLog.info("Patching level_idc to 32");
sps.level_idc = 32;
sps.levelIdc = 32;
}
else if (initialWidth == 1920 && initialHeight == 1080) {
else if (initialWidth <= 1920 && initialHeight <= 1080) {
// Max 4 buffered frames at 1920x1080x64
LimeLog.info("Patching level_idc to 42");
sps.level_idc = 42;
sps.levelIdc = 42;
}
else {
// Leave the profile alone (currently 5.0)
@@ -640,14 +658,14 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
// where we've enabled reference frame invalidation.
if (!refFrameInvalidationActive) {
LimeLog.info("Patching num_ref_frames in SPS");
sps.num_ref_frames = 1;
sps.numRefFrames = 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;
sps.vuiParams.videoSignalTypePresentFlag = false;
sps.vuiParams.colourDescriptionPresentFlag = false;
sps.vuiParams.chromaLocInfoPresentFlag = false;
if ((needsSpsBitstreamFixup || isExynos4) && !refFrameInvalidationActive) {
// The SPS that comes in the current H264 bytestream doesn't set bitstream_restriction_flag
@@ -657,22 +675,22 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
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;
sps.vuiParams.bitstreamRestriction.motionVectorsOverPicBoundariesFlag = true;
sps.vuiParams.bitstreamRestriction.log2MaxMvLengthHorizontal = 16;
sps.vuiParams.bitstreamRestriction.log2MaxMvLengthVertical = 16;
sps.vuiParams.bitstreamRestriction.numReorderFrames = 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;
// Some devices throw errors if maxDecFrameBuffering < numRefFrames
sps.vuiParams.bitstreamRestriction.maxDecFrameBuffering = sps.numRefFrames;
// 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.maxBytesPerPicDenom = 2;
sps.vuiParams.bitstreamRestriction.maxBitsPerMbDenom = 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.
@@ -686,109 +704,81 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
// If we need to hack this SPS to say we're baseline, do so now
if (needsBaselineSpsHack) {
LimeLog.info("Hacking SPS to baseline");
sps.profile_idc = 66;
sps.profileIdc = 66;
savedSps = sps;
}
// Patch the SPS constraint flags
doProfileSpecificSpsPatching(sps);
inputBufferIndex = dequeueInputBuffer();
if (inputBufferIndex < 0) {
// We're being torn down now
return MoonBridge.DR_NEED_IDR;
}
buf = getEmptyInputBuffer(inputBufferIndex);
if (buf == null) {
// We're being torn down now
return MoonBridge.DR_NEED_IDR;
}
// Write the annex B header
buf.put(frameData, 0, 5);
// The H264Utils.writeSPS function safely handles
// Annex B NALUs (including NALUs with escape sequences)
ByteBuffer escapedNalu = H264Utils.writeSPS(sps, frameLength);
buf.put(escapedNalu);
ByteBuffer escapedNalu = H264Utils.writeSPS(sps, decodeUnitLength);
if (queueInputBuffer(inputBufferIndex,
0, buf.position(),
timestampUs, codecFlags)) {
return MoonBridge.DR_OK;
}
else {
return MoonBridge.DR_NEED_IDR;
}
// H264 PPS
} else if (frameData[4] == 0x68) {
numPpsIn++;
codecFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
inputBufferIndex = dequeueInputBuffer();
if (inputBufferIndex < 0) {
// We're being torn down now
return MoonBridge.DR_NEED_IDR;
}
buf = getEmptyInputBuffer(inputBufferIndex);
if (buf == null) {
// We're being torn down now
return MoonBridge.DR_NEED_IDR;
}
if (needsBaselineSpsHack) {
LimeLog.info("Saw PPS; disabling SPS hack");
needsBaselineSpsHack = false;
// Give the decoder the SPS again with the proper profile now
needsSpsReplay = true;
}
// Batch this to submit together with PPS
spsBuffer = new byte[5 + escapedNalu.limit()];
System.arraycopy(decodeUnitData, 0, spsBuffer, 0, 5);
escapedNalu.get(spsBuffer, 5, escapedNalu.limit());
return MoonBridge.DR_OK;
}
else if (frameData[4] == 0x40) {
else if (decodeUnitType == MoonBridge.BUFFER_TYPE_VPS) {
numVpsIn++;
// Batch this to submit together with SPS and PPS per AOSP docs
vpsBuffer = new byte[frameLength];
System.arraycopy(frameData, 0, vpsBuffer, 0, frameLength);
vpsBuffer = new byte[decodeUnitLength];
System.arraycopy(decodeUnitData, 0, vpsBuffer, 0, decodeUnitLength);
return MoonBridge.DR_OK;
}
else if (frameData[4] == 0x42) {
// Only the HEVC SPS hits this path (H.264 is handled above)
else if (decodeUnitType == MoonBridge.BUFFER_TYPE_SPS) {
numSpsIn++;
// Batch this to submit together with VPS and PPS per AOSP docs
spsBuffer = new byte[frameLength];
System.arraycopy(frameData, 0, spsBuffer, 0, frameLength);
spsBuffer = new byte[decodeUnitLength];
System.arraycopy(decodeUnitData, 0, spsBuffer, 0, decodeUnitLength);
return MoonBridge.DR_OK;
}
else if (frameData[4] == 0x44) {
else if (decodeUnitType == MoonBridge.BUFFER_TYPE_PPS) {
numPpsIn++;
inputBufferIndex = dequeueInputBuffer();
if (inputBufferIndex < 0) {
// We're being torn down now
return MoonBridge.DR_NEED_IDR;
}
// If this is the first CSD blob or we aren't supporting
// adaptive playback, we will submit the CSD blob in a
// separate input buffer.
if (!submittedCsd || !adaptivePlayback) {
inputBufferIndex = dequeueInputBuffer();
if (inputBufferIndex < 0) {
// We're being torn down now
return MoonBridge.DR_NEED_IDR;
}
buf = getEmptyInputBuffer(inputBufferIndex);
if (buf == null) {
// We're being torn down now
return MoonBridge.DR_NEED_IDR;
}
buf = getEmptyInputBuffer(inputBufferIndex);
if (buf == null) {
// We're being torn down now
return MoonBridge.DR_NEED_IDR;
}
// When we get the PPS, submit the VPS and SPS together with
// the PPS, as required by AOSP docs on use of HEVC and MediaCodec.
if (vpsBuffer != null) {
buf.put(vpsBuffer);
}
if (spsBuffer != null) {
buf.put(spsBuffer);
}
// When we get the PPS, submit the VPS and SPS together with
// the PPS, as required by AOSP docs on use of MediaCodec.
if (vpsBuffer != null) {
buf.put(vpsBuffer);
}
if (spsBuffer != null) {
buf.put(spsBuffer);
}
// This is the HEVC CSD blob
codecFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
// This is the CSD blob
codecFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
}
else {
// Batch this to submit together with the next I-frame
ppsBuffer = new byte[decodeUnitLength];
System.arraycopy(decodeUnitData, 0, ppsBuffer, 0, decodeUnitLength);
// Next call will be I-frame data
submitCsdNextCall = true;
return MoonBridge.DR_OK;
}
}
else {
inputBufferIndex = dequeueInputBuffer();
@@ -802,10 +792,34 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
// We're being torn down now
return MoonBridge.DR_NEED_IDR;
}
if (submitCsdNextCall) {
if (vpsBuffer != null) {
buf.put(vpsBuffer);
}
if (spsBuffer != null) {
buf.put(spsBuffer);
}
if (ppsBuffer != null) {
buf.put(ppsBuffer);
}
submitCsdNextCall = false;
}
}
if (decodeUnitLength > buf.limit() - buf.position()) {
IllegalArgumentException exception = new IllegalArgumentException(
"Decode unit length "+decodeUnitLength+" too large for input buffer "+buf.limit());
if (!reportedCrash) {
reportedCrash = true;
crashListener.notifyCrash(exception);
}
throw new RendererException(this, exception);
}
// Copy data from our buffer list into the input buffer
buf.put(frameData, 0, frameLength);
buf.put(decodeUnitData, 0, decodeUnitLength);
if (!queueInputBuffer(inputBufferIndex,
0, buf.position(),
@@ -813,12 +827,18 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
return MoonBridge.DR_NEED_IDR;
}
if (needsSpsReplay) {
if (!replaySps()) {
return MoonBridge.DR_NEED_IDR;
}
if ((codecFlags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
submittedCsd = true;
LimeLog.info("SPS replay complete");
if (needsBaselineSpsHack) {
needsBaselineSpsHack = false;
if (!replaySps()) {
return MoonBridge.DR_NEED_IDR;
}
LimeLog.info("SPS replay complete");
}
}
return MoonBridge.DR_OK;
@@ -839,7 +859,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
inputBuffer.put(new byte[]{0x00, 0x00, 0x00, 0x01, 0x67});
// Switch the H264 profile back to high
savedSps.profile_idc = 100;
savedSps.profileIdc = 100;
// Patch the SPS constraint flags
doProfileSpecificSpsPatching(savedSps);
@@ -883,55 +903,82 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
}
public int getAverageEndToEndLatency() {
if (totalFrames == 0) {
if (totalFramesReceived == 0) {
return 0;
}
return (int)(totalTimeMs / totalFrames);
return (int)(totalTimeMs / totalFramesReceived);
}
public int getAverageDecoderLatency() {
if (totalFrames == 0) {
if (totalFramesReceived == 0) {
return 0;
}
return (int)(decoderTimeMs / totalFrames);
return (int)(decoderTimeMs / totalFramesReceived);
}
public class RendererException extends RuntimeException {
private static final long serialVersionUID = 8985937536997012406L;
static class DecoderHungException extends RuntimeException {
private int hangTimeMs;
private final Exception originalException;
private final MediaCodecDecoderRenderer renderer;
private ByteBuffer currentBuffer;
private int currentCodecFlags;
public RendererException(MediaCodecDecoderRenderer renderer, Exception e) {
this.renderer = renderer;
this.originalException = e;
}
public RendererException(MediaCodecDecoderRenderer renderer, Exception e, ByteBuffer currentBuffer, int currentCodecFlags) {
this.renderer = renderer;
this.originalException = e;
this.currentBuffer = currentBuffer;
this.currentCodecFlags = currentCodecFlags;
DecoderHungException(int hangTimeMs) {
this.hangTimeMs = hangTimeMs;
}
public String toString() {
String str = "";
str += "Format: "+renderer.videoFormat+"\n";
str += "Hang time: "+hangTimeMs+" ms\n";
str += super.toString();
return str;
}
}
static class RendererException extends RuntimeException {
private static final long serialVersionUID = 8985937536997012406L;
private String text;
RendererException(MediaCodecDecoderRenderer renderer, Exception e) {
this.text = generateText(renderer, e, null, 0);
}
RendererException(MediaCodecDecoderRenderer renderer, Exception e, ByteBuffer currentBuffer, int currentCodecFlags) {
this.text = generateText(renderer, e, currentBuffer, currentCodecFlags);
}
public String toString() {
return text;
}
private String generateText(MediaCodecDecoderRenderer renderer, Exception originalException, ByteBuffer currentBuffer, int currentCodecFlags) {
String str = "";
str += "Format: "+String.format("%x", renderer.videoFormat)+"\n";
str += "AVC Decoder: "+((renderer.avcDecoder != null) ? renderer.avcDecoder.getName():"(none)")+"\n";
str += "HEVC Decoder: "+((renderer.hevcDecoder != null) ? renderer.hevcDecoder.getName():"(none)")+"\n";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && renderer.avcDecoder != null) {
Range<Integer> avcWidthRange = renderer.avcDecoder.getCapabilitiesForType("video/avc").getVideoCapabilities().getSupportedWidths();
str += "AVC supported width range: "+avcWidthRange.getLower()+" - "+avcWidthRange.getUpper()+"\n";
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && renderer.hevcDecoder != null) {
Range<Integer> hevcWidthRange = renderer.hevcDecoder.getCapabilitiesForType("video/hevc").getVideoCapabilities().getSupportedWidths();
str += "HEVC supported width range: "+hevcWidthRange.getLower()+" - "+hevcWidthRange.getUpper()+"\n";
}
str += "Adaptive playback: "+renderer.adaptivePlayback+"\n";
str += "GL Renderer: "+renderer.glRenderer+"\n";
str += "Build fingerprint: "+Build.FINGERPRINT+"\n";
str += "Foreground: "+renderer.foreground+"\n";
str += "Consecutive crashes: "+renderer.consecutiveCrashCount+"\n";
str += "Initial video dimensions: "+renderer.initialWidth+"x"+renderer.initialHeight+"\n";
str += "RFI active: "+renderer.refFrameInvalidationActive+"\n";
str += "Video dimensions: "+renderer.initialWidth+"x"+renderer.initialHeight+"\n";
str += "FPS target: "+renderer.refreshRate+"\n";
str += "Bitrate: "+renderer.bitrate+" Mbps \n";
str += "Bitrate: "+renderer.prefs.bitrate+" Kbps \n";
str += "In stats: "+renderer.numVpsIn+", "+renderer.numSpsIn+", "+renderer.numPpsIn+"\n";
str += "Total frames: "+renderer.totalFrames+"\n";
str += "Frame losses: "+renderer.framesLost+" in "+frameLossEvents+" loss events\n";
str += "Average end-to-end client latency: "+getAverageEndToEndLatency()+"ms\n";
str += "Average hardware decoder latency: "+getAverageDecoderLatency()+"ms\n";
str += "Total frames received: "+renderer.totalFramesReceived+"\n";
str += "Total frames rendered: "+renderer.totalFramesRendered+"\n";
str += "Frame losses: "+renderer.framesLost+" in "+renderer.frameLossEvents+" loss events\n";
str += "Average end-to-end client latency: "+renderer.getAverageEndToEndLatency()+"ms\n";
str += "Average hardware decoder latency: "+renderer.getAverageDecoderLatency()+"ms\n";
if (currentBuffer != null) {
str += "Current buffer: ";
@@ -945,6 +992,20 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
str += "Is Exynos 4: "+renderer.isExynos4+"\n";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (originalException instanceof CodecException) {
CodecException ce = (CodecException) originalException;
str += "Diagnostic Info: "+ce.getDiagnosticInfo()+"\n";
str += "Recoverable: "+ce.isRecoverable()+"\n";
str += "Transient: "+ce.isTransient()+"\n";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
str += "Codec Error Code: "+ce.getErrorCode()+"\n";
}
}
}
str += "/proc/cpuinfo:\n";
try {
str += MediaCodecHelper.readCpuinfo();
@@ -7,9 +7,10 @@ import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.ActivityManager;
import android.content.Context;
import android.content.pm.ConfigurationInfo;
@@ -27,13 +28,18 @@ public class MediaCodecHelper {
private static final List<String> blacklistedDecoderPrefixes;
private static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
private static final List<String> whitelistedAdaptiveResolutionPrefixes;
private static final List<String> blacklistedAdaptivePlaybackPrefixes;
private static final List<String> deprioritizedHevcDecoders;
private static final List<String> baselineProfileHackPrefixes;
private static final List<String> directSubmitPrefixes;
private static final List<String> constrainedHighProfilePrefixes;
private static final List<String> whitelistedHevcDecoders;
private static final List<String> refFrameInvalidationAvcPrefixes;
private static final List<String> refFrameInvalidationHevcPrefixes;
private static final List<String> blacklisted49FpsDecoderPrefixes;
private static boolean isLowEndSnapdragon = false;
private static boolean initialized = false;
static {
directSubmitPrefixes = new LinkedList<>();
@@ -65,16 +71,14 @@ public class MediaCodecHelper {
blacklistedDecoderPrefixes = new LinkedList<>();
// Blacklist software decoders that don't support H264 high profile,
// but exclude the official AOSP emulator from this restriction.
if (!Build.HARDWARE.equals("ranchu") || !Build.BRAND.equals("google")) {
// but exclude the official AOSP and CrOS emulator from this restriction.
if (!Build.HARDWARE.equals("ranchu") && !Build.HARDWARE.equals("cheets")) {
blacklistedDecoderPrefixes.add("omx.google");
blacklistedDecoderPrefixes.add("AVCDecoder");
}
// Without bitstream fixups, we perform horribly on NVIDIA's HEVC
// decoder. While not strictly necessary, I'm going to fully blacklist this
// one to avoid users getting inaccurate impressions of Tegra X1/Moonlight performance.
blacklistedDecoderPrefixes.add("OMX.Nvidia.h265.decode");
// Never use ffmpeg decoders since they're software decoders
blacklistedDecoderPrefixes.add("OMX.ffmpeg");
// Force these decoders disabled because:
// 1) They are software decoders, so the performance is terrible
@@ -94,11 +98,13 @@ public class MediaCodecHelper {
baselineProfileHackPrefixes = new LinkedList<>();
baselineProfileHackPrefixes.add("omx.intel");
whitelistedAdaptiveResolutionPrefixes = new LinkedList<>();
whitelistedAdaptiveResolutionPrefixes.add("omx.nvidia");
whitelistedAdaptiveResolutionPrefixes.add("omx.qcom");
whitelistedAdaptiveResolutionPrefixes.add("omx.sec");
whitelistedAdaptiveResolutionPrefixes.add("omx.TI");
blacklistedAdaptivePlaybackPrefixes = new LinkedList<>();
// The Intel decoder on Lollipop on Nexus Player would increase latency badly
// if adaptive playback was enabled so let's avoid it to be safe.
blacklistedAdaptivePlaybackPrefixes.add("omx.intel");
// The MediaTek decoder crashes at 1080p when adaptive playback is enabled
// on some Android TV devices with H.265 only.
blacklistedAdaptivePlaybackPrefixes.add("omx.mtk");
constrainedHighProfilePrefixes = new LinkedList<>();
constrainedHighProfilePrefixes.add("omx.intel");
@@ -115,14 +121,21 @@ public class MediaCodecHelper {
// Exynos seems to be the only HEVC decoder that works reliably
whitelistedHevcDecoders.add("omx.exynos");
// TODO: This needs a similar fixup to the Tegra 3 otherwise it buffers 16 frames
//whitelistedHevcDecoders.add("omx.nvidia");
// On Darcy (Shield 2017), HEVC runs fine with no fixups required.
// For some reason, other X1 implementations require bitstream fixups.
if (Build.DEVICE.equalsIgnoreCase("darcy")) {
whitelistedHevcDecoders.add("omx.nvidia");
}
else {
// TODO: This needs a similar fixup to the Tegra 3 otherwise it buffers 16 frames
}
// Sony ATVs have broken MediaTek codecs (decoder hangs after rendering the first frame).
// I know the Fire TV 2 works, so I'll just whitelist Amazon devices which seem
// to actually be tested. Ugh...
// I know the Fire TV 2 and 3 works, so I'll just whitelist Amazon devices which seem
// to actually be tested.
if (Build.MANUFACTURER.equalsIgnoreCase("Amazon")) {
whitelistedHevcDecoders.add("omx.mtk");
whitelistedHevcDecoders.add("omx.amlogic");
}
// These theoretically have good HEVC decoding capabilities (potentially better than
@@ -134,20 +147,94 @@ public class MediaCodecHelper {
// during initialization to avoid SoCs with broken HEVC decoders.
}
public static void initializeWithContext(Context context) {
static {
deprioritizedHevcDecoders = new LinkedList<>();
// These are decoders that work but aren't used by default for various reasons.
// Qualcomm is currently the only decoders in this group.
}
static {
blacklisted49FpsDecoderPrefixes = new LinkedList<>();
// We see a bunch of crashes on MediaTek Android TVs running
// at 49 FPS (PAL 50 Hz - 1). Blacklist this frame rate for
// these devices and hope they fix it in Oreo.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
blacklisted49FpsDecoderPrefixes.add("omx.mtk");
}
}
private static boolean isPowerVR(String glRenderer) {
return glRenderer.toLowerCase().contains("powervr");
}
private static String getAdrenoVersionString(String glRenderer) {
glRenderer = glRenderer.toLowerCase().trim();
if (!glRenderer.contains("adreno")) {
return null;
}
Pattern modelNumberPattern = Pattern.compile("(.*)([0-9]{3})(.*)");
Matcher matcher = modelNumberPattern.matcher(glRenderer);
if (!matcher.matches()) {
return null;
}
String modelNumber = matcher.group(2);
LimeLog.info("Found Adreno GPU: "+modelNumber);
return modelNumber;
}
private static boolean isLowEndSnapdragonRenderer(String glRenderer) {
String modelNumber = getAdrenoVersionString(glRenderer);
if (modelNumber == null) {
// Not an Adreno GPU
return false;
}
// The current logic is to identify low-end SoCs based on a zero in the x0x place.
return modelNumber.charAt(1) == '0';
}
// This is a workaround for some broken devices that report
// only GLES 3.0 even though the GPU is an Adreno 4xx series part.
// An example of such a device is the Huawei Honor 5x with the
// Snapdragon 616 SoC (Adreno 405).
private static boolean isGLES31SnapdragonRenderer(String glRenderer) {
String modelNumber = getAdrenoVersionString(glRenderer);
if (modelNumber == null) {
// Not an Adreno GPU
return false;
}
// Snapdragon 4xx and higher support GLES 3.1
return modelNumber.charAt(0) >= '4';
}
public static void initialize(Context context, String glRenderer) {
if (initialized) {
return;
}
ActivityManager activityManager =
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
ConfigurationInfo configInfo = activityManager.getDeviceConfigurationInfo();
if (configInfo.reqGlEsVersion != ConfigurationInfo.GL_ES_VERSION_UNDEFINED) {
LimeLog.info("OpenGL ES version: "+configInfo.reqGlEsVersion);
isLowEndSnapdragon = isLowEndSnapdragonRenderer(glRenderer);
// Tegra K1 and later can do reference frame invalidation properly
if (configInfo.reqGlEsVersion >= 0x30000) {
LimeLog.info("Added omx.nvidia to AVC reference frame invalidation support list");
refFrameInvalidationAvcPrefixes.add("omx.nvidia");
LimeLog.info("Added omx.qcom to AVC reference frame invalidation support list");
refFrameInvalidationAvcPrefixes.add("omx.qcom");
LimeLog.info("Added omx.qcom to AVC reference frame invalidation support list");
refFrameInvalidationAvcPrefixes.add("omx.qcom");
// Prior to M, we were tricking the decoder into using baseline profile, which
// won't support RFI properly.
@@ -155,26 +242,49 @@ public class MediaCodecHelper {
LimeLog.info("Added omx.intel to AVC reference frame invalidation support list");
refFrameInvalidationAvcPrefixes.add("omx.intel");
}
}
// Qualcomm's early HEVC decoders break hard on our HEVC stream. The best check to
// tell the good from the bad decoders are the generation of Adreno GPU included:
// 3xx - bad
// 4xx - good
//
// Unfortunately, it's not that easy to get that information here, so I'll use an
// approximation by checking the GLES level (<= 3.0 is bad).
if (configInfo.reqGlEsVersion > 0x30000) {
// FIXME: We prefer reference frame invalidation support (which is only doable on AVC on
// older Qualcomm chips) vs. enabling HEVC by default. The user can override using the settings
// to force HEVC on.
//LimeLog.info("Added omx.qcom to supported HEVC decoders based on GLES 3.1+ support");
//whitelistedHevcDecoders.add("omx.qcom");
}
// Qualcomm's early HEVC decoders break hard on our HEVC stream. The best check to
// tell the good from the bad decoders are the generation of Adreno GPU included:
// 3xx - bad
// 4xx - good
//
// The "good" GPUs support GLES 3.1, but we can't just check that directly
// (see comment on isGLES31SnapdragonRenderer).
//
if (isGLES31SnapdragonRenderer(glRenderer)) {
// We prefer reference frame invalidation support (which is only doable on AVC on
// older Qualcomm chips) vs. enabling HEVC by default. The user can override using the settings
// to force HEVC on. If HDR or mobile data will be used, we'll override this and use
// HEVC anyway.
LimeLog.info("Added omx.qcom to deprioritized HEVC decoders based on GLES 3.1+ support");
deprioritizedHevcDecoders.add("omx.qcom");
}
else {
blacklistedDecoderPrefixes.add("OMX.qcom.video.decoder.hevc");
}
// Older MediaTek SoCs have issues with HEVC rendering but the newer chips with
// PowerVR GPUs have good HEVC support.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isPowerVR(glRenderer)) {
LimeLog.info("Added omx.mtk to HEVC decoders based on PowerVR GPU");
whitelistedHevcDecoders.add("omx.mtk");
// This SoC (MT8176 in GPD XD+) supports AVC RFI too, but the maxNumReferenceFrames setting
// required to make it work adds a huge amount of latency.
LimeLog.info("Added omx.mtk to RFI list for HEVC");
refFrameInvalidationHevcPrefixes.add("omx.mtk");
}
}
initialized = true;
}
private static boolean isDecoderInList(List<String> decoderList, String decoderName) {
if (!initialized) {
throw new IllegalStateException("MediaCodecHelper must be initialized before use");
}
for (String badPrefix : decoderList) {
if (decoderName.length() >= badPrefix.length()) {
String prefix = decoderName.substring(0, badPrefix.length());
@@ -190,20 +300,15 @@ public class MediaCodecHelper {
public static long getMonotonicMillis() {
return System.nanoTime() / 1000000L;
}
@TargetApi(Build.VERSION_CODES.KITKAT)
public static boolean decoderSupportsAdaptivePlayback(String decoderName) {
/*
FIXME: Intel's decoder on Nexus Player forces the high latency path if adaptive playback is enabled
so we'll keep it off for now, since we don't know whether other devices also do the same
if (isDecoderInList(whitelistedAdaptiveResolutionPrefixes, decoderName)) {
LimeLog.info("Adaptive playback supported (whitelist)");
return true;
}
public static boolean decoderSupportsAdaptivePlayback(MediaCodecInfo decoderInfo) {
// Possibly enable adaptive playback on KitKat and above
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (isDecoderInList(blacklistedAdaptivePlaybackPrefixes, decoderInfo.getName())) {
LimeLog.info("Decoder blacklisted for adaptive playback");
return false;
}
try {
if (decoderInfo.getCapabilitiesForType("video/avc").
isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback))
@@ -215,7 +320,7 @@ public class MediaCodecHelper {
} catch (Exception e) {
// Tolerate buggy codecs
}
}*/
}
return false;
}
@@ -236,7 +341,22 @@ public class MediaCodecHelper {
return isDecoderInList(baselineProfileHackPrefixes, decoderName);
}
public static boolean decoderSupportsRefFrameInvalidationAvc(String decoderName) {
public static boolean decoderBlacklistedFor49Fps(String decoderName) {
return isDecoderInList(blacklisted49FpsDecoderPrefixes, decoderName);
}
public static boolean decoderSupportsRefFrameInvalidationAvc(String decoderName, int videoHeight) {
// Reference frame invalidation is broken on low-end Snapdragon SoCs at 1080p.
if (videoHeight > 720 && isLowEndSnapdragon) {
return false;
}
// This device seems to crash constantly at 720p, so try disabling
// RFI to see if we can get that under control.
if (Build.DEVICE.equals("b3") || Build.DEVICE.equals("b5")) {
return false;
}
return isDecoderInList(refFrameInvalidationAvcPrefixes, decoderName);
}
@@ -244,7 +364,7 @@ public class MediaCodecHelper {
return isDecoderInList(refFrameInvalidationHevcPrefixes, decoderName);
}
public static boolean decoderIsWhitelistedForHevc(String decoderName) {
public static boolean decoderIsWhitelistedForHevc(String decoderName, boolean meteredData) {
// TODO: Shield Tablet K1/LTE?
//
// NVIDIA does partial HEVC acceleration on the Shield Tablet. I don't know
@@ -276,6 +396,15 @@ public class MediaCodecHelper {
return false;
}
// Some devices have HEVC decoders that we prefer not to use
// typically because it can't support reference frame invalidation.
// However, we will use it for HDR and for streaming over mobile networks
// since it works fine otherwise.
if (meteredData && isDecoderInList(deprioritizedHevcDecoders, decoderName)) {
LimeLog.info("Selected deprioritized decoder");
return true;
}
return isDecoderInList(whitelistedHevcDecoders, decoderName);
}
@@ -323,6 +452,10 @@ public class MediaCodecHelper {
// This is a different algorithm than the other findXXXDecoder functions,
// because we want to evaluate the decoders in our list's order
// rather than MediaCodecList's order
if (!initialized) {
throw new IllegalStateException("MediaCodecHelper must be initialized before use");
}
for (String preferredDecoder : preferredDecoders) {
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
@@ -3,8 +3,10 @@ package com.limelight.computers;
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.net.UnknownHostException;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
@@ -35,7 +37,7 @@ public class ComputerManagerService extends Service {
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 FAST_POLL_TIMEOUT = 1000;
private static final int OFFLINE_POLL_TRIES = 5;
private static final int INITIAL_POLL_TRIES = 2;
private static final int EMPTY_LIST_THRESHOLD = 3;
@@ -132,7 +134,7 @@ public class ComputerManagerService extends Service {
public void run() {
int offlineCount = 0;
while (!isInterrupted() && pollingActive) {
while (!isInterrupted() && pollingActive && tuple.thread == this) {
try {
// Only allow one request to the machine at a time
synchronized (tuple.networkLock) {
@@ -367,6 +369,10 @@ public class ComputerManagerService extends Service {
return true;
}
else {
if (!manuallyAdded) {
LimeLog.warning("Auto-discovered PC failed to respond: "+addr);
}
return false;
}
}
@@ -386,6 +392,7 @@ public class ComputerManagerService extends Service {
if (tuple.thread != null) {
// Interrupt the thread on this entry
tuple.thread.interrupt();
tuple.thread = null;
}
pollingTuples.remove(tuple);
break;
@@ -500,14 +507,26 @@ public class ComputerManagerService extends Service {
return ComputerDetails.Reachability.OFFLINE;
}
private static boolean isAddressLikelyLocal(String str) {
try {
// This will tend to be wrong for IPv6 but falling back to
// remote will be fine in that case. For IPv4, it should be
// pretty accurate due to NAT prevalence.
InetAddress addr = InetAddress.getByName(str);
return addr.isSiteLocalAddress() || addr.isLinkLocalAddress();
} catch (UnknownHostException e) {
e.printStackTrace();
return false;
}
}
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.localAddress.equals(details.remoteAddress)) {
reachability = ComputerDetails.Reachability.REMOTE;
reachability = isAddressLikelyLocal(details.localAddress) ?
ComputerDetails.Reachability.LOCAL : ComputerDetails.Reachability.REMOTE;
}
else {
// Do a TCP-level connection to the HTTP server to see if it's listening
@@ -553,7 +572,14 @@ public class ComputerManagerService extends Service {
return null;
}
if (polledDetails.remoteAddress.equals(reachableAddr)) {
// If both addresses are the same, guess whether we're local based on
// IP address heuristics.
if (reachableAddr.equals(polledDetails.localAddress) &&
reachableAddr.equals(polledDetails.remoteAddress)) {
polledDetails.reachability = isAddressLikelyLocal(reachableAddr) ?
ComputerDetails.Reachability.LOCAL : ComputerDetails.Reachability.REMOTE;
}
else if (polledDetails.remoteAddress.equals(reachableAddr)) {
polledDetails.reachability = ComputerDetails.Reachability.REMOTE;
}
else if (polledDetails.localAddress.equals(reachableAddr)) {
@@ -815,6 +841,7 @@ public class ComputerManagerService extends Service {
} while (waitPollingDelay());
}
};
thread.setName("App list polling thread for " + computer.localAddress);
thread.start();
}
@@ -1,7 +1,6 @@
package com.limelight.grid;
import android.app.Activity;
import android.graphics.BitmapFactory;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
@@ -46,13 +45,10 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
}
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()));
new DiskAssetLoader(context));
}
public void cancelQueuedOperations() {
@@ -1,7 +1,11 @@
package com.limelight.grid.assets;
import android.app.ActivityManager;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.ImageDecoder;
import android.os.Build;
import com.limelight.LimeLog;
import com.limelight.utils.CacheHelper;
@@ -13,45 +17,117 @@ import java.io.OutputStream;
public class DiskAssetLoader {
// 5 MB
private final long MAX_ASSET_SIZE = 5 * 1024 * 1024;
private static final long MAX_ASSET_SIZE = 5 * 1024 * 1024;
// Standard box art is 300x400
private static final int STANDARD_ASSET_WIDTH = 300;
private static final int STANDARD_ASSET_HEIGHT = 400;
private final boolean isLowRamDevice;
private final File cacheDir;
public DiskAssetLoader(File cacheDir) {
this.cacheDir = cacheDir;
public DiskAssetLoader(Context context) {
this.cacheDir = context.getCacheDir();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
this.isLowRamDevice =
((ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE)).isLowRamDevice();
}
else {
// Use conservative low RAM behavior on very old devices
this.isLowRamDevice = true;
}
}
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;
}
// https://developer.android.com/topic/performance/graphics/load-bitmap.html
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
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 (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// Calculates the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
if (bmp != null) {
LimeLog.info("Disk cache hit for tuple: "+tuple);
return inSampleSize;
}
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
File file = CacheHelper.openPath(false, cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
// Don't bother with anything if it doesn't exist
if (!file.exists()) {
return null;
}
// Make sure the cached asset doesn't exceed the maximum size
if (file.length() > MAX_ASSET_SIZE) {
LimeLog.warning("Removing cached tuple exceeding size threshold: "+tuple);
file.delete();
return null;
}
Bitmap bmp;
// For OSes prior to P, we have to use the ugly BitmapFactory API
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
// Lookup bounds of the downloaded image
BitmapFactory.Options decodeOnlyOptions = new BitmapFactory.Options();
decodeOnlyOptions.inJustDecodeBounds = true;
BitmapFactory.decodeFile(file.getAbsolutePath(), decodeOnlyOptions);
if (decodeOnlyOptions.outWidth <= 0 || decodeOnlyOptions.outHeight <= 0) {
// Dimensions set to -1 on error. Return value always null.
return null;
}
LimeLog.info("Tuple "+tuple+" has cached art of size: "+decodeOnlyOptions.outWidth+"x"+decodeOnlyOptions.outHeight);
// Load the image scaled to the appropriate size
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = calculateInSampleSize(decodeOnlyOptions,
STANDARD_ASSET_WIDTH / sampleSize,
STANDARD_ASSET_HEIGHT / sampleSize);
if (isLowRamDevice) {
options.inPreferredConfig = Bitmap.Config.RGB_565;
options.inDither = true;
}
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
options.inPreferredConfig = Bitmap.Config.HARDWARE;
}
bmp = BitmapFactory.decodeFile(file.getAbsolutePath(), options);
if (bmp != null) {
LimeLog.info("Tuple "+tuple+" decoded from disk cache with sample size: "+options.inSampleSize);
}
}
else {
// On P, we can get a bitmap back in one step with ImageDecoder
try {
bmp = ImageDecoder.decodeBitmap(ImageDecoder.createSource(file), new ImageDecoder.OnHeaderDecodedListener() {
@Override
public void onHeaderDecoded(ImageDecoder imageDecoder, ImageDecoder.ImageInfo imageInfo, ImageDecoder.Source source) {
imageDecoder.setTargetSize(STANDARD_ASSET_WIDTH, STANDARD_ASSET_HEIGHT);
if (isLowRamDevice) {
imageDecoder.setMemorySizePolicy(ImageDecoder.MEMORY_POLICY_LOW_RAM);
}
}
});
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
return bmp;
@@ -1,5 +1,12 @@
package com.limelight.preferences;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.concurrent.LinkedBlockingQueue;
import com.limelight.computers.ComputerManagerService;
@@ -17,6 +24,7 @@ import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.TextView;
@@ -39,36 +47,84 @@ public class AddComputerManually extends Activity {
}
};
private boolean isWrongSubnetSiteLocalAddress(String address) {
try {
InetAddress targetAddress = InetAddress.getByName(address);
if (!(targetAddress instanceof Inet4Address) || !targetAddress.isSiteLocalAddress()) {
return false;
}
// We have a site-local address. Look for a matching local interface.
for (NetworkInterface iface : Collections.list(NetworkInterface.getNetworkInterfaces())) {
for (InterfaceAddress addr : iface.getInterfaceAddresses()) {
if (!(addr.getAddress() instanceof Inet4Address) || !addr.getAddress().isSiteLocalAddress()) {
// Skip non-site-local or non-IPv4 addresses
continue;
}
byte[] targetAddrBytes = targetAddress.getAddress();
byte[] ifaceAddrBytes = addr.getAddress().getAddress();
// Compare prefix to ensure it's the same
boolean addressMatches = true;
for (int i = 0; i < addr.getNetworkPrefixLength(); i++) {
if ((ifaceAddrBytes[i / 8] & (1 << (i % 8))) != (targetAddrBytes[i / 8] & (1 << (i % 8)))) {
addressMatches = false;
break;
}
}
if (addressMatches) {
return false;
}
}
}
// Couldn't find a matching interface
return true;
} catch (SocketException e) {
e.printStackTrace();
return false;
} catch (UnknownHostException e) {
return false;
}
}
private void doAddPc(String host) {
String msg;
boolean finish = false;
boolean wrongSiteLocal = false;
boolean success;
SpinnerDialog dialog = SpinnerDialog.displayDialog(this, getResources().getString(R.string.title_add_pc),
getResources().getString(R.string.msg_add_pc), false);
if (!managerBinder.addComputerBlocking(host, true)){
msg = getResources().getString(R.string.addpc_fail);
}
else {
msg = getResources().getString(R.string.addpc_success);
finish = true;
success = managerBinder.addComputerBlocking(host, true);
if (!success){
wrongSiteLocal = isWrongSubnetSiteLocalAddress(host);
}
dialog.dismiss();
final boolean toastFinish = finish;
final String toastMsg = msg;
AddComputerManually.this.runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(AddComputerManually.this, toastMsg, Toast.LENGTH_LONG).show();
if (wrongSiteLocal) {
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title), getResources().getString(R.string.addpc_wrong_sitelocal), false);
}
else if (!success) {
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title), getResources().getString(R.string.addpc_fail), false);
}
else {
AddComputerManually.this.runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(AddComputerManually.this, getResources().getString(R.string.addpc_success), Toast.LENGTH_LONG).show();
if (toastFinish && !isFinishing()) {
if (!isFinishing()) {
// Close the activity
AddComputerManually.this.finish();
}
}
});
}
});
}
}
private void startAddThread() {
@@ -141,12 +197,7 @@ public class AddComputerManually extends Activity {
(keyEvent != null &&
keyEvent.getAction() == KeyEvent.ACTION_DOWN &&
keyEvent.getKeyCode() == KeyEvent.KEYCODE_ENTER)) {
if (hostText.getText().length() == 0) {
Toast.makeText(AddComputerManually.this, getResources().getString(R.string.addpc_enter_ip), Toast.LENGTH_LONG).show();
return true;
}
computersToAdd.add(hostText.getText().toString().trim());
return handleDoneEvent();
}
else if (actionId == EditorInfo.IME_ACTION_PREVIOUS) {
// This is how the Fire TV dismisses the keyboard
@@ -159,8 +210,28 @@ public class AddComputerManually extends Activity {
}
});
findViewById(R.id.addPcButton).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
handleDoneEvent();
}
});
// Bind to the ComputerManager service
bindService(new Intent(AddComputerManually.this,
ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE);
}
// Returns true if the event should be eaten
private boolean handleDoneEvent() {
String hostAddress = hostText.getText().toString().trim();
if (hostAddress.length() == 0) {
Toast.makeText(AddComputerManually.this, getResources().getString(R.string.addpc_enter_ip), Toast.LENGTH_LONG).show();
return true;
}
computersToAdd.add(hostAddress);
return false;
}
}
@@ -0,0 +1,41 @@
package com.limelight.preferences;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.os.Build;
import android.preference.DialogPreference;
import android.util.AttributeSet;
import android.widget.Toast;
import com.limelight.R;
import static com.limelight.binding.input.virtual_controller.VirtualControllerConfigurationLoader.OSC_PREFERENCE;
public class ConfirmDeleteOscPreference extends DialogPreference {
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public ConfirmDeleteOscPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public ConfirmDeleteOscPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public ConfirmDeleteOscPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public ConfirmDeleteOscPreference(Context context) {
super(context);
}
public void onClick(DialogInterface dialog, int which) {
if (which == DialogInterface.BUTTON_POSITIVE) {
getContext().getSharedPreferences(OSC_PREFERENCE, Context.MODE_PRIVATE).edit().clear().apply();
Toast.makeText(getContext(), R.string.toast_reset_osc_success, Toast.LENGTH_SHORT).show();
}
}
}
@@ -0,0 +1,37 @@
package com.limelight.preferences;
import android.content.Context;
import android.content.SharedPreferences;
public class GlPreferences {
private static final String PREF_NAME = "GlPreferences";
private static final String FINGERPRINT_PREF_STRING = "Fingerprint";
private static final String GL_RENDERER_PREF_STRING = "Renderer";
private SharedPreferences prefs;
public String glRenderer;
public String savedFingerprint;
private GlPreferences(SharedPreferences prefs) {
this.prefs = prefs;
}
public static GlPreferences readPreferences(Context context) {
SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, 0);
GlPreferences glPrefs = new GlPreferences(prefs);
glPrefs.glRenderer = prefs.getString(GL_RENDERER_PREF_STRING, "");
glPrefs.savedFingerprint = prefs.getString(FINGERPRINT_PREF_STRING, "");
return glPrefs;
}
public boolean writePreferences() {
return prefs.edit()
.putString(GL_RENDERER_PREF_STRING, glRenderer)
.putString(FINGERPRINT_PREF_STRING, savedFingerprint)
.commit();
}
}
@@ -8,7 +8,8 @@ import android.preference.PreferenceManager;
public class PreferenceConfiguration {
static final String RES_FPS_PREF_STRING = "list_resolution_fps";
static final String BITRATE_PREF_STRING = "seekbar_bitrate";
static final String BITRATE_PREF_STRING = "seekbar_bitrate_kbps";
private static final String BITRATE_PREF_OLD_STRING = "seekbar_bitrate";
private static final String STRETCH_PREF_STRING = "checkbox_stretch_video";
private static final String SOPS_PREF_STRING = "checkbox_enable_sops";
private static final String DISABLE_TOASTS_PREF_STRING = "checkbox_disable_warnings";
@@ -22,14 +23,21 @@ public class PreferenceConfiguration {
private static final String USB_DRIVER_PREF_SRING = "checkbox_usb_driver";
private static final String VIDEO_FORMAT_PREF_STRING = "video_format";
private static final String ONSCREEN_CONTROLLER_PREF_STRING = "checkbox_show_onscreen_controls";
private static final String BATTERY_SAVER_PREF_STRING = "checkbox_battery_saver";
private static final String ONLY_L3_R3_PREF_STRING = "checkbox_only_show_L3R3";
private static final String DISABLE_FRAME_DROP_PREF_STRING = "checkbox_disable_frame_drop";
private static final String ENABLE_HDR_PREF_STRING = "checkbox_enable_hdr";
private static final String ENABLE_PIP_PREF_STRING = "checkbox_enable_pip";
private static final String BIND_ALL_USB_STRING = "checkbox_usb_bind_all";
private static final String MOUSE_EMULATION_STRING = "checkbox_mouse_emulation";
private static final int BITRATE_DEFAULT_720_30 = 5;
private static final int BITRATE_DEFAULT_720_60 = 10;
private static final int BITRATE_DEFAULT_1080_30 = 10;
private static final int BITRATE_DEFAULT_1080_60 = 20;
private static final int BITRATE_DEFAULT_4K_30 = 40;
private static final int BITRATE_DEFAULT_4K_60 = 80;
private static final int BITRATE_DEFAULT_360_30 = 1000;
private static final int BITRATE_DEFAULT_360_60 = 2000;
private static final int BITRATE_DEFAULT_720_30 = 5000;
private static final int BITRATE_DEFAULT_720_60 = 10000;
private static final int BITRATE_DEFAULT_1080_30 = 10000;
private static final int BITRATE_DEFAULT_1080_60 = 20000;
private static final int BITRATE_DEFAULT_4K_30 = 40000;
private static final int BITRATE_DEFAULT_4K_60 = 80000;
private static final String DEFAULT_RES_FPS = "720p60";
private static final int DEFAULT_BITRATE = BITRATE_DEFAULT_720_60;
@@ -45,7 +53,13 @@ public class PreferenceConfiguration {
private static final boolean DEFAULT_USB_DRIVER = true;
private static final String DEFAULT_VIDEO_FORMAT = "auto";
private static final boolean ONSCREEN_CONTROLLER_DEFAULT = false;
private static final boolean ONLY_L3_R3_DEFAULT = false;
private static final boolean DEFAULT_BATTERY_SAVER = false;
private static final boolean DEFAULT_DISABLE_FRAME_DROP = false;
private static final boolean DEFAULT_ENABLE_HDR = false;
private static final boolean DEFAULT_ENABLE_PIP = false;
private static final boolean DEFAULT_BIND_ALL_USB = false;
private static final boolean DEFAULT_MOUSE_EMULATION = true;
public static final int FORCE_H265_ON = -1;
public static final int AUTOSELECT_H265 = 0;
@@ -59,10 +73,21 @@ public class PreferenceConfiguration {
public String language;
public boolean listMode, smallIconMode, multiController, enable51Surround, usbDriver;
public boolean onscreenController;
public boolean batterySaver;
public boolean onlyL3R3;
public boolean disableFrameDrop;
public boolean enableHdr;
public boolean enablePip;
public boolean bindAllUsb;
public boolean mouseEmulation;
public static int getDefaultBitrate(String resFpsString) {
if (resFpsString.equals("720p30")) {
if (resFpsString.equals("360p30")) {
return BITRATE_DEFAULT_360_30;
}
else if (resFpsString.equals("360p60")) {
return BITRATE_DEFAULT_360_60;
}
else if (resFpsString.equals("720p30")) {
return BITRATE_DEFAULT_720_30;
}
else if (resFpsString.equals("720p60")) {
@@ -131,12 +156,14 @@ public class PreferenceConfiguration {
}
public static void resetStreamingSettings(Context context) {
// We consider resolution, FPS, bitrate, and video format as "streaming settings" here
// We consider resolution, FPS, bitrate, HDR, and video format as "streaming settings" here
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
prefs.edit()
.remove(BITRATE_PREF_STRING)
.remove(BITRATE_PREF_OLD_STRING)
.remove(RES_FPS_PREF_STRING)
.remove(VIDEO_FORMAT_PREF_STRING)
.remove(ENABLE_HDR_PREF_STRING)
.apply();
}
@@ -144,9 +171,23 @@ public class PreferenceConfiguration {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
PreferenceConfiguration config = new PreferenceConfiguration();
config.bitrate = prefs.getInt(BITRATE_PREF_STRING, getDefaultBitrate(context));
config.bitrate = prefs.getInt(BITRATE_PREF_STRING, prefs.getInt(BITRATE_PREF_OLD_STRING, 0) * 1000);
if (config.bitrate == 0) {
config.bitrate = getDefaultBitrate(context);
}
String str = prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS);
if (str.equals("720p30")) {
if (str.equals("360p30")) {
config.width = 640;
config.height = 360;
config.fps = 30;
}
else if (str.equals("360p60")) {
config.width = 640;
config.height = 360;
config.fps = 60;
}
else if (str.equals("720p30")) {
config.width = 1280;
config.height = 720;
config.fps = 30;
@@ -200,7 +241,12 @@ public class PreferenceConfiguration {
config.enable51Surround = prefs.getBoolean(ENABLE_51_SURROUND_PREF_STRING, DEFAULT_ENABLE_51_SURROUND);
config.usbDriver = prefs.getBoolean(USB_DRIVER_PREF_SRING, DEFAULT_USB_DRIVER);
config.onscreenController = prefs.getBoolean(ONSCREEN_CONTROLLER_PREF_STRING, ONSCREEN_CONTROLLER_DEFAULT);
config.batterySaver = prefs.getBoolean(BATTERY_SAVER_PREF_STRING, DEFAULT_BATTERY_SAVER);
config.onlyL3R3 = prefs.getBoolean(ONLY_L3_R3_PREF_STRING, ONLY_L3_R3_DEFAULT);
config.disableFrameDrop = prefs.getBoolean(DISABLE_FRAME_DROP_PREF_STRING, DEFAULT_DISABLE_FRAME_DROP);
config.enableHdr = prefs.getBoolean(ENABLE_HDR_PREF_STRING, DEFAULT_ENABLE_HDR);
config.enablePip = prefs.getBoolean(ENABLE_PIP_PREF_STRING, DEFAULT_ENABLE_PIP);
config.bindAllUsb = prefs.getBoolean(BIND_ALL_USB_STRING, DEFAULT_BIND_ALL_USB);
config.mouseEmulation = prefs.getBoolean(MOUSE_EMULATION_STRING, DEFAULT_MOUSE_EMULATION);
return config;
}
@@ -16,7 +16,8 @@ import android.widget.TextView;
// Based on a Stack Overflow example: http://stackoverflow.com/questions/1974193/slider-on-my-preferencescreen
public class SeekBarPreference extends DialogPreference
{
private static final String SCHEMA_URL = "http://schemas.android.com/apk/res/android";
private static final String ANDROID_SCHEMA_URL = "http://schemas.android.com/apk/res/android";
private static final String SEEKBAR_SCHEMA_URL = "http://schemas.moonlight-stream.com/apk/res/seekbar";
private SeekBar seekBar;
private TextView valueText;
@@ -27,6 +28,7 @@ public class SeekBarPreference extends DialogPreference
private final int defaultValue;
private final int maxValue;
private final int minValue;
private final int stepSize;
private int currentValue;
public SeekBarPreference(Context context, AttributeSet attrs) {
@@ -34,27 +36,28 @@ public class SeekBarPreference extends DialogPreference
this.context = context;
// Read the message from XML
int dialogMessageId = attrs.getAttributeResourceValue(SCHEMA_URL, "dialogMessage", 0);
int dialogMessageId = attrs.getAttributeResourceValue(ANDROID_SCHEMA_URL, "dialogMessage", 0);
if (dialogMessageId == 0) {
dialogMessage = attrs.getAttributeValue(SCHEMA_URL, "dialogMessage");
dialogMessage = attrs.getAttributeValue(ANDROID_SCHEMA_URL, "dialogMessage");
}
else {
dialogMessage = context.getString(dialogMessageId);
}
// Get the suffix for the number displayed in the dialog
int suffixId = attrs.getAttributeResourceValue(SCHEMA_URL, "text", 0);
int suffixId = attrs.getAttributeResourceValue(ANDROID_SCHEMA_URL, "text", 0);
if (suffixId == 0) {
suffix = attrs.getAttributeValue(SCHEMA_URL, "text");
suffix = attrs.getAttributeValue(ANDROID_SCHEMA_URL, "text");
}
else {
suffix = context.getString(suffixId);
}
// Get default, min, and max seekbar values
defaultValue = attrs.getAttributeIntValue(SCHEMA_URL, "defaultValue", PreferenceConfiguration.getDefaultBitrate(context));
maxValue = attrs.getAttributeIntValue(SCHEMA_URL, "max", 100);
minValue = 1;
defaultValue = attrs.getAttributeIntValue(ANDROID_SCHEMA_URL, "defaultValue", PreferenceConfiguration.getDefaultBitrate(context));
maxValue = attrs.getAttributeIntValue(ANDROID_SCHEMA_URL, "max", 100);
minValue = attrs.getAttributeIntValue(SEEKBAR_SCHEMA_URL, "min", 1);
stepSize = attrs.getAttributeIntValue(SEEKBAR_SCHEMA_URL, "step", 1);
}
@Override
@@ -89,6 +92,12 @@ public class SeekBarPreference extends DialogPreference
return;
}
int roundedValue = ((value + (stepSize - 1))/stepSize)*stepSize;
if (roundedValue != value) {
seekBar.setProgress(roundedValue);
return;
}
String t = String.valueOf(value);
valueText.setText(suffix == null ? t : t.concat(suffix.length() > 1 ? " "+suffix : suffix));
}
@@ -2,16 +2,23 @@ package com.limelight.preferences;
import android.content.Intent;
import android.content.SharedPreferences;
import android.media.MediaCodecInfo;
import android.os.Build;
import android.os.Bundle;
import android.app.Activity;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.preference.PreferenceFragment;
import android.preference.PreferenceManager;
import android.preference.PreferenceScreen;
import android.util.Range;
import android.view.Display;
import com.limelight.LimeLog;
import com.limelight.PcView;
import com.limelight.R;
import com.limelight.binding.video.MediaCodecHelper;
import com.limelight.utils.UiHelper;
public class StreamSettings extends Activity {
@@ -50,6 +57,37 @@ public class StreamSettings extends Activity {
}
public static class SettingsFragment extends PreferenceFragment {
private static void removeResolution(ListPreference pref, String prefix) {
int matchingCount = 0;
// Count the number of matching entries we'll be removing
for (CharSequence seq : pref.getEntryValues()) {
if (seq.toString().startsWith(prefix)) {
matchingCount++;
}
}
// Create the new arrays
CharSequence[] entries = new CharSequence[pref.getEntries().length-matchingCount];
CharSequence[] entryValues = new CharSequence[pref.getEntryValues().length-matchingCount];
int outIndex = 0;
for (int i = 0; i < pref.getEntryValues().length; i++) {
if (pref.getEntryValues()[i].toString().startsWith(prefix)) {
// Skip matching prefixes
continue;
}
entries[outIndex] = pref.getEntries()[i];
entryValues[outIndex] = pref.getEntryValues()[i];
outIndex++;
}
// Update the preference with the new list
pref.setEntries(entries);
pref.setEntryValues(entryValues);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -65,6 +103,130 @@ public class StreamSettings extends Activity {
screen.removePreference(category);
}
// Remove PiP mode on devices pre-Oreo
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
PreferenceCategory category =
(PreferenceCategory) findPreference("category_basic_settings");
category.removePreference(findPreference("checkbox_enable_pip"));
}
// Hide non-supported resolution/FPS combinations
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Display display = getActivity().getWindowManager().getDefaultDisplay();
int maxSupportedResW = 0;
// Always allow resolutions that are smaller or equal to the active
// display resolution because decoders can report total non-sense to us.
// For example, a p201 device reports:
// AVC Decoder: OMX.amlogic.avc.decoder.awesome
// HEVC Decoder: OMX.amlogic.hevc.decoder.awesome
// AVC supported width range: 64 - 384
// HEVC supported width range: 64 - 544
for (Display.Mode candidate : display.getSupportedModes()) {
// Some devices report their dimensions in the portrait orientation
// where height > width. Normalize these to the conventional width > height
// arrangement before we process them.
int width = Math.max(candidate.getPhysicalWidth(), candidate.getPhysicalHeight());
int height = Math.min(candidate.getPhysicalWidth(), candidate.getPhysicalHeight());
if ((width >= 3840 || height >= 2160) && maxSupportedResW < 3840) {
maxSupportedResW = 3840;
}
else if ((width >= 1920 || height >= 1080) && maxSupportedResW < 1920) {
maxSupportedResW = 1920;
}
}
// This must be called to do runtime initialization before calling functions that evaluate
// decoder lists.
MediaCodecHelper.initialize(getContext(), GlPreferences.readPreferences(getContext()).glRenderer);
MediaCodecInfo avcDecoder = MediaCodecHelper.findProbableSafeDecoder("video/avc", -1);
MediaCodecInfo hevcDecoder = MediaCodecHelper.findProbableSafeDecoder("video/hevc", -1);
if (avcDecoder != null) {
Range<Integer> avcWidthRange = avcDecoder.getCapabilitiesForType("video/avc").getVideoCapabilities().getSupportedWidths();
LimeLog.info("AVC supported width range: "+avcWidthRange.getLower()+" - "+avcWidthRange.getUpper());
// If 720p is not reported as supported, ignore all results from this API
if (avcWidthRange.contains(1280)) {
if (avcWidthRange.contains(3840) && maxSupportedResW < 3840) {
maxSupportedResW = 3840;
}
else if (avcWidthRange.contains(1920) && maxSupportedResW < 1920) {
maxSupportedResW = 1920;
}
else if (maxSupportedResW < 1280) {
maxSupportedResW = 1280;
}
}
}
if (hevcDecoder != null) {
Range<Integer> hevcWidthRange = hevcDecoder.getCapabilitiesForType("video/hevc").getVideoCapabilities().getSupportedWidths();
LimeLog.info("HEVC supported width range: "+hevcWidthRange.getLower()+" - "+hevcWidthRange.getUpper());
// If 720p is not reported as supported, ignore all results from this API
if (hevcWidthRange.contains(1280)) {
if (hevcWidthRange.contains(3840) && maxSupportedResW < 3840) {
maxSupportedResW = 3840;
}
else if (hevcWidthRange.contains(1920) && maxSupportedResW < 1920) {
maxSupportedResW = 1920;
}
else if (maxSupportedResW < 1280) {
maxSupportedResW = 1280;
}
}
}
LimeLog.info("Maximum resolution slot: "+maxSupportedResW);
ListPreference resPref = (ListPreference) findPreference("list_resolution_fps");
if (maxSupportedResW != 0) {
if (maxSupportedResW < 3840) {
// 4K is unsupported
removeResolution(resPref, "4K");
}
if (maxSupportedResW < 1920) {
// 1080p is unsupported
removeResolution(resPref, "1080p");
}
// Never remove 720p
}
}
// Remove HDR preference for devices below Nougat
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
LimeLog.info("Excluding HDR toggle based on OS");
PreferenceCategory category =
(PreferenceCategory) findPreference("category_advanced_settings");
category.removePreference(findPreference("checkbox_enable_hdr"));
}
else {
Display display = getActivity().getWindowManager().getDefaultDisplay();
Display.HdrCapabilities hdrCaps = display.getHdrCapabilities();
// We must now ensure our display is compatible with HDR10
boolean foundHdr10 = false;
for (int hdrType : hdrCaps.getSupportedHdrTypes()) {
if (hdrType == Display.HdrCapabilities.HDR_TYPE_HDR10) {
foundHdr10 = true;
}
}
if (!foundHdr10) {
LimeLog.info("Excluding HDR toggle based on display capabilities");
PreferenceCategory category =
(PreferenceCategory) findPreference("category_advanced_settings");
category.removePreference(findPreference("checkbox_enable_hdr"));
}
}
// Add a listener to the FPS and resolution preference
// so the bitrate can be auto-adjusted
Preference pref = findPreference(PreferenceConfiguration.RES_FPS_PREF_STRING);
@@ -3,15 +3,21 @@ package com.limelight.ui;
import android.annotation.TargetApi;
import android.content.Context;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.SurfaceView;
public class StreamView extends SurfaceView {
private double desiredAspectRatio;
private InputCallbacks inputCallbacks;
public void setDesiredAspectRatio(double aspectRatio) {
this.desiredAspectRatio = aspectRatio;
}
public void setInputCallbacks(InputCallbacks callbacks) {
this.inputCallbacks = callbacks;
}
public StreamView(Context context) {
super(context);
}
@@ -52,4 +58,29 @@ public class StreamView extends SurfaceView {
setMeasuredDimension(measuredWidth, measuredHeight);
}
@Override
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
// This callbacks allows us to override dumb IME behavior like when
// Samsung's default keyboard consumes Shift+Space.
if (inputCallbacks != null) {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
if (inputCallbacks.handleKeyDown(event)) {
return true;
}
}
else if (event.getAction() == KeyEvent.ACTION_UP) {
if (inputCallbacks.handleKeyUp(event)) {
return true;
}
}
}
return super.onKeyPreIme(keyCode, event);
}
public interface InputCallbacks {
boolean handleKeyUp(KeyEvent event);
boolean handleKeyDown(KeyEvent event);
}
}
@@ -13,7 +13,7 @@ import java.io.OutputStream;
import java.io.Reader;
public class CacheHelper {
private static File openPath(boolean createPath, File root, String... path) {
public static File openPath(boolean createPath, File root, String... path) {
File f = root;
for (int i = 0; i < path.length; i++) {
String component = path[i];
@@ -13,18 +13,18 @@ public class Dialog implements Runnable {
private final String title;
private final String message;
private final Activity activity;
private final boolean endAfterDismiss;
private final Runnable runOnDismiss;
private AlertDialog alert;
private static final ArrayList<Dialog> rundownDialogs = new ArrayList<>();
private Dialog(Activity activity, String title, String message, boolean endAfterDismiss)
private Dialog(Activity activity, String title, String message, Runnable runOnDismiss)
{
this.activity = activity;
this.title = title;
this.message = message;
this.endAfterDismiss = endAfterDismiss;
this.runOnDismiss = runOnDismiss;
}
public static void closeDialogs()
@@ -40,9 +40,21 @@ public class Dialog implements Runnable {
}
}
public static void displayDialog(Activity activity, String title, String message, boolean endAfterDismiss)
public static void displayDialog(final Activity activity, String title, String message, final boolean endAfterDismiss)
{
activity.runOnUiThread(new Dialog(activity, title, message, endAfterDismiss));
activity.runOnUiThread(new Dialog(activity, title, message, new Runnable() {
@Override
public void run() {
if (endAfterDismiss) {
activity.finish();
}
}
}));
}
public static void displayDialog(Activity activity, String title, String message, Runnable runOnDismiss)
{
activity.runOnUiThread(new Dialog(activity, title, message, runOnDismiss));
}
@Override
@@ -65,9 +77,7 @@ public class Dialog implements Runnable {
alert.dismiss();
}
if (endAfterDismiss) {
activity.finish();
}
runOnDismiss.run();
}
});
alert.setButton(AlertDialog.BUTTON_NEUTRAL, activity.getResources().getText(R.string.help), new DialogInterface.OnClickListener() {
@@ -77,9 +87,7 @@ public class Dialog implements Runnable {
alert.dismiss();
}
if (endAfterDismiss) {
activity.finish();
}
runOnDismiss.run();
HelpLauncher.launchTroubleshooting(activity);
}
@@ -2,22 +2,42 @@ package com.limelight.utils;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Build;
import com.limelight.HelpActivity;
public class HelpLauncher {
private static boolean isKnownBrowser(Context context, Intent i) {
ResolveInfo resolvedActivity = context.getPackageManager().resolveActivity(i, PackageManager.MATCH_DEFAULT_ONLY);
if (resolvedActivity == null) {
// No browser
return false;
}
String name = resolvedActivity.activityInfo.name;
if (name == null) {
return false;
}
name = name.toLowerCase();
return name.contains("chrome") || name.contains("firefox");
}
private static void launchUrl(Context context, String url) {
// Try to launch the default browser
try {
// Fire TV devices will lie and say they do have a browser
// even though the OS just shows an error dialog if we
// try to use it.
if (!"Amazon".equalsIgnoreCase(Build.MANUFACTURER)) {
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse(url));
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse(url));
// Several Android TV devices will lie and say they do have a browser
// even though the OS just shows an error dialog if we try to use it. We need to
// be a bit more clever on these devices and detect if the browser is a legitimate
// browser or just a fake error message activity.
if (!context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK) ||
isKnownBrowser(context, i)) {
context.startActivity(i);
return;
}
@@ -28,6 +28,7 @@ public class ServerHelper {
intent.putExtra(Game.EXTRA_HOST, getCurrentAddressFromComputer(computer));
intent.putExtra(Game.EXTRA_APP_NAME, app.getAppName());
intent.putExtra(Game.EXTRA_APP_ID, app.getAppId());
intent.putExtra(Game.EXTRA_APP_HDR, app.isHdrSupported());
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
intent.putExtra(Game.EXTRA_STREAMING_REMOTE,
computer.reachability != ComputerDetails.Reachability.LOCAL);
@@ -5,13 +5,15 @@ import android.content.Context;
import android.content.Intent;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.graphics.Bitmap;
import android.graphics.drawable.Icon;
import android.os.Build;
import com.limelight.AppView;
import com.limelight.AppViewShortcutTrampoline;
import com.limelight.ShortcutTrampoline;
import com.limelight.R;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp;
import java.util.Collections;
import java.util.LinkedList;
@@ -80,8 +82,7 @@ public class ShortcutHelper {
public void reportShortcutUsed(String id) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
ShortcutInfo sinfo = getInfoForId(id);
if (sinfo != null) {
if (getInfoForId(id) != null) {
sm.reportShortcutUsed(id);
}
}
@@ -89,7 +90,7 @@ public class ShortcutHelper {
public void createAppViewShortcut(String id, String computerName, String computerUuid, boolean forceAdd) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
Intent i = new Intent(context, AppViewShortcutTrampoline.class);
Intent i = new Intent(context, ShortcutTrampoline.class);
i.putExtra(AppView.NAME_EXTRA, computerName);
i.putExtra(AppView.UUID_EXTRA, computerUuid);
i.setAction(Intent.ACTION_DEFAULT);
@@ -127,10 +128,42 @@ public class ShortcutHelper {
createAppViewShortcut(id, details.name, details.uuid.toString(), forceAdd);
}
@TargetApi(Build.VERSION_CODES.O)
public boolean createPinnedGameShortcut(String id, Bitmap iconBits, String computerName, String computerUuid, String appName, String appId) {
if (sm.isRequestPinShortcutSupported()) {
Icon appIcon;
Intent i = new Intent(context, ShortcutTrampoline.class);
i.putExtra(AppView.NAME_EXTRA, computerName);
i.putExtra(AppView.UUID_EXTRA, computerUuid);
i.putExtra(ShortcutTrampoline.APP_ID_EXTRA, appId);
i.setAction(Intent.ACTION_DEFAULT);
if (iconBits != null) {
appIcon = Icon.createWithAdaptiveBitmap(iconBits);
} else {
appIcon = Icon.createWithResource(context, R.mipmap.ic_pc_scut);
}
ShortcutInfo sInfo = new ShortcutInfo.Builder(context, id)
.setIntent(i)
.setShortLabel(appName + " (" + computerName + ")")
.setIcon(appIcon)
.build();
return sm.requestPinShortcut(sInfo, null);
} else {
return false;
}
}
public boolean createPinnedGameShortcut(String id, Bitmap iconBits, ComputerDetails cDetails, NvApp app) {
return createPinnedGameShortcut(id, iconBits, cDetails.name, cDetails.uuid.toString(), app.getAppName(), Integer.valueOf(app.getAppId()).toString());
}
public void disableShortcut(String id, CharSequence reason) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
ShortcutInfo sinfo = getInfoForId(id);
if (sinfo != null) {
if (getInfoForId(id) != null) {
sm.disableShortcuts(Collections.singletonList(id), reason);
}
}
@@ -5,8 +5,11 @@ import android.app.AlertDialog;
import android.app.UiModeManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.os.Build;
import android.view.View;
import android.view.WindowManager;
import com.limelight.R;
import com.limelight.preferences.PreferenceConfiguration;
@@ -56,6 +59,54 @@ public class UiHelper {
rootView.setPadding(horizontalPaddingPixels, verticalPaddingPixels,
horizontalPaddingPixels, verticalPaddingPixels);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// Allow this non-streaming activity to layout under notches.
//
// We should NOT do this for the Game activity unless
// the user specifically opts in, because it can obscure
// parts of the streaming surface.
activity.getWindow().getAttributes().layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
}
}
public static void showDecoderCrashDialog(Activity activity) {
final SharedPreferences prefs = activity.getSharedPreferences("DecoderTombstone", 0);
final int crashCount = prefs.getInt("CrashCount", 0);
int lastNotifiedCrashCount = prefs.getInt("LastNotifiedCrashCount", 0);
// Remember the last crash count we notified at, so we don't
// display the crash dialog every time the app is started until
// they stream again
if (crashCount != 0 && crashCount != lastNotifiedCrashCount) {
if (crashCount % 3 == 0) {
// At 3 consecutive crashes, we'll forcefully reset their settings
PreferenceConfiguration.resetStreamingSettings(activity);
Dialog.displayDialog(activity,
activity.getResources().getString(R.string.title_decoding_reset),
activity.getResources().getString(R.string.message_decoding_reset),
new Runnable() {
@Override
public void run() {
// Mark notification as acknowledged on dismissal
prefs.edit().putInt("LastNotifiedCrashCount", crashCount).apply();
}
});
}
else {
Dialog.displayDialog(activity,
activity.getResources().getString(R.string.title_decoding_error),
activity.getResources().getString(R.string.message_decoding_error),
new Runnable() {
@Override
public void run() {
// Mark notification as acknowledged on dismissal
prefs.edit().putInt("LastNotifiedCrashCount", crashCount).apply();
}
});
}
}
}
public static void displayQuitConfirmationDialog(Activity parent, final Runnable onYes, final Runnable onNo) {
+1 -7
View File
@@ -1,10 +1,4 @@
# Application.mk for Limelight
# Application.mk for Moonlight
# Our minimum version is Android 4.1
APP_PLATFORM := android-16
# Support all modern ABIs
APP_ABI := armeabi-v7a arm64-v8a x86 x86_64 mips mips64
# We want an optimized build
APP_OPTIM := release
+22 -18
View File
@@ -5,28 +5,32 @@ include $(call all-subdir-makefiles)
LOCAL_PATH := $(MY_LOCAL_PATH)
include $(CLEAR_VARS)
LOCAL_MODULE := evdev_reader
LOCAL_SRC_FILES := evdev_reader.c
LOCAL_LDLIBS := -llog
# Only build evdev_reader for the rooted APK flavor
ifeq (root,$(PRODUCT_FLAVOR))
include $(CLEAR_VARS)
LOCAL_MODULE := evdev_reader
LOCAL_SRC_FILES := evdev_reader.c
LOCAL_LDLIBS := -llog
# This next portion of the makefile is mostly copied from build-executable.mk but
# creates a binary with the libXXX.so form so the APK will install and drop
# the binary correctly.
# This next portion of the makefile is mostly copied from build-executable.mk but
# creates a binary with the libXXX.so form so the APK will install and drop
# the binary correctly.
LOCAL_BUILD_SCRIPT := BUILD_EXECUTABLE
LOCAL_MAKEFILE := $(local-makefile)
LOCAL_BUILD_SCRIPT := BUILD_EXECUTABLE
LOCAL_MAKEFILE := $(local-makefile)
$(call check-defined-LOCAL_MODULE,$(LOCAL_BUILD_SCRIPT))
$(call check-LOCAL_MODULE,$(LOCAL_MAKEFILE))
$(call check-LOCAL_MODULE_FILENAME)
$(call check-defined-LOCAL_MODULE,$(LOCAL_BUILD_SCRIPT))
$(call check-LOCAL_MODULE,$(LOCAL_MAKEFILE))
$(call check-LOCAL_MODULE_FILENAME)
# we are building target objects
my := TARGET_
# we are building target objects
my := TARGET_
$(call handle-module-filename,lib,$(TARGET_SONAME_EXTENSION))
$(call handle-module-built)
$(call handle-module-filename,lib,$(TARGET_SONAME_EXTENSION))
$(call handle-module-built)
LOCAL_MODULE_CLASS := EXECUTABLE
include $(BUILD_SYSTEM)/build-module.mk
endif
LOCAL_MODULE_CLASS := EXECUTABLE
include $(BUILD_SYSTEM)/build-module.mk
@@ -22,11 +22,11 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/manuallyAddPcText"
android:layout_toLeftOf="@+id/addPcButton"
android:layout_toStartOf="@+id/addPcButton"
android:layout_marginTop="25dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:ems="10"
android:singleLine="true"
android:inputType="textNoSuggestions"
@@ -34,5 +34,15 @@
<requestFocus />
</EditText>
<Button
android:id="@+id/addPcButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="25dp"
android:layout_below="@+id/manuallyAddPcText"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:text="@android:string/ok"/>
</RelativeLayout>
@@ -1,3 +1,4 @@
<!-- Portrait orientation only -->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
+3 -2
View File
@@ -61,7 +61,6 @@
<string name="lost_connection">Conexión perdida</string>
<!-- AppList activity -->
<string name="title_applist">Aplicaciones en</string>
<string name="applist_menu_resume">Reanudar sesión</string>
<string name="applist_menu_quit">Cerrar sesión</string>
<string name="applist_menu_quit_and_start">Cerrar juego actual e iniciar</string>
@@ -89,7 +88,7 @@
<string name="summary_resolution_list">Establecer unos valores demasiado altos puede causar lag o cierres inesperados</string>
<string name="title_seekbar_bitrate">Seleccionar bitrate de vídeo</string>
<string name="summary_seekbar_bitrate">Usa bitrate bajo para reducir "parpadeo". Incrementa el bitrate para mayor calidad de imagen.</string>
<string name="suffix_seekbar_bitrate">Mbps</string>
<string name="suffix_seekbar_bitrate">Kbps</string>
<string name="title_checkbox_stretch_video">Ajustar vídeo a pantalla completa</string>
<string name="title_checkbox_disable_warnings">Desactivar mensajes de advertencia</string>
<string name="summary_checkbox_disable_warnings">Desactivar mensajes de advertencia en pantalla durante la transmisión</string>
@@ -109,6 +108,8 @@
<string name="category_on_screen_controls_settings">Configuración de controles en pantalla</string>
<string name="title_checkbox_show_onscreen_controls">Mostrar controles en pantalla</string>
<string name="summary_checkbox_show_onscreen_controls">Muestra controles virtuales superpuestos en la pantalla táctil</string>
<string name="title_only_l3r3">Solo muestra L3 y R3</string>
<string name="summary_only_l3r3">Ocultar todo excepto L3 y R3</string>
<string name="category_ui_settings">Configuración de la interfaz</string>
<string name="title_language_list">Idioma</string>
+3 -2
View File
@@ -71,7 +71,6 @@
<string name="help">Aide</string>
<!-- AppList activity -->
<string name="title_applist">Applications sur</string>
<string name="applist_connect_msg">Connexion au PC…</string>
<string name="applist_menu_resume">Reprise de la session</string>
<string name="applist_menu_quit">Quitter la session</string>
@@ -100,7 +99,7 @@
<string name="summary_resolution_list">Le réglage de valeurs trop élevées pour votre appareil peut provoquer un retard ou un plantage</string>
<string name="title_seekbar_bitrate">Sélectionnez le bitrate vidéo à obtenir</string>
<string name="summary_seekbar_bitrate">Bitrate inférieur pour réduire la saccade. Augmentez le bitrate pour augmenter la qualité de l\'image.</string>
<string name="suffix_seekbar_bitrate">Mbps</string>
<string name="suffix_seekbar_bitrate">Kbps</string>
<string name="title_checkbox_stretch_video">Étirez la vidéo en plein écran</string>
<string name="title_checkbox_disable_warnings">Désactiver les messages d\'avertissement</string>
<string name="summary_checkbox_disable_warnings">Désactiver les messages d\'avertissement de connexion à l\'écran pendant le streaming</string>
@@ -120,6 +119,8 @@
<string name="category_on_screen_controls_settings">Paramètres des contrôles à l\'écran</string>
<string name="title_checkbox_show_onscreen_controls">Afficher les commandes à l\'écran</string>
<string name="summary_checkbox_show_onscreen_controls">Afficher la superposition du contrôleur virtuel sur l\'écran tactile</string>
<string name="title_only_l3r3">Montre seulement L3 et R3</string>
<string name="summary_only_l3r3">Cacher tout sauf L3 et R3</string>
<string name="category_ui_settings">Paramètres de l\'interface utilisateur</string>
<string name="title_language_list">Langue</string>
+78 -32
View File
@@ -1,6 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Shortcut strings -->
<string name="scut_deleted_pc">PC eliminato</string>
<string name="scut_not_paired">PC non accoppiato</string>
<!-- Help strings -->
<string name="help_loading_title">Visualizza assistenza</string>
<string name="help_loading_msg">Caricamento pagina di assistenza…</string>
<!-- PC view menu entries -->
<string name="pcview_menu_app_list">Lista applicazioni</string>
<string name="pcview_menu_pair_pc">Accoppia PC</string>
@@ -10,21 +17,22 @@
<!-- Pair messages -->
<string name="pairing">Accoppiamento…</string>
<string name="pair_pc_offline">PC offline</string>
<string name="pair_pc_offline">Il PC è offline</string>
<string name="pair_pc_ingame">PC con applicazione avviata. Devi chiudere l\'applicazione prima dell\'accoppiamento.</string>
<string name="pair_pairing_title">Accoppiamento</string>
<string name="pair_pairing_msg">Inserisci il seguente PIN sul PC:</string>
<string name="pair_incorrect_pin">PIN non corretto</string>
<string name="pair_fail">Accoppiamento fallito</string>
<string name="pair_already_in_progress">Accoppiamento già in corso</string>
<!-- WOL messages -->
<string name="wol_pc_online">PC già avviato</string>
<string name="wol_no_mac">Impossibile risvegliare il PC perchè GFE non ha inviato nessun indirizzo MAC</string>
<string name="wol_no_mac">Impossibile risvegliare il PC perché GFE non ha inviato nessun indirizzo MAC</string>
<string name="wol_waking_pc">Risveglio PC…</string>
<string name="wol_waking_msg">Il PC potrebbe impiegare qualche secondo per risvegliarsi.
Se non succede niente, assicurati che l\'opzione Wake-On-LAN sia configurata correttamente.
Se non succede niente, assicurati che l\'opzione Wake-On-LAN sia configurata correttamente.
</string>
<string name="wol_fail">Invio pacchetto Wake-On-LAN fallito</string>
<string name="wol_fail">Invio pacchetti Wake-On-LAN fallito</string>
<!-- Unpair messages -->
<string name="unpairing">Disaccoppiamento…</string>
@@ -37,33 +45,40 @@
<string name="error_manager_not_running">Il servizio ComputerManager non è avviato. Attendi qualche secondo o riavvia l\'applicazione.</string>
<string name="error_unknown_host">Risoluzione nome host fallita</string>
<string name="error_404">GFE ha ritornato un errore HTTP 404 error. Assicurati che il PC stia usando una GPU supportata.
Usare un software di remote-desktop può causare questo errore. Prova a riavviare il PC o a reinstallare GFE.
Usare un software di desktop remoto può causare questo errore. Prova a riavviare il PC o a reinstallare GFE.
</string>
<string name="title_decoding_error">Il decodificatore video ha smesso di funzionare</string>
<string name="message_decoding_error">Moonlight ha smesso di funzionare per colpa di un problema al decodificatore video di questo dispositivo. Prova a modificare le impostazioni di stream se il problema persiste.</string>
<string name="title_decoding_reset">Ripristino impostazioni video</string>
<string name="message_decoding_reset">Il decodificatore video del dispositivo continua a funzionare in modo anomalo con le impostazioni di streaming selezionate. Le impostazioni di streaming sono state ripristinate ai valori predefiniti.</string>
<string name="error_usb_prohibited">L\'accesso USB è vietato dall\'amministratore del dispositivo. Verifica le impostazioni Knox o MDM.</string>
<!-- Start application messages -->
<string name="conn_establishing_title">Connessione</string>
<string name="conn_establishing_msg">Connessione in corso</string>
<string name="conn_establishing_title">Connessione in corso</string>
<string name="conn_establishing_msg">Avvio connessione</string>
<string name="conn_metered">Attenzione: la rete attiva prevede costi aggiuntivi in base all\'utilizzo!</string>
<string name="conn_client_latency">Latenza frame media client-side:</string>
<string name="conn_client_latency_hw">latenza decoder hardware:</string>
<string name="conn_hardware_latency">Latenza decoder hardware media:</string>
<string name="conn_client_latency">Latenza decodifica fotogrammi media:</string>
<string name="conn_client_latency_hw">latenza decodificatore hardware:</string>
<string name="conn_hardware_latency">Latenza decodificatore hardware media:</string>
<string name="conn_starting">Avvio in corso…</string>
<string name="conn_error_title">Errore connessione</string>
<string name="conn_error_msg">Avvio fallito</string>
<string name="conn_terminated_title">Connessione terminata</string>
<string name="conn_terminated_title">Connessione interrotta</string>
<string name="conn_terminated_msg">La connessione è stata interrotta</string>
<!-- General strings -->
<string name="ip_hint">Indirizzo IP del PC</string>
<string name="searching_pc">Ricerca PC in corso…</string>
<string name="searching_pc">Ricerca di PC con GameStream avviato…\n\n
Assicurati che GameStream sia abilitato nelle impostazioni SHIELD di GeForce Experience.</string>
<string name="yes"></string>
<string name="no">No</string>
<string name="lost_connection">Connessione con il PC persa</string>
<string name="help">Assistenza</string>
<!-- AppList activity -->
<string name="title_applist">Applicazioni su</string>
<string name="applist_menu_resume">Riprendi Sessione</string>
<string name="applist_menu_quit">Chiudi Sessione</string>
<string name="applist_connect_msg">Connessione al PC in corso…</string>
<string name="applist_menu_resume">Riprendi sessione</string>
<string name="applist_menu_quit">Chiudi sessione</string>
<string name="applist_menu_quit_and_start">Chiudi sessione corrente e avvia</string>
<string name="applist_menu_cancel">Annulla</string>
<string name="applist_refresh_title">Lista applicazioni</string>
@@ -76,43 +91,74 @@
<string name="applist_quit_confirmation">Sei sicuro di voler chiudere l\'applicazione avviata? Tutti i dati non salvati saranno persi.</string>
<!-- Add computer manually activity -->
<string name="title_add_pc">Aggiungi PC Manualmente</string>
<string name="title_add_pc">Aggiungi PC manualmente</string>
<string name="msg_add_pc">Connessione al PC in corso…</string>
<string name="addpc_fail">Impossibile connettersi al PC. Assicurati che il firewall del PC sia configurato correttamente.</string>
<string name="addpc_fail">Impossibile connettersi al PC specificato. Assicurati che le porte nel firewall del PC siano configurate correttamente.</string>
<string name="addpc_success">PC aggiunto con successo</string>
<string name="addpc_unknown_host">Impossibile risovere l\'indirizzo del PC. Assicurati di aver scritto correttamente l\'indirizzo.</string>
<string name="addpc_enter_ip">Devi inserire un indirizzo IP</string>
<string name="addpc_wrong_sitelocal">Quell\'indirizzo non sembra corretto. È necessario utilizzare l\'indirizzo IP pubblico del router per lo streaming su Internet.</string>
<!-- Preferences -->
<string name="category_basic_settings">Impostazioni Base</string>
<string name="category_basic_settings">Impostazioni generali</string>
<string name="title_resolution_list">Risoluzione e FPS</string>
<string name="summary_resolution_list">Valori troppo elevati possono causare lag o crash</string>
<string name="title_seekbar_bitrate">Bitrate video</string>
<string name="summary_seekbar_bitrate">Abbassa il bitrate per ridurre lo stuttering; alza il bitrate per aumenteare la qualità dell\'immagine</string>
<string name="suffix_seekbar_bitrate">Mbps</string>
<string name="title_checkbox_stretch_video">Forza video in full-screen</string>
<string name="title_seekbar_bitrate">Velocità di trasmissione video</string>
<string name="summary_seekbar_bitrate">Abbassa la velocità di trasmissione per ridurre lo stuttering; alzala per migliorare la qualità dell\'immagine</string>
<string name="suffix_seekbar_bitrate">Kbps</string>
<string name="title_checkbox_stretch_video">Forza video a schermo intero</string>
<string name="title_checkbox_disable_warnings">Disabilita messaggi di warning</string>
<string name="summary_checkbox_disable_warnings">Disabilita i messaggi di warning sullo schermo durante lo streaming</string>
<string name="title_checkbox_enable_pip">Abilita modalità spettatore Picture-in-Picture</string>
<string name="summary_checkbox_enable_pip">Permette di osservare (ma non di controllare) la stream in multitasking</string>
<string name="category_gamepad_settings">Impostazioni Gamepad</string>
<string name="title_checkbox_multi_controller">Supporto controller multipli</string>
<string name="summary_checkbox_multi_controller">Quando disabilitato, tutti i controllers appaiono come uno solo</string>
<string name="title_seekbar_deadzone">Aggiusta deadzone degli stick analogici</string>
<string name="category_audio_settings">Impostazioni audio</string>
<string name="title_checkbox_51_surround">Abilita l\'audio 5.1 surround</string>
<string name="summary_checkbox_51_surround">Se riscontri problemi, disabilitalo. Richiede GFE 2.7 o versioni sucessive.</string>
<string name="category_gamepad_settings">Impostazioni controller</string>
<string name="title_checkbox_multi_controller">Supporto a più controller</string>
<string name="summary_checkbox_multi_controller">Quando disabilitato, tutti i controller appaiono come uno solo</string>
<string name="title_seekbar_deadzone">Regola i punti morti degli stick analogici</string>
<string name="suffix_seekbar_deadzone">%</string>
<string name="title_checkbox_xb1_driver">Driver del controller Xbox 360/One</string>
<string name="summary_checkbox_xb1_driver">Abilita un driver USB integrato per dispositivi senza supporto al controller Xbox nativo</string>
<string name="title_checkbox_usb_bind_all">Sovrascrivi il supporto ai controller su Android</string>
<string name="summary_checkbox_usb_bind_all">Forza i driver USB di Moonlight di assumere il controllo su tutti i controller Xbox supportati</string>
<string name="title_checkbox_mouse_emulation">Emulazione del mouse tramite controller</string>
<string name="summary_checkbox_mouse_emulation">Tenendo premuto il pulsante Start, il controller passerà alla modalità mouse</string>
<string name="category_on_screen_controls_settings">Impostazioni dei controlli a schermo</string>
<string name="title_checkbox_show_onscreen_controls">Mostra controlli a schermo</string>
<string name="summary_checkbox_show_onscreen_controls">Mostra l\'overlay virtuale del controller su schermo</string>
<string name="title_only_l3r3">Mostra solo L3 e R3</string>
<string name="summary_only_l3r3">Nasconde tutti i pulsanti virtuali tranne L3 e R3</string>
<string name="title_reset_osc">Ripristina il layout personalizzato dei controlli a schermo</string>
<string name="summary_reset_osc">Ripristina tutti i controlli a schermo nelle loro posizioni e dimensioni predefinite</string>
<string name="dialog_title_reset_osc">Ripristino del layout</string>
<string name="dialog_text_reset_osc">Sei sicuro di voler ripristinare ai valori predefiniti i layout dei controlli a schermo?</string>
<string name="toast_reset_osc_success">I controlli a schermo sono stati ripristinati</string>
<string name="category_ui_settings">Impostazioni Interfaccia</string>
<string name="category_ui_settings">Impostazioni dell\'interfaccia</string>
<string name="title_language_list">Lingua</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="summary_checkbox_list_mode">Visualizza le applicazioni e i PC in una lista invece di una griglia</string>
<string name="title_checkbox_small_icon_mode">Usa icone piccole</string>
<string name="summary_checkbox_small_icon_mode">Usa icone piccole nella vista a griglia per avere più oggetti sullo schermo</string>
<string name="summary_checkbox_small_icon_mode">Usa icone piccole nella griglia per avere più oggetti a schermo</string>
<string name="category_host_settings">Impostazioni Host</string>
<string name="category_host_settings">Impostazioni del PC host</string>
<string name="title_checkbox_enable_sops">Ottimizza le impostazioni dei giochi</string>
<string name="summary_checkbox_enable_sops">Permetti a GFE di modificare le impostazioni dei giochi per uno streaming ottimale</string>
<string name="title_checkbox_host_audio">Riproduci audio sul PC</string>
<string name="summary_checkbox_host_audio">Riproduci l\'audio sul computer e su questo dispositivo</string>
<string name="summary_checkbox_host_audio">Riproduce l\'audio sul PC e su questo dispositivo</string>
<string name="category_advanced_settings">Impostazioni Avanzate</string>
<string name="category_advanced_settings">Impostazioni avanzate</string>
<string name="title_disable_frame_drop">Non saltare i fotogrammi</string>
<string name="summary_disable_frame_drop">Potrebbe ridurre il micro-stuttering su alcuni dispositivi, ma può aumentare la latenza</string>
<string name="title_video_format">Modifica impostazioni H.265</string>
<string name="summary_video_format">H.265 riduce i requisiti di larghezza di banda video ma richiede un dispositivo molto recente</string>
<string name="title_enable_hdr">Abilita HDR (sperimentale)</string>
<string name="summary_enable_hdr">Utilizza l\'HDR quando il gioco e la scheda video del PC lo supportano. L\'HDR richiede una scheda video serie GTX 1000 o sucessive.</string>
</resources>
+1 -2
View File
@@ -57,7 +57,6 @@
<string name="lost_connection">コンピュータとの接続が失われました</string>
<!-- AppList activity -->
<string name="title_applist">ゲーム</string>
<string name="applist_menu_resume">セッションを続ける</string>
<string name="applist_menu_quit">セッションを終了する</string>
<string name="applist_menu_quit_and_start">現在のゲームを終了して新しいゲームを始める</string>
@@ -85,7 +84,7 @@
<string name="summary_resolution_list">品質が高いほどラグとクラッシュが発生しやすくなります</string>
<string name="title_seekbar_bitrate">映像のビットレート</string>
<string name="summary_seekbar_bitrate">ビットレートを低くすればカクつきが抑制され、高くすれば画質が向上します</string>
<string name="suffix_seekbar_bitrate">Mbps</string>
<string name="suffix_seekbar_bitrate">Kbps</string>
<string name="title_checkbox_stretch_video">映像を全画面に拡大</string>
<string name="title_checkbox_disable_warnings">警告を無効化</string>
<string name="summary_checkbox_disable_warnings">ストリーミング中に画面に警告メッセージを表示しない</string>
+1 -2
View File
@@ -71,7 +71,6 @@
<string name="help">도움말</string>
<!-- AppList activity -->
<string name="title_applist">앱 사용 가능</string>
<string name="applist_connect_msg">PC에 연결중…</string>
<string name="applist_menu_resume">세션 계속</string>
<string name="applist_menu_quit">세션 종료</string>
@@ -100,7 +99,7 @@
<string name="summary_resolution_list">세팅 값이 자신의 PC 성능보다 너무 높으면 렉이나 깨짐을 유발할 수 있습니다.</string>
<string name="title_seekbar_bitrate">비트레이트 타겟 지정</string>
<string name="summary_seekbar_bitrate">낮은 비트레이트는 끊김을 줄이고, 높은 비트레이트는 품질을 높입니다.</string>
<string name="suffix_seekbar_bitrate">Mbps</string>
<string name="suffix_seekbar_bitrate">Kbps</string>
<string name="title_checkbox_stretch_video">전체 화면으로 렌더링 스크린 늘이기</string>
<string name="title_checkbox_disable_warnings">경고 메세지 끄기</string>
<string name="summary_checkbox_disable_warnings">화면 상의 연결 경고 메세지를 스트리밍 중에 비활성화합니다.</string>
-9
View File
@@ -1,14 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="resolution_names">
<item>720p 30 FPS</item>
<item>720p 60 FPS</item>
<item>1080p 30 FPS</item>
<item>1080p 60 FPS</item>
<item>4K 30 FPS</item>
<item>4K 60 FPS</item>
</string-array>
<string-array name="decoder_names">
<item>Selecteer Decoder Automatisch</item>
<item>Forceer Software Decoderen</item>
+1 -2
View File
@@ -61,7 +61,6 @@
<string name="lost_connection">Verbinding met PC verloren</string>
<!-- AppList activity -->
<string name="title_applist">Apps op</string>
<string name="applist_menu_resume">Hervat Sessie</string>
<string name="applist_menu_quit">Stop Sessie</string>
<string name="applist_menu_quit_and_start">Stop Huidige Spel en Start</string>
@@ -89,7 +88,7 @@
<string name="summary_resolution_list">Te hoge instellingen kunnen crashes en haperingen veroorzaken.</string>
<string name="title_seekbar_bitrate">Selecteer doel video bitsnelheid</string>
<string name="summary_seekbar_bitrate">Verlaag bitsnelheid om haperingen te verminderen. Verhoog de bitsnelheid voor een betere videokwaliteit.</string>
<string name="suffix_seekbar_bitrate">Mbps</string>
<string name="suffix_seekbar_bitrate">Kbps</string>
<string name="title_checkbox_stretch_video">Rek video uit tot volledig scherm</string>
<string name="title_checkbox_disable_warnings">Verberg waarschuwingsberichten</string>
<string name="summary_checkbox_disable_warnings">Verberg on-screen verbindingswaarschuwingen tijdens het streamen</string>
+60 -27
View File
@@ -28,7 +28,7 @@
<!-- Unpair messages -->
<string name="unpairing">Разрыв пары…</string>
<string name="unpair_success">Разрыв пары закончился успешно.</string>
<string name="unpair_success">Разрыв пары закончился успешно</string>
<string name="unpair_fail">Разрыв пары не удался</string>
<string name="unpair_error">Устройство не было спарено</string>
@@ -36,14 +36,14 @@
<string name="error_pc_offline">Компьютер выключен или находится не в сети</string>
<string name="error_manager_not_running">Сервис ComputerManager не запущен. Пожалуйста, подождите несколько секунд или перезапустите приложение.</string>
<string name="error_unknown_host">Не удалось найти хост</string>
<string name="error_404">GFE вернул ошибку HTTP 404. Убедитесь что ваш PC is испольщует поддерживаемый GPU.
Использование программ для удалённого доступа также можнт вызывать эту ошибку. Попробуйте перезагрузить компьютер или переустановить GFE.
<string name="error_404">GFE вернул ошибку HTTP 404. Убедитесь что Ваш PC использует поддерживаемый GPU.
Использование программ для удалённого доступа также может вызывать эту ошибку. Попробуйте перезагрузить компьютер или переустановить GFE.
</string>
<!-- Start application messages -->
<string name="conn_establishing_title">Создание соединения.</string>
<string name="conn_establishing_title">Создание соединения</string>
<string name="conn_establishing_msg">Подключение</string>
<string name="conn_metered">Внимание: Происходит измерение вашего сетевого соединения!</string>
<string name="conn_metered">Внимание: Происходит измерение Вашего сетевого соединения!</string>
<string name="conn_client_latency">Средняя задержка декодирования кадра: </string>
<string name="conn_client_latency_hw">задержка аппаратного декодирования:</string>
<string name="conn_hardware_latency">Средняя задержка апаратного декодирования:</string>
@@ -54,14 +54,14 @@
<string name="conn_terminated_msg">Подключение было прервано</string>
<!-- General strings -->
<string name="ip_hint">IP адресс компьютера с GeForce</string>
<string name="searching_pc">Поиск компьютеров</string>
<string name="ip_hint">IP-адрес компьютера с GeForce</string>
<string name="searching_pc">Поиск компьютеров с запущенным GameStream…\n\n
Убедитесь что GameStream включен в настройках GeForce Experience в разделе SHIELD.</string>
<string name="yes">Да</string>
<string name="no">Нет</string>
<string name="lost_connection">Потеряно соединение с PC</string>
<!-- AppList activity -->
<string name="title_applist">Приложения на</string>
<string name="applist_menu_resume">Возобновить сессию</string>
<string name="applist_menu_quit">Выйти из сессии</string>
<string name="applist_menu_quit_and_start">Выйти из текущей игры и запустить</string>
@@ -79,49 +79,82 @@
<string name="title_add_pc">Добавление PC вручную</string>
<string name="msg_add_pc">Соединение с PC…</string>
<string name="addpc_fail">Не удалось подключиться к выбранному компьютеру. Удостоверьтесь, что необходимые порты разрешены в настройках брандмауэра.</string>
<string name="addpc_success">Компьютер добавлен успешно.</string>
<string name="addpc_unknown_host">Не удалось найти PC по указанному адресу. Убедитесь, что вы не совершили ошибок во время его написания.</string>
<string name="addpc_success">Компьютер добавлен успешно</string>
<string name="addpc_unknown_host">Не удалось найти PC по указанному адресу. Убедитесь, что Вы не совершили ошибок во время его написания.</string>
<string name="addpc_enter_ip">Вы должны ввести IP адрес</string>
<!-- Preferences -->
<string name="category_basic_settings">Базовые Настройки</string>
<string name="title_resolution_list">Выберите разрешение и частоту кадров.</string>
<string name="summary_resolution_list">Выбор слишком высокого значеня для своего устройства может вызвать тормоза или вылеты.</string>
<string name="title_seekbar_bitrate">Выберите битрейт видео.</string>
<string name="category_basic_settings">Общие Настройки</string>
<string name="title_resolution_list">Выберите разрешение и частоту кадров</string>
<string name="summary_resolution_list">Выбор слишком высокого значеня для своего устройства может вызвать тормоза или вылеты</string>
<string name="title_seekbar_bitrate">Выберите битрейт видео</string>
<string name="summary_seekbar_bitrate">Низкий битрейт уменьшит зависания. Увеличение битрейта улучшит качество изображения.</string>
<string name="suffix_seekbar_bitrate">Mbps</string>
<string name="suffix_seekbar_bitrate">Kbps</string>
<string name="title_checkbox_stretch_video">Растягивать видео на весь экран</string>
<string name="title_checkbox_disable_warnings">Отключить сообщения с предупреждениями</string>
<string name="summary_checkbox_disable_warnings">Выключить экранные предупреждения о соединении во время стрима.</string>
<string name="summary_checkbox_disable_warnings">Выключить экранные предупреждения о соединении во время трансляции</string>
<string name="category_audio_settings">Аудио Настройки</string>
<string name="title_checkbox_51_surround">Включить объёмный звук 5.1</string>
<string name="summary_checkbox_51_surround">Отключите, если появляются аудио проблемы. Требуется GFE 2.7 или выше.</string>
<string name="category_gamepad_settings">Настройки Гемпада</string>
<string name="category_gamepad_settings">Настройки Геймпада</string>
<string name="title_checkbox_multi_controller">Поддержка нескольких контроллеров</string>
<string name="summary_checkbox_multi_controller">Когда отключена, все контроллеры определяются как один. </string>
<string name="title_seekbar_deadzone">Регулировать мертвую зону аналогового стика.</string>
<string name="summary_checkbox_multi_controller">Когда отключена, все контроллеры определяются как один</string>
<string name="title_seekbar_deadzone">Регулировать мертвую зону аналогового стика</string>
<string name="suffix_seekbar_deadzone">%</string>
<string name="title_checkbox_xb1_driver">Драйвер контроллера от Xbox One</string>
<string name="summary_checkbox_xb1_driver">Включить встроенный USB драйвер для устройств без встроенной поддержки контроллера от Xbox One.</string>
<string name="title_checkbox_xb1_driver">Драйвер контроллеров Xbox 360/One</string>
<string name="summary_checkbox_xb1_driver">Включить встроенный USB драйвер для устройств без собственной поддержки контроллеров Xbox</string>
<string name="category_ui_settings">Настройки Интерфейса</string>
<string name="title_language_list">Язык</string>
<string name="summary_language_list">Язык, который будет использоваться в Moonlight</string>
<string name="title_checkbox_list_mode">Использовать списки вместо сеток.</string>
<string name="summary_checkbox_list_mode">Выводить приложения и компьютеры списком, вместо использования сетки.</string>
<string name="title_checkbox_list_mode">Использовать списки вместо сеток</string>
<string name="summary_checkbox_list_mode">Выводить приложения и компьютеры списком, вместо использования сетки</string>
<string name="title_checkbox_small_icon_mode">Использовать маленькие иконки</string>
<string name="summary_checkbox_small_icon_mode">Использовать маленькие иконки в сетки для увеличения числа элементов, отображаемых на экране.</string>
<string name="summary_checkbox_small_icon_mode">Использовать маленькие иконки в сетке для отображения большего числа элементов на экране</string>
<string name="category_host_settings">Настройки Хоста</string>
<string name="title_checkbox_enable_sops">Оптимизировать игровые настройки</string>
<string name="summary_checkbox_enable_sops">Разрешить GFE изменять настройки игр для оптимальной потоковой передачи</string>
<string name="summary_checkbox_enable_sops">Разрешить GFE изменять настройки игр для оптимальной трансляции</string>
<string name="title_checkbox_host_audio">Проигрывать звук на PC</string>
<string name="summary_checkbox_host_audio">Проигрывать звук на компьютере и текущем устройстве.</string>
<string name="summary_checkbox_host_audio">Проигрывать звук на компьютере и текущем устройстве</string>
<string name="category_advanced_settings">Расширенные Настройки</string>
<string name="title_video_format">Изменить настройки H.265</string>
<string name="summary_video_format">H.265 снижает требования к пропускной способности, но требует очень свежих устройств.</string>
<string name="summary_video_format">H.265 снижает требования к пропускной способности, но требует очень нового устройства</string>
<string name="category_on_screen_controls_settings">Настройки Экранных Кнопок</string>
<string name="title_checkbox_show_onscreen_controls">Показывать экранные кнопки</string>
<string name="summary_checkbox_show_onscreen_controls">Отображать оверлей виртуального контроллера на сенсорном экране</string>
<string name="title_only_l3r3">Показывать только L3 и R3</string>
<string name="summary_only_l3r3">Скрывать все экранные кнопки кроме L3 и R3</string>
<string name="scut_deleted_pc">PC удален</string>
<string name="scut_not_paired">PC не сопряжен</string>
<string name="help_loading_title">Просмотр Помощи</string>
<string name="help_loading_msg">Загрузка страницы помощи…</string>
<string name="pair_already_in_progress">Сопряжение уже в процессе</string>
<string name="help">Помощь</string>
<string name="applist_connect_msg">Подключение к PC…</string>
<string name="title_decoding_error">Сбой Видео Декодера</string>
<string name="message_decoding_error">Произошел сбой Moonlight из-за проблем с видео декодером данного устройства. Попробуйте изменить настройки трансляции если сбои будут продолжаться.</string>
<string name="title_decoding_reset">Видео Настройки Сброшены</string>
<string name="message_decoding_reset">Видео декодер Вашего устройства давал сбои с выбранными настройками. Настройки трансляции были сброшены до значений по умолчанию.</string>
<string name="error_usb_prohibited">USB доступ запрещен администратором устройства. Проверьте настройки Knox или MDM.</string>
<string name="addpc_wrong_sitelocal">Адрес указан неверно. Вы должны ввести публичный IP-адрес Вашего роутера для передачи через интернет.</string>
<string name="title_checkbox_enable_pip">Включить просмотр в режиме \"Картинка в картинке\"</string>
<string name="summary_checkbox_enable_pip">Позволяет просматривать трансляцию (но не управлять ей) во время работы в других приложениях</string>
<string name="title_checkbox_usb_bind_all">Переопределить поддержку контроллеров Android</string>
<string name="summary_checkbox_usb_bind_all">Заставляет USB драйвер Moonlight взять на себя работу со всеми поддерживаемыми Xbox геймпадами</string>
<string name="title_checkbox_mouse_emulation">Эмуляция мыши на геймпаде</string>
<string name="summary_checkbox_mouse_emulation">Долгое нажатие кнопки Start переключит геймпад в режим мыши</string>
<string name="title_reset_osc">Сбросить схему расположения экранных кнопок</string>
<string name="summary_reset_osc">Возвращает все экранные элементы управления к их расположениям по умолчанию</string>
<string name="dialog_title_reset_osc">Сбросить Схему</string>
<string name="dialog_text_reset_osc">Вы уверены что хотите удалить сохранненную схему расположения кнопок?</string>
<string name="toast_reset_osc_success">Экранные элементы управления возвращены к положениям по умолчанию</string>
<string name="title_disable_frame_drop">Никогда не пропускать кадры</string>
<string name="summary_disable_frame_drop">Может уменьшить микрозависания на некоторых устройствах, но также увеличить задержку</string>
<string name="title_enable_hdr">Включить HDR (Экспериментально)</string>
<string name="summary_enable_hdr">Транслировать в HDR если игра и GPU компьютера поддерживают это. HDR требует видеокарты GTX 1000 серии или более новой.</string>
</resources>
+2
View File
@@ -7,6 +7,8 @@
-->
<style name="AppBaseTheme" parent="android:Theme.Material">
<!-- API 21 theme customizations can go here. -->
<item name="android:statusBarColor">#212121</item>
<item name="android:navigationBarColor">#212121</item>
</style>
</resources>
+1 -2
View File
@@ -71,7 +71,6 @@
<string name="help">帮助</string>
<!-- AppList activity -->
<string name="title_applist">Apps on</string>
<string name="applist_connect_msg"> 连接到电脑中…… </string>
<string name="applist_menu_resume"> 恢复串流 </string>
<string name="applist_menu_quit"> 退出串流 </string>
@@ -100,7 +99,7 @@
<string name="summary_resolution_list"> 过高的设定会引起串流卡顿甚至软件闪退 </string>
<string name="title_seekbar_bitrate"> 选择目标视频码率 </string>
<string name="summary_seekbar_bitrate"> 低码率减少卡顿,高码率提高画质 </string>
<string name="suffix_seekbar_bitrate">Mbps</string>
<string name="suffix_seekbar_bitrate">Kbps</string>
<string name="title_checkbox_stretch_video"> 将画面拉伸至全屏 </string>
<string name="title_checkbox_disable_warnings"> 禁用错误提示 </string>
<string name="summary_checkbox_disable_warnings"> 串流过程中禁用连接错误提示 </string>
+1 -2
View File
@@ -71,7 +71,6 @@
<string name="help">幫助</string>
<!-- AppList activity -->
<string name="title_applist">Apps on</string>
<string name="applist_connect_msg"> 連接到電腦中…… </string>
<string name="applist_menu_resume"> 恢復串流 </string>
<string name="applist_menu_quit"> 退出串流 </string>
@@ -100,7 +99,7 @@
<string name="summary_resolution_list"> 過高的設定會引起串流卡頓甚至軟體閃退 </string>
<string name="title_seekbar_bitrate"> 選擇目標視頻碼率 </string>
<string name="summary_seekbar_bitrate"> 低碼率減少卡頓,高碼率提高畫質 </string>
<string name="suffix_seekbar_bitrate">Mbps</string>
<string name="suffix_seekbar_bitrate">Kbps</string>
<string name="title_checkbox_stretch_video"> 將畫面拉伸至全屏 </string>
<string name="title_checkbox_disable_warnings"> 禁用錯誤提示 </string>
<string name="summary_checkbox_disable_warnings"> 串流過程中禁用連接錯誤提示 </string>
+4
View File
@@ -1,6 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="resolution_names">
<item>360p 30 FPS</item>
<item>360p 60 FPS</item>
<item>720p 30 FPS</item>
<item>720p 60 FPS</item>
<item>1080p 30 FPS</item>
@@ -9,6 +11,8 @@
<item>4K 60 FPS</item>
</string-array>
<string-array name="resolution_values" translatable="false">
<item>360p30</item>
<item>360p60</item>
<item>720p30</item>
<item>720p60</item>
<item>1080p30</item>
+36 -8
View File
@@ -1,8 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_label" translatable="false">Moonlight</string>
<string name="app_label_root" translatable="false">Moonlight (Root)</string>
<!-- Shortcut strings -->
<string name="scut_deleted_pc">PC deleted</string>
<string name="scut_not_paired">PC not paired</string>
<string name="scut_pc_not_found">PC not found</string>
<string name="scut_invalid_uuid">Provided PC is not valid</string>
<string name="scut_invalid_app_id">Provided App is not valid</string>
<!-- Help strings -->
<string name="help_loading_title">Help Viewer</string>
@@ -14,6 +20,7 @@
<string name="pcview_menu_unpair_pc">Unpair</string>
<string name="pcview_menu_send_wol">Send Wake-On-LAN request</string>
<string name="pcview_menu_delete_pc">Delete PC</string>
<string name="pcview_menu_details">View Details</string>
<!-- Pair messages -->
<string name="pairing">Pairing…</string>
@@ -48,9 +55,11 @@
Using remote desktop software can also cause this error. Try rebooting your machine or reinstalling GFE.
</string>
<string name="title_decoding_error">Video Decoder Crashed</string>
<string name="message_decoding_error">Moonlight has crashed due to a problem with this device\'s video decoder. Try adjusting the streaming settings if the crashes continue.</string>
<string name="message_decoding_error">Moonlight has crashed due to an incompatibility with this device\'s video decoder. Ensure GeForce Experience is updated to the latest version on your PC. Try adjusting the streaming settings if the crashes continue.</string>
<string name="title_decoding_reset">Video Settings Reset</string>
<string name="message_decoding_reset">Your device\'s video decoder continues to crash at your selected streaming settings. Your streaming settings have been reset to default.</string>
<string name="error_usb_prohibited">USB access is prohibited by your device administrator. Check your Knox or MDM settings.</string>
<string name="unable_to_pin_shortcut">Your current launcher does not allow for creating pinned shortcuts.</string>
<!-- Start application messages -->
<string name="conn_establishing_title">Establishing Connection</string>
@@ -72,15 +81,17 @@
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="lost_connection">Lost connection to PC</string>
<string name="title_details">Details</string>
<string name="help">Help</string>
<!-- AppList activity -->
<string name="title_applist">Apps on</string>
<string name="applist_connect_msg">Connecting to PC…</string>
<string name="applist_menu_resume">Resume Session</string>
<string name="applist_menu_quit">Quit Session</string>
<string name="applist_menu_quit_and_start">Quit Current Game and Start</string>
<string name="applist_menu_cancel">Cancel</string>
<string name="applist_menu_details">View Details</string>
<string name="applist_menu_scut">Create Shortcut</string>
<string name="applist_refresh_title">App List</string>
<string name="applist_refresh_msg">Refreshing apps…</string>
<string name="applist_refresh_error_title">Error</string>
@@ -89,6 +100,7 @@
<string name="applist_quit_success">Successfully quit</string>
<string name="applist_quit_fail">Failed to quit</string>
<string name="applist_quit_confirmation">Are you sure you want to quit the running app? All unsaved data will be lost.</string>
<string name="applist_details_id">App ID:</string>
<!-- Add computer manually activity -->
<string name="title_add_pc">Add PC Manually</string>
@@ -97,6 +109,7 @@
<string name="addpc_success">Successfully added computer</string>
<string name="addpc_unknown_host">Unable to resolve PC address. Make sure you didn\'t make a typo in the address.</string>
<string name="addpc_enter_ip">You must enter an IP address</string>
<string name="addpc_wrong_sitelocal">That address doesn\'t look right. You must use your router\'s public IP address for streaming over the Internet.</string>
<!-- Preferences -->
<string name="category_basic_settings">Basic Settings</string>
@@ -104,12 +117,12 @@
<string name="summary_resolution_list">Setting values too high for your device may cause lag or crashing</string>
<string name="title_seekbar_bitrate">Select target video bitrate</string>
<string name="summary_seekbar_bitrate">Lower bitrate to reduce stuttering. Raise bitrate to increase image quality.</string>
<string name="suffix_seekbar_bitrate">Mbps</string>
<string name="suffix_seekbar_bitrate">Kbps</string>
<string name="title_checkbox_stretch_video">Stretch video to full-screen</string>
<string name="title_checkbox_disable_warnings">Disable warning messages</string>
<string name="summary_checkbox_disable_warnings">Disable on-screen connection warning messages while streaming</string>
<string name="title_checkbox_battery_saver">Battery saver</string>
<string name="summary_checkbox_battery_saver">Uses less battery, but may increase stuttering</string>
<string name="title_checkbox_enable_pip">Enable Picture-in-Picture observer mode</string>
<string name="summary_checkbox_enable_pip">Allows the stream to be viewed (but not controlled) while multitasking</string>
<string name="category_audio_settings">Audio Settings</string>
<string name="title_checkbox_51_surround">Enable 5.1 surround sound</string>
@@ -117,15 +130,26 @@
<string name="category_gamepad_settings">Gamepad Settings</string>
<string name="title_checkbox_multi_controller">Multiple controller support</string>
<string name="summary_checkbox_multi_controller">When unchecked, all controllers appear as one</string>
<string name="summary_checkbox_multi_controller">Uncheck for games with controller detection issues</string>
<string name="title_seekbar_deadzone">Adjust analog stick deadzone</string>
<string name="suffix_seekbar_deadzone">%</string>
<string name="title_checkbox_xb1_driver">Xbox 360/One controller driver</string>
<string name="summary_checkbox_xb1_driver">Enables a built-in USB driver for devices without native Xbox controller support.</string>
<string name="summary_checkbox_xb1_driver">Enables a built-in USB driver for devices without native Xbox controller support</string>
<string name="title_checkbox_usb_bind_all">Override Android controller support</string>
<string name="summary_checkbox_usb_bind_all">Forces Moonlight\'s USB driver to take over all supported Xbox gamepads</string>
<string name="title_checkbox_mouse_emulation">Mouse emulation via gamepad</string>
<string name="summary_checkbox_mouse_emulation">Long pressing the Start button will switch the gamepad into mouse mode</string>
<string name="category_on_screen_controls_settings">On-screen Controls Settings</string>
<string name="title_checkbox_show_onscreen_controls">Show on-screen controls</string>
<string name="summary_checkbox_show_onscreen_controls">Show virtual controller overlay on touchscreen</string>
<string name="title_only_l3r3">Only show L3 and R3</string>
<string name="summary_only_l3r3">Hide all virtual buttons except L3 and R3</string>
<string name="title_reset_osc">Clear saved on-screen controls layout</string>
<string name="summary_reset_osc">Resets all on-screen controls to their default size and position</string>
<string name="dialog_title_reset_osc">Reset Layout</string>
<string name="dialog_text_reset_osc">Are you sure you want to delete your saved on-screen controls layout?</string>
<string name="toast_reset_osc_success">On-screen controls reset to default</string>
<string name="category_ui_settings">UI Settings</string>
<string name="title_language_list">Language</string>
@@ -142,7 +166,11 @@
<string name="summary_checkbox_host_audio">Play audio from the computer and this device</string>
<string name="category_advanced_settings">Advanced Settings</string>
<string name="title_disable_frame_drop">Never drop frames</string>
<string name="summary_disable_frame_drop">May reduce micro-stuttering on some devices, but can increase latency</string>
<string name="title_video_format">Change H.265 settings</string>
<string name="summary_video_format">H.265 lowers video bandwidth requirements but requires a very recent device.</string>
<string name="summary_video_format">H.265 lowers video bandwidth requirements but requires a very recent device</string>
<string name="title_enable_hdr">Enable HDR (Experimental)</string>
<string name="summary_enable_hdr">Stream HDR when the game and PC GPU support it. HDR requires a GTX 1000 series GPU or later.</string>
</resources>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
</network-security-config>
+51 -10
View File
@@ -1,7 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:seekbar="http://schemas.moonlight-stream.com/apk/res/seekbar">
<PreferenceCategory android:title="@string/category_basic_settings">
<PreferenceCategory android:title="@string/category_basic_settings"
android:key="category_basic_settings">
<ListPreference
android:key="list_resolution_fps"
android:title="@string/title_resolution_list"
@@ -10,9 +12,11 @@
android:entryValues="@array/resolution_values"
android:defaultValue="720p60" />
<com.limelight.preferences.SeekBarPreference
android:key="seekbar_bitrate"
android:key="seekbar_bitrate_kbps"
android:dialogMessage="@string/summary_seekbar_bitrate"
android:max="100"
seekbar:min="500"
seekbar:step="500"
android:max="100000"
android:summary="@string/summary_seekbar_bitrate"
android:text="@string/suffix_seekbar_bitrate"
android:title="@string/title_seekbar_bitrate" />
@@ -21,9 +25,9 @@
android:title="@string/title_checkbox_stretch_video"
android:defaultValue="false" />
<CheckBoxPreference
android:key="checkbox_battery_saver"
android:title="@string/title_checkbox_battery_saver"
android:summary="@string/summary_checkbox_battery_saver"
android:key="checkbox_enable_pip"
android:title="@string/title_checkbox_enable_pip"
android:summary="@string/summary_checkbox_enable_pip"
android:defaultValue="false" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/category_audio_settings">
@@ -50,14 +54,40 @@
android:title="@string/title_checkbox_xb1_driver"
android:summary="@string/summary_checkbox_xb1_driver"
android:defaultValue="true" />
<CheckBoxPreference
android:key="checkbox_usb_bind_all"
android:dependency="checkbox_usb_driver"
android:title="@string/title_checkbox_usb_bind_all"
android:summary="@string/summary_checkbox_usb_bind_all"
android:defaultValue="false" />
<CheckBoxPreference
android:key="checkbox_mouse_emulation"
android:title="@string/title_checkbox_mouse_emulation"
android:summary="@string/summary_checkbox_mouse_emulation"
android:defaultValue="true" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/category_on_screen_controls_settings"
android:key="category_onscreen_controls">
<CheckBoxPreference
android:defaultValue="false"
android:key="checkbox_show_onscreen_controls"
android:title="@string/title_checkbox_show_onscreen_controls"
android:summary="@string/summary_checkbox_show_onscreen_controls"
android:defaultValue="false"/>
android:title="@string/title_checkbox_show_onscreen_controls" />
<CheckBoxPreference
android:defaultValue="false"
android:dependency="checkbox_show_onscreen_controls"
android:key="checkbox_only_show_L3R3"
android:summary="@string/summary_only_l3r3"
android:title="@string/title_only_l3r3" />
<com.limelight.preferences.ConfirmDeleteOscPreference
android:title="@string/title_reset_osc"
android:summary="@string/summary_reset_osc"
android:dialogTitle="@string/dialog_title_reset_osc"
android:dialogMessage="@string/dialog_text_reset_osc"
android:positiveButtonText="@string/yes"
android:negativeButtonText="@string/no"
/>
</PreferenceCategory>
<PreferenceCategory android:title="@string/category_host_settings">
<CheckBoxPreference
@@ -89,7 +119,8 @@
android:summary="@string/summary_checkbox_list_mode"
android:defaultValue="false" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/category_advanced_settings">
<PreferenceCategory android:title="@string/category_advanced_settings"
android:key="category_advanced_settings">
<ListPreference
android:key="video_format"
android:title="@string/title_video_format"
@@ -97,5 +128,15 @@
android:entryValues="@array/video_format_values"
android:summary="@string/summary_video_format"
android:defaultValue="auto" />
<CheckBoxPreference
android:key="checkbox_disable_frame_drop"
android:title="@string/title_disable_frame_drop"
android:summary="@string/summary_disable_frame_drop"
android:defaultValue="false" />
<CheckBoxPreference
android:key="checkbox_enable_hdr"
android:title="@string/title_enable_hdr"
android:summary="@string/summary_enable_hdr"
android:defaultValue="false" />
</PreferenceCategory>
</PreferenceScreen>
+2 -1
View File
@@ -2,5 +2,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Non-root application name -->
<application android:label="Moonlight" />
<!-- FIXME: We should set extractNativeLibs=false but this breaks installation on the Fire TV 3 -->
<application android:label="@string/app_label" />
</manifest>
+5 -1
View File
@@ -2,5 +2,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Root application name -->
<application android:label="Moonlight (Root)" />
<!-- Ensure native libraries are always extracted for root builds,
since we must invoke the evdev_reader binary ourselves -->
<application
android:label="@string/app_label_root"
android:extractNativeLibs="true" />
</manifest>
@@ -6,7 +6,6 @@ import android.os.Looper;
import android.widget.Toast;
import com.limelight.LimeLog;
import com.limelight.LimelightBuildProps;
import com.limelight.binding.input.capture.InputCaptureProvider;
import java.io.DataOutputStream;
@@ -152,7 +151,15 @@ public class EvdevCaptureProvider extends InputCaptureProvider {
break;
case EvdevEvent.BTN_SIDE:
listener.mouseButtonEvent(EvdevListener.BUTTON_X1,
event.value != 0);
break;
case EvdevEvent.BTN_EXTRA:
listener.mouseButtonEvent(EvdevListener.BUTTON_X2,
event.value != 0);
break;
case EvdevEvent.BTN_FORWARD:
case EvdevEvent.BTN_BACK:
case EvdevEvent.BTN_TASK:
@@ -186,10 +193,6 @@ public class EvdevCaptureProvider extends InputCaptureProvider {
this.libraryPath = activity.getApplicationInfo().nativeLibraryDir;
}
public static boolean isCaptureProviderSupported() {
return LimelightBuildProps.ROOT_BUILD;
}
private void reportDeviceNotRooted() {
activity.runOnUiThread(new Runnable() {
@Override
+3 -1
View File
@@ -2,14 +2,16 @@
buildscript {
repositories {
jcenter()
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.0-beta5'
classpath 'com.android.tools.build:gradle:3.2.0'
}
}
allprojects {
repositories {
jcenter()
google()
}
}
+15 -3
View File
@@ -1,9 +1,9 @@
This file serves to document some of the decoder errata when using MediaCodec hardware decoders on certain devices.
1. num_ref_frames is set to 16 by NVENC which causes decoders to allocate 16+ buffers. This can cause an error or lag on some devices.
- Affected decoders: TI OMAP4, Allwinner A20
- Affected decoders: TI OMAP4 crashes, Allwinner A20, MT8176 lags (HEVC not affected)
2. Some decoders have a huge per-frame latency with the unmodified SPS sent from NVENC. Setting max_dec_frame_buffering fixes this latency issue.
2. Some H.264 decoders have a huge per-frame latency with the unmodified SPS sent from NVENC. Setting max_dec_frame_buffering=1 fixes this latency issue.
- Affected decoders: NVIDIA Tegra 3 and 4, Broadcom VideoCore IV
3. Some decoders strictly require that you pass BUFFER_FLAG_CODEC_CONFIG and crash upon the IDR frame if you don't
@@ -28,4 +28,16 @@ This file serves to document some of the decoder errata when using MediaCodec ha
- 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
- Affected decoders: MediaTek decoder in Fire TV 2015
11. Attempting to use reference picture invalidation at 1080p causes the decoder to crash on low-end Snapdragon SoCs. 720p is unaffected.
- Affected decoders: Snapdragon 200, 410, 415, 430, 435, 616
12. Enabling adaptive playback causes H.265 1080p and 4K playback to fail on some MediaTek SoCs.
- Affected decoders: MT5832 in Sony BRAVIA 4K GB (BRAVIA_ATV2) and MT5890 in Phillips 55PUS6501
13. Some HEVC decoders hang when receiving a stream with 16 reference frames
- Affected decoders: Amlogic S905Z in Fire TV 3
14. Some HEVC decoders lag when receiving a stream with 16 reference frames
- Affected decoders: Tegra X1 in Pixel C (but NOT in SHIELD TV darcy)
+2 -2
View File
@@ -1,6 +1,6 @@
#Sun Sep 03 12:51:12 PDT 2017
#Tue May 08 18:56:31 PDT 2018
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
-19
View File
@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.id="moonlight-android" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" 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" LANGUAGE_LEVEL="JDK_1_7" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.gradle" />
</content>
<orderEntry type="jdk" jdkName="1.8" jdkType="JavaSDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>