Compare commits

...
153 Commits
Author SHA1 Message Date
Cameron Gutman 882b61ac63 Implement GCMouse API for relative mouse support
This seems to be quite broken in iOS 14.0. The Microsoft Surface Precision mouse is detected as a GCKeyboard instead of a GCMouse.
2020-09-17 13:56:09 -05:00
Cameron Gutman 4c68a5d8c3 Disable iOS 13.4+ mouse support on iOS 14 in preparation for GCMouse work 2020-09-16 18:36:49 -07:00
Cameron Gutman 452b001c99 Raise iOS deployment target to iOS 9.3 as required by Xcode 12 2020-09-16 17:17:25 -07:00
Cameron Gutman cac639806c Replace Citrix X1 toggle with touchscreen mode toggle 2020-09-16 17:13:26 -07:00
Cameron Gutman dbca1bb4ce Remove Citrix X1 support in preparation for GCMouse integration 2020-09-16 17:08:20 -07:00
Cameron Gutman dfec190b82 Version 6.0.1 2020-09-08 19:53:32 -07:00
Cameron Gutman 88f9423802 Update moonlight-common-c to improve connection reliability 2020-09-08 19:53:13 -07:00
Cameron Gutman beeec54420 Version 6.0 2020-09-06 18:14:39 -07:00
Cameron Gutman b7b7a88ef7 Display the prohibited address warning on failures too 2020-08-30 20:47:40 -07:00
Cameron Gutman 6bf643968b Display a loading spinner while adding a PC is in progress 2020-08-30 20:39:11 -07:00
Cameron Gutman dbab07838d Only allow PCs on the same LAN to be added via the Add PC dialog
This is required to comply with App Store Guideline 4.2.7a.
2020-08-30 17:59:28 -07:00
Cameron Gutman 06a4ced692 Only impose app store restrictions on release builds 2020-08-30 16:43:56 -07:00
Cameron Gutman fb9d778dab Purge mentions of Internet streaming for App Store Guideline 4.2.7a 2020-08-30 15:37:52 -07:00
Cameron Gutman 2a0b29a284 Revert "Add direct-to-desktop mode"
This reverts commit e445989ec7.
2020-08-30 15:30:58 -07:00
Cameron Gutman bf437669fb Add warning if no key frames can be received in 10 seconds 2020-08-29 21:21:46 -07:00
Cameron Gutman e445989ec7 Add direct-to-desktop mode 2020-08-12 22:26:56 -07:00
Cameron Gutman 0b712a1589 Add manual connection testing option 2020-08-12 22:20:15 -07:00
Cameron Gutman 1aa11df725 Fix simulator build for Apple silicon 2020-08-12 19:28:12 -07:00
Cameron Gutman ff5e8d167d Add support for iOS 14 local network privacy changes 2020-08-12 18:34:49 -07:00
Cameron Gutman 286b19d360 Add automatic network testing on connection failure 2020-08-10 22:56:47 -07:00
Cameron Gutman 3ab0829db5 Update common-c to fix server-side log spam 2020-08-09 18:38:53 -07:00
Cameron Gutman 6f2041dfb6 Sync WoL ports with Android and Qt 2020-08-09 14:48:27 -07:00
Cameron Gutman 78600a4e06 Update common-c with client connectivity test and select() replacement 2020-07-27 00:23:00 -07:00
Cameron Gutman e0bb48320f Avoid modulo bias for PIN generation 2020-07-12 11:40:31 -07:00
Cameron Gutman 46b9f84a31 Update moonlight-common-c to fix QoS-related connection issues 2020-06-12 22:10:41 -07:00
Cameron Gutman abb4b5f9b9 Pad the SHA-1 hash for Gen5 and below servers 2020-06-09 01:13:30 -07:00
Cameron Gutman a4f7861ccf Avoid using functions deprecated in OpenSSL 3.0 2020-06-09 00:56:58 -07:00
Cameron Gutman 39c03bc8a8 Attempt to fix Apple's concerns about cloud streaming 2020-06-05 16:03:38 -07:00
Cameron Gutman 059a943a3d Fix dragging with middle, X1, or X2 mouse buttons down 2020-05-31 12:40:54 -07:00
Cameron Gutman 3e2e4d13a9 Update moonlight-common-c 2020-05-29 19:17:37 -07:00
Cameron Gutman 4a537c1806 Fix tvOS build in Travis CI 2020-05-29 19:10:13 -07:00
Cameron Gutman 839f2018cb Update osx_image for Travis CI 2020-05-29 19:02:25 -07:00
Cameron Gutman f9d1840941 Update README.md 2020-05-27 19:55:52 -07:00
Cameron Gutman 7747188363 Update README 2020-05-27 19:53:17 -07:00
Cameron Gutman c43ed56751 Use a better workaround for the GFE 3.20.3 high FPS bug 2020-05-25 19:24:03 -07:00
Cameron Gutman a3b01b3cd4 Revert "Disable SOPS for streams over 60 FPS for GFE 3.20.3"
This reverts commit fdaf7f92a1.
2020-05-25 18:58:15 -07:00
Cameron Gutman 96c2ac9be5 Version 5.0.2-2 (iOS) 2020-05-09 15:02:26 -07:00
Cameron Gutman aee4e3bf3d Update moonlight-common-c 2020-05-09 15:00:41 -07:00
Cameron Gutman d9cb55a17f Don't update the mouse position if it hasn't moved from the last location 2020-05-09 14:42:09 -07:00
Cameron Gutman 4c8ad13f3a Version 5.0.2 (iOS) 2020-05-01 19:48:49 -07:00
Cameron Gutman 99c160f8e5 Require iOS 13.4.1 to enable pointer interaction to avoid crashing on beta iOS 13.4 builds 2020-05-01 19:47:52 -07:00
Cameron Gutman 78e16b0e29 Fix crash on iOS 8 2020-05-01 19:43:21 -07:00
Cameron Gutman fdc9e00c54 Add a friendly error message when no video traffic is received 2020-05-01 19:37:33 -07:00
Cameron Gutman b16eaafd2c Allow continuous scroll type gestures. Fixes #408 2020-04-29 18:00:17 -07:00
Cameron Gutman 2ec6639351 Add special error text for the -1 launch error code 2020-04-25 16:29:47 -07:00
Cameron Gutman f49a908dc0 Fix parsing rare GFE status code of 0xFFFFFFFF 2020-04-23 20:07:52 -07:00
Cameron Gutman b3a666af8e Version 5.0.1 for iOS 2020-04-20 16:50:29 -07:00
Cameron Gutman 5abad38956 Revert "Refactor legacy UITextField-based keyboard support out of StreamView"
This refactoring breaks support for hardware keyboards on iOS 13.3
and below. The root cause of the breakage is not currently known.

This reverts commit 69d1ff15a1.
2020-04-20 16:31:21 -07:00
Cameron Gutman 7f034e0338 Version 4.0.2 for tvOS 2020-04-19 15:09:10 -07:00
Cameron Gutman fb196f661c Reorder Info.plist 2020-04-19 14:57:56 -07:00
Cameron Gutman 3cca55703e Fix the LoadingFrameViewController not display on top of other content on tvOS
Also remove hacks that were put in place due to this bug previously.
2020-04-19 14:40:39 -07:00
Cameron Gutman 084018719a Version 5.0.0-2 for iOS 2020-04-18 17:25:16 -07:00
Cameron Gutman 4aca666df4 Fix race condition that could cause pairing state to be lost
If a mDNS response for a host happened during the period where we
had removed the host from discovery, it would be re-added by mDNS
without the pairing cert. Now we no longer completely remove the host
and instead just remember that it's paused.
2020-04-18 17:16:53 -07:00
Cameron Gutman b833d3b3b7 Fix broken isEqual function for TemporaryApp and TemporaryHost 2020-04-18 17:05:36 -07:00
Cameron Gutman cb94d7aad7 Version 5.0.0 for iOS 2020-04-18 15:26:44 -07:00
Cameron Gutman b8631db5df Allow rediscovery of hosts after deletion without having to force quit the app 2020-04-18 15:03:52 -07:00
Cameron Gutman bd5e0ecc40 Standardize mouse input scaling to avoid variance based on stream resolution 2020-04-18 14:44:57 -07:00
Cameron Gutman 10d2e1635b Only hide the cursor over the video region 2020-04-18 14:09:19 -07:00
Cameron Gutman 68e59aba74 Scale mouse input to fit the video region 2020-04-18 14:00:26 -07:00
Cameron Gutman a4255f4cad Implement absolute mouse support on iOS 13.4
Fixes #402
2020-04-18 13:11:24 -07:00
Cameron Gutman f12e2f16dc Add handling for cancelled touch events 2020-04-18 11:25:24 -07:00
Cameron Gutman 177ecdbe0b Enable pointer interaction on the PC and app grid items 2020-04-18 11:24:02 -07:00
Cameron Gutman de05214728 Version 4.0.1 2020-04-17 12:46:46 -07:00
Cameron Gutman b0149b2fe9 Merge pull request #405 from loki-47-6F-64/master
Don't send PIN to GFE
2020-04-17 12:40:29 -07:00
Loki 1366ede690 Don't send PIN to GFE 2020-04-17 14:45:46 +02:00
Cameron Gutman fdaf7f92a1 Disable SOPS for streams over 60 FPS for GFE 3.20.3 2020-04-12 11:25:53 -07:00
Cameron Gutman 8efbe2670a Bump version to 4.0.0 2020-04-12 11:10:23 -07:00
Cameron Gutman 8bc66749c3 Update moonlight-common-c to avoid termination delay on GFE 3.20.3 2020-04-12 10:43:33 -07:00
Cameron Gutman 4ab71a3cc7 Allow Command key to function as Control 2020-04-11 11:17:25 -07:00
Cameron Gutman 55b2f39721 Adjust X1_MOUSE_SPEED_DIVISOR based on user testing 2020-04-11 11:04:00 -07:00
Cameron Gutman e59f0764a2 Update tvOS target to Swift 5 2020-04-11 11:01:44 -07:00
Cameron Gutman 6194d80108 Upgrade to Swift 5 2020-04-08 20:11:10 -07:00
Cameron Gutman 5264fee655 Update to OpenSSL 1.1.1f
Built for targets: ios-sim-cross-i386 ios-sim-cross-x86_64 ios64-cross-arm64 ios64-cross-arm64e ios-cross-armv7s ios-cross-armv7 tvos-sim-cross-x86_64 tvos64-cross-arm64

https://github.com/keeshux/openssl-apple
2020-04-08 19:57:45 -07:00
Cameron Gutman ab17c57c09 Update to latest upstream X1Kit with support for new X1 firmware 2020-04-08 18:47:08 -07:00
Cameron Gutman c7a7fc3fbc Add 7.1 surround sound support 2020-04-03 17:46:04 -07:00
Cameron Gutman f32d4f2886 Reduce retransmission delay on packet loss and enable QoS marking on ENet traffic 2020-03-29 22:22:03 -07:00
Cameron Gutman 8e4471fbc0 Slow X1 mouse input by 2x to make it more usable 2020-03-27 23:05:53 -07:00
Cameron Gutman 733b992912 Update to OpenSSL 1.1.1e 2020-03-27 21:24:32 -07:00
Cameron Gutman 6d03f4bc4c Add a settings option for enabling Citrix X1 support 2020-03-27 20:26:01 -07:00
Cameron Gutman 75ab28a2c3 Enhance physical keyboard support on iOS 13.4
Fixes #394
2020-03-25 18:24:04 -07:00
Cameron Gutman 69d1ff15a1 Refactor legacy UITextField-based keyboard support out of StreamView 2020-03-24 18:14:50 -07:00
Cameron Gutman eceb23e1cd Allow Moonlight audio to mix with other audio 2020-03-21 16:35:07 -07:00
Cameron Gutman ab5a9ba067 Use 10 ms audio samples on slow networks 2020-03-21 16:06:25 -07:00
Cameron Gutman 742bb7b516 Change errorCode from long to int 2020-02-25 00:00:00 -08:00
Cameron Gutman 65578b8a64 Fix tvOS build 2020-01-19 21:15:15 -08:00
Cameron Gutman 1246de3f18 Centralize Discord invite links 2020-01-18 10:54:22 -08:00
Cameron Gutman 5b5fdff6d8 Add support for Citrix X1 mice using X1Kit (untested) 2020-01-16 19:52:49 -08:00
Cameron Gutman 47aaa02d08 Version 3.3.0 (iOS) 2019-12-07 11:48:02 -08:00
Cameron Gutman b71f06aa04 Change audio buffer duration back to 80 ms for now 2019-12-04 18:58:36 -08:00
Cameron Gutman 61c18c201c Prevent concurrent modification of host list 2019-12-03 20:02:16 -08:00
Cameron Gutman 32b7396070 Reduce buffering for 5 ms and 20 ms audio packets 2019-12-03 19:51:31 -08:00
Cameron Gutman c327c5b1ec Force remote streaming mode when a VPN is connected 2019-12-01 20:55:20 -08:00
Cameron Gutman 44d4553031 Add support for arbitrary audio duration 2019-12-01 20:51:18 -08:00
Cameron Gutman a2b8e3a8b8 Update common-c to fix RTSP handshake error when PMTUD is broken 2019-12-01 20:50:45 -08:00
Cameron Gutman 9e91627601 Fix auto-comment line breaks 2019-11-16 12:24:49 -08:00
Cameron Gutman 250f6618fd Plumb the presentation time from the server into the renderer 2019-11-11 18:42:11 -08:00
Cameron Gutman ab5e253a59 Version 3.2.0 (iOS) 2019-11-11 18:41:40 -08:00
Cameron Gutman 12a64f8626 Don't emulate buttons when the controller already has them 2019-11-09 17:47:47 -08:00
Cameron Gutman 99fec8919e Add configuration for auto-comment bot 2019-11-09 15:03:16 -08:00
Cameron Gutman 52dcdd68c4 Fix concurrency issues accessing TemporaryHost fields that could be changed by other threads 2019-11-06 21:51:23 -08:00
Cameron Gutman 5a0e4bcc05 Fix UBSan undefined behavior warning
moonlight-ios/Limelight/Input/ControllerSupport.m:294:32: runtime error: 255 is outside the range of representable values of type 'char'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior moonlight-ios/Limelight/Input/ControllerSupport.m:294:32 in
2019-11-06 20:57:53 -08:00
Cameron Gutman 2577f581ba Revert "Don't navigate to a host that is known to be offline"
This reverts commit 95ce78bb00.
2019-11-05 20:30:17 -08:00
Cameron Gutman 1757359fbf Fix "server busy" pairing error dialog 2019-11-04 21:41:42 -08:00
Cameron Gutman e89707e601 Don't send a STUN request when a VPN is connected 2019-11-04 21:28:53 -08:00
Cameron Gutman c89de92e23 Fix tvOS build 2019-11-03 15:25:43 -08:00
Cameron Gutman ee7df54842 Launch directly to the app grid if there is exactly one PC 2019-11-03 15:17:27 -08:00
Cameron Gutman 95ce78bb00 Don't navigate to a host that is known to be offline 2019-11-03 15:13:44 -08:00
Cameron Gutman 3c9314dff3 Disable both navbar buttons when disabling navigation 2019-11-03 15:13:13 -08:00
Cameron Gutman c5e1bbf61b Set title immediately when a PC is tapped 2019-11-03 15:12:43 -08:00
Cameron Gutman abe850cd72 Fix UI problems when a pairing error occurs in the background 2019-11-03 15:11:02 -08:00
Cameron Gutman b0a3e163c4 Restart PC discovery when pairing fails 2019-11-03 14:43:12 -08:00
Cameron Gutman 2ae7fefc0c Prevent app grid from populating with empty cells during server info polling 2019-11-03 14:40:32 -08:00
Cameron Gutman 72af5d5217 Update initial PC state more quickly on launch 2019-11-03 14:26:13 -08:00
Cameron Gutman d1eb7b45a2 Add quick action shortcuts to open paired PCs 2019-11-03 13:38:52 -08:00
Cameron Gutman 3b5652c3f5 Update moonlight-common-c submodule 2019-11-02 21:48:00 -07:00
Cameron Gutman df50bb9583 Use blank gray launch screen to workaround inconsistent nav bar size 2019-11-02 18:22:09 -07:00
Cameron Gutman 18e2d67f6b Suppress home bar hiding when user is interacting 2019-11-02 15:17:52 -07:00
Cameron Gutman 9f19f5da27 Fix app store link 2019-10-28 18:41:38 -07:00
Cameron Gutman 005c3e4082 Update readme to include tvOS 2019-10-28 18:40:35 -07:00
Cameron Gutman 35b3c01582 Sync tvOS version numbering with iOS 2019-10-28 18:28:21 -07:00
Cameron Gutman d8f55b57fa Fix Travis CI build with their updated Xcode 11 images 2019-10-22 00:40:39 -04:00
Cameron Gutman 5faa8a0b85 Hide the home bar when a gamepad is connected and no on-screen controls are visible
Fixes #386
2019-10-22 00:32:56 -04:00
Cameron Gutman 967ddd7d68 Refactor OSC handling 2019-10-22 00:16:20 -04:00
Cameron Gutman 26fac5b56e Fix asserts not catching the intended bugs (code analysis warning) 2019-10-21 17:53:18 -07:00
Cameron Gutman 75c1c85de9 Remove old IPA binaries
They don't work with recent GFE versions anyway
2019-10-21 17:41:47 -07:00
Cameron Gutman 653a72eaf8 Remove macOS port
We now have a proper native PC port (Moonlight Qt) and if we
wanted to bring the full iOS app over we can now use Catalyst.
2019-10-21 17:39:48 -07:00
Cameron Gutman 27d95ae0ba Update moonlight-common-c submodule 2019-10-21 17:23:50 -07:00
Cameron Gutman 7dca4c6c63 Fix incompatible pointer type warning 2019-10-21 17:05:03 -07:00
Cameron Gutman 12e7dd2181 Update moonlight-common-c submodule 2019-10-12 19:21:07 -07:00
Cameron Gutman c9225d468a Change title of launch error dialog 2019-10-12 19:20:53 -07:00
Cameron Gutman a824c23c59 Fix iOS build 2019-10-12 19:20:26 -07:00
Cameron Gutman a9e01fc9a9 Fix pointer speed on tvOS 2019-10-12 18:23:28 -07:00
Cameron Gutman fb2d58da6a Fix default resolution not taking effect on tvOS 2019-10-12 18:10:12 -07:00
Cameron Gutman 09ac6cc057 Clamp stream resolution and FPS to device capabilities 2019-10-12 18:07:47 -07:00
Cameron Gutman 1121a918d1 Improve description messages of error message dialogs 2019-10-12 16:41:32 -07:00
Cameron Gutman 565a8e56e6 Add NSError information to the HttpResponse on failure 2019-10-12 16:13:59 -07:00
Cameron Gutman f8cc7bb77f Improve text for WoL alert 2019-10-12 16:03:35 -07:00
Cameron Gutman 55c52e1ee1 Require tvOS 12.0 due to navigation issues on tvOS 11.4 2019-10-10 02:20:33 -07:00
Diego Waxemberg 366a94d018 Removing manually specified signing certs
This will make signing for iOS and tvOS using the automatic signing possible. It complains if there is a manually specifed signing id.
2019-10-06 09:42:01 -04:00
Cameron Gutman 4e5264d5e9 Fix menu button recognizer not always working after returning from settings 2019-09-24 19:08:22 -07:00
Cameron Gutman d98aba66a8 Add settings button for tvOS 2019-09-24 18:56:37 -07:00
Cameron Gutman 8ccbdc7923 Fix Start button not working on some MFi controllers on iOS 13 2019-09-24 18:13:56 -07:00
Cameron Gutman 769840d089 Immediately stop streaming when resigning active on tvOS 2019-09-24 17:37:35 -07:00
Cameron Gutman 33b050f84f Increase bitrate warning font size on tvOS 2019-09-24 17:36:26 -07:00
Cameron Gutman d2bc7209f7 Use the same bundle identifier for tvOS 2019-09-24 06:06:35 -07:00
Cameron Gutman d32d1aa09a Add tvOS UI screenshots 2019-09-23 21:33:26 -07:00
Cameron Gutman 7f6db298d0 Remove remove disallowed GCSupportedGameControllers value 2019-09-23 21:33:25 -07:00
Cameron Gutman f9ff6a6004 Add tvOS launch image 2019-09-23 21:33:25 -07:00
Cameron Gutman a6e6d0e491 Restore focus on the correct app after closing the stream 2019-09-23 21:33:24 -07:00
Cameron Gutman 6613976291 Increase icon resolution for tvOS 2019-09-23 21:33:24 -07:00
Cameron Gutman 6f8d898b77 Prevent box art from overlapping other tiles when focused 2019-09-23 21:33:24 -07:00
Cameron Gutman d1cbfd9c44 Rewrite UIComputerView to use a pop-out effect on tvOS 2019-09-23 21:33:24 -07:00
Cameron Gutman e6b1dd6b64 Rewrite UIAppView to avoid needing a custom hover pop-up effect on tvOS 2019-09-23 21:33:24 -07:00
258 changed files with 17686 additions and 26406 deletions

No files matched your search

+4
View File
@@ -0,0 +1,4 @@
issuesOpened: >
If this is a question about Moonlight or you need help troubleshooting a streaming problem, please use the help channels on our [Discord server](https://moonlight-stream.org/discord) instead of GitHub issues. There are many more people available on Discord to help you and answer your questions.<br /><br />
This issue tracker should only be used for specific bugs or feature requests.<br /><br />
Thank you, and happy streaming!
+6 -6
View File
@@ -6,20 +6,20 @@ git:
matrix:
include:
- os: osx
osx_image: xcode11
osx_image: xcode11.5
env:
- SCHEME="Moonlight"
- TARGET="platform=iOS Simulator,OS=12.1,name=iPhone SE"
- TARGET="platform=iOS Simulator,OS=13.5,name=iPhone 11 Pro"
- os: osx
osx_image: xcode11
osx_image: xcode11.5
env:
- SCHEME="Moonlight"
- TARGET="platform=iOS Simulator,OS=12.1,name=iPad Pro (9.7-inch)"
- TARGET="platform=iOS Simulator,OS=13.5,name=iPad Pro (9.7-inch)"
- os: osx
osx_image: xcode11
osx_image: xcode11.5
env:
- SCHEME="Moonlight TV"
- TARGET="platform=tvOS Simulator,OS=12.1,name=Apple TV 4K"
- TARGET="platform=tvOS Simulator,OS=13.4,name=Apple TV 4K"
script:
- set -o pipefail && xcodebuild -project Moonlight.xcodeproj -scheme "$SCHEME" -destination "$TARGET" build | xcpretty
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+8 -24
View File
@@ -1,35 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14868" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina6_1" orientation="landscape" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14824"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15509"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<navigationController id="vV9-32-ww6">
<navigationBar key="navigationBar" contentMode="scaleToFill" id="y2P-aE-Haa">
<rect key="frame" x="0.0" y="0.0" width="896" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<color key="barTintColor" red="0.66666668653488159" green="0.66666668653488159" blue="0.66666668653488159" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</navigationBar>
<viewControllers>
<viewController id="l6V-ue-5Om">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="rEZ-vx-akr"/>
<viewControllerLayoutGuide type="bottom" id="gBD-cy-cth"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="6lF-fc-kpK">
<rect key="frame" x="0.0" y="0.0" width="896" height="414"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.3333333432674408" green="0.3333333432674408" blue="0.3333333432674408" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view>
<navigationItem key="navigationItem" id="rW0-BH-s7l"/>
</viewController>
</viewControllers>
<point key="canvasLocation" x="34" y="149"/>
</navigationController>
<view contentMode="scaleToFill" id="VWy-YH-qYj">
<rect key="frame" x="0.0" y="0.0" width="896" height="414"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<color key="backgroundColor" white="0.33333333329999998" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<point key="canvasLocation" x="33" y="187"/>
</view>
</objects>
</document>
+2 -8
View File
@@ -6,19 +6,13 @@
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
//
#if TARGET_OS_IPHONE
#import <UIKit/UIKit.h>
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@property (strong, nonatomic) UIWindow *window;
#else
#import <Cocoa/Cocoa.h>
@interface AppDelegate : NSObject <NSApplicationDelegate>
@property (readonly, strong) NSPersistentContainer *persistentContainer;
@property (strong, nonatomic) NSWindow *window;
#endif
@property (strong, nonatomic) NSString *pcUuidToLoad;
@property (strong, nonatomic) void (^shortcutCompletionHandler)(BOOL);
@property (readonly, strong, nonatomic) NSManagedObjectContext *managedObjectContext;
@property (readonly, strong, nonatomic) NSManagedObjectModel *managedObjectModel;
+17 -18
View File
@@ -18,19 +18,29 @@ static NSOperationQueue* mainQueue;
#if TARGET_OS_TV
static NSString* DB_NAME = @"Moonlight_tvOS.bin";
#elif TARGET_OS_IPHONE
static NSString* DB_NAME = @"Limelight_iOS.sqlite";
#else
static NSString* DB_NAME = @"moonlight_mac.sqlite";
static NSString* DB_NAME = @"Limelight_iOS.sqlite";
#endif
#if TARGET_OS_IPHONE
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
#if !TARGET_OS_TV
if (@available(iOS 9.0, *)) {
UIApplicationShortcutItem* shortcut = [launchOptions valueForKey:UIApplicationLaunchOptionsShortcutItemKey];
if (shortcut != nil) {
_pcUuidToLoad = (NSString*)[shortcut.userInfo objectForKey:@"UUID"];
}
}
#endif
return YES;
}
#if !TARGET_OS_TV
- (void)application:(UIApplication *)application performActionForShortcutItem:(UIApplicationShortcutItem *)shortcutItem completionHandler:(void (^)(BOOL succeeded))completionHandler {
_pcUuidToLoad = (NSString*)[shortcutItem.userInfo objectForKey:@"UUID"];
_shortcutCompletionHandler = completionHandler;
}
#endif
- (void)applicationWillResignActive:(UIApplication *)application
{
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
@@ -58,17 +68,6 @@ static NSString* DB_NAME = @"moonlight_mac.sqlite";
// Saves changes in the application's managed object context before the application terminates.
[self saveContext];
}
#else
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
// Insert code here to initialize your application
}
- (void)applicationWillTerminate:(NSNotification *)aNotification {
// Insert code here to tear down your application
[self saveContext];
}
#endif
- (void)saveContext
{
+53 -38
View File
@@ -9,7 +9,6 @@
#import "CryptoManager.h"
#import "mkcert.h"
#include <openssl/aes.h>
#include <openssl/sha.h>
#include <openssl/x509.h>
#include <openssl/pem.h>
@@ -45,46 +44,47 @@ static NSData* p12 = nil;
}
- (NSData*) aesEncrypt:(NSData*)data withKey:(NSData*)key {
AES_KEY aesKey;
AES_set_encrypt_key([key bytes], 128, &aesKey);
int size = [self getEncryptSize:data];
unsigned char* buffer = malloc(size);
unsigned char* blockRoundedBuffer = calloc(1, size);
memcpy(blockRoundedBuffer, [data bytes], [data length]);
EVP_CIPHER_CTX* cipher;
int ciphertextLen;
cipher = EVP_CIPHER_CTX_new();
EVP_EncryptInit(cipher, EVP_aes_128_ecb(), [key bytes], NULL);
EVP_CIPHER_CTX_set_padding(cipher, 0);
NSMutableData* ciphertext = [NSMutableData dataWithLength:[data length]];
EVP_EncryptUpdate(cipher,
[ciphertext mutableBytes],
&ciphertextLen,
[data bytes],
(int)[data length]);
assert(ciphertextLen == [ciphertext length]);
EVP_CIPHER_CTX_free(cipher);
// AES_encrypt only encrypts the first 16 bytes so iterate the entire buffer
int blockOffset = 0;
while (blockOffset < size) {
AES_encrypt(blockRoundedBuffer + blockOffset, buffer + blockOffset, &aesKey);
blockOffset += 16;
}
NSData* encryptedData = [NSData dataWithBytes:buffer length:size];
free(buffer);
free(blockRoundedBuffer);
return encryptedData;
return ciphertext;
}
- (NSData*) aesDecrypt:(NSData*)data withKey:(NSData*)key {
AES_KEY aesKey;
AES_set_decrypt_key([key bytes], 128, &aesKey);
unsigned char* buffer = malloc([data length]);
// AES_decrypt only decrypts the first 16 bytes so iterate the entire buffer
int blockOffset = 0;
while (blockOffset < [data length]) {
AES_decrypt([data bytes] + blockOffset, buffer + blockOffset, &aesKey);
blockOffset += 16;
}
NSData* decryptedData = [NSData dataWithBytes:buffer length:[data length]];
free(buffer);
return decryptedData;
}
EVP_CIPHER_CTX* cipher;
int plaintextLen;
- (int) getEncryptSize:(NSData*)data {
// the size is the length of the data ceiling to the nearest 16 bytes
return (((int)[data length] + 15) / 16) * 16;
cipher = EVP_CIPHER_CTX_new();
EVP_DecryptInit(cipher, EVP_aes_128_ecb(), [key bytes], NULL);
EVP_CIPHER_CTX_set_padding(cipher, 0);
NSMutableData* plaintext = [NSMutableData dataWithLength:[data length]];
EVP_DecryptUpdate(cipher,
[plaintext mutableBytes],
&plaintextLen,
[data bytes],
(int)[data length]);
assert(plaintextLen == [plaintext length]);
EVP_CIPHER_CTX_free(cipher);
return plaintext;
}
+ (NSData*) pemToDer:(NSData*)pemCertBytes {
@@ -227,13 +227,28 @@ static NSData* p12 = nil;
Log(LOG_E, @"Unable to parse certificate in memory!");
return NULL;
}
return [NSData dataWithBytes:x509->signature->data length:x509->signature->length];
#if (OPENSSL_VERSION_NUMBER < 0x10002000L)
ASN1_BIT_STRING *asnSignature = x509->signature;
#elif (OPENSSL_VERSION_NUMBER < 0x10100000L)
ASN1_BIT_STRING *asnSignature;
X509_get0_signature(&asnSignature, NULL, x509);
#else
const ASN1_BIT_STRING *asnSignature;
X509_get0_signature(&asnSignature, NULL, x509);
#endif
NSData* sig = [NSData dataWithBytes:asnSignature->data length:asnSignature->length];
X509_free(x509);
return sig;
}
+ (NSData*)getKeyFromCertKeyPair:(CertKeyPair*)certKeyPair {
BIO* bio = BIO_new(BIO_s_mem());
PEM_write_bio_PrivateKey(bio, certKeyPair->pkey, NULL, NULL, 0, NULL, NULL);
PEM_write_bio_PrivateKey_traditional(bio, certKeyPair->pkey, NULL, NULL, 0, NULL, NULL);
BUF_MEM* mem;
BIO_get_mem_ptr(bio, &mem);
+50 -83
View File
@@ -5,19 +5,60 @@
#include <stdlib.h>
#include <openssl/pem.h>
#include <openssl/conf.h>
#include <openssl/pkcs12.h>
#ifndef OPENSSL_NO_ENGINE
#include <openssl/engine.h>
#endif
#include <openssl/rsa.h>
#include <openssl/x509.h>
#include <openssl/rand.h>
static const int NUM_BITS = 2048;
static const int SERIAL = 0;
static const int NUM_YEARS = 10;
static const int NUM_YEARS = 20;
int mkcert(X509 **x509p, EVP_PKEY **pkeyp, int bits, int serial, int years);
int add_ext(X509 *cert, int nid, char *value);
void mkcert(X509 **x509p, EVP_PKEY **pkeyp, int bits, int serial, int years) {
X509* cert = X509_new();
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL);
EVP_PKEY_keygen_init(ctx);
EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, bits);
// pk must be initialized on input
EVP_PKEY* pk = NULL;
EVP_PKEY_keygen(ctx, &pk);
EVP_PKEY_CTX_free(ctx);
X509_set_version(cert, 2);
ASN1_INTEGER_set(X509_get_serialNumber(cert), serial);
#if OPENSSL_VERSION_NUMBER < 0x10100000L
X509_gmtime_adj(X509_get_notBefore(cert), 0);
X509_gmtime_adj(X509_get_notAfter(cert), 60 * 60 * 24 * 365 * years);
#else
ASN1_TIME* before = ASN1_STRING_dup(X509_get0_notBefore(cert));
ASN1_TIME* after = ASN1_STRING_dup(X509_get0_notAfter(cert));
X509_gmtime_adj(before, 0);
X509_gmtime_adj(after, 60 * 60 * 24 * 365 * years);
X509_set1_notBefore(cert, before);
X509_set1_notAfter(cert, after);
ASN1_STRING_free(before);
ASN1_STRING_free(after);
#endif
X509_set_pubkey(cert, pk);
X509_NAME* name = X509_get_subject_name(cert);
X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC,
(const unsigned char*)"NVIDIA GameStream Client",
-1, -1, 0);
X509_set_issuer_name(cert, name);
X509_sign(cert, pk, EVP_sha256());
*x509p = cert;
*pkeyp = pk;
}
struct CertKeyPair generateCertKeyPair(void) {
BIO *bio_err;
@@ -25,32 +66,15 @@ struct CertKeyPair generateCertKeyPair(void) {
EVP_PKEY *pkey = NULL;
PKCS12 *p12 = NULL;
CRYPTO_mem_ctrl(CRYPTO_MEM_CHECK_ON);
bio_err = BIO_new_fp(stderr, BIO_NOCLOSE);
SSLeay_add_all_algorithms();
ERR_load_crypto_strings();
mkcert(&x509, &pkey, NUM_BITS, SERIAL, NUM_YEARS);
p12 = PKCS12_create("limelight", "GameStream", pkey, x509, NULL, 0, 0, 0, 0, 0);
if (p12 == NULL) {
printf("Error generating a valid PKCS12 certificate.\n");
}
// Debug Print statements
//RSA_print_fp(stdout, pkey->pkey.rsa, 0);
//X509_print_fp(stdout, x509);
//PEM_write_PUBKEY(stdout, pkey);
//PEM_write_PrivateKey(stdout, pkey, NULL, NULL, 0, NULL, NULL);
//PEM_write_X509(stdout, x509);
#ifndef OPENSSL_NO_ENGINE
ENGINE_cleanup();
#endif
CRYPTO_cleanup_all_ex_data();
CRYPTO_mem_leaks(bio_err);
BIO_free(bio_err);
return (CertKeyPair){x509, pkey, p12};
@@ -61,60 +85,3 @@ void freeCertKeyPair(struct CertKeyPair certKeyPair) {
EVP_PKEY_free(certKeyPair.pkey);
PKCS12_free(certKeyPair.p12);
}
int mkcert(X509 **x509p, EVP_PKEY **pkeyp, int bits, int serial, int years) {
X509 *x;
EVP_PKEY *pk;
RSA *rsa;
X509_NAME *name = NULL;
if (*pkeyp == NULL) {
if ((pk=EVP_PKEY_new()) == NULL) {
return(0);
}
} else {
pk = *pkeyp;
}
if (*x509p == NULL) {
if ((x = X509_new()) == NULL) {
goto err;
}
} else {
x = *x509p;
}
rsa = RSA_generate_key(bits, RSA_F4, NULL, NULL);
if (!EVP_PKEY_assign_RSA(pk, rsa)) {
goto err;
}
X509_set_version(x, 2);
ASN1_INTEGER_set(X509_get_serialNumber(x), serial);
X509_gmtime_adj(X509_get_notBefore(x), 0);
X509_gmtime_adj(X509_get_notAfter(x), (long)60*60*24*365*years);
X509_set_pubkey(x, pk);
name = X509_get_subject_name(x);
/* This function creates and adds the entry, working out the
* correct string type and performing checks on its length.
*/
X509_NAME_add_entry_by_txt(name,"CN", MBSTRING_ASC, (unsigned char*)"NVIDIA GameStream Client", -1, -1, 0);
/* Its self signed so set the issuer name to be the same as the
* subject.
*/
X509_set_issuer_name(x, name);
if (!X509_sign(x, pk, EVP_sha256())) {
goto err;
}
*x509p = x;
*pkeyp = pk;
return(1);
err:
return(0);
}
+2 -2
View File
@@ -18,12 +18,12 @@
height:(NSInteger)height
width:(NSInteger)width
onscreenControls:(NSInteger)onscreenControls
remote:(BOOL)streamingRemotely
optimizeGames:(BOOL)optimizeGames
multiController:(BOOL)multiController
audioOnPC:(BOOL)audioOnPC
useHevc:(BOOL)useHevc
enableHdr:(BOOL)enableHdr;
enableHdr:(BOOL)enableHdr
absoluteTouchMode:(BOOL)absoluteTouchMode;
- (NSArray*) getHosts;
- (void) updateHost:(TemporaryHost*)host;
+3 -3
View File
@@ -58,12 +58,12 @@
height:(NSInteger)height
width:(NSInteger)width
onscreenControls:(NSInteger)onscreenControls
remote:(BOOL)streamingRemotely
optimizeGames:(BOOL)optimizeGames
multiController:(BOOL)multiController
audioOnPC:(BOOL)audioOnPC
useHevc:(BOOL)useHevc
enableHdr:(BOOL)enableHdr {
enableHdr:(BOOL)enableHdr
absoluteTouchMode:(BOOL)absoluteTouchMode {
[_managedObjectContext performBlockAndWait:^{
Settings* settingsToSave = [self retrieveSettings];
@@ -72,12 +72,12 @@
settingsToSave.height = [NSNumber numberWithInteger:height];
settingsToSave.width = [NSNumber numberWithInteger:width];
settingsToSave.onscreenControls = [NSNumber numberWithInteger:onscreenControls];
settingsToSave.streamingRemotely = streamingRemotely;
settingsToSave.optimizeGames = optimizeGames;
settingsToSave.multiController = multiController;
settingsToSave.playAudioOnPC = audioOnPC;
settingsToSave.useHevc = useHevc;
settingsToSave.enableHdr = enableHdr;
settingsToSave.absoluteTouchMode = absoluteTouchMode;
[self saveData];
}];
+1 -1
View File
@@ -41,7 +41,7 @@
return YES;
}
if (![object isKindOfClass:[App class]]) {
if (![object isKindOfClass:[self class]]) {
return NO;
}
+14 -14
View File
@@ -11,24 +11,24 @@
@interface TemporaryHost : NSObject
@property (nonatomic) State state;
@property (nonatomic) PairState pairState;
@property (nonatomic, nullable) NSString * activeAddress;
@property (nonatomic, nullable) NSString * currentGame;
@property (atomic) State state;
@property (atomic) PairState pairState;
@property (atomic, nullable, retain) NSString * activeAddress;
@property (atomic, nullable, retain) NSString * currentGame;
@property (nonatomic, nullable, retain) NSData *serverCert;
@property (nonatomic, nullable, retain) NSString *address;
@property (nonatomic, nullable, retain) NSString *externalAddress;
@property (nonatomic, nullable, retain) NSString *localAddress;
@property (nonatomic, nullable, retain) NSString *ipv6Address;
@property (nonatomic, nullable, retain) NSString *mac;
@property (nonatomic) int serverCodecModeSupport;
@property (atomic, nullable, retain) NSData *serverCert;
@property (atomic, nullable, retain) NSString *address;
@property (atomic, nullable, retain) NSString *externalAddress;
@property (atomic, nullable, retain) NSString *localAddress;
@property (atomic, nullable, retain) NSString *ipv6Address;
@property (atomic, nullable, retain) NSString *mac;
@property (atomic) int serverCodecModeSupport;
NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, retain) NSString *name;
@property (nonatomic, retain) NSString *uuid;
@property (nonatomic, retain) NSMutableSet *appList;
@property (atomic, retain) NSString *name;
@property (atomic, retain) NSString *uuid;
@property (atomic, retain) NSSet *appList;
- (id) initFromHost:(Host*)host;
+1 -1
View File
@@ -90,7 +90,7 @@
return YES;
}
if (![object isKindOfClass:[Host class]]) {
if (![object isKindOfClass:[self class]]) {
return NO;
}
+1
View File
@@ -24,6 +24,7 @@
@property (nonatomic) BOOL playAudioOnPC;
@property (nonatomic) BOOL optimizeGames;
@property (nonatomic) BOOL enableHdr;
@property (nonatomic) BOOL absoluteTouchMode;
- (id) initFromSettings:(Settings*)settings;
+27 -24
View File
@@ -17,26 +17,28 @@
self.parent = settings;
#if TARGET_OS_TV
NSInteger _bitrate = [[NSUserDefaults standardUserDefaults] integerForKey:@"bitrate"];
NSInteger _framerate = [[NSUserDefaults standardUserDefaults] integerForKey:@"framerate"];
if (_bitrate) {
self.bitrate = [NSNumber numberWithInteger:_bitrate];
} else {
self.bitrate = [NSNumber numberWithInteger:20000];
// Apply default values from our Root.plist
NSString* settingsBundle = [[NSBundle mainBundle] pathForResource:@"Settings" ofType:@"bundle"];
NSDictionary* settingsData = [NSDictionary dictionaryWithContentsOfFile:[settingsBundle stringByAppendingPathComponent:@"Root.plist"]];
NSArray* preferences = [settingsData objectForKey:@"PreferenceSpecifiers"];
NSMutableDictionary* defaultsToRegister = [[NSMutableDictionary alloc] initWithCapacity:[preferences count]];
for (NSDictionary* prefSpecification in preferences) {
NSString* key = [prefSpecification objectForKey:@"Key"];
if (key != nil) {
[defaultsToRegister setObject:[prefSpecification objectForKey:@"DefaultValue"] forKey:key];
}
}
if (_framerate) {
self.framerate = [NSNumber numberWithInteger:_framerate];
} else {
self.framerate = [NSNumber numberWithInteger:60];
}
self.useHevc = [[NSUserDefaults standardUserDefaults] boolForKey:@"useHevc"] || NO;
self.playAudioOnPC = [[NSUserDefaults standardUserDefaults] boolForKey:@"audioOnPC"] || NO;
self.enableHdr = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableHdr"] || NO;
self.optimizeGames = [[NSUserDefaults standardUserDefaults] boolForKey:@"optimizeGames"] || YES;
self.multiController = [[NSUserDefaults standardUserDefaults] boolForKey:@"multipleControllers"] || YES;
[[NSUserDefaults standardUserDefaults] registerDefaults:defaultsToRegister];
self.bitrate = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"bitrate"]];
assert([self.bitrate intValue] != 0);
self.framerate = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"framerate"]];
assert([self.framerate intValue] != 0);
self.useHevc = [[NSUserDefaults standardUserDefaults] boolForKey:@"useHevc"];
self.playAudioOnPC = [[NSUserDefaults standardUserDefaults] boolForKey:@"audioOnPC"];
self.enableHdr = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableHdr"];
self.optimizeGames = [[NSUserDefaults standardUserDefaults] boolForKey:@"optimizeGames"];
self.multiController = [[NSUserDefaults standardUserDefaults] boolForKey:@"multipleControllers"];
NSInteger _screenSize = [[NSUserDefaults standardUserDefaults] integerForKey:@"streamResolution"];
switch (_screenSize) {
@@ -44,15 +46,16 @@
self.height = [NSNumber numberWithInteger:720];
self.width = [NSNumber numberWithInteger:1280];
break;
case 1:
self.height = [NSNumber numberWithInteger:1080];
self.width = [NSNumber numberWithInteger:1920];
break;
case 2:
self.height = [NSNumber numberWithInteger:2160];
self.width = [NSNumber numberWithInteger:3840];
break;
case 1:
default:
self.height = [NSNumber numberWithInteger:1080];
self.width = [NSNumber numberWithInteger:1920];
break;
abort();
}
self.onscreenControls = [NSNumber numberWithInteger:OnScreenControlsLevelOff];
#else
@@ -66,9 +69,9 @@
self.optimizeGames = settings.optimizeGames;
self.multiController = settings.multiController;
self.onscreenControls = settings.onscreenControls;
self.absoluteTouchMode = settings.absoluteTouchMode;
#endif
self.uniqueId = settings.uniqueId;
self.streamingRemotely = settings.streamingRemotely;
return self;
}
Binary file not shown.
+13 -11
View File
@@ -5,11 +5,13 @@
/Filter /FlateDecode
>>
stream
xœ]‘]! „ß{Š^ÀÊ”Àâ1<‚Ùøó êý …E␍ »ÛN‡Yp°µƒmª‰×=)´³×•÷§À×7iØ·¸Hbä"‹= ¯3_\W«F­Æ!K©%­ðÁ½ÛñÎX¤@;$Ú®‹ ”mÊê@Œ¬Ç¿ ‰µµ+¸“!ïd�±ùË}œ#¡TÙ;7_Û«†,ÚLÙ˜EˆMHS~»ì8Ðà—î�$ëô¾±ÍX™F—ØÙ´<òŸ\W¾Í¤MlæÜûþeÓeÇÿÎ é¦Lg7
xœ]‘[ƒ Eÿ³Šl )y¸Œ.¡ÃTû¡µûŸi€ Õa9×›�ÑȺ±l.zÌ+|ÀTm›ñþ48a”ä_°äÑ&&ë‘SØ^8cYÕÍÅíl"‹«Æ5ºá‚v¤�’"{
ò<9¶�j� ­7Ö!Db ×,Ú�–PÊbåÐjt¤•øzÅÚœÔ:ƒº,'á½â‚ýÒ‡ wRÁù‘Ê|µ8)’w
-{GÍÑsµ™�æ üÞG^’íosû»£MÅkäøà=h_
endstream
endobj
5 0 obj
212
214
endobj
3 0 obj
<<
@@ -22,7 +24,7 @@ endobj
2 0 obj
<< /Type /Page % 1
/Parent 1 0 R
/MediaBox [ 0 0 225 225 ]
/MediaBox [ 0 0 375 375 ]
/Contents 4 0 R
/Group <<
/Type /Group
@@ -41,7 +43,7 @@ endobj
endobj
6 0 obj
<< /Producer (cairo 1.16.0 (https://cairographics.org))
/CreationDate (D:20190830180928-07'00)
/CreationDate (D:20190922201805-07'00)
>>
endobj
7 0 obj
@@ -52,18 +54,18 @@ endobj
xref
0 8
0000000000 65535 f
0000000648 00000 n
0000000430 00000 n
0000000326 00000 n
0000000650 00000 n
0000000432 00000 n
0000000328 00000 n
0000000015 00000 n
0000000304 00000 n
0000000713 00000 n
0000000829 00000 n
0000000306 00000 n
0000000715 00000 n
0000000831 00000 n
trailer
<< /Size 8
/Root 7 0 R
/Info 6 0 R
>>
startxref
881
883
%%EOF
+11 -11
View File
@@ -5,11 +5,11 @@
/Filter /FlateDecode
>>
stream
xœeŒÑ Ã0 Dÿ5Å-Uªì(kt„Rhò¤Ýª8µ �t︓Bb�å¯B)öù̸=ó—Œõž1fã¸òä�W˜sW½P’jçjXbó†kG Ý’rî%c?õì=ÓãÍ?ÜðÒ~Óƒ~®L+·
xœUMA€ »ïý€¸� ßð ÆD=ÈAý"L4š&[»µ›€3ÉE<cJ´“ àXÐŽŒå¤`¬‡8)-A:gúWo(ÜšÂVŠ:Õ !ª±n«bμú”?©ØB*žL¸ܶðÿUS3␍tH(å
endstream
endobj
5 0 obj
115
110
endobj
3 0 obj
<<
@@ -21,7 +21,7 @@ endobj
2 0 obj
<< /Type /Page % 1
/Parent 1 0 R
/MediaBox [ 0 0 75 75 ]
/MediaBox [ 0 0 150 150 ]
/Contents 4 0 R
/Group <<
/Type /Group
@@ -40,7 +40,7 @@ endobj
endobj
6 0 obj
<< /Producer (cairo 1.16.0 (https://cairographics.org))
/CreationDate (D:20190830180932-07'00)
/CreationDate (D:20190922201819-07'00)
>>
endobj
7 0 obj
@@ -51,18 +51,18 @@ endobj
xref
0 8
0000000000 65535 f
0000000517 00000 n
0000000301 00000 n
0000000229 00000 n
0000000514 00000 n
0000000296 00000 n
0000000224 00000 n
0000000015 00000 n
0000000207 00000 n
0000000582 00000 n
0000000698 00000 n
0000000202 00000 n
0000000579 00000 n
0000000695 00000 n
trailer
<< /Size 8
/Root 7 0 R
/Info 6 0 R
>>
startxref
750
747
%%EOF
Binary file not shown.
@@ -1,21 +0,0 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "settings.pdf",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
@@ -1,70 +0,0 @@
%PDF-1.5
%µí®û
4 0 obj
<< /Length 5 0 R
/Filter /FlateDecode
>>
stream
xœeTIŽÜ0 ¼óú@‰Z,=#O ÌÌA}Hò ,.== °»d®Å¢j*òûVåq�t?èÕ„ßï÷ôýgIï¨]y¤V3�ôH­ç2Ù¡?¿ÁøvÐKÞm¹¥Þòåö÷ ƒþ|=òp,EØÏ„ö÷N4vnbjÏGª#OxRå\ÇL¯68Z+€™ß‰ �p´÷ä¼*G”iÆî3ÃÕ}ÓQ¯Þã ^Ê‘ûÜÿÄ<O¬ER„úR\¸Jï-ï2�‘G
´2×␍OA nŸ;J^CXFR¾V»!PïJ” 8 šÂ�øv8␍Ö,G³Î␍<Q°À.¾×¥VÛ�|ÄX¯Üa\2zí37)qT+fä‚䌗iGJ‚«¦ílyVîÒÏÖ K�iZã¦Wû¦ˆ Î–+¿L·±5jX�~µP‚J«šH�)»~6xïè␍^¼´@i°ŒÎE_wbަ.e‹«%V^PsÙg.ÙâêŒê´^ºrw€J0Ê„—°$Ã!@’ ti^[•­µiZØÅ€¬»‡PADt?áTúÃqê"zÔáô¤B�횕Ԕ Šz[±²naä·÷*ô Ë "·ÊÁ+Ã$úÇ;Hæ®*ò°2�á@qlÂóÙÁ³w‚␍WªÒωѣV9vYø2ºhžW„«Ê0™1s7pAz(—+©\ÎÞÃ�ÚU=êD±
l èk"␍š}�Ę•-_²—m }%ßO0ë›®½Ïë»-X·Ëö¾Ùú­€yÖ␍¶üÖZÛy^)&†¸nL(¯WÑÿWÓý ¿ ¾?1
endstream
endobj
5 0 obj
608
endobj
3 0 obj
<<
/ExtGState <<
/a0 << /CA 1 /ca 1 >>
>>
>>
endobj
2 0 obj
<< /Type /Page % 1
/Parent 1 0 R
/MediaBox [ 0 0 75 75 ]
/Contents 4 0 R
/Group <<
/Type /Group
/S /Transparency
/I true
/CS /DeviceRGB
>>
/Resources 3 0 R
>>
endobj
1 0 obj
<< /Type /Pages
/Kids [ 2 0 R ]
/Count 1
>>
endobj
6 0 obj
<< /Producer (cairo 1.16.0 (https://cairographics.org))
/CreationDate (D:20190830202122-07'00)
>>
endobj
7 0 obj
<< /Type /Catalog
/Pages 1 0 R
>>
endobj
xref
0 8
0000000000 65535 f
0000001010 00000 n
0000000794 00000 n
0000000722 00000 n
0000000015 00000 n
0000000700 00000 n
0000001075 00000 n
0000001191 00000 n
trailer
<< /Size 8
/Root 7 0 R
/Info 6 0 R
>>
startxref
1243
%%EOF
+1
View File
@@ -14,6 +14,7 @@
@property (nonatomic) int playerIndex;
@property (nonatomic) int lastButtonFlags;
@property (nonatomic) int emulatingButtonFlags;
@property (nonatomic) int supportedEmulationFlags;
@property (nonatomic) unsigned char lastLeftTrigger;
@property (nonatomic) unsigned char lastRightTrigger;
@property (nonatomic) short lastLeftStickX;
+11 -16
View File
@@ -6,35 +6,31 @@
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
//
// Swift
#if !TARGET_OS_IPHONE
#import "Gamepad.h"
#endif
#import "StreamConfiguration.h"
#import "Controller.h"
@class OnScreenControls;
@protocol GamepadPresenceDelegate <NSObject>
- (void) gamepadPresenceChanged;
@end
@interface ControllerSupport : NSObject
-(id) initWithConfig:(StreamConfiguration*)streamConfig;
-(id) initWithConfig:(StreamConfiguration*)streamConfig presenceDelegate:(id<GamepadPresenceDelegate>)delegate;
#if TARGET_OS_IPHONE
-(void) initAutoOnScreenControlMode:(OnScreenControls*)osc;
-(void) cleanup;
-(Controller*) getOscController;
#else
-(void) assignGamepad:(struct Gamepad_device *)gamepad;
-(void) removeGamepad:(struct Gamepad_device *)gamepad;
-(NSMutableDictionary*) getControllers;
#endif
-(void) updateLeftStick:(Controller*)controller x:(short)x y:(short)y;
-(void) updateRightStick:(Controller*)controller x:(short)x y:(short)y;
-(void) updateLeftTrigger:(Controller*)controller left:(char)left;
-(void) updateRightTrigger:(Controller*)controller right:(char)right;
-(void) updateTriggers:(Controller*)controller left:(char)left right:(char)right;
-(void) updateLeftTrigger:(Controller*)controller left:(unsigned char)left;
-(void) updateRightTrigger:(Controller*)controller right:(unsigned char)right;
-(void) updateTriggers:(Controller*)controller left:(unsigned char)left right:(unsigned char)right;
-(void) updateButtonFlags:(Controller*)controller flags:(int)flags;
-(void) setButtonFlag:(Controller*)controller flags:(int)flags;
@@ -46,7 +42,6 @@
+(int) getConnectedGamepadMask:(StreamConfiguration*)streamConfig;
@property (nonatomic, strong) id connectObserver;
@property (nonatomic, strong) id disconnectObserver;
-(NSUInteger) getConnectedGamepadCount;
@end
+213 -71
View File
@@ -10,10 +10,6 @@
#import "Controller.h"
#import "OnScreenControls.h"
#if !TARGET_OS_IPHONE
#import "Gamepad.h"
#import "Control.h"
#endif
#import "DataManager.h"
#include "Limelight.h"
@@ -21,10 +17,24 @@
@import GameController;
@import AudioToolbox;
static const double MOUSE_SPEED_DIVISOR = 2.5;
@implementation ControllerSupport {
id _controllerConnectObserver;
id _controllerDisconnectObserver;
id _mouseConnectObserver;
id _mouseDisconnectObserver;
id _keyboardConnectObserver;
id _keyboardDisconnectObserver;
NSLock *_controllerStreamLock;
NSMutableDictionary *_controllers;
NSTimer *_rumbleTimer;
id<GamepadPresenceDelegate> _presenceDelegate;
float accumulatedDeltaX;
float accumulatedDeltaY;
float accumulatedScrollY;
OnScreenControls *_osc;
@@ -94,21 +104,21 @@
}
}
-(void) updateLeftTrigger:(Controller*)controller left:(char)left
-(void) updateLeftTrigger:(Controller*)controller left:(unsigned char)left
{
@synchronized(controller) {
controller.lastLeftTrigger = left;
}
}
-(void) updateRightTrigger:(Controller*)controller right:(char)right
-(void) updateRightTrigger:(Controller*)controller right:(unsigned char)right
{
@synchronized(controller) {
controller.lastRightTrigger = right;
}
}
-(void) updateTriggers:(Controller*) controller left:(char)left right:(char)right
-(void) updateTriggers:(Controller*) controller left:(unsigned char)left right:(unsigned char)right
{
@synchronized(controller) {
controller.lastLeftTrigger = left;
@@ -137,15 +147,19 @@
if (controller.lastButtonFlags & PLAY_FLAG) {
// If LB and start are down, trigger select
if (controller.lastButtonFlags & LB_FLAG) {
controller.lastButtonFlags |= BACK_FLAG;
controller.lastButtonFlags &= ~(pressedButtons & (PLAY_FLAG | LB_FLAG));
controller.emulatingButtonFlags |= EMULATING_SELECT;
if (controller.supportedEmulationFlags & EMULATING_SELECT) {
controller.lastButtonFlags |= BACK_FLAG;
controller.lastButtonFlags &= ~(pressedButtons & (PLAY_FLAG | LB_FLAG));
controller.emulatingButtonFlags |= EMULATING_SELECT;
}
}
// If (RB or select) and start are down, trigger special
else if ((controller.lastButtonFlags & RB_FLAG) || (controller.lastButtonFlags & BACK_FLAG)) {
controller.lastButtonFlags |= SPECIAL_FLAG;
controller.lastButtonFlags &= ~(pressedButtons & (PLAY_FLAG | RB_FLAG | BACK_FLAG));
controller.emulatingButtonFlags |= EMULATING_SPECIAL;
if (controller.supportedEmulationFlags & EMULATING_SPECIAL) {
controller.lastButtonFlags |= SPECIAL_FLAG;
controller.lastButtonFlags &= ~(pressedButtons & (PLAY_FLAG | RB_FLAG | BACK_FLAG));
controller.emulatingButtonFlags |= EMULATING_SPECIAL;
}
}
}
@@ -194,6 +208,15 @@
[_controllerStreamLock unlock];
}
+(BOOL) hasKeyboardOrMouse {
if (@available(iOS 14.0, tvOS 14.0, *)) {
return GCMouse.mice.count > 0 || GCKeyboard.coalescedKeyboard != nil;
}
else {
return NO;
}
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
@@ -214,12 +237,21 @@
-(void) registerControllerCallbacks:(GCController*) controller
{
if (controller != NULL) {
// On iOS 13, we want to use the new buttonMenu property which lets users hold down Start.
// On prior versions, we must use the controllerPausedHandler.
// iOS 13 allows the Start button to behave like a normal button, however
// older MFi controllers can send an instant down+up event for the start button
// which means the button will not be down long enough to register on the PC.
// To work around this issue, use the old controllerPausedHandler if the controller
// doesn't have a Select button (which indicates it probably doesn't have a proper
// Start button either).
BOOL useLegacyPausedHandler = YES;
if (@available(iOS 13.0, tvOS 13.0, *)) {
// Nothing - buttonMenu is already handled in valueChangedHandler
if (controller.extendedGamepad != nil &&
controller.extendedGamepad.buttonOptions != nil) {
useLegacyPausedHandler = NO;
}
}
else {
if (useLegacyPausedHandler) {
controller.controllerPausedHandler = ^(GCController *controller) {
Controller* limeController = [self->_controllers objectForKey:[NSNumber numberWithInteger:controller.playerIndex]];
@@ -242,7 +274,7 @@
Controller* limeController = [self->_controllers objectForKey:[NSNumber numberWithInteger:gamepad.controller.playerIndex]];
short leftStickX, leftStickY;
short rightStickX, rightStickY;
char leftTrigger, rightTrigger;
unsigned char leftTrigger, rightTrigger;
UPDATE_BUTTON_FLAG(limeController, A_FLAG, gamepad.buttonA.pressed);
UPDATE_BUTTON_FLAG(limeController, B_FLAG, gamepad.buttonB.pressed);
@@ -268,12 +300,13 @@
}
if (@available(iOS 13.0, tvOS 13.0, *)) {
// Menu button is mandatory, so no need to check for nil
UPDATE_BUTTON_FLAG(limeController, PLAY_FLAG, gamepad.buttonMenu.pressed);
// Options button is optional (only present on Xbox One S and PS4 gamepads)
if (gamepad.buttonOptions != nil) {
UPDATE_BUTTON_FLAG(limeController, BACK_FLAG, gamepad.buttonOptions.pressed);
// For older MFi gamepads, the menu button will already be handled by
// the controllerPausedHandler.
UPDATE_BUTTON_FLAG(limeController, PLAY_FLAG, gamepad.buttonMenu.pressed);
}
}
@@ -316,6 +349,71 @@
}
}
-(void) unregisterMouseCallbacks:(GCMouse*)mouse API_AVAILABLE(ios(14.0)) {
mouse.mouseInput.mouseMovedHandler = nil;
mouse.mouseInput.leftButton.pressedChangedHandler = nil;
mouse.mouseInput.middleButton.pressedChangedHandler = nil;
mouse.mouseInput.rightButton.pressedChangedHandler = nil;
for (GCControllerButtonInput* auxButton in mouse.mouseInput.auxiliaryButtons) {
auxButton.pressedChangedHandler = nil;
}
}
-(void) registerMouseCallbacks:(GCMouse*) mouse API_AVAILABLE(ios(14.0)) {
mouse.mouseInput.mouseMovedHandler = ^(GCMouseInput * _Nonnull mouse, float deltaX, float deltaY) {
self->accumulatedDeltaX += deltaX / MOUSE_SPEED_DIVISOR;
self->accumulatedDeltaY += -deltaY / MOUSE_SPEED_DIVISOR;
short truncatedDeltaX = (short)self->accumulatedDeltaX;
short truncatedDeltaY = (short)self->accumulatedDeltaY;
if (truncatedDeltaX != 0 || truncatedDeltaY != 0) {
LiSendMouseMoveEvent(truncatedDeltaX, truncatedDeltaY);
self->accumulatedDeltaX -= truncatedDeltaX;
self->accumulatedDeltaY -= truncatedDeltaY;
}
};
mouse.mouseInput.leftButton.pressedChangedHandler = ^(GCControllerButtonInput * _Nonnull button, float value, BOOL pressed) {
LiSendMouseButtonEvent(pressed ? BUTTON_ACTION_PRESS : BUTTON_ACTION_RELEASE, BUTTON_LEFT);
};
mouse.mouseInput.middleButton.pressedChangedHandler = ^(GCControllerButtonInput * _Nonnull button, float value, BOOL pressed) {
LiSendMouseButtonEvent(pressed ? BUTTON_ACTION_PRESS : BUTTON_ACTION_RELEASE, BUTTON_MIDDLE);
};
mouse.mouseInput.rightButton.pressedChangedHandler = ^(GCControllerButtonInput * _Nonnull button, float value, BOOL pressed) {
LiSendMouseButtonEvent(pressed ? BUTTON_ACTION_PRESS : BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
};
if (mouse.mouseInput.auxiliaryButtons != nil) {
if (mouse.mouseInput.auxiliaryButtons.count >= 1) {
mouse.mouseInput.auxiliaryButtons[0].pressedChangedHandler = ^(GCControllerButtonInput * _Nonnull button, float value, BOOL pressed) {
LiSendMouseButtonEvent(pressed ? BUTTON_ACTION_PRESS : BUTTON_ACTION_RELEASE, BUTTON_X1);
};
}
if (mouse.mouseInput.auxiliaryButtons.count >= 2) {
mouse.mouseInput.auxiliaryButtons[1].pressedChangedHandler = ^(GCControllerButtonInput * _Nonnull button, float value, BOOL pressed) {
LiSendMouseButtonEvent(pressed ? BUTTON_ACTION_PRESS : BUTTON_ACTION_RELEASE, BUTTON_X2);
};
}
}
// TODO: Confirm scroll direction
mouse.mouseInput.scroll.yAxis.valueChangedHandler = ^(GCControllerAxisInput * _Nonnull axis, float value) {
self->accumulatedScrollY += -value;
short truncatedScrollY = (short)self->accumulatedScrollY;
if (truncatedScrollY != 0) {
LiSendHighResScrollEvent(truncatedScrollY);
self->accumulatedScrollY -= truncatedScrollY;
}
};
}
-(void) updateAutoOnScreenControlMode
{
// Auto on-screen control support may not be enabled
@@ -354,9 +452,18 @@
}
}
#if TARGET_OS_IPHONE
// If we didn't find a gamepad present and we have a keyboard or mouse, turn
// the on-screen controls off to get the overlays out of the way.
if (level == OnScreenControlsLevelFull && [ControllerSupport hasKeyboardOrMouse]) {
level = OnScreenControlsLevelOff;
// Ensure the virtual gamepad disappears to avoid confusing some games.
// If the mouse and keyboard disconnect later, it will reappear when the
// first OSC input is received.
LiSendMultiControllerEvent(0, 0, 0, 0, 0, 0, 0, 0, 0);
}
[_osc setLevel:level];
#endif
}
-(void) initAutoOnScreenControlMode:(OnScreenControls*)osc
@@ -382,7 +489,16 @@
limeController.playerIndex = i;
}
limeController.supportedEmulationFlags = EMULATING_SPECIAL | EMULATING_SELECT;
limeController.gamepad = controller;
if (@available(iOS 13.0, tvOS 13.0, *)) {
if (controller.extendedGamepad != nil &&
controller.extendedGamepad.buttonOptions != nil) {
// Disable select button emulation since we have a physical select button
limeController.supportedEmulationFlags &= ~EMULATING_SELECT;
}
}
[_controllers setObject:limeController forKey:[NSNumber numberWithInteger:controller.playerIndex]];
@@ -392,40 +508,9 @@
}
}
#if TARGET_OS_IPHONE
-(Controller*) getOscController {
return _player0osc;
}
#else
-(NSMutableDictionary*) getControllers {
return _controllers;
}
-(void) assignGamepad:(struct Gamepad_device *)gamepad {
for (int i = 0; i < 4; i++) {
if (!(_controllerNumbers & (1 << i))) {
_controllerNumbers |= (1 << i);
gamepad->deviceID = i;
NSLog(@"Gamepad device id: %u assigned", gamepad->deviceID);
Controller* limeController;
limeController = [[Controller alloc] init];
limeController.playerIndex = i;
[_controllers setObject:limeController forKey:[NSNumber numberWithInteger:i]];
break;
}
}
}
-(void) removeGamepad:(struct Gamepad_device *)gamepad {
_controllerNumbers &= ~(1 << gamepad->deviceID);
Log(LOG_I, @"Unassigning controller index: %ld", (long)gamepad->deviceID);
// Inform the server of the updated active gamepads before removing this controller
[self updateFinished:[_controllers objectForKey:[NSNumber numberWithInteger:gamepad->deviceID]]];
[_controllers removeObjectForKey:[NSNumber numberWithInteger:gamepad->deviceID]];
}
#endif
+(bool) isSupportedGamepad:(GCController*) controller {
return controller.extendedGamepad != nil || controller.gamepad != nil;
@@ -462,16 +547,14 @@
mask = 0x1;
}
#if TARGET_OS_IPHONE
DataManager* dataMan = [[DataManager alloc] init];
OnScreenControlsLevel level = (OnScreenControlsLevel)[[dataMan getSettings].onscreenControls integerValue];
// Even if no gamepads are present, we will always count one
// if OSC is enabled.
if (level != OnScreenControlsLevelOff) {
// Even if no gamepads are present, we will always count one if OSC is enabled,
// or it's set to auto and no keyboard or mouse is present.
if (level != OnScreenControlsLevelOff && (![ControllerSupport hasKeyboardOrMouse] || level != OnScreenControlsLevelAuto)) {
mask |= 0x1;
}
#endif
return mask;
}
@@ -493,7 +576,12 @@
}
}
-(id) initWithConfig:(StreamConfiguration*)streamConfig
-(NSUInteger) getConnectedGamepadCount
{
return _controllers.count;
}
-(id) initWithConfig:(StreamConfiguration*)streamConfig presenceDelegate:(id<GamepadPresenceDelegate>)delegate
{
self = [super init];
@@ -501,18 +589,13 @@
_controllers = [[NSMutableDictionary alloc] init];
_controllerNumbers = 0;
_multiController = streamConfig.multiController;
_presenceDelegate = delegate;
_player0osc = [[Controller alloc] init];
_player0osc.playerIndex = 0;
#if TARGET_OS_IPHONE
DataManager* dataMan = [[DataManager alloc] init];
_oscEnabled = (OnScreenControlsLevel)[[dataMan getSettings].onscreenControls integerValue] != OnScreenControlsLevelOff;
#else
_oscEnabled = false;
initGamepad(self);
Gamepad_detectDevices();
#endif
_rumbleTimer = [NSTimer scheduledTimerWithTimeInterval:0.20
target:self
@@ -527,11 +610,16 @@
if ([ControllerSupport isSupportedGamepad:controller]) {
[self assignController:controller];
[self registerControllerCallbacks:controller];
[self updateAutoOnScreenControlMode];
}
}
self.connectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCControllerDidConnectNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
if (@available(iOS 14.0, tvOS 14.0, *)) {
for (GCMouse* mouse in [GCMouse mice]) {
[self registerMouseCallbacks:mouse];
}
}
_controllerConnectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCControllerDidConnectNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
Log(LOG_I, @"Controller connected!");
GCController* controller = note.object;
@@ -548,8 +636,11 @@
// Re-evaluate the on-screen control mode
[self updateAutoOnScreenControlMode];
// Notify the delegate
[self->_presenceDelegate gamepadPresenceChanged];
}];
self.disconnectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCControllerDidDisconnectNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
_controllerDisconnectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCControllerDidDisconnectNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
Log(LOG_I, @"Controller disconnected!");
GCController* controller = note.object;
@@ -573,7 +664,48 @@
// Re-evaluate the on-screen control mode
[self updateAutoOnScreenControlMode];
// Notify the delegate
[self->_presenceDelegate gamepadPresenceChanged];
}];
if (@available(iOS 14.0, tvOS 14.0, *)) {
_mouseConnectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCMouseDidConnectNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
Log(LOG_I, @"Mouse connected!");
GCMouse* mouse = note.object;
// Register for mouse events
[self registerMouseCallbacks: mouse];
// Re-evaluate the on-screen control mode
[self updateAutoOnScreenControlMode];
}];
_mouseDisconnectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCMouseDidDisconnectNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
Log(LOG_I, @"Mouse disconnected!");
GCMouse* mouse = note.object;
// Unregister for mouse events
[self unregisterMouseCallbacks: mouse];
// Re-evaluate the on-screen control mode
[self updateAutoOnScreenControlMode];
}];
_keyboardConnectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCKeyboardDidConnectNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
Log(LOG_I, @"Keyboard connected!");
// Re-evaluate the on-screen control mode
[self updateAutoOnScreenControlMode];
}];
_keyboardDisconnectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCKeyboardDidDisconnectNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
Log(LOG_I, @"Keyboard disconnected!");
// Re-evaluate the on-screen control mode
[self updateAutoOnScreenControlMode];
}];
}
return self;
}
@@ -581,8 +713,12 @@
{
[_rumbleTimer invalidate];
_rumbleTimer = nil;
[[NSNotificationCenter defaultCenter] removeObserver:self.connectObserver];
[[NSNotificationCenter defaultCenter] removeObserver:self.disconnectObserver];
[[NSNotificationCenter defaultCenter] removeObserver:_controllerConnectObserver];
[[NSNotificationCenter defaultCenter] removeObserver:_controllerDisconnectObserver];
[[NSNotificationCenter defaultCenter] removeObserver:_mouseConnectObserver];
[[NSNotificationCenter defaultCenter] removeObserver:_mouseDisconnectObserver];
[[NSNotificationCenter defaultCenter] removeObserver:_keyboardConnectObserver];
[[NSNotificationCenter defaultCenter] removeObserver:_keyboardDisconnectObserver];
[_controllers removeAllObjects];
_controllerNumbers = 0;
for (GCController* controller in [GCController controllers]) {
@@ -590,6 +726,12 @@
[self unregisterControllerCallbacks:controller];
}
}
if (@available(iOS 14.0, tvOS 14.0, *)) {
for (GCMouse* mouse in [GCMouse mice]) {
[self unregisterMouseCallbacks:mouse];
}
}
}
@end
+1
View File
@@ -16,6 +16,7 @@ struct KeyEvent {
u_char modifier;
};
+ (BOOL)sendKeyEvent:(UIKey*)key down:(BOOL)down API_AVAILABLE(ios(13.4));
+ (struct KeyEvent) translateKeyEvent:(unichar) inputChar withModifierFlags:(UIKeyModifierFlags)modifierFlags;
@end
+211 -1
View File
@@ -11,6 +11,216 @@
@implementation KeyboardSupport
+ (BOOL)sendKeyEvent:(UIKey*)key down:(BOOL)down API_AVAILABLE(ios(13.4)){
char modifierFlags = 0;
short keyCode = 0;
if (key.modifierFlags & UIKeyModifierShift) {
modifierFlags |= MODIFIER_SHIFT;
}
if (key.modifierFlags & UIKeyModifierAlternate) {
modifierFlags |= MODIFIER_ALT;
}
if (key.modifierFlags & (UIKeyModifierControl | UIKeyModifierCommand)) {
modifierFlags |= MODIFIER_CTRL;
}
// This converts UIKeyboardHIDUsage values to Win32 VK_* values
// https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
if (key.keyCode >= UIKeyboardHIDUsageKeyboardA &&
key.keyCode <= UIKeyboardHIDUsageKeyboardZ) {
keyCode = (key.keyCode - UIKeyboardHIDUsageKeyboardA) + 0x41;
}
else if (key.keyCode == UIKeyboardHIDUsageKeyboard0) {
// This key is at the beginning of the VK_ range but the end
// of the UIKeyboardHIDUsageKeyboard range.
keyCode = 0x30;
}
else if (key.keyCode >= UIKeyboardHIDUsageKeyboard1 &&
key.keyCode <= UIKeyboardHIDUsageKeyboard9) {
keyCode = (key.keyCode - UIKeyboardHIDUsageKeyboard1) + 0x31;
}
else if (key.keyCode == UIKeyboardHIDUsageKeypad0) {
// This key is at the beginning of the VK_ range but the end
// of the UIKeyboardHIDUsageKeypad range.
keyCode = 0x60;
}
else if (key.keyCode >= UIKeyboardHIDUsageKeypad1 &&
key.keyCode <= UIKeyboardHIDUsageKeypad9) {
keyCode = (key.keyCode - UIKeyboardHIDUsageKeypad1) + 0x61;
}
else if (key.keyCode >= UIKeyboardHIDUsageKeyboardF1 &&
key.keyCode <= UIKeyboardHIDUsageKeyboardF12) {
keyCode = (key.keyCode - UIKeyboardHIDUsageKeyboardF1) + 0x70;
}
else if (key.keyCode >= UIKeyboardHIDUsageKeyboardF13 &&
key.keyCode <= UIKeyboardHIDUsageKeyboardF24) {
keyCode = (key.keyCode - UIKeyboardHIDUsageKeyboardF13) + 0x7C;
}
else {
switch (key.keyCode) {
case UIKeyboardHIDUsageKeyboardReturnOrEnter:
keyCode = 0x0D;
break;
case UIKeyboardHIDUsageKeyboardEscape:
keyCode = 0x1B;
break;
case UIKeyboardHIDUsageKeyboardDeleteOrBackspace:
keyCode = 0x08;
break;
case UIKeyboardHIDUsageKeyboardTab:
keyCode = 0x09;
break;
case UIKeyboardHIDUsageKeyboardSpacebar:
keyCode = 0x20;
break;
case UIKeyboardHIDUsageKeyboardHyphen:
keyCode = 0xBD;
break;
case UIKeyboardHIDUsageKeyboardEqualSign:
keyCode = 0xBB;
break;
case UIKeyboardHIDUsageKeyboardOpenBracket:
keyCode = 0xDB;
break;
case UIKeyboardHIDUsageKeyboardCloseBracket:
keyCode = 0xDD;
break;
case UIKeyboardHIDUsageKeyboardBackslash:
keyCode = 0xDC;
break;
case UIKeyboardHIDUsageKeyboardSemicolon:
keyCode = 0xBA;
break;
case UIKeyboardHIDUsageKeyboardQuote:
keyCode = 0xDE;
break;
case UIKeyboardHIDUsageKeyboardGraveAccentAndTilde:
keyCode = 0xC0;
break;
case UIKeyboardHIDUsageKeyboardComma:
keyCode = 0xBC;
break;
case UIKeyboardHIDUsageKeyboardPeriod:
keyCode = 0xBE;
break;
case UIKeyboardHIDUsageKeyboardSlash:
keyCode = 0xBF;
break;
case UIKeyboardHIDUsageKeyboardCapsLock:
keyCode = 0x14;
break;
case UIKeyboardHIDUsageKeyboardPrintScreen:
keyCode = 0x2A;
break;
case UIKeyboardHIDUsageKeyboardScrollLock:
keyCode = 0x91;
break;
case UIKeyboardHIDUsageKeyboardPause:
keyCode = 0x13;
break;
case UIKeyboardHIDUsageKeyboardInsert:
keyCode = 0x2D;
break;
case UIKeyboardHIDUsageKeyboardHome:
keyCode = 0x24;
break;
case UIKeyboardHIDUsageKeyboardPageUp:
keyCode = 0x21;
break;
case UIKeyboardHIDUsageKeyboardDeleteForward:
keyCode = 0x2E;
break;
case UIKeyboardHIDUsageKeyboardEnd:
keyCode = 0x23;
break;
case UIKeyboardHIDUsageKeyboardPageDown:
keyCode = 0x22;
break;
case UIKeyboardHIDUsageKeyboardRightArrow:
keyCode = 0x27;
break;
case UIKeyboardHIDUsageKeyboardLeftArrow:
keyCode = 0x25;
break;
case UIKeyboardHIDUsageKeyboardDownArrow:
keyCode = 0x28;
break;
case UIKeyboardHIDUsageKeyboardUpArrow:
keyCode = 0x26;
break;
case UIKeyboardHIDUsageKeypadNumLock:
keyCode = 0x90;
break;
case UIKeyboardHIDUsageKeypadSlash:
keyCode = 0x6F;
break;
case UIKeyboardHIDUsageKeypadAsterisk:
keyCode = 0x6A;
break;
case UIKeyboardHIDUsageKeypadHyphen:
keyCode = 0x6D;
break;
case UIKeyboardHIDUsageKeypadPlus:
keyCode = 0x6B;
break;
case UIKeyboardHIDUsageKeypadEnter:
keyCode = 0x0D; // FIXME: Is this correct?
break;
case UIKeyboardHIDUsageKeypadPeriod:
keyCode = 0x6E;
break;
case UIKeyboardHIDUsageKeyboardNonUSBackslash:
keyCode = 0xE2;
break;
case UIKeyboardHIDUsageKeypadComma:
keyCode = 0x6C;
break;
case UIKeyboardHIDUsageKeyboardCancel:
keyCode = 0x03;
break;
case UIKeyboardHIDUsageKeyboardClear:
keyCode = 0x0C;
break;
case UIKeyboardHIDUsageKeyboardCrSelOrProps:
keyCode = 0xF7;
break;
case UIKeyboardHIDUsageKeyboardExSel:
keyCode = 0xF8;
break;
case UIKeyboardHIDUsageKeyboardLeftGUI:
case UIKeyboardHIDUsageKeyboardLeftControl:
keyCode = 0xA2;
break;
case UIKeyboardHIDUsageKeyboardLeftShift:
keyCode = 0xA0;
break;
case UIKeyboardHIDUsageKeyboardLeftAlt:
keyCode = 0xA4;
break;
case UIKeyboardHIDUsageKeyboardRightGUI:
case UIKeyboardHIDUsageKeyboardRightControl:
keyCode = 0xA3;
break;
case UIKeyboardHIDUsageKeyboardRightShift:
keyCode = 0xA1;
break;
case UIKeyboardHIDUsageKeyboardRightAlt:
keyCode = 0xA5;
break;
default:
NSLog(@"Unhandled HID usage: %lu", (unsigned long)key.keyCode);
assert(0);
return false;
}
}
LiSendKeyboardEvent(keyCode,
down ? KEY_ACTION_DOWN : KEY_ACTION_UP,
modifierFlags);
return true;
}
+ (struct KeyEvent)translateKeyEvent:(unichar)inputChar withModifierFlags:(UIKeyModifierFlags)modifierFlags {
struct KeyEvent event;
event.keycode = 0;
@@ -22,13 +232,13 @@
case UIKeyModifierShift:
[KeyboardSupport addShiftModifier:&event];
break;
case UIKeyModifierCommand:
case UIKeyModifierControl:
[KeyboardSupport addControlModifier:&event];
break;
case UIKeyModifierAlternate:
[KeyboardSupport addAltModifier:&event];
break;
case UIKeyModifierCommand:
case UIKeyModifierNumericPad:
break;
}
+3 -3
View File
@@ -7,8 +7,8 @@
//
#import <Foundation/Foundation.h>
#import "StreamView.h"
@protocol EdgeDetectionDelegate;
@class ControllerSupport;
@interface OnScreenControls : NSObject
@@ -25,12 +25,12 @@ typedef NS_ENUM(NSInteger, OnScreenControlsLevel) {
OnScreenControlsLevelAutoGCExtendedGamepadWithStickButtons
};
#if TARGET_OS_IPHONE
- (id) initWithView:(UIView*)view controllerSup:(ControllerSupport*)controllerSupport swipeDelegate:(id<EdgeDetectionDelegate>)edgeDelegate;
- (BOOL) handleTouchDownEvent:(NSSet*)touches;
- (BOOL) handleTouchUpEvent:(NSSet*)touches;
- (BOOL) handleTouchMovedEvent:(NSSet*)touches;
- (void) setLevel:(OnScreenControlsLevel)level;
#endif
- (OnScreenControlsLevel) getLevel;
- (void) show;
@end
+19 -2
View File
@@ -7,6 +7,7 @@
//
#import "OnScreenControls.h"
#import "StreamView.h"
#import "ControllerSupport.h"
#import "Controller.h"
#include "Limelight.h"
@@ -64,6 +65,7 @@
CGRect _controlArea;
UIView* _view;
OnScreenControlsLevel _level;
BOOL _visible;
ControllerSupport *_controllerSupport;
Controller *_controller;
@@ -156,14 +158,29 @@ static float L3_Y;
_rightStick = [CALayer layer];
_edge = [CALayer layer];
return self;
}
- (void) show {
_visible = YES;
[self setupEdgeDetection];
return self;
[self updateControls];
}
- (void) setLevel:(OnScreenControlsLevel)level {
_level = level;
[self updateControls];
// Only update controls if we're showing, otherwise
// show will do it for us.
if (_visible) {
[self updateControls];
}
}
- (OnScreenControlsLevel) getLevel {
return _level;
}
- (void) updateControls {
+19 -3
View File
@@ -7,6 +7,8 @@
//
#import "ControllerSupport.h"
#import "OnScreenControls.h"
#import "StreamConfiguration.h"
@protocol EdgeDetectionDelegate <NSObject>
@@ -14,12 +16,26 @@
@end
@protocol UserInteractionDelegate <NSObject>
- (void) userInteractionBegan;
- (void) userInteractionEnded;
@end
#if TARGET_OS_TV
@interface StreamView : OSView <UITextFieldDelegate>
#else
@interface StreamView : OSView <UITextFieldDelegate, UIPointerInteractionDelegate>
#endif
@property (nonatomic, retain) IBOutlet UITextField* keyInputField;
- (void) setupStreamView;
- (void) setupOnScreenControls:(ControllerSupport*)controllerSupport swipeDelegate:(id<EdgeDetectionDelegate>)swipeDelegate;
- (void) setMouseDeltaFactors:(float)x y:(float)y;
- (void) setupStreamView:(ControllerSupport*)controllerSupport
swipeDelegate:(id<EdgeDetectionDelegate>)swipeDelegate
interactionDelegate:(id<UserInteractionDelegate>)interactionDelegate
config:(StreamConfiguration*)streamConfig;
- (void) showOnScreenControls;
- (OnScreenControlsLevel) getCurrentOscState;
@end
+335 -27
View File
@@ -8,11 +8,13 @@
#import "StreamView.h"
#include <Limelight.h>
#import "OnScreenControls.h"
#import "DataManager.h"
#import "ControllerSupport.h"
#import "KeyboardSupport.h"
static const int REFERENCE_WIDTH = 1280;
static const int REFERENCE_HEIGHT = 720;
@implementation StreamView {
CGPoint touchLocation, originalLocation;
BOOL touchMoved;
@@ -22,26 +24,38 @@
BOOL isDragging;
NSTimer* dragTimer;
float xDeltaFactor;
float yDeltaFactor;
float screenFactor;
float streamAspectRatio;
// iOS 13.4 mouse support
NSInteger lastMouseButtonMask;
float lastMouseX;
float lastMouseY;
// iOS 14 mouse support
double accumulatedMouseDeltaX;
double accumulatedMouseDeltaY;
#if TARGET_OS_TV
UIGestureRecognizer* remotePressRecognizer;
UIGestureRecognizer* remoteLongPressRecognizer;
#endif
id<UserInteractionDelegate> interactionDelegate;
NSTimer* interactionTimer;
BOOL hasUserInteracted;
NSDictionary<NSString *, NSNumber *> *dictCodes;
}
- (void) setMouseDeltaFactors:(float)x y:(float)y {
xDeltaFactor = x;
yDeltaFactor = y;
- (void) setupStreamView:(ControllerSupport*)controllerSupport
swipeDelegate:(id<EdgeDetectionDelegate>)swipeDelegate
interactionDelegate:(id<UserInteractionDelegate>)interactionDelegate
config:(StreamConfiguration*)streamConfig {
self->interactionDelegate = interactionDelegate;
self->streamAspectRatio = (float)streamConfig.width / (float)streamConfig.height;
TemporarySettings* settings = [[[DataManager alloc] init] getSettings];
screenFactor = [[UIScreen mainScreen] scale];
}
- (void) setupStreamView {
#if TARGET_OS_TV
remotePressRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(remoteButtonPressed:)];
remotePressRecognizer.allowedPressTypes = @[@(UIPressTypeSelect)];
@@ -51,14 +65,9 @@
[self addGestureRecognizer:remotePressRecognizer];
[self addGestureRecognizer:remoteLongPressRecognizer];
#endif
}
- (void) setupOnScreenControls:(ControllerSupport*)controllerSupport swipeDelegate:(id<EdgeDetectionDelegate>)swipeDelegate {
#else
onScreenControls = [[OnScreenControls alloc] initWithView:self controllerSup:controllerSupport swipeDelegate:swipeDelegate];
DataManager* dataMan = [[DataManager alloc] init];
OnScreenControlsLevel level = (OnScreenControlsLevel)[[dataMan getSettings].onscreenControls integerValue];
OnScreenControlsLevel level = (OnScreenControlsLevel)[settings.onscreenControls integerValue];
if (level == OnScreenControlsLevelAuto) {
[controllerSupport initAutoOnScreenControlMode:onScreenControls];
}
@@ -66,16 +75,88 @@
Log(LOG_I, @"Setting manual on-screen controls level: %d", (int)level);
[onScreenControls setLevel:level];
}
[self becomeFirstResponder];
if (@available(iOS 14.0, *)) {
// Use the new GCMouse API on iOS 14
}
else if (@available(iOS 13.4, *)) {
// Use the legacy mouse API on iOS 13.4
[self addInteraction:[[UIPointerInteraction alloc] initWithDelegate:self]];
UIPanGestureRecognizer *mouseWheelRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(mouseWheelMoved:)];
mouseWheelRecognizer.allowedScrollTypesMask = UIScrollTypeMaskAll;
mouseWheelRecognizer.allowedTouchTypes = @[@(UITouchTypeIndirectPointer)];
[self addGestureRecognizer:mouseWheelRecognizer];
}
#endif
}
- (Boolean)isConfirmedMove:(CGPoint)currentPoint from:(CGPoint)originalPoint {
// Movements of greater than 10 pixels are considered confirmed
return hypotf(originalPoint.x - currentPoint.x, originalPoint.y - currentPoint.y) >= 10;
- (void)startInteractionTimer {
// Restart user interaction tracking
hasUserInteracted = NO;
BOOL timerAlreadyRunning = interactionTimer != nil;
// Start/restart the timer
[interactionTimer invalidate];
interactionTimer = [NSTimer scheduledTimerWithTimeInterval:2.0
target:self
selector:@selector(interactionTimerExpired:)
userInfo:nil
repeats:NO];
// Notify the delegate if this was a new user interaction
if (!timerAlreadyRunning) {
[interactionDelegate userInteractionBegan];
}
}
- (void)interactionTimerExpired:(NSTimer *)timer {
if (!hasUserInteracted) {
// User has finished touching the screen
interactionTimer = nil;
[interactionDelegate userInteractionEnded];
}
else {
// User is still touching the screen. Restart the timer.
[self startInteractionTimer];
}
}
- (void) showOnScreenControls {
#if !TARGET_OS_TV
[onScreenControls show];
[self becomeFirstResponder];
#endif
}
- (OnScreenControlsLevel) getCurrentOscState {
if (onScreenControls == nil) {
return OnScreenControlsLevelOff;
}
else {
return [onScreenControls getLevel];
}
}
- (BOOL)isConfirmedMove:(CGPoint)currentPoint from:(CGPoint)originalPoint {
// Movements of greater than 5 pixels are considered confirmed
return hypotf(originalPoint.x - currentPoint.x, originalPoint.y - currentPoint.y) >= 5;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
if ([self handleMouseButtonEvent:BUTTON_ACTION_PRESS
forTouches:touches
withEvent:event]) {
// If it's a mouse event, we're done
return;
}
Log(LOG_D, @"Touch down");
// Notify of user interaction and start expiration timer
[self startInteractionTimer];
if (![onScreenControls handleTouchDownEvent:touches]) {
UITouch *touch = [[event allTouches] anyObject];
originalLocation = touchLocation = [touch locationInView:self];
@@ -97,7 +178,75 @@
}
}
- (BOOL)handleMouseButtonEvent:(int)buttonAction forTouches:(NSSet *)touches withEvent:(UIEvent *)event {
#if !TARGET_OS_TV
if (@available(iOS 13.4, *)) {
UITouch* touch = [touches anyObject];
if (touch.type == UITouchTypeIndirectPointer) {
if (@available(iOS 14.0, *)) {
// We'll handle this with GCMouse. Do nothing here.
return YES;
}
else {
UIEventButtonMask changedButtons = lastMouseButtonMask ^ event.buttonMask;
for (int i = BUTTON_LEFT; i <= BUTTON_X2; i++) {
UIEventButtonMask buttonFlag;
switch (i) {
// Right and Middle are reversed from what iOS uses
case BUTTON_RIGHT:
buttonFlag = UIEventButtonMaskForButtonNumber(2);
break;
case BUTTON_MIDDLE:
buttonFlag = UIEventButtonMaskForButtonNumber(3);
break;
default:
buttonFlag = UIEventButtonMaskForButtonNumber(i);
break;
}
if (changedButtons & buttonFlag) {
LiSendMouseButtonEvent(buttonAction, i);
}
}
lastMouseButtonMask = event.buttonMask;
return YES;
}
}
}
#endif
return NO;
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
#if !TARGET_OS_TV
if (@available(iOS 13.4, *)) {
UITouch *touch = [touches anyObject];
if (touch.type == UITouchTypeIndirectPointer) {
if (@available(iOS 14.0, *)) {
// We'll handle this with GCMouse. Do nothing here.
return;
}
else {
// We must handle this event to properly support
// drags while the middle, X1, or X2 mouse buttons are
// held down. For some reason, left and right buttons
// don't require this, but we do it anyway for them too.
// Cursor movement without a button held down is handled
// in pointerInteraction:regionForRequest:defaultRegion.
[self updateCursorLocation:[touch locationInView:self]];
return;
}
}
}
#endif
hasUserInteracted = YES;
if (![onScreenControls handleTouchMovedEvent:touches]) {
if ([[event allTouches] count] == 1) {
UITouch *touch = [[event allTouches] anyObject];
@@ -106,11 +255,8 @@
if (touchLocation.x != currentLocation.x ||
touchLocation.y != currentLocation.y)
{
int deltaX = currentLocation.x - touchLocation.x;
int deltaY = currentLocation.y - touchLocation.y;
deltaX *= xDeltaFactor * screenFactor;
deltaY *= yDeltaFactor * screenFactor;
int deltaX = (currentLocation.x - touchLocation.x) * (REFERENCE_WIDTH / self.bounds.size.width);
int deltaY = (currentLocation.y - touchLocation.y) * (REFERENCE_HEIGHT / self.bounds.size.height);
if (deltaX != 0 || deltaY != 0) {
LiSendMouseMoveEvent(deltaX, deltaY);
@@ -144,8 +290,56 @@
}
- (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event {
BOOL handled = NO;
if (@available(iOS 13.4, tvOS 13.4, *)) {
for (UIPress* press in presses) {
// For now, we'll treated it as handled if we handle at least one of the
// UIPress events inside the set.
if (press.key != nil && [KeyboardSupport sendKeyEvent:press.key down:YES]) {
// This will prevent the legacy UITextField from receiving the event
handled = YES;
}
}
}
if (!handled) {
[super pressesBegan:presses withEvent:event];
}
}
- (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event {
BOOL handled = NO;
if (@available(iOS 13.4, tvOS 13.4, *)) {
for (UIPress* press in presses) {
// For now, we'll treated it as handled if we handle at least one of the
// UIPress events inside the set.
if (press.key != nil && [KeyboardSupport sendKeyEvent:press.key down:NO]) {
// This will prevent the legacy UITextField from receiving the event
handled = YES;
}
}
}
if (!handled) {
[super pressesEnded:presses withEvent:event];
}
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
if ([self handleMouseButtonEvent:BUTTON_ACTION_RELEASE
forTouches:touches
withEvent:event]) {
// If it's a mouse event, we're done
return;
}
Log(LOG_D, @"Touch up");
hasUserInteracted = YES;
if (![onScreenControls handleTouchUpEvent:touches]) {
[dragTimer invalidate];
dragTimer = nil;
@@ -215,6 +409,15 @@
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
[dragTimer invalidate];
dragTimer = nil;
if (isDragging) {
isDragging = false;
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
}
[self handleMouseButtonEvent:BUTTON_ACTION_RELEASE
forTouches:touches
withEvent:event];
}
#if TARGET_OS_TV
@@ -239,6 +442,111 @@
isDragging = true;
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
}
#else
- (void) updateCursorLocation:(CGPoint)location {
// These are now relative to the StreamView, however we need to scale them
// further to make them relative to the actual video portion.
float x = location.x - self.bounds.origin.x;
float y = location.y - self.bounds.origin.y;
// For some reason, we don't seem to always get to the bounds of the window
// so we'll subtract 1 pixel if we're to the left/below of the origin and
// and add 1 pixel if we're to the right/above. It should be imperceptible
// to the user but it will allow activation of gestures that require contact
// with the edge of the screen (like Aero Snap).
if (x < self.bounds.size.width / 2) {
x--;
}
else {
x++;
}
if (y < self.bounds.size.height / 2) {
y--;
}
else {
y++;
}
// This logic mimics what iOS does with AVLayerVideoGravityResizeAspect
CGSize videoSize;
CGPoint videoOrigin;
if (self.bounds.size.width > self.bounds.size.height * streamAspectRatio) {
videoSize = CGSizeMake(self.bounds.size.height * streamAspectRatio, self.bounds.size.height);
} else {
videoSize = CGSizeMake(self.bounds.size.width, self.bounds.size.width / streamAspectRatio);
}
videoOrigin = CGPointMake(self.bounds.size.width / 2 - videoSize.width / 2,
self.bounds.size.height / 2 - videoSize.height / 2);
// Confine the cursor to the video region. We don't just discard events outside
// the region because we won't always get one exactly when the mouse leaves the region.
x = MIN(MAX(x, videoOrigin.x), videoOrigin.x + videoSize.width);
y = MIN(MAX(y, videoOrigin.y), videoOrigin.y + videoSize.height);
// Send the mouse position relative to the video region if it has changed
//
// NB: It is important for functionality (not just optimization) to only
// send it if the value has changed. We will receive one of these events
// any time the user presses a modifier key, which can result in errant
// mouse motion when using a Citrix X1 mouse.
if (x != lastMouseX || y != lastMouseY) {
if (lastMouseX != 0 || lastMouseY != 0) {
LiSendMousePositionEvent(x - videoOrigin.x, y - videoOrigin.y,
videoSize.width, videoSize.height);
}
lastMouseX = x;
lastMouseY = y;
}
}
- (UIPointerRegion *)pointerInteraction:(UIPointerInteraction *)interaction
regionForRequest:(UIPointerRegionRequest *)request
defaultRegion:(UIPointerRegion *)defaultRegion API_AVAILABLE(ios(13.4)) {
// This logic mimics what iOS does with AVLayerVideoGravityResizeAspect
CGSize videoSize;
CGPoint videoOrigin;
if (self.bounds.size.width > self.bounds.size.height * streamAspectRatio) {
videoSize = CGSizeMake(self.bounds.size.height * streamAspectRatio, self.bounds.size.height);
} else {
videoSize = CGSizeMake(self.bounds.size.width, self.bounds.size.width / streamAspectRatio);
}
videoOrigin = CGPointMake(self.bounds.size.width / 2 - videoSize.width / 2,
self.bounds.size.height / 2 - videoSize.height / 2);
// Move the cursor on the host if no buttons are pressed.
// Motion with buttons pressed in handled in touchesMoved:
if (lastMouseButtonMask == 0) {
[self updateCursorLocation:request.location];
}
// The pointer interaction should cover the video region only
return [UIPointerRegion regionWithRect:CGRectMake(videoOrigin.x, videoOrigin.y, videoSize.width, videoSize.height) identifier:nil];
}
- (UIPointerStyle *)pointerInteraction:(UIPointerInteraction *)interaction styleForRegion:(UIPointerRegion *)region API_AVAILABLE(ios(13.4)) {
// Always hide the mouse cursor over our stream view
return [UIPointerStyle hiddenPointerStyle];
}
- (void)mouseWheelMoved:(UIPanGestureRecognizer *)gesture {
switch (gesture.state) {
case UIGestureRecognizerStateBegan:
case UIGestureRecognizerStateChanged:
case UIGestureRecognizerStateEnded:
break;
default:
// Ignore recognition failure and other states
return;
}
CGPoint velocity = [gesture velocityInView:self];
if ((short)velocity.y != 0) {
LiSendHighResScrollEvent((short)velocity.y);
}
}
#endif
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
+8
View File
@@ -46,6 +46,14 @@
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSLocalNetworkUsageDescription</key>
<string>Moonlight uses the local network to connect to your gaming PC for streaming.</string>
<key>NSBonjourServices</key>
<array>
<string>_nvstream._tcp</string>
</array>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>Launch Screen</string>
<key>UIMainStoryboardFile</key>
@@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>Moonlight v1.4.xcdatamodel</string>
<string>Moonlight v1.6.xcdatamodel</string>
</dict>
</plist>
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="16119" systemVersion="19E266" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
<entity name="App" representedClassName="App" syncable="YES" codeGenerationType="class">
<attribute name="hdrSupported" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="id" attributeType="String" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
</entity>
<entity name="Host" representedClassName="Host" syncable="YES" codeGenerationType="class">
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="ipv6Address" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="serverCert" optional="YES" attributeType="Binary" syncable="YES"/>
<attribute name="serverCodecModeSupport" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
</entity>
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
<attribute name="btMouseSupport" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
<attribute name="height" attributeType="Integer 32" defaultValueString="720" usesScalarValueType="NO" syncable="YES"/>
<attribute name="multiController" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
<attribute name="optimizeGames" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<attribute name="playAudioOnPC" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="streamingRemotely" attributeType="Boolean" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
<attribute name="useHevc" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
</entity>
<elements>
<element name="App" positionX="0" positionY="54" width="128" height="105"/>
<element name="Host" positionX="0" positionY="0" width="128" height="210"/>
<element name="Settings" positionX="0" positionY="0" width="128" height="238"/>
</elements>
</model>
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17192" systemVersion="19G2021" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
<entity name="App" representedClassName="App" syncable="YES" codeGenerationType="class">
<attribute name="hdrSupported" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="id" attributeType="String" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
</entity>
<entity name="Host" representedClassName="Host" syncable="YES" codeGenerationType="class">
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="ipv6Address" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="serverCert" optional="YES" attributeType="Binary" syncable="YES"/>
<attribute name="serverCodecModeSupport" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
</entity>
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
<attribute name="absoluteTouchMode" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
<attribute name="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
<attribute name="height" attributeType="Integer 32" defaultValueString="720" usesScalarValueType="NO" syncable="YES"/>
<attribute name="multiController" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
<attribute name="optimizeGames" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<attribute name="playAudioOnPC" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
<attribute name="useHevc" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
</entity>
<elements>
<element name="App" positionX="0" positionY="54" width="128" height="105"/>
<element name="Host" positionX="0" positionY="0" width="128" height="210"/>
<element name="Settings" positionX="0" positionY="0" width="128" height="223"/>
</elements>
</model>
+1 -6
View File
@@ -17,12 +17,7 @@
self.statusCode = -1;
}
- (OSImage*) getImage {
#if TARGET_OS_IPHONE
OSImage* appImage = [[OSImage alloc] initWithData:self.data];
#else
OSImage* appImage = nil;
#endif
return appImage;
return [[OSImage alloc] initWithData:self.data];
}
@end
-4
View File
@@ -24,16 +24,12 @@ static const int MAX_ATTEMPTS = 5;
AppAssetResponse* appAssetResp = [[AppAssetResponse alloc] init];
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:appAssetResp withUrlRequest:[hMan newAppAssetRequestWithAppId:self.app.id]]];
#if TARGET_OS_IPHONE
if (appAssetResp.data != nil) {
NSString* boxArtPath = [AppAssetManager boxArtPathForApp:self.app];
[[NSFileManager defaultManager] createDirectoryAtPath:[boxArtPath stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:nil];
[appAssetResp.data writeToFile:boxArtPath atomically:NO];
break;
}
#else
#endif
if (![self isCancelled]) {
[NSThread sleepForTimeInterval:RETRY_DELAY];
+3 -1
View File
@@ -44,7 +44,7 @@ static const char* TAG_APP_INSTALL_PATH = "AppInstallPath";
xmlChar* statusStr = xmlGetProp(node, (const xmlChar*)[TAG_STATUS_CODE UTF8String]);
if (statusStr != NULL) {
int status = [[NSString stringWithUTF8String:(const char*)statusStr] intValue];
int status = (int)[[NSString stringWithUTF8String:(const char*)statusStr] longLongValue];
xmlFree(statusStr);
self.statusCode = status;
}
@@ -113,6 +113,7 @@ static const char* TAG_APP_INSTALL_PATH = "AppInstallPath";
xmlFreeDoc(docPtr);
#ifdef ENABLE_APP_STORE_RESTRICTIONS
// APP STORE REVIEW COMPLIANCE
//
// Remove default Steam entry from the app list to comply with Apple App Store Guideline 4.2.7d:
@@ -146,6 +147,7 @@ static const char* TAG_APP_INSTALL_PATH = "AppInstallPath";
[_appList removeObject:manuallyAddedSteamApp];
}
}
#endif
}
- (NSSet*) getAppList {
+5 -4
View File
@@ -18,11 +18,11 @@
HttpManager* hMan = [[HttpManager alloc] initWithHost:hostIP uniqueId:uniqueId serverCert:cert];
// Try up to 5 times to get the app list
AppListResponse* appListResp;
AppListResponse* appListResp = nil;
for (int i = 0; i < 5; i++) {
appListResp = [[AppListResponse alloc] init];
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:appListResp withUrlRequest:[hMan newAppListRequest]]];
if (appListResp == nil || ![appListResp isStatusOk] || [appListResp getAppList] == nil) {
if (![appListResp isStatusOk] || [appListResp getAppList] == nil) {
Log(LOG_W, @"Failed to get applist on try %d: %@", i, appListResp.statusMessage);
// Wait for one second then retry
@@ -30,10 +30,11 @@
}
else {
Log(LOG_I, @"App list successfully retreived - took %d tries", i);
return appListResp;
break;
}
}
return nil;
return appListResp;
}
@end
+3
View File
@@ -21,8 +21,11 @@
- (void) startDiscovery;
- (void) stopDiscovery;
- (void) stopDiscoveryBlocking;
- (void) resetDiscoveryState;
- (BOOL) addHostToDiscovery:(TemporaryHost*)host;
- (void) removeHostFromDiscovery:(TemporaryHost*)host;
- (void) pauseDiscoveryForHost:(TemporaryHost *)host;
- (void) resumeDiscoveryForHost:(TemporaryHost *)host;
- (void) discoverHost:(NSString*)hostAddress withCallback:(void (^)(TemporaryHost*, NSString*))callback;
@end
+202 -17
View File
@@ -15,8 +15,14 @@
#import "ServerInfoResponse.h"
#import "IdManager.h"
#include <Limelight.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netdb.h>
@implementation DiscoveryManager {
NSMutableArray* _hostQueue;
NSMutableSet* _pausedHosts;
id<DiscoveryCallback> _callback;
MDNSManager* _mdnsMan;
NSOperationQueue* _opQueue;
@@ -33,6 +39,7 @@
_callback = callback;
shouldDiscover = NO;
_hostQueue = [NSMutableArray array];
_pausedHosts = [NSMutableSet set];
for (TemporaryHost* host in hosts)
{
[self addHostToDiscovery:host];
@@ -47,10 +54,88 @@
return self;
}
- (void) discoverHost:(NSString *)hostAddress withCallback:(void (^)(TemporaryHost *, NSString*))callback {
HttpManager* hMan = [[HttpManager alloc] initWithHost:hostAddress uniqueId:_uniqueId serverCert:nil];
+ (BOOL) isAddressLAN:(in_addr_t)addr {
addr = htonl(addr);
// 10.0.0.0/8
if ((addr & 0xFF000000) == 0x0A000000) {
return YES;
}
// 172.16.0.0/12
else if ((addr & 0xFFF00000) == 0xAC100000) {
return YES;
}
// 192.168.0.0/16
else if ((addr & 0xFFFF0000) == 0xC0A80000) {
return YES;
}
// 169.254.0.0/16
else if ((addr & 0xFFFF0000) == 0xA9FE0000) {
return YES;
}
// 100.64.0.0/10 - RFC6598 official CGN address (shouldn't see this in a LAN)
else if ((addr & 0xFFC00000) == 0x64400000) {
return YES;
}
return NO;
}
// This ensures that only RFC 1918 IPv4 addresses can be passed to
// the Add PC dialog. This is required to comply with Apple App Store
// Guideline 4.2.7a.
+ (BOOL) isProhibitedAddress:(NSString*)address {
#ifdef ENABLE_APP_STORE_RESTRICTIONS
struct addrinfo hints;
struct addrinfo* result;
int err;
// We're explicitly using AF_INET here because we don't want to
// ever receive a synthesized IPv6 address here, even on NAT64.
// IPv6 addresses are not restricted here because we cannot easily
// tell whether they are local or not.
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_INET;
err = getaddrinfo([address UTF8String], NULL, &hints, &result);
if (err != 0 || result == NULL) {
Log(LOG_W, @"getaddrinfo(%@) failed: %d", address, err);
return NO;
}
if (result->ai_family != AF_INET) {
// This should never happen due to our hints
assert(result->ai_family == AF_INET);
Log(LOG_W, @"Unexpected address family: %d", result->ai_family);
freeaddrinfo(result);
return NO;
}
BOOL ret = ![DiscoveryManager isAddressLAN:((struct sockaddr_in*)result->ai_addr)->sin_addr.s_addr];
freeaddrinfo(result);
return ret;
#else
return NO;
#endif
}
- (ServerInfoResponse*) getServerInfoResponseForAddress:(NSString*)address {
HttpManager* hMan = [[HttpManager alloc] initWithHost:address uniqueId:_uniqueId serverCert:nil];
ServerInfoResponse* serverInfoResponse = [[ServerInfoResponse alloc] init];
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResponse withUrlRequest:[hMan newServerInfoRequest:false] fallbackError:401 fallbackRequest:[hMan newHttpServerInfoRequest]]];
return serverInfoResponse;
}
- (void) discoverHost:(NSString *)hostAddress withCallback:(void (^)(TemporaryHost *, NSString*))callback {
BOOL prohibitedAddress = [DiscoveryManager isProhibitedAddress:hostAddress];
NSString* prohibitedAddressMessage = [NSString stringWithFormat: @"Moonlight only supports adding PCs on your local network on %s.",
#if TARGET_OS_TV
"tvOS"
#else
"iOS"
#endif
];
ServerInfoResponse* serverInfoResponse = [self getServerInfoResponseForAddress:hostAddress];
TemporaryHost* host = nil;
if ([serverInfoResponse isStatusOk]) {
@@ -58,15 +143,74 @@
host.activeAddress = host.address = hostAddress;
host.state = StateOnline;
[serverInfoResponse populateHost:host];
// Check if this is a new PC
if (![self getHostInDiscovery:host.uuid]) {
// Enforce LAN restriction for App Store Guideline 4.2.7a
if ([DiscoveryManager isProhibitedAddress:hostAddress]) {
// We have a prohibited address. This might be because the user specified their WAN address
// instead of their LAN address. If that's the case, we'll try their LAN address and if we
// can reach it through that address, we'll allow it.
ServerInfoResponse* lanInfo = [self getServerInfoResponseForAddress:host.localAddress];
if ([lanInfo isStatusOk]) {
TemporaryHost* lanHost = [[TemporaryHost alloc] init];
[lanInfo populateHost:lanHost];
if (![lanHost.uuid isEqualToString:host.uuid]) {
// This is a different host, so it's prohibited
prohibitedAddress = YES;
}
else {
// This is the same host that is reachable on the LAN
prohibitedAddress = NO;
}
}
else {
// LAN request failed, so it's a prohibited address
prohibitedAddress = YES;
}
}
else {
// It's an RFC 1918 IPv4 address or IPv6 address which counts as LAN
prohibitedAddress = NO;
}
if (prohibitedAddress) {
callback(nil, prohibitedAddressMessage);
return;
}
else if ([DiscoveryManager isAddressLAN:inet_addr([hostAddress UTF8String])]) {
// Don't send a STUN request if we're connected to a VPN. We'll likely get the VPN
// gateway's external address rather than the external address of the LAN.
if (![Utils isActiveNetworkVPN]) {
// This host was discovered over a permissible LAN address, so we can update our
// external address for this host.
struct in_addr wanAddr;
int err = LiFindExternalAddressIP4("stun.moonlight-stream.org", 3478, &wanAddr.s_addr);
if (err == 0) {
char addrStr[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &wanAddr, addrStr, sizeof(addrStr));
host.externalAddress = [NSString stringWithFormat: @"%s", addrStr];
}
}
}
}
if (![self addHostToDiscovery:host]) {
callback(nil, @"Host information updated");
} else {
callback(host, nil);
}
} else {
} else if (!prohibitedAddress) {
callback(nil, @"Could not connect to host. Ensure GameStream is enabled in GeForce Experience on your PC.");
} else {
callback(nil, prohibitedAddressMessage);
}
}
- (void) resetDiscoveryState {
// Allow us to rediscover hosts that were already found before
[_mdnsMan forgetHosts];
}
- (void) startDiscovery {
@@ -77,8 +221,13 @@
Log(LOG_I, @"Starting discovery");
shouldDiscover = YES;
[_mdnsMan searchForHosts];
for (TemporaryHost* host in _hostQueue) {
[_opQueue addOperation:[self createWorkerForHost:host]];
@synchronized (_hostQueue) {
for (TemporaryHost* host in _hostQueue) {
if (![_pausedHosts containsObject:host]) {
[_opQueue addOperation:[self createWorkerForHost:host]];
}
}
}
}
@@ -141,21 +290,53 @@
return NO;
}
else {
[_hostQueue addObject:host];
if (shouldDiscover) {
[_opQueue addOperation:[self createWorkerForHost:host]];
@synchronized (_hostQueue) {
[_hostQueue addObject:host];
if (shouldDiscover) {
[_opQueue addOperation:[self createWorkerForHost:host]];
}
}
return YES;
}
}
- (void) removeHostFromDiscovery:(TemporaryHost *)host {
for (DiscoveryWorker* worker in [_opQueue operations]) {
if ([worker getHost] == host) {
[worker cancel];
@synchronized (_hostQueue) {
for (DiscoveryWorker* worker in [_opQueue operations]) {
if ([worker getHost] == host) {
[worker cancel];
}
}
[_hostQueue removeObject:host];
[_pausedHosts removeObject:host];
}
}
- (void) pauseDiscoveryForHost:(TemporaryHost *)host {
@synchronized (_hostQueue) {
// Stop any worker for the host
for (DiscoveryWorker* worker in [_opQueue operations]) {
if ([worker getHost] == host) {
[worker cancel];
}
}
// Add it to the paused hosts list
[_pausedHosts addObject:host];
}
}
- (void) resumeDiscoveryForHost:(TemporaryHost *)host {
@synchronized (_hostQueue) {
// Remove it from the paused hosts list
[_pausedHosts removeObject:host];
// Start discovery again
if (shouldDiscover) {
[_opQueue addOperation:[self createWorkerForHost:host]];
}
}
[_hostQueue removeObject:host];
}
// Override from MDNSCallback - called in a worker thread
@@ -167,16 +348,20 @@
[worker discoverHost];
if ([self addHostToDiscovery:host]) {
Log(LOG_I, @"Found new host through MDNS: %@:", host.name);
[self->_callback updateAllHosts:self->_hostQueue];
@synchronized (_hostQueue) {
[_callback updateAllHosts:_hostQueue];
}
} else {
Log(LOG_D, @"Found existing host through MDNS: %@", host.name);
}
}
- (TemporaryHost*) getHostInDiscovery:(NSString*)uuidString {
for (TemporaryHost* discoveredHost in _hostQueue) {
if (discoveredHost.uuid.length > 0 && [discoveredHost.uuid isEqualToString:uuidString]) {
return discoveredHost;
@synchronized (_hostQueue) {
for (TemporaryHost* discoveredHost in _hostQueue) {
if (discoveredHost.uuid.length > 0 && [discoveredHost.uuid isEqualToString:uuidString]) {
return discoveredHost;
}
}
}
return nil;
+3 -2
View File
@@ -89,8 +89,9 @@ static const float POLL_RATE = 2.0f; // Poll every 2 seconds
Log(LOG_D, @"%@ has %d unique addresses", _host.name, [addresses count]);
// Give the PC 2 tries to respond before declaring it offline
for (int i = 0; i < 2; i++) {
// Give the PC 2 tries to respond before declaring it offline if we've seen it before.
// If this is an unknown PC, update the status after 1 attempt to get the UI refreshed quickly.
for (int i = 0; i < (_host.state == StateUnknown ? 1 : 2); i++) {
for (NSString *address in addresses) {
if (self.cancelled) {
// Get out without updating the status because
+12 -5
View File
@@ -14,6 +14,8 @@
#include <libxml2/libxml/xmlreader.h>
#include <string.h>
#include <Limelight.h>
#define SHORT_TIMEOUT_SEC 2
#define NORMAL_TIMEOUT_SEC 5
#define LONG_TIMEOUT_SEC 60
@@ -129,6 +131,10 @@ static const NSString* HTTPS_PORT = @"47984";
[self executeRequestSynchronously:request];
}
}
else if (_error && request.response) {
request.response.statusCode = [_error code];
request.response.statusMessage = [_error localizedDescription];
}
}
- (NSURLRequest*) createRequestFromString:(NSString*) urlString timeout:(int)timeout {
@@ -198,9 +204,10 @@ static const NSString* HTTPS_PORT = @"47984";
- (NSURLRequest*) newLaunchRequest:(StreamConfiguration*)config {
// Using an FPS value over 60 causes SOPS to default to 720p60,
// so force it to 60 when starting. This won't impact our ability
// to get > 60 FPS while actually streaming though.
int fps = config.frameRate > 60 ? 60 : config.frameRate;
// so force it to 0 to ensure the correct resolution is set. We
// used to use 60 here but that locked the frame rate to 60 FPS
// on GFE 3.20.3.
int fps = config.frameRate > 60 ? 0 : config.frameRate;
NSString* urlString = [NSString stringWithFormat:@"%@/launch?uniqueid=%@&appid=%@&mode=%dx%dx%d&additionalStates=1&sops=%d&rikey=%@&rikeyid=%d%@&localAudioPlayMode=%d&surroundAudioInfo=%d&remoteControllersBitmap=%d&gcmap=%d",
_baseHTTPSURL, _uniqueId,
@@ -210,7 +217,7 @@ static const NSString* HTTPS_PORT = @"47984";
[Utils bytesToHex:config.riKey], config.riKeyId,
config.enableHdr ? @"&hdrMode=1&clientHdrCapVersion=0&clientHdrCapSupportedFlagsInUint32=0&clientHdrCapMetaDataId=NV_STATIC_METADATA_TYPE_1&clientHdrCapDisplayData=0x0x0x0x0x0x0x0x0x0x0": @"",
config.playAudioOnPC ? 1 : 0,
(config.audioChannelMask << 16) | config.audioChannelCount,
SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION(config.audioConfiguration),
config.gamepadMask, config.gamepadMask];
Log(LOG_I, @"Requesting: %@", urlString);
// This blocks while the app is launching
@@ -221,7 +228,7 @@ static const NSString* HTTPS_PORT = @"47984";
NSString* urlString = [NSString stringWithFormat:@"%@/resume?uniqueid=%@&rikey=%@&rikeyid=%d&surroundAudioInfo=%d",
_baseHTTPSURL, _uniqueId,
[Utils bytesToHex:config.riKey], config.riKeyId,
(config.audioChannelMask << 16) | config.audioChannelCount];
SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION(config.audioConfiguration)];
Log(LOG_I, @"Requesting: %@", urlString);
// This blocks while the app is resuming
return [self createRequestFromString:urlString timeout:LONG_TIMEOUT_SEC];
+10 -5
View File
@@ -55,21 +55,26 @@
xmlChar* statusStr = xmlGetProp(node, (const xmlChar*)[TAG_STATUS_CODE UTF8String]);
if (statusStr != NULL) {
int status = [[NSString stringWithUTF8String:(const char*)statusStr] intValue];
int status = (int)[[NSString stringWithUTF8String:(const char*)statusStr] longLongValue];
xmlFree(statusStr);
self.statusCode = status;
}
xmlChar* statusMsgXml = xmlGetProp(node, (const xmlChar*)[TAG_STATUS_MESSAGE UTF8String]);
NSString* statusMsg;
if (statusMsgXml != NULL) {
statusMsg = [NSString stringWithUTF8String:(const char*)statusMsgXml];
self.statusMessage = [NSString stringWithUTF8String:(const char*)statusMsgXml];
xmlFree(statusMsgXml);
}
else {
statusMsg = @"Server Error";
self.statusMessage = @"Server Error";
}
if (self.statusCode == -1 && [self.statusMessage isEqualToString:@"Invalid"]) {
// Special case handling an audio capture error which GFE doesn't
// provide any useful status message for.
self.statusCode = 418;
self.statusMessage = @"Missing audio capture device. Reinstalling GeForce Experience should resolve this error.";
}
self.statusMessage = statusMsg;
node = node->children;
+1
View File
@@ -21,6 +21,7 @@
- (id) initWithCallback:(id<MDNSCallback>) callback;
- (void) searchForHosts;
- (void) stopSearching;
- (void) forgetHosts;
@end
+21 -13
View File
@@ -63,6 +63,10 @@ static NSString* NV_SERVICE_TYPE = @"_nvstream._tcp";
[mDNSBrowser stop];
}
- (void) forgetHosts {
[services removeAllObjects];
}
+ (NSString*)sockAddrToString:(NSData*)addrData {
char addrStr[INET6_ADDRSTRLEN];
struct sockaddr* addr = (struct sockaddr*)[addrData bytes];
@@ -180,19 +184,23 @@ static NSString* NV_SERVICE_TYPE = @"_nvstream._tcp";
continue;
}
// Since we discovered this host over IPv4 mDNS, we know we're on the same network
// as the PC and we can use our current WAN address as a likely candidate
// for our PC's external address.
struct in_addr wanAddr;
int err = LiFindExternalAddressIP4("stun.moonlight-stream.org", 3478, &wanAddr.s_addr);
if (err == 0) {
char addrStr[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &wanAddr, addrStr, sizeof(addrStr));
host.externalAddress = [NSString stringWithFormat: @"%s", addrStr];
Log(LOG_I, @"External IPv4 address (STUN): %@ -> %@", [service hostName], host.externalAddress);
}
else {
Log(LOG_E, @"STUN failed to get WAN address: %d", err);
// Don't send a STUN request if we're connected to a VPN. We'll likely get the VPN
// gateway's external address rather than the external address of the LAN.
if (![Utils isActiveNetworkVPN]) {
// Since we discovered this host over IPv4 mDNS, we know we're on the same network
// as the PC and we can use our current WAN address as a likely candidate
// for our PC's external address.
struct in_addr wanAddr;
int err = LiFindExternalAddressIP4("stun.moonlight-stream.org", 3478, &wanAddr.s_addr);
if (err == 0) {
char addrStr[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &wanAddr, addrStr, sizeof(addrStr));
host.externalAddress = [NSString stringWithFormat: @"%s", addrStr];
Log(LOG_I, @"External IPv4 address (STUN): %@ -> %@", [service hostName], host.externalAddress);
}
else {
Log(LOG_E, @"STUN failed to get WAN address: %d", err);
}
}
host.localAddress = [MDNSManager sockAddrToString:addrData];
+1 -5
View File
@@ -10,7 +10,7 @@
@protocol PairCallback <NSObject>
- (void) showPIN:(NSString*)PIN;
- (void) startPairing:(NSString*)PIN;
- (void) pairSuccessful:(NSData*)serverCert;
- (void) pairFailed:(NSString*)message;
- (void) alreadyPaired;
@@ -19,8 +19,4 @@
@interface PairManager : NSOperation
- (id) initWithManager:(HttpManager*)httpManager clientCert:(NSData*)clientCert callback:(id<PairCallback>)callback;
- (NSString*) generatePIN;
- (NSData*) saltPIN:(NSString*)PIN;
- (void) initiatePair:(int)serverMajorVersion;
@end
+53 -60
View File
@@ -30,13 +30,13 @@
}
- (void) main {
// We have to call startPairing before calling any other _callback functions
NSString* PIN = [self generatePIN];
[_callback startPairing:PIN];
ServerInfoResponse* serverInfoResp = [[ServerInfoResponse alloc] init];
[_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResp withUrlRequest:[_httpManager newServerInfoRequest:false]
fallbackError:401 fallbackRequest:[_httpManager newHttpServerInfoRequest]]];
if (serverInfoResp == nil) {
[_callback pairFailed:@"Unable to connect to PC"];
return;
}
if ([serverInfoResp isStatusOk]) {
if ([[serverInfoResp getStringTag:@"state"] hasSuffix:@"_SERVER_BUSY"]) {
[_callback pairFailed:@"You cannot pair while a previous session is still running on the host PC. Quit any running games or reboot the host PC, then try pairing again."];
@@ -46,18 +46,30 @@
[_callback pairFailed:@"Missing XML element"];
return;
}
[self initiatePair: [[appversion substringToIndex:1] intValue]];
[self initiatePairWithPin:PIN forServerMajorVersion:[[appversion substringToIndex:1] intValue]];
} else {
[_callback alreadyPaired];
}
}
else {
[_callback pairFailed:serverInfoResp.statusMessage];
}
}
- (void) finishPairing:(UIBackgroundTaskIdentifier)bgId withError:(NSString*)errorMsg {
- (void) finishPairing:(UIBackgroundTaskIdentifier)bgId
forResponse:(HttpResponse*)resp
withFallbackError:(NSString*)errorMsg {
[_httpManager executeRequestSynchronously:[HttpRequest requestWithUrlRequest:[_httpManager newUnpairRequest]]];
if (bgId != UIBackgroundTaskInvalid) {
[[UIApplication sharedApplication] endBackgroundTask:bgId];
}
if (![resp isStatusOk]) {
// Use the response error if the request failed
errorMsg = resp.statusMessage;
}
[_callback pairFailed:errorMsg];
}
@@ -70,7 +82,7 @@
}
// All codepaths must call finishPairing exactly once before returning!
- (void) initiatePair:(int)serverMajorVersion {
- (void) initiatePairWithPin:(NSString*)PIN forServerMajorVersion:(int)serverMajorVersion {
Log(LOG_I, @"Pairing with generation %d server", serverMajorVersion);
// Start a background task to help prevent the app from being killed
@@ -79,26 +91,21 @@
Log(LOG_W, @"Background pairing time has expired!");
}];
NSString* PIN = [self generatePIN];
NSData* salt = [self saltPIN:PIN];
Log(LOG_I, @"PIN: %@, saltedPIN: %@", PIN, salt);
[_callback showPIN:PIN];
NSData* salt = [Utils randomBytes:16];
NSData* saltedPIN = [self concatData:salt with:[PIN dataUsingEncoding:NSUTF8StringEncoding]];
Log(LOG_I, @"PIN: %@, salt %@", PIN, salt);
HttpResponse* pairResp = [[HttpResponse alloc] init];
[_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:pairResp withUrlRequest:[_httpManager newPairRequest:salt clientCert:_clientCert]]];
if (![self verifyResponseStatus:pairResp]) {
[self finishPairing:bgId withError:@"Pairing stage #1 failed"];
return;
}
NSInteger pairedStatus;
if (![pairResp getIntTag:@"paired" value:&pairedStatus] || !pairedStatus) {
[self finishPairing:bgId withError:@"Pairing was declined by the target."];
[self finishPairing:bgId forResponse:pairResp withFallbackError:@"Pairing was declined by the target."];
return;
}
NSString* plainCert = [pairResp getStringTag:@"plaincert"];
if ([plainCert length] == 0) {
[self finishPairing:bgId withError:@"Another pairing attempt is already in progress."];
[self finishPairing:bgId forResponse:pairResp withFallbackError:@"Another pairing attempt is already in progress."];
return;
}
@@ -112,11 +119,11 @@
// Gen 7 servers use SHA256 to get the key
int hashLength;
if (serverMajorVersion >= 7) {
aesKey = [cryptoMan createAESKeyFromSaltSHA256:salt];
aesKey = [cryptoMan createAESKeyFromSaltSHA256:saltedPIN];
hashLength = 32;
}
else {
aesKey = [cryptoMan createAESKeyFromSaltSHA1:salt];
aesKey = [cryptoMan createAESKeyFromSaltSHA1:saltedPIN];
hashLength = 20;
}
@@ -125,11 +132,8 @@
HttpResponse* challengeResp = [[HttpResponse alloc] init];
[_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:challengeResp withUrlRequest:[_httpManager newChallengeRequest:encryptedChallenge]]];
if (![self verifyResponseStatus:challengeResp] ||
![challengeResp getIntTag:@"paired" value:&pairedStatus] ||
pairedStatus != 1) {
[_httpManager executeRequestSynchronously:[HttpRequest requestWithUrlRequest:[_httpManager newUnpairRequest]]];
[self finishPairing:bgId withError:@"Pairing stage #2 failed"];
if (![self verifyResponseStatus:challengeResp]) {
[self finishPairing:bgId forResponse:challengeResp withFallbackError:@"Pairing stage #2 failed"];
return;
}
@@ -148,15 +152,17 @@
else {
challengeRespHash = [cryptoMan SHA1HashData: challengeRespHashInput];
}
NSData* challengeRespEncrypted = [cryptoMan aesEncrypt:challengeRespHash withKey:aesKey];
assert([challengeRespHash length] <= 32);
NSMutableData* paddedHash = [NSMutableData dataWithData:challengeRespHash];
[paddedHash setLength:32];
NSData* challengeRespEncrypted = [cryptoMan aesEncrypt:paddedHash withKey:aesKey];
HttpResponse* secretResp = [[HttpResponse alloc] init];
[_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:secretResp withUrlRequest:[_httpManager newChallengeRespRequest:challengeRespEncrypted]]];
if (![self verifyResponseStatus:secretResp] ||
![secretResp getIntTag:@"paired" value:&pairedStatus] ||
pairedStatus != 1) {
[_httpManager executeRequestSynchronously:[HttpRequest requestWithUrlRequest:[_httpManager newUnpairRequest]]];
[self finishPairing:bgId withError:@"Pairing stage #3 failed"];
if (![self verifyResponseStatus:secretResp]) {
[self finishPairing:bgId forResponse:secretResp withFallbackError:@"Pairing stage #3 failed"];
return;
}
@@ -165,8 +171,7 @@
NSData* serverSignature = [serverSecretResp subdataWithRange:NSMakeRange(16, 256)];
if (![cryptoMan verifySignature:serverSecret withSignature:serverSignature andCert:[Utils hexToBytes:plainCert]]) {
[_httpManager executeRequestSynchronously:[HttpRequest requestWithUrlRequest:[_httpManager newUnpairRequest]]];
[self finishPairing:bgId withError:@"Server certificate invalid"];
[self finishPairing:bgId forResponse:secretResp withFallbackError:@"Server certificate invalid"];
return;
}
@@ -179,29 +184,22 @@
serverChallengeRespHash = [cryptoMan SHA1HashData: serverChallengeRespHashInput];
}
if (![serverChallengeRespHash isEqual:serverResponse]) {
[_httpManager executeRequestSynchronously:[HttpRequest requestWithUrlRequest:[_httpManager newUnpairRequest]]];
[self finishPairing:bgId withError:@"Incorrect PIN"];
[self finishPairing:bgId forResponse:secretResp withFallbackError:@"Incorrect PIN"];
return;
}
NSData* clientPairingSecret = [self concatData:clientSecret with:[cryptoMan signData:clientSecret withKey:[CryptoManager readKeyFromFile]]];
HttpResponse* clientSecretResp = [[HttpResponse alloc] init];
[_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:clientSecretResp withUrlRequest:[_httpManager newClientSecretRespRequest:[Utils bytesToHex:clientPairingSecret]]]];
if (![self verifyResponseStatus:clientSecretResp] ||
![clientSecretResp getIntTag:@"paired" value:&pairedStatus] ||
pairedStatus != 1) {
[_httpManager executeRequestSynchronously:[HttpRequest requestWithUrlRequest:[_httpManager newUnpairRequest]]];
[self finishPairing:bgId withError:@"Pairing stage #4 failed"];
if (![self verifyResponseStatus:clientSecretResp]) {
[self finishPairing:bgId forResponse:clientSecretResp withFallbackError:@"Pairing stage #4 failed"];
return;
}
HttpResponse* clientPairChallengeResp = [[HttpResponse alloc] init];
[_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:clientPairChallengeResp withUrlRequest:[_httpManager newPairChallenge]]];
if (![self verifyResponseStatus:clientPairChallengeResp] ||
![clientPairChallengeResp getIntTag:@"paired" value:&pairedStatus] ||
pairedStatus != 1) {
[_httpManager executeRequestSynchronously:[HttpRequest requestWithUrlRequest:[_httpManager newUnpairRequest]]];
[self finishPairing:bgId withError:@"Pairing stage #5 failed"];
if (![self verifyResponseStatus:clientPairChallengeResp]) {
[self finishPairing:bgId forResponse:clientPairChallengeResp withFallbackError:@"Pairing stage #5 failed"];
return;
}
@@ -210,14 +208,16 @@
// Caller calls finishPairing for us on failure
- (BOOL) verifyResponseStatus:(HttpResponse*)resp {
if (resp == nil) {
[_httpManager executeRequestSynchronously:[HttpRequest requestWithUrlRequest:[_httpManager newUnpairRequest]]];
return false;
} else if (![resp isStatusOk]) {
[_httpManager executeRequestSynchronously:[HttpRequest requestWithUrlRequest:[_httpManager newUnpairRequest]]];
if (![resp isStatusOk]) {
return false;
} else {
return true;
NSInteger pairedStatus;
if (![resp getIntTag:@"paired" value:&pairedStatus]) {
return false;
}
return pairedStatus == 1;
}
}
@@ -229,16 +229,9 @@
- (NSString*) generatePIN {
NSString* PIN = [NSString stringWithFormat:@"%d%d%d%d",
arc4random() % 10, arc4random() % 10,
arc4random() % 10, arc4random() % 10];
arc4random_uniform(10), arc4random_uniform(10),
arc4random_uniform(10), arc4random_uniform(10)];
return PIN;
}
- (NSData*) saltPIN:(NSString*)PIN {
NSMutableData* saltedPIN = [[NSMutableData alloc] initWithCapacity:20];
[saltedPIN appendData:[Utils randomBytes:16]];
[saltedPIN appendBytes:[PIN UTF8String] length:4];
return saltedPIN;
}
@end
+6 -2
View File
@@ -16,8 +16,12 @@
@implementation WakeOnLanManager
static const int numPorts = 5;
static const int ports[numPorts] = {7, 9, 47998, 47999, 48000};
static const int numPorts = 7;
static const int ports[numPorts] = {
9, // Standard WOL port (privileged port)
47998, 47999, 48000, 48002, 48010, // Ports opened by GFE
47009, // Port opened by Moonlight Internet Hosting Tool for WoL (non-privileged port)
};
+ (void) populateAddress:(struct sockaddr_storage*)addr withPort:(unsigned short)port {
if (addr->ss_family == AF_INET) {
+4 -2
View File
@@ -9,13 +9,15 @@
#import "VideoDecoderRenderer.h"
#import "StreamConfiguration.h"
#define CONN_TEST_SERVER "ios.conntest.moonlight-stream.org"
@protocol ConnectionCallbacks <NSObject>
- (void) connectionStarted;
- (void) connectionTerminated:(long)errorCode;
- (void) connectionTerminated:(int)errorCode;
- (void) stageStarting:(const char*)stageName;
- (void) stageComplete:(const char*)stageName;
- (void) stageFailed:(const char*)stageName withError:(long)errorCode;
- (void) stageFailed:(const char*)stageName withError:(int)errorCode portTestFlags:(int)portTestFlags;
- (void) launchFailed:(NSString*)message;
- (void) rumble:(unsigned short)controllerNumber lowFreqMotor:(unsigned short)lowFreqMotor highFreqMotor:(unsigned short)highFreqMotor;
- (void) connectionStatusUpdate:(int)status;
+84 -71
View File
@@ -7,6 +7,7 @@
//
#import "Connection.h"
#import "Utils.h"
#import <AudioUnit/AudioUnit.h>
#import <AVFoundation/AVFoundation.h>
@@ -32,15 +33,19 @@ static id<ConnectionCallbacks> _callbacks;
#define OUTPUT_BUS 0
#define CIRCULAR_BUFFER_SIZE 16
// My iPod touch 5th Generation seems to really require 80 ms
// of buffering to deliver glitch-free playback :(
// FIXME: Maybe we can use a smaller buffer on more modern iOS versions?
#define CIRCULAR_BUFFER_DURATION 80
static int audioBufferEntries;
static int audioBufferWriteIndex;
static int audioBufferReadIndex;
static int audioBufferStride;
static int audioSamplesPerFrame;
static short* audioCircularBuffer;
#define AUDIO_QUEUE_BUFFERS 4
#define AUDIO_QUEUE_BUFFERS 3
static AudioQueueRef audioQueue;
static AudioQueueBufferRef audioBuffers[AUDIO_QUEUE_BUFFERS];
@@ -71,7 +76,10 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit)
while (entry != NULL) {
// Submit parameter set NALUs directly since no copy is required by the decoder
if (entry->bufferType != BUFFER_TYPE_PICDATA) {
ret = [renderer submitDecodeBuffer:(unsigned char*)entry->data length:entry->length bufferType:entry->bufferType];
ret = [renderer submitDecodeBuffer:(unsigned char*)entry->data
length:entry->length
bufferType:entry->bufferType
pts:decodeUnit->presentationTimeMs];
if (ret != DR_OK) {
free(data);
return ret;
@@ -86,52 +94,84 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit)
}
// This function will take our picture data buffer
return [renderer submitDecodeBuffer:data length:offset bufferType:BUFFER_TYPE_PICDATA];
return [renderer submitDecodeBuffer:data
length:offset
bufferType:BUFFER_TYPE_PICDATA
pts:decodeUnit->presentationTimeMs];
}
int ArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int flags)
int ArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION originalOpusConfig, void* context, int flags)
{
int err;
AudioChannelLayout channelLayout = {};
OPUS_MULTISTREAM_CONFIGURATION opusConfig = *originalOpusConfig;
// Initialize the circular buffer
audioBufferWriteIndex = audioBufferReadIndex = 0;
audioSamplesPerFrame = opusConfig->samplesPerFrame;
audioBufferStride = opusConfig->channelCount * opusConfig->samplesPerFrame;
audioCircularBuffer = malloc(CIRCULAR_BUFFER_SIZE * audioBufferStride * sizeof(short));
audioSamplesPerFrame = opusConfig.samplesPerFrame;
audioBufferStride = opusConfig.channelCount * opusConfig.samplesPerFrame;
audioBufferEntries = CIRCULAR_BUFFER_DURATION / (opusConfig.samplesPerFrame / (opusConfig.sampleRate / 1000));
audioCircularBuffer = malloc(audioBufferEntries * audioBufferStride * sizeof(short));
if (audioCircularBuffer == NULL) {
Log(LOG_E, @"Error allocating output queue\n");
return -1;
}
opusDecoder = opus_multistream_decoder_create(opusConfig->sampleRate,
opusConfig->channelCount,
opusConfig->streams,
opusConfig->coupledStreams,
opusConfig->mapping,
switch (opusConfig.channelCount) {
case 2:
channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_Stereo;
break;
case 4:
channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_Quadraphonic;
break;
case 6:
channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_AudioUnit_5_1;
break;
case 8:
channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_AudioUnit_7_1;
// Swap SL/SR and RL/RR to match the selected channel layout
opusConfig.mapping[4] = originalOpusConfig->mapping[6];
opusConfig.mapping[5] = originalOpusConfig->mapping[7];
opusConfig.mapping[6] = originalOpusConfig->mapping[4];
opusConfig.mapping[7] = originalOpusConfig->mapping[5];
break;
default:
// Unsupported channel layout
Log(LOG_E, @"Unsupported channel layout: %d\n", opusConfig.channelCount);
abort();
}
opusDecoder = opus_multistream_decoder_create(opusConfig.sampleRate,
opusConfig.channelCount,
opusConfig.streams,
opusConfig.coupledStreams,
opusConfig.mapping,
&err);
#if TARGET_OS_IPHONE
// Configure the audio session for our app
NSError *audioSessionError = nil;
AVAudioSession* audioSession = [AVAudioSession sharedInstance];
[audioSession setPreferredSampleRate:opusConfig->sampleRate error:&audioSessionError];
[audioSession setCategory: AVAudioSessionCategoryPlayback error: &audioSessionError];
[audioSession setPreferredIOBufferDuration:0.005 error:&audioSessionError];
[audioSession setPreferredSampleRate:opusConfig.sampleRate error:&audioSessionError];
[audioSession setCategory:AVAudioSessionCategoryPlayback
withOptions:AVAudioSessionCategoryOptionMixWithOthers
error:&audioSessionError];
[audioSession setPreferredIOBufferDuration:(opusConfig.samplesPerFrame / (opusConfig.sampleRate / 1000)) / 1000.0
error:&audioSessionError];
[audioSession setActive: YES error: &audioSessionError];
// FIXME: Calling this breaks surround audio for some reason
//[audioSession setPreferredOutputNumberOfChannels:opusConfig->channelCount error:&audioSessionError];
#endif
OSStatus status;
AudioStreamBasicDescription audioFormat = {0};
audioFormat.mSampleRate = opusConfig->sampleRate;
audioFormat.mSampleRate = opusConfig.sampleRate;
audioFormat.mBitsPerChannel = 16;
audioFormat.mFormatID = kAudioFormatLinearPCM;
audioFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;
audioFormat.mChannelsPerFrame = opusConfig->channelCount;
audioFormat.mChannelsPerFrame = opusConfig.channelCount;
audioFormat.mBytesPerFrame = audioFormat.mChannelsPerFrame * (audioFormat.mBitsPerChannel / 8);
audioFormat.mBytesPerPacket = audioFormat.mBytesPerFrame;
audioFormat.mFramesPerPacket = audioFormat.mBytesPerPacket / audioFormat.mBytesPerFrame;
@@ -144,28 +184,14 @@ int ArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, v
}
// We need to specify a channel layout for surround sound configurations
if (opusConfig->channelCount > 2) {
AudioChannelLayout channelLayout = {};
switch (opusConfig->channelCount) {
case 6:
channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_MPEG_5_1_A;
break;
default:
// Unsupported channel layout
Log(LOG_E, @"Unsupported channel layout: %d\n", opusConfig->channelCount);
abort();
}
status = AudioQueueSetProperty(audioQueue, kAudioQueueProperty_ChannelLayout, &channelLayout, sizeof(channelLayout));
if (status != noErr) {
Log(LOG_E, @"Error configuring surround channel layout: %d\n", status);
return status;
}
status = AudioQueueSetProperty(audioQueue, kAudioQueueProperty_ChannelLayout, &channelLayout, sizeof(channelLayout));
if (status != noErr) {
Log(LOG_E, @"Error configuring surround channel layout: %d\n", status);
return status;
}
for (int i = 0; i < AUDIO_QUEUE_BUFFERS; i++) {
status = AudioQueueAllocateBuffer(audioQueue, audioFormat.mBytesPerFrame * opusConfig->samplesPerFrame, &audioBuffers[i]);
status = AudioQueueAllocateBuffer(audioQueue, audioFormat.mBytesPerFrame * opusConfig.samplesPerFrame, &audioBuffers[i]);
if (status != noErr) {
Log(LOG_E, @"Error allocating output buffer: %d\n", status);
return status;
@@ -203,10 +229,8 @@ void ArCleanup(void)
audioCircularBuffer = NULL;
}
#if TARGET_OS_IPHONE
// Audio session is now inactive
[[AVAudioSession sharedInstance] setActive: NO error: nil];
#endif
}
void ArDecodeAndPlaySample(char* sampleData, int sampleLength)
@@ -215,7 +239,7 @@ void ArDecodeAndPlaySample(char* sampleData, int sampleLength)
// Check if there is space for this sample in the buffer. Again, this can race
// but in the worst case, we'll not see the sample callback having consumed a sample.
if (((audioBufferWriteIndex + 1) % CIRCULAR_BUFFER_SIZE) == audioBufferReadIndex) {
if (((audioBufferWriteIndex + 1) % audioBufferEntries) == audioBufferReadIndex) {
return;
}
@@ -228,7 +252,7 @@ void ArDecodeAndPlaySample(char* sampleData, int sampleLength)
// This can race with the reader in the sample callback, however this is a benign
// race since we'll either read the original value of s_WriteIndex (which is safe,
// we just won't consider this sample) or the new value of s_WriteIndex
audioBufferWriteIndex = (audioBufferWriteIndex + 1) % CIRCULAR_BUFFER_SIZE;
audioBufferWriteIndex = (audioBufferWriteIndex + 1) % audioBufferEntries;
}
}
@@ -242,9 +266,9 @@ void ClStageComplete(int stage)
[_callbacks stageComplete:LiGetStageName(stage)];
}
void ClStageFailed(int stage, long errorCode)
void ClStageFailed(int stage, int errorCode)
{
[_callbacks stageFailed:LiGetStageName(stage) withError:errorCode];
[_callbacks stageFailed:LiGetStageName(stage) withError:errorCode portTestFlags:LiGetPortFlagsFromStage(stage)];
}
void ClConnectionStarted(void)
@@ -252,7 +276,7 @@ void ClConnectionStarted(void)
[_callbacks connectionStarted];
}
void ClConnectionTerminated(long errorCode)
void ClConnectionTerminated(int errorCode)
{
[_callbacks connectionTerminated: errorCode];
}
@@ -331,26 +355,22 @@ void ClConnectionStatusUpdate(int status)
_streamConfig.fps = config.frameRate;
_streamConfig.bitrate = config.bitRate;
_streamConfig.enableHdr = config.enableHdr;
_streamConfig.audioConfiguration = config.audioConfiguration;
// Use some of the HEVC encoding efficiency improvements to
// reduce bandwidth usage while still gaining some image
// quality improvement.
_streamConfig.hevcBitratePercentageMultiplier = 75;
// Detect remote streaming automatically based on the IP address of the target
_streamConfig.streamingRemotely = STREAM_CFG_AUTO;
_streamConfig.packetSize = 1392;
switch (config.audioChannelCount) {
case 2:
_streamConfig.audioConfiguration = AUDIO_CONFIGURATION_STEREO;
break;
case 6:
_streamConfig.audioConfiguration = AUDIO_CONFIGURATION_51_SURROUND;
break;
default:
Log(LOG_E, @"Unknown audio channel count: %d", config.audioChannelCount);
abort();
if ([Utils isActiveNetworkVPN]) {
// Force remote streaming mode when a VPN is connected
_streamConfig.streamingRemotely = STREAM_CFG_REMOTE;
_streamConfig.packetSize = 1024;
}
else {
// Detect remote streaming automatically based on the IP address of the target
_streamConfig.streamingRemotely = STREAM_CFG_AUTO;
_streamConfig.packetSize = 1392;
}
// HDR implies HEVC allowed
@@ -358,7 +378,6 @@ void ClConnectionStatusUpdate(int status)
config.allowHevc = YES;
}
#if TARGET_OS_IPHONE
// On iOS 11, we can use HEVC if the server supports encoding it
// and this device has hardware decode for it (A9 and later).
// Additionally, iPhone X had a bug which would cause video
@@ -367,13 +386,6 @@ void ClConnectionStatusUpdate(int status)
if (@available(iOS 11.3, tvOS 11.3, *)) {
_streamConfig.supportsHevc = config.allowHevc && VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC);
}
#else
if (@available(macOS 10.13, *)) {
// Streaming with limited bandwidth will result in better quality with HEVC
if (VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC) || _streamConfig.streamingRemotely != 0)
_streamConfig.supportsHevc = config.allowHevc;
}
#endif
// HEVC must be supported when HDR is enabled
assert(!_streamConfig.enableHdr || _streamConfig.supportsHevc);
@@ -397,7 +409,8 @@ void ClConnectionStatusUpdate(int status)
_arCallbacks.init = ArInit;
_arCallbacks.cleanup = ArCleanup;
_arCallbacks.decodeAndPlaySample = ArDecodeAndPlaySample;
_arCallbacks.capabilities = CAPABILITY_DIRECT_SUBMIT;
_arCallbacks.capabilities = CAPABILITY_DIRECT_SUBMIT |
CAPABILITY_SUPPORTS_ARBITRARY_AUDIO_DURATION;
LiInitializeConnectionCallbacks(&_clCallbacks);
_clCallbacks.stageStarting = ClStageStarting;
@@ -431,7 +444,7 @@ static void FillOutputBuffer(void *aqData,
// This can race with the reader in the AudDecDecodeAndPlaySample function. This is
// not a problem because at worst, it just won't see that we've consumed this sample yet.
audioBufferReadIndex = (audioBufferReadIndex + 1) % CIRCULAR_BUFFER_SIZE;
audioBufferReadIndex = (audioBufferReadIndex + 1) % audioBufferEntries;
}
else {
// No data, so play silence
+1 -2
View File
@@ -23,8 +23,7 @@
@property int gamepadMask;
@property BOOL optimizeGameSettings;
@property BOOL playAudioOnPC;
@property int audioChannelCount;
@property int audioChannelMask;
@property int audioConfiguration;
@property BOOL enableHdr;
@property BOOL multiController;
@property BOOL allowHevc;
+1 -1
View File
@@ -9,5 +9,5 @@
#import "StreamConfiguration.h"
@implementation StreamConfiguration
@synthesize host, appID, width, height, frameRate, bitRate, riKeyId, riKey, gamepadMask, streamingRemotely, appName, optimizeGameSettings, playAudioOnPC, audioChannelMask, audioChannelCount, enableHdr, multiController, allowHevc, serverCert;
@synthesize host, appID, width, height, frameRate, bitRate, riKeyId, riKey, gamepadMask, streamingRemotely, appName, optimizeGameSettings, playAudioOnPC, audioConfiguration, enableHdr, multiController, allowHevc, serverCert;
@end
+5 -10
View File
@@ -50,7 +50,11 @@
NSString* appversion = [serverInfoResp getStringTag:@"appversion"];
NSString* gfeVersion = [serverInfoResp getStringTag:@"GfeVersion"];
NSString* serverState = [serverInfoResp getStringTag:@"state"];
if (![serverInfoResp isStatusOk] || pairStatus == NULL || appversion == NULL || serverState == NULL) {
if (![serverInfoResp isStatusOk]) {
[_callbacks launchFailed:serverInfoResp.statusMessage];
return;
}
else if (pairStatus == NULL || appversion == NULL || serverState == NULL) {
[_callbacks launchFailed:@"Failed to connect to PC"];
return;
}
@@ -73,15 +77,6 @@
return;
}
}
#if TARGET_OS_IPHONE
// Set mouse delta factors from the screen resolution and stream size
CGFloat screenScale = [[UIScreen mainScreen] scale];
CGRect screenBounds = [[UIScreen mainScreen] bounds];
CGSize screenSize = CGSizeMake(screenBounds.size.width * screenScale, screenBounds.size.height * screenScale);
[((StreamView*)_renderView) setMouseDeltaFactors:_config.width / screenSize.width
y:_config.height / screenSize.height];
#endif
// Populate the config's version fields from serverinfo
_config.appVersion = appversion;
+1 -1
View File
@@ -18,6 +18,6 @@
- (void)updateBufferForRange:(CMBlockBufferRef)existingBuffer data:(unsigned char *)data offset:(int)offset length:(int)nalLength;
- (int)submitDecodeBuffer:(unsigned char *)data length:(int)length bufferType:(int)bufferType;
- (int)submitDecodeBuffer:(unsigned char *)data length:(int)length bufferType:(int)bufferType pts:(unsigned int)pts;
@end
+1 -12
View File
@@ -75,9 +75,6 @@
- (void)setupWithVideoFormat:(int)videoFormat refreshRate:(int)refreshRate
{
self->videoFormat = videoFormat;
#if !TARGET_OS_IPHONE
_view.codec = videoFormat;
#endif
if (refreshRate > 60) {
// HACK: We seem to just get 60 Hz screen updates even with a 120 FPS stream if
@@ -204,7 +201,7 @@
}
// This function must free data for bufferType == BUFFER_TYPE_PICDATA
- (int)submitDecodeBuffer:(unsigned char *)data length:(int)length bufferType:(int)bufferType
- (int)submitDecodeBuffer:(unsigned char *)data length:(int)length bufferType:(int)bufferType pts:(unsigned int)pts
{
unsigned char nalType = data[FRAME_START_PREFIX_SIZE];
OSStatus status;
@@ -255,11 +252,7 @@
Log(LOG_I, @"Constructing new HEVC format description");
#if TARGET_OS_IPHONE
if (@available(iOS 11.0, *)) {
#else
if (@available(macOS 10.13, *)) {
#endif
status = CMVideoFormatDescriptionCreateFromHEVCParameterSets(kCFAllocatorDefault,
3, /* count of parameter sets */
parameterSetPointers,
@@ -370,10 +363,6 @@
CFDictionarySetValue(dict, kCMSampleAttachmentKey_DependsOnOthers, kCFBooleanFalse);
}
#if !TARGET_OS_IPHONE
_view.frameCount++;
#endif
// Enqueue video samples on the main thread
dispatch_async(dispatch_get_main_queue(), ^{
// Enqueue the next frame
+1 -1
View File
@@ -15,7 +15,7 @@
@end
@interface UIAppView : UIView
@interface UIAppView : UIButton
- (id) initWithApp:(TemporaryApp*)app cache:(NSCache*)cache andCallback:(id<AppCallback>)callback;
- (void) updateAppImage;
+76 -75
View File
@@ -13,9 +13,9 @@ static const float REFRESH_CYCLE = 2.0f;
@implementation UIAppView {
TemporaryApp* _app;
UIButton* _appButton;
UILabel* _appLabel;
UIImageView* _appOverlay;
UIImageView* _appImage;
NSCache* _artCache;
id<AppCallback> _callback;
}
@@ -33,29 +33,41 @@ static UIImage* noImage;
if (noImage == nil) {
noImage = [UIImage imageNamed:@"NoAppImage"];
}
#if TARGET_OS_TV
_appButton = [UIButton buttonWithType:UIButtonTypeSystem];
self.frame = CGRectMake(0, 0, 200, 265);
#else
_appButton = [UIButton buttonWithType:UIButtonTypeCustom];
self.frame = CGRectMake(0, 0, 150, 200);
#endif
[_appButton setBackgroundImage:noImage forState:UIControlStateNormal];
[_appButton setContentEdgeInsets:UIEdgeInsetsMake(0, 4, 0, 4)];
[_appButton sizeToFit];
_appImage = [[UIImageView alloc] initWithFrame:self.frame];
[_appImage setImage:noImage];
[self addSubview:_appImage];
if (@available(iOS 9.0, tvOS 9.0, *)) {
[_appButton addTarget:self action:@selector(appClicked) forControlEvents:UIControlEventPrimaryActionTriggered];
[self addTarget:self action:@selector(appClicked) forControlEvents:UIControlEventPrimaryActionTriggered];
}
else {
[_appButton addTarget:self action:@selector(appClicked) forControlEvents:UIControlEventTouchUpInside];
[self addTarget:self action:@selector(appClicked) forControlEvents:UIControlEventTouchUpInside];
}
[self addSubview:_appButton];
[self sizeToFit];
[self addTarget:self action:@selector(buttonSelected:) forControlEvents:UIControlEventTouchDown];
[self addTarget:self action:@selector(buttonDeselected:) forControlEvents:UIControlEventTouchUpInside | UIControlEventTouchCancel | UIControlEventTouchDragExit];
#if TARGET_OS_TV
_appImage.adjustsImageWhenAncestorFocused = YES;
#else
// Rasterizing the cell layer increases rendering performance by quite a bit
// but we want it unrasterized for tvOS where it must be scaled.
self.layer.shouldRasterize = YES;
self.layer.rasterizationScale = [UIScreen mainScreen].scale;
if (@available(iOS 13.4.1, *)) {
// Allow the button style to change when moused over
self.pointerInteractionEnabled = YES;
}
#endif
[self updateAppImage];
[self startUpdateLoop];
@@ -66,52 +78,14 @@ static UIImage* noImage;
[_callback appClicked:_app];
}
#if TARGET_OS_TV
- (UIImageView*) renderToImageView:(UIImage*)appImage {
//custom image to do TvOS hover popup effect
UIImageView *imageView = [[UIImageView alloc] initWithImage:appImage];
imageView.userInteractionEnabled = YES;
imageView.adjustsImageWhenAncestorFocused = YES;
imageView.frame = self.frame;
UIGraphicsBeginImageContextWithOptions(self.frame.size, false, 0);
[imageView.layer renderInContext:(UIGraphicsGetCurrentContext())];
[_appLabel.layer renderInContext:(UIGraphicsGetCurrentContext())];
[_appOverlay.layer renderInContext:(UIGraphicsGetCurrentContext())];
UIImage *imageWithText = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
[imageView setImage:imageWithText];
[_appButton addSubview:imageView];
return imageView;
}
#endif
- (void) updateAppImage {
if (_appOverlay != nil) {
[_appOverlay removeFromSuperview];
_appOverlay = nil;
}
#if TARGET_OS_TV
_appButton.frame = CGRectMake(0, 0, 200, 265);
#else
_appButton.frame = CGRectMake(0, 0, 150, 200);
#endif
self.frame = _appButton.frame;
if ([_app.id isEqualToString:_app.host.currentGame]) {
// Only create the app overlay if needed
_appOverlay = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"Play"]];
_appOverlay.layer.shadowColor = [UIColor blackColor].CGColor;
_appOverlay.layer.shadowOffset = CGSizeMake(0, 0);
_appOverlay.layer.shadowOpacity = 1;
_appOverlay.layer.shadowRadius = 4.0;
#if TARGET_OS_TV
_appOverlay.contentMode = UIViewContentModeScaleAspectFit;
_appOverlay.frame = CGRectMake(0, 0, self.frame.size.width, self.frame.size.height / 3.f);
#else
_appOverlay.frame = CGRectMake(self.frame.size.width / 4, 10, self.frame.size.width / 2, self.frame.size.height / 4.f);
#endif
if (_appLabel != nil) {
[_appLabel removeFromSuperview];
_appLabel = nil;
}
BOOL noAppImage = false;
@@ -125,20 +99,13 @@ static UIImage* noImage;
[_artCache setObject:appImage forKey:_app];
}
}
if (appImage != nil) {
// This size of image might be blank image received from GameStream.
// TODO: Improve no-app image detection
if (!(appImage.size.width == 130.f && appImage.size.height == 180.f) && // GFE 2.0
!(appImage.size.width == 628.f && appImage.size.height == 888.f)) { // GFE 3.0
#if TARGET_OS_TV
[_appButton addSubview:[self renderToImageView:appImage]];
#else
[self addSubview:_appOverlay];
#endif
[_appButton setBackgroundImage:appImage forState:UIControlStateNormal];
[self setNeedsDisplay];
[_appImage setImage:appImage];
} else {
noAppImage = true;
}
@@ -146,31 +113,65 @@ static UIImage* noImage;
noAppImage = true;
}
if ([_app.id isEqualToString:_app.host.currentGame]) {
// Only create the app overlay if needed
_appOverlay = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"Play"]];
_appOverlay.layer.shadowColor = [UIColor blackColor].CGColor;
_appOverlay.layer.shadowOffset = CGSizeMake(0, 0);
_appOverlay.layer.shadowOpacity = 1;
_appOverlay.layer.shadowRadius = 4.0;
_appOverlay.contentMode = UIViewContentModeScaleAspectFit;
}
if (noAppImage) {
#if TARGET_OS_TV
_appLabel = [[UILabel alloc] init];
[_appLabel setTextColor:[UIColor whiteColor]];
[_appLabel setText:_app.name];
[_appLabel setFrame:self.frame];
#else
_appLabel = _appButton.titleLabel;
[_appButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
[_appButton setTitle:_app.name forState:UIControlStateNormal];
[_appButton setTitleEdgeInsets:UIEdgeInsetsMake(_appOverlay != nil ? _appOverlay.frame.size.height : 5, 5, 5, 5)];
#endif
[_appLabel setFont:[UIFont systemFontOfSize:24]];
[_appLabel setBaselineAdjustment:UIBaselineAdjustmentAlignCenters];
[_appLabel setTextAlignment:NSTextAlignmentCenter];
[_appLabel setLineBreakMode:NSLineBreakByWordWrapping];
[_appLabel setNumberOfLines:0];
#if TARGET_OS_TV
[_appButton addSubview:[self renderToImageView:noImage]];
#else
[self addSubview:_appOverlay];
#endif
}
[self positionSubviews];
#if TARGET_OS_TV
[_appImage.overlayContentView addSubview:_appLabel];
[_appImage.overlayContentView addSubview:_appOverlay];
#else
[self addSubview:_appLabel];
[self addSubview:_appOverlay];
#endif
}
- (void) buttonSelected:(id)sender {
_appImage.layer.opacity = 0.5f;
}
- (void) buttonDeselected:(id)sender {
_appImage.layer.opacity = 1.0f;
}
- (void) positionSubviews {
CGFloat padding = 5.f;
CGSize frameSize = _appImage.frame.size;
CGPoint center = _appImage.center;
if (_appLabel != nil) {
if (_appOverlay != nil) {
_appOverlay.frame = CGRectMake(0, 0, frameSize.width / 3, frameSize.width / 3);
_appOverlay.center = CGPointMake(frameSize.width / 2, padding + _appOverlay.frame.size.height / 2);
[_appLabel setFrame:CGRectMake(padding, _appOverlay.frame.size.height + padding, frameSize.width - 2 * padding, frameSize.height - _appOverlay.frame.size.height - 2 * padding)];
}
else {
[_appLabel setFrame:CGRectMake(padding, padding, frameSize.width - 2 * padding, frameSize.height - 2 * padding)];
}
}
else if (_appOverlay != nil) {
_appOverlay.frame = CGRectMake(0, 0, frameSize.width / 2, frameSize.width / 2);
_appOverlay.center = center;
}
}
- (void) startUpdateLoop {
+1 -1
View File
@@ -17,7 +17,7 @@
@end
@interface UIComputerView : UIView
@interface UIComputerView : UIButton
- (id) initWithComputer:(TemporaryHost*)host andCallback:(id<HostCallback>)callback;
- (id) initForAddWithCallback:(id<HostCallback>)callback;
+51 -46
View File
@@ -10,7 +10,7 @@
@implementation UIComputerView {
TemporaryHost* _host;
UIButton* _hostButton;
UIImageView* _hostIcon;
UILabel* _hostLabel;
UIImageView* _hostOverlay;
UIActivityIndicatorView* _hostSpinner;
@@ -29,50 +29,71 @@ static const int LABEL_DY = 20;
- (id) init {
self = [super init];
_hostButton = [UIButton buttonWithType:UIButtonTypeCustom];
[_hostButton setContentEdgeInsets:UIEdgeInsetsMake(0, 4, 0, 4)];
[_hostButton setBackgroundImage:[UIImage imageNamed:@"Computer"] forState:UIControlStateNormal];
[_hostButton sizeToFit];
#if TARGET_OS_TV
_hostButton.frame = CGRectMake(0, 0, 400, 400);
self.frame = CGRectMake(0, 0, 400, 400);
#else
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
_hostButton.frame = CGRectMake(0, 0, 200, 200);
self.frame = CGRectMake(0, 0, 200, 200);
} else {
_hostButton.frame = CGRectMake(0, 0, 100, 100);
self.frame = CGRectMake(0, 0, 100, 100);
}
#endif
_hostButton.layer.shadowColor = [[UIColor blackColor] CGColor];
_hostButton.layer.shadowOffset = CGSizeMake(5,8);
_hostButton.layer.shadowOpacity = 0.3;
_hostIcon = [[UIImageView alloc] initWithFrame:self.frame];
[_hostIcon setImage:[UIImage imageNamed:@"Computer"]];
self.layer.shadowColor = [[UIColor blackColor] CGColor];
self.layer.shadowOffset = CGSizeMake(5,8);
self.layer.shadowOpacity = 0.3;
[_hostButton addTarget:self action:@selector(hostButtonSelected:) forControlEvents:UIControlEventTouchDown];
[_hostButton addTarget:self action:@selector(hostButtonDeselected:) forControlEvents:UIControlEventTouchUpInside | UIControlEventTouchCancel | UIControlEventTouchDragExit];
[self addTarget:self action:@selector(hostButtonSelected:) forControlEvents:UIControlEventTouchDown];
[self addTarget:self action:@selector(hostButtonDeselected:) forControlEvents:UIControlEventTouchUpInside | UIControlEventTouchCancel | UIControlEventTouchDragExit];
_hostLabel = [[UILabel alloc] init];
_hostLabel.textColor = [UIColor whiteColor];
_hostOverlay = [[UIImageView alloc] initWithFrame:CGRectMake(_hostButton.frame.size.width / 3, _hostButton.frame.size.height / 4, _hostButton.frame.size.width / 3, _hostButton.frame.size.height / 3)];
_hostOverlay = [[UIImageView alloc] initWithFrame:CGRectMake(self.frame.size.width / 3, _hostIcon.frame.size.height / 4, _hostIcon.frame.size.width / 3, self.frame.size.height / 3)];
_hostSpinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
[_hostSpinner setFrame:_hostOverlay.frame];
_hostSpinner.userInteractionEnabled = NO;
_hostSpinner.hidesWhenStopped = YES;
[self addSubview:_hostButton];
[self addSubview:_hostLabel];
[self addSubview:_hostIcon];
#if TARGET_OS_TV
_hostIcon.clipsToBounds = NO;
_hostIcon.adjustsImageWhenAncestorFocused = YES;
_hostIcon.masksFocusEffectToContents = YES;
self.adjustsImageWhenHighlighted = NO;
_hostOverlay.masksFocusEffectToContents = YES;
_hostOverlay.adjustsImageWhenAncestorFocused = NO;
[_hostIcon.overlayContentView addSubview:_hostOverlay];
[_hostIcon.overlayContentView addSubview:_hostSpinner];
#else
[self addSubview:_hostOverlay];
[self addSubview:_hostSpinner];
if (@available(iOS 13.4.1, *)) {
// Allow the button style to change when moused over
self.pointerInteractionEnabled = YES;
}
#endif
return self;
}
- (void) hostButtonSelected:(id)sender {
_hostIcon.layer.opacity = 0.5f;
_hostSpinner.layer.opacity = 0.5f;
_hostOverlay.layer.opacity = 0.5f;
}
- (void) hostButtonDeselected:(id)sender {
_hostIcon.layer.opacity = 1.0f;
_hostSpinner.layer.opacity = 1.0f;
_hostOverlay.layer.opacity = 1.0f;
}
@@ -82,18 +103,15 @@ static const int LABEL_DY = 20;
_callback = callback;
if (@available(iOS 9.0, tvOS 9.0, *)) {
[_hostButton addTarget:self action:@selector(addClicked) forControlEvents:UIControlEventPrimaryActionTriggered];
[self addTarget:self action:@selector(addClicked) forControlEvents:UIControlEventPrimaryActionTriggered];
}
else {
[_hostButton addTarget:self action:@selector(addClicked) forControlEvents:UIControlEventTouchUpInside];
[self addTarget:self action:@selector(addClicked) forControlEvents:UIControlEventTouchUpInside];
}
[_hostLabel setText:@"Add Host"];
[_hostLabel setText:@"Add Host Manually"];
[_hostLabel sizeToFit];
float x = _hostButton.frame.origin.x + _hostButton.frame.size.width / 2;
_hostLabel.center = CGPointMake(x, _hostButton.frame.origin.y + _hostButton.frame.size.height + LABEL_DY);
[_hostOverlay setImage:[UIImage imageNamed:@"AddOverlayIcon"]];
[self updateBounds];
@@ -107,17 +125,16 @@ static const int LABEL_DY = 20;
_callback = callback;
UILongPressGestureRecognizer* longPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(hostLongClicked:)];
[_hostButton addGestureRecognizer:longPressRecognizer];
[self addGestureRecognizer:longPressRecognizer];
if (@available(iOS 9.0, tvOS 9.0, *)) {
[_hostButton addTarget:self action:@selector(hostClicked) forControlEvents:UIControlEventPrimaryActionTriggered];
[self addTarget:self action:@selector(hostClicked) forControlEvents:UIControlEventPrimaryActionTriggered];
}
else {
[_hostButton addTarget:self action:@selector(hostClicked) forControlEvents:UIControlEventTouchUpInside];
[self addTarget:self action:@selector(hostClicked) forControlEvents:UIControlEventTouchUpInside];
}
[self updateContentsForHost:host];
[self updateBounds];
[self startUpdateLoop];
return self;
@@ -129,16 +146,19 @@ static const int LABEL_DY = 20;
float width = 0;
float height;
x = MIN(x, _hostButton.frame.origin.x);
float iconX = _hostIcon.frame.origin.x + _hostIcon.frame.size.width / 2;
_hostLabel.center = CGPointMake(iconX, _hostIcon.frame.origin.y + _hostIcon.frame.size.height + LABEL_DY);
x = MIN(x, _hostIcon.frame.origin.x);
x = MIN(x, _hostLabel.frame.origin.x);
y = MIN(y, _hostButton.frame.origin.y);
y = MIN(y, _hostIcon.frame.origin.y);
y = MIN(y, _hostLabel.frame.origin.y);
width = MAX(width, _hostButton.frame.size.width);
width = MAX(width, _hostIcon.frame.size.width);
width = MAX(width, _hostLabel.frame.size.width);
height = _hostButton.frame.size.height +
height = _hostIcon.frame.size.height +
_hostLabel.frame.size.height +
LABEL_DY / 2;
@@ -167,8 +187,7 @@ static const int LABEL_DY = 20;
[_hostSpinner startAnimating];
}
float x = _hostButton.frame.origin.x + _hostButton.frame.size.width / 2;
_hostLabel.center = CGPointMake(x, _hostButton.frame.origin.y + _hostButton.frame.size.height + LABEL_DY);
[self updateBounds];
}
- (void) startUpdateLoop {
@@ -198,18 +217,4 @@ static const int LABEL_DY = 20;
[_callback addHostClicked];
}
#if TARGET_OS_TV
- (void) didUpdateFocusInContext:(UIFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator {
UIButton *previousButton = (UIButton *)context.previouslyFocusedItem;
UIButton *nextButton = (UIButton *) context.nextFocusedItem;
if (previousButton.superview == self) {
self.backgroundColor = nil;
}
if (nextButton.superview == self) {
nextButton.superview.backgroundColor = [UIColor darkGrayColor];
}
}
#endif
@end
+1 -1
View File
@@ -25,8 +25,8 @@ FOUNDATION_EXPORT NSString *const deviceName;
+ (NSData*) randomBytes:(NSInteger)length;
+ (NSString*) bytesToHex:(NSData*)data;
+ (NSData*) hexToBytes:(NSString*) hex;
+ (int) resolveHost:(NSString*)host;
+ (void) addHelpOptionToDialog:(UIAlertController*)dialog;
+ (BOOL) isActiveNetworkVPN;
@end
+10 -19
View File
@@ -50,27 +50,18 @@ NSString *const deviceName = @"roth";
return hex;
}
+ (int) resolveHost:(NSString*)host {
struct hostent *hostent;
if (inet_addr([host UTF8String]) != INADDR_NONE) {
// Already an IP address
int addr = inet_addr([host UTF8String]);
Log(LOG_I, @"host address: %d", addr);
return addr;
} else {
hostent = gethostbyname([host UTF8String]);
if (hostent != NULL) {
char* ipstr = inet_ntoa(*(struct in_addr*)hostent->h_addr_list[0]);
Log(LOG_I, @"Resolved %@ -> %s", host, ipstr);
int addr = inet_addr(ipstr);
Log(LOG_I, @"host address: %d", addr);
return addr;
} else {
Log(LOG_W, @"Failed to resolve host: %d", h_errno);
return 0;
+ (BOOL)isActiveNetworkVPN {
NSDictionary *dict = CFBridgingRelease(CFNetworkCopySystemProxySettings());
NSArray *keys = [dict[@"__SCOPED__"] allKeys];
for (NSString *key in keys) {
if ([key containsString:@"tap"] ||
[key containsString:@"tun"] ||
[key containsString:@"ppp"] ||
[key containsString:@"ipsec"]) {
return YES;
}
}
return NO;
}
+ (void) addHelpOptionToDialog:(UIAlertController*)dialog {
@@ -17,8 +17,8 @@
@interface MainFrameViewController : UICollectionViewController <DiscoveryCallback, PairCallback, HostCallback, AppCallback, AppAssetCallback, NSURLConnectionDelegate, SWRevealViewControllerDelegate>
#if !TARGET_OS_TV
@property (weak, nonatomic) IBOutlet UIBarButtonItem *settingsButton;
#if !TARGET_OS_TV
@property (weak, nonatomic) IBOutlet UIBarButtonItem *upButton;
#endif
@@ -29,10 +29,14 @@
#if !TARGET_OS_TV
#import "SettingsViewController.h"
#else
#import <sys/utsname.h>
#endif
#import <VideoToolbox/VideoToolbox.h>
#include <Limelight.h>
@implementation MainFrameViewController {
NSOperationQueue* _opQueue;
TemporaryHost* _selectedHost;
@@ -54,45 +58,47 @@
}
static NSMutableSet* hostList;
- (void)showPIN:(NSString *)PIN {
dispatch_async(dispatch_get_main_queue(), ^{
- (void)startPairing:(NSString *)PIN {
// Needs to be synchronous to ensure the alert is shown before any potential
// failure callback could be invoked.
dispatch_sync(dispatch_get_main_queue(), ^{
self->_pairAlert = [UIAlertController alertControllerWithTitle:@"Pairing"
message:[NSString stringWithFormat:@"Enter the following PIN on the host machine: %@", PIN]
preferredStyle:UIAlertControllerStyleAlert];
[self->_pairAlert addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleDestructive handler:^(UIAlertAction* action) {
self->_pairAlert = nil;
[self->_discMan startDiscovery];
[self hideLoadingFrame: nil];
[self hideLoadingFrame: ^{
[self showHostSelectionView];
}];
}]];
[[self activeViewController] presentViewController:self->_pairAlert animated:YES completion:nil];
});
}
- (void)displayFailureDialog:(NSString *)message {
- (void)displayPairingFailureDialog:(NSString *)message {
UIAlertController* failedDialog = [UIAlertController alertControllerWithTitle:@"Pairing Failed"
message:message
preferredStyle:UIAlertControllerStyleAlert];
[Utils addHelpOptionToDialog:failedDialog];
[failedDialog addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
[_discMan startDiscovery];
[self hideLoadingFrame: ^{
[self showHostSelectionView];
[[self activeViewController] presentViewController:failedDialog animated:YES completion:nil];
}];
[_discMan startDiscovery];
}
- (void)pairFailed:(NSString *)message {
dispatch_async(dispatch_get_main_queue(), ^{
if (self->_pairAlert != nil) {
[self->_pairAlert dismissViewControllerAnimated:YES completion:^{
[self displayFailureDialog:message];
[self displayPairingFailureDialog:message];
}];
self->_pairAlert = nil;
}
else {
[self displayFailureDialog:message];
}
});
}
@@ -121,12 +127,28 @@ static NSMutableSet* hostList;
#endif
}
- (void)updateTitle {
if (_selectedHost != nil) {
self.title = _selectedHost.name;
}
else if ([hostList count] == 0) {
self.title = @"Searching for PCs on your network...";
}
else {
self.title = @"Select Host";
}
}
- (void)alreadyPaired {
BOOL usingCachedAppList = false;
// Capture the host here because it can change once we
// leave the main thread
TemporaryHost* host = _selectedHost;
if (host == nil) {
[self hideLoadingFrame: nil];
return;
}
if ([host.appList count] > 0) {
usingCachedAppList = true;
@@ -136,9 +158,6 @@ static NSMutableSet* hostList;
return;
}
self.title = host.name;
[self enableUpButton];
[self updateAppsForHost:host];
[self hideLoadingFrame: nil];
});
@@ -146,13 +165,13 @@ static NSMutableSet* hostList;
Log(LOG_I, @"Using cached app list: %d", usingCachedAppList);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Exempt this host from discovery while handling the applist query
[self->_discMan removeHostFromDiscovery:host];
[self->_discMan pauseDiscoveryForHost:host];
AppListResponse* appListResp = [ConnectionHelper getAppListForHostWithHostIP:host.activeAddress serverCert:host.serverCert uniqueID:self->_uniqueId];
[self->_discMan addHostToDiscovery:host];
[self->_discMan resumeDiscoveryForHost:host];
if (appListResp == nil || ![appListResp isStatusOk] || [appListResp getAppList] == nil) {
if (![appListResp isStatusOk] || [appListResp getAppList] == nil) {
Log(LOG_W, @"Failed to get applist: %@", appListResp.statusMessage);
dispatch_async(dispatch_get_main_queue(), ^{
if (host != self->_selectedHost) {
@@ -160,16 +179,16 @@ static NSMutableSet* hostList;
return;
}
UIAlertController* applistAlert = [UIAlertController alertControllerWithTitle:@"Fetching App List Failed"
message:@"The connection to the PC was interrupted."
UIAlertController* applistAlert = [UIAlertController alertControllerWithTitle:@"Connection Interrupted"
message:appListResp.statusMessage
preferredStyle:UIAlertControllerStyleAlert];
[Utils addHelpOptionToDialog:applistAlert];
[applistAlert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
[self hideLoadingFrame: ^{
[self showHostSelectionView];
[[self activeViewController] presentViewController:applistAlert animated:YES completion:nil];
}];
host.state = StateOffline;
[self showHostSelectionView];
});
} else {
dispatch_async(dispatch_get_main_queue(), ^{
@@ -180,9 +199,6 @@ static NSMutableSet* hostList;
return;
}
self.title = host.name;
[self enableUpButton];
[self updateAppsForHost:host];
[self->_appManager stopRetrieving];
[self->_appManager retrieveAssetsFromHost:host];
@@ -194,10 +210,11 @@ static NSMutableSet* hostList;
- (void) updateApplist:(NSSet*) newList forHost:(TemporaryHost*)host {
DataManager* database = [[DataManager alloc] init];
NSMutableSet* newHostAppList = [NSMutableSet setWithSet:host.appList];
for (TemporaryApp* app in newList) {
BOOL appAlreadyInList = NO;
for (TemporaryApp* savedApp in host.appList) {
for (TemporaryApp* savedApp in newHostAppList) {
if ([app.id isEqualToString:savedApp.id]) {
savedApp.name = app.name;
savedApp.hdrSupported = app.hdrSupported;
@@ -207,7 +224,7 @@ static NSMutableSet* hostList;
}
if (!appAlreadyInList) {
app.host = host;
[host.appList addObject:app];
[newHostAppList addObject:app];
}
}
@@ -215,7 +232,7 @@ static NSMutableSet* hostList;
do {
appWasRemoved = NO;
for (TemporaryApp* app in host.appList) {
for (TemporaryApp* app in newHostAppList) {
appWasRemoved = YES;
for (TemporaryApp* mergedApp in newList) {
if ([mergedApp.id isEqualToString:app.id]) {
@@ -227,7 +244,7 @@ static NSMutableSet* hostList;
// Removing the app mutates the list we're iterating (which isn't legal).
// We need to jump out of this loop and restart enumeration.
[host.appList removeObject:app];
[newHostAppList removeObject:app];
// It's important to remove the app record from the database
// since we'll have a constraint violation now that appList
@@ -240,21 +257,28 @@ static NSMutableSet* hostList;
// Keep looping until the list is no longer being mutated
} while (appWasRemoved);
host.appList = newHostAppList;
[database updateAppsForExistingHost:host];
// This host may be eligible for a shortcut now that the app list
// has been populated
[self updateHostShortcuts];
}
- (void)showHostSelectionView {
#if TARGET_OS_TV
// Remove the menu button intercept to allow the app to exit
// when at the host selection view.
[self.view removeGestureRecognizer:_menuRecognizer];
[self.navigationController.view removeGestureRecognizer:_menuRecognizer];
#endif
[_appManager stopRetrieving];
_selectedHost = nil;
_sortedAppList = nil;
self.title = @"Select Host";
[self updateTitle];
[self disableUpButton];
[self.collectionView reloadData];
@@ -292,11 +316,13 @@ static NSMutableSet* hostList;
Log(LOG_D, @"Clicked host: %@", host.name);
_selectedHost = host;
[self updateTitle];
[self enableUpButton];
[self disableNavigation];
#if TARGET_OS_TV
// Intercept the menu key to go back to the host page
[self.view addGestureRecognizer:_menuRecognizer];
[self.navigationController.view addGestureRecognizer:_menuRecognizer];
#endif
// If we are online, paired, and have a cached app list, skip straight
@@ -311,16 +337,31 @@ static NSMutableSet* hostList;
[self showLoadingFrame: ^{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Wait for the PC's status to be known
while (host.state == StateUnknown) {
sleep(1);
}
// Don't bother polling if the server is already offline
if (host.state == StateOffline) {
dispatch_async(dispatch_get_main_queue(), ^{
[self hideLoadingFrame:^{
[self showHostSelectionView];
}];
});
return;
}
HttpManager* hMan = [[HttpManager alloc] initWithHost:host.activeAddress uniqueId:self->_uniqueId serverCert:host.serverCert];
ServerInfoResponse* serverInfoResp = [[ServerInfoResponse alloc] init];
// Exempt this host from discovery while handling the serverinfo request
[self->_discMan removeHostFromDiscovery:host];
[self->_discMan pauseDiscoveryForHost:host];
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResp withUrlRequest:[hMan newServerInfoRequest:false]
fallbackError:401 fallbackRequest:[hMan newHttpServerInfoRequest]]];
[self->_discMan addHostToDiscovery:host];
[self->_discMan resumeDiscoveryForHost:host];
if (serverInfoResp == nil || ![serverInfoResp isStatusOk]) {
if (![serverInfoResp isStatusOk]) {
Log(LOG_W, @"Failed to get server info: %@", serverInfoResp.statusMessage);
dispatch_async(dispatch_get_main_queue(), ^{
if (host != self->_selectedHost) {
@@ -328,8 +369,8 @@ static NSMutableSet* hostList;
return;
}
UIAlertController* applistAlert = [UIAlertController alertControllerWithTitle:@"Fetching Server Info Failed"
message:@"The connection to the PC was interrupted."
UIAlertController* applistAlert = [UIAlertController alertControllerWithTitle:@"Connection Failed"
message:serverInfoResp.statusMessage
preferredStyle:UIAlertControllerStyleAlert];
[Utils addHelpOptionToDialog:applistAlert];
[applistAlert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
@@ -337,13 +378,13 @@ static NSMutableSet* hostList;
// Only display an alert if this was the result of a real
// user action, not just passively entering the foreground again
[self hideLoadingFrame: ^{
[self showHostSelectionView];
if (view != nil) {
[[self activeViewController] presentViewController:applistAlert animated:YES completion:nil];
}
}];
host.state = StateOffline;
[self showHostSelectionView];
});
} else {
// Update the host object with this data
@@ -411,8 +452,8 @@ static NSMutableSet* hostList;
UIAlertController* longClickAlert = [UIAlertController alertControllerWithTitle:host.name message:message preferredStyle:UIAlertControllerStyleActionSheet];
if (host.state != StateOnline) {
[longClickAlert addAction:[UIAlertAction actionWithTitle:@"Wake" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
UIAlertController* wolAlert = [UIAlertController alertControllerWithTitle:@"Wake On LAN" message:@"" preferredStyle:UIAlertControllerStyleAlert];
[longClickAlert addAction:[UIAlertAction actionWithTitle:@"Wake PC" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
UIAlertController* wolAlert = [UIAlertController alertControllerWithTitle:@"Wake-On-LAN" message:@"" preferredStyle:UIAlertControllerStyleAlert];
[wolAlert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
if (host.mac == nil || [host.mac isEqualToString:@"00:00:00:00:00:00"]) {
wolAlert.message = @"Host MAC unknown, unable to send WOL Packet";
@@ -420,17 +461,54 @@ static NSMutableSet* hostList;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[WakeOnLanManager wakeHost:host];
});
wolAlert.message = @"Sent WOL Packet";
wolAlert.message = @"Successfully sent wake-up request. It may take a few moments for the PC to wake. If it never wakes up, ensure it's properly configured for Wake-on-LAN.";
}
[[self activeViewController] presentViewController:wolAlert animated:YES completion:nil];
}]];
}
[longClickAlert addAction:[UIAlertAction actionWithTitle:@"Test Network" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action) {
[self showLoadingFrame:^{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Perform the network test on a GCD worker thread. It may take a while.
unsigned int portTestResult = LiTestClientConnectivity(CONN_TEST_SERVER, 443, ML_PORT_FLAG_ALL);
dispatch_sync(dispatch_get_main_queue(), ^{
[self hideLoadingFrame:^{
NSString* message;
if (portTestResult == 0) {
message = @"This network does not appear to be blocking Moonlight. If you still have trouble connecting, check your PC's firewall settings.\n\nVisit the Moonlight Setup Guide on GitHub for additional setup help and troubleshooting steps.";
}
else if (portTestResult == ML_TEST_RESULT_INCONCLUSIVE) {
message = @"The network test could not be performed because none of Moonlight's connection testing servers were reachable. Check your Internet connection or try again later.";
}
else {
message = @"Your current network connection seems to be blocking Moonlight. Streaming may not work while connected to this network.\n\nThe following network ports were blocked:\n";
for (int i = 0; i < 32; i++) {
if (portTestResult & (1 << i)) {
NSString* portString = [NSString stringWithFormat:@"%s %u\n",
LiGetProtocolFromPortFlagIndex(i) == 17 ? "UDP" : "TCP",
LiGetPortFromPortFlagIndex(i)];
message = [message stringByAppendingString:portString];
}
}
}
UIAlertController* netTestAlert = [UIAlertController alertControllerWithTitle:@"Network Test Complete" message:message preferredStyle:UIAlertControllerStyleAlert];
[netTestAlert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
[[self activeViewController] presentViewController:netTestAlert animated:YES completion:nil];
}];
});
});
}];
}]];
#if !TARGET_OS_TV
if (host.state != StateOnline) {
[longClickAlert addAction:[UIAlertAction actionWithTitle:@"Connection Help" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting"]];
}]];
#endif
}
#endif
[longClickAlert addAction:[UIAlertAction actionWithTitle:@"Remove Host" style:UIAlertActionStyleDestructive handler:^(UIAlertAction* action) {
[self->_discMan removeHostFromDiscovery:host];
DataManager* dataMan = [[DataManager alloc] init];
@@ -454,28 +532,41 @@ static NSMutableSet* hostList;
- (void) addHostClicked {
Log(LOG_D, @"Clicked add host");
UIAlertController* alertController = [UIAlertController alertControllerWithTitle:@"Host Address" message:@"Please enter a hostname or IP address" preferredStyle:UIAlertControllerStyleAlert];
UIAlertController* alertController = [UIAlertController alertControllerWithTitle:@"Add Host Manually" message:@"If Moonlight doesn't find your local gaming PC automatically,\nenter the IP address of your PC" preferredStyle:UIAlertControllerStyleAlert];
[alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]];
[alertController addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
NSString* hostAddress = ((UITextField*)[[alertController textFields] objectAtIndex:0]).text;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self->_discMan discoverHost:hostAddress withCallback:^(TemporaryHost* host, NSString* error){
if (host != nil) {
dispatch_async(dispatch_get_main_queue(), ^{
@synchronized(hostList) {
[hostList addObject:host];
[self showLoadingFrame:^{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self->_discMan discoverHost:hostAddress withCallback:^(TemporaryHost* host, NSString* error){
if (host != nil) {
dispatch_async(dispatch_get_main_queue(), ^{
[self hideLoadingFrame:^{
@synchronized(hostList) {
[hostList addObject:host];
}
[self updateHosts];
}];
});
} else {
unsigned int portTestResults = LiTestClientConnectivity(CONN_TEST_SERVER, 443,
ML_PORT_FLAG_TCP_47984 | ML_PORT_FLAG_TCP_47989);
if (portTestResults != ML_TEST_RESULT_INCONCLUSIVE && portTestResults != 0) {
error = [error stringByAppendingString:@"\n\nYour device's network connection is blocking Moonlight. Streaming may not work while connected to this network."];
}
[self updateHosts];
});
} else {
UIAlertController* hostNotFoundAlert = [UIAlertController alertControllerWithTitle:@"Add Host" message:error preferredStyle:UIAlertControllerStyleAlert];
[Utils addHelpOptionToDialog:hostNotFoundAlert];
[hostNotFoundAlert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
dispatch_async(dispatch_get_main_queue(), ^{
[[self activeViewController] presentViewController:hostNotFoundAlert animated:YES completion:nil];
});
}
}];});
UIAlertController* hostNotFoundAlert = [UIAlertController alertControllerWithTitle:@"Add Host Manually" message:error preferredStyle:UIAlertControllerStyleAlert];
[Utils addHelpOptionToDialog:hostNotFoundAlert];
[hostNotFoundAlert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
dispatch_async(dispatch_get_main_queue(), ^{
[self hideLoadingFrame:^{
[[self activeViewController] presentViewController:hostNotFoundAlert animated:YES completion:nil];
}];
});
}
}];
});
}];
}]];
[alertController addTextFieldWithConfigurationHandler:nil];
[[self activeViewController] presentViewController:alertController animated:YES completion:nil];
@@ -492,9 +583,28 @@ static NSMutableSet* hostList;
TemporarySettings* streamSettings = [dataMan getSettings];
_streamConfig.frameRate = [streamSettings.framerate intValue];
_streamConfig.bitRate = [streamSettings.bitrate intValue];
if (@available(iOS 10.3, *)) {
// Don't stream more FPS than the display can show
if (_streamConfig.frameRate > [UIScreen mainScreen].maximumFramesPerSecond) {
_streamConfig.frameRate = (int)[UIScreen mainScreen].maximumFramesPerSecond;
Log(LOG_W, @"Clamping FPS to maximum refresh rate: %d", _streamConfig.frameRate);
}
}
_streamConfig.height = [streamSettings.height intValue];
_streamConfig.width = [streamSettings.width intValue];
#if TARGET_OS_TV
// Don't allow streaming 4K on the Apple TV HD
struct utsname systemInfo;
uname(&systemInfo);
if (strcmp(systemInfo.machine, "AppleTV5,3") == 0 && _streamConfig.height >= 2160) {
Log(LOG_W, @"4K streaming not supported on Apple TV HD");
_streamConfig.width = 1920;
_streamConfig.height = 1080;
}
#endif
_streamConfig.bitRate = [streamSettings.bitrate intValue];
_streamConfig.streamingRemotely = streamSettings.streamingRemotely;
_streamConfig.optimizeGameSettings = streamSettings.optimizeGames;
_streamConfig.playAudioOnPC = streamSettings.playAudioOnPC;
@@ -506,14 +616,16 @@ static NSMutableSet* hostList;
// Probe for supported channel configurations
Log(LOG_I, @"Audio device supports %d channels", [AVAudioSession sharedInstance].maximumOutputNumberOfChannels);
if ([AVAudioSession sharedInstance].maximumOutputNumberOfChannels >= 6) {
_streamConfig.audioChannelCount = 6;
_streamConfig.audioChannelMask = 0xFC;
long outputChannels = [AVAudioSession sharedInstance].maximumOutputNumberOfChannels;
Log(LOG_I, @"Audio device supports %d channels", outputChannels);
if (outputChannels >= 8) {
_streamConfig.audioConfiguration = AUDIO_CONFIGURATION_71_SURROUND;
}
else if (outputChannels >= 6) {
_streamConfig.audioConfiguration = AUDIO_CONFIGURATION_51_SURROUND;
}
else {
_streamConfig.audioChannelCount = 2;
_streamConfig.audioChannelMask = 0x3;
_streamConfig.audioConfiguration = AUDIO_CONFIGURATION_STEREO;
}
// HDR requires HDR10 game, HDR10 display, and HEVC Main10 decoder on the client.
@@ -566,7 +678,7 @@ static NSMutableSet* hostList;
HttpRequest* quitRequest = [HttpRequest requestForResponse: quitResponse withUrlRequest:[hMan newQuitAppRequest]];
// Exempt this host from discovery while handling the quit operation
[self->_discMan removeHostFromDiscovery:app.host];
[self->_discMan pauseDiscoveryForHost:app.host];
[hMan executeRequestSynchronously:quitRequest];
if (quitResponse.statusCode == 200) {
ServerInfoResponse* serverInfoResp = [[ServerInfoResponse alloc] init];
@@ -583,7 +695,7 @@ static NSMutableSet* hostList;
[serverInfoResp populateHost:app.host];
}
}
[self->_discMan addHostToDiscovery:app.host];
[self->_discMan resumeDiscoveryForHost:app.host];
// If it fails, display an error and stop the current operation
if (quitResponse.statusCode != 200) {
@@ -691,9 +803,7 @@ static NSMutableSet* hostList;
- (void)viewDidLoad
{
[super viewDidLoad];
self.title = @"Select Host";
#if !TARGET_OS_TV
// Set the side bar button action. When it's tapped, it'll show the sidebar.
[_settingsButton setTarget:self.revealViewController];
@@ -714,6 +824,14 @@ static NSMutableSet* hostList;
// if the user drags all the way off the screen opposite the settings pane.
self.revealViewController.bounceBackOnOverdraw = NO;
#else
// The settings button will direct the user into the Settings app on tvOS
[_settingsButton setTarget:self];
[_settingsButton setAction:@selector(openTvSettings:)];
// Restore focus on the selected app on view controller pop navigation
self.restoresFocusAfterTransition = NO;
self.collectionView.remembersLastFocusedIndexPath = YES;
_menuRecognizer = [[UITapGestureRecognizer alloc] init];
[_menuRecognizer addTarget:self action: @selector(showHostSelectionView)];
_menuRecognizer.allowedPressTypes = [[NSArray alloc] initWithObjects:[NSNumber numberWithLong:UIPressTypeMenu], nil];
@@ -754,30 +872,78 @@ static NSMutableSet* hostList;
[self retrieveSavedHosts];
_discMan = [[DiscoveryManager alloc] initWithHosts:[hostList allObjects] andCallback:self];
[self.view addSubview:hostScrollView];
if ([hostList count] == 1) {
[self hostClicked:[hostList anyObject] view:nil];
}
else {
[self updateTitle];
[self.view addSubview:hostScrollView];
}
}
-(void)beginForegroundRefresh:(bool)refreshAppList
#if TARGET_OS_TV
- (void)openTvSettings:(id)sender
{
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString] options:@{} completionHandler:nil];
}
#endif
-(void)beginForegroundRefresh
{
if (!_background) {
// This will kick off box art caching
[self updateHosts];
// Reset state first so we can rediscover hosts that were deleted before
[_discMan resetDiscoveryState];
[_discMan startDiscovery];
// This will refresh the applist when a paired host is selected
if (refreshAppList && _selectedHost != nil && _selectedHost.pairState == PairStatePaired) {
if (_selectedHost != nil && _selectedHost.pairState == PairStatePaired) {
[self hostClicked:_selectedHost view:nil];
}
}
}
-(void)handlePendingShortcutAction
{
// Check if we have a pending shortcut action
AppDelegate* delegate = (AppDelegate*)[UIApplication sharedApplication].delegate;
if (delegate.pcUuidToLoad != nil) {
// Find the host it corresponds to
TemporaryHost* matchingHost = nil;
for (TemporaryHost* host in hostList) {
if ([host.uuid isEqualToString:delegate.pcUuidToLoad]) {
matchingHost = host;
break;
}
}
// Clear the pending shortcut action
delegate.pcUuidToLoad = nil;
// Complete the request
if (delegate.shortcutCompletionHandler != nil) {
delegate.shortcutCompletionHandler(matchingHost != nil);
delegate.shortcutCompletionHandler = nil;
}
if (matchingHost != nil && _selectedHost != matchingHost) {
// Navigate to the host page
[self hostClicked:matchingHost view:nil];
}
}
}
-(void)handleReturnToForeground
{
_background = NO;
[self beginForegroundRefresh: YES];
[self beginForegroundRefresh];
// Check for a pending shortcut action when returning to foreground
[self handlePendingShortcutAction];
}
-(void)handleEnterBackground
@@ -802,6 +968,9 @@ static NSMutableSet* hostList;
[self.navigationController.navigationBar setShadowImage:fakeImage];
[self.navigationController.navigationBar setBackgroundImage:fakeImage forBarPosition:UIBarPositionAny barMetrics:UIBarMetricsDefault];
// Check for a pending shortcut action when appearing
[self handlePendingShortcutAction];
[[NSNotificationCenter defaultCenter] addObserver: self
selector: @selector(handleReturnToForeground)
name: UIApplicationDidBecomeActiveNotification
@@ -828,12 +997,7 @@ static NSMutableSet* hostList;
// this view via an error dialog from the stream
// view, so we won't get a return to active notification
// for that which would normally fire beginForegroundRefresh.
//
// On tvOS, we'll get a viewWillAppear when returning from the
// loading frame which will cause an infinite loop by starting
// another loading frame. To avoid this, just don't refresh
// if we're coming back from a loading frame view.
[self beginForegroundRefresh: !([_loadingFrame isShown] || [_loadingFrame isBeingDismissed])];
[self beginForegroundRefresh];
}
- (void)viewDidDisappear:(BOOL)animated
@@ -890,6 +1054,32 @@ static NSMutableSet* hostList;
});
}
- (void)updateHostShortcuts {
#if !TARGET_OS_TV
if (@available(iOS 9.0, *)) {
NSMutableArray* quickActions = [[NSMutableArray alloc] init];
@synchronized (hostList) {
for (TemporaryHost* host in hostList) {
// Pair state may be unknown if we haven't polled it yet, but the app list
// count will persist from paired PCs
if ([host.appList count] > 0) {
UIApplicationShortcutItem* shortcut = [[UIApplicationShortcutItem alloc]
initWithType:@"PC"
localizedTitle:host.name
localizedSubtitle:nil
icon:[UIApplicationShortcutIcon iconWithType:UIApplicationShortcutIconTypePlay]
userInfo:[NSDictionary dictionaryWithObject:host.uuid forKey:@"UUID"]];
[quickActions addObject: shortcut];
}
}
}
[UIApplication sharedApplication].shortcutItems = quickActions;
}
#endif
}
- (void)updateHosts {
Log(LOG_I, @"Updating hosts...");
[[hostScrollView subviews] makeObjectsPerformSelector:@selector(removeFromSuperview)];
@@ -914,6 +1104,12 @@ static NSMutableSet* hostList;
}
}
// Create or delete host shortcuts as needed
[self updateHostShortcuts];
// Update the title in case we now have a PC
[self updateTitle];
prevEdge = [self getCompViewX:addComp addComp:addComp prevEdge:prevEdge];
addComp.center = CGPointMake(prevEdge, hostScrollView.frame.size.height / 2);
@@ -1034,8 +1230,8 @@ static NSMutableSet* hostList;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
if (_selectedHost != nil) {
return _selectedHost.appList.count;
if (_selectedHost != nil && _sortedAppList != nil) {
return _sortedAppList.count;
}
else {
return 0;
@@ -1067,10 +1263,12 @@ static NSMutableSet* hostList;
- (void) disableNavigation {
self.navigationController.navigationBar.topItem.rightBarButtonItem.enabled = NO;
self.navigationController.navigationBar.topItem.leftBarButtonItem.enabled = NO;
}
- (void) enableNavigation {
self.navigationController.navigationBar.topItem.rightBarButtonItem.enabled = YES;
self.navigationController.navigationBar.topItem.leftBarButtonItem.enabled = YES;
}
#if TARGET_OS_TV
@@ -19,6 +19,7 @@
@property (strong, nonatomic) IBOutlet UISegmentedControl *multiControllerSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *audioOnPCSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *hevcSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *touchscreenModeSelector;
@property (strong, nonatomic) IBOutlet UIScrollView *scrollView;
#pragma clang diagnostic push
@@ -191,6 +191,7 @@ static const int bitrateTable[] = {
[self.hevcSelector setEnabled:NO];
}
[self.touchscreenModeSelector setSelectedSegmentIndex:currentSettings.absoluteTouchMode ? 1 : 0];
[self.optimizeSettingsSelector setSelectedSegmentIndex:currentSettings.optimizeGames ? 1 : 0];
[self.multiControllerSelector setSelectedSegmentIndex:currentSettings.multiController ? 1 : 0];
[self.audioOnPCSelector setSelectedSegmentIndex:currentSettings.playAudioOnPC ? 1 : 0];
@@ -290,17 +291,18 @@ static const int bitrateTable[] = {
BOOL multiController = [self.multiControllerSelector selectedSegmentIndex] == 1;
BOOL audioOnPC = [self.audioOnPCSelector selectedSegmentIndex] == 1;
BOOL useHevc = [self.hevcSelector selectedSegmentIndex] == 1;
BOOL absoluteTouchMode = [self.touchscreenModeSelector selectedSegmentIndex] == 1;
[dataMan saveSettingsWithBitrate:_bitrate
framerate:framerate
height:height
width:width
onscreenControls:onscreenControls
remote:NO
optimizeGames:optimizeGames
multiController:multiController
audioOnPC:audioOnPC
useHevc:useHevc
enableHdr:NO];
enableHdr:NO
absoluteTouchMode:absoluteTouchMode];
}
- (void)didReceiveMemoryWarning {
@@ -15,9 +15,9 @@
#if TARGET_OS_TV
@import GameController;
@interface StreamFrameViewController : GCEventViewController <ConnectionCallbacks, EdgeDetectionDelegate>
@interface StreamFrameViewController : GCEventViewController <ConnectionCallbacks, EdgeDetectionDelegate, GamepadPresenceDelegate, UserInteractionDelegate>
#else
@interface StreamFrameViewController : UIViewController <ConnectionCallbacks, EdgeDetectionDelegate>
@interface StreamFrameViewController : UIViewController <ConnectionCallbacks, EdgeDetectionDelegate, GamepadPresenceDelegate, UserInteractionDelegate>
#endif
@property (strong, nonatomic) IBOutlet UILabel *stageLabel;
@property (strong, nonatomic) IBOutlet UILabel *tipLabel;
@@ -24,6 +24,8 @@
UITapGestureRecognizer *_menuGestureRecognizer;
UITapGestureRecognizer *_menuDoubleTapGestureRecognizer;
UITextView *_overlayView;
StreamView *_streamView;
BOOL _userIsInteracting;
}
- (void)viewDidAppear:(BOOL)animated
@@ -35,7 +37,7 @@
#endif
}
#ifdef TARGET_OS_TV
#if TARGET_OS_TV
- (void)controllerPauseButtonPressed:(id)sender { }
- (void)controllerPauseButtonDoublePressed:(id)sender {
Log(LOG_I, @"Menu double-pressed -- backing out of stream");
@@ -50,8 +52,6 @@
[self.navigationController setNavigationBarHidden:YES animated:YES];
[(StreamView*)self.view setupStreamView];
[self.stageLabel setText:[NSString stringWithFormat:@"Starting %@...", self.streamConfig.appName]];
[self.stageLabel sizeToFit];
self.stageLabel.textAlignment = NSTextAlignmentCenter;
@@ -59,8 +59,12 @@
self.spinner.center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height / 2 - self.stageLabel.frame.size.height - self.spinner.frame.size.height);
[UIApplication sharedApplication].idleTimerDisabled = YES;
_controllerSupport = [[ControllerSupport alloc] initWithConfig:self.streamConfig];
_controllerSupport = [[ControllerSupport alloc] initWithConfig:self.streamConfig presenceDelegate:self];
_inactivityTimer = nil;
_streamView = (StreamView*)self.view;
[_streamView setupStreamView:_controllerSupport swipeDelegate:self interactionDelegate:self config:self.streamConfig];
#if TARGET_OS_TV
if (!_menuGestureRecognizer || !_menuDoubleTapGestureRecognizer) {
_menuGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(controllerPauseButtonPressed:)];
@@ -125,7 +129,7 @@
- (void)updateOverlayText:(NSString*)text {
if (_overlayView == nil) {
_overlayView = [[UITextView alloc] init];
#ifndef TARGET_OS_TV
#if !TARGET_OS_TV
[_overlayView setEditable:NO];
#endif
[_overlayView setUserInteractionEnabled:NO];
@@ -134,7 +138,11 @@
[_overlayView setTextAlignment:NSTextAlignmentCenter];
[_overlayView setTextColor:[OSColor lightGrayColor]];
[_overlayView setBackgroundColor:[OSColor blackColor]];
#if TARGET_OS_TV
[_overlayView setFont:[UIFont systemFontOfSize:24]];
#else
[_overlayView setFont:[UIFont systemFontOfSize:12]];
#endif
[_overlayView setAlpha:0.5];
[self.view addSubview:_overlayView];
}
@@ -160,6 +168,11 @@
[_inactivityTimer invalidate];
}
#if TARGET_OS_TV
// Terminate the stream immediately on tvOS
Log(LOG_I, @"Terminating stream after resigning active");
[self returnToMainFrame];
#else
// Terminate the stream if the app is inactive for 10 seconds
Log(LOG_I, @"Starting inactivity termination timer");
_inactivityTimer = [NSTimer scheduledTimerWithTimeInterval:10
@@ -167,6 +180,7 @@
selector:@selector(inactiveTimerExpired:)
userInfo:nil
repeats:NO];
#endif
}
- (void)inactiveTimerExpired:(NSTimer*)timer {
@@ -211,27 +225,59 @@
// the first frame of video.
self.stageLabel.hidden = YES;
self.tipLabel.hidden = YES;
#if !TARGET_OS_TV
[(StreamView*)self.view setupOnScreenControls: self->_controllerSupport swipeDelegate:self];
#endif
[self->_streamView showOnScreenControls];
});
}
- (void)connectionTerminated:(long)errorCode {
Log(LOG_I, @"Connection terminated: %ld", errorCode);
- (void)connectionTerminated:(int)errorCode {
Log(LOG_I, @"Connection terminated: %d", errorCode);
unsigned int portTestResults = LiTestClientConnectivity(CONN_TEST_SERVER, 443,
LiGetPortFlagsFromTerminationErrorCode(errorCode));
dispatch_async(dispatch_get_main_queue(), ^{
// Allow the display to go to sleep now
[UIApplication sharedApplication].idleTimerDisabled = NO;
if (errorCode == 0) {
[self returnToMainFrame];
} else {
UIAlertController* conTermAlert = [UIAlertController alertControllerWithTitle:@"Connection Terminated" message:@"The connection was terminated" preferredStyle:UIAlertControllerStyleAlert];
[conTermAlert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
[self returnToMainFrame];
}]];
[self presentViewController:conTermAlert animated:YES completion:nil];
NSString* title;
NSString* message;
if (portTestResults != ML_TEST_RESULT_INCONCLUSIVE && portTestResults != 0) {
title = @"Connection Error";
message = @"Your device's network connection is blocking Moonlight. Streaming may not work while connected to this network.";
}
else {
switch (errorCode) {
case ML_ERROR_GRACEFUL_TERMINATION:
[self returnToMainFrame];
return;
case ML_ERROR_NO_VIDEO_TRAFFIC:
title = @"Connection Error";
message = @"No video received from host. Check the host PC's firewall and port forwarding rules.";
break;
case ML_ERROR_NO_VIDEO_FRAME:
title = @"Connection Error";
message = @"Your network connection isn't performing well. Reduce your video bitrate setting or try a faster connection.";
break;
default:
title = @"Connection Terminated";
message = @"The connection was terminated";
break;
}
}
UIAlertController* conTermAlert = [UIAlertController alertControllerWithTitle:title
message:message
preferredStyle:UIAlertControllerStyleAlert];
[Utils addHelpOptionToDialog:conTermAlert];
[conTermAlert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
[self returnToMainFrame];
}]];
[self presentViewController:conTermAlert animated:YES completion:nil];
});
[_streamMan stopStream];
@@ -251,16 +297,22 @@
- (void) stageComplete:(const char*)stageName {
}
- (void) stageFailed:(const char*)stageName withError:(long)errorCode {
Log(LOG_I, @"Stage %s failed: %ld", stageName, errorCode);
- (void) stageFailed:(const char*)stageName withError:(int)errorCode portTestFlags:(int)portTestFlags {
Log(LOG_I, @"Stage %s failed: %d", stageName, errorCode);
unsigned int portTestResults = LiTestClientConnectivity(CONN_TEST_SERVER, 443, portTestFlags);
dispatch_async(dispatch_get_main_queue(), ^{
// Allow the display to go to sleep now
[UIApplication sharedApplication].idleTimerDisabled = NO;
NSString* message = [NSString stringWithFormat:@"%s failed with error %d", stageName, errorCode];
if (portTestResults != ML_TEST_RESULT_INCONCLUSIVE && portTestResults != 0) {
message = [message stringByAppendingString:@"\n\nYour device's network connection is blocking Moonlight. Streaming may not work while connected to this network."];
}
UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"Connection Failed"
message:[NSString stringWithFormat:@"%s failed with error %ld",
stageName, errorCode]
message:message
preferredStyle:UIAlertControllerStyleAlert];
[Utils addHelpOptionToDialog:alert];
[alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
@@ -279,7 +331,7 @@
// Allow the display to go to sleep now
[UIApplication sharedApplication].idleTimerDisabled = NO;
UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"Connection Failed"
UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"Connection Error"
message:message
preferredStyle:UIAlertControllerStyleAlert];
[Utils addHelpOptionToDialog:alert];
@@ -323,12 +375,60 @@
// Dispose of any resources that can be recreated.
}
- (void)gamepadPresenceChanged {
#if !TARGET_OS_TV
if (@available(iOS 11.0, *)) {
[self setNeedsUpdateOfHomeIndicatorAutoHidden];
}
#endif
}
- (void)userInteractionBegan {
// Disable hiding home bar when user is interacting.
// iOS will force it to be shown anyway, but it will
// also discard our edges deferring system gestures unless
// we willingly give up home bar hiding preference.
_userIsInteracting = YES;
#if !TARGET_OS_TV
if (@available(iOS 11.0, *)) {
[self setNeedsUpdateOfHomeIndicatorAutoHidden];
}
#endif
}
- (void)userInteractionEnded {
// Enable home bar hiding again if conditions allow
_userIsInteracting = NO;
#if !TARGET_OS_TV
if (@available(iOS 11.0, *)) {
[self setNeedsUpdateOfHomeIndicatorAutoHidden];
}
#endif
}
#if !TARGET_OS_TV
// Require a confirmation when streaming to activate a system gesture
- (UIRectEdge)preferredScreenEdgesDeferringSystemGestures {
return UIRectEdgeAll;
}
- (BOOL)prefersHomeIndicatorAutoHidden {
if ([_controllerSupport getConnectedGamepadCount] > 0 &&
[_streamView getCurrentOscState] == OnScreenControlsLevelOff &&
_userIsInteracting == NO) {
// Autohide the home bar when a gamepad is connected
// and the on-screen controls are disabled. We can't
// do this all the time because any touch on the display
// will cause the home indicator to reappear, and our
// preferredScreenEdgesDeferringSystemGestures will also
// be suppressed (leading to possible errant exits of the
// stream).
return YES;
}
return NO;
}
- (BOOL)shouldAutorotate {
return YES;
}
-1157
View File
File diff suppressed because it is too large. Load diff
@@ -3,6 +3,7 @@
{
"orientation" : "landscape",
"idiom" : "tv",
"filename" : "Simulator Screen Shot - Apple TV 4K - 2019-09-22 at 20.39.12.png",
"extent" : "full-screen",
"minimum-system-version" : "11.0",
"scale" : "2x"
+10 -6
View File
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder.AppleTV.Storyboard" version="3.0" toolsVersion="14868" targetRuntime="AppleTV" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="pui-7y-JNH">
<document type="com.apple.InterfaceBuilder.AppleTV.Storyboard" version="3.0" toolsVersion="16096" targetRuntime="AppleTV" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="pui-7y-JNH">
<device id="appleTV" appearance="dark"/>
<dependencies>
<deployment identifier="tvOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14824"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@@ -37,7 +37,7 @@
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" prefetchingEnabled="YES" id="8P0-wP-bDe">
<rect key="frame" x="0.0" y="0.0" width="1920" height="875"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="50" minimumInteritemSpacing="50" id="m6l-TM-5FZ">
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="75" minimumInteritemSpacing="50" id="m6l-TM-5FZ">
<size key="itemSize" width="300" height="400"/>
<size key="headerReferenceSize" width="0.0" height="0.0"/>
<size key="footerReferenceSize" width="0.0" height="0.0"/>
@@ -58,8 +58,11 @@
<outlet property="delegate" destination="vKn-MI-uG7" id="U6n-Uv-2Do"/>
</connections>
</collectionView>
<navigationItem key="navigationItem" id="4ja-Go-cNV"/>
<navigationItem key="navigationItem" id="4ja-Go-cNV">
<barButtonItem key="leftBarButtonItem" title="Settings" id="Iii-CS-fMJ" userLabel="Settings"/>
</navigationItem>
<connections>
<outlet property="settingsButton" destination="Iii-CS-fMJ" id="32S-2R-fU5"/>
<segue destination="YUB-6u-B7f" kind="show" identifier="createStreamFrame" id="7lZ-Tj-a6a"/>
</connections>
</collectionViewController>
@@ -70,12 +73,12 @@
<!--Loading Frame View Controller-->
<scene sceneID="lqn-qp-p7w">
<objects>
<viewController storyboardIdentifier="loadingFrame" id="Ap5-6u-5Wo" customClass="LoadingFrameViewController" sceneMemberID="viewController">
<viewController storyboardIdentifier="loadingFrame" modalTransitionStyle="crossDissolve" modalPresentationStyle="overCurrentContext" id="Ap5-6u-5Wo" customClass="LoadingFrameViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="AfI-ck-OwD"/>
<viewControllerLayoutGuide type="bottom" id="DNo-dy-ucM"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="TIa-HJ-DIv">
<view key="view" clearsContextBeforeDrawing="NO" contentMode="center" id="TIa-HJ-DIv">
<rect key="frame" x="0.0" y="0.0" width="1920" height="1080"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
@@ -84,6 +87,7 @@
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</activityIndicatorView>
</subviews>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.5" colorSpace="custom" customColorSpace="sRGB"/>
<viewLayoutGuide key="safeArea" id="3xZ-qb-ZmB"/>
</view>
<connections>
+13 -11
View File
@@ -17,24 +17,31 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>1</string>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>GCSupportedGameControllers</key>
<array>
<dict>
<key>ProfileName</key>
<string>ExtendedGamepad</string>
</dict>
<dict>
<key>ProfileName</key>
<string>Gamepad</string>
</dict>
</array>
<key>GCSupportsControllerUserInteraction</key>
<true/>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSLocalNetworkUsageDescription</key>
<string>Moonlight uses the local network to connect to your gaming PC for streaming.</string>
<key>NSBonjourServices</key>
<array>
<string>_nvstream._tcp</string>
</array>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIRequiredDeviceCapabilities</key>
@@ -43,10 +50,5 @@
</array>
<key>UIUserInterfaceStyle</key>
<string>Dark</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
</dict>
</plist>
File diff suppressed because it is too large. Load diff
@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:Moonlight macOS.xcodeproj">
</FileRef>
</Workspace>
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

@@ -1,68 +0,0 @@
{
"images" : [
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "16x icon.png",
"scale" : "1x"
},
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "32x icon-1.png",
"scale" : "2x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "32x icon.png",
"scale" : "1x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "64x icon.png",
"scale" : "2x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "128x icon.png",
"scale" : "1x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "256x icon-1.png",
"scale" : "2x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "256x icon.png",
"scale" : "1x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "512x icon-1.png",
"scale" : "2x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "512x icon.png",
"scale" : "1x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "1024x icon-1.png",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
-39
View File
@@ -1,39 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.entertainment</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSHighResolutionCapable</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2018 Moonlight Game Streaming Project. All rights reserved.</string>
<key>NSMainStoryboardFile</key>
<string>Mac</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>
-17
View File
@@ -1,17 +0,0 @@
//
// Control.h
// Moonlight macOS
//
// Created by Felix Kratz on 15.03.18.
// Copyright © 2018 Felix Kratz. All rights reserved.
//
#ifndef Control_h
#define Control_h
#include <stdio.h>
#import "ControllerSupport.h"
extern void initGamepad(ControllerSupport* controllerSupport);
#endif /* Control_h */
-223
View File
@@ -1,223 +0,0 @@
//
// Control.m
// Moonlight macOS
//
// Created by Felix Kratz on 15.03.18.
// Copyright © 2018 Felix Kratz. All rights reserved.
//
#include "Gamepad.h"
#include "Control.h"
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <math.h>
#include "Limelight.h"
#import "Moonlight-Swift.h"
@class Controller;
#ifdef _MSC_VER
#define snprintf _snprintf
#endif
Controller* _controller;
ControllerSupport* _controllerSupport;
NSMutableDictionary* _controllers;
typedef enum {
SELECT,
L3,
R3,
START,
UP,
RIGHT,
DOWN,
LEFT,
LB = 10,
RB,
Y,
B,
A,
X,
} ControllerKeys;
typedef enum {
LEFT_X,
LEFT_Y,
RIGHT_X,
RIGHT_Y,
LT = 14,
RT,
} ControllerAxis;
void onButtonDown(struct Gamepad_device * device, unsigned int buttonID, double timestamp, void * context) {
_controller = [_controllers objectForKey:[NSNumber numberWithInteger:device->deviceID]];
switch (buttonID) {
case SELECT:
[_controllerSupport setButtonFlag:_controller flags:BACK_FLAG];
break;
case L3:
[_controllerSupport setButtonFlag:_controller flags:LS_CLK_FLAG];
break;
case R3:
[_controllerSupport setButtonFlag:_controller flags:RS_CLK_FLAG];
break;
case START:
[_controllerSupport setButtonFlag:_controller flags:PLAY_FLAG];
break;
case UP:
[_controllerSupport setButtonFlag:_controller flags:UP_FLAG];
break;
case RIGHT:
[_controllerSupport setButtonFlag:_controller flags:RIGHT_FLAG];
break;
case DOWN:
[_controllerSupport setButtonFlag:_controller flags:DOWN_FLAG];
break;
case LEFT:
[_controllerSupport setButtonFlag:_controller flags:LEFT_FLAG];
break;
case LB:
[_controllerSupport setButtonFlag:_controller flags:LB_FLAG];
break;
case RB:
[_controllerSupport setButtonFlag:_controller flags:RB_FLAG];
break;
case Y:
[_controllerSupport setButtonFlag:_controller flags:Y_FLAG];
break;
case B:
[_controllerSupport setButtonFlag:_controller flags:B_FLAG];
break;
case A:
[_controllerSupport setButtonFlag:_controller flags:A_FLAG];
break;
case X:
[_controllerSupport setButtonFlag:_controller flags:X_FLAG];
break;
default:
break;
}
[_controllerSupport updateFinished:_controller];
}
void onButtonUp(struct Gamepad_device * device, unsigned int buttonID, double timestamp, void * context) {
_controller = [_controllers objectForKey:[NSNumber numberWithInteger:device->deviceID]];
switch (buttonID) {
case SELECT:
[_controllerSupport clearButtonFlag:_controller flags:BACK_FLAG];
break;
case L3:
[_controllerSupport clearButtonFlag:_controller flags:LS_CLK_FLAG];
break;
case R3:
[_controllerSupport clearButtonFlag:_controller flags:RS_CLK_FLAG];
break;
case START:
[_controllerSupport clearButtonFlag:_controller flags:PLAY_FLAG];
break;
case UP:
[_controllerSupport clearButtonFlag:_controller flags:UP_FLAG];
break;
case RIGHT:
[_controllerSupport clearButtonFlag:_controller flags:RIGHT_FLAG];
break;
case DOWN:
[_controllerSupport clearButtonFlag:_controller flags:DOWN_FLAG];
break;
case LEFT:
[_controllerSupport clearButtonFlag:_controller flags:LEFT_FLAG];
break;
case LB:
[_controllerSupport clearButtonFlag:_controller flags:LB_FLAG];
break;
case RB:
[_controllerSupport clearButtonFlag:_controller flags:RB_FLAG];
break;
case Y:
[_controllerSupport clearButtonFlag:_controller flags:Y_FLAG];
break;
case B:
[_controllerSupport clearButtonFlag:_controller flags:B_FLAG];
break;
case A:
[_controllerSupport clearButtonFlag:_controller flags:A_FLAG];
break;
case X:
[_controllerSupport clearButtonFlag:_controller flags:X_FLAG];
break;
default:
break;
}
[_controllerSupport updateFinished:_controller];
}
void onAxisMoved(struct Gamepad_device * device, unsigned int axisID, float value, float lastValue, double timestamp, void * context) {
if (fabsf(lastValue - value) > 0.01) {
// The dualshock controller has much more than these axis because of the motion axis, so it
// is better to call the updateFinished in the cases, because otherwise all of these
// motion axis will also trigger an updateFinished event.
switch (axisID) {
case LEFT_X:
_controller = [_controllers objectForKey:[NSNumber numberWithInteger:device->deviceID]];
_controller.lastLeftStickX = value * 0X7FFE;
[_controllerSupport updateFinished:_controller];
break;
case LEFT_Y:
_controller = [_controllers objectForKey:[NSNumber numberWithInteger:device->deviceID]];
_controller.lastLeftStickY = -value * 0X7FFE;
[_controllerSupport updateFinished:_controller];
break;
case RIGHT_X:
_controller = [_controllers objectForKey:[NSNumber numberWithInteger:device->deviceID]];
_controller.lastRightStickX = value * 0X7FFE;
[_controllerSupport updateFinished:_controller];
break;
case RIGHT_Y:
_controller = [_controllers objectForKey:[NSNumber numberWithInteger:device->deviceID]];
_controller.lastRightStickY = -value * 0X7FFE;
[_controllerSupport updateFinished:_controller];
break;
case LT:
_controller = [_controllers objectForKey:[NSNumber numberWithInteger:device->deviceID]];
_controller.lastLeftTrigger = value * 0xFF;
[_controllerSupport updateFinished:_controller];
break;
case RT:
_controller = [_controllers objectForKey:[NSNumber numberWithInteger:device->deviceID]];
_controller.lastRightTrigger = value * 0xFF;
[_controllerSupport updateFinished:_controller];
break;
default:
break;
}
}
}
void onDeviceAttached(struct Gamepad_device * device, void * context) {
[_controllerSupport assignGamepad:device];
_controllers = [_controllerSupport getControllers];
}
void onDeviceRemoved(struct Gamepad_device * device, void * context) {
[_controllerSupport removeGamepad:device];
_controllers = [_controllerSupport getControllers];
}
void initGamepad(ControllerSupport* controllerSupport) {
_controllerSupport = controllerSupport;
_controller = [[Controller alloc] init];
Gamepad_deviceAttachFunc(onDeviceAttached, NULL);
Gamepad_deviceRemoveFunc(onDeviceRemoved, NULL);
Gamepad_buttonDownFunc(onButtonDown, NULL);
Gamepad_buttonUpFunc(onButtonUp, NULL);
Gamepad_axisMoveFunc(onAxisMoved, NULL);
Gamepad_init();
}
@@ -1,4 +0,0 @@
//
// Use this file to import your target's public headers that you would like to expose to Swift.
//
Loaded 100 of 258 files, more files were not shown because too many files have changed in this diff. Show more